关于前端:现代-JavaScript-库打包指南

44次阅读

共计 8710 个字符,预计需要花费 22 分钟才能阅读完成。

本文首发于微信公众号:大迁世界, 我的微信:qq449245884,我会第一工夫和你分享前端行业趋势,学习路径等等。
更多开源作品请看 GitHub https://github.com/qq449245884/xiaozhi,蕴含一线大厂面试残缺考点、材料以及我的系列文章。

简介

本指南旨在提供一些大多数库都应该遵循的高深莫测的倡议。以及一些额定的信息,用来帮忙你理解这些倡议被提出的起因,或帮忙你判断是否不须要遵循某些倡议。这个指南仅实用于 库(libraries),不适用于利用(app)。

要强调的是,这只是一些 倡议,并不是所有库都必须要遵循的。每个库都是独特的,它们可能有短缺的理由不采纳本文中的任何倡议。

最初,这个指南不针对某一个特定的打包工具 —— 曾经有许多指南来阐明如何在配置特定的打包工具。相同咱们聚焦于每个库和打包工具(或不必打包工具)都实用的事项。


输入 esmcjsumd 格局

esm 是“EcmaScript module”的缩写。

cjs 是“CommonJS module”的缩写。

umd 是“Universal Module Definition”的缩写,它能够在 <script> 标签中执行、被 CommonJS 模块加载器加载、被 AMD 模块加载器加载。

esm 被认为是“将来”,但 cjs 依然在社区和生态系统中占有重要位置。esm 对打包工具来说更容易正确地进行 treeshaking,因而对于库来说,领有这种格局很重要。或者在未来的某一天,你的库只须要输入 esm

你可能曾经留神到,umd 曾经与 CommonJS 模块加载器兼容 —— 所以为什么还要同时具备 cjsumd 输入呢?一个起因是,与 umd 文件相比,CommonJS 文件在对依赖进行条件导入时通常体现更好;例如:

if (process.env.NODE_ENV === "production") {module.exports = require("my-lib.production.js");
} else {module.exports = require("my-lib.development.js");
}

下面的例子,当应用 CommonJS 模块时,只会引入 productiondevelopment 包中的一个。然而,对于 UMD 模块,最终可能会将两个包全副引入。无关更多信息,请参阅此探讨。

最初还须要留神是,开发者可能会在其利用中同时应用 cjsesm,产生双包危险。dual package hazard 一文介绍了一些缓解该问题的办法,利用 package.json#exports 进行 package exports 也能够帮忙避免这种状况的产生。

输入多文件

通过保留文件构造更好地反对 treeshaking

如果你对你的库应用了打包工具或编译器,能够对其进行配置以保留源文件目录构造。这样能够更容易地对特定文件进行 side effects 标记,有助于开发者的打包工具进行 threeshaking。参考这篇文章理解更多信息。

一个例外是,如果你要创立一个不依赖任何打包工具能够间接在浏览器中应用的产出(通常是 umd 格局,但也可能是古代的 esm 格局)。在这种状况下,最好让浏览器申请一个大文件,而不是申请多个小文件。此外,你应该进行代码压缩并为其创立 sourcemap。

要不要压缩代码

你能够将一些层面的代码压缩利用到你的库中,这取决于你对你的代码最终通过开发者的打包工具后的大小的谋求水平。

例如,大多数编译器曾经配置了删除空白符等其余简略的优化,即便是来自 NPM 模块的代码(在这里指的是你的库)。应用 terser —— 一个风行的 JavaScript 代码压缩工具 —— 这类压缩工具能够将包的最终大小缩小 95%。在某些状况下,你可能会对这些优化感到称心,且不须要你来付出任何致力。

但如果在公布前对你的库进行代码压缩,这能够失去一些额定的益处,但须要深刻理解压缩工具的配置和副作用。压缩工具通常不会将这类压缩用于 NPM 模块,因而,如果你不本人来做的话,你会错过这些节俭。请参阅这个 issue 理解更多信息。

最初,如果你正创立一个不依赖任何打包工具能够间接在浏览器中应用的产出(通常是 umd 格局,但也能够是古代的 esm 格局)。在这种状况下,你应该对代码进行压缩,并创立 sourcemap,并输入到一个单文件。

创立 sourcemap

对源代码进行任何模式的编译,都将导致将来某个异样的地位,无奈与源码对应起来。为了帮忙将来的本人,创立 sourcemap,即便只进行了很少的编译工作。

创立 TypeScript 类型

随着应用 TypeScript 的开发者数量一直增长,将类型内置到你的库中将有助于改善开发体验 (DX)。此外,不应用 TypeScript 的开发者在应用反对类型的编辑器(例如 VSCode,它应用类型来反对其 Intellisense 性能)时也会取得更好的 DX。

