首页 > 其他分享 >浅谈“配置化”与 normalize 在复杂嵌套组件开发中的应用

浅谈“配置化”与 normalize 在复杂嵌套组件开发中的应用

时间:2022-12-07 23:01:04浏览次数:47  
标签:normalize const 浅谈 field value 嵌套 视图 fieldConfigs name

简介

视图层相比脚本,具有不便于调试、无效信息过多(与当前逻辑不相关的属性)等特点,因此,同样的逻辑位于视图可能比位于脚本中的复杂程度更高。

因此,在开发复杂组件,尤其是嵌套组件时,最好遵循一定的规范,且尽量简化视图层需要处理的逻辑,应当在脚本中完成大部分视图层所需内容的处理,若是能直接将数据或内容绑定到视图层最好。

但实际开发中,总有一些场景无法完全通过脚本执行所有的预处理,否则可能使脚本过重。

思路介绍

本文通过配置化的方式,将视图结构抽象为脚本中的配置对象,然后在视图层上遍历该对象,并根据子对象的属性判定应当以何种方式渲染结构和内容。

同时,使用自定义的 normalize 函数,将初始的配置对象归一化为标准的配置对象(初始对象为了方便使用,允许使用特定的简化语法,而完整的对象则通过脚本自动生成)。

下文以表格嵌套展开行为例介绍实现思路(表格主题为 “亚马逊 FBA 货件” )。

示例

全局常量

const moduleName = ref('shipmentManagement'); // 模块名

// 货件状态;用于显示特定 type 的 el-tag 组件
const shipmentStatusTypes = {
  [0]: 'info',
  [1]: '',
  [2]: 'warning',
  [3]: 'success',
};
// 由于引入了 i18n ,切换语言时, el-table-column 组件的 filters 对象也要同步更新,否则会因语言不同而导致过滤失败
const shipmentStatusFilters = computed(() =>
  Object.keys(shipmentStatusTypes).map((type) => {
    return {
      text: $t(`${moduleName.value}.filters.shipmentStatus.${type}`),
      value: type,
    };
  })
);

格式化相关函数

const setupFieldFormatterConfig = (formatter) => {
  return {
    formatter: (value) => formatter(value),
  };
};

const weightFormatter = (value) => value + ' KG';
const volumeFormatter = (value) => value + ' (cm*cm*cm)';
const currencyFormatter = (value) => $t(`currency.${locale.value}`) + value;

// i18n 格式化;这里的函数是可以抽离到全局的
const setupFieldKeyI18n = (fieldName) =>
  $t(`${moduleName.value}.fieldKeys.${fieldName}`);
const setupFilersI18n = (fieldName, fieldVal) =>
  $t(`${moduleName.value}.filters.${fieldName}.${fieldVal}`);

初始的配置对象

首先,表格初始仅展示 5 个主要字段以及一列功能区(查看、编辑、删除;查看按钮与展开行的功能是一样的,只是点击时会弹出表单)。完整信息则通过展开行的形式显示,并且,各项数据根据各自的关联性进行分组,并展示在嵌套的卡片中。

因此,需要有两个配置对象:固定列配置对象,以及完整的包含所有字段的配置对象。

// 这些对象用不用 ref() 都可以;若允许用户设置固定列的话最好只用 ref,这样可以响应式更新

// 表格默认只展示这些列
//   这里使用对象可以添加单独作用列表格列的配置参数,区别于展开行的配置
const fixedColumnList = [
  { name: 'createdTime' },
  { name: 'shipmentTitle' },
  { name: 'shipmentId' },
  { name: 'warehouseId' },
];

