创作不易,喜欢就收藏吧!
Vue中大型项目的总结(纯原创)
技术栈:
- 基于 @vue/cli 搭建基础模板(大家都比较了解,节省开发时间,远胜于从零开始搭建)。
- 开发一些中大型的系统,vue-router 和 vuex 都是必不可少的。
- 快速开发UI组件库 Element UI 或Ant Design Vue(相对美观一点)。
- 工具库选择 lodash。
- 根据业务需求自己定义封装各种开发中可能用到的功能,
如:
组件库
状态管理
过滤器
指令、
CSS内置变量、
表单验证、
工具函数
样式:
CSS 预处理器的选择:
- Sass/Scss
- Less
- 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 进行操作等)
- 登陆(调接口)
- 菜单管理(储存路由信息,生成菜单,模糊查询等功能)
- 日志管理(消息提醒、日志留存、日志上报)
- 全屏操作
组件库:
可手动封装复用组件
过滤器:
过滤器是 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.构建优化:
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/vue@2.6.10/dist/vue.min.js',
'//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js',
'//unpkg.com/vuex@3.1.1/dist/vuex.min.js',
'//unpkg.com/axios@0.19.0/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添加
<!-- 使用CDN的CSS文件 -->
<% 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">