加快Vue專案的開發速度

MarkMan發表於2018-12-12

現如今的開發,比如是內部使用的管理平臺這種專案大都時間比較倉倉促。實際上來說在使用了webpack + vue 這一套來開發的話已經大大了提高了效率。但是對於我們的開發層面。還是有很多地方可以再次提高我們的專案開發效率,讓我們更加專注於業務,畢竟時間就是生命。下面我們挨個來探討。

巧用Webpack

Webpack是實現我們前端專案工程化的基礎,但其實她的用處遠不僅僅如此,我們可以通過Webpack來幫我們做一些自動化的事情。首先我們要了解require.context()這個API

require.context()

您可以使用require.context()函式建立自己的上下文。 它允許您傳入一個目錄進行搜尋,一個標誌指示是否應該搜尋子目錄,還有一個正規表示式來匹配檔案。

其實是Webpack通過解析 require() 的呼叫,提取出來如下這些資訊:

Directory: ./template
Regular expression: /^.*\.ejs$/
複製程式碼

然後來建立我們自己的上下文,什麼意思呢,就是我們可以通過這個方法篩選出來我們需要的檔案並且讀取

下面我們來簡單看一看使用:

/**
* @param directory 要搜尋的資料夾目錄不能是變數,否則在編譯階段無法定位目錄
* @param useSubdirectories  是否搜尋子目錄
* @param regExp 匹配檔案的正規表示式
* @return function 返回一個具有 resolve, keys, id 三個屬性的方法
          resolve() 它返回請求被解析後得到的模組 id
          keys() 它返回一個陣列,由所有符合上下文模組處理的請求組成。 
          id 是上下文模組裡面所包含的模組 id. 它可能在你使用 module.hot.accept 的時候被用到
*/
require.context('demo', useSubdirectories = false, regExp = /\.js$/)
// (建立了)一個包含了 demo 資料夾(不包含子目錄)下面的、所有檔名以 `js` 結尾的、能被 require 請求到的檔案的上下文。
複製程式碼

不要困惑,接下來我們來探討在專案中怎麼用。

組織路由

對於Vue中的路由,大家都很熟悉,類似於宣告式的配置檔案,其實已經很簡潔了。現在我們來讓他更簡潔

  1. 分割路由

首先為了方便我們管理,我們把router目錄下的檔案分割為以下結構

router                           // 路由資料夾
  |__index.js                    // 路由組織器:用來初始化路由等等
  |__common.js                   // 通用路由:宣告通用路由
  |__modules                     // 業務邏輯模組:所以的業務邏輯模組
        |__index.js              // 自動化處理檔案:自動引入路由的核心檔案
        |__home.js               // 業務模組home:業務模組
        |__a.js                  // 業務模組a
  
複製程式碼
  1. modules資料夾中處理業務模組

modules資料夾中存放著我們所有的業務邏輯模組,至於業務邏輯模組怎麼分,我相信大家自然有自己的一套標準。我們通過上面提到的require.context()接下來編寫自動化的核心部分index.js

const files = require.context('.', true, /\.js$/)

console.log(files.keys()) // ["./home.js"] 返回一個陣列
let configRouters = []
/**
* inject routers
*/
files.keys().forEach(key => {
  if (key === './index.js') return
  configRouters = configRouters.concat(files(key).default) // 讀取出檔案中的default模組
})
export default configRouters // 丟擲一個Vue-router期待的結構的陣列
複製程式碼

自動化部分寫完了,那業務元件部分怎麼寫? 這就更簡單了

import Frame from '@/views/frame/Frame'
import Home from '@/views/index/index'
export default [
    // 首頁
    {
      path: '/index',
      name: '首頁',
      redirect: '/index',
      component: Frame, 
      children: [ // 巢狀路由
        {
          path: '',
          component: Home
        }
      ]
    }
]
複製程式碼
  1. common路由處理 我們的專案中有一大堆的公共路由需要處理比如404阿,503阿等等路由我們都在common.js中進行處理。
export default [
  // 預設頁面
  {
    path: '/',
    redirect: '/index',
    hidden:true
  },
  // 無許可權頁面
  {
    path: '/nopermission',
    name: 'nopermission',
    component: () => import('@/views/NoPermission')
  },
  // 404
  {
    path: '*',
    name: 'lost',
    component: () => import('@/views/404')
  }
]
複製程式碼
  1. 路由初始化 這是我們的最後一步了,用來初始化我們的專案路由