// 展开行时的字段分组(卡片)配置
const nestedFieldConfigs = [
  {
    headerConfigs: 'shipmentInfo', // 若标题没有其他配置需求的话,允许使用简单的字符串表示
    // 字段同理,可以只使用简答的字符串数组表示
    fieldConfigs: [
      'createdTime',
      'shipmentTitle',
      'shipmentStatus',
      'shipmentComment',
      'shipmentId',
    ],
  },
  {
    headerConfigs: 'warehouseInfo',
    fieldConfigs: {
      warehouseId: 'warehouseId',
      // 这里需要对仓库类型做一次过滤,因为后端返回的数据可能是简单 0 | 1 
      // 同时,由于使用了国际化插件,因此,实际需要显示的文本会放在 i18n 模块下
      warehouseType: setupFieldFormatterConfig((val) =>
        setupFilersI18n('warehouseType', val)
      ),
    },
  },
  {
    headerConfigs: 'logisticsInfo',
    fieldConfigs: {
      carrierName: 'carrierName',
      logisticsChannelType: setupFieldFormatterConfig((val) =>
        setupFilersI18n('logisticsChannelType', val)
      ),
      logisticsChannelCode: 'logisticsChannelCode',
      logisticsCode: 'logisticsCode',
    },
  },
  {
    headerConfigs: 'timelinessInfo',
    fieldConfigs: [
      'estimateTransitDateCost',
      'estimateCarrierProcessDateCost',
      'estimateFBAProcessDateCost',
      'pickupDate',
      'deliveryDate',
      'registerDate',
      'completeDate',
    ],
  },
  {
    headerConfigs: 'boxInfo',
    fieldConfigs: {
      boxId: 'boxId',
      boxAmount: 'boxAmount',
      boxSize: setupFieldFormatterConfig(volumeFormatter),
      boxRealWeight: setupFieldFormatterConfig(weightFormatter),
      boxVolumeWeight: setupFieldFormatterConfig(weightFormatter),
      sku: 'sku',
      skuAmount: 'skuAmount',
    },
  },
  {
    headerConfigs: 'costInfo',
    fieldConfigs: {
      totalProducts: 'totalProducts',
      totalBoxes: 'totalBoxes',
      unitCharge: setupFieldFormatterConfig(weightFormatter),
      chargeWeight: setupFieldFormatterConfig(currencyFormatter),
      domesticFreight: setupFieldFormatterConfig(currencyFormatter),
      actualDeliveryFreight: setupFieldFormatterConfig(currencyFormatter),
      additionalCharge: setupFieldFormatterConfig(currencyFormatter),
    },
  },
];

自定义 normarlize 函数

/**
 * @param {原配置对象} config
 * @param {额外的 normalizer} normalizerOptions: Array<{ prop: string, normalizer: (value: string) => string }>
 * @param {unnormalized 是对象类型且未提供 name 属性时,通过该参数传入} nameField: string
 * @return normalizedConfig: { name: string, text: string, [key: string]: any }
 */
const normalizeFieldConfig = (
  unnormalized,
  normalizerOptions = {},
  nameField = ''
) => {
  let normalized = {};
  // 若配置对象为字符串,则使用该字符串作为 name 属性的值
  typeof unnormalized === 'string'
    ? (normalized.name = unnormalized)
    : (normalized = Object.assign({}, unnormalized));

  if (!normalized.name) {
    if (!nameField)
      throw new Error(`Type Error: name attribute can not be empty`);
    normalized.name = nameField;
  }

  Object.entries(normalizerOptions).forEach(([prop, normalizers]) => {
    if (!(normalizers instanceof Array)) normalizers = [normalizers];

    /**
     * 若 normalizerOption 对象中的 prop 属性在原 unnormalized 对象中已存在,则使用该属性的旧值作为参数;
     *   否则使用 normalized.name 的值作为参数
     */
    let oldVal = undefined;
    prop in Object.getOwnPropertyNames(unnormalized)
      ? (oldVal = normalized[prop])
      : (oldVal = normalized.name);

    // 依次调用 normalizer ,后面的函数会以前面函数的返回值作为参数
    normalized[prop] = normalizers.reduce((oldVal, normalizer) => {
      return normalizer(oldVal);
    }, oldVal);
  });
  return normalized;
};

归一化原对象,并输出一个打平后的配置对象

打平的配置对象主要是方便表单组件使用,因为表单字段的输入组件无法通过配置化简单处理,除非将所有输入组件的入口封装为一个统一的组件。因此,这里通过打平后的配置对象匹配对应的文本。

同时,某些场景下,字段中还存在嵌套的字段,而此时需要直接遍历后台返回的数据对象,而不是字段的配置对象。这类场景下,也需要通过打平后的配置对象来获取实际文本等属性。下面的视图层就有这种需求。

