如何基于vue开发ui组件库(heaven-ui)

前⾔

Vue是⼀套⽤于构建⽤户界⾯的渐进式框架,⽬前有越来越多的开发者在学习和使⽤。⽽组件库能帮我们节省开发精⼒,⽆需所有东⻄都从头开始去做,通过⼀个个⼩组件拼接起来,就得到了我们想要的最终⻚⾯。在⽇常开发中如果没有特定的⼀些业务需求,使⽤组件库进⾏开发⽆疑是更便捷⾼效,⽽且质量也相对更⾼的⽅案。

本⽂阐述了如何基于vue⼀步步完成⼀个UI组件库的打造。

组件库官⽹

github地址

npm地址

⼀、技术栈

我们先简单了解⼀下要搭建⼀个 UI 组件库,会涉及到哪些技术栈,下⾯是我选⽤的内容:

vue-cli:官⽅⽀持的 CLI 脚⼿架,提供⼀个零配置的现代构建设置;

Vue: 渐进式 JavaScript 库;

Jest:JavaScript 测试框架,⽤于组件库的单元测试;

⼆、组件开发

项⽬初始化

开始,需要创建⼀个空的vue项⽬,在此基础上我们才能开始接下来的组件库编写!

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组件的样式。

⽬录结构

这是我的⽬录结构和解释

|- 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

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

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

'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

'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

import Vue from 'vue'

import Button from '@/components/button'

describe('button.vue', () => {

it('button是否存在',()=>{

expect(Button).to.be.ok;

})

})

四、发布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即

可.具体流程如下:

// 登录

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项⽬的⼊⼝⽂件⾥

import Vue from vue

import Heaven from 'heaven-ui'

import 'heaven-ui/dist/heaven-ui.min.css'

// 其他代码 ...

Vue.use(Heaven)

这样设置好之后,我们创建的组件就可以在这个项⽬⾥使⽤了


原文链接:https://jue.leheavengame.com/article/60a89b56f631d777dcde0367

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容