了解 JavaScript 模块基础知识,搭建自己的库

664次阅读  |  发布于4年以前

我想很多“前端工程师”都听过说过 “JavaScript 模块”,那你们都知道如何处理它,以及它在日常工作中如何发挥作用吗?

JS 模块系统到底是什么呢

随着 JavaScript 开发越来越广泛,命名空间和依赖项变得越来越难以处理,极客们早已经开发出不同的模块系统解决方案来解决该问题。

为什么理解 JS 模块系统很重要

我的日常工作是设计和项目架构,并且我很快意识到跨项目需要许多通用功能。我总是一次又一次地将这些功能复制粘贴到新项目中。

问题是,每当更改一部分代码时,我都需要在所有项目中手动同步这些更改。为了避免所有这些繁琐的手动任务,我决定提取通用功能并从中组成一个 NPM 软件包。这样,团队中的其他人将能够将它们重新用作依赖项,并在每次推出新版本时都可以对其进行更新。

这种方法具有一些优点:

库的源码

因此,下一步是发布库

这是最困难的部分,因为我脑海中突然跳出一堆东西,例如:

  1. 如何使用摇树优化
  2. 应该针对哪些 JS 模块系统(CommonJS、AMD、ES modules)
  3. 需要转译源码吗
  4. 需要打包源码吗
  5. 应该发布哪些文件

在发布第三方库(组件库,工具库)时,我们每个人的脑海中都应该冒出这些问题。

来, 我们一步步解决以上的问题。

不同类型的 JS 模块系统

1. CommonJS

2. AMD 异步模块定义

3. UMD 通用模块定义

4. ES modules

现在,我们了解了不同类型的 JS 模块系统以及它们如何演变。

尽管所有工具和现代浏览器都支持 ES modules,但我们在发布库时不知道用户如何利用我们的库。因此,我们必须确保我们的库在所有环境中都能正常工作。

让我们深入研究并设计一个示例库,更好地回答与发布库有关的所有问题。

我已经建立了一个小型的 UI 库(你可以在 GitHub 上找到源代码),并且我将分享我在编译,打包和发布中的所有经验和探索。

目录结构

在这里,我们有一个小的 UI 库,其中包含 3 个组件:Button,Card 和 NavBar。让我们一步步进行编译并发布。

发布前的最佳实践

1. 摇树优化(Tree Shaking)

webpack 官方文档有说明

2. 发布所有模块形态

