純正後端的vue輪子筆記

高達達發表於2018-05-18

說明

由於公司需要,作為一個純正的後端工程師,已經自學了半年多的vue了,愣是被逼成了一個小全棧,當然,小全棧這是往好聽了說,事實上就是個前後端深度都不足的小菜雞,在深知自己眾多不足以及明白好記性不如筆頭的道理下,多造輪子多做筆記總是不會錯的:)

所以最近得空我把我剛學vuejs的時候寫的爛工程重構了一下,重構的時候針對性的分模組做了一些筆記如下

  • 路由
  • 狀態管理
  • 許可權管理
  • 控制元件封裝與使用
  • 混入
  • 資料模擬
  • 打包優化與使用者體驗

如果不想拉這麼長可以去 全球最大的同性交友網站 檢視

進入爛筆頭模式

路由

1. 路由載入

// 直接載入頁面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');
複製程式碼

2. 404路由

// 載入一個404頁面import page404 from '@/views/page404';
// 將以下路由配置放置在路由表的最末端,當路徑無法匹配前面的所有路由時將會跳轉至page404元件頁面{
path: '*', component: page404
}複製程式碼

3. 路由攔截

// 路由跳轉前的攔截器router.beforeEach((to, from, next) =>
{

});
// 路由跳轉後的攔截器router.afterEach(to =>
{
});
// 路由跳轉時出現錯誤時的攔截器router.onError((err) =>
{
});
複製程式碼

4. 動態路由

動態路由一般配合頁面級的許可權控制使用

// 通過router.addRoutes方法動態新增可訪問路由router.addRoutes(addRouters)// hack方法 確保addRoutes已完成next({ 
...to, replace: true
}) // set the replace: true so the navigation will not leave a history record 複製程式碼

5. 路由載入時動畫

路由載入時的loading動畫一般配合路由懶載入使用

// 在狀態管理中定義一個路由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>
複製程式碼

狀態管理

1. 小知識

  • state中的資料修改需要通過mutation或action觸發
  • mutation中的方法必須是同步函式
  • action可包含任意非同步操作,可返回一個Promise
  • mutation以及action可以重複,呼叫時將會依次呼叫,getter必須唯一

2. 多模組

業務比較複雜時可使用狀態管理中的多模組,有以下注意事項

  • 除state會根據組合時模組的別名來新增層級,其他的都是合併在根級下,所以在回撥函式獲取的getters、commit、dispatch都是全域性作用的
  • mutation的回撥引數只有state,state為當前模組的狀態樹,下同
  • action的回撥引數為state、rootState、getters、commit、dispatch,如果需要在action中呼叫其他的action可使用dispatch直接呼叫
  • getter的回撥引數為state、rootState、getters
  • 模組間可以通過回撥的rootState進行互動
  • 出現重名的mutation、action將依次觸發
// 多模組的實現 app以及user為各個子模組export default new Vuex.Store({ 
modules: {
app, user
}, getters
})複製程式碼

3. 輔助函式

Vuex除了提供了Store物件以外還對外提供了一些輔助函式

  • mapState、mapGetters將store中的state、getters屬性對映到vue元件區域性的計算屬性中
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' ])
}複製程式碼
  • mapActions、mapMutations將store中的dispatch、commit方法對映到vue元件區域性的方法中
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' ])
}複製程式碼

4. 資料持久化外掛

重新整理頁面時希望狀態不被丟失時可用此外掛

// 摘抄於 https://github.com/robinvdvleuten/vuex-persistedstateimport 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)
}
}) ]
})複製程式碼

5. 日誌外掛

開發環境中希望能夠跟蹤狀態變化並輸出時可用此外掛

// 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
})複製程式碼

許可權管理

1. 需要實現的功能

  • 根據使用者登入後的許可權表生成路由
  • 頁面級的許可權控制
  • dom元素級的許可權控制
  • 登入狀態失效的處理

2. 路由設計

首先我們需要設計路由物件需要有哪些必要引數資訊

為了實現許可權管理我們必須要有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
}]複製程式碼

3. 頁面級的許可權控制

