浏览器内运行原生 ESM 一直是众多 Web 佬所追求的,尤其从去年 vite 的流行开始,越来越多人在浏览器内直接用 ESM 进行开发,而约莫一年后的现在,2022 年,我们能否有把握直接将 ESM 特性用于生产环境 ?
00import React from 'react'; 01import ReactDOM from 'react-dom'; 02 03function App() { 04 return ( 05 <div>生产环境上 ESM</div> 06 ) 07} 08 09ReactDOM.render(<App />, $('#app')!);
# ESM 兼容性怎么样 ↵
很多人觉得兼容性是最大的问题,但实际上时间问题其实并不是问题。具体查阅了 caniuse 看了下,整体还算可以,但还不够,应该要 99%+ 才比较适合上。
看样子 ESM 全面流行似乎只是时间问题了,各大厂商都支持了好一段时间了。


比起兼容性,更麻烦的是很多模块并没有 ESM 导出,直接走的 CJS 打包,根本没法用 —— 虽然这也是时间问题,因为现在社区很多人在搞 pure-esm-package 了,也就是纯 ESM 导出的模块,不搞 CJS 了,而其中有的人甚至是直接搞 pure-typescript-package 了 —— 不编译直接导出 TS 源码
# ESM 路径解析问题 ↵
ESM 在路径解析 (resolve) 的时候必须要有参照,否则无法解析,比如右边的
./add.js
00import add from './add.js'; 01const result = add(1, 2, 3); 02console.log(result);
浏览器必须要知道其中的
.
是什么才能正确解析出这个 ./add.js
的真实 url 链接,比方说这个点取值为 example.com/js/
那么最终可以解析为 example.com/js/add.js
对于浏览器来说得到参照点并最终得到这样的完整 URL 才是 fetchable 的、executable 的
00import React from 'react'; 01import App from './app.js'; 02 03const result = <App />; 04 05console.log( 06 'res:', result 07);
然而,在这个例子里 react 的引入并没有点,它不是一个相对路径; 像这样的模块称为
在 nodejs 里裸模块通常解析到 package.json 目录所在的
而这并不是浏览器标准,而是 npm 标准,我们总不能解析到
裸模块 (bare module)
在 nodejs 里裸模块通常解析到 package.json 目录所在的
node_modules/
下 而这并不是浏览器标准,而是 npm 标准,我们总不能解析到
example.com/node_modules/react.js
吧?为了解决裸模块的加载问题,Web 工作组提出了 import-maps 特性以允许开发者定义模块和 URL 的映射关系。
比如这里这个定义了 react 解析为
比如这里这个定义了 react 解析为
HOST/node_modules/react
00<script type="importmap"> 01{ 02 "imports": { 03 "react": "/node_modules/react", 04 "lodash": "/node_modules/lodash", 05 } 06} 07</script>
import-map 能搞定全部问题吗?并不能,比如解决不了 ./add 到底是 add.js 还是 add/index.js 的问题?
长久以来,我们太习惯于 nodejs 的馈赠了,ESM 在编译成 ES5 require 的情况下,从来都不是一个静态过程,它是一个动态的递归过程。更重要的是非 js 模块的 ESM 导入特定后缀的过程比想象中的复杂 —— 虽然这也是习以为常的技术馈赠 (webpack)
import a from'./add'; import aa from './add.js'; import aaa from './add/index'; import aaaa from './add/index.js'; import aaaaa from './style.module.css'; import aaaaaa from './style.module.css.js';
除此之外还有更加严肃的问题,标准并没有定义 importmap 是手写的还是 auto-generated 的,难道要手写 importmap ?
如果是手写,我宁可不用;如果是生成的,谁来生成,webpack 吗?一个 Web 标准显然不应该反向依赖 webpack 等打包工具
# 非 JS 模块导入 ↵
也就是说如何导入 png 后缀的文件, 这也是 webpack 的技术馈赠
00import image from './image.png'; 01console.log(image); // ?
# 还没有强大到足以支撑生产环境 ↵
本文完 —— 屁咧,写本文更想分享记录的是: 本文是基于 ESM 实现的 —— 也就是说通过写 .tsx 来写博客。
点击立即更新随机数: 0.7584979256988604
点击立即更新随机数: 0.7584979256988604
00// end-txt.tsx 01import React from 'react'; 02import { P } from 'rally/@@'; 03 04export default () => { 05 const [rand, setRand] = React.useState(Math.random()); 06 const update = () => setRand(Math.random()); 07 08 React.useEffect(() => { 09 const timer= setInterval(update, 500); 10 return () => { 11 clearInterval(timer); 12 } 13 }, []); 14 15 return <P onClick={update}> 16 本文完 —— 屁咧,写本文更想分享记录的是: 17 本文是基于 ESM 实现的 18 —— 也就是说通过写 .tsx 来写博客。<br /> 19 点击立即更新随机数: <span>{rand}</span> 20 </P> 21}
在具体实现的时候不可避免地遇到了前文所说的问题,大概解决了一波,核心还是在于如何处理动态解析
resolve 从来都不是一个静态过程, 读取特定路径的过程比想象中的复杂
# 构建器补全 resolve 结果 ↵
构建期间补全相关 extension (这里有个坑 tsc 不支持补全这个 extension,所以我换 rollup 了)


