当前位置:首页 > 数据库

【Webpack】devServer 实验报告

 

Choosing a Development Tool

Webpack 的实验使用目前已经是前端开发工程师必备技能之一。若是报告想在本地环境启动一个开发服务快速开发我们的应用(而不是每次 coding 完,手动执行 run build,实验全量打包),报告大家只需在 Webpack 的实验配置中,增加 devServer 的报告配置即可。它的实验作用主要是用来伺服资源文件。webpack-dev-server(以下简称 wds) 已经为我们封装好了全面、报告丰富且可配置化的实验功能,配置工程师们只需通过 webpack.config 和 命令行参数 即可满足开发所需。报告 然而配置工程师们,实验发现 wds 的报告 hot、live reload 实际上相当于启用了一个 express 的实验 Http 服务器 + webpack-dev-middleware(以下简称 wdm) 等中间件。这个 Http 服务器 和 用户访问服务的报告 client 可以通过 websocket 通讯协议建立长连接,在 webpack watch 到原始文件作出改动后,实验wds 会使用 webpack 的实时编译,再用 wdm 将 webpack 编译后文件会输出到内存中。每当应用程序请求一个文件时,wdm 匹配到了就把内存中缓存的对应结果以文件的亿华云计算格式返回给 client ,反之则进入到下一个中间件。 如果想要使用更多 wds 提供的配置功能,比如 proxy、static、open 等, 在 server 端增加中间件即可,这样配置工程师摇身一变,配置开发工程师! 项目中的 devServer 我们更多是使用 webpack + express + webpack-dev-middleware + webpack-hot-middleware 的组合来完成 HMR。在系列文章中,有更加具体详细的学习分享来介绍这些,这里收缩一下,我们只关注 webpack-dev-server。

wds 在宏观世界的部分特性

Use webpack with a development server that provides live reloading. This should be used for development only. It uses webpack-dev-middleware under the hood, which provides fast in-memory access to the webpack assets.

webpack 配合一个开发服务器,可以提供热重载功能。但只用于开发模式下。 wds 的底层,集成了 wdm,可以提供快速内存访问打包资源的功能。

以上是 wds 对自己的一个简短自我介绍,我们来搞清楚它这么概括的点:

1. webpack 配合一个开发服务器,可以提供热重载功能。亿华云

webpack 可以通过 watch mode 的方式启动,指示 webpack watch 依赖图中所有文件的更改,并且自动打包。但是每次打包后的结果将会存储到本地硬盘中,而 IO 操作是非常耗资源时间的,无法满足本地开发调试需求。 wds 则可以提供一个开发服务器,并且提供 live reloading(实时重载)功能,在打包完成后通知客户端,刷新页面重新加载资源。

2. 快速内存访问打包资源

wdm 可以将 webpack 编译后的资源输出到内存中,当应用程序请求资源时,可以直接从内存中进行响应。 开发中,我们还会注意到,在编译期间,客户端的请求会被 delay 到最新的编译结果完成之后才会去响应。

「wdm」: wait until bundle finished: /myapp/ 

3. HMR 模块热替换

日常开发修改完代码后,你有没有傻乎乎地,手动刷新调试页面,来验证现在的 bug 还是不是之前的服务器租用那个 bug? 其实 HMR 会在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。 可以通过以下几种方式,来显著提速开发效率:

保留在完全重新加载页面期间丢失的应用程序状态 只更新变更内容,以节省宝贵的开发时间 在源代码中对 CSS/JS 进行修改,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式

4. 服务启动后自动启动浏览器 - open

有时候服务启动后,会打开 localhost:8080,或者打开浏览器的多个页签

5. 提供 history api 降级方案 - historyApiFallback

前端项目中,借助 history api,可以做到改变视图而不向后端发出请求。如果你手动刷新一个路由中匹配不到的页面,同时你的项目中没有配置 404 页面兜底逻辑,那就真的 404 Not Found 了。 wds 配置中有一项配置 historyApiFallback,可以配置一个页面代替所有的 404 响应。

「wds」: 404s will fallback to /index.html 

6. 提供代理功能 - proxy