然而,创立类型并不意味着你必须应用 TypeScript 来编写你的库。

一种抉择是持续在源代码中应用 JavaScript,而后通过 JSDoc 正文来反对类型。而后,你能够将 TypeScript 配置为仅从你的 JavaScript 源代码中构建类型文件。

另一种抉择是间接在 index.d.ts 文件中编写 TypeScript 类型文件。

取得类型文件后,请确保设置了 package.json#exportspackage.json#types 字段.

外置框架

不要将 React、Vue 等框架打包在你的库中

当构建的库依赖某个框架(例如 React、Vue 等),或是作为另一个库的插件,你可能须要将框架配置到“externals”中。这能够使你的库援用这个框架,但不会将其打包到最终的产出中。这会防止产生一些 bug,并缩小库的体积。

你应该还须要将框架增加到库的 package.json 的 peer dependencies 中,这将帮忙开发者发现你依赖于某个框架。

面向古代浏览器

应用古代的新个性,如果有须要,让开发者反对旧的浏览器
这篇 web.dev 上的文章提供了一个很好的案例,并提供了相干的领导准则:

  • 当应用你的库时,可能让开发者去反对老版本的浏览器。
  • 输入多个产进去反对不同版本的浏览器。

举个例子,如果你应用 TypeScript,你能够创立两个版本的包代码:

  1. 通过在 tsconfig.json 中设置 "target"="esnext",生成一个用古代 JavaScript 的 esm 版本
  2. 通过在 tsconfig.json 中设置 "target"="es5" 生成一个兼容低版本 JavaScript 的 umd 版本

有了这些设置,大多数用户将取得古代版本的代码,但那些应用老的打包工具配置或应用 <script> 加载代码的用户,将取得进行了额定编译来反对老版本浏览器的版本。

必要的编译

编译 TypeScript、将 JSX 转换为函数调用

如果库的源码是须要进行编译的模式,如 TypeScript、React 或 Vue 组件等,那么你库须要输入的是编译后的代码。

例如:

  • 你的 TypeScript 代码应该输入为 JavaScript。
  • 你的 React 组件,例如 <Example />,应该在输入中应用 jsx()createElement() 来替换 JSX 语法。

进行这样的编译时,请确保同时也创立 sourcemap

保护 changelog

记录更新和变更

只有能让开发者理解到有哪些变更和对他们的影响,至于是通过自动化工具还是通过亲自动手的形式来解决,这都无关紧要。现实状况下,库的每次版本变更都应该在 changelog 中进行相应的更新。

拆分出你的 CSS 文件

让开发者可能按需引入 CSS

如果你正在创立一个 CSS 库(如 Bootstrap、Tailwind 等),最简略的形式就是提供繁多文件,蕴含库的所有性能。然而,在这种状况下,你的 CSS 产出最终可能会变得很大,影响开发者网站的性能。为了防止这种状况,库通常会提供自定义生成 CSS 产出的性能,让产出中只蕴含开发者正在应用的必要 CSS(例如,参考 Bootstrap 和 Tailwind 是怎么做的)。

如果 CSS 只是你的库的一部分(例如,具备默认款式的组件库),那么最好将 CSS 按组件拆散独自构建产出,在应用相应的组件时按需导入。这方面的一个例子是 react-component。

配置 package.json

package.json 中有许多重要的配置字段值得探讨;我在这里将着重探讨其中最为重要的一些,这还有很多额定的字段,你同样能够进行配置。

设置 name 字段

给你的库取一个名

name 字段将决定你的包在 npm 上的名字,开发者能够通过这个名字去装置并应用你的库。

留神,库的命名是有限度的,如果你的代码库属于某个组织,你还能够创立一个命名空间。更多细节能够参考 name docs on npm。

name 和 version 的组合为库每次迭代创立一个惟一标识。

设置 version 字段

通过更改 version 来对你的库公布更新

正如 name 局部所说,nameversion 的组合为你的库在 npm 上创立一个惟一标识。当你更新库中的代码时,你能够更新 version 字段并公布以容许开发者获取该新代码。

举荐应用 semver 版本控制策略,但要留神的是有些库抉择 calver 或应用他们本人特有的版本控制策略。无论你抉择应用哪种策略,都应该记录下来,以便开发者理解你的库是如何进行版本控制的。

你还应该在 changelog 中记录你的更改。

定义你的 exports

exports 为你的库定义公共 API

package.json 中的 exports 字段 – 有时被称为“package exports”– 是一个十分有用的补充,只管它的确引入了一些复杂性。它做的最重要的两件事是:

  1. 定义哪些货色能够从你的库中导入,哪些则不能够,以及可导入的内容的名字。如果没有在 exports 中被列出,那么开发者就不能够 importrequire 它们。换句话说,exports 的体现像是给你的库用户查看的公共 API,帮忙定义哪些是内部的哪些是外部的。
  2. 容许你依据不同的条件(你能够定义)去抉择那个文件是被导入的,例如“文件是被 import 还是被 require?开发人员须要的是 development 版本的库还是 production 版本等等。

