加快Vue項目的開發速度

現如今的開發,比如是內部使用的管理平臺這種項目大都時間比較倉倉促。實際上來說在使用了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
      
  2. modules文件夾中處理業務模塊

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

    const files = require.context('.', true, /\.js$/)
    
    console.log(files.keys()) // ["./home.js"] 返回一個數組
    let configRouters = []
    /**

*/
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
      }
    ]
  }

]

3. `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')
}

]

4. 路由初始化
這是我們的最後一步了,用來初始化我們的項目路由

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組件
2. `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)

})

3. 使用和說明

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

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

## 充分利用NodeJS
放着`node`這麼好得東西不用真是有點浪費,那麼我們來看看`node`能爲我們增加效率做出什麼貢獻。

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

我們實現這個功能主要要藉助`Node`的[fs](http://nodejs.cn/api/fs.html)和[process](http://nodejs.cn/api/process.html), 感興趣的話可以深入研究一下。

首先我們要編寫我們的`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 = {

}
//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')
})

下面我們看使用的流程

![](https://user-gold-cdn.xitu.io/2018/12/12/167a00b07674bf7d?w=935&h=223&f=png&s=22262)
這樣我們就分別創建了`vue`和`router`的文件,而且已經注入了內容。按照我們提前聲明的組件

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

## 發揮Mixins的威力
`Vue`中的`混入`[mixins](https://cn.vuejs.org/v2/guide/mixins.html)是一種提供分發 `Vue` 組件中可複用功能的非常靈活的方式。聽說在3.0版本中可能會用Hooks的形式實現,但這並不妨礙它的強大。基礎部分的可以看[這裏](https://github.com/QDMarkMan/CodeBlog/blob/master/Vue/Vue%E5%BC%80%E5%8F%91%E4%B8%AD%E9%97%AE%E9%A2%98%E9%9B%86%E9%94%A6.md)。這裏主要來討論`mixins`能在什麼情景下幫助我們。


比如我們的大量的表格頁面,仔細一扒拉你發現非常多的東西都是可以複用的例如`分頁`,`表格高度`,`加載方法`, `laoding聲明`等一大堆的東西。下面我們來整理出來一個簡單的`list.vue`

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()

}
}
</script>

使用了`mixins`之後一個簡單的有`loadoing`, `分頁`,`數據`的表格大概就只需要上面這些代碼。

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

## 進一步對組件進行封裝

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

<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
}

}
}

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

# 總結

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

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

示例代碼還在整理中。。。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章