【工程化】从0搭建VueJS移动端组件库开发框架
之前發(fā)表過一篇《Vue-Donut——專用于構(gòu)建Vue的UI組件庫的開發(fā)框架》,僅僅是對(duì)框架一個(gè)粗略的介紹,并沒有針對(duì)里面的實(shí)現(xiàn)方式進(jìn)行詳細(xì)說明。
最近參與維護(hù)公司內(nèi)部的一個(gè)針對(duì)移動(dòng)端的UI組件庫,該組件庫缺乏文檔和嚴(yán)格的文件組織結(jié)構(gòu)。Vue-Donut的功能比較簡單,并不能方便地創(chuàng)建針對(duì)移動(dòng)端UI組件庫的文檔和預(yù)覽。在參考了mint-ui等業(yè)界內(nèi)成熟的方案之后,我在Vue-Donut的基礎(chǔ)上進(jìn)行了拓展,最后搭建出了一個(gè)非常方便且自動(dòng)化的開發(fā)框架。
由于覺得開發(fā)的過程非常有意思,也想記錄一下自己的開發(fā)思路,因此決定好好地寫一篇文章作為記錄分享。
項(xiàng)目地址:https://github.com/jrainlau/v...
1. 功能分析
首先我們來規(guī)劃一下這個(gè)框架的最終目的是什么:
如圖所示,通過該框架可以生成一個(gè)文檔頁面。這個(gè)頁面分為三個(gè)部分:導(dǎo)航、文檔、預(yù)覽。
導(dǎo)航:通過導(dǎo)航切換不同組件的文檔和預(yù)覽。
文檔:該類型組件所對(duì)應(yīng)的文檔,以markdown形式書寫。
預(yù)覽:該類型組件所對(duì)應(yīng)的預(yù)覽頁面。
為了讓組件的開發(fā)和文檔的維護(hù)更加高效,我們希望這個(gè)框架可以更加自動(dòng)化。如果我們只要開不同組件的預(yù)覽的頁面及其對(duì)應(yīng)的說明文檔README,框架就能自動(dòng)幫我們生成對(duì)應(yīng)的導(dǎo)航和HTML內(nèi)容,豈不妙哉?除此之外,當(dāng)我們已經(jīng)把所有的UI組件都開發(fā)好了,統(tǒng)統(tǒng)放在/components目錄下,如果能夠通過框架進(jìn)行一鍵構(gòu)建打包,最后產(chǎn)出一個(gè)npm包,那么別人使用這套UI組件庫也會(huì)變得非常簡單。帶著這個(gè)想法,我們來分析一下我們可能需要用到的關(guān)鍵技術(shù)。
2. 技術(shù)分析
使用webpack2作為框架核心:使用方便,高度可定制。同時(shí)webpack2文檔已經(jīng)相當(dāng)齊全,生態(tài)圈繁榮,社區(qū)活躍,遇到的坑基本上都可以在google和stackoverflow找到。
預(yù)覽頁面以iframe的形式插入到文檔頁面中:維護(hù)組件庫的時(shí)候只需要聚焦于組件的開發(fā)和預(yù)覽頁面的組織,無需分心維護(hù)導(dǎo)航和文檔,實(shí)現(xiàn)了解耦。因此意味著這是一個(gè)基于Vue.js的多頁應(yīng)用。
自動(dòng)生成導(dǎo)航:使用vue-router進(jìn)行頁面切換。每當(dāng)新建一個(gè)預(yù)覽頁面,就會(huì)自動(dòng)在頁面上生成對(duì)應(yīng)的導(dǎo)航,并自動(dòng)維護(hù)導(dǎo)航和路由的關(guān)系。因此,我們需要一套機(jī)制去監(jiān)聽文件結(jié)構(gòu)的變化。
自動(dòng)生成文檔:一個(gè)預(yù)覽頁面對(duì)應(yīng)一份文檔,所以文檔理應(yīng)以README.md的形式存放在對(duì)應(yīng)的預(yù)覽頁面文件夾內(nèi)。我們需要一個(gè)能夠把README.md直接轉(zhuǎn)化成html內(nèi)容的辦法。
開發(fā)者模式:通過一條命令,啟動(dòng)一個(gè)webpack-dev-server,提供熱更新和自動(dòng)刷新功能。
構(gòu)建打包模式:通過一條命令,自動(dòng)把/components目錄下的所有資源打包成一個(gè)npm包。
頁面構(gòu)建模式:通過一條命令,生成能夠直接部署使用的靜態(tài)資源文件。
通過對(duì)技術(shù)的梳理,我們腦海里面已經(jīng)有了一個(gè)印象,接下來就是一步一步地進(jìn)行開發(fā)了。
3. 梳理框架目錄結(jié)構(gòu)
一個(gè)好的目錄結(jié)構(gòu),能夠極大地方便我們接下來的工作。
. ├── index.html // 文檔頁的入口html ├── view.html // 預(yù)覽頁的入口html ├── package.json // 依賴聲明、npm script命令 ├── src │?? ├── document // 文檔頁目錄 │?? │?? ├── doc-app.vue // 文檔頁入口.vue文件 │?? │?? ├── doc-entry.js // 文檔頁入口.js文件 │?? │?? ├── doc-router.js // 文檔頁路由配置 │?? │?? ├── doc_comps // 文檔頁組件 │?? │?? └── static // 文檔頁靜態(tài)資源 │?? └── view // 預(yù)覽頁目錄 │?? ├── assets // 預(yù)覽頁靜態(tài)資源 │?? ├── components // UI組件庫 │?? ├── pages // 存放不同的預(yù)覽頁 │?? ├── view-app.vue // 預(yù)覽頁入口.vue文件 │?? ├── view-entry.js // 預(yù)覽頁入口.js文件 │?? └── view-router.js // 預(yù)覽頁路由配置 └── webpack├── webpack.base.config.js // webpack通用配置 ├── webpack.build.config.js // UI庫構(gòu)建打包配置├── webpack.dev.config.js // 開發(fā)模式配置└── webpack.doc.config.js // 靜態(tài)資源構(gòu)建配置可以看到,目錄結(jié)構(gòu)并不復(fù)雜,接下來我們首先對(duì)webpack進(jìn)行配置,以便我們能夠把項(xiàng)目跑起來。
4. webapck配置
4.1 基礎(chǔ)配置
進(jìn)入到/webpack目錄,新建一個(gè)webpack.base.config.js文件,其內(nèi)容如下:
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是標(biāo)配,建議加上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文件直接轉(zhuǎn)化成vue組件{test: /\.md$/,loader: 'vue-markdown-loader',options: markdown}]},resolve: {// 該項(xiàng)配置能夠在加載資源的時(shí)候省略后綴名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')}} }4.2 開發(fā)模式配置
基礎(chǔ)配置好了,我們就可以開始開發(fā)模式的配置了。在當(dāng)前目錄下,新建一個(gè)webpack.dev.config.js文件,并寫入如下內(nèi)容:
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, {// 由于是多頁應(yīng)用,所以應(yīng)該有2個(gè)入口文件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']})] })非常簡單的配置,值得注意的是因?yàn)槎囗搼?yīng)用的緣故,入口文件和HtmlWebpackPlugin都要寫多份。
4.3 構(gòu)件打包配置
接下來,還有把UI組件庫構(gòu)建打包成npm包的配置。新建一個(gè)名為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('./')}),// 把靜態(tài)資源復(fù)制出去,以便實(shí)現(xiàn)UI換膚等功能new CopyWebpackPlugin([{ from: 'src/view/assets', to: 'assets' }])] })4.4 一鍵生成文檔配置
最后,我們一起來配置一鍵生成文檔網(wǎng)站的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, {// 類似開發(fā)者模式,兩個(gè)入口文件,多了一個(gè)公共依賴包vendor// 以`js/`開頭能夠自動(dòng)輸出到`js`目錄下entry: {'js/app': './src/document/doc-entry.js','js/view': './src/view/view-entry.js','js/vendor': ['vue','vue-router']},devtool: 'source-map',// 輸出文件加hashoutput: {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']}),// 把構(gòu)建出的靜態(tài)資源注入到多個(gè)入口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('./')})] })通過上面這個(gè)配置,最終會(huì)產(chǎn)出一個(gè)index.html和一個(gè)view.html,以及各自所需的css和js文件。直接部署到靜態(tài)服務(wù)器上即可進(jìn)行訪問。
多說一句,webpack的配置乍一看上去好像很復(fù)雜,但實(shí)際上是相當(dāng)簡單,webpack2的官方文檔也挺完善且易讀,推薦對(duì)webpack2不熟悉的朋友花點(diǎn)時(shí)間認(rèn)真閱讀一下文檔。
至此,我們已經(jīng)把/webpack目錄下的相關(guān)配置都弄好了,框架的基礎(chǔ)骨架已經(jīng)搭建完畢,接下來開始對(duì)業(yè)務(wù)邏輯進(jìn)行開發(fā)。
5. 業(yè)務(wù)邏輯開發(fā)
在根目錄下新建兩個(gè)入口文件index.html和view.html,分別添加一個(gè)<div id="app"></div>和<div id="view"></div>標(biāo)簽。
進(jìn)入/src目錄,新建/document和/view目錄,按照前文目錄結(jié)構(gòu)所示新建需要的目錄和文件。
具體的內(nèi)容可以看這里,簡單來說就是初始化vue應(yīng)用,請(qǐng)暫時(shí)忽略router.js當(dāng)中的這一段代碼:
routeList.forEach((route) => {routes.splice(1, 0, {path: `/${route}`,component: resolve => require([`~pages/${route}/index`], resolve)}); });這個(gè)是監(jiān)聽目錄變化自動(dòng)管理導(dǎo)航相關(guān)的功能,會(huì)在后面詳細(xì)介紹。
邏輯很簡單。/document和/view分別屬于文檔和預(yù)覽兩個(gè)應(yīng)用,其中預(yù)覽以iframe的形式內(nèi)嵌到文檔應(yīng)用頁面內(nèi),相關(guān)的操作其實(shí)都是在文檔當(dāng)中進(jìn)行。當(dāng)點(diǎn)擊導(dǎo)航的時(shí)候,文檔應(yīng)用會(huì)自動(dòng)加載/view/pages/下相關(guān)預(yù)覽頁文件夾的README.md文件,同時(shí)修改iframe的鏈接,實(shí)現(xiàn)內(nèi)容的同步切換。
接下來,我們一起來研究一下如何監(jiān)聽文件目錄變化,自動(dòng)維護(hù)router導(dǎo)航。
6. 自動(dòng)維護(hù)router導(dǎo)航
如果你有用過Nuxt,一定對(duì)其自動(dòng)維護(hù)router的功能不會(huì)陌生。如果沒有用過也沒關(guān)系,我們自己來實(shí)現(xiàn)這個(gè)功能!
使用vue-router的同學(xué)可能都經(jīng)歷過這么一個(gè)痛點(diǎn),每當(dāng)新建頁面,都要往router.js的數(shù)組里面添加一個(gè)聲明,最終router.js很可能會(huì)變成這樣:
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) },... ]很煩,對(duì)不對(duì)?如果可以自動(dòng)維護(hù)就好了。首先我們要做一個(gè)約定,約定好不同的“頁面”應(yīng)該如何組織。
在/src/view/pages目錄下,每新建一個(gè)“頁面”,我們就要新建一個(gè)和該頁面同名的文件夾,往里添加文檔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約定好了文件的組織方式,接下來我們需要用到一個(gè)工具去負(fù)責(zé)監(jiān)聽和處理。這里我們使用了chokidar來實(shí)現(xiàn)。
在/webpack目錄下新建一個(gè)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// 監(jiān)聽目錄添加.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}]`)}})// 監(jiān)聽目錄變化(刪除、重命名).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件事:監(jiān)聽目錄變化、維護(hù)目錄名列表、把列表寫入文件。當(dāng)開啟watcher后,可以在/src底下看到一個(gè)route-list.js文件,內(nèi)容如下:
module.exports = ['頁面A','頁面B','頁面C','頁面D']然后我們就可以愉快地使用了……
// view-router.jsimport 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.jsimport 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)}); });同理,在頁面的導(dǎo)航組件里面,我們也加載這個(gè)route-list.js文件,實(shí)現(xiàn)導(dǎo)航內(nèi)容的自動(dòng)更新。
放個(gè)視頻,大家可以感受一下(SF竟然不允許內(nèi)嵌視頻,不科學(xué)):
https://v.qq.com/x/page/a0510...
7. UI庫文件組織約定
這個(gè)框架的根本目的,其實(shí)是為了UI庫的開發(fā)。那么我們也應(yīng)該對(duì)UI庫的文件組織進(jìn)行約定。
進(jìn)入/src/view/components目錄,我們的整個(gè)UI庫就放在這里面:
└── components├── index.js // 入口文件├── 組件A│ ├── index.vue├── 組件B│ ├── index.vue├── 組件C│ ├── index.vue└── 組件D└── index.vue當(dāng)中的index.js,將會(huì)以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)的形式對(duì)UI庫進(jìn)行引用了。
擴(kuò)展一下,考慮到UI可能有“換膚”的功能,那么我們可以在/src/view目錄下新建一個(gè)/assets目錄,專門存放樣式相關(guān)的文件,這個(gè)目錄最終也會(huì)被打包到/dist目錄下,在使用的時(shí)候引入相應(yīng)樣式文件即可。
8. 構(gòu)建運(yùn)行命令
前面做了那么多,最終我們希望能夠通過簡單的npm script命令就把整個(gè)框架運(yùn)行起來,應(yīng)該怎么做呢?
還記得在/webpack目錄下的三個(gè)config.js文件嗎?它們就是框架跑通的關(guān)鍵,但是我們并不打算直接運(yùn)行它們,而是在其之上封裝一下。
在/webpack目錄下新建一個(gè)dev.js文件,內(nèi)容如下:
require('./watcher.js') module.exports = require('./webpack.dev.config.js')同樣的,分別新建build.js和doc.js文件,分別引入webpack.build.config.js和webpack.doc.config.js即可。
為什么要這么做呢?因?yàn)閣ebpack運(yùn)行的時(shí)候會(huì)讀取config.js文件,如果我們希望在webpack工作之前先進(jìn)行一些預(yù)處理,那么這種做法就非常方便了,比如這里添加的監(jiān)聽目錄文件變化的功能。如果將來有什么擴(kuò)展,也可以通過類似的方式進(jìn)行。
接下來就是在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"值得注意的是,在生產(chǎn)模式下,需要加-p才能充分啟動(dòng)webpack2的tree-shaking功能。
在根目錄下通過npm run 命令的方式測(cè)試一下是否已經(jīng)跑起來了呢?
9. 后續(xù)工作
添加單元測(cè)試
加入PWA功能
10. 尾聲
本文篇幅較長,能夠看到這里的估計(jì)已經(jīng)有點(diǎn)暈了吧。很久都沒有寫文章了,終于被我攢了個(gè)大招發(fā)出來,特別爽。搭建開發(fā)框架的過程是一個(gè)不斷嘗試,不斷google和stackoverflow的過程。在這個(gè)過程中,大到對(duì)架構(gòu)設(shè)計(jì),小到對(duì)文件組織、工具使用,都有了更進(jìn)一步的理解。
這個(gè)框架的運(yùn)作模式,其實(shí)也是參考了很多業(yè)界內(nèi)的方案,更多的是想要“偷懶”。能讓機(jī)器自動(dòng)幫忙搞的,絕對(duì)不自己手動(dòng)搞,這才是技術(shù)進(jìn)步的動(dòng)力嘛。
該項(xiàng)目已經(jīng)被改裝成vue-cli的模板,通過vue init jrainlau/vue-donut#mobile即可使用,歡迎嘗試,期待反饋和PR,謝謝大家~
總結(jié)
以上是生活随笔為你收集整理的【工程化】从0搭建VueJS移动端组件库开发框架的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 项目笔记:2017年(SSM架构)
- 下一篇: vue中过滤器比较两个数组取相同值