{T}

组件库

在项目开发中需要一个组件库快速搭建项目,组件库提供了各式各样的封装完备的组件。现在社区可选择的组件库有 element-plus、antd-vue,Naive-UI、Element3 等。Element3 官网

typescript
npm install element3 --save

src/main.js

typescript
import { createApp } from 'vue'

import Element3 from 'element3'
import 'element3/lib/theme-chalk/index.css'

import store from './store/index'
import App from './App.vue'
import router from './router/index'

const app = createApp(App)

app.use(store).use(router).use(Element3).mount('#app')

axios

安装 axios 了(axios 跟 Vue 版本没有直接关系)

bash
npm i axios --save

封装网络请求

javascript
import axios from 'axios'
import { useMsgbox, Message } from 'element3'
import store from '@/store'
import { getToken } from '@/utils/auth'

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  timeout: 5000, // request timeout
})

service.interceptors.request.use(
  config => {
    if (store.getters.token) {
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    console.log(error) // for debug
    return Promise.reject(error)
  },
)

service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 20000) {
      console.log('接口信息报错',res.message)
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('接口信息报错' + error) 
    return Promise.reject(error)
  },
)

export default service

Sass

在项目里集成 CSS 预编译器,CSS 预编译器可以帮我们更快、更高效地管理和编写 CSS 代码

javascript
npm install -D sass

有了 Sass 之后,我们在 CSS 里使用了变量、嵌套、继承等逻辑,并定义了 white 这两个变量。这样我们就可以嵌套书写 CSS 选择器

Vue SFC
<style lang="scss" scoped>
  $padding:10px;
  $white:#fff;
  ul {
    width:500px;
    margin:0 auto;
    padding: 0;
    li {
      &:hover {
        cursor: pointer;
      }
      list-style-type: none;
      margin-bottom: $padding;
      padding: $padding;
      background: $white;
      box-shadow: 1px 3px 5px rgba(0, 0, 0, 0.1);
    }
  }
</style>

至此基于 Vite + Vue 3 + Vue Router + Vuex + Element3 + Axios + Sass 的前端项目开发环境搭建完毕

代码规范和提交规范

进入到项目文件夹安装 ESLint

bash
npm i eslint -D

# 初始化 eslint
npx eslint --init

ESLint 安装成功后,在项目根目录下执行 npx eslint --init,然后按照终端操作的提示完成一系列设置来创建配置文件

img

开发权限系统

角色权限

通常使用的权限解决方案就是 RBAC 权限管理机制。每个用户有不同的角色,每个角色对应不同的页面权限,这个数据结构的关系设计主要是由后端来实现。

这样有一部分页面是写在代码的 src/router/index.js 中,另外一部分页面通过 axios 获取数据后,通过调用 vue-router 的 addRoute 方法动态添加进项目整体的路由配置中

在 Vuex 中注册 addRoute 这个 action,通过后端返回的权限页面数据,调用 router.addRoute 新增路由

javascript
addRoutes({ commit }, accessRoutes) {
  // 添加动态路由,同时保存移除函数,将来如果需要重置路由可以用到它们
  const removeRoutes = []
  accessRoutes.forEach(route => {
    const removeRoute = router.addRoute(route)
    removeRoutes.push(removeRoute)
  })
  commit('SET_REMOVE_ROUTES', removeRoutes)
},

与新增路由对应,在页面重新设置权限的时候,需要用 router.removeRoute来删除注册的路由,需要把动态路由的状态存储在本地存储里,否则刷新页面之后,动态的路由部分就会被清空,页面就会显示 404 报错。

需要在 localStorage 中把静态路由和动态路由分开对待,在页面刷新的时候,通过 src/router/index.js 入口文件中的 routes 配置,从 localStorage 中获取完整的路由信息,并且新增到 vue-router 中,才能加载完整的路由

在常见的登录状态下 token 过期,即使依然存在 token,刷新页面后也会跳转到登录页。登录状态过期,有一个单独的报错信息,需要在接口拦截函数中,统一对接口的响应结果进行拦截。如果报错信息显示的是登录过期,需要清理所有的 token 和页面权限数据,并且跳转到登录页面

javascript
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012) {
  // to re-login
  Msgbox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
    confirmButtonText: 'Re-Login',
    cancelButtonText: 'Cancel',
    type: 'warning',
  }).then(() => {
    store.dispatch('user/resetToken').then(() => {
      location.reload()
    })
  })
}
return Promise.reject(new Error(res.message || 'Error'))

Vue3 集成第三方框架

nprogress

javascript
npm install nprogress -D

需要使用 NProgress 的地方进行 import 的相关操作,比如在页面跳转的时候,就需要使用 NProgress 作为进度条。导入 NProgress 库之后,我们就不需要使用 Vue3 的插件机制进行注册

javascript
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style

