Vue中大型项目的总结(纯原创)

欢迎点击: 个人官网博客

创作不易,喜欢就收藏吧!

Vue中大型项目的总结(纯原创)

  • 技术栈:
  • 样式:
    • CSS 预处理器的选择:
    • 局部样式与全局样式:
  • 异步请求:
      • 封装 Axios:
      • 跨域问题:
      • Mock 数据:
        • 规则:
  • 路由:
      • 布局三大类:
      • 动态路由原理:
      • 权限验证:
        • 1.auth
        • 2.permissions
  • Vuex:
  • 组件库:
  • 过滤器:
  • 指令:
  • 项目优化上线:
    • 1.体验优化:
      • 页面顶部加载条:
      • 美化滚动条样式:
      • 静态资源加载页面(首次加载白屏):
      • 移动端 100vh 问题:
    • 2.构建优化:
      • 1.包分析工具:webpack-bundle-analyzer
      • 2.开启 Gzip(后端配置支持你的 .gz 文件)
      • 3.路由懒加载
      • 4.抽取公共包,引入CDN
      • 5.压缩图片使用image-webpack-loader
      • 6.去除console
      • 其他一些优化:
        • 1.合理使用keep-alive避免不必要的加载
        • 2.合理使用本地存储一些简单的数据
        • 3.图片懒加载
        • 4.使用字体图标代替图片,或使用精灵图(雪碧图),或svg图片

技术栈:

  1. 基于 @vue/cli 搭建基础模板(大家都比较了解,节省开发时间,远胜于从零开始搭建)。
  2. 开发一些中大型的系统,vue-routervuex 都是必不可少的。
  3. 快速开发UI组件库 Element UIAnt Design Vue(相对美观一点)。
  4. 工具库选择 lodash
  5. 根据业务需求自己定义封装各种开发中可能用到的功能,
    如:
    组件库
    状态管理
    过滤器
    指令、
    CSS内置变量、
    表单验证、
    工具函数

样式:

CSS 预处理器的选择:

  1. Sass/Scss
  2. Less
  3. Stylus

公司更倾向于使用 scss 开发,less 是为了覆盖 ant design vue 的样式,stylus 比较少人用。

局部样式与全局样式:

局部样式:
一般都是使用 scoped 方案:

<style lang="scss" scoped>
@import "base.css";
  ...
</style>

