在 html 中直接使用 Esm、Jsx 脚本快速调试和使用 React@19 和 Vue@3 源码,解决 React19 UMD 构建等问题
当然说这么多可能的缺点,不是说构建工具存在的无意义,而是在 Html 中,你也可以使用 Jsx、Tsx、Esm 进行 demo 编写,简单场景中,可以获得和脚手架一样的编码体验,其优势旨在于代码与源码的关系更加纯粹,调试更加方便,无其他心智负担。
React@19 in Html
React@19中移除了umd的相关构建,若你的项目中使用了externals类似功能,可参考 react-debug-in-html 和 umd-react 继续使用umd构建(不推荐生产使用)
当然 webpack 的 externals 也支持原生 esm 模块的外部引入,看需求抉择
babel 转义 Jsx
importmap 缺点就是, 无论配置的是本地的 相对路径,还是网络的 绝对路径,在 IDE 中,无法直接定位方法对应的代码位置,这将在调试中造成一定困扰。当然你也可以直接使用本地的相对路径,可配合 IDE 进行快速调试
import React from 'react';
function App() {
return <h1>Hello, React!</h1>;
}
export default App;
import React from 'react';
function App() {
return /*#__PURE__*/ React.createElement('h1', {
children: 'Hello, React!',
});
}
export default App;
后续介绍使用 Jsx 部分时,有该问题的解决方案
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- esm from esm.sh, you also can use jsdelivr -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react?dev",
"react-dom/": "https://esm.sh/react-dom@19.1.0&dev/"
}
}
</script>
</head>
<body>
<div id="app"></div>
</body>
<script type="text/babel" data-type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
// babel 不会处理引入的脚本,你可以将 Count 中的 jsx 使用 createElement 手动转义,也行
// import Count from './Count.js';
function App() {
return <h1>Hello, React!</h1>;
}
const root = createRoot(document.getElementById('app'));
root.render(<App />);
</script>
</html>
【推荐】在 Html 中使用 jsx 后缀的脚本文件
注意,从这里将不使用远程的
esm模块,而是使用本地的esm模块(方便调试),且使用importmap定义模块路径
将 cjs 转为 esm
下面是需要转换的
React@19的相关源码地址(19在构建时将Client和Schedule从Dom中拆分出来,所以这两个也需要转换)
建议使用的时候去除 版本号 再转换,可以获取最新版
react:https://cdn.jsdelivr.net/npm/react@19.1.0/cjs/react.development.jsReact-Dom:https://cdn.jsdelivr.net/npm/react-dom@19.1.0/cjs/react-dom.development.jsReact-Dom/client: https://cdn.jsdelivr.net/npm/react-dom@19.1.0/cjs/react-dom-client.development.jsscheduler: https://cdn.jsdelivr.net/npm/scheduler@0.26.0/cjs/scheduler.development.js
通过 serviceWorker 转义 jsx 脚本文件
当然也可以将 tsx 转为 js,demo 项目使用 ts 不是在找罪受嘛?还是 jsx 效率点~ 若需要转换 ts 或 vue 文件,需要注意 Response 中的 Content-Type 是否为 application/javascript,毕竟浏览器只认这个
self.addEventListener('install', (e) => e.waitUntil(getBabel()));
self.addEventListener('fetch', (e) => e.respondWith(handleRequest(e.request)));
async function getBabel() {
const r = await fetch('https://unpkg.com/@babel/standalone@7.27.0/babel.min.js');
const babel = await r.text();
new Function(babel).apply(self);
}
async function handleRequest(request) {
const url = new URL(request.url);
const r = await fetch(request);
const nextResponse = new Response(r.body, r);
if (url.pathname.endsWith('.jsx')) {
nextResponse.headers.set('Content-Type', 'application/javascript');
}
if (nextResponse.status === 200 && url.pathname.endsWith('.jsx')) {
const jsx = await nextResponse.text();
const js = self.Babel.transform(jsx, { presets: ['react'] }).code;
return new Response(js, nextResponse);
} else {
return nextResponse;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>React Source Debug</title>
<script>
navigator.serviceWorker.register('/transform-code.js', {
scope: '/',
});
</script>
<script type="importmap">
{
"imports": {
"react": "/public/react/react.js",
"react-dom": "/public/react/react-dom.js",
"react-dom/client": "/public/react/react-dom-client.js",
"scheduler": "/public/react/scheduler.js"
}
}
</script>
</head>
<body>
<div id="app"></div>
</body>
<script type="module">
import './src/app.jsx';
</script>
</html>
serviceWorker 不生效?
注意 scope 的配置和
serviceWorker文件的路径,推荐将注册脚本放在root下,这样可以灵活进行scope的配置。(scope可以理解为serviceWorker在哪个域生效)serviceWorker注册成功了,但未生效?可以尝试刷新,或在控制面板中service workers手动刷新serviceWorker,或重新注册
vue 在 Html 中使用
vue 在官网就提供了相关示例,这里就不过多废话了。通过 CDN 使用 Vue
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
</head>
<body>
<div id="app">
<p>count: {{count}}</p>
</div>
</body>
<script type="module">
import { createApp, onMounted, ref } from 'vue';
createApp({
// template 优先 html 中的模板(app 中)
// template 中的语法遵循 SFC 中的规则,如 ref 变量在这里不需要 value(自动浅层解包)
template: `
<div>
<button @click="increment">计数:{{ count }}</button>
<p>双倍:{{ doubleCount }}</p>
<div>static node - 1</div>
<div>static node - 2</div>
<div>static node - 3</div>
</div>
`,
setup() {
const count = ref(0);
const increment = () => {
count.value++; // 更新计数
};
onMounted(function () {
console.log('mounted', count.value);
});
return {
count,
doubleCount: 0,
increment,
};
},
}).mount('#app');
</script>
</html>
调试建议
使用
console.trace在源码中进行log,可以快速定位到当前方法的完整调用栈,比debugger更清晰虽然是
development的源码,但在压缩后,会出现与源码不完全一致的情况,如常量会直接使用对应值,好处是结合源码可以直接了解常量,坏处是缺失一定的语义化,不好理解,所以还是推荐结合源码注意源码的版本!,如html中引入的是React@19.1,则需要注意对应源码的版本,因为在github中,几乎每天都在主分支上进行改动,这将是后面的feature,clone的版本与实际使用的版本不一致# 推荐设置 depth 减少 commit 的储存占用和 clone 时间 git clone git@github.com:facebook/react.git -b v19.1.0 --depth=1也可通过
performance标签分析执行流程或当前流程执行的是 宏任务还是微任务,也可以观察渲染时机等(Chrome Plugin会影响采集内容)
总结
preview 无内容?请刷新后再查看,若仍无法预览,请确保
services worker可在你的浏览器中正常使用