使用路由攔截來實現頁面級的許可權控制

攔截路由跳轉判斷使用者是否登入

從拉取的使用者資訊中提取許可權表通過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');
// 否則全部重定向到登入頁
}
}
});
複製程式碼

4. 元素級的許可權控制

使用自定義指令來實現元素級的許可權控制

在被繫結元素插入父節點時驗證使用者是否包含該元素的所需許可權

根據鑑權結果來決定是否移除該元素

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函式

1. 如何封裝一個支援render渲染的元件

  • 首先建立一個函式式元件
// 表格擴充函式式元件的實現// see https://github.com/calebman/vue-DBM/blob/master/src/components/table/expand.jsexport 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.vueimport expand from "./expand.js";
<
span v-if="typeof col.render ==='function'">
<
expand :field="col.field" :row="item" :render="col.render" :index="rowIndex">
<
/expand>
<
/span>
複製程式碼
  • 使用render函式渲染
// 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
}
}) ])複製程式碼

混入

1. 小知識

  • 混入物件將享有被混入元件的生命週期
  • 資料物件混入衝突時將以元件資料優先
  • 物件選項(如methods、components、directives)混入衝突時取元件物件的鍵值對
  • 同名鉤子混合為陣列,混入物件的鉤子將在元件自身鉤子之前呼叫

2. 應用場景

  • 希望部分路由頁面在離開時銷燬但是不希望每個路由頁面都定義區域性路由時
// 定義混入物件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         # 滑鼠移動時的樣式改變複製程式碼

資料模擬

1. 需要實現的功能

  • 攔截Ajax請求並延時響應
  • 返回的統一的資料格式
  • 響應不同的模擬資料

2. 配置Mockjs攔截Ajax請求

// see https://github.com/calebman/vue-DBM/blob/master/src/mock/index.js// 引入Mockjsimport 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);
複製程式碼

3. 響應的統一資料格式

// 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
}
}
}複製程式碼

4. 配置響應邏輯

// see https://github.com/calebman/vue-DBM/blob/master/src/mock/login.jsimport { 
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()
}複製程式碼

5. 模擬隨機資料

// see https://github.com/nuysoft/Mock/wikiimport 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;

}複製程式碼

打包優化

1. 做哪部分的優化

  • cdn優化
  • 路由懶載入
  • 其他優化
  • 使用者體驗

2. cdn優化

類似於vue、vue-router、moment、element-ui等提供了cdn的架或者工具類可在index.html中直接引入,然後配置webpack的externals使其不加入打包配置,從而減小app.js、vendor.js的體積

  • 在index.html使用cdn引入依賴庫
<
!-- 網路請求工具類 -->
<
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>
複製程式碼
  • 配置build資料夾下webpack.base.conf.js檔案
module.exports = { 
// ... externals: {
'axios': 'axios', 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'moment': 'moment', 'element-ui': 'ELEMENT'
}
}複製程式碼

3. 路由懶載入

路由懶載入能夠將程式碼根據路由配置進行分割,加快首屏渲染的速度,在大型的單頁應用中是必不可少的

參見路由管理的實現

5. 其他優化

  • 儘量少的註冊全域性元件,使用UI框架可以參考文件做按需載入
  • 可以和服務端配合採用gzip壓縮,減少傳輸耗時
  • 在更新不是很頻繁的應用可考慮提高快取時間
  • 例如moment、lodash這種龐大的工具庫在使用的功能不多的情況下可考慮尋找替代品

6. 使用者體驗

一個單頁應用到了一定規模不管怎麼優化首屏渲染還是一個比較慢的過程,此時可以考慮在首屏渲染時使用一個載入動畫告訴使用者系統正在初始化

  • 首先在index.html中定義一個渲染動畫
<
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>
複製程式碼
  • 然後在App.vue元件的mounted鉤子中移除這個loading
export default { 
// ... mounted() {
document.body.removeChild(document.getElementById("system-loading"));

}
};
複製程式碼

來源:https://juejin.im/post/5afe72eb6fb9a07ab379b025

相關文章