- 教程
【免费下载链接】typescript-book
:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 🌹
动态 import 表达式(Dynamic import expressions)是 ECMAScript 的新特性,允许在程序运行到任意位置时异步请求一个模块,它与 webpack 的 Code Splitting(代码分割)配合,可以让应用先加载一个最小的 bootstrap 包、按需异步加载其余功能。本文以本仓库(typescript-book)的 dynamic-import-expressions 示例 为骨架,完整讲解 TypeScript 2.4+ 中import()的写法、tsconfig.json的关键配置(尤其"module": "esnext"),以及如何验证编译产物确实把import()原样保留给 webpack 做代码分割。读完本文,你将掌握"懒加载 + 代码分割"这一现代前端优化的标准姿势,并理解它为什么依赖编译配置而非自动产生。
什么是动态 import 表达式
动态 import 表达式是 ECMAScript 的新特性,它允许用户在程序的任意时间点异步请求一个模块——而不是像静态import那样只能在模块顶层声明、且必须在模块加载时同步解析。
- 静态形式:
import moment from "moment";—— 在模块顶层声明依赖,加载时机固定。 - 动态形式:
import("moment")—— 在函数体、事件回调、条件分支等任意位置调用,返回一个Promise,模块在请求时才被解析和加载。
这一特性由TC39JavaScript 委员会提出,其官方提案名为import() proposal for JavaScript,当时处于 stage 3 阶段。在 TypeScript 2.4 中,这一特性被正式支持(详见 TypeScript 官方的What's new in TypeScript说明)。
本仓库将这一主题收录在 Project(项目组织) 章节下,与 Modules、Namespaces 等模块化话题并列,可见它属于"如何组织 TypeScript 项目"这一层面的关键能力。
import() 的运行时语义
从使用方式看,import()与fetch、Promise类似,是异步的:
import("moment") .then((moment) => { // 模块加载完成后在这里使用 }) .catch((err) => { // 加载失败时的处理 });由于它返回Promise,加载失败会走catch分支,这让你可以优雅地处理网络错误或资源缺失的情况,例如降级提示、重试逻辑等。
为什么需要代码分割:先加载最小的 bootstrap
单个巨型 bundle 的问题是:用户访问首页时,浏览器必须下载、解析并执行整个应用的代码,哪怕其中 90% 的功能他根本不会立刻用到。随着应用增长,首屏时间会越来越长。
webpack等打包工具提供了一项名为Code Splitting(代码分割)的能力:把 bundle 拆分成多个 chunk,这些 chunk 可以在稍后的某个时间点被异步下载。典型收益包括:
- 先服务一个最小的 bootstrap bundle,让首屏尽快可交互;
- 把重量级依赖(如日期库、图表库、地图库)单独成 chunk,只在真正用到时才下载;
- 按路由拆包,用户进入某个页面时才加载该页面的代码。
动态 import 表达式正是实现代码分割的语法基础——打包器看到import()调用,就知道这里应当切出一个异步 chunk。
常见误区:TypeScript 不会"自动"帮你做代码分割
一个非常自然的想法是:既然我用了 TypeScript 2.4 的动态 import 表达式,而开发流程里又用了 webpack,那么编译后是不是就自动产生bundle chunk、自动对最终 JS 做代码分割了?
并非如此,事情没那么简单。原因在于:这取决于我们所使用的 tsconfig.json 配置。
关键点在于:webpack 的代码分割支持两种类似的技术来实现目标:
| 技术 | 状态 | 说明 |
|---|---|---|
import() | 推荐(preferred) | 基于 ECMAScript 提案,标准语法 |
require.ensure() | 旧方案(legacy) | webpack 特有的历史 API |
而这意味着:期望的 TypeScript 编译输出是"把 import() 语句原样保留",而不是把它转译成其他任何东西。只有import()被原样保留在产物中,webpack 才能识别它并执行代码分割;如果 TypeScript 把它转译成了require(...)或Promise.resolve().then(() => require(...)),webpack 就无从切分 chunk 了。
实战:懒加载 moment 并实现代码分割
下面进入本仓库提供的完整示例。目标需求很明确:懒加载 moment 库,同时实现代码分割——即让 moment 库位于一个独立的 JS chunk 中,只在需要时才被加载。
源代码:dynamicImportExpression.ts
完整的可运行源码位于 code/dynamic-import-expressions/dynamicImportExpression.ts:
function myApp() { import(/* webpackChunkName: "momentjs" */ "moment") .then((moment) => { // lazyModule has all of the proper types, autocomplete works, // type checking works, code references work \o/ const time = moment().format(); console.log("TypeScript >= 2.4.0 Dynamic Import Expression:"); console.log(time); }) .catch((err) => { console.log("Failed to load moment", err); }); }逐点拆解这段代码:
import(...)出现在函数体内:这正是"在程序中任意点异步请求模块"的体现,静态import无法写在函数体里。/* webpackChunkName: "momentjs" */魔法注释:这是 webpack 支持的注释指令,用来给生成的 chunk 命名。编译时该注释会被原样保留,webpack 解析后即可生成名为momentjs的独立 chunk 文件。.then((moment) => ...):回调参数moment拥有完整的类型信息——注释中特别强调:autocomplete(自动补全)可用、type checking(类型检查)可用、code references(代码引用跳转)可用。这是 TypeScript 相对纯 JS 的显著优势:动态导入的模块依然享受静态类型推断,而不是退化为any。.catch((err) => ...):处理模块加载失败的情况。
tsconfig.json 配置详解
示例配套的tsconfig.json位于 code/dynamic-import-expressions/tsconfig.json:
{ "compilerOptions": { "target": "es5", "module": "esnext", "lib": [ "dom", "es5", "scripthost", "es2015.promise" ], "strict": true, "moduleResolution": "node" }, "files": [ "./dynamicImportExpression.ts" ] }其中与动态 import 直接相关的选项:
"module": "esnext"—— 本文的核心关键配置。module选项控制"生成什么模块格式的 JavaScript"。当取值为esnext(在 TS 2.4 时代对应 webpack 需要的语义)时,TypeScript不会把import()转译为require或System.import等任何形式,而是原样保留 import() 语句,作为 webpack Code Splitting 的输入。若module取commonjs等传统格式,import()就会被转译掉,代码分割也就无从谈起。"target": "es5":编译目标为 ES5,保证产物能在旧浏览器运行;import()本身不依赖 ES 模块运行时,交给 webpack 处理后即可工作。"lib": ["dom", "es5", "scripthost", "es2015.promise"]:显式声明需要的库文件。加入es2015.promise是因为.then/.catch链依赖Promise类型,需要引入 ES2015 的 Promise 声明才能通过类型检查。"strict": true:开启全部严格类型检查选项,保证示例代码在严格的类型环境下依然能通过编译。"moduleResolution": "node":使用 Node 风格的模块解析策略,这是 webpack 项目(解析node_modules中的moment)的常见配置。关于module与moduleResolution等选项的完整说明,可参考 docs/project/tsconfig.md。
值得注意的是,本仓库还保留了一份更完整的配置(见 docs/project/dynamic-import-expressions.md),包含jsx: "react"、declaration、sourceMap、outDir、typeRoots、types等选项,用于真实的 React + 类型声明场景:
{ "compilerOptions": { "target": "es5", "module": "esnext", "lib": [ "dom", "es5", "scripthost", "es2015.promise" ], "jsx": "react", "declaration": false, "sourceMap": true, "outDir": "./dist/js", "strict": true, "moduleResolution": "node", "typeRoots": [ "./node_modules/@types" ], "types": [ "node", "react", "react-dom" ] } }其中typeRoots与types用于精确控制从node_modules/@types引入哪些类型声明,outDir控制编译产物输出目录,sourceMap生成调试用的 source map——这些都是真实 webpack 工程里的常见配置。
编译产物验证:import() 被原样保留
验证"配置是否生效"最直接的办法,是查看 TypeScript 的实际编译输出。仓库中的编译产物 code/dynamic-import-expressions/dynamicImportExpression.js 展示了结果:
"use strict"; function myApp() { import(/* webpackChunkName: "momentjs" */ "moment") .then(function (moment) { // lazyModule has all of the proper types, autocomplete works, // type checking works, code references work \o/ var time = moment().format(); console.log("TypeScript >= 2.4.0 Dynamic Import Expression:"); console.log(time); }) .catch(function (err) { console.log("Failed to load moment", err); }); }对比源码可以清晰看到:
import(/* webpackChunkName: "momentjs" */ "moment")被原样保留,包括魔法注释webpackChunkName: "momentjs"也完整保留——这正是 webpack Code Splitting 需要的输入形式;- 箭头函数被降级为
function、const降级为var(因为target: "es5"); - 文件顶部出现
"use strict"——从源码结构看,这是严格模式相关编译选项(strict)开启后的典型产物特征; .then/.catch链原样保留,等待 webpack 在构建期替换为实际的 chunk 加载逻辑。
这个编译产物从源码级证实了本文的核心论断:TypeScript 的工作是把import()原样交给打包器,切分 chunk 的工作由 webpack 完成。
依赖与版本环境
示例工程还包含 package.json,记录了运行前提:
typescript: ^2.4.1:动态 import 表达式从 TypeScript 2.4 开始支持,因此编译器版本不得低于 2.4;moment: ^2.18.1:示例中被懒加载的目标库;- 工程作者为 Jose Quinto Zamora。
复现实验的前提是:安装上述依赖后,运行tsc(TypeScript 编译器会在当前目录及上级目录查找tsconfig.json,也可用tsc -p ./path-to-project-directory指定项目目录,详见 docs/project/tsconfig.md),再将编译产物交给 webpack 构建,观察是否生成独立的momentjschunk。
重要注意事项总结
结合原文档与仓库源码,使用动态 import 表达式做代码分割时有几个关键点必须牢记:
"module": "esnext"是让 TypeScript 产生"供 webpack Code Splitting 使用的 mimic import() 语句"的关键。若省略或改用其他模块格式,import()会被转译,代码分割失效。- TypeScript 只负责保留语法,不负责切分 chunk。期望的产物是
import()语句原样保留,chunk 的切分、命名、异步下载完全由 webpack 在构建期完成。 - 务必使用
import()而非require.ensure()。后者是 webpack 特有的 legacy API,前者是基于 ECMAScript 提案的标准语法,是官方推荐方式。 - 类型体验完整保留。动态导入的模块在
.then回调中依然具备完整的类型推导、自动补全与引用跳转能力,不必牺牲类型安全来换取性能优化。
进一步探索
本仓库围绕该主题提供的完整素材如下,便于你对照阅读与复现:
- 主文档:docs/project/dynamic-import-expressions.md
- 源码与产物:code/dynamic-import-expressions/dynamicImportExpression.ts、code/dynamic-import-expressions/dynamicImportExpression.js
- 工程配置:code/dynamic-import-expressions/tsconfig.json、code/dynamic-import-expressions/package.json
- 关联主题:模块化基础见 docs/project/modules.md,编译上下文与 tsconfig 全局概念见 docs/project/compilation-context.md 与 docs/project/tsconfig.md
从项目组织层面理解这套机制后,你便能在实际工程中放心地用import()按需加载路由、组件与重型依赖,在保持 TypeScript 类型安全的同时获得 webpack 代码分割带来的首屏性能收益。
- 教程
【免费下载链接】typescript-book
:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 🌹
相关推荐
Repomix 社群生態系全覽:從 VSCode 擴充、桌面應用到語言實作與 AI 整合工具
Repomix 社群生態系全覽:從 VSCode 擴充、桌面應用到語言實作與 AI 整合工具 导读 本文聚焦 Repomix 開源專案(將整個程式庫打包成單一
开发工具MCP 服务AI 应用webpack 代码分割实战:基于 ES Module `import()` 的按需加载与动态 import 异步上下文全面解析
webpack 代码分割实战:基于 ES Module import 的按需加载与动态 import 异步上下文全面解析 ES Modules 的标准 impo
前端构建开发工具提升设计效率300%!Size Marks让UI尺寸标注不再繁琐
提升设计效率300%!Size Marks让UI尺寸标注不再繁琐 Size Marks是一款专为Web设计师和前端工程师打造的Photoshop脚本工具,能将矩
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考