对于这部分的内容 NodeJS 团队和 Webpack 团队提供了一些很优良的文档。在此我列出一个涵盖大部分常见场景的例子:

{
  "exports": {
    ".": {
      "types": "index.d.ts",
      "module": "index.js",
      "import": "index.js",
      "require": "index.cjs",
      "default": "index.js"
    },
    "./package.json": "./package.json"
  }
}

让咱们深刻理解这些字段的含意以及我抉择这个例子的起因:

  • "." 示意你的库的默认入口
  • 解析过程是 从上往下 的,并在找到匹配的字段后立刻进行;所以入口的程序是十分重要的
  • types 字段应始终放在第一位,帮忙 TypeScript 查找类型文件
  • module 是一个“非官方”字段,它被 Webpack 和 Rollup 等打包工具所反对。它应该被放在 importrequire 之前,并且指向 esm 格局的产出 — 如果你的源代码是纯 esm 的,它也能够指向你的源代码。正如在格局局部中指出的那样,它旨在帮忙打包工具只蕴含你的库的一个正本,无论它是通过 import 还是 require 形式引入的。你能够从这里、这里、还有 这里理解更多对于 module 的内容
  • import 用于当有人通过 import 应用你的库时
  • require 用于当有人通过 require 应用你的库时
  • default 字段用于兜底,在没有任何条件匹配时应用。尽管目前可能并不会匹配到它,但为了面对“未知的将来场景”,应用它是好的

当一个打包工具或者运行时反对 exports 字段的时候,那么 package.json 中的顶级字段 main、types、module 还有 browser 将被疏忽,被 exports 取代。然而,对于尚不反对 exports 字段的工具或运行时来说,设置这些字段依然很重要。

如果你有一个 “development” 和一个 “production” 的产出(例如,你有一些正告在 development 产出中有但在 production 产出中没有),那么你能够通过在 exports 字段中 "development""production" 来设置它们。留神一些打包工具例如 webpackvite 将会自动识别这些导出条件,而 Rollup 也能够通过配置来辨认它们,你须要揭示开发者在他们本人打包工具的配置中去做这些事。

列出要公布的 files

files 定义你的 NPM 包中要蕴含哪些文件

files 决定 npm CLI 在打包库时哪些文件和目录蕴含到最终的 NPM 包中。

例如,如果你将代码从 TypeScript 编译为 JavaScript,你可能就不想在 NPM 包中蕴含 TypeScript 的源代码。(相同,你应该蕴含 sourcemap)。

files 能够承受一个字符串数组(如果须要,这些字符串能够蕴含相似 glob 的语法),例如:

{"files": ["dist"]
}

留神,文件数组不承受相对路径示意;"files": ["./dist"] 将无奈失常工作。

验证你已正确设置 files 的一种好办法是运行 npm publish --dry-run,它将依据此设置列出将会蕴含的文件。

为你的 JS 文件设置默认的模块 type

type 规定你的 .js 文件应用哪个模块零碎

运行时和打包工具须要一种办法来确定你的 .js 文件采纳哪种模块零碎 —— ESM 还是 CommonJS。因为 CommonJS 首先呈现,所以它被打包工具视为默认的 – 但你能够通过在你的 package.json 中增加 "type" 来管制这种行为。

你能够抉择 "type":"module""type":"commonjs",也能够不增加该字段(默认为 CommonJS),但仍强烈建议你进行设置,显式地申明你正在应用哪一个。

