我平常比較喜歡對一些東西做一些記錄和總結,其中包括一些組件,積累的量比較多的時候,發現零散的堆積已經不太適合進行管理了。
於是我開始思考,有什麼好的辦法可以比較規範地來管理這些比較零散的東西呢?如果以組件庫這種形式來對組件進行管理的話,會不會更適合自己的積累並方便以後的工作呢?
於是我開始參考市場上一些優秀的UI 組件庫,比如element-ui 、 vux 、 vant等,對其源碼進行拜讀,了解其架構的搭建,隨後整理出一套屬於自己的移動端UI 組件庫vui 。
我在業餘時間活躍於各大技術社區,常有一些或工作一段時間的、或還在準備找實習工作的小伙伴問筆者一些問題:怎樣沉澱自己,做自己的框架、輪子、庫?怎樣做一個組件庫?自己做過一個組件庫會不會成為簡歷的亮點?你能不能寫一些有關組件庫開發的相關文章? ...
本著答惑解疑和分享的心情,這篇博文便誕生了。
如果小伙伴在閱讀文章實戰的時候有什麼問題的話,歡迎加入討論群一起討論(群裡除了一群大佬天天騷話外還有一群妹紙哦~ )
前端大雜燴:731175396
github:https://github.com/xuqiang521
廢話不多說,接下來,讓我們直接進入到實戰篇吧~
這裡我只談Mac 和window 下NODE 的安裝
如果你還沒有安裝mac 軟件包管理器homebrew的話第一步得先安裝它
/usr/bin/ruby -e " $( curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install ) "使用homebrew安裝node
brew install nodewindow環境的話直接進入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展示文件打包配置文件(新增文件)1、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
} )
]
}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.js和webpack.dev.conf.js兩個文件都調整好後,重新執行一下npm run dev
出現上圖表示此時你們的本地服務文件已經按照預想修改成功啦~
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 = config2、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
# + [email protected]這個時候你再去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
# + [email protected] 選擇一個本地存在的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的測試框架Promisecoverage測試報告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|bbd+|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地址:https://github.com/xuqiang521/personal-component-library
文章末尾再打一波廣告~~~
前端交流群:731175396
美團點評長期招人,如果有興趣的話,歡迎一起搞基,簡歷投遞方式交流群中有說明~
小伙伴們你們還在等什麼呢?趕緊先給文章點波贊,然後關注我一波,然後加群和大佬們一起交流啊~~~