Posted on Leave a comment

[技術支援-8] 產品發佈設定

將產品發佈到正式環境

  • 最簡單的方法是
    ng build
  • 將所有dist/資料夾底下的文件複製一份到伺服器上
  • 如果想順帶設置base href<base href="/my/app/">,則可加下面參數
    ng build –base-href=/my/app/
  • 若有使用Router功能,將所有找不到的頁面都導向index.html(下面會有詳細介紹)

優化我們的Angular專案

加上--prod參數,會能輸出優化過的檔案
ng build –prod
這個參數會為我們的專案做這些事情

  • 改使用AOT編譯:預先編譯而不是用JIT的方式
  • 開啟production mode: 可以讓網站執行更快,等同於下這個參數--environment=prod
  • Bundling: 將所有的所使用的library和許多應用綁在一起
  • 縮小檔案:刪除多餘的空白,註釋和可選的令牌。
  • Uglification:將變數名稱做混淆的動作。
  • 消除死代碼:刪除未引用的模塊和很多未使用的代碼

添加--build-optimizer可進一步減少檔案的大小
ng build –prod –build-optimizer

如果要使用惰性加載去載入部份的JS檔案,記得不要在需要被首先加載的模組裡面import要被惰性加載的元件,否則會在一開始就被載入。
AOT預設在編譯時會對ngModules做識別並且做惰性加載的設定。

善用bundles工具