wds 的 proxy配置使用方法,可以详见 Webpack-dev-server 的 proxy 用法。

如果你有一个单独的后台 API 服务,你可以通过代理,把前端项目域名下发起的 API 请求,代理到后台的域名。 解决开发环境中的跨域问题 通过代理你还可以定制返回的 html 页面,比如同一个项目中你想要提供 PC、H5 两端的产物,通过 UA 判断,返回不同的打包产物 index.html or index.mobile.html

7. 代码打包编译出现警告和错误时,会在在页面上显示错误信息 - overlay

控制代码打包编译时出现警告和错误时,是否在页面上显示错误信息

8. output.path、output.publicPath、devServer.publicPath、devServer.contentBase

output.path 打包输出产物的绝对路径 output.publicPath 它会为所有打包后的资源指定一个基础路径,多用于静态资源服务器或者 CDN 托管静态资源 devServer.publicPath 挂载到服务器中间件的可访问虚拟地址 devServer.contentBase 加载这个配置下(文件夹下)的静态资源到服务器 output.path 打包产物的绝对路径,没有什么疑问。对于 output.publicPath、devServer.publicPath,还是有点迷惑不解吧? 假如有一个域名 example.com,但是你们应用部署在 example.com/myapp/ 。没有指定 output.publicPath,默认为 /,这时 index.html 引用其他模块的 url 会是 /bundle.xxxhashxxx.js,这时这个资源的 url 就变成了 example.com/bundle.xxxhashxxx.js,毫无疑问,这个资源会 404 Not Found。如果指定 output.publicPath: /myapp/,那么 index.html 中资源的 url 就变成了 /myapp/bundle.xxxhashxxx.js。 同理 wds 中,指定 devServer.publicPath: /myapp/,devServer 就会在 http://localhost:8080/myapp/ 下伺服资源访问。模拟生产环境下的运维配置。 「wds」: webpack output is served from /myapp/ 

contentBase 呢?它只作用于 wds,只有你想要伺服静态资源文件的时候使用。换句话说,wds 会加载这个文件夹下的静态资源到服务器,而不需要 bundle 这个文件夹。 假如,你的 app 中需要加载一些 mp4 文件,这些文件基本不会被改动,所以你不必把这些资源打包到 /dist 文件下,可以把这些文件维护在 /src、/dist 的同级目录下的 /static。然后设置 contentBase: path.join(__dirname, static),然后就可以在代码中这样引用静态资源了。

「wds」: Content not from webpack is served from /Volumes/bomb/git/webpack-learning/webpack-demo/static 

9. more

构建满足这些特性的自洽模型

为了验证我们构建的自洽模型,能够自洽,我们需要一个参照物来进行修正。 我们使用 devServer 官方配置,来伺服资源文件。 为了不影响体验,自洽模型 和 参考物 的代码都维护在第四节的参考,有兴趣的可以自己 debugger 一下。

1. 模拟 http 服务器

首先我们使用 express 启动一个本地 server,让浏览器可以访问本地的静态资源。

// wds.server.js const app = express(); const listeningApp = http.createServer(app); listeningApp.listen(8888, 127.0.0.1, (err) => {    createSocketServer(); }); 

这里创建 http 服务器,没有使用 app.listen(8888, callback),而是使用 http.createServer(app) 的原因有两点:

在创建 websocket server(代码片段中的 createSocketServer)时,需要复用 http 服务器实例 listeningApp,在下一小节会介绍 wss

express 只返回 http 服务器实例,而 devServer 是支持配置 https 的,所以可以直接用 https.createServer(app),更加方便

2. 模拟监听代码文件更新

wds 调用 webpack api 对文件系统进行 watch,当文件发生改变后,webpack 会重新对文件进行编译打包,然后保存到内存中。 这一系列操作,主要有两点:1、watch 文件更改;2、内存响应。所幸,wdm 完成了这部分功能,我们在自洽模型中直接引用 wdm 。

// wds.server.js const webpack = require(webpack); const wdm = require(webpack-dev-middleware); const config = require(./webpack.config.js); const compiler = webpack(config); // 将 webpack.config.js 配置文件作为基础配置 const app = express(); app.use(wdm(compiler)) // 告知 express 使用 webpack-dev-middleware 

