【工程化】從0搭建VueJS移動端組件庫開發框架

以前發表過一篇《Vue-Donut——專用於構建Vue的UI組件庫的開發框架》,僅僅是對框架一個粗略的介紹,並無針對裏面的實現方式進行詳細說明。css

最近參與維護公司內部的一個針對移動端的UI組件庫,該組件庫缺少文檔和嚴格的文件組織結構。Vue-Donut的功能比較簡單,並不能方便地建立針對移動端UI組件庫的文檔和預覽。在參考了mint-ui等業界內成熟的方案以後,我在Vue-Donut的基礎上進行了拓展,最後搭建出了一個很是方便且自動化的開發框架。html

因爲以爲開發的過程很是有意思,也想記錄一下本身的開發思路,所以決定好好地寫一篇文章做爲記錄分享。vue

項目地址:https://github.com/jrainlau/v...node

1. 功能分析

首先咱們來規劃一下這個框架的最終目的是什麼:webpack

clipboard.png

如圖所示,經過該框架能夠生成一個文檔頁面。這個頁面分爲三個部分:導航、文檔、預覽。git

  1. 導航:經過導航切換不一樣組件的文檔和預覽。github

  2. 文檔:該類型組件所對應的文檔,以markdown形式書寫。web

  3. 預覽:該類型組件所對應的預覽頁面。vue-router

爲了讓組件的開發和文檔的維護更加高效,咱們但願這個框架能夠更加自動化。若是咱們只要開不一樣組件的預覽的頁面及其對應的說明文檔README,框架就能自動幫咱們生成對應的導航和HTML內容,豈不妙哉?除此以外,當咱們已經把全部的UI組件都開發好了,通通放在/components目錄下,若是可以經過框架進行一鍵構建打包,最後產出一個npm包,那麼別人使用這套UI組件庫也會變得很是簡單。帶着這個想法,咱們來分析一下咱們可能須要用到的關鍵技術。vue-cli

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',

  // 輸出文件加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目錄下的相關配置都弄好了,框架的基礎骨架已經搭建完畢,接下來開始對業務邏輯進行開發。

5. 業務邏輯開發

在根目錄下新建兩個入口文件index.htmlview.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.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...

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.jsdoc.js文件,分別引入webpack.build.config.jswebpack.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,謝謝你們~

相關文章
相關標籤/搜索