合格前端系列第十彈-揭祕元件庫一二事

qiangdada發表於2018-03-17

一、寫在前面

1、靈感來源

我平常比較喜歡對一些東西做一些記錄和總結,其中包括一些元件,積累的量比較多的時候,發現零散的堆積已經不太適合進行管理了。

於是我開始思考,有什麼好的辦法可以比較規範地來管理這些比較零散的東西呢?如果以元件庫這種形式來對元件進行管理的話,會不會更適合自己的積累並方便以後的工作呢?

於是我開始參考市場上一些優秀的 UI 元件庫,比如 element-uivuxvant等,對其原始碼進行拜讀,瞭解其架構的搭建,隨後整理出一套屬於自己的移動端 UI 元件庫 vui

我在業餘時間活躍於各大技術社群,常有一些或工作一段時間的、或還在準備找實習工作的小夥伴問筆者一些問題:怎樣沉澱自己,做自己的框架、輪子、庫?怎樣做一個元件庫?自己做過一個元件庫會不會成為簡歷的亮點?你能不能寫一些有關元件庫開發的相關文章?…...

本著答惑解疑和分享的心情,這篇博文便誕生了。

2、最終效果圖

api-1
PC 端預覽圖

api-2
移動端預覽圖

3、問題交流

如果小夥伴在閱讀文章實戰的時候有什麼問題的話,歡迎加入討論群一起討論(群裡除了一群大佬天天騷話外還有一群妹紙哦 ~ )

前端大雜燴:731175396

github:github.com/xuqiang521

廢話不多說,接下來,讓我們直接進入到實戰篇吧 ~

二、環境搭建

1、搭建 NODE 環境

這裡我只談 Mac 和 window 下 NODE 的安裝

i. Mac 下的安裝

  • 如果你還沒有安裝 mac 軟體包管理器 homebrew 的話第一步得先安裝它

    /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    複製程式碼
  • 使用 homebrew 安裝 node

    brew install node
    複製程式碼

ii. window 下的安裝

window 環境的話直接進入 node 官網進行對應版本的下載,然後瘋狂點選下一步即可安裝完成

安裝完成後,檢視 nodenpm 版本

node -v
# v9.6.1
npm -v
# 5.6.0
複製程式碼

自此你電腦上 node 環境就已經搭建好了,接下來,我們需要安裝元件庫構建依賴的腳手架了。

2、構建一個 vue 專案

i. 安裝 vue-cli

# 全域性安裝
npm i -g vue-cli
# 檢視vue-cli用法
vue -h
# 檢視版本
vue -V
# 2.9.3
複製程式碼

ii. 使用 vue-cli 構建專案

使用 vue-cliinit 指令初始化一個名為 personal-components-library 的專案

# 專案基於 webpack
vue init webpack personal-components-library
複製程式碼

構建時腳手架會讓你填寫專案的一些描述和依賴,參考下面我選擇的內容進行填寫即可

# 專案名稱
Project name? personal-components-library
# 專案描述
Project description? A Personal Vue.js components Library project
# 專案作者
Author? qiangdada
# 專案構建 vue 版本(選擇預設項)
Vue build? standalone
# 是否下載 vue-router (後期會用到,這裡選 Yes)
Install vue-router? Yes
# 是否下載 eslint (為了制定合理的開發規範,這個必填)
Use ESLint to lint your code? Yes
# 安裝預設的標準 eslint 規則
Pick an ESLint preset? Standard
# 構建測試案例
Set up unit tests? Yes
# 安裝 test 依賴 (選擇 karma + mocha)
Pick a test runner? karma
# 構建 e2e 測試案例 (No)
Setup e2e tests with Nightwatch? No
# 專案初始化完是否安裝依賴 (npm)
Should we run `npm install` for you after the project has been created? (recom
mended) npm
複製程式碼

當你選好之後就可以等了,vue-cli 會幫你把專案搭建好,並且進行依賴安裝。

初始化專案的結構如下:

├── build                     webpack打包以及本地服務的檔案都在裡面
├── config              	  不同環境的配置都在這裡
├── index.html                入口html
├── node_modules              npm安裝的依賴包都在這裡面
├── package.json              專案配置資訊
├── README.md              	  專案介紹
├── src                       我們的原始碼
│   ├── App.vue               vue主入口檔案
│   ├── assets                資源存放(如圖片)
│   ├── components            可以複用的模組放在這裡面
│   ├── main.js               入口js
│   ├── router                路由管理
└── webpack.config.js         webpack配置檔案
├── static                    被copy的靜態資源存放地址
├── test                      測試文件和案例
複製程式碼

如果你用 npm 下載依賴太慢或者部分資源被牆的話,建議利用 cnpm 進行依賴的下載

# 全域性安裝 cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
# 使用 cnpm 進行依賴安裝
cnpm i
複製程式碼

依賴安裝完成就可以啟動你的 vue 專案啦 ~

npm run dev
複製程式碼

然後訪問 http://localhost:8080 便可以成功訪問通過 vue-cli 構建出來的 vue 專案,至此你元件庫依賴的開發環境便已經安裝完畢。

三、構建新目錄

首先,我們要明確本節的目的,我們需要修改目錄,為了更好的開發元件庫。

我們上一節已經把搭建好了 vue 專案,但初始化出來的專案的目錄卻不能滿足一個元件庫的後續開發和維護。因此這一章節我們需要做的事情就是改造初始化出來的 vue 專案的目錄,將其變成元件庫需要的目錄,接下來就讓我們行動起來吧。

1、元件庫目錄

  1. build:這個目錄主要用來存放構建相關的檔案
  2. packages: 這個目錄下主要用來存放所有元件
  3. examples:這個目錄下主要用來存放元件庫的展示 demo文件的所有相關檔案
  4. src:這個目錄主要用來管理元件的註冊的主入口,工具,mixins等(對此我們需要改造初始化出來的 src 目錄)
  5. test:這個目錄用來存放測試案例(繼續延用初始化出來的目錄)
  6. lib:元件庫打包出來後的目錄
  7. .github:作為一個開源元件庫,如果你想和別人一起開發,那麼這個目錄用來存放你自己定義的一些開發規則指導,也是非常不錯的

OK,開始改造你初始化出來的專案的目錄吧。

2、讓專案能夠重新跑起來

i. 改造 examples 目錄

從前面我們知道,我們啟動本地服務的時候,頁面的的主入口檔案是 index.html 。現在我們第一步就是講頁面的主入口 htmljs 挪到 examples 目錄下面。examples 具體目錄如下

├── assets						css,圖片等資源都在這
├── pages                     	路由中所有的頁面
├── src              	      	
│   ├── components            	demo中可以複用的模組放在這裡面
│   ├── index.js              	入口js
│   ├── index.tpl              	頁面入口
│   ├── App.vue               	vue主入口檔案
│   ├── router.config.js		路由js
複製程式碼

各個檔案修改後的程式碼如下

  • index.js

    import Vue from 'vue'
    import App from './App'
    import router from './router.config'
    
    Vue.config.productionTip = false
    
    /* eslint-disable no-new */
    new Vue({
      el: '#app-container',
      router,
      components: { App },
      template: '<App/>'
    })
    複製程式碼
  • index.tpl

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
      <title>My Component Library</title>
    </head>
    <body>
      <div id="app-container">
        <app></app>
      </div>
    </body>
    </html>
    複製程式碼
  • App.vue

    <template>
      <div id="app">
        <router-view/>
      </div>
    </template>
    
    <script>
    export default {
      name: 'App'
    }
    </script>
    複製程式碼
  • router.config.js

    import Vue from 'vue'
    import Router from 'vue-router'
    import hello from '../pages/hello'  // 請自行去pages下面建立一個hello.vue,以方便之後的測試
    
    Vue.use(Router)
    
    export default new Router({
      routes: [
        {
          path: '/',
          component: hello
        }
      ]
    })
    複製程式碼

