Angular 17+ 高级教程 – Routing 路由 (原理篇)

前言

Angular 是 Single Page Application (SPA) 单页面应用,所谓的单页是站在服务端的角度看,不管游览器请求什么路径,它都返回 index.html 即可。

那站在客户端的角度,它就不是单页面了,不同的路径所呈现的内容都是不同的。

简单的说,就是把原本服务端负责的路由 + 多页面,交给了客户端负责。

Angular 自带了一套完整的路由方案,这个方案使用起来是简单的,尽管其中的功能和配置比较多比较杂,但不用担心,我们逐个学习就可以了。

然而,要想深入理解整套路由机制原理就不是那么简单了,这个难度大概和 NodeInjector、Change Detection、Dynamic Component 不相上下。

过去我们都是由浅入深的学习 Angular,这一次我们来点不一样的吧🤪,反过来由深入浅。

我会分 2 篇文章来讲解 Angular Routing,本篇会先讲解原理,下一篇才教各种配置玩法。

开始吧🚀。

 

参考

Angular Router: Getting to know UrlTree, ActivatedRouteSnapshot and ActivatedRoute

 

Routing 的基本原理

要让客户端负责路由,有几个步骤需要做到:

  1. 路由配对

    不同的路径要对应不同的内容,或者说不同的组件,比如说路径 /about 对应 About 组件,显示 About 的内容,路径 /contact 对应 Contact 组件,显示 Contact 内容。

    使用 Angular Routing,我们只需要提供路径和组件的配对关系就可以了,Angular 会获取游览器的 URL 地址通过配对找出对应的组件,

    然后通过 Dynamic Component 的方式创建组件,接着用 ViewContainerRef.insert 插入到页面里。

  2. 屏蔽游览器的路由机制

    Angular 会监听所有 <a> element 的点击事件,通过 event.preventDefault 方法阻止游览器默认行为。

    游览器默认会做 2 件事件,第一件是更新 URL 地址,第二件是发请求到服务端。

    虽然我们只是希望阻止它发请求,但是 preventDefault 没得选,它同时也会阻止 URL 地址的更新,

    因此,在执行 preventDefault 之后,Angular 还会利用 History API 自行更新 URL 地址。

  3. 监听 URL 地址变更

    无论是 <a> href 还是 history back / forward,Angular 都会监听到。

    它是透过 window popstate 事件监听到 history back / forward 的。

    每一次变更,Angular 就会重新配对然后切换组件。 

大体上就是下图这两套步骤

当然往细看还有很多小概念,比如 lazyload 组件、reuse 组件、multiple outlet、multilayer outlet、authen/auth、scrolling 等等等。

本篇我们主要是深入理解最核心的几个机制/概念/原理,其它较上层/独立/额外的部分,我们留给下一篇。

 

Routing Get Started

我们先来大体感受一下,使用 Angular CLI 创建一个带 routing 的项目。

ng new routing --routing --skip-tests --ssr=false --style=scss

关键是 --routing

多了几样东西

  1. app-routes.ts

    这个是让我们写路径与组件配对逻辑的地方。

  2. app.config.ts

    多了一个 provideRouter,provideRouter 函数的源码在 provide_router.ts

    里面最关键的是 APP_BOOTSTRAP_LISTENER,顾名思义它就是一个启动函数。

    默认情况下,routing 会在 App 组件渲染完成后启动,相关源码在 application_ref.ts 里的 _loadComponent 函数。

    这个 _loadComponent 函数以前我们在逛 NodeInjector 源码就有研究过了,这里不再复述细节。

  3. App Template

    这个 <router-outlet /> 是一个结构型指令,它的职责就是插入对应的组件到这个位置。

    通常我们可以把 App 组件作为 layout,header 和 footer 是每个页面重复的,交给 layout 负责,中间的内容是随着不同路径改变的,这个则交给 <router-outlet /> 负责。

我们创建 3 个组件来表现 Home、About、Contact 页面 (我这里是以企业网站作为例子)。

ng g c home; ng g c about; ng g c contact

接着添加路径与组件配对到 app-routes.ts

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },           // 配对 URL: '/'
  { path: 'about', component: AboutComponent },     // 配对 URL: '/about'
  { path: 'contact', component: ContactComponent }, // 配对 URL: '/contact'
];

提醒:path 不需要也不可以有 leading slash。

效果

 

Angular の URL 结构

URL -> 配对 -> 输出组件是本篇要讲解的核心,我们就从 URL 地址结构开始吧。

下图是我们比较熟悉的 URL 结构。

前半段的 scheme host port 我们不管,我们只关注后半段 path query fragment 就好。

Angular Routing URL 比起常见的 URL 结构要复杂得多,它多了 4 个概念:

Segment & Segment Group

长长的 path 在 Angular 会被 split by slash '/' 变成一个一个 Segment。

比如:/products/iphone-14 代表有 2 个 Segment,用 Array 来表示的话长这样 ['products', 'iphone-14']。

[/path] will become [/segment1/segment2/segment3]

而多个 Segment 合在一起则被称为 Segment Group。

简而言之,一个 path 被分解为多个 Segment,多个 Segment 又称作 Segment Group,所以 path 等于 Segment Group。

Segment Parameters (a.k.a Matrix Parameters)

/products;key1=value1/iphone-14;key2=value2

;key1=value1 是 segment1 (products) 的 parameters

;key2=value2 是 segment2 (iphone-14) 的 parameters

?query 是整个 URL 的 parameters (a.k.a query parameters),一个 URL 只能有一个 query,在复杂的项目或许会不够用,所以 Angular 搞了多一个 Segment Parameters 的概念。

每一个 Segment 都可以设置专属于这个 Segment 的 parameters。

Multiple Segment Group (multiple <router-outlet />)

