[杂七杂八的学习记录] 模块化 & webpack & npm

113 阅读13分钟

模块化

模块化的背景

随着 web 的发展,一个网站所需的 javascript 越来越复杂,开发者自然而然地希望能够将不同功能的代码拆分开来,以利于阅读和维护。

在没有模块化之前,开发者首先尝试了只简单地、将不同功能的代码拆分到不同的 .js 文件中,然后在 html 中使用多个 script 标签,分别导入对应不同功能的 .js 文件,比如:

image.png

这种简单粗暴的方法只是在形式上把代码分离了,没有分离代码的作用域,所以依然存在一些问题,例如,命名冲突、可以直接修改其他代码文件中的变量(作用域污染)、各功能间的依赖关系不明显等。

早期的开发者为了解决问题,做出了各种各样的努力:

  1. 代码放在立即执行的函数作用域中,只把希望被其他脚本使用的接口函数暴露出来,避免了作用域污染,同时可以通过函数的参数来表现依赖关系;
  2. 定义一个全局变量的对象,把一个 .js 文件中的接口都挂在这个对象上。这个对象相当于文件的命名空间,避免了命名冲突的问题;

image.png

模块化为以上的问题提供了一个更简单的解决答案。一个模块就是实现一个特定功能的文件,开发者可以通过引入模块来使用功能。

模块化的意义

  1. 避免命名冲突、作用域污染等问题;
  2. 有利于复用代码、表现依赖关系;
  3. 有利于阅读和维护;

模块化的标准

在早期,服务端对于模块化的需求更加强烈,所以第一个的模块化规范 CommonJS 是针对 Node.js 设计的,它在 2010 年左右诞生并沿用至今。CommonJS 以同步的方式加载模块,而 Node.js 在启动时加载模块、执行时使用模块。

服务器端加载模块相当于从本地硬盘中读取文件,需要的时间很少,同步加载是可行的。但浏览器向服务器请求模块文件的耗时是难以预测的,所以浏览器如果也以同步的方式加载模块,可能会被阻塞较长的时间,出现“假死”的情况。

基于以上原因,开发者为浏览器设计了一个新的模块化规范 AMD (Asynchronous Module Definition),同时推出了支持 AMD 规范的模块加载器 RequireJS。。

国内在同期推出了另一个浏览器模块化规范 CMD(Common Module Definition)和相应的模块加载器 SeaJS。

总而言之,这时候的浏览器依然没有原生支持模块化,但是可以借助 RequireJS、SeaJS 等第三方库实现模块化功能。

ES6 Module 提出后,浏览器终于开始原生支持模块功能,这比使用第三方库更有效率。

image.png

最后,还有一点令人头痛 —— 那就是上面这些模块化标准并不兼容。所以为了模块代码能够在不同的模块环境都能正常运行,又做了额外的工作:

  1. UMD(Universal Module Definition):通用模块定义规范,可以处理 CommonJS、AMD、CMD 的差异兼容;
  2. webpack: 打包编译的时候,会把统一替换成 _webpack_require_ 来实现模块的引入和导出;
  3. babel:babel 可以将 ES 6 标准降级为 CommonJS,在 Node.js 上使用;(Node.js 13.2.0 开始支持 ES 6,所以这可能是个过时的信息)

CommonJS 和 ES6 Module

CommonJS 通过 require 同步加载模块,通过 exports 或 module.exports 导出模块:

image.png

ES6 Module 通过 import from 或 import() 加载模块,通过 export 导出模块。

使用 import from 时,模块为静态加载,即在编译的时候确定模块的依赖关系以及输入输出的变量:

image.png

使用 import() 时,模块为动态加载,即在需要时动态加载模块:

image.png

导出模块:

image.png

CommonJS 和 ES6 Module 的区别

来源:www.jianshu.com/p/7db2df4ec…

image.png

模块(module)、库(library)、包(package)

(这一部分只代表本人理解,因为没有找到很靠谱的答案)

  1. module:对于模块化的文件,一个文件就相当于一个模块,实现特定的功能,模块内变量有自己的作用域;
  2. library:使用一个或多个模块来提供一组功能;
  3. package:包括了模块和其他资源,是保障功能正常运行、提供给其他开发者安装和使用的单位;

webpack

“本质上,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。”

模块化打包的目标是什么:

  1. 打包文件,减少加载文件数量,从而减少浏览器向服务器请求文件的次数;
  2. 不同类型的文件都能通过模块化方式导入导出;
  3. 将浏览器不能直接运行、或功能不兼容的语言转换成合适的格式;

webpack 本身只能实现第一、第二个目标,但是它提供了 loader 和 plugin 两种实现功能拓展的方式。我们可以借助第三方的 loader 和 plugin 实现第三个目标。例如,使用 babel-loader 和 babel 插件(@babel/plugin-xxx)对 ES 6 代码降级,使用 less-loader 将 .less 文件转换成 .css 文件。

几个 tips:

  1. webpack 运行在 Node.js 中,自身遵循 CommonJS 规范,使用 require 和 module.exports 导入导出;
  2. 写在 entry 内的 chunk 默认是静态加载模块的,而在 webpack 中也可以异步加载模块;