这里不难看出,wdm(compiler) 的执行结果返回的是一个中间件,它将 webpack 编译后的文件存储到内存中,然后在用户访问 express 服务时,将内存中对应的资源输出返回。 那么 wdm 内部是如何实现的呢? wdm 的源码并不多,其核心只有 /index.js,/lib/middleware。

// webpack-dev-middleware/index.js // 在 compiler 的 invalid、run、done、watchRun 这 4 个编译生命周期上,注册对应的处理方法。 // 通过 tapable 来调用插件功能,主要是 report 编译的状态信息以及执行 context.callbacks 回调函数 const context = createContext(compiler, options); ... // 以监控的方式启动 webpack,调用 compiler 的 watch 方法,之后 webpack 便会监听文件变更,一旦检测到文件变更,就会重新执行编译。 context.watching = compiler.watch(options.watchOptions, (err) => {  ... }); ... // 使用 memory-fs,将 webpack 的编译内容,输出至内存中 setFs(context, compiler); 

// webpack-dev-middleware/lib/middleware // 核心逻辑是:针对 request 请求,根据各种条件判断,最终返回对应的文件 module.exports = function wrapper(context) {    // 返回 express 中间件函数的包装函数   return function middleware(req, res, next) {      // 如果不是 SSR,直接 next,流转到下一个中间件     // 如果是 SSR,调用 util/ready,根据 state 判断执行回调 fn,还是将 fn 存储到 callbacks 队列中     // ready 也是“在编译期间,停止提供旧版的 bundle 并且将请求延迟到最新的编译结果完成之后”的实现    function goNext() {  ... }     // 根据请求的 req.url 地址,在 compiler 的内存文件系统中查找对应的文件,若查找不到,则直接调用 goNext() 方法处理请求     let filename = getFilenameFromUrl( ... )     if (filename === false) {        return goNext();     }     // 根据上文找到的 filename 路径获取到对应的文件内容,并构造 response 对象返回     // 最后也是调用 ready     return new Promise((resolve) => {       handleRequest(context, filename, processRequest, req);       function processRequest() {         ...       }     })   } } 

3. 模拟 server 端:server 端 通知 client 端 文件发生改变

使用 HMR 的过程中,通过 network 我们知道 client 端 是通过 websocket 和 server 端 进行通信的。client 端 和 server 端 之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知 client 端。最关键的还是 wds 注册 compiler hooks(compile、done、watchRun 等),当进入 webpack compile 生命周期时调用 hooks 回调注册方法。 compilation done 时,server 端 传递的最主要的信息是 stats.hash 和 ok,然后 client 端 根据 hash 进行模块热更新。

// wds.server.js let connect = null // 长连接实例 // 调用 webpack api 监听 compile 的 done 事件 // 注册 compiler hooks -- done const {  done } = compiler.hooks done.tap(myappPligins, (stats) => {    if (connect) {      let _stats = stats.toJson({ ...})     // 将编译打包后的新模块 hash 值发送到 client 端     connect.write(JSON.stringify({        "type": "hash",       "data": _stats.hash     }))     // 通知 client 端编译完成,可以进行 reloadApp 操作     connect.write(JSON.stringify({        "type": "ok"     }))   } }); // 创建 websocket server(wss) // 目前 webpack-dev-server@4.X 使用 sockjs 会出错,webpack-dev-server@3.X 使用 ws 会报错 function createSocketServer () {    let socket = sockjs.createServer({      sockjs_url: ./sockjs-client   });   // 复用 http 服务器实例 listeningApp   socket.installHandlers(listeningApp, {      prefix: /sockjs-node,   });   socket.on(connection, (connection) => {      connect = connection     ...   }); } 

4. client 端接收 wss 消息并触发响应

我们在业务代码中并没有添加接收 wss 消息的代码,那 client 端 的逻辑怎么实现的呢? 其实 wds 修改了 webpack.config.js 的基础配置,它会往 chunk 中偷偷塞入两个文件 webpack-dev-server/lib/client/index.js 和 webpack/hot/dev-server。 我们在自洽模型中也这么操作,这样这两段代码就植入到 client 端了。

