模块化发展与打包工具
写在前面:乐,没怎么打包过来着
什么是模块化
模块化是一种编程方法,它将大型复杂的程序分解为互相独立的子程序,每个子程序被称为一个模块。每个模块都有特定的功能,并且可以独立完成这个功能。
前端的模块化主要是为了解决命名冲突、文件依赖、复用性等问题。在前端开发中,模块化可以帮助我们更好地组织和管理代码,提高代码的可维护性和可复用性。
具有以下几个优点:
- 可维护性:模块化可以将复杂的程序分解为简单的模块,每个模块都有明确的功能。这样,当需要修改某个功能时,只需要修改对应的模块,而不需要理解整个程序。
- 可复用性:模块化可以将常用的功能封装为模块,然后在需要的地方导入这个模块。这样,可以避免重复编写相同的代码。
- 可测试性:模块化可以将程序分解为独立的模块,每个模块都可以独立测试。这样,可以确保每个模块的功能都是正确的,从而提高整个程序的质量。
模块化发展历程与规范
发展历程主要包括以下几个阶段:
全局函数:在早期的 JavaScript 中,所有的变量和函数都是全局的。这种方式容易导致命名冲突和代码难以维护。
命名空间:为了解决全局函数的问题,开发者开始使用命名空间来组织代码。但这种方式仍然无法完全解决命名冲突的问题,而且代码的依赖关系不明确。
CommonJS:Node.js 推出了 CommonJS 模块规范,每个文件就是一个模块,文件内部的所有变量和函数默认都是私有的,对其他文件不可见,通过
require和module.exports来导入和导出模块。这种方式使得 JavaScript 的模块化成为可能,但它主要用于服务器端(由于同步加载),不适合浏览器环境。AMD 和 CMD:为了解决 CommonJS 在浏览器环境中的问题,社区提出了
AMD(Asynchronous Module Definition和CMD(Common Module Definition)规范。AMD(Asynchronous Module Definition)
AMD 是 RequireJS 在提出的规范。它支持异步加载模块,即可以在需要的时候再加载模块。AMD 使用 define 函数来定义模块,require 函数来加载模块。AMD 推崇依赖前置,在定义模块的时候,需要先指明依赖的模块JS// 定义模块 define(['dependency'], function(dependency) { // 模块代码 }); // 加载模块 require(['module'], function(module) { // 使用模块 });CMD(Common Module Definition)
CMD 是 SeaJS 在提出的规范。CMD 也支持异步加载模块,推崇依赖就近,即只在需要的地方加载模块。CMD 使用 define 函数来定义模块,require 函数来加载模块JS// 定义模块 define(function(require, exports, module) { // 加载依赖 var dependency = require('dependency'); // 模块代码 }); // 加载模块 seajs.use('module', function(module) { // 使用模块 });
ES6 模块
ES6 模块化是 JavaScript 的官方模块系统,它使用 import 和 export 关键字来导入和导出模块。有以下几个特点:- 静态导入和导出:ES6 模块的导入和导出都是静态的,也就是说不能在运行时改变模块的导入和导出。这使得工具可以在编译时进行优化,如 tree shaking(摇树优化:描述移除 JavaScript 上下文中未引用或未使用代码的过程)
- 模块作用域:ES6 模块内部的变量和函数默认都是私有的,如果你想把它们暴露给其他模块,你需要使用 export 关键字。
- 支持默认导出和命名导出:ES6 模块支持默认导出和命名导出,这使得你可以导出任意值,如函数、对象、字符串、数字等。
然而,由于浏览器对 ES6 模块的支持程度不一,通常需要通过打包工具将 ES6 模块转换为能在浏览器中运行的代码。
打包工具
webpack
webpack是一个用于现代JavaScript应用程序的静态模块打包工具。
静态模块
Webpack 的"静态模块"是指在编译时就能确定模块的依赖关系,以及输入和输出的模块。与动态加载(在运行时确定这些事情)形成对比。
在 Webpack 中,每个文件都被视为一个模块,这些模块通过 import 或 require 语句进行连接。Webpack 会从一个或多个入口文件开始,解析这些文件中的 import 或 require 语句,构建出一个依赖图(dependency graph)。这个依赖图包含了应用程序所需的每个模块,并且所有模块都被打包成一个(或多个)bundle。
这种静态的模块结构允许 Webpack 在打包时进行大量优化,如代码分割、懒加载、移除未使用的代码(tree shaking)等。这些优化可以帮助提高应用程序的加载性能和运行性能。
webpack 的核心概念
- entry(入口):一个可执行模块或者库的入口。定义了打包后的入口文件。
- output(出口):指示 webpack 如何去输出,以及在哪里输出。
- path: 打包文件存放的绝对路径
- publicPath: 网站运行时的访问路径
- filename: 打包后的文件名
- module(模块):在 webpack 里,一切皆模块,一个模块对应一个文件。webpack 会从配置的 entry 中开始递归找出所有依赖的模块。
- chunk(代码块):一个 chunk 由多个 模块 组合而成。可以将可执行的模块和他所依赖的模块组合成一个 chunk ,这就是打包。
- loader(模块转换器):用于把一个模块原内容按照需求转换成新的内容。例如:es6 转换为 es5,scss 转换为 css 等。
- plugin(扩展):扩展 webpack 功能的插件。在 webpack 构建的生命周期节点上加入扩展 hook,添加功能。
构建流程

- 初始化参数 Webpack 启动时,将从命令行(shell)传入的参数和 webpack.config.js 文件中的配置参数进行合并,初始化出一个 Compiler 对象
- 编译
上一步得到的参数初始化 compiler 对象,注册所有配置的插件,插件监听 webpack 构建生命周期的事件节点,做出相应的反应,执行对象的 run 方法开始执行编译 - 确定入口
根据配置的 entry 入口,开始解析文件构建的 AST 语法树,找出依赖,递归下去 - 编译模块
根据文件类型和 loader 配置,调用所有配置的 loader 对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。 - 完成模块编译并输出 递归完后,得到每个文件结果,包含了每个模块及她们之间的依赖关系,根据 entry 配置生成代码块 chunk
- 输出完成
输出所有的 chunk 到系统文件
编译生命周期钩子
- entry-option :初始化 option
- run:在编译器开始读取记录之前触发
- compile: 真正开始的编译,在创建 compilation 对象之前
- compilation :生成好了 compilation 对象
- make: 从 entry 开始递归分析依赖,准备对每个模块进行 build
- after-compile: 编译 build 过程结束
- emit :在将内存中 assets 内容写到磁盘文件夹之前
- after-emit :在将内存中 assets 内容写到磁盘文件夹之后
- done: 完成所有的编译过程
- failed: 编译失败的时候
Loader(模块转换器)
loader 用于对模块的"源代码"进行转换,在 import 或"加载"模块时预处理文件
Loader 特性
- 转换文件
Loader 是用来对模块源代码进行转换。Webpack 本身只能处理 JavaScript,但是 loader 能够让 Webpack 处理那些非 JavaScript 文件(例如 CSS,LESS,TypeScript 等)。Loader 可以将这些文件转换为有效的模块,并且可以添加到依赖图中。 - 链式处理
Loader 可以链式调用。一组链式的 Loader 将按照相反的顺序执行。Loader 链中的第一个 Loader 返回值给下一个。在最后一个 Loader,返回 webpack 所预期的 JavaScript。 - 支持同步和异步
Loader 支持同步和异步转换。这意味着 Loader 可以进行一些耗时的操作,例如网络请求 - 模块热替换
如果模块在运行时进行更改,Loader 可以将更改的模块发送给运行时,而无需完全重新构建。
配置 Loader 的方式
- 配置方式(推荐):在 webpack.config.js 文件中指定 loader
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: 'css-loader'
}
]
}
};- 内联方式:在每个 import 语句中显式指定 loader
import Styles from 'style-loader!css-loader?modules!./styles.css';- CLI 方式:在 shell 命令中指定它们
webpack --module-bind js=babel-loader常见 Loader
style-loader: 将 css 添加到 DOM 的内联样式标签 style 里
css-loader :允许将 css 文件通过 require 的方式引入,并返回 css 代码
less-loader: 处理 less。将 less 代码转换为 CSS 代码
sass-loader: 处理 sass。将 sass 代码转换为 CSS 代码
postcss-loader: 用 postcss 来处理 CSS
file-loader: 分发文件到 output 目录并返回相对路径
url-loader: 和 file-loader 类似,但是当文件小于设定的 limit 时可以返回一个 Data Url
html-minify-loader: 压缩 HTML
babel-loader :用 babel 来转换 ES6 文件到 ES
Plugin
webpack 中的 plugin 赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在 webpack 的不同阶段(钩子 / 生命周期),贯穿了 webpack 整个编译周期 
Plugin 特性
其本质是一个具有apply方法javascript对象 apply方法会被 webpack compiler调用,并且在整个编译生命周期都可以访问 compiler对象compiler对象代表了 webpack 从启动到关闭的整个生命周期。你可以在 apply 方法内部监听 compiler上的各种事件,并在特定时机执行自定义的行为。
常见的 Plugin
HtmlWebpackPlugin 这个插件会创建一个 HTML 文件,其中包括使用 script 标签的所有 webpack 包。
JS// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // ... 其他配置项 plugins: [ new HtmlWebpackPlugin({ title: 'My App', // 生成的 HTML 文件的标题 filename: 'index.html', // 输出的 HTML 文件名,默认为 'index.html' template: 'src/index.html', // 模板文件路径,插件会根据这个模板生成 HTML 文件 inject: 'body', // 打包后的脚本插入到哪个标签中,可选值有 'head' 和 'body' }), ], };配置后,在
html模板中,可以通过<%=htmlWebpackPlugin.options.XXX%>的方式获取配置的值HTML<!--./src/html/index.html--> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title><%=htmlWebpackPlugin.options.title%></title> </head> <body> <h1>html-webpack-plugin</h1> </body> </html>clean-webpack-plugin: 这个插件会在每次成功重建后,删除
webpack的output.path目录中的所有文件。mini-css-extract-plugin:这个插件会将 CSS 提取到单独的文件中。它为每个包含 CSS 的 JS 文件创建一个 CSS 文件。
DefinePlugin:允许在编译时创建配置的全局对象,是一个
webpack内置的插件copy-webpack-plugin:复制文件或目录到执行区域,如
vue的打包过程中,如果我们将一些文件放到public的目录下,那么这个目录会被复制到dist文件夹中
Vite
Vite ,是一种新型前端构建工具,能够显著提升前端开发体验
Vite 特点
- 快速冷启动:Vite 利用原生 ES Modules (ESM) 实现了快速的冷启动,无需等待打包过程。
- 即时热更新:Vite 提供了即时的模块热更新,当你修改文件时,只有被修改的文件会被重新编译和更新,而不是整个应用。
- 按需加载:在浏览器中,只有实际需要的模块才会被加载和执行,这可以减少不必要的网络请求和计算。
Vite 原理
在热模块 HMR 方面,当修改一个模块的时候,仅需让浏览器重新请求该模块即可,无须像 webpack 那样需要把该模块的相关依赖模块全部编译一次,效率更高
