2022-01-04
浏览器内的 ESM 是否已足够强大
浏览器内运行原生 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 的引入并没有点,它不是一个相对路径; 像这样的模块称为
裸模块 (bare module)

在 nodejs 里裸模块通常解析到 package.json 目录所在的
node_modules/

而这并不是浏览器标准,而是 npm 标准,我们总不能解析到
example.com/node_modules/react.js
吧?
为了解决裸模块的加载问题,Web 工作组提出了 import-maps 特性以允许开发者定义模块和 URL 的映射关系。
比如这里这个定义了 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
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」

# 直出和 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 过程。




回到顶部