--- 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的路由映射實際的路徑。 ![enter image description here](https://lh3.googleusercontent.com/67ExmI1js2dAWnfsOj5ozhL1qQgzf_sohGJVNvDkPTKUAtXhHq18Dc9_tHcCEK7PDXVmAPu0DL0- "Reslove proxy") 上圖是反向代理的運作流程,`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`目錄,裡頭存放著編譯完成的檔案。![enter image description here](https://lh3.googleusercontent.com/plxdlZxnnHWto3QGDSYK8LVd_RZowjB5YOniFa6D4enEJkB0T-f5rDV5yay1KnOHUj6BElNelubt "dist") 備註: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 ```