// 将原来的嵌套化配置对象格式化,并输出一个新的打平后的配置对象
const normalizeNestedConfigs = (nestedConfigs) => {
  return nestedConfigs.reduce((normalizedFlatConfigs, config) => {
    // 处理嵌套区域的 header 配置
    let headerConfigs = normalizeFieldConfig(config.headerConfigs, {
      text: setupFieldKeyI18n,
    });

    normalizedFlatConfigs[headerConfigs.name] = headerConfigs;
    config.headerConfigs = headerConfigs;

    // 处理嵌套区域的字段配置
    let fieldConfigs = config.fieldConfigs;
    // 将数组类型转为对象
    if (fieldConfigs instanceof Array) {
      fieldConfigs = {};
      Object.entries(config.fieldConfigs).forEach(([i, field]) => {
        fieldConfigs[field] = { name: field };
      });
    }

    Object.entries(fieldConfigs).forEach(([field, configs]) => {
      const normalized = normalizeFieldConfig(
        configs,
        {
          text: setupFieldKeyI18n,
        },
        field
      );
      fieldConfigs[field] = normalized;
      normalizedFlatConfigs[field] = normalized;
    });
    config.fieldConfigs = fieldConfigs;

    return normalizedFlatConfigs;
  }, {});
};

// 打平字段配置对象并转换为 i18n 文本
//   这里仅仅是依赖 i18n.locale ,但不需要判定具体的值,只是为了让 vue 框架能够检测 locale 并自动更新 i18n
//   这样实现较为简单,而且计算也很快
const flatFieldConfigs = computed(
  () => locale.value && normalizeNestedConfigs(nestedFieldConfigs)
);

/*
* 若是想要像原本在视图上直接插入 i18n 那样使用的话,就需要给 normalize 函数加一个 appendOptions 参数,
* 然后在 normalizeNestedConfigs 函数的调用处,添加相应的 { textFormatter: ()=> {} } 参数;这里 formatter 属性已经用于字段值的格式化,为避免冲突只能使用其他命名
* 最后是在视图层上添加对应的判定逻辑 flatFieldConfigs[field].textFormatter ? flatFieldConfigs[field].textFormatter(fieldValue) : fieldValue
* 可以看出,不论视图层还是脚本中,都变得很麻烦,所里这里暂时不加这个参数
*/

视图

视图方面还可以将内嵌的卡片抽象为独立组件,方便复用。