webpack 打包过程

webpack 从入口开始构建依赖图,并将依赖文件交给 loader 处理,最后将处理结果打包成 bundle;

最终产物是什么

参考:segmentfault.com/a/119000001…

打包过后生成了一个 .js 文件,内容是一个IIFE(立即执行函数),其中自定义了 webpack 的依赖方法 _webpack_require_ (下称 w_require)。立即执行函数中 w_require 了作为入口文件的 module,入口 module 中又会 w_require 其他 module 的依赖,最后形成了网状的依赖结构。

w_require 内部实现了一个缓存机制,即每个 module 只会加载一次,重复 w_require 会返回缓存内容。

不管是 commonJS 的 requie,还是 ES 6 的 import,webpack 都会替换成 w_require。

webpack 事件流和 tapble

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。—— 吴浩麟《深入浅出 webpack 》

Tapable 是一个事件管理类,它采用订阅 - 发布模式对于事件进行管理。Tapable 类对外暴露出了一组钩子,每个钩子实例上又会暴露 tap 方法用于绑定回调函数、call 用于调用回调函数。(根据同步、异步,每个钩子的接口名称可能会有不同)。

webpack 中的许多对象,如编译器(Compiler)、构建器(Compilation)都是 Tapable 的子类,所以我们可以通过钩子,把事件绑定在这些 webpack 的子类上。

在 webpack 从入口文件开始遍历前,就会调用 plugin 里的 apply 方法,绑定插件中的回调函数。

chunk 和 bundle

  • A Chunk is a unit of encapsulation for Modules.
  • Chunks are "rendered" into bundles that get emitted when the build completes.

引文来自 webpack 源码,翻译过来就是,chunk 是 webpack 对 module 进行打包的单位,当构建结束后,chunk 就变成了 bundle,而 bundle 是 webpack 最后的输出结果。

chunk 和 bundle 在大部分情况下是一对一的关系,但也可能是一对多的,例如使用了 source-map 后,chunk 除了生成 .js 文件、还会生成 .js.map 文件,这样一共是两个 bundle。

除了指定 entry 以外,按需加载、部分优化选项也能引入新的 chunk,参考:juejin.cn/post/684490…

loader

loader 用于对模块的源代码进行转换。loader 在 import 或加载模块时预处理文件。

有两种使用 loader 的方法:

  1. 配置方法:webpack.config.js 文件中按规则指定 loader;
  2. 内联方法:在每个 import 语句中显式指定 loader;

loader interface:webpack.docschina.org/api/loaders

自定义 loader

loader 其实就是导出了一个转换函数,拿到要处理的数据、处理完后导出结果就可以了。

image.png

转换过程中,我们可能还需要额外的信息,可以从 loader 上下文中获取: webpack.docschina.org/api/loaders…

loader 的运行顺序

loader runner 有两个阶段:

  1. pitch 阶段:loader 按照后置(post)、内联(inline)、普通(normal)、前置(pre)的顺序调用,按照从左至右(从上至下)的顺序调用,如果某一个 loader 在 pitch 阶段返回了一个结果(通过 module.exports.pitch),那么会跳过剩下的 leader,直接进入上一个 loader 的 normal 阶段。
  2. normal 阶段(实际调用阶段):loader 按照前置(pre)、普通(normal)、内联(inline)、后置(post)的顺序调用,按照从右至左(从下至上)的顺序调用。每个 leader 将其处理过的结果,作为输入传递给下一个 loader。

例如,配置了['a-loader', 'b-loader', 'c-loader'] 三个 loader时,正常的执行顺序:

a-pitch -> b-pitch -> c-pitch -> requested module is picked up as a dependency -> c-normal -> b-normal -> a-normal

如果 b-pitch 阶段返回了一个结果,那么:

a-pitch -> b-pitch -> a-normal

为什么是从右往左呢

函数式编程里两种组合函数的模式,一种叫 compose(从右往左,compose(f,g,h) = (...args) => f(g(h(...args)))),一种叫 pipe(从左往右, pipe(f,g,h) = (...args) => h(g(f(...args)))),webpack 只是选择了 compose 的组合模式。

内联 loader

image.png

loader 的具体调用方法

juejin.cn/post/699275… (没太看懂,有空再看吧)

常用 loader

参考:webpack.docschina.org/loaders/

  1. babel-loader:降级 .js 文件;
  2. css-loader: 加载 .css 文件,转化为 webpack 可识别格式,如将 @import 转化成 require;
  3. style-loader:使用 style 标签将 css-loader 的解析结果注入 html;
  4. postcss-loader:进一步处理 .css 文件,如添加浏览器前缀、压缩 css 等;
  5. sass-loader / less-loader: 转换 .sass / .less 文件到 .css;
  6. ts-loader:转换 .ts 文件;
  7. file-loader:处理图片等文件,将文件直接输出到出口;
  8. source-map-loader:从现有的源文件中提取 source maps;
  9. remark-loader:加载 markdown;

