Webpack
是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过 loader
的转换,任何形式的资源都可以视作模块,比如 CommonJs
模块、 AMD
模块、 ES6
模块、CSS
、图片、 JSON
、Coffeescript
、 LESS
等。
一、背景
模块系统主要解决模块的定义、依赖和导出。我们所期望的模块系统:可以兼容多种模块风格,尽量可以利用已有的代码,不仅仅只是 JavaScript
模块化,还有 CSS、图片、字体等资源也需要模块化。
前端模块要在客户端中执行,所以他们需要增量加载到浏览器中。模块的加载和传输,我们首先能想到两种极端的方式:
显而易见,每个模块都发起单独的请求造成了请求次数过多,导致应用启动速度慢;一次请求加载所有模块导致流量浪费、初始化过程慢。这两种方式都不是好的解决方案,它们过于简单粗暴。
分块传输,按需进行懒加载,在实际用到某些模块的时候再增量更新,才是较为合理的模块加载方案。要实现模块的按需加载,就需要一个对整个代码库中的模块进行静态分析、编译打包的过程。
模块不仅仅是指JavaScript
模块文件。然而,在前端开发过程中还涉及到样式、图片、字体、HTML 模板等等众多的资源。这些资源还会以各种方言的形式存在,比如 coffeescript
、 less
、 sass
、众多的模板库、多语言系统(i18n
)等等。
如果他们都可以视作模块,并且都可以通过require
的方式来加载,将带来优雅的开发体验,比如:
require("./style.css");
require("./style.less");
require("./template.jade");
require("./image.png");
那么如何做到让 require
能加载各种资源呢?
在编译的时候,要对整个代码进行静态分析,分析出各个模块的类型和它们依赖关系,然后将不同类型的模块提交给适配的加载器来处理。比如一个用 LESS
写的样式模块,可以先用 LESS
加载器将它转成一个CSS
模块,在通过 CSS
模块把他插入到页面的 标签中执行。
Webpack
就是在这样的需求中应运而生。
同时,为了能利用已经存在的各种框架、库和已经写好的文件,我们还需要一个模块加载的兼容策略,来避免重写所有的模块。
二、什么是 Webpack
本质上,webpack
是一个用于现代 JavaScript
应用程序的 静态模块打包工具。当 webpack
处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph
),然后将你项目中所需的每一个模块组合成一个或多个 bundles
,它们均为静态资源,用于展示你的内容。
入口起点(entry point)
指示 webpack
应该使用哪个模块,来作为构建其内部 依赖图(dependency graph
) 的开始。进入入口起点后,webpack
会找出有哪些模块和库是入口起点(直接和间接)依赖的。
./src/index.js
,webpack configuration
中配置 entry 属性,来指定一个(或多个)不同的入口起点。例如:在webpack.config.js
中,定义如下:module.exports = {
entry: './path/to/my/entry/file.js',
};
output
属性告诉 webpack
在哪里输出它所创建的 bundle
,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js
,其他生成文件默认放置在 ./dist
文件夹中。
output
字段,来配置这些处理过程:在配置文件webpack.config.js
中,定义如下: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',
},
};
webpack
只能理解 JavaScript
和 JSON
文件,这是 webpack
开箱可用的自带能力。loader
让 webpack
能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
webpack
的其中一个强大的特性就是能通过 import
导入任何类型的模块(例如.css
文件),其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是很有必要的,因为这可以使开发人员创建出更准确的依赖关系图。
在更高层面,在 webpack
的配置中,loader
有两个属性:
test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个 loader
。在webpack.config.js中,定义如下:
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
转换一下。”
loader
用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
想要使用一个插件,你只需要 require()
它,然后把它添加到 plugins
数组中。多数插件可以通过选项(option
)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new
操作符来创建一个插件实例。
在webpack.config.js中,定义如下:
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' })],
};
通过选择 development
, production
或 none
之中的一个,来设置 mode
参数,你可以启用 webpack
内置在相应环境下的优化。其默认值为 production
。
module.exports = {
mode: 'production',
};
Webpack
支持所有符合 ES5
标准 的浏览器(不支持 IE8
及以下版本)。webpack
的 import()
和 require.ensure()
需要 Promise
。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载 polyfill
。
Webpack 5
运行于 Node.js v10.13.0+
的版本。
三、模块系统的演进
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="libraryA.js"></script>
<script src="module3.js"></script>
这是最原始的 JavaScript
文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window
对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口,典型的例子如 YUI
库。
这种原始的加载方式暴露了一些显而易见的弊端: