文章目录

Vue

固定依赖版本

注意:使用 element-ui 时, 遇到过 Vue 版本未固定, 小版本升级导致页面出现问题的

原版本 vue 2.7.14 , element-ui": "2.13.0 , 遇到的问题是:

  1. 升级到 2.7.15 导致 popper 错位
  2. 升级到 2.7.16 导致 dialog 无法编辑…

深度作用选择器

scoped 只作用于本页面, 甚至不作用于页面引入的组件, 所以想让页面既不影响其他页面, 又能改变组件的样式, 可以使用 >>> 深度作用选择器;

iView 框架里的组件的样式无法在 scoped 里改, 但是用这个就可以, 当然, 自己写的其他组件也一样;

注意

  1. 预处理器有可能不支持 >>> , 可以用 /deep/::v-deep 代替 >>>

    搜了下, 关于 /deep/::v-deep 有几种说法:

    1. /deep/ 在某些时候会报错, ::v-deep 更保险并且编译速度更快.
    2. ::v-deep 针对的是 dart-sass 的 npm 包的, node-sass 用 /deep/

    不知道哪种说法是对的, 我本地项目两个都可以(装的是 node-sass 4.13.1), 出错了的话就换一种试试吧

  2. 不要在预处理器中嵌套写 /deep/ , 本身这种写法是错误的, 嵌套中的 /deep/ 不会被解析, 大部分客户端做了兼容不会出错, 但苹果不支持 /deep/错误嵌套用法导致的ios移动端真机样式失效问题( >>>、 /deep/、::v-deep)

  3. Chrome89 也不再兼容嵌套 /deep/ 了 /deep/的使用与导致样式失效问题处理

  4. less 文件里写 /deep/ , @import 它时加上 scoped , 好像可以 scoped , 参见Vue style里面使用scoped属性并@import引入外部css, 作用域是全局的解决方案

$nextTick 和 SetTimeout (宏任务微任务) ❌

使用 $nextTick() 方法,这个方法好像是让其内部的函数在 DOM 更新后再调用(具体没查);

有时想刷新一个页面, 常常用这个, 比如先把动态组件置空,然后在此方法中再把此组件还原回去;

使用 v-if 刷新组件和页面也是一样;

computed

  1. 不要在 computed 中使用 ajax 请求数据

  2. computed 属性为对象数组, 可以直接改对象的属性(v-model 绑定也可以)

watch

1. deep

watch 要监控对象属性的变化,需要使用 deep: true , 详见 官方教程

2. val 和 oldVal 一模一样

watch 监控对象时, valoldVal 一模一样,官网上关于这点的解释好像是在 迁移 那一部分;

变通方法: 可以直接 watch 对象的某个属性

3. immediate 属性