<el-table :data="tableData" border stripe style="width: 100%">
  <el-table-column type="selection" width="38"></el-table-column>
  <!-- 通过展开行显示完整信息 -->
  <el-table-column type="expand">
    <template #default="{ row }">
      <el-scrollbar class="bi-shipment-detail" height="300">
        <div
          class="bi-shipment-detail-section"
          v-for="({ headerConfigs, fieldConfigs }, i) in nestedFieldConfigs"
          :key="i"
        >
          <div class="bi-shipment-detail-section__header">
            {{ headerConfigs.text }}
          </div>
          <div class="bi-shipment-detail-section__content">
            <!-- 箱规有单独的渲染规则 -->
            <template v-if="headerConfigs.name === 'boxInfo'">
              <!-- 
                  注意,这里是遍历每一行的数据,因此 name 和 formatter 是通过 flatFieldConfigs 取出的
                  而下面的其他信息区域内的字段,则是遍历各自的 fieldConfigs 对象取出的
              -->
              <div
                class="bi-shipment-detail-section bi-shipment-detail-section--vertical"
                v-for="(box, j) in row.boxList"
                :key="j"
              >
                <div class="bi-shipment-detail-section__header">
                  {{ flatFieldConfigs.boxId.text + ': ' + box.boxId }}
                </div>

                <div class="bi-shipment-detail-section__content">
                  <template
                    v-for="([field, value], k) in Object.entries(box)"
                    :key="k"
                  >
                    <template v-if="field === 'skuList'">
                      <template v-for="(skuItem, l) in box.skuList" :key="l">
                        <div class="bi-shipment-detail-item">
                          {{ flatFieldConfigs.sku.text + ': ' + skuItem.sku }}
                        </div>
                        <div class="bi-shipment-detail-item">
                          {{
                            flatFieldConfigs.skuAmount.text +
                            ': ' +
                            skuItem.skuAmount
                          }}
                        </div>
                      </template>
                    </template>
                    <div
                      v-else-if="field !== 'boxId'"
                      class="bi-shipment-detail-item"
                    >
                      {{
                        flatFieldConfigs[field]?.formatter
                          ? flatFieldConfigs[field].text +
                            ': ' +
                            flatFieldConfigs[field].formatter(value)
                          : flatFieldConfigs[field].text + ': ' + value
                      }}
                    </div>
                  </template>
                </div>
              </div>
            </template>
            <!-- 其他信息区域 -->
            <template v-else>
              <div
                class="bi-shipment-detail-item"
                v-for="([field, config], j) in Object.entries(fieldConfigs)"
                :key="j"
              >
                <template v-if="field === 'shipmentStatus'">
                  {{ $t(`${moduleName}.fieldKeys.shipmentStatus`) + ': ' }}
                  <el-tag :type="shipmentStatusTypes[row.shipmentStatus]">{{
                    setupFilersI18n('shipmentStatus', row.shipmentStatus)
                  }}</el-tag>
                </template>
                <template v-else>{{
                  config.text +
                  ': ' +
                  (config?.formatter
                    ? config.formatter(row[field])
                    : row[field])
                }}</template>
              </div>
            </template>
          </div>
        </div></el-scrollbar
      >
    </template>
  </el-table-column>
  <!-- 默认显示列 -->
  <el-table-column
    :prop="name"
    :label="flatFieldConfigs[name].text || name"
    v-for="({ name }, i) in fixedColumnList"
    :key="i"
  />
  <el-table-column
    prop="shipmentStatus"
    :label="flatFieldConfigs.shipmentStatus.text || 'shipmentStatus'"
    :filters="shipmentStatusFilters"
    :filter-method="filterShipmentStatus"
  >
    <template v-slot="{ row }">
      <el-tag :type="shipmentStatusTypes[row.shipmentStatus]">{{
        setupFilersI18n('shipmentStatus', row.shipmentStatus)
      }}</el-tag>
    </template>
  </el-table-column>
  <!-- 功能区 -->
  <el-table-column
    class="bi-table-action"
    :label="$t('common.action')"
    :width="locale == 'en' ? '210' : '200'"
    fixed="right"
  >
    <!-- 暂时无法解决表格列自适应宽度的问题,plus 的源码有些复杂 -->
    <template v-slot="{ $index }">
      <div class="bi-action-group">
        <!-- v-fitColumn="{ subElClass: 'bi-action-btn', gap: 20, numberInRow: 3 }" -->
        <el-button
          class="bi-action-btn"
          size="small"
          @click="handleView($index)"
          >{{ $t(`form.action.view`) }}</el-button
        >
        <el-button
          class="bi-action-btn"
          size="small"
          type="primary"
          @click="handleEdit($index)"
          >{{ $t(`form.action.edit`) }}</el-button
        >
        <el-button
          class="bi-action-btn"
          size="small"
          type="danger"
          @click="handleDelete($index)"
          >{{ $t(`form.action.delete`) }}</el-button
        >
      </div>
    </template>
  </el-table-column>
</el-table>

i18n 模块

最后是该模块的 i18n 配置文本。表格部分只需要看 shipmentManagement.fieldKeysshipmentManagement.filters 即可。其他文本由于未展示相关的视图和逻辑,因此不在此处列出。

