重学webpack系列(五) -- webpack的devServer实践与原理
上一章重学webpack系列(四) -- webpack的plugins机制的解读我们讲解了webpack
的插件系统的基本使用与插件的基本原理,并去讨论了实现一个插件的必要
要求,其实webpack
除了集成了loader
、plugin
还提供了,webpack-devServer
功能,用于启动一个本地服务快速构建应用。那么本章我们就来一起讨论一下这个特性吧。
为什么要使用devServer
如果没有devServer
,我们的开发方式将会是编写源码
->webpack打包
->文件引入
->浏览器查看
,这种方式整个周期
比较长,而且容易出错
,devServer
能够提供功能在于这几点。
- 以
http
服务形式加载文件,而非文件手动引入。 - 更加贴近
生产环境
,能够解决一些webApi
形式在单文件下产生的问题。 - 提供
sourceMap
支持,能够帮助我们更快速的定位错误。 - 开发环境下能够
自动编译
,自动刷新
浏览器界面,提高开发效率。
devServer的核心原理
devServer
可以启动一个http
服务,在webpack
构建的时候,监听文件,如果文件发生变化,将会启动webpack
的自动编译。
关于devServer打包产物存放位置
devServer
为了提高效率,webpack
打包的结果会放在内存里面,httpServer
能够从内存中读取这些文件。倘若放在硬盘中,那么每一次的读写会带来这些问题。
- 硬盘读写需要
消耗
时间,而且比内存消耗的时间要大
很多,明显提高
效率。 - 多次读写
硬盘
,会给硬盘造成大量的磁盘碎片
,减少使用寿命。
devServer的基础实践
devServer
作为一个第三方的开发工具,自然也需要安装后再使用了。
安装:
npm i webpack-dev-server -S
启动:
npx webpack-dev-server
// webpack.config.js
module.export = {
...
const path = require('path');
module.exports = {
//...
devServer: {
// 目录
static: {
directory: path.join(__dirname, 'public'),
},
// 启用gzip压缩
compress: true,
// httpServer端口
port: 9000,
proxy: {
"/api": {},
...
},
// 允许访问域名,设置白名单,all为全部允许
allowedHosts: [
'host.com',
'xxx.com',
],
// 日志设置,设置reconnect: n可以设置重连次数,
client: {
logging: 'info',
},
// 更多属性请查看:https://webpack.js.org/configuration/dev-server
},
};
}
// package.json
{
...
"scripts":{
"build": "webpack || webpack-cli --watch",
"server": "webpack-dev-server || npx webpack-dev-server --open" // 启动命令
}
...
"devDependencies":{
...
"webpack-dev-server": "^4.11.1" // 版本
...
}
}
根据上面的配置你就可以在npm run server
之后,开启一个本地httpServer
。
proxy
因为在实际开发中,肯定会去调用后端接口,根据同源策略,此时一定会有跨域的问题。所以在开发环境下我们一般处理跨域问题的方式是跨域资源共享CROS
和跨域中间件devServerApplymiddleWare
,那么后者就是webpack-dev-server
提供的功能。
module.export = {
...
devServer: {
...
proxy: {
"/api": {
// 目标地址
target: "https://api.xx.xx.xx",
// 必要时重写路径
pathReWrite: {
"^/api":''
},
// 确保请求主机名是target中的主机名
changeOrigin: true
},
...
}
}
}
devServer源码
我们可以以几种方式启动devServer
。
webpack server
webpack-cli server
webpack-dev-server
npx webpack-dev-server
// package.json
{
...
"scripts":{
"build": "webpack || webpack-cli --watch",
"server": "webpack server",
// "server": "webpack-cli server"
// "server": "webpack-dev-server"
// "server": "npx webpack-dev-server"
}
...
}
很明显上述启动分为两种,第一种使用webpack
去启动devServer
,第二种使用webpack-dev-server
包启动devServer
,两种的区别在于后者在使用之前会检查是否有安装过cli
,没有的话会提醒你去安装。
// 如果没安装cli
if (!cli.installed) {
const path = require("path");
const fs = require("graceful-fs");
const readLine = require("readline");
// 给一个notify
const notify = `CLI for webpack must be installed.\n ${cli.name} (${cli.url})\n`;
console.error(notify);
// 检查本地.lock文件,确定包安装方式,npm yard pnpm
/**
* @type {string}
*/
let packageManager;
if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
packageManager = "yarn";
} else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
packageManager = "pnpm";
} else {
packageManager = "npm";
}
const installOptions = [packageManager === "yarn" ? "add" : "install", "-D"];
// 给个notice
console.error(
`We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
" "
)} ${cli.package}".`
);
// 用户选择yes or no
const question = `Do you want to install 'webpack-cli' (yes/no): `;
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr,
});
process.exitCode = 1;
// answer 回调
questionInterface.question(question, (answer) => {
questionInterface.close();
// 输入 y 或者yes
const normalizedAnswer = answer.toLowerCase().startsWith("y");
if (!normalizedAnswer) {
console.error(
"You need to install 'webpack-cli' to use webpack via CLI.\n" +
"You can also install the CLI manually."
);
return;
}
process.exitCode = 0;
console.log(
`Installing '${
cli.package
}' (running '${packageManager} ${installOptions.join(" ")} ${
cli.package
}')...`
);
// 执行runCommand => runCli
runCommand(packageManager, installOptions.concat(cli.package))
.then(() => {
runCli(cli);
})
.catch((error) => {
console.error(error);
process.exitCode = 1;
});
});
} else {
// 如果安装过了cli,直接执行runCli
runCli(cli);
}
runCli
runCli
的作用就是,找到依赖模块webpack-cli
的路径,加载webpack-cli
,所以webpack-dev-server
还是依赖webpack-cli
去执行的。
const runCli = (cli) => {
if (cli.preprocess) {
cli.preprocess();
}
const path = require("path");
//node_modules/webpack-cli/package.json
const pkgPath = require.resolve(`${cli.package}/package.json`);
// 资源包信息
const pkg = require(pkgPath);
// 导入执行
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};
webpack-cli和webpack中的devServer处理
// webpack-cli.js
// 根据路径加载配置
devServer = await this.loadJSONFile("webpack-dev-server/package.json", false);
// webpack/bin/webpack.js
const CreateCompiler = rawOptions => {
// 这里的rawOptions 得到的就是webpack.config.json里面的配置
// entry:{}.output:{},module:{},devServer:{}...
// 在getNormalizedWebpackOptions函数里面对所有配置文件雨webpack模块进行了处理
const options = getNormalizedWebpackOptions(rawOptions);
...
//getNormalizedWebpackOptions
...
dependencies: config.dependencies,
devServer: optionalNestedConfig(config.devServer, devServer => ({
...devServer
})),
devtool: config.devtool,
entry:{...},
...
}
开启httpServer服务
在webpack
构建过程当中,devServer
创建httpServer
,node
本地服务器,与client
建立socket
联系,满足server
与client
通信需要,当文件的hash
变化的时候,会重新触发Compiler
流程。在compiler
的done
钩子函数里调用sendStats
发放向client
发送ok
或warning
消息,并同时发送向client
发送hash
值,在client
保存下来。
- client接收到
ok
或warning
消息后调用reloadApp
发布客户端检查更新事件(webpackHotUpdate
)
// webpack/node_modules/webpack-dev-server/client/index.js
ok: function ok() {
sendMessage("Ok"); // 收到来自server的ok
...
reloadApp(options, status);
},
warnings: function warnings(_warnings, params) {
...
sendMessage("Warnings", x); // 收到来自server的warnings
...
reloadApp(options, status);
},
调用reloadApp
,实际上调用的webpackHotUpdate
事件,当然还会有一些额外的处理,比如本地刷新
,是否允许热更新
等。
function reloadApp(_ref, status) {
var hot = _ref.hot,
liveReload = _ref.liveReload;
// status为isUnloading,不更新
if (status.isUnloading) {
return;
}
// 更新client的文件的hash值
var currentHash = status.currentHash,
previousHash = status.previousHash;
// 用去重的方法,检测每次发过来的hash是否还在维护,如果在维护表示文件未更新。
var isInitial = currentHash.indexOf(
/** @type {string} */
previousHash) >= 0;
if (isInitial) {
return;
}
/**
* @param {Window} rootWindow
* @param {number} intervalId
*/
// 更新浏览器界面
function applyReload(rootWindow, intervalId) {
clearInterval(intervalId);
log.info("App updated. Reloading...");
rootWindow.location.reload();
}
// 额外的变量,用户判断是否需要热更新,刷新啥的
var search = self.location.search.toLowerCase();
var allowToHot = search.indexOf("webpack-dev-server-hot=false") === -1;
var allowToLiveReload = search.indexOf("webpack-dev-server-live-reload=false") === -1;
if (hot && allowToHot) {
log.info("App hot update...");
// 这里用到了发布订阅,发布一个webpackHotUpdate事件执行。
hotEmitter.emit("webpackHotUpdate", status.currentHash);
if (typeof self !== "undefined" && self.window) {
// broadcast update to window
self.postMessage("webpackHotUpdate".concat(status.currentHash), "*");
}
} // 页面更新
else if (liveReload && allowToLiveReload) {
var rootWindow = self; // use parent window for reload (in case we're in an iframe with no valid src)
// 以异步宏任务的形式去更新window,最大程度不占用js线程,提高效率。
var intervalId = self.setInterval(function () {
if (rootWindow.location.protocol !== "about:") {
// reload immediately if protocol is valid
applyReload(rootWindow, intervalId);
} else {
rootWindow = rootWindow.parent;
if (rootWindow.parent === rootWindow) {
// if parent equals current window we've reached the root which would continue forever, so trigger a reload anyways
applyReload(rootWindow, intervalId);
}
}
});
}
}
处理到这里的时候,webpack
的hot
部分监听到webpackHotUpdate
事件去完成热更新流程了,直通车 >>> 重学webpack系列(七) -- webpack的HMR的实践
总结
本文讲述了webpack
提供的devServer
的功能与原理。
webpack
除了这些功能,还支持了sourceMap
,他能够帮助开发者快速定位问题在源码
中的位置。下一章我们就一起来看看这个特性吧, 直通车 >>> 重学webpack系列(六) -- webpack的sourceMap实践与原理
转载自:https://juejin.cn/post/7147636643099312136