跳至主要内容

· 閱讀時間約 9 分鐘
Guy Chien

前言

由於最近想更加深入的了解 Design Pattern ,於是挑選了 Observer Pattern 當作練習的目標,這篇文章後續會提到:

  1. 滿足 Observer Pattern 觀察者模式至少需要滿足的介面
  2. 如何使用 Observer Pattern 實作一個通知中心,能夠滿足 廣播 ( broadcast ) 給每個人。
  3. 新增一些介面來讓現有實作能滿足 推播 ( push ) 給某個人。

那廢話不多說,我們開始吧!


先讓我們來看看 Observer Pattern 的定義:

The observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

由此段定義可以知道,我們會需要至少兩樣物件,其中一項是其中所敘述到的 subject (在以下案例當中我們稱作 Observable 可被訂閱的 ),而另一項則是被呼叫的 observer。

因此滿足此 pattern 至少要滿足以下介面:
  1. 讓 Observable 能夠被觀察 subscribe
  2. 讓 Observable 能夠被取消觀察 unsubscribe
  3. 讓 Observable 能夠在事件發生時能夠通知所有的觀察者 notify observers

IObservable

被通知後的 Observer 實際上的行為由觀察者自己本身實作 update

interface IObservable {
observers: IObserver[];
subscribe: (observer: IObserver) => void;
unsubscribe: (observer: IObserver) => void;
notify: (payload: any) => void;
}

interface IObserver {
update: (payload: any) => void;
}

而先讓我們實作最一開始的 NotificationCenter ( IObservable ):

class NotificationCenter implements IObservable {
//透過此介面能夠維護目前現有觀察此事件的所有觀察者。
observers: typeNotificationObserver[];

constructor() {
this.observers = [];
}

//透過此介面能夠通知所有觀察者事件發生了。
notify(payload): void {
this.observers.forEach((observer) => {
observer.update({ ...payload });
});
}

//透過此介面能夠讓外界的觀察者留下自身訊息,使後續事件發生時,被觀察者可以通知。
subscribe(observer): void {
this.observers.push(observer);
}

//透過此介面,觀察者可以抹去自己的紀錄,讓往後事件發生,不會再收到通知。
unsubscribe(observer): void {
const target = this.observers.findIndex((obr) => {
return obr === observer;
});

if (target !== -1) {
this.observers.splice(target, 1);
}
}
}

而由於我們是在實作一個通知中心,並且希望這通知中心能夠有個簡單的介面讓我們能達到,紀錄通知的歷史,獲得所有通知歷史,及推播、廣播功能。因此定義了 INotificationCenter,並且讓上方的 NotificationCenter 多重實作介面 IObservable、INotificationCenter,且我們還會需要一個簡單的通知物件介面,讓我們能在這些功能中傳遞。

INotificationCenter

  1. 用來維護所有通知的歷史紀錄 notifications
  2. 廣播給所有的觀察者 broadcast
  3. 推播給某一位觀察者 push
  4. 獲得所有的通知歷史紀錄 getAllNotifications
interface INotificationCenter {
notifications: INotification[];
broadcast: (notification: INotification) => void;
push: (notification: INotification, target: IPerson) => void;
getAllNotifications: () => INotification[];
}

INotification

  1. 通知訊息的識別 id
  2. 通知訊息的時間點 date
  3. 通知訊息的內容 message
interface INotification {
id: string;
date: Date;
message: string;
}

而原先的 NotificationCenter 將被擴充並改寫為以下樣子:

class NotificationCenter implements IObservable, INotificationCenter {
//原有介面
observers: typeNotificationObserver[];

//所有通知的歷史紀錄
notifications: INotification[];

constructor() {
this.observers = [];
this.notifications = [];
}

/* 由於要滿足可以推播給某一位觀察者,這裏實作有一些調整,
* 可以理解為當今天目標是 `all` 時將會對所有觀察者進行通知也就是廣播,
* 而當今天目標是某一個觀察者 id 時只針對那位觀察者進行通知。*/
notify(payload): void {
if (payload.target.id === 'all') {
this.observers.forEach((obr) => {
obr.update({
...payload,
});
});
} else {
// 此處聰明如你,會發現 observer 介面有新增,先別急,下面做解釋。
const target = this.observers.find(
(obr) => obr.id === payload.target.id
);
if (target) {
target.update({
...payload,
});
}
}
}

//原有介面
subscribe(observer): void {
this.observers.push(observer);
}

//原有介面
unsubscribe(observer): void {
const target = this.observers.findIndex((obr) => {
return obr === observer;
});

if (target !== -1) {
this.observers.splice(target, 1);
}
}

//分別傳入通知、目標對象,達到推播功能。
push(notification: INotification, target: IPerson): void {
this.notifications.push(notification);
this.notify({
notification,
target,
});
}

//傳入通知,並且目標對象為所有觀察者 all,達到廣播功能。
broadcast(notification: INotification): void {
this.notifications.push(notification);
this.notify({ notification, target: { id: 'all' } });
}

//獲取所有通知歷史紀錄
getAllNotifications(): INotification[] {
return this.notifications;
}
}
小提醒