// wds.server.js config.entry.app = [require.resolve(webpack/hot/dev-server), ./wds.client.js, config.entry.app] // HMR 作为一个 Webpack 内置的功能,可以通过 HotModuleReplacementPlugin 开启 config.plugins.push(   new webpack.HotModuleReplacementPlugin() ) 

client 端 通过 websocket 接收 server 端 最新编辑后的模块 hash 值,这个值会被存起来(currentHash),在接收到 ok 后才会 reloadApp。 如果配置了 hot,开启 HMR,会把程序控制权交给 webpack 的客户端代码进行 HMR。如果没有开启,就直接调用 location.reload() 刷新页面。

// wds.client.js const SockJS = require(./sockjs-client) const socketUrl = http://127.0.0.1:8888/sockjs-node let currentHash =  // 最新代码模块的 hash 值 function reloadApp () {    if (options.hot) {      let hotEmitter = require(webpack/hot/emitter);     // webpackHotUpdate 是 webpack 在 webpack/hot/dev-server.js 定义的一个事件,事件回调是获取此次编译的最新代码     hotEmitter.emit(webpackHotUpdate, currentHash);   } else if (options.liveReload) {  // 没有配置 hmr,就直接 live reload 刷新页面     location.reload();   } } // 处理 wss 通知 const onSocketMessage = {    ...   hash: function hash(_hash) {      currentHash = _hash; // wss 端 通知 client 端 最新编辑后的模块 hash 值,这个值会被存起来(currentHash),在接收到 ok 后才会 reloadApp   },   ok: function ok() {      reloadApp();   } }; const socket = (url, handlers) => {    client = new SockJS(url)  ...   client.onmessage = function (data) {  // 接收 wss 通知     var msg = JSON.parse(data.data);     if (handlers[msg.type]) {        handlers[msg.type](msg.data);     }   }   ... } socket(socketUrl, onSocketMessage) 

5. 模拟 HMR or live reload当 client 端 收到 ok 的通知后,开启 hot 的 wds,会执行 reload 方法,然后调用 webpackHotUpdate

// wds.client.js let hotEmitter = require(webpack/hot/emitter); hotEmitter.emit(webpackHotUpdate, currentHash); 

然后程序被 client 端 的 webpack 接管(第四步中我们注入到 plugins 中的 webpack.HotModuleReplacementPlugin 就派上用场了),webpack 监听到 webpackHotUpdate 事件,并获取到最新的 hash 值,然后开始检查更新。

// webpack/hot/dev-server.js var hotEmitter = require("./emitter"); hotEmitter.on("webpackHotUpdate", function (currentHash) {    lastHash = currentHash;   ...   check(); }); // 检查更新 var check = function check() {    module.hot.check(true)     .then(updatedModules => {        ...     }) } 

源码中,追踪到 module.hot.check,就不知道路该怎么走了,hot.check 是哪里来的? 系列文章中有单独介绍 HMR 的一章,这里我们就偷个懒,粗线条的勾勒一下大致过程。 hot.check 来自于 /webpack/lib/hmr/HotModuleReplacement.runtime.js。

利用上一次保存的hash值,调用 hotDownloadManifest 发送 xxx.hash.hot-update.json 的 ajax 请求

请求结果获取热更新模块相关信息,并进入热更新准备阶段。

c: chunkIds m: removedChunks r: removedModules

通过 JSONP 的方式,调用 loadUpdateChunk 在 document.head 添加 script 标签,发送 chunkId.hash.hot-update.js 请求。

下面就是拿到的 hot-update.js 的内容。

JSONP 返回的 js 文件立即执行,会调用 window.webpackHotUpdatewebpack_demo 方法。此方法会把更新的模块 moreModules (图中入参的第二个参数对象)赋值给全局全量 currentUpdate。

最后会调用 HMR runtime 的 hotApply 进行热更新模块替换

6. 模拟 history api 降级方案

// wds.server.js // 添加中间件 connect-history-api-fallback,解决 history api 降级 app.use(historyApiFallback({    htmlAcceptHeaders: [text/html, application/xhtml+xml], // 只对这些类型的请求进行 rewrite   rewrites: [     {  from: /./, to: /myapp/index.html }   ] })) 

