提升项目档次——封装

442 阅读5分钟

更多文章

前言

项目开发是小伙伴们的家常便饭,追旅也参与了很多项目的开发,抛去大厂基本都是在做傻瓜式式开发,追旅大多数时候也是在着傻瓜式的开发,例如用umijsvue-cli或者react的各种脚手架(我们不谈这些脚手架的优劣),参考脚手架的api基本上也就可以快速启动一个项目,相当的方便,这让开发者可以把更多的精力投入业务开发中去,所以想要在这块有所突破要么webpack自行撸一套脚手架,要么想尤大大那样创造新大陆vite,追旅这里分享一下更实际的业务开发,如何更好的落地组件化模块化

UI层

ui层组件的抽离,相信大家都做过很多了

基础组件

element-uiantd-design等组件库中你看得到的均属于基础组件,组件库不包含的基础组件也是基础组件,只是要自己去封装,对于绝大多数场景来说这些组件已经是最小单位了,但是在实际项目中基础组件也是需要进行业务封装的,原因如下:

  1. 基础组件在书写业务时也会导致大量的重复性代码和功能
  2. 基础组件在组装成业务组件时也有可能因为不满足需求需要拓展
  3. ui层的操作不变可以通过控制数据层影响ui层

tableformdemo(后续会使用到):

form

// form.vue
// 根据实际业务去适当的封装form
// 后续表单类提交的均使用form组件
// 不必一次性封装完毕,遇见差异元素添加
<template lang="pug">
el-form(
  ref="formRef"
  class="form-box"
  :model="formData"
  :label-position="labelPosition"
)
  el-row(
    :gutter='20'
    :class="inline ? '' : 'form-column'"
  )
    el-col(
      :span="inline ? item.span ? item.span : 4 : 24"
      v-for="item in formConfig"
      :key="item.key"
    )
      el-form-item(
        style="width: 100%;"
        :label="item.label"
        :prop="item.key"
      )
        //- input
        el-input(
          style="width: 100%;"
          v-if="item.type === 'input'"
          v-model="formData[item.key]"
          :placeholder="item.placeholder ? item.placeholder: '请输入'"
          :disabled="item.disabled ? item.disabled : false"
          :type="item.kind ? item.kind : 'text'"
        )
        //- select
        el-select(
          style="width: 100%;"
          v-if="item.type === 'select'"
          v-model="formData[item.key]"
          :disabled="item.disabled ? item.disabled : false"
          :placeholder="item.placeholder ? item.placeholder: '请选择'"
        )
          el-option(
            v-for="child in item.options"
            :key="item.otherKeys ? child[item.otherKeys.value] : child.value"
            :label="item.otherKeys ? child[item.otherKeys.label] : child.label"
            :value="item.otherKeys ? child[item.otherKeys.value] : child.value"
          )
    div(:class="inline ? 'form-sumbit' : ''")
      slot
</template>
<script>
import { ref } from 'vue'
export default {
  props: {
    formData: {
      type: Object,
      default: () => ({})
    },
    formConfig: {
      type: Array,
      default: () => ([])
    },
    inline: {
      type: Boolean,
      default: true
    },
    labelPosition: {
      type: String,
      default: 'top'
    }
  },
  setup() {
    const formRef = ref()

    const submitHandle = () => {
      return new Promise((resolve, reject) => {
        formRef.value.validate(valid => {
          valid && resolve(valid)
          !valid && reject(valid)
        })
      })
    }

    const resetHandle = () => formRef.value.resetFields()

    const getForm = () => formRef.value

    return {
      formRef,
      submitHandle,
      resetHandle,
      getForm
    }
  }
}
</script>

table

// table.vue
<template lang="pug">
el-table(:data="dataSource", v-loading="loading")
  el-table-column(
    v-for="(item, index) in columns"
    :key="item.key ? item.key : index"
    :label="item.label"
    :width="item.width"
    :fixed="item.fixed ? item.fixed : false"
  )
    template(slot-scope="scope")
      span(v-if="item.key !== 'operate'") {{ item.render ?  item.render(scope.row, scope.row[item.key]) : scope.row[item.key] }}
      div(v-if="item.key === 'operate'")
        el-button(
          v-for="(btn, index) in item.options"
          :key="index"
          type="text"
          @click="() => buttonHandle({row: scope.row, label: btn.render ? btn.render(scope.row) : btn.label})"
        ) {{ btn.render ? btn.render(scope.row) : btn.label }}

