rerumaccusamus
Version:
The meta-framework suite designed from scratch for frontend-focused modern web development.
185 lines (116 loc) • 7.75 kB
Markdown
---
title: Unbundled 开发模式
sidebar_position: 2
---
以 Webpack 为主的传统构建工具,执行构建会一次性将项目源代码和第三方依赖,编译打包到一起。随着项目体积增大,构建耗时也会越来越长,直接导致开发阶段 Dev Server 启动的时间也会越来越长,严重影响了开发效率和体验。
Modern.js 提供 **Unbundled 开发模式**。当项目以 **Unbundled 开发模式**启动时,项目依赖的模块不会被进行打包处理,而是直接使用原生的 ESM 模块,由现代浏览器负责模块的解析和加载,从而实现模块的按需加载。**Unbundled 开发模式**可以让 Dev Server 拥有秒级的启动速度和热更新速度,极大地提高了开发效率和体验。
:::note 注
ES modules(ESM)是 JavaScript 官方的标准化模块系统。
:::
:::note 注
Unbundled 模式暂不支持在 Windows 平台使用,支持即将上线。
:::
:::caution 注意
1. 由于浏览器兼容性等问题,**Unbundled 开发模式**仅适用于开发阶段。在生产环境部署的项目,仍然需要先经过编译打包处理。
2. **Unbundled 开发模式** 暂不支持 SSR。
:::
## 开始使用
在 Modern.js 中使用 **Unbundled 开发模式**,需要在项目根路径下执行 `pnpm run new`,然后进行如下选择:
```bash
? 请选择你想要的操作: 启用可选功能
? 启用可选功能: 启用「Unbundled 开发」模式
```
现在,执行 `pnpm run dev:esm` 命令,项目就会以 **Unbundled 开发模式**启动。浏览器访问 `http://localhost:8080`,打开开发者工具的 Network 窗口,发现加载的 JS 资源都是未经过打包的 ESM 模块规范的文件。
## 实现原理和注意事项
为了更好的理解 **Unbudled 开发模式**,本章节从下面几个方面分别介绍:
### 依赖预处理
当前,很多第三方依赖只提供了 CommonJS 产物,无法直接在浏览器中运行,另外,即使第三方依赖提供 ESM 产物,如果按照习惯使用,例如:
```js
import { something } from 'some-package';
```
在浏览器中直接运行也会报错。 Modern.js 为了解决上述问题,会对第三方依赖进行如下处理。
:::info 对第三方依赖进行如下处理方式
1. 首次启动 Dev Server 时,分析项目源代码,找出使用到的第三方依赖,例如 [react](https://www.npmjs.com/package/react)、[react-dom](https://www.npmjs.com/package/react-dom) 等。
2. 根据依赖在 `node_modules/` 目录下的实际安装位置,获取精确的版本号信息。
3. 根据包名和版本号,依次检查是否命中本地缓存和 Modern.js 的云端缓存,均未命中的情况下,本地编译该模块,转换为 ESM 格式。后续针对获取到的 ESM 文件,使用 [esbuild](https://esbuild.github.io/) 打包成一个文件,以减少项目运行时浏览器中的请求数量。
4. Dev Server 启动时,动态改写源码文件中对第三方依赖的引用路径,例如:
```js
import { useState } from 'react'
```
会被改写为:
```js
import { useState } from 'node_modules/.modern_js_web_modules/react.js'
```
从而保证浏览器能够正确加载第三方依赖。
:::
相同依赖的项目,使用 Modern.js 的 **Unbundled 开发模式**和使用 [Snowpack](https://www.snowpack.dev/) 进行开发,Dev Server 的启动时间对比如下:
| | 首次预处理 node_modules | 添加依赖后,再次运行 | 依赖无变化时,再次运行 |
| ---------------- | ---------------------------------------- | ----------------------- | -------------------- |
| Snowpack | 24.27s | 25.88s | < 1s |
| Modern.js Unbundled | 命中云端缓存: 9.31s;本地直接编译: 30.81s | 2.6s | < 1s |
可以发现,在常见的开发场景中,使用 **Unbundled 开发模式** ,能够节省更多依赖预处理时间。
### JSX/TSX
`.jsx` 和 `.tsx` 文件不能直接在浏览器中运行。当请求这类格式的文件资源时,Modern.js 会使用 [esbuild](https://esbuild.github.io/) 将原始文件编译成 `.js` 文件。
:::warning 注意
Modern.js 利用 Babel 插件支持的一些语法,esbuild 并不支持,使用 **Unbundled 开发模式** 时,请避开这些语法的使用:
[Pipeline Operator](https://github.com/tc39/proposal-pipeline-operator)。
另外,使用 TypeScript 开发时,还需要注意:
1. esbuild 的 [`transform`](https://esbuild.github.io/api/#transform-api) 不支持从 `.d.ts` 中跨文件导入 [const enum](https://www.typescriptlang.org/docs/handbook/enums.html#const-enums) 使用。
2. Dev Server 不会对 TS 文件执行类型检查,因此依赖 IDE 进行类型校验。
:::
### CSS
浏览器中通过 `import` 语法导入的资源,要求资源类型是 `application/javascript`。当 JS 文件中导入 CSS 文件时,Modern.js 会将 CSS 内容**包装**到 JS 文件中,最终创建 `style` 标签,插入到 `head` 标签内。
### 图片资源处理
#### JS 中使用图片
JS 文件中引入的图片资源会返回解析之后的 URL:
```js title=src/App.jsx
import logoUrl from './logo.png';
console.log(logoUrl); // 输出: '/src/logo.png';
```
#### Base64 编码内联
默认情况下,小于 10kb 的图片、字体文件,会经过 Base64 编码,内联进页面,不会再发送独立的请求。
可以通过配置 [`output.dataUriLimit`](/docs/apis/config/output/data-uri-limit) 修改这个阈值。
#### JS 中使用 SVG
针对 SVG 资源,默认启用了 [SVGR](https://react-svgr.com/),可以通过 React 组件的形式导入:
```js title=App.jsx
import logoUrl, { ReactComponent as LogoComponent } from './logo.svg';
```
#### CSS 中使用图片
在 CSS 文件中既可以通过相对路径也可以通过别名的方式引入图片:
```css
.logo {
background: url('./foo.png');
}
/** or **/
.logo {
background: url('@/foo.png');
}
```
### JSON
支持直接使用默认导入的方式导入 JSON 文件:
```json title=data.json
{
"name": "a"
}
```
``` javascript title=App.jsx
import jsonData from './data.json';
console.log(jsonData); // => { name: 'a'}
```
### 热更新( HMR )
一般情况下,不需要进行任何修改,**Unbundled 开发模式**下就可以正常使用热更新功能。此外,**Unbundled 开发模式**在 [ESM-HMR Spec](https://github.com/snowpackjs/esm-hmr) 的基础上,增加了 Webpack 场景常用的 `module.hot` 用法支持。同时也可以直接在代码中使用 `import.meta.hot.accpet` 方式注册依赖更新时的回调,例如:
```js title=b.js
export const name = 'b';
```
```js title=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](https://zh.wikipedia.org/wiki/HTTP/2),可以进一步优化资源加载速度。配置 [`dev.https`](/docs/apis/config/dev/https) 为 `true`,即可同时启用 [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) 和 [HTTP 2.0](https://zh.wikipedia.org/wiki/HTTP/2) 的支持。
### 其他注意事项
[`output.inject`](/docs/apis/config/output/inject)、[`output.copy`](/docs/apis/config/output/copy)、[`output.polyfill`](/docs/apis/config/output/polyfill) 配置在 **Unbundled 模式**下不支持。