Angular的懒加载由浅入深

Angular 懒加载详解

懒加载(Lazy Loading)是 Angular 中一种重要的性能优化技术,它允许你将应用分割成多个按需加载的模块,从而减少初始加载时间。

什么是懒加载

懒加载是一种路由级别的代码分割技术,它允许 Angular 应用在用户导航到特定路由时才加载对应的模块,而不是在应用启动时就加载所有模块。这可以显著减少应用的初始包大小,提高首屏加载速度。

懒加载的优势

  1. 减少初始包大小:只有核心模块在应用启动时加载
  2. 提高首屏加载速度:用户不需要等待所有代码下载完才能使用应用
  3. 按需加载:只有当用户访问特定功能时才加载对应代码
  4. 更好的用户体验:特别是对于大型应用或网络条件较差的用户

实现懒加载的步骤

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 }
];

验证懒加载是否工作

  1. 打开浏览器开发者工具
  2. 切换到 Network 选项卡
  3. 刷新应用,观察初始加载的文件
  4. 导航到懒加载路由,观察是否加载了新 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 }
}

懒加载的最佳实践

  1. 合理划分模块:按功能域划分模块,避免模块过大或过小
  2. 共享模块:将常用组件、指令、管道放入共享模块
  3. 避免服务重复:确保服务只在根模块或核心模块提供
  4. 路由守卫:可以在懒加载模块中使用路由守卫
  5. 性能监控:使用 Angular 的性能工具监控懒加载效果

常见问题解决

1. 循环依赖问题

确保模块之间没有循环依赖,特别是共享模块和懒加载模块之间。

2. 服务作用域问题

如果希望在懒加载模块中使用单例服务,确保服务在根模块提供或在核心模块中使用 providedIn: 'root'

3. 路由配置错误

确保懒加载路由的路径正确,并且模块正确导出。

懒加载的核心机制

  1. 生成独立 chunk 文件

    • 当使用 loadChildren 配置懒加载时,Angular CLI 和 Webpack 会将这些模块打包成独立的 JavaScript 文件(chunk)
    • 这些文件通常命名为 products-module-[hash].js 等形式
    • 它们不会包含在主包(main.js)中
  2. 异步加载模块

    • 使用 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 实际引用流程

当用户访问懒加载路由时:

  1. 路由触发

    javascript 复制代码
    // Angular 路由器内部处理
    router.navigateByUrl('/products');
  2. 解析懒加载配置

    javascript 复制代码
    // Angular 路由器的内部处理流程
    const loadModule = route.loadChildren; // 获取配置的加载函数
    loadModule().then(module => {
      // 模块加载完成后的处理
    });
  3. Webpack 动态加载

    javascript 复制代码
    // Webpack 编译后的实际代码
    __webpack_require__.e("products-module")
      .then(__webpack_require__.bind(__webpack_require__, "./src/app/products/products.module.ts"))
      .then(m => m.ProductsModule)
  4. 网络请求

    • 浏览器会发起对 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; });
            // 模块实际代码...
          }
        }
      ]);
  5. 模块注册

    • Webpack 运行时接收到 chunk 后,会将其注册到模块系统中
    • Angular 接收到模块类后,会初始化该模块

为什么 Angular 选择 Webpack 动态导入?

  1. 与框架深度集成

    • Angular 的模块系统(NgModule)和依赖注入需要完整的运行时环境,<script> 标签无法满足这种需求。
  2. 工程化优势

    • Webpack 可以自动处理代码拆分、依赖树摇树(Tree-shaking)和哈希缓存。
  3. 开发体验

    • 开发者只需关注业务逻辑(通过 loadChildren 配置路由),无需手动管理脚本加载。
  4. 性能优化

    • 支持更复杂的策略(如预加载、并行加载),而 <script> 标签的功能有限。
相关推荐
tianzhiyi1989sq15 分钟前
Vue3 Composition API
前端·javascript·vue.js
今禾21 分钟前
Zustand状态管理(上):现代React应用的轻量级状态解决方案
前端·react.js·前端框架
用户25191624271123 分钟前
Canvas之图形变换
前端·javascript·canvas
今禾31 分钟前
Zustand状态管理(下):从基础到高级应用
前端·react.js·前端框架
gnip37 分钟前
js模拟重载
前端·javascript
Naturean40 分钟前
Web前端开发基础知识之查漏补缺
前端
curdcv_po41 分钟前
🔥 3D开发,自定义几何体 和 添加纹理
前端
单身汪v1 小时前
告别混乱:前端时间与时区实用指南
前端·javascript
鹏程十八少1 小时前
2. Android 深度剖析LeakCanary:从原理到实践的全方位指南
前端
我是ed1 小时前
# cocos2 场景跳转传参
前端