目录
- 一、背景
-
- [1.1 前端模块加载](#1.1 前端模块加载)
- [1.2 所有资源都是模块](#1.2 所有资源都是模块)
- [1.3 静态分析](#1.3 静态分析)
- [二、什么是 Webpack](#二、什么是 Webpack)
-
- [2.1 入口(entry)](#2.1 入口(entry))
- [2.2 输出(output)](#2.2 输出(output))
- [2.3 loader](#2.3 loader)
- [2.4 插件(plugin)](#2.4 插件(plugin))
- [2.5 模式(mode)](#2.5 模式(mode))
- [2.6 浏览器兼容性(browser compatibility)](#2.6 浏览器兼容性(browser compatibility))
- [2.7 环境(environment)](#2.7 环境(environment))
- 三、模块系统的演进
-
- [3.1 原始方案](#3.1 原始方案)
- [3.2 CommonJS](#3.2 CommonJS)
- [3.3 AMD](#3.3 AMD)
- [3.4 CMD](#3.4 CMD)
- [3.5 UMD](#3.5 UMD)
- [3.6 ES6 模块](#3.6 ES6 模块)
Webpack
是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过 loader
的转换,任何形式的资源都可以视作模块,比如 CommonJs
模块、 AMD
模块、 ES6
模块、CSS
、图片、 JSON
、Coffeescript
、 LESS
等。
一、背景
模块系统主要解决模块的定义、依赖和导出。我们所期望的模块系统:可以兼容多种模块风格,尽量可以利用已有的代码,不仅仅只是 JavaScript
模块化,还有 CSS、图片、字体等资源也需要模块化。
1.1 前端模块加载
前端模块要在客户端中执行,所以他们需要增量加载到浏览器中。模块的加载和传输,我们首先能想到两种极端的方式:
- 一种是每个模块文件都单独请求
- 另一种是把所有模块打包成一个文件然后只请求一次。
显而易见,每个模块都发起单独的请求造成了请求次数过多,导致应用启动速度慢;一次请求加载所有模块导致流量浪费、初始化过程慢。这两种方式都不是好的解决方案,它们过于简单粗暴。
分块传输,按需进行懒加载,在实际用到某些模块的时候再增量更新,才是较为合理的模块加载方案。要实现模块的按需加载,就需要一个对整个代码库中的模块进行静态分析、编译打包的过程。
1.2 所有资源都是模块
模块不仅仅是指JavaScript
模块文件。然而,在前端开发过程中还涉及到样式、图片、字体、HTML 模板等等众多的资源。这些资源还会以各种方言的形式存在,比如 coffeescript
、 less
、 sass
、众多的模板库、多语言系统(i18n
)等等。
如果他们都可以视作模块,并且都可以通过require
的方式来加载,将带来优雅的开发体验,比如:
javascript
require("./style.css");
require("./style.less");
require("./template.jade");
require("./image.png");
那么如何做到让 require
能加载各种资源呢?
1.3 静态分析
在编译的时候,要对整个代码进行静态分析,分析出各个模块的类型和它们依赖关系,然后将不同类型的模块提交给适配的加载器来处理。比如一个用 LESS
写的样式模块,可以先用 LESS
加载器将它转成一个CSS
模块,在通过 CSS
模块把他插入到页面的 <style>
标签中执行。Webpack
就是在这样的需求中应运而生。
同时,为了能利用已经存在的各种框架、库和已经写好的文件,我们还需要一个模块加载的兼容策略,来避免重写所有的模块。
二、什么是 Webpack
本质上,webpack
是一个用于现代 JavaScript
应用程序的 静态模块打包工具。当 webpack
处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph
),然后将你项目中所需的每一个模块组合成一个或多个 bundles
,它们均为静态资源,用于展示你的内容。
2.1 入口(entry)
入口起点(entry point)
指示 webpack
应该使用哪个模块,来作为构建其内部 依赖图(dependency graph
) 的开始。进入入口起点后,webpack
会找出有哪些模块和库是入口起点(直接和间接)依赖的。
- 默认值是
./src/index.js
, - 但你可以通过在
webpack configuration
中配置 entry 属性,来指定一个(或多个)不同的入口起点。例如:在webpack.config.js
中,定义如下:
javascript
module.exports = {
entry: './path/to/my/entry/file.js',
};
2.2 输出(output)
output
属性告诉 webpack
在哪里输出它所创建的 bundle
,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js
,其他生成文件默认放置在 ./dist
文件夹中。
- 你可以通过在配置中指定一个
output
字段,来配置这些处理过程:在配置文件webpack.config.js
中,定义如下:
javascript
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
};
2.3 loader
webpack
只能理解 JavaScript
和 JSON
文件,这是 webpack
开箱可用的自带能力。loader
让 webpack
能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
webpack
的其中一个强大的特性就是能通过 import
导入任何类型的模块(例如.css
文件),其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是很有必要的,因为这可以使开发人员创建出更准确的依赖关系图。
在更高层面,在 webpack
的配置中,loader
有两个属性:
test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个loader
。
在webpack.config.js中,定义如下:
javascript
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',
},
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
以上配置中,对一个单独的 module
对象定义了 rules
属性,里面包含两个必须属性:test
和 use
。这告诉 webpack
编译器(compiler
) 如下信息:
"当你碰到「在 require()/import
语句中被解析为 '.txt'
的路径」时,在你对它打包之前,先 use
(使用) raw-loader
转换一下。"
2.4 插件(plugin)
loader
用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
想要使用一个插件,你只需要 require()
它,然后把它添加到 plugins
数组中。多数插件可以通过选项(option
)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new
操作符来创建一个插件实例。
在webpack.config.js中,定义如下:
javascript
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
2.5 模式(mode)
通过选择 development
, production
或 none
之中的一个,来设置 mode
参数,你可以启用 webpack
内置在相应环境下的优化。其默认值为 production
。
javascript
module.exports = {
mode: 'production',
};
2.6 浏览器兼容性(browser compatibility)
Webpack
支持所有符合 ES5
标准 的浏览器(不支持 IE8
及以下版本)。webpack
的 import()
和 require.ensure()
需要 Promise
。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载 polyfill
。
2.7 环境(environment)
Webpack 5
运行于 Node.js v10.13.0+
的版本。
三、模块系统的演进
3.1 原始方案
javascript
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="libraryA.js"></script>
<script src="module3.js"></script>
这是最原始的 JavaScript
文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window
对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口,典型的例子如 YUI
库。
这种原始的加载方式暴露了一些显而易见的弊端:
- 全局作用域下容易造成变量冲突
- 文件只能按照
<script>
的书写顺序进行加载 - 开发人员必须主观解决模块和代码库的依赖关系
- 在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪
3.2 CommonJS
服务器端的 Node.js
遵循 CommonJS
规范,该规范的核心思想是允许模块通过 require
方法来同步 加载所要依赖的其他模块,然后通过 exports
或 module.exports
来导出需要暴露的接口。
javascript
require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;
优点:
- 服务器端模块便于重用
- NPM 中已经有将近20万个可以使用模块包
- 简单并容易使用
缺点:
- 同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的
- 不能非阻塞的并行加载多个模块
3.3 AMD
AMD(Asynchronous Module Definition
)规范其实只有一个主要接口 define(id?, dependencies?, factory)
,它要在声明模块的时候指定所有的依赖 dependencies
,并且还要当做形参传到 factory
中,对于依赖的模块提前执行,依赖前置 。例如:RequireJS
javascript
define("module", ["dep1", "dep2"], function(d1, d2) {
return someExportedValue;
});
require(["module", "../file"], function(module, file) { /* ... */ });
优点:
- 适合在浏览器环境中异步加载模块
- 可以并行加载多个模块
缺点:
- 提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
- 不符合通用的模块化思维方式,是一种妥协的实现
3.4 CMD
CMD( Common Module Definition
)规范和 AMD
很相似,尽量保持简单,并与 CommonJS
和 Node.js
的 Modules
规范保持了很大的兼容性。例如:seajs
javascript
define(function(require, exports, module) {
var $ = require('jquery');
var Spinning = require('./spinning');
exports.doSomething = ...
module.exports = ...
})
优点:
- 依赖就近,延迟执行
- 可以很容易在 Node.js 中运行
缺点:
- 依赖 SPM 打包,模块的加载逻辑偏重
3.5 UMD
UMD(Universal Module Definition
)规范类似于兼容 CommonJS
和 AMD
的语法糖,是模块定义的跨平台解决方案。
3.6 ES6 模块
ECMAScript6
标准增加了 JavaScript
语言层面的模块体系定义。ES6
模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS
和 AMD
模块,都只能在运行时确定这些东西。
javascript
import "jquery";
export function doStuff() {}
module "localModule" {}