// package.json
{
  "name": "js-module-system",
  "version": "0.0.1",

鲜为人知的事实:webpack 使用 resolve.mainfields 确定检查 package.json 中的哪些字段。

性能提示:由于所有现代浏览器现在都支持 ES 模块,因此也请务必发布 ES 版本的库/包。这样一来,可以减少编译次数,最终可以减少向用户交付的代码。这将提高应用程序的性能。

那么,下一步是什么?编译还是打包?我们应该使用什么工具?啊,这是最棘手的部分!让我们深入研究研究。

webpack vs Rollup vs Babel

这些我们在日常工作中使用的工具,用于承载我们的应用程序/库/软件包。没有它们,我无法想象现代的 Web 开发有多么糟糕。因此,我们无法将它们进行比较 ❌

每种工具都有其自身的优势,并根据使用者的需求达到不同的目的。

现在让我们看一下这些工具:

webpack

webpack 是一个很棒的模块打包工具, 它被广泛接受并且主要用于构建 SPA。它提供了开箱即用的所有功能,例如代码拆分、按需加载、摇树优化等,并且它本身使用的是 CommonJS 模块系统。

RollupJS

RollupJS 还是类似于 webpack 的模块打包器。但是,RollupJS 的主要优点是它遵循 ES6 修订版中包含的代码模块的新标准化格式,因此你可以使用它来打包 ES module variant 的 library/package,但它不支持按需加载。

Babel

Babel 是 JavaScript 的编译器,以将 ES6 代码转换为可在你的浏览器(或服务器)中运行的代码而闻名。请记住,它只是编译而不会打包你的代码。

我的建议:对库使用 Rollup.js,对应用程序使用 webpack。

编译(Babel-ify)源代码还是直接打包源代码

在构建我的 NPM 库时,我花费了大量时间来试图找出该问题(如何编译、如何打包)的答案。我开始挖掘自己的 node_modules,查找所有优秀的库并检查它们的构建系统。

对比 libraries/packages 构建的输出

在查看了不同 libraries/packages 的构建输出之后,我清楚地了解了这些库的作者在发布之前可能会想到的不同策略。以下是我的观察。

如你在上图中所看到的,我已根据它们的特性将这些库/软件包分为两组:

你可能已经弄清楚了这两组之间的区别。

UI Libraries

Core Packages

但是,为什么 UI Libraries 和 Core Packages 的构建输出有所不同?

UI Libraries

想象一下,如果我们只是发布库的 bundled version 将其托管在 CDN 上,我们的用户将直接在 <script /> 标记中使用它。现在,如果使用者只想使用 <Button /> 组件,则他们必须加载整个库。另外,在浏览器中,没有可以解决 tree shaking 的打包工具,最终我们会将整个库代码发送给我们的使用者。因此,我们不能像如下代码引入整个库文件。

<script type="module">
  import { Button } from "https://unpkg.com/uilibrary/index.js";
</script>

现在,如果我们只是简单地将 src 转换为 lib 并将该 lib 托管在 CDN 上,那么我们的使用者实际上可以得到他们想要的任何东西而没有任何开销,“代码更少,加载更快” ✅

<script type="module">
  import { Button } from "https://unpkg.com/uilibrary/lib/button.js";
</script>

Core Packages

Core Packages(核心包)永远不会通过 <script /> 标记使用,因为它们必须是主应用程序的一部分。因此,我们可以安全地发布这些软件包的构建版本( UMDES),并将构建后的系统交给用户使用。

例如,他们可以使用 UMD 而不使用摇树优化,或者如果打包器能够识别并获得摇树优化的好处,则可以使用 ES

// CJS require
const Button = require("uilibrary/button");
// ES import
import {Button} from "uilibrary";

对于 UI 库

下面我们修改 package.json 以指向对应的模块系统。

// package.json
{
  "name": "js-module-system",
  "version": "0.0.1",
  // for umd/cjs builds
  "main": "dist/index.js",
  // for es build
  "module": "dist/index.es.js"
}

对于 core packages,我们不需要 lib 版本。我们只需要针对 cjs/umd 模块系统和 es 模块系统,使用 rollup 进行 打包和压缩源代码即可。

提示:对于愿意通过 <script /> 标记下载整个库/软件包的用户,我们也可以在 CDN 上托管 dist 文件夹

我们怎么进行打包

我们应在在 package.json 中为了不同的目的编写不同的脚本。你能在 GitHub 上面找到 Rollup 的一些配置—— rollup config。

// package.json
{
  "scripts": {
    "clean": "rimraf dist",
    "build": "run-s clean && run-p build:es build:cjs build:lib:es",
    "build:es": "NODE_ENV=es rollup -c",
    "build:cjs": "NODE_ENV=cjs rollup -c",
    "build:lib:es": "BABEL_ENV=es babel src -d lib"
  }
}

我们应该发布哪些东西

package.json 中, "files" 字段是一个数组类型 ,用来表示软件包被当做第三方依赖安装时,都有哪些文件或文件夹需要下载到业务项目中。如果你在数组中加入了一个文件夹,那么在你 npm install 时,文件夹及下面的文件都会被下载。

在我的示例项目中,我在 "files" 中加入了 libdist 文件夹。

// package.json
{
  "files": ["dist", "lib"]
}

最后,终于可以准备发布了。只需在终端中键入 npm run build 命令,你就看到以下输出。仔细查看 distlib 文件夹都有哪些东西。

可以发布了?

总结

至此,我们已经了解了 JavaScript 模块系统以及如何创建自己的库并发布它。下面是一些注意事项:

1. 是否可以启用摇树优化

2. 至少需要构建 ES modules and CommonJS 两种模块系统

3. 使用 Babel 和 Bundlers 搭建 libraries

4. 使用 Bundlers 搭建 Core packages

5. 在 package.json 中使用 "module" 字段 来构建 es 模块的版本(PS:这有助于使用 tree shaking)

6. 发布已编译的文件夹以及模块的编译版


Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8