現(xiàn)如今的開發(fā)呵俏,比如是內(nèi)部使用的管理平臺這種項目大都時間比較倉倉促霸奕。實際上來說在使用了webpack + vue 這一套來開發(fā)的話已經(jīng)大大了提高了效率吭敢。但是對于我們的開發(fā)層面胚膊。還是有很多地方可以再次提高我們的項目開發(fā)效率眷蜓,讓我們更加專注于業(yè)務(wù)分瘾,畢竟時間就是生命。下面我們挨個來探討吁系。
巧用Webpack
Webpack
是實現(xiàn)我們前端項目工程化的基礎(chǔ)德召,但其實她的用處遠(yuǎn)不僅僅如此,我們可以通過Webpack來幫我們做一些自動化的事情垮抗。首先我們要了解require.context()這個API
require.context()
您可以使用require.context()函數(shù)創(chuàng)建自己的上下文氏捞。 它允許您傳入一個目錄進(jìn)行搜索,一個標(biāo)志指示是否應(yīng)該搜索子目錄冒版,還有一個正則表達(dá)式來匹配文件液茎。
其實是Webpack
通過解析 require() 的調(diào)用,提取出來如下這些信息:
Directory: ./template
Regular expression: /^.*\.ejs$/
然后來創(chuàng)建我們自己的上下文,什么意思呢捆等,就是我們可以通過這個方法篩選出來我們需要的文件并且讀取
下面我們來簡單看一看使用:
/**
* @param directory 要搜索的文件夾目錄不能是變量滞造,否則在編譯階段無法定位目錄
* @param useSubdirectories 是否搜索子目錄
* @param regExp 匹配文件的正則表達(dá)式
* @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` 結(jié)尾的、能被 require 請求到的文件的上下文明郭。
不要困惑买窟,接下來我們來探討在項目中怎么用。
組織路由
對于Vue中的路由薯定,大家都很熟悉始绍,類似于聲明式的配置文件,其實已經(jīng)很簡潔了』爸叮現(xiàn)在我們來讓他更簡潔
- 分割路由
首先為了方便我們管理亏推,我們把router目錄下的文件分割為以下結(jié)構(gòu)
router // 路由文件夾
|__index.js // 路由組織器:用來初始化路由等等
|__common.js // 通用路由:聲明通用路由
|__modules // 業(yè)務(wù)邏輯模塊:所以的業(yè)務(wù)邏輯模塊
|__index.js // 自動化處理文件:自動引入路由的核心文件
|__home.js // 業(yè)務(wù)模塊home:業(yè)務(wù)模塊
|__a.js // 業(yè)務(wù)模塊a
- modules文件夾中處理業(yè)務(wù)模塊
modules
文件夾中存放著我們所有的業(yè)務(wù)邏輯模塊,至于業(yè)務(wù)邏輯模塊怎么分年堆,我相信大家自然有自己的一套標(biāo)準(zhǔn)吞杭。我們通過上面提到的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期待的結(jié)構(gòu)的數(shù)組
自動化部分寫完了变丧,那業(yè)務(wù)組件部分怎么寫芽狗? 這就更簡單了
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
}
]
}
]
- common路由處理 我們的項目中有一大堆的公共路由需要處理比如404阿,503阿等等路由我們都在common.js中進(jìn)行處理锄贷。
export default [
// 默認(rèn)頁面
{
path: '/',
redirect: '/index',
hidden:true
},
// 無權(quán)限頁面
{
path: '/nopermission',
name: 'nopermission',
component: () => import('@/views/NoPermission')
},
// 404
{
path: '*',
name: 'lost',
component: () => import('@/views/404')
}
]
- 路由初始化 這是我們的最后一步了译蒂,用來初始化我們的項目路由
import Vue from 'vue'
import VueRouter from 'vue-router'
import RouterConfig from './modules' // 引入業(yè)務(wù)邏輯模塊
import CommonRouters from './common' // 引入通用模塊
Vue.use(VueRouter)
export default new VueRouter({
mode: 'history',// 需要服務(wù)端支持
scrollBehavior: () => ({ y: 0 }),
routes: RouterConfig.concat(CommonRouters)
})
估計有些朋友代碼寫到這還不知道到底這樣做好處在哪里。我們來描述一個場景谊却,比如按照這種結(jié)構(gòu)來劃分模塊柔昼。正常的情況是我們創(chuàng)建完home.js
要手動的把這個模塊import
到路由文件聲明的地方去使用。但是有了上面的index.js
炎辨,在使用的時候你只需要去創(chuàng)建一個home.js
并拋出一個符合VueRouter規(guī)范的數(shù)組捕透,剩下的就不用管了。import RouterConfig from './modules' //
引入業(yè)務(wù)邏輯模塊 已經(jīng)幫你處理完了碴萧。另外擴展的話你還可以把hooks
拿出來作為一個單獨文件乙嘀。
全局組件統(tǒng)一聲明
同樣的道理,有了上面的經(jīng)驗破喻,我們照葫蘆畫瓢來處理一下我們的全局組件虎谢。這就沒什么可說的了,直接上核心代碼
1.組織結(jié)構(gòu)
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)置的組件名稱 進(jìn)行全局組件注冊
Vue.component(componentEntity.name, componentEntity)
})
- 使用和說明
這個使用起來就更簡單了曹质,直接在app.js引用這個文件就行婴噩。
注意:我之前看到有些人做法是使用組件名去區(qū)分全局組件和普通組件擎场,然后通過正則去判斷需不需要全局注冊。我是直接把全局的組件放到global文件夾下几莽,然后組件的注冊名稱直接使用component.name迅办。至于使用哪種方式就比較看個人了。
充分利用NodeJS
放著node
這么好得東西不用真是有點浪費章蚣,那么我們來看看node
能為我們增加效率做出什么貢獻(xiàn)站欺。
有這么一個場景,我們每次創(chuàng)建模塊的時候都要新建一個vue
文件和對應(yīng)的router
配置纤垂,而且新頁面的大部分東西都還差不多矾策,還得去復(fù)制粘貼別得頁面。這想想就有點low
洒忧。那既然有了node
我們可不可以通過node
來做這寫亂七八糟得事情蝴韭? 下面來把我們的想法付諸于顯示。
我們實現(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']
// 和命令行進(jìn)行交互 獲取的創(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強大的文件處理能力,我們能做的事情遠(yuǎn)不止這些矛辕。
通用mixins
如果我們有大量的表格頁面笑跛,仔細(xì)一扒拉你發(fā)現(xiàn)非常多的東西都是可以復(fù)用的例如分頁,表格高度聊品,加載方法飞蹂, 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: {
// 分頁回掉事件
handleSizeChange(val) {
this.pageSize = val
// todo
},
handleCurrentChange (val) {
this.pageNo = val
// todo
},
/**
* 表格數(shù)據(jù)請求成功的回調(diào) 處理完公共的部分(分頁,loading取消)之后把控制權(quán)交給頁面
* @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個模塊取使用但是又達(dá)不到全局的這種規(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 // 假設(shè)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來去管理那些只有在模塊間復(fù)用幾次的數(shù)據(jù)毁欣,而且非常方便得去取我們想要得數(shù)據(jù)售担,連定義都省了。但是這有一個缺點署辉。就是每次都會去重新請求這些數(shù)據(jù)族铆。如果你不在乎這一點點瑕疵的話,我覺得用起來是完全ok得哭尝。
注意: mixins它固然是簡單的哥攘,但是注釋和引用一定要做好,不然的話新成員進(jìn)入團(tuán)隊大概是一臉的懵逼材鹦,而且也不利于后期的維護(hù)逝淹。也是一把雙刃劍。另外:全局mixins一定要慎用桶唐,如果不是必須要用的話我還是不建議使用栅葡。
進(jìn)一步對組件進(jìn)行封裝
大家都知道組件化的最大的好處就是高度的可復(fù)用性和靈活性。但是組件怎么封裝好尤泽,封裝到什么程度讓我們更方便欣簇。這是沒有標(biāo)準(zhǔn)的答案的。我們只有根據(jù)高內(nèi)聚坯约,低耦合的這個指導(dǎo)思想來對我們的業(yè)務(wù)通用組件來進(jìn)行封裝熊咽,讓我們的業(yè)務(wù)頁面結(jié)構(gòu)更加的簡潔,加快我們的開發(fā)效率闹丐。封裝多一點的話頁面可能會變成這樣:
<template>
<box-content>
<!-- 頭部標(biāo)題部分 -->
<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部分卿拴。沒有什么需要復(fù)雜的交互衫仑,但是我們每個頁面又都得寫。你說不拿它開刀拿誰開??
<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è)務(wù)中重復(fù)寫得文狱。封裝到靠配置來進(jìn)行表格得書寫得一步我覺得就差不多了,下面是一個小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>
<!-- 復(fù)選框 -->
<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的時候 那么當(dāng)前格可以不顯示航徙,可以選擇顯示自定義的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>
<!--默認(rèn)的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>
<!-- 默認(rèn)的slot 如果交互很復(fù)雜 我們還可以直接使用表格內(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
}
}
}
總結(jié)
這些東西并不是什么語法糖如贷,是真正可以在項目中加快我們的效率。讓我們的自己乃至整個團(tuán)隊從繁雜的重復(fù)復(fù)制粘貼中解脫一點到踏。至于速度和質(zhì)量的問題杠袱。我是覺得使用公共組件質(zhì)量可控性會更高一些。我建議公共得東西注釋一定要寫得全面和詳細(xì)窝稿,這樣可以極大的降低我們的交流成本楣富。至于組件的封裝還是要看你的業(yè)務(wù)。