---
title: 使用 Docker 建立Angular的開發環境 # 簡報的名稱
lang: zh-tw
tags: Angular, Docker # 簡報的標籤
slideOptions: # 簡報相關的設定
transition: 'slide' # 換頁動畫
width: 1200
---
# 使用 Docker 建立Angular的開發環境
[TOC]
## 前言
由於前端套件的版本進化快速,為了方便管理開發環境,避免不同版本之間產生衝突,便可以考慮使用近年來最流行的容器化技術來隔離各種開發環境(包括開發工具、環境變數等),本文將介紹如何使用Docker將Angular的開發套件包裝成一個容器。
## 建置一個容器化的開發環境
一般而言,前端開發環境大致上可以區分為Command Line Interface(例如[Angular CLI](https://cli.angular.io/))以及IDE工具(例如[Visual Studio Code](https://code.visualstudio.com/)),但是不論是哪一種作業環境,前端開發套件版本的多元性一直是開發人員的痛,雖然目前已經有[nvm](https://github.com/nvm-sh/nvm)套件可以用來管理npm的版本,但卻無法針對CLI本身作版本控制,因此透過Docker將CLI套件打包成容器使用會是比較推薦的作法。
### Step 1. 安裝WSL2
在Windows 10 以上的版本,都支援WSL2;可以使用較輕量化的WSL2取代VM。參考[官方文件](https://learn.microsoft.com/zh-tw/windows/wsl/install)來安裝與啟用WSL2,以下是簡單的指令。
```shell=
# 預設會安裝Ubuntu
wsl --install
# 可指定要安裝的Linux,例如 Debian
wsl --install Debian
```
安裝並重新開機後,使用 `wsl -l -v ` 列出已安裝的散發套件。
### Step 2. 安裝Docker
在WSL2的環境下安裝Docker,最簡單的方式便是安裝Docker Desktop。從Docker的官方網站的[下載頁面](https://www.docker.com/get-started/)並且依照教學安裝後便可以看到Docker的管理介面。
另外,若公司內不允許安裝Docker Desktop,可以考慮直接進入WSL2之中安裝,請參考[官方文件](https://docs.docker.com/engine/install/ubuntu/)。
簡單的指令如下:
```shell=
# 啟用 Windows 子系統 Linux 版
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
# 啟用虛擬機器平台
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
# 登入WSL
wsl
# 更新套件
sudo apt-get update
# 移除舊版的Docker.io (如果有的話)
sudo apt-get remove docker docker-engine docker.io
# 安裝最新版本的Docker
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 啟動docker
sudo service docker start
# 測試功能是否正常
sudo docker run hello-world
```
### Step 3 製作Dockerfile
所有的Docker image都可以藉由Dockerfile來建置,一般情況下會依照不同的環境選用不同的作業系統,例如說Linux的centos或ubuntu(選用不同的作業系統會影響到後續的指令與操作);為了簡化製作Dockerfile的動作,許多的軟體社群與廠商都有分享各自的Dockerfile在[Docker Hub](https://hub.docker.com/)上,在製作時可以直接尋找適合的Dockerfile作為基底,以Angular CLI為例,相信大家都知道 Angular-CLI 是透過 Node.js 來執行的,因此可以直接選用node官方提供的[印象檔](https://hub.docker.com/_/node)來製作是最簡單的選項,檔案的內容如下:
```dockerfile=
#
FROM node:current-alpine
# Webpack uses a port to do live reload of the application. That port is 49153 by default.
# Ng Serve listen request by 4200 port.*
EXPOSE 4200 49153
# Install npm and Angular CLI as the root user
RUN npm install -g npm@latest \
&& npm install -g @angular/cli@latest
RUN chown -R node:node /usr/local/lib/node_modules \
&& chown -R node:node /usr/local/bin
USER node
```
**Step 3 建立映象檔(Image)與容器**
當Dockerfile製作完成後,便可以使用docker build 指令來建立Image
```shell=
docker build --rm -f "Dockerfile" -t angular:latest .
```
當映檔案製作完成後再透過*docker run*指令來執行容器,並且綁定容器與本機的工作目錄。
```shell=
# 使用命令提示符(Command Prompt)的環境下
docker run -d -it -p 4200:4200/tcp -p 49153:49153/tcp -v %cd%:"/var/sandbox" --name dev-angular angular:latest
# 使用shell(powershell 或 Bash Shell)的環境下
docker run -d -it -p 4200:4200/tcp -p 49153:49153/tcp -v ${PWD}:"/var/sandbox" --name dev-angular angular:latest
```
上述的指令是將Host的路徑對應到容器內的`/var/sandbox`路徑,使用`docker attach`指令進入容器後,找到該路徑可以看到與Host相同的資料夾。
在WSL2上,建議將路徑設定在`/mnt/{Work Directory}`目錄底下,詳細的內容可參考WSL的說明文件。
### Step 4 新增Angular專案
當容器執行時可以透過`Docker ps`確認容器的ID或名稱後進入容器內部操作Angular CLI,利用CLI建立專案
```shell=
# 使用root權限登入容器
docker exec -u root -it dev-angular /bin/sh
# 使用 Angular Cli新增專案
cd var/sandbox
ng new my-first-project --style=scss
...
# 編譯專案
cd my-first-project
ng serve --watch --host 0.0.0.0 --poll 1000
```
>若是在Windows環境中編譯,存檔時,ng serve --watch無作用的原因是因為二邊的檔案系統不一致,範例中使用 --poll 選項。這將使 Angular CLI 定期輪詢文件以檢測更改。1000 是輪詢的時間間隔,以毫秒為單位。
>若只是執行`ng serve`的話,會發現Windows的瀏覽器無法連線,這是因為對於Docker內的環境而言,本機是屬於外部環境,而localhost是不允許外部連線的,需搭配`--host 0.0.0.0`參數讓angular lite-Server能夠允許來自容器外部的連線。
### Step 5 調整Unit Test的測試設定
接著我們要調整測試環境的設定檔,首先是unit test的設定,在檔案`karma.conf.js`中加入自訂的啟動程式,選擇使用Chromium並開啟headless等模式
```javascript=
customLaunchers: {
ChromiumHeadlessCI: {
base: 'Chromium',
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--remote-debugging-port=9222']
}
}
```
再使用指令進行測試:
```shell=
ng test --watch=false --progress=false --browsers=ChromiumHeadlessCI
```
# 附錄A:開發與佈署Angular的函式庫
就軟體技術的概念來說,Angular 是一套完整的framework,就像 .net framework提供了大量的Library 來擴展程式的功能,Angular也提供了大量的Module來實現程式功能的擴展,就連作為程式進入的的核心AppModule本身也是一個Module。因此在實務上將自已開發的模組分享給團隊的成員使用時,可以將所有的功能以物件導向的概念包裝成數個模組之後上傳到NPM的repository 供人下載使用 ,團隊成員只需要輸入 下述的指令便可以安裝與使用。
>npm install [module-name] | --save
>
## 開發步驟
在Angular7以上的版本,新增專案時提供了一個新的Tag `--create-application=false`,讓開發者建置一個最精簡的開發環境;使得Angular的具有類似Solution與Project的概念,讓Application與Library有了區隔,語法如下所述:
>ng new [solution name] --create-application=false
>
當指令執行完成後只會產生**package.json**以及**angular.json**這些必要的檔案,此時可以使用`ng generate`來產生Angular的Library以及要執行的應用程式,簡單的範例如下:
```shell=
ng new my-solution --create-application=false
cd my-solution
ng generate library @lib/firstlib
ng generate application demoapp
```
執行完後,可以在**angular.json**中找到到一個projects物件,當中包含了二個專案的進入點,可以發現**projectType**屬性值來描述專案的性質,而最底下則有defaultProject屬性,在未指定要編譯的專案之前,angular會通過該屬性的值作為編譯的專案。
```json=
"projects": {
"@lib/firstlib": {
...
"projectType": "library",
},
"demoapp": {
...
"projectType": "application",
}
}
.
.
.
"defaultProject": "demoapp"
```
接著可以透過`ng build @lib/firstlib --watch` 並搭配`ng serve`指令進行開發。
>`ng build --watch`會觀察目前已存在的檔案;若是在開發的過程中有新增檔案,則需要重新執行指令。
## 擴展專案
專案之間是獨立的存在,但仍然可以使用`import`來互相參考,在開發時可以視為一般的專案來開發,若是要透過angular CLI來新增元件或建置專案,只需要加入`--project=[project-name]` 便可以在指定的專案下進行擴展,例如:
```
ng generate component firstlibcomponent --project=@lib/firstlib
```
CLI便會在**@lib/firstlib**專案底下加入firstlibcomponent 並且還很好心的修改firstlib.module將新增的componet元件匯出。
```typescript=
import { NgModule } from '@angular/core';
import { firstlibcomponent } from './firstlib.component';
@NgModule({
declarations: [firstlibcomponent ],
imports: [],
exports: [firstlibcomponent ]
})
export class FirstlibModule { }
```
另外還需要使用`public_api.ts`檔案來通知ng-packagr要匯出的模組
```typescript==
/*
* Public API Surface of harbor-core
*/
export * from './lib/harbor-core.module';
export * from './lib/harbor-core.model';
export * from './lib/ship/ship.module';
export * from './lib/firstlibcomponent';
```
## 建置類別庫專案
在建置專案的時候,同樣可以使用`--project=[project-name]`語法來指定要建置的專案:
>ng build --project=@lib/firstlib
>
ng-packagr會依照專案內的**package.json**來進行建置的動作。
```json=
{
"name": "@lib/firstlib",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^7.2.0",
"@angular/core": "^7.2.0"
}
}
```
這三個屬性的用途如下:
1. **name**
很顯然地,這是用於宣告library的名稱。當外部的元件需要引用這個library時,都需要透過`import {*} from '{library-name}'`,而**library-name**必須與**name**的設定一致。
2. **version**
對於library而言,version是很重要的一件事,開發者必須確保自已用的套件版本是否正確。
3. **dependencies**(**peerDependencies**)
這個部份包含了library所依賴的元件 ,npm 會依照描述在安裝Library的同時也自動下載依賴的元件。最後ng-packagr便會依照**ng-package.json**中的配置將編譯後的檔案放在指定的路徑。
## 包裝與參考
最後,我們需要將建置完成的專案打包成npm的套件並上傳到npm的套件資源庫。執行
```
ng build @lib/firstlib
cd /dist/@lib/firstlib
npm pack
```
順利的話,會產生一個`firstlib-0.0.1.tgz`的壓縮檔,若要測試包裝後的library是否可以正常使用,直接在方案目錄下使用`npm install`指令進行安裝,例如:
```
npm install ./dist/@lib/firstlib/firstlib-0.0.1.tgz
```
執行成功後會在**package.json**中發現下列依賴項目:
```json=
"dependencies": {
...
"@lib/firstlib": "file:./dist/@lib/firstlib/firstlib-0.0.1.tgz"
}
```
後續便可以在demoapp專案的**app.module.ts**中直接參考 @lib/firstlib進行開發。
```typescript=
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FirstlibModule } from '@lib/firstlib';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
FirstlibModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
```
而**app.component.html**
```htmlembedded=
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
<lib-firstlib></lib-firstlib>
</div>
```
## 發行
若是要發行包裝後的tgz檔到私有的倉庫,需要先設定 **.npmrc**的**registry property**,可以利用`npm config`指令來修改
npm config set registry http://dockerregistry.fhsit.com/npm/Npm
npm config get registry
http://dockerregistry.fhsit.com/npm/Npm
或
npm config edit //直接修改.npmrc
當設定完成後,npm將私有的registry作為預設值,便可以直接使用`npm publish`指令發行要公開的函式庫。
npm publish ./dist/@lib/firstlib/firstlib-0.0.1.tgz
最後,若是希望發行到外部的倉庫,可以參 考[這篇](https://blog.angularindepth.com/the-angular-library-series-publishing-ce24bb673275)
# 附錄B:建置佈署Angular的應用程式
雖然Angular 提供了`ng build`指令作為打包應用程式的指令,但若是要整合於Nginx的反向代理環境,需要再額外進行處理,一般的情況下,Angular + Docker + Nginx的文章已經相當豐富 ,但若是針對容器化後的網頁應用程式採用Nginx會有殺雞牛刀之嫌,本範例將採用[superstatic](https://github.com/firebase/superstatic)作為Web host。`superstatic`是一個輕量化的WebServer,支援HTML5 pushState模式,非常適合用在SPA的應用情境,搭配Docker使得網頁佈署的動作可以更加單純。
**Setp1 編譯Angular 應用程式**
用於Host的Image功能非常精簡,`superstatic`只需要負責派送應用程式所用的資源即可,Angular在編釋後會產生幾個必要的檔案以及引用到的圖檔,常見的包括:
- index.html
- runtime.js
- polyfills.js
- styles.css
觀察index.html後不難發現,瀏覽器會在取得文件後才向伺服端要求js以及css等資源,而資源的路徑會描述在index.html。所以在搭配Nginx的反向代理服務時,路徑的基底名稱必須一致才能透過Nginx的路由映射實際的路徑。 
上圖是反向代理的運作流程,`path`是這段URI的路徑名稱(每個URI可以有無限多且唯一的路徑),Nginx在收到`\path\`路徑的請求後會將連線轉拋到另一個URI以請求資源。首先請求的資源是`http://localhost/path/index.html`,再從index.html取得js檔案的URI,而這些js檔案的URI便是由angular 編譯後產生的,因此在編譯時就需要知道path的名稱。完整的指令為
ng build --prod --base-href /path/
如此一來,在index.html中就會產生`<base href="/path/">`來作為所有資源的前導路徑,
而網頁中引用的資源路徑則必須以相對路徑來表示,例如`<img src="assets/image/logo.png" />`。在編譯完成後會在根目錄下產生`dist/path`目錄,裡頭存放著編譯完成的檔案。
備註:superstatic.json是用於superstatic的設定檔,
**Setp2 製作Docker Image**
編譯完的程式放在根目錄下的*dist*路徑內,Dockerfile將編譯後的檔案完整複制到容器內。範例中利用node.js官方提供的映象檔作為基底映象檔。
```dockerfile=
FROM node:stretch-slim
## Install superstatic
RUN npm install superstatic -g
## Copy filies to /app
WORKDIR /app
COPY /dist/<project-name> .
## Run superstatic use 80 port
ENTRYPOINT superstatic --host 0.0.0.0 --port 80 --gzip $USE_GZIP --debug $LOG_REQUESTS
```
最後使用`docker build -t sample .`來建立映象檔,在使用時要記得容器的對外的埠為80 `docker run --rm -it -p 8080:80 --name myapp sample`,成功啟用之後在瀏覽器輸入`http://localhost:8080`便可以成功取得位於容器內的應用程式。
**Setp3 設定Nginx**
啟用Nginx並且修改`nginx.conf`檔案,新增一筆location且URI路徑需與index.html的`<base href="/">`元素一致(以本例來看,應該是`<base href="/path/">`)。
```yaml=
location /path/ {
proxy_pass http://<IP>:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
最後測試是否能透過Nginx 將實際對外的連線http://localhost/path/路徑透過反向代理路由到容器所在的位址http://localhost:8080,順利取得位於容器內的網頁。
# 附錄C:加入第三方應用程式
## DevExtreme
若要將DevExtreme套件加入Angular,最快的方式就是透過Angular Schematics。
>ng add devextreme-angular
上述的指令除了會自動執行`npm install --save devextreme devextreme-angular`之外,還在Angular.json檔內設定的CSS,執行完成後,可以在angular.json檔內發現自動添加下列內容
```json=
"styles": [
"node_modules/devextreme/dist/css/dx.common.css",
"node_modules/devextreme/dist/css/dx.light.css",
"src/styles.scss"
],
```
~~另外,還要安裝stream套件,執行`npm install --save stream`就可以正常編譯,後續可以參考DevExtreme的[教學文件](https://js.devexpress.com/Documentation/Guide/Getting_Started/Widget_Basics_-_Angular/Create_and_Configure_a_Widget/)來進行後續的步驟。另外還可以參考[這篇](https://www.npmjs.com/package/devextreme-angular)。~~
## Bootstrap 5
Bootstrap是個受歡迎的CSS套件庫,目前已經來到第五版。若想在Angular的環境下加入Bootstrap, 可以透過NPM來新增。
> npm install bootstrap@next
下載完成後,需要在angular.json手動添加參考。
```json=
"styles": [
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.scss",
],
"scripts": [
"./node_modules/bootstrap/dist/js/bootstrap.min.js"
]
```
>由於載入了第三方的CSS,若是在ng build出現下列錯誤,可以考慮修改Angular.json的設定來解決。
```json=
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "5mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
```
# 附錄D : 實務上在Dockerfile中的一些常見的配置
## 加入Proxy
```dockerfile=
FROM node:current-alpine
EXPOSE 4200 49153
# 設定代理環境變數(如果有的話)
ENV HTTP_PROXY=http://{IP}:{Port}
ENV HTTPS_PROXY=https://{IP}:{Port}
# 安裝angular/cli
RUN npm install @angular/cli
# 更改使用者為 node
USER node
```
## 修復npm的憑証
### 方法一:重新設定npm
```dockerfile=
FROM node:current-alpine
EXPOSE 4200 49153
# 更新 npm 設定
RUN npm config set registry http://registry.npmjs.org/ \
&& npm config set strict-ssl false \
&& npm config set cafile /etc/ssl/certs/ca-certificates.crt
# 修復 npm 證書問題
RUN apk --update add ca-certificates
RUN npm config set cafile /etc/ssl/certs/ca-certificates.crt
# 安裝angular/cli
RUN npm install @angular/cli
# 更改使用者為 node
USER node
```
### 方法二:允許不安全的憑証
```dockerfile=
FROM node:current-alpine
EXPOSE 4200 49153
# 安裝 npm 和 @angular/cli
RUN npm install -g --unsafe-perm=true --allow-root npm \
&& npm install -g --unsafe-perm=true --allow-root @angular/cli
# 更改使用者為 node
USER node
```