眼尖的讀者可能會發現,上方 notify 當中所使用的 observer 多了一個 id 介面可以做使用,目的是為了讓 notify 能辨別 observer,只通知目標觀察者。

而為此定義了一項新介面 ITargetObserver,當今天有針對目標的需求時,能夠透過多重實作此介面來達到需求。而介面如下:

ITargetObserver

  1. 用於識別 Observer id
interface ITargetObserver {
id: string;
}

那我們最終需求的觀察者 Observer 的介面就滿足了,讓我們來看看實作,其中的 update 就是當 observer 收到通知後最終的行為:

interface IObserver {
update: (payload: any) => void;
}

interface ITargetObserver {
id: string;
}

type typeNotificationObserver = IObserver & ITargetObserver;

class NotificationObserver implements typeNotificationObserver {
update: (payload: any) => void;
id: string;

constructor(update, id) {
this.update = update;
this.id = id;
}
}

const update = (payload) => {
const { notification: ntfc, target } = payload;
if (target.id !== 'all') {
console.log(
`[push] ${ntfc.id} , ${ntfc.date.toDateString()} , ${
ntfc.message
} send to ${target.name}`
);
} else {
console.log(
`[broadcast] ${ntfc.id} , ${ntfc.date.toDateString()} , ${
ntfc.message
}`
);
}
};

最後我們將建立一個人的物件,去註冊這些事件,一但 push / broadcast 事件發生時,能夠第一時間的去通知這些註冊此事件的人。

interface IPerson {
id: string;
name: string;
}

type typeEvent = { observable: IObservable; observer: IObserver };

class Person implements IPerson {
id: string;
name: string;
constructor(id, name) {
this.id = id;
this.name = name;
}

register(events: typeEvent[]) {
events.forEach((event) => {
event.observable.subscribe(event.observer);
});
}

unRegister(event: typeEvent) {
event.observable.unsubscribe(event.observer);
}
}

const brian = new Person('001', 'brian');
const brainRegisterNotificationEvent = {
observable: center,
observer: new NotificationObserver(update, brian.id),
};
brian.register([brainRegisterNotificationEvent]);

const guy = new Person('002', 'guy');
const guyRegisterNotificationEvent = {
observable: center,
observer: new NotificationObserver(update, guy.id),
};
guy.register([guyRegisterNotificationEvent]);

最後我們將可以透過 push 跟 broadcast 這兩介面,達到通知中心的推播跟廣播功能:

center.broadcast({
id: '001',
date: new Date(),
message: 'this is broadcast',
});

brian.unRegister(brainRegisterNotificationEvent);

center.push(
{
id: '002',
date: new Date(),
message: 'this is the push notification for brian',
},
brian
);

center.push(
{
id: '003',
date: new Date(),
message: 'this is the push notification for guy',
},
guy
);

· 閱讀時間約 8 分鐘

前言

此篇文章將從註冊網域到將網站部署到 AWS CloudFront CDN 上的流程,和設定解說 ,使用到 aws 服務如下 :

  • AWS Route 53
  • AWS Certificate Manager
  • AWS S3
  • AWS CloudFront

如果有興趣就跟著我一起看下去吧 !

註冊網域

首先先導覽到 Route 53 的服務,點擊左側導覽頁,Registered domains,接著點擊 Register Domain ,輸入喜好的 domain name,點擊加入購物車,點擊繼續,完成結帳,第一步就完成了。

買完後要稍微等一下,domain 會處於 pending request 的狀態,過不久就會移轉到 registered domains 下,就可以看到剛剛註冊好的 domain 已經生效。

建立網域託管區域 ( Route53 hosted zone )

此部分如果是在 AWS 購買網域, AWS 會很貼心的自動幫你建立起一個託管區域,並且將其中的 NS records 也都設定好, 如果不是在 AWS 購買網域就需要在此手動建立託管區域,並且將 AWS 提供給你的 4 筆 NS Records 回填到購買網域的 registrar 去。

將網站掛上 https ( AWS Certificate Manager : ACM )

此部分我們將使用 AWS Certificate Manager,將準備要部署上去的站掛上 https,首先點擊 Request a certificate 選擇 Request a public certificate 並點擊下一步,

