学习 Webpack
的原因 目前前端技术发展很快,引入了越来越多的思想、框架和工具 现阶段的大型应用就要求前端必须要有独立的项目,独立的项目想要有足够的效率就必须进行工程化。 具有复杂数据状态的应用开发过程就必须要有合适的框架,采用数据驱动开发的方式增强可维护性。 复杂项目结构必须进行模块化管理,一来提高部分公共内容的可复用性,二来增强团队并行协作能力。 重复规律性的工作必须采用自动化工具实现,一来提高效率,二来避免人为出错。 现代化前端开发工作,离不开 Webpack
相关技术栈,是提升前端生产力的利器 Webpack
在前端项目中实践模块化思想Webpack
本质上是一个模块化打包工具,它通过“万物皆模块”这种设计思想,巧妙地实现了整个前端项目的模块化。Webpack
本身的架构中有两个很核心的特性,分别是 Loader 机制和插件机制。前端项目中的任何资源都可以作为一个模块,任何模块都可以经过 Loader 机制的处理,最终再被打包到一起。 它的插件机制形成了非常繁荣的生态,所以造就了它现在“无所不能”的现状,所以让 Webpack
慢慢发展成了现在很多前端开发者眼中的构建系统。 Webpack
的设计思想比较先进,起初的使用过程比较烦琐,再加上文档也晦涩难懂,所以在最开始的时候,Webpack
对开发者并不友好,但是随着版本的迭代,官方文档的不断更新,目前 Webpack
对开发者已经非常友好了。Webpack
作为目前最主流的前端模块打包器,提供了一整套前端项目模块化方案,而不仅仅局限于对 JavaScript 的模块化。通过 Webpack
,我们可以轻松的对前端项目开发过程中涉及的所有资源进行模块化。做一个优秀的前端开发者 整体上对于 Webpack
的基本使用其实并不复杂,特别是在 Webpack 4
以后,很多配置都已经被简化了,在这种配置并不复杂的前提下,开发人员对它的掌握程度主要就体现在了是否能够理解它的工作机制和原理上了。 对 Webpack
这类工具的认知程度,是辨别前端开发人员优秀与否的分水岭。 很多前端开发者只是掌握技术的使用,而没有深入理解为什么这么设计,就很容易陷入“学不动”的状态。 其实通过探索会发现,当打开“黑盒子”后,里面的东西并没有想象的那么复杂,很多时候离成功就只有一步之遥,而驱使走向成功的是好奇心。好奇心应该是一个优秀开发者的基本素质,对待未知的好奇就是进步的源泉。 Webpack
和模块化模块化的演进过程 第一阶段 - 文件划分方式 最早是基于文件划分的方式实现模块化,也就是 Web 最原始的模块系统。 具体做法是将每个功能及其相关状态数据各自单独放到不同的 JS 文件中,约定每个文件是一个独立的模块。 使用某个模块将这个模块引入到页面中,一个 script 标签对应一个模块,然后直接调用模块中的成员(变量 / 函数)。 代码语言: javascript
复制 └─ stage-1
├── module-a.js
├── module-b.js
└── index.html
代码语言: javascript
复制 // module-a.js
function foo () {
console.log('moduleA#foo')
}
代码语言: javascript
复制 // module-b.js
var data = 'something'
代码语言: javascript
复制 <!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>第一阶段</title>
</head>
<body>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
foo()
// console.log(data)
data = 'other'
</script>
</body>
</html>
缺点:模块直接在全局工作,大量模块成员污染全局作用域; 没有私有空间,所有模块内的成员都可以在模块外部被访问或者修改; 一旦模块增多,容易产生命名冲突; 无法管理模块与模块之间的依赖关系; 在维护的过程中也很难分辨每个成员所属的模块。 第二阶段 - 命名空间方式 每个模块只暴露一个全局对象,所有模块成员都挂载到这个全局对象中。 具体做法是在第一阶段的基础上,通过将每个模块“包裹”为一个全局对象的形式实现,这种方式就好像是为模块内的成员添加了“命名空间”,所以又称之为命名空间方式。 代码语言: javascript
复制 // module-a.js
window.moduleA = {
method1: function () {
// console.log('moduleA#method1')
}
}
代码语言: javascript
复制 // module-b.js
window.moduleB = {
data: 'something'
method1: function () {
// console.log('moduleB#method1')
}
}
代码语言: javascript
复制 <!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>第二阶段</title>
</head>
<body>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
moduleA.data = 'foo'
</script>
</body>
</html>
这个阶段只是解决了命名冲突的问题,但是其它问题依旧存在。 第三阶段 - 立即执行函数表达式 使用立即执行函数表达式(Immediately-Invoked Function Expression, IIFE)为模块提供私有空间,带来了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。 具体做法是将每个模块成员都放在一个立即执行函数所形成的私有作用域中,对于需要暴露给外部的成员,通过挂到全局对象上的方式实现。 代码语言: javascript
复制 // module-a.js
;(function () {
var name = 'module-a'
function method1 () {
console.log(name '#method1')
}
window.moduleA = {
method1: method1
}
})()
代码语言: javascript
复制 // module-b.js
;(function () {
var name = 'module-b'
function method1 () {
console.log(name '#method1')
}
window.moduleB = {
method1: method1
}
})()
这个阶段解决了前面所提到的全局作用域污染和命名冲突的问题,但是仍有其它问题。 第四阶段 - IIFE 依赖参数 在 IIFE 的基础之上,利用 IIFE 参数作为依赖声明使用,这使得每一个模块之间的依赖关系变得更加明显。 代码语言: javascript
复制 // module-a.js
;(function ($) { // 通过参数明显表明这个模块的依赖
var name = 'module-a'
function method1 () {
console.log(name '#method1')
$('body').animate({ margin: '200px' })
}
window.moduleA = {
method1: method1
}
})(jQuery)
模块加载的问题 代码语言: javascript
复制 <!DOCTYPE html>
<html>
<head>
<title>模块化的演进过程</title>
</head>
<body>
<script src="https://unpkg.com/jquery"></script>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
</script>
</body>
</html>
以上 4 个阶段是早期的开发者在没有工具和规范的情况下对模块化的落地方式,解决了模块代码的组织问题,但模块加载的问题却被忽略了。通过 script 标签的方式直接在页面中引入的这些模块,这意味着模块的加载并不受代码的控制,时间久了维护起来会十分麻烦。 比如,代码需要用到某个模块,如果 HTML 中忘记引入这个模块,又或是代码中移除了某个模块的使用,而 HTML 还忘记删除该模块的引用,都会引起很多问题和不必要的麻烦。 更为理想的方式应该是在页面中引入一个 JS 入口文件,其余用到的模块可以通过代码控制,按需加载进来。 模块化规范的出现 不同的开发者在实施模块化的过程中会出现一些差别,所以为了统一不同开发者、不同项目之间的差异,就需要制定一个行业标准去规范模块化的实现方式。 接合模块加载的问题,需求有两点:一个统一的模块化标准规范 一个可以自动加载模块的基础库 Node.js 的模块加载机制的 CommonJS
规范是以同步的方式加载模块。Node.js 主要用于服务器编程,模块一般都是存在本地硬盘中,加载比较快。 如果要在浏览器端使用同步的加载模式,就会引起大量的同步模式请求,导致应用运行效率低下。 在早期制定前端模块化标准时,并没有直接选择 CommonJS
规范,而是专门为浏览器端重新设计了一个规范,叫做 AMD ( Asynchronous Module Definition) 规范,即异步模块定义规范。目前绝大多数第三方库都支持 AMD 规范,但是它使用起来相对复杂,而且当项目中模块划分过于细致时,就会出现同一个页面对 js 文件的请求次数过多的情况,从而导致效率降低。 在当时的环境背景下,AMD 规范为前端模块化提供了一个标准,但这只是一种妥协的实现方式,并不能成为最终的解决方案。 同期出现的规范还有淘宝的 Sea.js,只不过它实现的是另外一个标准,叫作 CMD。CMD 标准类似于 CommonJS
,在使用上基本和 AMD 相同,可以算上是重复的轮子。 尽管上面介绍的这些方式和标准都已经实现了模块化,但是都仍然存在一些让开发者难以接受的问题。 模块化的标准规范 如今的前端模块化已经发展得非常成熟了,而且对前端模块化规范的最佳实践方式也基本实现了统一:在 Node.js 环境中,我们遵循 CommonJS
规范来组织模块。 在浏览器环境中,我们遵循 ES Modules 规范。 在最新的 Node.js 提案中表示,Node 环境也会逐渐趋向于 ES Modules 规范,也就是说作为现阶段的前端开发者,应该重点掌握 ES Modules 规范。目前 ES Modules 已发展成为现今最主流的前端模块化标准。绝大多数浏览器都已经开始能够原生支持 ES Modules 这个特性了,所以说在未来几年,它还会有更好的发展,短期内应该不会有新的轮子出现了。 ES Modules 特性 ES Modules 的学习 首先,需要了解它作为一个规范标准,到底约定了哪些特性和语法; 其次,需要学习如何通过一些工具和方案去解决运行环境兼容带来的问题。 模块打包工具的出现 随着模块化思想的引入,我们的前端应用又会产生了一些新的问题,比如: ES Modules 模块系统本身就存在环境兼容问题。 模块化的方式划分出来的模块文件过多,而前端应用又运行在浏览器中,每一个文件都需要单独从服务器请求回来。零散的模块文件必然会导致浏览器的频繁发送网络请求,影响应用的工作效率。 随着应用日益复杂,在前端应用开发过程中不仅仅只有 JavaScript 代码需要模块化,HTML 和 CSS 这些资源文件也会面临需要被模块化的问题。 为了解决上面提出的 3 个问题,需要引入模块打包工具: 第一,它需要具备编译代码的能力,也就是将我们开发阶段编写的那些包含新特性的代码转换为能够兼容大多数环境的代码,解决我们所面临的环境兼容问题。 第二,它能够将散落的模块再打包到一起,这样就解决了浏览器频繁请求模块文件的问题。这里需要注意,只是在开发阶段才需要模块化的文件划分,因为它能够帮我们更好地组织代码,到了实际运行阶段,这种划分就没有必要了。 第三,它需要支持不同种类的前端模块类型,也就是说可以将开发过程中涉及的样式、图片、字体等所有资源文件都作为模块使用,这样我们就拥有了一个统一的模块化方案,所有资源文件的加载都可以通过代码控制,与业务代码统一维护,更为合理。 针对上面的三个设想: 第一和二个设想可以借助 Gulp 之类的构建系统配合一些编译工具和插件去实现。第三个设想就很难通过这种方式去解决了。 目前,前端领域有一些工具能够很好的满足以上这 3 个设想,其中最为主流的就是 Webpack
:Webpack
作为一个模块打包工具,本身就可以解决模块化代码打包的问题,将零散的 JavaScript 代码打包到一个 JS 文件中。对于有环境兼容问题的代码,Webpack
可以在打包过程中通过 Loader 机制对其实现编译转换,然后再进行打包。 对于不同类型的前端模块类型,Webpack
支持在 JavaScript 中以模块化的方式载入任意类型的资源文件,例如,我们可以通过 Webpack
实现在 JavaScript 中加载 CSS 文件,被加载的 CSS 文件将会通过 style 标签的方式工作。 使用 Webpack
Webpack
作为模块化打包工具模块化打包工具让我们可以在开发阶段更好的享受模块化带来的优势,同时又不必担心模块化在生产环境中产生新的问题。 除此之外,Webpack
还具备代码拆分的能力,它能够将应用中所有的模块按照我们的需要分块打包。这样就不用担心全部代码打包到一起,产生单个文件过大,导致加载慢的问题。我们可以把应用初次加载所必需的模块打包到一起,其他的模块再单独打包,等到应用工作过程中实际需要用到某个模块,再异步加载该模块,实现增量加载,或者叫作渐进式加载,非常适合现代化的大型 Web 应用。 Webpack
快速上手案例 代码语言: javascript
复制 └─ 02-configuation
├── src
│ ├── heading.js
│ └── index.js
└── index.html
代码语言: javascript
复制 // ./src/heading.js
export default () => {
const element = document.createElement('h2')
element.textContent = 'Hello webpack'
element.addEventListener('click', () => alert('Hello webpack'))
return element
}
代码语言: javascript
复制 // ./src/index.js
import createHeading from './heading.js'
const heading = createHeading()
document.body.append(heading)
代码语言: javascript
复制 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack - 快速上手</title>
</head>
<body>
<!- type="module" 表示是一个模块 ->
<script type="module" src="src/index.js"></script>
</body>
</html>
heading.js 中以 ES Modules 的方式导出了一个创建元素的函数,然后在 index.js 中导入 heading.js 并使用了这个模块,最后在 html 文件中通过 script 标签,以模块化的方式引入了 index.js。 按照 ES Modules 的标准,这里的 index.html 可以直接在浏览器中正常工作,但是对于不支持 ES Modules 标准的浏览器,直接使用就会出现错误,所以需要使用 Webpack
这样的工具,将拆分的 JS 代码再次打包到一起。 引入 Webpack
去处理上述案例中的 JS 模块打包。 由于 Webpack 是一个 npm 工具模块,所以先初始化一个 package.json 文件,用来管理 npm 依赖版本,完成之后,再来安装 Webpack 的核心模块以及它的 CLI 模块,具体操作如下: npm init --yes npm i webpack webpack-cli --save-dev webpack
是 Webpack
的核心模块,webpack-cli
是 Webpack
的 CLI 程序,用来在命令行中调用 Webpack
。安装完成之后,webpack-cli
所提供的 CLI 程序就会出现在 node_modules/.bin
目录当中,我们可以通过 npx
快速找到 CLI 并运行它,具体操作如下:
$ npx webpack --version v4.42.1 npx
是 npm 5.2
以后新增的一个命令,可以用来更方便的执行远程模块或者项目 node_modules 中的 CLI 程序。这里我们使用的 Webpack
版本是 v4.42.1
,有了 Webpack
后,就可以直接运行 webpack
命令来打包 JS 模块代码,具体操作如下:
$ npx webpack 这个命令在执行的过程中,Webpack
会自动从 src/index.js
文件开始打包,然后根据代码中的模块导入操作,自动将所有用到的模块代码打包到一起。 完成之后,控制台会提示:顺着 index.js
有两个 JS 文件被打包到了一起。与之对应的就是项目的根目录下多出了一个 dist
目录,我们的打包结果就存放在这个目录下的 main.js
文件中。 回到 index.html
中修改引入文件的路径,由于打包后的代码就不会再有 import 和 export 了,所以我们可以删除 type="module"
。再次回到浏览器中,查看这个页面,这时我们的代码仍然可以正常工作,index.html
的代码如下所示: 代码语言: javascript
复制 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack - 快速上手</title>
</head>
<body>
<script src="dist/main.js"></script>
</body>
</html>
也可以将 Webpack
命令定义到 npm scripts
中,这样每次使用起来会更加方便,具体如下: 代码语言: javascript
复制 {
"name": "01-getting-started",
"version": "0.1.0",
"main": "n/a",
"author": "zce <w@zce.me> (https://zce.me)",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11"
}
}
Webpack
最基本的使用的总结 先安装 webpack
相关的 npm
包,然后使用 webpack-cli
所提供的命令行工具进行打包。 配置 Webpack
的打包过程 基本配置介绍 Webpack 4
以后的版本支持零配置的方式直接启动打包,整个过程会按照约定将 src/index.js
作为打包入口,最终打包的结果会存放到 dist/main.js
中。自定义路径约定。例如,如果需要它的打包入口为 src/main.js
,那此时我们通过配置文件的方式修改 Webpack
的默认配置,在项目的根目录下添加一个 webpack.config.js
,具体结构如下: 代码语言: javascript
复制 └─ 02-configuation
├── src
│ ├── heading.js
│ └── main.js
├── index.html
├── package.json
└── webpack.config.js ···················· Webpack 配置文件
webpack.config.js
是一个运行在 Node.js
环境中的 JS 文件,也就是说我们需要按照 CommonJS
的方式编写代码,这个文件可以导出一个对象,我们可以通过所导出对象的属性完成相应的配置选项。这里先加一个 entry 属性,这个属性的作用就是指定 Webpack
打包的入口文件路径。我们将其设置为 src/main.js
,具体代码如下所示: 代码语言: javascript
复制 // ./webpack.config.js
module.exports = {
entry: './src/main.js'
}
配置完成之后,回到命令行终端重新运行打包命令,此时 Webpack
就会从 src/main.js
文件开始打包。 除了 entry
的配置以外,还可以通过 output
属性设置输出文件的位置。output
属性的值必须是一个对象,通过这个对象的 filename
指定输出文件的文件名称,path
指定输出的目录,具体代码如下所示: 代码语言: javascript
复制 // ./webpack.config.js
const path = require('path')
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
}
}
webpack.config.js
是运行在 Node.js
环境中的代码,所以直接可以使用 path 之类的 Node.js
内置模块。更多 Webpack
相关的配置可以在 Webpack
的官网中找到:https://webpack.js.org/configuration/#options 让配置文件支持智能提示 因为 Webpack
的配置项比较多,而且很多选项都支持不同类型的配置方式。如果开发工具能够为 Webpack
配置文件提供智能提示的话,配置效率和准确度也会大大提高。 VSCode
对于代码的自动提示是根据成员的类型推断出来的,换句话说,如果 VSCode
知道当前变量的类型,就可以给出正确的智能提示。即便你没有使用 TypeScript
这种类型友好的语言,也可以通过类型注释的方式去标注变量的类型。默认 VSCode
并不知道 Webpack
配置对象的类型,我们通过 import
的方式导入 Webpack
模块中的 Configuration
类型,然后根据类型注释的方式将变量标注为这个类型,这样我们在编写这个对象的内部结构时就可以有正确的智能提示了,具体代码如下所示: 代码语言: javascript
复制 // ./webpack.config.js
import { Configuration } from 'webpack'
/**
* @type {Configuration}
*/
const config = {
entry: './src/index.js',
output: {
filename: 'bundle.js'
}
}
module.exports = config
需要注意的是:添加的 import
语句只是为了导入 Webpack
配置对象的类型,这样做的目的是为了标注 config
对象的类型,从而实现智能提示。在配置完成后一定要记得注释掉这段辅助代码,因为在 Node.js
环境中默认还不支持 import
语句,如果执行这段代码会出现错误。 代码语言: javascript
复制 // ./webpack.config.js
// 一定记得运行 Webpack 前先注释掉这里。
// import { Configuration } from 'webpack'
/**
* @type {Configuration}
*/
const config = {
entry: './src/index.js',
output: {
filename: 'bundle.js'
}
}
module.exports = config
Webpack
工作模式Webpack 4
新增了一个工作模式的用法,这种用法大大简化了 Webpack
配置的复杂程度。你可以把它理解为针对不同环境的几组预设配置:
production 模式下,启动内置优化插件,自动优化打包结果,打包速度偏慢; development 模式下,自动优化打包速度,添加一些调试过程中的辅助插件; none 模式下,运行最原始的打包,不做任何额外处理。针对工作模式的选项,如果你没有配置一个明确的值,打包过程中命令行终端会打印一个对应的配置警告。在这种情况下 Webpack
将默认使用 production 模式
去工作。 production 模式
下 Webpack
内部会自动启动一些优化插件,例如,自动压缩打包后的代码。这对实际生产环境是非常友好的,但是打包的结果就无法阅读了。修改 Webpack
工作模式的方式有两种:
通过 CLI --mode 参数传入; 通过配置文件设置 mode 属性。 上述三种 Webpack
工作模式的详细差异可以在官方文档中查看:https://webpack.js.org/configuration/mode/ 打包结果运行原理 通过学习 Webpack
打包后生成的 bundle.js 文件,深入了解 Webpack
是如何把这些模块合并到一起,而且还能正常工作的。 为了更好的理解打包后的代码,先将 Webpack
工作模式设置为 none
,这样 Webpack
就会按照最原始的状态进行打包,所得到的结果更容易理解和阅读。 按照 none
模式打包完成后,打开最终生成的 bundle.js
文件。 然后通过分析生成的 bundle.js
文件来深入深入了解 Webpack
。 建议:为了更好的理解 bundle.js
的执行过程,可以把它运行到浏览器中,然后通过 Chrome
的 Devtools
单步调试一下。