以前發表過一篇《Vue-Donut——專用於構建Vue的UI組件庫的開發框架》,僅僅是對框架一個粗略的介紹,並無針對裏面的實現方式進行詳細說明。css
最近參與維護公司內部的一個針對移動端的UI組件庫,該組件庫缺少文檔和嚴格的文件組織結構。Vue-Donut
的功能比較簡單,並不能方便地建立針對移動端UI組件庫的文檔和預覽。在參考了mint-ui
等業界內成熟的方案以後,我在Vue-Donut
的基礎上進行了拓展,最後搭建出了一個很是方便且自動化的開發框架。html
因爲以爲開發的過程很是有意思,也想記錄一下本身的開發思路,所以決定好好地寫一篇文章做爲記錄分享。vue
項目地址:https://github.com/jrainlau/v...node
首先咱們來規劃一下這個框架的最終目的是什麼:webpack
如圖所示,經過該框架能夠生成一個文檔頁面。這個頁面分爲三個部分:導航、文檔、預覽。git
導航:經過導航切換不一樣組件的文檔和預覽。github
文檔:該類型組件所對應的文檔,以markdown形式書寫。web
預覽:該類型組件所對應的預覽頁面。vue-router
爲了讓組件的開發和文檔的維護更加高效,咱們但願這個框架能夠更加自動化。若是咱們只要開不一樣組件的預覽的頁面及其對應的說明文檔README
,框架就能自動幫咱們生成對應的導航和HTML內容,豈不妙哉?除此以外,當咱們已經把全部的UI組件都開發好了,通通放在/components
目錄下,若是可以經過框架進行一鍵構建打包,最後產出一個npm包,那麼別人使用這套UI組件庫也會變得很是簡單。帶着這個想法,咱們來分析一下咱們可能須要用到的關鍵技術。vue-cli
使用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居然不容許內嵌視頻,不科學):
https://v.qq.com/x/page/a0510...
這個框架的根本目的,實際上是爲了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 命令
的方式測試一下是否已經跑起來了呢?
添加單元測試
加入PWA功能
本文篇幅較長,可以看到這裏的估計已經有點暈了吧。好久都沒有寫文章了,終於被我攢了個大招發出來,特別爽。搭建開發框架的過程是一個不斷嘗試,不斷google和stackoverflow的過程。在這個過程當中,大到對架構設計,小到對文件組織、工具使用,都有了更進一步的理解。
這個框架的運做模式,其實也是參考了不少業界內的方案,更多的是想要「偷懶」。能讓機器自動幫忙搞的,絕對不本身手動搞,這纔是技術進步的動力嘛。
該項目已經被改裝成vue-cli
的模板,經過vue init jrainlau/vue-donut#mobile
便可使用,歡迎嘗試,期待反饋和PR,謝謝你們~