打包(Bundle)是指将前端代码进行编译、压缩、合并等操作,将其打包成一个或多个文件,使其成为一个整体,以便于部署和使用。打包后的文件可以是一个或多个文件,可以是js、css、html等文件,也可以是图片、字体等资源文件。
项目配置文件
package.json 是一个标准的 npm 配置文件,用于描述项目的基本信息、依赖、脚本等。在项目根目录下执行 npm init -y
可以生成一个默认的 package.json
文件。
{
"name": "project", // 项目名称
"version": "1.0.0", // 项目版本
"description": "", // 项目描述
"main": "index.js", // 项目入口文件
"scripts": { // 指定运行脚本命令的 npm 命令行缩写
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",// 作者
"license": "ISC" // 许可证
}
项目概况
name
和version
项目名称和版本号,是项目的唯一标识。
- 项目名称必须是小写字母,可以包含字母、数字、下划线、连字符,不能包含空格。可以包含组织名(Scope),如
@myorg/project
。 - 版本号遵循 语义化版本规范。
description
和keywords
项目描述和关键字,用于描述项目的基本信息。两者都是为了方便搜索引擎和用户查找项目。
- 项目描述是一个字符串,用于描述项目的基本信息。
- 关键词是一个数组,用于描述项目的关键字。
homepage
,bugs
和repository
项目主页、问题反馈、代码仓库地址。用于用户查找项目的相关信息及反馈问题。
homepage
是项目的主页地址。bugs
是项目的问题反馈地址。repository
是项目的代码仓库地址。
author
,contributors
和license
项目作者、贡献者和许可证。用于说明项目的版权信息。
author
是项目的作者。contributors
是项目的贡献者。license
是项目的许可证。
项目运行环境
engines
用于指定项目运行所需的环境,包括 node, npm 等。
node
是项目运行所需的 Node.js 版本。npm
是项目运行所需的 npm 版本。
{
"engines": {
"node": ">=10.0.0",
"npm": ">=6.0.0"
}
}
2. os
用于指定项目运行所需的操作系统,包括 Windows, macOS, Linux 等。
{
"os": [
"darwin",
"linux",
"!win32"
]
}
3. cpu
用于指定项目运行所需的 CPU 架构,包括 x86, x64 等。
{
"cpu": [
"x64",
"!arm"
]
}
文件&目录
参考连接:
- NPM package.json
- Node.js field definitions
- Webpack package exports
- Browser field spec
- Mark the file as side-effect-free
- Including declarations in your npm package
main
项目的入口文件,用于指定项目的主要文件。可以是 js 文件(commonjs,ES Module),也可以是其他文件。
{
"main": "index.js"
}
2. module
(非标准)
打包工具(Webpack,rollup 等)扩展的字段,类似于 main
字段,只不过用于指定项目的 ES Module 入口文件。
{
"module": "index.mjs"
}
3. browser
(非标准)
用于指定项目在浏览器环境下的入口文件。
Webpack 在构建时有一个 target
配置项,默认为 web
。如果使用 import
语法引入模块,优先级 browser
> module
> main
;如果使用 require
语法引入模块,优先级 main
> module
> browser
。
jsdelivr
和unpkg
(非标准)
由 CDN 服务商扩展的字段,用于指定项目在 CDN 上的入口文件。
type
(非标准)
Node.js 扩展的字段,用于指定当前 package.json
范围下,所有 .js
文件的类型。包括 module
,commonjs
等。默认为 commonjs
。
例如:
- 当
type
为module
时,所有.js
文件都会被当作 ES Module 处理。如果需要使用 CommonJS,可以使用.cjs
后缀。 - 当
type
为commonjs
时,所有.js
文件都会被当作 CommonJS 处理。如果需要使用 ES Module,可以使用.mjs
后缀。
bin
用于指定项目的可执行文件,可以是一个或多个文件。
{
"bin": {
"project": "./bin/project.js"
}
}
用户在安装项目时,会将可执行文件链接到全局环境变量中,以便于用户在命令行中使用 project
直接执行。
types
(非标准)
由 TypeScript
扩展的字段,用于指定项目的类型定义文件。
{
"types": "index.d.ts"
}
8. exports
用于指定项目的导出文件,可以是一个或多个文件。
在 commonjs 时代,包中的所有文件都是导出的,用户可以通过 require
语法引入任意文件。
但是在 ES Module 时代,可以通过 exports
字段指定导出文件,用户只能引入指定文件。
{
"exports": {
".": "./index.js",
"./module": "./module.js"
}
}
{
"exports": {
".": {
"import": "./index.js",
"require": "./index.cjs",
"types": "./index.d.ts"
"default": "./index.js"
},
"./module": {
"import": "./module.js",
"require": "./module.cjs",
"types": "./module.d.ts"
"default": "./module.js"
}
}
}
用户在引入项目时,会根据导出文件的配置,自动选择合适的文件进行导入。
imports
(非标准)
用于指定项目导入文件的 alias
,必须以 #
开头。
{
"imports": {
"#package": "vue"
}
}
10. files
用于指定项目发布到 npm 时,需要包含的文件和目录。
{
"files": [
"dist",
"src"
]
}
以下文件总是会被包含,所以不用在 files
字段中指定:
package.json
README
LICENSE
或LICENCE
main
字段指定的文件bin
字段指定的文件
sideEffects
(非标准)
用于指定项目的副作用文件,即项目的文件是否有副作用。副作用文件是指在导入时会执行一些操作,而不是导出一些内容。副作用文件会影响 tree-shaking
的效果。
多数情况下可以直接设置为 false,这样打包工具就会自动删除不需要的 import
。但是有些情况例外:
- 注册全局事件监听器、修改全局状态等
- 样式文件
- Polyfill
{
"sideEffects": false
}
false
表示项目的所有文件都没有副作用,可以进行tree-shaking
。true
表示项目的所有文件都有副作用,不可以进行tree-shaking
。["*.css"]
表示项目的所有 css 文件都有副作用,不可以进行tree-shaking
。
补充说明
Pure ESM package
Pure ESM package 是指只包含 ES Module 的包,不包含 CommonJS 的包。提出的目的是推进 ES Module 的使用,减少 CommonJS 的使用。
链接:gist.github.com/sindresorhu…
Pure ESM package
的 package.json
配置如下:
{
"type": "module",
"exports": {
".": {
"import": "./dist/my-lib.js"
"types": "./dist/my-lib.d.ts"
}
}
"engines": {
"node": ">=18"
}
}
import
可以导入 commonjs 和 ES Module 文件,require()
只能导入 commonjs 文件。如果想在 commonjs 中引入 Pure ESM package 中的 js 文件,可以使用 import()
函数。
从 Node.js 22 开始,require()
也可以同步导入 ES Module 文件,但是强烈建议向 import
迁移。
对于任意一个包,推荐的 package.json
的最佳实践是:
{
"name": "my-lib",
"type": "module", // 优先使用 ES Module
"files": ["dist"],
"main": "./dist/my-lib.umd.cjs", // 兼容 CommonJS
"module": "./dist/my-lib.js", // 兼容旧的打包工具
"types": "./dist/my-lib.d.ts", // 提供类型定义
"exports": { // 使用新规范来导出文件
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.umd.cjs",
"types": "./dist/my-lib.d.ts"
}
},
"sideEffects": false, // 无副作用,按需设置
}
项目依赖
dependencies
和devDependencies
项目依赖和开发依赖,用于指定项目的依赖包及版本号。
{
"dependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"webpack": "^5.0.0"
}
}
- 项目依赖用于指定在生产环境下需要的依赖包。或者说是项目运行时需要的依赖包。
- 开发依赖用于指定在开发环境下需要的依赖包。包括构建工具、测试工具、代码检查工具等。
当用户安装项目时,只会安装 dependencies
中的依赖包,不会安装 devDependencies
中的依赖包。
peerDependencies
对等依赖,这表示项目依赖于其他包运行,但是不会将其打包到项目中,而是期望用户在安装项目时,手动安装这些包。
例如:
- 当前开发的包 A 是 B 的一个插件,A 依赖于 B 运行,但是 B 不会打包到 A 中,而是期望用户在安装 A 时,手动安装 B。
- 当前包 A 依赖 C,当 A 被 B 引用时,B 项目已经安装了 C 的特定版本,当这个版本与 A 依赖的版本不一致时,会提示用户安装正确的版本。
{
"peerDependencies": {
"vue": "^3.0.0"
}
}
3. optionalDependencies
可选依赖,用于指定项目的可选依赖包。这表示项目依赖于其他包,但是不是必须的,如果用户安装了这些包,项目会使用这些包,如果没有安装,项目会正常运行。
{
"optionalDependencies": {
"colors": "^1.4.0"
}
}
4. peerDependenciesMeta
定义对等依赖的元数据,用于指定对等依赖的一些特性,如:是否可选。
{
"peerDependencies": {
"vite": "^2.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
}
设置 optional
为 true
,表示对等依赖是可选的。如果没有安装对等依赖,也不会报错。
- 当前包依赖的版本和用户安装的版本不一致
- 当前包还可以在其他环境下运行,不依赖于对等依赖。例如:Vite 和 Rollup
它和 optionalDependencies
的区别是 optionalDependencies
会直接被引用,而 peerDependencies
不会被直接引用。
项目的打包
常见的打包工具有 Webpack、Rollup、Vite 等。按照项目的需求,选择合适的打包工具进行打包。
个人推荐:一般的包,都使用 Rollup 进行打包,因为 Rollup 的打包速度快,打包结果体积小。对于 Vue 项目,使用 Vite 进行打包。
为了创建更加通用的包,推荐使用 TypeScript 进行开发。打包时同时生成 CommonJS 和 ES Module 文件,并且提供类型定义文件。
Webpack
Webpack 打包输出 ES Module 格式还处于试验阶段,要打包输出 ES Module 格式,需要配置 experiments.outputModule
为 true
。
export default {
experiments: {
outputModule: true,
},
output: {
filename: 'index.js',
library: {
type: 'module',
},
},
}
而且 Webpack 打包的结果会包含一些额外的代码,如:__webpack_require__
等,这些代码会影响打包结果的体积。
所以,Webpack 打包的结果不适合作为库发布,适合作为应用程序的打包工具。
Rollup
Rollup 对 ES Module 的支持更好,打包结果更加纯净,适合作为库的打包工具。
export default {
input: 'src/index.js',
output: [
{
entryFileNames: '[name].js',
dir: 'dist',
format: 'es',
},
{
entryFileNames: '[name].cjs',
dir: 'dist',
format: 'cjs',
},
{
entryFileNames: '[name].browser.js',
dir: 'dist',
format: 'iife',
},
],
}
preserveModules
选项可以保留模块结构,不会将所有模块打包成一个文件。
export default {
input: 'src/index.js',
output: {
entryFileNames: '[name].js',
dir: 'dist',
format: 'es',
preserveModules: true,
preserveModulesRoot: 'src',
},
}
Vite
Vite 是一个基于 Rollup 的打包工具,基本配置和 Rollup 一样,打包库时需要使用 Library Mode。
export default {
build: {
lib: {
entry: ['src/index.js'],
formats: ['es', 'cjs', 'iife'], // 打包输出格式
name: 'MyLib',
fileName: '[name]',
},
outDir: 'dist',
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue',
},
},
},
},
}
Bebel.js ?
在打包时,虽然可以使用 Babel.js 进行语法转换,但是不推荐使用。
因为使用 Babel.js 会添加很多 Polyfill 代码,而通常主项目已经配置了 Babel.js,这会导致打包结果中包含了很多重复的代码。
所以我推荐不在库中使用 Babel.js,而是通过配置主项目来处理。(browser
文件除外。)
例如,在 Vue-cli 中,设置 transpileDependencies
:
module.exports = {
transpileDependencies: [/@vue[/\].*/],
}
而在 Vite 中,默认的 target
配置为:
['es2020', 'edge88', 'firefox78', 'chrome87', 'safari14']
而且 esbuild
打包时会自动转换语法。
如果需要兼容老的浏览器,@vitejs/plugin-legacy
插件可以帮助我们自动转换语法和添加 Polyfill。
import { defineConfig } from 'vite'
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
legacy({
polyfills: true,
targets: ['defaults', 'not IE 11'],
}),
],
})
外部依赖
在打包库时,默认情况下会将所有依赖包打包到库中,这会导致打包结果体积过大。并且在用户使用库时,如果用户的项目中已经安装了这些依赖包,会导致重复打包。
由于安装时会自动安装依赖包,所以在打包库时,可以将依赖包设置为外部依赖,打包结果中就不包含这些依赖包。 源码:
import mitt from 'mitt'
export const rollup = 1
export const bus = mitt()
排除前:
排除后:
项目的发布
在发布项目之前,最好先进行测试,确保项目的功能正常。然后更新项目的版本号。
配置好需要发布的文件和目录,确保 package.json
中的 files
字段包含了所有需要发布的文件和目录。
{
"files": ["dist"]
}
如果 package.json
中的 files
字段不存在,会默认发布所有文件和目录。
确保 package.json
中的 private
字段为 false
,否则无法发布。
{
"private": false
}
发布配置
package.json
中的 publishConfig
字段用于指定发布配置,包括发布的目标地址、发布的 tag 等。
{
"publishConfig": {
"registry": "https://npm-fe.transsion.com/",
"tag": "latest"
}
}
PNPM 对 publishConfig
进行了扩展:pnpm.io/package_jso…
使我们得以在发布时,替换 package.json
中的字段,如:main
、module
、exports
等。
一个问题
由于 exports
的存在,我们在引入文件时可以忽略物理目录结构,只需要配置 exports
字段即可。但是在不支持 exports
的环境中,我们需要保留物理目录结构,以正常引入文件。
为了使不同环境下导入文件时具有相同目录结构,最好在发布时避免目录的嵌套,例如去掉 dist
目录。
PNPM 提供的 publishConfig.directory
字段可以指定发布的目录。
{
"publishConfig": {
"directory": "dist",
"linkDirectory": true
}
}