</template>
<script>
export default {
  props: {
    dataSource: {
      type: Array,
      default: () => ([])
    },
    columns: {
      type: Array,
      default: () => ([])
    },
    loading: Boolean
  },
  setup(props, { emit }) {
    const buttonHandle = (detail) => {
      emit('buttonHandle', detail)
    }

    return {
      buttonHandle
    }
  }
}
</script>

业务组件

一次性的业务组件可以一次性封装,但绝大多数场景不是这样的,还是以formtable为例,不单单是只有列表查询的场景会使用到formtable新增添加等场景会用到form,对于其他场景亦会用到table,假设我们将上述两个基础组件放在一起没有做抽离,我也只有列表查询一个固定场景,而对于其他表单提交场景或者列表展示场景则需要额外的再去写一个form或者table组件,这样导致的问题是:

  1. 场景单一、无法服用
  2. 业务组件中的基础组件无法复用,利用度不高
  3. 对于样式统一度较高的平台不利于统一

所以结论就是:

业务组件 += 基础组件

业务组件必定是基础组件组装的(除非你能保证你永远不在用着业务组件中的基础组件),最终查询表单场景ui层组件代码如下(稍后讨论逻辑层):

<template lang="pug">
.list
  Form(
    ref="formRef"
    :formData="formData",
    :formConfig="formConfig"
  )
    el-button(type="primary", @click="searchFormHandle") 搜索
    el-button(@click="resetFormHandle") 重置
  Table(:dataSource="dataSource", :columns="columns", :loading="loading")
  Pagination(:pagination="pagination")
</template>

hooks-逻辑层

react16+hooks刚出来的时候追旅在的公司刚好也是react的,怀着激动的心情去尝试了一下,当时的感受就是逻辑抽离从未如此简单,之后就是尤大大的vue3.x也采用类似的方案,组合式API是两大框架最直观的特点,这极大的方便了我们对逻辑层的抽离,继续回到我们的主题

这里顺便讲一下基础逻辑和工具的区别,hooks的本质是对公共逻辑的抽离并暴露出相应的api,而工具如时间处理函数、数字转千分位格式等依然是属于utils

依然是tableformdemo

useTbale

// useTable.js
// 如果你了解ts,建议使用ts
import { reactive, toRefs, ref } from 'vue'

/**
 * @param request 接口或Array<any>
 * @param defaultParams 默认入参
 * @function isInit 是否初始化调用
 */

function useTable(
  request,
  defaultParams,
  isInit = true
) {
  const c = defaultParams && defaultParams.current ? defaultParams.current : 1
  const p = defaultParams && defaultParams.pageSize ? defaultParams.pageSize : 10

  const current = ref(c)
  const pageSize = ref(p)

  const state = reactive({
    params: Object.assign({ pageNo: current, pageSize: pageSize }, defaultParams),
    searchInfo: {},
    dataSource: [],
    loading: false,
    pagination: {
      current: current,
      pageSize: pageSize,
      total: 0,
      onChange: (page, pageSize) => pageChange(page, pageSize),
      onShowSizeChange: (current, size) => showSizeChange(current, size)
    }
  })
  // api请求
  const _request = (params) => {
    // json
    if (Object.prototype.toString.call(request) === '[object Array]') {
      state.dataSource = request
      state.pagination.total = request.length
    }
    // api 请求
    if (Object.prototype.toString.call(request) === '[object Function]') {
      state.loading = true
      request({ ...state.params, ...params }).then((res = {}) => {
        state.loading = false
        const { data: { code, data } } = res
        if (code === '0') {
          const { totalCount, pagedRecords } = data || {}
          state.pagination.total = totalCount || pagedRecords.length
          state.dataSource = pagedRecords
        }
      })
    }
  }
  // 查询
  const searchHandle = (searchInfo) => {
    current.value = c
    pageSize.value = p
    if (searchInfo.pageNo) {
      state.pagination.current = searchInfo.pageNo
    }
    if (searchInfo.pageSize) {
      state.pagination.pageSize = searchInfo.pageSize
    }
    state.searchInfo = searchInfo
    _request(searchInfo)
  }
  // 切换页数
  const pageChange = (page, pageSize) => {
    current.value = page
    state.searchInfo.pageNo = page
    _request(state.searchInfo)
  }
  // 切换条数
  const showSizeChange = (current, size) => {
    pageSize.value = current
    state.searchInfo.pageSize = current
    _request(state.searchInfo)
  }
  // 重置
  const resetHandle = () => {
    state.searchInfo = {}
    current.value = c
    pageSize.value = p
    _request({})
  }
  // 初始化数据
  isInit && _request(state.params)

  return {
    ...toRefs(state),
    searchHandle,
    resetHandle
  }
}