import Vue from 'vue'
import VueRouter from 'vue-router'
import RouterConfig from './modules' // 引入業務邏輯模組
import CommonRouters from './common' // 引入通用模組
Vue.use(VueRouter)
export default new VueRouter({
  mode: 'history',// 需要服務端支援
  scrollBehavior: () => ({ y: 0 }),
  routes: RouterConfig.concat(CommonRouters)
})
複製程式碼

估計有些朋友程式碼寫到這還不知道到底這樣做好處在哪裡。我們來描述一個場景,比如按照這種結構來劃分模組。正常的情況是我們建立完home.js要手動的把這個模組import到路由檔案宣告的地方去使用。但是有了上面的index.js,在使用的時候你只需要去建立一個home.js並丟擲一個符合VueRouter規範的陣列,剩下的就不用管了。import RouterConfig from './modules' // 引入業務邏輯模組 已經幫你處理完了。另外擴充套件的話你還可以把hooks拿出來作為一個單獨檔案。

全域性元件統一宣告

同樣的道理,有了上面的經驗,我們照葫蘆畫瓢來處理一下我們的全域性元件。這就沒什麼可說的了,直接上核心程式碼

  1. 組織結構
components                       // 元件資料夾
  |__xxx.vue                     // 其他元件
  |__global                      // 全域性元件資料夾
        |__index.js              // 自動化處理檔案
        |__demo.vue              // 全域性demo元件
複製程式碼
  1. global處理
import Vue from 'vue'
let contexts = require.context('.', false, /\.vue$/)
contexts.keys().forEach(component => {
  let componentEntity = contexts(component).default
  // 使用內建的元件名稱 進行全域性元件註冊
  Vue.component(componentEntity.name, componentEntity)
})
複製程式碼
  1. 使用和說明

這個使用起來就更簡單了,直接在app.js引用這個檔案就行。

注意:我之前看到有些人做法是使用元件名去區分全域性元件和普通元件,然後通過正則去判斷需不需要全域性註冊。我是直接把全域性的元件放到global資料夾下,然後元件的註冊名稱直接使用component.name。至於使用哪種方式就比較看個人了。

充分利用NodeJS

放著node這麼好得東西不用真是有點浪費,那麼我們來看看node能為我們增加效率做出什麼貢獻。

有這麼一個場景,我們每次建立模組的時候都要新建一個vue檔案和對應的router配置,而且新頁面的大部分東西都還差不多,還得去複製貼上別得頁面。這想想就有點low。那既然有了node我們可不可以通過node來做這寫亂七八糟得事情? 下面來把我們的想法付諸於顯示。

我們實現這個功能主要要藉助Nodefsprocess, 感興趣的話可以深入研究一下。

首先我們要編寫我們的node指令碼,這裡是一個比較簡單的版本。什麼驗證資料夾或者檔案的都沒有,只是來實現我們這個想法:

/*
 * fast add new module script
 */
const path = require('path')
const fs = require('fs')
const chalk = require('chalk')
const reslove = file => path.resolve(__dirname, '../src', file)
// symbol const
const RouterSymbol = Symbol('router'),
      ViewsSymbol = Symbol('views')
// root path
const rootPath = {
  [RouterSymbol]: reslove('router/modules'),
  [ViewsSymbol]: reslove('views')
}
//loggs
const errorLog = error => console.log(chalk.red(`${error}`))
const defaultLog = log => console.log(chalk.green(`${log}`))
// module name
let moduleName = new String()
let fileType = new String()
//const string
const vueFile = module => (`<template>

</template>

<script>
export default {
  name: '${module}',
  data () {
    return {

    }
  },
  methods: {

  },
  created() {
    
  }
}
</script>

<style lang="less">

</style>
`)
// route file
const routerFile = module => (`// write your comment here...
export default [
  {
    path: '/${module}',
    name: '',
    redirect: '/${module}',
    component: () => import('@/views/frame/Frame'),
    children: [
      {
        path: '',
        fullPath: '',
        name: '',
        component: () => import('@/views/${module}/index')
      }
    ]
  }
]
`)
/**
 * generate file
 * @param {*} filePath 
 * @param {*} content 
 * @param {*} dirPath 
 */
