跳转到主文档

Unbundled 开发模式

以 Webpack 为主的传统构建工具,执行构建会一次性将项目源代码和第三方依赖,编译打包到一起。随着项目体积增大,构建耗时也会越来越长,直接导致开发阶段 Dev Server 启动的时间也会越来越长,严重影响了开发效率和体验。

Modern.js 提供 Unbundled 开发模式。当项目以 Unbundled 开发模式启动时,项目依赖的模块不会被进行打包处理,而是直接使用原生的 ESM 模块,由现代浏览器负责模块的解析和加载,从而实现模块的按需加载。Unbundled 开发模式可以让 Dev Server 拥有秒级的启动速度和热更新速度,极大地提高了开发效率和体验。

ES modules(ESM)是 JavaScript 官方的标准化模块系统。

Unbundled 模式暂不支持在 Windows 平台使用,支持即将上线。

注意
  1. 由于浏览器兼容性等问题,Unbundled 开发模式仅适用于开发阶段。在生产环境部署的项目,仍然需要先经过编译打包处理。
  2. Unbundled 开发模式 暂不支持 SSR。

开始使用

在 Modern.js 中使用 Unbundled 开发模式,需要在项目根路径下执行 pnpm run new,然后进行如下选择:

? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用「Unbundled 开发」模式

现在,执行 pnpm run dev:esm 命令,项目就会以 Unbundled 开发模式启动。浏览器访问 http://localhost:8080,打开开发者工具的 Network 窗口,发现加载的 JS 资源都是未经过打包的 ESM 模块规范的文件。

实现原理和注意事项

为了更好的理解 Unbundled 开发模式,本章节从下面几个方面分别介绍:

依赖预处理

当前,很多第三方依赖只提供了 CommonJS 产物,无法直接在浏览器中运行,另外,即使第三方依赖提供 ESM 产物,如果按照习惯使用,例如:

import { something } from 'some-package';

在浏览器中直接运行也会报错。 Modern.js 为了解决上述问题,会对第三方依赖进行如下处理。

对第三方依赖进行如下处理方式
  1. 首次启动 Dev Server 时,分析项目源代码,找出使用到的第三方依赖,例如 reactreact-dom 等。
  1. 根据依赖在 node_modules/ 目录下的实际安装位置,获取精确的版本号信息。
  1. 根据包名和版本号,依次检查是否命中本地缓存和 Modern.js 的云端缓存,均未命中的情况下,本地编译该模块,转换为 ESM 格式。后续针对获取到的 ESM 文件,使用 esbuild 打包成一个文件,以减少项目运行时浏览器中的请求数量。
  1. Dev Server 启动时,动态改写源码文件中对第三方依赖的引用路径,例如:

    import { useState } from 'react'

    会被改写为:

    import { useState } from 'node_modules/.modern_js_web_modules/react.js'

    从而保证浏览器能够正确加载第三方依赖。

相同依赖的项目,使用 Modern.js 的 Unbundled 开发模式和使用 Snowpack 进行开发,Dev Server 的启动时间对比如下:

首次预处理 node_modules添加依赖后,再次运行依赖无变化时,再次运行
Snowpack24.27s25.88s< 1s
Modern.js Unbundled命中云端缓存: 9.31s;本地直接编译: 30.81s2.6s< 1s

可以发现,在常见的开发场景中,使用 Unbundled 开发模式 ,能够节省更多依赖预处理时间。

JSX/TSX

.jsx.tsx 文件不能直接在浏览器中运行。当请求这类格式的文件资源时,Modern.js 会使用 esbuild 将原始文件编译成 .js 文件。

注意

Modern.js 利用 Babel 插件支持的一些语法,esbuild 并不支持,使用 Unbundled 开发模式 时,请避开这些语法的使用: Pipeline Operator。 另外,使用 TypeScript 开发时,还需要注意:

  1. esbuild 的 transform 不支持从 .d.ts 中跨文件导入 const enum 使用。
  2. Dev Server 不会对 TS 文件执行类型检查,因此依赖 IDE 进行类型校验。

CSS

浏览器中通过 import 语法导入的资源,要求资源类型是 application/javascript。当 JS 文件中导入 CSS 文件时,Modern.js 会将 CSS 内容包装到 JS 文件中,最终创建 style 标签,插入到 head 标签内。

图片资源处理

JS 中使用图片

JS 文件中引入的图片资源会返回解析之后的 URL:

src/App.jsx
import logoUrl from './logo.png';

console.log(logoUrl); // 输出: '/src/logo.png';

Base64 编码内联

默认情况下,小于 10kb 的图片、字体文件,会经过 Base64 编码,内联进页面,不会再发送独立的请求。

可以通过配置 output.dataUriLimit 修改这个阈值。

JS 中使用 SVG

针对 SVG 资源,默认启用了 SVGR,可以通过 React 组件的形式导入:

App.jsx
import logoUrl, { ReactComponent as LogoComponent } from './logo.svg';

CSS 中使用图片

在 CSS 文件中既可以通过相对路径也可以通过别名的方式引入图片:

.logo {
background: url('./foo.png');
}

/** or **/
.logo {
background: url('@/foo.png');
}

JSON

支持直接使用默认导入的方式导入 JSON 文件:

data.json
{
"name": "a"
}
App.jsx
import jsonData from './data.json';

console.log(jsonData); // => { name: 'a'}

热更新( HMR )

一般情况下,不需要进行任何修改,Unbundled 开发模式下就可以正常使用热更新功能。此外,Unbundled 开发模式ESM-HMR Spec 的基础上,增加了 Webpack 场景常用的 module.hot 用法支持。同时也可以直接在代码中使用 import.meta.hot.accept 方式注册依赖更新时的回调,例如:

b.js
export const name = 'b';
a.js
import { name } from './b';

export const age = 1;

import.meta.hot.accept('./b', modules => {
console.log(modules); // 输出最新的 a、b 模块
});

当修改 b.js 后,在浏览器的控制台可以看到对应的日志输出。

HTTP 2.0

Unbundled 模式下,每个依赖都会对应一个网络请求,因此会存在大量的网络请求。通过开启 HTTP 2.0,可以进一步优化资源加载速度。配置 dev.httpstrue,即可同时启用 TLSHTTP 2.0 的支持。

其他注意事项

output.injectoutput.copyoutput.polyfill 配置在 Unbundled 模式下不支持。