前⾔ Vue是⼀套⽤于构建⽤户界⾯的渐进式框架,⽬前有越来越多的开发者在学习和使⽤。⽽组件库能帮我们节省开发精⼒,⽆需所有东⻄都从头开始去做,通过⼀个个⼩组件拼接起来,就得到了我们想要的最终⻚⾯。在⽇常开发中如果没有特定的⼀些业务需求,使⽤组件库进⾏开发⽆疑是更便捷⾼效,⽽且质量也相对更⾼的⽅案。
本⽂阐述了如何基于vue⼀步步完成⼀个UI组件库的打造。
组件库官⽹
github地址
npm地址
⼀、技术栈 我们先简单了解⼀下要搭建⼀个 UI 组件库,会涉及到哪些技术栈,下⾯是我选⽤的内容:
vue-cli:官⽅⽀持的 CLI 脚⼿架,提供⼀个零配置的现代构建设置;
Vue: 渐进式 JavaScript 库;
Jest:JavaScript 测试框架,⽤于组件库的单元测试;
⼆、组件开发 项⽬初始化
开始,需要创建⼀个空的vue项⽬,在此基础上我们才能开始接下来的组件库编写!
1 2 3 4 npm i -g vue-cli // yarn add global vue-cli vue init webpack heaven-ui //(heaven-ui)可以随意更换成你的名称 cd heaven-ui npm run dev
我们安装完依赖并进⼊项⽬启动服务后vue-cli3会⾃动给我们展示⼀个默认⻚⾯,这⾥我使⽤sass,⽤于美化ui组件的样式。
⽬录结构
这是我的⽬录结构和解释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |- build/ # webpack打包配置 |- lib/ # 打包生成的文件放这里 |- src/ # 在这里写代码 |- components/ # 各个组件,每个组件是一个子目录 |- mixins/ # 复用的mixin |- utils # 工具目录 |- App.vue # 本地运行的开发预览 |- index.js # 打包入口,组件的导出 |- main.js # 本地运行的运行 |- static/ # 存放一些额外的资源文件,图片之类的 |- test/ # 测试文件夹 |- specs/ # 存放所有的测试用例 |- jest.conf.js/ # jest单元测试配置 |- .npmignore |- .gitignore |- .babelrc |- README.md |- package.json
组件库结构
暴露⼀个所有组件的⼊⼝,组件详细代码只展示button部分
src/components/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 import Alert from './components/alert/index.js' import Button from './components/button/index.js' import ButtonGroup from './components/button-group/index.js' import Checkbox from './components/checkbox/index.js' import CheckboxGroup from './components/checkbox-group/index.js' import DatePicker from './components/date-picker/index.js' import Form from './components/form/index.js' import FormItem from './components/form-item/index.js' import Icon from './components/icon/index.js' import Input from './components/input/index.js' import Option from './components/option/index.js' import Pagination from './components/pagination/index.js' import Radio from './components/radio/index.js' import RadioGroup from './components/radio-group/index.js' import Rate from './components/rate/index.js' import Select from './components/select/index.js' import Switch from './components/switch/index.js' import Table from './components/table/index.js' import HTableColumn from './components/table-column/index.js' import Tag from './components/tag/index.js' const components = [ Button, ButtonGroup, Checkbox, CheckboxGroup, DatePicker, Form, FormItem, Icon, Input, Option, Pagination, Radio, RadioGroup, Rate, Select, Switch, Table, HTableColumn, Tag, ] const install = function(Vue, opts = {}) { components.map(component => { Vue.component(component.name, component); }) Vue.prototype.$alert = Alert; } /* 支持使用标签的方式引入 */ if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } export default { install, Alert, Button, ButtonGroup, Checkbox, CheckboxGroup, DatePicker, Form, FormItem, Icon, Input, Option, Pagination, Radio, RadioGroup, Rate, Select, Switch, Table, HTableColumn, Tag, }
src/components/button/index.js
1 2 3 4 5 6 7 import HButton from './src/button'; HButton.install = function(Vue) { Vue.component(HButton.name, HButton); }; export default HButton;
组件部分⼤概是这样⼦了
打包配置 ⽬录建好了,那就该填充⾎⾁了,要打包⼀个组件库项⽬,肯定是要先配置好我们的webpack,不然写了源码也没法跑起来。所以我们先定位到 build⽬录下
webpack.base.js 。存放基本的⼀些rules配置
webpack.prod.js 。整个组件库的打包配置
build/webpack.base.conf.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 'use strict' const path = require('path') const utils = require('./utils') const config = require('../config') const vueLoaderConfig = require('./vue-loader.conf') function resolve (dir) { return path.join(__dirname, '..', dir) } module.exports = { context: path.resolve(__dirname, '../'), entry: { app: process.env.NODE_ENV === 'production' ? './src/index.js' : './src/main.js' }, output: { path: config.build.assetsRoot, filename: '[name].js', publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), 'untils': resolve('src/untils'), } }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: vueLoaderConfig }, { test: /\.js$/, loader: 'babel-loader', include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash:7].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } }, { test: /\.scss$/, loaders:['style','css','sass'] } ] }, node: { // prevent webpack from injecting useless setImmediate polyfill because Vue // source contains it (although only uses it if it's native). setImmediate: false, // prevent webpack from injecting mocks to Node native modules // that does not make sense for the client dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty' } }
build/webpack.prod.conf.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 'use strict' const path = require('path') const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const baseWebpackConfig = require('./webpack.base.conf') const ExtractTextPlugin = require('extract-text-webpack-plugin') const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const env = require('../config/prod.env') const webpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true, usePostCSS: true }) }, devtool: config.build.productionSourceMap ? config.build.devtool : false, output: { path: config.build.assetsRoot, filename: 'heaven-ui.min.js', library: 'heaven-ui', libraryTarget: 'umd' }, plugins: [ // http://vuejs.github.io/vue-loader/en/workflow/production.html new webpack.DefinePlugin({ 'process.env': env }), new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false } }, sourceMap: config.build.productionSourceMap, parallel: true }), // extract css into its own file new ExtractTextPlugin({ filename: 'heaven-ui.min.css', }), // Compress extracted CSS. We are using this plugin so that possible // duplicated CSS from different components can be deduped. new OptimizeCSSPlugin() ] }) if (config.build.productionGzip) { const CompressionWebpackPlugin = require('compression-webpack-plugin') webpackConfig.plugins.push( new CompressionWebpackPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', test: new RegExp( '\\.(' + config.build.productionGzipExtensions.join('|') + ')$' ), threshold: 10240, minRatio: 0.8 }) ) } if (config.build.bundleAnalyzerReport) { const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin webpackConfig.plugins.push(new BundleAnalyzerPlugin()) } module.exports = webpackConfig
这⾥我配置的输出⽬录为lib,打包后的结果如下图:
三、单元测试 单元测试包含以下优点:
\1. 可能会测出功能的隐藏bug
\2. 保证代码重构的安全性。
组件库中每⼀个组件都可能会重构或者更新迭代,如果单元测试覆盖率⾼的话,修改代码之后就越可能会发现潜在的问题。⽐如版本升级后,导致某部分功能的缺失。
组件库开发调试完成后,我们需要编写每个组件对应的单元测试,以达到100%的覆盖率为⽬标。
以button为例:
test/specs/Button.spec.js
1 2 3 4 5 6 7 8 import Vue from 'vue' import Button from '@/components/button' describe('button.vue', () => { it('button是否存在',()=>{ expect(Button).to.be.ok; }) })
这⾥我配置的输出⽬录为lib
四、发布NPM 发布npm之前,我们必须按照npm的发包规则来编写我们的package.json, 我们先来解决组件库打包的问题,⾸先我们需要让脚⼿架编译我们的组件代码,并输出到指定⽬录下,我们按照发包规范⼀般会输出到lib⽬录下。项⽬打包完成后,我们需要编写package⽂件的description,keywords等,具体介绍如下:
description 组件库的描述⽂本
keywords 组件库的关键词
license 许可协议
repository 组件库关联的git仓库地址
homepage 组件库展示的⾸⻚地址
main 组件库的主⼊⼝地址(在使⽤组件时引⼊的地址)
private 声明组件库的私有性,如果要发布到npm公⽹上,需删除该属性或者设置为false
publishConfig ⽤来设置npm发布的地址,这个配置作为团队内部的npm服务器来说⾮常关键,可以设置为私有的npm仓库
发布到npm的⽅法也很简单, ⾸先我们需要先注册去npm官⽹注册⼀个账号, 然后控制台登录即可,最后我们执⾏npm publish即
可.具体流程如下:
1 2 3 4 5 6 // 登录 npm login // 发布 npm publish // 如果发布失败提示权限问题,请执行以下命令 npm publish --access public
注意:本次publish 版本号都需要修改版本号
五、总结 我们可以⽤vue-cli 或其他⼯具另外⽣成⼀个demo项⽬,⽤这个项⽬去引⼊我们的组件库。如果你的包还没有发布出去,可以在你的
组件库项⽬⽬录下 ⽤ npm link 或者 yarn link的命令创建⼀个link
然后在你的demo⽬录下使⽤ npm link package_name 或者 yarn link package_name 这⾥的package_name就是你的组件库的
包名,然后在你的demo项⽬的⼊⼝⽂件⾥
1 2 3 4 5 6 import Vue from vue import Heaven from 'heaven-ui' import 'heaven-ui/dist/heaven-ui.min.css'// 其他代码 ... Vue.use(Heaven)
这样设置好之后,我们创建的组件就可以在这个项⽬⾥使⽤了