const generateFile = async (filePath, content, dirPath = '') =>{
  try {
    // create file if file not exit
    if (dirPath !== '' && ! await fs.existsSync(dirPath)) {
      await fs.mkdirSync(dirPath)
      defaultLog(`created ${dirPath}`)
    }
    if (! await fs.existsSync(filePath)) {
      // create file
      await fs.openSync(filePath, 'w')
      defaultLog(`created ${filePath}`)
    }
    await fs.writeFileSync(filePath, content, 'utf8')
  } catch (error) {
    errorLog(error)
  }
}
// module-method map
const generates = new Map([
  ['view', async (module) => {
    // module file
    const filePath = path.join(rootPath[ViewsSymbol], module)
    const vuePath = path.join(filePath, '/index.vue')
    await generateFile(vuePath, vueFile(module), filePath)
  }],
  // router is not need new folder
  ['router',async (module) => {
    const routerPath = path.join(rootPath[RouterSymbol], `/${module}.js`)
    await generateFile(routerPath, routerFile(module))
  }]
])
defaultLog(`請輸入模組名稱(英文):`)
// files
const files = ['view', 'router']
// 和命令列進行互動 獲取的建立的模組名稱
process.stdin.on('data', (chunk) => {
  try {
    if (!moduleName) {
      moduleName = chunk
    } else {
      chunk = chunk.slice(0,-2) // delete /n
      defaultLog(`new module name is ${chunk}`)
      files.forEach(async (el, index) => {
        // 執行建立語句
        await generates.get(`${el}`).call(null, chunk.toString())
        if (index === files.length-1) {
          process.stdin.emit('end')
        }
      })
    }
  } catch (error) {
    errorLog(error)
  }
})
process.stdin.on('end', () => {
  defaultLog('create module success')
})
複製程式碼

下面我們看使用的流程

加快Vue專案的開發速度
這樣我們就分別建立了vuerouter的檔案,而且已經注入了內容。按照我們提前宣告的元件

注意:這只是一個簡單的思路,通過Node強大的檔案處理能力,我們能做的事情遠不止這些。

發揮Mixins的威力

Vue中的混入mixins是一種提供分發 Vue 元件中可複用功能的非常靈活的方式。聽說在3.0版本中可能會用Hooks的形式實現,但這並不妨礙它的強大。基礎部分的可以看這裡。這裡主要來討論mixins能在什麼情景下幫助我們。

通用mixins

如果我們有大量的表格頁面,仔細一扒拉你發現非常多的東西都是可以複用的例如分頁表格高度載入方法laoding宣告等一大堆的東西。下面我們來整理出來一個簡單通用混入list.js

const list = {
  data () {
    return {
      // 這些東西我們在list中處理,就不需要在每個頁面再去手動的做這個了。
      loading: false, // 伴隨loading狀態
      pageNo: 1, // 頁碼
      pageSize: 15, // 頁長
      totalCount: 0, // 總個數
      pageSizes: [15, 20, 25, 30], //頁長數
      pageLayout: 'total, sizes, prev, pager, next, jumper', // 分頁佈局
      list: []
    }
  },
  methods: {
    // 分頁回掉事件
    handleSizeChange(val) {
      this.pageSize = val
      // todo
    },
    handleCurrentChange (val) {
      this.pageNo = val
      // todo
    },
    /**
     * 表格資料請求成功的回撥 處理完公共的部分(分頁,loading取消)之後把控制權交給頁面
     * @param {*} apiResult 
     * @returns {*} promise
     */
    listSuccessCb (apiResult = {}) {
      return new Promise((reslove, reject) => {
        let tempList = [] // 臨時list
        try {
          this.loading = false
          // todo
          // 直接丟擲
          reslove(tempList)
        } catch (error) {
          reject(error)
        }
      })
    },
    /**
     * 處理異常情況
     * ==> 簡單處理  僅僅是對錶格處理為空以及取消loading
     */
    listExceptionCb (error) {
      this.loading = false
      console.error(error)
    }
  },
  created() {
    // 這個生命週期是在使用元件的生命週期之前
    this.$nextTick().then(() => {
      // todo
    })
  }
}
export default list
複製程式碼

下面我們直接在元件中使用這個mixins

import mixin from '@/mixins/list' // 引入
import {getList} from '@/api/demo'
export default {
  name: 'mixins-demo',
  mixins: [mixin], // 使用mixins
  data () {
    return {
    }
  },
  methods: {
    // 載入列表
    load () {
      const para = {
      }
      this.loading = true
      getList(para).then((result) => {
        this.listSuccessCb(result).then((list) => {
          this.list = list
        }).catch((err) => {
          console.log(err)
        })
      }).catch((err) => {
        this.listExceptionCb(err)
      })
    }
  },
  created() {
    this.load()
  }
}
複製程式碼

