打造一款适合自己的快速开发框架-前端篇之字典组件设计与实现

2,684

前言

后端篇的时候已经对字典模块进行了设计,相应的接口也已经完成。在前后端未分离的情况下,因为页面是由服务端渲染的,所以一般都会自定义一个字典标签用于对字典数据的取值、渲染。该种情况下,服务端很方便地对字典做缓存处理。前后端分离后,前端与后端都是通过接口进行交互的,所以维护字典的方式也会有所区别。

字典组件设计

我们简单的分析一下字典组件应该具备的功能。

先分析使用场景

在后台管理中,常见的使用字典的场景有三个:

  1. 添加/修改表单,下拉选择
  2. 列表页,由值转为显示名称
  3. 搜索表单,下拉选择

使用场景不同,对于前端来说,其实就是呈现方式的不同。所以我们在做组件的时候,可以先默认按场景分三种布局。

关于缓存的思路

缓存的思路有很多种,这里简单讲一下:

  1. 系统登录后,一次性返回所有的字典数据,缓存在本地的cookies或vuex上;

    优点:减轻服务器压力

    缺点:一次性返回,字典量多的话,可能会影响体验

  2. 不进行缓存,每次都调用接口获取数据;

    优点:无

    缺点:频繁请求,页面中字典多的话,影响体验

  3. 使用vuex,基于dictKey进行缓存,保证在同一个vue实例下,同一个key,只调用一次接口。

    方案三是本框架采用的方式,也不能说是最优的。但是相对而已,可能会比前两个方案会好一些。当然,除了这三个方案,肯定还有别的方案,这里就不讨论了。

组件参数说明

暂时定几个常用的参数,后续可能还会有追加

参数名 类型 默认值 说明
dictKey String undefined 字典唯一编码(表名_字段名)
type String enum 字典类型(enum->枚举类字典类型,db->数据库字典类型,local->本地字典类型)
value String, Number undefined 绑定的值
size String medium 对应el-select的size,medium/small/mini
mode String form form->普通表单,list->列表页,searchForm->搜索表单

接口说明

先简单说一下后端提供的接口

请求地址:

{{api_base_url}}/sys/dict/getByDictKey

数据类型:

application/json

请求示例:

{
	"dictKey": "sys_role_role_type",
	"type": "enum"
}

响应示例:

{
	"code": 0,         				// 返回状态码0,成功
	"msg": "通过字典唯一编码查询成功",	// 消息描述
	"data": {
		"name": "角色类型",
		"dictKey": "sys_role_role_type",	// 字典唯一编码
		"items": [{
			"name": "管理员",
			"dictItemValue": 10
		}, {
			"name": "流程审核员",
			"dictItemValue": 20
		}]
	}
}

开始编码

目录结构

├── src
	├──	components/m
		├──	Dict
			└── index.vue
	├── store
		├── modules
			└── dict.js
		├── getters.js
		└── index.js
	├── views
		├──	dashboard
			└── index.vue
	└── main.js

文件详解

  • src/components/m/Dict/index.vue

字典组件

<template>
  <div class="m-dict">
    <!--表单布局模式-->
    <slot v-if="mode==='form'" v-bind:dict="dict">
      <el-select :size="size" v-model="mValue" v-if="dict.items" @change="handleChange">
        <el-option
          v-for="item in dict.items"
          :key="item.dictItemValue"
          :label="item.name"
          :value="item.dictItemValue">
        </el-option>
      </el-select>
    </slot>
    <!--列表布局模式-->
    <slot v-else-if="mode==='list'" v-bind:dict="dict">
      <span v-for="item in dict.items" :key="item.dictItemValue">
        <el-tag :type="type" size="mini" v-if="item.dictItemValue === value">{{ item.name }}</el-tag>
      </span>
    </slot>
    <!--搜索表单布局模式-->
    <slot v-else-if="mode==='searchForm'" v-bind:dict="dict">
      <el-select :size="size" v-model="mValue" v-if="dict.items" @change="handleChange">
        <el-option label="所有" :value="undefined"></el-option>
        <el-option
          v-for="item in dict.items"
          :key="item.dictItemValue"
          :label="item.name"
          :value="item.dictItemValue">
        </el-option>
      </el-select>
    </slot>
  </div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
  name: 'MDict',
  props: {
    // 字典唯一编码(表名_字段名)
    dictKey: {
      type: String,
      default: undefined
    },
    // 字典类型(enum->枚举类字典类型,db->数据库字典类型,local->本地字典类型)
    // 不传的话,后端先查enum,再查db
    type: {
      type: String,
      default: 'enum'
    },
    // 绑定的值
    value: {
      type: [String, Number],
      default: undefined
    },
    size: { // medium/small/mini
      type: String,
      default: 'medium'
    },
    mode: { // form->普通表单,list->列表页,searchForm->搜索表单
      type: String,
      default: 'form'
    }
  },
  data() {
    return {
      mValue: this.value
    }
  },
  computed: {
    ...mapGetters([
      'dictMap'
    ]),
    // 当前字典
    dict() {
      return this.dictMap[this.dictKey] || {}
    }
  },
  watch: {
    value(n) { // 监听父组件值变动,子组件也要变动
      this.mValue = n
    }
  },
  created() {
    if (!this.dictMap[this.dictKey]) {
      // 这里调用store/modules/dict.js/action->getByDictKey
      this.$store.dispatch('dict/getByDictKey', {
        dictKey: this.dictKey,
        type: this.type
      })
    }
  },
  methods: {
    // 子组件值变化要通知父组件
    handleChange(value) {
      this.$emit('input', value)
    }
  }
}
</script>
  • src/store/modules/dict.js
