我日常比较喜欢对一些东西作一些记录和总结,其中包括一些组件,积累的量比较多的时候,发现零散的堆积已经不太适合进行管理了。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
我的准备从新捡回本身的公众号了,以后每周保证一篇高质量好文,感兴趣的小伙伴能够关注一波。
美团点评长期招人,若是有兴趣的话,欢迎一块儿搞基,简历投递方式交流群中有说明 ~
小伙伴们大家还在等什么呢?赶忙先给文章点波赞,而后关注我一波,而后加群和大佬们一块儿交流啊 ~~~