此步驟分為三部分,

  • 會需要輸入需要被認證的 domain 列舉,此步驟建議可以將 root domain , 以及使用 wildcard 泛指所有的 sub domain 將兩項填入 domain names weed-ui.org *.weed-ui.org ( 此處是示範的 domain,請使用自己剛才註冊的哦! )
  • 選擇驗證的方式,而此處我們選擇 aws 建議的 DNS validation,此步驟只要將等等產生出來的 records 加入 route53 hosted zone 稍作等待就可以驗證成功了。
  • 此處是 optional 的,在此先跳過。

接著就可以按下送出。

送出後即可以看到 request 送出的通知,可以順著此通知的 view certificate 看到此 certificate 所有的設定值,並且我們要將其中所提供的 record 加入 Route53 hosted zone 才可以使證書可以通過驗證,進到證書詳細資訊後,請點擊 Create Records in Route 53,順著點擊下一步,就可以順利的將 records 加入 Route53 hosted zone 裡,接著只要等待 certificate 核發下來就行了,通常很快,會在 5 分鐘內,如果沒有核發下來,可能要確認一下自己的 domain 有沒有生效、被 suspend,亦或是 NS Records 沒有設定正確。

在此需要注意,ACM 是有不同區域性上的差異的,在此因為後續要配和 cloudfront distribution,需要將申請所在區域調整為 US East (N. Virginia) 才能使此證書在各個 CDN server 中生效

AWS 官方原文

To use an ACM certificate with Amazon CloudFront, you must request or import the certificate in the US East (N. Virginia) region. ACM certificates in this region that are associated with a CloudFront distribution are distributed to all the geographic locations configured for that distribution.

開啟 S3 bucket 存放要上傳的靜態檔案

進入 S3 服務後,我們即將建立一個 S3 bucket 存放要上傳的靜態檔案,而對於接下來 S3 的設定如下 :

  • 點擊 Create bucket,此處要注意,bucket name 記得要和 domain 相同,這是 AWS 對 static site hosting 的 hard rule,並且此處可以設置 S3 Bucket 機房所在地區,此處是選擇 us-ease-1 此部分會和後續 deploy 有關聯,可以隨意設置,但要記得自己的設定值。
  • 開啟 ACLs,並且將預設勾選的 Block all public access 關閉,目的是讓外部的成員可以瀏覽此些檔案,才能提供大家瀏覽網頁。
  • 開啟 Static website hosting,並且將 default page 設定為 index.html,此處設定完後,就可以點選建立 bucket。
  • 建立後,即可以看見上圖新增出一筆新的 bucket record,但我們還需要更新 bucket policy,讓外部成員可以 access 這些檔案。可將下列 policy 更新到 bucket > permissions > bucket policy 並且點擊 edit 更新。此處要注意記得要將 resource 替換掉,將下述 <bucket-name> 換成自己的 resource。 ex. weed-ui.org
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:GetObjectVersion"],
"Resource": "arn:aws:s3:::<bucket-name>/*"
}
]
}
  • 上述步驟做完後,將會獲得一個外部可以存取的 S3 bucket,可以嘗試先撰寫一份 index.html,並且上傳到剛才建立的 bucket,並且可以到 bucket > properties > static website hosting 找到 bucket website endpoint 點擊此 endpoint 就可以看見剛才所上傳的 index.html ,而可以先將此 endpoint 先記錄下來,我們將在後續建立 cloudfront distribution 時使用到。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>s3 bucket</title>
</head>
<body>
<h1>hello world!</h1>
</body>
</html>

建立 CloudFront Distribution

  • 首先要先填入 origin domain,此處請填入剛才上述所說的 S3 Bucket Website Endpoint,請注意不要直接選取 AWS 所提供的選項。
  • 接著可填入 alternate domain name,此處可以自行填寫想要 access 的 domain,此處這裡填寫 root domain 跟 使用 wildcard 開啟所有 subdomain 存取。 weed-ui.org *.weed-ui.org , 並於 Custom SSL certificate 選項中選取剛才已經在 ACM 準備好的 certificate。
  • 最後只要回到 Route 53 建立一筆 A record 由 root domain 指向 aws 提供的 alias 至剛才所建立的 CloudFront Distribution。而此處也順便將 www. 使用 cname record 指向 root domain,使得拜訪 www.weed-ui.org 的外部成員也會被導去拜訪 weed-ui.org
  • 大功告成,以上步驟接做完後,將可以拜訪以下兩個擁有 SSL certificate 的網址了!

https://weed-ui.org

https://www.weed-ui.org

