工作中对 Vue 和 iView 的一些技术备忘总结

文章目录

    • @[toc]
  • Vue
      • 1. 深度作用选择器
      • 2. $nextTick
      • 3. computed
        • 1. 不要在 `computed` 中使用 `ajax` 请求数据
      • 4. watch
        • 1. deep
        • 2. val 和 oldVal 一模一样
        • 3. immediate 属性
      • 5. keep-alive
      • 6. 组件通讯
        • 1.参考文章:
        • 2. 注意 eventBus 监听的时机
      • 7. 重置组件数据为初始状态
      • 8. Vue 实现定时提醒
        • 1. 提交要创建的任务
        • 2. 最外层页面实现提醒
        • 3. 在 Vuex 中设置好 taskChangeObj
      • 9. 在 JS 中引用 Vuex 和 Vue-Router
        • 1. Vuex
        • 2. Vue-Router
        • 3. this.$route
        • 4. prototype
      • 10. axios 在 response 拦截器中取消请求 (token过期, 跳转到登录页)
  • iView
      • 1. DatePicker 组件国际时间和本地时间问题
      • 2. 在 iView 组件的事件中传递额外参数
        • 1. $event
        • 2. arguments
      • 3. transfer 属性
      • 4. Select 组件联动时的 BUG
      • 5. 让 Select 组件既可手动输入, 也可以下拉选择(非 filterable 属性)
      • 6. 让 Cascader 动态请求一级目录, 点击后继续请求下级目录
      • 7. 给 iview-admin 框架右上角"关闭所有, 关闭其他"按钮下加上"刷新当前组件"功能
      • 8. 修改 iView 源码重新编译
      • 9. 让 Cascader 组件多选和清空
      • 10. Table 组件实现多页全选
      • 11. 在 JS 文件中使用 iView 组件

Vue

1. 深度作用选择器


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

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

注意, 预处理器要用可以用 /deep/ 代替 >>>

2. $nextTick


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

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

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

3. computed


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

4. watch


1. deep

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

2. val 和 oldVal 一模一样

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

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

3. immediate 属性

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

5. keep-alive


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

6. 组件通讯


1.参考文章:

vue2父子组件间相互通信

Vue 组件通信之 Bus

Vue使用EventBus传递数据的坑

2. 注意 eventBus 监听的时机

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

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

7. 重置组件数据为初始状态


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

8. 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

9. 在 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 等价…

10. 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
    };
    

iView

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


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

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

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


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

1. $event

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

2. arguments

某些函数默认带有两个参数, 此时用 $event 只能取到第一个, 搜索后发现可以通过 arguments 代替 $event

3. transfer 属性


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

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

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

4. 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] 取组件数据

5. 让 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 字体标黑(通过的情况也要显式操作,不然也可能变红),不通过标红

6. 让 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 了,毕竟展开就说明是要搜索,这也说的过去

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






    




    



update: true, 
excludePage: ''


updateCurrent() {
    this.excludePage = this.$route.name;
    this.update = false;
    this.$nextTick(() => {
        this.update = true;
        this.excludePage = '';
    });
}

8. 修改 iView 源码重新编译


  1. 直接把 iView 的 GitHub 仓库下载下来
  2. 改动代码后,可以 npm run dev 查看效果, npm run dist 重新编译
  3. 编译完成之后,用新的 dist 文件把原来的框架中的 node_modules 中的 iView 文件夹的 dist 代码覆盖掉

9. 让 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 = [];
      

10. Table 组件实现多页全选


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

11. 在 JS 文件中使用 iView 组件


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

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

你可能感兴趣的:(前端备忘)