鳳凰專案:看IT部門如何讓公司從谷底翻身的傳奇故事

這本書算是一本故事書,也因為是以故事的型態去描述一間公司如何從谷底翻身,讀者會更能夠明白『管理』對於一間公司有多麼重要。
也能從故事裡面直接理解書中所闡述的管理方式在實際情況是怎樣的去運用,在運用時也不會是直接的一帆風順,而是有重重的考驗並且需要高層的全力支持與理解。
我覺得此本書是管理相關書籍中相當值得一看的書。

因為故事情節用簡述的,就沒辦法讓讀者感受書中情境,因此本文主要為筆者自己讀後的筆記
主要紀錄我認為書中很重要的管理方法和管理思維,書內的附錄也有此書管理思維的整理。

有關故事內容,想了解的話建議自己去買書來看。

Work in process控制

在這邊他們使用工廠管理來比喻我們在開發專案的狀況。

對工廠管理而言,在生產線會盡量的去避免讓工廠內同時有過多的在製品,也就是所謂庫存。
對開發專案來說,在製品指的是開發中但是尚未開發完成的功能,還無法帶給公司收益,也是要極力去減少的。

在生產線上,一定會有處理效率最差的點,我們稱為瓶頸點(或稱約束點),創造約束理論的艾利高德拉特告訴我們,在瓶頸以外任何地方做的改進都是假像
的確,在瓶頸點之後做的任何改進都是徒勞無功的,因為只能乾等瓶頸把工作送過來,而在瓶頸點之前做的任何改進只會導致瓶頸處堆積更多庫存。

因此,在開發專案時,要了解公司的瓶頸點在那,是什麼地方讓最多的專案在等待這個資源而讓需求被卡住無法繼續進行。
以鳳凰專案而言,因為過多的核心資訊被掌握在一個天才員工布倫特手中,只有他知道怎麼處理,導至這個員工每天都必須處理非常多的事也有非常多的事在等待這一個資源。

下圖表示當一個越忙錄的資源,其他資源想等待這個資源的時間會越來越高

因此改善這個狀況,第一步就是要先釐清約束點所在,第二步就是充分利用約束點

以此書案例來說,約束點是天才員工布倫特,那他們首先做的就是保護這個約束點,不讓大家直接指使這個約束點去做他們認為重要的事,他們安排了一位專門的人員來安排這位約束點的工作內容,避免大家一直想私自去使喚他做事。接著他們要讓約束點的時間不被浪費,確認約束點永遠不會因為要遷就其他資源而枯等。再來他們將布倫特的工作標準化,讓更多其他的人也可以去接手做布倫特正在做的事,也就是提升約束點的產出。

總之,我們要記得任何對非約束點的改善都只是鏡花水月

三步工作法

  • 第一步工作法

    幫助我們理解如何建立快速的工作流,讓工作順暢地從開發部移動到IT運維部,因為那正是公司與客戶之間的銜接。

    這邊需要去了解公司的價值點,相較於把更多的工作投入系統,將不需要的工作從系統中剃除甚至更為重要,讓資訊部門能夠很快速的產出最精簡可用的有價值的系統,並且獲得反饋。為此,我們需要知道,與達成企業目標息息相關的東西是什麼,不論是專案,運營,戰略,合規,安全性等,通通有可能(請見下方的訂定組織目標)。

  • 第二步工作法

    告訴我們如何縮短及增強回饋循環,因而能夠從源頭開始解決品質問題,並且避免重工。在這邊我們也必須設法去根除計劃外工作的最大來源。

    所有計劃中的工作,在執行工作前,必須要分門別類地列出完成工作所需的一切先決條件:例如說,筆電型號,使用者資訊的規格,軟體及需要的授權,以及他們的組態,版本資訊,資安要求,處理能力和連續性需求等。

    也就是建構資源清單,亦是物料清單,以及需要的工作中心與生產途程,一旦備妥,加上工單和你的資源,就可以釐清產能及需求為何,並弄清楚可否接受為新的工作,並實際為它進行排程。

    第二步工作法的關鍵部份是以視覺化的方式呈現等待時間,那樣就能知道你的工作正在某人的佇列中排隊好幾天,或者是工作必須往後退,因為未備妥完整的零件。藉由需求清單被列出,也可以讓接受需求的人在接受訂單時,可以先確認每一個必須參與的資源都能夠有需要的投入,讓需求,銷售,開發能夠一起擬定生產計劃。

  • 第三步工作法
    告訴我們如何建立一種文化,既能鼓勵大家探索,從失敗中汲取教訓,又能理解反覆與練習是精通工作的先決條件。

    提升預防性工作是全面生產維護(Productive Maintenance, TPM)這類計劃的核心,精實社群(Lean Community)信奉全面生產維護的精神,主張我們應不惜一切代價提升維護水準。『改善日常工作甚至比進行日常工作更重要』。持續給系統施加壓力,從而不斷強化習慣並且改善某件事情。

    也就是所謂『改善型(Improvement Kata)』,書中是使用為期兩周的改善循環,每個改善循環都要實施一個小型的計畫》執行》查核》行動的專案,持續朝目標邁進。

    在此必要的實務作為包括:建立創新,勇於冒險及高度信任的團隊文化。把至少20%的開發和IT運維週期分配給非功能性需求,並持續強化及鼓勵大家進行改善活動