ii. 改造 src 目錄

src 目錄主要用來存放元件的註冊的主入口檔案,工具方法,mixins等檔案。我們從上面 examples 的目錄可以知道,原先 src 中的一些檔案是需要刪掉的,改造後的目錄如下

├── mixins						mixins方法存放在這
├── utils                     	一些常用輔助方法存放在這
├── index.js              	    元件註冊主入口
複製程式碼

iii. 改造 build 目錄下部分打包檔案

想想小夥伴看到這,也應該知道我們現在需要做的事是什麼。沒錯,就是修改本地服務的入口檔案。如果只是能夠跑起來,那麼修改 entry 中的 js 入口以及 html-webpack-plugin 的頁面入口引用即可。程式碼如下(只放關鍵性程式碼)

entry: {
  'vendor': ['vue', 'vue-router'],
  'vui': './examples/src/index.js'
},
// ...
plugins: [
  // ...
  // 將入口改成examples/src/index.tpl
  new HtmlWebpackPlugin({
    chunks: ['vendor', 'vui'],
    template: 'examples/src/index.tpl',
    filename: 'index.html',
    inject: true
  })
]
複製程式碼

OK,修改好了。重新執行一次 npm run dev,然後你的專案便能在新的入口檔案下跑起來

3、在本地使用元件

這一小節,我們需要實現的就是我們本地啟動的服務,能夠使用 packages 下面的元件。下面我們開發一個最簡單的 hello 元件進行講解

i. 在 packages 下建立一個 hello 元件

為了有一個良好約束性,這裡我們約束:一個元件在開始寫之前,得有一個規定的目錄及檔名進行統一管理。 packages 目錄下 hello 元件下的檔案如下

├── hello						
│   ├── hello.vue
複製程式碼

hello.vue 內容如下

<template>
  <div class="v-hello">
    hello {{ message }}
  </div>
</template>

<script>
export default {
  name: 'v-hello',
  props: {
    message: String
  }
}
</script>
複製程式碼

ii. 在 src/index.js 對元件進行註冊

sec/index.js 檔案在上面也有提及,它主要用來管理我們元件庫中所有元件的註冊

import Hello from '../packages/hello'

const install = function (Vue) {
  if (install.installed) return

  Vue.component(Hello.name, Hello)
}

if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default {
  install,
  Hello
}
複製程式碼

iii. 在 examples/src/index.js 入口 js 檔案中進行引用

接下來,我需要在上節改造好的 examples 中對我們寫好的 hello 元件進行引用

import vui from 'src/index.js'
// 完整引用
Vue.use(vui)
// 獨立引用
const { Hello } = vui
Vue.component(Hello.name, Hello)
複製程式碼

iv. 在 examples/pages/hello.vue 直接使用

examples/pages 中我們需要建立和元件名同名的 demo 檔案,並對元件進行使用

<v-hello message="my component library"></v-hello>
複製程式碼

hello
Hello

當你執行的結果和上圖一樣的話,那麼恭喜。你又成功向元件庫的開發邁開了一步 ~

看到這裡,我需要各位讀者能夠按照自己的喜好對檔案進行集中化的管理(當然,也可以參考我上面給出的 demo),只有這樣,才能夠讓我們元件庫後續的開發工作能夠順暢起來。

下一節,我們會優化 build 下面的打包檔案,並帶著大家把自己的開發好的元件釋出到 npm 官網,讓你的元件庫能夠被人更方便的使用!

四、改造打包檔案,釋出 npm 包

老規矩,章節正文開始之前,我們得清楚本章節需要做什麼以及為什麼這麼做。

  1. 由於腳手架初始的專案對於 build 檔案只有一個集中打包的檔案 webpack.prod.conf.js

  2. 為了之後我們的元件庫能更好的使用起來,我們需要將元件庫對應的模組抽離全部打包到 vui.js 一個檔案中(名字你喜歡啥取啥),這樣我們之後就能通過以下方式來引用我們得元件庫了

    import Vue from 'vue'
    import vui from 'x-vui'
    Vue.use(vui)
    複製程式碼
  3. 我們還需要將 examples 中相關的檔案進行打包管理,因為我們後面還得開發元件庫的文件官網,而文件官網相關入口都在 examples

1、改造 build 打包檔案

i. 本地服務檔案的整合

我們從初始化出來專案可以看到,build 檔案中的有關 webpack 的檔案如下

├── webpack.base.conf.js					基礎配置檔案
├── webpack.dev.conf.js                     本地服務配置檔案
├── webpack.prod.conf.js             	    打包配置檔案
├── webpack.test.conf.js             	    測試配置檔案(這裡先不做過多描述)
複製程式碼

初始化的打包 output 輸出的目錄是 dist ,這個目錄是整個專案打包後輸出的目錄,並不是我們元件庫需要的目錄。既然不是我們想要的,那我們想在需要的目錄是怎麼樣的呢?

  1. 元件庫主入口 js 檔案 lib/vui.js(元件庫 js 主檔案)
  2. 元件庫主入口 css 檔案 lib/vui-css/index.css (元件庫 css 主檔案,這一章節我們對 css 打包不做過多描述,後面章節會單獨講解)
  3. examples 檔案打包出來的檔案 examples/dist(後期文件官網的主入口)

既然目標已經定了,接下來我們需要做的就是先整理好相關的 webpack 打包檔案,如下

├── webpack.base.conf.js			基礎配置檔案(配置方面和webpack.dev.conf.js的配置進行部分整合)
├── webpack.dev.conf.js             本地服務配置檔案(將純配置檔案進行對應的刪減)
├── webpack.build.js             	元件庫入口檔案打包配置檔案(將webpack.prod.conf.js重新命名)
├── webpack.build.min.js            examples展示檔案打包配置檔案(新增檔案)
複製程式碼

1、webpack.base.conf.js

開始改造 webpack.base.conf.js 檔案之前我們需要先了解兩個打包檔案需要做的事情

  1. webpack.build.js :輸出 lib/vui.js 元件庫 js 主檔案,會用到 webpack.base.conf.jswebpack.dev.conf.js 相關配置
  2. webpack.build.min.js :輸出 examples/dist 文件相關檔案,會用到 webpack.base.conf.jswebpack.dev.conf.js 相關配置

既然兩個 webpack 打包檔案都會用到 webpack.base.conf.jswebpack.dev.conf.js 相關配置,那麼我們何不將相同的一些檔案都整合到 webpack.base.conf.js 檔案中呢?目標明確了,接下來跟著我開搞吧

'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const createLintingRule = () => ({
  test: /\.(js|vue)$/,
  loader: 'eslint-loader',
  enforce: 'pre',
  include: [resolve('src'), resolve('test')],
  options: {
    formatter: require('eslint-friendly-formatter'),
    emitWarning: !config.dev.showEslintErrorsInOverlay
  }
})
module.exports = {
  context: path.resolve(__dirname, '../'),
  // 檔案入口 
  entry: {
    'vendor': ['vue', 'vue-router'],
    'vui': './examples/src/index.js'
  },
  // 輸出目錄
  output: {
    path: path.join(__dirname, '../examples/dist'),
    publicPath: '/',
    filename: '[name].js'
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    // 此處新增了一些 alias 別名
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
      'src': resolve('src'),
      'packages': resolve('packages'),
      'lib': resolve('lib'),
      'components': resolve('examples/src/components')
    }
  },
  // 延用原先的大部分配置
  module: {
    rules: [
      // 原先的配置...
      // 整合webpack.dev.conf.js中css相關配置
      ...utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
    ]
  },
  // 延用原先的配置
  node: {
    // ...
  },
  devtool: config.dev.devtool,
  // 整合webpack.dev.conf.js中的devServer選項
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },
  // 整合webpack.dev.conf.js中的plugins選項
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    // 頁面主入口
    new HtmlWebpackPlugin({
      chunks: ['manifest', 'vendor', 'vui'],
      template: 'examples/src/index.tpl',
      filename: 'index.html',
      inject: true
    })
  ]
}
複製程式碼

