Vue2划分 atoms、molecules、organism(原子层、分子层、应用层/有机层)

424 阅读5分钟

@createTime: 2023/04/17 @updateTime: 2023/04/26

我的博客原文

前言

之前与外企的伙计交流感情后得知他们的前端开发思路,于是乎便深入了解了这一套开发,深受震撼,写一篇文章记录记录 ( ̄︶ ̄)↗

概念

不听不听王八念经

  • atoms 原子层
    • 只有数据展示和样式,最好不能涉及代码逻辑
  • molecules 分子层
    • 导入并组合atoms(原子),会涉及部分样式调整(比如说间距,位置),会涉及代码逻辑(比如数据绑定,原子间的相互交互逻辑,应用层给的数据处理)
  • organism 应用层(注:直译是“有机体”)
    • 导入bing组合molecules(分子),建议只涉及接口请求(只是建议,毕竟分子层优先满足公用,不能满足一些应用层特殊情况)
  • hooks 逻辑层(vue2能把函数封装成公共函数就用吧)
    • 将常用的逻辑封装成公共函数,写入这里面,需要调用就导入调用

示例

之前写的一团糟,故写了个demo方便理解(vue2的写法)

demo地址:article14 国内镜像:gitee地址

table的二次封装

第零步,组件写在哪儿

别笑,我是认真的

(~ ̄(OO) ̄)ブ

分析:组件涉及展示操作,还涉及很多其他组件相互配合(如进度条,标签),所以说放在molecules分子层

第一步,确定数据来源
<!-- demo部分代码
     ./src/components/molecules/MyTable.vue:2 -->
<a-table
  class="my-table"
  row-key="id"
  :loading="loading"
  :bordered="true"
  :columns="tableColumns"
  :data-source="dataSource"
  :pagination="pagination"
>
......

这里可以看见部分要用的table组件的参数,这里我们要做第一步分析,分析它的数据是 从父组件获取 还是 直接配置,这里我就直接分析了

  • 父组件获取:列配置columns、列数据dataSource、加载状态loading
  • 直接配置:key取值rowKey、边框bordered、分页设置pagination

当然,还有其他可配置项,如 滚动scroll、表标题title、部分自定义功能如选择框,反选rowSelection

第二步,简化数据输入

确定完父元素所有传入值后,对每一个值进行分析

  • 列配置columns:大部分配置的值和key值绑定,故可以简化
  • 列数据dataSource:数据来源于后端JSON/JSONP,无法判断内容,故不可简化
  • 加载状态loading大佬,这个能简化么(╬▔皿▔)╯

对columns进行简化:

// ./src/components/organisms/PageTable.vue:20
const columns: myTableColumns = [
  { key: 'name', value: '名称', type: 'string' },
  { key: 'status', value: '状态', type: 'badge' },
  { key: 'dateTime', value: '时间', type: 'dateTime' },
  { key: 'progress', value: '进度条', type: 'progress' },
  { key: 'others', value: '操作', type: 'others' }
]

myTableColumns类型

// ./src/components/types/myTable.ts:3
export type myTableColumn = {
  // 关键词, 如'name'之类的
  key: string
  // 展示文本, 如'名称'之类
  value: string
  // 展示类型,这里可以用枚举限制, 如展示'进度条'、'标签'、'徽标'
  type: string
  // 根据业务需要,请自己添加其他东西
  // width?: number
}

export type myTableColumns = myTableColumn[]
第三步,补全简化数据

将上述的columns代码补全成ant-design-vue能识别的类型

// ./src/components/molecules/MyTable.vue:53
computed: {
  tableColumns (vm: { columns: columns }) { // vm = this
    return vm.columns.map(setColumn)
  }
}

因为列配置的细节有些许多,故我将列配置的细节单独导出成hooks

import { myTableColumn, antTableColumn } from '../types/myTable'
import { h } from 'vue'
import MyProgress from '../atoms/MyProgress.vue'
import MyBadge from '../atoms/MyBadge.vue'
import MyTime from '../atoms/MyTime.vue'

/** 将外层配置的列选项转化成 ant-table 能识别的column
 *
 * @param item: {myTableColumn}
 */
export const setColumn = (item: myTableColumn): antTableColumn => {
  const out: antTableColumn = {
    title: item.value,
    dataIndex: item.key,
    key: item.key,
    ellipsis: true
  }
  if (item.type === 'others') {
    out.scopedSlots = { customRender: item.key }
  } else {
    out.customRender = (text) => {
      // 对 type进行处理,这里做示例
      switch (item.type) {
        case 'progress':
          return h(MyProgress, {
            props: { data: text, color: '#18e87f' },
            domProps: {}
          })
        case 'badge':
          let color = '' // eslint-disable-line no-case-declarations
          let str = '' // eslint-disable-line no-case-declarations
          switch (text.toString()) {
            case '1':
              color = '#2db7f5'
              str = '正常'
              break
            case '2':
              color = '#fadb14'
              str = '下线'
              break
            case '3':
              color = '#f5222d'
              str = '错误'
              break
            default:
              color = '#414141'
              str = '未知'
          }
          return h(MyBadge, {
            props: { data: str, color: color },
            domProps: {}
          })
        case 'dateTime':
          return h(MyTime, { props: { time: text } })
        default:
          return text || '--'
      }
    }
  }
  return out
}