plugin

“plugin 是具有 apply 方法的对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。”

plugin 工作原理

  1. 读取配置时,实例化一个 plugin 对象;
  2. 调用对象的 apply 方法,在 compiler 上绑定事件的回调函数;
  3. 当 webpack 广播相应事件时,触发回调函数;

image.png

常用plugin

  1. html-webpack-plugin:生成 html 文件,并自动引入打包好的 .js 文件;
  2. uglifyjs-webpack-plugin:压缩 .js 文件;
  3. mini-css-extract-plugin:把 css-loader 解析结果放入新建的 .css 文件中;
  4. optimize-css-assets-webpack-plugin:优化 css;
  5. clean-webpack-plugin:打包前清理上一次输出目录;
  6. copy-webpack-plugin:将静态资源复制到输出目录;
  7. DLLPlugin:webpack 内置,只编译核心代码;
  8. HotModuleReplacementPlugin:webpack 内置,自动热更新;
  9. IgnorePlugin:webpack 内置,忽略第三方指定目录;

webpack.config.js 配置

参考:webpack.docschina.org/configurati…

入口(entry)和上下文(context)

“入口对象是用于 webpack 查找开始构建 bundle 的地方。上下文是入口文件所处的目录的绝对路径的字符串。”

image.png

模式(mode)

“提供 mode 配置选项,告知 webpack 使用相应模式的内置优化”

image.png

image.png

输出(output)

"output 位于对象最顶级键(key),包括了一组选项,指示 webpack 如何去输出、以及在哪里输出你的「bundle、asset 和其他你所打包或使用 webpack 载入的任何内容」"

image.png

模块(module)

"这些选项决定了如何处理项目中的不同类型的模块。"

image.png image.png

解析(resolve)

"设置模块如何被解析"

解析:webpack.docschina.org/configurati…

package.json 中的解析配置会比 webpack.config.js 配置更高

image.png

插件(plugin)

"plugins选项用于以各种方式自定义 webpack 构建过程。"

非配置项

替换模板字符串(template strings)

webpack.docschina.org/configurati…

webpack 的其他特性

热更新(Hot Module Replacement)

修改文件后,无需刷新页面就可以更新模块。

参考:www.jianshu.com/p/4bc89dd91…

image.png

代码分割(Code Splitting)

webpack 通过代码分割,实现了依赖的动态加载;

  1. 当需要引入一个依赖时,如果依赖不在缓存中,则创建一个 promise 并缓存;
  2. 构建一个 script 标签,挂到 head 中,src 指向需要动态加载的依赖;
  3. 根据 script 标签的 onload 和 onerror 事件,确定模块是否加载成功;

treeshaking

删除无法到达的代码,减小产物体积

  1. 标记出模块导出值哪些没有被用过;
  2. 使用 Terser 删除掉没有被用到的导出语句;

webpack 5.0 更新

webpack.docschina.org/blog/2020-1…

尝试用持久性缓存来提高构建性能

  1. 可将缓存类型设置为文件系统,构建后果会被持久性缓存在本地;

尝试用更好的算法和默认值来改进长期缓存

  1. 长期缓存算法在生产模式下是默认启用的;
  2. 用真正的文件内容哈希值,当文件只有注释被修改或变量被重命名时,不会影响缓存;

尝试用更好的 Tree Shaking 和代码生成来改善包大小

  1. 嵌套的 tree-shaking;
  2. 内部模块 tree-shaking;
  3. 支持对 commonJS 导出做 tree-shaking;
  4. 自动标记 no sideEffects;
  5. 运行时优化,在真正需要的地方导出依赖;
  6. 模块合并(没太理解);
  7. 通用 Tree Shaking 改进;
  8. 开发与生产的一致性问题;
  9. 支持生成 ES 6 代码;
  10. 更多的 target 选项;
  11. 代码块拆分与模块大小;

尝试改善与网络平台的兼容性

  1. 支持全新的 Web 平台特性(json 模块、资源模块、异步模块、原生 worker 等);
  2. 支持全新的 Node.js 生态特性;

尝试在不引入任何破坏性变化的情况下,清理那些在实现 v4 功能时处于奇怪状态的内部结构

试图通过现在引入突破性的变化来为未来的功能做准备,使其能够尽可能长时间地保持在 v5 版本上

  1. 允许启用实验功能;

webpack 与其他打包工具的对比

xieyufei.com/2021/01/28/…

npm

npm 发布

  1. npm init
  2. 配置 package.json
  3. 编写 README.MD 等
  4. npm publish

package.json 配置

  1. name:名称;
  2. version:版本;
  3. desription:描述;
  4. keywords:关键词;
  5. license:开源协议;
  6. main:文件入口;
  7. bin:可执行文件位置;
  8. scripts:npm 命令执行脚本;
  9. dependencies:项目运行依赖;
  10. devDependencies:项目开发依赖;
  11. peerDependencies:指定依赖库被多个项目依赖时,所需依赖库的版本号;
  12. engines:运行平台;

包版本管理

语义化版本 2.0.0:semver.org/lang/zh-CN/