结束了一季的忙碌,我这封笔已久的博客也终究该从春困的咒印中复苏,想来写些实用易读的做为开篇,天然是最好不过。css
新开个 webpack 插件/工具介绍的文章系列,约莫每周更新一篇篇幅适中的文章聊以共勉,兴许合适。html
本来指望每篇文章里能够介绍若干个插件,但鉴于部分插件略为复杂,且单篇内容不想写的唇焦舌敝惹人倦烦,因此像本文要介绍的 webpack-dev-server 就独立一文了。node
回归主题,今天你或许会花上30分钟的时间读完本章,并掌握 webpack-dev-server 的使用方法、理清一些容易困惑的配置(诸如 publicPath)或概念(如HMR)。webpack
另外,本章涉及的相关用例,能够在个人github(https://github.com/VaJoy/webpack-plugins/tree/master/char1)上下载到。git
一. webpack-dev-server 他爹和他爹的朋友github
咱们并不急着把 webpack-dev-server 直接拉出来介绍一通,咱们先了解下他的两位长辈 —— 他爹 webpack-dev-middleware,以及他爹的朋友 webpack-hot-middleware。web
他们三人有着某些亲密的联系,很多读者可能会对其身份存在认知混乱,因此颇有必要按辈分次序来分别介绍。express
1.1 webpack-dev-middlewarejson
假设咱们在服务端使用 express 开发一个站点,同时也想利用 webpack 对静态资源进行打包编译,那么在开发环节,每次修改完文件后,都得先执行一遍 webpack 的编译命令,等待新的文件打包到本地,再作进一步调试。虽然我们能够利用 webpack 的 watch mode 来监听变动、自动打包,但等待 webpack 从新执行的过程每每很耗时。后端
而 webpack-dev-middleware 的出现很好地解决了上述问题 —— 做为一个 webpack 中间件,它会开启 watch mode 监听文件变动,并自动地在内存中快速地从新打包、提供新的 bundle。
说白了就是 —— 自动编译(watch mode)+速度快(所有走内存)!
webpack-dev-middleware 的配置与使用其实很轻松,咱们经过一个很是简单的项目来示例(能够点这里获取):
PROJECT │ app.js //应用入口文件 │ express.config.js // express 服务启动配置 │ package.json │ webpack.config.js // webpack 配置 │ └─src ├─html │ index.html //首页 │ └─js └─page index.js //首页脚本模块
它的 webpack.config.js 配置文件以下:
module.exports = { entry: './app.js', output: { publicPath: "/assets/", filename: 'bundle.js', //path: '/' //只使用 dev-middleware 的话能够忽略本属性 } };
这里有一个很是关键的配置 —— publicPath,熟悉 webpack 的同窗都知道,它是生成的新文件所指向的路径,能够用于模拟 CDN 资源引用。
打个比方,当咱们使用 url-loader 来处理图片时,把 publickPath 设为“http://abcd/assets/”,则最终打包后,样式文件里所引用的图片地址会加上这个前缀:
/**-------------webpack配置项--------------**/ output: { publicPath: "http://abcd/assets/", //模拟CDN地址 filename: 'bundle.js', path: path.join(__dirname, 'dist/') }, module: { rules: [{ test: /\.css$/, loader: ['style-loader', 'css-loader'] }, { test: /\.(png|jpg|gif)$/, loader: 'url-loader' }] } /**-------------页面引入的样式模块 index.css--------------**/ section{ width:300px; height: 300px; background-image: url(a.jpg); }
打包后(dist/bundle.js 里的样式执行效果):
固然若是你没把资源(好比这张md5化后的图片)托管到CDN上,是请求不到的,不过经过Fiddler配置代理映射,能够解决这个问题。
然而,在使用 webpack-dev-middleware (或其它走内存的工具)的状况下,publicPath 只建议配置相对路径 —— 由于 webpack-dev-middleware 在使用的时候,也须要再配置一个 publicPath(见下文 express.config.js 的配置),用于标记从内存的哪一个路径去存放和查找资源,这意味着 webpack-dev-middleware 的 publicPath 必须是相对路径。
而若是 webpack.config.js 里的 publicPath 跟 webpack-dev-middleware 的 publicPath 不一致的话(好比前者配置了 http 的路径),会致使资源请求到了内存外的地方去了(本地也没这个文件,也无法走 Fiddler 代理来解决),从而返回404~
若是上面这段话瞧着糊涂,建议暂时搁置它,后续回过头再来咀嚼,咱们先了解下所谓的“webpack-dev-middleware 的 publicPath”是什么。
以下是 express.config.js 文件:
const path = require('path'); const express = require("express"); const ejs = require('ejs'); const app = express(); const webpack = require('webpack'); const webpackMiddleware = require("webpack-dev-middleware"); let webpackConf = require('./webpack.config.js'); app.engine('html', ejs.renderFile); app.set('views', path.join(__dirname, 'src/html')); app.set("view engine", "html"); var compiler = webpack(webpackConf); app.use(webpackMiddleware(compiler, { //使用 webpack-dev-middleware publicPath: webpackConf.output.publicPath //保持和 webpack.config.js 里的 publicPath 一致 })); app.get("/", function(req, res) { res.render("index"); }); app.listen(3333);
可见 webpack-dev-middleware 的使用语法其实就这么简练,不外乎是:
var webpackMiddleware = require("webpack-dev-middleware"); app.use(webpackMiddleware(webpack(webpackConfig), options));
其中 options 是 webpack-dev-middleware 的配置对象,详尽的可选项可参考官方文档,限于篇幅,此处只介绍 publicPath —— 它用于决定 webpack 打包编译后的文件,要存放在内存中的哪个虚拟路径,并提供一个 SERVER,将路径和文件映射起来(即便它们都是虚拟的,但依旧可请求的到)。
当前的例子,是将内存路径配置为 /assets/,这意味着打包后的 bundle.js 会存放在虚拟内存路径 SERVERROOT/assets/ 下(这里的“SERVERROOT”实际上即 html 文件的访问路径),也意味着咱们能够直接在 src/html/index.html 中经过 src='assets/bundle.js' 的形式引用和访问内存中的 bundle 文件:
<body> <div></div> <script src="assets/bundle.js"></script> </body>
咱们执行一遍 node express.config,而后访问 http://localhost:3333,便能正常访问页面、请求和执行 bundle.js:
同时,只要咱们修改了页面的脚本模块(好比 src/js/index.js),webpack-dev-middleware 便会自行从新打包到内存,替换掉旧的 bundle,咱们只须要刷新页面便可看到刚才的变动。
这里写个关于 webpack-dev-middleware 的小 tips:
1. webpack-dev-middleware 配置项里的 publicPath 要与 webpack.config 里的 output.publicPath 保持一致(而且只能是相对路径),否则会出现问题;
2. 使用 webpack-dev-middleware 的时候,其实能够彻底无视 webpack.config 里的 output.path,甚至不写也能够,由于走的纯内存,output.publicPath 才是实际的 controller;
3. publicPath 配置的相对路径,实际是相对于 html 文件的访问路径。
1.2 HMR
机智的小伙伴们在读完 webpack-dev-middleware 的介绍后,会洞悉出它的一处弱点 —— 虽然 webpack-dev-middleware 会在文件变动后快速地从新打包,可是每次都得手动刷新客户端页面来访问新的内容,仍是略为麻烦。这是由于 webpack-dev-middleware 在应用执行的时候,没办法感知到模块的变化。
那么是否有办法可让页面也能自动更新呢?webpack-hot-middleware 即是帮忙填这个坑的人,因此我在前文称之为 —— webpack-dev-middleware 的好朋友。
webpack-hot-middleware 提供的这种能力称为 HMR,因此在介绍 webpack-hot-middleware 以前,咱们先来科普一下 HMR。
HMR 即模块热替换(hot module replacement)的简称,它能够在应用运行的时候,不须要刷新页面,就能够直接替换、增删模块。
webpack 能够经过配置 webpack.HotModuleReplacementPlugin 插件来开启全局的 HMR 能力,开启后 bundle 文件会变大一些,由于它加入了一个小型的 HMR 运行时(runtime),当你的应用在运行的时候,webpack 监听到文件变动并从新打包模块时,HMR 会判断这些模块是否接受 update,若容许,则发信号通知应用进行热替换。
这里说起的“判断模块是否接受 update”是指判断模块里是否执行了 module.hot.accept(), 这里举个小例子:
如图,白色的部分是编译后的模块依赖树,这时候咱们修改了 B 模块,致使 B 模块以及依赖它的 A 模块都出现了变化(绿色部分)。
模块变动的时候,webpack 会顺着依赖树一层一层往上冒泡,查询哪一个模块是接受 update 的,查询到了则终止冒泡,并通知 SERVER 更新其爬过的模块。
假设咱们把 module.hot.accept() 放在 B 模块执行,则 webpack 会查找到 B` 模块的变动就中止继续往上冒泡查找了(A`是不容许变动的模块)—— 若是 B 的内容变动,是直接在 B 模块调用的,那页面就能直接展现出新的内容出来,这样效率也高(绕过了A模块);但若是 B 的内容,其实是要通过 A 来调用,才能在页面上展现出来,那此时页面就不会刷新(即便 B 的内容变了)。
说白了就是 module.hot.accept() 放的好,就能够绕过一些没必要要的模块变动检查来提高效率,不过对于懒人来讲,直接置于最顶层的模块(好比入口模块)最为省心。
关于更多的 HMR 的知识点,能够参考官方文档。
1.3 webpack-hot-middleware
聊完了 HMR,咱们回头了解下 webpack-hot-middleware 的使用。
咱们试着对前文使用的项目来作一番改造 —— 引入 webpack-hot-middleware 来提高开发体验。
首先往 express.config.js 加上一小段代码:
app.engine('html', ejs.renderFile); app.set('views', path.join(__dirname, 'src/html')); app.set("view engine", "html"); var compiler = webpack(webpackConf); app.use(webpackMiddleware(compiler, { publicPath: webpackConf.output.publicPath })); //添加的代码段,引入和使用 webpack-hot-middleware app.use(require("webpack-hot-middleware")(compiler, { path: '/__webpack_hmr' })); app.get("/", function(req, res) { res.render("index"); }); app.listen(3333);
即在原先的基础上引入了 webpack-hot-middleware:
app.use(require("webpack-hot-middleware")(webpackCompiler, options));
这里的 options 是 webpack-hot-middleware 的配置项,详细见官方文档,这里我们只填一个必要的 path —— 它表示 webpack-hot-middleware 会在哪一个路径生成热更新的事件流服务,且访问的页面会自动与这个路径经过 EventSource 进行通信,来拉取更新的数据从新粉饰本身。
这里要了解下,实际上 webpack-hot-middleware 最大的能力,是让 SERVER 可以和 HMR 运行时进行通信,从而对模块进行热更新。
而后是 webpack.config.js 文件:
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: ['webpack-hot-middleware/client', './app.js'], //修改点1 output: { publicPath: "/assets/", filename: 'bundle.js' }, plugins: [ //修改点2 new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin() //出错时只打印错误,但不从新加载页面 ] };
首先是 entry 里要多加上 'webpack-hot-middleware/client',此举是与 server 建立链接。
接着加上两个相关的插件来打通 webpack HMR 的任督二脉,其中的 webpack.HotModuleReplacementPlugin 咱们在上一节说起过,它是 HMR 的功能提供者。
最后一步很重要,不少新手容易漏掉。咱们须要在入口文件 app.js 里加上一小段代码:
import {init} from './src/js/page/index'; //灰常重要,知会 webpack 容许此模块的热更新 if (module.hot) { module.hot.accept(); } init();
此处的 module.hot.accept() 是知会 webpack 接受此模块的 HMR update,在上一节已经说起屡次。
补充好上述的代码,执行 node express.config 并访问 http://localhost:3333,以后的模块修改,都会自动打包并更新客户端页面模块:
1.4 webpack-dev-server
虽然 webpack-dev-middleware + webpack-hot-middleware 的组合为开发过程提供了便利,但它们仅适用于服务侧开发的场景。
不少时候咱们仅仅对客户端页面作开发,没有直接的 server 来提供支持,这时候就须要 webpack-dev-server 来解囊相助了。
顾名思义,webpack-dev-server 相对前两个工具多了个“server”,实际上它的确也是在 webpack-dev-middleware 的基础上多套了一层壳来提供 CLI 及 server 能力(这也是为什么我称 webpack-dev-middleware 是 webpack-dev-server 他爹)。
此处依旧以一个简单的项目来展现如何配置、使用 webpack-dev-server,你能够点这里获取相关代码。
脱离了 express,咱们再也不需求配置后端脚本,不过对于 webpack.config.js,须要多加一个名为“devServer”的 webpack-dev-server 配置项:
const path = require('path'); module.exports = { entry: './app.js', output: { publicPath: "/assets/", filename: 'bundle.js' }, devServer: { //新增配置项 contentBase: path.join(__dirname, "src/html"), port: 3333 } };
其中 devServer.port 表示 SERVER 的监听端口,即运行后咱们能够经过 http://localhost:3333 来访问应用;
而 devServer.contentBase 表示 SERVER 将从哪一个目录去查找内容文件(即页面文件,好比 HTML)。
确保安装好 webpack-dev-server 后执行其 CLI 命令来召唤支持热更新的 SERVER:
webpack-dev-server
接着访问 http://localhost:3333,彷佛便能得到前文 webpack-dev-middleware + webpack-hot-middleware 的热更新能力~
不过事实并不是如此,虽然在咱们修改模块后,页面的确自动刷新了。但截止此处,webpack-dev-server 跑起来其实只至关于捎上了 SERVER 的 webpack-dev-middleware,而没有 HMR —— 在咱们修改应用模块后,页面是整个刷新了一遍,而并不是热更新。
但愿读者们能够记住,HMR 提供了局部更新应用模块的能力,而不须要刷新整个应用页面!
这块的验证也很简单,直接在 index.html 里加个 script 打印 Date.now() 便可,若刷新页面,打印的值直接会变。
要让 webpack-dev-server 加上 HMR 的翅膀,其实就得像前面 webpack-hot-middleware 的配置那样,把 HMR 相关的东西统统加上,同时将 devServer.hot 设为 true:
// webpack.config.js const path = require('path'); const webpack = require('webpack'); module.exports = { entry: './app.js', output: { publicPath: "/assets/", filename: 'bundle.js' }, devServer: { contentBase: path.join(__dirname, "src/html"), port: 3333, hot: true // 让 dev-server 开启 HMR }, plugins: [ new webpack.HotModuleReplacementPlugin() //让 webpack 启动全局 HMR ] };
// 入口文件 app.js import {init} from './src/js/page/index'; if (module.hot) { // 知会 webpack 该模块接受 HMR update module.hot.accept(); } init();
这时候,再执行 webpack-dev-server,才是正宗的有 HMR 加持的 SERVER。
关于完整的 devServer 配置项可参考官方文档,在文章的最后,咱们罗列几个经常使用项作简单介绍。
1. contentBase
即 SERVERROOT,如上方示例配置为 “path.join(__dirname, "src/html")”,后续访问 http://localhost:3333/index.html 时,SERVER 会从 src/html 下去查找 index.html 文件。
它能够是单个或多个地址的形式:
contentBase: path.join(__dirname, "public") //多个: contentBase: [path.join(__dirname, "public"), path.join(__dirname, "assets")]
若不填写该项,默认为项目根目录。
2. port
即监听端口,默认为8080。
3. compress
传入一个 boolean 值,通知 SERVER 是否启用 gzip。
4. hot
传入一个 boolean 值,通知 SERVER 是否启用 HMR。
5. https
能够传入 true 来支持 https 访问,也支持传入自定义的证书:
https: true //也能够传入一个对象,来支持自定义证书 https: { key: fs.readFileSync("/path/to/server.key"), cert: fs.readFileSync("/path/to/server.crt"), ca: fs.readFileSync("/path/to/ca.pem"), }
6. proxy
代理配置,适用场景是,除了 webpack-dev-server 的 SERVER(SERVER A) 以外,还有另外一个在运行的 SERVER(SERVER B),而咱们但愿能经过 SERVER A 的相对路径来访问到 SERVER B 上的东西。
举个例子:
devServer: { contentBase: path.join(__dirname, "src/html"), port: 3333, hot: true, proxy: { "/api": "http://localhost:5050" } }
运行 webpack-dev-server 后,你若访问 http://localhost:3333/api/user,则至关于访问 http://localhost:5050/api/user。
更多可行的 proxy 配置见 https://webpack.js.org/configuration/dev-server/#devserver-proxy,这里不赘述。
7. publicPath
如同 webpack-dev-middleware 的 publicPath 同样,表示从内存中的哪一个路径去存放和检索静态文件。
不过官方文档有一处错误须要堪正 —— 当没有配置 devServer.publicPath 时,默认的 devServer.publicPath 并不是根目录,而是 output.publicPath:
这也是为什么我们的例子里压根没写 devServer.publicPath,但还能正常请求到 https://localhost:3333/assets/bundle.js。
8. setup
webpack-dev-server 的服务应用层使用了 express,故能够经过 express app 的能力来模拟数据回包,devServer.setup 方法就是干这事的:
devServer: { contentBase: path.join(__dirname, "src/html"), port: 3333, hot: true, setup(app){ //模拟数据 app.get('/getJSON', function(req, res) { res.json({ name: 'vajoy' }); }); } }
而后咱们能够经过请求 http://localhost:3333/getJSON 来取得模拟的数据: