这篇文章介绍下,我在vue日常使用中积累的一些最佳实践,涉及到代码规范、鉴权、组件按需引入、获取同目录下文件 axios封装等方面的最佳实践 如有不足敬请补充。请耐心阅读,一定会有所收获。
1. 代码规范篇
1.1 组件目录结构划分及文件命名规范
collapse
---- collapse.vue
---- collapse-item.vue
统一使用kebab-case进行命名
1.2 组件命名规范
使用PascalCase 进行声明约定, 在html中遵循 kebab-case 的使用约定。
components: {
ElForm: { /* ... */ }
}
<pascal-cased-component></pascal-cased-component>
1.3 组件配置项与事件命名
props: {
labelWidth: {
type: String,
default: ()=>""
}
},
methods: {
click() {
this.$emit("handle-click")
}
}
<el-form label-width="160px" @handle-click="fn"></el-form>
1.4 保持合理的书写顺序
参考示例
export default {
name: 'BaseButton',
components: [],
props: {
},
computed: {
},
data() {
return {
}
},
mounted() {
}
watch: {
},
methods: {
}
}
一般methods代码最多,把methods下置有利于代码阅读
1.5 配置eslint
安装插件
eslint --save-dev
babel-eslint --save-dev
eslint-friendly-formatter --save-dev
eslint-loader --save-dev
eslint-plugin-vue --save-dev
添加校验规则文件 .eslintrc.js
eslint --init
添加忽略校验文件 .eslintignore
/build/
/config/
/cmas/
/node_modules/
/src/utils/
配置webpack rules
rules: [
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'per',
include: [resolve('src')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: true
}
}
]
配置 webpack自动修复
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
2. 路由模块化
- 常量路由表
- 异步路由表
3. UI组件按需加载
// 安装element 插件
yarn add element
const components = {Button, Header, Footer,Main, Container,Row, Col}
Object.values(components).forEach(([key,component])=> {
Vue.use(component)
})
Vue.prototype.$message = Message
4. 封装axios
// utils/request.js
import axios from "axios";
import { baseURL } from "@/config";
class Http {
constructor(baseUrl) { }
setInterceptor(instance) {
instance.interceptors.request.use((config) => {
// 一般增加一些token
return config;
});
instance.interceptors.response.use(
(res) => {
if (res.status == 200) {
if(res.data.err===0) {
return Promise.resolve(res.data);
}else {
return Promise.regect(res.data.data)
}
} else {
return Promise.reject(res.data.data);
}
},
(err) => {
switch (err.response.status) {
case 401:
break;
default:
break;
}
return Promise.reject(err);
}
);
}
mergeOptions(options) {
return {
baseURL: this.baseURL,
timeout: this.timeout,
...options,
};
}
request(options) {
const instance = axios.create({ baseURL, timeout: 3000 });
const opts = this.mergeOptions(options);
this.setInterceptor(instance);
return instance(opts);
}
get(url, config = {}) {
return this.request({
method: "get",
url: url,
...config,
});
}
post(url, data) {
return this.request({
method: "post",
url,
data,
});
}
}
export default new Http();
5. vuex模块化
store
--index.js
--mutation-types.js
--modules
----common.js
----user.js
----note.js
----notebook.js
----trash.js
- mutation-types.js
export const SET_SLIDERS = 'SET_SLIDERS'
- index.js
import Vue from 'vue'
import Vuex from 'vuex'
const files = require.context('./modules',false,/\.js$/)
const modules = {}
files.keys().forEach(key=> {
let store = files(key).default
const moduleName = path.replace(/^\.\/(\w+)\.js$/,(str,$1)=> $1)
modules[moduleName] = store
})
Vue.use(Vuex)
export default new Vuex.Store({
modules
})
// common.js
import {getSilder} from '../api/public'
import * as types from './mutation-types'
export default {
state: {
sliders: []
},
gettters: {
},
mutations: {
[type.SET_SLIDERS](state,payload) {
state.sliders = payload
}
},
actions: {
async getSliders({commit}) {
const { data } = await getSlider()
commit(type.SET_SLIDERS,data)
}
},
}
6. 封装apis
api
---config // 接口地址抽离
---------common.js
---------user.js
---------article.js
user.js
article.js
//config/common.js
export default {
getSlider: '/public/getSlider' // 轮播图地址
}
// common.js
import config from './config/public'
import axios from '@/utils/request'
// 获取轮播图,返回一个promise
export const getSlider = ()=> axios.get(config.getSlider)
7. 封装localStage
export default {
//存储
setLocal(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
//取出数据
getLocal(key) {
return JSON.parse(localStorage.getItem(key));
},
// 删除数据
removeLocal(key) {
localStorage.removeItem(key);
}
}
8. 登陆权限
- mutation-types.js
export const SET_SLIDERS = 'SET_SLIDERS'
+ export const SET_USERINFO = 'SET_USERINFO'
+ export const SET_PERMISSION = 'SET_PERMISSION'
- store/user.js
用户登陆成功后设置userInfo, hasPermission, 本地存储token
import * as types from './mutation-types.js'
import {setLocal,geLocal,removeLocal} from '@/utils/localStorage'
export default {
state: {
userInfo: {},
hasPermission: false // 是否获取到用户权限
},
mutations: {
[type.SET_USERINFO](state,userInfo) {
state.userInfo = userInfo
if(userInfo && userInfo.token) {
setLocal('token',userInfo.token)
}else {
removeLocal('token')
}
},
[type.SET_PERMISSION](state,hasPermission) {
state.hasPermission = hasPermission
},
actions: {
// 用户登陆
async login({commit},payload) {
const result = await login(payload)
commit(types.SET_USERINFO,result.data)
commit(type.SET_PERMISSION,true)
}
// 检测用户是否登陆
async validate({commit},payload) {
if(!getLocal('token')) return false
try {
let result = await user.validate()
commit(types.SET_USERINFO,result.data)
commit(type.SET_PERMISSION,true)
return true
}catch(e) {
commit(types.SET_USERINFO,{})
commit(type.SET_PERMISSION,false)
return false
}
}
}
}
}
- request配置请求头
instance.interceptors.request.use(config=> {
config.headers.authorization = 'Bearer '+ getLocal('token')
return config
})
- 路由切换鉴权
import store from '../store'
import * as types from '../store/mutation-types.js'
const loginPermission = function (to,from,next) {
let isValidated = await store.dispatch(types.USER_VALIDATE)
let needLogin = to.matched.some(item=>item.meta.needLogin) // a/b 只要有一个就需要校验
if(!isValidated) { //没登陆
if(needLogin) { // 需要登陆
if(isValidated) {
next()
}else {
next('/login')
}
}else {
next()
}
}else {// 登陆过
if(to.path==='/login'){
next('/')
}else {
next()
}
}
}
router.beforeEach(loginPermission)
9. 鉴权
9.1 路由权限
路由权限校验是中后台系统用户权限校验的重要组成部分,目前通用的方法有两种
- 根据用户权限判断是否能够进行某个路由
- 直接根据用户权限生成专属路由表
本文采取第二种方式,有后端返回用户权限列表,根据用户权限和路由配置,生成专属路由表
- mutation_types.js
export const SET_ROUTER_PERMISSION = 'SET_ROUTER_PERMISSION'
- router/async_router_map.js
// 需要异步添加的路由表
- store/user.js
import asyncRouterMap from '@/router/async_router_map.js'
const filterRouter = (authList)=> {
function filter(routes) {
let result = asyncRouterMap.filter(route=> {
if(auths.includes(route.meta.auth)) {
if(route.children) {
route.children = filter(route.children)
}
return route
}
})
return result
}
let auths = authList.map(item=> item.auth)
return filter(asyncRouterMap)
}
state: {
hasAddRoute: false
},
mutations: {
[type.SET_ROUTER_PERMISSION](state,hasAddRoute) {
state.hasAddRoute = hasAddRoute
}
},
actions: {
addRoute({commit,state}) {
let authList = state.userInfo.authList
if(authList) {
let routes = filterRouter(authList)
routes.forEach(route=> {
router.addRoute('manage',route )
})
commit(types.SET_ROUTER_PERMISSIONI,true)
}
}
}
- router.js
export const menuPermission = async function(to, from, next) {
if (store.state.user.hasPermission) {
if (!store.state.user.menuPermission) {
store.dispatch('addRoute');
next({...to,replace:true}); // hack处理,处理组件异步加载
} else {
next();
}
} else {
next();
}
}
router.beforeEach(menuPermission)
9.2 菜单权限
- myMenu.js
const {mapState} from '@/store'
export default {
computed: {
...mapState(['userInfo'])
},
mounted() {
this.list = this.getMenuList(this.userInfo.authList)
},
methods: {
getMenList(authList) {
let menu = [];
let sourceMap = {};
authList.forEach(m => {
m.children = [];
sourceMap[m.id] = m;
if (m.pid === -1) {
menu.push(m);
} else {
sourceMap[m.pid] && sourceMap[m.pid].children.push(m);
}
});
return menu;
}
},
render() {
let renderChildren = (data) => {
return data.map(child => {
return child.children.length ?
<el-submenu index={child._id}>
<div slot="title">{child.name}</div>
{renderChildren(child.children)}
</el-submenu> :
<el-menu-item index={child.path}>{child.name}</el-menu-item>
})
}
return <el-menu
background-color="#333"
text-color="#fff"
default-active={this.$route.path}
router={true}
>
{renderChildren(this.menuList)}
</el-menu>
}
}
10. 特殊情况补充
10.1 如何处理特殊接口地址
假如某一个接口和我们的的baseUrl不同,在书写路径时如何处理
// 写成绝对路径,axios将不会默认添加baseUrl
const prefix = 'http://www.xxx.com:4000'
export default {
path: prefix+'/user/reg'
}
10.2 v-if/v-if-else/v-else中使用key
如果一组v-if/v-else 的元素类型相同,最好使用属性key(比如两个div)
<div
v-if="error"
key="search-status"
>
错误: {{error}}
</div>
<div
v-else
key="search-results"
>
{{results}}
</div>
10.3 路由切换组件不变
const routes = [
{
path: '/detail/:id',
name: 'detail',
component: Detail
}
]
当我们从路由/detail/1 切换到/detail/2时,组件不会发生任何变化。
beforeRouteUpdate(to,from,next) {
if(to.fullPath!=from.fullPath){
next()
this.changeUser()
}
}
10.4 避免v-if和v-for一起使用
Vue.js官方强烈建议不要把v-if和v-for同时用在同一个元素上,通常有以下两种场景
- 如果是为了过滤列表中的子项,使用计算属性对数据进行过滤
- 如果是为了避免渲染整个列表,可以把v-if转移到template容器组件上
之所以这样做是因为v-for具有更高的优先级,则每一个子项都要重新计算
10.5 避免滥用vuex管理数据以及请求接口
许多新手在使用vuex后会把所有的数据和逻辑都存储在vuex, 使本来简单的处理变得复杂化。针对这种情况可以从以下三个边界进行判断
- 数据是否被多处引用
- 逻辑是否可以多处复用
- 对于展示数据是否需要缓存
10.6 使用Object.freeze 冻结不需要响应话的数据
- 在data或vuex里我们可以使用freeze冻结对象,对于纯展示的大数据,都可以使用Object.freeze提升性能
- Object.freeze()冻结的是值,你仍然可以将变量的引用替换掉
new Vue({
data: {
// vue不会对list里的object做getter、setter绑定
list: Object.freeze([
{ value: 1 },
{ value: 2 }
])
},
created () {
// 界面不会有响应
this.list[0].value = 100;
// 下面两种做法,界面都会响应
this.list = [
{ value: 100 },
{ value: 200 }
];
this.list = Object.freeze([
{ value: 100 },
{ value: 200 }
]);
}
})