1.4 打包第一个应用
现在让我们趁热打铁来打包刚刚的示例工程。如果你是第一次接触Webpack,建议按照下面的指引一步步进行操作。代码中不熟悉的地方也不必深究,这个示例只是为了让我们直观地认识Webpack的一些特性。
1.4.1 Hello World
首先,我们在工程目录下添加以下几个文件。
index.js:
import addContent from './add-content.js'; document.write('My first Webpack app.<br />'); addContent();
add-content.js:
export default function() { document.write('Hello world!'); }
index.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>My first Webpack app.</title> </head> <body> <script src="./main.js"></script> </body> </html>
然后在控制台输入打包命令:
npx webpack --entry=./index.js --mode=development
用浏览器打开index.html,这时应该可以看到在页面上会显示“My first Webpack app. Hello world!”,如图1-1所示。
图1-1 index.html输出结果
Webpack帮我们完成了一项最基本的模块组装工作,现在回顾一下刚刚输入的指令。
命令行的第1个参数entry是资源打包的入口。Webpack从这里开始进行模块依赖的查找,找到index.js和add-content.js两个模块,并通过它们来生成最终产物。
命令行的第2个参数output-filename是输出资源名。你会发现打包完成后工程中出现了一个dist目录,其中包含的main.js就是Webpack的打包结果。
最后的参数mode指的是打包模式。Webpack为开发者提供了development、production、none三种模式。当置于development和production模式下时,它会自动添加适合当前模式的一系列配置,减少了人为的工作量。在开发环境下,一般设置为development模式就可以了。
为了验证打包结果,可以用浏览器打开index.html。项目中的index.js和content.js现在已经成为bundle.js,被页面加载和执行,并输出了各自的内容。
1.4.2 使用npm scripts
从上面的例子不难发现,我们每进行一次打包都要输入一段冗长的命令,这样做不仅耗时,而且容易出错。为了使命令行指令更简洁,可以在package.json中添加一个脚本命令。
编辑工程中的package.json文件:
…… "scripts": { "build": "webpack --entry=./index.js --mode=development" }, ……
scripts是npm提供的脚本命令功能,在这里我们可以直接使用由模块添加的指令(比如用webpack取代之前的npx webpack)。
为了验证打包结果,可以对add-content.js的内容稍加修改:
export default function() { document.write('I\'m using npm scripts!'); }
重新打包,这次输入npm命令即可:
npm run build
打开浏览器验证效果,如图1-2所示。
图1-2 index.html内容变为“I'm using npm scripts!”
1.4.3 使用默认目录配置
上面的index.js是放在工程根目录下的,而通常情况下我们会分别设置源码目录与资源输出目录。工程源代码放在/src中,输出资源放在/dist中。本书后续章节的示例也会按照该标准进行目录划分。
在工程中创建一个src目录,并将index.js和add-content.js移动到该目录下。对于资源输出目录来说,Webpack已经默认是/dist,我们不需要做任何改动。
另外需要提到的是,Webpack默认的源代码入口就是src/index.js,因此现在可以省略entry的配置了。编辑package.json:
…… "scripts": { "build": "webpack --output-filename=bundle.js --mode=development" }, ……
虽然目录命名并不是强制的,且Webpack提供了配置项让我们进行更改,但还是建议遵循统一的命名规范,这样会使得大体结构比较清晰,也利于多人协作。
1.4.4 使用配置文件
为了满足不同应用场景的需求,Webpack拥有非常多的配置项以及相对应的命令行参数。我们可以通过Webpack的帮助命令来进行查看。
npx webpack –h
部分参数如图1-3所示。
图1-3 Webpack配置参数
从之前我们在package.json中添加的脚本命令来看,当项目需要越来越多的配置时,就要往命令中添加更多的参数,那么到后期维护起来就会相当困难。为了解决这个问题,可以把这些参数改为对象的形式专门放在一个配置文件里,在Webpack每次打包的时候读取该配置文件即可。
Webpack的默认配置文件为webpack.config.js(也可以使用其他文件名,需要使用命令行参数指定)。现在让我们在工程根目录下创建webpack.config.js,并添加如下代码:
module.exports = { entry: './src/index.js', output: { filename: 'main.js', }, mode: 'development', }
上面通过module.exports导出了一个对象,也就是打包时被Webpack接收的配置对象。先前在命令行中输入的一大串参数就都要改为key-value的形式放在这个对象中。
目前该对象包含两个关于资源输入输出的属性——entry和output。entry就是我们的资源入口,output则是一个包含更多详细配置的对象。在Webpack配置中,我们经常会遇到这种层叠的属性关系。这是由于Webpack本身的配置实在太多,如果都放在同一级会难以管理,因此出现了这种多级配置。当开发者要修改某个配置项时,通过层级关系找下来会更加清晰、快捷。
之前的参数--output-filename和--output-path现在都成为output下面的属性。filename和先前一样都是bundle.js,不需要改动,而path和之前有所区别。Webpack对于output.path的要求是使用绝对路径(从系统根目录开始的完整路径),之前我们在命令行中为了简洁而使用了相对路径。而在webpack.config.js中,我们通过调用Node.js的路径拼装函数path.join,将__dirname(Node.js内置全局变量,值为当前文件所在的绝对路径)与dist(输出目录)连接起来,得到了最终的资源输出路径。
现在我们可以去掉package.json中配置的打包参数了:
…… "scripts": { "build": "webpack" }, ……
为了验证最终效果,我们再对add-content.js的内容稍加修改:
export default function() { document.write('I\'m using a config file!'); }
执行npm run build命令,Webpack就会预先读取webpack.config.js,然后打包。完成之后我们打开index.html进行验证,结果如图1-4所示。
图1-4 index.html内容变为“I'm using a config file!”
1.4.5 webpack-dev-server
到这里,其实我们已经把Webpack的初始环境配置好了。你可能会发现,单纯使用Webpack以及它的命令行工具来进行开发调试的效率并不高。以往只要编辑项目源文件(JS、CSS、HTML等),刷新页面即可看到效果,现在多了一步打包,即我们在改完项目源码后要执行npm run build更新bundle.js,然后才能刷新页面生效。有没有更简便的方法呢?
其实Webpack社区已经为我们提供了一个便捷的本地开发工具——webpack-dev-server。用以下命令进行安装:
npm install webpack-dev-server -D
安装指令中的-D参数是将webpack-dev-server作为工程的devDependencies(开发环境依赖)记录在package.json中。这样做是因为webpack-dev-server仅仅在本地开发时才会用到,在生产环境中并不需要它,所以放在devDependencies中是比较恰当的。假如工程上线时要进行依赖安装,就可以通过npm install --only=prod过滤掉devDependencies中的冗余模块,从而加快安装和发布的速度。
为了便捷地启动webpack-dev-server,我们在package.json中添加一个dev指令:
…… "scripts": { "build": "webpack", "dev": "webpack-dev-server" }, ……
最后,我们还需要对webpack-dev-server进行配置。编辑webpack.config.js:
module.exports = { entry: './src/index.js', output: { filename: './main.js', }, mode: 'develpoment', devServer: { publicPath: '/dist', }, };
可以看到,我们在配置中添加了一个devServer对象,它是专门用来放webpack-dev-server配置的。webpack-dev-server可以看作一个服务者,它的主要工作就是接收浏览器的请求,然后将资源返回。当服务启动时,它会先让Webpack进行模块打包并将资源准备好(在示例中就是bundle.js)。当webpack-dev-server接收到浏览器的资源请求时,它会首先进行URL地址校验。如果该地址是资源服务地址(上面配置的publicPath),webpack-dev-server就会从Webpack的打包结果中寻找该资源并返回给浏览器。反之,如果请求地址不属于资源服务地址,则直接读取硬盘中的源文件并将其返回。
综上我们可以总结出webpack-dev-server的两大职能。
·令Webpack进行模块打包,并处理打包结果的资源请求。
·作为普通的Web Server,处理静态资源文件请求。
最后,在启动服务之前,我们还是更改一下add-content.js:
export default function() { document.write('I\'m using webpack-dev-server!'); }
一切就绪,执行 npm run dev命令并用浏览器打开http://localhost:8080/,可以看到如图1-5所示的输出结果。
图1-5 index.html内容变为“I'm using webpack-dev-server!”
这里有一点需要注意。直接用Webpack开发和使用webpack-dev-server有一个很大的区别,前者每次都会生成main.js,而webpack-dev-server只是将打包结果放在内存中,并不会写入实际的bundle.js,在每次webpack-dev-server接收到请求时都只是将内存中的打包结果返回给浏览器。
这一点可以通过删除工程中的dist目录来验证,你会发现即便dist目录不存在,刷新页面后功能仍然是正常的。从开发者的角度来看,这其实是符合情理的。在本地开发阶段我们经常会调整目录结构和文件名,如果每次都写入实际文件,最后就会产生一些没用的垃圾文件,还会干扰我们的版本控制,因此webpack-dev-server的处理方式显得更加简洁。
webpack-dev-server还有一项很便捷的特性——live-reloading(自动刷新)。例如我们保持本地服务启动以及浏览器打开的状态,到编辑器中去更改add-content.js:
export default function() { document.write('This is from live-reloading!'); }
此时切回到浏览器,你会发现浏览器的内容自动更新了,这就是live-reloading的功能。当webpack-dev-server发现工程源文件进行了更新操作后就会自动刷新浏览器,显示更新后的内容。该特性可以提升本地开发的效率。在后面我们还会讲到更先进的hot-module-replacement(模块热替换),甚至不需要刷新浏览器就能获取更新之后的内容。