关键词: 可选链操作符、vue-template-babel-compiler、vue 插槽、v-for
最近在项目中遇到一个莫名其妙的 bug,其牵扯的知识点比较多,也耗费了我不少时间来排查问题。这里记录一下,希望能给遇到类似问题的小伙伴提供一点思路吧。
背景说明
这里先说明下问题引发的背景,我在使用 vue2.0 搭建的前端项目中,在单模板组件的 template 中使用了比较新的语法“可选链操作符”,不清楚什么是可选链操作符的小伙伴可以点击链接了解一下。举个例子大家就知道可选链是啥了:
// 可选链操作符写法
console?.log('输出');
// 等价写法:
// 短路逻辑写法
console && console.log('输出');
上面代码中的问号“?”就是可选链语法,他的作用就是用于判断对象是否存在,如果不存在的话就直接返回,防止因为对象不存在而抛出异常,其在 es5 中的等价写法就是短路逻辑。
由于这个新语法过于新(2020年刚出的),但是用法又特别吸引我,导致我想法设法想要在项目中用上它,最终我通过千方百计终于实现在 vue template 中使用上这种新语法,其实如果不做特殊配置,高版本的 webpack 也是可以正常识别出 js 代码中的可选链语法的,只是在 vue 的 template 中使用可选链语法无法识别,这是因为 vue-template-compiler 还不支持,所以我的做法就是引入 vue-template-babel-compiler 插件来替代 vue 本身的模板编译器:vue-template-compiler,以下是我的 vue-cli 的配置。
// vue.config.js
/* eslint-disable */
const path = require('path')
module.exports = {
lintOnSave: false,
chainWebpack: config => {
const dir = path.resolve(__dirname, 'src/assets/icons')
config.module
.rule("svg")
.exclude.add(dir)
.end();
config.module
.rule('icons')
.test(/.svg$/)
.include.add(dir)
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end();
// --------- 新增插件配置 ---------
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compiler = require('vue-template-babel-compiler');
return options;
});
// --------- 新增插件配置 ---------
config.entry('app').clear().add('./src/main.js');
},
}
增加好插件后,项目编译通过,正常识别了可选链语法。我本以为万事大吉了,谁曾想这个操作为我后面的开发埋下了一个巨大隐患。
问题的发生
事情是这样的,我在项目中使用了 element-ui 的表格组件来展示数据,里面用到了双层 v-for 嵌套 vue 插槽,来构造表格结构。
<template>
<el-table
:data="rowValues"
style="width: 100%">
<el-table-column
type="selection"
align="center"
width="45"/>
<el-table-column
prop="index"
label="序号"
align="center"
type="index"
sortable=true
/>
<el-table-column
v-for="(column, index) in config"
:key="column.field"
:label="column.label"
:prop="column.field"
:show-overflow-tooltip="true"
:align="column.align"
:header-align="column.headerAlign"
:width="column.width"
:sortable="column.sortable">
<template v-if="!column.columns" v-slot="scope">
<span v-if="!column.dataSource?.svg)">
{{ (column.dataSource?.formatter ? scope.row[index] : scope.row[index] }}
</span>
<svg-icon v-else :icon-class="column.dataSource?.svg[scope.row[index]] || 'unknown'"/>
</template>
<template v-if="column.columns">
<el-table-column
v-for="(item, i) in column.columns"
:key="item.field"
:label="item.label"
:prop="item.field"
:show-overflow-tooltip="true"
:align="item.align"
:header-align="item.headerAlign"
:width="item.width"
>
<template v-slot="scope">
<span v-if="!column.dataSource?.svg">
{{ scope.row[index + i] }}
</span>
<svg-icon v-else :icon-class="column.dataSource?.svg[scope.row[index+i]] || 'unknown'"/>
</template>
</el-table-column>
</template>
</el-table-column>
</el-table>
</template>
运行项目后表格数据一更新,就抛出下面的异常,说外层的的 v-for 循环的 index 找不到。
点击异常下面的代码位置,跳转到对应的调试代码位置。
可以看到很明显 vue 代码编译后的 render 部分结果并不符合预期。index 并不是我在单文件模板中定义的 index,而是编译为组件(_vm = this)上的 index,所以很显然代码执行结果肯定不会符合预期,因为组件上并没有 index 这个属性。
问题其实已经很清晰了,但是到底是什么原因导致了 vue 代码编译不符合预期?我百思不得其解,一开始我以为是我使用错了 slot(这里使用了匿名作用域插槽),但经过再三确认,翻遍 vue 官方文档和网络上的博客、论坛、视频教程,发现都没有错。然后,我又想是否是因为两层嵌套的 v-for + slot,触发了 vue 什么不为人知的 bug。我尝试使用 console.log 大法来检验我的想法,因此我在内层的 v-for 里面的 slot 内外使用 log 尝试打印 index。
打印结果如下:
显然,在 v-slot 前一刻 index 还能识别,但是一进入 v-slot,编译器就找不到index 了,这就邪门了,不科学啊。作用域插槽的限制是父组件无法直接访问子组件的数据,可没说自己不能访问自己的数据啊...特别说明一下,这里使用了 el-table-column,其实就是 element-ui 官方封装的一个带插槽的组件,这里我封装的组件使用 el-table-column,我的组件是父组件,el-table-column 是插槽子组件,使用 v-slot:default="scope" 来让父组件获取子组件数据。可以看到,插槽在使用上没有任何问题,但是编译器偏偏无法识别自己定义的 index 属性(v-for 中定义的)
问题的突破
在我思考再三,静下心来思考过后,发现前不久我的一个操作可能是引发这个问题的元凶。我们现在的问题很明确,即:为什么 vue 编译器无法正确识别模板里面定义的变量?。想要找到问题的真正原因,我们就需要知道谁编译了 vue 的 template?没错正常情况下是 vue-template-compiler,但是我的项目里已经被我替换成了 vue-template-babel-compiler,对就是我为了使用可选链语法而引入的这个插件,从插件的名字可以看出来,它就是一个 vue template 的编译器,有了它 vue 自带的模板编译器就会被替代,而这个插件的开发人员并不是 vue 官方,所以有问题在所难免。。。或者因为版本的问题,插件能够编译识别的 vue 版本过低,稍微高级一点的 vue 语法,它就可能无法识别,或识别出错。
找到问题可能原因后,我果断删除了问题插件,并使用了短路逻辑来替换之前的模板内可选链语法,再次编译运行后发现问题解决,vue 模板编译正常。
总结
本文是我的一次踩坑记录,给我的启发主要还是问题的思考过程,发现问题了如何分析解决,是我们程序员的一项基本技能,而不是两眼抓瞎一头雾水最后放弃。其实理清思路,问题就能迎刃而解,过程中我们需要多尝试不断检验我们的猜想,当然我们一般可能会走很多弯路,最终才能找到正确的方向,但这才是真实的开发过程,真实的开发哪有一帆风顺的。