7. 模拟 proxy 代理

proxy 配置,我们经常使用,那 node 是如何代理请求的呢?

在 wds 中,借助创建的 http 服务器,其 proxy 功能的实现就是解析配置项,并挂载 http-proxy-middleware 中间件到 http 服务上。结合 proxy 的用法,wds/lib/Server.js 中的代码显得一目了然。 http-proxy-middleware 则借助于 node-http-proxy,用于将 node 服务端接收到的请求,转发到目标服务器,实现代理服务器的功能。 可以预见,整个流程的大致实现思路就是,通过配置项注册全局的请求转发规则,在中间件中拦截客户端的 request 请求匹配转发规则,然后调用 node-http-proxy 的 .web、.ws 方法进行转发请求。 http-proxy-middleware 将转发规则分为两大类进行配置,context 和 options。

// Proxy middleware configuration. var proxy = require(http-proxy-middleware); var apiProxy = proxy(/api, {  target: http://www.example.org }); //                   \____/   \_____________________________/ //                     |                    | //                   context             options // apiProxy is now ready to be used as middleware in a server. 

context 用于匹配需要进行转发的客户端请求,默认值是 /,客户端发起的所有请求都会被转发;也可以字符串 url、字符串 url 数组、通配符或者个性化方法,来决定哪些请求会被代理转发。http-proxy-middleware 使用 options 中

target 用于设置要转发到的目标服务器的 host; pathRewrite: object/function,用于改写要转发到的目标服务器的 url path; router: object/function,根据配置,将匹配的客户端请求,改写这次请求的 host。 // rewrite path pathRewrite: { ^/old/api : /new/api} // remove path pathRewrite: { ^/remove/api : } // add base path pathRewrite: { ^/ : /basepath/} // custom rewriting pathRewrite: function (path, req) { } router: {      integration.localhost:3000 : http://localhost:8001,  // host only     staging.localhost:3000     : http://localhost:8002,  // host only     localhost:3000/api         : http://localhost:8003,  // host + path     /rest                      : http://localhost:8004   // path only } 

// http-proxy-middleware/lib/index.js function HttpProxyMiddleware(context, opts) {   ...   var config = configFactory.createConfig(context, opts) // 解析获取 context、options   ...   var proxy = httpProxy.createProxyServer({ }) // 创建代理服务器,由这个服务器进行转发请求   ...   var pathRewriter = PathRewriter.create(proxyOptions.pathRewrite) // 将客户端请求路径转化为目标服务器的路径(pathname 部分),既可以是 key-value,也可以函数。   ...   function shouldProxy(context, req) {  // 判断请求是否需要转发     var path = req.originalUrl || req.url     return contextMatcher.match(context, path, req) // 通过多种匹配方法校验客户端 req 是否需要转发   }   function prepareProxyRequest(req) {      req.url = req.originalUrl || req.url     var originalPath = req.url     var newProxyOptions = _.assign({ }, proxyOptions)     __applyRouter(req, newProxyOptions) // 遍历 options.router,校验是否匹配客户端 req,匹配的话就改写这次请求的 host     __applyPathRewrite(req, pathRewriter) // 如果有 pathRewriter,就匹配当前请求,匹配的话就将设置的目标服务器路径写入 req.url     return newProxyOptions   }   ...   function middleware(req, res, next) {  // 真正的代理中间件     if (shouldProxy(config.context, req)) {        var activeProxyOptions = prepareProxyRequest(req)       proxy.web(req, res, activeProxyOptions) // node-http-proxy 进行代理转发     } else {        next()     }   }   ...   return middleware } 

这样看来,http-proxy-middleware 主要做的是解析转发规则、最终把代理转发的事情交给了 node-http-proxy,同时配置了相关的 Logger、绑定事件。

解析获取 context、options,并配置 Logger 实例

通过 node-http-proxy 创建代理服务器,并 attach proxy-events

根据 options.pathRewrite 生成路径转化器

匹配客户端请求,通过代理服务器转发 http, https, websocket 请求

自洽实是自娱自乐