2、webpack.dev.conf.js

這裡只需要將整合到 webpack.base.conf.js 中的配置刪掉即可,避免程式碼重複

'use strict'
const utils = require('./utils')
const config = require('../config')
const baseWebpackConfig = require('./webpack.base.conf')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      process.env.PORT = port
      baseWebpackConfig.devServer.port = port

      baseWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${baseWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(baseWebpackConfig)
    }
  })
})
複製程式碼

webpack.base.conf.jswebpack.dev.conf.js 兩個檔案都調整好後,重新執行一下 npm run dev

run-dev
npm run dev

出現上圖表示此時你們的本地服務檔案已經按照預想修改成功啦 ~

ii. 改造打包檔案

1、webpack.build.js

本檔案主要目的就是將元件庫中所有元件相關的檔案打包到一起並輸出 lib/vui.js 主檔案

'use strict'
const webpack = require('webpack')
const config = require('./webpack.base.conf')
// 修改入口檔案
config.entry = {
  'vui': './src/index.js'
}
// 修改輸出目錄
config.output = {
  filename: './lib/[name].js',
  library: 'vui',
  libraryTarget: 'umd'
}
// 配置externals選項
config.externals = {
  vue: {
    root: 'Vue',
    commonjs: 'vue',
    commonjs2: 'vue',
    amd: 'vue'
  }
}
// 配置plugins選項
config.plugins = [
  new webpack.DefinePlugin({
    'process.env': require('../config/prod.env')
  })
]
// 刪除devtool配置
delete config.devtool

module.exports = config
複製程式碼

2、webpack.build.min.js

該檔案主要目的是為了單開一個打包地址,將 examples 中相關的檔案輸出到 examples/dist 目錄(即後續文件官網入口)

const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

const webpackConfig = merge(baseWebpackConfig, {
  output: {
    chunkFilename: '[id].[hash].js',
    filename: '[name].min.[hash].js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      output: {
        comments: false
      },
      sourceMap: false
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: '[name].[contenthash].css',
      allChunks: true,
    }),
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),
  ]
})

module.exports = webpackConfig
複製程式碼

當我們把這些檔案都弄好的時候,最後一步就是將打包命令寫入到 package.jsonscripts 中了

"scripts": {
  "build:vui": "webpack --progress --hide-modules --config build/webpack.build.js && rimraf examples/dist && cross-env NODE_ENV=production webpack --progress --hide-modules --config build/webpack.build.min.js"
},
複製程式碼

執行命令,npm run build:vui,走你

build
npm run build:vui

至此,有關本地服務以及兩個打包檔案便已改造完成,下面我們嘗試將 npm 使用起來 ~

2、釋出 npm 包

注意,如果你還沒有屬於自己的 npm 賬號的話,請先自行到 npm 官網註冊一個賬號,點選這裡進入官網進行註冊 ,註冊步驟比較簡單,這裡我就不過多做描述了,如果有疑問,可以在討論群問我

i. 先來個最簡單的 demo

mkdir qiangdada520-npm-test
cd qiangdada520-npm-test
# npm 包主入口js檔案
touch index.js
# npm 包首頁介紹(具體啥內容你自行寫入即可)
touch README.md
npm init
# package name: (qiangdada520-npm-test)
# version: (1.0.0)
# description: npm test
# entry point: (index.js) index.js
# test command:
# git repository:
# keywords: npm test
# author: qiangdada
# license: (ISC)
複製程式碼

然後確定,則會生成 package.json ,如下

{
  "name": "qiangdada-npm-test",
  "version": "1.0.0",
  "description": "npm test",
  "main": "index.js",  // npm 包主入口js檔案
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "npm",
    "test"
  ],
  "author": "qiangdada",
  "license": "MIT"
}
複製程式碼

接下來,我們需要在本地連線我們註冊號的 npm 賬號

npm adduser
# Username: 填寫你自己的npm賬號
# Password: npm賬號密碼
# Email: (this IS public) 你npm賬號的認證郵箱
# Logged in as xuqiang521 on https://registry.npmjs.org/.  連線成功
複製程式碼

執行 npm publish 開始釋出

npm publish
# + qiangdada-npm-test@1.0.0
複製程式碼

這個時候你再去 npm 官網就能搜尋並看到你剛釋出好的包啦 ~

ii. 釋出元件庫

目前元件庫,我們寫了一個最簡單的 hello 元件,不過這絲毫不影響我們將其釋出到 npm 官網,並且釋出步驟和上面的例子一樣簡單。

修改 package.json 檔案中的部分描述

// npm 包js入口檔案改為 lib/vui.js
"main": "lib/vui.js",
// npm 釋出出去的包包含的檔案
"files": [
  "lib",
  "src",
  "packages"
],
// 將包的屬性改為公共可釋出的
"private": false,
複製程式碼

注意,測試 npm 包釋出的時候,記得每一次的 package.json 中的 version 版本要比上一次高。

開始釋出

# 打包,輸出lib/vui.js
npm run build:vui
# 釋出
npm publish
# + component-library-test@1.0.1
複製程式碼

iii. 使用我們釋出到 npm 的元件

選擇一個本地存在的 vue 專案,進入到專案

npm i component-library-test
# or 
cnpm i component-library-test
複製程式碼

在專案入口檔案中進行元件的註冊

import Vue from 'vue'
import vui from 'component-library-test'
Vue.use(vui)
複製程式碼

在頁面使用

<v-hello message="component library"></v-hello>
複製程式碼

use-npm
use npm

至此,我們便已經成功改造了本地服務檔案,實現了元件庫主檔案的打包以及文件官網主入口的打包,並在最後學會了如何使用 npm 進行專案的釋出。

下一章節,我將對元件庫中 css 檔案打包進行講解。

五、css檔案管理與打包

上一節,我們已經弄好了 js 檔案的打包。但對於元件庫,我們要做到的不僅僅只是對 js 檔案進行管理,還需要對 css 檔案進行管理,這樣才能保證元件庫後續的使用。

本節中,我將會講述如何在基於 webpack 構建基礎的專案中合理使用 gulp 對 css 檔案進行單獨的打包管理。

開始之前,我們需要明確兩個目標:

  1. 元件庫中元件相關的 css 檔案該如何進行管理,放哪進行統一管理以及使用何種方式進行編寫
  2. css 檔案將如何進行打包,單個元件如何輸出對應的單個 css

1、css 檔案管理

為了方便管理,每建立一個新元件時,我們需要建立一個對應的 css 檔案來管理元件的樣式,做到單一管理

i. css 目錄

這裡,我們將會把所有的 css 檔案都存放到 packages/vui-css 目錄下,具體結構如下

├── src              	
│   ├── common         		存放元件公用的css檔案
│   ├── mixins				存放一些mixin的css檔案
│   ├── index.css			css主入口檔案
│   ├── hello.css			對應hello元件的單一css檔案
├── gulpfile.js          	css打包配置檔案
├── package.json         	相關的版本依賴
複製程式碼

ii. css 檔案編寫方式

開始寫元件的 css 前,我們要明確一些點:

  1. 當使用者引入元件庫並使用時,元件的樣式不能與使用者專案開發中樣式衝突
  2. 使用者在一些特殊情況能夠對元件樣式進行覆蓋,且能比較方便的進行修改。

符合這兩種情況的方式,個人覺得目前市場上比較好的方式就是對元件進行單一的 css 管理,並使用 bem 對 css 進行編寫。想了解 bem 的同學,點選以下連結即可

