Angular 懒加载详解
懒加载(Lazy Loading)是 Angular 中一种重要的性能优化技术,它允许你将应用分割成多个按需加载的模块,从而减少初始加载时间。
什么是懒加载
懒加载是一种路由级别的代码分割技术,它允许 Angular 应用在用户导航到特定路由时才加载对应的模块,而不是在应用启动时就加载所有模块。这可以显著减少应用的初始包大小,提高首屏加载速度。
懒加载的优势
- 减少初始包大小:只有核心模块在应用启动时加载
- 提高首屏加载速度:用户不需要等待所有代码下载完才能使用应用
- 按需加载:只有当用户访问特定功能时才加载对应代码
- 更好的用户体验:特别是对于大型应用或网络条件较差的用户
实现懒加载的步骤
1. 创建可懒加载的特性模块
首先,你需要创建一个独立的特性模块,这个模块将包含一组相关的功能。
typescript
// products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductsListComponent } from './products-list/products-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
import { RouterModule } from '@angular/router';
import { productsRoutes } from './products.routes';
@NgModule({
declarations: [ProductsListComponent, ProductDetailComponent],
imports: [
CommonModule,
RouterModule.forChild(productsRoutes)
]
})
export class ProductsModule { }
2. 配置懒加载路由
在主应用的路由配置中,使用 loadChildren
属性来指定懒加载模块的路径。
typescript
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
},
// 其他路由...
];
3. 配置特性模块的路由
在特性模块中定义自己的子路由:
typescript
// products.routes.ts
import { Routes } from '@angular/router';
import { ProductsListComponent } from './products-list/products-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
export const productsRoutes: Routes = [
{ path: '', component: ProductsListComponent },
{ path: ':id', component: ProductDetailComponent }
];
验证懒加载是否工作
- 打开浏览器开发者工具
- 切换到 Network 选项卡
- 刷新应用,观察初始加载的文件
- 导航到懒加载路由,观察是否加载了新 chunk
预加载策略
Angular 提供了几种预加载策略来平衡初始加载和后续导航的性能:
1. 不预加载(默认)
typescript
RouterModule.forRoot(routes, { preloadingStrategy: NoPreloading })
2. 预加载所有模块
typescript
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
3. 自定义预加载策略
typescript
// custom-preloading.strategy.ts
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data?.preload ? load() : of(null);
}
}
// 使用
RouterModule.forRoot(routes, { preloadingStrategy: CustomPreloadingStrategy })
// 路由配置
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule),
data: { preload: true }
}
懒加载的最佳实践
- 合理划分模块:按功能域划分模块,避免模块过大或过小
- 共享模块:将常用组件、指令、管道放入共享模块
- 避免服务重复:确保服务只在根模块或核心模块提供
- 路由守卫:可以在懒加载模块中使用路由守卫
- 性能监控:使用 Angular 的性能工具监控懒加载效果
常见问题解决
1. 循环依赖问题
确保模块之间没有循环依赖,特别是共享模块和懒加载模块之间。
2. 服务作用域问题
如果希望在懒加载模块中使用单例服务,确保服务在根模块提供或在核心模块中使用 providedIn: 'root'
。
3. 路由配置错误
确保懒加载路由的路径正确,并且模块正确导出。
懒加载的核心机制
-
生成独立 chunk 文件:
- 当使用
loadChildren
配置懒加载时,Angular CLI 和 Webpack 会将这些模块打包成独立的 JavaScript 文件(chunk) - 这些文件通常命名为
products-module-[hash].js
等形式 - 它们不会包含在主包(main.js)中
- 当使用
-
异步加载模块:
- 使用
import()
语法(动态导入)实现模块的异步加载,基于 JavaScript 的import()
语法(ES2020 动态导入标准)。 - 当用户导航到懒加载路由时,Angular 会动态请求并加载对应的 chunk 文件
- 使用
具体实现方式
1. 路由配置中的异步加载
typescript
// 这是典型的懒加载路由配置
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
}
2. 编译后的结果
Angular CLI 使用 Webpack 编译后,会生成类似这样的代码:
typescript
// 编译后的懒加载路由配置
{
path: 'products',
loadChildren: () => __webpack_require__.e("products-module")
.then(() => __webpack_require__(/*! ./products/products.module */ "./src/app/products/products.module.ts"))
.then(m => m.ProductsModule)
}
3. 生成的文件结构
构建后的 dist
目录中会有类似这样的文件:
text
dist/
main.[hash].js // 主应用包
polyfills.[hash].js // polyfills
runtime.[hash].js // Webpack 运行时
products-module.[hash].js // 懒加载模块
other-module.[hash].js // 另一个懒加载模块
4. 运行时引用机制
4.1 Webpack 的运行时管理
Webpack 会生成一个运行时(runtime.js),它包含以下关键功能:
__webpack_require__.e
: 用于加载 chunk 文件- 模块缓存系统
- 已加载 chunk 的跟踪记录
4.2 实际引用流程
当用户访问懒加载路由时:
-
路由触发:
javascript// Angular 路由器内部处理 router.navigateByUrl('/products');
-
解析懒加载配置:
javascript// Angular 路由器的内部处理流程 const loadModule = route.loadChildren; // 获取配置的加载函数 loadModule().then(module => { // 模块加载完成后的处理 });
-
Webpack 动态加载:
javascript// Webpack 编译后的实际代码 __webpack_require__.e("products-module") .then(__webpack_require__.bind(__webpack_require__, "./src/app/products/products.module.ts")) .then(m => m.ProductsModule)
-
网络请求:
-
浏览器会发起对
products-module-[hash].js
的请求 -
文件内容示例:
javascript(window["webpackJsonp"] = window["webpackJsonp"] || []).push([ ["products-module"], { "./src/app/products/products.module.ts": (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ProductsModule", function() { return ProductsModule; }); // 模块实际代码... } } ]);
-
-
模块注册:
- Webpack 运行时接收到 chunk 后,会将其注册到模块系统中
- Angular 接收到模块类后,会初始化该模块
为什么 Angular 选择 Webpack 动态导入?
-
与框架深度集成:
- Angular 的模块系统(
NgModule
)和依赖注入需要完整的运行时环境,<script>
标签无法满足这种需求。
- Angular 的模块系统(
-
工程化优势:
- Webpack 可以自动处理代码拆分、依赖树摇树(Tree-shaking)和哈希缓存。
-
开发体验:
- 开发者只需关注业务逻辑(通过
loadChildren
配置路由),无需手动管理脚本加载。
- 开发者只需关注业务逻辑(通过
-
性能优化:
- 支持更复杂的策略(如预加载、并行加载),而
<script>
标签的功能有限。
- 支持更复杂的策略(如预加载、并行加载),而