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

程序源代码

共 5731字,需浏览 12分钟

 ·

2020-10-17 03:16

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

JS 模块系统到底是什么呢

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

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

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

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

这种方法具有一些优点:

  • 如果核心库中有一些更改,则只需在一个地方进行更改,而无需为同一件事重构所有应用程序的代码。
  • 所有应用程序保持同步。无论何时进行更改,所有应用程序仅需要运行 npm update  命令。

库的源码

因此,下一步是发布库

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

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

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

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

不同类型的 JS 模块系统

1. CommonJS

  • 由 Node.js 实现
  • 多用在服务器端安装模块时
  • 没有 runtime/async 模块
  • 通过 require  导入模块
  • 通过 module.exports  导出模块
  • 无法使用摇树优化,因为当你导入时会得到一个模块时,得到的是一个对象,所以属性查找在运行时进行,无法静态分析
  • 会得到一个对象的副本,因此模块本身不会实时更改
  • 循环依赖的不能优雅处理
  • 语法简单

2. AMD 异步模块定义

  • 由  RequireJs  实现
  • 当你在客户端(浏览器)环境中,异步加载模块时使用
  • 通过 require  实现导入
  • 语法复杂

3. UMD 通用模块定义

  • CommonJs + AMD  的组合(即 CommonJs 的语法 + AMD 的异步加载)
  • 可以用于 AMD/CommonJs 环境。
  • UMD 还支持全局变量定义,因此,UMD 模块能够在客户端和服务器上工作。

4. ES modules

  • 用于服务器/客户端
  • 支持模块的 Runtime/static loading
  • 当你导入时,获得是实际对象
  • 通过 import  导入,通过 export  导出
  • 静态分析——你可以决定编译时的导入和导出(静态),你只需要看源码,不需要执行它
  • 由于 ES6 支持静态分析,因此摇树优化是可行的
  • 始终获取实际值,以便实时更改模块本身
  • 比 CommonJS 有更好的循环依赖管理

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

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

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

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

目录结构

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

发布前的最佳实践

1. 摇树优化(Tree Shaking)

webpack 官方文档有说明

  • 摇树优化是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 [import][4]  和 [export][5]。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。新的 webpack 4 正式版本,扩展了这个检测能力,通过 package.json  的 "sideEffects"  属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是纯的 ES2015 模块,由此可以安全地删除文件中未使用的部分。
  • webpack 和 Rollup 都支持摇树优化,这意味着我们需要牢记某些事情,以便我们的代码可被 Tree Shaking。

2. 发布所有模块形态

  • 我们应该发布所有模块形态,例如 UMD 和 ES Module,因为我们永远不知道用户在哪个版本的浏览器或 webpack 中使用此库/包。
  • 即使所有打包程序(如  webpack  和  Rollup)都能解析 ES Module ,但如果我们的使用者使用的是 webpack 1.x,则它无法解析 ES 模块。
// package.json
{
  "name""js-module-system",
  "version""0.0.1",


  • package.json 文件的 main 字段通常用于指向 UMD 版本的库/包。
  • package.jso 文件的 module 字段用于指向 ES 版本的库/包。

鲜为人知的事实: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-UI 库(styled-components, material-ui
  • Core Packages-核心包(reactreact-dom

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

UI Libraries

  • 有一个 dist 文件夹,该文件夹是针对 ES 和 UMD/CJS 模块系统 的打包和压缩版本。
  • 有一个 lib 文件夹,用来存放被编译后的代码。

Core Packages

  • 只有一个文件夹,其中包含针对 CJS 或 UMD 模块系统的打包和压缩版本。

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

UI Libraries

想象一下,如果我们只是发布库的 bundled version 将其托管在 CDN 上,我们的用户将直接在