基于vue的前端架构
1、局部样式与全局样式
局部样式:一般都是使用scoped方案:
<style lang="scss" scoped>
...
</style>
全局样式:variable.scss 全局变量管理;mixins.scss 全局Mixins管理;global.scss 全局样式
其中variable.scss和mixins.scss会优先于global.scss加载,并且可以不通过import的方式在项目中任何位置使用这些变量和mixins
// vue.config.js module.exports = { css: { loaderOptions: { sass: { prependData: ` @import '@/styles/variable.scss'; @import '@/styles/mixins.scss'; `, }, }, }, }
2、体验优化
页面载入进度条
使用nprogress对路由跳转时做一个伪进度条,这样做在网络不好的情况下可以让用户知道页面已经在加载了:
import NProgress from 'nprogress'; router.beforeEach(() => { NProgress.start(); }); router.afterEach(() => { NProgress.done(); });
美化滚动条
::-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); }
3、移动端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)); }
4、静态资源与图标
静态资源
所有的静态资源文件都会上传到 阿里云 OSS 上,所以在环境变量上加以区分。
.env.development
与 .env.production
的 VUE_APP_STATIC_URL
属性分别配置了本地的静态资源服务器地址和线上 OSS 的地址。
本地的静态资源服务器是通过 pm2 + http-server 创建的,设计师切完直接扔进来就好了。
自动注册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
const requireAll = (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>
放置在 @/assets/icons 文件夹下的文件名
5、Axios封装
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 = get(error, 'response.status'); switch (status) { /* eslint-disable no-param-reassign */ 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 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; /* 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) { case 200: // [ 示例 ] code === 200 代表没有错误 return dataAxios.data; case 'xxx': // [ 示例 ] 其它和后台约定的 code return 'xxx'; default: // 不是正确的 code return '不是正确的code'; } } }, errorHandler); export default request;
6、跨域问题
可以用devServer提供的proxy代理:
// vue.config.js devServer: { proxy: { '/api': { target: 'http://47.100.186.132/your-path/api', ws: true, changeOrigin: true, pathRewrite: { '^/api': '' } } } }
7、路由
Layout
布局暂时分为三大类:
frameln:基于BasicLayout,通常需要登陆或权限认证的路由
frameOut:不需要动态判断权限的路由,如登录页或通用页面
errorPage:例如404
权限验证
通过获取当前用户的权限去比对路由表,生成当前用户的权限可访问的路由表,通过router.addRoutes动态挂载到router上
判断页面是否需要登陆状态,需要则跳转到/user/login
本地存储中不存在token则跳转到/user/login
如果存在token,用户信息不存在,自动调用vuex、‘/system/user/getInfo’
在路由中,集成了权限验证的功能,需要为页面增加权限时,在meta下添加相应的key
auth:当auth为true时,此页面需要进行登录权限验证,只针对frameIn路由有效
permissions:permissions每一个key对应权限功能的验证,当key的值为true时,代表具有权限,若key为false,配合v-permission指令,可以隐藏相应的DOM。
import router from '@/router'; import store from '@/store'; import storage from 'store'; 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 = storage.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); });
8、构建优化
包分析工具
const WebpackBundleAnalyzer = require('webpack-bundle-analyzer'); module.exports = { chainWebpack: (config) => { if (process.env.use_analyzer) { config .plugin('webpack-bundle-analyzer') .use(WebpackBundleAnalyzer.BundleAnalyzerPlugin); } }, };
开启Gzip
chainWebpack: (config) => { config .plugin('CompressionPlugin') .use(CompressionPlugin, []); },
路由懒加载
{ path: 'home', name: 'Home', component: () => import( /* webpackChunkName: "home" */ '@/views/home/index.vue' ), },