使用了mixins之後一個簡單的有loadoing, 分頁,資料的表格大概就只需要上面這些程式碼。

mixins做公共資料的管理

有些時候我們有一些公共的資料它可能3,4個模組取使用但是又達不到全域性的這種規模。這個時候我們就可以用mixins去管理他們,比如我們有幾個模組要使用使用者型別這個列表,我們來看使用mixins來實現共享。

// types.js
import {getTypes} from '@/api/demo' // ajax
export default {
  data () {
    return {
      types: [] // ==>  {name: '', value: ''}
    }
  },
  methods: {
    // 獲取列表
    getAllTypesList () {
      getApiList().then((result) => {
        // todo
        this.types = result // 假設result就是我們需要使用的資料
      }).catch((err) => {
        console.error(err)
      })
    }
  },
  created() {
    // 在需要使用這個mixins的時候取自動請求資料  這個可要可不要  你想在父元件中執行也是ok的
    this.getAllTypesList()
  }
}
複製程式碼

在元件中引用

import typeMixin from '@/mixins/types'
export default {
  name: 'template',
  mixins: [typeMixin],
  data () {
    return {
      // types這個陣列在使用元件中不用多餘的定義,直接拿來用就行
      type: ''
    }
  },
  methods: {
  }
}
複製程式碼

至於mixins中得資料我們可以在元件中直接使用

<!--  -->
<el-select v-model="type" clearable placeholder="請選擇型別">
    <el-option v-for="item in types" :key="item.id" :label="item.templateName" :value="item.id"></el-option>
  </el-select>
複製程式碼

我們這樣就可以不用vuex來去管理那些只有在模組間複用幾次的資料,而且非常方便得去取我們想要得資料,連定義都省了。但是這有一個缺點。就是每次都會去重新請求這些資料。如果你不在乎這一點點瑕疵的話,我覺得用起來是完全ok得。

注意: mixins它固然是簡單的,但是註釋和引用一定要做好,不然的話新成員進入團隊大概是一臉的懵逼,而且也不利於後期的維護。也是一把雙刃劍。另外:全域性mixins一定要慎用,如果不是必須要用的話我還是不建議使用。

進一步對元件進行封裝

大家都知道元件化的最大的好處就是高度的可複用性和靈活性。但是元件怎麼封裝好,封裝到什麼程度讓我們更方便。這是沒有標準的答案的。我們只有根據高內聚,低耦合的這個指導思想來對我們的業務通用元件來進行封裝,讓我們的業務頁面結構更加的簡潔,加快我們的開發效率。封裝多一點的話頁面可能會變成這樣:

<template>
  <box-content>
    <!-- 頭部標題部分 -->
    <page-title>
      <bread slot="title" :crumbs="[{name: 'xx管理', path: '', active: true, icon: ''}, {name: 'xxxx', path: '', active: true, icon: ''}]"></bread>
    </page-title>
    <!-- 表格部分 -->
    <div>
      <base-table v-loading="loading" :columns="headers" :list="list" :page-no ="pageNo" :page-size="pageSize" :total-count="totalCount" @delete="deleteItm"  @change-size="handleSizeChange" @change-page="handleCurrentChange">
      </base-table>
    </div>
  </box-content>
</template>
複製程式碼

有什麼東西一目瞭然。

無狀態元件

最容易勾起我們封裝慾望的就是無狀態HTML元件,例如我們除去header, menu之後的content部分。沒有什麼需要複雜的互動,但是我們每個頁面又都得寫。你說不拿它開刀拿誰開?

<template>
  <div class="container-fluid" :class="[contentClass]">
      <el-row>
          <el-col :span="24">
              <!-- box with #fff bg -->
              <div class="box">
                  <div class="box-body">
                      <slot></slot>
                  </div>
              </div>
          </el-col>
      </el-row>
  </div>
</template>
複製程式碼

上面這個處理非常的簡單,但是你在專案中會非常頻繁的使用過到,那麼這個封裝就很有必要了。

ElementUI table元件封裝

ElementUI中得元件其實已經封裝得很優秀了,但是表格使用得時候還是有一堆得程式碼在我看來是不需要在業務中重複寫得。封裝到靠配置來進行表格得書寫得一步我覺得就差不多了,下面是一個小demo

