基于Vue的前端架构,我做了这15点

2022-09-25 12:05:19

1.分解需求

技术栈

  • 考虑到后续招人和现有人员的技术栈,选择 Vue 作为框架。

  • 公司主要业务是 GIS 和 BIM,通常开发一些中大型的系统,所以 vue-router 和 vuex 都是必不可少的。

  • 放弃了 Element UI 选择了 Ant Design Vue(最近 Element 好像复活了,麻蛋)。

  • 工具库选择 lodash。

建立脚手架

  • 搭建 NPM 私服。

  • 使用 Node 环境开发 CLI 工具,参考我自己写过的一篇 -【搭建自己的脚手架—“优雅”生成前端工程】。

  • 基于 @vue/cli 搭建基础的模板(大家都比较了解,节省开发时间,远胜于从零开始搭建)。

  • 根据业务需求定义各种开发中可能用到的功能(组件库、状态管理、过滤器、指令、CSS内置变量、CSS Mixins、表单验证、工具函数等)。

  • 性能优化,例如对 Ant Design Vue 组件库的优化。

开发规范

  • 对代码风格、命名规则、目录结构进行统一规范。

  • 静态资源的使用规范。

  • 单元测试、提交线上测试规范。

  • Git 提交记录和多人协作规范。

2.样式

CSS 预处理器的选择

  • Sass/Scss ✅
  • Less ✅
  • Stylus ⭕

为什么选择了两个?因为公司团队跟倾向于使用 scss 开发,less 是为了覆盖 ant design vue 的样式,stylus 只有我自己喜欢这种风格。

局部样式与全局样式

局部样式

一般都是使用 scoped 方案:

<style lang="scss" scoped>
  ...
</style>

全局样式

全局样式 目录:@/styles

variable.scss: 全局变量管理
mixins.scss: 全局 Mixins 管理
global.scss: 全局样式

其中 variable.scss 和 mixins.scss 会优先于 global.css 加载,并且可以不通过 import 的方式在项目中任何位置使用这些变量和 mixins。

// vue.config.js
module.exports={
  css:{
    loaderOptions:{
      sass:{
        prependData:`
        @import '@/styles/variable.scss';
        @import '@/styles/mixins.scss';
        `,},},},}

体验优化

页面载入进度条

使用nprogress 对路由跳转时做一个伪进度条,这样做在网络不好的情况下可以让用户知道页面已经在加载了:

import NProgressfrom'nprogress';

router.beforeEach(()=>{
  NProgress.start();});

router.afterEach(()=>{
  NProgress.done();});

美化滚动条

一直用 Mac 做前端,突然发现同事的 Windows 上出现了十分丑陋的滚动条,为了保持一致:

