vue jsx 不完全指北

4,694 阅读5分钟

Vue 作为日前最为火热的前端框架之一,其流行程度很大部分得益于对开发者友好。尤其是SFC(单文件组件)的模式深得人心,开发者通过在一个文件里同时书写模板,JS 逻辑以及样式,就能完成组件的封装,相比其他方式,组件更加内聚,便于维护。

render 函数简介

在 Vue 2.0 版本之后,Vue 增加了 vdom 以及 render函数等新特性, template 模板并不会直接生成真实的 dom, 而是先编译为 render 函数, 再由 render函数 生成虚拟DOM。 因此除了使用"传统"的模板来构建 UI,也可以使用 render 函数,借助于 JavaScript 的 power,开发者可以更灵活的控制 UI。

Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力,这就是 render 函数,它比 template 更接近编译器。

于 React 栈(React & React Native)的开发者而言, render 函数再熟悉不过了(毕竟 React 中只能通过 render 函数来声明 UI) 。那是不是说明,我们可以用类似的方式来实现 Vue 组件。别高兴太早,我们先来看看官网给的例子吧。

image

这是什么鬼?render 函数里居然不是熟悉的 jsx,而是调用了非常原始的 createElement函数。这个本没有什么毛病,毕竟刚入门 React 时,是用的这个函数,且对于比较简单的UI ,这个函数足以胜任。我们再来看一个稍微复杂一些的没有使用 jsxrender 函数。

image

看到这样的 render 函数, 有没有很怀念简洁可读的jsx,没有的话,那大概是个大神或者自虐狂吧😂。

拥抱 JSX

虽然Vue并没有提供开箱即用的jsx 支持,但其提供了babel 插件,让我们也能像 React 一样(90%相似)使用 jsx 来 构建 UI。废话说了一堆,是时候开始 vue jsx 之旅了。开始之前先对比下两种表格的使用方式。

element ui 中的 Table

<el-table
  :data="tableData"
  style="width: 100%">
  <el-table-column prop="date" label="日期" width="180"></el-table-column>
  <el-table-column prop="name" label="姓名" width="180"></el-table-column>
  <el-table-column prop="address" label="地址"></el-table-column>
</el-table>

iview 中的 Table

 <Table :columns="columns1" :data="data1"></Table>

直观的看来,第二种更符合数据驱动的思想。当然我们在这里并不比较这两种方式孰优孰劣,仁者见仁智者见智。那么是否有办法也让 element ui 中的 table 也支持第二种方式呢?办法肯定是有的。用jsx 写组件应该再合适不过了,下面我们就以 jsx 的方式来对 el-table 进行二次封装,

目标结果

<table-panel
    showHeaderAction
    :data="taskList"
    :totalSize="totalSize"
    :columns="tableColumns"
    :onPageChange="handlePageChange"
    :onHeaderNew="handleOpenEditPage"
  />

jsx 相关配置

npm install\
  babel-plugin-syntax-jsx\
  babel-plugin-transform-vue-jsx\
  babel-helper-vue-jsx-merge-props\
  babel-preset-env\
  --save-dev
npm i babel-plugin-jsx-v-model -D

这个模块是可选的,可以让 jsx 支持 v-model指令

  • 配置Babel 插件(根目录下.babelrc文件)
{
  "presets": ["env"],
  "plugins": ["jsx-v-model", "transform-vue-jsx"]
}

render with jsx

注,在实现该组件过程中,并没有直接继承 ElTable,而是利用inheritAttrs特性,父组件上非父props 的属性都会回退到 dom 上的 attr 属性,组件内可以通过 this.$attrs获取到这些属性,并传递给ElTable 达到"继承"ElTable 的效果。

image

在上面的render 函数中,我们用了三个函数分别来返回 Header, Body, 以及 Footer 来组合我们的新组件。我们先来看下renderPanelBody函数中到底是什么东西

renderPanelBody(h) {
      const props = {
        props: this.$attrs,
      };

      const on = {
        on: this.$listeners,
      };

      const { body } = this.$slots;
      if (body) return body.map(item => item);

      return (
        <el-table
          ref="table"
          {...props}
          {...on}
        >
          {
            this.columns.map(item => this.renderTableColumn(h, item))
          }
        </el-table>
      );
    },

首先我们先创建了一个attributes来保存非父组件上的 props 以及Events,然后使展开运算符就可以将 attributes注入到el-table组件内,通过this.slots.body 拿到 `<div slot='body'></div>`名字为 body 的具名 `slot`,如果定义了这个 `slot`,则直接返回用户定义的内容,由于 this.slots对象中的每个key均是Array类型,因此需要 map 再返回。如果用户没有自定义内容,则返回el-table。注意到 el-table的内容我们通过map 外面传递进来的 columns属性来生成el-table 中内置的 el-table-column

renderTableColumn(h, colOptions) {
      // 兼容 iview 表格的部分配置
      colOptions.prop = colOptions.key || colOptions.prop;
      colOptions.label = colOptions.title || colOptions.label;

      const props = {
        props: colOptions,
      };
      const { render } = colOptions;

      const slotScope = {
        scopedSlots: {
          default(scope) {
            return typeof render === 'function' ? render(h, scope)
              : scope.row[colOptions.prop];
          },
        },
      };
      return (
        <el-table-column
          {...props}
          {...slotScope}
        >
        </el-table-column>
      );
    },

colums 表格列配置

[
  {
      title: '操作人',
      key: 'operatorId',
      minWidth: 120,
      render?
    },
]

renderPanelBody中可以知道,renderTableColumn中的第二个参数为用户传进来的表格列配置数组中的一项,它应该由 el-table-column的 props 构成。对于熟悉的同学可能知道原生的自定义内容是通过 slot-scope 的方式实现的,而通过配置的方式,我们只能给每一列配置一个render函数来实现自定义内容。怎么才能让这两者等价呢?

<el-table-column prop="enableStatus" label="商品状态"  min-width="140">
      <template slot-scope="scope">
        <span>{{getDisplayName(statusDropdown, scope.row.enableStatus)}}</span>
      </template>
</el-table-column>

查看vue文档得知,每个vue 实例上存在一个$scopedSlots的属性,它可以访问作用域插槽。这和我们要实现的需求不谋而和。

scopedSlots, 用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 的函数。**vm.scopedSlots 在使用渲染函数开发一个组件时特别有用**。

因此我们可以根据 外面配置的 render 函数来自定义默认作用域插槽的内容了。

 const slotScope = {
        scopedSlots: {
          default(scope) {
            return typeof render === 'function' ? render(h, scope)
              : scope.row[colOptions.prop];
          },
        },
      };

写到这里,我们基于 jsx 的 组件二次封装也就完成了。整体下来,以下几点需要注意

  1. this.$slots 对象中的每个 key,均是 Array 类型的;
  2. template 中 的 v-if 指令可以用 if 条件语句实现;
  3. template 中 的 v-for 指令可以用 数组map 循环来实现;
  4. template 中的 v-model指令可以使用 babel-plugin-jsx-v-model插件;
  5. template 中的 数据绑定(:key="value")使用 key={this.value}的方式来实现

其他 Vue jsx 相关的问题都可以在babel-plugin-transform-vue-jsx及其相关 issue 中找到答案。