#### 前言 在这部分,我主要会写一些在vue使用中遇到的问题以及常见的解决方法。 #### 1.Vue-Router中多次渲染顶层组件 下面是Vue-Router中的router-view的简单用法。下面是webpack入口文件中内容: ```js new Vue({ router, render: h => h(Layout) // data: function() { // return { name: "qingtian" }; // } // components: { Main }, // 必须指定template或者render方法,但是如果指定了会导致Main组件被重复渲染 // 下面的/的path又会重新渲染一次Main组件,所以Layout组件就只要指定router-view即可 // template: "
" }).$mount("#app"); ``` 注意:在使用的时候不要添加components,比如上面的注释部分,其实会立马渲染一个Main组件,导致和router里面的指定的path为"/"为Main组件重复渲染。其实在Layout组件中,我们只需要如下内容即可: ```js ``` 而下面是我们具体的router即前端路由部分: ```js export default new Router({ routes: [ { path: "/", // 1.默认路由的ref,React-Router里面的IndexRouter或者采用空的子路由 name:'default', // chunkName component: Main, children: [ { path: "", name: "default", component: resolve => require(["@/components/$ref.vue"], resolve) }, { path: "ref", name: "ref", component: resolve => require(["@/components/$ref.vue"], resolve) }, { path: "slot", name: "slot", component: resolve => require(["@/components/slotPlusSlotScope.vue"], resolve) } ] } ] }); ``` 这里当你访问的path为"/"的时候,其实上面app下的router-view将会被Main以及$ref.vue的组件替换掉。这一点一定要注意。因为他是router-view最本质的东西。 #### 2.Vue-Router如何获取到上一次的路由? 其实一开始我也是陷入了死胡同了,我想知道上一次的路由,然后判断**它是否在白名单中**,如果在白名单中我从localStorage中获取数据,如果不在白名单中我就执行采用默认的数据来渲染页面。这样就可以满足**用户点击后退按键的时候能够保存上一次的组件状态**。 ```js /** * 1.mounted的执行时机早于beforeRouteEnter * 2.这里面无法获取this实例 */ beforeRouteEnter(to, from, next) { next(vm => { // if (from.path && from.path.indexOf('landingPageCreate') != -1) { // const msgStatusSelect = localStorage.getItem('msgStatusSelect'); // const msgTypeSelect = localStorage.getItem('msgTypeSelect'); // msgStatusSelect2localStorage = msgStatusSelect; // msgTypeSelect2localStorage = msgTypeSelect; // } }) }, ``` 因为受限于上面两个条件:**第一点**:在beforeRouteEnter里面没法获取到this,那也意味着我不能通过this设置组件的值,即无法通过它完成UI的更新(虽然此时我明确的知道页面来源)。**第二点**:mounted的执行时机早于beforeRouteEnter,这也意味着组件已经更新了,然后你才去获取值,所以,即使你通过vm.xxx=xxx这种方式赋值,在mounted里面也是无法获取到的,因为beforeRouteEnter在后面执行的,即还没有赋值,所以当然无法读取! 所以我的判断是beforeRouteEnter一般只是用于路由跳转redirect等,而无法通过这个方法修改组件数据。最后我抛弃了白名单的策略,**采用的策略如下**:
1.每次文本框的值修改后都写到localStorage,同时调用this.$router.replace方法
2.浏览器后退的时候可以从localStorage里面获取数据,然后做一次重新搜索,在created或者mounted里面都可以
```js watch:{ // 监听两个变量的变化并调用this.$router.replace更新到URL 'msgTypeSelect': function(curVal, oldVal) { const href = window.location.href; const hash = window.location.hash; const idx = href.indexOf('?'); let query = href.substring(idx + 1); if (idx != -1) { query = query.split('&').reduce((prev, cur) => { const [key, value] = cur.split('='); prev[`${key}`] = value; return prev }, {}); // 已经有查询字符串 this.$router.replace({ path: '/pushlist', query: { ...query, msgTypeSelect: curVal, } }); } else { this.$router.replace({ path: '/pushlist', query: { msgTypeSelect: curVal } }); } }, 'msgStatusSelect': function(cur, old) { const href = window.location.href; const hash = window.location.hash; const idx = href.indexOf('?'); let query = href.substring(idx + 1); if (idx != -1) { query = query.split('&').reduce((prev, cur) => { const [key, value] = cur.split('='); prev[`${key}`] = value; return prev }, {}); this.$router.replace({ path: '/pushlist', query: { ...query, msgStatusSelect: cur } }); } else { this.$router.replace({ path: '/pushlist', query: { msgStatusSelect: cur } }); } }, }, data(){ return { msgStatusSelect: this.getQueryString('msgStatusSelect', true) || "-1", msgTypeSelect: this.getQueryString('msgTypeSelect', true) || "-1", // 默认从URL上获取,更新this.data的值 // 初始情况URL为空,所以搜索出全部的结果(-1表示全部) } }, created(){ // 初次进入或者后退的时候走一次接口,如果能从URL中拿到数据就表示后退,否则-1表示直接进入 const href = window.location.href; const hash = window.location.hash; const idx = href.indexOf('?'); let query = href.substring(idx + 1); if (idx != -1) { query = query.split('&').reduce((prev, cur) => { const [key, value] = cur.split('='); prev[`${key}`] = value; return prev }, {}); // 已经有查询字符串 this.$router.replace({ path: '/pushlist', query: { ...query, gmtCreateBegin: `${date} 00:00:00`, gmtCreateEnd: `${date} 23:59:59`, msgTypeSelect: this.getQueryString('msgTypeSelect', true) || "-1", msgStatusSelect: this.getQueryString('msgStatusSelect', true) || "-1" } }); } else { this.$router.replace({ path: '/pushlist', query: { gmtCreateBegin: `${date} 00:00:00`, gmtCreateEnd: `${date} 23:59:59`, msgTypeSelect: this.getQueryString('msgTypeSelect', true) || "-1", msgStatusSelect: this.getQueryString('msgStatusSelect', true) || "-1" } }); }, methods:{ getQueryString: function(name, isSpa) { name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), results = regex.exec(isSpa ? location.hash : window.location.search.substr(1)); return results == null ? "" : decodeURIComponent(results[1]); } } } ``` 这样不需要使用localStorage就可以直接完成页面后退的功能了,而不用判断上一次的路由来源。只要操作的时候修改URL,然后从URL中读取参数并查询,没有参数的话直接传入**-1表示直接进入而不是浏览器后退**。当然上面的重复代码应该提取到公共的函数,此处不再赘述。关于replace的用法你可以[参考这个文档](https://router.vuejs.org/zh-cn/essentials/navigation.html)。 #### 3.Vue中使用布尔值控制显示与隐藏 ```js // 默认cfg.disableAlgorithm为true 算法实时投放<\/el-checkbox> ``` 下面是对变量进行监听: ```js watch: { 'cfg.freq': function(cur, prev) { if (cur == 1) { this.cfg.disableAlgorithm = true; } else { this.cfg.disableAlgorithm = false; } } } ``` 主要在于使用:disable,而不能是disabled="cfg.disableAlgorithm",因为**后者是字符串了**。 #### 4.Vue-cli中proxy代理不生效 修改build/dev-server.js,添加如下代码: ```js var proxyMiddleware = require("http-proxy-middleware"); var devMiddleware = require("webpack-dev-middleware")(compiler, { publicPath: webpackConfig.output.publicPath, quiet: true }); var hotMiddleware = require("webpack-hot-middleware")(compiler, { log: () => {} }); // handle fallback for HTML5 history API app.use(require("connect-history-api-fallback")()); app.use( "/", proxyMiddleware(["/baoluo/**", "/hotkeywords/**"], { // target: "http://30.55.144.240:7001", // target: "http://10.101.16.16:80/", target:"http://test.heyi.test", changeOrigin: true }) ); // 是不是代理服务器10.101.16.16 // 在devMiddeleware,hotMiddleware之前 // serve webpack bundle output app.use(devMiddleware); // enable hot-reload and state-preserving // compilation error display app.use(hotMiddleware); ``` 而且http-proxy-middleware必须在webpack-hot-middleware和webpack-dev-middleware之前。这一点是测试的时候验证的,至少在我的情况下是这样的。而对于代理的http-proxy-middleware第一个参数可以是一个数组的。之所以会想到上面的方式是因为修改proxyTable并没有生效: ```js proxyTable: { '/hotkeywords/query': { target: 'http://30.55.144.240:7001/', changeOrigin: true } }, ``` #### 4.el-table默认选中之toggleRowSelection方法 ```js // 下面的toggleRowSelection必须通过$nextTick方法 this.$nextTick(function() { this.configureRowSelections(itemList); }); // itemLists为所有的表格元素 configureRowSelections(itemList) { const ids = [45, 116]; const preSelections = itemList.filter((el, idx) => { return ids.indexOf(el.id) != -1 }); //逐个调用 preSelections.forEach(el => { this.$refs.scgAddTable.toggleRowSelection(el, true); }) }, ``` 从上面可以知道,toggleRowSelection必须**逐个调用**才行。 #### 5.vue中的三目运算符 在template中可以这么写: ```html <\/i> 配置 ``` #### 6.el-tab获取到当前切换到的tab的id 比如下面的值: ```html ``` 在@tab-click回调函数中可以如下处理: ```js selectTab: function(vu, evt) { const { id } = vu.$el; this.selectedTab = id; this.$nextTick(function() { this.onSearch(); }); }, ``` 即通过**vu.$el.id**获取到当前切换到tab页面! #### 7.vue的回调函数传入参数 比如template可以如下方式书写: ```html 添加 ``` 这样showVideo函数中就可以获取到传递过去的值,这和React是不一样的,React的回调函数是不能直接通过showVideo()这种方式注册的,因为**它相当于直接调用**。 #### 8.el-table需要格式化数据,比如日期 ```html ``` 通过在methods里面定义一个函数formatter,这样可以通过**moment**来对时间进行格式化了!!! #### 9.vue中的{{}}符号可以写出运算 比如下面的代码: ```html ``` 这样当scope.row.used或者scope.row.total为null等的时候就会显示0. #### 10.vue的回调函数中直接修改this中的值 ```html 取 消 确 定 ``` 比如上面的@click回调函数就直接设置**dialogVisible = false**。 #### 11.vue模板中设置disable的值 ```html 自动过审 提交待审 ``` 这样当cfg.isAudit的值为false的时候,自动过审选项将不能选择。 #### 12.vue中设置互斥的el-checkbox组/按钮组 比如下面的图: ![](./images/exclude.png) 假如我选择了"突发全量(值为1)"就不能选择前面的任意一个checkbox,此时我的代码逻辑如下: ```js data:function(){ return { hasOne: false, // 表示是否选中了值为1的checkbox,因为有两种情况需要区分 // 情况1:先选中了前面,然后选择的1 // 情况2:先选中了1,后选中的前面的 cfg:[ {typeOption:[]} ] } } ``` 在created里面默认设置: ```js created:function(){ this.hasOne = typeOption.indexOf('1') != -1 } ``` ```js optimalChange: function(val) { // 含有突发全量 if (!!~val.indexOf(1)) { this.cfg[0].typeOption = [1]; if (this.hasOne) { // 已经选择过了突发全量了 this.cfg[0].typeOption = val.filter(el => { return el != 1; }) this.hasOne = false; } else { if (this.cfg[0].typeOption.indexOf(1) == -1) { this.cfg[0].typeOption.push(1); } // 前一次没有1,那么插入1 this.hasOne = true; } } else { this.hasOne = false; this.cfg[0].typeOption = val; } console.log('设置后为===', this.cfg[0].typeOption); } ``` 下面是vue的template: ```html 优化选项: 日活保护 低活保护 突发全量 ``` #### 13.[emoji-picker](https://github.com/OneSignal/emoji-picker)屏蔽某一个表情 上次从服务端接受一个前端项目,遇到类型为relax的emoji在编辑的时候不能正常显示,找过了很多方法最后也没有查到具体的问题。最后采用了一个极端暴力的方法:将这个emoji去掉,即修改他的load方法: ```js // 每次切换面板都会重新load一次 EmojiMenu.prototype.load = function(category) { var html = []; var options = $.emojiarea.icons; var path = $.emojiarea.assetsPath; var self = this; if (path.length && path.charAt(path.length - 1) !== "/") { path += "/"; } var updateItems = function() { self.$items.html(html.join("")); }; if (category > 0) { for (var key in options) { // 这里做了处理,如果是:relaxed的emoji直接隐藏掉 // category>0表示不是经常使用的面板 var is = key == ":relaxed:" ? true : false; if (options.hasOwnProperty(key) && options[key][0] === category - 1) { if (is) { html.push( '' + EmojiArea.createIcon(options[key], true) + '' + util.htmlEntities(key) + "" ); } else { html.push( '' + EmojiArea.createIcon(options[key], true) + '' + util.htmlEntities(key) + "" ); } } } updateItems(); } else { ConfigStorage.get("emojis_recent", function(curEmojis) { curEmojis = curEmojis || defaultRecentEmojis || []; var key, i; for (i = 0; i < curEmojis.length; i++) { // 这里做了处理,如果是:relaxed的emoji直接隐藏掉 var is = util.htmlEntities(key) == ":relaxed:"; key = curEmojis[i]; if (options[key]) { if (is) { html.push( '' + EmojiArea.createIcon(options[key], true) + '' + util.htmlEntities(key) + "" ); } else { html.push( '' + EmojiArea.createIcon(options[key], true) + '' + util.htmlEntities(key) + "" ); } } } updateItems(); }); } }; ``` 修改后在"经常使用/emoji"面板都不会有relax的emoji了! #### 14.使用[vue-treeselect](https://vue-treeselect.js.org/)实现tree-select单选逻辑 ```js data:function(){ return { msgTypeEnum: [{ id: "1000", label: 'pp', children: [{ id: "1000:4", label: 'xx' }, { id: "1000:2", label: 'yy' }, { id: "1000:1", label: 'ttt' }] }, { id: "1001", label: 'rrr', children: [{ id: "1001:4", label: 'ff' }, { id: "1001:2", label: 'gg' }, { id: "1001:1", label: 'ee' }] }, { id: "1002", label: 'gg', children: [{ id: "1002:2", label: 'uu' }, { id: "1002:1", label: 'oo' }] }] } } /** * 取消选中某一个子级元素,如果是父级元素被取消选择,那么在msgTypeSelect中可以监听到,因为选中了另外一个 */ msgTypeDeselect: function(value) { const { id, label, children } = value; const idx = this.searchTreeParams.indexOf(id); const [msgType, typeOption] = id.split(':'); // 取消msgType和typeOption // console.log('取消选中的值以及下标为', value, idx); if (idx != -1) { this.searchTreeParams.splice(idx, 1); } console.log('取消选中后的searchTreeParams==', this.searchTreeParams); }, /** * 取消选中某一个子级元素,如果是父级元素被取消选择,那么在msgTypeSelect中可以监听到,因为选中了另外一个 */ msgTypeDeselect: function(value) { const { id = "", label, children } = value; const idx = this.searchTreeParams.indexOf(id); const [msgType, typeOption] = id.split(':'); // 取消msgType和typeOption const searchTreeParams = this.searchTreeParams; const isLeaf = id.indexOf(":") != -1; // 如果是叶子节点 if (idx != -1 && isLeaf) { this.searchTreeParams.splice(idx, 1); // 如果你要取消选择的这个在数组里面,那么直接删除掉即可 console.log('searchTreeParams====', searchTreeParams); } else { // 是根节点,如1001,1002...要删除所有的根节点下的子节点 const childrenEls = this.msgTypeEnum.filter(el => { return el.id == id; })[0].children; childrenEls.forEach(el=>{ const idx = this.searchTreeParams.indexOf(el.id); if(idx!=-1){ this.searchTreeParams.splice(idx,1); } }); } console.log('取消选中后的searchTreeParams==', this.searchTreeParams); } /** * 选中某一个 * */ msgTypeSelect: function(cur) { const { id, label, children } = cur; if (!children) { //没有children表示是子级元素 const [msgType = '', typeOption = []] = id && id.split(':'); this.searchTreeParams = this.searchTreeParams.filter(el => { const dot = el.indexOf(':'); if (dot != -1) { const [localMsgType, localOption] = el.split(':'); if (localMsgType == msgType) { return true; } } return false; }); this.searchTreeParams.push(id); } else { // 选中的这个有子级 const childrenItems = this.msgTypeEnum.filter(el => { return el.id == id; })[0].children.reduce((prev, cur) => { return prev.concat(cur.id); }, []); this.searchTreeParams = childrenItems; } console.log('选中后的searchTreeParams===', this.searchTreeParams); } ``` 下面是vue的template的逻辑: ```html ``` 最后的效果就是,tree-select的**最外层是单选的,而里面的子级选项是多选**的! #### 15.在radio-group切换之前弹出确认框 在radio-group切换之前我们希望弹出确认框,用户**点击了确认后才真实切换,点击了取消后不会切换**。一开始使用的是v-model,最后发现v-model会立即导致值的变化从而无法在值变化之前进行拦截确认。最后修改为v-bind: ```html ``` 下面是方法的代码: ```js mockSendChange: function(value, arg) { const h = this.$createElement; // 切换到是的时候提示 if (value) { this.$msgbox({ title: '消息', message: h('p', null, [ h('span', null, ' '), h('i', { style: 'color: red' }, '模拟发送不会真实发送,用于测试同学模拟全量发送') ]), showCancelButton: true, showClose:false, confirmButtonText: '确定', cancelButtonText: '取消', beforeClose: (action, instance, done) => { if (action === 'confirm') { this.mockSend = value; // 点击确定设置为指定的值 setTimeout(() => { done(); }) } else { // 点击取消保持原来的值不变 done(); } } }).then(action => { }); } else { // 切换到"否""不做提示 this.mockSend = value; } } ``` 这个例子有三点需要注意,第一点是使用了radio-group的**@input而不是[@change](https://github.com/ElemeFE/element/issues/3181)**事件: ```js @input="mockSendChange($event,arguments)" ``` 第二点使用$event可以传递参数,第三点是使用v-bind而不是v-model。 #### 16.删除前二次确认的vue实现 我们可以开发下面的组件: ```js import Vue from "vue"; Vue.component("confirmation-modal", { // template: "#modal", // 这种指定id的例子是如何使用的,必须在index.html中书写 props: ["open", "user"], template: ` `, methods: { // confirmation-modal就是template渲染后的内容,点击取消或者确认的时候 // emit特定的事件 onConfirm() { this.$emit("confirm"); }, onCancel() { this.$emit("cancel"); } } }); ``` 然后在template中按照如下方式来使用: ```html ``` 在confirmation-modal这个子组件中我们通过@confirm和@cancel注册了两个事件,这样在子组件中通过**emit就能触发父组件事件**。完整的代码可以[点击这里](./source/confirm.vue),而具体效果可以[点击这里](https://codepen.io/blokche/pen/eWMrdN)。 #### 17.Vue为点击的按钮添加一个.active类 比如下面是template的代码: ```html
万象定向包 上传UTDID号码包 上传UID号码包
``` 下面是selectDT的代码,主要是修改selectedDTIdx的值: ```js selectDT: function(id) { this.selectedDTIdx = id; } ``` #### 18.重置搜索条件 ```js this.searchParams = this.$options.data().searchParams; // 获取到的是初始的搜索默认值 // this.$data.searchParams ``` 前者是获取默认值,而后者$data是获取当前的值 #### 19.vue的keep-alive不生效问题 比如路由配置如下: ```js { path: "/datamanager/overview", component: Home, name: "home", // Home组件 children: [ { path: "/createNew", name: "createNew", component: PushCreateNew, meta: { keepAlive: true } } ] } ``` 很显然Home组件下可能会出现各种子级组件,所以我们Home组件的模板为: ```html ``` 很显然模板中也出现了keep-alive包裹的router-view,但是有一点需要注意的是**keep-alive必须在transition里层,而不能是处于transition外层**,否则不会生效。还有一点也要注意,即[keep-alive要求被切换的组件都要具有name](https://vuefe.cn/v2/guide/components-dynamic-async.html#dynamic-component-demo),要么是使用组件的name选项,要么就是通过局部/全局注册。 #### 20.el-table手动调用selection-change方法 比如表格中某一个列要求设置为el-input,同时这个el-input失去焦点的时候要获取当前表格选中的所有的**选中数据**,此时可以通过如下方法: ```jsx ``` 即在el-input失去焦点的时候获取它: ```js dialogListRulesBlur: function(value) { const $tableEl = this.$refs.dialogListRules; const selections = $tableEl.selection; //可以获取selection this.handleSelectionChange(selections); }, handleSelectionChange: function(selections) { const groupRule = selections.reduce((prev, cur) => { const { priority, ruleId } = cur; return prev.concat({ priority: +priority, ruleId: +ruleId }); }, []); this.cfg[1].groupRule = groupRule.sort(compare('priority')); } ``` #### 21.手动调用createElement创建element-ui的组件 ```js renderHeader: function() { const h = this.$createElement; return h("el-popover", { trigger: "click", title: '优先级说明', content: "优先级数字越小,优先级越高", placement: "bottom", width: "200" }, [h('i', { class: "el-icon-question", slot: "reference" }, "优先级")]) }, ``` 下面是vue的[官方$createElement说明](https://cn.vuejs.org/v2/guide/render-function.html#createElement-%E5%8F%82%E6%95%B0): ```js createElement( // {String | Object | Function} // 一个 HTML 标签字符串,组件选项对象,或者 // 解析上述任何一种的一个 async 异步函数,必要参数。 'div', // {Object} // 一个包含模板相关属性的数据对象 // 这样,您可以在 template 中使用这些属性。可选参数。 { }, // {String | Array} // 子节点 (VNodes),由 `createElement()` 构建而成, // 或使用字符串来生成“文本节点”。可选参数。 [ '先写一些文字', createElement('h1', '一则头条'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ] ) ``` #### 22.Vue传递给子组件的props变化了,但是没有重新渲染 可以通过添加一个key来完成更新(这种情况常见于**复杂对象的情况**): ```js ``` #### 23.el-tab下使用自定义组件问题 ```js ``` el-tabs在每一次循环的时候mtr-tab组件都会被挂载,也就是说:"el-tabs不等待该tab呈现的时候才挂载"(是否可通过[lazy](http://element-cn.eleme.io/#/zh-CN/component/tabs)处理暂未研究),这对于数据初始化很重要。 #### 24.el-tab下通过ref获取动态产生的自定义组件 ```js this.$on('getInnerValue', function() { for (let loop = 0, len = this.editableTabs2.length; loop < len; loop++) { const name = this.editableTabs2[loop].name; // v-for中是个数组,注意上一点23的mtr-tab组件的ref生成逻辑 // 这里不能通过this.$refs[`refMaterial${name}`].$emit('getInnerValue')获取 this.$refs[`refMaterial${name}`][0].$emit('getInnerValue'); } this.localStoreMaterial(this.materialMap); }) ``` 需要通过this.$refs[`refMaterial${name}`][0]获取产生的自定义组件句柄。 参考文献: [vue实现前进刷新,后退不刷新](https://juejin.im/post/5a69894a518825733b0f12f2)