OrderSystem
這是第一次參加IT邦幫忙鐵人賽30天
如果時間足夠會再額外實作放上GCP的系統整合
相信看到內容後就能大概知道為什麼標題會有Google好棒棒了!
就是因為大多都使用了Google的技術XD
哪天Google出Bot就真的整套Google了
預期接下來會撰寫的方式沒意外會如下表(可能會順序會進行調動)
在我們公司常常會有集體訂飲料,訂午餐的事件發生,以往我們都是用Mail發送問大家說要甚麼,然後透過Email回信(掉信)或是共同控制Excel(會有檔案鎖定問題),統整好後打電話通知店家或是透過第三方點餐系統(UBerEat、FoodPanda)點餐,而當餐點到了之後,更常發生的問題是
我點了什麼?
沒錯 最常發生的就是忘記自己點了什麼,常常可能很急著去開會或是開發中就可能先點隨意點個餐點,而開完會或是進入開發思緒過後,再回頭來看就會忘記自己點了什麼。
因此才有了這個想法,希望能透過BOT來做點餐系統,避免掉信或是檔案鎖定的問題,並且希望管理人員只要去我們設計好的網頁上說今天吃甚麼,開啟活動後,各自要得人員只需要自己去對BOT做溝通、點餐等等。
而當餐點到了之後,忘記自己點了什麼還可以透過此系統來做詢問或是查詢(對話紀錄)。
在這個系統裡面的角色架構圖會如下
整體時序圖會變成如下
大框架有了之後,接下來就要開始進一步分析後臺系統端使用者的需求
以及BOT端使用者的需求分析了。
前面有大致描述了此系統的目標,而本篇會針對後台的使用者進行需求上的分析。
先看看在還沒有此系統的世界,團購的團主會遇到什麼問題,在這裡簡單將團購事件的週期分成三部分並探討每段時期可能會遇到的問題
以上情境為個人憑空發想,更好的方式是融入“團主”生活,藉由訪談與體驗發掘更貼近使用者的需求。
我們的目標是從無到有產出一個團購系統,但如果要一次解決使用者所有的問題相當困難,所以在這次我們先挑出一些我們只想做的必要功能,簡單實作一個 alpha 版本,雖然只是個 alpha 版本,我們的目標還是要幫助使用者達到他們的目標,也就是
團主能順利開團,完成團購週期
所以,我們想一下團主需要哪些東西
透過以上步驟,釐清沒系統的情況下使用者(團主)到底遇到了什麼問題,再用這些問題去想出系統可以怎麼幫助使用者(團主)去解決問題,接下來會依今天列出的必要功能產出使用者故事。
產出使用者故事的目的在於,盡可能地重現人們需要使用軟體替他們做的那些事,也就是我們必須做出貼近現實的東西,才會有人用啊!以下會基於這個樣板來造樣造句:
作為<某類使用者>
我想<做某事>
這樣就能<創造出某些價值>
在上一篇文章想出基本必要的功能後,我們就可以大概想一下團購系統的後台應該有哪些具體的功能了,最後決定分成以下兩大類
在這項功能中,團主可以新增將來會拿來開團的商家,並為這些商家新增項目清單。
團主可以發起活動,透過伺服器發給前台的人,並可以查看所有活動狀態與訂購情形。
透過以上步驟,擠出了一些基礎的功能與使用者故事,這些故事可以當作待辦清單,用來檢視目前的開發進度,在後面將會利用這些簡單的故事,實作出我們預期的後台系統。
Chris Sims, Hillary Louise Johnson (2015) 敏捷與Scrum軟體開發速成。博碩文化
使用者可以主動詢問Bot可以提供什麼服務,此時Bot會依照預先設定的腳本回覆給使用者,使用者即可依照設定好的服務腳本進行雙向的溝通取得所需的服務。
當使用者依照服務的腳本點選按鈕或是依照指示輸入相關的資訊時,Bot會將收到的訊息與用戶等資訊轉交給後端的伺服器進行確認與執行對應的商業邏輯。
後端的系統依照收到的需求進行對應的商業邏輯,將處理的結果即時回傳給使用者知悉。
當後端系統有需要主動發送最新訊息給群組的使用者或是發送通知給特定使用者時,也會透過Telegram服務進行發送。
是指程式間隔一定時間透過 getUpdates(上面所使用的方法)取得用戶訊息,缺點是浪費資源、不夠即時,所以適合在程式還沒有 deploy,在 develop 和 test 階段時使用。
是指向 Telegram 設定一組 callback url,只要當用戶傳送訊息給你的 Chatbot,Telegram 就會把用戶訊息連同 metada 傳送到你設定的 url 。適合使用在程式已經 deploy,有自己 server 專屬 url 的 production 環境。
Client: Browser & Telegram App
Presentation Layer: Angular
Business Layer: Golang Restful API
Data Store Layer: MongoDB
Browser :
Firefox、Google Chrome、Microsoft Edge、Safari 與 Opera 是市場中主要的競爭者。過去十年來,行動裝置發展成為大部分使用者主要的上網方式。目前大部分使用者只使用行動瀏覽器與應用程式來上網。主要的瀏覽器也都為 iOS 及 Android 裝置提供行動版瀏覽器。雖然這些應用程式在特定用途上相當好用,但僅提供了受限的上網功能。
Telegram App :
簡單、小巧、速度、令人愉悅的程式語言!? 是這樣嗎?
目前看來,Go在中介軟體開發或是需要處理的邏輯密集度越高,Go的優勢會越明顯。在一些極高併發系統設計中,除了容易撰寫外搭配使用Redis進行架構設計,可以處理比如搶購的秒殺需求。
MongoDB 是一種文件導向的 NoSQL 資料庫系統 (document-oriented NoSQL database system)。主要使用 C++ 程式語言撰寫,並以 BSON(類似於 JSON 的格式)為其儲存資料結構的架構。
今天來介紹一下Go Lang的安裝方式
首先先到GO官網依據你的作業系統下載你要對應的檔案
安裝過後只要套入我們最愛的模式瘋狂的下一步即可…
打開我們的Terminal後 輸入以下指令
go version
如果有出現版本的話就代表安裝已經成功了
在你想要的資料夾中建立一個新的檔案叫做Main.go
所有Go的程式碼的附檔名都是go結尾 C#的話就是.cs
將以下程式碼放入檔案中
package main
import "fmt"
func main() {
fmt.Println("Hello, World")
}
在打開你的Terminal到你開啟的資料夾 執行以下指令進行編譯
go run main.go
執行以上指令後就可以在Terminal上看到你輸出的結果了
在這裡簡單的介紹一下以上程式碼的簡介
我們有看到幾個重點
這三個是在GO裡面最基礎的格式
相信有寫過JAVA的人一定不陌生(C#叫做NameSpace)
這其實意思是代表這個的檔案是隸屬於哪個Package(包、函式庫)
一個Package裡面可以擁有多個GO的檔案
在其他檔案裡我可以去參考別的Package 就可使用其中內的所有Public Func
參考到其他的套件或其他的Package
import其實有兩種寫法
import "fmt"
import "net/http"
import (
"fmt"
"net/http"
)
兩種都可以 只是看自己喜好即可
在Go的語言特性裡面有個特色 Function Name的首字的大小寫有強烈的代表性
首字的大小寫會代表著這個Function是否為Public
JAVA&C#為例
public void fucntionName(){
logic.......
}
Go
func FucntionName(){
logic.......
}
同理如果把FucntionName改成小寫的話
他就會變成Private了
未來建議建立專案都改用GoModule
go mod init <module name>
執行以上指令建立一個Module File (不填Module Name 會預設帶專案資料夾名稱)
今天就稍微簡單的帶過一下GO的語法特性
下一篇會再來更加詳細的介紹GO的變數命名方式
迴圈的用法以及方法的回傳方式
在昨天講了一個檔案裡最重要的幾個元件後
今天來講寫程式最基礎的變數宣告以及Function使用方法
Go的變數宣告方式有點特別
寫習慣C#或是Java的來看會覺得是鬼畫符
最簡單也最常見的宣告方式如下
這跟JavaScript宣告方式大致上一樣
var i = 1;
如果不透過型別他會自動在設定值的同時賦予型別上去(上述的例子就是自動變成int)
如果不給給值系統會給預設值
nil相當於物件的null 他本身也是一種型別
這跟第一種很類似 差異在於自定義型別
var x, y int = 3, 4
型別清單可以參考官網的文件
Go的特色還有可以一次宣告多個(你有20個變數 = 後面就可以有20個值)
這是Go最特別也是最潮的地方
x := 1
b := float64(3.51)
這樣的寫法可以連var都省略了 而且又讓寫別的語言的人看不懂
顯得自己很強
不過這也是大家最常宣告的方式 因為他可以直接接方法當作變數的承接值(後面會提到)
可以透過Go原生的套件reflect這個套件來查詢變數所代表的型別
package main
import (
"fmt"
"reflect"
)
func main() {
var i = 1
fmt.Println(reflect.TypeOf(i))
}
// OutPut => int
如昨天所提到的在Function裡面有個特性
首字大小寫代表Public或是Private
package main
import (
"fmt"
)
func main() {
PublicTest()
privateTest()
}
func PublicTest() {
fmt.Println("PublicTest")
}
func privateTest() {
fmt.Println("PrivateTest")
}
// OutPut => PublicTest
// OutPut => PrivateTest
依照上面這個例子 雖然兩個方法都有辦法成功執行
但今天如果其他的檔案參考Main這個函示庫
就會無法使用privateTest這個方法
package main
import (
"fmt"
)
func main() {
answer := add(1, 2)
fmt.Println(answer)
}
func add(x, y int) int {
return x + y
}
// OutPut => 3
package main
import (
"fmt"
)
func main() {
answer1, answer2 := add(1, 2)
fmt.Println(answer1)
fmt.Println(answer2)
}
func add(x, y int) (int, int) {
return x + y, x * y
}
// OutPut => 3
// OutPut => 2
package main
import (
"fmt"
)
func main() {
answer := add(1, 2)
fmt.Println(answer)
}
func add(x, y int) (z int) {
z = x + y
return
}
// OutPut => 3
寫了這麼多種寫法,在Go的世界裡都可以執行
差別在於團隊習慣的用法而已 只要溝通好就都能使用
所有程式碼的最基礎邏輯運算元
我想非If莫屬了
傳言只要會If Else就會寫程式了(誤)
在C#與Java裡面我們用的格式如下
if (條件式) {
條件成立的邏輯
} else {
條件不成立的邏輯
}
而在Go中 我們只需要將條件式的括號省略即可
if 條件式 {
條件成立的邏輯
} else {
條件不成立的邏輯
}
但是這裡有一個更具特色的寫法
package main
import (
"fmt"
)
func main() {
if i := geti(); i == 5 {
fmt.Println("i是5")
} else {
fmt.Println("i不是5")
}
}
func geti() int {
return 6
}
我可以在比較前去賦值 再進行邏輯的比較
特別注意else不可換行
Switch的特性跟If大同小異
switch {condition}{
case {Value1}:
//logic
case {Value2}:
//logic
default:
//logic
}
package main
import (
"fmt"
"runtime"
)
func main() {
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.\n", os)
}
}
package main
import (
"fmt"
"math/rand"
)
func main() {
i := rand.Intn(80)
fmt.Println(i)
switch {
case i < 30:
fmt.Println("index<30")
case i < 60:
fmt.Println("index<60")
default:
fmt.Println("index>60")
}
}
先在外部宣告好判斷的變數
透過Case去宣告判斷的範圍並執行
Defer有點特別 他是當"當前"的Function執行完的時候
才會開始執行的意思
package main
import (
"fmt"
)
func main() {
fmt.Println("Start")
test()
fmt.Println("End")
}
func finish() {
fmt.Println("Finish")
}
func test() {
defer finish()
fmt.Println("Test")
}
// OutPut
// ======
// Start
// Test
// Finish
// End
根據上面的範例Code 就可以知道它的原理了
defer也可以堆疊多次 他是Stack的概念
也就是所謂的後進先出 只要越後面宣告的 就會在結束時優先執行
GoLang的語法大致上就介紹到這樣了
For迴圈此專案目前沒用到就沒特別寫出來了
詳細可以到Go的官網去參訪一下
在Web的世界裡常見的通訊協定就兩種
而WebSocket在本次的範例中不會用到
在我們瀏覽器的操控下,其實我們打的網址按下Enter後
他做的就是打需求(Request)到伺服器(Server)取網頁資料回來
瀏覽器針對取的資料格式不同進行不同的呈現方式
例如:
瀏覽器上輸入http://www.google.com
按下Enter
瀏覽器就會透過HttpMethod的Get取回HTML並顯示在畫面上
回歸正題 在Go裡面已有人寫好HttpServer的套件
這裡就使用Go最常見的 gorilla/mux 來使用
他是基於原生'net/http'的擴充套件
輸入以下指令安裝套件(建議使用Go Mod 可參考 第七天的補充說明)
go get -u github.com/gorilla/mux
建立一個Router.go的檔案
負責管理所有Controller進來的路由
type Route struct {
Method string
Pattern string
Handler http.HandlerFunc
Middleware mux.MiddlewareFunc
}
func register(method, pattern string, handler http.HandlerFunc, middleware mux.MiddlewareFunc) {
routes = append(routes, Route{method, pattern, handler, middleware})
}
func NewRouter() http.Handler {
r := mux.NewRouter()
for _, route := range routes {
r.Methods(route.Method).
Path(route.Pattern).
Handler(route.Handler)
if route.Middleware != nil {
r.Use(route.Middleware)
}
}
handler := cors.Default().Handler(r)
return handler
}
func init() {
fmt.Println("Route Init")
register("GET", "/api", Hello, nil)
fmt.Printf("%+v", routes)
}
func Hello(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Hello World")
fmt.Fprintf(w, "Hello World")
}
這裡的mygo是當初在New Mod時所用的名稱
package main
import (
"fmt"
"net/http"
Router "mygo/router"
)
func main() {
fmt.Println("Start")
r := Router.NewRouter()
http.ListenAndServe(":3001", r)
}
在昨天建立好了HttpServer,但我們總需要一個地方來存取資料
而這次要用的儲存方式就是以文本存取為特色,並且不用下SQL的
在開發階段我們就偷懶一點點 使用Docker去安裝一個單台的MongoDB
首先需要安裝的有Docker For Windows(我是Windows版本)
安裝詳細的內容可參考官方教程
安裝好後右下角點開就會有一隻可愛的鯨魚在那邊游著噴水嗎?
安裝好後開啟終端機輸入以下指令
docker help
有跳出跟你說可以用的指令的話 就代表安裝成功了
再來是到Dockerhub下載MongoDB的Image
在終端機輸入
docker pull mongo
就會把Image下載下來了
下載完成後會如下圖
安裝好Image後 我們會需要把他Run起來成一個服務
首先先下個查詢ImageId的指令
dcoker images
依照指令的內容輸入ID的前四碼(通常四碼即可)
Docker run -d -p 27017:27017 {imageid}
-d 意思讓他起在背景執行
-p 後面是只要串接的Port {本機的Port}:{Docker的Port}
跑起來後可以安裝MongoCompass連線起來看
連線的位址就是localhost:27017
能連進去就代表成功啦
下一篇再來進行串接DB 就可以建立一個完整的API存入DB了
昨天把開發環境的MongoDB透過Docker架起來了
今天要把前面Go的Http與MongoDB進行整合並串接
製作成一個完整的API
我希望這次有的是店家的新增功能
資料格式有如下
StoreName: string
Addr: string
Alias: string
Phone: string
WebSite: string
Image: string
資料格式定義好了以後就是要來弄路由
在前天有製作了Router的檔案其中init是定義路由
今天新增一個POST的路由
register("POST", "/api/store", controller.CreateStore, nil)
並且新增一個資料夾叫controller(負責控制商業邏輯)
新增一個storeController.go
並開一個CreateStore的方法
package controller
import (
"OrderApi/model"
"OrderApi/services"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func CreateStore(w http.ResponseWriter, r *http.Request) {
setupResponse(&w, r)
b, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var store model.Store
err = json.Unmarshal(b, &store)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
services.SaveStore(store)
}
新增一個資料夾services並在其中新增一個mongoService.go
package services
import (
"OrderApi/model"
"context"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func SaveStore(store interface{}) {
fmt.Println("Save Store")
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://localhost:27017"))
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err = client.Connect(ctx)
defer func() {
if err = client.Disconnect(ctx); err != nil {
panic(err)
}
}()
database := client.Database("OrderAPI")
storeCol := database.Collection("Store")
storeCol.InsertOne(ctx, store)
}
這樣就設置完成整個從
Main=>Router=>Controller=>Service=>DB
資料流了
這樣就完成了一整個HttpServer 從Http發Request到落地的整個流程
前面已經有稍微提了HttpServer的第三方套件
以及判定型別的reflect
今天再來介紹其他GO語言中常用的幾個官方套件
fmt是一個官方將資料顯示於畫面的一個套件
我們以往在C#與Java於沒有介面的模式情況下
就會使用終端機介面的輸入來代表他有沒有正常運行到該段程式
或是有錯誤的時候會進行輸出錯誤訊息
C# Console.WriteLine()
Java System.out.Println()
Go fmt.Println()
列印時我們可以去定義他的Format為何
詳細可以參考官方文件
在操控的資料的時候也常常會有需要做時間戳記
最常用的就是
time.Now() //顯示現在的時間
相信有在寫演算法或是LeetCode的一定不陌生
math這個函式庫 不管是絕對值、n次方等等都會在數學的函示庫裡面
math.Pi //列印圓周率
前幾天介紹的httpServer某些也是基於net/http來實作完成的
前面完成了從外面打到Server端 這邊來講解從Sever端打到另外的Server如何操作
resp, err := http.Get("URL")
resp, err := http.Get("URL", url.Values{"key": {"Value"}, "id": {"123"}})
url.Values就是RequestBody的意思
未來再實作Bot的時候就會用到這個元件了
因為要主動打到TelegramServer去進行傳播給其他使用者
今天就大概介紹一些常用的套件(某種程度再偷懶)
下一篇就來介紹後台管理的Angular了
在網路上已經有許多的環境建置文章,在這裡還是依照慣例來個 Angular 的 Hello World!(提醒:這裡介紹的是 Angular 2 以上的版本,而非 AngularJS)
安裝 Angular CLI 可以幫忙我們以下的事情
接下來開始建置開發環境吧!安裝 Angular cli 之前,需要確保你的電腦有 Node.js 與 npm 套件管理器。
通常在 Node.js 官網直接在下載 LTS 版的就可以。而在 Angular 的官網中,有提供此連結來參考當下的版本需求。
別擔心,我們不需要寫 Node.js
基本上將 Node.js 安裝完後, npm 套件管理工具就一起安裝好了,可以用以下指令來檢查安裝版本。(參考開啟 command-line 方式)
node --version
npm --v
安裝以上兩個工具後,就可以開始安裝 Angular CLI 了。用 npm 指令來安裝最新版本的 Angular CLI
npm install -g @angular/cli
安裝完後,輸入已下指令確認版本
ng --version
環境安裝好後,開啟新的 cmd ,輸入
ng new project1
接者會詢問你否加入 Angular routing
? Would you like to add Angular routing? (y/N)
這裡可以選就算選擇 N 在開發階段也可以用手動方式加入 Angular routing ,所以選哪個都可以
接著問你想用哪種樣式表,這裡選擇 SCSS,有關這些預處理器的資訊這邊就描述了
Which stylesheet format would you like to use? (Use arrow keys)
> CSS
SCSS [ http://sass-lang.com/documentation/file.SASS_REFERENCE.html#syntax ]
Sass [ http://sass-lang.com/documentation/file.INDENTED_SYNTAX.html ]
Less [ http://lesscss.org ]
Stylus [ http://stylus-lang.com ]
Angular CLI 就會幫你在 project1 目錄下自動產生應用程式的骨架與一些檔案,並安裝一些必要的相依檔案。
在 cmd 輸入
cd project1
npm start
就可以啟動第一個應用程式了!
微軟推出的編輯器,非常好用。
為什麼好用?可參考新一代的編輯器 — VSCode這篇文章。
這邊推薦 Will 保哥 整理出來好用的 Angular 相關套件,只要安裝一個套件,就能把其他的套件一起安裝進來。
在編輯器左邊點擊方塊圖示,並搜尋 angular
下載 Angular Extension Pack,有興趣的人可以仔細看看各個套件的功能
此為 chorme 的擴充功能,可以在 chrome 線上應用程式商店下載到。 Augury 可以幫助開發者分析 Angular 程式頁面中所用元件的狀態與方法。
大致說明了一些開發上會需要用到的工具,接下來會介紹 Angular 的世界裡會有哪些角色,這些角色有哪些職責,他們是如何合作建構出一個應用程式的。
大概介紹完了 Angular 的建置與開發後,我們來看看 Angular 的世界包含了哪些角色,在今天先向大家介紹模組。
在日漸複雜的前端領域,模組化的設計概念是非常重要的,簡單的說,被封裝在模組裡面的程式不會污染其他模組或全域,是透過公開的介面給外界引用。Angular 的程式也是模組化概念的,一個 angular module 可能包含了某種功能(像是商家管理)或整個程式會重複使用的邏輯。以下介紹本專案開發時會使用的三類模組:
如同其名,是依功能自成一個模組,這類模組可能會有自己的 Routing Module (管理路由的模組),可以匯入 App Module(根模組)。我們的專案中會有商家、商品、活動與 Layout 模組,其中前三的都會有各自的商業邏輯與畫面邏輯,而 Layout 模組比較不一樣,他的目的僅僅是提供畫面的 component 給 App Module 使用,所以不需要路由的配置。
包含一些單例的(singleton) services,通常是整個專案通用的 services ( LoggingService, ErrorService, DataService),或是 Auth Interceptors,在我們的專案裡面有一個用來深拷貝物件的 service。注意:core module 不可以匯入至除了跟模組以外的地方,避免在 @NgModule() 配置的 services 變得不是單例的。
補充:Angular 的官網文件有特別提供防止多次引用 module 的寫法,你說有哪家可以把文件寫得那麼仔細的啊,一定要推一下!
包含一些可能是會重複使用的 components, pipes, directives (calendarComponent, AutoCompleteComponent),或是一些第三方的套件。本次專案要貫徹 Google 好棒棒的原則,用了很多Angular material 套件來實作,這些套件將匯到 shared module 裡,之後需要用到的其他 modules 再匯入 shared module 即可。
今天講了一下模組的概念,以及我們的專案大概會怎麼樣切分模組,各個模組都會有對應的職責,明天在真的實作一遍,來感受一下今天所說明的部分。
上一篇文章有提到一些這次專案會用到的模組,本篇簡單把他實作一遍。
用 terminal 開啟該專案的資料夾後,輸入以下指令就可以馬上建立出我們要的功能模組,其中前三項因為加了 --routing
,所以會把路由模組一起建立出來
ng g m merchant --routing
ng g m product --routing
ng g m activity --routing
ng g m layout
由下圖可以看到,Angular CLI 會幫我們建立好模組需要的檔案與資料夾
聰明如你,一定知道要怎麼再新增一個模組,這裡要特別注意的是 core module 不需要路由模組的,所以輸入下面指令
ng g m core
此外,只能被 app module 所引用,所以我們要在 core.module.ts 中的 class 加入以下程式碼
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'Core is already loaded. Import it in the AppModule only'
);
}
}
}
在這裡可以注意到@Optional()
@SkipSelf()
兩個裝飾子,
@Optional()
代表著這個依賴項是可選的,如果沒有找到 ElementInjector,就也不會報錯。
@SkipSelf()
代表除了自己以外的 ElementInjector,再向上找父級的ElementInjector,而不是當下的。
組合在一起的意思大概就是,如果我在其他的子模組(有可能是功能模組)匯入此模組時,要往上找上一層級的模組(app module)是否有 core module,如果沒有就沒事,有的話就會拋出錯誤。(主要是針對懶載入的模組匯入 core module 時才會拋出錯誤)
shared module 也是一樣用一下指令,也不需要路由模組
ng g m shared
在這裡我會在 shared module 裡再建立 material module,這麼模組主要管理 material 套件
cd ./src/app/shared
ng g m material
Material 的網站有詳細說明如何安裝,這邊快速用以下指令來把它加到專案裡
ng add @angular/material
這個指令會幫我們設定顏色的主題、字體、動畫等等,並很自動的幫我們修改 app.module.ts、angular.json、src/index.html、src/styles.scss 檔案,省去我們匯入一些模組與 css 的步驟。
這裡先將我們專案可能會用到的模組通通會進這個模組裡,除了加到 @NgModule 裡的 imports 以外,記得還要加到 exports,這樣引用這個 material module 的模組才能使用套件的東西。
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatChipsModule } from '@angular/material/chips';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatMenuModule } from '@angular/material/menu';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { MatExpansionModule } from '@angular/material/expansion';
@NgModule({
imports: [
MatButtonModule,
... 省略
MatExpansionModule,
],
exports: [
MatButtonModule,
... 省略
MatExpansionModule,
})
shared module 裡面的東西大多是給其他功能模組使用的,所以一些通用的東西(表單等等…)在這裡先匯入後,其他有匯入 shared module 的模組就不需匯入了
import { MaterialModule } from './material/material.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [],
imports: [CommonModule, MaterialModule, FormsModule, ReactiveFormsModule],
exports: [CommonModule, MaterialModule, FormsModule, ReactiveFormsModule],
})
export class SharedModule {}
模組實作的重點在這裡大概說完了,直接看文章實在很難馬上理解,所以可以搭配在 github上程式碼會更好懂,接下來要講 Angular Component 的部分了。
對於寫過 React 來說的人一定對 component 不陌生,在 Angular 一樣有這樣的概念存在,而且可以說是組成 Angular app 的核心。一個畫面可以由多個 Component 共同組合成,而且這些 Component 可能有機會一直被重複使用。另外,依照 component 的職責,還可把他去區分成 Container components 與 Presentational components。
在比較傳統的前端開發方式,很習慣以整個網頁的思考方式寫程式。如果以 component 的方式思考,一開始會把 UI 拆解成 component 層級,以我們將開發的後台系統為例:
可大致將畫面拆成這 5 個部分,當然每個部分都還有可能可以拆的更小,像是 sidenav 就可能可以在拆成 1 種按扭 component。該怎麼判斷是否拆得夠小呢?可以仔細檢視該 component 是否只負責做一件事情,如果做很多事情,就可能是他又要拆解的時候了!
在把這些 component 組合在一起的時候,會產生樹狀的結構。以剛剛的 main 為例,他的內部有 product-list、edit-product 兩種 component。
當專案較複雜和 component 切得比較細的時候,component 樹的結構會很容易很多層,這時候職責與狀態的管理就會非常的重要,若子層的 component 直接去修改到父層狀態,後續的維護會很難追蹤,而且這樣的子層 component 會很難重複使用。比較好的方式是把 component 區分成 Container components 與 Presentational components。而 Container components 通常有以下特性:
至於 Presentational components 只要關注在呈現 UI 與將使用者事件委派給上層 components 處理就好了。
網路上有許多介紹 Angular component 的文章,本篇簡單介紹 component 的思考方式,若一開始沒搞懂,很容易寫出很多非常相似的 components 又混雜多種關注的邏輯,這樣就失去 component 能被重複使用的優勢了。下一篇將會帶來一些實作,讓這些概念更加清楚。
本篇會介紹如何建立 component,以及用一些實際的功能說明上一篇的概念
在實作 modules 的那篇,有建立了 layout module 了。以下指令的 1 ~ 3 行是到 layout 的資料夾下,建立 containers、components 資料夾,用來準備放置 Container components 與 Presentational components
cd ./layout
mkdir containers components
cd ./containers
ng g c layout
cd ../components
ng g c nav
ng g c header
ng g c sidenav
ng g c footer
若其他模組想要用到 layout module 的 component,需要先把這些 component 加到 @NgModule
的 expoorts
(11 行),再以模組為單位加到要用到 component 的 @NgModule
imports
(24 行)
// layout.module.ts
@NgModule({
declarations: [
LayoutComponent,
NavComponent,
HeaderComponent,
SidenavComponent,
FooterComponent,
],
imports: [SharedModule],
exports: [LayoutComponent, NavComponent],
// LayoutComponent 是 Container components ,他裝著HeaderComponent, SidenavComponent, FooterComponent
// NavComponent 將會直接給 appComponent 所使用
})
export class LayoutModule {}
// app.module.ts
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
LayoutModule, // 匯入 LayoutModule
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
再來我們可以直接在 app.component.html,加入 來自 layout module 的 components
本系列不介紹 css ,所以 css 的部分可以直接查看 styles.scss 或 各個 component 的 scss 檔案
<!-- app.component.html -->
<app-nav class="mat-elevation-z6"></app-nav>
<app-layout></app-layout>
回到 layout module 的 components,我們先實作 nav component,第二行是 material 套件的按鈕,css 就到今日程式碼裡面查看囉
<!-- nav.component.html -->
<nav class="nav-header">
<a mat-flat-button color="primary" >My Group Buying System</a>
<div class="flex-spacer"></div>
</nav>
這一步就要開始注意了,我們把 layout component 當作 Container,所以要把其他還沒用到的 component 放進來,並更新每個 component 的內容(包含 ts、html、scss 三個檔案)。其中比較值得一提的是第 2 行,透過 Property binding
把值傳給 header component,在 header component 也要設定 @Input() 來去接收這個值 (header.component.ts 第 8 行)
<!-- layout.component.html -->
<app-header [headText]="'測試標頭'"></app-header>
<app-sidenav></app-sidenav>
<app-footer></app-footer>
<!-- header.component.html -->
<header class="primary-header component-page-header">
<h1>{{headText}}</h1>
</header>
<!-- sidenav.component.html -->
<div class="viewer-nav">
<div class="viewer-nav-content">
<mat-selection-list [multiple]="false">
<mat-list-option class="list-item">商家菜單</mat-list-option>
<mat-list-option class="list-item" >團購活動</mat-list-option>
</mat-selection-list>
</div>
</div>
<!-- footer.component.html -->
<footer class="footer">
<div class="footer-list"></div>
</footer>
// header.component.ts
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
@Input() headText;
}
下一篇會開發商家管理的功能,這個功能會出現在下圖 main 的區域
所以下調整一下 layout component 的 html 與 css,因為還沒有講到路由,所以先暫時用 ng-content
來投射(第 5 行)的方式來代替
<div class="sidenav-inner-content">
<app-header [headText]="'測試標頭'"></app-header>
<main class="sidenav-body-content">
<app-sidenav></app-sidenav>
<ng-content></ng-content>
</main>
<app-footer></app-footer>
</div>
在這次的練習中大概可以看到 component 的樹狀結構( app > layout > header、sidenav、footer),也帶有一些 Container components 與 Presentational components 的溝通( layout > header,用 @Input()
傳值 ),下一篇將會再帶出如何用 @Output()
溝通。
今日練習的程式碼可參考這裡。
本篇延續上篇的實作 components ,用商家管理的功能來說明 Container components 與 Presentational components 之間用 @Input()
@Output()
溝通
與上一篇大同小異,不一樣的地方是在這裡新增了 models 的資料夾與 model 的檔案 (第 3、16、17 行),需要 model 是因為我們實作的功能會有有關商家的 Data,我們要定義商家這個型別有什麼屬性,讓我們在開發上能用到 TypeScript 帶來的好處。
cd ./merchant
touch index.ts
mkdir components containers models
cd ./containers
ng g c merchant-list
cd ../components
ng g c merchant-item
ng g c merchant-edit
cd ../models
ng g class merchant --type=model
將 merchant-list component 與 merchant 模組匯出與上一篇類似,這裡就不一一說明,如果不太知道怎麼做可以看今日程式碼
再來是修改一下 app.component.html 檔案,把 app-merchant-list
加上去
<app-nav class="mat-elevation-z6"></app-nav>
<app-layout>
<app-merchant-list></app-merchant-list>
</app-layout>
這裡為 Merchant 的 class 定義裡了這些屬性,分別是識別碼、名稱、商家地址、商家電話、商家網站、商家 LOGO
export class Merchant {
id: string;
name: string;
adress: string;
phone: string;
website: string;
logo: string;
}
依照剛剛在產生這 component 放的資料夾位置,大家應該可以猜到他會被定義成一個 Container component。在這我會先給他儲存狀態與操作狀態的職責(將來這個狀態會交給 service),然後給這個 class 一些屬性與方法,分別為 merchant(各個商家的資料)、openEditModal(打開編輯商家資訊的跳窗)、createMerchant(新增新增商家)、updateMerchant(更新商家的資訊)、deleteMerchant(刪除商家),這次只先實作刪除商家。
// ... 省略
export class MerchantListComponent implements OnInit {
merchants: Merchant[] = fakeMerchants;
constructor() {}
ngOnInit(): void {}
openEditModal(mode, merchantId?): void {
console.log('mode', mode);
console.log('id', merchantId);
}
createMerchant(merchant): void {}
updateMerchant(merchant): void {}
deleteMerchant(merchantId): void {
this.merchants = this.merchants.filter(
(merchant) => merchant.id !== merchantId
);
}
}
merchant-list.component.html 中,因為畫面要顯示每個商家個別的資訊,所以要加入 app-merchant-item
這個 component,並用 Property binding (第 4 行)與 Event binding (第 5、6 行)的方式讓 components 溝通 (稍後再說明如何實作)
第 2 行的 *ngFor 為內建的 Structural directives,會依照模板(第 2~8 行)與屬性綁定的數據 (merchants) 建立一到多個物件
<div class="category-list">
<div *ngFor="let merchant of merchants" class="category-list-item">
<app-merchant-item
[merchant]="merchant"
(openModal)="openEditModal('edit', $event)"
(deleteItem)="deleteMerchant($event)"
></app-merchant-item>
</div>
<div class="category-list-item add-button" (click)="openEditModal('create')">
<p>+ 新增店家</p>
</div>
</div>
merchant-item 是一個 presentational component ,他的職責在顯示 UI 與把使用者事件傳出去,在他的 class 雖然有 editMerchant、deleteMerchant 的兩個方法,實際上沒有實作的邏輯,還是藉由事件的方式把 merchantId 傳出去(第 4、5、12、16 行);而這裡的 merchant 屬性(第 3 行)則是由父 component 傳值進來的。 html 與 css 這邊就不再多說明了,主要是一些畫面呈現與使用 material 套件的技巧。
// ... 省略
export class MerchantItemComponent implements OnInit {
@Input() merchant: Merchant;
@Output() editItem = new EventEmitter<string>();
@Output() deleteItem = new EventEmitter<string>();
constructor() {}
ngOnInit(): void {}
editMerchant(merchantId?: string): void {
this.editItem.emit(merchantId);
}
deleteMerchant(merchantId: string): void {
this.deleteItem.emit(merchantId);
}
}
components 的概念就介紹到這裡,這是今天的程式碼。題外話,對於完全沒接觸過 Angular 的人來說可能會有點難消化,不過說實在,Angular 從零開始教學的類似主題已有很多厲害的大大整理出來過了,這邊想透過另一種方式來介紹 Angular 的各個角色。再來下一篇會介紹 service 是做什麼用的。
Angular Services 基本上依然是個 class,他常常是有明確職責定義的,有可能是與後端要數據、驗證使用者的輸入或是 log 服務。相較於先前提到的 Components,Services 較注重可共用且 UI 沒那麼直接相關的邏輯。所以 Services 通常可提供 method 或可共用的暫存資料給不同的 Component 使用。
對應用程式來說依賴注入 (DI) 是很重要的設計模式。在 Angular 中有他自己的 DI 架構來提高程式的模組化程度。被依賴的那個東西有可能是我們剛剛提到的 service ,且可以藉由調整 provider 的參考替換掉被依賴的那個東西。
我們在 merchant 資料夾與 core 資料夾建立名為 service 的資料夾,並使用 Angular CLI 產生 service 的骨架(第 1~3 行)。之後在安裝深拷貝物件的套件,在 cloner service 會使用到。
ng g s services/merchants
cd ../core
ng g s services/cloner
npm i --save clone
這個 service 的用途在於對物件進行深拷貝產生 immutable objects,而 immutable objects 對開發上與應用程式效能上有什麼好處這邊就不說明了,有興趣可參考這篇文章。
cloner sevice 本身是個 class ,在這提供 deepClone 的方法,可提供給注入這個 service 的 class 使用。在程式碼的第 4~5 行,是 Angular 的裝飾器,用來定義 service, providedIn: 'root'
是宣告這個 service 應該在 root application injector 的層級被建立,詳細可參考官方文件。
import { Injectable } from '@angular/core';
import * as clone from 'clone';
@Injectable({
providedIn: 'root',
})
export class ClonerService {
constructor() {}
deepClone<T>(value: T): T {
return clone<T>(value);
}
}
merchants service 目標是處理商家的狀態,所以會有它會提供以下方法
getMerchants、getMerchantById、createMerchant、updateMerchant、deleteMerchant,實作邏輯的部分我自己用 immutable objects 更新 merchants$ 這個 BehaviorSubject,利用數據流的方式暫存商家狀態,並在更新時推送給有訂閱的地方。
在第 12 行,注入剛剛建立的 cloner service,就可以在這個 class 內使用了。
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ClonerService } from 'src/app/core/services/cloner.service';
import { Merchant } from '../models/merchant.model';
@Injectable({
providedIn: 'root',
})
export class MerchantsService {
merchants$ = new BehaviorSubject(fakeMerchants);
constructor(private clonerService: ClonerService) {}
getMerchants(): Observable<Merchant[]> {
return this.merchants$.asObservable();
}
getMerchantById(id): Observable<Merchant> {
return this.merchants$.pipe(
map((val) => val.find((merchant) => merchant.id === id))
);
}
createMerchant(merchant): void {
let updatedMerchants = this.clonerService.deepClone(
this.merchants$.getValue()
);
updatedMerchants = [
...updatedMerchants,
{ ...merchant, id: (+new Date() + Math.random()).toString() },
];
this.merchants$.next(updatedMerchants);
}
updateMerchant(merchant): void {
const updatedMerchants = this.clonerService.deepClone(
this.merchants$.getValue()
);
const merchantIndex = updatedMerchants.findIndex(
(merchantData) => merchantData.id === merchant.id
);
updatedMerchants[merchantIndex] = merchant;
this.merchants$.next(updatedMerchants);
}
deleteMerchant(merchantId): void {
let updatedMerchants = this.clonerService.deepClone(
this.merchants$.getValue()
);
updatedMerchants = updatedMerchants.filter(
(merchantData) => merchantData.id !== merchantId
);
this.merchants$.next(updatedMerchants);
}
}
這時將 merchants service 注入到 merchant list component (第 8 行),並將 createMerchant、updateMerchant、deleteMerchant 交給依賴去實作,component 只要把參數傳出去就好了。
component 的屬性改成 merchants$,是一個 Observable ,且在 component 的 template (用 async pipe)有做訂閱,所以當 merchants 有變化時,畫面就會依數據改變。(詳細程式看今日程式碼)
// ...省略
export class MerchantListComponent implements OnInit {
merchants$: Observable<Merchant[]>;
constructor(private merchantsService: MerchantsService) {}
ngOnInit(): void {
this.merchants$ = this.merchantsService.getMerchants();
}
openEditModal(mode, merchantId?): void {
console.log('mode', mode);
console.log('id', merchantId);
}
createMerchant(merchant): void {
this.merchantsService.createMerchant(merchant);
}
updateMerchant(merchant): void {
this.merchantsService.updateMerchant(merchant);
}
deleteMerchant(merchantId): void {
this.merchantsService.deleteMerchant(merchantId);
}
}
這邊只是很簡單的介紹 service 與依賴注入,事實上 Angular 的依賴注入非常強大、彈性很高,有興趣了解的人一定要讀官方文件的介紹,這邊因為專案還沒複雜到那種程度所以就沒詳細解說了,今天的程式碼在這裡。下一篇要講 Angular 的表單,用使用者的資料創建新的商家資訊或更新商家資訊。
上一篇在實作 merchant service 時,有人應該注意到已經把新增、更新商家的方法寫出來了,現在就是要用 Angular 的內建表單把它們串起來,利用表單取得使用者提供的資料,之後更新商家清單的資訊。
主要是透過 Template (component 的 html 檔) 中, element 加上 Directive 的方式去建立或更新 data ,較常用在比較簡單的表單,但彈性較低,本次範例就是以此表單來演練
在 component 的 ts 檔實體化 form 的 model,相對來說更具擴展性、重用性與可預測性。
詳細的介紹可在參考官方文件
這裡使用 Material 的 Dialog ,所以需要在 merchant-list.component.ts 注入 MatDialog, 並修改 openEditModal 的方法,他負責開啟 Dialog,在這次的範例裡他可能是新增商家資訊或編輯商家資訊。在第 23、29 行是開啟 Dialog 的方法,open 方法第 1 個參數是要參考的 Dialog component、第 2 個參數是給 Dialog 的組態物件。
第 35 行之後,是當 Dialog 關閉時會傳出型別為 Merchant 物件,要傳出什麼樣的 data 將會在 Dialog component 中設定。之後再依照開啟 Dailog 時的模式(新增或編輯),呼叫之前已經實作的 createMerchant 或 updateMerchant 方法。
如果版本在 Angular 9 之前,需要再 module 中把 Dialog component 加到 entryComponents 陣列中。參考這裡
// ...省略
export class MerchantListComponent implements OnInit {
merchants$: Observable<Merchant[]>;
constructor(
private merchantsService: MerchantsService,
public dialog: MatDialog
) {}
ngOnInit(): void {
this.merchants$ = this.merchantsService.getMerchants();
}
openEditModal(mode, merchantId?): void {
let dialogRef;
const modalComponent = MerchantEditComponent;
const modalWith = '500px';
if (merchantId) {
this.merchantsService
.getMerchantById(merchantId)
.pipe(take(1))
.subscribe((merchant) => {
dialogRef = this.dialog.open(modalComponent, {
width: modalWith,
data: { mode, merchant },
});
});
} else {
dialogRef = this.dialog.open(modalComponent, {
width: modalWith,
data: { mode },
});
}
dialogRef.afterClosed().subscribe((merchant) => {
if (!merchant) return;
switch (mode) {
case 'create':
this.createMerchant(merchant);
break;
case 'edit':
this.updateMerchant(merchant);
break;
}
});
}
// ...省略
}
在前面已經有建立 merchant-edit.component 了,我們現在將要做一些調整,目標如下
在 class 裡,需要 merchant 屬性(第 3 行),這將會是 Template-driven forms 要綁定的 model。然後注入 MAT_DIALOG_DATA
,從此物件中取得傳來的 merchant(第 7 行),這裡要注意的是傳來的 merchant 是 by reference 的 object ,所以這裡需要做做深拷貝(第 13 行),避免在更動表單資訊時,同時改到外部 componet 的狀態。
// ...省略
export class MerchantEditComponent implements OnInit {
merchant = new Merchant();
constructor(
public dialogRef: MatDialogRef<MerchantEditComponent>,
@Inject(MAT_DIALOG_DATA) public data,
private clonerSevice: ClonerService
) {}
ngOnInit(): void {
if (this.data.merchant) {
this.merchant = this.clonerSevice.deepClone(this.data.merchant);
}
}
onNoClick(): void {
this.dialogRef.close();
}
}
Template-driven forms 的重點在於 element 的 Directives,我們把表單層級(ngForm)的模板指定給名為 merchantForm 的 Template reference variables(第 3 行),以及每個 field 層級 (ngModel) 的模板指定給對應名稱的 Template reference variables(第 12 行),這裡的用途在於可以在 template 中取用該物件,像是在顯示驗證訊息時(第 14 行)。每個 input 需定義 name (第 11 行) 且需要用 [(ngModel)]
綁定對應 model (剛剛在 class 裡的 merchant)(第 12 行)。
最後在關閉 Dailog 時,要利用[mat-dialog-close]
directive 把使用者已經填好的資訊,且通過表單驗證後的資料傳出去(第 77 行)。
<h1 mat-dialog-title>店家資訊</h1>
<div mat-dialog-content>
<form #merchantForm="ngForm">
<div>
<mat-form-field>
<mat-label>店名</mat-label>
<input
matInput
required
[(ngModel)]="merchant.name"
name="name"
#name="ngModel"
/>
<mat-error *ngIf="name.invalid">{{ "此欄位必填" }}</mat-error>
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>地址</mat-label>
<input
matInput
required
[(ngModel)]="merchant.adress"
name="adress"
#adress="ngModel"
/>
<mat-error *ngIf="adress.invalid">{{ "此欄位必填" }}</mat-error>
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>電話</mat-label>
<input
matInput
required
[(ngModel)]="merchant.phone"
name="phone"
#phone="ngModel"
/>
<mat-error *ngIf="phone.invalid">{{ "此欄位必填" }}</mat-error>
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>網站</mat-label>
<input
matInput
required
[(ngModel)]="merchant.website"
name="website"
#website="ngModel"
/>
<mat-error *ngIf="website.invalid">{{ "此欄位必填" }}</mat-error>
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>圖片</mat-label>
<input
matInput
required
[(ngModel)]="merchant.logo"
name="logo"
#logo="ngModel"
/>
<mat-error *ngIf="logo.invalid">{{ "此欄位必填" }}</mat-error>
</mat-form-field>
</div>
</form>
</div>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()">取消</button>
<button
[disabled]="!merchantForm.valid"
mat-raised-button
color="primary"
[mat-dialog-close]="merchant"
>
儲存
</button>
</div>
因為本篇開發的表單邏輯並不複雜,所以使用 Template-driven forms 方式達到目的,可參考完整程式碼,下一篇將會介紹 Angular 的路由機制。
路由在網頁是非常常見的功能,在 Angular 也有屬於自己的路由系統 (Angular 真的好多功能都是內建!)。Angular 路由系統是藉由 component 的替換,來決定當下畫面要顯示什麼。Angular 是屬於 SPA (Single Page Application),所以換頁的動作會在前端完成,在這裡並不會跟伺服器端請求。
接續上次的程式碼,我們需要另外的 component 讓我們可以換頁,如下圖所示,目標是希望點擊管理菜單後(藍色框框),可以轉跳到項目清單的頁面,但這裡先不真的實作項目清單的細節。
所以我的在之前建立的 product module 的資料夾下,輸入以下指令,建立需要的資料夾與 component (這些資料夾的用處可參考之前的文章)
cd ./src/app/product
mkdir containers components services models
touch index
ng g c ./containers/product-list
由於我們預計項目清單的功能可能會越來越複雜,所以在這裡設計這個模組會是懶載入。首先開啟 product-routing.module.ts 檔案,設定路由的路徑 (第 5 行),以及對應的 component (第 6 行),與設定提供給當前路由的 data 物件的 Observable (第 7 行,用來顯示在 header 的標題,後面會再提到)
// product-routing.module.ts
// ...省略
const routes: Routes = [
{
path: '',
component: ProductListComponent,
data: { moduleName: '項目清單' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProductRoutingModule {}
在 merchant-routing.module.ts 裡也是一樣的設置方式
// merchant-routing.module.ts
// ...省略
const routes: Routes = [
{
path: '',
component: MerchantListComponent,
data: { moduleName: '商家菜單' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class MerchantRoutingModule {}
而在 app-routing.module.ts 就比較不一樣了。在 AppComponent 我們會先載入 LayoutComponent (第 6 行),在 LayoutComponent 裡面去置換 merchant module 與 product module 裡的 component,這裡的語法是懶載入的寫法,兩個 module 的路由在剛剛已經定義好了(第 9~12 行與第 16~18 行),此外,第 20、23 行是萬用字元路徑,就是當沒有匹配路徑時,會重新導向到指定路徑。
因為懶載入的關係,先前匯入 AppModule 的 MerchantModule 可以拿掉了
// app-routing.module.ts
// ...省略
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{
path: 'merchant',
loadChildren: () =>
import('./merchant/merchant.module').then(
(mod) => mod.MerchantModule
),
},
{
path: 'product/:merchantId',
loadChildren: () =>
import('./product/product.module').then((mod) => mod.ProductModule),
},
{ path: '**', redirectTo: 'merchant' },
],
},
{ path: '**', redirectTo: '' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
router-outlet
當路徑都設好後,接下來要改 html 的部分,就是利用 router-outlet
tag 在 html 內排版。我們先修改 app.component.html,這裡到時候會帶入剛剛設定的 LayoutComponent
<app-nav class="mat-elevation-z6"></app-nav>
<router-outlet></router-outlet>
所以,我們在往 layout.component.html 設定,在這裡也是利用 router-outlet
加到在 html 內,另外還用 Template reference variables 存入 outlet 物件(第 6 行),與用 activate 事件綁定,當路由在轉換時會呼叫 setModuleName (第 7 行)
<div class="sidenav-inner-content">
<app-header [headText]="moduleName"></app-header>
<main class="sidenav-body-content">
<app-sidenav></app-sidenav>
<router-outlet
#routerOutlet="outlet"
(activate)="setModuleName(routerOutlet)"
></router-outlet>
</main>
<app-footer></app-footer>
</div>
setModuleName 方法,會傳來把 outlet 物件裡的 data (剛剛在上面設定 product 與 merchant 路由路徑時帶的 data 物件)
,指定給 moduleName 屬性,由於 HeaderComponent 有屬性綁定 moduleName,所以當畫面轉換時, Header 會依 data 的值一起改變。
// layout.component.ts
// ...省略
export class LayoutComponent implements OnInit {
moduleName;
constructor() {}
ngOnInit(): void {}
setModuleName(outlet: RouterOutlet): void {
const moduleName = outlet.activatedRouteData.moduleName;
this.moduleName = moduleName;
}
}
這裡的 LayoutModule 之前沒匯入 RouterModule,因為會用到
router-outlet
所以需要匯入。
routerLink
,觸發轉跳頁面接下來要修改 merchant-item.component.html,在管理菜單的按鈕加上routerLink
,並提供相對於現在位置要到達到路徑(第 5 行)。5這樣就大功告成了。
<!-- ...省略 -->
<mat-card-actions>
<a
mat-stroked-button
[routerLink]="['..', 'product', merchant.id]"
(click)="$event.stopPropagation()"
>
管理菜單
</a>
<button
mat-icon-button
class="card-delete-btn"
(click)="deleteMerchant(merchant.id)"
>
<mat-icon>delete</mat-icon>
</button>
</mat-card-actions>
<!-- ...省略 -->
今天說完了 router 的概念了,也是非常簡單的說明而已,事實上還有很多很深的實作,不過目前的專案這樣算是夠用了。完整的範例程式碼。下一篇會講用 Angular 的 http client 來發送 request。
上一篇練習完 router 後,這篇要說明 Angular 內建的 http 呼叫,由於在寫這篇文章的時候,前後端還沒整合好,所以這邊只會介紹 GET 方法,以及透過 http 呼叫拿到 json 檔案。
由於 HttpClientModule 整個專案的會用到,我就把它從 core module 匯入,之後再把 core module 匯入 app module 中,這樣整個專案就可以使用 HttpClientModule 提供的東西了
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [],
imports: [HttpClientModule],
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'Core is already loaded. Import it in the AppModule only'
);
}
}
}
在路徑 src/assets
的資料夾中,建立一個名為 merchantData.json
的檔案並把以下假資料加上去
[
{
"id": "1",
"name": "gogo",
"adress": "300 新竹市東區關新街xx號",
"phone": "03 668 7123",
"website": "https://www.facebook.com/gogofreshtea.tw/",
"logo": "https://dummyimage.com/280x150/8a8a8a/fff"
},
{
"adress": "300新竹市東區建功一路XX號",
"id": "2",
"logo": "https://dummyimage.com/280x150/8a8a8a/fff",
"name": "天地茶會",
"phone": "+88635165123",
"website": "https://ice-cream-and-drink-shop-2406.business.site/"
},
{
"id": "3",
"adress": "30068新竹市東區學府路XX號",
"logo": "https://dummyimage.com/280x150/8a8a8a/fff",
"name": "飲料王",
"phone": "+88635165123",
"website": "http://www.chingshin.tw/product.php"
},
{
"adress": "300新竹市東區金山街XXX樓",
"id": "4",
"logo": "https://dummyimage.com/280x150/8a8a8a/fff",
"name": "很渴紅茶",
"phone": "+88635630123",
"website": "http://www.kebuke.com/#shop"
},
{
"id": "5",
"name": "gogo3",
"adress": "300 新竹市東區關新街xx號",
"phone": "03 668 7123",
"website": "https://www.facebook.com/gogofreshtea.tw/",
"logo": "https://dummyimage.com/280x150/8a8a8a/fff"
},
{
"id": "6",
"name": "gogo4",
"adress": "300 新竹市東區關新街xx號",
"phone": "03 668 7123",
"website": "https://www.facebook.com/gogofreshtea.tw/",
"logo": "https://dummyimage.com/280x150/8a8a8a/fff"
},
{
"id": "7",
"name": "gogo5",
"adress": "300 新竹市東區關新街xx號",
"phone": "03 668 7123",
"website": "https://www.facebook.com/gogofreshtea.tw/",
"logo": "https://dummyimage.com/280x150/8a8a8a/fff"
}
]
在 merchants.service.ts 中,注入 HttpClient (第 7 行),然後新增一個方法 fetchMerchant()
,此方法目的在於將商家的資料取得後,更新 merchants$ 這個 BehaviorSubject
,有訂閱的就會收到更新的通知。在 fetchMerchant()
使用 HttpClient 的 get 方法,並傳入 api endpoint 作為參數,此方法會把 response 以 observable 的方式回傳(第 10~12 行)。
// ...省略
@Injectable({
providedIn: 'root',
})
export class MerchantsService {
merchants$ = new BehaviorSubject<Merchant[]>([]);
constructor(private clonerService: ClonerService, private http: HttpClient) {}
fetchMerchant(): void {
this.http
.get('./assets/merchantData.json')
.subscribe((merchants: Merchant[]) => this.merchants$.next(merchants));
}
// ...省略
之後在 merchant-list.component.ts
初始化時,呼叫 service 的 fetchMerchant()
方法,載入商家資訊。
// ...省略
@Component({
selector: 'app-merchant-list',
templateUrl: './merchant-list.component.html',
styleUrls: ['./merchant-list.component.scss'],
})
export class MerchantListComponent implements OnInit {
merchants$: Observable<Merchant[]>;
constructor(
private merchantsService: MerchantsService,
public dialog: MatDialog
) {}
ngOnInit(): void {
this.merchantsService.fetchMerchant();
this.merchants$ = this.merchantsService.getMerchants();
}
// ...省略
今天的程式碼,由於在寫這篇時還沒完成串接,所以這裡就很簡單的介紹,未來等串結完成後,在把詳細的介紹補上。Angular 的主要概念就先講到這邊!
將BotFather加入對話
輸入 /newbot
的指令,再輸入欲產生的Bot名稱,將產生後token記錄下來後續使用
go get -u github.com/go-telegram-bot-api/telegram-bot-api
package main
import (
"log"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
func main() {
bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken")
if err != nil {
log.Panic(err)
}
bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates, err := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil { // ignore any non-Message Updates
continue
}
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
msg.ReplyToMessageID = update.Message.MessageID
bot.Send(msg)
}
}
https://api.telegram.org/13919:AAFQ-MrwPrEVbBfNWUQv8GX/sendMessage?chat_id=13919&text=Hello+World
API Request
https://api.telegram.org/13919:AAFQ-MrwPrEVbBfNWUQv8GX/getMe
得到接收的訊息
https://api.telegram.org/13919:AAFQ-MrwPrEVbBfNWUQv8GX/getUpdates
https://api.telegram.org/13919:AAFQ-MrwPrEVbBfNWUQv8GX/sendMessage?chat_id=13919&text=Hello+World
application/json
application/x-www-form-urlencoded (無法上傳檔案)
multipart/form-data (用來上傳檔案 )
Response (JSON 格式)
成功
ok = True
result = 回覆內容
失敗
ok = False
error_code
description = 錯誤描述
Method (無視大小寫)
getMe 等同 GeTmE
必須使用 UTF-8 編碼
保存時間 (24 hrs)
兩種獲得方式 ,兩者無法並行使用
[方式一] 設定 Webhook (有新訊息時,Telegram 將會主動告知)
需要 HTTPS (TLS 1.0) 伺服器
並且將埠開在 443, 8443, 80, 8080 其一 (就算 port 80 也要求 TLS)
setWebhook 設定伺服器
https://api.telegram.org/bot<token>/setWebhook?url=<server>
留白表示刪除
https://api.telegram.org/bot<token>/setWebhook?url=
getWebhookInfo 得知設定
https://api.telegram.org/bot<token>/getWebhookInfo
deleteWebhook 刪除設定
https://api.telegram.org/bot<token>/deleteWebhook
[方式二] 主動輪詢
getUpdates 請求,將會回傳一個 JSON 陣列
https://api.telegram.org/bot<token>/getUpdates
[模式一] 隱私模式 Privacy Mode (預設)
由 / 開頭的指令
對機器人 Reply 的訊息
系統訊息 (e.g., 新成員)
自己是管理員的頻道
[模式二] 關閉可收到全部訊息
私訊 @BotFather
輸入 /setprivacy 指令
選擇 Bot
點擊 Disable
點選Start Free (一個Mail帳號可以提供一個免費的Sandbox 512mb空間的MongoDB)
填寫基本資料
選取Create a cluster 開始吧
選取Cloud的供應商,這邊我們選擇GCP,在選擇GCP在台灣的機房,未來也可以依照你想要提供服務的區域進行相關的選擇部屬
選擇M0 Sandbox,此為開發與測試環境,當需要部署到正式環境時可以修改設定即可不停機的狀況下輪巡升級
進階設定包含備份等機制,可以在這邊設定,不過需要付費的才有。再輸入Cluster 名稱即可點下Create
這邊如果選擇付費的服務,則會顯示價錢by hour 收費
接下來等待完成建置
建立DB使用者,並選取其角色 (https://docs.atlas.mongodb.com/security-add-mongodb-users/#database-user-privileges)
建立可連線到DB的IP白名單,很重要,建議一定要設定,避免被駭客加密勒索 (也可以設定CDN等網址進行授權)
回到Cluster,點選Connect,這裡可以依照需求選取連線方式,透過AP連線可以取得連線字串,並且可以取得對應程式語言的SampleCode,是不是很方便呢!!
到這就完成MongoDB的部屬
gcloud projects create PROJECT_ID
gcloud app create
runtime: go114 # or go112 or go113 for Go 1.12 or Go 1.13
instance_class: F2
env_variables:
BUCKET_NAME: "example-gcs-bucket"
handlers:
- url: /stylesheets
static_dir: stylesheets
- url: /(.*\.(gif|png|jpg))$
static_files: static/\1
upload: static/.*\.(gif|png|jpg)$
- url: /.*
script: auto
gcloud app deploy
gcloud app browse
ng build --prod
runtime: python27
api_version: 1
threadsafe: true
handlers:
# Initial route that will serve up index.html, main entry point to your app
- url: /
secure: always
static_files: {$ your app folder name}/index.html
upload: {$ your app folder name}/.*
# Routing for typedoc, assets and favicon.ico to serve directly
- url: /((?:assets|docs)/.*|favicon\.ico)
secure: always
redirect_http_response_code: 301
static_files: {$ your app folder name}/\1
upload: {$ your app folder name}/.*
# Routing for any js files
- url: /(.*\.js)
secure: always
redirect_http_response_code: 301
static_files: {$ your app folder name}/\1
upload: {$ your app folder name}/.*\.js
# Routing for any css files
- url: /(.*\.css)
secure: always
redirect_http_response_code: 301
static_files: {$ your app folder name}/\1
mime_type: text/css
upload: {$ your app folder name}/.*\.css
# Routing for anything (wild card) after
- url: /.*
secure: always
static_files: {$ your app folder name}/index.html
upload: {$ your app folder name}/.*
將Angular Build 的 dist 裡面的資料夾和 app.ymal 上傳
Deploy
gcloud app deploy
arc
Ro