w3ctech

vue jsx 不完全指北

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"
          {...on}
          {...props}
        >
          {
            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 中找到答案。

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复