+ build
| - webpack.config.js
+ demo
+ lib
| - button.js
| - icon.js
+ public
| + button
| - button.md
+ src
| + button
| - button.vue
| - index.js
+ test
| + unit
| + spec
| - button.spec.js
开发 ThunderUI
组件库的组件应该遵循以下几个约定:
-
一个组件应该单独一个文件夹,该文件夹需以组件的名称命名,使用中线连接
-
一个组件一般由三个文件构成但不限于这三个文件,如下:
文件 | 必需 | 作用 |
---|---|---|
index.js |
是 | 注册组件为 Vue 的一个插件,并导出该组件 |
xxx.js | xxx.vue |
是 | 组件结构与逻辑,其中 xxx 为组件的名称 |
- 每个组件都必需提供 name 属性,并且 name 属性应该由
Td
前缀加上组件名称组成,如下
// menu.js
export default {
name: `${NAMESPACE}-button`,
}
- 组件引用另外一个组件的时候,引入依赖组件时应该使用下面的方式
// good
import Icon from 'src/icon';
// bad
import Icon from '../icon';
- 因为采用
rollup
打包,只添加了类似src/icon
进行切割,所以引入文件时不要使用后缀,比如不要这样写src/icon/index.js
为了兼顾开发者的开发体验,需要在开发环境中给予必要的错误提示,注意添加环境,比如
if (someWarn) {
process.env.NODE_ENV !== 'production' && warn(something)
return
}
if (someError) {
process.env.NODE_ENV !== 'production' && error(something)
return
}
TODO
居多组件会依赖于其他组件,这个时候考虑到依赖组件的灵活性,可以使用一个对象来作为组件的props
,如果是使用vue
模板可以使用v-bind
,如果是jsx可以使用{...props}
<template>
<tooltip v-bind="tooltipProps" />
</template>
<script>
export default {
props: {
tooltipProps: Object
}
}
</script>
export default {
props: {
tooltipProps: Object
},
render() {
return <Tooltip {...props: this.tooltipProps} />
}
}
每个组件开发完成后需要对所有功能进行测试,包括props event api
等,单元测试存放在test/unit/specs
文件夹下,使用组件名.spec.js
命名,目前还差大量的组件单元测试
如果只是某个组件内使用的工具函数,可以直接存放在当前组件下,引入时不需要被external
,如果是公共工具函数,需要存放到src/utils
下,引入时也需要注意,必须通过src/utils/xx
来引入,这样才能达到external
的效果
传统的vue-cli生成的配置,使用webpack,这个没啥好介绍的
生产环境使用rollup进行构建,主要考虑文件体积,特别是按需加载
最开始打包是基于webpack的,在按需加载上存在的体积冗余会比较大,如:
webpack
打包特有的模块加载器函数,这部分其实有些多余,最好去掉- 使用
babel
转码时,babel
带来的helper
函数全部是内联状态,需要转成import
或require
来引入 - 使用
transform-rumtime
对一些新特性添加polyfill
,也是内联状态,需要转成import
或require
来引入 vue-loader
带来的额外代码,如normalizeComponent
、ssr处理、style处理等等,不做处理也是内联transform-vue-jsx
带来的额外函数引入,如mergeJSXProps
,不做处理也是内联
以上几个问题,如果只是一份代码,那不会有太大问题,但是如果是按需加载,用户一旦引入多个组件,以上的代码就会出现多份,带来严重的影响
采用后编译
采用后编译可以解决上面的各种问题,也有组件库是这样做的,比如cube-ui,但是这样有些不方便,因为用户需要设置各种alias
,还要保证好各种编译环境,如jsx
,使用太不方便,所以暂时不考虑
使用rollup打包,设置external(当然webpack也可以)外联helper函数
使用rollup
打包,可以直接解决问题1和问题4,设置external
可以解决transform-runtime
等带来的helper
,这取决于相关插件实现时是不是通过import
或require
来添加helper
的,如果是直接copy
的话,那就还得另找办法。最后决定就这种方案进行尝试
使用rollup
打包可能某些习惯和webpack
有些出入,在这里很多事需要引入插件来完成,比如引入node_modules
中的模块的话,需要加入rollup-plugin-node-resolve
,加载commonjs
模块需要引入rollup-plugin-commonjs
等等。另外还有些比较麻烦的,比如经常会这样写
import xx from './xx-folder'
然后希望模块打包器可以识别成
import xx from './xx-folder/index.js'
在rollup
里还是需要用插件来完成这件事,找到的插件都没能满足各种需求,比如还需要对alias
也能识别然后加上index.js
,最后还是需要自己实现这个插件
基本的rollup配置应该差不多是这样的
{
output: {
format: 'es',
// file: xx,
// paths:
},
input: 'xxx',
plugins: [
vue({
compileTemplate: true,
htmlMinifier: {
customAttrSurround: [[/@/, new RegExp('')], [/:/, new RegExp('')]],
collapseWhitespace: true,
removeComments: true
}
}),
babel({
...babelrc({
addModuleOptions: false,
addExternalHelpersPlugin: false
}),
exclude: 'node_modules/**',
runtimeHelpers: true
}),
localResolve({
components: path.resolve(__dirname, '../src')
}),
alias({
components: path.resolve(__dirname, '../src'),
resolve: ['.js', '.vue']
}),
replace({
'process.env.NODE_ENV': JSON.stringify('production')
})
],
// external
}
这里采用的rollup-plugin-vue
的版本是v3.0.0
,不采用v4
,因为打包出来的体积更小,功能完全满足组件库需要。因为会存在各种约定,比如组件肯定是存在render
函数(不一定指的就是手写render
或jsx
,只是不会有在js
中使用template
这种情况,这样的好处是可以使用runtime-only
的vue
),组件肯定不存在style
部分等等。
babel
的配置上基本不会有改变,只是rollup-plugin-babel
加上了runtimeHelpers
,用来开启transform-runtme
的。可能你会觉得为了更精简体积,应该去掉transform-runtime
,这里使用transform-runtime
的主要作用是为了接管babel-helpers
,因为这个babel-helpers
无法被external
。另外整个组件库用到的babel-runtime
其实也不多,主要是类似Object.assign
这样的函数,像这些函数,使用的话还是需要加上transform-runtime
的,或者需要自己实现,感觉没什么必要。类似Array.prototype.includes
这种无法被transform-runtime
处理的还是会避免使用的
localResolve
是自己实现的插件,用来添加index.js
,并且能支持alias
,
alias
插件用来添加alias
,并且需要设置后缀
replace
插件用来替换一些环境变量,比如开发环境会有错误提示,生成环境不会有,这里展示的是生产环境的配置。
所有优化的关键在于external
上,除了最基本引入的第三方库,比如vue
,需要external
外,还有比如Dialog
组件内部依赖了Button
组件,那是需要把Button
组件external
的
// Dialog 组件
import Button from 'src/Button'
其实就是所有的组件和共用的util
函数都需要external
,另外,
主要还需要处理的是babel-helper
等helper
函数,但是这里不能做到,我也没有去了解babel
是如何对这块进行处理的,最后还是需要transform-runtime
来接管它。
rollup
的external
配置是支持函数类型的,大概看tranform-runtime
这个插件源码可以找到addImport
这些方法,可以知道polyfill
是通过import
来引入的,可以被external
,所以只需要在rollup
配置的external
添加上类似函数就可以达到我们想要的效果
{
external (id) {
// 对babel-runtime进行external
return /^babel-runtime/.test(id) // 当然别忘了还有很多 比如vue等等,这里就不写了
}
}
这里就可以解决问题2和问题3
另外问题5,这个是如何来的呢,比如在写jsx
时,可能会这样写
在某个组件中依赖了另一个组件,考虑到扩展性,是支持对另一个组件进行`props`设置的,所以经常会这样写,在`template`中的话就类似于`v-bind="tolltipProps"`
这个时候`transform-vue-jsx`插件是会引入一个`helper`函数的,也就是`babel-helper-vue-jsx-merge-props`,大概看看`transform-vue-jsx`源码也可以得知,这个`helper`也是`import`进来的,所以可以把`external`改成
```js
{
external (id) {
return /^babel/.test(id)
}
}
这样就可以做到对所有helper
都使用import
的形式来引入,而且使用rollup
打包后的代码更可读,大概长这样
// Input组件
import _Object$assign from 'babel-runtime/core-js/object/assign';
var NAMESPACE = 'td';
var Input = { render: function render() {
var _this = this;
var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _vm.type === 'text' ? _c('label', { staticClass: "td-input", class: { 'is-warn': _vm.warn, 'is-disabled': _vm.disabled } }, [_c('span', { staticClass: "td-input__label" }, [_vm._v(_vm._s(_vm.label))]), _vm._v(" "), _c('input', _vm._g(_vm._b({ ref: "input", staticClass: "td-input__inner", attrs: { "disabled": _vm.disabled }, domProps: { "value": _vm.value }, on: { "blur": function blur(e) {
_this.$emit('blur', e);
}, "focus": function focus(e) {
_this.$emit('focus', e);
} } }, 'input', _vm.$attrs, false), _vm.inputListeners))]) : _vm.type === 'textarea' ? _c('label', { staticClass: "td-textarea", class: { 'is-warn': _vm.warn, 'is-disabled': _vm.disabled } }, [_c('span', { staticClass: "td-textarea__label" }, [_vm._v(_vm._s(_vm.label))]), _vm._v(" "), _c('textarea', _vm._g(_vm._b({ ref: "input", staticClass: "td-textarea__inner", attrs: { "disabled": _vm.disabled }, domProps: { "value": _vm.value } }, 'textarea', _vm.$attrs, false), _vm.inputListeners))]) : _vm._e();
}, staticRenderFns: [],
name: NAMESPACE + '-input',
props: {
type: {
type: String,
default: 'text'
},
value: [Number, String],
label: String,
disabled: {
type: Boolean,
default: false
},
warn: {
type: Boolean,
default: false
}
},
computed: {
inputListeners: function inputListeners() {
return _Object$assign({}, this.$listeners, {
input: this.handleInput
});
}
},
methods: {
select: function select() {
this.$refs.input.select();
},
handleInput: function handleInput(e) {
this.$emit('input', e.target.value);
}
}
};
Input.install = function (Vue) {
Vue.component(Input.name, Input);
};
export default Input;
vue插件把vue组件中的template
转成render
函数,babel插件做语法转换,因为external
的存在,保留了模块关系,整个代码看起来很清晰,很舒服,不像webpack
,都会添加一个模块加载函数...
最后贴下完整配置
// rollup.config.js
import path from 'path'
import resolve from 'rollup-plugin-node-resolve'
import babel from 'rollup-plugin-babel'
import babelrc from 'babelrc-rollup'
import vue from 'rollup-plugin-vue'
import alias from 'rollup-plugin-alias'
import localResolve from './local-resolve'
import replace from 'rollup-plugin-replace'
import deepClone from 'lodash.clonedeep'
import { uglify } from 'rollup-plugin-uglify'
import { minify } from 'uglify-es'
import commonjs from 'rollup-plugin-commonjs'
const utils = require('./utils')
// 获取所有的组件
const components = utils.readComponents('src')
// 获取 directives, mixins, utils 目录下的文件列表
const utilList = ['directives', 'mixins', 'utils'].reduce(
(list, util) => list.concat(utils.readUtils(`src/${util}`, util)),
[]
)
const config = {
output: {
format: 'es'
},
plugins: [
vue({
compileTemplate: true,
htmlMinifier: {
customAttrSurround: [[/@/, new RegExp('')], [/:/, new RegExp('')]],
collapseWhitespace: true,
removeComments: true
}
}),
babel({
...babelrc({
addModuleOptions: false,
addExternalHelpersPlugin: false
}),
exclude: 'node_modules/**',
runtimeHelpers: true
}),
localResolve({
src: path.resolve(__dirname, '../src')
}),
resolve({
extensions: ['.js', '.vue']
}),
alias({
src: path.resolve(__dirname, '../src'),
resolve: ['.js', '.vue']
}),
commonjs({
include: 'node_modules/**'
})
]
}
const configs = []
const output = process.env.TARGET === 'public' ? 'public/lib' : 'lib'
function genConfig() {
if (process.env.TARGET !== 'public') {
const externals = {}
const external = ['vue']
components.forEach(component => {
const isDir = component.indexOf('.') === -1
const key = `src/${component}`
if (isDir) {
// 重写引入路径
externals[key] = `@xunlei/thunder-ui-vue/lib/${component}.js`
// 所有组件需要external
external.push(key)
}
})
utilList.forEach(item => {
const name = item.split('.')[0]
const key = `src/${name}`
// 重写引入路径
externals[key] = `@xunlei/thunder-ui-vue/lib/${item}`
// 所有util需要external
external.push(key)
})
config.output.paths = externals
config.external = id => {
// 将依赖external
return (
external.indexOf(id) >= 0 || /^babel/.test(id) || /^\@xunlei/.test(id)
)
}
components.forEach(component => {
const isDir = component.indexOf('.') === -1
const name = component.split('.')[0]
const key = `src/${name}`
let _config = deepClone(config)
if (isDir) {
// 写入入口文件
_config.input = `src/${component}/index.js`
// 输出位置
_config.output.file = `${output}/${component}.js`
configs.push(_config)
}
})
utilList.forEach(item => {
const name = item.split('.')[0]
const key = `src/${name}`
let _config = deepClone(config)
// 写入入口文件
_config.input = `${key}.js`
// 输出位置
_config.output.file = `${output}/${name}.js`
configs.push(_config)
})
// 所有组件的引入入口,ES Module 格式
let esmGlobalConfig = deepClone(config)
esmGlobalConfig.input = 'src/index.js'
esmGlobalConfig.output.file = `${output}/index.js`
configs.push(esmGlobalConfig)
}
let globalConfig = deepClone(config)
// 所有组件的引入入口 打包成一个包含所有组件的js文件
globalConfig.input = 'src/index.js'
globalConfig.output.file = `${output}/thunder-ui-vue.js`
globalConfig.output.format = 'umd'
globalConfig.output.name = 'ThunderUIVue'
globalConfig.plugins.push(
uglify(
{
compress: {
warnings: false
}
},
minify
)
)
globalConfig.plugins.push(
replace({
'process.env.NODE_ENV': JSON.stringify('production')
})
)
// 对于全局引入的方式 内部组件不需要external
delete globalConfig.output.paths
delete globalConfig.external
configs.push(globalConfig)
}
genConfig()
export default configs
下面的截图是生产环境的版本,也就是没有了代码提示,也已经压缩混淆后的代码体积对比 左边是优化前,右边是优化后
node build/release xxVersion
发布代码到xnpm 需要遵守SemVer