因爲公司須要,我做爲一個純正的後端工程師,已經自學了半年多的vue了,愣是被逼成了一個小全棧,固然,小全棧這是往好聽了說,事實上就是個先後端深度都不足的小菜雞,在深知本身衆多不足以及明白好記性不如爛筆頭的道理下,多造輪子多作筆記老是不會錯的:)javascript
因此最近得空我把我剛學vuejs的時候寫的爛工程重構了一下,重構的時候針對性的分模塊作了一些筆記以下css
若是不想拉這麼長能夠去 全球最大的同性交友網站 查看html
路由
// 直接加載頁面
import page from '@/views/page';
// 懶加載頁面
() => import('@/views/page');
// 指定打包名稱的懶加載,可將多個頁面打包成一個js進行加載
() => import(/* webpackChunkName: "group-page" */'@/views/page1');
() => import(/* webpackChunkName: "group-page" */'@/views/page2');
() => import(/* webpackChunkName: "group-page" */'@/views/page3');
複製代碼
// 加載一個404頁面
import page404 from '@/views/page404';
// 將如下路由配置放置在路由表的最末端,當路徑沒法匹配前面的全部路由時將會跳轉至page404組件頁面
{ path: '*', component: page404}
複製代碼
// 路由跳轉前的攔截器
router.beforeEach((to, from, next) => {
});
// 路由跳轉後的攔截器
router.afterEach(to => {
});
// 路由跳轉時出現錯誤時的攔截器
router.onError((err) => {
});
複製代碼
動態路由通常配合頁面級的權限控制使用vue
// 經過router.addRoutes方法動態添加可訪問路由
router.addRoutes(addRouters)
// hack方法 確保addRoutes已完成
next({ ...to, replace: true }) // set the replace: true so the navigation will not leave a history record
複製代碼
路由加載時的loading動畫通常配合路由懶加載使用java
// 在狀態管理中定義一個路由loading標誌
const app = {
state: {
routerLoading: false, //路由的loading過渡
},
mutations: {
//修改路由loading狀態
UPDATE_ROUTER_LOADING(state, status) {
state.routerLoading = status
}
}
}
// 在路由攔截器中修改loading狀態
router.beforeEach((to, from, next) => {
store.commit('UPDATE_ROUTER_LOADING', true); // 展現路由加載時動畫
});
router.afterEach(to => {
store.commit('UPDATE_ROUTER_LOADING', false);
});
router.onError(err => {
console.error(err); // for bug
store.commit('UPDATE_ROUTER_LOADING', false);
});
// 在router-view定義loading動畫
// element-ui提供了v-loading指令能夠直接使用
<router-view v-loading="$store.getters.routerLoading"></router-view> 複製代碼
狀態管理
業務比較複雜時可以使用狀態管理中的多模塊,有如下注意事項node
// 多模塊的實現 app以及user爲各個子模塊
export default new Vuex.Store({
modules: {
app,
user
},
getters
})
複製代碼
Vuex除了提供了Store對象之外還對外提供了一些輔助函數webpack
import { mapState } from 'vuex'
computed: mapState([
// 映射 this.name 到 this.$store.state.name
'name'
])
import { mapGetters } from 'vuex'
computed: {
// 映射 this.name 到 this.$store.getters.name
...mapGetters([ 'name' ])
}
複製代碼
import { mapActions } from 'vuex'
methods: {
// 映射 this.LoginByUsername() 到 this.$store.dispatch('LoginByUsername')
...mapActions([ 'LoginByUsername' ]),
// 映射 this.login() to this.$store.dispatch('LoginByUsername')
...mapActions({ login: 'LoginByUsername'})
}
import { mapMutations } from 'vuex'
methods: {
// 映射 this.SET_NAME() 到 this.$store.commit('SET_NAME') ])
...mapMutations([ 'SET_NAME' ]) ,
// 映射 this.setName() 到 this.$store.commit('SET_NAME') })
...mapMutations({ setName: 'SET_NAME' ])
}
複製代碼
刷新頁面時但願狀態不被丟失時可用此插件ios
// 摘抄於 https://github.com/robinvdvleuten/vuex-persistedstate
import createPersistedState from 'vuex-persistedstate'
import * as Cookies from 'js-cookie'
const store = new Store({
// ...
plugins: [
createPersistedState({
storage: {
getItem: key =* Cookies.get(key),
// Please see https://github.com/js-cookie/js-cookie#json, on how to handle JSON.
setItem: (key, value) =* Cookies.set(key, value, { expires: 3, secure: true }),
removeItem: key =* Cookies.remove(key)
}
})
]
})
複製代碼
開發環境中但願可以跟蹤狀態變化並輸出時可用此插件git
// createLogger是vuex中的內置插件
import createLogger from 'vuex/dist/logger'
let vuexPlugins = [];
if(process.env.NODE_ENV !== 'production'){ // 開發環境加載該插件
vuexPlugins.push(createLogger);
}
const store = new Store({
// ...
plugins: vuexPlugins
})
複製代碼
權限管理
首先咱們須要設計路由對象須要有哪些必要參數信息github
爲了實現權限管理咱們必需要有roles參數表明該路由必須擁有哪些權限才能訪問
爲了更好的展現路由在這裏設計了title、icon兩個參數用於側邊欄的菜單展現
而有些路由不須要在側邊欄展現,這裏使用hidden參數來告訴程序哪些路由是不須要展現的
// 首先設計路由對象參數
/** * hidden: true 若是hidden爲true則在左側菜單欄展現,默認爲false * name:'router-name' 路由名稱,路由惟一標識 * meta : { roles: ['admin','editor'] 權限列表,用於頁面級的權限控制,默認不設置表明任何權限都可訪問 title: 'title' 對應路由在左側菜單欄的標題名稱 icon: 'icon-class' 對應路由在左側菜單欄的圖標樣式 } **/
複製代碼
接下來咱們須要實現路由的動態加載
系統初始化時加載必要路由,以後根據登陸用戶的權限加載符合條件的路由
// 定義系統初始化時加載的必要路由信息
export const constantRouterMap = [
{ path: '/login', name: 'login', meta: { title: "系統登陸", hidden: true }, component: login },
{ path: "/404", name: "page404", meta: { title: "頁面走丟了", hidden: true }, component: page404 },
{ path: "/401", name: "page401", meta: { title: "權限不足", hidden: true }, component: page401 }
]
// 定義佈局頁面
const layout = () => import(/* webpackChunkName: "group-index" */ '@/views/layout');
// 定義異步加載的路由信息
export const asyncRouterMap = [
{
path: '/',
name: 'main',
redirect: '/dashboard',
hidden: true,
component: layout,
children: [
{ path: 'dashboard', name: 'dashboard', meta: { title: "儀表盤" }, component: () => import(/* webpackChunkName: "group-index" */'@/views/dashboard') }
]
},
{
path: '/permission',
name: 'permission',
meta: { title: "權限頁", icon: "dbm d-icon-quanxian" },
redirect: '/permission/adminpermission',
component: layout,
children: [
{ path: "adminpermission", name: "adminPermission", meta: { title: "管理員權限頁", roles: ["admin"] }, component: () => import('@/views/permission/admin') },
{ path: "watcherpermission", name: "watcherPermission", meta: { title: "遊客權限頁", roles: ["admin", "watcher"] }, component: () => import('@/views/permission/watcher') },
{ path: "elementpermission", name: "elementPermission", meta: { title: "元素級別權限" }, component: () => import('@/views/permission/element') }
]
},
{ path: '*', redirect: '/404', hidden: true }
]
複製代碼
使用路由攔截來實現頁面級的權限控制
攔截路由跳轉判斷用戶是否登陸
從拉取的用戶信息中提取權限表經過addRoutes方法動態加載異步路由表
每次路由跳轉時判斷用戶是否擁有該路由的訪問權限實現動態權限匹配
// 定義免登白名單
const whiteList = ['/login', '/404', '/401'];
// 攔截路由跳轉
router.beforeEach((to, from, next) => {
store.commit('UPDATE_ROUTER_LOADING', true); // 展現路由加載時動畫
if (getToken()) { // 存在token
if (to.path === '/login') {
next({ path: '/' })
} else {
if (store.getters.roles.length === 0) { // 判斷當前用戶是否已拉取完用戶信息
store.dispatch('GetUserInfo').then(data => { // 拉取用戶信息
const roles = data.roles // 權限表必須爲數組,例如: ['admin','editer']
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根據roles權限生成可訪問的路由表
router.addRoutes(store.getters.addRouters) // 動態添加可訪問路由表
next({ ...to, replace: true }) // hack方法 確保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch(err => { // 拉取用戶信息失敗,提示登陸狀態失效
store.dispatch('FedLogOut').then(() => {
Message.error('登陸狀態失效, 請從新登陸');
next({ path: '/login' });
})
})
} else {
if (hasPermission(store.getters.roles, to.meta.roles)) { // 動態權限匹配
next();
} else {
next({ path: '/401', replace: true, query: { noGoBack: true } });
}
}
}
} else { // 沒有token
if (whiteList.indexOf(to.path) !== -1) { // 在免登陸白名單,直接進入
next();
} else {
next('/login'); // 不然所有重定向到登陸頁
}
}
});
複製代碼
使用自定義指令來實現元素級的權限控制
在被綁定元素插入父節點時驗證用戶是否包含該元素的所需權限
根據鑑權結果來決定是否移除該元素
import store from '@/store'
export default {
inserted(el, binding, vnode) {
const { value } = binding; // 獲取自定義指令傳入的鑑權信息
const roles = store.getters && store.getters.roles; // 從狀態管理中獲取當前用戶的路由信息
if (value && value instanceof Array && value.length > 0) {
const permissionRoles = value;
const hasPermission = roles.some(role => { // 判斷用戶是否包含該元素所需權限
return permissionRoles.includes(role);
})
if (!hasPermission) { // 權限不足
el.parentNode && el.parentNode.removeChild(el); // 移除該dom元素
}
} else {
throw new Error(`必需要有權限寫入,例如['admin']`)
}
}
}
// 在vue組件上使用它
// 引入並註冊permission指令
import permission from "@/directive/permission/index.js";
export default {
directives: {
permission
}
}
// 使用permission指令
<el-button v-permission="['admin']">admin 可見</el-button>
<el-button v-permission="['admin','watcher']">watcher 可見</el-button>
複製代碼
render函數
// 表格拓展函數式組件的實現
// see https://github.com/calebman/vue-DBM/blob/master/src/components/table/expand.js
export default {
name: 'TableExpand',
functional: true, // 標記組件爲 functional,這意味它是無狀態 (沒有響應式數據),無實例 (沒有 this 上下文)。
props: {
row: Object, // 當前行對象
field: String, // 列名稱
index: Number, // 行號
render: Function // 渲染函數
},
render: (h, ctx) => { // 提供ctx做爲上下文
const params = {
row: ctx.props.row,
field: ctx.props.field,
index: ctx.props.index
};
return ctx.props.render(h, params);
}
};
複製代碼
// see https://github.com/calebman/vue-DBM/blob/master/src/components/table/table.vue
import expand from "./expand.js";
<span v-if="typeof col.render ==='function'"> <expand :field="col.field" :row="item" :render="col.render" :index="rowIndex"></expand> </span>
複製代碼
// see https://github.com/calebman/vue-DBM/blob/master/src/views/demo/datatable/data-table.vue
// 引入自定義組件
import IndexColumn from "@/components/business/index-column.vue";
// 註冊
components: {
// ...
IndexColumn
}
// 使用
// 獲取當前組件的上下文
let self = this;
// 定義渲染函數
render: (h, params) =>
h("div", [
h(IndexColumn, {
props: {
field: params.field,
index: params.index,
pagingIndex:
(self.pagination.pageCurrent - 1) * self.pagination.pageSize
},
on: { "on-value-delete": self.deleteRow }
})
])
複製代碼
混入
// 定義混入對象
export default {
beforeRouteLeave(to, from, next) {
if (to.meta && to.meta.destroy) {
this.$destroy();
}
next();
}
}
// 混入須要此功能的組件頁面
import routeLeaveDestoryMixin from "routeleave-destory-mixin";
export default {
// ...
mixins: [routeLeaveDestoryMixin]
}
複製代碼
// see https://github.com/calebman/vue-DBM/blob/master/src/components/business/render-column-mixin.js
// 定義混入對象
export default {
// ...
computed: {
// 是否選中此單元格
inSelect() {
if (this.cellClickData.index == this.index &&
this.cellClickData.field == this.field) {
this.focus();
return true;
}
}
},
methods: {
// 獲取焦點
focus() {
let self = this;
setTimeout(function () {
if (self.$refs["rendercolumn"]) {
self.$refs["rendercolumn"].focus();
}
}, 100);
},
// 失去焦點
blur() {
if (this.v != this.value) {
this.$emit("on-value-change", this.field, this.index, this.v);
}
this.$emit("on-value-cancel", this.field, this.index);
},
// 數據修改
changeValue(val) {
this.$emit("on-value-change", this.field, this.index, val);
this.$emit("on-value-cancel", this.field, this.index);
}
},
watch: {
// 監聽父組件數據變化
value(val) {
this.v = val;
}
}
}
// 文本列
// see https://github.com/calebman/vue-DBM/blob/master/src/components/business/text-column.vue
<template>
<div> <input v-show="inSelect" ref="rendercolumn" @blur="blur" @keyup="enter($event)" v-model="v" /> <span v-show="!inSelect" class="cell-text">{{v}}</span> </div> </template>
// 時間列
// see https://github.com/calebman/vue-DBM/blob/master/src/components/business/datetime-column.vue
<template>
<div> <el-date-picker v-show="inSelect" ref="rendercolumn" v-model="v" type="datetime" @change="changeValue" @blur="blur"></el-date-picker> <span v-show="!inSelect">{{coverValue}}</span> </div> </template>
複製代碼
# see https://github.com/calebman/vue-DBM/tree/master/src/components/table
├─table
│ cell-edit-mixin.js # 單元格編輯
│ classes-mixin.js # 表格樣式
│ scroll-bar-control-mixin.js # 表格滾動
│ table-empty-mixin.js # 無數據時的處理
│ table-resize-mixin.js # 表格的自適應
│ table-row-mouse-events-mixin.js # 鼠標移動時的樣式改變
複製代碼
數據模擬
// see https://github.com/calebman/vue-DBM/blob/master/src/mock/index.js
// 引入Mockjs
import Mock from 'mockjs';
// 配置延時
Mock.setup({
timeout: '300-1000'
});
// 配置攔截
Mock.mock(/\/user\/login/, 'post', loginAPI.loginByUsername);
Mock.mock(/\/user\/logout/, 'post', loginAPI.logout);
Mock.mock(/\/user\/info\.*/, 'get', loginAPI.getUserInfo);
複製代碼
// see https://github.com/calebman/vue-DBM/blob/master/src/mock/response.js
/** * 統一響應工具類 * 響應統一格式的數據 * response : { * errCode: 00 響應結果碼 * errMsg: 0000000(成功) 響應詳細結果碼 * data: null 具體數據 * } */
export default {
// 成功
success: data => {
return {
errCode: '00',
errMsg: '0000000(成功)',
data: data ? data : null
}
},
// 失敗
fail: (errCode, errMsg) => {
return {
errCode: errCode ? errCode : '04',
errMsg: errMsg ? errMsg : '0401001(未知錯誤)',
data: null
}
},
// 權限不足
unauthorized: () => {
return {
errCode: '43',
errMsg: '4300001(無權訪問)',
data: null
}
}
}
複製代碼
// see https://github.com/calebman/vue-DBM/blob/master/src/mock/login.js
import { param2Obj } from '@/utils';
import Response from './response';
const userMap = {
admin: {
password: 'admin',
roles: ['admin'],
token: 'admin',
introduction: '我是超級管理員',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
watcher: {
password: 'watcher',
roles: ['watcher'],
token: 'watcher',
introduction: '我是遊客',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Watcher'
}
}
export default {
// 使用用戶名登陸
loginByUsername: config => {
const { username, password } = JSON.parse(config.body);
if (userMap[username] && userMap[username].password === password) {
return Response.success(userMap[username]);
} else {
return Response.fail("01", "0101001(用戶名或密碼錯誤)")
}
},
// 拉取用戶信息
getUserInfo: config => {
const { token } = param2Obj(config.url);
if (userMap[token]) {
return Response.success(userMap[token]);
} else {
return Response.fail();
}
},
// 註銷
logout: () => Response.success()
}
複製代碼
// see https://github.com/nuysoft/Mock/wiki
import Mock from 'mockjs';
// 隨機字符串
function mockStr() {
let result = Mock.mock({ 'str': '@name' });
return result.str;
}
// 隨機數字
function mockNumber(min, max) {
let key = 'num|' + min + '-' + max;
let param = {}
param[key] = 100;
return Mock.mock(param).num;
}
// 隨機小數,最高小數點後三位
function mockDecimal() {
return Mock.Random.float(1, 100, 1, 3)
}
// 隨機數組一項
const arr = ["image2.jpeg", "image3.jpeg", "image4.jpeg", "image5.jpeg", "image6.jpeg"];
function mockOneFileAddress() {
return Mock.mock({ 'oneFile|1': arr }).oneFile;
}
// 隨機日期
function mockDate() {
let mockDateStr = Mock.Random.datetime('yyyy-MM-dd HH:mm:ss');
// 在這裏使用了momentjs將其解析爲Date類型
let mockDate = moment(mockDateStr, 'YYYY-MM-DD HH:mm:ss').toDate();
return mockDate;
}
複製代碼
打包優化
相似於vue、vue-router、moment、element-ui等提供了cdn的架或者工具類可在index.html中直接引入,而後配置webpack的externals使其不加入打包配置,從而減少app.js、vendor.js的體積
<!-- 網絡請求工具類 -->
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<!-- vue -->
<script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script>
<!-- vue-router -->
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
<!-- vuex -->
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
<!-- momentjs的中文包 -->
<script src="https://cdn.bootcss.com/moment.js/2.22.1/moment-with-locales.min.js"></script>
<!-- momentjs -->
<script src="https://cdn.bootcss.com/moment.js/2.22.1/locale/zh-cn.js"></script>
<!-- element-ui樣式 -->
<script src="https://cdn.bootcss.com/element-ui/2.3.6/theme-default/index.css"></script>
<!-- element-ui -->
<script src="https://cdn.bootcss.com/element-ui/2.3.6/index.js"></script>
複製代碼
module.exports = {
// ...
externals: {
'axios': 'axios',
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'moment': 'moment',
'element-ui': 'ELEMENT'
}
}
複製代碼
路由懶加載可以將代碼根據路由配置進行分割,加快首屏渲染的速度,在大型的單頁應用中是必不可少的
參見路由管理的實現
一個單頁應用到了必定規模無論怎麼優化首屏渲染仍是一個比較慢的過程,此時能夠考慮在首屏渲染時使用一個加載動畫告訴用戶系統正在初始化
<body>
<div id="app"></div>
<!-- 首屏渲染時的加載動畫 -->
<div id="system-loading" class="showbox">
<div class="loader">
<svg class="circular" viewBox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10" />
</svg>
</div>
<div class="text">
<span>系統初始化中...</span>
</div>
</div>
<!-- built files will be auto injected -->
</body>
複製代碼
export default {
// ...
mounted() {
document.body.removeChild(document.getElementById("system-loading"));
}
};
複製代碼