export default {}
  • 这里说一下,列表展示中会有按钮操作,如编辑、启/禁用等,所以说一定要设置others处理
  • 里面涉及了一些其他的没见过的以My开头的组件,下一个涉及的部分
第四步,封装特殊组件

我这里是封装了 进度条,徽标数,时间显示(这个可以用hooks代替的)

<template>
  <a-badge
    class="my-badge"
    :color="color || '#2db7f5'"
    :text="data"
  />
</template>

<script>
import { Badge as ABadge } from 'ant-design-vue'
export default {
  name: 'MyBadge',
  components: { ABadge },
  props: ['data', 'color']
}
</script>

<style scoped>

</style>

  • props直接写数组就行,降低代码量
  • 若是不得不涉及变量计算的,统一使用computed处理
  • 封装后统一写在atoms内部

description的二次封装

第零步

别想了,写在 molecules 里面

第一步,数据来龙去脉
<!-- demo部分代码
     ./src/components/molecules/MyDescription.vue:2 -->
<a-descriptions class="my-description" size="default" bordered :column="column" :title="title">

这里就直接上分析了

  • 父组件获取:标题title、列数量columns
  • 直接配置:尺寸size、边框border
  • 其他可配项:布局layout

聪明的你一定发现了,这个数据怎么展示呢?

description有它的儿子description-item,咱对它儿子进行循环就行了(总感觉怪怪的),循环数组一定得从父组件内获取,再根据父组件传入的展示对象展示即可

<!-- demo部分代码
     ./src/components/molecules/MyDescription.vue:4 -->
    <template v-for="(item, index) of config">
      <a-descriptions-item
        :key="index"
        :label="item.label"
        :span="item.span"
      >
        <my-level v-if="item.value === 'level'" :level="value[item.value]" />
        <my-time v-else-if="item.method === 'dateTime'" :time="value[item.value]" format="YYYY-MM-DD hh:mm:ss" />
        <div v-else v-html="value[item.value]" />
      </a-descriptions-item>
    </template>
  props: {
    value: {
      type: Object,
      default: () => ({})
    },
    config: {
      type: Array,
      default: () => ([])
    },
    title: {
      type: String,
      default: () => '基本信息'
    },
    column: {
      type: Number,
      default: () => 4
    }
  }
第二步,规范传入类型

直接上父组件吧,方便理解

<template>
  <div class="description-show">
    <my-description
      :config="descriptionConfig"
      :value="descriptionInfo"
    />
  </div>
</template>

<script lang="ts">
import MyDescription from '../molecules/MyDescriptions.vue'

export default {
  name: 'DescriptionShow',
  components: { MyDescription },
  data () {
    return {
      descriptionConfig: [
        { label: '名称', value: 'name', span: 1 },
        { label: '类型', value: 'type', span: 1 },
        { label: '级别', value: 'level', span: 1 },
        { label: '创建时间', value: 'createTime', span: 1, method: 'dateTime' },
        { label: '次数', value: 'count', span: 1 },
        { label: '更新时间', value: 'updateTime', span: 1, method: 'dateTime' },
        { label: '描述', value: 'description', span: 2 }
      ],
      descriptionInfo: {
        name: '张三',
        type: '部长',
        level: '7',
        createTime: 1662476859719,
        count: 114514,
        updateTime: 1682476859719,
        description: '这是一个很牛的大佬,我说的是真的'
      }
    }
  }
}
</script>

<style scoped>

</style>

这里我偷懒了,应该设置config输入的数据类型

= ̄ω ̄=

第三步,分析调用原子

直接上源码

<!--  ./src/components/molecules/MyDescription.vue:9 -->
<my-level v-if="item.value === 'level'" :level="value[item.value]" />
<my-time v-else-if="item.method === 'dateTime'" :time="value[item.value]" format="YYYY-MM-DD hh:mm:ss" />
<div v-else v-html="value[item.value]" />

按照原型图、UI设计图调用封装的atoms组件,完成数据展示

后话

我写的demo层级结构是这样的

components
	atoms		[MyBadge.vue, MyLevel, MyProgress, MyTime]
	hooks		[myTable]
	molecules	[MyDescription, MyTable]
	organisms	[DescriptionShow, PageTable]
	types(忽略了)

个人使用感觉:

  • 优势
    • 传入就用,节约代码开发成本
    • 层级少,封装就调用
    • 需求更改时只修改配置代码
  • 劣势
    • 业务不清晰时会频繁修改封装内容
    • 提升了新人学习成本
    • 提前规划好管理atoms层组件名称、作用、注释
    • 需要定期熟悉atoms层内组件