聲明
本系列文章內(nèi)容梳理自以下來源:
官方的教程,其實已經(jīng)很詳細且易懂,這里再次梳理的目的在于復(fù)習(xí)和鞏固相關(guān)知識點,剛開始接觸學(xué)習(xí) Angular 的還是建議以官網(wǎng)為主。
因為這系列文章,更多的會帶有我個人的一些理解和解讀,由于目前我也才剛開始接觸 Angular 不久,在該階段的一些理解并不一定是正確的,擔(dān)心會有所誤導(dǎo),所以還是以官網(wǎng)為主。
正文- 架構(gòu)概覽
接觸 Angular 大概一個月吧,期間寫了個項目,趁現(xiàn)在稍微有點時間,來回顧梳理一下。
其實,如果前端網(wǎng)站并不是特別復(fù)雜,那么使用 Angular 無非也就是常跟幾個重要的知識點打交道,在官網(wǎng)的核心知識的第一節(jié)中就將這些知識點羅列出來了,也就是:架構(gòu)概覽。
畫了這個圖來大概表示下 Angular 的架構(gòu)概覽,基本涉及到一些常見的重要的知識點了,比如:
- 模塊
- 路由
- 組件
- 模板
- 服務(wù)
- 指令
- 管道
不同的類型,文件名通常會都按照一定的規(guī)范來命名,以便直接看出該文件的角色。
當(dāng)然,文件命名只是給開發(fā)人員來方便維護、辨別,對于 Angular 來說,這些都是一份份的 ts 文件代碼,所以,都需要在相對應(yīng)的文件中加上一些裝飾器比如:@Directive,@Pipe,@Component,@NgModel 等這些,才能夠讓 Angular 識別出該文件的角色、用途。
基本上,用 Angular 做一個簡單的前端項目,就是跟上面這些打交道,理清它們各自的用途及用法,還有之間的聯(lián)系,基本上,就可以上手進行一些開發(fā)了。
當(dāng)然,像在 Service 服務(wù)中,還會有異步編程、HttpClient 網(wǎng)絡(luò)編程的相關(guān)知識點;
在 Component 組件中,也還會有表單、動畫相關(guān)的編程知識點,這些都是需要進一步去深入學(xué)習(xí)研究,但從總體架構(gòu)上來看,就是要先了解以上這些知識點了。
模塊
一個 Angular 項目,至少會有一個模塊,即最少都會有一份用 @NgModel 聲明的 ts 文件,表明該文件作為模塊角色,來管理其他角色。
其他角色包括:組件、指令、管道、服務(wù)等等,這些角色必須在模塊文件中聲明了,才能夠被該模塊內(nèi)的其他角色所使用,而且同一個組件、指令、管道不允許同時在多個模塊中進行聲明,只能通過模塊 exports 給其他模塊使用。
Angular 里的模塊,并不等同于 Android 項目中的模塊概念。
在 Android 項目代碼中,可能我們會根據(jù)功能來進行模塊的劃分,但這個模塊僅僅是抽象上的概念,也就是建個包,把代碼都集中管理。
而 Angular 里的模塊,不僅可以在項目結(jié)構(gòu)上集中管理同一個模塊的代碼文件,還可以為模塊內(nèi)的代碼提供一個運行的上下文。
意思就是說,不同模塊在運行期間互不影響,就好像各自運行在各自的沙箱容器中一樣。舉個簡單的例子,在不同模塊中聲明相同的變量名,或相同的 css 的類選擇器,它們之間并不會起沖突。
當(dāng)然,模塊之間可以有交互,模塊可以依賴于另一模塊,模塊內(nèi)的可以共享資源等等,所以,NgModel 中有許多需要配置的聲明項,比如:
- declarations:聲明屬于本模塊內(nèi)的組件、指令、管道
- providers:聲明屬于本模塊內(nèi)的服務(wù)
- imports:聲明本模塊所引用的其他模塊,通常是 imports 其他模塊在 exports 中聲明的項
- exports:聲明本模塊對外公開的組件、指令、管道等,在這里公開的項才可以被其他模塊所使用
- bootstrap:只有根模塊才需要配置,用來設(shè)置應(yīng)用主視圖,Angular 應(yīng)用啟動后,這里就是入口,類似于 Android 中的入口 Activity
- 還有其他一些可選配置,比如應(yīng)用主題,或者動態(tài)的組件聲明等等
在 Angular 中,大多數(shù)的模式就是,一個根模塊管理著很多功能模塊,然后,每個模塊管理自己模塊內(nèi)部所使用到的組件、指令、管道、服務(wù)、或者需要依賴于其他模塊,如果該模塊內(nèi)部的這些角色,有些可以供其他模塊使用,那么就需要對外暴露。
路由
一個項目這么多模塊,Angular 并不會一開始就把所有模塊都加載,而是惰性加載,按需加載。
那么,什么時候會去加載呢?
就是等某個模塊內(nèi)部的組件被使用的時候會加載,而組件是什么時候會被使用的呢?
有兩個時機,一是組件被直接調(diào)用;二是觸發(fā)了路由去加載;
路由通常的配置方式是用一個 @NgModel 聲明的模塊,但只用其中兩項配置:imports 和 exports,imports 用來導(dǎo)入當(dāng)前模塊所有組件與 url 的映射表,而 exports 用來將這些映射表信息暴露,以供相對應(yīng)的模塊去引入使用。
當(dāng)然,你不想抽離路由配置,直接將其配置在對應(yīng)模塊的 imports 內(nèi)也可以,抽離的話,相對獨立,可維護。
區(qū)別于傳統(tǒng)的前端網(wǎng)頁的跳轉(zhuǎn)方式,Angular 項目是一個單頁應(yīng)用,所謂的單頁應(yīng)用就是說只有一個頁面,所有頁面的跳轉(zhuǎn),其實是將當(dāng)前頁面的顯示內(nèi)容進行替換,頁面仍舊只有一個,并不會打開新的頁面。
而頁面的跳轉(zhuǎn),通常有以下幾種場景:
- 用戶輸入 url 進行跳轉(zhuǎn)
- 用戶點擊交互按鈕進行跳轉(zhuǎn)
- 用戶操作前進或后退進行跳轉(zhuǎn)
這些場景,路由的工作機制都能夠很好的支持。
如果網(wǎng)頁很簡單,只有一個首頁,并不存在頁面跳轉(zhuǎn)場景,那么可以不用配置路由,只需要在 index.html 中配置根視圖,以及在根模塊的 bootstrap 中配置根視圖組件即可。
但如果項目劃分成了多個功能模塊,那么應(yīng)該交由每個模塊管理自己的路由表,而后選擇一個上層模塊,來統(tǒng)一關(guān)聯(lián)各個模塊路由,有兩種方式:一是在上層模塊的 imports 內(nèi)按照一定順序來導(dǎo)入各個功能模塊;但這種方式想要按照路由層級來查看路由表就比較麻煩,需要到各個模塊內(nèi)部去查看或者借助一些工具。
另一種方式是,在上層模塊的路由表中使用 loadChildren 加載各個功能模塊,然后各個功能模塊默認路由都顯示成空視圖,各自內(nèi)部再通過配置 children 的路由表方式來管理各個模塊內(nèi)部自己的路由表。
組件與模板
在 Angular 中,最常接觸的應(yīng)該就是組件了。
我是這么理解的,組件可以是你在界面上看到的任何東西,可以是一個頁面,可以是頁面上的一個按鈕。
而對于瀏覽器解析并呈現(xiàn)前端頁面時,Html、CSS、JavaScript 這三分文件通常都是需要的,而 Angular 是使用了 TypeScript,所以一個組件,其實就包括了:Html,CSS,TypeScript。
在 Angular 中,可以說,是以組件為單位來組成頁面的,組件是核心,因為 Angular 提供的功能基本都是用來為組件服務(wù)的。
以上,是我的理解。
但要注意,官網(wǎng)教程中,很多地方的組件描述,更多時候是傾向于表示 TypeScript 的那份文件,因為對于組件來說,TypeScript 可以說是它的核心,CSS 只是樣式文件,Html 更類似于模板存在。
所以這里將組件和模板放在一起講,因為就像開頭那張圖一樣,組件是一份 TypeScript 文件,在該文件中,定義了這個組件的模板(template)來源和 CSS 樣式來源。
模板提供了該組件的呈現(xiàn)結(jié)構(gòu),而 TypeScript 里定義了組件的數(shù)據(jù)來源及交互行為,它們兩一起組織成一個視圖呈現(xiàn)給用戶。
既然,這份 TypeScript 的組件文件和模板文件需要共同合作,那么它們之間就少不了交互,所以就涉及到很多所謂的模板語法,也就是所謂的組件和模板之間的交互方式。
比如,當(dāng)要往模板中嵌入 TypeScript 中的變量數(shù)據(jù)時,可以使用 {{value}}
這種語法形式,同樣的,還有模板中標(biāo)簽的屬性綁定,事件回調(diào)注冊的交互方式的語法。
總之,Angular 支持雙向數(shù)據(jù)綁定,是一種以數(shù)據(jù)驅(qū)動的思想來讓頁面進行交互刷新的方式,區(qū)別于傳統(tǒng)的前端模式。在以往,如果需要動態(tài)的更新 DOM 上的信息時,需要先獲取到相對應(yīng)的元素實例對象,然后調(diào)用相應(yīng)的 DOM API 來操縱 DOM;
而使用 Angular 的話,可以直接在模板的相應(yīng)元素中,將某個屬性與 TypeScript 文件中某個變量直接進行綁定,后續(xù)這個變量值變化時,Angular 會自動去更新相應(yīng) DOM 的屬性,也就是說,原本那些操縱 DOM 的代碼,Angular 幫我們做了,我們不用再自己去處理了。
另外,注意,以上出現(xiàn)的 TypeScript 的描述,你可以理解成官網(wǎng)中的組件,我之所以不想用組件的方式來進行描述,是因為,我覺得,組件是一個整體,它本身就包括了 TypeScript 文件和模板文件,所以官網(wǎng)中說的組件和模板的交互,我覺得,換成組件中的 TypeScript 文件與模板文件的交互更為適合。
當(dāng)然,這只是我目前階段的理解。
服務(wù)
服務(wù)是一個廣義上的概念,通常用來處理那些跟 UI 交互無關(guān)的事情,比如網(wǎng)絡(luò)請求的工作等。
所以它也是為組件服務(wù),而且 Angular 有一套依賴注入機制,也就是說,組件只需要告訴 Angular,它需要哪些服務(wù),至于這些服務(wù)的實例是什么時候創(chuàng)建,交給誰去管理等這些組件內(nèi)部都不用自己去處理了。
Angular 會自動創(chuàng)建相關(guān)的服務(wù)實例,然后在組件適當(dāng)?shù)臅r候,將這個實例注入給組件去使用。
這種模式跟以前在 Android 端開發(fā)時有所區(qū)別,在 Android 端中,當(dāng)需要業(yè)務(wù)層某個實例對象時,通常都需要自己內(nèi)部去初始化,或者這個實例是個單例的話,也需要自己去實現(xiàn)單例。
但在 Angular 中,你可以借助它依賴注入的機制,來讓 Angular 幫你去做這些依賴的對象的實例管理的事,如果需要一個全局的單例服務(wù),那么可以將該服務(wù)聲明成 root 即全局可用;如果需要一個模塊內(nèi)的單例,那么可以在該模塊的 providers 中聲明該服務(wù);如果需要一個組件自己的實例對象,那么可以在組件的元數(shù)據(jù)塊的 providers 中配置該服務(wù)。
總之,就是,跟 UI 交互無關(guān)的工作,可以抽到服務(wù)中去處理,而該服務(wù)實例的管理,交給 Angular 就可以了,組件只需要告訴 Angular 它需要哪種形式的服務(wù)即可。
那么,組件是怎么告訴 Angular 的呢?
同樣在 Android 項目或者后端項目中,也有一些依賴注入框架,那些通常都是借助注解的方式來實現(xiàn)。
但在 Angular 中,不用這么麻煩,直接在組件的構(gòu)造函數(shù)的參數(shù)中,聲明某個服務(wù)類型的參數(shù)即可。
指令
指令也是為組件服務(wù)的,但是,是在組件的模板文件中來使用。
因為組件的模板,其實就是一份 HTML 文件,基于 HTML 的標(biāo)簽之上,加上一些 Angular 的模板語法,而 Angular 在將這份 HTML 文件代碼交給瀏覽器解析之前,會先自行解析一遍,去將模板中不屬于 HTML 的那些語法解析出相應(yīng)的行為。
而指令分為結(jié)構(gòu)型指令和屬性型指令,它們的區(qū)別,其實就在于,一個是改變 DOM 的結(jié)構(gòu),一個是改變 DOM 元素的樣式。
所以說,指令的目的,其實就是簡化一些操縱 DOM 的工作,比如你需要讓某些按鈕都具有統(tǒng)一的行為和樣式,當(dāng)被點擊時先做什么,再做什么。
實現(xiàn)這個,你當(dāng)然可以在 TypeScript 中去書寫這些邏輯,但要應(yīng)用到每個按鈕上,就比較繁瑣。
這個時候,就可以將這些工作都封裝到指令內(nèi)部,然后在每個按鈕標(biāo)簽上加上該指令,Angular 在解析模板時,發(fā)現(xiàn)了這個指令,就會為每個按鈕都加上這么一段程序邏輯。
我個人覺得,指令的功能,讓我們處理一些相同的行為,可以更好的去封裝,減少冗余和繁瑣。
當(dāng)然,上面舉的場景,也可以自己封裝個按鈕組件,然后在其他模板中,不使用原生按鈕,而使用封裝后的按鈕組件,也可以達到目的。
所以,組件其實也是指令的一種,但組件的實現(xiàn)方式會比較重,有時候,只需要封裝一些簡單的行為邏輯,就可以直接借助指令的方式封裝。
指令的原理也很簡單,在模板中某個元素標(biāo)簽上,添加上某個指令后,解析到這個指令時,會進入這個指令的相關(guān)工作,而指令內(nèi)部,會獲取到一個當(dāng)前指令掛載的元素標(biāo)簽對象,既然都拿到這個對象了,那么,在指令內(nèi)部想對這個元素做什么,都可以了。
指令還有另一個通途,通常用來擴展原有的功能,因為可能項目中,在模板里使用的組件或者 HTML 元素的標(biāo)簽因為種種原生無權(quán)或不方便進行修改,而又想在其基礎(chǔ)上擴展一些功能,此時就可以利用指令來實現(xiàn)。
管道
管道同樣是為組件服務(wù),也同樣是在組件的模板文件中來使用。
它的用途,在于,將數(shù)據(jù)按照一定的規(guī)則進行轉(zhuǎn)換,比如 Object 對象,轉(zhuǎn)換成 json 格式數(shù)據(jù),再比如,long 型的時間,轉(zhuǎn)換成具體的時間日期等等。
Angular 中已經(jīng)內(nèi)置了一些管道,也可以自定義管道。
示例
大概了解了 Angular 的架構(gòu)概覽,接下去就來看看一個簡單的 Angular 項目結(jié)構(gòu),以及各個文件、模塊的用途,稍微講一下。
這是用 WebStrom 創(chuàng)建一個 Angular 項目后,自動生成的簡單架構(gòu)。
在利用 Angular Cli 工具生成腳手架時,默認就已經(jīng)生成了很多配置項,而且此時,項目已經(jīng)是可以運行的,因為也自動生成了一個根模塊和根視圖,默認頁面是 Angular 的歡迎界面。
挑幾個來講講。
angular.json
這是 Angular-CLI 的配置文件,而 Angular-CLI 是自動化的工程構(gòu)建工具,也就是利用這個工具,可以幫助我們完成很多工作,比如創(chuàng)建項目、創(chuàng)建文件、構(gòu)建、打包等等。
原本的 HTML、CSS、JavaScript 的前端開發(fā)模式,并沒有工程的概念,只要用瀏覽器打開 HTML 文件就能夠運行。而 Angular 引入了 TypeScript,Scss 等瀏覽器并不無法識別的語言,自然,要讓瀏覽器運行 Angular 項目之前,需要進行一次編譯,一次轉(zhuǎn)換。
這些工作就可以借助 Angular-CLI 來進行。另外,創(chuàng)建一個模塊,創(chuàng)建一個組件,也都可以通過 Angular-CLI 來。
那么,在創(chuàng)建這些文件或者說,打包編譯這些項目文件時,該按照怎樣的規(guī)則,就是參照 angular.json 這份配置文件。
大概看一下內(nèi)容:
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", // 默認的配置項,比如默認配置了 ng g component 生成組件時應(yīng)該生成哪些文件等等
"version": 1,
"newProjectRoot": "projects",
"projects": {
"daView": { // 項目的配置
"root": "",
"sourceRoot": "src", // 源代碼路基
"projectType": "application", // 項目的類型,是應(yīng)用還是三方庫(library)
"prefix": "app", // 利用命令生成 component 和 directive 的前綴
"schematics": {}, // 替換掉第一行的 schema.json 中的一些默認配置項,不如創(chuàng)建組件時,不要生成spec文件
"architect": { // 執(zhí)行一些構(gòu)造工作時的配置
"build": { // 執(zhí)行 ng build 時的一些配置項
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/daView", // 編譯后的文件輸出的位置
"index": "src/index.html", // 構(gòu)建所需的模板 Index.html
"main": "src/main.ts", // 構(gòu)建所需的文件
"polyfills": "src/polyfills.ts", // 構(gòu)建所需的文件
"tsConfig": "src/tsconfig.app.json", // 對 typescript 編譯的配置文件
"assets": [ // 構(gòu)建所需的資源
"src/favicon.ico",
"src/assets"
],
"styles": [ // 構(gòu)建所需的樣式文件,可以是 scss
"src/styles.css"
],
"scripts": [] // 構(gòu)建所需的三方庫,比如 jQuery
},
"configurations": {/*...*/}
},
"serve": {/*...*/}, // 執(zhí)行 ng serve 時的一些配置項
"extract-i18n": {/*...*/},
"test": {/*...*/},
"lint": {/*...*/}
}
}
},
"daView-e2e": {/*...*/},
"defaultProject": "daView"
}
所以,利用 Angular-CLI 生成的初始項目中,有許多基本的文件,這些文件,基本也都在 angular.json 中被配置使用了,每個配置文件基本都有各自的用途。
比如,tslint 用來配置 lint 檢查,tsconfig 用來配置 TypeScript 的編譯配置,其他那些 html,css,ts,js 文件基本都是 Angular 項目運行所需的基礎(chǔ)文件。
package.json
對于一個工程項目來說,依賴的三方庫管理工具也很重要,在 Android 項目中,通常是借助 Gradle 或 maven 來管理三方庫。
而在 Angular 項目中,是使用 npm 來進行三方庫的管理,對應(yīng)的配置文件就是 package.json。
在這份配置文件中,配置了項目所需要的三方庫,npm 會自動去將這些三方庫下載到 node_modules
目錄中。然后,再去將一些需要一起打包的三方庫在 angular.json 中進行配置。
app/src 源碼
以上就是利用 Angular-CLI 創(chuàng)建項目生成的初始架構(gòu)中各個文件的大概用途,下面講講 Angular 項目的大概運行流程。
在 src 中的 index.html
文件就是單頁應(yīng)用的頁面文件,里面的 body 標(biāo)簽內(nèi),自動加入了一行根視圖的組件:
<app-root></app-root>
就是根組件 AppComponent (自動生成的)的組件標(biāo)簽,當(dāng) Angular 在 HTML 文件中發(fā)現(xiàn)有組件標(biāo)簽時,就會去加載該組件所屬的模塊,并去解析組件的模板文件,將其嵌入到 HTML 文件的組件標(biāo)簽中。
看一下自動生成的根模塊的部分內(nèi)容:
//app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
//app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'daView';
}
app.module.ts
文件用 @NgModule 表示該文件角色是模塊,并在內(nèi)部配置了它的組件 AppComponent,這樣 AppComponent 組件就只屬于該模塊了,并能夠在該模塊內(nèi)的其他組件中被使用。
另外,由于該模塊是根模塊,所以還需要配置 bootstrap,設(shè)置應(yīng)用的根視圖,這個配置需要和 index.html
里的 body 標(biāo)簽內(nèi)的根視圖組件是同一個組件,否則運行時就會報錯了。
當(dāng)項目中模塊多了的時候,各模塊之間基本是通過路由或者組件來進行相互關(guān)聯(lián)。
比如,我們新創(chuàng)建個 Home 模塊,然后在根模塊中創(chuàng)建個 app-routing 路由配置文件:
//app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'home', loadChildren: './home/home.module#HomeModule'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
然后在 app.module.ts 的 imports 中將該路由配置導(dǎo)入,這樣當(dāng)路由到 home 時,會去加載 home 模塊,然后看看 home 模塊的路由配置:
//home-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomeComponent} from './home.component';
import {HomeCenterComponent} from './component/home-center.component';
const routes: Routes = [
{
path: '',
children: [
{
path: '', component: HomeCenterComponent
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }
home 模塊的默認視圖為空,但交由其子視圖來控制,所以,當(dāng)導(dǎo)航到 home 時,home 模塊會去加載它內(nèi)部的 HomeCenterComponent 組件。
以上,是當(dāng)項目中有多模塊時,我的處理方式。
當(dāng)按照這種方式來實現(xiàn)時,對于了解一個 Angular,就有一定的規(guī)律可循了:
- 先找根視圖組件,然后確認根視圖組件中的 router-outlet 標(biāo)簽的區(qū)域,因為這個區(qū)域展示的就是由根模塊路由導(dǎo)航到的新的組件內(nèi)容;
- 去根模塊的配置中找到根模塊的路由配置表,來查看第一個層級的路由分別對應(yīng)哪些模塊;
- 去這些相應(yīng)的模塊中,查看它們各自內(nèi)部的路由配置表,來確定各自模塊的默認視圖組件是哪個,下一個層級的各個路由所對應(yīng)的視圖組件;
- 這樣,一個頁面的組件層次結(jié)構(gòu)就能夠很快的理清。
大家好,我是 dasu,歡迎關(guān)注我的公眾號(dasuAndroidTv),公眾號中有我的聯(lián)系方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內(nèi)容有幫助到你,可以轉(zhuǎn)載但記得要關(guān)注,要標(biāo)明原文哦,謝謝支持~