watch 第一次绑定时是不会执行的, 加上这个 immediate: true 就可以了. 用法: 比如一个 Modal 使用了 v-if 绑定了父组件某个属性, 那么 ``immediate可以让Modal一被创建就能watch` 到外部传递给它的相关属性.

4. 直接使用函数的字面量名称为 handler
methods: {
    methodName() {
        //...
    }
},
watch: {
    watcher: {
        handler: 'methodName'
    }
}

keep-alive

<keep-alive> 标签包裹组件后,只有第一次加载组件才会触发 mounted 生命周期,后面再切换就是 activateddeactivated

自定义事件

  1. 不同于组件和 prop,事件名不存在任何自动化的大小写转换
  2. v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),因此,推荐始终使用 kebab-case 的事件名

组件通讯

1.参考文章:

vue2父子组件间相互通信

Vue 组件通信之 Bus

Vue使用EventBus传递数据的坑

vue组件间通信六种方式(完整版)

2. 注意 eventBus 监听的时机

使用 eventBus 机制传递数据时,要注意监听事件的时机,不然有可能出现----跳转前的页面发送事件带参数过去时,对方还没来得及绑定监听事件,

举例: 我在跳转后的页面的 mounted 里加上了发送 "已 mounted 完成"eventBus 事件, 当跳转前的页面接收到此事件才会传参数过去,因为两个页面被 <keep-alive> 标签包裹,所以也不用担心跳转后发不了事件

3. inheritAttrs, $attrs, $listeners
  • inheritAttrs

    所有父组件传入子组件, 但未被子组件 props 注册的变量, 都会变成普通 html 元素属性, 作用到子组件根元素上; 在子组件声明时, 加上 inheritAttrs: false , 就能避免此行为

    <script>
        export default {
            name: 'authorityDrawerList',
            inheritAttrs: false,
            // ...
        }
    </script>
    
  • $attrs

    所有未被子组件 props 注册的变量(非 props 属性)都会放入 $attrs 中 (class 和 style 除外), 子组件使用 this.$attrs.xxx 获取父组件传入变量, 也可以使用 v-bind="$attrs" 传给孙组件, 曾孙组件…

    <template>
    	<Drawer v-bind="$attrs"></Drawer>
    </template>
    
  • $listeners

    $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=“$listeners” 传入内部组件

    <template>
    	<Drawer v-on="$listeners"></Drawer>
    </template>
    
  • 应用

    我在项目中实际中用到的地方, 是无限嵌套 Drawer — 用户点击表格行打开详情页, 在详情页又点击按钮打开新的详情页, 一层层嵌套;

    此时, 如果使用 props 和 $emit , 就要一层层传递数据/数据, 这显然很麻烦; 此时使用 $attrs , 就能让最新的详情页直接获取最外层的数据, 使用 $listeners , 就能直接让最外层执行方法, 方便很多(虽然也要一层层 v-bind v-on)

4. inject provide

这个只看了文档, 没咋用过, 略

重置组件数据为初始状态

// 这个我没弄成功,但是思路应该是这样,自己去官网搜 $options
Object.assign(this, this.$options.data.bind(this)())

Vue 实现定时提醒

场景: 做任务计划模块,计划有提醒模块,到时间后提醒

实现

1. 提交要创建的任务
// 这里是发送请求完成的回调, 参数是外面定义的
if (response.data.code === '1') { // 创建任务成功
    if (this.taskObj.remindTimeValue === -1) { // 不提醒
        this.$store.commit('setTaskChangeObj', { // 设置 taskChangeObj (Vuex)
            id: JSON.parse(response.data.data).id,
            type: 'noNotice'
        });
    } else { // 需要提醒
        let time = this.taskObj.taskEndTime.getTime() - this.taskObj.remindTimeValue * 60000 - Date.now(); // 计算任务截止时间 - 任务提前多久提醒(准时, 提前多少分钟, 提前多少天) - 当前时间
        if (time > 0) { // 如果计算得到的值大于 0 
            this.$store.commit('setTaskChangeObj', {
                id: JSON.parse(response.data.data).id,
                type: type === '新建' ? 'newTask' : type === '编辑' ? 'editTask' : '',
                taskName: this.taskObj.taskName,
                taskEndTime: this.taskObj.taskEndTime,
                taskDetails: this.taskObj.taskDetails,
                time: time
            }); // 存入相应的 taskChangeObj
        } else if (time === 0) { // 如果刚好需要提醒
            this.$store.dispatch('taskNotice', {
                tip: this.taskObj.taskName,
                fromNick: this.taskEndTime.format('yyyy-MM-dd hh:mm'), // 这里的 format 是重写了 Date.prototype 上的方法
                text: this.taskObj.taskDetails
            }); // 那么直接调用通知方法(这里是之前其他同事用插件做的, 我直接照着用的)
        }
    }
    // 其他代码
} else {
    this.$Message.error(`${type}任务失败!`);
}

// 除此之外, 还有"标记任务为已完成/未完成"可以改变任务状态
2. 最外层页面实现提醒
// 1. 定义, 用 Map 格式存储任务提醒, 格式为 任务id(字符串): 相应的 setTimeout 返回的 id 
taskNoticeTimeoutMap: new Map(),
    
// 2. 定义, 进入最外层页面就要请求任务详情
getTaskNotice() {
    util.ajax({
        url: '请求URL',
        method: 'post',
        data: {
            // 参数
        }
    }).then(response => {
        if (response.data.code === '1') { // 请求成功
            if (Array.isArray(response.data.data)) { // 如果返回数组
                response.data.data.map(item => { // 遍历数组
                    if (item.remindTimeLatest < Date.now()) { // 如果提醒时间已过
                        // 相关处理
                    } else if (item.remindTimeLatest > Date.now()) { // 如果提醒时间还没到
                        let timeout = setTimeout(() => { // 设置 setTimeout
                            this.taskNoticeTimeoutMap.delete(item.id + ''); // 到时间后从 map 中去掉此任务
                            this.$store.dispatch('taskNotice', { // 触发提醒
                                tip: item.taskName,
                                fromNick: new Date(item.taskEndTime).format('yyyy-MM-dd hh:mm:ss '),
                                text: item.taskDetails
                            });
                        }, item.remindTimeLatest - Date.now()); // 时间设置为相差时间
                        this.taskNoticeTimeoutMap.set(item.id + '', timeout); // 加入此任务到 map
                    } else { // 时间刚好到了, 那就直接提醒
                        this.$store.dispatch('taskNotice', {
                            tip: item.taskName,
                            fromNick: new Date(item.taskEndTime).format('yyyy-MM-dd hh:mm:ss '),
                            text: item.taskDetails
                        });
                    }
                });
            }
        } else {
            this.$Message.error(response.data.data || '任务提醒功能故障');
        }
    });
},
    
// 这个是调用上面的方法的, 此方法的作用主要是为了保证过 0 点了重新请求提醒列表
// 因为任务创建时可以设置重复(最低以天为单位), 然后请求是按天算的, 后天计算得到当天有哪些任务需要提醒, 传递给前端
updateTaskNotice() {
    this.getTaskNotice(); // 先调用上面的方法请求和加入相关任务到提醒map
    let tomorrowTimeStamp = new Date(new Date(Date.now() + 24 * 60 * 60 * 1000).toLocaleDateString()).getTime(); // 存入明天时间戳
    if (Date.now() < tomorrowTimeStamp) { // 如果当前时间戳小于明天时间戳
        setTimeout(() => { // 设置 setTimeout
            setInterval(() => { // 每天 0 点
                for (let i of this.taskNoticeTimeoutMap) { // 遍历 map clearTimeout
                    this.taskNoticeTimeoutMap.delete(i[0])
                }
                this.getTaskNotice(); // 重新获取任务提醒
            }, 24 * 60 * 60 * 1000)
        }, tomorrowTimeStamp - Date.now());
    } else { // 否则从现在开始就执行上面的相关操作
        setTimeout(() => {
            setInterval(() => {
                for (let i of this.taskNoticeTimeoutMap) {
                    this.taskNoticeTimeoutMap.delete(i[0])
                }
                this.getTaskNotice();
            }, 24 * 60 * 60 * 1000)
        }, 24 * 60 * 60 * 1000);
    }
},

// 3. computed 里取到 Vuex 的 taskChangeObj

// 4. 然后在 watch 中监控 taskChangeObj
taskChangeObj(obj) {
    if (obj) {
        switch (obj.type) { // 根据不同的任务类型处理相关的提醒
            case 'newTask': // 新建
            case 'editTask': // 编辑
            case 'sign': // 标为未完成
                if (obj.id) {
                    // 如果当前 map 中已存在此任务, 那么 clearTimeout
                    if (this.taskNoticeTimeoutMap.has(obj.id + '')) {
                        clearTimeout(this.taskNoticeTimeoutMap.get(obj.id + ''));
                    }
                    // setTimeout 到时间提醒 + 从 map 删除
                    let timeout = setTimeout(() => {
                        this.taskNoticeTimeoutMap.delete(obj.id + '');
                        this.$store.dispatch('taskNotice', {
                            tip: obj.taskName,
                            fromNick: obj.taskEndTime.format('yyyy-MM-dd hh:mm:ss '),
                            text: obj.taskDetails
                        });
                    }, obj.time);
                    // 加入 clearTimeout 到 map
                    this.taskNoticeTimeoutMap.set(obj.id + '', timeout);
                }
                break;
            case 'noNotice': // 不提醒
            case 'unSign': // 标记为已完成
            case 'delete': // 删除任务
                // 清除相应的 setTimeout 和从 map 中删除
                if (obj.id && this.taskNoticeTimeoutMap.has(obj.id + '')) {
                    clearTimeout(this.taskNoticeTimeoutMap.get(obj.id + ''));
                    this.taskNoticeTimeoutMap.delete(obj.id + '');
                }
                break;
        }
    }
    this.$store.commit('setTaskChangeObj', null);
}
3. 在 Vuex 中设置好 taskChangeObj

在 JS 中引用 Vuex 和 Vue-Router

1. Vuex
// 想在 js 中使用 Vuex, 发现前辈在项目里是这么写的
// 之前 Vuex 定义时, 存放在 store/index.js 里, 生成了 Vuex 实例, 然后 export 了
// 只要直接引用这个变量就行了
const store = new Vuex.Store(Obj);

export default store;


/* 实际使用 */
import store from '../store'; // 引入实例
store.state.app.firstRedirect // 就这么用, 相当于把 this.$store 替换成 store
store.commit('changeFirstRedirect');
2. Vue-Router
// 这个是根据上面来的, 大体一致
new VueRouter(RouterConfig); // router/index.js 中生成了实例并 export 了

/* 实际使用 */
import {router} from '../router/index'; //引入
router.push({
    name: 'login'
}); // 使用
3. this.$route

这个可以用 router.currentRoute 来获取当前路由信息对象

4. prototype

自己在 Vue.prototype 上定义一个方法(别用箭头函数), 在 Vue 文件中调用, 这时根据"一般情况下, 谁调用此函数, 函数里的 this 就指向谁"规则, this 和 Vue 文件里的 this 等价…

axios 在 response 拦截器中取消请求 (token过期, 跳转到登录页)

  1. 首先我们都知道 axios 中有拦截器, 在请求回来发现 token 过期时, 我们需要在 success拦截器函数中, 中断后续的请求处理逻辑, 并跳转到登录页重新登录, 并显示错误提示.

  2. 假定后台返回的数据格式如下:

    {
        code: '0'/'1'/'NEED_REDIRECT'/..., // 0 错误, 1 正常, NEED_REDIRECT 代表 token过期, 需要跳转到登录页重新登录
        data: obj
    }
    
  3. 可能有多个请求受到 token 过期的影响, 为了防止错误提示弹出多次, 在 Vuex 定义一个变量, 比如firstRedirect;

    它初始时为 true, 当发现后台返回 code: 'NEED_REDIRECT' 时, 如果当前它为true,

    说明这是第一个被拦截下来的函数, 这时, 我们把它赋值为 false;

    这样, 设置一个 if (store.state.app.firstRedirect) 就能拦截下后面的请求(具体在 Vuex 里怎么存看自己的代码结构), 避免弹出多次错误提示.

    跳转到登录页面后, 在 router.push 的回调函数中, 再把 firstRedirect 赋值回 true

  4. 但是实际发现, 本来会弹出多次的错误提示, 现在变成只弹两次, 而不是只弹一次, 猜想可能是这样:

    前面几个都被拦截下来了, 但是到了某次请求返回结果时, 已经跳到了登录页面, firstRedirect 已经变回 true 了, 于是通过了 if 语句, 但是因为已经在登录页了, router.push 实际上没有跳转, 也就没有触发它的回调函数, firstRedirect 从此变为 false , 还会影响到后续的拦截

    所以, 需要加上判断——当前页面是否在登录页.

  5. 最后是发现过期后如何中断请求, 直接看下面的代码吧:

  6. 最终拦截器代码如下:

    // 这里是在 js 文件中里引用 Vuex 和 Message(iView 组件) , 用法可见本页相关知识点
    
    // 定义拦截器函数
    let success_interceptor_func = response => {
        if (response.data.code === 'NEED_REDIRECT') { // 如果 token 过期
            if (store.state.app.firstRedirect && router.currentRoute.name !== 'login') { // 如果是第一次拦截到过期请求, 且当前页面不是登录页
                store.commit('changeFirstRedirect'); // Vuex 修改 firstRedirect 的值
                Message.error(lang[Vue.config.lang].tokenExpired); // 弹出错误提示
                Cookies.remove('token'); // 清除过期 token , 避免跳回登录页时被判断成"已有token, 不能再回登录页重复登录" (这里 Cookies 是引用的包)
                router.push({
                    name: 'login' // 跳转到登录页
                }, () => {
                    if (!store.state.app.firstRedirect) {
                        store.commit('changeFirstRedirect'); // 改回 true
                    }
                });
            }
            throw new axios.Cancel('Token expired'); // 抛出错误, 中断请求
        }
        return response; // 正常返回 response
    };
    

axios多次发送相同请求后只保留最后一次请求

场景: 短时间内发送多个筛选表格数据请求(筛选条件不同), 想要的是最后一次请求得到的筛选数据, 然而可能最后一次请求完成时间反而比前面的更早, 最后一次请求写入的数据被更早的请求(但请求完成时间靠后)覆盖了, 导致筛选出来的数据(更早的)和筛选条件(当前的)不对应;

解决方案: 其他的当然也有, 但是看到 axios 有一个相关的设置, 就用这个了:

<script>
    export default {
        data() {
        	return {
                cancelFunc: null, // 取消请求函数
                ...
            };
        },
        methods: {
            get_table_datas() {
                // 如果之前有发送表格数据请求, 则取消之前的请求
                if (typeof this.cancelFunc == 'function') {
                    this.cancelFunc()
                }
                // 暂存 this
                let vm = this;
                // ajax 是 axios.create 创建的方法, 现在发送新的表格数据请求
                ajax({
                    ...
                    cancelToken: new axios.CancelToken(function executor (c) {
                        vm.cancelFunc = c
                    })
                }).then(...)
            }
        }
    }
</script>

接口健康检查, 动态切换线路

场景:

部分接口海外客户不可用, 于是需要在客户无法某域名时动态切换到其他域名

解决方案:

  1. 设置定时器, 定时请求不同域名下的同一个健康检查接口, 谁先返回就设定该线路为最优线路, 相关接口走该线路请求
  2. 在 axios 的 request 拦截器中, 判断请求 url 是否要走健康检查逻辑, 要走的话, 替换原域名为当前最优线路域名

vue-cli3 项目 npm run dev 内存溢出

转载: vue-cli3内存溢出,JavaScript heap out of memory

为啥小组里只有我一个人有这毛病…

render时加的ref不起作用, $createElement

转载: Vue 里怎样在 Render 中使用 $refs

在 render 函数体里把 h 改为 this.$createElement 例如:

let create = this.$createElement; 
return create('div', ...)

大佬厉害!

vue 将外部的函数传递给子组件并绑定 this

  1. 父组件把要传递的函数写在 export default 外部 let myFunc = function(params) {}; (function 形式, 不能是箭头函数)
  2. 父组件在 data 中定义一个变量 myFunc , 值为 myFunc
  3. v-bind 传递上一步定义的变量 myFunc 给子组件
  4. 子组件定义计算属性, 里面 return this.myFunc.bind(this) , 即可得到函数

将节流和防抖生成的函数直接设置为 watch 的 handler(使用 $watch)

问题: 节流和防抖函数生成函数并返回, 想把这个直接设置为 watch 的 handler , 发现 handler: this.methodName 会报错 this undefined

解决方法: 在 created 中使用 $watch 注册监听函数, 并在 destroyed 时销毁

点击底部消息提醒, 弹出对话框 ❌

主要是vuex和组件通信

vue监听子组件的生命周期钩子

在模板中, 父组件通过 @hooks:created 这种形式监听

ps: 我看也有人写可以通过 vm.$on('hooks:created', cb) 或者 vm.$once('hooks:created', cb) 监听, 猜测是不是可以用 $parent , $children , $refs 来获取组件实例, 再调用 $on , 还没实际尝试

带参数刷新当前路由

新建空白路由 refresh , 带参数 $router.replace 到 refresh 页后, 在 refresh 的 beforeRouteEnter 中再 replace 到指定 name

注意: 地址栏会有闪动, 另外, replace 时 title 记得别改 title , 避免 title 闪动

vue 全局方法的写法 ❌

插件的正确写法 ❌

优化思考 ❌

https://cn.vuejs.org/v2/guide/plugins.html
使用插件统一管理 vue.protoType.methods , vue.methods , mixins , filters , directives
https://cli.vuejs.org/zh/guide/mode-and-env.html
使用环境变量和模式来指定相关地址等数据
https://cn.vuejs.org/v2/guide/render-function.html#JSX
https://github.com/vuejs/jsx
可以考虑使用 jsx 来更方便地 render VNode
统一管理接口调用

vue 指令鼠标拖拽元素

  1. 本体

    directives: {
        drag: {
            // 指令的定义
            bind: function(el, binding) {
                // 阻止事件冒泡
                // 不仅仅要stopPropagation,还要preventDefault
                function pauseEvent(e) {
                    if (e.stopPropagation) e.stopPropagation();
                    if (e.preventDefault) e.preventDefault();
                    e.cancelBubble = true;
                    e.returnValue = false;
                    return false;
                }
    
                const oDiv = el; // 获取当前元素
                oDiv.onmousedown = (e) => {
                    // 在事件中
                    e = e || window.event;
                    pauseEvent(e);
    
                    // 算出鼠标按下元素时的初始位置
                    let disX = e.clientX;
                    let disY = e.clientY;
    
                    // 获取元素的 computedStyle
                    const styleObj = window.getComputedStyle(oDiv);
    
                    // 鼠标移动时设置元素的定位属性
                    document.onmousemove = e => {=
                        // 用鼠标移动后的位置减去鼠标的初始位置得到鼠标位移
                        const left = e.clientX - disX;
                        const top = e.clientY - disY;
    
                        if (left || top) {
                            // 确保移动时不触发点击内部点击事件
                            binding.value.changeDragStatus(true);
    
                            // 算出鼠标相对元素的位置
                            disX = e.clientX;
                            disY = e.clientY;
    
                            // 移动当前元素
                            oDiv.style.right = `${Number(styleObj.right.replace('px', '')) - left}px`;
                            oDiv.style.bottom = `${Number(styleObj.bottom.replace('px', '')) - top}px`;
                        }
                    };
    
                    // 鼠标松开时清除掉鼠标移动监听
                    document.onmouseup = (e) => {
                        document.onmousemove = null;
                        document.onmouseup = null;
                        binding.value.changeDragStatus(false);
                    };
                };
            }
        }
    }
    
  2. 避免触发子元素 click 事件

    // 插件绑定方法
    v-drag="{changeDragStatus}"
    
    // 方法定义
    changeDragStatus(val) {
        this.dragging = val;
    }
    
    // html 部分, 拖拽时给不想触发 click 的元素加上 disableClick 类
    :class="{'disableClick': dragging}"
    
    // css 部分, 使用 point-event 避免触发鼠标事件
    .disableClick {
        pointer-events: none;
    }
    
    // 插件内部使用
    // 搜索 changeDragStatus 部分
    
  3. 避免 onmouseup 失效

    pauseEvent

  4. 备注:

    1. 还有边界判断之类的没加, 可以继续优化
    2. 可以改成不使用指令的方式给非 vue 项目使用
    3. 注意 onmousemove , 很奇怪的是, 第一次 oDiv.onmousedown 时, 哪怕没有移动鼠标, 也会触发其内部定义的 document.onmousemove , 这有可能导致 BUG , 因此, 加上了 if (left || top) 来确保鼠标真的是有位移

记一次定位内存泄漏

问题定位:

  1. axios.created 创建的 ajax 配置底下加了个莫名其妙的 cancelToken , 用的同一个 axios.cancelToken , 传的cancelToken.source().token
    逐个代码块注释解注, 发现在某个请求之后出现内存问题, 控制台打快照对比切换前后变化(有个选项可以看到在快照 2 和快照 1 之间被分配的数据), 发现就一个 cancelToken 看着眼熟
    跳到该请求 ajax 配置定义处, 发现比之前多了一个 cancelToken (其他同事加的), 上 Google 搜索这个是否会导致内存泄漏, 搜索栏自动联想"axios canceltoken memory leak", 应该就是这个了

  2. 频繁切换组件后, 使用 vue-i18n, this.$t 可能取不到 this 导致报错
    定位问题时, 我是注释大块代码后在左侧目录快速切换子组件 50 次看看内存上升多少, 发现在这种情况下控制台经常报错"TypeError: Cannot read property ‘_t’ of null", 这时内存有变化
    搜了下, 找到一个回答
    https://stackoverflow.com/questions/54666293/vue-i18n-cannot-read-property-t-of-undefined-at-proxy-vue-t
    照着这个, 在 new Vue 之后加上了 ‘Vue.prototype._i18n = i18n;’ , 提示语消失了, 不过之后又有出现, 只是频率降低很多, 有兴趣的可以继续看看

  3. 某组件绑定事件后未解绑事件
    最后一路解注释到这块儿, 加上解绑后整体解决

使用 \xa0 代替 &nbsp;

可以使用 {{ ‘\xa0’ }} 代替 &nbsp;

记一次优化首屏加载速度

背景:

登录页加载了两个 1m 以上的 js , 太大了, 打开非常简单的登录页需要 7 - 9s

优化方向:

  1. webpack 配置优化

    1. 主要是打包增加 gzip 压缩 — 前端加上 compress-webpack-plugin 设置好, 也需要后端配合设置好

      效果, 两个 js 文件大小瞬间减少了一半还多

    2. 加上 webpack-bundle-analyzer 插件, 打包后看看各文件里还有哪些模块占大头的, 能不能拆分之类的

    3. 其他…网上搜相关文章 + 对照着 webpack 官方文档慢慢尝试

  2. 之前没有走按需加载的路由组件统一改为走按需加载

    1. 非常坑的是, 之前做过按需加载, 但这次排查时, 发现实际上只有初始化二级路由时才用了按需加载, 根据目录权限接口的返回生成一级路由时, 一级路由的 component (按需加载 Main.vue)不是走的按需加载, 还有个别新增的不从属于 Main.vue 的单页也没走按需加载

      改为按需加载, 两个 js 文件大小再次降低一半多

  3. 将 main.js 内大部分不需要在登录页加载的引入挪到 Main.vue 中, 减轻首屏 js 文件大小(相应地, main.vue 对应的 js 文件变大)
    其他非 Main.vue 界面(登录页, 忘记密码页, 中转页, 邮件单页)中使用的全局 api 可能有缺失, 需要调整引入方式

    1. 微前端引了一堆组件, 改成从 main.js 挪到 mian.vue mounted 中
    2. 大部分 Vue.component , Vue.mixin , Vue.filter , Vue.protoType.xxxx = YYYY 之类的, 都从 main.js 里去掉, 放到 main.vue created 里
  4. 将 index.html 里没必要第一时间引入的资源, 往后移动, 或者加上 async 或者 defer , fetchPriority="low"之类的

    1. index.html 里大部分的静态 script 标签改为在相应页面动态插入 script 元素 — 动态插入默认是 async , 注意是否要设置好 script 元素的上面这些属性再插入
    2. 百度地图埋点使用了 document.write , 这种不能动态插入, 只能在 document.write 还可用时同步引用代码, 写到 前吧, 避免阻塞
    3. 从控制台 Network 看, 音频资源也会造成阻塞, 耗时较长, 可以设置 preload=“none” 避免第一时间加载音频资源造成阻塞
  5. 去掉冗余的引入, 各种 import 了没使用的都注释掉

  6. 其他非主体框架页面(Main.vue), 比如 login 页面, 忘记密码页面, 各类单页等, 如果有用到全局 component , api 等, 需要检查一遍是否还有效, 无效了最好自己单独引入

结果:

首页加载的最大的两个文件 , appXXXX.js 和 chunk-vendorXXXX.js , 降到了 200 多 K , 300 多 K

google 网址测速, pc 首屏显示时间为 1.5s 左右, 移动端首屏显示时间为 6s 左右

未来方向:

  1. 经过优化, 登录页和主体框架页面(Main.vue)的 js 大小大概从 5:5 变为了 2:8 , 开启 gzip 压缩后, 主体框架页面仍然有 1.3M 大小, 还是太大了;
    通过 bundle 插件, 该 js 里还是有一些大的外部模块, 可以单独提取出来的, 比如通过 CDN 加载, 或者通过 webpack.DllPlugin 把多个静态库打包成一个文件, 这样更能把握 js 大小和数量之间的平衡
  2. 实际上可以把登录页改造成单独的 html 页面, 登录页本身也不复杂, 不使用或者少使用 Vue 和那些 UI 框架, 应该能大大加快速度
  3. 发现一篇文章, https://juejin.cn/post/7188894691356573754 , 提到 Vue 文件引入子组件也可以按需加载, 以及这里说的骨架屏插件, 感觉也不错, 之后可以试试

Vue 单页面应用 SEO 探索

参考资料

实现思路

参考资料 1 中提出的几条路线(服务端渲染, 打包预渲染, ), 因为是改造现有应用, 所以综合下来选了 prerender-spa-plugin 路线;

此路线大意为: 此插件可以在页面渲染后再打包生成静态页面, 生成的页面因为有了内容, 可以被 Google 收录

具体步骤

  1. 安装 prerender-spa-plugin 插件(当前 ^3.4.0)

  2. 修改 Vue.config.js , 应用上述 plugin

    // 引入
    const PrerenderSPAPlugin = require('prerender-spa-plugin');
    const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
    
    // 配置变量新增内容
    const VueConfig = {
        publicPath: '/Front-Vue', // 原 publicPath , 不用改
        outputDir: 'dist/Front-Vue', // 新增 outputDir 属性(之前没有), 指向打包后的文件夹 + publicPath
        /* ... */
        // configureWebpack 新增 plugin 配置
        configureWebpack: {
            plugins: [
                new PrerenderSPAPlugin({
                    staticDir: resolve('dist'),
                    outputDir: resolve('/dist/Front-Vue/'),
                    indexPath: resolve('dist/Front-Vue/index.html'),
                    routes: ['/', '/login'], // 需要渲染的页面, 与路由对应
                    postProcess(renderedRoute) {
                        return renderedRoute;
                    },
                    renderer: new Renderer({
                        inject: {
                            foo: 'bar'
                        },
                        headless: false,
                        renderAfterDocumentEvent: 'render-event' // 自定义的渲染完成事件, 触发后开始抓取
                        // 还有些"等待xx元素加载完毕", "延时等待渲染完成"之类的参数, 详见 github 上 prerender-spa-plugin 的 README 文档
                    })
                })
            ]
        }
    }
    
  3. 在 new Vue 的 mounted 函数中加上触发自定义的加载完成事件

    该事件通知到 plugin : 页面渲染完毕, 可开始生成静态页; 也可以换成在其他处触发此事件 — 但有文章提到:“不是在根组件中设置此事件无效”

    document.dispatch(new Event('render-event'))
    
  4. 修改 Vue-router 配置

    {
        // before...
        mode: 'history', // 此插件要求, 必须为 history 模式
        base: '/Front-Vue' // 与 Vue.config.js 的 publicPath 相同; --- 我之前是 hash 模式, 对 history 的配置不熟, 试了下这里保持一致才能正常加载页面
        // after...
    }
    
  5. 打包, 上传页面到 Google 收录, 确定当前可以收录

    访问 请求(重新)编入索引, 按操作将要收录的页面收录到 Google , 已收录后, 去 Google 搜索下页面标题/网址, 看能否搜索到

  6. 可使用 Vue-meta-info 组件添加 meta 信息

注意事项

  1. mode 从 hash 改为 history 还需要关注下有没有问题
  2. 修改 mode 后, 微前端配置也要改
  3. 修改 mode 后, 各页面跳转地址是否有变化,

Vue-devtools 选中微前端组件

参照:

[反馈]使用qiankun时,vue-devtools无法调试子项目

实现:

// vue-devtool 也会监测页面上组件的新增删除等,并识别这些元素的__vue__属性,所以第二种方式,把子应用的挂载到一个空div上丢到页面中

// 以下代码都要注意在开发环境开启即可
const subDiv = document.createElement('div')
subDiv.__vue__ = sub
document.body.appendChild(subDiv)

生产/线上环境开启 Vue-devtools (Vue2 适用)

参照:

如何在生产环境排查 Vue 应用问题

为生产构建的Vue应用强制开启Vue Devtools

以及, 第一篇文章末尾有提到Vue force dev插件可以直接用

实现:

// 选中任意一个组件的根元素
const $el = xxx;

// 拿到该元素的 Vue 组件信息
// $el.__vue__

// 获取其 constructor
let Vue = $el.__vue__.constructor

// 层层向上, 找到 Vue (相当于代码 import Vue 的结果)
while (Vue.super) { Vue = Vue.super }

// 开启
Vue.config.devtools = true
__VUE_DEVTOOLS_GLOBAL_HOOK__.Vue = Vue

// 关闭并重新打开“开发者工具”面板, 可用

template v-for 产生"failed to execute ‘insertbefore’ on ‘node’ parameter 1 is not of type ‘node’"错误

背景:

Vue3 项目 - 时间轴组件 - 遍历渲染单个时间轴条目组件 - 条目组件遍历渲染显示字段, 编辑某个条目后, 会重新请求数据列表替换原数据数组重新渲染, 偶现此报错

排查:

条目组件内容较简单, 注释怀疑的代码块, 排查问题, 发现是如下代码导致问题:

<template v-for="field in showFields">
    <div
     	v-if="isFieldShouldShow(field)"
	    :key="field.id"
    >
    <!-- 显示字段组件 -->
    </div>
</template>

将 :key 放到 template 上解决

(之前这么写是因为 vue2 项目里, :key 到 template 上, 编辑器会标红, 但现在修改后也不会标红了)


iView(已放弃, 包括新版本 ViewUI)

iView 因为多个组件有内存泄漏问题, 且官方不处理问题直接关闭 issue , 现已放弃, 改为使用 Element-UI

DatePicker 组件国际时间和本地时间问题

iView 框架的 DatePicker 组件直接使用 v-model 有问题(国际时间和本地时间的问题),可以使用 value@on-change ,手动赋值,这样显示就完全正常;

  • 如果页面上有多个 Datepicker 组件,可以使用 @on-open-change ,在其中设置好唯一标志本次打开的 Datepicker 的属性,然后再在 @on-change 中处理
  • 但是要注意,使用这种方法,直接改 value 并不会让 Datepicker 组件的显示也跟着改

在 iView 组件的事件中传递额外参数

iView 中有些组件可以绑定的一些事件,其本身不需要传入参数,只需要在定义事件方法时写入形参就能取到,但如果在传入时附加参数,就取不到默认参数了

1. $event

此时可以使用 @on-change="setOption($event, 其他参数)" , 此时 $event 就是默认参数

2. arguments

某些函数默认带有两个参数, 此时用 $event 只能取到第一个(**查了一下好像是原生事件 e v e n t 获取事件对象 , 非原生事件只取第一个参数 ∗ ∗ ) , 搜索后发现可以通过 ‘ a r g u m e n t s ‘ 代替 ‘ event 获取事件对象, 非原生事件只取第一个参数**), 搜索后发现可以通过 `arguments` 代替 ` event获取事件对象,非原生事件只取第一个参数),搜索后发现可以通过arguments代替event`

transfer 属性

Poptip 组件而言(其他的没观察), 它的 transfer 属性, 是把组件的气泡放到全局 body 中.

这种情况下, 在本页面的样式中操作无用, 只有在全局中调整才可以, 但是要使用 popper-class 绑定类名到 Poptip 组件才行

但是也要注意, 在这种情况下, 触发气泡的元素(trigger)还在组件中, 使用深度选择器在 scoped 中选择 ivu-poptip-rel 即可

Select 组件联动时的 BUG

  1. 场景: iView , 三个 Select 组件,第一个的 @on-change 会动态改变第二个的 option 数组,以及第三个的类型( inputDatePicker ), 它的使用场景是填写筛选条件, 比如第一项选 最近更新时间,那第二项就会是 ['早于', '晚于', '时间段'] , 第三项变为 Datepicker, 第一项选 客户星级,那第二项就是 ['大于', '小于', '等于'], 第三项变为 InputNumber

    问题: 改变首项筛选条件时, change 前后若根据首项获取的二项的 option 数组前后 length 相等(如,都有三个选项), 那么选择二项时,虽然实际上可以正确筛选,但文字总是显示成 change 前的选项.

    解决:直接用 ref 取数据,当 Select 组件的 data 上的 selectedSingle 不等于 model 时, @on-change 的方法直接把 model 赋给前者

    需要注意: v-for 中写的 ref 取到的值是个数组,具体到我当时的实例中,是个只有一项的数组,用 [0] 取组件数据

让 Select 组件既可手动输入, 也可以下拉选择(非 filterable 属性)

  1. 场景: 做邮件系统,选择收件人,需要既可以显示候选账号进行下拉选择,又可以直接手动输入账号,但是 iView 本身没有符合需求的组件

    思路:

    1. 首先想到的是一个伪装成输入框的 div ,在其中加入 Tag 系列,后面追加一个 input , 再用隐藏的 span 实现 input 的动态变长(在这过程中我还找到了 contentEditable 这个可以让 div 可编辑的属性),这一步我是直接用的 filterable + multipleSelect 组件生成的 HTML 代码
    2. 随后同事提醒我, 何不在 input 前再加一个 div , 再把 input 长度限制到只能显示一个光标, 动态填入输入内容到前面的 div ,这样就不用变长,也不用担心溢出和让 input 换行
    3. 最后同事又说,原来的 filterable + multipleSelect 组件已经可以了,稍作调整就行.

    实现:

    1. 观察 filterable + multipleSelect 组件生成的代码结构, mounted 时,为输入框绑定 blur 事件,根据 e.target.value 判断输入值是否已在下拉列表(v-for Option 数组),是否已被选中,如果没有,则 push 进去
    2. Select@on-change 中,询盘判断当前数组各项是否正则校验邮箱(推荐 Regulex ----正则可视化工具+邮箱正则表达式)通过,如果通过,则通过 DOM 绑定对应位置的 .ivu-tag-text 字体标黑(通过的情况也要显式操作,不然也可能变红),不通过标红

让 Cascader 动态请求一级目录, 点击后继续请求下级目录

  1. 场景: 使用 Cascader 级联组件,要求动态加入一级目录,点击一级目录后的请求二级目录还是用组件自带的搜索+动态加载功能

    实现:

    1. 首先想的是直接请求一级目录动态添加,但是组件本身的机制是,使用筛选后得到的值点击后就直接被认定为完成选择,跟我想的点击动态生成的一级目录就会请求二级目录不同

    2. 于是我使用 slot ,在其中定义一个 Input 组件,这样看上去和原来一样,也能运行,但是会报错

    3. 在 Input 组件上绑定 @input.native ,

      Cascader 组件上 @on-change 做回填到 Input 用,

      @on-visible-change , 本意是想要让它在关闭 panel 时检测是否已经有选中的选项,有的话就回填

      避免用户已选择选项后又输入字符,没有匹配到就关闭了 cascader ,这时没有触发 cascader 的 on-change 函数,所以没有回填,这时其实 cascader 是有数据的,但是 input 里却仍然显示之前没匹配到时输入的字符

      结果发现 on-visible-change 好像只检测到了 visible 为 true 的情况,

      控制台报错,因为组件源码里定义的 slot 默认内容中的 input 被 slot 中的 input 组件替换了,后面用到默认 input 时就会报错,不知道是不是因为这个原因才没检测到 visible 为 false 的情况,报错的时机和 panel 关闭时间相合

      不得已只能在展开时清空 cascader 的 v-model 了,毕竟展开就说明是要搜索,这也说的过去

给 iview-admin 框架右上角"关闭所有, 关闭其他"按钮下加上"刷新当前组件"功能

<!-- 
	思路: 框架里用了 include 但是没用 exclude, 动态检测当前页面的 name , 把它加入到 exclude 中然后清掉 router-view , 再把它从 exclude 中去掉, 再显示 router-view
	注意: 这种方式没有从服务器重新请求文件
-->

<!-- 框架原代码 -->
<keep-alive :include="cachePage">
    <router-view></router-view>
</keep-alive>

<!-- 变动后 -->
<keep-alive :include="cachePage" :exclude="excludePage">
    <router-view v-if="update"></router-view>
</keep-alive>

<!-- 在 data 中定义 -->
update: true, 
excludePage: ''

<!-- 更新方法  -->
updateCurrent() {
    this.excludePage = this.$route.name;
    this.update = false;
    this.$nextTick(() => {
        this.update = true;
        this.excludePage = '';
    });
}

修改 iView 源码重新编译

  1. 直接把 iView 的 GitHub 仓库下载下来

  2. 改动代码后,可以 npm run dev 查看效果, npm run dist 重新编译

  3. 编译完成之后,用新的 dist 文件把原来的框架中的 node_modules 中的 iView 文件夹的 dist 代码覆盖掉, 如果需要引用 iview 里的组件, 把 src 也替换掉(一般也只会改 src 吧?)

  4. 如果不想每次编译那么麻烦, 可以直接把下载下来的 iview src 文件夹复制到项目中, 引用的时候直接按路径引它而不是安装好的 iview 包;

    但是这样也有麻烦:

    • 一开始运行不了, 报错, 安装了 iview package.json里面的一些依赖(主要是 babel 相关, 另外还有 babel 配置文件也要改)才跑起来
    • 想上线时你会发现, 安装了那些包, 修改了 babel 配置文件后, 不能按需加载了(打包/运行时使用 webpack-bundle-analyzer 发现), 且体积也变大了

    所以最后还是放弃直接引入了, 只在早期开发阶段不急着上线时用

让 Cascader 组件多选和清空

  1. 多选:

    1. 使用 Cascader 的 自定义显示 功能, 用带 multiple 的 Select 替换掉组件自身的文本框

    2. 修改 Select 相关样式

      .select {
          /* 让 select 框内的文字能正常换行 */
          white-space: normal;
          /* 深度作用选择器, 让 scoped 的样式能影响到子组件 */
          /* 让已选项 tag 的高度自适应 */
          /deep/ .ivu-tag.ivu-tag-checked {
              height: auto; 
          }
          /* 隐藏掉 Select 的下拉框 */
          /deep/ .ivu-select-dropdown {
              display: none;
          }
      }
      
    3. 为 Cascader 组件绑定 on-change 事件

      当事件传入的已选项数据不为空时, 取出已选项数据, 格式化后填入 Select 的 option 列表和 v-model 绑定的数组

    4. 为 Cascader 组件绑定 on-visible-change 事件

      每次 visible-change 时, 都通过给 Cascader 设置的 ref 来清空 Cascader 的已选项

      /* 通过查看 iview 源码可知点击清空图标时组件执行的操作, 从而得到下面的代码 */
      this.$refs.cas.currentValue = this.$refs.cas.selected = this.$refs.cas.tmpSelected = [];
      

Table 组件实现多页全选

  1. 查看源码, 发现代码写的是只有全选选中才能触发 on-select-all 事件, 现在觉得要么是自己改源码, 要么是自定义表头然后绑定相应的事件
  2. 最简单的当然是直接改 iview.js , 然后检测选中数量为 0 就是取消全选, 反之就是全选, 但是这样不好维护, 听说可以打个 iview 的分支改动后同步到这个分支上, 但是没弄
  3. 第二个想法是, 隐藏表头, 用 slot=“header” 来当表头, 暂且搁置
  4. 最终采用: render 和 renderHeader, 在这两个函数里渲染 Checkbox , 用计算属性得到表头全选 Checkbox 的 value , 两种 Checkbox 都使用 on-change 改变 _checked 属性,

在 JS 文件中使用 iView 组件

  1. 直接 Ctrl + B (webstorm 下)跳转到组件的定义处, 就能发现源码中也是 export 组件在引用使用的, 于是照着来:

    // 以 this.$Message 为例
    import Message from 'iview/src/components/message'
    Message.error('出错信息');
    

表格组件 exportCsv 时数据出错

参考webpack+iview 在数据导出csv格式数据时变为科学计数问题

  1. 问题: exportCsv 时, 如果数据以 = 开头, 则会被 Excel 识别为公式, 错误显示.
  2. 像参考文章中那样, 遍历数据然后在前面加上 "\t" (注意双引号) 就行了
  3. 还有表格中有 , 的内容被分割成多列的情况, 用 iView 文档中说的 quoted: true 给数据统一包裹上""就行了

让 DatePicker 始终展开(直接展示时间选择面板)的方法

<DatePicker open ...>
    <span></span>
</DatePicker>
  • open 属性始终为 true , 保证始终展开
  • 内部 slot 加一个不占位的元素做触发器, 相当于隐藏掉了上面的框

但是注意, 这样展示的 Datepicker 仍然会触发 on-open-change 事件, 造成后果和解决方案参看下方

iview 下拉型组件(DatePicker , Select等等)阻塞点击事件

  • 上面 Datepicker 的 open 始终打开, 但仍会触发 on-open-change 事件造成阻塞, 导致: 点击其它地方时会先触发 on-open-change(false) 随后不再执行, 导致第一次点击无效

  • 另外, 上 GitHub 看了下, iview 项目有人提 issue 表示, Select 展开情况下, 点击别处想执行操作不会生效, 因为跟上面一样要先关闭 Select , 官方表示就是这么设计的

  • 解决方法: 在第一次渲染(open 变为 true)时就点击一下(选不会影响页面行为的地方点击, 比如这里选的是 Datepicker 的父级 Poptip 的 $el)提前触发这个 on-open-change(false), 之后就不会阻塞了

    // Datepicker 的 @on-open-change
    change_date_popper(val) {
        if (val) {
            this.$refs.poptip.$el.click();
        }
    }
    

以后这类情况(Datepicker, open 始终打开)都要这么弄, 类似的弹出型出现同样的问题应该也可以这么弄

无限嵌套 Drawer

场景: 表格页点击进入详情页, 详情页又有一些的关联的详情页, 比如 商品列表 => 商品详情页 => 商品厂家详情页 => 厂家下别的商品的详情页 => …

实现:

详情页分开写成组件, 用一个中间层总体处理 Drawer 和 详情页, 外部直接引用这个中间层.

中间层大致代码:

<template>
	<div>
		<!-- 最外层 Drawer -->
		<Drawer :value="visible" 
                @on-visible-change="toggle_drawer($event, 'main')"
                ...>
			<template v-if="type === 'customer'">
				<CustomerDetail ...></CustomerDetail>
			</template>
			<template v-else-if="type === 'contact'">
				<ContactDetail ...></ContactDetail>
			</template>
			....
		</Drawer>
		
		
		<!-- 后续连环 Drawer -->
		<Drawer v-for="(item, index) in drawer_list"
                v-model="item.visible"
                @on-visible-change="toggle_drawer($event, index)"
                ...>
			<template v-if="type === 'customer'">
				<CustomerDetail ...></CustomerDetail>
			</template>
			<template v-else-if="type === 'contact'">
				<ContactDetail ...></ContactDetail>
			</template>
			....
		</Drawer>
	</div>
</template>

<script>
    import ...
    
    export default {
        ...
        props: [
        	'visible', // 最外层 Drawer 显隐, 外部引用时加上 .sync
            'type', // 最外层 Drawer 类型
            'id', // 最外层 Drawer 唯一标志
            ...
        ],
        data() {
        	return {
            	drawer_list: [],
                ...
            };
        },
        methods: {
            // 切换 drawer 可视状态
            toggle_drawer(val, index) {
                // 如果更改最外层 Drawer 的可视状态
                if (index === 'main') {
                    // 先把 drawer_list 清空
                    // 如果直接更改数组, 会导致关闭所有 drawer 后数组为空但仍存在不可点击的 drawer 的 DOM , 所以先调整 DOM 可视状态为 false , 再在 setTimeout 中更改数组, 时间间隔自己调, 以 console 不报错为准
                    if (this.drawer_list.length) {
                        this.drawer_list.forEach(item => item.visible = false);
                        setTimeout(() => {
                            this.drawer_list = [];
                        }, 400);
                    }
                    // 更新外部 prop: visible
                    if (!val) {
                        this.$emit('update:visible', false);
                    }
                } else {
                    // 如果是隐藏掉 drawer_list 的某一项
                    if (!val) {
                        this.drawer_list[index].visible = false;
                        setTimeout(() => {
                            this.drawer_list.splice(index, 1);
                        }, 400);
                    }
                }
            },
                
            // 预备绑定给当前所处的最外层 DOM (此时以 .single-page 为例)的点击事件, 点击表格条目时打开相应的详情页, 点击其它位置关闭最上一层 Drawer
            click_blank(e) {
                let boo = true;
                let temp_el = e.target;
                // 以 e.target (触发点击事件的元素)一路向上搜索直到搜索到最外层
                while (!temp_el.classList.contains('single-page')) {
                    if (temp_el.classList.contains('ivu-table-row')) {
                        boo = false;
                        break;
                    } else {
                        temp_el = temp_el.parentElement;
                    }
                }
                // 如果确定不是点击的不是表格行 (Table 组件生成的 ivu-table-row)
                if (boo) {
                    if (this.drawer_list.length && this.drawer_list.some(item => item.visible)) {
                        let temp = this.drawer_list.slice().reverse();
                        let index = this.drawer_list.length - 1 - temp.findIndex(item => item.visible);
                        this.drawer_list[index].visible = false;
                        setTimeout(() => {
                            this.drawer_list.splice(index, 1);
                        }, 400);
                    } else {
                        this.$emit('update:visible', false);
                    }
                }
            }
        },
        watch: {
            id(val) {
                // 外部 id 不传入时, 把详情页关掉
                if (!val) {
                    this.$emit('update:visible', false);
                }
                // id 发生变化时, 把 drawer_list 清空(比如我点击第一行查看该行详情, 不管我后续在详情页里又点了多少层 Drawer, 我点击第二行时, id 发生变化, 清空后续的 Drawer , 只保留最外层 Drawer 显示新的表格行详情信息)
                if (this.drawer_list.length) {
                    this.drawer_list.forEach(item => item.visible = false);
                    setTimeout(() => {
                        this.drawer_list = [];
                    }, 400);
                }
            },
            visible: {
                handler(val) {
                    // 切换最外层 Drawer 显隐
                    this.toggle_drawer(val, 'main');
                    // 加上点击清除 Drawer 方法
                    if (document.getElementsByClassName('single-page')[0]) {
                        if (val) {
                            document.getElementsByClassName('single-page')[0].addEventListener('click', this.click_blank);
                        } else {
                            document.getElementsByClassName('single-page')[0].removeEventListener('click', this.click_blank);
                        }
                    }
                }
            }
        }
    }
</script>

Select 组件 BUG

如果发现 Select 高度消失或者选项不显示, 看看是不是 placeholder 为空或者没写 label

Table 组件 BUG

  1. 自定义滚动条样式后, 若水平和数值滚动条宽度不一致, 可能会有样式上的错误, 原因是 Table 组件只计算一边(忘了哪边了)的宽度, 随后拿这个宽度来调整宽高.

    自己定制 iview 框架, 找到地方(Table.vue 里面的 scrollBarWidth 之类的)修改一路"跳转到定义", 修改成各按各的来.

  2. Table 的 columns 配置 tooltip 属性后, 鼠标不能移上去 tooltip 复制上面的文字

    我提的修改方案: [Feature Request]I am trying to solve this problem: use the table column property tooltip, the mouse cannot be moved to the contents. #5867

    另外, 表格有些时候需要 render , 这时候想要兼顾 render 和 tooltip 配置, 只能自定义一个 Tooltip 组件(先按上面的修改方案改好 Table 组件了才能用这个), 把要 render 的元素放到新组件的 slot 里, 代码如下:

    <!-- 基本照搬 tooltip 组件, 加上了处理上面修改的代码传递过来的事件的代码 -->
    <template>
        <Tooltip
                transfer
                :placement="placement"
                :content="content"
                :delay="delay"
                :controlled="controlled"
                :always="always"
                :theme="theme"
                :maxWidth="maxWidth"
                :disabled="!showTooltip"
                @mouseIn="handleMouseInTooltip"
                class="ivu-table-cell-tooltip">
            <!-- 这里的 slot 方便把 render 要渲染的元素放进来 -->
            <span ref="content" class="ivu-table-cell-tooltip-content"><slot>{{ content }}</slot></span>
            <div slot="content">
                <slot name="content">{{ content }}</slot>
            </div>
        </Tooltip>
    </template>
    
    <script>
        import util from '@/libs/util'; // 这里用到的 util 的方法也是复制的 tooltip 组件的
    
        export default {
            name: 'tooltipAuto',
            props: {
                placement: {
                    validator(value) {
                        return util.oneOf(value, ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end']);
                    },
                    default: 'bottom'
                },
                content: {
                    type: [String, Number],
                    default: ''
                },
                delay: {
                    type: Number,
                    default: 100
                },
                controlled: {    // under this prop,Tooltip will not close when mouseleave
                    type: Boolean,
                    default: false
                },
                always: {
                    type: Boolean,
                    default: false
                },
                theme: {
                    validator(value) {
                        return util.oneOf(value, ['dark', 'light']);
                    },
                    default: 'dark'
                },
                maxWidth: {
                    type: [String, Number],
                    default: 300
                }
            },
            data() {
                return {
                    showTooltip: false,  // 鼠标滑过overflow文本时,再检查是否需要显示
                };
            },
            methods: {
                handleTooltipIn () {
                    const $content = this.$refs.content;
                    this.showTooltip = $content.scrollWidth > $content.offsetWidth;
                },
                handleTooltipOut () {
                    this.showTooltip = false;
                },
                // 新增的事件处理方法
                handleMouseInTooltip(boo) {
                    if (boo) {
                        this.handleTooltipIn();
                    } else {
                        this.handleTooltipOut();
                    }
                }
            }
        };
    </script>
    

iview 实现国家地区选择组件

iview 实现表格顶部筛选

复杂筛选的拼合

Select 的 Option 如何触发点击事件@click.native, 以及如何避免触发默认事件

汇率转换, 动态关联

Tree 组件写了 render 之后 on-select-change 就失效了

Poptip 组件 transfer 情况下, 点击其内部 transfer 的组件的浮层(如果有的话, 比如 DatePicker , Poptip), 外部 Poptip 关闭

原因

内部 transfer 了, 点击其浮层, 就相当于点击 body 中其他元素, 所以会关闭 Poptip

解决方案

  1. 简单行为, 如: Poptip 里又有一个 transfer 的 Poptip, 点击其内部, 最外的 Poptip 消失

    // 在内部 Poptip 的 on-popper-show 里加上 
    this.refs.poptip.handleTransferClick();
    // 或
    this.refs.poptip.disableCloseUnderTransfer = true;
    
    // 总之, 就是修改外部 Poptip 的 disableCloseUnderTransfer 值
    
  2. 复杂行为, 如: Poptip 里有 DatePicker 组件, 点击 DatePicker 内部进行选择

    // 因为 DatePicker 选择时间/日期可能会点击多次, 
    // 所以像上面一样在类似 on-popper-show 的 on-visible-change 只修改一次 disableCloseUnderTransfer 是没用的, 
    // 而且 DatePicker 本身也不提供监测内部点击事件的 events , 
    // 那么, 就只能在 DatePicker 的组件选择面板上绑定原生 click 事件, 监控到点击事件后更改 disableCloseUnderTransfer
    
    // refName 是 DatePicker 上的 ref 属性, 如果是在循环里还要加上 index
    bind_onclick_for_pickerPanel(refName, index) {
        if (this.$refs[refName] && this.$refs[refName][index]) {
            this.$refs[refName][index].$refs.pickerPanel.$el.onclick = () => {
                this.$emit('prevent_close_popper'); // 外部传入的修改 disableCloseUnderTransfer 的方法
            }
        }
    },
    
    @on-visible-change="bind_onclick_for_pickerPanel('dateRange', index)"
    

不定制 iview 组件的理由:

本来是想着干脆改了 Poptip 组件, 弄一个类似 disableCloseUnderTransfer 的 prop 在 Poptip handleClose 时阻止其关闭, 但是这样一来, 正常点击空白页面, 也不能关闭 Poptip 了, 所以还是一个个地在内部 transfer 的组件上加上修改 disableCloseUnderTransfer 的相应代码

tooltipAuto 的 scrollWidth 四舍五入错误导致 text-overflow显示正确而文字浮窗显示失败❌


Element-UI

Element-UI 数据量大时页面操作卡顿❌

element 实现表头筛选

背景:

之前已有了单独的表格筛选组件, 公司要求直接对表格顶部进行筛选, 方便用户操作

思路:

使用 element 表格 tableColumn 的 renderHeader 属性, 其内部绑定全局注册的筛选框组件, 传 column 的值给筛选框组件, 筛选框组件根据条件渲染不同的下拉框

代码实现:

  1. renderHeader_filter 函数

    // 返回字段筛选渲染函数
    renderHeader_filter(colIndex) {
        return h => {
            // 获取字段属性
            const column = this.allColumns[colIndex];
    
            /* DOM */
            // 标题
            const title = h('div', {
                attrs: {
                    title: column.title
                },
                class: 'table-filter-title'
            }, column.title);
    
            // 排序组件, 显示当前字段是升序还是降序
            let order_column = column.key;
            // 以下结构参照 element 表格排序的样式, Icon 为引入的 iview Icon 组件
            const sortIcons = h('span', {
                class: {
                    'table-sort-icon-container': true
                }
            }, [
                h('Icon', {
                    class: {
                        'icon-custom-on': true,
                        'icon-custom-on-active': this.order_column === order_column && this.order_type === 'asc'
                    },
                    props: {
                        type: 'md-arrow-dropup'
                    }
                }),
                h('Icon', {
                    class: {
                        'icon-custom-on': true,
                        'icon-custom-on-active': this.order_column === order_column && this.order_type === 'desc'
                    },
                    props: {
                        type: 'md-arrow-dropdown'
                    }
                })
            ]);
    
            // 筛选框组件
            const filterPop = h('FilterBox', {
                props: Object.assign({
                    // 是否筛选中
                    filtering: this.allColumns[colIndex].filtering,
                    // 字段 key
                    columnKey: column.key,
                    // 字段排序升序降序
                    order_type: this.order_type,
                    // 当前排序中的字段
                    order_column: this.order_column,
                    // 字段类型(文本, 多选, 单选)
                    columnType: this.columnType
                }, this.config_tableFilter[column.key]),
                // config_tableFilter 为字段筛选相关配置, 格式如下
                /*
                	{
                        nickName: {
                            sort: true, // 是否允许排序
                            filter: true, // 是否允许筛选
                            filterType: 'string', // 筛选类型(文本, 多选, 单选...)
                            query: '', // 筛选值
                            allList: null // 所有可能的筛选条件
                        }
                	}
                */
                on: {
                    // 触发筛选
                    updateList: () => {
                        const temp = this.config_tableFilter[column.key];
                        if (temp) {
                            /* 处理筛选 */
    
                            // 判断是否有值, 调整筛选条件对象
                            const con = column.key
                            if (temp.query.length) {
                                // 根据字段筛选类型的不同进行不同处理
                                switch (temp.filterType) {
                                    case 'string':
                                        // 根据后端要求的格式, 处理筛选条件, 赋值到筛选条件对象中 
                                        this.$set(this.sqlList_tableFilter, con, {
                                            $regex: temp.query
                                        });
                                        break;
                                     // ...
                                }
                            } else {
                                // 无筛选值, 删除筛选条件
                                this.$delete(this.sqlList_tableFilter, con);
                            }
    
                            /* 刷新列表 */
                            this.refreshData_filter();
                        }
                    },
                    // sync
                    'update:query': (val) => {
                        // 更新 query 数据
                        if (this.config_tableFilter[column.key]) {
                            this.$set(this.config_tableFilter[column.key], 'query', val);
                        }
                    },
                    'update:order_column': (val) => {
                        this.order_column = val;
                    },
                    'update:order_type': (val) => {
                        this.order_type = val;
                    }
                }
            });
    
            /* 返回 DOM */
            return h('div', {
                class: 'table-filter-container'
            }, [
                title,
                (this.order_column === order_column && this.order_type) ? sortIcons : undefined,
                filterPop
            ]);
        };
    }
    
  2. 筛选组件

    // 1. 全局注册筛选组件, 便于 render 函数引用
    import FilterBox from './filter-box.vue';
    Vue.component('FilterBox', FilterBox);
    
    // filter-box.vue
    <template>
        <el-poppover
            :width="width_popper"
            popper-class="filter-popper"
            ref="poptip"
            @hide="hide"
            @show="show"
            >
            <!-- 按钮, Poptip 触发器(默认隐藏, 鼠标 hover 时显示) -->
            <Icon
                slot="reference"
                custom="custom-table-head-filter"
                :color="filtering ? '#2d8cf0' : undefined"
                @mouseenter.native="hover_poptip_icon"
                @click.stop
                class="down-icon"
                ref="reference"
                ></Icon>
    
            <!-- Poptip 内容 -->
            <div v-if="!isHidden" style="font-weight: 400">
                <!-- 升序降序 -->
                <ul v-if="sort" class="filter-seq">
                    <li @click="sortBy('asc')">
                        <Icon custom="custom-ascending"></Icon>
                        <span>升序</span>
                    </li>
                    <li @click="sortBy('desc')">
                        <Icon custom="custom-descending"></Icon>
                        <span>降序</span>
                    </li>
                </ul>
    
                <!-- 内容筛选 -->
                <div v-if="filter" class="filter-content clearfix">
                    <!-- 文本型 -->
                    <template v-else-if="filterType === 'string'">
                        <el-input
                            v-model="keywords"
                            @keyup.enter.native="confirm" />
                    </template>
                    <!-- 其他略 -->
                    <!-- 选项型 -->
                    <!-- 单选型 -->
                    <!-- 数字型 -->
                    <!-- 国家地区型 -->
                    <!-- 日期时间型 -->
                    <!-- 用户型 -->
                    <!-- 树形筛选 -->
    
                    <!-- 底部按钮 -->
                    <div class="search-btns">
                        <el-button @click="cancel" type="minor" size="small">取消</el-button>
                        <el-button
                            type="success"
                            size="small"
                            @click="confirm"
                            :class="class_headerscreening"
                            >确认
                        </el-button>
                    </div>
                </div>
            </div>
        </el-poppover>
    </template>
    
    export default {
        name: 'filter_box',
        props: [
            'columnKey', // 当前 column.key
            'sort', // 是否允许排序
            'filter', // 是否允许筛选
            'filterType', // 筛选类型
            'query', // 筛选值
            'filtering', // 是否正在筛选
            'order_column', // 表格当前的筛选字段在数据库中的字段名
            'order_type', // 表格当前的筛选类型
            // 不同类型筛选可能需要的值
            'allList', // 单选多选所有可能的筛选选项
            'blank', // 是否显示选项 空(未填写)
            'dateOptions' // 日期类筛选的 dateOptions 配置
        ],
        computed: {
            ...mapState({
                enterpriseId: 'enterpriseId',
                userId: 'userId',
                userName: 'userName',
                fullName: 'fullName'
            }),
            // 根据类型的不同设置不同的宽度
            width_popper() {
                switch (this.filterType) {
                    case 'date':
                        return 682 - (this.dateOptions ? 0 : 92);
                    case 'users':
                        return 320;
                    default:
                        return 230;
                }
            }
        },
        data() {
            return {
                /* 筛选 */
                // 所有类型通用
                showing: false, // 当前面板是否显示中
                
                // 文本型
                keywords: '', // 搜索词
    
                // 其他略
                // 选择型
                // 负责人型
                // 单选型
                // 日期型
                // 国家地区型
                // 用户型
            };
        },
        methods: {
            /* Poptip 显隐 */
            // 按钮默认隐藏, 鼠标移上去时 click 触发筛选框的显示
            hover_poptip_icon() {
                if (!this.showing) {
                    this.$refs.reference.$el.click();
                }
            },
            show() {
                this.showing = true;
                switch (this.filterType) {
                    case 'string':
                        this.keywords = this.query;
                        break;
                    // ...
                }
            },
            hide() {
                this.showing = false;
                // 重置各种筛选条件
                this.keywords = '';
                this.closePoptip();
            },
    
            /* 排序 */
            sortBy(type) {
                this.$emit('update:order_type', type);
                this.$emit('update:order_column', this.columnCon);
                this.$emit('update:storageName_orderColumn', this.columnKey);
                this.$emit('updateList');
                this.closePoptip();
            },
            
            // 清空筛选条件
            clear_filter_data() {
                switch (this.filterType) {
                    case 'string':
                        this.keywords = '';
                        this.$emit('update:query', this.keywords);
                        break;
                    // ...
                }
            },
            // 取消筛选
            clear_filter() {
                switch (this.filterType) {
                    case 'string':
                    case 'number':
                        this.keywords = '';
                        this.$emit('update:query', this.keywords);
                        break;
                    // ...
                }
                this.$emit('updateList');
            },
    
            /* 确定取消 */
            confirm() {
                switch (this.filterType) {
                    case 'string':
                        this.keywords =
                            typeof this.keywords === 'string'
                            ? this.keywords.trim()
                        : this.keywords;
                        this.$emit('update:query', this.keywords);
                        break;
                    // ....
                }
                this.$emit('updateList');
                this.closePoptip();
            },
            cancel() {
                this.closePoptip();
            },
            closePoptip() {
                // 此处查看 element popover 组件源码, 找到此方法, 忘了为啥要这么弄了
                this.$refs.poptip.doClose();
            }
        },
        mounted() {
            // 不确定当前版本是否存在这个问题, 所以上面没写 isHidden 相关逻辑
            /* 页面有可能会渲染两个 thead, 一个显示出来让用户操作, 一个隐藏
            所以页面中有多个相同的 filterBox, props 变化使 poptip 显示弹窗时, 第二个看不见的 poptip 也会弹出
            这个属性就是为了去掉第二个显示出来的 poptip
            */
            this.isHidden = this.$refs.container.parentElement.parentElement.parentElement.classList.contains('is-hidden');
        }
    };
    
    // style ....
    
  3. 其他

    其他还有表格页对排序和筛选的各种处理, 筛选框的隐藏显示问题, 样式问题, 筛选完成后显示筛选标签在顶部方便用户查看筛选条件和快速删除筛选条件, 这里略过

element 实现国家地区选择组件 ❌

虚拟滚动❌

pl-table el-bigdata-table

右键新窗口打开功能❌

Datepicker 增加 inline 属性直接展示面板❌

element 多选 select 高度抖动

问题背景:

客户报 BUG : 多选 select (比如产品分类)选中三个或四个以上选项(每个选项只占都只有一行, 未换行)时, select 会不停抖动;

重现:

正常情况下未重现, 缩放浏览器(我是缩放到 110%)后重现

解决:

网上搜了下说可能是 tag 的问题, 于是定位;

发现之前为了让标签换行, 给 tag 加上了自定义样式 height: auto , 去掉了 height: auto 后问题解决; 但是去掉之后又会换行, 于是加回来了, 再加了一句 min-height: 24px , 问题解决;(24px 是不加 height: auto 时 tag 的固定高度)

element checkboxGroup 点击动态生成的 checkbox 无效, change 事件返回的数据不变(RadioGroup 也是)

通过 vue 调试工具查看, 发现触发了一个 input 事件, 其 payload 数据正常, 于是改 changeinput, v-model:value (在 input 时间 handler 中修改 value 值)

渲染 node 内容, 直接 return (<component>…</component>) 即可, 可以不用 h/createElement

看同事的代码, 直接在渲染时返回 template 格式的代码, 而不是用 h , 问了下同事, 说这么弄没问题, 不过自己还没试20

select 组件内部自定义❌

popover 组件 reference 元素高度变化时, 使用 updatePopover 更新 popover 定位

背景:

需要做一个多选下拉组件, 与多选 select 类似, 但下拉框中是 tree 而不是 option

解决:

  1. 直接用多选 select 做 reference 元素, 使用 popover-class 属性隐藏 select 框
  2. 选中树节点时, 更新 select 的 option 数组和 select 的 value (主要是图 select 的多选标签效果, 也可以自己实现)
  3. 在 select value 变化时, setTimeout 调用 this.$refs.popover的ref的属性.updatePopover , 更新弹出框位置, 避免 select 多选换行之后, popover 位置不变导致遮挡(如果部分情况下不奏效, 适当调节 setTimeout 的毫秒数, 我用的 50 ms)

其他:

本来看 elemnet-ui 文档后, 想从它说的 popover.js 找解决方法, 但没找到, 直接看 element-ui 源码, 看了 popover.js 和 vue-popper.js , 没啥头绪, 干脆去看 select , 发现 select-dropdown 文件很简单很直观, 发现了这个 updatePopover 方法, 试了下确实可以

以后遇到类似的, 其他用了 mixin: [import 的 vue-popper] 的组件, 应该也可以如此解决

在 popover 内部点击按钮打开弹框时, 阻止 popover 隐藏

背景:

需要做一个多选下拉组件, 与多选 select 类似, 但下拉框中是 tree 而不是 option

解决:

从源码可以看出, trigger=“click” 时, 组件是在监控 document 的点击事件, 判断点击事件是否发生在 popover 之外

于是想到, 给相关弹框加上 @click.native.stop , 阻止它向上传递 click 到 document , 问题就解决了

ps: 要注意下会不会有其他问题, 比如全局在追踪一些按钮的点击事件, 不传递了会不会追踪失败这之类的, 要结合具体功能看

未解之谜:定时累加计时器, 导致页面一直重新计算

背景:

请求接口获取会话列表, 每个会话有一个"客户最后回复时间", 是以"距今时间差"形式返回毫秒数, 前端定时每秒给时间差 + 1000ms , 更新这个时间差, 做到实时显示哪些会话是活跃会话(距今 x 小时内), 是的话就加上相关样式

结果给会话头像加上 :class="getMethod(lastReplyTime)" 样式计算逻辑之后, 与头像无关的"会话最后一条消息"等其他数据, 也会每秒就重新计算一次

解决:

不知道原因

思路上, 认为这是会话数据对象被更新导致重新计算, 于是, 转变为"给 class 绑上不会经常变的变量"

改为: 在定时器中, 执行计算逻辑, 结果单独存放到一个对象中, 各个会话以其唯一 id 从此对象中取 class :class="classObj[item.id]"

Logo

前往低代码交流专区

更多推荐