接下來,我們就著簡單的 hello 元件來做個講解,開始前,先放上 hello.vue 的內容

<template>
  <div class="v-hello">
    <p class="v-hello__message">hello {{ message }}</p>
  </div>
</template>

<script>
export default {
  name: 'v-hello',
  props: {
    message: String
  }
}
</script>
複製程式碼

packages/vui-css/src 目錄下建立 hello.css

@b v-hello {
  color: #fff;
  transform: scale(1);

  @e message {
    background: #0067ED;
  }
}
複製程式碼

然後在主入口 index.css 中引入 hello.css 檔案

@import './hello.css';
複製程式碼

examples/src/index.js 中引入元件庫樣式

import 'packages/vui-css/src/index.css'
複製程式碼

但從 hello.css 內容我們可以看出,這是典型的 bem 的寫法,正常是不能解析的。我們需要引入相應的 postcss 外掛對 bem 語法進行解析。這裡我們將使用 餓了麼團隊 開發出來的 postcss-salad 外掛對 bem 語法進行解析,其次,這種 sass-like 風格的 css 檔案,還需要用到一個外掛叫 precss ,先安裝好依賴吧 ~

npm i postcss-salad precss -D
複製程式碼

依賴安裝完成後,我們需要在專案根目錄下新建 salad.config.json 用來配置 bem 規則,具體規則如下

{
  "browsers": ["ie > 8", "last 2 versions"],
  "features": {
    "bem": {
      "shortcuts": {
        "component": "b",
        "modifier": "m",
        "descendent": "e"
      },
      "separators": {
        "descendent": "__",
        "modifier": "--"
      }
    }
  }
}
複製程式碼

接下來我們需要在專案初始化出來的 .postcssrc 檔案中使用 postcss-saladprecss 外掛,如下

module.exports = {
  "plugins": {
    "postcss-import": {},
    "postcss-salad": require('./salad.config.json'),
    "postcss-url": {},
    "precss": {},
    "autoprefixer": {},
  }
}
複製程式碼

OK,這個時候再次執行專案,則能看到 css 生效,如圖

bem
bem

2、css 檔案打包

為了將元件庫中的 css 檔案進行更好的管理,更為了使用者只想引入元件庫中某一個或者幾個元件的時候也可以引入元件對應的 css 檔案。因此我們需要對 css 檔案進行單獨的打包,這裡我們需要用到 gulp 來進行對應的打包操作,在你開始弄打包細節前,請先確保你已經全域性安裝過了 gulp ,如果沒有,請進行安裝

npm i gulp -g
# 檢視版本
gulp -v
# CLI version 3.9.1
複製程式碼

接下來,我們看看 packages/vui-css/package.json 檔案中需要用到什麼依賴

{
  "name": "vui-css",
  "version": "1.0.0",
  "description": "vui css.",
  "main": "lib/index.css",
  "style": "lib/index.css",
   // 和元件釋出一樣,也需要指定目錄
  "files": [
    "lib",
    "src"
  ],
  "scripts": {
    "build": "gulp build"
  },
  "license": "MIT",
  "devDependencies": {
    "gulp": "^3.9.1",
    "gulp-cssmin": "^0.2.0",
    "gulp-postcss": "^7.0.1",
    "postcss-salad": "^2.0.1"
  },
  "dependencies": {}
}
複製程式碼

我們可以看到,這裡其實和元件庫中對於 css 檔案需要的依賴差不多,只不過這裡是基於 gulppostcss 外掛。開始配置 gulpfile.js 前,別忘記執行 npm i 進行依賴安裝。

接下來我們開始配置 gulpfile.js,具體如下

const gulp = require('gulp')
const postcss = require('gulp-postcss')
const cssmin = require('gulp-cssmin')
const salad = require('postcss-salad')(require('../../salad.config.json'))

gulp.task('compile', function () {
  return gulp.src('./src/*.css')
    // 使用postcss-salad
    .pipe(postcss([salad]))
    // 進行css壓縮
    .pipe(cssmin())
    // 輸出到 './lib' 目錄下
    .pipe(gulp.dest('./lib'))
})

gulp.task('build', ['compile'])
複製程式碼

現在,你可以開始執行 gulp build 命令對 css 檔案進行打包了。當然為了方便並更好的執行打包命令,我們現在需要在專案根目錄下的 package.json 中加上一條 css 的 build 命令,如下

"scripts": {
  "build:vui-css": "gulp build --gulpfile packages/vui-css/gulpfile.js && rimraf lib/vui-css && cp-cli packages/vui-css/lib lib/vui-css && rimraf packages/vui-css/lib"
}
複製程式碼

執行 npm run build:vui-css, 走你,最後打包出來的元件庫的 js 和 css 檔案如下圖所示

build-vui-css
build vui-css

OK,到這裡,你已經可以單獨引入元件及其樣式了。最後為了讓使用者能夠直接使用你元件的 css ,別忘記將其釋出到 npm 官網哦 ~ 步驟如下

# 進到vui-css目錄
cd packages/vui-css
# 釋出
npm publish
複製程式碼

至此,我們已經完成了 css 檔案的管理和單獨打包,完成了對 css 檔案單一的輸出。如此這樣,我們能夠對元件庫 css 檔案的開發和管理有了一個較好的方式的同時,能夠方便元件庫的使用!

六、單元測試

目前為止,我們已經構建好了元件庫需要的新目錄,js 檔案和 css 檔案的打包我們也改造好了,元件庫開發的前置工作我們已經做好了比較充實的準備,但我們仍需做一些非常重要的前置工作以方便元件庫後續元件的開發和維護。

而對於前端測試,它是前端工程方面的一個重要分支,因此,在我們的元件庫中怎麼能少掉這麼重要的一角呢?對於單元測試,主要分為兩種

  • TDD(Test-Driven Development):測試驅動開發,注重輸出結果。
  • BDD(Behavior Driven Development):行為驅動開發,注重測試邏輯。

在本章節中,我將帶領大家使用基於專案初始化自帶的 Karma + Mocha 這兩大框架對我們的元件庫中的元件進行單元測試。

1、框架簡介

對於 Karma + Mocha 這兩大框架,相信大多數接觸過單元測試的人都不會陌生,但這裡我覺得還是有必要單獨開一小節對著兩大框架進行一個簡單的介紹。

i. Karma 框架

  • Karma 是一個基於 Node.js 的 JavaScript 測試執行過程管理工具(Test Runner)
  • Karma 是一個測試工具,能讓你的程式碼在瀏覽器環境下測試
  • Karma 能讓你的程式碼自動在多個瀏覽器,比如 chrome,firefox,ie 等環境下執行

為了能讓我們的元件庫中的元件能夠執行在各大主流 Web 瀏覽器中進行測試,我們選擇了 Karma 。最重要的是 Karmavue-cli 推薦的單元測試框架。如果你想了解更多有關 Karma 的介紹,請自行查閱 Karma 官網

ii. Mocha 框架

  • Mocha 是一個 simpleflexiblefun 的測試框架
  • Mocha 支援非同步的測似用例,如 Promise
  • Mocha 支援程式碼覆蓋率 coverage 測試報告
  • Mocha 允許你使用任何你想使用的斷言庫,比如 chaishould.js (BDD風格)、expect.js 等等
  • Mocha 提供了 before(), after(), beforeEach(), 以及 afterEach() 四個鉤子函式,方便我們在不同階段設定不同的操作以更好的完成我們的測試