::-webkit-scrollbar{width: 6px;height: 6px;}::-webkit-scrollbar-track{width: 6px;background:rgba(#101F1C, 0.1);-webkit-border-radius: 2em;-moz-border-radius: 2em;border-radius: 2em;}::-webkit-scrollbar-thumb{background-color:rgba(#101F1C, 0.5);background-clip: padding-box;min-height: 28px;-webkit-border-radius: 2em;-moz-border-radius: 2em;border-radius: 2em;}::-webkit-scrollbar-thumb:hover{background-color:rgba(#101F1C, 1);}

静态资源加载页面

首次加载页面时,会产生大量的白屏时间,这时做一个 loading 效果看起来会很友好,其实很简单,直接在 public/index.html 里写一些静态的样式即可。

移动端 100vh 问题

在移动端使用 100vh 时,发现在 Chrome、Safari 浏览器中,因为浏览器栏和一些导航栏、链接栏导致不一样的呈现:

你以为的 100vh === 视口高度

实际上 100vh === 视口高度 + 浏览器工具栏(地址栏等等)的高度

解决方案

安装 vh-checknpm install vh-check --save

import vhCheckfrom'vh-check';vhCheck('browser-address-bar');

定义一个 CSS Mixin

@mixin vh($height: 100vh) {
  height: $height;
  height: calc(#{$height} - var(--browser-address-bar, 0px));
}

之后就是哪里不会点哪里。

3.组件库

因为 Element UI 长期没更新,并且之前使用过 React 的 Ant Design(重点),所以组件库选择了Ant Design Vue。

覆盖 Ant Design Vue 样式

设计师眼中的 Ant Design === ‘丑’(心酸)。

1.使用 .less 文件

Ant Design Vue 的样式使用了 Less 作为开发语言,并定义了一系列全局/组件的样式变量,所以需要安装了 less、less-loader,在@/styles/antd-theme.less 可以覆盖默认样式。

优点是:

方便快捷,可以修改 class,覆盖默认变量。

缺点是:

必须引入@import '~ant-design-vue/dist/antd.less'; ,引入后会将所有的组件样式全部引入,导致打包后的 css 体积达到 500kb 左右。

2.使用 JavaScript 对象

通过 JavaScript 对象的方式可以修改内置变量,需要对 Less 进行配置:

// vue.config.jsconst modifyVars=require('./src/styles/antdTheme.js');

module.exports={
  css:{
    loaderOptions:{
      less:{
        lessOptions:{
          javascriptEnabled:true,
          modifyVars,},},},},}

这一步还可以继续优化,通过 babel-plugin-import 使 Ant Design Vue 的组件样式可以按需加载:

// babel.config.js
module.exports={
  presets:['@vue/cli-plugin-babel/preset',],
  plugins:[['import',{ libraryName:'ant-design-vue', libraryDirectory:'es', style:true},],],};

优点是:

可以按需引入,打包后的 CSS 体积取决于你引用了多少个组件。

缺点是:

不能使用 class 进行样式覆盖。

干掉无用的图标

Ant Design Vue 把所有的 Icon 一次性引入(不管你因用了多少个组件),这使得体积打包后图标所占的体积竟然有几百 kb 之多。这些图标大多数不会被设计师所采纳,所以部分图标都应该被干掉:

创建一个 icons.js 来管理 Ant Design Vue 图标,这里以一个 Loading 图标为例:

// @/src/assets/icons.jsexport{defaultas LoadingOutline}from'@ant-design/icons/lib/outline/LoadingOutline';

如何知道你要加载的图标在什么路径下?

在 @ant-design/icons/lib 目录下有三种风格的图标,分别是 fill、outline、twotone,这里面内部的文件并不是 svg 格式,而是 js 和 ts 格式,这就是为什么我们可以这么引入图标的关键所在了。

下一步是通过配置 vue.config.js 将这个文件引入进来:

// vue.config.js
module.exports={
  configureWebpack:{
    resolve:{
      alias:{'@ant-design/icons/lib/dist$': path.resolve(__dirname,'./src/assets/icons.js'),},},},}

解决 Moment 多国语

解决到这之后,Ant Design Vue 居然还很大,这是因为 moment 是 Ant Design Vue 中有强依赖该插件,所以使用 webpack 插件减小打包体积,这里我们只保留 zh-cn 语言包:

// vue.config.js
module.exports={
  chainWebpack:(config)=>{
    config.plugin('ContextReplacementPlugin').use(webpack.ContextReplacementPlugin,[/moment[/\\]locale$/,/zh-cn/]);},}

部分组件需要在页面内引用

Ant Design Vue 中部分体积较大的组件,例如DatePicker,根据业务需求,应考虑在页面中进行加载,尽量保证首屏加载的速度:

<script>
import { DatePicker } from 'ant-design-vue';
export default {
  components: {
    ADatePicker: DatePicker,
  },
}
</script>

4.静态资源与图标

静态资源

所有的静态资源文件都会上传到 阿里云 OSS 上,所以在环境变量上加以区分。

.env.development.env.productionVUE_APP_STATIC_URL 属性分别配置了本地的静态资源服务器地址和线上 OSS 的地址。

本地的静态资源服务器是通过 pm2 + http-server 创建的,设计师切完直接扔进来就好了。

自动注册 Svg 图标

在日常的开发中,总是会有着大量的图标需要使用,这里我们直接选择使用 SVG 图标。但是如果每次使用图标还需要通过路径找到这张图标岂不是很麻烦?

下面这种才是我想要的方案(直接 name 等于 文件名即可):

<template>
	<svg name="logo" />
</template>

而且最后打包后需要合并成一张雪碧图。

首先需要对@/assets/icons 文件夹下的 svg 图标进行自动注册,需要对 webpack 和 svg-sprite-loader 进行了相关设置,文件全部打包成 svg-sprite。

module.exports={
  chainWebpack:(config)=>{
    config.module.rule('svg').exclude.add(resolve('src/assets/icons')).end();

    config.module.rule('icons').test(/\.svg$/).include.add(resolve('src/assets/icons')).end().use('svg-sprite-loader').loader('svg-sprite-loader');},}

写一个全局用的 Vue 组件<m-svg />:

// @/components/m-svg/index.js

constrequireAll=(requireContext)=> requireContext.keys().map(requireContext);const req= require.context('@/assets/icons',false,/\.svg$/);requireAll(req);

@/components/m-svg/index.vue

<template>
  <svg class="mw-svg" aria-hidden="true">
    <use :xlink:href="iconName"></use>
  </svg>
</template>
<script>
export default {
  name: 'm-svg',
  props: {
    name: { type: String, default: '' },
  },
  computed: {
    iconName() {
      return `#${this.name}`;
    },
  },
};
</script>
<style lang="scss" scoped>
.mw-svg {
  width: 1.4em;
  height: 1.4em;
  fill: currentColor;
  overflow: hidden;
  line-height: 1em;
  display: inline-block;
}
</style>

参数 name

  • 类型:String
  • 默认值:null
  • 说明:放置在@/assets/icons 文件夹下的文件名

样式

  • 图标的大小可以通过 width + height 属性改变。
  • 通过改变 font-size 属性改变,宽高 = font-zise * 1.4

5.异步请求

封装 Axios

@/libs/request.js 路径下对 Axios 进行封装,封装了请求参数,请求头,以及错误提示信息、 request 拦截器、response 拦截器、统一的错误处理、baseURL 设置等。

废话不说直接贴代码:

import axiosfrom'axios';importgetfrom'lodash/get';import storagefrom'store';// 创建 axios 实例const request= axios.create({// API 请求的默认前缀
 baseURL: process.env.VUE_APP_BASE_URL,
 timeout:10000,// 请求超时时间});// 异常拦截处理器consterrorHandler=(error)=>{const status=get(error,'response.status');switch(status){/* eslint-disable no-param-reassign */case400: error.message='请求错误';break;case401: error.message='未授权,请登录';break;case403: error.message='拒绝访问';break;case404: error.message=`请求地址出错:${error.response.config.url}`;break;case408: error.message='请求超时';break;case500: error.message='服务器内部错误';break;case501: error.message='服务未实现';break;case502: error.message='网关错误';break;case503: error.message='服务不可用';break;case504: error.message='网关超时';break;case505: error.message='HTTP版本不受支持';break;default:break;/* eslint-disabled */}return Promise.reject(error);};// request interceptor
request.interceptors.request.use((config)=>{// 如果 token 存在// 让每个请求携带自定义 token 请根据实际情况自行修改// eslint-disable-next-line no-param-reassign
 config.headers.Authorization=`bearer${storage.get('ACCESS_TOKEN')}`;return config;}, errorHandler);// response interceptor
request.interceptors.response.use((response)=>{const dataAxios= response.data;// 这个状态码是和后端约定的const{ code}= dataAxios;// 根据 code 进行判断if(code=== undefined){// 如果没有 code 代表这不是项目后端开发的接口return dataAxios;// eslint-disable-next-line no-else-return}else{// 有 code 代表这是一个后端接口 可以进行进一步的判断switch(code){case200:// [ 示例 ] code === 200 代表没有错误return dataAxios.data;case'xxx':// [ 示例 ] 其它和后台约定的 codereturn'xxx';default:// 不是正确的 codereturn'不是正确的code';}}}, errorHandler);exportdefault request;
  • 通过 VUE_APP_BASE_URL 区分线上与开发环境的 API 地址。
  • code 起到一个比较关键的作用,例如 token 过期时的验证。
  • 使用了一个叫 sotre 的包作为本地储存的工具用来存储 token。

跨域问题

跨域问题一般情况直接找后端解决了,你要是不好意思打扰他们的话,可以用 devServer 提供的 proxy 代理:

// vue.config.js
devServer:{
  proxy:{'/api':{
      target:'http://47.100.186.132/your-path/api',
      ws:true,
      changeOrigin:true,
      pathRewrite:{'^/api':''}}}}

Mock 数据

一个很常见的情况,后端接口没出来,前端在这干瞪眼。

Mock 数据功能是基于mock.js (opens new window)开发,通过 webpack 进行自动加载 mock 配置文件。

规则

  • 所有的 mock 配置文件均应放置在@/mock/services 路径内。
  • @/mock/services 内部可以建立业务相关的文件夹分类存放配置文件。
  • 所有的配置文件应按照***.mock.js 的命名规范创建。
  • 配置文件使用 ES6 Module 导出export defaultexport 一个数组。

入口文件

import Mockfrom'mockjs';

Mock.setup({
  timeout:'500-800',});const context= require.context('./services',true,/\.mock.js$/);

context.keys().forEach((key)=>{
  Object.keys(context(key)).forEach((paramKey)=>{
    Mock.mock(...context(key)[paramKey]);});});

示例模板

import Mockfrom'mockjs';const{ Random}= Mock;exportdefault[RegExp('/example.*'),'get',{'range|50-100':50,'data|10':[{// 唯一 ID
        id:'@guid()',// 生成一个中文名字
        cname:'@cname()',// 生成一个 url
        url:'@url()',// 生成一个地址
        county: Mock.mock('@county(true)'),// 从数组中随机选择一个值'array|1':['A','B','C','D','E'],// 随机生成一个时间
        time:'@datetime()',// 生成一张图片
        image: Random.dataImage('200x100','Mock Image'),},],},];

6.路由

Layout

布局暂时分为三大类:

  • frameIn:基于BasicLayout,通常需要登录或权限认证的路由。

  • frameOut:不需要动态判断权限的路由,如登录页或通用页面。

  • errorPage:例如404。

权限验证

通过获取当前用户的权限去比对路由表,生成当前用户具的权限可访问的路由表,通过 router.addRoutes 动态挂载到 router 上。

  • 判断页面是否需要登陆状态,需要则跳转到 /user/login
  • 本地存储中不存在 token 则跳转到 /user/login
  • 如果存在 token,用户信息不存在,自动调用 vuex ‘/system/user/getInfo’

在路由中,集成了权限验证的功能,需要为页面增加权限时,在 meta 下添加相应的 key:

auth

  • 类型:Boolean
  • 说明:当 auth 为 true 时,此页面需要进行登陆权限验证,只针对 frameIn 路由有效。

permissions

  • 类型:Object
  • 说明:permissions 每一个 key 对应权限功能的验证,当 key 的值为 true 时,代表具有权限,若 key 为 false,配合v-permission 指令,可以隐藏相应的 DOM。

在这里贴一下路由跳转时权限验证的代码:

import routerfrom'@/router';import storefrom'@/store';import storagefrom'store';import utilfrom'@/libs/utils';// 进度条import NProgressfrom'nprogress';import'nprogress/nprogress.css';const loginRoutePath='/user/login';const defaultRoutePath='/home';/**
 * 路由拦截
 * 权限验证
 */
router.beforeEach(async(to,from, next)=>{// 进度条
  NProgress.start();// 验证当前路由所有的匹配中是否需要有登录验证的if(to.matched.some((r)=> r.meta.auth)){// 是否存有token作为验证是否登录的条件const token= storage.get('ACCESS_TOKEN');if(token&& token!=='undefined'){// 是否处于登录页面if(to.path=== loginRoutePath){next({ path: defaultRoutePath});// 查询是否储存用户信息}elseif(Object.keys(store.state.system.user.info).length===0){
        store.dispatch('system/user/getInfo').then(()=>{next();});}else{next();}}else{// 没有登录的时候跳转到登录界面// 携带上登陆成功之后需要跳转的页面完整路径next({
        name:'Login',
        query:{
          redirect: to.fullPath,},});
      NProgress.done();}}else{// 不需要身份校验 直接通过next();}});

router.afterEach((to)=>{// 进度条
  NProgress.done();
  util.title(to.meta.title);});

页面开发

  • 根据业务需要划分,按照路由层级在 views 中创建相对应的页面组件,以文件夹的形式创建,并在文件夹内创建 index.vue 文件作为页面的入口文件。
  • 页面内的组件:在页面文件夹下创建 components 文件夹,在其内部对应创建相应的组件文件,如果是复杂组件,应以文件夹的形式创建组件。
  • 工具模块:能够高度抽象的工具模块,应创建在 @/src/libs 内创建 js 文件。

7、构建优化

包分析工具

构建代码之后,到底是什么占用了这么多空间?靠直觉猜测或者使用 webpack-bundle-analyzer。

const WebpackBundleAnalyzer=require('webpack-bundle-analyzer');

module.exports={
  chainWebpack:(config)=>{if(process.env.use_analyzer){
      config.plugin('webpack-bundle-analyzer').use(WebpackBundleAnalyzer.BundleAnalyzerPlugin);}},};

开启 Gzip

对,这这么一句话,后端就得支持你的 .gz 文件了,而你只需要坐着等老板夸:

chainWebpack:(config)=>{
  config.plugin('CompressionPlugin').use(CompressionPlugin,[]);},

路由懒加载

这块 @vue/cli 已经帮忙处理好了,但也需要了解一下他的原理和如何配置。

{
  path:'home',
  name:'Home',
  component:()=>import(/* webpackChunkName: "home" */'@/views/home/index.vue'),},

webpackChunkName 这条注释还是很有必要加的,至少你打包后知道又是哪个页面变得又臭又大。

Preload & Prefetch

不清楚这两个功能的去 @vue/cli 补课,这两个功能非常有助于你处理加载的性能。

8.测试框架

直接使用了官方提供的 Vue Test Utils,这东西可以对组件进行测试,很不错。

写单元测试在团队里其实很难推进,不知道大家怎么看。

9.组件库

对于很多第三方的工具,我坚持认为二次封装成 vue 插件并没有多少开发成本,反而让你在后续的开发中变得很灵活。

我对以下库进行了 vue 插件的封装,并提交到 npm 私服:

  • 数字动画
  • 代码高亮
  • 大文件上传(切片、断点续传、秒传)需要与后端配合
  • 图片预览
  • Excel 导入导出
  • 富文本编
  • 作者:codexu.
  • 原文链接:https://blog.csdn.net/github_35432979/article/details/110440817
    更新时间:2022-09-25 12:05:19