Esbuild 虽然 bundler 非常快,但是其没有提供 HMR 的能力,在开发过程中只能采用 live-reload 的方案,一有代码改动,页面就需要全量 reload ,这极大降低开发体验。为此添加 HMR 功能至关重要。

经过调研,社区内目前存在两种 HMR 方案,分别是 Webpack/ Parcel 为代表的 Bundler HMR 和 Vite 为代表的 Bundlerless HMR。经过考量,我们决定实现 Bundler HMR,在实现过程中遇到一些问题,做了一些记录,希望大家有所了解。

ModuleLoader 模块加载器

Esbuild 本身具有 Scope hosting 的功能,这是生产模式经常会开启的优化,会提高代码的执行速度,但是这模糊了模块的边界,无法区分代码具体来自于哪个模块,针对模块的 HMR 更无法谈起,为此需要先禁用掉 Scope hosting 功能。由于 Esbuild 未提供开关,我们只能舍弃其 Bundler 结果,自行 Bundler。

受 Webpack 启发,我们将模块内的代码转换为 Common JS,再 wrapper 到我们自己的 Moduler loader 运行时,其中循环依赖的情况需要提前导出 module.exports 需要注意一下。

转换为 Common JS 目前是使用 Esbuild 自带的 transform,但需要注意几个问题。

  • Esbuild dynamic import 遵循 浏览器 target 无法直接转换 require,目前是通过正则替换 hack。

  • Esbuild 转出的代码包含一些运行时代码,不是很干净。

  • 代码内的宏(process.env.NODE_ENV 等)需要注意进行替换。

比如下面的模块代码的转换结果:

// a.ts
import { value } from 'b'// transformed to moduleLoader.registerLoader('a'/* /path/to/a */, (require, module, exports) => {const { value } = require('b');});
  • Cjs 动态导出模块的特性。

export function name(a) {return a + 1
}const a = name(2)
export default a

如上模块转换后结果如下:

var __defProp = Object.defineProperty;
var __export = (target, all) => {for (var name2 in all)__defProp(target, name2, { get: all[name2], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var entry_exports = {};
// 注意这里
__export(entry_exports, {default: () => entry_default,name: () => name
});
module.exports = __toCommonJS(entry_exports);
function name(a2) {return a2 + 1;
}
var a = name(2);
var entry_default = a;

注意两部分:

  1. 第 7 行代码可以看到,ESMCJS 后会给模块加上 __esModule 标记。

  2. 第 10 行代码中可以看到,CJS 的导出是 computed 的, module.exports 赋值时需要保留 computed 导出。

ModuleLoader 的实现注意兼容此行为,伪代码如下:

class Module {_exports = {}get exports() {return this._exports}set exports(value) {if(typeof value === 'object' && value) {if (value.__esModule) {this._exports.__esModule = true;}for (const key in value) {Object.defineProperty(this._exports, key, {get: () => value[key],enumerable: true,});}}}
}

由于 Scope Hosting 的禁用,在 bundler 期间无法对模块的导入导出进行检查,只能得到在运行期间的代码报错,Webpack 也存在此问题。

Module Resolver

虽然对模块进行了转换,但无法识别 alias,node_modules 等模块。

如下面例子, node 模块 b 无法被执行,因为其注册时是 /path/to/b

// a.ts
import { value } from 'b'

另外,由于 HMR API 接受子模块更新也需要识别模块。

module.hot.accpet('b', () => {})

有两种方案来解决:

  1. Module URL Rewrite

Webpack/Vite 等都采用的是此方案,对模块导入路径进行改写。

  1. 注册映射表

由于 Module Rerewrite 需要对 import 模块需要分析,会有一部分开销和工作量,为此采用注册映射表,在运行时进行映射。如下:

moduleLoader.registerLoader('a'/* /path/to/a */, (require, module, exports) => {const { value } = require('b');expect(value).equal(1);
});
moduleLoader.registerResolver('a'/* /path/to/a */, {'b': '/path/to/b'});

HMR

当某个模块发生变化时,不用刷新页面就可以更新对应的模块。

首先看个 HMR API 使用的例子:

// bar.js
import foo from './foo.js'foo()if (module.hot) {module.hot.accept('./foo.js' ,(newFoo) => {newFoo.foo()})
}

在上面例子中,bar.js./foo.js 的 HMR Boundary ,即接受更新的模块。如果./foo.js 发生更新,只要重新执行 ./foo.js 并且执行第七行的 callback 即可完成更新。

具体的实现如下:

  1. 构建模块依赖图。

在 ModuleLoader 过程中,执行模块的同时记录了模块之间的依赖关系。

img

如果模块中含有 module.hot.accept 的 HMR API 调用则将模块标记成 boundary。

img
  1. 当模块发生变更时,会重新生成此模块相关的最小 HMR Bundle,并且将其通过 websocket 消息告知浏览器此模块发生变更,浏览器端依据模块依赖图寻找 boundaries,并且开始重新执行模块更新以及相应的 calllback。

img

注意 HMR API 分为 接受子模块的更新接受自更新 ,在查找  HMR Boundray 的过程需要注意区分。

目前,只在 ModulerLoader 层面支持了 accpet dispose API。

Bundle

由于模块转换后没有先后关系,我们可以直接把代码进行合并即可,但是这样会缺少 sourcemap。

为此,进行了两种方案的尝试:

  1. Magic-string Bundle + remapping

伪代码如下:

import MagicString from 'magic-string';
import remapping from '@ampproject/remapping';const module1 = new MagicString('code1')
const module1Map = {}
const module2 = new MagicString('code2')
const module2Map = {}function bundle() {const bundle = new MagicString.Bundle();bundle.addSource({filename: 'module1.js',content: module1});bundle.addSource({filename: 'module2.js',content: module2});const map = bundle.generateMap({file: 'bundle.js',includeContent: true,hires: true});remapping(map, (file) => {if(file === 'module1.js') return module1Mapif(file === 'module2.js') return module2Mapreturn null})return {code: bundle.toString(),map: }
}

实现过后发现二次构建存在显著的性能瓶颈,remapping 没有 cache 。

  1. Webpack-source

伪代码如下:

import { ConcatSource, CachedSource, SourceMapSource } from 'webpack-sources';const module1Map = {}
const module1 = new CachedSource(new SourceMapSource('code1'), 'module1.js', module1Map)
const module2 = new CachedSource(new SourceMapSource('code2'), 'module2.js', module1Map)function bundle(){const concatSource = new ConcatSource();concatSource.add(module1)concatSource.add(module2)const { source, map } = concatSource.sourceAndMap();return {code: source,map,};
}

CacheModule 有每个模块的 sourcemap cache,内部的 remapping 开销很小,二次构建是方案一的数十倍性能提升。

另外,由于 esbuild 因为开启了生产模式的优化,metafile.inputs 中并不是全部的模块,其中没有可执行代码的模块会缺失,所以合并代码时需要从模块图中查找全部的模块。

Lazy Compiler(未实现)

页面中经常会包含 dynamic import 的模块,这些模块不一定被页面首屏使用,但是也被 Bundler,因此 Webpack 提出了 Lazy Compiler 。Vite 利用 ESM Loader 的 unbundler 天生避免了此问题。

React Refresh

What is React Refresh and how to integrate it .

和介绍的一样,分为两个过程。

  1. 将源代码通过 react-refresh/babel 插件进行转换,如下:

function FunctionDefault() {return <h1>Default Export Function</h1>;
}export default FunctionDefault;

转换结果如下:

var _jsxDevRuntime = require("node_modules/react/jsx-dev-runtime.js");
function FunctionDefault() {return (0, _jsxDevRuntime).jsxDEV("h1", {children: "Default Export Function"}, void 0, false, {fileName: "</Users/bytedance/bytedance/pack/examples/react-refresh/src/FunctionDefault.tsx>",lineNumber: 2,columnNumber: 10}, this);
}
_c = FunctionDefault;
var _default = FunctionDefault;
exports.default = _default;
var _c;
$RefreshReg$(_c, "FunctionDefault");

依据 bundler hmr 实现加入一些 runtime。

var prevRefreshReg = window.$RefreshReg$;
var prevRefreshSig = window.$RefreshSig$;
var RefreshRuntime = require('react-refresh/runtime');
window.$RefreshReg$ = (type, id) => {RefreshRuntime.register(type, fullId);
}
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
// source code
window.$RefreshReg$ = prevRefreshReg;
window.$RefreshSig$ = prevRefreshSig;
// accept self update
module.hot.accept();
const runtime = require('react-refresh/runtime');
let enqueueUpdate = debounce(runtime.performReactRefresh, 30);
enqueueUpdate();
  1. Entry 加入下列代码。

const runtime = require('react-refresh/runtime');runtime.injectIntoGlobalHook(window);window.$RefreshReg$ = () => {};window.$RefreshSig$ = () => type => type;

注意这些代码需要运行在 react-dom 之前。

- END -

【Web技术】1395- Esbuild Bundler HMR相关推荐

  1. Esbuild Bundler HMR

    Esbuild 虽然 bundler 非常快,但是其没有提供 HMR 的能力,在开发过程中只能采用 live-reload 的方案,一有代码改动,页面就需要全量 reload ,这极大降低开发体验.为 ...

  2. HTML5 Dashboard – 那些让你激动的 Web 技术

    HTML5 Dashboard 是一个 Mozilla 推出的项目,里面展示了最前沿的 HTML5,CSS3,JavaScript 技术.每一项技术都有简洁,在线演示以及详细的文档链接.这些技术将成为 ...

  3. Web技术栈中不可或缺的Linux技术

    随着第三次信息浪潮的冲击,web技术在近年来可谓发生了天翻地覆的变革.从单向信息的web1.0时代,逐步过渡到信息和人交互的web2.0再到数据主动与人发生关系的web3.0时代,这些成就无疑归功于W ...

  4. 一个html文档所需要的最基本的标记对是,川大《WEB技术》19秋在线作业1题目【标准答案】...

    <WEB技术>18秋在线作业1-0001 试卷总分:100  得分:100 一.单选题 (共 21 道试题,共 84 分) 1.Dreamweaver 是( )软件. A.网页编辑 B.字 ...

  5. Java Web技术经验总结(二)

    该系列的第一篇在此:Java Web技术经验总结一,主要包含我在日常工作中的经验和心得体会(如有不足之处欢迎指出). Maven的使用经验 依赖的scope有test.provided.compile ...

  6. 2011年使用率增长最快的十大Web技术

    W3techs 网站评出了 2011 年十大使用增速最快的 Web 技术,本文对其进行编译供各位参考.注意,该评选结果是在针对前 100 万流行网站(根据 Alexa 值统计)进行调查统计出的,点击这 ...

  7. 一款不错的基于WEB技术的文件服务器

    首先,让我感谢朋友ZHANGBIN给介绍的这个程序.一个不错的,只有500KB左右且免安装的小程序.     说明:这是一款不错的基于WEB技术的文件服务器,能够使用它进行文件的上传和下载.呵呵,如果 ...

  8. 深入分析Java Web技术内幕pdf

    下载地址:网盘下载 内容简介  · · · · · · <深入分析Java Web技术内幕(修订版)>新增了淘宝在无线端的应用实践,包括:CDN 动态加速.多终端化改造. 多终端Sessi ...

  9. java,php,asp,asp.net,ror等几种Web技术对比(第一版本)

    交一篇作业,自己对这些技术认识还并不深刻,如果有错的地方还请指点,不要介意. 目录 1.1  8月编程语言榜分析 1.2  Web开发语言技术分析 1.1  8月编程语言榜分析 我个人认为作为一个初学 ...

  10. 关于web技术的一些见解

    在目前的软件技术领域中,互联网方面的技术是其中最热门的一部分.现在做一个普通的网站,就涉及到大部分的web技术了:前端展示,后端数据处理,功能模块等.我觉得,也就分两个部分的技术:前端,后端. 前端, ...

最新文章

  1. BZOJ2275[Coci2010]HRPA——斐波那契博弈
  2. python 客户端 如何获取手机_Python学习---Django的request扩展[获取用户设备信息]
  3. 简单的REST的框架实现
  4. Hystrix文档-实现原理
  5. python如何删除代码_Python列表删除的三种方法代码分享
  6. JavaScript中函数的变量提升问题
  7. 使用lamba中stream 进行分组统计
  8. redis aof持久化遇到的Can't open the append-only file Permissi
  9. linux 安装git失败,Linux运维知识之linux下安装git常见故障整理
  10. Autodesk CAD 2023简体中文正式版
  11. 中国行业应用软件领域恶性循环的原因是什么?【转载】
  12. 诗一首,程序员不仅仅只会写程序
  13. react-ssr之路由配置
  14. 叔叔阿姨,我真的不会修电脑
  15. 手工植锡、焊接BGA芯片
  16. NYOJ_613_免费馅饼
  17. FFMPEG保存视频流数据至本地(rtsp转mp4)
  18. Windows7 打开任务计划提示“任务计划程序服务不可用。任务计划程序将尝试重新与其建立连接。”解决办法
  19. NC 完工报检单 推单 产成品入库单 批次问题
  20. 海龟交易 matlab,海龟交易系统是什么时候按2N止损,什么时候按10日止损,驴兄的见解!...

热门文章

  1. 《Pajek社会网络探索性分析》书籍简介
  2. 教你炒股票25:吻,MACD、背弛、中枢
  3. ad网络标号怎么用_altium designer网络标号的作用范围
  4. win7 设置电脑保护色
  5. 在java中class是什么意思_java 中Class? 中的?代表什么意思?
  6. 首开先河 | 脑机接口让这位ALS患者可读可写
  7. DMA导致的CACHE一致性问题解决方案
  8. win10+deepin安装 linux修改系统启动项
  9. Windows更新后双系统引导消失manjaro启动项丢失修复
  10. 计算机用户授权原则,涉密信息系统严格设定用户权限,按照什么密级防护和什么授权管理的原则...