import request from '@/utils/request'

const getDefaultState = () => {
  return {
    // 字典map
    dictMap: {}
  }
}

const state = getDefaultState()

const mutations = {
  // 保存字典项
  SAVE_DICT_ITEM: (state, data) => {
    var obj = {}
    obj[data.dictKey] = data
    // 需要拷贝一份,要不然数据变动监听不到
    state.dictMap = Object.assign({}, state.dictMap, obj)
  },
  // 移除字典项
  DELETE_DICT_ITEM: (state, dictKey) => {
    delete state.dictMap[dictKey]
  }
}

const actions = {
  // 获取字典的action
  getByDictKey({ commit }, data) {
    return new Promise((resolve, reject) => {
      if (state.dictMap[data.dictKey]) {
        resolve()
      } else {
        // 防止同一个key多次请求
        commit('SAVE_DICT_ITEM', {
          dictKey: data.dictKey,
          items: []
        })
        // 这里暂不用api.service.js
        request({
          url: '/sys/dict/getByDictKey',
          method: 'post',
          data
        }).then(res => {
          if (res.code === 0 && res.data) {
            commit('SAVE_DICT_ITEM', res.data)
          } else {
            commit('DELETE_DICT_ITEM', data.dictKey)
          }
          resolve()
        }).catch(error => {
          commit('DELETE_DICT_ITEM', data.dictKey)
          reject(error)
        })
      }
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}
  • src/store/getters.js

定义dictMap的get方法

const getters = {
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  name: state => state.user.name,
  // 这里追加dictMap的get方法,可以使用mapGetters,详见src/components/m/Dict/index.vue
  dictMap: state => state.dict.dictMap
}
export default getters
  • src/store/index.js

这里引入dict.js模块,充分利用了require.contex

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
// import app from './modules/app'
// import settings from './modules/settings'
// import user from './modules/user'
// 自动注册vuex模块
const files = require.context('./modules', true, /\.js$/)
var modules = {}
files.keys().forEach((routerPath) => {
  const name = routerPath.replace(/^\.\/(.*)\.\w+$/, '$1')
  const value = files(routerPath)
  const fileModule = value.default
  modules[name] = fileModule
}, {})
Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    ...modules
  },
  getters
})

export default store

  • src/main.js

主入口全局注册自定义组件,这里也用了require.context,代码片段

import Vue from 'vue'

// 处理自定义组件全局注册
const files = require.context('./components/m', true, /\.vue$/)
files.keys().forEach((routerPath) => {
  const componentName = routerPath.replace(/^\.\/(.*)\/index\.\w+$/, '$1')
  const value = files(routerPath)
  Vue.component('m-' + componentName.toLowerCase(), value.default)
}, {})
  • src/views/dashboard/index.vue

这里提供了使用样例:

自定义布局

<m-dict v-model="form.roleType" dict-key="sys_role_role_type">
    <template v-slot:default="{ dict }">
	<el-select v-model="form.roleType" v-if="dict.items">
   		<el-option
               v-for="item in dict.items"
               :key="item.dictItemValue"
               :label="item.name"
               :value="item.dictItemValue">
        </el-option>
        </el-select>
    </template>
</m-dict>

表单布局模式

<m-dict mode="form" v-model="form.roleType" dict-key="sys_role_role_type"></m-dict>

列表布局模式

<m-dict mode="list" v-model="form.roleType" dict-key="sys_role_role_type"></m-dict>

搜索表单布局模式

<m-dict mode="searchForm" v-model="form.roleType" dict-key="sys_role_role_type"></m-dict>

效果图

小结

本文对字典组件的封装还是比较粗糙,不过也基本上满足平常使用,如后续场景需要,再考虑继续扩展。比如预留的local本地字典类型,如果本地存在字典配置,就可以不走接口请求。目前该参数也未实现。

项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相关文章

打造一款适合自己的快速开发框架-先导篇

打造一款适合自己的快速开发框架-前端脚手架搭建

打造一款适合自己的快速开发框架-前端篇之登录与路由模块化

打造一款适合自己的快速开发框架-前端篇之框架分层及CURD样例