目标:搭建一个基于vue的ui脚手架,库的加载方式支持
script标签
,模块化
加载,组件引入方式支持按需加载和全量加载。 脚手架链接
目录、文件约定
完整的一个组件库脚手架不但应该有合理的组件存放目录,也应该拥有组件demo展示页方便组件的开发和展示,所以需要我们提供两种webpack配置,一个用于组件打包,一个用于组件单页应用,然后,慢慢加上gulp/rollup。
出于开发调试方便的考虑,现在直接把包打进了node_modules中去,正式使用时按需修改build/const.js
中LIB_ROOT
的值。
正确的调试顺序:
- npm install
- npm run build:lib 这个命令会先把目录清空,所以放在build:es前
- npm run build:es
- npm run dev
如何使用webpack打包类库
平时使用
webpack
打包应用程序较多,如何使用它来打包工具库
,ui库
秘密藏在ouput
配置项里,webpack既可以打包一般的网站应用,也可以打包出支持多种环境下使用的类库。libraryTarget
声明打包出来的模块类型,library
跟模块导出命名相关。
简单来说,我们平时打包应用就是生成多个提供给script标签
引用的文件。配置了上面两个字段,生成出来的文件就可以被script
/require
来使用了。而其他配置大概一致,也是按你的文件类型,生成物要求添加对应的loader、plugin。譬如需要vue-loader
处理.vue文件,使用了vue-jsx需要安装@vue/babel-preset-jsx
相关插件支持,babel-loader
等。
output: {
path: path.resolve(__dirname, '../', LIB_DIR),
// filename: '[name]/index.js',
library: 'wind-ui-vue',
libraryTarget: 'umd',
},
组件库组件的开发模式,书写模板及全量打包
组件库的组件如何被使用
平时做应用,我们一个组件就是一个.vue
文件,需要的时候就import进来。但是开发组件库是不太一样的,因为组件库环境没有Vue
变量,需要开发者提供,所以需要做一层包装去获取实际开发环境才提供的Vue
变量,每个组件都被自己的index.js
包裹一层。使用打包器时,这里用到了Vue.use
的功能,它会调用对象参数提供的install
方法。只要我们在方法里添加了注册函数,就自动完成组件的全局注册了。
import Button from './Button.vue'
Button.install = Vue => {
Vue.component(Button.name, Button)
}
if (typeof window != undefined && window.Vue) {
Button.install(Vue)
}
export default Button
import Vue from 'vue'
import { Button } from 'wind-ui-vue'
Vue.use(Button)
如何全量引入组件
既然每个组件有自己的注册文件了,那么只要再搞一个入口文件,把每个组件都引入进去。
import Button from './components/button/index'
import './components/button/index.scss'
import Card from './components/card/index'
import './components/card/index.scss'
const install = (Vue) => {
Vue.use(Button)
Vue.use(Card)
}
if (typeof window != undefined && window.Vue) {
install(Vue)
}
export {
Button,
Card
}
export default {
install
}
全量打包来了
全量组件的入口文件已经写好,这时把它做成一个webpack入口,那么就能把所有的组件打包成一个大包了。
组件如何打包以支持按需加载 —— webpack 多入口
组件库越来越大,如何让每个组件单独打包,一个组件一个包
按需加载的前提是什么?
是全部组件不能打包到一起。webpack是一个入口对应一个出口,所以我们需要为每个组件生成一个入口。
主要是用node的fs模块读写文件操作居多,把所有组件的入口文件index.js
找出来,做成webpack入口,再配置对应的出口路径,我们设计成源码和打包代码的目录结构大致相同。
const getFiles = function (dirs, fileReg) {
if (!Array.isArray(dirs)) {
dirs = [ dirs ]
}
return dirs.reduce((arr, dir) => {
let files = fs.readdirSync(dir)
if (!files.length) return []
files = files.reduce((arr, file) => {
let res = []
const filePath = path.join(dir, file)
fileReg.test(file) && res.push(filePath)
// if is directory
if (fs.statSync(filePath).isDirectory()) {
res = res.concat(getFiles(filePath, fileReg))
}
return arr.concat(res)
}, [])
return arr.concat(files)
}, [])
}
const entries = (() => {
let files = getFiles('./src/components', /^index\.js$/)
files = files.reduce((obj, file) => {
return Object.assign(obj, {
[file.replace('src', '').replace('.js', '')]: './' + file
})
}, {})
return files
})()
module.exports = merge(baseConf, {
mode: 'development',
entry: {
...entries,
'index': './src/index.js'
},
output: {
path: path.resolve(__dirname, '../', LIB_DIR),
// filename: '[name]/index.js',
library: 'wind-ui-vue',
libraryTarget: 'umd',
},
plugins: [
new CleanWebpackPlugin()
]
})
看起来,类库的配置相比于一般的应用配置复杂度可以降低一点,因为不用考虑缓存,开发打包效率等。
如何按需加载?
每个组件自己一个文件这个前提有了。接下来呢?简单啊!!譬如需要Button
组件就import Button from 'wind-ui-vue/lib/components/button/index.js'
这样引入,和我们平时在项目里引入文件一样的道理。但这么写多了,不得不说有点累,而且需要记住组件的目录位置,谁会去node_modules
里找啊。这时该babel-import-plugin
出场了,它能帮我们优化这么烦人的操作,看看文档。
get到了没?它在帮我们替换源码!!!把es6的import语法转换到对应的路径下去找组件。虽然这不是tree-shaking,是掩眼法。但是确实帮我们大忙了。可是我的Button不是在wind-ui-vue/lib/button
,而是在wind-ui-vue/lib/components
下,怎么办?其实还有个字段libraryDirectory
可以声明组件的路径前缀。按实际情况处理就可以了。
webpack打包后的文件结构
组件样式与组件逻辑分离,单独打包 —— gulp
不同的组件的样式分开打包,组件的样式表可单独打包,不与组件js逻辑捆绑。看看以下几种使用姿势
按需加载组件
import { Button } from 'wind-ui-vue'
,理想情况下当然希望组件对应的样式一并加载进来。对于vue组件,可以直接把<style>
写进.vue
文件是可以的。但如果想考虑得通用些,希望把样式表单独拿出来,这样样式就与逻辑分离了。
现在考虑的问题:样式分离后如何被自动地被组件引用?上面提到的babel-import-plugin
插件提供了解决办法,看下图,style
字段看起来是声明组件样式表所在的目录(true时为style目录,其他值表示子路径)。
因此我们要把样式文件放在每个组件的style目录中去。这里我们可以生成一个.js
文件,在里面去引入对应的.scss
文件。该文件会被插件自动插入到组件中去。
需要注意的是,这种思路下样式文件是与组件逻辑相互独立的文件,因此库打包时样式文件没经过webpack的处理,是不是更干净?只有在应用打包时再由webpack
处理这些scss
文件。gulp
处理后的文件结构如图所示。
核心的代码就这个,也是在声明目录下找出所有scss文件进行加工。
gulp.task('esScss', () => {
return scssTask(ES_DIR)
})
function scssTask (DIR) {
// 把所有组件的样式表找出来
const files = getFiles(['./src/components'], /^index\.scss$/)
files.forEach(file => {
const {
dir,
base
} = path.parse(file);
// 拷贝库的一些公用的样式
fse.copySync(
path.resolve(__dirname, 'src/style'),
path.resolve(__dirname, DIR, 'style')
)
// generate a .js file to require matched .scss file in every component directory and copy it
const destPath = path.join(__dirname,
DIR,
dir.replace(/[^\/]+\//, '')
)
// 把.scss源码文件也拷贝过去
fse.copyFileSync(
path.resolve(__dirname, file),
path.resolve(
destPath,
base
)
)
// 生成一个引入了scss文件的js文件,给babel-import-plugin用的
mkdirp.sync(path.resolve(destPath, 'style'))
fs.writeFileSync(path.resolve(destPath, 'style', 'index.js'), `require ('../index.scss')`)
})
}
全量加载组件
import win from 'wind-ui-vue'
,此时没有了babel插件的帮忙,我们只能手动引入全部的组件样式,在入口文件挨个地import .scss
文件就是了,和import .js
文件一个道理
import Button from './components/button/index'
import './components/button/index.scss'
import Card from './components/card/index'
import './components/card/index.scss'
const install = (Vue) => {
Vue.use(Button)
Vue.use(Card)
}
if (typeof window != undefined && window.Vue) {
install(Vue)
}
export {
Button,
Card
}
export default {
install
}
考虑到umd
这种模块化方案使用,需要提供.css
文件。
- 组件单独引用时,每个组件的样式是单独的样式文件,用gulp处理。
gulp.task('lib', ['libScss'], () => {
return gulp.src('src/**/*.scss')
.pipe(sass.sync().on('error', sass.logError))
.pipe(postcss([ autoprefixer() ]))
.pipe( gulp.dest(LIB_DIR, { sourcemaps: true }) )
})
- 全量引入组件时,将所有组件样式打包成一份
.css
。回头看看我们注册了所有组件的index.js
文件里已经引入了所有的组件样式,所以把它们抽离出来就是了。-
webpack打包器使用
mini-css-extract-plugin
插件(自己控制生成目录)const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { module: { rules: [ { test: /\.scss$/, use: [ { loader: MiniCssExtractPlugin.loader, }, 'css-loader', 'postcss-loader', { loader: 'sass-loader', } ] } ] }, plugins: [ new MiniCssExtractPlugin() ] }
-
rollup打包器使用
rollup-plugin-scss
插件(自己控制生成目录)import scss from 'rollup-plugin-scss' module.exports = { input: './' + file, output: { file: file.replace('src', ES_DIR), format: 'esm' }, plugins: [ scss(), ] }
-
提供ui库的es6模块版本 —— rollup
像上面打包成
umd规范
的模块,虽然提供了所有的环境支持,但是依然无法通过import win from 'wind-ui'
这种es6的语法引入,即不支持esModule
。另外,由于webpack兼容上的考虑,打包组件上加入了自己一些多余的编译代码,造成冗余。那有没办法单独打包es6
的版本呢?可以试试rollup
emmmm... rollup-plugin-vue
相当于webpack的vue-loader
,output.format
配置为esm
即为esModule
,babel.runtimeHelpers
设为true
可以减少内联的runtime代码,让打出来的包更精简。
思路和webpack一致,也是把所有组件文件找出来,进行转码后输出。rollup因为目标更单一,所以输出文件体量更可观。和webpack互为补充吧。
import commonjs from 'rollup-plugin-commonjs'
import VuePlugin from 'rollup-plugin-vue'
import resolve from '@rollup/plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import scss from 'rollup-plugin-scss'
const babelConf = require('./babel.config')
const { getFiles } = require('./build/util')
const { ES_DIR } = require('./build/const')
const entries = (() => {
let files = getFiles('./src/components', /^index\.js$/)
console.log('files', files)
files.push('src/index.js')
files = files.map(file => {
return {
input: './' + file,
output: {
file: file.replace('src', ES_DIR),
format: 'esm'
},
// .replace('.js', '')
plugins: [
scss(),
resolve(),
babel({
runtimeHelpers: true,
exclude: 'node_modules/**'
}),
commonjs(),
VuePlugin()
]
}
})
console.log('files', files)
return files
})()
module.exports = entries
执行npm run build:es
后,webpack生成的Lib
目录同级多了一个es
目录了。当然对生成资源还可以视实际情况进行更多的发挥。
在项目上使用ui库
具体在demo页上查看效果,源码在site/src/main.js
(省略了<script>
标签引入的展示)
// esModule方式全量引入
import win from 'wind-ui-vue/es'
import 'wind-ui-vue/es/index.css'
Vue.use(win)
// commonjs module方式全量引入
const win = require('wind-ui-vue').default
import 'wind-ui-vue/lib/index.css'
Vue.use(win)
// 按需加载,需配合`babel-import-plugin`使用
// 此时Card组件并没有打包进去,Button组件的样式已自动加载
import { Button, Card } from 'wind-ui-vue'
Vue.use(Button)
参考资料
- vant-ui 之前项目用了vant,本组件库搭建脚手架的思路也多参考于vant,谢谢开源