模块化
产生原因
- 大量模块成员污染全局作用域
- 没有私有空间,模块内成员可以被模块外成员访问修改
- 模块较多时,容易产生命名冲突
- 无法管理模块间的依赖关系
- 维护过程中也很难辨别成员所属模块
commonJS
CommonJS规范,是Node.js中所遵循的模块规范。约定:一个文件是一个模块,每个模块有单独的作用域。通过module.exports导出,通过require引入。
CommonJS 是以同步的方式加载模块,因为Node.js执行机制是在启动时加载模块,执行时使用。但并不适用于浏览器,如果浏览器使用同步加载,会引发大量的同步模式请求,导致应用运行效率低。
AMD
AMD是每个模块通过define() 函数去定义。如果当前模块中需要向外部导出成员,可以通过return的方式实现。require() 函数用于自动加载模块。可以实现异步加载模块。
require.js是基于AMD规范实现的库,主要实现原理是需要加载一个模块是,内部就会自动创建script标签去请求并执行响应模块的代码。
// ADM 定义一个模块
define(['A', 'B'], function(moduleA, moduleB) {
return moduleC
})
// ADM 引入一个模块
require(['C'], function(moduleC) {
moduleC.init
})
CMD
CMD类似于CommonJS规范,具体实现有Sea.js, 后面Require.js 兼容了。同步加载模块。
define(function(require, exports,module) {
// 通过require 引入依赖
var A = require('moduleA')
// 通过exports 或者 module.exports对外暴露成员
module.exports = function () {
return B
}
})
ES Module
支持可按需导入单独模块,支持异步导入。通过import导入, export、export default 导出。
import A form B
import { default as A} form B
export default A
import {a,b,c} from C
import * as D from C
export {a,b,c}
将import()作为函数调用,将其作为参数传递给模块的路径。 它返回一个 promise,所以可以异步加载。
import('/modules/myModule.mjs')
.then((module) => {
// Do something with the module.
});
Webpack 核心特性
Webpack 想要实现的是整个前端项目的模块化,项目中的各种资源(包括 CSS 文件、图片等)都应该属于需要被管理的模块。换句话说, Webpack 不仅是 JavaScript 模块打包工具,还是整个前端项目(前端工程)的模块打包工具。也就是说,我们可以通过 Webpack 去管理前端项目中任意类型的资源文件。
在日常开发中我们对打包方案或设想是:
- 可以兼容用户浏览器
- 能够将散落的模块打包到一起,避免浏览器频繁发送网络请求
- 支持不同种类的前端资源模块
Tips
- webpack.config.js 是一个运行在Node.js环境中的JS文件,所以我们需要按照CommonJS的方式编写代码。使用module.export。
- 在开发过程中使用VS code 时,可以在头部加上
import { Configuration } from 'webpack'用来只能提示webpack 配置项设置 - VS Code 折叠代码
在例子中简单打包后会生成bundle.js,bunde.js 是一个自执行函数,内含会加载模块数组,以及定义工具方法如webpack.require 用来加载其他模块等。
Loader
webpack 中默认只能处理JS模块代码,在打包过程中默认将所有文件都当作JS 代码处理。Webpack 实现不同种类资源模块加载的核心就是 Loader。
CSS
css-loader: css-loader 只会把 CSS 模块加载到 JS 代码中,而并不会使用这个模块。
style-loader: 将 css-loader 中所加载到的所有样式模块,通过创建 style 标签的方式添加到页面上。
所以在配置过程中需要按照执行顺序配置,从后往前,从右往左依次配置。
如何编写一个loader
Webpack 加载资源文件的过程类似于一个工作管道,你可以在这个过程中依次使用多个 Loader,但是最终这个管道结束过后的结果必须是一段标准的 JS 代码字符串。
- 每个loader 是一个独立的模块,引入需要处理的文件内容,输出处理完成的内容。
- 处理每类文件最后的loader需要在这个 Loader 的最后返回一段 JS 代码字符串。
- 找到一个合适的加载器,在后面接着处理我们得到的结果。
插件Plugins
Webpack 插件机制的目的是为了增强 Webpack 在项目自动化构建方面的能力。
插件常见应用场景
- 实现自动在打包之前清除 dist 目录(上次的打包结果);
clean-webpack-plugin - 自动生成应用所需要的 HTML 文件;
html-webpack-plugin - 根据不同环境为代码注入类似 API 地址这种可能变化的部分;
- 拷贝不需要参与打包的资源文件到输出目录;
copy-webpack-plugin - 压缩 Webpack 打包完成后输出的文件;提取css在单独的文件:
mini-css-extract-plugin压缩样式文件:optimize-css-assets-webpack-plugin内置的 JS 压缩插件:terser-webpack-plugin - 自动发布打包结果到服务器实现自动部署;
ssh2-sftp-client - 分析打包后体积:
webpack-bundle-analyzer模块代码缓存:hard-source-webpack-plugin并行压缩: `terser-webpack-plugin
如何编写一个Plugin
Webpack 的插件机制就是我们在软件开发中最常见的钩子机制。
钩子机制也特别容易理解,它有点类似于 Web 中的事件。在 Webpack 整个工作过程会有很多环节,为了便于插件的扩展,Webpack 几乎在每一个环节都埋下了一个钩子。这样我们在开发插件的时候,通过往这些不同节点上挂载不同的任务,就可以轻松扩展 Webpack 的能力。
那么plugin的核心就是明确任务的执行时机,并确定应该把这个任务挂载到哪个钩子上。
Tips
- npx可以调用项目内部安装的模块,npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
- 可以使用npm的
process.env.npm_config_xxx来获取当前执行的命令行参数XXX变量 npm之config (也可以使用yargs来获取命令行参数)
webpack 核心步骤
-
通过 Loader 处理特殊类型资源的加载,例如加载样式、图片;
-
通过 Plugin 实现各种自动化的构建任务,例如自动压缩、自动发布。
Webpack 启动后,会根据我们的配置,找到项目中的某个指定文件(一般这个文件都会是一个 JS 文件)作为入口。然后顺着入口文件中的代码,根据代码中出现的 import(ES Modules)或者是 require(CommonJS)之类的语句,解析推断出来这个文件所依赖的资源模块,然后再分别去解析每个资源模块的依赖,周而复始,最后形成整个项目中所有用到的文件之间的依赖关系树。
有了这个依赖关系树过后, Webpack 会遍历(递归)这个依赖树,找到每个节点对应的资源文件,然后根据配置选项中的 Loader 配置,交给对应的 Loader 去加载这个模块,最后将加载的结果放入 bundle.js(打包结果)中,从而实现整个项目的打包。
webpack cli 启动打包流程
- 找到入口文件获取命令行参数: webpack-cli 的入口文件 bin/cli.js 中,会通过 yargs 模块解析 CLI 参数,所谓 CLI 参数指的就是我们在运行 webpack 命令时通过命令行传入的参数,例如 --mode=production;
- 合并默认参数和配置参数: 调用了 bin/utils/convert-argv.js 模块,将得到的命令行参数转换为 Webpack 的配置选项对象。在 convert-argv.js 工作过程中,首先为传递过来的命令行参数设置了默认值,然后判断了命令行参数中是否指定了一个具体的配置文件路径,如果指定了就加载指定配置文件,反之则需要根据默认配置文件加载规则找到配置文件。找到配置文件过后,将配置文件中的配置和 CLI 参数中的配置合并,如果出现重复的情况,会优先使用 CLI 参数,最终得到一个完整的配置选项;
- 开始载入 Webpack 核心模块,传入配置选项,创建 Compiler 对象,这个 Compiler 对象就是整个 Webpack 工作过程中最核心的对象了,负责完成整个项目的构建工作。
webpack 开始构建
- 完成 Compiler 对象的创建过后,紧接着这里的代码开始判断配置选项中是否启用了监视模式
- 如果是监视模式就调用 Compiler 对象的 watch 方法,以监视模式启动构建,但这不是我们主要关心的主线。
- 不是监视模式就调用 Compiler 对象的 run 方法,开始构建整个应用。
- run方法就是先触发了beforeRun 和 run 两个钩子,然后最关键的是调用了当前对象的 compile 方法,真正开始编译整个项目
- compile 方法内部主要就是创建了一个 Compilation 对象,Compilation 字面意思是“合集”,实际上,你就可以理解为一次构建过程中的上下文对象,里面包含了这次构建中全部的资源和信息。
- 创建完 Compilation 对象过后,紧接着触发了一个叫作 make 的钩子,进入整个构建过程最核心的 make 阶段。
make 阶段
make 阶段主体的目标就是:根据 entry 配置找到入口模块,开始依次递归出所有依赖,形成依赖关系树,然后将递归到的每个模块交给不同的 Loader 处理。
Webpack 的插件系统是基于官方自己的 Tapable 库实现的,我们想要知道在哪里注册了某个事件,必须要知道如何注册的事件。Tapable 的注册方式具体如下:
所以,我们只需要通过开发工具搜索源代码中的 make.tap,就应该能够找到事件注册的位置。
- SingleEntryPlugin (单一入口打包方式) 中调用了 Compilation 对象的 addEntry 方法,开始解析入口;
- addEntry 方法中又调用了 _addModuleChain 方法,将入口模块添加到模块依赖列表中;
- 紧接着通过 Compilation 对象的 buildModule 方法进行模块构建;
- buildModule 方法中执行具体的 Loader,处理特殊资源加载;
- build 完成过后,通过 acorn 库生成模块代码的 AST 语法树;
- 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环 build 每个依赖;
- 所有依赖解析完成,build 阶段结束;
- 最后合并生成需要输出的 bundle.js 写入 dist 目录。
webpack 开发配置技巧
Dev Server 本地开发
需要满足以下条件:
- 使用Http 服务运行,更接近生产环境
- 修改代码后,webpack 自动完成构建,浏览器及时显示最新结果
- 提供source map,便于更方便更快速定位问题
webpack-dev-server
运行 webpack-dev-server 这个命令时,它内部会启动一个 HTTP Server,为打包的结果提供静态文件服务,并且自动使用 Webpack 打包我们的应用,然后监听源代码的变化,一旦文件发生变化,它会立即重新打包。
webpack-dev-server 为了提高工作速率,它并没有将打包结果写入到磁盘中,而是暂时存放在内存中,内部的 HTTP Server 也是从内存中读取这些文件的。这样一来,就会减少很多不必要的磁盘读写操作,大大提高了整体的构建效率。
devtool
模块热替换机制(HMR)
指的是我们可以在应用运行过程中,实时的去替换掉应用中的某个模块,而应用的运行状态不会因此而改变。
并不是开箱即用,需要额外增加一些代码。vue.js HMR
Tree-shaking
Tree-shaking 的实现,整个过程用到了 Webpack 的两个优化功能:
- usedExports - 打包结果中只导出外部用到的成员;
- minimize - 压缩打包结果。
module.exports = {
// ... 其他配置项
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 压缩输出结果
minimize: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
}
}
Code Splitting(分块打包)
webpack 实现分包方式:
- 根据业务不同配置多个打包入口,输出多个打包结果, 提取公共模块
splitChunks - 结合 ES Modules 的动态导入(Dynamic Imports)特性,按需加载模块。
Tips
- 用多个文件配置时,简单的使用
Object.assgin无法满足合并配置需求,可以安装webpack-merge - Define Plugin配置全局变量