webpack-dev-server原理分析与HMR实现
2017-03-02 17:31
1396 查看
建议在github阅读,我会保证内容及时更新,并欢迎star,issue。如果你想深入了解webpack-dev-server的内部原理,你也可以查看我写的这个打包工具,通过它可以完成三种打包方式,其中devServer模式就是通过webpack-dev-server来完成的,并且支持HMR(webpack-dev-server虽然说可以支持无刷新更新数据,但是在大多数情况下都是刷新页面的,而该打包工具已经无需刷新而完成数据更新了)。对于webpack的HMR不了解的可以查看这里。其中也牵涉到webpack-dev-middleware中间件。希望对您有用
(1)首先看看”webpack/hot/only-dev-server”的文件内容:
./log-apply-result模块内容如下:
所以”webpack/hot/only-dev-server”的文件内容就是检查哪些模块更新了(通过webpackHotUpdate事件完成),其中哪些模块更新成功,而哪些模块由于某种原因没有更新成功。其中没有更新的原因可能是如下的:
至于模块什么时候接受到需要更新是和webpack的打包过程有关的,这里也给出触发更新的时机:
也就是说当客户端接受到服务器端发送的ok和warning信息的时候,同时支持HMR的情况下就会要求检查更新,同时发送过来的还有服务器端本次编译的hash值。我们继续深入一步,看看服务器什么时候发送’ok’和’warning’消息:
也就是说更新是通过上面这个方法完成的,我们看看上面这个方法什么时候调用就可以了:
是不是豁然开朗了,也就是每次compiler的’done’钩子函数被调用的时候就会要求客户端去检查模块更新,进而完成HMR基本功能!
(2)再来看看webpack/hot/dev-server
也就是说webpack/hot/dev-server相较于前面在入口文件中添加的”webpack/hot/only-dev-server”来说,区别在于后者传入的是哪些已经被更新的模块,也就是已经被自己模块本身dispose处理了。如下:
(3)如果你注意到上面其实我们还添加了一个client/index.js,这个客户端代码只是添加了我们的客户端的socket.js代码,这时候我们客户端就可以获取到服务器端发送到的socket命令
也就是我们关注的这些module.hot.decline方法都是在Parser上封装的!
此时表示,我们这个模块支持HMR,任何其依赖的模块变化都会被捕捉到。当依赖的模块更新后回调函数被调用。当然,如果是下面这种方式:
那么表示我们接受当前模块所有依赖的模块的代码更新,而且这种更新不会冒泡到父级中去。这当我们模块没有导出任何东西的情况下有用(比如entry)。
其中decline方法签名如下:
这表明我们不会接受特定模块的更新,如果该模块更新了,那么更新失败同时失败代码为”decline”。而上面的代码表明我们不会接受jquery模块的更新。当前也可以是如下模式:
这表明我们当前的模块是不会更新的,也就是不会HMR。如果更新了那么错误代码为”decline”;
这表示我们会添加一个一次性的处理函数,这个函数在当前模块更新后会被调用。此时,你需要移除或者销毁一些持久的资源,如果你想将当前的状态信息转移到更新后的模块中,此时可以添加到data对象中,以后可以通过module.hot.data访问。如下面的例子用于保存指定模块实例化的时间,从而防止模块更新后数据丢失(刷新后还是会丢失的)。
其中内容是:
从内容你也可以看出,只是将我们修改的模块push到exports对象中!而hotUpdateChunkFilename就是为了让你能够执行script的src中的值的!而同样的hotUpdateMainFilename是一个json文件用于指定哪些模块发生了变化,在output目录下。
上面这个从URL到路径的转化就是通过webpack-dev-middleware来完成的。
参考资料:
webpack-dev-middlware
wcf编译工具开发
webpack-dev-server在我们的entry中添加的hot模块内容
看看下面的方法你就知道了,在hot模式下,我们的entry最后都会被添加两个文件:module.exports = function addDevServerEntrypoints(webpackOptions, devServerOptions) { if(devServerOptions.inline !== false) { //表示是inline模式而不是iframe模式 const domain = createDomain(devServerOptions); const devClient = [`${require.resolve("../../client/")}?${domain}`]; //客户端内容 if(devServerOptions.hotOnly) devClient.push("webpack/hot/only-dev-server"); else if(devServerOptions.hot) devClient.push("webpack/hot/dev-server"); //配置了不同的webpack而文件到客户端文件中 [].concat(webpackOptions).forEach(function(wpOpt) { if(typeof wpOpt.entry === "object" && !Array.isArray(wpOpt.entry)) { /* entry:{ index:'./index.js', => entry:[] index1:'./index1.js' } */ Object.keys(wpOpt.entry).forEach(function(key) { wpOpt.entry[key] = devClient.concat(wpOpt.entry[key]); }); //添加我们自己的入口文件 } else if(typeof wpOpt.entry === "function") { wpOpt.entry = wpOpt.entry(devClient); //如果entry是一个函数那么我们把devClient数组传入 } else { wpOpt.entry = devClient.concat(wpOpt.entry); //数组直接传入 } }); } };
(1)首先看看”webpack/hot/only-dev-server”的文件内容:
if(module.hot) { var lastHash; //___webpack_hash__ //Access to the hash of the compilation. //Only available with the HotModuleReplacementPlugin or the ExtendedAPIPlugin var upToDate = function upToDate() { return lastHash.indexOf(__webpack_hash__) >= 0; //如果两个hash相同那么表示没有更新 }; //检查更新 var check = function check() { //Check all currently loaded modules for updates and apply updates if found. module.hot.check().then(function(updatedModules) { //没有更新的模块直接返回 if(!updatedModules) { console.warn("[HMR] Cannot find update. Need to do a full reload!"); console.warn("[HMR] (Probably because of restarting the webpack-dev-server)"); return; } //apply方法:If status() != "ready" it throws an error. //开始更新 return module.hot.apply({ ignoreUnaccepted: true, ignoreDeclined: true, ignoreErrored: true, onUnaccepted: function(data) { console.warn("Ignored an update to unaccepted module " + data.chain.join(" -> ")); }, onDeclined: function(data) { console.warn("Ignored an update to declined module " + data.chain.join(" -> ")); }, onErrored: function(data) { console.warn("Ignored an error while updating module " + data.moduleId + " (" + data.type + ")"); } //renewedModules表示哪些模块已经更新了 }).then(function(renewedModules) { if(!upToDate()) { check(); } //更新的模块updatedModules,renewedModules表示哪些模块已经更新了 require("./log-apply-result")(updatedModules, renewedModules); if(upToDate()) { console.log("[HMR] App is up to date."); } }); }).catch(function(err) { var status = module.hot.status(); if(["abort", "fail"].indexOf(status) >= 0) { console.warn("[HMR] Cannot check for update. Need to do a full reload!"); console.warn("[HMR] " + err.stack || err.message); } else { console.warn("[HMR] Update check failed: " + err.stack || err.message); } }); }; var hotEmitter = require("./emitter"); //emitter模块内容,也就是导出一个events实例 /* var EventEmitter = require("events"); module.exports = new EventEmitter(); */ hotEmitter.on("webpackHotUpdate", function(currentHash) { lastHash = currentHash; //表示本次更新后得到的hash值 if(!upToDate()) { //有更新 var status = module.hot.status(); if(status === "idle") { console.log("[HMR] Checking for updates on the server..."); check(); } else if(["abort", "fail"].indexOf(status) >= 0) { console.warn("[HMR] Cannot apply update as a previous update " + status + "ed. Need to do a full reload!"); } } }); console.log("[HMR] Waiting for update signal from WDS..."); } else { throw new Error("[HMR] Hot Module Replacement is disabled."); }
./log-apply-result模块内容如下:
module.exports = function(updatedModules, renewedModules) { //renewedModules表示哪些模块被更新了,剩余的模块表示,哪些模块由于 ignoreDeclined,ignoreUnaccepted配置没有更新 var unacceptedModules = updatedModules.filter(function(moduleId) { return renewedModules && renewedModules.indexOf(moduleId) < 0; }); //哪些模块无法HMR,打印log if(unacceptedModules.length > 0) { console.warn("[HMR] The following modules couldn't be hot updated: (They would need a full reload!)"); unacceptedModules.forEach(function(moduleId) { console.warn("[HMR] - " + moduleId); }); } //没有模块更新,表示模块是最新的 if(!renewedModules || renewedModules.length === 0) { console.log("[HMR] Nothing hot updated."); } else { console.log("[HMR] Updated modules:"); //更新的模块 renewedModules.forEach(function(moduleId) { console.log("[HMR] - " + moduleId); }); //每一个moduleId都是数字那么建议使用NamedModulesPlugin var numberIds = renewedModules.every(function(moduleId) { return typeof moduleId === "number"; }); if(numberIds) console.log("[HMR] Consider using the NamedModulesPlugin for module names."); } };
所以”webpack/hot/only-dev-server”的文件内容就是检查哪些模块更新了(通过webpackHotUpdate事件完成),其中哪些模块更新成功,而哪些模块由于某种原因没有更新成功。其中没有更新的原因可能是如下的:
ignoreUnaccepted ignoreDecline ignoreErrored
至于模块什么时候接受到需要更新是和webpack的打包过程有关的,这里也给出触发更新的时机:
ok: function() { sendMsg("Ok"); if(useWarningOverlay || useErrorOverlay) overlay.clear(); if(initial) return initial = false; reloadApp(); }, warnings: function(warnings) { log("info", "[WDS] Warnings while compiling."); var strippedWarnings = warnings.map(function(warning) { return stripAnsi(warning); }); sendMsg("Warnings", strippedWarnings); for(var i = 0; i < strippedWarnings.length; i++) console.warn(strippedWarnings[i]); if(useWarningOverlay) overlay.showMessage(warnings); if(initial) return initial = false; reloadApp(); }, function reloadApp() { //如果开启了HMR模式 if(hot) { log("info", "[WDS] App hot update..."); var hotEmitter = require("webpack/hot/emitter"); hotEmitter.emit("webpackHotUpdate", currentHash); //重新启动webpack/hot/emitter,同时设置当前hash if(typeof self !== "undefined" && self.window) { // broadcast update to window self.postMessage("webpackHotUpdate" + currentHash, "*"); } } else { //如果不是Hotupdate那么我们直接reload我们的window就可以了 log("info", "[WDS] App updated. Reloading..."); self.location.reload(); } }
也就是说当客户端接受到服务器端发送的ok和warning信息的时候,同时支持HMR的情况下就会要求检查更新,同时发送过来的还有服务器端本次编译的hash值。我们继续深入一步,看看服务器什么时候发送’ok’和’warning’消息:
Server.prototype._sendStats = function(sockets, stats, force) { if(!force && stats && (!stats.errors || stats.errors.length === 0) && stats.assets && stats.assets.every(function(asset) { return !asset.emitted; //每一个asset都是没有emitted属性,表示没有发生变化。如果发生变化那么这个assets肯定有emitted属性 }) ) return this.sockWrite(sockets, "still-ok"); this.sockWrite(sockets, "hash", stats.hash); //设置hash if(stats.errors.length > 0) this.sockWrite(sockets, "errors", stats.errors); else if(stats.warnings.length > 0) this.sockWrite(sockets, "warnings", stats.warnings); else this.sockWrite(sockets, "ok"); }
也就是说更新是通过上面这个方法完成的,我们看看上面这个方法什么时候调用就可以了:
compiler.plugin("done", function(stats) { this._sendStats(this.sockets, stats.toJson(clientStats)); this._stats = stats; }.bind(this));
是不是豁然开朗了,也就是每次compiler的’done’钩子函数被调用的时候就会要求客户端去检查模块更新,进而完成HMR基本功能!
(2)再来看看webpack/hot/dev-server
if(module.hot) { var lastHash; //__webpack_hash__是每次编译的hash值是全局的 //Only available with the HotModuleReplacementPlugin or the ExtendedAPIPlugin var upToDate = function upToDate() { return lastHash.indexOf(__webpack_hash__) >= 0; }; var check = function check() { // check([autoApply], callback: (err: Error, outdatedModules: Module[]) => void // If autoApply is truthy the callback will be called with all modules that were disposed. apply() is automatically called with autoApply as options parameter.(传入哪些代码已经被更新的模块) //If autoApply is not set the callback will be called with all modules that will be disposed on apply(). (不是true那么传入的是哪些需要被apply处理的模块) module.hot.check(true).then(function(updatedModules) { //检查所有要更新的模块,如果没有模块要更新那么回调函数就是null if(!updatedModules) { console.warn("[HMR] Cannot find update. Need to do a full reload!"); console.warn("[HMR] (Probably because of restarting the webpack-dev-server)"); window.location.reload(); return; } //如果还有更新 if(!upToDate()) { check(); } require("./log-apply-result")(updatedModules, updatedModules); //已经被更新的模块都是updatedModules if(upToDate()) { console.log("[HMR] App is up to date."); } }).catch(function(err) { var status = module.hot.status(); //如果报错直接全局reload if(["abort", "fail"].indexOf(status) >= 0) { console.warn("[HMR] Cannot apply update. Need to do a full reload!"); console.warn("[HMR] " + err.stack || err.message); window.location.reload(); } else { console.warn("[HMR] Update failed: " + err.stack || err.message); } }); }; var hotEmitter = require("./emitter"); //获取MyEmitter对象 hotEmitter.on("webpackHotUpdate", function(currentHash) { lastHash = currentHash; if(!upToDate() && module.hot.status() === "idle") { //调用module.hot.status方法获取状态 console.log("[HMR] Checking for updates on the server..."); check(); } }); console.log("[HMR] Waiting for update signal from WDS..."); } else { throw new Error("[HMR] Hot Module Replacement is disabled."); }
也就是说webpack/hot/dev-server相较于前面在入口文件中添加的”webpack/hot/only-dev-server”来说,区别在于后者传入的是哪些已经被更新的模块,也就是已经被自己模块本身dispose处理了。如下:
if (module.hot) { module.hot.accept(); // dispose handler module.hot.dispose(() => { window.clearInterval(intervalId); }); }
(3)如果你注意到上面其实我们还添加了一个client/index.js,这个客户端代码只是添加了我们的客户端的socket.js代码,这时候我们客户端就可以获取到服务器端发送到的socket命令
var onSocketMsg = { //设置hot为true hot: function() { hot = true; log("info", "[WDS] Hot Module Replacement enabled."); }, //打印invalid invalid: function() { log("info", "[WDS] App updated. Recompiling..."); sendMsg("Invalid"); }, //设置hash hash: function(hash) { currentHash = hash; }, //继续可用 "still-ok": function() { log("info", "[WDS] Nothing changed.") if(useWarningOverlay || useErrorOverlay) overlay.clear(); sendMsg("StillOk"); }, //设置log级别 "log-level": function(level) { logLevel = level; }, /* Shows a full-screen overlay in the browser when there are compiler errors or warnings. Disabled by default. If you want to show only compiler errors: overlay: true If you want to show warnings as well as errors: overlay: { warnings: true, errors: true } */ "overlay": function(overlay) { if(typeof document !== "undefined") { if(typeof(overlay) === "boolean") { useWarningOverlay = overlay; useErrorOverlay = overlay; } else if(overlay) { useWarningOverlay = overlay.warnings; useErrorOverlay = overlay.errors; } } }, //ok ok: function() { sendMsg("Ok"); if(useWarningOverlay || useErrorOverlay) overlay.clear(); if(initial) return initial = false; reloadApp(); }, //客户端检测到服务器端有更新,通过chokidar检测到文件的变化 "content-changed": function() { log("info", "[WDS] Content base changed. Reloading...") self.location.reload(); }, warnings: function(warnings) { log("info", "[WDS] Warnings while compiling."); var strippedWarnings = warnings.map(function(warning) { return stripAnsi(warning); }); sendMsg("Warnings", strippedWarnings); for(var i = 0; i < strippedWarnings.length; i++) console.warn(strippedWarnings[i]); if(useWarningOverlay) overlay.showMessage(warnings); if(initial) return initial = false; reloadApp(); }, errors: function(errors) { log("info", "[WDS] Errors while compiling. Reload prevented."); var strippedErrors = errors.map(function(error) { return stripAnsi(error); }); sendMsg("Errors", strippedErrors); for(var i = 0; i < strippedErrors.length; i++) console.error(strippedErrors[i]); if(useErrorOverlay) overlay.showMessage(errors); }, //发送消息close close: function() { log("error", "[WDS] Disconnected!"); sendMsg("Close"); } }; socket(socketUrl, onSocketMsg);
module.hot等相关方法什么时候被调用
其实通过上面的分析,我们肯定有一点疑问就是,我们的这些module.hot等方法是在什么时候调用的,其实看看HotModuleReplacementPlugin就明白了,下面贴出一部分代码:parser.plugin("call module.hot.accept", function(expr) { if(!this.state.compilation.hotUpdateChunkTemplate) return false; if(expr.arguments.length >= 1) { var arg = this.evaluateExpression(expr.arguments[0]); var params = [], requests = []; if(arg.isString()) { params = [arg]; } else if(arg.isArray()) { params = arg.items.filter(function(param) { return param.isString(); }); } if(params.length > 0) { params.forEach(function(param, idx) { var request = param.string; var dep = new ModuleHotAcceptDependency(request, param.range); dep.optional = true; dep.loc = Object.create(expr.loc); dep.loc.index = idx; this.state.module.addDependency(dep); requests.push(request); }.bind(this)); if(expr.arguments.length > 1) this.applyPluginsBailResult("hot accept callback", expr.arguments[1], requests); else this.applyPluginsBailResult("hot accept without callback", expr, requests); } } }); parser.plugin("call module.hot.decline", function(expr) { if(!this.state.compilation.hotUpdateChunkTemplate) return false; if(expr.arguments.length === 1) { var arg = this.evaluateExpression(expr.arguments[0]); var params = []; if(arg.isString()) { params = [arg]; } else if(arg.isArray()) { params = arg.items.filter(function(param) { return param.isString(); }); } params.forEach(function(param, idx) { var dep = new ModuleHotDeclineDependency(param.string, param.range); dep.optional = true; dep.loc = Object.create(expr.loc); dep.loc.index = idx; this.state.module.addDependency(dep); }.bind(this)); } }); parser.plugin("expression module.hot", function() { return true; }); }); });
也就是我们关注的这些module.hot.decline方法都是在Parser上封装的!
如何写出支持HMR的代码
这里就是一个例子,你也可以查看这个仓库,然后克隆下来,执行”node ./bin/wcf –dev”命令,你就会发现访问localhost:8080的时候代码是可以支持HMR(你可以修改test目录下的所有的文件),而不会出现页面刷新的情况。import * as dom from './dom';
import * as time from './time';
import pulse from './pulse';
require('./styles.scss');
const UPDATE_INTERVAL = 1000; // milliseconds
const intervalId = window.setInterval(() => {
dom.writeTextToElement('upTime', time.getElapsedSeconds() + ' seconds');
dom.writeTextToElement('lastPulse', pulse());
}, UPDATE_INTERVAL);
// Activate Webpack HMR
if (module.hot) { module.hot.accept(); // dispose handler module.hot.dispose(() => { window.clearInterval(intervalId); }); }
其中accept函数签名如下:
accept(dependencies: string[], callback: (updatedDependencies) => void) => void accept(dependency: string, callback: () => void) => void //直接接受当前模块某一个依赖模块的HMR
此时表示,我们这个模块支持HMR,任何其依赖的模块变化都会被捕捉到。当依赖的模块更新后回调函数被调用。当然,如果是下面这种方式:
accept([errHandler]) => void
那么表示我们接受当前模块所有依赖的模块的代码更新,而且这种更新不会冒泡到父级中去。这当我们模块没有导出任何东西的情况下有用(比如entry)。
其中decline函数
上面的例子中我们的dom.js是如下方式写的:import $ from 'jquery'; export function writeTextToElement(id, text) { $('#' + id).text(text); } if (module.hot) { module.hot.decline('jquery');//不接受jquery更新 }
其中decline方法签名如下:
decline(dependencies: string[]) => void decline(dependency: string) => void
这表明我们不会接受特定模块的更新,如果该模块更新了,那么更新失败同时失败代码为”decline”。而上面的代码表明我们不会接受jquery模块的更新。当前也可以是如下模式:
decline() => void
这表明我们当前的模块是不会更新的,也就是不会HMR。如果更新了那么错误代码为”decline”;
其中dispose函数
函数签名如下:dispose(callback: (data: object) => void) => void addDisposeHandler(callback: (data: object) => void) => void
这表示我们会添加一个一次性的处理函数,这个函数在当前模块更新后会被调用。此时,你需要移除或者销毁一些持久的资源,如果你想将当前的状态信息转移到更新后的模块中,此时可以添加到data对象中,以后可以通过module.hot.data访问。如下面的例子用于保存指定模块实例化的时间,从而防止模块更新后数据丢失(刷新后还是会丢失的)。
let moduleStartTime = getCurrentSeconds(); function getCurrentSeconds() { return Math.round(new Date().getTime() / 1000); // return new Date().getTime() / 1000; } export function getElapsedSeconds() { return getCurrentSeconds() - moduleStartTime; } // Activate Webpack HMR if (module.hot) { const data = module.hot.data || {}; // Update our moduleStartTime if we are in the process of reloading if (data.moduleStartTime) moduleStartTime = data.moduleStartTime; // dispose handler to pass our moduleStart time to the next version of our module // 首次进入我们把当前时间保存到moduleStartTime中以后就可以直接访问 module.hot.dispose((data) => { data.moduleStartTime = moduleStartTime; }); }
hotUpdateChunkFilename vs hotUpdateMainFilename
当你修改了test目录下的文件的时候,比如修改了scss文件,此时你会发现在页面中多出了一个script元素,内容如下:<script type="text/javascript" charset="utf-8" src="0.188304c98f697ecd01b3.hot-update.js"></script>
其中内容是:
webpackHotUpdate(0,{ /***/ 15: /***/ (function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__(46)(); // imports // module exports.push([module.i, "html {\n border: 1px solid yellow;\n background-color: pink; }\n\nbody {\n background-color: lightgray;\n color: black; }\n body div {\n font-weight: bold; }\n body div span {\n font-weight: normal; }\n", ""]); // exports /***/ }) }) //# sourceMappingURL=0.188304c98f697ecd01b3.hot-update.js.map
从内容你也可以看出,只是将我们修改的模块push到exports对象中!而hotUpdateChunkFilename就是为了让你能够执行script的src中的值的!而同样的hotUpdateMainFilename是一个json文件用于指定哪些模块发生了变化,在output目录下。
webpack和webpack-dev-server关系
webpack首先打包成文件放在具体的目录下,并通过publicPath配置成了虚拟路径,而当通过URL访问服务器的时候就会从req.url寻找了具体的输出文件,最后得到这个输出文件并原样发送到客户端。看下面的方法你就明白了:var pathJoin = require("./PathJoin"); var urlParse = require("url").parse; function getFilenameFromUrl(publicPath, outputPath, url) { var filename; // localPrefix is the folder our bundle should be in // 第二个参数如果为false那么查询字符串不会被decode或者解析 // 第三个参数为true,那么//foo/bar被解析为{host: 'foo', pathname: '/bar'},也就是第一个"//"后,'/'前解析为host // 如配置为 publicPath: "/assets/"将会得到下面的结果: /* Url { protocol: null, slashes: null, auth: null, host: null, port: null, hostname: null, hash: null, search: null, query: null, pathname: '/assets/ path: '/assets/', href: '/assets/' } */ var localPrefix = urlParse(publicPath || "/", false, true); var urlObject = urlParse(url); //URL是http请求的真实路径,如http://localhost:1337/hello/world,那么req.url得到的就是/hello/world // publicPath has the hostname that is not the same as request url's, should fail // 访问的url的hostname和publicPath中配置的host不一致,直接返回。这只有在publicPath是绝对URL的情况下出现 if(localPrefix.hostname !== null && urlObject.hostname !== null && localPrefix.hostname !== urlObject.hostname) { return false; } // publicPath is not in url, so it should fail // publicPath和req.url必须一样 if(publicPath && localPrefix.hostname === urlObject.hostname && url.indexOf(publicPath) !== 0) { return false; } // strip localPrefix from the start of url // 如果url中的pathname和publicPath一致,那么请求成功,文件名为urlObject中除去publicPath那一部分的结果 // 如上面/hello/world表示req.url,而且publicPath为/hello/那么得到的文件名就是world if(urlObject.pathname.indexOf(localPrefix.pathname) === 0) { filename = urlObject.pathname.substr(localPrefix.pathname.length); } if(!urlObject.hostname && localPrefix.hostname && url.indexOf(localPrefix.path) !== 0) { return false; } // and if not match, use outputPath as filename //如果有文件名那么从output.path中获取该文件,文件名为我们获取到的文件名。否则返回我们的outputPath //也就是说:如果没有filename那么我们直接获取到我们的output.path这个目录 return filename ? pathJoin(outputPath, filename) : outputPath; } module.exports = getFilenameFromUrl;
上面这个从URL到路径的转化就是通过webpack-dev-middleware来完成的。
参考资料:
webpack-dev-middlware
wcf编译工具开发
相关文章推荐
- Webpack devServer中的 proxy 实现跨域的解决
- 一步步深入学习webpack(入门困惑express和dev-server区别及分别使用dev-server和webpack-hot-middleware实现的热加载区别)
- webpack-dev-server启动后, localhost:8080返回index.html的原理
- Webpack devServer中的 proxy 实现跨域
- webpack webpack-dev-server使用指南
- webstorm修改文件,webpack-dev-server不会自动编译刷新
- webstorm修改文件,webpack-dev-server不会自动编译刷新
- 【简单Web服务器搭建】基于Socket实现的最简单的Web服务器【ASP.NET原理分析】
- 借助webpack-dev-server让我们的代码热编译
- Linux Ubuntu 下webpack无法热重载(webpack-dev-server webpack-watch)
- express:webpack dev-server中如何将对后端的http请求转到https的后端服务器中?
- webpack-dev-server
- 也谈WEB打印(二):简单的分析一下IE的打印原理并实现简单的打印和预览
- webpack-dev-server+vue构建开发环境(一)
- webpack-dev-server
- hadoop单线程实现server多socket连接读取数据原理分析
- 分析shell命令中 >/dev/null 2>&1的语法实现原理
- 基于webpack的前端工程化开发解决方案探索(三):webpack-dev-server
- webpack-dev-server使用方法
- hadoop单线程实现server多socket连接读取数据原理分析