Vue之JSX的实际运用

327 阅读3分钟

1.JSX是什么

  • JSX是一个 JavaScript 的语法扩展,全称JavaScript xml,可以很好地描述 UI 应该呈现出它应有交互的本质形式,相较于模板,它具有 JavaScript 的全部功能。

2.为什么使用JSX

  • 在处理一些较为灵活和动态的场景时,受限于vue中模板和脚本分离的限制,这时候可以使用jsx将两者结合在一起。
  • 面对复杂逻辑时,可以充分发挥js的语法特性,避免模板语法的限制和冗余。
  • 易于封装和创建组件。

3.JSX的安装及使用

npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props

babel.config.js中添加如下配置:

module.exports = {
    presets: ['@vue/babel-preset-jsx'],
}

4.基本语法(官方文档

{/* 插值和属性绑定 */}
<td rowspan={rowspan}>{label}</td>
{/* 事件绑定 */}
<div onClick={() => this.handleClick('add')}>添加</div>
{/* 列表渲染 (类似v-for) */}
<table>{ this.columns.map((tds, inx) => <tr key={inx}>{tds}</tr>) }</table>;
{/* 自定义默认插槽 && 自定义具名插槽 && 作用域插槽 */}
<div class="cell">
  <span class="title">{this.$slots.default}</span>
  <span class="title">{this.$slots.title}</span>
  <myComp scopedSlots={{ title: ({text}) => <span>{text}</span> }}></myComp>
</div>

5.实际运用

由于接触的PC项目中,几乎都涉及到如下图所示的业务场景,故需要封装一个查看详情/表单编辑组件。综合考虑还是使用jsx封装。组件支持手动/自动设置单元格占位、表单检验等。 实际页面效果如下:

image.png 嵌套数据对应的DOM元素结构

image.png 用于定义组件结构的数据如下,包括单行数据的结构和多行嵌套结构的数据。

// json数据
// colspan: 手动设置值所占单元格列数,选填。
// rowspan: 手动设置名称所占单元格行数,选填。
// grids: 当数据为嵌套类型时,设置每个嵌套类型块的所占列数,必填
// rules: 自定义检验规则,适配elementUi表单校验规则,选填。
const columns = [
  [
    { prop: 'creatorName', label: '创建人', colspan: 2 },
    { prop: 'createTime', label: '创建时间' },
    { prop: 'creator', label: 'creator' },
  ],
  [
    { prop: 'affairType', label: '事物类型', required: true },
  ],
  [
    { prop: 'isEffect', label: '是否立即失效' }
  ],
  [
    { prop: 'remindObject', label: '提醒对象' }
  ],
  [
    // 为了直观的表示嵌套类型的数据,对应的数据将使用树形结构。
    { 
      label: '详情1',
      grids: 5,
      children: [
        { prop: 'sub1' },
        {
          label: '详情1-2',
          children: [
            { label: '详情1-1-1', prop: 'tub1' },
            { label: '详情1-1-2', prop: 'tub2' },
          ]
        },
        { label: '详情1-3', prop: 'sub3' },
      ]
    },
    {
      label: '详情2',
      grids: 3,
      children: [
        { label: '详情2-1', prop: 'sub4', rowspan: 2 },
        { label: '详情2-2', prop: 'sub5', rowspan: 2 },
      ]
    },
  ],
  [
    { 
      prop: 'smsContent', 
      label: '短信内容', 
      rules: [{ required: true, message: '短信内容不能为空' }],
    }
  ],
]

组件的使用

<template>
  <checkDetail class="check-detail" :detail="detail" :columns="columns">
    <template #affairType>
	    <el-input v-model="detail.affairType" type="text" placeholder="请输入" />
    </template>
    <template #remindSize>
      <el-select v-model="detail.remindSize" class="w-full" placeholder="请选择">
	      <el-option
           v-for="it in remindSizeOps"
	        :key="it.value"
	        :label="it.label"
	        :value="it.value"
	      />
      </el-select>
    </template>
    <template #smsContent>
      <el-input v-model="detail.smsContent" type="textarea" :rows="4" placeholder="请输入" />
    </template>
  </checkDetail>
</template>

组件核心代码如下(完整代码):

{
  props: {
    columns: { // 表格字段名数据
      type: Array,
      default: () => [],
      required: true,
    },
    detail: { // 数据
      type: Object,
      default: () => ({}),
    },
    baseGrids: { // 表格的栅格数
      type: Number,
      default: 8,
    },
    isRequired: { // 表单是否存在必填
      type: Boolean,
      default: false,
    },
  },
  // 组件的UI层
  render() {
    return this.getForm(
      <div class="check-detail">
        <table
          class="detail"
          border="0"
          cellspacing="0"
          cellpadding="0"
          width="100%"
        >
          {this.columns.map((items, index) => {
            const isNest = items.some((item) => item.children?.length);
            // 是否为嵌套类型
            if (isNest) {
              const groupTds = items.map((item) => {
                const { result } = this.toFlat(item, item.grids);
                return result;
              });
              return this.mergeTds(groupTds).map((tds, inx) => (
                <tr key={`${index}${inx}`}>{tds}</tr>
              ));
            }
            // 计算每条数据所占单元格列数
            const averageGrids = this.getAverageGrids(this.baseGrids, items);
            return (
              <tr key={index}>
                {items
                  .map((it, i) => this.createTds(it, { cols: averageGrids[i] }))
                  .flat()}
              </tr>
            );
          })}
        </table>
      </div>
    );
  },
  methods: {
    // 使用jsx定义一个el-form外层组件,便于在表单需要检验的时候,套入组件最外层
    getForm(content) {
      if (this.isRequired) {
        return (
          <el-form
            ref="formRef"
            props={{
              model: this.detail,
              size: "small",
            }}
          >
            {content}
          </el-form>
        );
      }
      return content;
    },
    // 处理嵌套结构数据,并扁平化返回,同时获取每个父节点下的所有叶子节点数目
    toFlat(item, grids = this.baseGrids) {
      const result = [];
      const label = item.label;
      let count = 0;

      if (label) grids--;

      for (const subItem of this.getFullChild(item.children)) {
        const { prop, children } = subItem;
        if (prop) {
          count++;
          result.push(this.createTds(subItem, { cols: grids }));
        } else if (children) {
          const { result: subTds, count: subCount } = this.toFlat(subItem, grids);
          count += subCount;
          result.push(...subTds);
        } else {
          result.push(null);
        }
      }

      if (label && result?.length) {
        result[0].unshift(this.createTds(item, { rows: count }));
      }
      return { result, count };
    },
    // 将对应数据生成虚拟dom
    createTds(item, { cols, rows }) {
      const { label, prop, rowspan, colspan, rules } = item;
      const result = [];
      if (label) {
        const curRowspan = rowspan || rows || 1;
        result.push(
          <td class="tb_th" rowspan={curRowspan} colspan={1}>
            <div class={`cell ${this.isRequired && rules ? "isRequired" : ""}`}>
              {this.$slots[label] || label}
            </div>
          </td>
        );
        if (cols) cols--;
      }
      if (prop) {
        const curColspan = colspan || cols || 1;
        result.push(
          <td class="tb_td" rowspan={rowspan} colspan={curColspan}>
            {this.getFormItem(
              item,
              <div class="cell">
                {this.$slots[prop] || this.detail[prop]}
              </div>
            )}
          </td>
        );
      }
      return result ?? (result.length === 1 ? result[0] : result);
    },
    // 在表单项需要校验时,套上一层el-form-item组件
    getFormItem(item, content) {
      const { prop, rules } = item;
      if (this.isRequired && rules) {
        return (
          <el-form-item
            prop={prop}
            rules={rules}
            scopedSlots={{ // 自定义error提示样式
              error: ({ error }) => (
                <span
                  class="custom-error"
                  {...{ directives: [{ name: "hidden" }] }}
                >
                  {error}
                </span>
              ),
            }}
          >
            {content}
          </el-form-item>
        );
      }
      return content;
    },
  }
}