踩坑:一场由 vue-template-babel-compiler 插件 bug 引发的血案

2,921 阅读5分钟

关键词: 可选链操作符、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 找不到。

image.png

点击异常下面的代码位置,跳转到对应的调试代码位置。

image.png

可以看到很明显 vue 代码编译后的 render 部分结果并不符合预期。index 并不是我在单文件模板中定义的 index,而是编译为组件(_vm = this)上的 index,所以很显然代码执行结果肯定不会符合预期,因为组件上并没有 index 这个属性。

问题其实已经很清晰了,但是到底是什么原因导致了 vue 代码编译不符合预期?我百思不得其解,一开始我以为是我使用错了 slot(这里使用了匿名作用域插槽),但经过再三确认,翻遍 vue 官方文档和网络上的博客、论坛、视频教程,发现都没有错。然后,我又想是否是因为两层嵌套的 v-for + slot,触发了 vue 什么不为人知的 bug。我尝试使用 console.log 大法来检验我的想法,因此我在内层的 v-for 里面的 slot 内外使用 log 尝试打印 index。

image.png

打印结果如下: image.png

显然,在 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 模板编译正常。

总结

本文是我的一次踩坑记录,给我的启发主要还是问题的思考过程,发现问题了如何分析解决,是我们程序员的一项基本技能,而不是两眼抓瞎一头雾水最后放弃。其实理清思路,问题就能迎刃而解,过程中我们需要多尝试不断检验我们的猜想,当然我们一般可能会走很多弯路,最终才能找到正确的方向,但这才是真实的开发过程,真实的开发哪有一帆风顺的。