這裡我介紹一下 mocha 的三種基本用法,以及 describe 的四個鉤子函式(生命週期)

  1. describe(moduleName, function): describe 是可巢狀的,描述***測試用例***是否正確

    describe('測試模組的描述', () => {
      // ....
    });
    複製程式碼
  2. **it(info, function):**一個 it 對應一個單元測試用例

    it('單元測試用例的描述', () => {
      // ....
    })
    複製程式碼
  3. 斷言庫的用法

    expect(1 + 1).to.be.equal(2)
    複製程式碼
  4. describe 的生命週期

    describe('Test Hooks', function() {
    
      before(function() {
        // 在本區塊的所有測試用例之前執行
      });
    
      after(function() {
        // 在本區塊的所有測試用例之後執行
      });
    
      beforeEach(function() {
        // 在本區塊的每個測試用例之前執行
      });
    
      afterEach(function() {
        // 在本區塊的每個測試用例之後執行
      });
    
      // test cases
    });
    複製程式碼

想了解更多 mocha 操作的同學可以點選下面的連結進行查閱

  1. Mocha 官網
  2. 測試框架 Mocha 例項教程

2、單元測試實戰

上面一小節,我給大家簡單介紹了一下 Vue 官方推薦的測試框架 KarmaMocha,也希望大家看到這裡的時候能夠對單元測試及常見測試框架能有個簡單的瞭解。

i. 對 hello 元件進行單元測試

在單元測試實戰開始前,我們先看看 Karma 的配置,這裡我們直接看 vue-cli 腳手架初始化出來的 karma.conf.js 檔案裡面的配置(具體用處我做了註釋)

var webpackConfig = require('../../build/webpack.test.conf')

module.exports = function karmaConfig (config) {
  config.set({
    // 瀏覽器
    browsers: ['PhantomJS'],
    // 測試框架
    frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
    // 測試報告
    reporters: ['spec', 'coverage'],
    // 測試入口檔案
    files: ['./index.js'],
    // 前處理器 karma-webpack
    preprocessors: {
      './index.js': ['webpack', 'sourcemap']
    },
    // webpack配置
    webpack: webpackConfig,
    // webpack中介軟體
    webpackMiddleware: {
      noInfo: true
    },
    // 測試覆蓋率報告
    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    }
  })
}
複製程式碼

接下來,我們再來對我們自己的 hello 元件進行簡單的測試(只寫一個測試用例),在 test/unit/specs 新建 hello.spec.js 檔案,並寫入以下程式碼

import Vue from 'vue' // 匯入Vue用於生成Vue例項
import Hello from 'packages/hello' // 匯入元件
// 測試指令碼里面應該包括一個或多個describe塊,稱為測試套件(test suite)
describe('Hello.vue', () => {
  // 每個describe塊應該包括一個或多個it塊,稱為測試用例(test case)
  it('render default classList in hello', () => {
    const Constructor = Vue.extend(Hello) // 獲得Hello元件例項
    const vm = new Constructor().$mount() // 將元件掛在到DOM上
    // 斷言:DOM中包含class為v-hello的元素
    expect(vm.$el.classList.contains('v-hello')).to.be.true
    const message = vm.$el.querySelector('.v-hello__message')
    // 斷言:DOM中包含class為v-hello__message的元素
    expect(message.classList.contains('v-hello__message')).to.be.true
  })
})
複製程式碼

測試例項寫完,接下來就是進行測試了。執行 npm run test,走你 ~ ,輸出結果

hello.vue
    ✓ render default classList in hello
複製程式碼

ii. 優化單元測試

從上面 hello 元件的測試例項可以看出,我們需要將元件例項化為一個Vue例項,有時還需要掛載到 DOM 上

const Constructor = Vue.extend(Hello)
const vm = new Constructor({
  propsData: {
    message: 'component'
  }
}).$mount()
複製程式碼

如果之後每個元件擁有多個單元測試例項,那這種寫法會導致我們最後的測試比較臃腫,這裡我們可以參考 element 封裝好的 單元測試工具 util.js 。我們需要封裝 Vue 在單元測試中常用的一些方法,下面我將列出工具裡面提供的一些方法

/**
 * 回收 vm,一般在每個測試指令碼測試完成後執行回收vm。
 * @param  {Object} vm
 */
exports.destroyVM = function (vm) {}

/**
 * 建立一個 Vue 的例項物件
 * @param  {Object|String}  Compo     - 元件配置,可直接傳 template
 * @param  {Boolean=false}  mounted   - 是否新增到 DOM 上
 * @return {Object} vm
 */
exports.createVue = function (Compo, mounted = false) {}

/**
 * 建立一個測試元件例項
 * @param  {Object}  Compo          - 元件物件
 * @param  {Object}  propsData      - props 資料
 * @param  {Boolean=false} mounted  - 是否新增到 DOM 上
 * @return {Object} vm
 */
exports.createTest = function (Compo, propsData = {}, mounted = false) {}

/**
 * 觸發一個事件
 * 注: 一般在觸發事件後使用 vm.$nextTick 方法確定事件觸發完成。
 * mouseenter, mouseleave, mouseover, keyup, change, click 等
 * @param  {Element} elm      - 元素
 * @param  {String} name      - 事件名稱
 * @param  {*} opts           - 配置項
 */
exports.triggerEvent = function (elm, name, ...opts) {}

/**
 * 觸發 “mouseup” 和 “mousedown” 事件,既觸發點選事件。
 * @param {Element} elm     - 元素
 * @param {*} opts          - 配置選項
 */
exports.triggerClick = function (elm, ...opts) {}
複製程式碼

下面我們將使用定義好的測試工具方法,改造 hello 元件的測試例項,將 hello.spec.js 檔案進行改造

import { destroyVM, createTest } from '../util'
import Hello from 'packages/hello'

describe('hello.vue', () => {
  let vm
  // 測試用例執行之後銷燬例項
  afterEach(() => {
    destroyVM(vm)
  })
  it('render default classList in hello', () => {
    vm = createTest(Hello)
    expect(vm.$el.classList.contains('v-hello')).to.be.true
    const message = vm.$el.querySelector('.v-hello__message')
    expect(message.classList.contains('v-hello__message')).to.be.true
  })
})
複製程式碼

重新執行 npm run test,輸出結果

hello.vue
    ✓ render default classList in hello
複製程式碼

iii. 更多單元測試的用法

上面我們介紹了單元測試的部分有關靜態判定的用法,接下來我們將測試一些非同步用例以及一些互動事件。在測試之前,我們需稍微改動一下我們的 hello 元件的程式碼,如下

<template>
  <div class="v-hello" @click="handleClick">
    <p class="v-hello__message">hello {{ message }}</p>
  </div>
</template>

<script>
export default {
  name: 'v-hello',
  props: {
    message: String
  },
  methods: {
    handleClick () {
      return new Promise((resolve) => {
        resolve()
      }).then(() => {
        this.$emit('click', 'this is click emit')
      })
    }
  }
}
</script>
複製程式碼

接下來我們要測試 hello 元件通過 Promise 是否能夠成功將資訊 emit 出去,測試案例如下

it('create a hello for click with promise', (done) => {
  let result
  vm = createVue({
    template: `<v-hello @click="handleClick"></v-hello>`,
    methods: {
      handleClick (msg) {
        result = msg
      }
    }
  }, true)
  vm.$el.click()
  // 斷言訊息是非同步emit出去的
  expect(result).to.not.exist
  setTimeout(_ => {
    expect(result).to.exist
    expect(result).to.equal('this is click emit')
    done()
  }, 20)
})
複製程式碼

重新開始測試,執行npm run test,輸出結果

hello.vue
    ✓ render default classList in hello
    ✓ create a hello for click with promise
複製程式碼

至此,我們便學會了單元測試的配置以及一些常用的用法。如果需要了解更多有關單元測試的細節,請根據我前面提供的連結進入更深入的研究

七、文件官網開發(上)

小夥伴們跟著我將前面5個章節實戰下來,已經將我們元件開發的基本架子給搭建好了。接下來我將帶著大家一起把元件庫中重要成分很高的文件官網給擼完。