四種工作類型

在這邊這本書將工作份為四種類型:

  • 業務專案:公司所有的正式專案
  • 內部IT專案:由業務專案衍生出來的基礎架構或IT運維專案,以及內部生成的改善專案(如建立新環境和部署自動化)。這些專案經常未被集中管理,而是隸屬於預算所有者。
  • 變更:包括需求變更或BUG等
  • 計劃外工作或救火工作:包括production issue,通常由上面三類工作導致,而且往往以犧牲其他計畫內工作為代價。在書中說這類型的工作是最具有破壞性的,它並非實質的工作,其他三種工作都是基於需求而事先計劃好的。計劃外的工作會阻止我們進行其他三類的工作,就像物質和反物質,在計畫外的工作面前,所有計劃內的工作都會被延後。
    另外一點則是未被償還的技術債,源自於走捷徑,短時間內,那樣或許行的通,但是就像金融債一樣,久而久之,利息越滾越多。如果一個組織沒有還清它的技術債,那麼,就必須一點一滴,耗費心力,以計劃外的工作的形式來償還那些技術債的衍生利息

訂定組織目標

在這本書有列出了CFO的營收目標:

  • 公司體質健全
  • 營收
  • 市占率
  • 平均訂單規模
  • 盈利能力
  • 資產收益率
  • 財務狀況
  • 訂單轉化成現金的周期
  • 應收帳款
  • 準確且及時的財務報告
  • 借貸成本

在本書他們針對每一個營收目標列出『倚賴IT的區域』及『IT導致的業務風險』以及『倚賴的IT控制』來列出IT在這些目標項目中的角色。倚賴的IT控制指的是防範這些錯誤發生的反制措施,或者至少能夠偵測到問題及做出回應。

在這本書很戲劇化的是SOX-404的稽核專案,資安部門原本一直要求開發部門開發一個安全控制的系統來應付發生錯誤時的資安問題,最後才發現檢測重大錯誤所倚靠的控制手段是人工對帳步驟,而不是上游的IT系統。其實財務部門早已用人工的方式來達到這個資安的要求了,不需額外再開發相關的系統。

因此,不同部門間的資訊透明度真的是一件很重要的事。

遠程目標則包括:

  • 我們具競爭力嗎?
  • 了解客戶的需求和期望:我們知道要創造什麼嗎?
  • 產品組合:我們有正確的產品嗎?
  • 研發效能:我們能夠有效地建立產品嗎?
  • 產品上市時間:我們能夠盡快把產品推向市場,應且搶占一席之地嗎?
  • 銷售管道:我們的產品能夠觸及感興趣的潛在客戶嗎?
  • 我們的作法有效嗎?
  • 按時交貨:我們有尊守對客戶的承諾嗎?
  • 顧客維繫:我們正在增加客戶還是流失客戶?
  • 銷售預測準確率:我們可以把銷售預測準確率納入銷售計畫流程嗎?

相關連結

Agile LEGO City workshop

這是由公司同事Jed所發起的公司內部課程。
Agile LEGO City workshop是用樂高積木來模擬敏捷開發的實際情形的一個小遊戲,藉著遊戲可以從中體認到軟體開發時,可能會遇到的許多狀況,並藉由會後的討論與檢討來讓參與者更能夠理解敏捷開發的意義。

敏捷開發宣言

在一開始時,我們先讀了下面的敏捷開發宣言

藉著親自並協助他人進行軟體開發,
我們正致力於發掘更優良的軟體開發方法。
透過這樣的努力,我們已建立以下價值觀:

個人與互動 重於 流程與工具
可用的軟體 重於 詳盡的文件
與客戶合作 重於 合約協商 
 回應變化 重於 遵循計劃 

也就是說,雖然右側項目有其價值,
但我們更重視左側項目。

讓參與者能夠從中理解敏捷的核心精神,也就是重視團隊間的溝通互動,接受需求的變化
並且讓我們知道敏捷開發最核心的就是這個宣言裡所描述的,所有敏捷實作方法如SCRUM等都需要建構於這個核心精神上。

遊戲規則

接著就是介紹LEGO遊戲的規則:
1. 依照下圖建造客戶所需的樂高城市,項目越前面代表客戶越重視的

2. 這個建城市的PO就是主講人Jed,在開始之前可以先和Jed提問,在每一個sprint的結束也會由PO來做驗收
3. 總共會有8個sprint,每個sprint開始之前要先計劃好這個sprint要做的,並移到TO DO去
PS:在這邊使用如下圖的看板方法去追每一個TASK的進度

