ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 前言 本文主要总结了vue实际开发项目当中应该如何解决一些实际的开发问题,可能你认为很简单,但短时间内也许你并没解决思路的。 ## 常见技术解答 ### for循环中针对ui样式的特征性样式或者事件 - 针对ui有特定的数据字段进行判断(也叫数据模型方法) 这种书数据的要求比较高,且要求你能够找到比较好的对应关系,需要针对class进行特征性的组件渲染。当你需要改变时改变数据即可重新渲染达到改变样式的目的。 ~~~ <li v-for="item of list" :key="item.id" :class="item.status?'color':''" @click="changeColor(item.id)">{{item.name}}</li> return { list:[ {id:1,status:true,name:1111}, {id:2,status:true,name:222}] } methods:{ changeColor(id){ this.list.map((item)=>{ if(item.id==id){ item.status=!item.status; } return item; }) } } ~~~ - 传入对应的参数以及事件源,可以进行相应的判断改变class 特点更加灵活,也可以根据需要传入你需要传入的item属性参数进行与class的匹配判断,不用改变接口返回的数据结构。 ~~~ <li v-for="item of list" :key="item.id" @click="changeColor($event)">{{item.name}}</li> return { list:[ {id:1,name:1111}, {id:2,name:222}] } changeColor(e){ let el=e.target; if(el.classList.contains("color")){ el.classList.remove("color") }else{ el.classList.add("color") } } ~~~ ### 计算属性方法的使用 问题描述:如果你的计算属性依赖于data的部分,而你的data对应的字段在data里没有申明,只是在请求接口时进行申明赋值,那么当接口请求时,虽然数据发生了变化,但是计算属性的值不会发生更新。 解决方案 :需要你在data里申明你计算属性依赖的字段,哪怕是空或者null ### 事件执行顺序问题 问题描述 :定义了输入框blur,再按钮点击事件问题,其中默认click的话,执行顺序是先执行blur再执行click.如果你需要场景在点击的时候不执行blur的事件 解决方案: 1 常规方案 : 需要吧点击事件变成@mousedown.prevent ,前者会让点击优于blur执行,后者会阻止blur执行 2 el-input并不生效,可以用计时器延迟执行 将失去焦点的事件计时器延迟执行,然后点击事件里清除定时器,也是可以只执行点击事件逻辑的 ### 路由参数变化组件不更新 问题描述 :路由参数变化,但是组件没有对应的更新,主要是因为一般获取参数写在了created路由钩子函数中,路由参数变化的时候,这个生命周期不会重新执行。 解决方案1:watch监听router ~~~ watch: { // 方法1 '$route' (to, from) { //监听路由是否变化 if(this.$route.params.articleId){// 判断条件1 判断传递值的变化 //获取文章数据 } } //方法2 判断页面路径 '$route'(to, from) { if (to.path == "/page") { /// 判断条件2 监听路由名 监听你从什么路由跳转过来的 this.message = this.$route.query.msg } } //方法 3 设置路径变化时的处理函数 '$route': { handler: 'resetData', immediate: true } } ~~~ 解决方案2 :为了实现这样的效果可以给router-view添加一个不同的key,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。 ``` <router-view :key="$route.fullpath"></router-view> ``` 解决方案3 :如果组件被放在<keep-alive>中的话,可以把获取新数据的方法放在activated钩子,代替原来在created、mounted钩子中获取数据的任务 ### 异步函数中使用this无法指向vue实例对象 问题描述 : 在定时器或者其他异步函数中使用传统的func导致this指向不到vue实例,主要原因是因为this指向的问题,详细的可以参考我的《神奇的this》这篇文章。 解决方案 :用箭头函数或者指定变量赋值为this(其他一些不能用箭头函数的地方自己也要注意) ### 定时器在组件销毁后还在执行 问题描述 :一些耗费性能的计时器或者动画在组件销毁之后还是执行的,导致性能变低。 解决方案 :在销毁组件的生命周期中销毁定时器或者一些动画的js ~~~ //组件销毁前执行的钩子函数,跟其他生命周期钩子函数的用法相同。 beforeDestroy(){ //我通常是把setInterval()定时器赋值给this实例,然后就可以像下面这么停止。 clearInterval(this.intervalId); }, ~~~ 这里也给出第二种方案,通过$once这个事件侦听器器在定义完定时器之后的位置来清除定时器。以下是完整代码: ~~~ const timer = setInterval(() =>{ // 某些定时器操作 }, 500); // 通过$once来监听定时器,在beforeDestroy钩子可以被清除。 this.$once('hook:beforeDestroy', () => { clearInterval(timer); }) ~~~ 类似于其他需要在当前页面使用,离开需要销毁的组件(例如一些第三方库的picker组件等等),都可以使用此方式来解决离开后以后在背后运行的问题。 如果不清楚$once、$on、$off的使用,这里送上官网的地址教程,在[程序化的事件侦听器](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E7%A8%8B%E5%BA%8F%E5%8C%96%E7%9A%84%E4%BA%8B%E4%BB%B6%E4%BE%A6%E5%90%AC%E5%99%A8)那里。 ### 组件名与引入时大小写不一致导致报错 问题描述: ~~~ This can lead to unexpected behavior when compiling on a filesystem with other case-semantic. Use equal casing. Compare these module identifiers: ~~~ 解决方案 :需要严格对应组件的大小写,避免低级错误 ### 动态添加的dom没有样式 问题描述:作为常识我们知道style中的样式都会追加scoped,这样针对模板dom中的样式就可以生效,但其生效后的最终样式并不是我们写的样式名,而是编码后的,所以我们在js中拼接上的dom结构样式并不会生效。 解决思路: 1 当添加的部分样式不会太多,而且是动态加载的,可以将其设置为非scopred的 2 将添加dom部分用的样式放到非scoped样式标签中 3 将添加的部分,如果有必要,可以另外写一个页面拆分的vue组件 拓展 : 项目中引入的其他ui框架的样式,如果你想覆盖修改,也是需要不加scoped的,如果你想整个项目覆盖,就可以在src/styles下定义customer-element.scss 这样的来重写覆盖样式。 ### vue中直接修改对象数据,页面视图不更新 问题描述 :你的数据对象类型,没有在data中进行定义,就没有增加对其的监听绑定,当你直接去使用或者定义数据时,会导致这个数据的vm视图层不会及时更新. 解决方案:这时候需要你将其定义到data中就会增加对其的监听。 备注:与此同理,你需要增加监听的数据或者变化的数据如果需要数据变化时马上更新,都要定义到data一份。 ### vue中直接修改数组数据,页面视图不更新 问题描述 :在常规理解中,视图与数据是双向绑定的,但是有时候修改data的数组或者对象值,视图不会更新 。 ~~~ data() { // data数据 return { arr: [1,2,3], obj:{ a: 1, b: 2 } }; }, // 数据更新 数组视图不更新 this.arr[0] = 'OBKoro1'; this.arr.length = 1; console.log(arr);// ['OBKoro1']; // 数据更新 对象视图不更新 this.obj.c = 'OBKoro1'; delete this.obj.a; console.log(obj); // {b:2,c:'OBKoro1'} ~~~ 解决方案 :由于js的限制,Vue 不能检测以上数组的变动,以及对象的添加/删除,很多人会因为像上面这样操作,出现视图没有更新的问题。 1 this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value) ~~~ this.$set(this.arr, 0, "OBKoro1"); // 改变数组 this.$set(this.obj, "c", "OBKoro1"); // 改变对象 ~~~ 2 数组原生方法触发视图更新: splice()、 push()、pop()、shift()、unshift()、sort()、reverse() 推荐使用splice方法会比较好自定义,因为slice可以在数组的任何位置进行删除/添加操作 3 替换数组 比方说:你想遍历这个数组/对象,对每个元素进行处理,然后触发视图更新。 ~~~ // 文档中的栗子: filter遍历数组,返回一个新数组,用新数组替换旧数组 example1.items = example1.items.filter(function (item) { return item.message.match(/Foo/) }) ~~~ - [codepen地址,可以进行问题重现](https://codepen.io/robinson90/pen/ZRWyYX) ### 需要无脑重复某内容 ~~~ <div v-for="n in 5"> <span>这里会被渲染5次,渲染模板{{n}}</span> </div> ~~~ ### babel-plugin-transform-runtime使用 * 出现问题:这个插件可以兼容并转化大部分的es6语法,但是部分语法也是不能转化或者存在具体问题的。 1. 异步加载组件时,会产生 polyfill 代码冗余 2. 不支持对全局函数与实例方法的 polyfill。不支持全局函数(如:Promise、Set、Map),Set 跟 Map 这两种数据结构应该大家用的也不多,影响较小。但是 Promise 影响可能就比较大了。不支持实例方法(如:'abc'.include('b')、['1', '2', '3'].find((n) => n 等等),这个限制几乎废掉了大部分字符串和一半左右数组的新特性。 而两个问题的原因均归因于 babel-plugin-transform-runtime 采用了沙箱机制来编译我们的代码(即:不修改宿主环境的内置对象)。由于异步组件最终会被编译为一个单独的文件,所以即使多个组件中使用了同一个新特性(例如:Object.keys()),那么在每个编译后的文件中都会有一份该新特性的 polyfill 拷贝。如果项目较小可以考虑不使用异步加载,但是首屏的压力会比较大。 * 解决方案:一般情况下 babel-plugin-transform-runtime 能满足大部分的需求,当不满足需求时,推荐使用完整的 babel-polyfill。 * 首先,从项目中移除 babel-plugin-transform-runtime,卸载该依赖: `npm un babel-plugin-transform-runtime -D`, * 接着修改 babel 配置文件 ~~~ // .babelrc { //... "plugins": [ // - "transform-runtime" ] //... } ~~~ * 然后,安装 babel-polyfill 依赖: `npm i babel-polyfill -D` * 最后,在入口文件中导入 ~~~ // src/main.js import 'babel-polyfill' ~~~ ### ES6 import 引用问题 在 ES6 中,模块系统的导入与导出采用的是引用导出与导入(非简单数据类型),也就是说,如果在一个模块中定义了一个对象并导出,在其他模块中导入使用时,导入的其实是一个变量引用(指针),如果修改了对象中的属性,会影响到其他模块的使用。 通常情况下,系统体量不大时,我们可以使用 JSON.parse(JSON.stringify(str)) 简单粗暴地来生成一个全新的深度拷贝的 数据对象。不过当组件较多、数据对象复用程度较高时,很明显会产生性能问题,这时我们可以考虑使用 Immutable.js。 鉴于这个原因,进行复杂数据类型的导出时,需要注意多个组件导入同一个数据对象时修改数据后可能产生的问题。 此外,模块定义变量或函数时即便使用 let 而不是 const,在导入使用时都会变成只读,不能重新赋值,效果等同于用 const 声明。 ### 动态懒加载组件 背景:在webpack的新特性中支持组件的懒加载,也就是说我们可以在加载到该路由的时候再把这部分脚本进行加载,同时这个在项目进行打包的时候,对应的文件也会被单独打包,对于首屏优化以及其他页面的资源加载优化都是非常好的。这也要求我们在每个页面组件使用组件的时候尽量按需引入,提升体验。 问题场景:那么我们需要解决的问题是: 0 webpack是静态解析路径的,直接传入变量并不可行 1 每次都写一串加载组件的代码很不方便,是否可以支持写成一个加载组件的方法 2 是否支持区分生产和开发环境,因为开发环境使用懒加载会导致热更新,导致更新变慢,所以开发环境使用全量默认加载,生产环境使用懒加载 解决方案如下 : 1 webpack的路径使用变量拼接,必须预先给出一个相对路径,然后把具体的组件路径在传入 2 用一个箭头函数,将需要传入的组件名或者相对路径传入 3 用process.env.NODE_ENV确定使用哪种加载方式 代码如下: 在原来的router/index.js中,定义一个加载组件的_import方法。 ~~~ // router/index.js const _import = require('./_import_' + process.env.NODE_ENV) //使用时 { path: '/', name: 'HelloWorld', component: _import('HelloWorld') }, // router/_import_development.js module.exports = file => require('@/views/' + file + '.vue').default // vue-loader at least v13.0.0+ // router/_import_production.js 如果你加载的vue不是这个路径 请自定义哦 module.exports = file => () => import('@/views/' + file + '.vue') ~~~ **注意事项:** 1 如果配置完之后,可能会有部分样式文件或者图片引入的src路径之外的,比如说static,报错找不到对应的文件,这时候需要改为根路径static的路径就可以解决这个报错?你需要装的模块:sass-loader,node-sass,stylus,stylus-loader,style-loader,css-loader,url-loader即可。具体的包需要进一步确定。 2 在具体的页面中,如果你想懒加载组件,也是如此的写法 ``` import vOther from '@/components/other' //修改后 const vOther = () => import('@/components/other') ``` ### vue中的data必须为函数 **场景** :vue入门的人可能在页面单独引入vue的时候,直接使用data为对象类型的,并没有问题,但是在spa应用中,如果组件中的data为对象类型就会报错。 **解决方案** :data换为函数,返回对象类型的键值对。 拓展 :你可能知道要这样做,这里稍微科普下原因,主要是因为根组件只会用一次,所以可以用对象,而子组件可能在一个应用中被多次使用,为了避免多个组件使用同一数据互相影响,所以讲data约定为了返回函数类型,返回需要的对象,以此保证子组件在数据渲染的时候不会互相影响。 ### 有父子标签关系的自定义组件渲染失败 **场景** :在自定义组件的时候,很多时候需要将ul下的li标签,table下的tr\td标签进行封装为自定义组件,但直接使用自定义组件会导致其最终生成的位置不是我们想要的。截图如下: ~~~ Vue.component("row",{ template:'<tr><td>{{content}}</td></tr>', data(){ return { content:'this is a row' } }, }) ~~~ ![](https://box.kancloud.cn/5c44cd9258f16d1add4c41625b7cc6fd_494x314.png) ![](https://box.kancloud.cn/983d443988a8e4817a1cea1610c58d7e_600x322.png) **解决方案** :原因是因为html会进行标签解析,tbody下的标签必须为tr,其他的同理。那么我们可以将其子标签设置为原来的标签类型,然后用is="selfComponent" 来解决这个问题。 `<tr is="row"></tr>` - [vueComponents使用](https://codepen.io/robinson90/pen/GGjmyx) 拓展: - 不要将渲染vue的容器元素定位到html或者body上,否则提示:` Do not mount Vue to <html> or <body> - mount to normal elements instead.` - 确保有在vue新建实例的时候将el属性绑定到一个html模板的标签上 ### ref使用 **场景** :虽然vue不建议直接操作dom,但是在复杂的场景中,我们需要进行dom的操作,这时候就可以借助ref实现。比如下面我们举一个简单的例子,通过ref获取dom节点,拿到其内容。 **解决方案** : ~~~ <div @click="handleClick" ref="hello">hello world </div> handleClick(){ console.log(this.$refs.hello) } ~~~ - 拓展案例 :实现计数器加和 场景 :假设我们有两个计数器组件的实例,现在需要用ref的方案得到两个计数器的加和。 代码如下: ~~~ <counter ref="one" @change="handleChange"></counter> <counter ref="two" @change="handleChange"></counter> <span>{{total}}</span> Vue.component("counter",{ template:"<div @click='change'>{{number}}</div>", data(){ return { number:0} }, methods:{ change(){ this.number++; this.$emit("change") } } }) //app父组件方法 handleChange(){ this.total=this.$refs.one.number+this.$refs.two.number }, ~~~ - 拓展认知 : this.$refs.name中如果是原生标签,拿到的是原生标签的节点,如果是组件,拿到的是组件的引用。 如果你用.name获取不到,可以尝试用【name】方式。 - [vueRefDemo使用](https://codepen.io/robinson90/pen/OERmra) ## 参考文档 - [vue项目实践中的一些问题](http://mp.weixin.qq.com/s/fgFOvWBC_P78hG154gyXYQ)