大家應該都知道,好的開源專案肯定是有文件官網的,所以為了讓我們的 UI 庫也成為優秀中的一員的話,我們也應該擼一個自己文件官網。

一個好的文件官網,需要做到兩點。

  1. 將自己的開源專案的 API 梳理清楚,讓使用者能夠用的更舒心
  2. 有示例 demo ,讓使用者能線上就看到效果

由於本博文中,我帶領大家開發的元件庫是適配移動端的,那麼如何讓我們的文件官網既有 API 文件的描述,還有移動端示例的 Demo 呢。這就要求我們需要開發兩套頁面進行適配,對此我們需要的做的事有以下幾點:

  • PC 端展示元件 API 文件
  • 移動端的展示元件 Demo
  • 路由動態生成

在實戰開始前,我們先看下本章節需要用到的目錄結構

├── assets						css,圖片等資源都在這
├── dist                     	打包好的檔案都在這
├── docs                     	PC端需要展示的markdown檔案都在這
├── pages                     	移動端所有的demo都在這
├── src              	      	
│   ├── components            	demo中可以複用的模組放在這裡面
│   ├── index.tpl              	頁面入口
│   ├── is-mobile.js            判斷裝置
│   ├── index.js              	PC端主入口js
│   ├── App.vue               	PC端入口檔案
│   ├── mobile.js              	移動端端主入口js
│   ├── MobileApp.vue           移動端入口檔案
│   ├── nav.config.json			路由控制檔案
│   ├── router.config.js		動態註冊路由
複製程式碼

本章節,主要帶著大家實現 markdown 檔案的轉化,以及不同裝置的路由適配。

思路捋清後,接下來繼續我們的文件官網開發實戰吧!

1、markdown 檔案轉化

從上面我給出的目錄可以看到,在 docs 資料夾裡面存放的都是 markdown 檔案,每一個 markdown 檔案都對應一個元件的 API 文件。我們是想要的結果是,轉化 docs 裡面的每一個 markdown 檔案,使其變成一個個 Vue 元件,並將轉化好的 Vue 元件註冊到路由中,讓其可以通過路由對每一個 markdown 檔案進行訪問。

對於 markdown 檔案解析成 Vue 元件,市場上有很多三方 webpack 外掛,當然如果你要是對 webpack 造詣比較深的話,你也可以嘗試自己擼一個。這裡我是直接使用的 餓了麼團隊 開發出來的 vue-markdown-loader

i. 使用 vue-markdown-loader

第一步,依賴安裝

npm i vue-markdown-loader -D
複製程式碼

第二步,在 webpack.base.conf.js 檔案中使用 vue-markdown-loader

{
  test: /\.md$/,
  loader: 'vue-markdown-loader',
  options: {
    // 阻止提取指令碼和樣式標籤
    preventExtract: true
  }
}
複製程式碼

第三步,try 一 try。先在 docs 裡面新增 hello.md 檔案,然後寫入 hello 元件的使用說明

## Hello
**Hello 元件,Hello 元件,Hello 元件,Hello 元件**
### 基本用法```html
<template>
  <div class="hello-page">
    <v-hello message="my component library" @click="handleClick"></v-hello>
    <p>{{ msg }}</p>
  </div>