本文从开发过程中遇到的痛点出发,梳理了现代打包工具对我们日常开发的帮助和提效,并自娱自乐的结合表现和源码,照虎画猫完成了所谓的自洽模型。其实自洽模型画的远不如猫,最多就是一个四支腿生物的简笔画了。权当梳理了一遍 wds 的工作流程,更加细节的东西,还需要大家一起动手才能挖掘出来,希望能对你的理解过程起到一定的帮助作用。

参考

参考一:社区文章

webpack 官网 publicPath、contentBase 十分钟搞懂 webpack 轻松理解 webpack 热更新原理 再不怕被问到 HMR wdm 源码解读 Webpack-dev-server 的 proxy 用法 知乎 HMR 原理解析 关于 tapable 你需要知道这些 相关包的版本:

"webpack": "5.24.0", "webpack-cli": "4.5.0", "webpack-dev-server": "^3.11.2"

参考二:自洽模型的参照物

// webpack.config.js 使用通用的 Vue 项目配置 module.exports = {    mode: development,   entry: {      app: ./src/app.js   },   output: {      filename: [name].bundle.js,     path: path.resolve(__dirname, dist),     publicPath: /myapp/   },   devtool: inline-source-map,   plugins: [   ...some plugins   ],   module: {      rules: [ ...some loaders ]   } }; 

然后把 devServer 的配置单独维护在 webpack.dev.js

// webpack.dev.js devServer 配置 module.exports = merge([   base, // webpack.config.js   {      mode: development,     devServer: {        host: 127.0.0.1, // 服务器 host,默认为 localhost       port: 7777, // 服务器端口号,默认为 8080       open: true, // string | boolean,启动后是否打开浏览器,当为字符串时,打开指定浏览器       openPage: myapp/, // string | Array<string>, [, index.html], index.html, 打开浏览器后默认打开的页面,Array 打开多个页面       compress: true,       hot: true, // 是否启动热更新(HMR),热更新使用的是 webpack 中 HotModuleReplacementPlugin       http2: false, // 是否设置 HTTP/2 服务器,为 true,则默认使用 https 作为服务       // https: {        //   key: ,//fs.readFileSync(/path/to/server.key),       //   cert: ,//fs.readFileSync(/path/to/server.crt),       //   ca: ,//fs.readFileSync(/path/to/ca.pem)       // },       proxy: {          /api: {            target: http://localhost:7777,           pathRewrite: {  ^/api:  },           secure: false // HTTPS 设置为无效证书         }       },       // 静态文件属性       publicPath: /myapp/, // 挂载到服务器中间件的可访问虚拟地址       contentBase: path.join(__dirname, static), // devServer 伺服这个文件夹下的静态资源。换句话说会加载本地 /static 目录下的静态文件到服务器       stats: minimal,       // 设置编译出错或警告后,页面是否会直接显示信息, boolean | { }       // 默认为 false,当失败后会显示空白页       // 设置为 true 后,编译失败会显示错误/警告的覆盖层,也可以设置为 object,显示多种类型信息       overlay: {          warnings: true,         errors: true       },       injectClient: true, // 是否要注入 WebSocket 客户端,将此属性设置为 false,那么 hot、overlay 等功能都会失效       injectHot: true, // 是否注入 HMR, 这个属性是 injectClient 的子集。只影响热更新       liveReload: false, // 是否开启自动刷新浏览器功能,优先级低于 hot       // 是否将所有 404 页面都跳转到 index.html,当此属性设置为 true 或为 object 时并且使用 history API 时所有 404 页面会跳转到 index.html 或指定的页面       historyApiFallback: {          rewrites: [           {  from: /./, to: /myapp/index.html },         ]       },       //  设置 WebSocket,设置使用的 WebSocket 库,内置为 sockjs 或 ws       transportMode: {          //  目前 webpack-dev-server@4.X 使用 sockjs 会出错,webpack-dev-server@3.X 使用 ws 会报错         server: sockjs       }     }   } ]) 

package.json

// package.json "scripts": {    "start": "webpack serve --config webpack.dev.js",    "start:dev": "node wds.server.js", } 

参考三:自洽模型