# 使用 System.js ↵
System.js 是一个开源项目,用于 polyfill ESM,让老浏览器也能支持 ESM 模块加载, TypeScript/Rollup 在编译打包的时候可以配置输出为 System.js 模块,比如:
00// ./src/test.ts 01import React from 'react'; 02export function Hello() { 03 return <div>hello</div>; 04} 05 06//// 编译后变为 //// 07 08// ./dist/test.js 09System.register(["react"], (exports_1, context_1) => { 10 "use strict"; 11 var react_1; 12 var __moduleName = context_1 && context_1.id; 13 function Hello() { 14 return react_1.default.createElement( 15 "div", null, "hello" 16 ); 17 } 18 exports_1("Hello", Hello); 19 return { 20 setters: [ 21 function (react_1_1) { 22 react_1 = react_1_1; 23 } 24 ], 25 execute: function () { 26 } 27 }; 28});
然后在浏览器或者 nodejs 环境下就可以通过 systemjs 加载这个模块了
00const { Hello } = await System.import('./dist/test.js'); 01// import { Hello } from './dist/test'; 02console.log(<Hello />);
# 裸模块运行时注入 ↵
00// my-blog.tsx 01import { Hello } from 'rally/@@'; 02export default () => ( 03 // 运行时注入 04 <Hello /> 05);
通过 systemjs 硬点 'rally/@@' 对应模块,以此来注入组件帮助编写内容。
而借助于完全的 js 运行时,写文章的表现力大大提高了:「点击计数器 n=0」
而借助于完全的 js 运行时,写文章的表现力大大提高了:「点击计数器 n=0」
# 直出和 hotreload ↵
直出的话用 system.js 将编译后的组件读取并渲染即可,同时注意同构的问题,但这里通过 system.js 引入 js 的话会让 system.js cache 住文件导致不能 hot reload
对比 .md 文件,在 md 被改写的时候重新跑编译就可以实现 hotreload,但这个办法对 tsx 博文不太适用,执行过的模块会 cached , 不会重新执行。
00const a = await import('./add'); 01// 改了 add.tsx 后, 再次 import 02const b = await import('./add'); 03console.log( 04 a === b // true 05);
00# 起子进程并收集 stdout 01# 并用 JSON.parse 解析它 02$ node relax-esm-node.js 03[{略}, "<div>略</div>"]
在查阅了 Systemjs 源码后发现它的 cache 是内置的,没有暴露,也就是说没办法做到删掉 cache 的方式重新 reload,最后的解决办法是起子进程来实现加载和做 html 直出以此实现 hotreload
# 末尾 ↵
总的来说,现阶段 ESM 还不够强大到足以支撑生产环境,如果真的想上,建议配合 systemjs 来做,尤其需要注意介入动态的 resolve 过程。