/about(secondary:contact//tertiary:blog)

关键是中间多了  ...(secondary:...//tertiary:...)

它的意义是让我们在 URL 里能表达 multiple path (a.k.a multiple Segment Group),它有什么用呢?

一个 path 配对一个组件,插入一个 <router-outlet />。

multiple path 配对 multiple 组件,插入 multiple <router-outlet />。

看例子

上面设置了 3 个配对,不同的 path,不同的组件,不同的 router-outlet。

这个 URL /about(secondary:contact//tertiary:blog) 会同时配对成功上面 3 个。

配对成功后,3 个组件都会被创建,然后插入到对应的 router-outlet 位置。

效果

Multiple Segment Group with Multilayer (nested <router-outlet />)

我们先了解什么是 Multilayer。

下面是 Single Layer:

访问 /products/iphone-14 效果:

我们把它改成 Multilayer,在 ProductDetail Template 加入一个 <router-outlet />。

这个 outlet 要展现 3 种内容:

  1. product description
  2. product reviews
  3. related products

添加路径与组件配对

效果

<router-outlet /> 输出的组件内又有另一个 <router-outlet />,这就是所谓的 Multilayer。

Multilayer 对项目管理很有帮助,后端 (比如 ASP.NET Core Razor Pages) 方案,一个页面最多只能分成 Layout 和 Body 两层,

而 Angular 这种 Multilayer 不只能实现 Layout 和 Body,Body 还可以继续往下细分,非常灵活。

当 Multiple Segment Group 遇上 Multilayer

好,回到主题,当 Multiple Segment Group 遇到 Multilayer 时,URL 结构会有点不同。

这个是 root layer 有 Multiple Segment Group 的 URL

/about(secondary:contact//tertiary:blog)

这个是 child layer 有 Multiple Segment Group 的 URL

/products/iphone-14/(reviews//secondary:related-products)

可以看到 root layer 和 child layer 的写法是不同的

child layer 一开始就括弧了,primary path 和 secondary path 通通在括弧里面,而 root layer 则一开始是 primary path 接着才是括弧,primary path 没有在括弧里面。

root 和 child 同时有 Multiple Segment Group 的 URL

/about/(about-a1//secondary:about-a2)(secondary:contact//tertiary:blog)

grandchild 有 Multiple Segment Group 的 URL

/about/(about-a1/(about-b1//secondary:about-b2)//secondary:about-a2)

child 和 grandchild 的写法是一样的,primary path 和 secondary path 通通在括弧里面。

Multilayer + Multiple Segment Group 完整例子

/about/(about-a1/(about-b1//secondary:about-b2)//secondary:about-a2)(secondary:contact//tertiary:blog)

URL 有点吓人。它有 3 个 layer,每一个都有 Multiple Segment Group。 

配对

export const routes: Routes = [
  {
    outlet: 'primary',
    path: 'about',
    component: AboutComponent,
    children: [
      {
        outlet: 'primary',
        path: 'about-a1',
        component: AboutA1Component,
        children: [
          {
            outlet: 'primary',
            path: 'about-b1',
            component: AboutB1Component,
          },
          {
            outlet: 'secondary',
            path: 'about-b2',
            component: AboutB2Component,
          },
        ],
      },
      { outlet: 'secondary', path: 'about-a2', component: AboutA2Component },
    ],
  },
  { outlet: 'secondary', path: 'contact', component: ContactComponent },
  { outlet: 'tertiary', path: 'blog', component: BlogComponent },
];

App Template

<header>header</header>

<router-outlet name="primary" />   <!--这里会插入 About 组件-->
<router-outlet name="secondary" /> <!--这里会插入 Contact 组件-->
<router-outlet name="tertiary" />  <!--这里会插入 Blog 组件-->

<footer>footer</footer>

About Template

<p>about works!</p>
<router-outlet name="primary" />   <!--这里会插入 AboutA1 组件-->
<router-outlet name="secondary" /> <!--这里会插入 AboutA2 组件-->

AboutA1 Template

<p>about-a1 works!</p>
<router-outlet name="primary" />   <!--这里会插入 AboutB1 组件-->
<router-outlet name="secondary" /> <!--这里会插入 AboutB2 组件-->

最终一个 URL 配对出 7 个组件

注:上面只有 About 有 Multilayer,其实 Blog 和 Contact 也是可以有 Multilayer 的。我没有加进去只是因为它们的原理和 About 是一样的,所以没有必要作为例子。

总结

Angular URL 结构与众不同的地方主要是在 path 的部分,?query 和 #fragment 和常见的 URL 是一样的。

path 的部分主要有 4 个概念:

  1. Segment 和 Segment Group 概念

  2. Segment Parameters (a.k.a Matrix Parameters) 概念

  3. Multiple Segment Group 的概念

  4. Multilayer + Multiple Segment Group 的概念

由于本篇主要是讲解原理,所以我们不需要过多的在意具体的实现代码,我们关注它的原理就好,下一篇我会给更多具体例子。

 

UrlTree

Angular 团队非常喜欢树,之前的几篇文章中,我们就学习过好几棵树,比如 NodeInjector Tree、Logical View Tree、DOM Tree。

Angular Routing 又多出了好几棵树 😧。

由于 Angular 的 URL 结构异常复杂,如果只使用 string 会很难操作,于是 Angular 搞了一个 UrlTree 的概念,简单说就是把 URL string 变成 URL 树形结构对象。

class UrlTree

UrlTree 长这样,源码在 url_tree.ts

上一 part 我们有提到,一个 URL 包含 /path ?query #fragment 

UrlTree 的 queryParams 属性对应 ?query

fragment 属性对应 #fragment 

/path 则被分割成多个 Segment,而多个 Segment 又称为 Segment Group,它就对应了 root 属性。

class UrlSegmentGroup 源码也是在 url_tree.ts

segments 属性装的是从 path 分割出来的 Segment List。

children 用于表达 Multilayer 和 Multiple Segment Group,它是一个 key value pair 类型,key 指的是 Segment Group 的名字,比如 primary、secondary、tertiary。

class Segment 源码也是在 url_tree.ts

属性 path 就是被分割出来的 string value,parameters 则是属于这个 Segment 的 Parameters (Segment Parameters or Matrix Parameters)。

Example for UrlTree

我们来看一些例子,感受一下:

query parameters & frament

URL 长这样

/about?key1=value%201#target-id

UrlTree 长这样

属性 fragment 对应 #fragment

属性 queryParams 对应 ?query

URL string 是 encoded 的,比如空格是 %20,而 UrlTree 是 decoded 的,%20 会转换回空格,所以我们在操控 UrlTree 时不需要去顾虑 encode/decode 的问题。

Root Segment Group & empty path

URL 长这样

/

UrlTree 长这样

无论如何,UrlTree 一定会有 Root Segment Group,哪怕是 empty path。

segments 是空的,children 也是空的。

Single & Multiple Segment Group

URL 长这样

/about

UrlTree 长这样

这个 UrlTree 有几个反直觉的地方:

  1. 为什么是两个 Segment Group,而不是一个?
  2. 为什么 Root Segment Group 的 segments 是空 array?
  3. 为什么 about 被放入了 children primary Segment Group 里,而不是 Root Segment Group?

感觉 Root Segment Group 在这里根本是个多余的丫,它完全没有负责任何东西,资料都记入在 children primary Segment Group 里。

原因是这样的,试想想如果 URL 是一个 Multiple Segment Group,比如 

/about(secondary:contact//tertiary:blog)

它的 UrlTree 长这样

此时 Root Segment Group segments 是空 Array 就变得合理了。

为了统一结构,Root Segment Group 是不用于配对 path 的,它的 segments 一定是一个空 Array,path 只会交给 children。

绝大部分的情况下,UrlTree 里面最少有 2 层 -- Root and Children。(empty path 是例外,它不会有 children)

另外一点,如果 URL 是 Single Segment Group,而且没有表明 Segment Group Name,那它默认名是 'primary'。

比如上面的 /about

那如果 URL 有表明 Segment Group Name,那就依据 Segment Group Name

比如 /(secondary:about)

Segment with Segment Parameters

URL 长这样

/prodcuts;key1=value%201/iphone-14;key2=value%202

UrlTree 长这样

Segment Parameters 被记入到各自的 Segment 中。

Multilayer + Single Segment Group

URL 其实没有 Multilayer + Single Segment Group 的概念。我们看一个例子

URL 长这样

/products/iphone-14/reviews

配对长这个

export const routes: Routes = [
  {
    path: 'products/iphone-14',
    component: ProductDetailComponent,
    children: [{ path: 'reviews', component: ProductReviewComponent }],
  },
];

效果

有 2 层 router-oulet,所以它是 Multilayer,但是它没有 Multiple Segment Group。

UrlTree 长这样

只有 2 层而已。

不管是 Single Layer 还是 Multilayer,只要不是 Multiple Segment Group,那它们的 URL 结构都是一样的,不同的地方是配对设置和 router-outlet 指令。

Multilayer + Multiple Segment Group

URL 长这样

/about/(about-a1//secondary:about-a2)

UrlTree 长这样

第一层 Root 和第二层 primary Segment Group 

第三层 primary 和 secondary Segment Group

只有 Multilayer + Multiple Segment Group 才会让 UrlTree 突破 2 层。

to UrlTree and from UrlTree

通过 Router.parseUrl 可以把一个 URL string 转换成 UrlTree 对象。

export class AppComponent {
  constructor() {
    const router = inject(Router);
    const urlTree = router.parseUrl('/about(secondary:contact)');
    console.log(urlTree.root.children['secondary'].segments[0].path); // 'contact'
  }
}

Router 还有很多功能,本篇会一一介绍。

只要执行 UrlTree.toString 方法,就可以把 UrlTree 对象转换成 URL string。

export class AppComponent {
  constructor() {
    const router = inject(Router);
    const urlTree = router.parseUrl('/about(secondary:contact)');
    console.log(urlTree.toString()); // '/about(secondary:contact)'
  }
}

build UrlTree by command

用 Router.parseUrl 生成 UrlTree 不是一个好主意,更方便的方式是通过 command。

export class AppComponent {
  constructor() {
    const router = inject(Router);
    const urlTree = router.createUrlTree([
      { outlets: { primary: ['about'], secondary: ['contact'] } },
    ]);
    console.log(urlTree.toString() === '/about(secondary:contact)'); // true
  }
}

使用 Router.createUrlTree 方法可以通过 command 的方式创建 UrlTree。

command 是一个 string Array,它有一些潜规则,我们看看各种例子:

export class AppComponent {
  constructor() {
    const router = inject(Router);

    router.createUrlTree(['about']).toString(); // '/about' 

    // Query Parameters
    router.createUrlTree(['about'], { queryParams: { key1: 'value1' } }).toString(); // '/about?key1=value1' 注:Angular 会提我们 encode
    
    // Fragment
    router.createUrlTree(['about'], { fragment: 'target-id' }).toString(); // '/about#target-id
 
    router.createUrlTree(['products', 'iphone-14']).toString(); // '/products/iphone-14' 
    router.createUrlTree(['products/iphone-14']).toString();    // '/products/iphone-14' 和上一个是一样的,不过推荐统一使用上一个就好

    // Segment Parameters (a.k.a Matrix Parameters)
    router.createUrlTree(['products', { key1: 'value1' }, 'iphone-14']).toString(); // '/products;key1=value1/iphone-14'

    // Multiple Segment Group
    router.createUrlTree([{ outlets: { primary: ['about'], secondary: ['contact'], tertiary: ['blog'] } }]); // '/about(secondary:contact//tertiary:blog)'

    // Multilayer + Multiple Segment Group
    router.createUrlTree(['products', { outlets: { primary: ['iphone-14'], secondary: ['contact'], tertiary: ['blog'] } }]); // '/products/(iphone-14//secondary:contact//tertiary:blog)'
  }
}

好,UrlTree 就介绍到这里。

 

Route

URL 讲完了,下一个是配对。

Route 又名 Route Config (就是我们上面一直提到的配对设置),它的主要作用是配置不同的 URL 要做出什么对应的 action。

比如说,当 URL 是 /about 时,创建 About 组件然后插入到指定 router-outlet。

又比如,当 URL 是 /contact-us 时,redirect 到 URL /contact。

除了配置 URL 与 action,Route 还可以配置其它的小功能,不过这篇我们 focus 在输出组件和 redirect 就好,其它的留给下一篇。

Routes

Routes 就是 Route Array,长这样

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },           // 当 URL 是 / 时,router-outlet 输出 Home 组件
  { path: 'about', component: AboutComponent },     // 当 URL 是 /about 时,router-outlet 输出 About 组件
  { path: 'contact', component: ContactComponent }, // 当 URL 是 /contact 时,router-outlet 输出 Contact 组件
  { path: 'about-us', redirectTo: 'about' }         // 当 URL 是 /about-us 时,redirect 到 URL /about
];

URL 的配对过程是 for loop Routes,一个一个 Route 与 URL 进行配对,配对成功就采取 Route 的 action,然后停止配对,配对失败则继续尝试下一个。

Route.matcher

Route.path 是一个上层配置,配对的底层原理其实依靠的是 Route.matcher 方法。只是 Angular 内置了一个 matcher 作为默认,因此我们在日常使用中才可以通过上层的 Route.path 表达配对关系。

既然我们要搞清楚原理,那自然是要从 matcher 方法学习起咯。

一个 Multilayer 的例子

export const routes: Routes = [
  {
    path: 'products/iphone-14', // 配对 /products/iphone-14
    component: ProductDetailComponent, 
    children: [
      { path: '', component: ProductDescriptionComponent },             // 配对 /products/iphone-14
      { path: 'reviews', component: ProductReviewComponent },           // 配对 /products/iphone-14/reviews
      { path: 'related-products', component: RelatedProductComponent }, // 配对 /products/iphone-14/related-products
    ],
  },
];

我们把它改成用 matcher 方法来实现。

matcher 方法长这样

export const routes: Routes = [
  {
    matcher: (segments, group, route) => {
      return null;
    },
    component: ProductDetailComponent,
  },
];

它有三个参数:

  1. segments

    在配对的过程中,我们比对的不是 URL string 而是 UrlTree 对象。

    这个 segments 指的就是 UrlTree 里的 Sement Group 里的 segments。

  2. group

    group 就是 Segment Group。

    我们知道 Segment Group 有可能会有 Multiple 和 Multilayer 概念,这里 Angular 会替我们选好,

    比如说,UrlTree 的第一层 Segment Group 是 root Segment Group,它不用于配对,它的 segments 一定是空 Array,所以 Angular 会直接跳过它,拿第二层的 Segment Group 来配对。

    再比如,当我们设置 Route.outlet: 'secondary',那 Angular 就会拿 UrlTree.root.children['secondary'] 的 Segment Group 来调用 matcher 方法。

    segments 就是从 Segment Group 拿出来的而已。

  3. route

    route 就是当前 Route 的 clone 版本。

return null 表示配对失败,Angular 会尝试下一个 Route。

那如果配对成功的话,需要这样返回

  {
    matcher: (segments, _group, _route) => {
      // URL: /products/iphone-14/reviews
      if (segments.slice(0, 2).join('/') === 'products/iphone-14') {
        return {
          consumed: segments.slice(0, 2),
        };
      }
      return null;
    },
    component: ProductDetailComponent,
  },
];

只要头两个 Segment 分别为 'products' 和 'iphone-14' 就算配对成功。

返回一个带有 consumed 属性的对象。

consumed 的意思是 “已消耗",怎么理解?

例子中的 URL 是 '/products/iphone-14/reviews',split by slash 后会有 3 个 Segment。

而这个 matcher 方法只检查了头两个 Segment 就配对成功了,那第三个 Segment 'reviews' 又谁来处理呢?

答案是交给 children Routes。matcher 方法需要表达自己只消耗了头两个 Segment,然后 Angular 会把第三个 Segment 交给 children Routes 去处理。

我们加上 children Routes:

export const routes: Routes = [
  {
    // URL: /products/iphone-14/reviews
    matcher: (segments, _group, _route) => {
      if (segments.slice(0, 2).join('/') === 'products/iphone-14') {
        return {
          consumed: segments.slice(0, 2),
        };
      }
      return null;
    },
    component: ProductDetailComponent,
    children: [
      {
        matcher: (segments, _group, _route) => {
          console.log(segments.length === 1); // true
          console.log(segments[0].path === 'reviews'); // true

          if (segments[0].path === 'reviews') {
            return {
              consumed: segments,
            };
          }
          return null;
        },
        component: ProductReviewComponent,
      },
    ],
  },
];

children 的 matcher 方法参数 segments 就只剩下一个 Segment,因为上一层已经消耗了 2 个 Segment。

消耗不完 Segment 怎么办?

上面例子中有 3 个 Segment ['products', 'iphone-14', 'reviews'],如果我们只有一个 Route,它 consumed 了头两个 Segment,但没有 children Routes 了,

那剩下的第三个 Segment 会怎样?答案是直接配对失败,Angular 会尝试下一个 Route,又从 3 个 Segment 开始执行 matcher 方法。

过早消耗完 Segment 怎么办?

假设只有 2 个 Segment ['products', 'iphone-14'],Route consumed 了两个 Segment,但它还有 children Routes 会怎么样?

答案是上层的 Route 配对成功可以执行 action(比如输出组件),而 children Routes 配对失败,不执行 child Route 的 action。

消耗不完 vs 过早消耗完

消耗不完会导致上层原本配对成功的 Route 也算失败,而过早消耗完则会保留上层原本配对成功的 Route。

好,Route 我们就先学到这里。

 

ActivatedRouteSnapshot、RouterStateSnapshot、ActivatedRoute、RouterState

ActivatedRouteSnapshot、RouterStateSnapshot、ActivatedRoute、RouterState 息息相关,而且长的很像,基本上掌握一个就等于掌握了全部。

What is ActivatedRouteSnapshot?

什么是 ActivatedRouteSnapshot? 我们先忽略掉结尾的 snapshot,ActivatedRoute 顾名思义就是 "已激活" 的 Route。

这是 Routes

export const routes: Routes = [
  { path: '', component: HomeComponent },           // 当 URL 是 / 时,App Template 的 router-outlet 输出 Home 组件
  { path: 'about', component: AboutComponent },     // 当 URL 是 /about 时,App Template 的 router-outlet 输出 About 组件
  { path: 'contact', component: ContactComponent }, // 当 URL 是 /contact 时,App Template 的 router-outlet 输出 Contact 组件
  {
    path: 'products/iphone-14',                     // 当 URL 是 /products/iphone-14 时,App Template 的 router-outlet 输出 ProductDetail 组件
    component: ProductDetailComponent,
    children: [
      // 当 URL 是 /products/iphone-14 时,ProductDetail Template 的 router-outlet 输出 ProductDescription 组件
      { path: '', component: ProductDescriptionComponent },             

      // 当 URL 是 /products/iphone-14/reviews 时,ProductDetail Template 的 router-outlet 输出 ProductReview 组件
      { path: 'reviews', component: ProductReviewComponent },           

      // 当 URL 是 /products/iphone-14/related-products 时,ProductDetail Template 的 router-outlet 输出 RelatedProduct 组件
      { path: 'related-products', component: RelatedProductComponent }  
    ]
  },
];

每一次 URL 变更的时候,Angular Routing 都会进行一轮 UrlTree vs Routes 配对,配对成功的 Route 会拿来生成 ActivatedRouteSnapshot,表示这个 Route 被 "激活" 了。

比如

ProductDetail Route 和 /products/iphone-14 Segment Group 配对成功,生成了 ProductDetail ActivatedRouteSnapshot。

RelatedProduct Route 和 /related-products Segment Group 配对成功,生成了 RelatedProduct ActivatedRouteSnapshot。

Route 是树形结构,ActivatedRouteSnapshot 自然也是树形结构。yeah🎉我们又学了多一棵树。

ActivatedRouteSnapshot 有啥用

ActivatedRouteSnapshot 保存了 Route 和 UrlTree 的资料,比如 Query Paramteres、Segment Parameters 等等。

至于为什么它是 snapshot 呢?下面讲解 ActivatedRoute 时会揭晓。

 

 

 

 

Root ActivatedRouteSnapshot

UrlTree 有一个 Root Segment Group,它很特别,它不被用于配对 path,它的 segments 总是空 Array,它就是一个根。

ActivatedRouteSnapshot 也有这么一个特别的根。

export const routes: Routes = [
  { path: 'about', component: AboutComponent }
];
export class AboutComponent {
  constructor() {
    const activateRoute = inject(ActivatedRoute);
    console.log(activateRoute.parent!.routeConfig); // null
    console.log(activateRoute.parent!.component === AppComponent); // true
    console.log(activateRoute.parent!.parent); // null
  }
}

About ActivatedRoute 并不是 Root ActivatedRoute。

Root ActivatedRoute 是没有 routeConfig 的,而且它对应的 component 是 Root Component 也就是 App 组件。是不是也很特别?

 

 

 

What is ActivatedRouteSnapshot?

顾名思义 ActivatedRouteSnapshot 就是 ActivatedRoute 的快照版本,那为什么要搞一个快照呢?

Route Reuse

我们需要理解一个新概念 Route Reuse。

服务端的 Routing 行为是:每当更换 URL 地址,整个页面都会销毁重新渲染,JavaScript state 全部清空。

Angular Routing 默认行为是:每当更换 URL 地址,部分组件会被销毁创建新的,部分组件不会被销毁,它们会被 reuse。

举例:

URL 从 

/products/iphone-14/related-products

切换到

/products/iphone-14/reviews

ActivatedRoute 从 ProductDetail -> RelatedProduct

切换到 ProductDetail -> ProductReview

Angular 会 reuse ProductDetail ActivatedRoute 和 ProductDetail 组件,

会销毁 RelatedProduct,并创建新的 ProductReview ActivatedRoute 和组件。

这就是 Route Reuse 概念。

RouteReuseStrategy

哪一些 ActivatedRoute 会被 reuse,哪一些要销毁创建新的,取决于 RouteReuseStrategy Service Provider。

RouteReuseStrategy 源码在 route_reuse_strategy.ts

RouteReuseStratefy 源码也在 route_reuse_strategy.ts

好,Route Reuse 就介绍到这里,下一篇我会给出更多 Route Reuse 的例子。

我们回到 what is ActivatedRouteSnapshot?

ActivatedRouteSnapshot 是 ActivatedRoute 的快照资料,ActivatedRoute 有可能被 reuse,所以它的资料是通过 RxJS 流曝露出去的,

使用者需要监听 RxJS 流才能获取到资料。ActivatedRouteSnapshot 是快照资料,它没有 reuse 概念,每一次都没有被重新创建,所以

 

ActivatedRoute 有可能 reuse,而 ActivatedRouteSnapshot 则绝对不会,每一次 ActivatedRouteSnapshot 都会被创建新的。 

而且 Angular Routing 在处理导航过程中其实是先创建 ActivatedRouteSnapshot 后才创建或 reuse ActivatedRoute 的。

ActivatedRoute 有啥用?

ActivatedRoute 的主要用途是让组件获取到和 Route 相关的资料。

举例:

/products/iphone-14;key1=value1/reviews

它会配对到 2 个 Route。

;key1=value1 这个 Segment Parameters 属于第二个 Segment,而第二个 Segment 被 ProductDetail Route 消耗了,于是这个 Segment Parameters 将被记入到它生成的 ProductDetail ActivatedRoute 中。

我们在 ProductDetail 组件里可以获取到这个 Segment Parameters。

ProductDetail 组件

export class ProductDetailComponent {
  constructor() {
    const activatedRoute = inject(ActivatedRoute);
    console.log('Segment Parameters', activatedRoute.snapshot.params); // { key1: 'value1' }
  }
}

三个重点:

  1. ActivatedRoute 是通过 DI 注入的

  2. 不同组件注入的 ActivatedRoute 是不同的,

    比如 ProductDetail 组件注入的是 ProductDetail ActivatedRoute,

    RelatedProduct 组件注入的是 RelatedProduct ActivatedRoute。

    这个是依据 Route.component 的配置。

    我们再看多两个极端的例子

    例子一:

    这是 Routes

    export const routes: Routes = [
      { path: 'about', component: AboutComponent }, 
    ];

    在 About Template 中有一个 AboutContent 组件

    问:About 组件和 AboutContent 组件 inject ActivatedRoute 是同一个吗?

    答:是同一个。

    其原理大致是这样,About Route 配对成功后会创建 About ActivatedRoute,然后会动态创建 About 组件。

    动态创建组件时可以传入一个 Injector (不熟悉 Dynamic Component 的朋友请看这篇)。

    Angular 会传入一个 OutletInjector 

    OutletInjector 会对 inject(ActivatedRoute) 做特别处理。

    About 组件通过这个 OutletInjector 就会 inject 到 About ActivatedRoute。

    AboutContent 组件 inject ActivatedRoute 时,AboutContent NodeInjetor 会往上找,最终就找到了 About ActivatedRoute。(不熟悉 NodeInjector 的朋友请看这篇)

    例子二:

    添加一个 children Routes

    export const routes: Routes = [
      {
        path: 'about',
        component: AboutComponent,
        children: [{ path: '', component: AboutContentComponent }],
      },
    ];

    在 About Template 中添加一个 router-outlet

    AboutContent 组件会被创建 2 次,一次来自 <app-about-content /> 一次来自 <router-outlet />。

    问:这两次 AboutContent inject 的 ActivatedRoute 是同一个吗?

    答:不是同一个,<app-about-content /> inject 到的是 About ActivatedRoute,而 <router-outlet /> 输出的 AboutContent 组件 inject 到的是 AboutContent ActivatedRoute。

    因为 <router-outlet /> 输出的组件是动态创建的,它有 OutletInjector 这就如同 About 组件 inject 的是 About ActivatedRoute 那样。

  3. snapshot 是资料的快照,所以可以直接读取,如果不使用 snapshot,资料将以 RxJS 流的方式获取。

    export class ProductDetailComponent {
      constructor() {
        const activatedRoute = inject(ActivatedRoute);
        activatedRoute.params.subscribe((params) => console.log(params)); // { key1: 'value1' }
      }
    }

    当 URL 从 /products/iphone-14;key1=value1/reviews 切换到 /products/iphone-14;key1=value2/reviews,

    只有 value1 变成了 value2,此时所有的 ActivatedRoute 都会被 reuse,组件也不会被销毁,我们唯一能感知和获取到最新 params 的方法就是通过 subscribe ActivatedRoute.params 流。

    Route Reuse 概念这篇不会深入,下一篇才会详细讲解,这篇我们只要知道有这个概念就可以了。

此外,ActivatedRoute 也可以获取到 Query Parameters 和 Fragment

const activatedRoute = inject(ActivatedRoute);
console.log(activatedRoute.snapshot.queryParams); // { key1: 'value1' }
console.log(activatedRoute.snapshot.fragment); // 'target-id'

Segment Paramters 有专属于某个 Segment 的概念,Query Parameters 和 Fragment 就没有,所以不管哪一个 ActivatedRoute 获取到的 Query Parameters 和 Fragment 都是一样的。

此外,ActivatedRoute.routeConfig 可以获取到对应的 Route 对象。

export const routes: Routes = [
  { path: 'about', component: AboutComponent }
];
export class AboutComponent {
  constructor() {
    const activateRoute = inject(ActivatedRoute);
    console.log(activateRoute.routeConfig); // { path: 'about', component: AboutComponent }
  }
}

 

What is RouterState?

RouterState 是一棵 ActivatedRoute Tree,它维护着所有 ActivatedRoute 的 parent child 关系。

我们通过 Router.routerState 可以获取到当前 URL 配对后生成的 ActivatedRoute Tree。

export class AppComponent {
  constructor() {
    const router = inject(Router);
    router.events.subscribe((e) => {
      if (e.type === EventType.NavigationEnd) {
        console.log(router.routerState);
      }
    });
  }
}

它的结构是这样的

关键是 _root 属性,它是一个 TreeNode 类型。

TreeNode 有 2 个属性,一个是 value,一个是 children。

value 装的是 ActivatedRoute 对象

children 装的是子层 TreeNode。

日常中我们很少会直接使用 RouterState,我们通常是透过 ActivatedRoute 间接使用到 RouterState。

每一个 ActivatedRoute 都有一个私有属性 _routerState 指向 RouterState 对象,

ActivatedRoute.parent 可以获取到父 ActivatedRoute。

ActivatedRoute.children 可以获取到子 ActivatedRoute Array。

parent 和 children 是 getter 方法,内部就是依靠 _routerState 的树形结构完成查找的。

What is RouterStateSnapshot?

顾名思义,RouterState 是 ActivatedRoute Tree,那 RouterStateSnapshot 自然是 ActivatedRouteSnapshot Tree 咯。

通过 RouterState.snapshot 就可以获取到 RouterStateSnapshot 对象了。

好,ActivatedRoute 就先介绍到这里。

 

UrlTree、Route Tree、ActivatedRoute Tree、OutletContext Tree

这 4 棵树的关系就有点像 NodeInjector Tree、Logical View Tree、DOM Tree 那样,长得像但又不完全一样,却又息息相关。

总之,Angular Team 就喜欢搞这种让人傻傻分不清楚的东西出来😡。

下面我把这 4 棵树的结构特性列出来,帮助大家做区分。

UrlTree

UrlTree 是 URL string 的树形版本,这个树形结构是依据 URL string 解析出来的,它跟 Route、ActivatedRoute、Outlet 都没有关系。

只要给 Angular 一个 URL string 它就可以生成出 UrlTree 对象。

Route Tree

Route Tree 指的是我们配置的 Routes Array。

Route 的树形结构完全是由我们自己掌控的。

export const routes: Routes = [
  // 配对 /products/iphone-14
  {
    path: 'products/iphone-14',
    component: ProductDetailComponent,
  },
  // 同样配对 /products/iphone-14
  {
    path: 'products',
    component: ProductComponent,
    children: [
      {
        path: 'iphone-14',
        component: ProductDetailComponent,
      },
    ],
  },
];

上面 2 个 Route 都能配对 URL ‘products/iphone-14’,但是这 2 个 Route 的结构是不一样的,第一个 Route 没有 children,第二个却有 children。

所以说,虽然 Route Tree 是用于配对 UrlTree 的,但是 Route Tree 的树形结构是不受限于 UrlTree 结构的。

ActivatedRoute Tree

ActivatedRoute Tree 是配对成功的 Route Tree,所以它的树形结构不会脱离 Route Tree。

OutletContext Tree

通过 inject ChildrenOutletContexts,我们可以获取到 OutletContext Tree。

export class AppComponent {
  constructor() {
    const router = inject(Router);
    const childrenOutletContexts = inject(ChildrenOutletContexts);
    router.events.subscribe((e) => {
      if (e.type === EventType.NavigationEnd) {
        console.log('childrenOutletContexts', childrenOutletContexts);
      }
    });
  }
}

我们看个例子

URL 长这样

/products/iphone-14/reviews

Routes 长这样

export const routes: Routes = [
  {
    path: 'products/iphone-14',
    component: ProductDetailComponent,
    children: [{ path: 'reviews', component: ProductReviewComponent }],
  },
];

配对后会产生 2 个 ActivatedRoute。

ChildrenOutletContexts 长这样

Child OutletContext

OutletContext 的职责是把 ActivatedRoute 关联到它对应的 <router-outlet />。

OutletContext.outlet 就是 <router-outlet /> 指令的实例 (上图是 null 只因为我偷懒没有放 <router-outlet /> 指令在 Template😅)。

通常 OutletContext 的数量和 ActivatedRoute 的数量是一样的,树形结构也是一样的。

唯一的不同是 OutletContext Tree 是没有 Root OutletContext 的,它开头就是 ChildrenOutletContexts.contexts 一个 Map<string, OutletContext> 类型。

所以,没有任何 OutletContext 会关联到 Root ActivatedRoute,这也挺合理的,Root ActivatedRoute.component 是 App 组件,它自然不可能关联上任何 <router-outlet />。 

有一种情况会导致 OutletContext 和 ActivatedRoute 的数量不一样,树形结构也不一样。那就是当 Route 没有设置 component。

同一个例子,我们把 ProductDetail Route 的 component 属性注释掉。

export const routes: Routes = [
  {
    path: 'products/iphone-14',
    // component: ProductDetailComponent, // 注释掉 component 属性
    children: [{ path: 'reviews', component: ProductReviewComponent }],
  },
];

配对后依然会产生 2 个 ActivatedRoute,但只会产生一个 OutletContext,这个 OutletContext.route 指向 ProductReview ActivatedRoute。

OutletContext Tree 长这样

最后 ProductReview 组件会被插入到 App Template 的 <router-outlet />。

总结:OutletContext Tree 是依据 ActivatedRoute Tree 一对一生成的,只是它会过滤掉了没有 component 的 ActivatedRoute。

 

与 Routing 相关的 Internal Service Providers

Angular 内部使用了许多和 Routing 相关的 Services。我们要深入理解 Routing 最好把它们过一遍。

PlatformLocation

class PlatformLocation 的源码在 platform_location.ts

我们的 platform 是 Browser,所以看 class BrowserPlatformLocation,它的源码也是在 platform_location.ts

BrowserPlatformLocation 是 Angular 对 window.location 和 window.history 的封装。

它的所有方法内部都是调用了 window.location 或 window.history。

比如:

OnPopState 方法内部其实是监听了 window popstate 事件

href, protocal, hostname, port, pathname, search, hash 也只是调用 window.location 而已。

还有各种 History API 操作也是一样的

所以在 Angular 内部,它们不直接操作 window.location 和 window.history,取而代之的是通过 PlatformLocation 间接操作。

LocationStrategy

LocationStrategy 是对 PlatformLocation 的又一层封装。

PlatformLocation 封装的目的是让 Angular 与环境无关 (Angular 不局限于 Browser 环境),

那为什么又要 wrap 多一层 LocationStrategy 呢?

我们先了解一下什么是 hash location。

Routes

export const routes: Routes = [
  {
    path: 'products/iphone-14',
    component: ProductDetailComponent,
    children: [{ path: 'reviews', component: ProductReviewComponent }],
  },
];

app.config.ts

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes, withHashLocation())],
};

添加了一个 withHashLocation()

效果

下面这个是默认的 URL,又称为 path 版本 URL

http://localhost:4200/products/iphone-14

下面这个则是 hash 版本 URL

http://localhost:4200/#/products/iphone-14

关键是在中间多了个 /#/ 

它的用意是在 refresh browser 的时候,服务端是否需要处理多种不同路径还是只需要处理一种路径。

在 refresh browser 的时候,游览器一定会发请求到服务端。

如果使用 path URL,路径可能是

  1. http://localhost:4200/products/iphone-14

  2. http://localhost:4200/about

  3. http://localhost:4200/contact

  4. 只要是前端能匹配的路径都有可能在 refresh browser 时被发送到服务端作为请求

服务端需要处理所有的路径,通通返回同样的 index.html 内容。

如果使用 hash URL 情况就不同了

  1. http://localhost:4200/#/products/iphone-14

  2. http://localhost:4200/#/about

  3. http://localhost:4200/#/contact

  4. 只要是前端能匹配的路径都有可能在 refresh browser 时被发送到服务端作为请求

由于 # 后面的路径是不会被发送到服务端的 (这个是 browser 的行为),所以上面所有请求路径,通通会变成 http://localhost:4200/。

那服务端就只需要处理这一个路径就够了。

Angular 有 2 种 LocationStrategy:

  1. PathLocationStrategy (源码在 location_strategy.ts)

    它负责生成 path 版本 URL,比如 http://localhost:4200/products/iphone-14

  2. HashLocatonStrategy (源码在 hash_location_strategy.ts)

    它负责生成 hash 版本 URL,比如 http://localhost:4200/#/products/iphone-14

默认 LocationStrategy 是 PathLocationStrategy 

通过 withHashLocation 函数可以把它换成 HashLocationStrategy

在 Angular 内部,它们不直接使用 PlatformLocation,取而代之的是通过 LocationStrategy 间接调用 PlatformLocation。

Location

看名字就猜到了,Location 是对 LocationStrategy 的又又一层封装。 Angular Team 真的是好喜欢一层一层啊😔

Location 主要是多了一些事件管理。Location 源码在 location.ts

一个典型的观察者模式。

除了 pushState,replaceState 这些前进的 History API,Location 也可以监听到 popstate 事件。

在初始化时 Location 会通过 LocationStrategy 监听 window popstate 事件。

接着通过 Location.subscribe 就可以监听到 _subject 发布的事件(也就是 window popstate 事件)

通过 onUrlChange 可以同时监听 push, replace, popstate 事件。

题外话:Location, PlatformLocation, LocationStrategy, PathLocationStrategy, HashLocationStrategy 这些 Service Provider 并不是 export 在 @angular/router 哦,

它们 export 在 @angular/common。

import { Location, PlatformLocation, LocationStrategy, PathLocationStrategy, HashLocationStrategy } from '@angular/common';

UrlSerializer

UrlSerializer 负责把 URL string 转换成 UrlTree 和把 UrlTree 转换成 URL string。它的源码在 url_tree.ts

Router.parseUrl 内部就是调用了 UrlSerializer.parse 方法

UrlTree.toString 方法内部就是调用了 UrlSerializer.serialize 方法

StateManager

StateManager 负责维护当前的 UrlTree 和 RouterState (ActivatedRoute Tree),同时也负责更新 browser URL,源码在 state_manager.ts

Router.routerState 内部就是调用了 StateManger.getRouterState 方法。

NavigationTransitions

NavigationTransitions 负责启动导航和处理导航中的各个阶段,比如:UrlTree 与 Route 配对 -> 创建 ActivatedRoute Tree -> 创建组件插入 router-outlet 等等。

NavigationTransitions 源码在 navigation_transition.ts

handleNavigationRequest 就是启动一次导航的方法

request 的类型是 NavigationTransition,它包含了所有导航的信息

这些信息并不是在一开始导航就齐全的,一开始只有当前的信息和 rawUrl,targetRouterState 是在导航处理过程中添加进去的,

只是 Angular 为了方便代码管理,把所有信息都集中到 NavigationTransition 对象中,然后让它贯穿整个导航过程。

transitions 是一个 RxJS 流,它代表了整个导航过程。

NavigationTransitions 在初始化时,会添加各种处理导航的过程到这个流上。

比如:UrlTree 与 Route 配对 -> 创建 ActivatedRoute Tree -> 创建组件插入 router-outlet 等等。下一个 part 逛源码时,我们再仔细看一看。

ViewportScroller

ViewportScroller 的职责是控制游览器的 scrollbar。它之所以与 Routing 有关是因为导航后移动 scrollbar 是游览器的默认行为,比如 next page scroll to top,history back 恢复之前的 scrolled position。

为此 Angular Routing 也提供了类似的功能。为了不直接和运行环境有关联,Angular 一如既往的搞了一个 BrowserViewportScroller 间接调用 window.scrollTo 方法。

ViewportScroller 的源码在 viewport_scroller.ts

BrowserViewportScroller 的源码也是在 viewport_scroller.ts

RouterScroller

RouterScroller 的职责是监听 NavigationTransitions 发出的导航事件,然后调用 ViewportScroller 完成 next page scroll to top,history back 恢复之前的 scrolled position 等等操作。

RouterScroller 源码在 router_scroller.ts

Router

上面介绍的所有 Service Provider 有些是完全不公开的,有些虽然是公开但在日常开发中是很少会用到的,除非我们需要 customization。

唯独只有 Router 是日常会用到的。Router 的源码在 router.ts

上面我们已经有介绍过了几个 Router 的属性和方法:

  1. Router.parseUrl

    把 URL string 转换成 UrlTree 对象

  2. Router.createUrlTree

    通过 command 的方式创建 UrlTree 对象

  3. Router.routerState

    获取当前的 ActivatedRoute Tree

我们再看它的导航功能,以及它如何贯穿上面介绍的各种 Internal Server Provider。

Router.navigate 是一个导航方法

navigateByUrl 方法

scheduleNavigation 方法

handleNavigationRequest 方法源码在 navigation_transition.ts

监听 transitions 的是 setupNavigations 方法

它有一系列的 RxJS operators,里面会做非常多事情,我们看最重要的几个就好

最主要有 4 件事:

  1. UrlTree 与 Route 配对 (a.k.a Recognize)

    配对完成后,ActivatedRouteSnapshot 和 RouterStateSnapshot 就创建好了,注意!这里只是创建 snapshot 而且,ActivatedRoute 和 RouterState 还没有被创建。

  2. 创建 ActivatedRoute 和 RouterState

    这里会涉及 Route Reuse 概念。

    ActivatedRouteSnapshot 和 RouterStateSnapshot 没有 reuse 的概念,每一次都会全部创建新的。

    RouterState 和 ActivatedRoute 则有 reuse 的概念,不一定会创建新的。

    这或许就是它们被分开在两个不同阶段创建的原因之一吧。

  3. 创建 OutletContext Tree,创建组件并把组件插入到对应的 router-outlet。

  4. 发布每个阶段的 event

    NavigationTransitions 有一个 private 的 transitions BehaviorSubject 和一个 public 的 events Subject。


    transitions 是导航的起点,只有 NavigationTranstions.setupNavigations 方法监听了这个流,然后在一系列 RxJS operators 中,它会发布不同阶段的事件到 events Subject。

    外部可以通过监听 events Subject 获取到不同阶段的导航事件。

    比如:

RouterScroller 就是其中一个监听 NavigationTranstions.events 的 Service

createScrollEvents 方法上面我们讲解过了,它负责在导航完成后 scroll to top 或者在 history back 的时候恢复之前的 scrolled position 等等。

Router 也是其中一个监听 NavigationTranstions.events 的 Service

StateManager.handleRouterEvent 上面讲解过,它负责维护当前的 UrlTree 和 RouterState,同时也负责更新 browser URL。

更新 browser URL 需要用到 Location -> Location 内又用到 LocationStrategy -> 然后是 PlatformLocation -> 最后才是 Browser History API。

Router 之后会转发导航阶段 event。

日常开发中,我们若想监听导航阶段事件,我们不使用 NavigationTranstions.events(因为 Angular 完全没有公开 NavigationTranstions Service),取而代之的是 Router.events。

总结

除了 Router,其它 Service Provider 日常都很少会被使用,它们都是 Angular 内部在使用而已。

通过这些 Service Provider 我们也可以看出 Angular 架构的职责分离设计,虽然很繁琐,但是对长期代码维护是非常有利的,非常值得学习。

 

Routing 初始化 の 源码逛一逛

上一 part 逛 Router.navigate 源码,我们基本上对 导航 -> Recognize -> 创建 ActivatedRoute -> 创建组件和插入 router-outlet -> Scrolling -> 更新 browser URL 都有一个大致理解了。

这 part 我们逛一下 Routing 的初始化过程。

Routing 初始化的起点在 app.config.ts

provideRouter 函数源码在 provide_router.ts

APP_BOOTSTRAP_LISTENER 会在 App 组件渲染后被执行,相关源码在 application_ref.ts 里的 ApplicationRef._loadComponent 函数 (这个方法我们在 NodeInjector 文章中研究过)。

getBootstrapListener 函数源码在 provide_router.ts

Router constructor 源码在 router.ts

currentUrlTree 属性

StateManager.getCurrentUrlTree 方法源码在 state_manager.ts

class UrlTree 源码在 url_tree.ts

都是空壳。

回到 Router constructor

setupNavigations 方法源码在 navigation_transition.ts

回到 Router constructor

subscribeToNavigationEvents 方法

Router constructor 执行结束,回到 getBootstrapListener 函数。

initialNavigation 方法源码在 router.ts

setUpLocationChangeListener 方法

registerNonRouterCurrentEntryChangeListener 方法源码在 state_manager.ts

回到 setUpLocationChangeListener 方法

回到 initialNavigation 方法

至此,第一个 navigation transition 就诞生了。那一批 RxJS operators 会负责 Recognize -> 创建 ActivatedRoute -> 创建组件和插入 router-outlet -> Scrolling 等等。

 

Routing 导航关键阶段 の 源码逛一逛

上一 part 在讲解 Router.navigate 方法时,我们提到了导航的几个重要阶段,比如 Recognize -> 创建 ActivatedRoute -> 创建组件和插入 router-outlet -> Scrolling 等等。

不过只能算一个简单的 overview,这一 part 我想更详解的逛一下这个部分的源码,因为我觉得搞清楚这几个重要阶段会对接下来学习上层 API 有帮助。

Recognize / RouterStateSnapshot 阶段

Recognize 阶段主要是做 UrlTree 和 Route 配对,然后生成 RouterStateSnapshot (a.k.a ActivatedRouteSnapshot Tree)。另外,redirect 过程其实也是在这个阶段完成的哦。

我们从 Router.scheduleNavigation 方法看起

里面通过 NavigationTransitions.handleNaviagationRequest 方法启动了导航,rawUrl 是目的地 (它的类型是 UrlTree),我们留意它。

handleNavigationRequest 方法

没什么特别的只是发布了 transitions 事件。transitions 类型是 RxJS BehaviorSubject<NavigationTransition>。

这个 NavigationTransition 对象会贯穿所有导航阶段。

这些属性并不是一开始就全部填完的,它们会在一个一个阶段被慢慢填补上,然后一直传下去给下一个阶段使用。

我们盯着 rawUrl 属性,它是起点。

在 NavigationTransitions.setupNavigations 方法中 (这个方法是在 Routing 初始化时被执行的) 会监听 transitions 流,然后是一系列 RxJS operators。

所有导航阶段都发生在这些 RxJS operators 中。

rawUrl 被赋值到了 extractedUrl,UrlHandlingStrategy.extract 不是重点,默认情况下它只是简单赋值 extractedUrl = rawUrl 而已。

我们把焦点从 rawUrl 移到这个 extractedUrl 身上,关注它。

接着就是这 part 的主角 -- Recognize 阶段,它是一个 Angular 扩展的 Custom RxJS operator。

recognize 函数源码在 recognize.ts

recognizeFn 函数 recognize.ts

recognize 方法

split 函数源码在 config_matching.ts

addEmptyPathsToChildrenIfNeeded 函数

回到 recognize 方法

match 方法

processSegmentGroup 方法

processChildren 方法

processChildren 方法里面只是做了一个 foreach 和 flat 的动作,真正做配对工作的依然是回到 processSegmentGroup 方法。

processSegment 方法

processSegmentAgainstRoute 方法

我先讲解一下 redirect 的情况,expandSegmentAgainstRouteUsingRedirect 方法我们就不逛了,讲它的过程就好了。

redirect 的过程是这样的,当配对成功后,本来是应该要创建 ActivatedRouteSnapshot,但是由于它是要 redirect,

所以此时要做的是调整目的地 UrlTree 然后继续新的配对。

比如说:

export const routes: Routes = [
  { path: 'about-us', redirectTo: 'about' },
  { path: 'about', component: AboutComponent },
];

第一个 Route 配对成功后本来应该要创建一个 ActivatedRouteSnapshot,

但由于 Route 是想要 redirect to 'about',那么就要从原本的 URL '/about-us' 换成 '/about' 在重新做配对。

另外,redirect 是可以在半中央的,什么意思?

export const routes: Routes = [
  {
    path: 'products/iphone-14',
    component: ProductDetailComponent,
    children: [
      { path: 'old-reviews', redirectTo: 'reviews' },
      { path: 'reviews', component: ProductReviewComponent },
      { path: 'related-products', component: RelatedProductComponent },
    ],
  },
];

redirect 发生在 child layer,此时已经配对成功的 Route path: 'products/iphone-14' 已经生成了 ActivatedRouteSnapshot,

这个会被保留,只有后续的配对会受影响而已。

可是,如果 redirectTo 是 '/about',开头是 slash '/',那它是一个绝对路径,表示要重头开始配对。

此时 expandSegmentAgainstRouteUsingRedirect  会 throw 一个 redirect error 到外面,外面会负责重新配对,这个部分上面我们有提到过。

好,我们看回没有 redirect 的情况。

matchSegmentAgainstRoute 方法

matchWithChecks 函数源码在 config_matching.ts

回到 matchSegmentAgainstRoute 方法

好,match 方法逛完了,回到 recognize 方法

match 返回 TreeNode<ActivatedRouteSnapshot>[],这里做一个 Root Node,整个 RouterStateSnapshot 就形成了。

RouterStateSnapshot 和 ActivatedRouteSnapshot 创建过程的细节

上面我们有谈及过 RouterStateSnapshot 和 ActivatedRouteSnapshot 的关系,

ActivatedRouteSnapshot 是一个树形结构,因为它有属性 parent 和 children。

而 RouterStateSnapshot 也是一个树形结构,而且它比 ActivatedRouteSnapshot 更干净。

RouterStateSnapshot 有一个 root 属性,类型是 TreeNode。TreeNode 只有 2 个属性,一个是 value 表示当前 TreeNode 对应的 ActivatedRouteSnapshot,另一个 children 属性表示子层 TreeNode Array。

在 Recognizer.match 方法中,首先创建的是 ActivatedRouteSnapshot,然后是它对应的 TreeNode,每一个 ActivatedRouteSnapshot 都会有自己的 TreeNode (TreeNode.value === ActivatedRouteSnapshot)。

如果有子层的话,那 TreeNode 会被延后创建,先创建子层的 ActivatedRouteSnapshot 和子层的 TreeNode,然后才回到父层创建 TreeNode,并且把子层 TreeNode 放入到父层 TreeNode.children 里。

这一轮走完,ActivatedRouteSnapshot 和 TreeNode 都创建完了。

TreeNode.value 关联着 ActivatedRouteSnapshot,TreeNode.children 关联着子层 TreeNode。

至此 TreeNode 的树形结构就诞生了。

但是,此时 ActivatedRouteSnapshot 的 parent 和 children 其实还是空的。

因为 ActivatedRouteSnapshot 内部其实是透过 _routerState 属性获取到 parent 和 children 的,而此时 RouterStateSnapshot 还没有被创建,所以 _routerState 是 undefined。

match 方法之后,Root ActivatedRouteSnapshot 和 Root TreeNode 和 RouterStateSnapshot 才被创建。

在创建 RouterStateSnapshot 的过程中,所以的 ActivatedRouteSnapshot._routerState 才被赋值 RouterStateSnapshot 对象。

总结:正真维护 ActivatedRouteSnapshot 树形结构的是 RouterStateSnapshot,ActivatedRouteSnapshot parent children 只是一个 shortcut 而已。

RouterState 阶段

在创建 RouterState 之前,其实还有 Guards, Resolve, Load Components 阶段。这里我们带过就好,下一篇才教。

  1. Guards 阶段

    Guards 是守卫的意思,顾名思义就是依据条件判断 URL 是否能被访问。

    它可以用来实现 authen 和 auth 功能。

  2. Resolve 阶段

    这个是冷门功能,我们下一篇才讲。

  3. Load Components 阶段

    组件是可以 lazy load 的,这个阶段就会加载。

RouterState 阶段自然就是创建 ActivatedRoute 和 RouterState 咯。

createRouterState 函数源码在 create_router_state.ts

createRouterState 会依据 RouteStateSnapshot 树形结构创建出 RouteState。

RouteStateSnapshot 里面装的是 ActivatedRouteSnapshot,RouteState 里面装的是 ActivatedRoute。

createNode 里有一个很重要概念 -- Route Reuse,不过这篇不会深入讲,留给下一篇。

这里我们只要知道 ActivatedRoute 是可以 reuse 的,它不一定会 new ActivatedRoute 做一个新的。

TreeNode 也是可以 reuse 的,不过我们一般上不在意 TreeNode 是否 reuse,因为 TreeNode 只是维护 ActivatedRoute 的树形结构而已,关键是 ActivatedRoute 是否 reuse。

Activate Routes 阶段

 

未完待续。。。

 

  

   

  

 

目录

上一篇 Angular 17+ 高级教程 – Component 组件 の Control Flow

下一篇 Angular 17+ 高级教程 – Routing 路由 (功能篇)

想查看目录,请移步 Angular 17+ 高级教程 – 目录

 

posted @ 2024-01-16 23:28  兴杰  阅读(457)  评论(0编辑  收藏  举报