寫於 2017.06.05css
以前發表過一篇《Vue-Donut——專用於構建Vue的UI組件庫的開發框架》,僅僅是對框架一個粗略的介紹,並無針對裏面的實現方式進行詳細說明。html
最近參與維護公司內部的一個針對移動端的UI組件庫,該組件庫缺少文檔和嚴格的文件組織結構。Vue-Donut
的功能比較簡單,並不能方便地建立針對移動端UI組件庫的文檔和預覽。在參考了mint-ui
等業界內成熟的方案以後,我在Vue-Donut
的基礎上進行了拓展,最後搭建出了一個很是方便且自動化的開發框架。vue
因爲以爲開發的過程很是有意思,也想記錄一下本身的開發思路,所以決定好好地寫一篇文章做爲記錄分享。node
項目地址:github.com/jrainlau/vu…webpack
首先咱們來規劃一下這個框架的最終目的是什麼:git
如圖所示,經過該框架能夠生成一個文檔頁面。這個頁面分爲三個部分:導航、文檔、預覽。github
導航:經過導航切換不一樣組件的文檔和預覽。web
文檔:該類型組件所對應的文檔,以markdown形式書寫。vue-router
預覽:該類型組件所對應的預覽頁面。vue-cli
爲了讓組件的開發和文檔的維護更加高效,咱們但願這個框架能夠更加自動化。若是咱們只要開不一樣組件的預覽的頁面及其對應的說明文檔README
,框架就能自動幫咱們生成對應的導航和HTML內容,豈不妙哉?除此以外,當咱們已經把全部的UI組件都開發好了,通通放在/components
目錄下,若是可以經過框架進行一鍵構建打包,最後產出一個npm包,那麼別人使用這套UI組件庫也會變得很是簡單。帶着這個想法,咱們來分析一下咱們可能須要用到的關鍵技術。
使用webpack2做爲框架核心:使用方便,高度可定製。同時webpack2文檔已經至關齊全,生態圈繁榮,社區活躍,遇到的坑基本上均可以在google和stackoverflow找到。
預覽頁面以iframe
的形式插入到文檔頁面中:維護組件庫的時候只須要聚焦於組件的開發和預覽頁面的組織,無需分心維護導航和文檔,實現瞭解耦。所以意味着這是一個基於Vue.js的多頁應用。
自動生成導航:使用vue-router
進行頁面切換。每當新建一個預覽頁面,就會自動在頁面上生成對應的導航,並自動維護導航和路由的關係。所以,咱們須要一套機制去監聽文件結構的變化。
自動生成文檔:一個預覽頁面對應一份文檔,因此文檔理應以README.md
的形式存放在對應的預覽頁面文件夾內。咱們須要一個可以把README.md
直接轉化成html內容的辦法。
開發者模式:經過一條命令,啓動一個webpack-dev-server
,提供熱更新和自動刷新功能。
構建打包模式:經過一條命令,自動把/components
目錄下的全部資源打包成一個npm包。
頁面構建模式:經過一條命令,生成可以直接部署使用的靜態資源文件。
經過對技術的梳理,咱們腦海裏面已經有了一個印象,接下來就是一步一步地進行開發了。
一個好的目錄結構,可以極大地方便咱們接下來的工做。
.
├── index.html // 文檔頁的入口html
├── view.html // 預覽頁的入口html
├── package.json // 依賴聲明、npm script命令
├── src
│ ├── document // 文檔頁目錄
│ │ ├── doc-app.vue // 文檔頁入口.vue文件
│ │ ├── doc-entry.js // 文檔頁入口.js文件
│ │ ├── doc-router.js // 文檔頁路由配置
│ │ ├── doc_comps // 文檔頁組件
│ │ └── static // 文檔頁靜態資源
│ └── view // 預覽頁目錄
│ ├── assets // 預覽頁靜態資源
│ ├── components // UI組件庫
│ ├── pages // 存放不一樣的預覽頁
│ ├── view-app.vue // 預覽頁入口.vue文件
│ ├── view-entry.js // 預覽頁入口.js文件
│ └── view-router.js // 預覽頁路由配置
└── webpack
├── webpack.base.config.js // webpack通用配置
├── webpack.build.config.js // UI庫構建打包配置
├── webpack.dev.config.js // 開發模式配置
└── webpack.doc.config.js // 靜態資源構建配置
複製代碼
能夠看到,目錄結構並不複雜,接下來咱們首先對webpack進行配置,以便咱們可以把項目跑起來。
進入到/webpack
目錄,新建一個webpack.base.config.js
文件,其內容以下:
const { join } = require('path')
const hljs = require('highlight.js')
// 配置markdown解析、以便高亮顯示markdown中的代碼塊
const markdown = require('markdown-it')({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' +
hljs.highlight(lang, str, true).value +
'</code></pre>';
} catch (__) {}
}
return '<pre class="hljs"><code>' + markdown.utils.escapeHtml(str) + '</code></pre>';
}
})
const resolve = dir => join(__dirname, '..', dir)
module.exports = {
// 只配置輸出路徑
output: {
filename: 'js/[name].js',
path: resolve('dist'),
publicPath: '/'
},
// 配置不一樣的loader以便資源加載
// eslint是標配,建議加上
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
'babel-loader',
'eslint-loader'
]
},
{
enforce: 'pre',
test: /\.vue$/,
loader: 'eslint-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url-loader'
},
{
test: /\.css$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}]
},
{
test: /\.less$/,
use: [{
loader: 'style-loader' // creates style nodes from JS strings
}, {
loader: 'css-loader' // translates CSS into CommonJS
}, {
loader: 'less-loader' // compiles Less to CSS
}]
},
// vue-markdown-loader可以把.md文件直接轉化成vue組件
{
test: /\.md$/,
loader: 'vue-markdown-loader',
options: markdown
}
]
},
resolve: {
// 該項配置可以在加載資源的時候省略後綴名
extensions: ['.js', '.vue', '.json', '.css', '.less'],
modules: [resolve('src'), 'node_modules'],
// 配置路徑別名
alias: {
'~src': resolve('src'),
'~components': resolve('src/view/components'),
'~pages': resolve('src/view/pages'),
'~assets': resolve('src/view/assets'),
'~store': resolve('src/store'),
'~static': resolve('src/document/static'),
'~docComps': resolve('src/document/doc_comps')
}
}
}
複製代碼
基礎配置好了,咱們就能夠開始開發模式的配置了。在當前目錄下,新建一個webpack.dev.config.js
文件,並寫入以下內容:
const { join } = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const resolve = dir => join(__dirname, '..', dir)
module.exports = merge(basicConfig, {
// 因爲是多頁應用,因此應該有2個入口文件
entry: {
app: './src/document/doc-entry.js',
view: './src/view/view-entry.js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
devtool: 'inline-source-map',
// webpack-dev-server配置
devServer: {
contentBase: resolve('/'),
compress: true,
hot: true,
inline: true,
publicPath: '/',
stats: 'minimal'
},
plugins: [
// 熱更新插件
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
// 把生成的js注入到入口html文件
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true,
chunks: ['app']
}),
new HtmlWebpackPlugin({
filename: 'view.html',
template: 'view.html',
inject: true,
chunks: ['view']
})
]
})
複製代碼
很是簡單的配置,值得注意的是由於多頁應用的緣故,入口文件和HtmlWebpackPlugin
都要寫多份。
接下來,還有把UI組件庫構建打包成npm包的配置。新建一個名爲webpack.build.config.js
的文件:
const { join } = require('path')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const resolve = dir => join(__dirname, '..', dir)
module.exports = merge(basicConfig, {
// 入口文件
entry: {
app: './src/view/components/index.js'
},
devtool: 'source-map',
// 輸出位置爲dist目錄,名字自定義,輸出格式爲umd格式
output: {
path: resolve('dist'),
filename: 'index.js',
library: 'my-project',
libraryTarget: 'umd'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 每一次打包都把上一次的清空
new CleanWebpackPlugin(['dist'], {
root: resolve('./')
}),
// 把靜態資源複製出去,以便實現UI換膚等功能
new CopyWebpackPlugin([
{ from: 'src/view/assets', to: 'assets' }
])
]
})
複製代碼
最後,咱們一塊兒來配置一鍵生成文檔網站的webpack.doc.config.js
:
const { join } = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const basicConfig = require('./webpack.base.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const resolve = dir => join(__dirname, '..', dir)
module.exports = merge(basicConfig, {
// 相似開發者模式,兩個入口文件,多了一個公共依賴包vendor
// 以`js/`開頭可以自動輸出到`js`目錄下
entry: {
'js/app': './src/document/doc-entry.js',
'js/view': './src/view/view-entry.js',
'js/vendor': [
'vue',
'vue-router'
]
},
devtool: 'source-map',
// 輸出文件加hash
output: {
path: resolve('docs'),
filename: '[name].[chunkhash:8].js',
chunkFilename: 'js/[name].[chunkhash:8].js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: ExtractTextPlugin.extract({
use: ['css-loader']
}),
less: ExtractTextPlugin.extract({
use: ['css-loader', 'less-loader']
})
}
}
}
]
},
plugins: [
// 提取css文件並指定其輸出位置和命名
new ExtractTextPlugin({
filename: 'css/[name].[contenthash:8].css',
allChunks: true
}),
// 抽離公共依賴
new webpack.optimize.CommonsChunkPlugin({
names: ['js/vendor', 'js/manifest']
}),
// 把構建出的靜態資源注入到多個入口html中
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunks: ['js/vendor', 'js/manifest', 'js/app'],
chunksSortMode: 'dependency'
}),
new HtmlWebpackPlugin({
filename: 'view.html',
template: 'view.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunks: ['js/vendor', 'js/manifest', 'js/view'],
chunksSortMode: 'dependency'
}),
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new CleanWebpackPlugin(['docs'], {
root: resolve('./')
})
]
})
複製代碼
經過上面這個配置,最終會產出一個index.html
和一個view.html
,以及各自所需的css和js文件。直接部署到靜態服務器上便可進行訪問。
多說一句,webpack的配置乍一看上去好像很複雜,但其實是至關簡單,webpack2的官方文檔也挺完善且易讀,推薦對webpack2不熟悉的朋友花點時間認真閱讀一下文檔。
至此,咱們已經把/webpack
目錄下的相關配置都弄好了,框架的基礎骨架已經搭建完畢,接下來開始對業務邏輯進行開發。
在根目錄下新建兩個入口文件index.html
和view.html
,分別添加一個<div id="app"></div>
和<div id="view"></div>
標籤。
進入/src
目錄,新建/document
和/view
目錄,按照前文目錄結構所示新建須要的目錄和文件。
具體的內容能夠看這裏,簡單來講就是初始化vue
應用,請暫時忽略router.js
當中的這一段代碼:
routeList.forEach((route) => {
routes.splice(1, 0, {
path: `/${route}`,
component: resolve => require([`~pages/${route}/index`], resolve)
});
});
複製代碼
這個是監聽目錄變化自動管理導航相關的功能,會在後面詳細介紹。
邏輯很簡單。/document
和/view
分別屬於文檔
和預覽
兩個應用,其中預覽
以iframe
的形式內嵌到文檔
應用頁面內,相關的操做其實都是在文檔
當中進行。當點擊導航的時候,文檔
應用會自動加載/view/pages/
下相關預覽頁文件夾的README.md
文件,同時修改iframe
的連接,實現內容的同步切換。
接下來,咱們一塊兒來研究一下如何監聽文件目錄變化,自動維護router
導航。
router
導航若是你有用過Nuxt,必定對其自動維護router
的功能不會陌生。若是沒有用過也不要緊,咱們本身來實現這個功能!
使用vue-router
的同窗可能都經歷過這麼一個痛點,每當新建頁面,都要往router.js
的數組裏面添加一個聲明,最終router.js
極可能會變成這樣:
const route = [
{ path: '/a', component: resolve => require(['a'], resolve) },
{ path: '/b', component: resolve => require(['b'], resolve) },
{ path: '/c', component: resolve => require(['c'], resolve) },
{ path: '/d', component: resolve => require(['d'], resolve) },
{ path: '/e', component: resolve => require(['e'], resolve) },
{ path: '/f', component: resolve => require(['f'], resolve) },
...
]
複製代碼
很煩,對不對?若是能夠自動維護就行了。首先咱們要作一個約定,約定好不一樣的「頁面」應該如何組織。
在/src/view/pages
目錄下,每新建一個「頁面」,咱們就要新建一個和該頁面同名的文件夾,往裏添加文檔README.md
和入口index.vue
,效果以下:
└── view
└── pages
├── 頁面A
│ ├── index.vue
│ └── README.md
├── 頁面B
│ ├── index.vue
│ └── README.md
├── 頁面C
│ ├── index.vue
│ └── README.md
└── 頁面D
├── index.vue
└── README.md
複製代碼
約定好了文件的組織方式,接下來咱們須要用到一個工具去負責監聽和處理。這裏咱們使用了chokidar來實現。
在/webpack
目錄下新建一個watcher.js
文件:
console.log('Watching dirs...');
const { resolve } = require('path')
const chokidar = require('chokidar')
const fs = require('fs')
const routeList = []
const watcher = chokidar.watch(resolve(__dirname, '../src/view/pages'), {
ignored: /(^|[\/\\])\../
})
watcher
// 監聽目錄添加
.on('addDir', (path) => {
let routeName = path.split('/').pop()
if (routeName !== 'pages' && routeName !== 'index') {
routeList.push(`'${routeName}'`)
fs.writeFileSync(resolve(__dirname, '../src/route-list.js'), `module.exports = [${routeList}]`)
}
})
// 監聽目錄變化(刪除、重命名)
.on('unlinkDir', (path) => {
let routeName = path.split('/').pop()
const itemIndex = routeList.findIndex((val) => {
return val === `'${routeName}'`
})
routeList.splice(itemIndex, 1)
fs.writeFileSync(resolve(__dirname, '../src/route-list.js'), `module.exports = [${routeList}]`)
})
module.exports = watcher
複製代碼
這裏面主要作了3件事:監聽目錄變化、維護目錄名列表、把列表寫入文件。當開啓watcher
後,能夠在/src
底下看到一個route-list.js
文件,內容以下:
module.exports = ['頁面A','頁面B','頁面C','頁面D']
複製代碼
而後咱們就能夠愉快地使用了……
// view-router.js
import routeList from '../route-list.js';
const routes = [
{ path: '/', component: resolve => require(['~pages/index'], resolve) },
{ path: '*', component: resolve => require(['~pages/index'], resolve) },
];
routeList.forEach((route) => {
routes.splice(1, 0, {
path: `/${route}`,
component: resolve => require([`~pages/${route}/index`], resolve)
});
});
複製代碼
// doc-router.js
import routeList from '../route-list.js';
const routes = [
{ path: '/', component: resolve => require(['~pages/index/README.md'], resolve) }
];
routeList.forEach((route) => {
routes.push({
path: `/${route}`,
component: resolve => require([`~pages/${route}/README.md`], resolve)
});
});
複製代碼
同理,在頁面的導航組件裏面,咱們也加載這個route-list.js
文件,實現導航內容的自動更新。
放個視頻,你們能夠感覺一下(SF居然不容許內嵌視頻,不科學): v.qq.com/x/page/a051…
這個框架的根本目的,實際上是爲了UI庫的開發。那麼咱們也應該對UI庫的文件組織進行約定。
進入/src/view/components
目錄,咱們的整個UI庫就放在這裏面:
└── components
├── index.js // 入口文件
├── 組件A
│ ├── index.vue
├── 組件B
│ ├── index.vue
├── 組件C
│ ├── index.vue
└── 組件D
└── index.vue
複製代碼
當中的index.js
,將會以vue plugin的方式編寫:
import MyHeader from './組件A'
import MyContent from './組件B'
import MyFooter from './組件C'
const install = (Vue) => {
Vue.component('my-header', MyHeader)
Vue.component('my-content', MyContent)
Vue.component('my-footer', MyFooter)
}
export {
MyHeader,
MyContent,
MyFooter
}
export default install
複製代碼
這樣,就可以在入口.js
文件中以Vue.use(UILibrary)
的形式對UI庫進行引用了。
擴展一下,考慮到UI可能有「換膚」的功能,那麼咱們能夠在/src/view
目錄下新建一個/assets
目錄,專門存放樣式相關的文件,這個目錄最終也會被打包到/dist
目錄下,在使用的時候引入相應樣式文件便可。
前面作了那麼多,最終咱們但願可以經過簡單的npm script
命令就把整個框架運行起來,應該怎麼作呢?
還記得在/webpack
目錄下的三個config.js
文件嗎?它們就是框架跑通的關鍵,可是咱們並不打算直接運行它們,而是在其之上封裝一下。
在/webpack
目錄下新建一個dev.js
文件,內容以下:
require('./watcher.js')
module.exports = require('./webpack.dev.config.js')
複製代碼
一樣的,分別新建build.js
和doc.js
文件,分別引入webpack.build.config.js
和webpack.doc.config.js
便可。
爲何要這麼作呢?由於webpack運行的時候會讀取config.js
文件,若是咱們但願在webpack工做以前先進行一些預處理,那麼這種作法就很是方便了,好比這裏添加的監聽目錄文件變化的功能。若是未來有什麼擴展,也能夠經過相似的方式進行。
接下來就是在package.json
裏面定義咱們的npm script
了:
"dev": "node_modules/.bin/webpack-dev-server --config webpack/dev.js",
"doc": "node_modules/.bin/webpack -p --config webpack/doc.js --progress --profile --colors",
"build": "node_modules/.bin/webpack -p --config webpack/build.js --progress --profile --colors"
複製代碼
值得注意的是,在生產模式下,須要加-p
才能充分啓動webpack2的tree-shaking
功能。
在根目錄下經過npm run 命令
的方式測試一下是否已經跑起來了呢?
本文篇幅較長,可以看到這裏的估計已經有點暈了吧。好久都沒有寫文章了,終於被我攢了個大招發出來,特別爽。搭建開發框架的過程是一個不斷嘗試,不斷google和stackoverflow的過程。在這個過程當中,大到對架構設計,小到對文件組織、工具使用,都有了更進一步的理解。
這個框架的運做模式,其實也是參考了不少業界內的方案,更多的是想要「偷懶」。能讓機器自動幫忙搞的,絕對不本身手動搞,這纔是技術進步的動力嘛。
該項目已經被改裝成vue-cli
的模板,經過vue init jrainlau/vue-donut#mobile
便可使用,歡迎嘗試,期待反饋和PR,謝謝你們~