模块化
模块化的背景
随着 web 的发展,一个网站所需的 javascript 越来越复杂,开发者自然而然地希望能够将不同功能的代码拆分开来,以利于阅读和维护。
在没有模块化之前,开发者首先尝试了只简单地、将不同功能的代码拆分到不同的 .js 文件中,然后在 html 中使用多个 script 标签,分别导入对应不同功能的 .js 文件,比如:
这种简单粗暴的方法只是在形式上把代码分离了,没有分离代码的作用域,所以依然存在一些问题,例如,命名冲突、可以直接修改其他代码文件中的变量(作用域污染)、各功能间的依赖关系不明显等。
早期的开发者为了解决问题,做出了各种各样的努力:
- 代码放在立即执行的函数作用域中,只把希望被其他脚本使用的接口函数暴露出来,避免了作用域污染,同时可以通过函数的参数来表现依赖关系;
- 定义一个全局变量的对象,把一个 .js 文件中的接口都挂在这个对象上。这个对象相当于文件的命名空间,避免了命名冲突的问题;
模块化为以上的问题提供了一个更简单的解决答案。一个模块就是实现一个特定功能的文件,开发者可以通过引入模块来使用功能。
模块化的意义
- 避免命名冲突、作用域污染等问题;
- 有利于复用代码、表现依赖关系;
- 有利于阅读和维护;
模块化的标准
在早期,服务端对于模块化的需求更加强烈,所以第一个的模块化规范 CommonJS 是针对 Node.js 设计的,它在 2010 年左右诞生并沿用至今。CommonJS 以同步的方式加载模块,而 Node.js 在启动时加载模块、执行时使用模块。
服务器端加载模块相当于从本地硬盘中读取文件,需要的时间很少,同步加载是可行的。但浏览器向服务器请求模块文件的耗时是难以预测的,所以浏览器如果也以同步的方式加载模块,可能会被阻塞较长的时间,出现“假死”的情况。
基于以上原因,开发者为浏览器设计了一个新的模块化规范 AMD (Asynchronous Module Definition),同时推出了支持 AMD 规范的模块加载器 RequireJS。。
国内在同期推出了另一个浏览器模块化规范 CMD(Common Module Definition)和相应的模块加载器 SeaJS。
总而言之,这时候的浏览器依然没有原生支持模块化,但是可以借助 RequireJS、SeaJS 等第三方库实现模块化功能。
ES6 Module 提出后,浏览器终于开始原生支持模块功能,这比使用第三方库更有效率。
最后,还有一点令人头痛 —— 那就是上面这些模块化标准并不兼容。所以为了模块代码能够在不同的模块环境都能正常运行,又做了额外的工作:
- UMD(Universal Module Definition):通用模块定义规范,可以处理 CommonJS、AMD、CMD 的差异兼容;
- webpack: 打包编译的时候,会把统一替换成 _webpack_require_ 来实现模块的引入和导出;
- babel:babel 可以将 ES 6 标准降级为 CommonJS,在 Node.js 上使用;(Node.js 13.2.0 开始支持 ES 6,所以这可能是个过时的信息)
CommonJS 和 ES6 Module
CommonJS 通过 require 同步加载模块,通过 exports 或 module.exports 导出模块:
ES6 Module 通过 import from 或 import() 加载模块,通过 export 导出模块。
使用 import from 时,模块为静态加载,即在编译的时候确定模块的依赖关系以及输入输出的变量:
使用 import() 时,模块为动态加载,即在需要时动态加载模块:
导出模块:
CommonJS 和 ES6 Module 的区别
来源:www.jianshu.com/p/7db2df4ec…
模块(module)、库(library)、包(package)
(这一部分只代表本人理解,因为没有找到很靠谱的答案)
- module:对于模块化的文件,一个文件就相当于一个模块,实现特定的功能,模块内变量有自己的作用域;
- library:使用一个或多个模块来提供一组功能;
- package:包括了模块和其他资源,是保障功能正常运行、提供给其他开发者安装和使用的单位;
webpack
“本质上,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。”
模块化打包的目标是什么:
- 打包文件,减少加载文件数量,从而减少浏览器向服务器请求文件的次数;
- 不同类型的文件都能通过模块化方式导入导出;
- 将浏览器不能直接运行、或功能不兼容的语言转换成合适的格式;
webpack 本身只能实现第一、第二个目标,但是它提供了 loader 和 plugin 两种实现功能拓展的方式。我们可以借助第三方的 loader 和 plugin 实现第三个目标。例如,使用 babel-loader 和 babel 插件(@babel/plugin-xxx)对 ES 6 代码降级,使用 less-loader 将 .less 文件转换成 .css 文件。
几个 tips:
- webpack 运行在 Node.js 中,自身遵循 CommonJS 规范,使用 require 和 module.exports 导入导出;
- 写在 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 的方法:
- 配置方法:webpack.config.js 文件中按规则指定 loader;
- 内联方法:在每个 import 语句中显式指定 loader;
loader interface:webpack.docschina.org/api/loaders
自定义 loader
loader 其实就是导出了一个转换函数,拿到要处理的数据、处理完后导出结果就可以了。
转换过程中,我们可能还需要额外的信息,可以从 loader 上下文中获取: webpack.docschina.org/api/loaders…
loader 的运行顺序
loader runner 有两个阶段:
- pitch 阶段:loader 按照后置(post)、内联(inline)、普通(normal)、前置(pre)的顺序调用,按照从左至右(从上至下)的顺序调用,如果某一个 loader 在 pitch 阶段返回了一个结果(通过 module.exports.pitch),那么会跳过剩下的 leader,直接进入上一个 loader 的 normal 阶段。
- 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
loader 的具体调用方法
juejin.cn/post/699275… (没太看懂,有空再看吧)
常用 loader
参考:webpack.docschina.org/loaders/
- babel-loader:降级 .js 文件;
- css-loader: 加载 .css 文件,转化为 webpack 可识别格式,如将 @import 转化成 require;
- style-loader:使用 style 标签将 css-loader 的解析结果注入 html;
- postcss-loader:进一步处理 .css 文件,如添加浏览器前缀、压缩 css 等;
- sass-loader / less-loader: 转换 .sass / .less 文件到 .css;
- ts-loader:转换 .ts 文件;
- file-loader:处理图片等文件,将文件直接输出到出口;
- source-map-loader:从现有的源文件中提取 source maps;
- remark-loader:加载 markdown;
plugin
“plugin 是具有 apply 方法的对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。”
plugin 工作原理
- 读取配置时,实例化一个 plugin 对象;
- 调用对象的 apply 方法,在 compiler 上绑定事件的回调函数;
- 当 webpack 广播相应事件时,触发回调函数;
常用plugin
- html-webpack-plugin:生成 html 文件,并自动引入打包好的 .js 文件;
- uglifyjs-webpack-plugin:压缩 .js 文件;
- mini-css-extract-plugin:把 css-loader 解析结果放入新建的 .css 文件中;
- optimize-css-assets-webpack-plugin:优化 css;
- clean-webpack-plugin:打包前清理上一次输出目录;
- copy-webpack-plugin:将静态资源复制到输出目录;
- DLLPlugin:webpack 内置,只编译核心代码;
- HotModuleReplacementPlugin:webpack 内置,自动热更新;
- IgnorePlugin:webpack 内置,忽略第三方指定目录;
webpack.config.js 配置
参考:webpack.docschina.org/configurati…
入口(entry)和上下文(context)
“入口对象是用于 webpack 查找开始构建 bundle 的地方。上下文是入口文件所处的目录的绝对路径的字符串。”
模式(mode)
“提供 mode 配置选项,告知 webpack 使用相应模式的内置优化”
输出(output)
"output 位于对象最顶级键(key),包括了一组选项,指示 webpack 如何去输出、以及在哪里输出你的「bundle、asset 和其他你所打包或使用 webpack 载入的任何内容」"
模块(module)
"这些选项决定了如何处理项目中的不同类型的模块。"
解析(resolve)
"设置模块如何被解析"
解析:webpack.docschina.org/configurati…
package.json 中的解析配置会比 webpack.config.js 配置更高
插件(plugin)
"plugins选项用于以各种方式自定义 webpack 构建过程。"
非配置项
替换模板字符串(template strings)
webpack.docschina.org/configurati…
webpack 的其他特性
热更新(Hot Module Replacement)
修改文件后,无需刷新页面就可以更新模块。
参考:www.jianshu.com/p/4bc89dd91…
代码分割(Code Splitting)
webpack 通过代码分割,实现了依赖的动态加载;
- 当需要引入一个依赖时,如果依赖不在缓存中,则创建一个 promise 并缓存;
- 构建一个 script 标签,挂到 head 中,src 指向需要动态加载的依赖;
- 根据 script 标签的 onload 和 onerror 事件,确定模块是否加载成功;
treeshaking
删除无法到达的代码,减小产物体积
- 标记出模块导出值哪些没有被用过;
- 使用 Terser 删除掉没有被用到的导出语句;
webpack 5.0 更新
webpack.docschina.org/blog/2020-1…
尝试用持久性缓存来提高构建性能
- 可将缓存类型设置为文件系统,构建后果会被持久性缓存在本地;
尝试用更好的算法和默认值来改进长期缓存
- 长期缓存算法在生产模式下是默认启用的;
- 用真正的文件内容哈希值,当文件只有注释被修改或变量被重命名时,不会影响缓存;
尝试用更好的 Tree Shaking 和代码生成来改善包大小
- 嵌套的 tree-shaking;
- 内部模块 tree-shaking;
- 支持对 commonJS 导出做 tree-shaking;
- 自动标记 no sideEffects;
- 运行时优化,在真正需要的地方导出依赖;
- 模块合并(没太理解);
- 通用 Tree Shaking 改进;
- 开发与生产的一致性问题;
- 支持生成 ES 6 代码;
- 更多的 target 选项;
- 代码块拆分与模块大小;
尝试改善与网络平台的兼容性
- 支持全新的 Web 平台特性(json 模块、资源模块、异步模块、原生 worker 等);
- 支持全新的 Node.js 生态特性;
尝试在不引入任何破坏性变化的情况下,清理那些在实现 v4 功能时处于奇怪状态的内部结构
试图通过现在引入突破性的变化来为未来的功能做准备,使其能够尽可能长时间地保持在 v5 版本上
- 允许启用实验功能;
webpack 与其他打包工具的对比
npm
npm 发布
- npm init
- 配置 package.json
- 编写 README.MD 等
- npm publish
package.json 配置
- name:名称;
- version:版本;
- desription:描述;
- keywords:关键词;
- license:开源协议;
- main:文件入口;
- bin:可执行文件位置;
- scripts:npm 命令执行脚本;
- dependencies:项目运行依赖;
- devDependencies:项目开发依赖;
- peerDependencies:指定依赖库被多个项目依赖时,所需依赖库的版本号;
- engines:运行平台;
包版本管理
语义化版本 2.0.0:semver.org/lang/zh-CN/