【工程化】从0搭建VueJS移动端组件库开发框架
之前發表過一篇《Vue-Donut——專用于構建Vue的UI組件庫的開發框架》,僅僅是對框架一個粗略的介紹,并沒有針對里面的實現方式進行詳細說明。
最近參與維護公司內部的一個針對移動端的UI組件庫,該組件庫缺乏文檔和嚴格的文件組織結構。Vue-Donut的功能比較簡單,并不能方便地創建針對移動端UI組件庫的文檔和預覽。在參考了mint-ui等業界內成熟的方案之后,我在Vue-Donut的基礎上進行了拓展,最后搭建出了一個非常方便且自動化的開發框架。
由于覺得開發的過程非常有意思,也想記錄一下自己的開發思路,因此決定好好地寫一篇文章作為記錄分享。
項目地址:https://github.com/jrainlau/v...
1. 功能分析
首先我們來規劃一下這個框架的最終目的是什么:
如圖所示,通過該框架可以生成一個文檔頁面。這個頁面分為三個部分:導航、文檔、預覽。
導航:通過導航切換不同組件的文檔和預覽。
文檔:該類型組件所對應的文檔,以markdown形式書寫。
預覽:該類型組件所對應的預覽頁面。
為了讓組件的開發和文檔的維護更加高效,我們希望這個框架可以更加自動化。如果我們只要開不同組件的預覽的頁面及其對應的說明文檔README,框架就能自動幫我們生成對應的導航和HTML內容,豈不妙哉?除此之外,當我們已經把所有的UI組件都開發好了,統統放在/components目錄下,如果能夠通過框架進行一鍵構建打包,最后產出一個npm包,那么別人使用這套UI組件庫也會變得非常簡單。帶著這個想法,我們來分析一下我們可能需要用到的關鍵技術。
2. 技術分析
使用webpack2作為框架核心:使用方便,高度可定制。同時webpack2文檔已經相當齊全,生態圈繁榮,社區活躍,遇到的坑基本上都可以在google和stackoverflow找到。
預覽頁面以iframe的形式插入到文檔頁面中:維護組件庫的時候只需要聚焦于組件的開發和預覽頁面的組織,無需分心維護導航和文檔,實現了解耦。因此意味著這是一個基于Vue.js的多頁應用。
自動生成導航:使用vue-router進行頁面切換。每當新建一個預覽頁面,就會自動在頁面上生成對應的導航,并自動維護導航和路由的關系。因此,我們需要一套機制去監聽文件結構的變化。
自動生成文檔:一個預覽頁面對應一份文檔,所以文檔理應以README.md的形式存放在對應的預覽頁面文件夾內。我們需要一個能夠把README.md直接轉化成html內容的辦法。
開發者模式:通過一條命令,啟動一個webpack-dev-server,提供熱更新和自動刷新功能。
構建打包模式:通過一條命令,自動把/components目錄下的所有資源打包成一個npm包。
頁面構建模式:通過一條命令,生成能夠直接部署使用的靜態資源文件。
通過對技術的梳理,我們腦海里面已經有了一個印象,接下來就是一步一步地進行開發了。
3. 梳理框架目錄結構
一個好的目錄結構,能夠極大地方便我們接下來的工作。
. ├── 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進行配置,以便我們能夠把項目跑起來。
4. webapck配置
4.1 基礎配置
進入到/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')}} }4.2 開發模式配置
基礎配置好了,我們就可以開始開發模式的配置了。在當前目錄下,新建一個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都要寫多份。
4.3 構件打包配置
接下來,還有把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' }])] })4.4 一鍵生成文檔配置
最后,我們一起來配置一鍵生成文檔網站的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',// 輸出文件加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']}),// 把構建出的靜態資源注入到多個入口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目錄下的相關配置都弄好了,框架的基礎骨架已經搭建完畢,接下來開始對業務邏輯進行開發。
5. 業務邏輯開發
在根目錄下新建兩個入口文件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導航。
6. 自動維護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.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)}); });同理,在頁面的導航組件里面,我們也加載這個route-list.js文件,實現導航內容的自動更新。
放個視頻,大家可以感受一下(SF竟然不允許內嵌視頻,不科學):
https://v.qq.com/x/page/a0510...
7. UI庫文件組織約定
這個框架的根本目的,其實是為了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目錄下,在使用的時候引入相應樣式文件即可。
8. 構建運行命令
前面做了那么多,最終我們希望能夠通過簡單的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 命令的方式測試一下是否已經跑起來了呢?
9. 后續工作
添加單元測試
加入PWA功能
10. 尾聲
本文篇幅較長,能夠看到這里的估計已經有點暈了吧。很久都沒有寫文章了,終于被我攢了個大招發出來,特別爽。搭建開發框架的過程是一個不斷嘗試,不斷google和stackoverflow的過程。在這個過程中,大到對架構設計,小到對文件組織、工具使用,都有了更進一步的理解。
這個框架的運作模式,其實也是參考了很多業界內的方案,更多的是想要“偷懶”。能讓機器自動幫忙搞的,絕對不自己手動搞,這才是技術進步的動力嘛。
該項目已經被改裝成vue-cli的模板,通過vue init jrainlau/vue-donut#mobile即可使用,歡迎嘗試,期待反饋和PR,謝謝大家~
總結
以上是生活随笔為你收集整理的【工程化】从0搭建VueJS移动端组件库开发框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 项目笔记:2017年(SSM架构)
- 下一篇: ERP问题解决