- 1. 前言
- 2. JS模块化编程相关背景
- 3. 什么是webpack?
- 4. webpack的安装
- 5. webpack的使用
- 6. webpack中的重要工具:webpack-dev-server
- 7. 模块热替换 Hot Module Replacement 的应用
前言
webpack是一款极为灵活的前端模块打包构建工具。其诞生和流行主要是由于以下这几个发展趋势:
- 为了完成更加复杂的交互逻辑,一个网页中使用的js代码数量越来越多;
- 即便有了更多代码,却要求使用中有更少的整页面重新加载;
- 现代浏览器提供了更多的API接口。
基于这样的情况,大量代码就需要能够被良好地管理起来,像webpack这样的模块系统就为我们将代码切分为若干模块提供了很好的选择。本篇在webstorm编辑器中构建了一个最简单的使用webpack管理的前端项目,算是对webpack使用的入门探究。
JS模块化编程相关背景
在前端开发中有很多定义模块依赖的方法:
- 在html页面中使用
<script>
标签手动引入(这并没有使用模块管理系统)
在这种方式下,各模块是将接口暴露给了全局对象,比如window。常见问题:
- 全局对象中的命名变量冲突
- 各js文件加载顺序十分重要,一旦A依赖B而A又在B之前引入,则会报错
- 需要开发者自己来维护和解决依赖的问题
- 在大型项目中可能需要引入数十个js文件,代码冗长,难于管理
- 编写符合CommonJS规范的代码
这种规范使用同步的require
方法加载依赖,并返回一个暴露出的接口,一个模块可以通过给export
对象指定属性的方式来指定输出:1
2
3
4require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;
这种方式主要用在服务端,比如Node.js。
编写符合AMD规范的代码
这种规范使用异步的require
方法加载依赖,主要适用于客户端,因为在客户端如果同步加载的话会阻塞页面渲染等其他内容的进行,造成假死。1
2
3
4require(["module", "../file"], function(module, file) { /* ... */ });
define("mymodule", ["dep1", "dep2"], function(d1, d2) {
return someExportedValue;
});使用ES6标准原生模块
ECMAScript 2015 (6th Edition)给javascript语言增加了新的内容-import关键字,从而构成了另一个模块系统1
2
3import "jquery";
export function doStuff() {}
module "localModule" {}
什么是webpack?
一句话来概括:webpack能够将相互依赖的模块进行打包,并生成代表着这些模块的静态文件。
官网上给出的示意图如下:
市面上已经存在的模块管理和打包工具并不适合大型的项目,尤其单页面Web应用程序。最紧迫的原因是如何在一个大规模的代码库中,维护各种模块资源的分割和存放,维护它们之间的依赖关系,并且无缝的将它们整合到一起生成适合浏览器端请求加载的静态资源。这些已有的模块化工具并不能很好的完成如下的目标:
- 将依赖树拆分成按需加载的块
- 初始化加载的耗时尽量少
- 各种静态资源都可以视作模块
- 将第三方库整合成模块的能力
- 可以自定义打包逻辑的能力
- 适合大项目,无论是单页还是多页的Web应用
webpack有以下明显特点:
- 代码拆分
Webpack 有两种组织模块依赖的方式,同步和异步。异步依赖作为分割点,形成一个新的块。在优化了依赖树后,每一个异步区块都作为一个文件被打包。 - Loader
Webpack 本身只能处理原生的 JavaScript 模块,但是 loader 转换器可以将各种类型的资源转换成 JavaScript 模块。这样,任何资源都可以成为 Webpack 可以处理的模块。 - 智能解析
Webpack 有一个智能解析器,几乎可以处理任何第三方库,无论它们的模块形式是 CommonJS、 AMD 还是普通的 JS 文件。甚至在加载依赖的时候,允许使用动态表达式 require(“./templates/“ + name + “.jade”)。 - 插件系统
Webpack 还有一个功能丰富的插件系统。大多数内容功能都是基于这个插件系统运行的,还可以开发和使用开源的 Webpack 插件,来满足各式各样的需求。 - 快速运行
Webpack 使用异步 I/O 和多级缓存提高运行效率,这使得 Webpack 能够以令人难以置信的速度快速增量编译。
webpack的安装
1.安装Node.js
关于Node.js的安装有很多参考,在之前的文章【首篇】记录本博客www.gcidea.info的搭建过程也有提及。
2.安装webpack
2.1 打开webstorm,创建一个新文件夹webpack-usage作为根目录,后续所有操作都在该项目中完成:
2.2 Node.js安装完成后,会同时安装npm这个Node.js的包管理工具。我们使用这个工具全局安装webpack。打开Terminal窗口输入如下命令:$ npm install webpack -g
可以看到安装过程正在进行:
完成后,显示信息如下:
3.在项目中添加webpack
根据最佳实践,最好也在具体的项目中添加webpack。这样的话你可以任意指定在该项目中使用的webpack版本,而不至于局限于使用全局的唯一版本。
3.1 为此,通过命令在项目根目录下创建package.json文件,将项目通过npm管理起来:npm init
这个过程中需要填写一些信息,最终会写入package.json:
之后,项目根目录下多了一个文件,正是package.json:
3.2 此时,再通过如下命令安装webpack,其中--save-dev
表示会将webpack添加到package.json的devDependencies中:npm install webpack --save-dev
并且,该命令会自动检查安装webpack所需的全部依赖,执行该命令后,项目有3处变化:控制台打印出依赖结构树;根目录下生成node_modules文件夹用于存放所有依赖包;package.json中多出一个devDependencies属性。如下图所示:
4.安装webpack-dev-server
这是一个Node.js Express server,可以帮我们在本地建立一个服务器。此处仅安装,具体使用在下文描述。npm install webpack-dev-server --save-dev
可以预想,现在package.json文件的依赖项已经变为:1
2
3
4"devDependencies": {
"webpack": "^1.13.2",
"webpack-dev-server": "^1.16.2"
}
webpack的使用
简单示例
1.新建cats.js文件1
2var cats = ['dave', 'henry', 'martha'];
module.exports = cats;
2.新建app.js文件1
2cats = require('./cats.js');
console.log(cats);
3.使用webpack命令进行打包webpack ./app.js app.bundle.js
结果如下:
可以发现webpack已经帮我们将两个文件合并在一起并加入很多其他内容,这个过程在官方上详细描述如下:
现在,假如我们要将项目部署上线,那么只需要打包后的静态文件即可,实验如下:
可以看出,在node环境下执行打包文件,可以正常输出源文件的执行结果。
规范使用
项目结构
现在我们已经具备基本的webpack环境且大致了解了webpack的打包使用。为了规范使用,删除app.bundle.js,app.js,cat.js,重新规定项目目录结构如下:
- src:存放源码
- build: 打包后静态文件目录
- node_modules: node模块依赖目录
- webpack.config.js:webpack配置目录–使用命令行携带参数的方式不便于扩展,统一用此文件管理
- index.html: 项目入口页面
修改后的项目结构如下:
文件配置
配置webpack.config.js1
2
3
4
5
6
7module.exports = {
entry: './src/app.js',
output: {
path: './build',
filename: 'app.bundle.js'
}
};
以上简单的配置表示指定webpack打包入口为./src/app.js文件,打包结果为app.bundle.js,在./build目录下。
该文件还有很多配置项,详见:http://webpack.github.io/docs/configuration.html
现在,只需要简单的命令webpack
而不需要参数,就可以执行打包。
使用loaders
webpack只能对原生javascript文件打包,但是通过各种强大的loaders,我们可以处理各种类型的文件。Loader 可以理解为是模块和资源的转换器,它本身是一个函数,接受源文件作为参数,返回转换的结果。这样,我们就可以通过 require 来加载任何类型的模块或文件,比如 CoffeeScript、 JSX、 LESS 或图片。
loaders具备以下特性:
- Loader 可以通过管道方式链式调用,每个 loader 可以把资源转换成任意格式并传递给下一个
loader ,但是最后一个 loader 必须返回 JavaScript。 - Loader 可以同步或异步执行。
- Loader 运行在 node.js 环境中,所以可以做任何可能的事情。
- Loader 可以接受参数,以此来传递配置项给 loader。
- Loader 可以通过文件扩展名(或正则表达式)绑定给不同类型的文件。
- Loader 可以通过 npm 发布和安装。
为了使浏览器能够解析,使用ES6语法编写的代码需要使用babel进行转码,为此我们安装babel-loader。
1.安装Babel 和 presetsnpm install --save-dev babel-core babel-preset-es2015
2.安装babel-loadernpm install --save-dev babel-loader
3.在根目录创建Babel配置文件—.babelrc
内容为: { "presets": [ "es2015" ] }
4.修改webpack.config.js如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14module.exports = {
entry: './src/app.js',
output: {
path: './build',
filename: 'app.bundle.js',
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
}
}
安装其他可能需要的库
npm install --save jquery babel-polyfill
--save
而非--save-dev
表示库将会在运行环境使用--save-dev
的内容将会放在package.json 的devDependencies
而--save
会放在dependencies里面
产品模式用dependencies,开发模式用devDependencies
因此将会实际运行在用户端浏览器环境中的库就要采用--save
安装
通过使用babel-polyfill可以让老版本浏览器能够使用ES6新提供的API
修改app.js文件(ES6语法+jquery库)
1 | import 'babel-polyfill'; |
将以上代码写入后,编辑器会报错:
原因是编辑器未能识别ES6语法,在webstorm的settings中做如下配置即可:
编辑index.html文件
1 |
|
重新打包
webpack
访问index.html
可以看到正确结果
使用plugins
在打包的工作流中可能还会有一些其他的操作,会使用到不同的插件plugins,比较典型的就是uglify plugin这个压缩工具。为此,在webpack.config.js中增加配置项plugins:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26const webpack = require('webpack');
module.exports = {
entry: './src/app.js',
output: {
path: './build',
filename: 'app.bundle.js',
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
},
output: {
comments: false,
},
}),
]
}
在执行命令webpack
重新打包之前,我们先看看app.bundle.js的文件大小,为523KB:
现在执行命令webpack
重新打包,再看app.bundle.js的文件大小,为171KB:
可以看到,压缩工具去掉了无用的空字符等:
webpack中的重要工具:webpack-dev-server
webpack-dev-server本质上是一个小型的Node.js的Express服务器,使用webpack-dev-middleware作为中间件来支持webpack打包。
安装webpack-dev-server
npm install webpack-dev-server
package.json变为如下形式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22{
"name": "webpack-usage",
"version": "1.0.0",
"description": "webpack-usage-demo",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "gaochang",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.17.0",
"babel-loader": "^6.2.5",
"babel-preset-es2015": "^6.16.0",
"webpack": "^1.13.2",
"webpack-dev-server": "^1.16.2"
},
"dependencies": {
"babel-polyfill": "^6.16.0",
"jquery": "^3.1.1"
}
}
启动webpack-dev-server
安装完成后,在不指定任何参数的情况下,可以直接通过命令webpack-dev-server
在本地8080端口运行一个服务器,访问http://localhost:8080
可以看到效果:
访问http://localhost:8080/webpack-dev-server/
可以显示webpack服务器状态条
指定content base
webpack-dev-server默认监控的是当前目录的文件,除非声明一个特殊的基本路径:webpack-dev-server --content-base build/
这样的话,webpack就会只监控build文件夹下的静态文件。当你的文件在任何时候发生变化时,都会触发重新编译打包。验证如下:
1.使用上述命令启动server:
控制台会显示打包过程日志,并最终提示打包完成已经可用:
2.此时,访问http://localhost:8080/webpack-dev-server/
可看到:
即显示了build目录下的文件列表,此时只有app.bundle.js,点开内容如下:
红色框中是我们app.js中的部分源码。
3.现在修改上述部分源码,观察控制台日志和http://localhost:8080/webpack-dev-server/
目录下文件变化。
修改<h1>
内容后,发现控制台日志在之前“bundle is now VALID”的基础上,自动开始重新编译,进入“bundle is now INVALID”的状态:
同时,看到页面状态条变为:
等待重编译完成后,发现页面文件内容确实变为我们修改后的内容:
但是,值得注意的是,这个修改的内容是保存在内存中的,并没有写入我们在webpack.config.js的output中指定的文件:
发现文件还是修改之前的内容。也就是说,当相同URL下已经有一个打包的文件时,默认情况下内存中的文件享有优先权。而如果想要让build目录下的输出文件的内容也跟随发生变化,则要使用webpack
命令重新打包。
除了以上分析,还有一点要注意,通过给webpack.config.js的output指定publicPath,可以定制浏览器中的项目访问路径。1
2
3
4
5output: {
path: './build',
publicPath: "/assets/",
filename: 'app.bundle.js',
}
重新打包,可以在如下路径访问相应文件:
由于之前content-base指定了监控build目录,我们在该目录下创建一个index.html文件,用来加载并使用我们打包好的js文件,而原先在项目根目录webpack-usage下创建的index.html文件就不起作用了:1
2
3
4
5
6
7
8
9
10
11
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<p>在build目录下创建的index.html文件</p>
<script src="assets/app.bundle.js"></script>
</body>
</html>
访问http://localhost:8080/
:
访问http://localhost:8080/webpack-dev-server
:
其中,第一个是打包文件app.bundle.js本身:
注意路径为http://localhost:8080/assets/app.bundle.js
,符合publicPath的配置。
第二个是根据打包文件生成的html文件,注意路径为http://localhost:8080/assets/app.bundle
,符合publicPath的配置
点击“app.bundle”:
点击“webpack-dev-server”:
(该html文件仅针对app.bundle.js,和直接访问http://localhost:8080/
对应的index.html并不相同,以上两图中没有“在build目录下创建的index.html文件”这句话就说明了这一点。)
关于js文件引入的路径,经测试发现以下几种情况,应该与webpack的文件查找算法有关,具体待进一步探究:
1.
src="assets/app.bundle.js"
由于publicPath指定了”/assets/“,因此这种方式肯定能找到app.bundle.js文件;2.
src="app.bundle.js"
默认在build目录下找该文件,能找到3.
src="/app.bundle.js"
默认在build目录下找该文件,能找到4.
src="./app.bundle.js"
默认在build目录下找该文件,能找到5.
src="../app.bundle.js"
默认在build目录下找该文件,能找到
自动刷新
webpack-dev-server支持两种自动刷新页面的方式。并且两种方式都支持模块热替换。
iframe模式
在这种方式下,页面实际上是被嵌入了一个iframe,使用这种方式不需要额外配置,只要js文件以改变,就会自动”recompiling”。直接访问http://«host»:«port»/webpack-dev-server/«path»
,在我们的配置中就是访问http://localhost:8080/webpack-dev-server/index.html
。该方式的特点是:
- 无需改变配置
- 编译信息通过页面顶部信息条体现
- 浏览器地址栏不会体现出地址的变化
inline模式
在这种方式下,我们需要额外的配置,使用以下两种方式之一:
- 在启动命令上加参数”–inline”:
webpack-dev-server --content-base build/ --inline
- 在webpack.config.js加上
devServer: { inline: true }
使用这种方式不需要给URL加上/webpack-dev-server/,直接访问http://«host»:«port»/«path»
,在我们的配置中就是访问http://localhost:8080/index.html
该方式的特点是:
- 需要改变配置
- 编译信息通过浏览器的console体现:
- 浏览器地址栏会体现出地址的变化
模块热替换 Hot Module Replacement 的应用
在webpack-dev-server中使用模块热替换 Hot Module Replacement最简单的办法就是在inline模式中使用。我们只需要在参数中加上”–hot”即可。webpack-dev-server --content-base build/ --inline --hot
这样,就进入了监听状态,当文件发生修改,看到console打出如下日志:
发现提示错误,热替换失败,整个页面进行了重载:1
2
3
4
5
6
7[HMR] Cannot apply update. Need to do a full reload!
[HMR] Error: Aborted because 77 is not accepted
at l (http://localhost:8080/assets/app.bundle.js:1:4169)
at f (http://localhost:8080/assets/app.bundle.js:1:3117)
at u (http://localhost:8080/assets/app.bundle.js:1:3017)
at webpackHotUpdate (http://localhost:8080/assets/app.bundle.js:1:5790)
at http://localhost:8080/assets/0.ae586ae503d4fe64ca9a.hot-update.js:1:1
查阅了很多资料,如https://github.com/webpack/webpack-dev-server/issues/395,但并没有找到准确原因,暂时搁置。