純正後端的vue輪子筆記

說明

因爲公司須要,做爲一個純正的後端工程師,已經自學了半年多的vue了,愣是被逼成了一個小全棧,固然,小全棧這是往好聽了說,事實上就是個先後端深度都不足的小菜雞,在深知本身衆多不足以及明白好記性不如筆頭的道理下,多造輪子多作筆記老是不會錯的:)javascript

因此最近得空我把我剛學vuejs的時候寫的爛工程重構了一下,重構的時候針對性的分模塊作了一些筆記以下css

  • 路由
  • 狀態管理
  • 權限管理
  • 控件封裝與使用
  • 混入
  • 數據模擬
  • 打包優化與用戶體驗

若是不想拉這麼長能夠去 全球最大的同性交友網站 查看html

進入爛筆頭模式

路由

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. 動態路由

動態路由通常配合頁面級的權限控制使用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 
複製代碼

5. 路由加載時動畫

路由加載時的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> 複製代碼

狀態管理

1. 小知識

  • state中的數據修改須要經過mutation或action觸發
  • mutation中的方法必須是同步函數
  • action可包含任意異步操做,可返回一個Promise
  • mutation以及action能夠重複,調用時將會依次調用,getter必須惟一

2. 多模塊

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

  • 除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對象之外還對外提供了一些輔助函數webpack

  • 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. 數據持久化插件

刷新頁面時但願狀態不被丟失時可用此插件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)
      }
    })
  ]
})
複製代碼

5. 日誌插件

開發環境中但願可以跟蹤狀態變化並輸出時可用此插件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
})

複製代碼

權限管理

1. 須要實現的功能

  • 根據用戶登陸後的權限表生成路由
  • 頁面級的權限控制
  • dom元素級的權限控制
  • 登陸狀態失效的處理

2. 路由設計

首先咱們須要設計路由對象須要有哪些必要參數信息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 }
]
複製代碼

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.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>
複製代碼
  • 使用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
// 引入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);
複製代碼

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.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()
}
複製代碼

5. 模擬隨機數據

// 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;
}
複製代碼

打包優化

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"));
  }
};
複製代碼
相關文章
相關標籤/搜索