源代码是无法直接在浏览器端运行的,所以需要打包工具转换代码到es5。当然我们需要熟悉babel
,
熟悉前端的打包工具 webpack
、rollup
、gulp
,其中webpack偏向于应用开发,rollup偏向库开发。
需求:
1、ES6转ES5,支持JSX
2、生成ESM和UMD规范文件,UMD区分压缩和未压缩版
3、支持SASS预编译样式
使用 webpack 打包 UMD 和 ESM
我们还行先用webpack的方式来编译组件库,从中可以看到一些困难,再换成rollup的方式,对比下看到rollup推荐更简单
npm i webpack webpack-cli terser-webpack-plugin clean-webpack-plugin copy-webpack-plugin -D
UMD 很容易生成
webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
entry: {
myUI: "./src/index.js",
"myUI.min": "./src/index.js",
},
output: {
// 导出的文件名
filename: "[name].js",
path: __dirname + "/dist",
// umd selft is not defined 报错
globalObject:'this',
library: {
// 指定库的全局变量名称
name: "myUI",
type: "umd",
export: "default",
}
},
mode: "none",
optimization: {
// 默认mode:'development'会压缩文件,mode:'none'就不会压缩了,使用TerserPlugin只对min.js压缩
minimize: true,
minimizer: [
new TerserPlugin({
include: /\.min\.js$/,
}),
],
},
plugins: [
// 清空 dist 目录
new CleanWebpackPlugin(),
// 复制 umd main.js 文件,to 会根据output定位
// new CopyPlugin({
// patterns: [{ from: "./main.js", to: "./main.js" }],
// }),
],
};
ESM 很麻烦生成
webpack还不支持esm模式打包,该特性仍然是实验性的,并且没有完全支持,确保事先启用 [experiments.outputModule]
module.exports = {
// …
experiments: {
outputModule: true,
},
output: {
library: {
// do not specify a `name` here
type: 'module',
},
},
};
所以操作方式改为:
每一个组件都作为入口文件导出'umd'模式,都打包成单独的文件
const TerserPlugin = require("terser-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require("path");
const fs = require("fs");
const browserslistrc = (() => {
const content = fs.readFileSync(
path.resolve(__dirname, ".browserslistrc"),
"UTF-8"
);
return content.split("\n").map((m) => m.trim());
})();
// 获得所有组件 {"button":"./src/components/button/index.js"}
const componentsObject = glob
.sync(`src/components/**/index.js`, {
dot: true,
})
.map((x) => path.resolve(x))
.map((x) => path.dirname(x).split(path.sep).pop())
.reduce((p, name) => {
p[name] = `./src/components/${name}/index.js`;
return p;
}, {});
module.exports = {
entry: {
myUI: "./src/index.js",
"myUI.min": "./src/index.js",
...componentsObject,
},
output: {
// 导出的文件名
filename: "[name].js",
path: __dirname + "/dist",
// umd self is not defined 报错
globalObject: "this",
library: {
// 指定库的全局变量名称
name: "[name]",
type: "umd",
export: "default",
},
},
mode: "none",
optimization: {
// 默认mode:'development'会压缩文件,mode:'none'就不会压缩了,使用TerserPlugin只对min.js压缩
minimize: true,
minimizer: [
new TerserPlugin({
include: /\.min\.js$/,
}),
],
},
plugins: [
// 清空 dist 目录
new CleanWebpackPlugin(),
// 拆分 css 到独立文件
new MiniCssExtractPlugin({
filename: "theme-chalk/[name].css",
}),
// 复制 umd main.js 文件,to 会根据output定位
// new CopyPlugin({
// patterns: [{ from: "./main.js", to: "./main.js" }],
// }),
],
resolve: {
extensions: [".js", ".ts", ".jsx", ".tsx"],
},
module: {
rules: [
{
test: /\.(woff|woff2|eot|ttf)$/,
type: "asset",
generator: {
filename: "fonts/[name].[contenthash:8][ext]",
},
},
{
test: /\.(png|jpe?g|gif|svg)/i,
type: "asset", // asset asset/inline asset/resource
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 10K
},
},
},
{
test: /\.(jsx?|tsx?)$/i,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: "3",
targets: { browsers: browserslistrc },
},
],
[
"@babel/preset-react",
{
runtime: "automatic", // classic automatic
},
],
"@babel/preset-typescript",
],
},
},
},
{
test: /\.(css|sass|scss)/i,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"autoprefixer",
{
overrideBrowserslist: browserslistrc,
},
],
],
},
},
},
"sass-loader",
],
},
],
},
};
方式一:通过文件路径进行引用,单独去引入每个需要的组件完全路径
import 'echarts/lib/chart/pie'
import 'echarts/lib/component/title' ,
方式二:使用 babel-plugin-component 引入组件
使用 babel-plugin-component
就能像下面一样引用组件了,需要在babel
中增加配置
import { Button, Select } from 'element-ui'
Vue.use(Button)
Vue.use(Select)
babel-plugin-component
插件将:
import { Button } from 'myLib'
转换成:
var button = require('myLib/lib/button')
require('myLib/lib/theme-chalk/button.css')
-
lib
是默认查找的文件夹,所以我们也把组件js代码生成到lib
文件夹下 -
组件的css分离到单独的文件,这里我们用
mini-css-extract-plugin
插件,放到lib/theme-chalk
目录下
修改 babel.config.js
配置:
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk",
},
"element-ui",
],
[
"component",
{
libraryName: "myui",
/**
* styleLibraryName: 'theme-chalk' 等价于下面,不过会要求有base.css
*/
styleLibrary: {
name: "theme-chalk", // same with styleLibraryName
base: false, // if theme package has a base.css
},
},
],
],
};
报错解决
-
Uncaught TypeError: (0 , vue__WEBPACK_IMPORTED_MODULE_0__.pushScopeId) is not a function
新建vue3项目测试正常,vue2项目会这样报错,可能是一些webpack版本问题
-
JSX 编写组件,Invalid VNode type: Symbol(Text)
本地用 npm link 项目调试报错,发布到npmjs上安装就正常了
使用 Rollup 打包
和webpack相比,rollup更加的小巧简介,它更加适用于构建各种类库,要比webpack方便很多,按需加载组件的时候也。不需要借助插件
,不需要像上面webpack还需要bable-plugin-component
,在类库打包方面是挺优秀的
大家如果先看了 rollup从入门到打包一个按需加载的组件库 ,前端工程化(一)从零开始搭建组件库这篇文章,最终会出现两个问题:
- esm 规范 打包在一个文件里面后,新项目像 `import { button } from 'myui';` 依然没有摇树
- css文件全部打包进一个文件了,css还是得要全量引入
所以还是得 umd和每个组件分开打包,然后web项目依然通过 babel-plugin-component
按需引入
装包
npm i rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-commonjs rollup-plugin-postcss autoprefixer@8.0.0 rollup-plugin-vue@6.0.0 @vue/compiler-sfc cssnano rollup-plugin-terser sass rollup-plugin-delete @vue/babel-preset-app glob -D
vue2和vue3项目所用的rollup-plugin-vue版本不一样,vue的编译器也不一样。
- vue2:
rollup-plugin-vue^5.1.9
+vue-template-compiler
- vue3:
rollup-plugin-vue^6.0.0
+@vue/compiler-sfc
Rollup 和 webpack 打包差不多
- 区分 umd 和 esm 文件,esm文件还是按照组件文件多个入口
rollup.config.js
import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import postcss from "rollup-plugin-postcss";
import autoprefixer from "autoprefixer";
import cssnano from "cssnano";
import vue from "rollup-plugin-vue";
import { terser } from "rollup-plugin-terser";
import del from "rollup-plugin-delete";
const glob = require("glob");
const path = require("path");
// 获得所有组件 {"button":"./src/components/button/index.js"}
const componentsObject = glob
.sync(`src/components/**/index.js`, {
dot: true,
})
.map((x) => path.resolve(x))
.map((x) => path.dirname(x).split(path.sep).pop())
.reduce((p, name) => {
p[name] = `./src/components/${name}/index.js`;
return p;
}, {});
const configFn = (name) => ({
plugins: [
vue(),
babel({
exclude: "node_modules/**",
runtimeHelpers: true,
}),
commonjs(),
postcss({
plugins: [autoprefixer(), cssnano()],
extract: `theme-chalk/${name}.css`,
}),
terser(),
],
external: [
//外部库, 使用'umd'文件时需要先引入这个外部库
"vue",
],
});
const comConfigs = Object.keys(componentsObject).map((name) => {
const config = configFn(name);
config.input = [componentsObject[name]];
config.output = {
file: "./lib/" + name + ".js",
format: "es",
};
return config;
});
const umdConfig = {
input: "./src/index.js",
output: [
{
file: "./dist/my-lib-umd.js",
format: "umd",
name: "myLib",
},
{
file: "./dist/my-lib-es.js",
format: "es",
},
{
file: "./dist/my-lib-cjs.js",
format: "cjs",
},
],
...configFn("index"),
};
umdConfig.plugins.unshift(del({ targets: ["lib/*", "dist/*"] }));
export default [umdConfig, ...comConfigs];
package.json
{
"name": "tryuirollup",
"version": "1.0.12",
"description": "",
"main": "dist/my-lib-cjs.js",
"module": "dist/my-lib-es.js",
"scripts": {
"build": "rollup -c"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@vue/babel-preset-app": "^5.0.8",
"@vue/compiler-sfc": "^3.2.37",
"autoprefixer": "^8.0.0",
"cssnano": "^5.1.12",
"rollup": "^2.77.2",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-vue": "^6.0.0",
"sass": "^1.54.4"
},
"dependencies": {
"glob": "^8.0.3"
}
}