vue最佳实践

412 阅读3分钟

这篇文章介绍下,我在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 }
             ]);
    }
})