rollup+Vue2+elementUI 搭建公司业务组件库
前言
我这个小公司没有自己的组件库,导致许多项目中相同功能的组件都是使用复制张贴的形式放到根目录的 components 文件夹下面。但是如果修改了一些代码需要进到每个项目的对应文件再修改(一般情况是不会更改),所以搭建一个公司自己的组件库还是很有必要。
搭建公司私有Npm仓库: Docker + Verdaccio 搭建Npm私有仓库
项目地址: gitee.com/little_desi…
我后面是对着项目的文件作用还有每个包的作用给大家说明,不会一步步带着做。
我这里是搭建Vue2的组件库,想看Vue3的推荐: 手摸手教你用Vue3+Typescript+Rollup+Tailwinds打造插拔式的业务组件库
项目结构
代码规范文件
这里代码规范文件有 .husky、.eslintrc、commitlint.config.js、prettier.config.js 文件。
这里可以看 项目规范(husky+prettier+eslint+commitlint)
package.json
我最想讲这个,搭建的过程中认识到了很多包的作用,理解为什么需要使用到这个包。
代码
{
"name": "ac-common-components",
"version": "1.1.2",
"main": "dist/ac-common-components.esm.js",
"author": "yxy",
"scripts": {
"prepare": "husky install",
"lint:lint-staged": "lint-staged",
"commit": "cz",
"build": "npm run clean && npm run build:esm && npm run build:umd",
"build:esm": "rollup --config build/rollup.esm.config.js",
"build:umd": "rollup --config build/rollup.umd.config.js",
"clean": "rimraf ./dist"
},
"dependencies": {
"core-js": "^3.22.0",
"element-ui": "^2.11.1",
"vue": "2.6.10"
},
"devDependencies": {
"@babel/core": "^7.17.9",
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@rollup/plugin-node-resolve": "^13.2.0",
"@vue/compiler-sfc": "^3.2.33",
"@rollup/plugin-strip": "^2.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-latest": "^6.24.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.13.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-vue": "^8.6.0",
"husky": "^7.0.4",
"lint-staged": "^12.3.8",
"node-sass": "^7.0.1",
"postcss-import": "^14.1.0",
"prettier": "^2.6.2",
"rimraf": "^3.0.2",
"rollup": "^2.70.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-vue": "5.1.9",
"postcss": "^8.4.12",
"rollup-plugin-postcss": "^4.0.2",
"sass-loader": "^12.6.0",
"vue-template-compiler": "2.6.10"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
"prettier --write--parser json"
],
"package.json": [
"prettier --write"
],
"*.vue": [
"eslint --fix",
"prettier --write"
],
"*.{scss,less,styl,html}": [
"prettier --write"
],
"*.md": [
"prettier --write"
]
},
"browserslist": [
"> 1%",
"last 2 versions"
],
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
name、version、author
name: 这个包的名字。
version: 包的版本。
# 发布前的版本修改, 修改第一个 x.0.0
$ npm version major
# 发布前的版本修改, 修改第二个 1.x.0
$ npm version minor
# 发布前的版本修改, 修改第二个 1.0.x
$ npm version patch
# 当不记得后面的参数
$ npm version -h
author: 包作者的名字。
main
main很关键,当项目使用这个包的主入口。
执行:
npm run build
可以看打包后dist下面的文件 dist/ac-common-components.esm.js 这个就是入口。
为什么会有4个文件
先讲这个2个js文件的产生。就是后面的script
script
-
prepare、lint:lint-staged、commit 这三个不讲了就是代码规范上面用到的 项目规范(husky+prettier+eslint+commitlint)
-
build:
-
npm run clean
就是删除目录下的dist文件夹,删除掉之前打包的文件。使用到了 rimraf 包 -
npm run build:esm
rollup打包输出 esm 格式,打包的入口 build/rollup.esm.config.js -
npm run build:umd
rollup打包输出 umd 格式,打包的入口 build/rollup.umd.config.js
-
具体 这两个文件代码可以自行去看,没几行。
为什么打包两个格式呢? 最主要使用esm打包出来的文件。它是通过 export
命令显式指定输出的代码,再通过 import
命令输入。因此,esm
格式打出来的包,可读性确实非常棒。 umd
为了兼容 amd
和 CommonJS
环境打包的(公司没有使用低版本浏览器)。
这些格式有啥区别参考:
前端模块标准之CommonJS、ES6 Module、AMD、UMD介绍
dependencies 和 devDependencies 的区别
推荐我的上一篇文章: 你真的知道 dependencies 和 devDependencies ?
如果看了就发现我这个项目应该把 dependencies 换成 peerDependencies 更好点
dependencies
"core-js": "^3.22.0",
"element-ui": "^2.11.1",
"vue": "2.6.10"
安装了这 3 个包。
devDependencies
包
{
"devDependencies": {
"@babel/core": "^7.17.9",
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@rollup/plugin-node-resolve": "^13.2.0",
"@vue/compiler-sfc": "^3.2.33",
"@rollup/plugin-strip": "^2.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-latest": "^6.24.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.13.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-vue": "^8.6.0",
"husky": "^7.0.4",
"lint-staged": "^12.3.8",
"node-sass": "^7.0.1",
"postcss-import": "^14.1.0",
"prettier": "^2.6.2",
"rimraf": "^3.0.2",
"rollup": "^2.70.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-vue": "5.1.9",
"postcss": "^8.4.12",
"rollup-plugin-postcss": "^4.0.2",
"sass-loader": "^12.6.0",
"vue-template-compiler": "2.6.10"
}
}
把项目规范化的去掉
"devDependencies": {
"@babel/core": "^7.17.9",
"@rollup/plugin-node-resolve": "^13.2.0",
"@vue/compiler-sfc": "^3.2.33",
"@rollup/plugin-strip": "^2.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-external-helpers": "^6.22.0",
"node-sass": "^7.0.1",
"postcss-import": "^14.1.0",
"rimraf": "^3.0.2",
"rollup": "^2.70.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-vue": "5.1.9",
"postcss": "^8.4.12",
"rollup-plugin-postcss": "^4.0.2",
"sass-loader": "^12.6.0",
"vue-template-compiler": "2.6.10"
}
}
这里也把 "node-sass": "^7.0.1", "sass-loader": "^12.6.0", "rimraf": "^3.0.2",
去掉。
rollup 插件
rollup-plugin-postcss
它需要 "postcss": "^8.4.12",
。
PostCSS 是一种 JavaScript 工具,可将你的 CSS 代码转换为抽象语法树 (AST),然后提供 API(应用程序编程接口)用于使用 JavaScript 插件对其进行分析和修改。
PostCSS 提供了一个庞大的插件生态系统来执行不同的功能,如 linting、缩小、插入供应商前缀和许多其他事情。
它解决我们的问题是为什么?优势何在?
比如,我们用 SASS 来处理 box-shadow 的前缀,我们需要这样写:
/* CSS3 box-shadow */
@mixin box-shadow($top, $left, $blur, $size, $color, $inset: false) {
@if $inset {
-webkit-box-shadow: inset $top $left $blur $size $color;
box-shadow: inset $top $left $blur $size $color;
} @else {
-webkit-box-shadow: $top $left $blur $size $color;
box-shadow: $top $left $blur $size $color;
}
}
使用 PostCSS 我们只需要按标准的 CSS 来写就行了,因为最后 autoprefixer 会帮我们做添加这个事情~
box-shadow: 0 0 3px 5px rgba(222, 222, 222, .3);
所以,这里就出现了一个经常大家说的未来编码的问题。实际上,PostCSS 改变的是一种开发模式。
- SASS等工具:源代码 -> 生产环境 CSS
- PostCSS:源代码 -> 标准 CSS -> 生产环境 CSS
这样能体会出优势吧,但是目前大家都是 SASS + PostCSS 这样的开发模式,其实我认为是不错的,取长补短嘛,当然,在 PostCSS 平台上都是可以做到的,只是目前这个过渡期,这样更好,更工程化。
总结:
PostCSS 会跟 babel 一样把 css 代码转成 ast 然后在 css 代码上面添加一些想要的操作。
postcss-import
This plugin can consume local files, node modules or web_modules. To resolve path of an @import
rule, it can look into root directory (by default process.cwd()
), web_modules
, node_modules
or local modules. When importing a module, it will look for index.css
or file referenced in package.json
in the style
or main
fields. You can also provide manually multiples paths where to look at.
例子:
@import "cssrecipes-defaults";
然后这个 cssrecipes-defaults 文件是在 node modules 下面。 引用第三方 css 文件。
rollup 插件
-
rollup-plugin-node-resolve --- rollup 无法识别
node_modules
中的包,帮助 rollup 查找外部模块,然后导入 -
rollup-plugin-commonjs --- 将 CommonJS 模块转换为 ES6 供 rollup 处理
-
rollup-plugin-babel --- ES6 转 ES5,让我们可以使用 ES6 新特性来编写代码,使用了 @babel/core
-
rollup-plugin-terser --- 压缩 js 代码,包括 ES6 代码压缩
-
rollup-plugin-vue --- 编译 vue 文件, 内部就是使用了 vue-template-compiler
-
@rollup/plugin-strip --- 用于从代码中删除
debugger
语句和如assert.equal
和console.log
类似的函数。
@vue/compiler-sfc 与 vue-template-compiler
@vue/compiler-sfc
: 是解析 Vue3 的文件, 可以与 vue 的版本不一致。
vue-template-compiler
: 解析 Vue2 的文件, 但是需要跟 vue 版本一致。
还有一个混淆视听的概念不清楚 vue-loader 又是干嘛的。
vue-loader
: 是为了给 webpack
能够编译 Vue 文件做的一个 loader, 内部的编译还是使用到了 vue-template-compiler。
在它的 peerDependenciesMeta 下面 vue-template-compiler 是必须的。
vue 有编译器为什么还需要使用 vue-loader 呢?
因为 vue 的编译器编译 template 但是每个组件有导入其他的文件,在 html 中是使用不了 import 加载的,所以需要 webpack、rollup等打包工具把 import 转化成浏览器可以加载,所以 vue-loader 在打包的时候会告诉 webpack 如何来处理 .vue 的文件。
在 lib/index.js 下面
babel-plugin-component
因为我的项目中 packages/Download 中使用到 Elementui 的按需加载,其他文件使用到 Elementui 都是些元素节点被 vue-template-compiler 解析成了一个标签,所以只要有 Elementui 项目下都是能够使用的。 但是 packages/Download 中是使用到 js 加载。
import { Message, MessageBox } from 'element-ui';
所以就需要安装 babel-plugin-component 按需导入。
其实如果直接写成编译后的 代码也就不用了这个插件,当时为了学习所以还是搞了下。
// 直接写
var Button = require('element-ui/lib/message.js')
require('element-ui/lib/theme-chalk/message.css')
var Checkbox = require('element-ui/lib/message-box.js')
require('element-ui/lib/theme-chalk/message-box.css')
参考文章 按需加载原理分析
babel-plugin-external-helpers
以 async/await 为例子
// babel 添加一个方法,把 async 转化为 generator
function _asyncToGenerator(fn) { return function () {....}} // 很长很长一段
// 具体使用处
var _ref = _asyncToGenerator(function* (arg1, arg2) {
yield (0, something)(arg1, arg2);
});
不用过于纠结具体的语法,只需看到,这个 _asyncToGenerator
在当前文件被定义,然后被使用了,以替换源代码的 await
。但每个被转化的文件都会插入一段 _asyncToGenerator
这就导致重复和浪费了。
因此使用 babel-plugin-external-helpers
// 从直接定义改为引用,这样就不会重复定义了。
var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);
// 具体使用处是一样的
var _ref = _asyncToGenerator3(function* (arg1, arg2) {
yield (0, something)(arg1, arg2);
});
从定义方法改成引用,那重复定义就变成了重复引用,就不存在代码重复的问题了。
还有一个 babel-plugin-transform-runtime
也包含了这个功能。但是需要依赖 babel-runtime
想了解 babel 参考: 一口(很长的)气了解 babel
build
rollup.config.js
import vue from 'rollup-plugin-vue';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { name } from '../package.json';
import postcss from 'rollup-plugin-postcss';
import postcssImport from 'postcss-import';
import commonjs from 'rollup-plugin-commonjs';
import babel from 'rollup-plugin-babel';
import { terser } from 'rollup-plugin-terser';
import strip from '@rollup/plugin-strip';
const file = type => `dist/${name}.${type}.js`;
export { name, file };
export default {
input: 'packages/index.js',
output: {
name,
file: file('esm'),
format: 'es',
},
plugins: [
nodeResolve(),
babel({
exclude: 'node_modules/**', // 只转译我们的源代码
}),
vue({
css: true, // Dynamically inject css as a <style> tag
compileTemplate: true, // Explicitly convert template to render function
}),
postcss({
extensions: ['.css'],
extract: true,
plugins: [postcssImport()],
}),
commonjs({
include: ['node_modules/**', 'node_modules/**/*'],
}),
// 压缩代码
terser(),
// 剔除debugger、assert.equal和 console.log 类似的函数
strip({
labels: ['unittest'],
}),
],
external: ['vue'],
};
-
input: 项目的入口出。
-
output: 打包出的文件名称 文件路径 文件类型。
-
plugins: rollup 使用到的插件。
-
external: 我们使用到了 vue 但是不想把 vue打包到输出文件中去,所以使用 external。 有时候需要搭配 globals ,因为我们有时候是
import Vue from 'vue'
, rollup 就不认识这个 Vue 所以需要转换。
rollup 打包出来的文件格式区别参考以下文章:
前端模块标准之CommonJS、ES6 Module、AMD、UMD介绍
为什么使用rollup来打包而不是webpack?
rollup 诞生在esm标准出来后
-
出发点就是希望开发者去写esm模块,这样适合做代码静态分析,可以做tree shaking减少代码体积,也是浏览器除了script标签外,真正让JavaScript拥有模块化能力。是js语言的未来
-
rollup完全依赖高版本浏览器原生去支持esm模块,所以无额外代码注入,打包后的代码结构也是清晰的(不用像webpack那样iife)
-
目前浏览器支持模块化只有3种方法:
- ①script标签(缺点没有作用域的概念)
- ②script标签 + iife + window + 函数作用域(可以解决作用域问题。webpack的打包的产物就这样)
- ③esm (什么都好,唯一缺点 需要高版本浏览器)
-
webpack 诞生在esm标准出来前,commonjs出来后
-
当时的浏览器只能通过script标签加载模块
-
script标签加载代码是没有作用域的,只能在代码内 用iife的方式 实现作用域效果,
- 这就是webpack打包出来的代码 大结构都是iife的原因
- 并且每个模块都要装到function里面,才能保证互相之间作用域不干扰。
- 这就是为什么 webpack打包的代码为什么乍看会感觉乱,找不到自己写的代码的真正原因
-
-
关于webpack的代码注入问题,是因为浏览器不支持cjs,所以webpack要去自己实现require和module.exports方法(才有很多注入)
-
这么多年了,甚至到现在2022年,浏览器为什么不支持cjs?
- cjs是同步的,运行时的,node环境用cjs,node本身运行在服务器,无需等待网络握手,所以同步处理是很快的
- 浏览器是 客户端,访问的是服务端资源,中间需要等待网络握手,可能会很慢,所以不能 同步的 卡在那里等服务器返回的,体验太差
-
-
后续出来esm后,webpack为了兼容以前发在npm上的老包(并且当时心还不够决绝,导致这种“丑结构的包”越来越多,以后就更不可能改这种“丑结构了”),所以保留这个iife的结构和代码注入,导致现在看webpack打包的产物,乍看结构比较乱且有很多的代码注入,自己写的代码都找不到
总结
- rollup 打包成 esm 标准,打包后的文件会小于 webpack 打包的, webpack 由于内置许多自己的函数,所以代码量会多些。
- 还有 rollup 打包的文件(未 babel ) 可读性比 webpack 好。
参考文章: rollup打包产物解析及原理(对比webpack)
组件的开发
全局的install
import AcPage from './Page/index.js';
import AcUplaod from './Upload/index.js';
import AcSelect from './Select/index.js';
import Download from './Download/index.js';
import loadmore from './directives/load-more/index.js';
import onlyNumber from './directives/only-number/index.js';
// 放组件
const components = {
AcPage,
AcUplaod,
AcSelect,
};
// 放指令
const directives = {
loadmore,
onlyNumber,
};
// 绑定到Vue实例下的方法
const vueInstanceFun = {
Download,
};
// 暴露出一个 install 方法
const install = function (Vue) {
if (install.installed) return;
install.installed = true;
Object.values(components).map(component => {
Vue.component(component.name, component);
});
Object.values(directives).map(directive => {
Vue.directive(directive.name, directive);
});
Object.values(vueInstanceFun).map(fun => {
Vue.use(fun);
});
};
/** 支持使用标签方式引入 */
if (typeof window != 'undefined' && window.Vue) {
install(window.Vue);
}
export default install;
// 按需加载所需组件
export { AcPage, AcUplaod, Download, loadmore, onlyNumber };
这里我们把所以组件、自定义指令和自定义的函数,通过暴露出去的 install 函数,使用 Vue.use 方法,把 Vue 的实例传了进来,再根据各个不同的api进行注册。
./Page/index.js 为例
import AcPage from './src/index.vue'
// 按需加载
AcPage.install = Vue => Vue.component(AcPage.name,AcPage)
// 暴露组件
export default AcPage
下面看了Vue.use 就知道为什么有自定义一个 install 方法和在组件上面绑定一个。
Vue.use 源码
use api 的源码位置在 vue/src/core/global-api/use.js
.
/* @flow */
import { toArray } from '../util/index'
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 避免多次注册,只要已经注册过不会重新再进行注册。
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters, 把Vue.use(xxx, option) 这个option 获取到变成数组对象形式
const args = toArray(arguments, 1)
// this 就是 Vue的当前实例 放到参数的首位,这个就是前面install 函数可以获取到Vue
args.unshift(this)
// 这里就是 AcPage.install 就是走这个函数,因为在AcPage绑定有install
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args) // 这里是 全局 install 走的地方
}
// 再把注册完成插入到 installedPlugins 数组
installedPlugins.push(plugin)
return this
}
}
参考文章
参考文章
手摸手教你用Vue3+Typescript+Rollup+Tailwinds打造插拔式的业务组件库
绝对干货,为了彻底学会rollup打包vue组件,花了6小时逐步实现组件库打包、码文
转载自:https://juejin.cn/post/7094951355541880845