4. 有一個計分表去計算每個sprint計劃要做的和實際驗收成功的任務比較表

遊戲進行

在開始遊戲後,本組所交付的任務頻頻被打槍,首先是我們在第一個sprint交付了平房,結果被打槍~原因是,請依照客戶需求優先順序做交付,我們前面都還沒有交付,不接受。
接著河濱公園安全設施因為圍牆沒蓋滿被拒絕,然後開始沒有做門,門沒有面對道路,沒有畫道路,圓環要在市中心,托兒園應臨近住宅….
等等原因一直被拒絕交付退回重改(火大)。

這是我們小組最後所交付的成果圖

會後檢討

我覺得這部份是這整個遊戲中最精華的部份,很感謝Jed還特別為我和Jack再做一次報告,讓我得以有機會聽到這個最精華的部份。

首先他讓我們提出我們覺得在遊戲中的感想,最多人提出的就是我們在一開始沒有搞清楚客戶需求,導致頻頻被退件
以及在一開始沒有先對整個城市做完整的規劃,例如圓環要在市中心等等…
造成我們所做的產品一直無法即時被release

然後Jed分享了敏捷開發的原則

The Agile Principles (敏捷原則)

1. 最為優先的事情是透過早期與持續交付有價值的軟體來使客戶滿意。
有價值的軟體指的是要重視客戶的優先順序,交付對客戶最有價值的東西
2. 歡迎需求的變動,即使是在開發的晚期。敏捷式流程駕馭變動來作為客戶的競爭優勢。
重點在於掌握變更,歡迎改動
3. 頻繁的交付工作產生的軟體,自數週至數月,週期越短越好。(2~4周)
4. 領域專家與開發成員必須一同作業,並貫穿整個專案開發時期。團隊的概念
5. 使用積極的工作成員來建構專案,給予他們環境以及支援所需的一切,然後信任他們能夠完成工作。
支援積極的個人(指願意主動多做更多事的人)帶動團隊氣氛,也就是信任團隊成員
6. 在開發團隊中最快也最有效的傳遞資訊方法就是面對面的溝通。
7. 工作產生的軟體是衡量進度最主要的依據。
這是最主要度量進度的方法,對使用者來說一定要完成的部份,因此找到使用者需求的核心價值十分重要
8. 敏捷式流程倡導水平一致的軟體開發
持續的開發,維持穩定步調,讓團隊狀況保持穩定
9. 專案發起者,開發人員以及使用者都必須持續的維持專案進度。
精簡,避免over design,不做太多額外的設計
10.持續重視技術的優勢以及設計品質
追求優良設計與技術
11.最好的架構、需求以及設計會出現在能夠自我管理的團隊裡
可以不需要PM就可以完成架構需求與規劃
12.在規律的反覆之間,團隊會反省與思考如何更有效率,然後相對的來調整與修正團隊的開發方式。
以天為單位定時自省,適當調整與修正自己的行為

在上述的精神底下的意義主要就是承諾,責任與互相信任。
敏捷是一個價值觀,也就是敏捷宣言與原則所述說的這樣的價值觀,Scrum是敏捷開發的一個框架,在Scurm裡所說的站立會議是一個scrum的儀式
優文分享:我對Daily Scrum的理解與看法
一般來說敏捷開發只估算這一回合要做的事情,過去我們在估算專案講的是人/天,這是以成本的角度去切入
而敏捷開發主要講的是故事點,也就是客戶需求的核心價值,重心在於價值。

對敏捷開發而言,估算是為了針對團隊生產力訂出並製定策略,重視的是產品的核心價值
因此可以看成是這樣

因此對於敏捷開發來說,每一個sprint只規劃該sprint要做的事,將客戶所需的功能切分到最小,每2-4週交付一次,每一次的sprint會將所有任務填上相對的一個數字,由多個成員來估算所需工時,並取一個平均值(若有差異較大的則需確認原因)
並在該sprint去計算本來我們預估這個sprint可以完成幾個工時,但實際完成了幾個?
再使用這個實際完成的數字去調整下一次sprint要release的總工時。
在這樣個估算方式下,隨著團隊穩定成長與默契的增加,估算的準確度會慢慢提升,如下圖

也因此在敏捷開發會特別重視團隊的穩定,惟有穩定的團隊才能帶來穩定的產品開發。

參考資料

雖然本文實際上沒有參考到這些文章,不過我去網路上爬了一些別人做LEGO workshop的經驗分享的文章
感覺滿有趣的,因此也在此附上

[技術支援-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"
} ]

[技術支援-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”]

[技術支援-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

[技術支援-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
}

參考資料

[技術支援-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

參考資料

[技術支援-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

參考資料

[技術支援-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

參考資料

[技術支援-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 { }

參考資料