// wds.server.js const express = require(express); const webpack = require(webpack); const http = require(http); const webpackDevMiddleware = require(webpack-dev-middleware); const historyApiFallback = require(connect-history-api-fallback); const sockjs = require(sockjs); const app = express(); const config = require(./webpack.config.js); config.entry.app = [require.resolve(webpack/hot/dev-server), ./wds.client.js, config.entry.app] config.plugins.push(   new webpack.HotModuleReplacementPlugin() ) // 告知 express 使用 webpack-dev-middleware, // 以及将 webpack.config.js 配置文件作为基础配置。 const compiler = webpack(config) // historyApiFallback app.use(historyApiFallback({    htmlAcceptHeaders: [text/html, application/xhtml+xml],   rewrites: [     {  from: /./, to: /myapp/index.html }   ] })) app.use(   webpackDevMiddleware(compiler, {      publicPath: config.output.publicPath,   }) ) let connect = null const {  done } = compiler.hooks done.tap(myappPligins, (stats) => {    if (connect) {      let _stats = stats.toJson({        all: false,       hash: true,       assets: true,       warnings: true,       errors: true,       errorDetails: false,     })     connect.write(JSON.stringify({        "type": "hash",       "data": _stats.hash     }))     connect.write(JSON.stringify({        "type": "ok"     }))   } }); const listeningApp = http.createServer(app); function createSocketServer () {    let socket = sockjs.createServer({      sockjs_url: ./sockjs-client   });   socket.installHandlers(listeningApp, {      prefix: /sockjs-node,   });   socket.on(connection, (connection) => {      if (!connection) {        return;     }     connect = connection     // 通知 client enable 了哪些功能     connection.write(JSON.stringify({        "type": "hot"     }))   }); } listeningApp.listen(8888, 127.0.0.1, (err) => {    console.log(Example app listening on port 8888!\n);   createSocketServer(); }); listeningApp.on(error, (err) => {    console.error(err); }); 

// wds.client.js console.log(this is from client.) const SockJS = require(./sockjs-client) const socketUrl = http://127.0.0.1:8888/sockjs-node const options = {    hot: true,   hotReload: true,   liveReload: false,   initial: true,   useWarningOverlay: false,   useErrorOverlay: false,   useProgress: false } let currentHash =  function reloadApp () {    if (options.hot) {      console.log([WDS] App hot update...);     let hotEmitter = require(webpack/hot/emitter);     hotEmitter.emit(webpackHotUpdate, currentHash);     // broadcast update to window     window.postMessage("webpackHotUpdate".concat(currentHash), *);   } else if (options.liveReload) {      location.reload();   } } const onSocketMessage = {    hot: function hot() {      options.hot = true;     console.info([WDS] Hot Module Replacement enabled.)   },   liveReload: function liveReload() {      options.liveReload = true;     console.info([WDS] Live Reloading enabled.)   },   invalid: function invalid() {      console.info([WDS] App updated. Recompiling...)   },   hash: function hash(_hash) {      currentHash = _hash;   },   ok: function ok() {      reloadApp();   },   close: function close() {      console.error([WDS] Disconnected!);   } }; let retries = 0 let client = null const socket = (url, handlers) => {    client = new SockJS(url)   client.onopen = function () {      retries = 0   }   client.onmessage = function (data) {      var msg = JSON.parse(data.data);     if (handlers[msg.type]) {        handlers[msg.type](msg.data);     }   }   client.onclose = function () {      if (retries === 0) {        handlers.close();     } // Try to reconnect.     client = null; // After 10 retries stop trying, to prevent logspam.     if (retries <= 10) {        var retryInMs = 1000 * Math.pow(2, retries) + Math.random() * 100;       retries += 1;       setTimeout(function () {          socket(url, handlers);       }, retryInMs);     }   } } socket(socketUrl, onSocketMessage) 

 【编辑推荐】

鸿蒙,就算套壳安卓又能怎样呢? Kubernetes为什么要弃用Docker? 从“PPT系统”走向现实:HarmonyOS,你真香了吗? 这6款Python IDE&代码编辑器,你都用过吗? Kubernetes实践之优雅终止

分享到:

滇ICP备2023006006号-16