[Angular 基礎] - routing 路由(上)
之前部分 Angular 筆記:
-
[Angular 基礎] - 生命周期函數
-
[Angular 基礎] - 自定義指令,深入學習 directive
-
[Angular 基礎] - service 服務
終于到 routing 了……這部分的內容比我想象的要復雜很多,果然 Angular 的學習曲線不是開玩笑的 ˉ\_(ツ)_/ˉ
基礎頁面布局
下面是一個簡單的 wireframe,在沒有實現路由時候的布局:
其中:
-
第一個模塊對應的就是主頁,一個非常簡單的歡迎信息
-
第二個模塊對應的是服務器管理
這里的實現是
edit
所屬的模塊與單獨展示的server
平級 -
第二個模塊對應的是用戶信息展示
src/app/
├── home
├── servers
│ ├── edit-server
│ └── server
└── users└── user
結構如上所示
在沒有實現路由功能的時候,可以結合前面學習的案例,采用 ngIf
+ services 去實現。
其主要邏輯是:
-
使用
ngIf
去判斷當前應該渲染什么頁面這個就需要在
app
層添加一個變量去控制當前展示的頁面,實現一個 service 去管理對應的點擊和更新事件 -
創建多個組件層級 services
如一個 service 去管理當前展示的 server,一個 service 去管理當前的 user
就像之前在案例項目 第一個 Angular 項目 - 添加服務 中實現的那樣。不過這樣的實現也有一點問題,比如實現會麻煩一些,或者無法根據網址訪問對應的資源,如通過 domain/user_id
的方式訪問對應的用戶,有些驗證方式也無法通過,如有些登錄驗證的方式是通過在 URL 后拼接一些 state id 的方式進行雙向驗證,這種多為第三方驗證驗證方式。
Angular 本身自帶路由的實現
添加路由
創建一個新的 route module
這里的創建方式就是手動創建一個 TS 文件,文件名為 app-routing.module.ts
,實現方式如下:
const appRoutes: Routes = [{ path: '', component: HomeComponent },{path: 'users',component: UsersComponent,},{path: 'servers',component: ServersComponent,},
];@NgModule({imports: [RouterModule.forRoot(appRoutes)],exports: [RouterModule],
})
export class AppRoutingModule {}
隨后在 app.module.ts
中導入 AppRoutingModule
:
@NgModule({declarations: [// ...],imports: [BrowserModule, FormsModule, AppRoutingModule],
})
export class AppModule {}
將路由單獨拆分成一個 module 是為了代碼的可讀性,以及跟一下 SRP(Single Responsibility Principle),如果不拆分的話,直接將 appRoutes
定義在 app.module.ts
中,并且在 imports
中添加 RouterModule.forRoot(appRoutes)
也可以
這里代碼的分析也比較簡單,首先 Route
就是 Angular 定義好的類型:
上面這是 Angular 提供的最簡單的配置,需要一個路徑,一個組件,這兩個是需要的最基礎的配置。children 也 是可選項,代表著子組件(nested component),這里后面會說。
forRoot()
會創建一個新的,包含所有提供的鹿筋和指令的 ngModule,其語法為:
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterModule>
可以看到這是一個靜態函數,換言之,這也是一個 singleton
使用 route
渲染對應 router
這里需要更新的是 V 層,拋除一些樣式的上的內容,核心部分的代碼如下:
<ul class="nav nav-tabs"><lirole="presentation"routerLinkActive="active"[routerLinkActiveOptions]="{ exact: true }"><a routerLink="/">Home</a></li><li role="presentation" routerLinkActive="active"><a [routerLink]="['/servers']">Servers</a></li><li role="presentation" routerLinkActive="active"><a [routerLink]="'/users'">Users</a></li>
</ul><router-outlet></router-outlet>
這里總共有這么幾個需要注意的點:
-
routerLinkActive
routerLinkActive
也是一個指令,它會動態的添加指定的類名,當前情況下這個類名就是active
,展示效果如下:可以看到,隨著 nav link 的變動,Angular 也會自動修改對應的類名——增添或是刪除
active
-
routerLinkActiveOptions
這是比較經常搭配使用
routerLinkActive
的指令,比較常見的選項是[routerLinkActiveOptions]="{ exact: true }"
,這樣可以保證瀏覽器的路徑和路由提供的 URL 100% 一致時,才會增加對應的 active class如果不加的話,所有的路徑都會 match
/
這個路徑,因此就會出現兩個 active tabs 的情況: -
routerLink
routerLink
取代了href
,通過href
進行定位的方式會導致整個頁面重新刷新,從而丟失掉所有的狀態——這點和 React 是一樣的這是配置 path 的方法,我這里一共顯示了 3 種寫法
-
routerLink="/"
語法糖縮寫,和下一種寫法一致,具體在 [Angular 基礎] - 自定義指令,深入學習 directive 有提到過
-
[routerLink]="'/users'"
這是在 path 比較簡單的情況下使用,直接提供一個字符串即可
-
[routerLink]="['/servers']"
這是一個比較常見的用法,主要可以用來比較方便的接受靜態數據
以
/users/user
為例,-
[routerLink]="'/users/user'"
會生成一個靜態路徑,即永遠都是/users/user
如果想要生成動態路徑,那么就需要使用
+
做拼接 -
[routerLink]="['/users', user]"
會生成一個動態路徑,如user
是一個變量名,那么 Angular 就會獲取對應的變量,并拼接出對應的路徑也就是說,生成的路徑名可能是
/users/user
,也有可能是/users/user1234
-
-
-
router-outlet
這就是一個 placeholder,當 Angular 完成渲染后,它會動態加載對應的組件
也就取代之前提到的用
ngIf
渲染的 template
編程式導航
這個情況為需要在組件內觸發一些事件后進行重定向,如在登陸后重新導航到首頁這種重定向操作
這里的案例為在 Home 頁面通過點擊事件定向到其他的頁面,V 層修改如下:
<button class="btn btn-primary" (click)="onLoadServers()">To Server Page
</button>
VM 層實現:
export class HomeComponent implements OnInit {private servers: { id: number; name: string; status: string }[] = [];constructor(private router: Router, private route: ActivatedRoute) {}onLoadServers() {this.router.navigate(['servers'], {relativeTo: this.route,queryParams: { allowEdit: '1' },fragment: 'loading',});}
}
constructor 中的內容通過 dependency injection 實現,這部分具體可以查看 [Angular 基礎] - service 服務 這篇筆記,這里不多贅述。這里的 Router
和 ActivatedRoute
都是 Angular 提供用于導航的 service
其中:
-
Router
是導航及歷史記錄的相關服務
對應的 React Hook 有
useHistory
/useNavigate
-
ActivatedRoute
顧名思義,這是對當前的 active route 進行的封裝,可以通過這個 service 輕松獲取當前的 path 以及包含的相關數據
對應的 React Hook 有
useLocation
,useParams
,useMatch
,useLoaderData
這里的點擊事件觸發的就是重定向到 servers
這個路徑去,注意這里采用的是相對路徑,Angular 的路由可以接受絕對路徑,也可以接受相對路徑,甚至還可以使用 ../
這樣的相對路徑。后面的參數則是定向的路由配置:
-
relativeTo: this.route
這里指的是導航的地址所參考的路徑,如當前為
/
,那么路徑拼接的就是/servers
。如果當前路徑是/servers
,那么拼接的路徑就是/servers/servers
使用相對路徑時,一定要使用
relativeTo
,因為Router
不知道當前路徑在哪里。當沒有接收到relativeTo
時,Angular 會將所有的路徑默認為絕對路徑 -
queryParams: { allowEdit: '1' }
這就是添加 query parameter 的地方
-
fragment: 'loading'
fragment 為
#some_value
,一般用來定向到 HTML 頁面中的某一個id
上去
定向效果為:
動態接受路徑數據
上面一個 section 提到了相對路徑和動態修改路徑,這里繼續實操一下,修改的是 servers component。
V 層修改如下
<div class="row"><div class="col-xs-12 col-sm-4"><div class="list-group"><a[routerLink]="['/servers', server.id]"[queryParams]="{ allowEdit: server.id === 3 ? '1' : '0' }"fragment="loading"class="list-group-item"*ngFor="let server of servers">{{ server.name }}</a></div></div>
</div>
VM 層不需要修改就此跳過,這個時候點擊路徑會發現沒有任何的變化:
但是查看 HTML 元素又能發現,router-link
中是有值的。這是因為當前 Angular 的 routing 只針對 /servers
進行了處理,但是并沒有對 /servers/id
進行處理,因此這里需要修改一下 app-routing module:
const appRoutes: Routes = [// 其余不變{path: 'servers/:id',component: ServerComponent,},
];
其中 :id
代表的是一個動態變量
這時候就能成功實現重定向:
這個時候的數據顯示是不完整的,如果想在在 server component 中獲取對應的 server 數據,則需要使用到 ActivatedRoute
這個 service,VM 層修改如下:
export class ServerComponent implements OnInit {server: { id: number; name: string; status: string };constructor(private serversService: ServersService,private route: ActivatedRoute,private router: Router) {}ngOnInit() {this.server = this.serversService.getServer(parseInt(this.route.snapshot.params.id));}
}
其中 serversService
只是用來獲取當前 server 數據的一個 service,具體實現這里不會提及
實現后效果如下:
這里可以發現,數據已經可以正常渲染了
這里需要注意的是這個 snapshot
會獲取當前路由的狀態,其包含的數據如下:
這里獲取的 id
對應的就是 path: 'servers/:id'
中的 :id
,也是對 routerLink 中的 ['/servers', server.id]
,之前的 section 提到過,使用數組傳參數,數組中的值可以是字符串,也可以是變量,Angular 會自動拼接變量的值到路由中去。
同樣,這里也可以注意到 navigation 中的 Servers
還是處于 active 的狀態,這也是因為沒有實現 exact: true
,Angular 在匹配字符串的時候,發現當前路徑與 /servers
可以匹配,因此還是會添加 active
這一類名到對應的元素上
動態更新路由數據
現在更新一下 VM 層,更新如下:
<h5>{{ server?.name }}</h5>
<p>Server status is {{ server?.status }}</p><!-- <button class="btn btn-primary" (click)="onEdit()">Edit Server</button> --><div class=""><a [routerLink]="['/servers', 2]">Click me to server 2</a>
</div>
主要是新增加了一個超鏈接,然后完成重定向到 /servers/2
的實現,效果如下:
可以看到,路徑是從 http://localhost:4200/servers/1?allowEdit=0#loading
變成了 http://localhost:4200/servers/2
,但是數據卻沒有任何的更新。
造成這個的原因是,對于 Angular 來說,當前的頁面沒有重新渲染——url 仍然是 /servers/:id
,因此當前組件不會重新經歷一個 銷毀 --> 新建
的過程,自然 ngOnInit
并沒有重新被觸發,數據自然也不會完成對應的更新。
想要解決這個問題,就需要 subscribe ActivatedRoute
的數據變化,在每次 ActivatedRoute
的數據更新時,也需要更新組件內的數據。
這里實現如下:
export class ServerComponent implements OnInit {ngOnInit() {this.server = this.serversService.getServer(parseInt(this.route.snapshot.params.id));console.log(this.route.snapshot);this.route.params.subscribe((params: Params) => {this.server = this.serversService.getServer(parseInt(params.id));});}
}
實現后效果如下:
這個實現會在每一次 this.route.params
產生變動時,更新 this.server
。另外從實踐上來說,這里最好在 ngOnDestroy
里去 unsubscribe 去防止內存泄露,不過因為 ActivatedRoute
是 Angular 提供的 service,Angular 會在組件被銷毀的時候自動 unsubscribe。
如果是自己實現的 service,那就 一定 要做好對應 unsubscribe 的處理
這里涉及到了 Observable,后面會有專門的部分復習回顧一下 Observable……雖然之前也有筆記寫過 rxjs 的 Observable,大概了解過這個的用法