router.beforeEach(async (to, from, next) => {
  // start progress bar
  NProgress.start()
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

组件的封装

以可视化组件为例,来分析复杂组件的封装。用可视化框架 ECharts 去封装 Vue 的组件,来实现可视化组件。不管你选择用百度的 ECharts,还是蚂蚁的 G2 等框架,在框架的使用方法上,都是类似的。首先,你需要完成图表库的配置,并且填入图表数据,然后把这个数据渲染在一个 DOM 上就可以了

Vue SFC
<template>
  <div ref="chartRef" class="chart"></div>
</template>

<script setup>
  import * as echarts from 'echarts'
  import {ref,onMounted,onUnmounted} from 'vue'
  // 通过ref获得DOM
  let chartRef = ref()
  let myChart 
  
  onUnmounted(()=>{
    myChart.dispose()
    myChart = null
  })
  
  onMounted(()=>{
    myChart = echarts.init(chartRef.value)
    const option = {
      tooltip: {
        trigger: 'item'
      },
      color: ['#ffd666', '#ffa39e', '#409EFF'],
      series: [
        {
          name: '前端课程',
          type: 'pie',
          radius: '70%',
          data: [
            {value: 43340, name: '重学前端'},
            {value: 7003, name: 'Javascript核心原理解析'},
            {value: 4314, name: '玩转Vue3全家桶'}
          ]
        }
      ]
    }
    myChart.setOption(option)
  })
</script>

ECharts 是一个非常复杂的可视化框架,有饼图、地图等不同的图表类型,如果引入 ECharts 全部代码的话,项目的体积会变得非常臃肿。要按需引入

指令的封装

在 Vue 中注册指令和组件略有不同,注册实现了 v-focus 指令,然后在 input 标签中加上 v-focus 指令

javascript
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
  // 当被绑定的元素挂载到 DOM 中时……
  mounted(el) {
    // 聚焦元素
    el.focus()
  }
})

指令的生命周期和组件类似,首先要让指令能够支持 Vue 的插件机制,需要在 install 函数内注册 lazy 指令,并且实现了 mounted、updated、unmounted 三个钩子函数

javascript
const lazyPlugin = {
  install (app, options) {
    app.directive('lazy', {
      mounted: ...,
      updated: ...,
      unmounted: ...
    })
  }
}

通过 lazy 指令获取到当前图片的标签,并且计算图片的位置信息,判断图片是否在首页显示。如果不在首页的话,图片就加载一个默认的占位符就可以了,并且在页面发生变化的时候,重新进行计算,这样就实现了页面图片的懒加载

与懒加载类似的,还有我们组件库中常用的 v-loading 指令,它用来显示组件内部的加载状态下面的代码中,我们注册了 loadingDirective 指令,并且注册了 mounted、updated、unmounted 三个钩子函数,通过 v-loading 的值来对显示效果进行切换,实现了组件内部的 loading 状态

动态切换的 Loading 组件能够显示一个 circle 的 div 标签,通过 v-loading 指令的注册,在后续表格、表单等组件的提交状态中,加载状态就可以很方便地使用 v-loading 来实现

javascript
const loadingDirective = {
  mounted: function (el, binding, vnode) {
    const mask = createComponent(Loading, {
      ...options,
      onAfterLeave() {
        el.domVisible = false
        const target =
          binding.modifiers.fullscreen || binding.modifiers.body? document.body: el
        removeClass(target, 'el-loading-parent--relative')
        removeClass(target, 'el-loading-parent--hidden')
      }
    })
    el.options = options
    el.instance = mask.proxy
    el.mask = mask.proxy.$el
    el.maskStyle = {}

    binding.value && toggleLoading(el, binding)
  },

  updated: function (el, binding) {
    el.instance.setText(el.getAttribute('element-loading-text'))
    if (binding.oldValue !== binding.value) {
      toggleLoading(el, binding)
    }
  },

  unmounted: function () {
    el.instance && el.instance.close()
  }
}

export default {
  install(app) {
    // if (Vue.prototype.$isServer) return
    app.directive('loading', loadingDirective)
  }
}

引入第三方库的注意事项

封装第三方库的目的是实现第三方框架和 Vue 框架的融合,提高开发效率。引入第三方库相关的注意事项。

  • 无论是引用第三方库还是自己封装的底层库,要考虑到项目的长期可维护性;其次,尽可能不要因为排期等问题,一股脑地把第三方库堆在一起,虽然这样做可以让项目在早期研发进度上走得很快,但这样会导致项目中后期的维护成本远远大于重写一遍代码 xxx 的成本

  • 然后是 Vue 中的 mixin,extends 机制能不用就不用,这两个 API 算是从 Vue 2 时代继承下来的产物,都是扩展和丰富 Vue 2 中 this 关键字,在项目复杂了之后,mixin 和 extends 隐式添加的 API 无从溯源,一旦多个 mixin 有了命名冲突,调试起来难度倍增

  • 项目中的全局属性也尽可能少用,全局变量是最原始的共享数据的方法,Vue 3 中我们使用 app.config.globalProperties.x 注册全局变量,要少用它的主要原因也是项目中的全局变量会极大的提高维护成本。有些监控场景必须要用到,就要把所有注册的全局变量放在一个独立的文件去管理