const shipmentManagement = {
  info: {
    moduleName: '货件',
  },
  fieldKeys: {
    // 货件
    shipmentInfo: '货件',
    createdTime: '创建时间',
    shipmentTitle: '货件标题',
    shipmentStatus: '货件状态',
    shipmentComment: '备注',
    shipmentId: '货件编号',
    // 仓库
    warehouseInfo: '仓库',
    warehouseId: '仓库编号',
    warehouseType: '仓库类型',
    // 物流
    logisticsInfo: '物流',
    carrierName: '物流商名称',
    logisticsChannelType: '渠道类型',
    logisticsChannelCode: '渠道编号',
    logisticsCode: '运单号',
    // 时效
    timelinessInfo: '时效',
    estimateTransitDateCost: '头程天数',
    estimateCarrierProcessDateCost: '物流商处理天数',
    estimateFBAProcessDateCost: 'FBA 处理天数',
    // 日期
    pickupDate: '提取日期',
    deliveryDate: '签收日期',
    registerDate: '登记日期',
    completeDate: '完成日期',
    // 箱规
    boxInfo: '箱规',
    boxId: '箱子编号',
    boxAmount: '数量',
    boxSize: '尺寸',
    boxRealWeight: '箱子实重',
    boxVolumeWeight: '体积重',
    sku: 'SKU',
    skuAmount: '产品件数',
    // 计费
    costInfo: '计费',
    totalProducts: '总产品件数',
    totalBoxes: '总箱数',
    unitCharge: '运费单价',
    chargeWeight: '实际计费重',
    domesticFreight: '国内运费',
    actualDeliveryFreight: '实际头程收费',
    additionalCharge: '额外收费',
  },
  filters: {
    shipmentStatus: {
      [0]: '待发货',
      [1]: '在途',
      [2]: '上架中',
      [3]: '已完成',
    },
    warehouseType: {
      [0]: 'FBA',
      [1]: '海外仓',
    },
    logisticsChannelType: {
      [0]: '快递',
      [2]: '空运',
      [1]: '海运',
      [3]: '小包',
    },
  },
};

export default { shipmentManagement };

标签:normalize,const,浅谈,field,value,嵌套,视图,fieldConfigs,name
From: https://www.cnblogs.com/cjc-0313/p/16964840.html

相关文章

  • 浅谈软件编程中的8大数据结构
    文章目录​​前言​​​​一、为什么要研究数据结构​​​​二、数据结构的分类​​​​1.数组(Array)​​​​2.链表(LinkedList)​​​​3.队列(Queue)​​​​4.栈(Stack)​​​......
  • 浅谈电动汽车充电桩建设现状及规划方案
    摘要:为实现对电动汽车充电桩的优化建设,对其建设现状及规划方案展开研究。当前充电桩建设存在建设区域分布不均匀、社会公共停车场充电费用较高和部分充电设施维护不及时等......
  • 浅谈无线测温在化工行业配电系统的应用
    摘要:稳定的电力供应是保障企业正常生产的基础,因此如何保证电力设备的正常运行,是企业重点关注的一个问题。温度是表征电力设备运行正常的一个重要参数,在电力设备运行的过程中......
  • 浅谈船舶岸电系统绝缘监测及故障定位需求及应用
     摘要:随着现代船舶发展,船舶电气化程度越来越高,船舶电站的的容量也越来越大,随之而来的是电网的绝缘问题更加复杂化。船舶电力系统一般采用IT系统,即不接地系统。由于电网是......
  • vue 多级嵌套路由,页面不变化问题解决
      component:{render(c){returnc('router-view')}},redirect:'/set/origin',在二级路由出添加上面的代码,并重定向二级路由页面就可以了......
  • Vue中多条件图片路径通过Map存储获取避免嵌套if-else
    场景若依前后端分离版手把手教你本地搭建环境并运行项目:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108465662前端接收到后台数据之后需进行多个条件......
  • 浅谈Linux容器安全:chroot,capability与namespace技术
    作者只是个萌新,大佬轻喷。文章最终确定以时间顺序浅谈Linux容器安全原理。安全原理相关知识网上已经有很多了,咱通过几个具体攻击实例来讲讲它们的真实作用。演示均在猫......
  • Vue2(笔记21) - 组件 - 组件的嵌套
    组件的嵌套官方的图中,就可以看出,组件之间是可以嵌套的;没有嵌套的组件为了方便,就直接把前面写好的拿来用了;<divid="root"><h1>{{msg}}</h1><hr><school></schoo......
  • python中ImmutableMultiDict嵌套字典的值获取和解决400状态码的问题
    在写接口的过程中遇到了一次请求状态码400原因是用elementupload组件上传照片,后端采用flask的时候用request.form读取上传携带的其他参数,data=request.formtitle=......
  • (转)JS核心系列:浅谈函数的作用域
    一、作用域(scope)所谓作用域就是:变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。复制代码1functionscope(){2varfoo="global";3if(windo......