現(xiàn)如今的開發(fā),比如內(nèi)部使用的管理平臺這種項目大都時間比較倉促。實際上來說,在使用了webpack + vue 這一套來開發(fā)的話已經(jīng)大大了提高了效率。但是對于我們的開發(fā)層面。還是有很多地方可以再次提高我們的項目開發(fā)效率,讓我們更加專注于業(yè)務,畢竟時間就是生命。下面我們挨個來探討。
巧用Webpack
Webpack是實現(xiàn)我們前端項目工程化的基礎,但其實她的用處遠不僅僅如此,我們可以通過Webpack來幫我們做一些自動化的事情。首先我們要了解require.context()這個API
require.context()
您可以使用require.context()函數(shù)創(chuàng)建自己的上下文。 它允許您傳入一個目錄進行搜索,一個標志指示是否應該搜索子目錄,還有一個正則表達式來匹配文件。
其實是Webpack通過解析 require() 的調(diào)用,提取出來如下這些信息:
Directory:?./template
Regular expression:?/^.*.ejs$/
然后來創(chuàng)建我們自己的上下文,什么意思呢,就是我們可以通過這個方法篩選出來我們需要的文件并且讀取
下面我們來簡單看一看使用:
/**
* @param directory 要搜索的文件夾目錄不能是變量,否則在編譯階段無法定位目錄
* @param useSubdirectories 是否搜索子目錄
* @param regExp 匹配文件的正則表達式
* @return function 返回一個具有 resolve, keys, id 三個屬性的方法
??????????resolve() 它返回請求被解析后得到的模塊 id
??????????keys() 它返回一個數(shù)組,由所有符合上下文模塊處理的請求組成。
??????????id 是上下文模塊里面所包含的模塊 id. 它可能在你使用 module.hot.accept 的時候被用到
*/
require.context('demo',?useSubdirectories?=?false,?regExp?=?/.js$/)
// (創(chuàng)建了)一個包含了 demo 文件夾(不包含子目錄)下面的、所有文件名以 `js` 結尾的、能被 require 請求到的文件的上下文。
不要困惑,接下來我們來探討在項目中怎么用。
組織路由
對于Vue中的路由,大家都很熟悉,類似于聲明式的配置文件,其實已經(jīng)很簡潔了?,F(xiàn)在我們來讓他更簡潔。
1、分割路由
首先為了方便我們管理,我們把router目錄下的文件分割為以下結構:
router?// 路由文件夾
??|__index.js?// 路由組織器:用來初始化路由等等
??|__common.js?// 通用路由:聲明通用路由
??|__modules?// 業(yè)務邏輯模塊:所以的業(yè)務邏輯模塊
????????|__index.js?// 自動化處理文件:自動引入路由的核心文件
????????|__home.js?// 業(yè)務模塊home:業(yè)務模塊
????????|__a.js?// 業(yè)務模塊a
2、modules文件夾中處理業(yè)務模塊
modules文件夾中存放著我們所有的業(yè)務邏輯模塊,至于業(yè)務邏輯模塊怎么分,我相信大家自然有自己的一套標準。我們通過上面提到的require.context()接下來編寫自動化的核心部分index.js。
const?files?=?require.context('.',?true,?/.js$/)
console.log(files.keys())?// ["./home.js"] 返回一個數(shù)組
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期待的結構的數(shù)組
自動化部分寫完了,那業(yè)務組件部分怎么寫? 這就更簡單了。
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'?// 引入業(yè)務邏輯模塊
import?CommonRouters?from?'./common'?// 引入通用模塊
Vue.use(VueRouter)
export?default?new?VueRouter({
??mode:?'history',// 需要服務端支持
??scrollBehavior:?()?=>?({?y:?0?}),
??routes:?RouterConfig.concat(CommonRouters)
})
估計有些朋友代碼寫到這還不知道到底這樣做好處在哪里。我們來描述一個場景,比如按照這種結構來劃分模塊。正常的情況是我們創(chuàng)建完home.js要手動的把這個模塊import到路由文件聲明的地方去使用。但是有了上面的index.js,在使用的時候你只需要去創(chuàng)建一個home.js并拋出一個符合VueRouter規(guī)范的數(shù)組,剩下的就不用管了。import RouterConfig from './modules' // 引入業(yè)務邏輯模塊 已經(jīng)幫你處理完了。另外擴展的話你還可以把hooks拿出來作為一個單獨文件。
全局組件統(tǒng)一聲明
同樣的道理,有了上面的經(jīng)驗,我們照葫蘆畫瓢來處理一下我們的全局組件。這就沒什么可說的了,直接上核心代碼。
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
??// 使用內(nèi)置的組件名稱 進行全局組件注冊
??Vue.component(componentEntity.name,?componentEntity)
})
3、使用和說明
這個使用起來就更簡單了,直接在app.js引用這個文件就行。
注意:我之前看到有些人做法是使用組件名去區(qū)分全局組件和普通組件,然后通過正則去判斷需不需要全局注冊。我是直接把全局的組件放到global文件夾下,然后組件的注冊名稱直接使用component.name。至于使用哪種方式就比較看個人了。
充分利用NodeJS
放著node這么好的東西不用真是有點浪費,那么我們來看看node能為我們提高效率做出什么貢獻。
有這么一個場景,我們每次創(chuàng)建模塊的時候都要新建一個vue文件和對應的router配置,而且新頁面的大部分東西都還差不多,還得去復制粘貼別的頁面。這想想就有點low。那既然有node我們可不可以通過node來做這些亂七八糟的事情? 下面來把我們的想法付諸于現(xiàn)實。
我們實現(xiàn)這個功能主要要借助Node的fs和process, 感興趣的話可以深入研究一下。
首先我們要編寫我們的node腳本,這里是一個比較簡單的版本。什么驗證文件夾或者文件的都沒有,只是來實現(xiàn)我們這個想法:
/*
?* 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']
// 和命令行進行交互 獲取的創(chuàng)建的模塊名稱
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)?=>?{
????????// 執(zhí)行創(chuàng)建語句
????????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')
})
下面我們看使用的流程
這樣我們就分別創(chuàng)建了vue和router的文件,而且已經(jīng)注入了內(nèi)容。按照我們提前聲明的組件
注意:這只是一個簡單的思路,通過Node強大的文件處理能力,我們能做的事情遠不止這些。
發(fā)揮Mixins的威力
Vue中的混入mixins是一種提供分發(fā) Vue 組件中可復用功能的非常靈活的方式。聽說在3.0版本中可能會用Hooks的形式實現(xiàn),但這并不妨礙它的強大。這里主要來討論mixins能在什么情景下幫助我們。
通用mixins
如果我們有大量的表格頁面,仔細一扒拉你發(fā)現(xiàn)非常多的東西都是可以復用的例如分頁,表格高度,加載方法, laoding聲明等一大堆的東西。下面我們來整理出來一個簡單通用混入list.js
const?list?=?{
??data?()?{
????return?{
??????// 這些東西我們在list中處理,就不需要在每個頁面再去手動的做這個了。
??????loading:?false,?// 伴隨loading狀態(tài)
??????pageNo:?1,?// 頁碼
??????pageSize:?15,?// 頁長
??????totalCount:?0,?// 總個數(shù)
??????pageSizes:?[15,?20,?25,?30],?//頁長數(shù)
??????pageLayout:?'total, sizes, prev, pager, next, jumper',?// 分頁布局
??????list:?[]
????}
??},
??methods:?{
????// 分頁回調(diào)事件
????handleSizeChange(val)?{
??????this.pageSize?=?val
??????// todo
????},
????handleCurrentChange?(val)?{
??????this.pageNo?=?val
??????// todo
????},
????/**
?????* 表格數(shù)據(jù)請求成功的回調(diào) 處理完公共的部分(分頁,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之后一個簡單的有l(wèi)oadoing, 分頁,數(shù)據(jù)的表格大概就只需要上面這些代碼。
mixins做公共數(shù)據(jù)的管理
有些時候我們有一些公共的數(shù)據(jù)它可能3,4個模塊取使用但是又達不到全局的這種規(guī)模。這個時候我們就可以用mixins去管理他們,比如我們有幾個模塊要使用用戶類型這個列表,我們來看使用mixins來實現(xiàn)共享。
// 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就是我們需要使用的數(shù)據(jù)
??????}).catch((err)?=>?{
????????console.error(err)
??????})
????}
??},
??created()?{
????// 在需要使用這個mixins的時候取自動請求數(shù)據(jù) 這個可要可不要 你想在父組件中執(zhí)行也是ok的
????this.getAllTypesList()
??}
}
在組件中引用
import?typeMixin?from?'@/mixins/types'
export?default?{
??name:?'template',
??mixins:?[typeMixin],
??data?()?{
????return?{
??????// types這個數(shù)組在使用組件中不用多余的定義,直接拿來用就行
??????type:?''
????}
??},
??methods:?{
??}
}
至于mixins中得數(shù)據(jù)我們可以在組件中直接使用
<!--?-->
<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來去管理那些只有在模塊間復用幾次的數(shù)據(jù),而且非常方便得去取我們想要得數(shù)據(jù),連定義都省了。但是這有一個缺點。就是每次都會去重新請求這些數(shù)據(jù)。如果你不在乎這一點點瑕疵的話,我覺得用起來是完全ok的。
注意: mixins它固然是簡單的,但是注釋和引用一定要做好,不然的話新成員進入團隊大概是一臉的懵逼,而且也不利于后期的維護。也是一把雙刃劍。另外:全局mixins一定要慎用,如果不是必須要用的話我還是不建議使用。
進一步封裝組件
大家都知道組件化的最大的好處就是高度的可復用性和靈活性。但是組件怎么封裝好,封裝到什么程度讓我們更方便。這是沒有標準的答案的。我們只有根據(jù)高內(nèi)聚,低耦合的這個指導思想來對我們的業(yè)務通用組件來進行封裝,讓我們的業(yè)務頁面結構更加的簡潔,加快我們的開發(fā)效率。封裝多一點的話頁面可能會變成這樣:
<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>
有什么東西一目了然。
無狀態(tài)組件
最容易勾起我們封裝欲望的就是無狀態(tài)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中得組件其實已經(jīng)封裝得很優(yōu)秀了,但是表格使用得時候還是有一堆得代碼在我看來是不需要在業(yè)務中重復寫得。封裝到靠配置來進行表格得書寫得一步我覺得就差不多了,下面是一個小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>
??????????<!--?具體內(nèi)容?-->
??????????<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}}
????????????????????
??????????????????</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:?{
????// 核心數(shù)據(jù)
????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 如果交互很復雜 我們還可以直接使用表格內(nèi)部的組件?-->
??<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
????}
??}
}
這樣封裝過的表格,應付基本的一些需求問題應該不大。至于特殊的要求可以一步一步的進行完善。
總結
這些東西并不是什么語法糖,是真正可以在項目中加快我們的效率,讓我們自己乃至整個團隊從繁雜的重復復制粘貼中解脫的方法。至于速度和質(zhì)量的問題,我是覺得使用公共組件質(zhì)量可控性會更高一些。我建議公共的東西注釋一定要寫得全面和詳細,這樣可以極大地降低我們的交流成本。至于組件的封裝還是要看你的業(yè)務。
以上觀點純屬個人意見,如有錯誤,多謝指正。
作者:markman
https://github.com/QDMarkMan/CodeBlog/edit/master/Vue/%E5%8A%A0%E5%BF%ABVue%E9%A1%B9%E7%9B%AE%E7%9A%84%E5%BC%80%E5%8F%91%E9%80%9F%E5%BA%A6.md
感興趣的小伙伴,可以關注公眾號【grain先森】,回復關鍵詞 “vue”,獲取更多資料,更多關鍵詞玩法期待你的探索~