前言
最近帮公司搭建ui组件库,主要考虑如下:
- 沉淀业务组件,避免高质量组件流失
- 形成文档,减少对组件内部代码关注,通过文档demo以及暴露出的属性方法使用组件
- 通过npm安装组件,更好的管理项目&组件
- 提升前端组件封装能力
技术
目前公司绝大多数项目是vue2.0
+ element2.x
的东西,已经做得非常成熟了,大概率不会升级vue3.0
,使用技术&工具如下:
- vue2.x
- markdown-it
- markdown-it-container: 处理markdown块
- markdown-it-front-matter:处理
front matter
- highlight.js:代码高亮
- standard-version:版本&日志
- rimraf:删除文件
- gulp:打包scss
- cp-cli:copy文件
目录
关于ui展示平台内容均在src
下,组件相关内容均在packages
下,将ui文档平台和ui组件分离开来
qjd-ui
├── build
│ ├── md_loader markdown文件解析
| |—— lib.config.js 组件打包配置
| |—— utils.js 工具
├── packages 基础&业务组件
│ ├── theme-default 组件样式统一编写打包
├── src
│ ├── components ui展示平台组件
│ │ ├── demo-block.vue md展示demo模板
│ ├── consts
│ │ ├── slider.js 导航栏路由信息
│ ├── docs 各个组件文档
│ │ ├── button.md
│ ├── pages ui平台页面
│ ├── test 测试代码
│ ├── router 路由
│ └── styles css
│ ├── md markdown样式
│ ├── utils ui平台工具
│ ├── getTestRoutes.js 动态匹配测试组件,无需手动配置
markdown相关
文档使用markdown
编写,根据路由信息匹配对应.md
文件即可,如下:
{
path: `/main/${e.key}`,
name: e.key,
// 日志在根目录
component: resolve => e.key == 'CHANGELOG' ? require([`../../CHANGELOG.md`], resolve) : require([`@/docs/${e.key}.md`], resolve)
}
markdown-it
我们需要借助markdown-it
等工具帮助vue
识别.md
文件:
// index.js
const markdown = require('markdown-it');
const hljs = require('highlight.js');
const md = markdown({
html: true,
typographer: true,
// 处理代码高亮
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return `<pre class="hljs" v-pre>
<code>
${hljs.highlight(lang, str, true).value}
</code>
</pre>`
} catch (error) {
console.log('error:' + error)
}
}
return `<pre class="hljs">
<code>${md.utils.escapeHtml(str)}</code>
</pre>`
}
})
const html = md.render(src)
return (
`<template>\n
<div class="markdown">
${html}
</div>\n
</template>\n
`
)
markdown-it-container
通过markdown-it-container
解析demo
并通过已写好的插槽(src/components/demo-block
)插入
// containerjs
const container = require('markdown-it-container')
module.exports = md => {
md.use(...createDemo('demo'))
}
function createDemo(kclass) {
return [container, kclass, {
validate: params => params.trim().match(/^demo\s*(.*)$/),
render: function (tokens, idx) {
const token = tokens[idx]
const info = token.info.replace('demo', '').trim()
let desc = info ? `<div class="demo_desc" slot="desc" >${info}</div>` : null
if (token.nesting === 1) {
return `<demo-block>
${desc}
<div slot="code">
`
}
return '</div></demo-block>\n'
}
}]
}
// 然后再index.js中引入
const markdown = require('markdown-it');
const containers = require('./container');
const md = markdown({...}).use(containers)
之后在.md
文件中就可以这样写demo了
<div class="demo_block">
我是代码
</div>
:::demo
```html
<!-- 我是代码 -->
```
:::
markdown-it-front-matter
借助markdown-it-front-matter
处理front matter
use(require('markdown-it-front-matter'), function (fm) {
const data = fm.split('\n')
data.forEach((item, index) => data[index] = item.split(':'))
data.map(item => result += `<span>${kinds[item[0]]}: ${item[1]}</span>`)
});
return (
`<template>\n
<div class="markdown">
<div class="demo-info">
${result}
</div>
${html}
</div>\n
</template>\n
`
)
之后在.md
文件中就可以这样写front matter
---
author: xxx
create: xxxx-xx-xx
update: xxxx-xx-xx
---
抽离script&style
抽离css和js,一个markdown文件仅允许有一个script和style
// 抽离
const scriptRe = /^<script(?=(\s|>|$))/i;
const styleRe = /^<style(?=(\s|>|$))/i;
md.renderer.rules.html_block = (tokens, idx) => {
const content = tokens[idx].content
if (scriptRe.test(content.trim())) {
scriptContent = content;
return ''
} if(styleRe.test(content.trim())) {
styleContent = content;
return ''
} else {
return content
}
}
return (
`<template>\n
<div class="markdown">
<div class="demo-info">
${result}
</div>
${html}
</div>\n
</template>\n
${scriptContent}
${styleContent}
`
)
配置loader
最后配置一下loader
{
test: /\.md$/,
use: [
{ loader: 'vue-loader' },
{
loader: require.resolve('./md_loader')
}
]
}
完成上述配置就可以在.md
文件中编写文档了
完整md_loader
// index.js
const markdown = require('markdown-it');
const hljs = require('highlight.js');
const kinds = require('./consts');
const containers = require('./container');
const scriptRe = /^<script(?=(\s|>|$))/i;
const styleRe = /^<style(?=(\s|>|$))/i;
module.exports = function (src) {
let result = '', scriptContent = '', styleContent = '';
const md = markdown({
html: true,
typographer: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return `<pre class="hljs" v-pre>
<code>
${hljs.highlight(lang, str, true).value}
</code>
</pre>`
} catch (error) {
console.log('error:' + error)
}
}
return `<pre class="hljs">
<code>${md.utils.escapeHtml(str)}</code>
</pre>`
}
})
.use(containers)
.use(require('markdown-it-front-matter'), function (fm) {
const data = fm.split('\n')
data.forEach(item => {
item = item.split(':')
result += `<span>${kinds[item[0]]}: ${item[1] ? item[1] : '--'}</span>`
})
});
md.renderer.rules.html_block = (tokens, idx) => {
const content = tokens[idx].content
if (scriptRe.test(content.trim())) {
scriptContent = content;
return ''
} if (styleRe.test(content.trim())) {
styleContent = content;
return ''
} else {
return content
}
}
const html = md.render(src)
return (
`<template>\n
<div class="markdown">
<div class="demo-info">
${result}
</div>
${html}
</div>\n
</template>\n
${scriptContent}
${styleContent}
`
)
}
// container.js
const container = require('markdown-it-container')
module.exports = md => {
md.use(...createDemo('demo'))
}
function createDemo(kclass) {
return [container, kclass, {
validate: params => params.trim().match(/^demo\s*(.*)$/),
render: function (tokens, idx) {
const token = tokens[idx]
const info = token.info.replace('demo', '').trim()
let desc = info ? `<div class="demo_desc" slot="desc" >${info}</div>` : null
if (token.nesting === 1) {
return `<demo-block>
${desc}
<div slot="code">
`
}
return '</div></demo-block>\n'
}
}]
}
// consts.js
module.exports = {
author: '作者',
create: '创建时间',
update: '更新时间'
}
ui组件
在packages
下统一编写组件,theme-default/src
下编写样式,通过gulp
打包样式
按需加载需要对各个组件单独打包,我们会在打包时会动态匹配packages
下.vue
组件,所以各个组件都必须建立相应的文件并建立index.js
组件并暴露出组件,否则会导致按需引入时失败,即使像ButtonGroup
这样的组件,虽然实现在button
文件夹下也仍需建立button-group
文件夹
entry
// 动态匹配各组件
exports.getEntries = () => {
const componentsContext = requireContext('../packages', true, /\.vue$/, __dirname).keys();
const defaultCom = { "qjd-ui": "./packages/index.js" }; // 批量打包
let coms = {}; // 存储各个组件用于独自打包
componentsContext.forEach(item => {
const keys = item ? item.split('\\') : [];
const key = keys[keys.length - 1] ? keys[keys.length - 1].split('.')[0] : '';
if (key) {
coms[key] = `./packages/${key}`;
}
});
return { ...defaultCom, ...coms }
}
// webpack中配置entry
{
entry: utils.getEntries(),
}
output
我们的组件最终会发布到npm
,所以跟普通打包不一样的是我们的入口需要配置library
{
output: {
path: path.resolve(__dirname, '../lib'),
publicPath: '/',
filename: '[name].js', // [入口名称].js
library: '[name]', // 暴露出的名称
libraryTarget: 'umd', // 通常选择umd,umd支持各个环境
umdNamedDefine: true
}
}
externals
因为是基于element-ui
基础组件进行二次封装的组件,我们在打包时对于第三方工具通常不会进行打包,如果在项目中使用,需在项目中自行安装element-ui
并引入相应的组件
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
},
'element-ui': 'ELEMENT'
}
批量打包
这个是我们批量打包组件是的入口文件
import Button from './button'
const components = {
Button,
}
const install = function (Vue) {
if (install.installed) return
Object.keys(components).forEach(key => {
Vue.component(components[key].name, components[key])
})
}
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
const API = {
install,
...components
}
module.exports = API
单独打包
这里是我们单独打包时对应的各个组件入口,为方便按需引入组件,为每个组价单独注入install
方法:
// install.js
export default el => el.install = Vue => Vue.component(el.name, el);
// 以button为例
import install from '../utils/install'
install(Button)
完整配置
// lib.config.js
const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const utils = require('./utils')
module.exports = {
entry: utils.getEntries(),
output: {
path: path.resolve(__dirname, '../lib'),
publicPath: '/',
filename: '[name].js',
library: '[name]',
libraryTarget: 'umd',
umdNamedDefine: true
},
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue'
},
'element-ui': 'ELEMENT'
},
resolve: {
extensions: ['.js', '.vue']
},
module: {
loaders: [{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: 'vue-style-loader!css-loader',
sass: 'vue-style-loader!css-loader!sass-loader'
},
postLoaders: {
html: 'babel-loader'
}
}
}, {
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}, {
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
loader: 'url-loader?limit=8192'
}]
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.optimize.UglifyJsPlugin({
uglifyOptions: {
ie8: false,
output: {
comments: false,
beautify: false,
},
mangle: {
keep_fnames: true
},
compress: {
warnings: false,
drop_console: true
}
}
}),
new CopyWebpackPlugin([
{
from: `./packages`,
to: `./packages`,
ignore: [
'theme-default/**'
]
}
]),
]
}
// util.js
exports.getEntries = () => {
const componentsContext = requireContext('../packages', true, /\.vue$/, __dirname).keys();
const defaultCom = { "qjdui": "./packages/index.js" }; // 批量打包
let coms = {}; // 存储各个组件用于独自打包
componentsContext.forEach(item => {
const keys = item ? item.split('\\') : [];
const key = keys[keys.length - 1] ? keys[keys.length - 1].split('.')[0] : '';
if (key) {
coms[key] = `./packages/${key}`;
}
});
return { ...defaultCom, ...coms }
}
打包命令
{
"clean": "rimraf lib",
"build": "node build/build.js",
"build:theme": "gulp build --gulpfile packages/theme-default/gulpfile.js && cp-cli packages/theme-default/lib lib/theme-default",
"build:packages": "webpack --config build/lib.config.js",
"build:ui": "npm run clean && npm run build:theme && npm run build:packages"
}
ui展示平台打包结果存放于dist文件夹,命令:
npm run build
组件库打包结果存放于lib文件夹,命令:
npm run build:ui
commit规范
好的commit
规范可以提升团队开发效率,以Angular
的commit
规范为模板制定规范,Commitizen可以帮助约束自身提交规范,日志的生成也会依赖commit
内容
type类型
type | 描述 |
---|---|
feat | 新功能 |
fix | 修复bug |
refactor | 代码重构 |
docs | 文档 |
test | 测试代码 |
pref | 优化 |
chore | 构建过程或辅助工具的变动 |
模块
每次commit涉及的模块
描述
每次改动的描述
示例
一次完整的commit
如下:
git commit -m 'feat(button): 为button添加disabled效果'
git commit -m 'docs(button): 编写button组件文档'
版本&日志
使用standard-version控制版本&生成日志,commit
类型为feat
和fix
会出现在日志中,分别对应Features
和Bug Fixes
模块
- major:主版本
- minor:次版本
- patch:修订版
- npm run release -- 1.0.0:执行该命令, 自定义版本为 1.0.0
- npm run release:100:执行该命令, 如果当前版本是 1.0.0 那么版本将被提升至 2.0.0
- npm run release:010: 执行该命令, 如果当前版本是 1.0.0 那么版本将被提升至 1.1.0
- npm run release:001: 执行该命令, 如果当前版本是 1.0.0 那么版本将被提升至 1.0.1
执行上述命令时,会有三个动作:生成版本、打tag、生成日志,日志存放于CHANGELOG.md
中,具体效果见日志部分
安装
npm install qjd-ui --save
使用
CDN引入
<!-- css -->
<link rel="stylesheet" href="https://unpkg.com/qjd-ui/lib/theme-default/index.css">/
<!-- html -->
<div id="app">
<xw-button>默认按钮</xw-button>
<xw-button type="primary">主要按钮</xw-button>
<xw-button type="success">成功按钮</xw-button>
<xw-button type="info">信息按钮</xw-button>
<xw-button type="warning">警告按钮</xw-button>
<xw-button type="error">危险按钮</xw-button>
</div>
<!-- js -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/qjd-ui/lib/qjdui.js"></script>
<script>
new Vue({
el: '#app'
})
</script>
批量引入
import qjdui from 'qjd-ui'
import 'qjd-ui/lib/theme-default/index.css'
Vue.use(qjdui)
按需引入
方式一(需手动引入组件&css)
import Button from 'qjd-ui/lib/button'
import 'qjd-ui/lib/theme-default/button.css'
Vue.component(Button.name, Button)
方式二
// 安装babel-plugin-import
yarn add babel-plugin-import -D
// 配置babel.config.js
{
"plugins": [
[
"import",
{
libraryName: 'qjd-ui',
customStyleName: (name) => {
return `qjd-ui/lib/theme-default/${name}.css`;
},
},
]
]
}
完成上述配置后引入方式如下:
import { Button, Input } from 'qjd-ui';
Vue.component(Button.name, Button)
Vue.component(Input.name, Input)
或者
Vue.use(Button)
Vue.use(Input)
结语
目前只是完成了初版,参考了emelent-ui
结构和css
的处理方式