1、“共享组件不直接打包进代码”什么意思?
比如你写了一个组件库,里面有Button
、Dialog
、MessageBox
3个组件,其中Dialog
、MessageBox
组件都引用了Button
组件,那么Button
组件就是共享组件。当对这3个组件挨个打包完成后去查看Dialog
、MessageBox
组件打包完成后的产物代码,会发现Dialog
、MessageBox
组件里面都包含了Button
组件产物代码,这样一来产物体积就变大了。
那有没有什么办法使得打包完成后的Dialog
、MessageBox
组件产物里不包含Button
组件产物代码呢?
答案肯定是有的,比如ant-design-vue
、element-ui
它们打包后的产物代码中就不包含共享组件代码,它们是通过import
或require
去加载共享组件。
如ant-design-vue
打包产物代码:
2 、怎么实现呢?
这个问题也困扰了我很久,直到我用esbuild
去打包我的【vue3 bootstrap图标组件库】时我才解决这个问题。
原理很简单:把共享组件当成外部扩展(Externals)
在打包时通常会把vue
设置为外部扩展,那共享组件为甚么不能设置为外部扩展呢?将共享组件设置为外部扩展后webpack或其他打包工具就不会将其打包进产物中,而是以import
或require
的形式去加载。
看到这里你应该豁然开朗了吧!接下来的代码你就会写了。
3、代码实现(vue3)
我这里以esbuild
打包为例,webpack或vite代码差不多。
首先需要安装2个关键依赖:npm i esbuild esbuild-plugin-vue -D
接下来安装esbuild打包进去依赖:npm i esbuild-plugin-progress -D
安装css处理依赖:npm i postcss esbuild-sass-plugin autoprefixer postcss-preset-env postcss-import -D
目录结构:
-my-project
+node_modules
-src
-components
yn-button.scss
YnButton.vue
YnDialog.vue
YnMessageBox.vue
App.vue
main.js
build-lib.js
package.json
yn-button.scss
.yn-button{
transition: all .3s;
}
YnButton.vue
YnDialog.vue
YnMessageBox.vue
3.1、常规打包(共享组件打包进产物)
build-lib.js
const path = require('path');
const vue = require('esbuild-plugin-vue').default; // 处理vue组件
const esBuild = require('esbuild');
const progress = require('esbuild-plugin-progress'); // esbuild打包进度条
// scss、css处理
const { sassPlugin } = require('esbuild-sass-plugin');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const postcssPresetEnv = require('postcss-preset-env');
const postcssImport = require('postcss-import');
function build (entryPoint) {
esBuild.build({
bundle: true,
entryPoints: [entryPoint],
outdir: 'libs',
external: ['vue'],
loader: {
'.ts': 'ts'
},
format: 'esm',
drop: ["console", "debugger"], // 移除console、debugger信息
treeShaking: true,
plugins: [
sassPlugin({
async transform (source) {
const { css } = await postcss([
autoprefixer,
postcssPresetEnv(),
postcssImport()
]).process(source, { from: undefined });
return css;
}
}),
vue(),
progress()
]
});
}
build(path.resolve(__dirname, './src/components/YnButton.vue'));
build(path.resolve(__dirname, './src/components/YnDialog.vue'));
build(path.resolve(__dirname, './src/components/YnMessageBox.vue'));
在控制台执行node ./build-lib.js
,在libs目录将会有6个打包产物:
有没有发现不对,应该打包出4个产物才对,因为YnDialog、YnMessageBox组件根本没有添加css,但打包后却多出了2个css文件,这就是将共享组件打包进代码的问题之一
再来看下打包后的产物:
YnButton.js
YnButton.css
YnDialog.js
YnDialog.css
通过查看YnDialog.js
产物的内容,发现里面包含了YnButton
组件的代码,显然这不是我们想要的
3.2、分离共享组件代码打包
esbuild
有一个external
参数可以配置指定的依赖为外部扩展,但它是静态的,只适用于配置一些全局的外部扩展,对于动态的、需要根据条件去判断的外部扩展它是做不到的。
那有什么办法可以做到呢?答案就是:plugin(插件)
我们可以写一个插件来完成
const path = require('path');
const vue = require('esbuild-plugin-vue').default; // 处理vue组件
const esBuild = require('esbuild');
const progress = require('esbuild-plugin-progress'); // esbuild打包进度条
// scss、css处理
const { sassPlugin } = require('esbuild-sass-plugin');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const postcssPresetEnv = require('postcss-preset-env');
const postcssImport = require('postcss-import');
function build (entryPoint) {
esBuild.build({
bundle: true,
entryPoints: [entryPoint],
outdir: 'libs',
external: ['vue'],
loader: {
'.ts': 'ts'
},
format: 'esm',
drop: ["console", "debugger"], // 移除console、debugger信息
treeShaking: true,
plugins: [
sassPlugin({
async transform (source) {
const { css } = await postcss([
autoprefixer,
postcssPresetEnv(),
postcssImport()
]).process(source, { from: undefined });
return css;
}
}),
{ // 自定义设置外部扩展 esbuild插件
name: 'my-esbuild-plugin',
setup (build) {
let filter = /./;
/*
onResolve钩子是一个路径解析的钩子,是esbuild去解析路径的时候触发的,
也就是你执行import ... from ...的时候会调用onResolve钩子
*/
build.onResolve({ filter: filter }, function (args) {
// console.log('args', args);
let modulePath = args.importer;
let kind = args.kind;
let path = args.path;
let fileInComponents = modulePath.includes('/components/');
// 只有通过导入的,并且在components目录下的、以.vue结尾的文件才能视为外部扩展
let external = fileInComponents && kind === 'import-statement' && path.endsWith('.vue');
// 不需要指定为外部扩展的依赖直接return null
if (!external) {
return null;
}
/*
将import路径中的'src/components/'部分替换成 './',因为打包后它们都是在同一个文件夹。
打包后都产物中共享组件将以 import XX from './YY.vue' 形式引入
*/
// tip: 这部分逻辑需要根据自己项目目录结构来判断是否需要这样做
if (path.startsWith('src/components/')) {
path = path.replace('src/components/', './');
path = path.replace('.vue', '.js');
}
return {
path,
// 是否需要 external (如果该模块为external,则esbuild构建时不会去加载该模块,而是保留原有的import xxx from 'yyy'代码)
external
};
});
}
},
vue(),
progress()
]
});
}
build(path.resolve(__dirname, './src/components/YnButton.vue'));
build(path.resolve(__dirname, './src/components/YnDialog.vue'));
build(path.resolve(__dirname, './src/components/YnMessageBox.vue'));
再到控制台运行node ./build-lib.js
命令,此时在libs目录下只有4个产物了:
再来看下产物代码:
YnButton.js
YnButton.cssYnButton
组件打包产物没有变化,没问题
YnDialog.jsYnDialog
组件打包产物没有再包含YnButton组件代码,而是通过import
形式引入,这就是我们想要的结果。
大功告成!