vue项目的一些最佳实践提炼和经验总结
- 项目组织结构
- ajax数据请求的封装和api接口的模块化管理
- 第三方库按需加载
- 利用less的深度选择器优雅覆盖当前页面ui库组件的样式
- webpack实时打包进度
- vue组件中选项的顺序
- 路由的懒加载
- 路由模块拆分化管理
项目组织结构
清晰的项目结构能让别人开发进来更容易理解,当然,每个人都有一定的代码风格习惯。但基于vue开发框架的项目,vue-cli脚手架搭建的项目组织结构大同小异。同时,预想到后面的需求变更及功能增加进展得更有效率,下面截图是我觉得比较好的项目组织结构:
这个截图只是针对个人觉得比较通用的vue工程结构,不过这个结构要根据具体的项目情况调整,不必为了模块化而模块化。模块化的优势就是体现在项目业务比较复杂的情况,如果项目业务逻辑并不复杂,可以适当的删减部分模块或文件。
相关说明:
assets: 存放图片、ui设计的图标文件
componets:自研的业务型及通用型组件
router:项目的路由管理模块
store:基于vuex的状态管理容器,api存放各模块的数据请求,modules存放将store分割成模块(module),按官网的说法,每个模块应该拥有自己的 state、mutation、action、getter,主要是解决应用的所有状态如果全部集中到一个比较大的store对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿而难以维护。
例子:
其中的一个模块configmanage.js
import { configmanageservice } from "../api/index" // state const state = { accountmenulist:[] } // getters const getters = { // 菜单 menutree: state => { return state.accountmenulist; }, } // actions const actions = { async get_accout_menu({ state, commit }, model) { // 参数 state为当前局部状态,commit响应式改变当前绑定的菜单数据 const res = await configmanageservice.getacountmenu(model); commit("change_menu", res.data); } } // mutations const mutations = { change_menu: (data) => { state.accountmenulist = data; } } export default { state, getters, actions, mutations }
index.js,统一出口,导出全部的store模块
import vue from 'vue' import vuex from 'vuex' import index from './modules/index' import report from './modules/report' import createlogger from 'vuex/dist/logger' // 控制台输出当前变化的某个状态 vue.use(vuex) const debug = process.env.node_env !== 'production' // 生产或开发环境打包 export const indexstore = new vuex.store({ modules: { report, index }, strict: debug, // 按照官网建议,改变state的状态只能通过getter plugins: debug ? [createlogger()] : [] })
style:
存放重写ui库的样式和不同组件公共样式文件
util:
存放用es6封装的工具类,http请求类,配置类、校验类、事件类等
views:
存放各路由模块页面
static:
存放全局配置文件,环境域名等
iconfont:
存放字体图标文件
ajax数据请求的封装和api接口的模块化管理
基于vue的项目,与后台请求数据我们通常使用的是axios,它是基于promise的http库,其提供的优秀的特性被广泛运用在项目当中,官方已推荐使用axios,放弃原有的vue-resource。
1、axios的封装,在很多业务场景下用来进行请求的拦截、响应的拦截及请求超时等;
// axios请求类,一些基础化配置 class ajaxrequestmodel { constructor(model) { this.url = model.url || ""; this.data = model.data || {}; this.method = model.method || "post"; // this.success = model.success || function () {}; // this.fail = model.fail || function () {}; // this.slientsuccess = model.slientsuccess || true; this.failmsg = model.failmsg || true; this.baseurl = model.baseurl || window.sysconfig.baseurl; this.loading = model.loading || true; // this.setdata(); this.seturl(); } setdata() { // let options = { // sessionid: "" // }; this.data = object.assign({}, this.data); } seturl() { this.url = this.baseurl + this.url; } } // 实例化axios,配置请求超时时间 const axiosinstance = axios.create({ timeout: 1000 * 20 }); // 封装ajaxservice函数,以更少的代码处理get、post、delete、put请求方式,同时支持async、await异步处理方案,返回promise const ajaxservice = param => { let model = new ajaxrequestmodel(param); let o = { url: model.url, data: model.data, method: model.method }; // if (model.loading) { // ak.msg.showloading(); // } if (model.method === "get") { o = { url: model.url, params: model.data, method: model.method }; } return new promise((resolve, reject) => { axiosinstance .request(o) .then(res => { if (res.data.code === 200 || res.data.code === 0) { resolve(res.data); } else { ak.msg.toast(res.data.message, "error"); reject(res.data); } }) .catch(err => { httpresponsehandle.call(err); reject(err); }); }); };
2、在请求的拦截中,可以携带用于接口身份验证的token,配置headers请求头、提交参数的序列化等
// 请求头相关配置 axiosinstance.interceptors.request.use( function (config) { const info = ak.utils.getsessionstorage("user_info"); config.headers.common['token'] = info ? info[0].token : ""; // config.headers.common['content-type'] = "application/json"; return config; }, function (error) { return promise.reject(error); } );
3、在响应的拦截中,可以进行根据各种状态码来进行错误的统一处理等
const httpresponsehandle = err => { const opt = err.response; // 请求超时 if (err.code === "econnaborted") { ak.msg.toast("请求超时,请稍后再试", "error"); } if (opt.status === 401) { ak.msg.confirm("用户登录超时,请重新登录", () => { sessionstorage.removeitem("user_info"); window.utryvue.$router.replace("/login"); location.reload(); }); } else { ak.msg.toast(opt.data.message, "error"); } };
4、api接口模块化管理,业务逻辑和数据请求分层,这样可以很方便统一管理我们的接口
如图,把不同的功能拆分,实现代码模块化管理,全部的接口均放在api文件夹下面。index.js是一个api接口的导出的出口,这样就可以把api接口根据功能划分为多个模块,利于多人协作开发,比如一个人只负责一个模块的开发等,还能方便每个模块中接口的命名
index.js:
import report from './report'; // 报表模块 import accountservice from './accountservice'; // 登陆、用户信息相关 // 导出接口 export { accountservice, report }
api请求service层:
// 报表管理请求模块,与后台请求的参数、请求方式、url均看作一个model import http from "@/util/http.js"; const api_context = "sys/"; // 请求的上下文 const report = { async getmenulist() { let model = {}; model.url = api_context + "category/getcategorytree"; model.method = "get"; let res = await http.ajaxservice(model); return res; }, async removemenu(model) { model.data = { ...model }; model.url = api_context + "category/removecategory"; let res = await http.ajaxservice(model); return res; } } export default report;
组件的业务逻辑层调用方式:
// 说明:async、await的写法省去了不少的回调,在有些必须请求两个接口或者两个接口以上场景下,async、await优势就显示出来了 import { reportservice } from "../../store/api/index"; async getmenulist() { const param = { role: "" }; const res = await reportservice.getmenulist(param); // 下面代码返回成功时才执行,错误由上面所讲的axios封装ajaxservice统一处理 this.menulist = res.data; }
5、如果后期维护需要修改的接口,我们就直接在api.js中找到对应的修改就好了,而不用去每一个页面查找我们的接口然后再修改会很麻烦,如果修改的量比较大,难免会自测不充分产生bug,直接gg。还有就是如果直接在我们的业务代码修改接口,一不小心还容易动到我们的业务代码造成不必要的麻烦
6、处理接口域名、端口有多个情况
// 无需前端打包,运维环境快速修改配置,eg: window.sysconfig = { // 运维平台 baseurl: 'http://10.0.33.97:7083/', // 租户平台 tenanturl: 'http://10.0.33.96:7082/' } // 区分不同平台的url地址在http.js文件下的ajaxrequestmodel类实例化会统一处理 this.baseurl = model.baseurl ? window.sysconfig.baseurl : window.sysconfig.tenanturl
第三方库按需加载
按需加载是针对某些第三方库体积比较大的情况下,优化webpack打包后的js体积,减少页面的加载时间
以echart为例子:
优化前:
// 全导入 import * as echarts from "echarts";
webpack打包后:
优化后(主js体积减少了400kb,同时build编译打包速度也得到了减少)
import echarts from "echarts/lib/echarts"; // 依赖注入,目前项目只用到折线图、饼图和柱形图,故只需引入对应的模块即可,tooltip是提示类,title是鼠标悬停显示的对应的图表名称 import 'echarts/lib/chart/bar'; import 'echarts/lib/chart/line'; import 'echarts/lib/chart/pie'; import 'echarts/lib/component/tooltip'; import 'echarts/lib/component/title';
利用less的深度选择器优雅覆盖当前页面ui库组件的样式
vue页面组件的样式基本是写在<style scoped lang="less"></style>中,增加scoped属性的目的让其样式只在当前页面有效。按照这些写的方式,编译后当前标签会加上类似于[data-v-]这样的属性,但是第三方的ui组件库并没有编译为带[data-v-]这样的属性,所以就遇到了当前页面覆盖的样式没生效的情况,有没有方法处理这种问题呢。有些小伙伴可能会想到我在公共样式里面写,额外添加类名来覆盖当前组件的样式,其实,这也不失为一种方案,但是会引来样式全局污染和命名可能重名的情况。下面列举更简单粗暴的方式,同时避免了样式污染和命名冲突的问题:
.menu-tree { /deep/ .el-tree-node__content { height: 32px; } /deep/ .is-current .el-tree-node__content { background-color: #f2f2fa; } }
编译后,默认给menu-tree加上了[data-v-3c93a211]
/deep/深度选择器支持less或者sass,如果你用的是原生的css,可以用<<<符号
webpack实时打包进度
在项目用jenkins自动化打包前端项目的时候,常常会遇到打包速度慢而体验很差,在优化减负依赖包的情况下,同时没有一个测试环境或生产环境当前打包进度捉鸡。这里推荐一个第三方的插件包
progress-bar-webpack-plugin。
// 需安装依赖 npm install progress-bar-webpack-plugin --save-dev const progressbarplugin = require('progress-bar-webpack-plugin') // 在生产环境webpack配置文件的plugin是加上 new progressbarplugin(), // 打包进度
vue组件中写选项的顺序
这里纯属个人观点,可能有些小伙伴用vue开发不是遵从这个。为什么要规定组件的写法顺序呢,或者说它是官方要求的规范,不如说是能让的代码更加优雅,更易于维护,因为你写的代码不仅是你一个人维护。要是一个团队都按这个规范来,大家在维护代码的时候认知一样,那效率就提高了。
组件依赖:
components(自研的子组件或第三方组件)
service(api请求类,其他服务类)
utils(工具类等)
事件传递(vue eventbus)
mixins(复用的属性或方法)
组合:
mixins
组件的属性、接口:
components
props
本地响应式属性、状态:
data
computed
事件注册:
watch
组件生命周期:
created
mounted
destroyed等
组件的方法:
methods
例子:
// 例子 import utrytree from "@/components/utry-tree/utry-tree.vue"; import { reportservice } from "@/store/api/index"; import validation from "../../util/validation"; import eventbus from "@/util/eventbus";
import reportmixins from "@/mixins/reportmixins"; export default { mixins: [], components: { }, props: { menulist: { type: array, default() { return []; } } }, data(){}, computed:{}, watch:{}, mounted(){}, methods:{}, }
路由的懒加载
有时候,针对有些复杂组件,初始化页面其实并不需要把全部组件资源加载进来,把业务复杂的组件抽离出来,从而能减少初始化页面的加载时间
优化前:
import reportmanage from '@/views/reportmanage/index'; import reportpreview from '@/views/reportmanage/reportpreview'; export default [ { path: 'reportmanage/index', name: 'reportmanage', component: reportmanage }, { path: 'reportmanage/reportpreview', name: 'reportpreview', component: reportpreview } ];
初始化页面的加载耗时:
优化后:
import reportmanage from '@/views/reportmanage/index'; export default [ { path: 'reportmanage/index', name: 'reportmanage', component: reportmanage }, { path: 'reportmanage/reportpreview', name: 'reportpreview', component: () => import('@/views/reportmanage/reportpreview'), meta: { keepalive: false } } ];
初始化页面加载耗时:
时间的差别主要是在js的解析上,主要是是因为初始化页面没有加载当前模块的二次组件的js,等到跳转到二次页面再去解析静态资源,总体优化后初始化页面的加载时间快了100多毫秒。
路由模块的拆分化管理
这里的路由拆分,是指按模块拆分成不同的路由文件,针对单页面应用这样更方便团队的多人协调同步开发,自己写的功能模块互不影响。如果当业务需求多起来的时候,它的优势就越能体现出来。我们并不想就在一个router.js写整个工程的路由,这样会是单文件代码量庞大而变得很槽糕,同时也会带来其他同事误改的问题。
我们在router文件夹下面创建router.js作为路由的入口文件,其他以router.js后缀的文件存放着各个模块的路由。
router.js:
import vue from "vue"; import router from "vue-router"; import nprogress from "nprogress"; // 引入nprogress,每次路由变化网页顶端有个加载条效果 import ak from "@/util/ak.js"; // 业务路由 import login from "@/views/index/login"; // 租户平台 import oamlogin from "@/views/index/oamlogin"; // 运维平台 import indexrouter from "./index.router"; // 首页相关 import reportmanage from "./reportmanage.router"; // 报表管理 vue.use(router); // 默认登录 let routes = [ { path: "/", redirect: "login" }, { path: "/login", name: "login", component: login }, { path: "/oamlogin", name: "oamlogin", component: oamlogin } ]; routes = routes.concat( indexrouter, reportmanage ); // router register const router = new router({ routes }); // 路由相关的拦截操作,在这里处理,之前有的router相关操作写在main.js,并不是很友好 router.beforeeach((to, from, next) => { // 每次切换页面时,调用进度条 nprogress.start(); // cache机制 const info = ak.utils.getsessionstorage("user_info"); const token = info ? info[0].token : ""; if (token) { next(); } else { if (to.path === "/oamlogin") { next(); } else if (to.path === "/login") { next(); } else { next("/login"); } } }); router.aftereach(() => { // 在即将进入新的页面组件前,关闭掉进度条 nprogress.done(); });
index.router.js:
import home from '@/views/index/home'; export default [ { path: '/index/home', name: 'home', component: home } ];
这里把首页的路由放在一个数组里,然后导出去,有router.js统一引入,并实例化当前路由
未完待续......
上一篇: InnoDB的锁机制浅析