</template>
<script>
export default {
  name: 'hello',
  data () {
    return {
      msg: ''
    }
  },
  methods: {
    handleClick (msg) {
      this.msg = msg
    }
  }
}
</script>
​```
### Attributes
| 引數      | 說明    | 型別      | 可選值       | 預設值   |
|---------- |-------- |---------- |-------------  |-------- |
| message  | 文字資訊    | string   | — | — |
### Events
| 事件名稱      | 說明    | 回撥引數      |
|---------- |-------- |---------- |
| click  | 點選操作    | — |
複製程式碼

第四步,將 hello.md 註冊到路由中

route.push({
  path: '/component/hello',
  component: require('../docs/hello.md')
})
複製程式碼

最後,訪問頁面。這個時候可以發現 hello.md 的內容已經被轉成 Vue 元件,並且能夠通過路由載入的方式進行訪問,但是頁面卻很醜很醜 ~ 就像這樣

markdown
markdown

ii. 為 md 加上高亮主題和樣式

當然,出現這種情況不用我說明,大家可能也知道了。對的,解析出來的 markdown 檔案這麼醜,只是因為我們既沒有給我們的 markdown 檔案加上高亮主題,也沒有設定好文件頁面的基本樣式而已。所以,接下來,我們需要給我們的 markdown 檔案加上漂亮的高亮主題和簡潔的基本樣式。

對於主題,這裡我們將使用 highlight.js 裡面的 atom-one-dark 主題。

第一步,安裝 highlight.js

npm i highlight -D
複製程式碼

第二步,在 examples/src/App.vue 引入主題,並且為了設定文件的基本樣式,我們還需要修改 App.vue 的佈局

<template>
  <div class="app">
    <div class="main-content">
      <div class="page-container clearfix">
        <div class="page-content">
          <router-view></router-view>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import 'highlight.js/styles/atom-one-dark.css'
export default {
  name: 'App'
}
</script>
複製程式碼

第三步,設定文件的基本樣式。在 assets 中新建 docs.css,寫入初始樣式,由於程式碼量偏多,就不往這裡貼了。大家可自行 copy docs.css 裡面的程式碼到本地的 docs.css 檔案中,然後在 examples/src/index.js 中進行引入

import '../assets/docs.css'
複製程式碼

最後,改造 markdown 解析規則,vue-markdown-loader 提供了一個 preprocess 介面給我們自由操作,接下來,我們對解析好的 markdown 檔案的結構進行定義吧,在 webpack.base.conf.js 檔案中寫入

// 定義輔助函式wrap,將<code>標籤都加上名為'hljs'的class
function wrap (render) {
  return function() {
    return render.apply(this, arguments)
      .replace('<code v-pre class="', '<code class="hljs ')
      .replace('<code>', '<code class="hljs">')
  }
}
// ...
{
  test: /\.md$/,
  loader: 'vue-markdown-loader',
  options: {
    preventExtract: true,
    preprocess: function(MarkdownIt, source) {
      // 為table標籤加上名為'table'的class
      MarkdownIt.renderer.rules.table_open = function() {
        return '<table class="table">'
      };
      MarkdownIt.renderer.rules.fence = wrap(MarkdownIt.renderer.rules.fence);
      return source;
    }
  }
}
複製程式碼

然後,重新訪問 localhost:8080/#/component/hello

markdown
markdown 高亮預覽

OK,我們的 md 檔案已經成功解析成 Vue 元件,並有了漂亮的高亮主題和簡潔的基本樣式了 ~

2、不同裝置環境下路由的適配

前面我有說過,本文帶領大家開發的元件庫是適配移動端的,所以我們需要做到 PC 端展示文件,移動端展示 Demo。

在這一小節,我會帶著大家進行不同端路由的適配。當然,這個東西不難,主要是利用 webpack 構建多頁面的特性,那麼具體怎麼做呢?好了,不多扯,我們們直接開始吧

i. 入口檔案註冊

第一步,註冊 js 入口檔案,在 webpack.base.conf.js 檔案中寫入

entry: {
  // ...
  'vui': './examples/src/index.js',  // PC端入口js
  'vui-mobile': './examples/src/mobile.js'  // 移動端入口js
}
複製程式碼

第二步,註冊頁面入口,在 webpack.base.conf.js 檔案中寫入

plugins: [
  // ...
  // PC端頁面入口
  new HtmlWebpackPlugin({
    chunks: ['manifest', 'vendor', 'vui'],
    template: 'examples/src/index.tpl',
    filename: 'index.html',
    inject: true
  }),
  // 移動端頁面入口
  new HtmlWebpackPlugin({
    chunks: ['manifest', 'vendor', 'vui-mobile'],
    template: 'examples/src/index.tpl',
    filename: 'mobile.html',
    inject: true
  })
]
複製程式碼

ii. 裝置環境判定

入口檔案註冊完成,接下來我們需要做的是對裝置環境進行判定。這裡,我將使用 navigator.userAgent 配合正規表示式的方式判斷我們元件庫執行的環境到底是屬於 PC 端還是移動端?

第一步,在examples/src/is-mobile.js 檔案中寫入以下程式碼

/* eslint-disable */
const isMobile = (function () {
  var platform = navigator.userAgent.toLowerCase()
  return (/(android|bb\d+|meego).+mobile|kdtunion|weibo|m2oapp|micromessenger|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i).test(platform) ||
  (/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i).test(platform.substr(0, 4));
})()
// 返回裝置所處環境是否為移動端,值為boolean型別
export default isMobile
複製程式碼

第二步,在 PC 端 js 入口檔案 examples/src/index.js 中寫入以下判定規則

import isMobile from './is-mobile'
// 是否為生產環境
const isProduction = process.env.NODE_ENV === 'production'
router.beforeEach((route, redirect, next) => {
  if (route.path !== '/') {
    window.scrollTo(0, 0)
  }
  // 獲取不同環境下,移動端Demo對應的地址
  const pathname = isProduction ? '/vui/mobile' : '/mobile.html'
  // 如果裝置環境為移動端,則直接載入移動端Demo的地址
  if (isMobile) {
    window.location.replace(pathname)
    return
  }
  document.title = route.meta.title || document.title
  next()
})
複製程式碼

第三步,在移動端 js 入口檔案examples/src/mobile.js 中寫入與上一步類似的判定規則

import isMobile from './is-mobile'
const isProduction = process.env.NODE_ENV === 'production'
router.beforeEach((route, redirect, next) => {
  if (route.path !== '/') {
    window.scrollTo(0, 0)
  }
  // 獲取不同環境下,PC端對應的地址
  const pathname = isProduction ? '/vui/mobile' : '/mobile.html'
  // 如果裝置環境不是移動端,則直接載入PC端的地址
  if (!isMobile) {
    window.location.replace(pathname)
    return
  }
  document.title = route.meta.title || document.title
  next()
})
複製程式碼

最後,完善 examples/src/mobile.js 檔案,和移動端頁面入口 MobileApp.vue 檔案

examples/src/mobile.js 中寫入以下程式碼

import Vue from 'vue'
import VueRouter from 'vue-router'
import MobileApp from './MobileApp'
import Vui from 'src/index'
import isMobile from './is-mobile.js'
import Hello from '../pages/hello.vue'

import 'packages/vui-css/src/index.css'

Vue.use(Vui)
Vue.use(VueRouter)

const isProduction = process.env.NODE_ENV === 'production'
const router = new VueRouter({
  base: isProduction ? '/vui/' : __dirname,
  routes: [{
    path: '/component/hello',
    component: Hello
  }]
})
router.beforeEach((route, redirect, next) => {
  if (route.path !== '/') {
    window.scrollTo(0, 0)
  }
  const pathname = isProduction ? '/vui/' : '/'
  if (!isMobile) {
    window.location.replace(pathname)
    return
  }
  document.title = route.meta.title || document.title
  next()
})

new Vue({
  el: '#app-container',
  router,
  components: { MobileApp },
  template: '<MobileApp/>'
})

複製程式碼

MobileApp.vue 中寫入

<template>
  <div class="mobile-container">
      <router-view></router-view>
  </div>
</template>
複製程式碼

接下來,你可以去瀏覽器中試試效果了,看看不同的裝置環境是否能展示對應的內容 ~

到這裡,我們本章制定好的計劃便已經全部完成。md 檔案的"完美"轉化,以及不同裝置環境下路由的適配。文件官網的開發(上)到這裡就要告一段落了,下一章節,我們將繼續完成文件官網剩餘的開發工作!

八、文件官網開發(下)

上一章節,我們已經完成了:

  1. markdown 檔案的轉化,併為其加上了漂亮的高亮主題和樣式
  2. 文件官網在不同的裝置環境下的適配

這一章節,我們將完善文件官網的細節,開發出一個完整的文件官網。

1、路由管理

從上一章給出的目錄我們可以知道,docs 目錄是用來存放 PC 需要展示的 md 檔案的,pages 目錄是用來存放移動端 Demo 檔案的。那麼如何讓元件在不同的裝置環境下展示其對應的檔案呢(PC 端展示元件對應的 md 檔案,移動端展示元件對應 vue 檔案)?這種情況又該如何合理的管理好我們元件庫的路由呢?接下來,我們就著這些問題繼續下面的開發。這裡肯定會用到 is-mobile.js 去進行裝置環境的判定,具體工作大家跟著我慢慢來做

第一步,在 examples/src 下新建檔案 nav.config.json 檔案,寫入以下內容

{
  // 為了之後元件文件多語言化
  "zh-CN": [
    {
      "name": "Vui 元件",
      "showInMobile": true,
      "groups": [
        {
		  // 管理相同型別下的所有元件
          "groupName": "基礎元件",
		  "list": [
		    {
			  // 訪問元件的相對路徑
              "path": "/hello",
              // 元件描述
			  "title": "Hello"
			}
          ]
        }
      ]
    }
  ]
}
複製程式碼

第二步,改善 router.config.js 檔案,將其改成一個路由註冊的輔助函式

const registerRoute = (navConfig, isMobile) => {
  let route = []
  // 目前只有中文版的文件
  let navs = navConfig['zh-CN']
  // 遍歷路由檔案,逐一進行路由註冊
  navs.forEach(nav => {
    if (isMobile && !nav.showInMobile) {
      return
    }

    if (nav.groups) {
      nav.groups.forEach(group => {
        group.list.forEach(nav => {
          addRoute(nav)
        })
      })
    } else if (nav.children) {
      nav.children.forEach(nav => {
        addRoute(nav)
      })
    } else {
      addRoute(nav)
    }
  })
  // 進行路由註冊
  function addRoute (page) {
    // 不同的裝置環境引入對應的路由檔案
    const component = isMobile
      ? require(`../pages${page.path}.vue`)
      : require(`../docs${page.path}.md`)
    route.push({
      path: '/component' + page.path,
      component: component.default || component
    })
  }

  return route
}

export default registerRoute
複製程式碼

第三步,在 PC 端主入口 js 檔案 examples/src/index.js 和移動端主入口 js 檔案 examples/src/mobile.js 裡面註冊路由,都寫入以下程式碼

import registerRoute from './router.config'
import navConfig from './nav.config'

const routesConfig = registerRoute(navConfig)
const router = new VueRouter({
  routes: routesConfig
})
複製程式碼

然後再訪問一下我們現在的元件庫文件官網

2、PC 端 API 展示

從上一章節的最終效果圖我們可以看出來,PC端分為三個部分,分別為:

  1. 頭部,元件庫的簡單描述,以及專案 github 的連結
  2. 左側欄,元件路由及標題展示
  3. 右側欄,元件 API 文件展示

接下來,讓我們開始來完成PC 端 API 的展示吧

i. 頭部

頭部相對簡單點,我們只需要在 examples/src/components 下新建 page-header.vue 檔案,寫入以下內容

<template>
  <div class="page-header">
    <div class="page-header__top">
      <h1 class="page-header__logo">
        <a href="#">Vui.js</a>
      </h1>
      <ul class="page-header__navs">
        <li class="page-header__item">
          <a href="/" class="page-header__link">元件</a>
        </li>
        <li class="page-header__item">
          <a href="https://github.com/Brickies/vui" class="page-header__github" target="_blank"></a>
        </li>
        <li class="page-header__item">
          <span class="page-header__link"></span>
        </li>
      </ul>
    </div>
  </div>
</template>
複製程式碼

具體樣式,請直接訪問 page-header.vue 進行檢視

ii. 左側欄

左側欄,是我們展示元件路由和標題的地方。其實就是對 examples/src/nav.config.json 進行解析並展示。

我們在 examples/src/components 下新建 side-nav.vue 檔案,檔案正常結構如下

<li class="nav-item">
  <a href="javascript:void(0)">Vui 元件</a>
  <div class="nav-group">
    <div class="nav-group__title">基礎元件</div>
    <ul class="pure-menu-list">
      <li class="nav-item">
        <router-link
           active-class="active"
           :to="/component/hello"
           v-text="navItem.title">Hello
        </router-link>
      </li>
    </ul>
  </div>
</li>

複製程式碼

但我們現在要基於目前的結構對 examples/src/nav.config.json 進行解析,完善後的程式碼如下

<li class="nav-item" v-for="item in data">
  <a href="javascript:void(0)" @click="handleTitleClick(item)">{{ item.name }}</a>
  <template v-if="item.groups">
    <div class="nav-group" v-for="group in item.groups">
      <div class="nav-group__title">{{ group.groupName }}</div>
      <ul class="pure-menu-list">
        <template v-for="navItem in group.list">
          <li class="nav-item" v-if="!navItem.disabled">
            <router-link
              active-class="active"
              :to="base + navItem.path"
              v-text="navItem.title" />
          </li>
        </template>
      </ul>
    </div>
  </template>
</li>
複製程式碼

完整程式碼點這裡 side-nav.vue

iii. App.vue

我們把我們寫好的 page-header.vueside-nav.vue 兩個檔案在 App.vue 中使用

<template>
  <div class="app">
    <page-header></page-header>
    <div class="main-content">
      <div class="page-container clearfix">
        <side-nav :data="navConfig['zh-CN']" base="/component"></side-nav>
        <div class="page-content">
          <router-view></router-view>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import 'highlight.js/styles/atom-one-dark.css'
import navConfig from './nav.config.json'
import PageHeader from './components/page-header'
import SideNav from './components/side-nav'

export default {
  name: 'App',
  components: { PageHeader, SideNav },
  data () {
    return {
      navConfig: navConfig
    }
  }
}
</script>

複製程式碼

然後,再次訪問頁面,結果如圖

api-7
頁面預覽

3、移動端 Demo

移動端 Demo 和 PC 端原理差不多,都得解析 nav.config.json 檔案從而進行展示

i. 移動端首頁元件

目前我們移動端除了主入口頁面 MobileApp.vue 以外,是沒有根目錄元件依賴的,接下來我們將先完成根目錄元件的開發,在 examples/src/components 下新建 demo-list.vue 檔案,寫入一些內容

<template>
  <div class="side-nav">
    <h1 class="vui-title"></h1>
    <h2 class="vui-desc">VUI 移動元件庫</h2>
  </div>
</template>
複製程式碼

然後我們需要在路由中對其進行引用,在 mobile.js 檔案中寫入

import DemoList from './components/demo-list.vue'
routesConfig.push({
  path: '/',
  component: DemoList
})
複製程式碼

然後開始完善 demo-list.vue 檔案

<template>
  <div class="side-nav">
    <h1 class="vui-title"></h1>
    <h2 class="vui-desc">VUI 移動元件庫</h2>
    <div class="mobile-navs">
      <div v-for="(item, index) in data" :key="index">
        <div class="mobile-nav-item" v-if="item.showInMobile">
          <mobile-nav v-for="(group, s) in item.groups" :group="group" :base="base" :key="s"></mobile-nav>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import navConfig from '../nav.config.json';
import MobileNav from './mobile-nav';

export default {
  data() {
    return {
      data: navConfig['zh-CN'],
      base: '/component'
    };
  },

  components: {
    MobileNav
  }
};
</script>

<style lang="postcss">
.side-nav {
  width: 100%;
  box-sizing: border-box;
  padding: 90px 15px 20px;
  position: relative;
  z-index: 1;

  .vui-title,
  .vui-desc {
    text-align: center;
    font-weight: normal;
    user-select: none;
  }

  .vui-title {
    padding-top: 40px;
    height: 0;
    overflow: hidden;
    background: url(https://raw.githubusercontent.com/xuqiang521/vui/master/src/assets/logo.png) center center no-repeat;
    background-size: 40px 40px;
    margin-bottom: 10px;
  }

  .vui-desc {
    font-size: 14px;
    color: #666;
    margin-bottom: 50px;
  }
}
</style>
複製程式碼

這裡我們引用了 mobile-nav.vue 檔案,這也是我們接下來要完成的移動端 Demo 列表展示元件

ii. nav 列表

examples/src/components 下新建 mobile-nav.vue 檔案,解析 nav.config.json 檔案,從而進行 Demo 列表展示。

<template>
  <div class="mobile-nav-group">
    <div
      class="mobile-nav-group__title mobile-nav-group__basetitle"
      :class="{
        'mobile-nav-group__title--open': isOpen
      }"
      @click="isOpen = !isOpen">
      {{group.groupName}}
    </div>
    <div class="mobile-nav-group__list-wrapper" :class="{ 'mobile-nav-group__list-wrapper--open': isOpen }">
      <ul class="mobile-nav-group__list" :class="{ 'mobile-nav-group__list--open': isOpen }">
        <template v-for="navItem in group.list">
          <li
            class="mobile-nav-group__title"
            v-if="!navItem.disabled">
            <router-link
              active-class="active"
              :to="base + navItem.path">
              <p>
                {{ navItem.title }}
              </p>
            </router-link>
          </li>
        </template>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    group: {
      type: Object,
      default: () => {
        return [];
      }
    },
    base: String
  },
  data() {
    return {
      isOpen: false
    };
  }
};
</script>
複製程式碼

然後寫入列表樣式

<style lang="postcss">
@component-namespace mobile {
  @b nav-group {
    border-radius: 2px;
    margin-bottom: 15px;
    background-color: #fff;
    box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);

    @e basetitle {
      padding-left: 20px;
    }

    @e title {
      font-size: 16px;
      color: #333;
      line-height: 56px;
      position: relative;
      user-select: none;

      @m open {
        color: #38f;
      }

      a {
        color: #333;
        display: block;
        user-select: none;
        padding-left: 20px;
        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);

        &:active {
          background: #ECECEC;
        }

        > p {
          border-top: 1px solid #e5e5e5;
        }
      }
    }

    @e list-wrapper {
      height: 0;
      overflow: hidden;

      @m open {
        height: auto;
      }
    }

    @e list {
      transform: translateY(-50%);
      transition: transform .2s ease-out;

      @m open {
        transform: translateY(0);
      }
    }

    li {
      list-style: none;
    }

    ul {
      padding: 0;
      margin: 0;
      overflow: hidden;
    }
  }
}
</style>
複製程式碼

接下來,重新訪問 http://localhost:8080/mobile.html ,不出意外你便能訪問到我們預想的結果

合格前端系列第十彈-揭祕元件庫一二事

到這一步為止,我們“粗陋”的元件庫架子便已經全部搭建完畢。

博文到這裡也差不多要結束了,文章中所有的程式碼都已經託管到了 github 上,後續我還會寫一篇文章,帶著大家逐步完善我們元件庫中的一些細節,讓我們的元件庫能夠更加的完美。

github地址:github.com/xuqiang521/…

文章末尾再打一波廣告 ~~~

前端交流群:731175396

美團點評長期招人,如果有興趣的話,歡迎一起搞基,簡歷投遞方式交流群中有說明 ~

小夥伴們你們還在等什麼呢?趕緊先給文章點波贊,然後關注我一波,然後加群和大佬們一起交流啊 ~~~

合格前端系列第十彈-揭祕元件庫一二事
大佬們快到碗裡來

相關文章