typescript-book 实战:TypeScript 动态 import 表达式与 webpack 代码分割集成指南
2026/9/20 9:00:13 网站建设 项目流程
  • 教程

【免费下载链接】typescript-book

:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 🌹

项目地址:https://gitcode.com/gh_mirrors/ty/typescript-book
点击查看免费下载

动态 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()fetchPromise类似,是异步的:

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()转译为requireSystem.import等任何形式,而是原样保留 import() 语句,作为 webpack Code Splitting 的输入。若modulecommonjs等传统格式,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)的常见配置。关于modulemoduleResolution等选项的完整说明,可参考 docs/project/tsconfig.md。

值得注意的是,本仓库还保留了一份更完整的配置(见 docs/project/dynamic-import-expressions.md),包含jsx: "react"declarationsourceMapoutDirtypeRootstypes等选项,用于真实的 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" ] } }

其中typeRootstypes用于精确控制从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); }); }

对比源码可以清晰看到:

  1. import(/* webpackChunkName: "momentjs" */ "moment")被原样保留,包括魔法注释webpackChunkName: "momentjs"也完整保留——这正是 webpack Code Splitting 需要的输入形式;
  2. 箭头函数被降级为functionconst降级为var(因为target: "es5");
  3. 文件顶部出现"use strict"——从源码结构看,这是严格模式相关编译选项(strict)开启后的典型产物特征;
  4. .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 表达式做代码分割时有几个关键点必须牢记:

  1. "module": "esnext"是让 TypeScript 产生"供 webpack Code Splitting 使用的 mimic import() 语句"的关键。若省略或改用其他模块格式,import()会被转译,代码分割失效。
  2. TypeScript 只负责保留语法,不负责切分 chunk。期望的产物是import()语句原样保留,chunk 的切分、命名、异步下载完全由 webpack 在构建期完成。
  3. 务必使用import()而非require.ensure()。后者是 webpack 特有的 legacy API,前者是基于 ECMAScript 提案的标准语法,是官方推荐方式。
  4. 类型体验完整保留。动态导入的模块在.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 🌹

项目地址:https://gitcode.com/gh_mirrors/ty/typescript-book
点击查看免费下载

相关推荐

上一篇:如何永久保存你的微信聊天记录?WeChatMsg完整指南让数据真正属于你!🚀
下一篇:Hono.js 静态文件服务与SPA路由的实现技巧

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询