· 閱讀時間約 6 分鐘
Guy Chien
  • 在 ES6 尚未流行前,大部分 JS 開發者都是使用 var 關鍵字做變數宣告,很多開發者在宣告函式時也比較習慣使用 function declaration 又稱作 function statement 做函式的宣告。
var cat = 'Civet';

// function declaration || function statement

function sayHi() {
var cat = 'Doraemon';
console.log("hello I'm " + cat);
}

sayHi();
  • 而以上程式碼相信大家腦袋中早已有答案,我們在 Devtool 中將會看到字串 hello I'm Doraemon ,但如果今天的情況變成是以下程式碼?那結果會是如何呢?
var cat = 'Civet';

// function declaration || function statement

function sayHi() {
console.log("hello I'm " + cat);
var cat = 'Doraemon';
}

sayHi();
  • 答案將會是 hello I'm undefined ,驚不驚喜,意不意外? cat 的值竟然不是向外找的 Civet 也沒有錯誤訊息說明 ReferenceError: cat is not defined 取而代之的值卻是 undefined ! 別緊張,通常一般教授 JS 的書籍會很貼心的先分解步驟給學者看,當然這裡也不意外。可以先想像上述的程式碼和下述的程式碼是等價的。
var cat = 'Civet';

// function declaration || function statement

function sayHi() {
var cat;
console.log("hello I'm " + cat);
cat = 'Doraemon';
}

sayHi();
  • 這樣看起來是不是相對合理多了呢!? 這也是為何用提升 ( Hoisting ) 這個詞來描述這種現象,從 sayHi 這個函式當中我們看見了 cat 這個變數被 “提” 早於函式一開始前就做宣告,但並沒有初始化,也就是賦予了這個變數記憶體位置卻沒有賦予它值。而 JS 在此種情況會給予該變數 undefined 的值。

  • 但為什麼會發生提升 ( Hoisting ) 這種現象呢? 這就要說到 JS 在運行時,會有兩個階段

  1. 創建階段 ( creation phase )

  2. 執行階段 ( execution phase )

  • 大家皆明白 JS 是由上而下一行一行的讀取程式碼,而在開始讀取第一行程式碼前,JS 會先進入創建階段,建立全域執行環境 (Global Execution Context),為接下來要運行的 JS 程式碼做準備,以上方程式碼為例,在建立階段會做以下的準備。
  • 會為變數 cat 先保留記憶體位置,以及用 function declaration 宣告的函式 sayHi 做記憶體分配,在此要注意的是這裏建立的 cat 並不是 sayHi 函式中的 cat,而是全域變數中的 cat 。 接著環境準備好後,JS 緊接著就會開始進入執行階段 ( execution phase ) ,執行第一行程式碼,也就是:
var cat = 'Civet';

而此時全域變數 cat 就在這行程式碼被賦予了值 Civet 。

接著繼續向下執行…

function sayHi() {
console.log("hello I'm " + cat);
var cat = 'Doraemon';
}

saiHi();

JS 遇見了第二個作用域 ( 別忘了JS 是以函式作為一等公民切分作用域的 ), 因此 JS 再次進入創建階段 開始進入建立局部區域執行環境 ( Local Execution Context ) ,這時並還沒有執行 sayHi 函式裡的任何一行程式碼,而是為了要執行 sayHi 做前置準備,JS 會將 sayHi 作用域裡的程式碼掃過一遍,看看有沒有使用 var 關鍵字做宣告的變數,並先分配給此變數記憶體,所以此時 cat 就佔了一席之地了,但尚未被初始化。

執行完創建階段後,就開始進入執行階段,先行執行了 console.log("hello I'm" + cat); 而此時局部變數 cat 仍然尚未賦值, 所以想當然就得到了 hello I'm undefined 的值啦!QQ

  • 而提升此種現象並非只出現在 var 關鍵字上,還會出現在上述的 function declaration 所宣告的函式上,所以如果遇見以下狀況別懷疑,是提升搞的鬼。明明就在還沒宣告前就 call sayGoodBye 為什麼還呼叫的到?原因就是 sayGoodBye 函式早已在建立全域執行環境 (Global Execution Context) 時,函式的內容已經做好了準備。
sayGoodBye();

function sayGoodBye() {
console.log('Bye Bye');
}
// output: Bye Bye

以上是我個人對於提升的見解,如果有誤,歡迎告訴我,我會盡快的更改有誤的內容,並且謝謝你讓我有機會更加的進步。

參考資料: JavaScript Visualizer

大家可以到上面這個網站模擬看看,很好玩的,不玩會後悔。

我知道你懂 hoisting,可是你了解到多深?
Javascript Execution Context 簡介