请留神,你能够通过几个技巧在我的项目中混用模块类型:

  • .mjs 文件总是 ESM 模块,即便你的 package.json"type": "commonjs"(或者没有 type
  • .cjs 文件总是 CommonJS 模块,即便你的 package.json"type": "module"
  • 你能够在子目录下增加其余 package.json 文件;运行时和打包工具将向上遍历文件目录,直到寻找到最近的 package.json。这意味着你能够有两个不同的文件夹,都应用 .js 文件,但每个文件夹都有本人的 package.json 并设置为不同的 type 以取得基于 CommonJS 和 ESM 的文件夹。

参考优良的 NodeJS 文档 这里 和 这里 理解更多信息。

列出哪些模块有 sideEffects

设置 sideEffects 来容许 treeshaking

创立一个“纯模块”带来的长处与创立一个纯函数非常相似;打包工具可能对你的库更好的进行 treeshaking。

通过设置 sideEffects 让打包工具晓得你的模块是否是“纯”的。不设置这个字段,打包工具将不得不假如你 所有 的模块都是有副作用。

sideEffects 能够设为 false,示意没有任何模块具备副作用,也能够设置为字符串数组来列出哪些文件具备副作用。例如:

{
  // 所有模块都是“纯”的
  "sideEffects": false
}

{
  // 除了 "module.js",所有模块都是“纯”的
  "sideEffects": ["module.js"]
}

所以,什么让一个模块具备副作用?例如批改一个全局变量,发送 API 申请,或导出 CSS,而且开发人员不须要做任何事件这些动作就会被执行。例如:

// 具备副作用的模块

export const myVar = "hello";

window.example = "testing";

导入 myVar 时,你的模块主动设置 window.example

例如:

import {myVar} from "library";

console.log(window.example);
// 打印 "testing"

在某些状况下,如 polyfill,这种行为是无意的。然而,如果咱们想让这个模块是“纯”的,咱们能够将对 window.example 的赋值挪动到一个函数中。例如:

// 一个“纯”模块

export const myVar = "hello";

export function setExample() {window.example = "testing";}

当初这是一个“纯”模块。留神,从开发者的角度来看会有不同:

import {myVar, setExample} from "library";

console.log(window.example);
// 打印 "undefined"

setExample();

console.log(window.example);
// 打印 "testing"

通过这篇文章来理解更多。

设置 main 字段

main 定义 CommonJS 入口

main 是一个当打包工具或运行时不反对 package.json#exports 时的兜底计划;如果打包工具或运行时反对 package exports,则不会应用 main

main 应该指向一个兼容 CommonJS 格局的产出;它应该与 package exports 中的 require 保持一致。

设置 module 字段

module 定义 ESM 入口

module 是一个当打包工具或运行时不反对 package.json#exports 时的兜底计划;如果打包工具或运行时反对 package exports,则不会应用 module

module 应该指向一个兼容 ESM 格局的产出;它应该与 package exports 中的 moduleimport 保持一致。

设置给 CDN 应用的附加字段

反对 CDN,例如 unpkgjsdelivr

为让你的库在 CDN 上“以默认的形式失常工作”,如 unpkg 和 jsdelivr,你能够设置它们的特定字段指向你的 umd 产出。例如:

{
  "unpkg": "./dist/index.umd.js",
  "jsdelivr": "./dist/index.umd.js"
}

设置 browser 字段

browser 指向能在浏览器中工作的产出

browser 是一个当打包工具或运行时不反对 package.json#exports 时的兜底计划;如果打包工具或运行时反对 package exports,则不会应用 browser

browser 应该指向能在浏览器中工作的 esm 产出。然而,只有在为浏览器和服务器(等其余非浏览器环境)创立不同的产出时,才须要设置该字段。如果你没有为多个环境创立多个产出,或者你的产出是“纯 JavaScript”或“通用”的,能够在任何 JavaScript 环境中运行,那么你就不须要设置 browser 字段。

如果你的确须要设置该字段,这里有一个优良的指南,介绍了配置它的不同办法。

留神,browser 字段不应该指向 umd 产出,因为那样的话,你的库就不会被打包工具(如 Webpack)进行 treeshaking,这些打包工具会优先思考这个字段,而不是其余字段,比方 module 和 main。

设置 types 字段

types 定义 TypeScript 类型

types 是一个当打包工具或运行时不反对 package.json#exports 时的兜底计划;如果打包工具或运行时反对 package exports,则不会应用 types

types 应该指向你的 TypeScript 入口文件,例如 index.d.ts;它应该与 package exports 中的 types 字段指向同一个文件。

列出 peerDependencies

如果你依赖别的框架或库,将它设置为 peer dependency

你应该外置框架。然而,这样做后,你的库只有在开发人员自行装置你须要的框架后能力工作。设置 peerDependencies 让他们晓得他们须要装置的框架。- 例如,如果你在创立一个 React 库:

{
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

通过这篇文章来理解更多。

你应该以书面形式来体现这些依赖;例如,npm v3-v6 不装置 peer dependencies,而 npm v7+ 将主动装置 peer dependencies。

阐明你的库应用哪个 许可证

爱护你本人和其余的贡献者

开源许可证用于爱护贡献者和用户。没有这种爱护,企业和有教训的开发者不会应用该我的项目。

上述援用自 Choose a License,这也是一篇很好的文章,帮忙你来决定哪个许可证适宜你的我的项目。

当你决定了许可证,对于许可证的 npm 文档中形容了许可证字段的格局。例如:

{"license": "MIT"}

除此之外,你能够在我的项目的根目录下创立一个 LICENSE.txt 文件,并将许可证的文本复制到这里。

编辑中可能存在的 bug 没法实时晓得,预先为了解决这些 bug, 花了大量的工夫进行 log 调试,这边顺便给大家举荐一个好用的 BUG 监控工具 Fundebug。

起源:https://github.com/frehner/mo…

交换

有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

正文完
 0