<template>
  <el-row>
    <el-col :span="24">
      <el-table :data="list" border size="mini" @selection-change="handleSelectionChange" :max-height="tableHeight" v-bind="$attrs"> <!--   -->
        <template v-for="(column, index) in columns">
          <slot name="front-slot"> </slot>
          <!-- 序號 -->
          <el-table-column :key="index" v-if="column.type === 'selection'" type="selection" width="55"> </el-table-column>
          <!-- 核取方塊 -->
          <el-table-column :key="index" v-else-if="column.type === 'index'"  type="index" width="50" label="序號"> </el-table-column>
          <!-- 具體內容 -->
          <el-table-column :key="index" v-else align="left" :label="column.title" :width="column.width">
            <template slot-scope="scope">
              <!-- 僅僅顯示文字 -->
              <label v-if="!column.hidden"> <!-- 如果hidden為true的時候 那麼當前格可以不顯示,可以選擇顯示自定義的slot-->
                <!-- 操作按鈕 -->
                <label v-if="column.type === 'operate'">
                  <a href="javascript:void(0)" class="operate-button" v-for="(operate, index) in column.operates" :key="index" @click="handleClick(operate, scope.row)">
                    {{operate.name}}
                    &nbsp;&nbsp;
                  </a>
                </label>
                <span v-else>
                  {{scope.row[column.key]}}
                </span>
              </label>
              <!-- 使用slot的情況下 -->
              <label v-if="column.slot">
                <!-- 具名slot -->
                <slot v-if="column.slot" :name="column.slot" :scope="scope"></slot>
              </label>
            </template>
          </el-table-column>
        </template>
        <!--預設的slot -->
        <slot/>
      </el-table>
    </el-col>
  </el-row>
</template>
複製程式碼
export default {
  name: 'base-table',
  props: {
    // 核心資料
    list: {
      type: Array,
      default: () => []
    },
    // columns
    columns: {
      type: Array,
      required: true,
      default: () => []
    }
  },
  data () {
    return {
      tableHeight: xxx
    }
  },
  methods: {
    // 處理點選事件
    handleClick(action, data) {
      // emit事件
      this.$emit(`${action.emitKey}`, data)
    }
  }
}
複製程式碼

使用:

<base-table v-loading="loading" :columns="headers" :list="list"  @view="viewCb">
  <!-- 自定義的slot -->
  <template slot="demoslot" slot-scope="{scope}">
    <span>
      {{scope.row}}
    </span>
  </template>
  <!-- 預設的slot  如果互動很複雜 我們還可以直接使用表格內部的元件 -->
  <el-table-column
    label="操作"
    width="200"
  >
    <template slot-scope="scope">
      <a href="javascript:void(0)" @click="defaultSlot(scope.row)">xxx</a>
    </template>
  </el-table-column>
</base-table>
複製程式碼
export default {
  name: 'table-demo',
  data () {
    return {
      // 表格頭部配置
      headers: [
        { key: 'xxx', title: '測試' },
        { title: 'xxx', hidden: true, slot: 'demoslot'},
        {
          title: '操作', type: 'operate',
          operates: [
            {name: '詳情',emitKey: 'view'}
          ]
        }
      ]
    }
  },
  methods: {
    viewCb(){
      // todo
    },
    defaultSlot(){
      // todo
    }
  }
}
複製程式碼

這樣封裝過的表格,應付基本的一些需求問題應該不大。至於特殊的要求可以一步一步的進行完善。

關於Element-UI中的table元件的封裝,我打算另寫一篇文章詳細的解讀我的思路和更復雜的實現。

總結

這些東西並不是什麼語法糖,是真正可以在專案中加快我們的效率。讓我們的自己乃至整個團隊從繁雜的重複複製貼上中解脫一點。至於速度和質量的問題。我是覺得使用公共元件質量可控性會更高一些。我建議公共得東西註釋一定要寫得全面和詳細,這樣可以極大的降低我們的交流成本。至於元件的封裝還是要看你的業務。

以上觀點純屬個人意見,如有錯誤,多謝指正。

沒想到這麼多人看?,加班加點整理出來了示例程式碼, 示例程式碼中也是一個Vue中後臺開發的pro版本,業務需求對的上的話直接用也是可以的??

原文地址 如果覺得有用得話給個⭐吧

相關文章