我日常比較喜歡對一些東西作一些記錄和總結,其中包括一些組件,積累的量比較多的時候,發現零散的堆積已經不太適合進行管理了。javascript
因而我開始思考,有什麼好的辦法能夠比較規範地來管理這些比較零散的東西呢?若是以組件庫這種形式來對組件進行管理的話,會不會更適合本身的積累並方便之後的工做呢?css
因而我開始參考市場上一些優秀的 UI 組件庫,好比 element-ui
、vux
、 vant
等,對其源碼進行拜讀,瞭解其架構的搭建,隨後整理出一套屬於本身的移動端 UI 組件庫 vui
。html
我在業餘時間活躍於各大技術社區,常有一些或工做一段時間的、或還在準備找實習工做的小夥伴問筆者一些問題:怎樣沉澱本身,作本身的框架、輪子、庫?怎樣作一個組件庫?本身作過一個組件庫會不會成爲簡歷的亮點?你能不能寫一些有關組件庫開發的相關文章?…...前端
本着答惑解疑和分享的心情,這篇博文便誕生了。vue
若是小夥伴在閱讀文章實戰的時候有什麼問題的話,歡迎加入討論羣一塊兒討論(羣裏除了一羣大佬每天騷話外還有一羣妹紙哦 ~ )java
前端大雜燴:731175396node
github:github.com/xuqiang521android
廢話很少說,接下來,讓咱們直接進入到實戰篇吧 ~webpack
這裏我只談 Mac 和 window 下 NODE 的安裝git
若是你尚未安裝 mac 軟件包管理器 homebrew
的話第一步得先安裝它
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
複製代碼
使用 homebrew
安裝 node
brew install node
複製代碼
window
環境的話直接進入 node 官網進行對應版本的下載,而後瘋狂點擊下一步便可安裝完成
安裝完成後,查看 node
和 npm
版本
node -v
# v9.6.1
npm -v
# 5.6.0
複製代碼
自此你電腦上 node
環境就已經搭建好了,接下來,咱們須要安裝組件庫構建依賴的腳手架了。
# 全局安裝
npm i -g vue-cli
# 查看vue-cli用法
vue -h
# 查看版本
vue -V
# 2.9.3
複製代碼
使用 vue-cli
的 init
指令初始化一個名爲 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
項目的目錄,將其變成組件庫須要的目錄,接下來就讓咱們行動起來吧。
demo
和 文檔
的全部相關文件mixins
等(對此咱們須要改造初始化出來的 src
目錄)OK,開始改造你初始化出來的項目的目錄吧。
從前面咱們知道,咱們啓動本地服務的時候,頁面的的主入口文件是 index.html
。如今咱們第一步就是講頁面的主入口 html
和 js
挪到 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
}
]
})
複製代碼
src
目錄主要用來存放組件的註冊的主入口文件,工具方法,mixins
等文件。咱們從上面 examples
的目錄能夠知道,原先 src
中的一些文件是須要刪掉的,改造後的目錄以下
├── mixins mixins方法存放在這
├── utils 一些經常使用輔助方法存放在這
├── index.js 組件註冊主入口
複製代碼
想一想小夥伴看到這,也應該知道咱們如今須要作的事是什麼。沒錯,就是修改本地服務的入口文件。若是隻是可以跑起來,那麼修改 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
組件進行講解
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>
複製代碼
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
}
複製代碼
examples/src/index.js
入口 js 文件中進行引用接下來,我須要在上節改造好的 examples
中對咱們寫好的 hello
組件進行引用
import vui from 'src/index.js'
// 完整引用
Vue.use(vui)
// 獨立引用
const { Hello } = vui
Vue.component(Hello.name, Hello)
複製代碼
examples/pages/hello.vue
直接使用在 examples/pages
中咱們須要創建和組件名同名的 demo 文件,並對組件進行使用
<v-hello message="my component library"></v-hello>
複製代碼
當你運行的結果和上圖同樣的話,那麼恭喜。你又成功向組件庫的開發邁開了一步 ~
看到這裏,我須要各位讀者可以按照本身的喜愛對文件進行集中化的管理(固然,也能夠參考我上面給出的 demo),只有這樣,纔可以讓咱們組件庫後續的開發工做可以順暢起來。
下一節,咱們會優化 build
下面的打包文件,並帶着你們把本身的開發好的組件發佈到 npm
官網,讓你的組件庫可以被人更方便的使用!
老規矩,章節正文開始以前,咱們得清楚本章節須要作什麼以及爲何這麼作。
因爲腳手架初始的項目對於 build
文件只有一個集中打包的文件 webpack.prod.conf.js
爲了以後咱們的組件庫能更好的使用起來,咱們須要將組件庫對應的模塊抽離所有打包到 vui.js
一個文件中(名字你喜歡啥取啥),這樣咱們以後就能經過如下方式來引用咱們得組件庫了
import Vue from 'vue'
import vui from 'x-vui'
Vue.use(vui)
複製代碼
咱們還須要將 examples
中相關的文件進行打包管理,由於咱們後面還得開發組件庫的文檔官網,而文檔官網相關入口都在 examples
中
咱們從初始化出來項目能夠看到,build
文件中的有關 webpack
的文件以下
├── webpack.base.conf.js 基礎配置文件
├── webpack.dev.conf.js 本地服務配置文件
├── webpack.prod.conf.js 打包配置文件
├── webpack.test.conf.js 測試配置文件(這裏先不作過多描述)
複製代碼
初始化的打包 output
輸出的目錄是 dist
,這個目錄是整個項目打包後輸出的目錄,並非咱們組件庫須要的目錄。既然不是咱們想要的,那咱們想在須要的目錄是怎麼樣的呢?
lib/vui.js
(組件庫 js 主文件)lib/vui-css/index.css
(組件庫 css 主文件,這一章節咱們對 css 打包不作過多描述,後面章節會單獨講解)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
文件以前咱們須要先了解兩個打包文件須要作的事情
webpack.build.js
:輸出 lib/vui.js
組件庫 js 主文件,會用到 webpack.base.conf.js
和 webpack.dev.conf.js
相關配置webpack.build.min.js
:輸出 examples/dist
文檔相關文件,會用到 webpack.base.conf.js
和 webpack.dev.conf.js
相關配置既然兩個 webpack
打包文件都會用到 webpack.base.conf.js
和 webpack.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.js
和 webpack.dev.conf.js
兩個文件都調整好後,從新執行一下 npm run dev
出現上圖表示此時大家的本地服務文件已經按照預想修改爲功啦 ~
一、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.json
的 scripts
中了
"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
,走你
至此,有關本地服務以及兩個打包文件便已改造完成,下面咱們嘗試將 npm
使用起來 ~
注意,若是你尚未屬於本身的 npm
帳號的話,請先自行到 npm
官網註冊一個帳號,點擊這裏進入官網進行註冊 ,註冊步驟比較簡單,這裏我就不過多作描述了,若是有疑問,能夠在討論羣問我
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
官網就能搜索並看到你剛發佈好的包啦 ~
目前組件庫,咱們寫了一個最簡單的 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
複製代碼
選擇一個本地存在的 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>
複製代碼
至此,咱們便已經成功改造了本地服務文件,實現了組件庫主文件的打包以及文檔官網主入口的打包,並在最後學會了如何使用 npm
進行項目的發佈。
下一章節,我將對組件庫中 css
文件打包進行講解。
上一節,咱們已經弄好了 js 文件的打包。但對於組件庫,咱們要作到的不只僅只是對 js 文件進行管理,還須要對 css 文件進行管理,這樣才能保證組件庫後續的使用。
本節中,我將會講述如何在基於 webpack
構建基礎的項目中合理使用 gulp
對 css 文件進行單獨的打包管理。
開始以前,咱們須要明確兩個目標:
爲了方便管理,每建立一個新組件時,咱們須要建立一個對應的 css 文件來管理組件的樣式,作到單一管理
這裏,咱們將會把全部的 css 文件都存放到 packages/vui-css
目錄下,具體結構以下
├── src
│ ├── common 存放組件公用的css文件
│ ├── mixins 存放一些mixin的css文件
│ ├── index.css css主入口文件
│ ├── hello.css 對應hello組件的單一css文件
├── gulpfile.js css打包配置文件
├── package.json 相關的版本依賴
複製代碼
開始寫組件的 css 前,咱們要明確一些點:
符合這兩種狀況的方式,我的以爲目前市場上比較好的方式就是對組件進行單一的 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-salad
和 precss
插件,以下
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-salad": require('./salad.config.json'),
"postcss-url": {},
"precss": {},
"autoprefixer": {},
}
}
複製代碼
OK,這個時候再次運行項目,則能看到 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 文件須要的依賴差很少,只不過這裏是基於 gulp
的 postcss
插件。開始配置 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 文件以下圖所示
OK,到這裏,你已經能夠單獨引入組件及其樣式了。最後爲了讓使用者可以直接使用你組件的 css ,別忘記將其發佈到 npm
官網哦 ~ 步驟以下
# 進到vui-css目錄
cd packages/vui-css
# 發佈
npm publish
複製代碼
至此,咱們已經完成了 css 文件的管理和單獨打包,完成了對 css 文件單一的輸出。如此這樣,咱們可以對組件庫 css 文件的開發和管理有了一個較好的方式的同時,可以方便組件庫的使用!
目前爲止,咱們已經構建好了組件庫須要的新目錄,js 文件和 css 文件的打包咱們也改造好了,組件庫開發的前置工做咱們已經作好了比較充實的準備,但咱們仍需作一些很是重要的前置工做以方便組件庫後續組件的開發和維護。
而對於前端測試,它是前端工程方面的一個重要分支,所以,在咱們的組件庫中怎麼能少掉這麼重要的一角呢?對於單元測試,主要分爲兩種
在本章節中,我將帶領你們使用基於項目初始化自帶的 Karma
+ Mocha
這兩大框架對咱們的組件庫中的組件進行單元測試。
對於 Karma
+ Mocha
這兩大框架,相信大多數接觸過單元測試的人都不會陌生,但這裏我以爲仍是有必要單獨開一小節對着兩大框架進行一個簡單的介紹。
爲了能讓咱們的組件庫中的組件可以運行在各大主流 Web 瀏覽器中進行測試,咱們選擇了 Karma 。最重要的是 Karma 是 vue-cli
推薦的單元測試框架。若是你想了解更多有關 Karma 的介紹,請自行查閱 Karma 官網
simple
,flexible
,fun
的測試框架Promise
coverage
測試報告before()
, after()
, beforeEach()
, 以及 afterEach()
四個鉤子函數,方便咱們在不一樣階段設置不一樣的操做以更好的完成咱們的測試這裏我介紹一下 mocha
的三種基本用法,以及 describe
的四個鉤子函數(生命週期)
describe(moduleName, function): describe
是可嵌套的,描述***測試用例***是否正確
describe('測試模塊的描述', () => {
// ....
});
複製代碼
**it(info, function):**一個 it
對應一個單元測試用例
it('單元測試用例的描述', () => {
// ....
})
複製代碼
斷言庫的用法
expect(1 + 1).to.be.equal(2)
複製代碼
describe
的生命週期
describe('Test Hooks', function() {
before(function() {
// 在本區塊的全部測試用例以前執行
});
after(function() {
// 在本區塊的全部測試用例以後執行
});
beforeEach(function() {
// 在本區塊的每一個測試用例以前執行
});
afterEach(function() {
// 在本區塊的每一個測試用例以後執行
});
// test cases
});
複製代碼
想了解更多 mocha
操做的同窗能夠點擊下面的連接進行查閱
上面一小節,我給你們簡單介紹了一下 Vue 官方推薦的測試框架 Karma
和 Mocha
,也但願你們看到這裏的時候可以對單元測試及常見測試框架能有個簡單的瞭解。
在單元測試實戰開始前,咱們先看看 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
複製代碼
從上面 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
複製代碼
上面咱們介紹了單元測試的部分有關靜態斷定的用法,接下來咱們將測試一些異步用例以及一些交互事件。在測試以前,咱們需稍微改動一下咱們的 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 庫也成爲優秀中的一員的話,咱們也應該擼一個本身文檔官網。
一個好的文檔官網,須要作到兩點。
因爲本博文中,我帶領你們開發的組件庫是適配移動端的,那麼如何讓咱們的文檔官網既有 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 文件的轉化,以及不一樣設備的路由適配。
思路捋清後,接下來繼續咱們的文檔官網開發實戰吧!
從上面我給出的目錄能夠看到,在 docs 文件夾裏面存放的都是 markdown 文件,每個 markdown 文件都對應一個組件的 API 文檔。咱們是想要的結果是,轉化 docs 裏面的每個 markdown 文件,使其變成一個個 Vue 組件,並將轉化好的 Vue 組件註冊到路由中,讓其能夠經過路由對每個 markdown 文件進行訪問。
對於 markdown 文件解析成 Vue 組件,市場上有不少三方 webpack
插件,固然若是你要是對 webpack
造詣比較深的話,你也能夠嘗試本身擼一個。這裏我是直接使用的 餓了麼團隊
開發出來的 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 文件加上高亮主題,也沒有設置好文檔頁面的基本樣式而已。因此,接下來,咱們須要給咱們的 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
OK,咱們的 md 文件已經成功解析成 Vue 組件,並有了漂亮的高亮主題和簡潔的基本樣式了 ~
前面我有說過,本文帶領你們開發的組件庫是適配移動端的,因此咱們須要作到 PC 端展現文檔,移動端展現 Demo。
在這一小節,我會帶着你們進行不一樣端路由的適配。固然,這個東西不難,主要是利用 webpack 構建多頁面的特性,那麼具體怎麼作呢?好了,很少扯,我們直接開始吧
第一步,註冊 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
})
]
複製代碼
入口文件註冊完成,接下來咱們須要作的是對設備環境進行斷定。這裏,我將使用 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 文件的"完美"轉化,以及不一樣設備環境下路由的適配。文檔官網的開發(上)到這裏就要告一段落了,下一章節,咱們將繼續完成文檔官網剩餘的開發工做!
上一章節,咱們已經完成了:
這一章節,咱們將完善文檔官網的細節,開發出一個完整的文檔官網。
從上一章給出的目錄咱們能夠知道,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端分爲三個部分,分別爲:
接下來,讓咱們開始來完成PC 端 API 的展現吧
頭部相對簡單點,咱們只須要在 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 進行查看
左側欄,是咱們展現組件路由和標題的地方。其實就是對 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
咱們把咱們寫好的 page-header.vue
和 side-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>
複製代碼
而後,再次訪問頁面,結果如圖
移動端 Demo 和 PC 端原理差很少,都得解析 nav.config.json
文件從而進行展現
目前咱們移動端除了主入口頁面 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 列表展現組件
在 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
我的準備從新撿回本身的公衆號了,以後每週保證一篇高質量好文,感興趣的小夥伴能夠關注一波。
美團點評長期招人,若是有興趣的話,歡迎一塊兒搞基,簡歷投遞方式交流羣中有說明 ~
小夥伴們大家還在等什麼呢?趕忙先給文章點波贊,而後關注我一波,而後加羣和大佬們一塊兒交流啊 ~~~