export default useTable

useForm

// useForm.js
// 只针对追旅目前遇到的业务做相应封装
import { reactive, toRefs } from 'vue'

export default ({
  formRef,
  formData,
  formConfig
}) => {
  const state = reactive({
    formData,
    formConfig
  })
  // 提交表单
  const submitHandle = (callback) => {
    const { value: { submitHandle } } = formRef
    submitHandle().then(() => {
      callback(state.formData)
    })
  }
  // 重置表单
  const resetHandle = () => formRef.value.resetHandle()
  // 设置formConfig-options,异步下拉框等
  const setOptions = (key, options = []) => {
    const item = state.formConfig.find(item => item.key === key)
    if (!item) return
    item.options = options
  }
  // 查询formConfig-item
  const getConfig = (key) => state.formConfig.find(item => item.key === key)

  return {
    submitHandle,
    resetHandle,
    setOptions,
    getConfig,
    ...toRefs(state)
  }
}

使用

<template lang="pug">
div
  Form(
    ref="formRef"
    :formData="formData",
    :formConfig="formConfig"
  )
    el-button(type="primary", @click="searchFormHandle") 搜索
    el-button(@click="resetFormHandle") 重置
  Table(:dataSource="dataSource", :columns="columns", :loading="loading")
  Pagination(:pagination="pagination")
</template>
<script>
import { ref } from 'vue'
import Form from '@/components/qjd/form'
import Table from '@/components/qjd/table'
import Pagination from '@/components/qjd/pagination'
import useForm from '@/hooks/useForm'
import useTable from '@/hooks/useTable'
import { getSearchList } from '@/api/list'

export default {
  components: {
    Form,
    Table,
    Pagination,
  },
  setup(props, { root }) {
    const formRef = ref()
    const columns = ref([
      { label: '姓名', key: 'name' },
      { label: '性别', key: 'sex' },
      { label: '年龄', key: 'age' },
      { label: '身高', key: 'height' },
    ])
    // form
    const {
      formData,
      formConfig,
      submitHandle,
      resetHandle,
    } = useForm({
      formRef,
      formData: { name: undefined, sex: undefined },
      formConfig: [
        { type: 'input', key: 'name', label: '姓名', span: 6 },
        { type: 'input', key: 'sex', label: '性别', span: 6 }
      ]
    })

    // table & page
    const {
      loading,
      dataSource,
      pagination,
      searchHandle,
      resetHandle: resetTableHandle
    } = useTable(getSearchList, {})
    // 查询
    const searchFormHandle = () => submitHandle(searchHandle)
    // 重置
    const resetFormHandle = () => {
      resetHandle()
      resetTableHandle()
    }

    return {
      formRef,
      formData,
      formConfig,
      searchFormHandle,
      resetFormHandle,
      loading,
      dataSource,
      pagination,
      columns
    }
  },
}
</script>

尽管场景简单,但这也足以让组合式API优势尽显

高内聚、低耦合

高内聚、低耦合是个经久不衰的话题,低耦合就是拆分的过程,高内聚就是组装的过程,一辆可以跑起来的车是由无数个零件组装起来的,如果可以把组装和拆分做好,项目的可维护度、可读性就变得非常的高,各大框架核心依然是组件化、模块化,当开发者做的足够好时tslinteslint等辅助性工具也只是锦上添花而已,不知道你是否见到过2000行的ts代码?组件化、模块化是种思想,这种思想深入人心的同时,是否可以将这种思想落地的更好

总结

封装是门艺术