source-map-explorer是一個很棒的bundles工具。可以將許多的js綑綁在一起。
首先先安裝
npm install source-map-explorer –save-dev
然後建構應用程式
ng build –prod –sourcemaps
在dist生成綑綁包
ls dist/*.bundle.js
運行管理器來生成這個被綑綁的檔案的分析圖
node_modules/.bin/source-map-explorer dist/main.*.bundle.js
產生的圖如下:

善用效能觀察工具

首推的當然就是google chrome囉!
這邊有相關的教學系列文:認識 Chrome 開發者工具
官網也有推薦使用WebPageTest 來衡量自己網頁的速度

伺服器啟用Routee功能的配置方法

Apache設定方式

添加一個重寫規則.htaccess
RewriteEngine On
# If an existing asset or directory is requested go to it as it is
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ – [L]
# If the requested resource doesn’t exist, use index.html
RewriteRule ^ /index.html

NGinx

使用前端控制器模式Web應用程序中try_files描述的方法 修改為:index.html

try_files $uri $uri/ /index.html;

IIS

添加一個重寫規則web.config

<system.webServer>
  <rewrite>
    <rules>
      <rule name="Angular Routes" stopProcessing="true">
        <match url=".*" />
        <conditions logicalGrouping="MatchAll">
          <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
          <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
        </conditions>
        <action type="Rewrite" url="/src/" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>

GitHub Pages

我們無法直接配置GitHub Pages服務器,但是可以添加一個404頁面。複製index.html到404.html。
它仍將作為404響應,但瀏覽器將處理該頁面並正確加載應用程序。從服務docs/上創建一個.nojekyll文件

Firebase hosting

添加一個重寫規則

"rewrites": [ {
  "source": "**",
  "destination": "/index.html"
} ]
Posted on Leave a comment

[技術支援-7] TypeScript設定

TypeScript配置

TypeScript是Angular應用開發中使用的主語言。瀏覽器不能直接執行TypeScript,得先用tsc編譯器轉譯(transpile)成JavaScript,而且編譯器需要進行一些配置。而配置的檔案名稱就是tsconfig.json

這邊是官方關於此配置文件的詳細說明:tsconfig.json

tsconfig.json

下面為一個tsconfig.json的範例
{
“compileOnSave”: false,
“compilerOptions”: {
“outDir”: “./dist/out-tsc”,
“sourceMap”: true,
“declaration”: false,
“moduleResolution”: “node”,
“emitDecoratorMetadata”: true,
“experimentalDecorators”: true,
“target”: “es5”,
“typeRoots”: [
“node_modules/@types”
],
“lib”: [
“es2017”,
“dom”
],
“noImplicitAny”: true,
“suppressImplicitAnyIndexErrors”: true
}
}

關於設定檔裡的noImplicitAny意思是是否不允許typescript編譯時隱性將沒有設定類型的變數設定為any。如果設定為true的話,如果typescript裡面有沒有設定類型的變數則會產生錯誤訊息。當這個值設定為true時,記得要將suppressImplicitAnyIndexErrors也設定為true,不然會發生隱式報錯。

lib.d.ts 文件

TypeScript有一個特殊的聲明文件lib.d.ts。包含了JavaScript運行庫和DOM的各種常用Javascript環境聲明。
基於–target,TypeScript添加額外的環境聲明,例如如果目標為es6時將添加Promise。
“lib”: [“es2017”, “dom”]

Posted on Leave a comment

[技術支援-6] NPM設定

安裝NPM環境

Angular應用程序以及Angular本身都依賴於很多第三方包(包括Angular自己)提供的特性和功能。這些包由Node包管理器( npm )負責安裝和維護。
因此Node.js和npm是做Angular開發的基礎。
這邊是Node.js下載連結:Downloads NPM

如果在電腦中需要同時運行多個不同版本的npm,可以使用nvm來管理他們:nvm

package.json設定

這邊有詳細的說明文檔:Specifics of npm’s package.json handling
下面是一個預設CLI設定檔的範例
{
“name”: “專案名稱應為惟一”,
“version”: “0.0.0”,
“license”: “MIT”,
“description”: “對於這個專案的說名”,
“keywords”: “可幫助在npm網站上能被搜尋到”,
“homepage”: “http://claire-chang.com”,
“bugs”: {
“url” : “https://github.com/owner/project/issues(ISSUE要被回報的網址和MAIL)”, “email” : “project@hostname.com”
},
“author”: “Barney Rubble (http://barnyrubble.tumblr.com/)”,
“scripts”: {
“ng”: “ng”,
“start”: “ng serve”,
“build”: “ng build”,
“test”: “ng test”,
“lint”: “ng lint”,
“e2e”: “ng e2e”
},
“private”: true,
“dependencies”: {
“@angular/animations”: “^5.0.0”,
“@angular/common”: “^5.0.0”,
“@angular/compiler”: “^5.0.0”,
“@angular/core”: “^5.0.0”,
“@angular/forms”: “^5.0.0”,
“@angular/http”: “^5.0.0”,
“@angular/platform-browser”: “^5.0.0”,
“@angular/platform-browser-dynamic”: “^5.0.0”,
“@angular/router”: “^5.0.0”,
“@angular/upgrade”: “^5.0.0”,
“core-js”: “^2.4.1”,
“mobx”: “^3.3.1”,
“mobx-angular”: “^1.9.0”,
“rxjs”: “^5.5.2”,
“zone.js”: “^0.8.14”
},
“devDependencies”: {
“@angular/cli”: “1.5.0”,
“@angular/compiler-cli”: “^5.0.0”,
“@angular/language-service”: “^5.0.0”,
“@types/jasmine”: “~2.5.53”,
“@types/jasminewd2”: “~2.0.2”,
“@types/node”: “~6.0.60”,
“codelyzer”: “~3.2.0”,
“jasmine-core”: “~2.6.2”,
“jasmine-spec-reporter”: “~4.1.0”,
“karma”: “~1.7.0”,
“karma-chrome-launcher”: “~2.1.1”,
“karma-cli”: “~1.0.1”,
“karma-coverage-istanbul-reporter”: “^1.2.1”,
“karma-jasmine”: “~1.1.0”,
“karma-jasmine-html-reporter”: “^0.2.2”,
“protractor”: “~5.1.2”,
“ts-node”: “~3.2.0”,
“tslint”: “~5.7.0”,
“typescript”: “~2.4.2”
}
}

這邊說明幾個比較重要的設定值:

  • script: 是一個script commands的列表,這些script commands會在生命週期中的不同時間運行。其內容的key是生命週期事件,value是要執行的命令。可獲得的生命周期列表:請見此
  • dependencies: 要運行這個應用程式必要一定要安裝的package
  • devDependencies: 只有在開發時需要用到的功能則寫在這邊,當別人要導入我們所開發的專案時,不用一定要下載完整的devDependencies內容。

在dependencies需載入的Angular Packages

  • @angular/animations:網頁切換動畫效果
  • @angular/common: services, pipes, and directives等都在這邊
  • @angular/compiler:Angular的模板編譯器,在JIT
  • @angular/core: 基本angular框架所需的功能如metadata decorators, Component, Directive, dependency injection,以及元件的lifecycle hooks都在裡面
  • @angular/forms:template-driven和reactive forms的表單驗證功能都在這
  • @angular/http:Angular的舊版即將被棄用的HTTP客戶端
  • @angular/platform-browser:一切DOM和瀏覽器相關的在這,AOT編譯方法也在這
  • @angular/platform-browser-dynamic:JIT編譯器在這
  • @angular/router:路由器功能
  • @angular/upgrade:從angularJS升級所需使用的
  • core-js: Polyfill packages
  • zone.js: 一個為 Zone規範提供的填充庫
  • rxjs: 新版的Angular主要使用的HTTP方法模組
  • bootstrap: bootstrap是一個可以簡單做出UI的視覺框架
  • angular-in-memory-web-api: 一個Angular的支持庫,它能模擬一個遠端服務器的Web API,而不需要依賴一個真實的服務器或發起真實的HTTP調用。對演示、文檔範例和開發的早期階段(那時候我們可能還沒有服務器呢)非常有用。

devDependencies需載入的Packages

列在文件中devDependencies區的包會幫助我們開發該應用程序。我們不用把它們部署到產品環境的應用程序中——雖然這樣做也沒什麼壞處。
另外在devDependencies也需要列出所有在Angular裡列為peerDependencies的package

Posted on Leave a comment

[技術支援-5] 佈署方式JIT及AOT介紹

Ahead-of-Time (AOT)編譯

在Angular 2 中有兩種編譯模式:

  • JIT(Just-in-Time):Angular預設是使用 Just-in-Time (JIT) 即時編譯,等瀏覽器下載完 *.js 檔案後,會在用戶端的瀏覽器編譯 Angular 的 JS 程式碼,接著才會渲染畫面。
  • AOT(Ahead-of-Time):在程式發佈之前就透過 Angular Compiler 進行編譯,所以瀏覽器下載完的 *.js 檔案,就可以直接被執行,然後渲染畫面。

下表為這兩種佈署方式的簡單比較表:

Characteristic JiT AoT
Compilation target Browser Server
Compilation context Runtime Build
Bundle size Huge (~1.2 MB) Smaller (~400 KB)
Execution Performance Better
Startup time Shorter

AoT的要點是將編譯從運行時移動到構建過程。因此我們不需要讓用戶載入完整的編譯器功能,也不需要在用戶端才去做編譯的動作,所以從對照表可以看出,這個動作可以讓效能以及檔案大小都變得更好。

另外,預先編譯可以在編譯期就發現一些模版的錯誤,而不需要等到實際在客戶端執行才發現。使用AOT,編譯器僅僅使用一組庫在構建期間運行一次;使用JIT,編譯器在每個用戶的每次運行期間都要用不同的庫運行一次。

AOT編譯的優點

  • 更快:瀏覽器直接載入可運行的程式碼,可以立即使用,而不用等待編譯完成。
  • 減少HTTP異部請求:編譯器把外部HTML模板和CSS樣式表內聯到了該應用的JavaScript中。消除了用來下載那些源文件的Ajax請求。
  • 檔案更小:客戶端不用載入完整的Angular編譯器
  • 提早檢測模板錯誤:編譯時會跳出模板綁定錯誤警告,提早發現錯誤
  • 更安全:AOT編譯遠在HTML模版和組件被服務到客戶端之前,將它們編譯到JavaScript文件。沒有模版可以閱讀,沒有高風險客戶端HTML或JavaScript可利用,所以注入攻擊的機會較少。

使用AOT編譯

Angular5大大的簡化了AOT流程,我們只需在生成文件時加上--aot
ng build –aot
ng serve –aot
如果我們使用--prod預設也會是aot輸出。

用AOT編譯的程式編寫限制

如果使用AOT預編譯,在撰寫angular的metadata要注意下面幾點:

不支援function expression

var myFunction = function [name]([param1[, param2[, ..., paramN]]]) {
   statements
};

不支援Arrow functions

(param1, param2, …, paramN) => { statements }

像下面這種設定方法在AOT是不被支援的

@Component({
  ...
  providers: [{provide: server, useFactory: () => new Server()}]
})

需改成這樣:

export function serverFactory() {
  return new Server();
}

@Component({
  ...
  providers: [{provide: server, useFactory: serverFactory}]
})

在元數據內單獨使用常數

因為常數是在編譯時就編譯進JS裡,如下面的寫法會造成AOT在編譯時遺失template常數的值:

const template = '<div>{{hero.name}}</div>';

@Component({
  selector: 'app-hero',
  template: template
})
export class HeroComponent {
  @Input() hero: Hero;
}

我們可以改用下面這種寫法

@Component({
  selector: 'app-hero',
  template: '<div>{{hero.name}}</div>'
})
export class HeroComponent {
  @Input() hero: Hero;
}

或將常數放至一個運算表達式內:

const template = '<div>{{hero.name}}</div>';

@Component({
  selector: 'app-hero',
  template: template + '<div>{{hero.title}}</div>'
})
export class HeroComponent {
  @Input() hero: Hero;
}

裝飾和數據綁定的類成員必須公開

在metadata中只支援下面的angular decorators

DECORATOR MODULE
Attribute @angular/core
Component @angular/core
ContentChild @angular/core
ContentChildren @angular/core
Directive @angular/core
Host @angular/core
HostBinding @angular/core
HostListener @angular/core
Inject @angular/core
Injectable @angular/core
Input @angular/core
NgModule @angular/core
Optional @angular/core
Output @angular/core
Pipe @angular/core
Self @angular/core
SkipSelf @angular/core
ViewChild @angular/core

如果表達式使用不受支持的語法,則collector將錯誤的項目寫入.metadata.json文件。如果編譯器需要這段元數據來生成應用程序代碼,則編譯器稍後將報告該錯誤。
若希望能立即顯示錯誤,則可以將tsconfigstrictMetadataEmit設為true
“angularCompilerOptions”: {

“strictMetadataEmit” : true
}

參考資料

Posted on Leave a comment

[技術支援-4] Angular Universal

什麼是Angular Universal

下面是一段截錄於https://itw01.com/G3MBE3P.html對Angular Universal目的的說明

雖然Angular是一種構建Web應用的強大方式,但是長期以來,開發人員都知道它在SEO和可訪問性方面的限制。當然,Google的爬蟲能夠執行JavaScript,但是它並不是唯一的爬蟲方案。例如,在將一個連結提交給Slack之後,它的爬蟲會抓取一個預覽,但是並不會執行JavaScript,所以原始的Angular HTML模板將會顯示在預覽之中。爲了消除這種現象所產生的問題,Jeff Whelpley和Patrick Stapleton開發了Angular Universal,它允許在服務端進行渲染。

從上面的文字可以了解,Angular Universal可以在server side預先對網頁模版做處理,變成一個有資料的靜態頁面。可以在使用者要求資料的當下產生靜態頁面,也可以事先產生好靜態檔案以供未來使用。

Angular Universal的主要目的有以下三個

  • 方便網路爬蟲(SEO)
  • 提高在手機裝置和效能較差的設備上的性能表現
  • 快速顯示第一頁

Google,Bing,Facebook,Twitter和其他社交媒體網站都依靠網路爬蟲來為網站編索引,並在搜索內容。Angular Universal可以生成應用程序的靜態版本,無需JavaScript即可輕鬆搜索,鏈接和導航。這也使網站預覽可用,因為每個URL返回一個完全呈現的頁面。

啟用網頁抓取工具通常被稱為 搜索引擎優化(SEO)。

安裝Angular Universal所需工具

  • @angular/platform-server – 通用服務器端組件。
  • @nguniversal/module-map-ngfactory-loader – 用於在服務器渲染的上下文中處理延遲加載。
  • @nguniversal/express-engine – 通用應用程序的快速引擎。
  • ts-loader – 傳輸服務器應用程序

使用下面指令安裝上面四個package
npm install –save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine

更新 AppModule 模組

修正預設AppModuleBrowserModule的匯入方式

BrowserModule.withServerTransition({ appId: 'tour-of-heroes' }),

根據使用者的瀏覽器來決定是否使用SSR(server-side rendering)

import { PLATFORM_ID, APP_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    @Inject(APP_ID) private appId: string) {
    const platform = isPlatformBrowser(platformId) ?
      'on the server' : 'in the browser';
    console.log(`Running ${platform} with appId=${appId}`);
  }

撰寫SSR Server的程式碼

src/app/app.server.module.ts

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ModuleMapLoaderModule
  ],
  providers: [
    // Add universal-only providers here
  ],
  bootstrap: [ AppComponent ],
})
export class AppServerModule {}

在這邊使用Express framework來做server,利用Universal的renderModuleFactory功能來輸出靜態檔案

// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { enableProdMode } from '@angular/core';

import * as express from 'express';
import { join } from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');

// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

// TODO: implement data requests securely
app.get('/api/*', (req, res) => {
  res.status(404).send('data requests are not supported');
});

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);
});

下面是Express的Server檔案內容server.ts

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));
// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

Universal的設定檔

src/tsconfig.server.json的內容如下:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "commonjs",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"
  }
}

BUILD出檔案

npm run build:universal

參考資料

Posted on Leave a comment

[技術支援-3] Service worker離線支援

關於Service worker

Angular在第5版新推出了支援Service worker的功能,主要是可以讓Angular更能符合PWA的概念

所謂PWA(Progressive Web App)是希望能夠讓Web application盡可能的在各種環境(網路環境、手機作業系統等)下都能順暢且不減功能性的運作。
例如要可以支援離線功能,在使用 App 收信、寫信、刪除信件等動作,都需要將結果丟回伺服器儲存,但在某些環境下並無法一直使用網路連線,因此必須使用一種機制,讓我們仍能順暢的使用這些功能,待網路正常連線,再將剛才所執行的一切動作反應回伺服器
而Service Worker可以讓Web application做到上面這樣的事情。

使用Service worker所需環境

要使用Angular Service worker,您必須具有以下Angular和CLI版本:

  • Angular 5.0.0或更高版本。
  • Angular CLI 1.6.0或更高版本。

Web application必須在支援Service worker的Web瀏覽器中運行。
目前支持最新版本的Chrome和Firefox。其他瀏覽器的支援狀況如下圖(連結):

Service Worker 的生命週期

圖片來源:Browser push notifications using JavaScript

在創建新的應用程序時使用Service worker

--service-worker標誌添加到ng new命令中:
ng new my-project –service-worker

在現有的應用程序加入Service worker

  • 步驟1:添加service worker package
    yarn add @angular/service-worker
  • 步驟2:在CLI中啟用service worker
    ng set apps.0.serviceWorker=true
  • 步驟3:導入並註冊service worker
    src/app/app.module.ts

    import { ServiceWorkerModule } from '@angular/service-worker';
    import { environment } from '../environments/environment';

    app.module.ts

    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production})//註冊ServiceWorker腳本的名稱
      ],
      providers: [
        CheckForUpdateService,
        LogUpdateService,
        PromptUpdateService,
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
  • 步驟4:創建配置文件ngsw-config.json
    大多數src/ngsw-config.json合理的默認值如下:
    {
    “index”: “/index.html”,
    “assetGroups”: [{
    “name”: “app”,
    “installMode”: “prefetch”,
    “resources”: {
    “files”: [
    “/favicon.ico”,
    “/index.html”
    ],
    “versionedFiles”: [
    “/*.bundle.css”,
    “/*.bundle.js”,
    “/*.chunk.js”
    ]
    }
    }, {
    “name”: “assets”,
    “installMode”: “lazy”,
    “updateMode”: “prefetch”,
    “resources”: {
    “files”: [
    “/assets/**”
    ]
    }
    }]
    }
  • 第5步:構建項目
    ng build –prod

觀察Service worker的運行

可以使用Chrome Debug Tool,設置瀏覽器狀態為離線

從Network的資料會可以看到現在HTTP資料的來源是來自於service worker

這個面版可以觀察Service worker的運行狀況

SwUpdate service

下面是利用SwUpdate來取得現有可用及被啟用的notified的方式

@Injectable()
export class LogUpdateService {

  constructor(updates: SwUpdate) {
    updates.available.subscribe(event => {
      console.log('current version is', event.current);
      console.log('available version is', event.available);
    });
    updates.activated.subscribe(event => {
      console.log('old version was', event.previous);
      console.log('new version is', event.current);
    });
  }
}

檢查更新的方式

import { interval } from 'rxjs/observable/interval';

@Injectable()
export class CheckForUpdateService {

  constructor(updates: SwUpdate) {
    interval(6 * 60 * 60).subscribe(() => updates.checkForUpdate());
  }
}

強制更新資料

@Injectable()
export class PromptUpdateService {

  constructor(updates: SwUpdate) {
    updates.available.subscribe(event => {
      if (promptUser(event)) {
        updates.activateUpdate().then(() => document.location.reload());
      }
    });
  }
}

Service worker和快取緩存離線資料

我們可以將Service worker想像成像瀏覽器快取或者CDN edge一樣,都是會將網頁資料暫存起來。只是Service worker是將資料存在瀏覽器上。

透過設置src/ngsw-config.json來設定版本資訊,被分組到同一個版本的文件通常包括HTML,JS和CSS,這些文件的完整性非常重要,因為JS和CSS常常會互相引用並彼此依賴。
例如在index.html裡可能引用了bundle.js並呼叫了它的方法startApp(),如果某次改版我們將startApp()改名為runApp(),但是在index.html裡若是舊的版本,依舊呼叫startApp(),就會造成執行上的錯誤。

因此快取設定資料的完整性和版本的一致性非常的重要,可以確保ANGULAR在離線執行時可以正常運作。

每次使用者打開或更新網頁時,Angular service worker都會透過ngsw.json的更新來檢查應用程序的更新。如果有發現有更新的頁面,將會自動下載並緩存,在下次載入網頁時提供。

Debugging the Angular service worker

有關於service worker的debug資訊都在ngsw/底下,公開的網址為ngsw/state

下面是一個ngsw/state的範例:

NGSW Debug Info:
//這邊是指service worker的Driver狀態,有三種值:NORMAL(正常),EXISTING_CLIENTS_ONLY(舊的快取可安全使用),SAFE_MODE(版存資料失效,所有資料都會由網路提供)
Driver state: NORMAL ((nominal))
//最新清單的SHA hash
Latest manifest hash: eea7f5f464f90789b621170af5a569d6be077e5c
//上次更新檢查
Last update check: never
//版本資訊
=== Version eea7f5f464f90789b621170af5a569d6be077e5c ===

Clients: 7b79a015-69af-4d3d-9ae6-95ba90c79486, 5bc08295-aaf2-42f3-a4cc-9e4ef9100f65
//空閒任務隊列
=== Idle Task Queue ===
Last update tick: 1s496u
Last update run: never
Task queue:
 * init post-load (update, cleanup)
//調試日誌
Debug log:

配置文件格式介紹

詳細說明請見Service Worker Configuration

參考資料

Posted on Leave a comment

[技術支援-2] 讓IDE支援Angular Language Service

語法自動補完

自動完成可以通過在輸入時提供可能要填入的值讓我們選擇,以加速開發的速度,如下圖:

錯誤檢查

可以檢查像是模版變數是否在元件內有對應的變數供綁定

跳轉到模版變數或方法所指向的程式碼位置

單擊並按下F12進入相對應的變數位置

可使用Angular Language Service的IDE

Visual Studio Code

1. 直接在VSCode的擴充功能裡下載Angular language service

2. 自行使用VSIX安裝
打開VSCode,先安裝.VSIX

下載附加元件:直接按此下載 /
原始專案位置:vscode-ng-language-service
下載之後,按下command+shift+P來打開指令列,並輸入
install from VSIX

然後選擇剛剛所下載的檔案,Reload。

接著你的VSCode就可以支援Angular Language Service了!

WebStorm

package.json加上
devDependencies {
“@angular/language-service”: “^4.0.0”
}
然後執行
npm install

Sublime Text

在專案下執行下面兩個指令:
npm install –save-dev typescript
npm install –save-dev @angular/language-service

參考資料

Posted on 1 Comment

[技術支援-1] 支援多語系

Angular i18n可做到的事

  • 以本地格式顯示日期,數量,百分比和貨幣。
  • 在組件模板中翻譯文字。
  • 翻譯單數和複數
  • 翻譯HTML屬性的替代文字

可以透過CLI來產生XLS檔案,並透過XLS檔案來設定多語系字串,接著使用下面的字串來產生網站檔案:
ng serve –aot –locale zh-Hant
假如是使用JIT的方式去佈暑網站,則也要在頁面設定LOCALE_ID的值如下:

import { LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from '../src/app/app.component';

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ AppComponent ],
  providers: [ { provide: LOCALE_ID, useValue: 'zh-Hant' } ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

從上面的範例可以看出,Angular多語系的實作方式是讓每個語言獨立一個 index.html 版本。這樣優點是網站瀏覽較為快速,缺點則是修改時要重新REBULE的工程較大。

此連結有Angular所有支援的語系:Angular repository
在angular5的地區設定是使用BCP47,這個規格隨著時間而有些微改變如下:

LOCALE NAME OLD LOCALE ID NEW LOCALE ID
Indonesian in id
Hebrew iw he
Romanian Moldova mo ro-MD
Norwegian Bokmål no, no-NO nb
Serbian Latin sh sr-Latn
Filipino tl fil
Portuguese Brazil pt-BR pt
Chinese Simplified zh-cn, zh-Hans-CN zh-Hans
Chinese Traditional zh-tw, zh-Hant-TW zh-Hant
Chinese Traditional Hong Kong zh-hk zh-Hant-HK

以本地格式顯示日期,數量,百分比和貨幣

預設所有管道如DatePipeCurrencyPipeDecimalPipe 和PercentPipe所使用語言環境的數據格式都是en-US,如果要設置為其他語言環境,則需要導入該新語言環境的語言環境數據。

利用CLI的--locale會自動為我們設置這部份,若要手動設置方法如下:

import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';

// the second parameter 'fr' is optional
registerLocaleData(localeFr, 'fr');

template內的多語系支援

在angular內,設定多語系檔案的順序如下:

  • 開發於template先使用預設的語言做開發,如:
    <h1>Hello i18n!</h1>
  • 增加i18n的標記,如:
    <h1 i18n>Hello i18n!</h1>
  • 利用CLI產生messages.xlf,如下:
    ng xi18n
  • 將完成的翻譯文件合併到應用程式中:
    ng serve –aot –i18nFile=src/locale/messages.fr.xlf –i18nFormat=xlf –locale=fr

為了使翻議者能夠更準確的翻議,可以在i18n的指令裡面增加上下文說明,如:

<h1 i18n="An introduction header for this sample">Hello i18n!</h1>

如果相同的文字需要有不同的翻議,則可增加meaning的設定。其格式為meaning|description,如:

<h1 i18n="site header|An introduction header for this sample">Hello i18n!</h1>

若有相同翻議字串但meaning不同,則會有不同的翻譯。若翻議字串相同,description不同,則依然是相同的翻議。
除了使用meaning以外,也可以自己定義一個翻議字串的ID來使用,如:

<h1 i18n="@@introductionHeader">Hello i18n!</h1>

假如兩個翻議字串不同卻取了相同的ID,會造成翻譯後有著相同的字串:

<h3 i18n="@@myId">Hello</h3>
<p i18n="@@myId">Good bye</p>

翻譯檔案內容如下:

<trans-unit id="myId" datatype="html">
  <source>Hello</source>
  <target state="new">Bonjour</target>
</trans-unit>

則生成的HTML內容如下:

<h3>Bonjour</h3>
<p>Bonjour</p>

如果有時候只是想要翻譯文字而不想使用任何HTML TAG,則可以使用ng-container

<ng-container i18n>I don't output any element</ng-container>

翻譯HTML屬性的替代文字

假如有一個像下面的圖片有著純文字的替代文字

<img &#91;src&#93;="logo" title="Angular logo">

可以使用這樣的tag來做i18n的設定

<img &#91;src&#93;="logo" i18n-title title="Angular logo" />

這種技術適用於任何元素的任何屬性。
使用i18n-x="<meaning>|<description>@@" 語法來指定含義,說明和標識。

翻譯單數和複數

在一些語言的狀況下,不同的數量使用的詞彙不一樣,例如時間,可以顯示”just now”, “one minute ago”或”x minutes ago”
這時候可以用這樣的方式來設定翻譯文字

<span i18n>Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{minutes}} minutes ago}}</span>

上面第一個參數minutes是最重要的,用來設定這個字串要放入的變數
第二個參數plural為翻譯類型(請參見:ICU Message Format
第三個參數則是設定各種數字要顯示的文字
第三個參數可設定的選項有下面這些:

  • =0 (or any other number)
  • zero
  • one
  • two
  • few
  • many
  • other

也可以根據要顯示的變數內容來顯示不同的翻譯字串

<span i18n>The author is {gender, select, m {male} f {female} o {other}}</span>

利用ng xi18n來產生翻譯原檔

使用CLI打入可產生messages.xlf的翻譯原檔,預設是產生xlf檔案
ng xi18n
如果想要產出其他類型檔案可以加上--i18nFormat的參數如下:
ng xi18n –i18nFormat=xlf
ng xi18n –i18nFormat=xlf2
ng xi18n –i18nFormat=xmb
接著把messages.xlf改名為messages.fr.xlf(假使要翻譯的目標語言為fr)
下面為一個產出的xlf檔案內容的範例:

<trans-unit id="introductionHeader" datatype="html">
  <source>Hello i18n!</source>
  <target>Bonjour i18n !</target>
  <note priority="1" from="description">An introduction header for this sample</note>
  <note priority="1" from="meaning">User welcome</note>
</trans-unit>

target裡面要填的就是目標翻譯語言要顯示的文字

合併翻譯檔案回應用程式裡

使用AOT的方式

使用下面三個參數:

  • --i18nFile: 翻譯檔案的位置
  • --i18nFormat: 翻譯檔案的格式
  • --locale: 被翻譯的語系名稱

ng serve –aot –i18nFile=src/locale/messages.fr.xlf –i18nFormat=xlf –locale=fr

使用JIT的方式

src/main.ts裡設定所使用的語系
import { enableProdMode, TRANSLATIONS, TRANSLATIONS_FORMAT } from ‘@angular/core’;
import { platformBrowserDynamic } from ‘@angular/platform-browser-dynamic’;

import { AppModule } from ‘./app/app.module’;
import { environment } from ‘./environments/environment’;

if (environment.production) {
enableProdMode();
}

// use the require method provided by webpack
declare const require;
// we use the webpack raw-loader to return the content as a string
const translations = require(`raw-loader!./locale/messages.fr.xlf`);

platformBrowserDynamic().bootstrapModule(AppModule, {
providers: [
{provide: TRANSLATIONS, useValue: translations},
{provide: TRANSLATIONS_FORMAT, useValue: ‘xlf’}
]
});
src/app/app.module.ts裡加上LOCALE_ID

import { LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from '../src/app/app.component';

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ AppComponent ],
  providers: [ { provide: LOCALE_ID, useValue: 'fr' } ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

參考資料

Posted on Leave a comment

[功能介紹-15] Router進階介紹

設定Router Module

若我們的Router設定較為複雜時,可將Router配置為一個Router Module。

首先,設定一個路由模組app-routing.module.ts

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';

import { CrisisListComponent }   from './crisis-list.component';
import { HeroListComponent }     from './hero-list.component';
import { PageNotFoundComponent } from './not-found.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes',        component: HeroListComponent },
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  &#93;,
  exports: &#91;
    RouterModule
  &#93;
})
export class AppRoutingModule {}&#91;/code&#93;
接著,更新<code>app.module.ts</code>文件
[code lang="js"]import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { CrisisListComponent }   from './crisis-list.component';
import { HeroListComponent }     from './hero-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

多層的Router Module

我們在做網站時,會希望每個模組專注在自己的功能上,Router也不例外。因此像英雄列表的元件,如果希望點選列表內容,可以進入該英雄詳細資料的頁面如下:

首先,創建一個pre-routing檔案heroes.module.ts

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

新增一個heroes-routing.module.ts,內容如下:

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

@NgModule({
  imports: [
    RouterModule.forChild(heroesRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class HeroRoutingModule { }

將Router Module的檔案放在與其伴隨的模組在相同的文件夾中。這兩個heroes-routing.module.tsheroes.module.ts在同一個src/app/heroes文件夾中。

考慮給每個功能模組自己的路由配置文件。當功能路線很簡單時,看起來可能會過早。但是,路線趨於變得更加複雜,模式的一致性會隨著時間的推移而得到回報。

接著在heroes.module.ts裡導入HeroRoutingModule

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

import { HeroRoutingModule } from './heroes-routing.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroRoutingModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

path: 'heroes'目前定義在兩個地方:HeroesRoutingModule和AppRoutingModule。
由功能模塊提供的route由Router合併到其導入的模塊的route中。這使您可以繼續定義功能模塊路由,而無需修改主路由配置。

如果不想設定兩次相同的資料,可以把舊的Router設定移除

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';

import { CrisisListComponent }   from './crisis-list.component';
// import { HeroListComponent }  from './hero-list.component';  // <-- delete this line
import { PageNotFoundComponent } from './not-found.component';

const appRoutes: Routes = &#91;
  { path: 'crisis-center', component: CrisisListComponent },
  // { path: 'heroes',     component: HeroListComponent }, // <-- delete this line
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
&#93;;

@NgModule({
  imports: &#91;
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  &#93;,
  exports: &#91;
    RouterModule
  &#93;
})
export class AppRoutingModule {}&#91;/code&#93;

在應用程式裡面使用AppRoutingModule,打開<code>src/app/app.module.ts</code>:
[code lang="js"]import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule }     from './heroes/heroes.module';

import { CrisisListComponent }   from './crisis-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HeroesModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

這邊要注意的是模塊導入順序很重要

當所有的路由都在AppRoutingModule時,我們將通用path: '**'符號的設定放在路由/heroes的後面,這樣路由器才有機會匹配一個URL到/heroes路由,而不會先被AppRoutingModule的這個設定匹配走。

{ path: '**', component: PageNotFoundComponent }

如果Routing在兩個不同的模組內,也是要先載入HeroesModule再載入AppRoutingModule,這樣才會使用HeroesModule裡的路由設定而不會因為在AppRoutingModule找到匹配的通用符號而顯示PageNotFoundComponent。
如果將import的順序倒過來,則會顯示PageNotFoundComponent的頁面。

在程式裡去操控路由器

使用this.router.navigate來在程式內操作路由工作

gotoHeroes() {
  this.router.navigate(['/heroes']);
}

若需要傳一個參數,則可以用

gotoHeroes() {
  this.router.navigate(['/hero', hero.id]);
}

要傳送兩個參數則如下

gotoHeroes(hero: Hero) {
  this.router.navigate(['/hero', { id: hero.id, foo: 'foo' }]);
}

點下後出現的連結如下
localhost:3000/heroes;id=15;foo=foo
這個叫做Matrix URL,在URL查詢字符串中,用分號隔開“;” 不同的參數,而不是用&

取得Router的參數

export class HeroListComponent implements OnInit {
  heroes$: Observable<hero&#91;&#93;>;

  private selectedId: number;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    this.heroes$ = this.route.paramMap
      .switchMap((params: ParamMap) => {
        // (+) before `params.get()` turns the string into a number
        this.selectedId = +params.get('id');
        return this.service.getHeroes();
      });
  }
}

添加換頁效果

首先要載入BrowserAnimationsModule這個動態效果模組。

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
  imports: [
    BrowserAnimationsModule

animations.ts在根src/app/文件夾中創建一個文件。內容如下所示:

import { animate, AnimationEntryMetadata, state, style, transition, trigger } from '@angular/core';

// Component transition animations
export const slideInDownAnimation: AnimationEntryMetadata =
  trigger('routeAnimation', [
    state('*',
      style({
        opacity: 1,
        transform: 'translateX(0)'
      })
    ),
    transition(':enter', [
      style({
        opacity: 0,
        transform: 'translateX(-100%)'
      }),
      animate('0.2s ease-in')
    ]),
    transition(':leave', [
      animate('0.5s ease-out', style({
        opacity: 0,
        transform: 'translateY(100%)'
      }))
    ])
  ]);

然後在src/app/heroes/hero-detail.component.ts增加使用這個動態效果的綁定

@HostBinding('@routeAnimation') routeAnimation = true;
@HostBinding('style.display')   display = 'block';
@HostBinding('style.position')  position = 'absolute';

Child routing component

子路由元件與直接使用參數去定義路由,Router預設的狀況下,若瀏覽器重新導航到相同的元件時,會重新使用該元件既有的實體,而不會重新創建。因此在物件被重用的狀況下,該元件的ngOnInit只會被呼叫一次,即使是要顯示不同的內容資料。
但是被創建的元件實體會在離開頁面時被銷毀並取消註冊,因此在前面範例的heroes-routing.module.ts檔案中裡,所使用的導航設定方式,由於在瀏覽HeroDetailComponent之後,一定要先回到HeroListComponent,才能進入下一個Detail頁面。

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

這會造成因為在回到HeroListComponent時已把HeroDetailComponent刪除掉了,再選擇另一個英雄要查看細節時,又會再創立一個新的HeroDetailComponent。因此每次選擇不同的英雄時,組件都會重新創建

如果想要保留頁面的狀態,就可以改使用子路由的方式來定義

下面是使用子路由的範例:

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

因為CrisisDetailComponentCrisisCenterHomeComponent都是CrisisListComponent的子組件,因此在不同的子組件內的狀態得以被保存,直到頁面切換離開CrisisCenterComponent時才會將元件實體刪除,這可以讓我們在瀏覽不同CrisisDetail時,得以使用到Router預設的重用設定。

使用相對路徑

./ 是在目前的位置。
../ 在上一層的位置。
下面為使用範例

// Relative navigation back to the crises
this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });

網址認證功能

很多時候某些頁面或許需要登入才可以檢視,有些要登入後具備某些身份才可以開啟該網址,這時候就可以用到Router的CanActivate功能。

下面是src/app/auth-guard.service.ts的內容:

import { Injectable }     from '@angular/core';
import { CanActivate }    from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate() {
    console.log('AuthGuard#canActivate called');
    return true;
  }
}

下面是src/app/admin/admin-routing.module.ts的內容

import { AuthGuard }                from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ],
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

本篇所有範例可見:live demo / download example.

參考資料

Posted on Leave a comment

[功能介紹-14] Router基礎介紹

什麼是Routing?

Routing意指路由器,也就是由一個路由器來決定現在要顯示的頁面是什麼
在套用Routing時,會有下列的實踐流程
1. 套用轉址設定(讓伺服器不去真正網址所在的位置去讀取資料,而改由Routing來決定現在要顯示什麼畫面)
2. 由url分析要顯示的狀態是什麼
3. 由狀態去獲得真正要取得那些資訊
4. 從這些資訊組成實體
5. 套用導覽動作,由這個畫面切換至另一個畫面

在Angular裡,最佳做法是將載入和設定Routing放在一個top-level的模組內,並於AppModule內來import所有的路由資訊。
按照慣例,模組AppRoutingModuleapp-routing.module.ts會放在src/app

Angular的Routing 產生的虛擬 URL,並不是真的存在於檔案系統裡,因此需要伺服器能夠支援,否則重新整理時會出現404 not found。

設定base href

Router使用history.pushState來進行導航。靠著pushState,可以讓瀏覽器網頁路徑看起來像是更換真實的網址。因此Angular APP內的網址可能與伺服器的網址無法區分。
我們需要添加元素到index.html讓pushState路由能夠生效。當引用CSS或JS檔案時應用base href的值來設定相對URL路徑。

src/index.html檔案裡增加下面的文字:

<base href="/">

應被放置在在標籤的後面。不論是不是位於根目錄,都需要設置這個項目。

像是Plunker不會有固定的網站基準位置,這時可以用動態設定base path如下:

<script>document.write('<base href="' + document.location + '" />');</script>

使用Routing

在src/app/app.module.ts導入RouterModule及Routes

import { RouterModule, Routes } from '@angular/router';

在一個Angular的應用程式中只會有一個Routing,當瀏覽器的URL更改時,router會尋找Route裡確認要顯示的組件及畫面。
要使用Route,要將Router透過RouterModule.forRoot做配置並且將添加到AppModule的imports陣列
以下為src/app/app.module.ts的部份內容:

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'hero/:id',      component: HeroDetailComponent },
  {
    path: 'heroes',
    component: HeroListComponent,
    data: { title: 'Heroes List' }
  },
  { path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    //在這邊用RouterModule.forRoot來配置Router
    RouterModule.forRoot(
      appRoutes, // 設置要如何導航的資料
      { enableTracing: true } // <-- 為DEBUG而設置的
    )
    // other imports here
  &#93;,
  ...
})
export class AppModule { }&#91;/code&#93;

<h3>RouterModule.forRoot</h3>
在之前<a href="https://angular.io/guide/ngmodule#approutingmodule" rel="noopener noreferrer" target="_blank">ngModule</a>的部份,有講到如果我們希望有一個子元件能夠如同核心元件一般,比所有其他子元件都更優先被載入,或者是希望能夠在被所有元件初始化之前,優先設定好裡面的值可以用下面的方式來將元件使用forRoot方法傳回ModuleWithProviders。
[code lang="js"]static forRoot(config: UserServiceConfig): ModuleWithProviders {
  return {
    ngModule: CoreModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}

Routing就是使用這個方法,因此可以傳參數進去並且比所有其他的元件更早載入以決定現在所要顯示的頁面。

放置Router

Router會根據網址決定要顯示那個元件的VIEW,所以需要設定一個區塊來顯示要被插入的VIEW,我們可以插入下面這段文字

<router-outlet></router-outlet>
<!-- Routed views go here -->

設定Router規則

如果要設定一個連至我們設定的位置的超連結,可參考下面的寫法

  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>

然後就可以有如下圖的頁面功能出現:

更多Router的設定細節

更多的Router設定方式可看官方說明:Routes

下面為幾個範例:

設定子連結

[{
  path: 'team/:id',
 component: Team,
  children: [{
    path: 'user/:name',
    component: User
  }]
}]

上面的設定當我們的URL為/team/11/user/bob時,Router會顯示Team這個元件並在裡面生成一個User元件

Multiple Outlets

[{
  path: 'team/:id',
  component: Team
}, {
  path: 'chat/:user',
  component: Chat
  outlet: 'aux'
}]

上面的設定當我們URL為/team/11(aux:chat/jim)時,會先建立Team元件然後再建立Chat元件,接著將Chat元件放置至Team元件內名為aux的outlet。

百搭符號

[{
  path: '**',
  component: Sink
}]

上面的設定無論我們在那邊,都會創建Sink元件

重新轉向

[{
  path: 'team/:id',
  component: Team,
  children: [{
    path: 'legacy/user/:name',
    redirectTo: 'user/:name'
  }, {
    path: 'user/:name',
    component: User
  }]
}]

上面的設定當我們的URL為/team/11/legacy/user/jim時,會自動導轉到/team/11/user/jim,然後產生Team元件並在裡面生成User元件。

於path設定空字串

[{
  path: 'team/:id',
  component: Team,
  children: [{
    path: '',
    component: AllUsers
  }, {
    path: 'user/:name',
    component: User
  }]
}]

上面的設定當我們導航至/team/11,則會產生AllUsers元件實體。
空路徑也可以設定子項目

[{
  path: 'team/:id',
  component: Team,
  children: [{
    path: '',
    component: WrapperCmp,
    children: [{
      path: 'user/:name',
      component: User
    }]
  }]
}]

當URL為/team/11/user/jim時,將會產生一個WrapperCmp實體與一個User在裡面。

設定pathMatch

[{
  path: '',
  pathMatch: 'prefix', //=>這是default值
  redirectTo: 'main'
}, {
  path: 'main',
  component: Main
}]

上面這種寫法,即使我們導航至/main,因為其前綴字符合path: ''的設定,因此Router還是會執行轉址至main的動作。
因此需要改成下面這樣

[{
  path: '',
  pathMatch: 'full',
  redirectTo: 'main'
}, {
  path: 'main',
  component: Main
}]

延遲加載

[{
  path: 'team/:id',
  component: Team,
  loadChildren: 'team'
}]

Router將使用註冊的NgModuleFactoryLoader來獲取與’team’關聯的NgModule。

RouterLink

routerLink是用來設定這個連結要連到的router位置,要填的值會需與appRoutes裡設定的path一致。
這邊有routerLink的詳細說明:https://angular.io/api/router/RouterLink

例如我們傳入queryParams與fragment的一個範例如下:

<a &#91;routerLink&#93;="&#91;'/user/bob'&#93;" &#91;queryParams&#93;="{debug: true}" fragment="education">
  link to user component
</a>

會產生網址為/user/bob#education?debug=true的連結

如果我們想要保留所有額外傳來的GET參數,可以用下面的方式來表達:
PS: 在4.0之前是使用queryParamsHandling,而在angular4.0之後停止支援這個參數,而需要改使用上面的方法

<a &#91;routerLink&#93;="&#91;'/user/bob'&#93;" preserveQueryParams preserveFragment>
  link to user component
</a>

另外我們若有設定queryParams,可以決定若是使用者原本從網址列就有傳自己的GET參數時要如何處理,有下面幾種選項

  • 'merge': merge the queryParams into the current queryParams
  • 'preserve': preserve the current queryParams
  • default/'': use the queryParams only

下面為一個使用範例

<a &#91;routerLink&#93;="&#91;'/user/bob'&#93;" &#91;queryParams&#93;="{debug: true}" queryParamsHandling="merge">
  link to user component
</a>

RouterLinkActive

routerLinkActive則是用來設定若現在的網址與所設定的連結一致時,要加上去的Class名稱
這邊有routerLinkActive的詳細說明:https://angular.io/api/router/RouterLink

我們可以傳入參數exact: true來設定是不是需要網址完全符合才顯示該class

<a routerLink="/user/bob" routerLinkActive="active-link" &#91;routerLinkActiveOptions&#93;="{exact:
true}">Bob</a>

或者如果我們希望連結符合時,能夠展開下面的子頁面時,可以用將routerLinkActive存入模版變數來使用,下面是一個範例

<a routerLink="/user/bob" routerLinkActive #rla="routerLinkActive">
  Bob {{ rla.isActive ? '(already open)' : ''}}
</a>

取得Router目前狀態

在每個Router完成它的生命週期之後,Router將產生當前導航頁面的ActivatedRoute及RouterState。
下面為一個使用範例:

@Component({templateUrl:'template.html'})
class MyComponent {
  constructor(router: Router) {
    const state: RouterState = router.routerState;
    const root: ActivatedRoute = state.root;
    const child = root.firstChild;
    const id: Observable<string> = child.params.map(p => p.id);
    //...
  }
}

ActivatedRoute有許多有用的資訊如下:

屬性 描述
url 在router裡所設定的path的值(為一個Observable物件)。
data 在router裡所設定的value的值,亦會包含從resolve guard來的值。
paramMap 包含這個Route所設定的必需和可選參數的列表。
取代舊版angular的params
queryParamMap 所有Route可獲得的query parameter
取代舊版angular的queryParams
fragment 所有Route可獲得的HTML錨點
outlet 被使用來顯示這個Router的名稱(RouterOutlet
routeConfig 用於包含原始路徑的路由的路由配置。
parent 所取得的資料為ActivatedRoute。可獲得目前頁面在Router設定的parent。
firstChild 所取得的資料為ActivatedRoute。可獲得目前頁面下面第一個子連結。
children 包含目前路徑下的所有子連結

Router events

我們可以藉由Router物件來監聽所有此Router相關的事件,一個簡單使用範例如下:

    constructor(private router: Router) {

        router.events.subscribe( (event: Event) => {

            if (event instanceof NavigationStart) {
                // Show loading indicator
            }

            if (event instanceof NavigationEnd) {
                // Hide loading indicator
            }
        });

    }

下面為所有可監聽的狀態列表及說明:

Router Event Description
NavigationStart 當導航開始時會觸發
RoutesRecognized 當Router正在解析URL及Routes時觸發
RouteConfigLoadStart 在Lazy loaded router配置之前觸發的事件。
RouteConfigLoadEnd Lazy loaded router載入後觸發的事件
NavigationEnd 導航成功完成後觸發
NavigationCancel 導航取消時觸發
NavigationError 由於意外錯誤導致導航失敗時觸發