突然接到一个需求,改造某个项目的打包产物,原来会生成多个文件,现在要求修改为只要一个ESM
文件。
定睛一看,是个Vite
项目,Vite
的版本是v5.0.10
,vite.config.ts
是这样的:
typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'property',
fileName: 'property'
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
}
})
这个与Vite
官方推荐的库模式差不多,应该是参考配置的。
现状
打包后的dist
目录如下:
bash
dist
|-- abap-936FyI36.js
|-- ...
|-- ...
|-- property.js
|-- property.umd.cjs
|-- ...
|-- ...
|-- style.css
|-- ...
|-- ...
`-- yaml-GSgOAuiZ.js
0 directories, 81 files
我们看到,property.umd.cjs
中,是全量的代码。本来引用它是不错的,但是需求是要一份ESM
代码,不是UMD
的。
UMD
这里稍微解释下什么是UMD
,年轻的朋友可能不认识它。
UMD
,全称为Universal Module Definition
,即通用模块定义,是一种JavaScript
模块定义的规范。它的目标是使一个模块的代码在各种模块加载器或者没有模块加载器的环境下都能正常运行。
UMD实现了这个目标,通过检查存在的JavaScript
模块系统并适应性地提供一个模块定义。如果AMD
(如RequireJS
)存在,它将定义一个AMD模块,如果CommonJS
(如Node.js
)存在,它将定义一个CommonJS
模块,如果都不存在,那么它将定义一个全局变量。
我们以一个名为AI
的包为例,以下就是UMD
的核心代码:
javascript
(function (root, factory) {
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = factory()
} else if (typeof define === 'function' && define.amd) {
define([], factory)
} else if (typeof exports === 'object') {
exports['AI'] = factory()
} else {
root['AI'] = factory()
}
})(this, function () {
// AI的业务代码
})
这种方式使得你的模块可以在各种环境中使用,包括浏览器和服务器。
UMD
是ESM
(ECMAScript Modules
)出现前的产物,它当然不可能兼容ESM
,而由于ESM
的特殊性,UMD
也做不到兼容ESM
。
ESM
我们再看property.js
文件,它是标准的ESM
,内容是这样的:
javascript
import { i as f } from "./index-o0DaZxYG.js";
export {
f as default
};
dist
目录下生成了这么多的文件,是因为默认情况下,将node_modules
里的包都生成了一个JavaScript文件。
下来,我们一步步处理这个需求。
方案
合并node_modules
先把node_modules
中的文件合并成一个。这涉及到Vite
的分包策略。
这时,为rollupOptions
中配置manualChunks
即可:
diff
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
},
+ manualChunks: (id: string) => {
+ if (id.includes('node_modules')) {
+ return 'vendor'
+ }
+ }
}
}
不好,居然报错了:
error during build:
RollupError: Invalid value for option "output.manualChunks" - this option is not supported for "output.inlineDynamicImports".
我们到Rollup文档里找下inlineDynamicImports
:
这明显是说锅是UMD
的,inlineDynamicImports
与manualChunks
是冲突的,不能同时存在。Vite
底层处理UMD
时估计配置了这个选项。我们到Vite
的GitHub源码里也找到这段,验证了我们的想法:
这是说UMD
和IIFE
(Immediately Invoked Function Expression
,立即调用函数表达式)以及SSR
的某种条件下,会开启这个选项。
正好我们也不需要UMD
,就把Vite
的默认选项替换掉,只需要添加formats
即可:
diff
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'property',
fileName: 'property',
+ formats: ['es', 'cjs']
},
这样只会生成ESM
和CommonJS
两种了:
diff
dist
|-- property.cjs
|-- property.js
|-- style.css
|-- vendor-T520oB1z.js
`-- vendor-XYPt9q-o.cjs
0 directories, 5 files
这个名称我们未必满意,可以进行一次修改:
diff
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'property',
formats: ['es', 'cjs'],
- fileName: 'property',
+ fileName: (format) => `property.${format}.js` // 打包后的文件名
},
这样,新的文件名就是这样了:
diff
dist
|-- property.cjs.js
|-- property.es.js
|-- style.css
|-- vendor-T520oB1z.js
`-- vendor-XYPt9q-o.cjs
0 directories, 5 files
如果你喜欢.mjs
、.cjs
的后缀,也都是可以的。
合并vendor
我们的需求是只要一个主JS文件,那么还不能要vendor
,怎么办呢?
很简单:
diff
manualChunks: (id: string) => {
- if (id.includes('node_modules')) {
return 'vendor'
- }
}
这样,就将所有文件都合并成一个JS了。
bash
dist
|-- property.cjs.js
|-- property.es.js
`-- style.css
0 directories, 3 files
至于函数返回的字符串是什么,就无所谓了。
合并style.css
我们的组件的样式都在style.css
中,还需要用户单独引入,多少不便。
Vite
官方并没有提供相关的插件或配置项。
这个看着像,但是可能只针对外部的chunk,我们这种情况未生效。
使用插件
我在掘金上找到了这篇文章《实现一个打包时将CSS注入到JS的Vite插件》:
大佬就是厉害,帮我们造好了轮子。
不过当我打开大佬留的仓库,居然变成只读了:
好吧,毕竟这篇文章是22年的了。
好在大佬说明了归档的原因,原来强中自有强中手,大佬安利了另一个插件:
于是,我们只需要引用这个插件就可以了:
diff
+ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
export default defineConfig({
plugins: [
vue(),
+ cssInjectedByJsPlugin()
],
build: {
}
})
这次只会生成2个文件:
bash
dist
|-- property.cjs.js
`-- property.es.js
0 directories, 2 files
我们来看下文件中第一行注入的内容(以下是展开后的):
javascript
(function () {
"use strict";
try {
if (typeof document < "u") {
var A = document.createElement("style");
A.appendChild(
document.createTextNode(
'@charset "UTF-8";:root{--el-color-white:#ffffff;}'
// style.CSS的内容
)
),
document.head.appendChild(A);
}
} catch (e) {
console.error("vite-plugin-css-injected-by-js", e);
}
})();
其实就是往HTML的head
中注入了这一段全局的CSS。
我们再仔细看这一句typeof document < "u"
很有意思,typeof document
有两种可能的值,当它存在时是object
,不存在是undefined
,object < u
,undefined > u
,所以typeof document < "u"
相当于是typeof document === "object"
或者typeof document !== "undefined"
。这是JS代码压缩的一种特殊手段,我们平时要是写出这样的代码肯定要给人骂死。
大功告成!

TIPS
其实,如果不考虑必须内嵌CSS的话,有个更简便的方法可以这样处理:
diff
rollupOptions: {
external: ['vue'],
output: {
+ intro: 'import "./style.css";',
manualChunks: (id: string) => {
return 'vendor'
}
}
}
这个intro
与banner是等价的配置项,表示往bundle后的代码的头部注入一段信息,与之对应的是outro
和footer
。
typescript
// rollup.config.js
export default {
...,
output: {
...,
banner: '/* my-library version ' + version + ' */',
footer: '/* follow me on Twitter! @rich_harris */'
}
};
这样,生成的JS文件中就有了以下内容:
typescript
var Ea = (s, e, t) => (Bg(s, typeof e != "symbol" ? e + "" : e, t), t);
import "./style.css";
import { getCurrentScope, onScopeDispose, unref, getCurrentInstance, onMounted, nextTick, watch, ref, defineComponent, openBlock, createElementBlock, createElementVNode, warn, computed, inject, isRef, shallowRef, onBeforeUnmount, onBeforeMount, provide, mergeProps, renderSlot, toRef, onUnmounted, useAttrs as useAttrs$1, useSlots, withDirectives, createCommentVNode, Fragment, normalizeClass, createBlock, withCtx, resolveDynamicComponent, withModifiers, createVNode, toDisplayString as toDisplayString$1, normalizeStyle, vShow, cloneVNode, Text as Text$1, Comment, Teleport, Transition, readonly, onDeactivated, vModelRadio, createTextVNode, reactive, toRefs, onUpdated, withKeys, vModelText, pushScopeId, popScopeId, renderList } from "vue";
...
当用户引入这个JS时,就会动态引用CSS了。
当然,前提是这个JS是要被Vite
或Webpack
等打包工具处理的,如果在HTML里直接引入,虽然网络里下载到了这个CSS文件,但浏览器校验它不是JavaScript,仍是要报错的。
修改package.json
你是不是以为万事大吉了?慢着,别着急走。
由于我们修改了打包后生成的文件名,别忘了修改package.json
中对应的这几项配置:
json
{
"main": "./dist/property.cjs.js",
"module": "./dist/property.es.js",
"exports": {
".": {
"import": "./dist/property.es.js",
"require": "./dist/property.cjs.js"
}
}
}
总结
本文介绍了如何将Vite
项目中的lib仓库打包为一个JavaScript
文件,提供了详细的步骤和代码示例,包括如何合并node_modules
、修改Vite
配置以及使用插件进行样式注入等,如果修改了文件名,记得修改package.json
中对应的配置。
最终修改的代码如下:
typescript
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
export default defineConfig({
plugins: [
...,
cssInjectedByJsPlugin(),
],
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'property',
formats: ['es', 'cjs'],
fileName: (format) => `property.${format}.js` // 打包后的文件名
},
rollupOptions: {
output: {
manualChunks: (id: string) => {
return 'vendor'
}
}
}
}
})