全局样式:
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';
        `,
      },
    },
  },
}

异步请求:

封装 Axios:

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

import axios from 'axios';
import get from 'lodash/get';
import storage from 'store';
// 创建 axios 实例
const request = axios.create({
 // API 请求的默认前缀
 baseURL: process.env.VUE_APP_BASE_URL,
 timeout: 10000, // 请求超时时间
});
// 异常拦截处理器
const errorHandler = (error) => {
 const status = error.response?.status//缩写,表示如果error.response存在就拿error.response.status
 switch (status) {
   case 400: error.message = '请求错误'; break;
   case 401: error.message = '未授权,请登录'; break;
   case 403: error.message = '拒绝访问(登录过期,请重新登录)'; break;
   case 404: error.message = `请求地址出错,未找到资源: ${error.response.config.url}`; break;
   case 405: error.message = '请求方法未允许'; break;
   case 408: error.message = '请求超时'; break;
   case 500: error.message = '服务器内部错误'; break;
   case 501: error.message = '服务未实现'; break;
   case 502: error.message = '网关错误'; break;
   case 503: error.message = '服务不可用'; break;
   case 504: error.message = '网关超时'; break;
   case 505: error.message = 'HTTP版本不受支持'; break;
   default: break;
 }
 return Promise.reject(error);
};
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map()
/**
 * 添加请求
 * @param {Object} config 
 */
const addPending = (config) => {
    const url = [
        config.method,
        config.url,
        JSON.stringify(config.params),
        JSON.stringify(config.data)
    ].join('&')
    config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
        if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
            pending.set(url, cancel)
        }
    })
}
/**
 * 移除请求
 * @param {Object} config 
 */
const removePending = (config) => {
    const url = [
        config.method,
        config.url,
        JSON.stringify(config.params),
        JSON.stringify(config.data)
    ].join('&')
    console.log('url=', url)
    if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
        const cancel = pending.get(url)
        cancel(url)
        pending.delete(url)
    }
}
/**
 * 清空 pending 中的请求(在路由跳转时调用)
 */
export const clearPending = () => {
    for (const [url, cancel] of pending) {
        cancel(url)
    }
    pending.clear()
}
/** 
 * 请求拦截器 
 * 每次请求前,如果存在token则在请求头中携带token 
 */
request.interceptors.request.use((config) => {
		removePending(config) // 在请求开始前,对之前的请求做检查取消操作
    	addPending(config) // 将当前请求添加到 pending 中
        //Vue.ls本地储存的工具包
        const token = Vue.ls.get('ACCESS_TOKEN')
        if (token) {
            config.headers['Authorization'] = 'Bearer ' + token // 
        }
        return config;
}, errorHandler);

// 响应拦截器
request.interceptors.response.use((response) => {
 removePending(response.config) // 在请求结束后,移除本次请求
 const dataAxios = response.data;
 // 这个状态码是和后端约定的
 const { code } = dataAxios;
 // 根据 code 进行判断
 if (code === undefined) {
   // 如果没有 code 代表这不是项目后端开发的接口
   return dataAxios;
 } else {
   // 有 code 代表这是一个后端接口 可以进行进一步的判断
   switch (code) {
     case 200:
       // code === 200 代表没有错误
       return dataAxios.data;
     case 'xxx':
       // [ 示例 ] 其它和后台约定的 code
       return 'xxx';
     default:
       return '不是正确的code';
   }
 }
}, errorHandler);
export default request;

应用实例:

export const getBanner = () => {
    return instance({
        method: 'get',
        url: 'public/v1/home/swiperdata',
        params: {
            firstName: 'Fred',
            lastName: 'Flintstone'
        }
    });
}

将clearPending()方法添加到vue路由钩子函数中

router.beforeEach(async (to, from, next) => {
    clearPending()
    next();
});

跨域问题:

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

不过proxy只能在开发环境使用,线上还得后端处理设置cors(如:Nodejs:res.header(“Access-Control-Allow-Origin”,"*"了解更多,点击这里),允许我们访问。

// 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 default 或 export 一个数组。

入口文件:

import Mock from '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 Mock from 'mockjs';
const { Random } = Mock;
export default [
  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'),
      },
    ],
  },
];

路由:

布局三大类:

  • frameIn:基于 BasicLayout,通常需要登录或权限认证的路由。
  • frameOut:不需要动态判断权限的路由,如登录页或通用页面。
  • errorPage:例如404页面。

动态路由原理:

路由需要分成两类,静态路由和动态路由。静态路由是任何菜单权限下都能查看的界面路由;动态路由是根据菜单权限动态生成的路由集合。

大体步骤:拦截路由->后台取到路由->保存路由到localStorage或vuex(用户登录进来只会从后台取一次,其余都从本地取,所以用户,只有退出在登录路由才会更新)

核心:beforeEach、addRoutes、localStorage

import axios from 'axios'

var getRouter //用来获取后台拿到的路由

router.beforeEach((to, from, next) => {
  if (!getRouter) {//不加这个判断,路由会陷入死循环
    if (!getObjArr('router')) {
      axios.get('https://www.easy-mock.com/mock/5a5da330d9b48c260cb42ca8/example/antrouter').then(res => {
        getRouter = res.data.data.router//后台拿到路由
        saveObjArr('router', getRouter) //存储路由到localStorage

        routerGo(to, next)//执行路由跳转方法
      })
    } else {//从localStorage拿到了路由
      getRouter = getObjArr('router')//拿到路由
      routerGo(to, next)
    }
  } else {
    next()
  }

})


function routerGo(to, next) {
  getRouter = filterAsyncRouter(getRouter) //过滤路由
  router.addRoutes(getRouter) //动态添加路由
  global.antRouter = getRouter //将路由数据传递给全局变量,做侧边栏菜单渲染工作
  next({ ...to, replace: true })
}

function saveObjArr(name, data) { //localStorage 存储数组对象的方法
  localStorage.setItem(name, JSON.stringify(data))
}

function getObjArr(name) { //localStorage 获取数组对象的方法
  return JSON.parse(window.localStorage.getItem(name));

}

或者vue3.0

import { createRouter, createWebHashHistory } from "vue-router";
import {clearPending} from '../axios'

// 静态路由
const initRouter = [
    {
        path: "/",
        name: "login",
        component: () => import("@/views/login.vue"),
    },
    {
        path: "/home",
        name: "home",
        component: () => import("@/views/home.vue"),
    },
];
// 动态路由
const asyncRouter = [
    {
        path: "/a",
        name: "a",
    },
    {
        path: "/b",
        name: "b",
    },
];
const asyncRouterList = function () {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(asyncRouter)
        }, 300)
    })
}
const router = createRouter({
    history: createWebHashHistory(),
    routes: initRouter,
});

router.beforeEach(async (to, from, next) => {
    //路由跳转前,清空请求
    clearPending()
    // 请求后台路由
    const a = await asyncRouterList()
    //添加请求到的路由
    a.forEach(i => {
        router.options.routes.push(i);
        router.addRoute(i.name, i);
    })
    next();
});
export default router;

权限验证:

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

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

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

1.auth

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

2.permissions

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

import router from '@/router';
import store from '@/store';
import Vuefrom 'Vue';
import util from '@/libs/utils';
// 进度条
import NProgress from '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 = Vue.ls.get('ACCESS_TOKEN');
    if (token && token !== 'undefined') {
      // 是否处于登录页面
      if (to.path === loginRoutePath) {
        next({ path: defaultRoutePath });
        // 查询是否储存用户信息
      } else if (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);
});

Vuex:

内置一些功能,主要是对以下这些功能做了一些封装:

  • 用户信息管理(储存信息、对 token 进行操作等)
  • 登陆(调接口)
  • 菜单管理(储存路由信息,生成菜单,模糊查询等功能)
  • 日志管理(消息提醒、日志留存、日志上报)
  • 全屏操作

组件库:

可手动封装复用组件

  • 数字动画
  • 大文件上传(切片、断点续传、秒传)需要与后端配合
  • 图片预览
  • Excel 导入导出
  • 富文本编辑器
  • pdf 生成 及预览
  • 国际化 vue-i18n
  • 等等…

过滤器:

过滤器是 Vue 提供的一个很好用的功能,vue3 已去除。

  • 格式化时间
  • 格式化数字金额
  • 格式化文本
  • 区分环境的链接(主要针对本地静态资源服务器和 OSS )

指令:

  • 组件权限验证
  • 文本复制
  • 图片懒加载
  • 焦点,文本框聚焦
  • 滚动至指定位置
  • 快捷键绑定

项目优化上线:

1.体验优化:

页面顶部加载条:

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

npm install nprogress
npm install @types/nprogress//如果使用ts
import NProgress from 'nprogress';
router.beforeEach(() => {
  NProgress.start();
});
router.afterEach(() => {
  NProgress.done();
});

美化滚动条样式:

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-check npm install vh-check --save

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

定义一个 CSS Mixin

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

2.构建优化:

完整的vue.config.js

1.包分析工具:webpack-bundle-analyzer

(构建代码之后,到底是什么占用了这么多空间)

2.开启 Gzip(后端配置支持你的 .gz 文件)

const CompressionWebpackPlugin = require('compression-webpack-plugin')
 const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i; // 开启gzip压缩
 new CompressionWebpackPlugin({
                algorithm: 'gzip',
                test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
                threshold: 10240,
                minRatio: 0.8
            }),

Nginx服务器也要有相应的配置:

server{ 
listen 8087;
 server_name localhost; 
 gzip on; gzip_min_length 1k;
 gzip_comp_level 9; 
 gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; 
 gzip_vary on; 
 gzip_disable "MSIE [1-6]\.";
  }

3.路由懒加载

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

4.抽取公共包,引入CDN

//vue.config.js
const externals = {
  vue: 'Vue',
  'vue-router': 'VueRouter',
  vuex: 'Vuex',
  axios: 'axios'
}
const cdnMap = {
  css: [],
  js: [
      '//unpkg.com/[email protected]/dist/vue.min.js',
      '//unpkg.com/[email protected]/dist/vue-router.min.js',
      '//unpkg.com/[email protected]/dist/vuex.min.js',
      '//unpkg.com/[email protected]/dist/axios.min.js'
  ]
}
module.exports = {
  chainWebpack: config => {
    config.externals(externals)
    config.plugin('html').tap(args => {
      args[0].cdn = cdnMap
      args[0].minify && (args[0].minify.minifyCSS = true) // 压缩html中的css
      return args
    })
  }
}

然后在public/index.html添加

<!-- 使用CDNCSS文件 -->
<% for (var i in
  htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
  <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style" />
  <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
<% } %>
  <!-- 使用CDN加速的JS文件,配置在vue.config.js下 -->
<% for (var i in
  htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
  <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>

5.压缩图片使用image-webpack-loader

6.去除console

npm i -D babel-plugin-transform-remove-console

在 babel.config.js 中配置

// NODE_ENV
const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

const plugins = [
  //按需引入elementui
  [
    "component",
    {
      libraryName: "element-ui",
      styleLibraryName: "theme-chalk",
    },
  ],
];

// 去除 console.log
if (IS_PROD) {
  plugins.push("transform-remove-console");
}

module.exports = {
  presets: ["@vue/cli-plugin-babel/preset", ["@babel/preset-env", { modules: false }]],
  plugins: plugins,
};

其他一些优化:

1.合理使用keep-alive避免不必要的加载

2.合理使用本地存储一些简单的数据

3.图片懒加载

 npm install vue-lazyload --save-dev
 <img v-lazy="imgsrc">
 <img v-lazy:background-image="imgsrc">

4.使用字体图标代替图片,或使用精灵图(雪碧图),或svg图片

vue3.0+element-plus实现国际化语言,动态路由菜单:demo案例

你可能感兴趣的:(js,vue,vue,web,webpack,后台管理,经验分享)