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

1、寫在前面

一、靈感來源

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

因而我開始思考,有什麼好的辦法能夠比較規範地來管理這些比較零散的東西呢?若是以組件庫這種形式來對組件進行管理的話,會不會更適合本身的積累並方便之後的工做呢?css

因而我開始參考市場上一些優秀的 UI 組件庫,好比 element-uivuxvant等,對其源碼進行拜讀,瞭解其架構的搭建,隨後整理出一套屬於本身的移動端 UI 組件庫 vuihtml

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

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

二、最終效果圖

api-1
PC 端預覽圖

api-2
移動端預覽圖

三、問題交流

若是小夥伴在閱讀文章實戰的時候有什麼問題的話,歡迎加入討論羣一塊兒討論(羣裏除了一羣大佬每天騷話外還有一羣妹紙哦 ~ )java

前端大雜燴:731175396node

github:github.com/xuqiang521android

廢話很少說,接下來,讓咱們直接進入到實戰篇吧 ~webpack

2、環境搭建

一、搭建 NODE 環境

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

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 環境就已經搭建好了,接下來,咱們須要安裝組件庫構建依賴的腳手架了。

二、構建一個 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 項目,至此你組件庫依賴的開發環境便已經安裝完畢。

3、構建新目錄

首先,咱們要明確本節的目的,咱們須要修改目錄,爲了更好的開發組件庫。

咱們上一節已經把搭建好了 vue 項目,但初始化出來的項目的目錄卻不能知足一個組件庫的後續開發和維護。所以這一章節咱們須要作的事情就是改造初始化出來的 vue 項目的目錄,將其變成組件庫須要的目錄,接下來就讓咱們行動起來吧。

一、組件庫目錄

  1. build:這個目錄主要用來存放構建相關的文件
  2. packages: 這個目錄下主要用來存放全部組件
  3. examples:這個目錄下主要用來存放組件庫的展現 demo文檔的全部相關文件
  4. src:這個目錄主要用來管理組件的註冊的主入口,工具,mixins等(對此咱們須要改造初始化出來的 src 目錄)
  5. test:這個目錄用來存放測試案例(繼續延用初始化出來的目錄)
  6. lib:組件庫打包出來後的目錄
  7. .github:做爲一個開源組件庫,若是你想和別人一塊兒開發,那麼這個目錄用來存放你本身定義的一些開發規則指導,也是很是不錯的

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

二、讓項目可以從新跑起來

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,而後你的項目便能在新的入口文件下跑起來

三、在本地使用組件

這一小節,咱們須要實現的就是咱們本地啓動的服務,可以使用 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 官網,讓你的組件庫可以被人更方便的使用!

4、改造打包文件,發佈 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

一、改造 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展現文件打包配置文件(新增文件)
複製代碼

一、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
    })
  ]
}
複製代碼

二、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. 改造打包文件

一、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
複製代碼

二、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 使用起來 ~

二、發佈 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 文件打包進行講解。

5、css文件管理與打包

上一節,咱們已經弄好了 js 文件的打包。但對於組件庫,咱們要作到的不只僅只是對 js 文件進行管理,還須要對 css 文件進行管理,這樣才能保證組件庫後續的使用。

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

開始以前,咱們須要明確兩個目標:

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

一、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

二、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 文件的開發和管理有了一個較好的方式的同時,可以方便組件庫的使用!

6、單元測試

目前爲止,咱們已經構建好了組件庫須要的新目錄,js 文件和 css 文件的打包咱們也改造好了,組件庫開發的前置工做咱們已經作好了比較充實的準備,但咱們仍需作一些很是重要的前置工做以方便組件庫後續組件的開發和維護。

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

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

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

一、框架簡介

對於 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 實例教程

二、單元測試實戰

上面一小節,我給你們簡單介紹了一下 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
複製代碼

至此,咱們便學會了單元測試的配置以及一些經常使用的用法。若是須要了解更多有關單元測試的細節,請根據我前面提供的連接進入更深刻的研究

7、文檔官網開發(上)

小夥伴們跟着我將前面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 文件的轉化,以及不一樣設備的路由適配。

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

一、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 組件,並有了漂亮的高亮主題和簡潔的基本樣式了 ~

二、不一樣設備環境下路由的適配

前面我有說過,本文帶領你們開發的組件庫是適配移動端的,因此咱們須要作到 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 文件的"完美"轉化,以及不一樣設備環境下路由的適配。文檔官網的開發(上)到這裏就要告一段落了,下一章節,咱們將繼續完成文檔官網剩餘的開發工做!

8、文檔官網開發(下)

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

  1. markdown 文件的轉化,併爲其加上了漂亮的高亮主題和樣式
  2. 文檔官網在不一樣的設備環境下的適配

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

一、路由管理

從上一章給出的目錄咱們能夠知道,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
})
複製代碼

而後再訪問一下咱們如今的組件庫文檔官網

二、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
頁面預覽

三、移動端 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

我的準備從新撿回本身的公衆號了,以後每週保證一篇高質量好文,感興趣的小夥伴能夠關注一波。

美團點評長期招人,若是有興趣的話,歡迎一塊兒搞基,簡歷投遞方式交流羣中有說明 ~

小夥伴們大家還在等什麼呢?趕忙先給文章點波贊,而後關注我一波,而後加羣和大佬們一塊兒交流啊 ~~~

大佬們快到碗裏來
相關文章
相關標籤/搜索