0%

筆記 JavaScript 中非常重要的概念:繼承 inheritance、原型 prototype 和原型鍊 prototype chain。

Object-oriented programming(OOP) 物件導向程式設計

JavaScript 並不像 Java、C++ 這些典型的物件導向語言具有「類別」(class)來區分概念與實體(instance)或天生具有繼承的能力,而 JavaScript 只有「物件」,因此只能利用設計模式來模擬這些功能。在 JavaScript 世界中,到底是怎麼實現物件導向的概念的? JavaScript 的物件透過原型 (Prototype) 機制相互繼承功能,與典型的物件導向程式語言相較,運作方式有所差異。
在討論 物件導向 JavaScript 之前,需要先知道物件導向程式設計的意義:OOP 基本概念是採用物件(objects)來模塑真實的實物世界;也就是在程式中透過 objects 來塑造模型,並提供簡單方式存取「難以或不可能採用的功能」,物件可裝載相關的資料與程式碼,資料部分是你塑造某個模型的資訊,而程式碼部分則用是操作行為(Method)實現。

為了簡化程式撰寫,我們可以為某個複雜東西建立簡單的模型,藉以代表其最重要的概念或特質,且該模型建立方式極易於搭配我們的程式設計用途:譬如用「類別」建立物件實體 Object instance — 該物件包含了類別中所定義的資料與功能

在根據類別建立物件實體時,就是執行類別的「建構子 Constructor 函式」所建立,而這個「根據類別來建立物件實體」的過程即稱為「實體化 Instantiation」,物件實體就是從類別實體化而來。我們可根據某一個類別建立許多新的子類別,新的子類別可繼承 (Inherit) 其母類別的資料與程式碼特性。你可重複使用所有物件類型共有的功能,而不需再複製之。若功能需與類別有所差異,則可直接於其上定義特殊功能。

inheritance 繼承

一個物件可以存取其他物件的屬性 properties、方法 methods,就叫做繼承 inheritance
繼承可以分成兩種,一種是 classical inheritance 類別繼承,這種方式用在 C# 或 JAVA 當中;另一種則是 JavaScript 使用的 prototypal inheritance 原型繼承。
在「典型 OO」中,你必須定義特定的類別物件,才能定義哪些類別所要繼承的類別,而 JavaScript 使用不同的系統:「繼承」的物件並不會一併複製功能過來,而是透過原型鍊連接其所繼承的功能,亦即所謂的原型繼承 (Prototypal inheritance)
基於 JavaScript 運作的方式 (原型鍊),物件之間的功能共享一般稱為「委託 (Delegation)」,即特定物件將功能委託至通用物件類型。「委託」其實比繼承更精確一點。因為「所繼承的功能」並不會複製到「進行繼承的物件」之上,卻是保留在通用物件之中。

Prototype原型 與 __proto__[[Prototype]] 的意義

  • Prototype原型

物件之間可以互相成為各自的 Prototype 原型,被繼承的物件將會繼承 父物件本身的屬性和方法 以及 它的 Prototype 的所有屬性,原型鍊上可追溯的物件都可以找到其屬性、方法。

每個物件都有一個隱藏的內部屬性(internal property) [[Prototype]],這個屬性指向一個物件,也就是繼承的物件;也可以說是該物件的原型

所謂的原型繼承就是指繼承上一個物件的 prototype 屬性 (你也能稱之為 子命名空間 sub namespace) 中定義的成員,也就是以「Object.prototype.」開頭的屬性內容。Object.prototype. 屬性值就是一個物件,儲存了許多我們想「讓原型鍊上的物件一路繼承下去」的屬性與函式。

[[Prototype]] 不允許外部存取,從 ES6 開始,[[Prototype]] 可以通過 Object.getPrototypeOf()Object.setPrototypeOf() 訪問器來訪問,也可以使用物件屬性 __proto__ 呼叫。

__proto__ 發音 dunder prototype,它原先並非標準但許多瀏覽器實現,最先被 Firefox 使用,後來在 ES6 被列為 Javascript 的標準內建屬性的。它的出現是為了解決讀寫 Object.prototype 的麻煩,提供一個快捷讀寫的 API,而且它是透過連結內部屬性 [[Prototype]] 完成這個功能。

舉例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}

var nick = new Person('nick', 18);
console.log(nick.__proto__ === Person.prototype) // true
console.log(nick.__proto__ === Object.getPrototypeOf(nick)) // true

var peter = new Person('peter', 20);
console.log(nick.log === peter.log) // true

使用 __proto__ 屬性可以透過這個接口指向 class 的原型 也就是 nick.__proto__ 會指向 Person.prototype 指向的物件;Person.prototype 指向 Person 的原型屬性。

上述範例中,當呼叫 nick.log() 的時候,nick 這個 instance 本身並沒有 log 這個 function,那 JavaScript 是怎麼找到這個 method 的? 根據 JavaScript 的機制;nick 是 Person 的 instance,所以如果在 nick 本身找不到,它會試著從 Person.prototype 去找。可是,JavaScript 怎麼知道要到這邊去找? 一定是 nick 跟 Person.prototype 會透過某種方式連接起來,才知道要往哪邊去找 log 這個 function,這個連接方式,就是原型鍊。

假如 JavaScript 在 Person.prototype 還是沒找到 log() method,會繼續依照這個規則,去看 Person.prototype.__proto__ (指 Person 的原型物件的原型屬性)裡面有沒有 log() method。就這樣一直不斷找下去,直到找到某個東西的 __proto__null 為止,就表示這邊是最上層了,而這一個不斷串起來的鍊,也就是原型鍊,透過原型鍊可以呼叫自己 parent class 的 method,達到繼承的功能。

prototype chain 原型鍊

傳統的 OOP 在複製物件時,是先定義了類別,建立物件實例之後,將類型上定義的所有屬性與函式複製到此實例。
但 JavaScript 不會複製這些屬性與函式,卻是在物件實例與其建構子之間設定連結 (原型鍊中的連結),只要順著原型鍊就能在建構子之中找到屬性與函式

新的物件 instance 透過建構子函式 constructor() 產生後,其核心將會透過原型鏈 Prototype chain 的機制傳遞;物件的原型可能也有自己的原型,透過由 prototype 定義的參照鏈連在一起,繼承了其上的函式與屬性。

這樣不斷繼承的過程,使物件可以使用其他物件的屬性和方法,所以精確來說的話,物件實例的屬性與函式都是透過物件的建構子函式所定義,並非物件實例本身:在 JavaScript 主控台中輸入 Object.prototype. 會看到 Object 的 prototype 屬性中所定義的許多函式,而繼承自 Object 的物件也能找到這些函式。只要試著尋找如 String、Date、Number、Array 等全域物件的原型上定義的函式與屬性,就會看到 JavaScript 中的其他原型鍊繼承範例。

Reference

javascript.info:function-prototype
MDN Web Docs:继承与原型链

了解 JavaScript 中,繼承 inheritance、原型 prototype 和原型鍊 prototype chain 的概念
你懂 JavaScript 嗎?#19 原型(Prototype)
該來理解 JavaScript 的原型鍊了
Javascripter 必須知道的繼承 prototype, [[prototype]], __proto__

JavaScript Class

In Object-oriented programming, a class is an extensible program-code-template for creating objects, providing initial values for state(member variables) and implementations of behavior(member functions or methods).class 是一種可以拓展的編程模板,用來創建對象(object),提供狀態初始值(變量)和行為實現(函數或方法)。

JavaScript 雖然具有物件導向程式語言的特性,但與 Java、C++ 等以類別為基礎的程式設計規範不同,JavaScript 是一種以原型(prototype-based)為基礎的語言。

由於開發上經常需要創建許多相似類型的對象,傳統的 JavaScript 寫法,可以用構造函數(Constructor)和 operator new 來達成這個使用目的,但在 ES6 中有一個更進階的用法叫做 Class

ECMAScript 6 中引入了類別 (class) 作為 JavaScript 現有原型程式(prototype-based)繼承的語法糖。語法糖可以讓現有語法操作變得更容易。類別(class)語法並不是要引入新的物件導向繼承模型到 JavaScript 中,而是提供一個更簡潔的語法來建立物件和處理繼承

Class 允許使用更簡潔的結構語法來定義 prototype-based 的類別。在 prototype-based 類別中,所有 methods 都存在 prototype 屬性中,藉以與其他延伸出去的類別共用,如此可以節省 memory 空間。
比較傳統原型繼承的寫法和使用 class 的寫法會發現;class 省略了 prototype chaining 並且整個 class 的宣告定義集中描述在 class 區塊內,幫助讀者了解這段程式的作用範圍,使 class 的目的更為清楚

  • Constructor 構造函數

    • 是一種常規函數,函數名稱首字必須大寫
    • 創建時必須使用 new 來操作
    • 需在 prototype 物件上定義方法
    • 示範:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      function User(name) {
      this.name = name;
      this.isAdmin = false;
      }

      User.prototype.sayHi = function() {
      console.log(this.name);
      }

      let user = new User("Jack"); // 創建一個 User Constructor

      alert(user.name); -> Jack
      alert(user.isAdmin); -> false
      user.sayHi() -> Jack

    當一個函數作為 new User(...)執行時,它執行以下步驟:

    1. 一個新的空對像被創建並分配給 this
    2. 函數體執行。通常它會修改 this,為其添加新的屬性。
    3. 返回 this 的值。
      所以 new User("Jack") 的结果是相同的對象
  • Class 用法與特色

    • 使用 constructor() 建構子來定義初始狀態,而且創建時也必須使用 new 來操作
      建構子(constructor)方法是一個特別的方法,用來建立和初始化一個類別的物件,一個類別只能有一個建構子(constructor)。
      Class 的 constructor()函數是默認的 method,即使沒有寫出來也會自動生成一個默認的空構造函數,並加入到 class.prototype 上。
      class syntax:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class User {
      constructor(name) {
      this.name = name;
      }

      sayHi() {
      alert(this.name);
      }
      }
      let user = new User("John");
      user.sayHi();

    此時 User.prototype 包括 constructorsayHi 兩個 methods。

    • class expression 像一般 function 一樣,也可以寫作沒有名稱的表達式:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      let User = class {
      sayHi() {
      alert("Hello");
      }
      };

      function makeClass(phrase) {
      // 返回並宣告一個 class
      return class {
      sayHi() {
      alert(phrase);
      };
      };
      }
      let User = makeClass("Hello");

      new User().sayHi(); // Hello
    • 使用 extends() 以及 super() 來繼承父類的參數給子類。
      關鍵字 extends 是在類別宣告或是類別敘述中建立子類別的方法。
      若在子類別中有建構子(constructor),要使用this前必須先呼叫super()函式:在子類別建構子中用關鍵字 super 來呼叫父類別的建構子。

      1
      2
      3
      4
      5
      const createPersonClass = name => class extends User {
      constructor() {
      super(name)
      }
      }
    • 內部 method 之間不用逗號區分,與一般物件不同,可以避免我們描述二者時產生混淆

    • Class 結構中自動開啟了嚴格模式 (Use Strict)

    • Class 的宣告不會被 hoisting 提升到最高作用區,與函式宣告式不同,編譯器尚未抵達和執行 class 宣告之前無法建立或存取 class

    • Just like literal objects, classes may include getters/setters computed properties etc. Protection and Control:確保內部才能修改或訪問資料,防止洩漏。

Reference

javascript.info
mozilla.org - MDN Web Docs

JavaScript Data Type

JavaScript 的資料類型總共有七種,其中 6 種被稱為 原始型別 primitive data types: string, number, boolean, null, undefined, symbol (new in ECMAScript 2015),以及最後一種 Object 對象資料型別。

  • number:number for numbers of any kind: integer or floating-point.
  • string:string for strings. A string may have one or more characters, there’s no separate single-character type.
  • boolean:boolean for true/false.
  • null:null for unknown values – a stand alone type that has a single value null.
  • undefined:undefined for unassigned values – a standalone type that has a single value undefined.
  • object:object for more complex data structures. 包括 functionfunction(){}, array[], object{key:value}等不同對象物件資料類型。
  • symbol:symbol for unique identifiers.

JavaScript 是弱型別,也能說是動態的程式語言,這代表不必特別宣告變數的型別,程式在運作時,型別會自動轉換,這也代表可以用不同的型別使用同一個變數。
A variable in JavaScript can contain any data. A variable can at one moment be a string and at another be a number:

1
2
3
let message = "hello";
message = 123456;
<!-- no error -->

Programming languages that allow such things are called “dynamically typed”, meaning that there are data types, but variables are not bound to any of them.

JavaScript Equals

在 JavaScript 語法中,比較值是否相等有分為一般相等比較和嚴格相等比較兩種:

  • Double Equals == 一般相等比較,若等號兩邊的資料類型不同,會轉換為相同類型再比較
  • Triple Equals === 嚴格相等比較,若類型不同即為不相等

字串的比較:依字典順序,按字母逐個進行比較

1
2
3
4
alert('Z'>'A') -> true
alert('Bee'>'Be') -> true
alert('Glow'>'Glee') -> true
alert('a'<'A') -> false;在 unicode 中 小寫 a 大於 大寫 A

若為不同類型比較會先轉換為數字(number)在判定大小:

1
2
3
4
5
6
alert('2'>1) -> ture
alert('01'== 1) -> true
alert('2'>'12') -> true
alert(0 == '') -> true
alert(true == 1) -> true
alert(false == 0) -> true
1
2
3
4
5
let a = 0;
alert(Boolean(a)) -> false
let b = '0';
alert(Boolean(b)) -> true
alert(a == b) -> true

Type Conversions 類型轉換

  • String()

    1
    2
    3
    4
    5
    6
    let value = true;
    alert(typeof value); -> boolean
    value = String(value); -> "true"
    alert(typeof value); -> string
    String({}); -> "[object object]"
    String([]); -> "" 空字串
  • Number()

    • undefined -> NaN
    • null -> 0
    • true 和 false -> 1 and 0
    • string
      • “按原樣讀取”字符串,兩端的空白會被忽略。空字符串變成 0。轉換出錯則輸出 NaN。
      • 幾乎所有的算術運算符都將值轉換為數字進行運算,加號 + 是個例外:如果其中一個運算元是字符串,則另一個也會被轉換為字符串。
        alert( 1 + '2' ); -> '12'
  • Boolean()

    • 0, null, undefined, NaN, “” -> false
    • 1, 其他值 -> true
  • 總結注意點:

    • 當用 Number() 轉換型別成 number 時,須注意 “” 空字符會轉為 0,undefined 會變成 NaN。
    • 用 Boolean() 轉換型別成 boolean 時, 0 為 false;1 為 true,而 null, undefined, NaN 及 “” 都會是 false。
  • 補充:

    • 強制轉型 Coercion:
      強制轉型(coercion)分為兩種,分別是「明確的」強制轉型(explicit coercion)和「隱含的」強制轉型(implicit coercion),「明確的」強制轉型是程式碼中刻意寫出來的型別轉換的動作,例如String(), Number()。反之,在程式碼中沒有明確指出要轉換型別卻轉型的,就是隱含的強制轉型,例如 +運算子串接 數字和文字時
    • 舉例: []+{}{}+[]的結果?
      • [] + {} 中,[] 會轉為空字串"",而 {} 會轉為字串 "[object Object]"
      • {} + [] 中,{} 被當成空區塊而無作用,+[] 被當成強制轉型為數字 Number([]) (由於陣列是物件,中間會先使用 toString 轉成字空串,導致變成 Number(‘’))而得到 0

null, NaN, undefined

  • null 表示無、空值,未知的特殊值,typeof(null) -> object,這是一個誤解,因為 typeof 用型別標籤來辨別,null的型別標籤為 000,符合 object 的 type 所以會產生這個結果,這可以說是一種 bug,詳見:typeof
  • NaN 表示不是一個數字(not a number),typeof(NaN) -> number
  • undefined 表示未被賦值,有宣告但未給予數值、字串等值,typeof(undefined) -> undefined,不等於 not defined,not defined 是沒有宣告所以系統不認得的意思

Reference

javascript.info:type-conversions
JavaScript 的資料型別與資料結構
型別(Type)
強制轉型(Coercion)

DOM (Document Object Model)

W3C(World Wide Web Consortium) 全球資訊網協會訂定了 DOM 的標準,將 DOM 區分為三部分:core DOM、XML DOM、HTML DOM。HTML DOM 是用來取得、修改、新增、刪除 HTML 元素,有了明確的標準不同瀏覽器才能提供一套相同的操作方式給開發者使用。
DOM 的樹狀結構是一種有階層(父子關係)的資料結構,以節點(node)代表一個物件,物件本身具有屬性和方法,上層的節點為父節點(parent)下層節點為子節點(child),一個父節點可能有很多個子節點,但子節點只會有一個父節點。
在 document 之上其實還有一個 window 物件,它不屬於 DOM 但實作時經常會使用到 window 提供的屬性及方法,例如: setInterval, clearIntervel, confirm, alert…等。

DOM 事件傳遞機制

事件在 DOM 裡面有既定的傳遞順序,假設你有一個 ul 元素,底下有很多 li ,代表不同的 item,當你點擊任何一個 li 的時候,其實也點擊了 ul,因為 ul 把所有的 li 都包住了。假如我在兩個元素上面都加了eventListener,哪一個會先執行?這時候知道事件的執行順序就很重要。

DOM 事件觸發有三個階段;當點擊事件發生時,會先從 window 開始往下傳遞,一直傳到被點擊的該物件為止,到這邊就叫做 CAPTURING_PHASE 捕獲階段;接著事件傳遞到物件本身,這時候叫做 AT_TARGET;最後事件會從物件一路傳回去 window,這時候叫做 BUBBLING_PHASE 冒泡階段。

  • 先捕獲再冒泡;當事件傳到 target 本身,沒有分捕獲跟冒泡:
    一般而言是先捕獲再冒泡,但是當事件傳遞到點擊的真正對象,也就是 event.target 的時候,無論你使用 addEventListener 的第三個參數是 true 還是 false,這邊的 event.eventPhase 都會變成 AT_TARGET,所以執行順序就會根據 addEventListener 的順序而定,先添加的先執行,後添加的後執行。

  • 事件註冊,加上監聽機制的方法:target.addEventListener(type, listener[, useCapture]);
    addEventListener 函數的第三個參數是 boolean 值,可以決定事先採取被觸發事件的動作反應,還是其外部元素的觸發動作。設定為 true 代表把這個 listener 添加到捕獲階段false 代表把這個 listener 添加到冒泡階段,預設為false;在冒泡階段監聽事件觸發。一個 element 可以有多個 eventlistener,監聽不同事件(event),設置指定動作(function)。

stopPropagation & preventDefault

  • stopPropagation:取消事件繼續往下傳遞
    加在哪邊,事件的傳遞就斷在哪裡,不會繼續往下傳遞給下一個節點。
    用法如下:

    1
    2
    3
    4
    $list.addEventListener('click', (e) => {
    console.log('list capturing');
    e.stopPropagation();
    }, true)

    儘管已經用e.stopPropagation,但對於同一個層級,剩下的 listener 還是會被執行到,若是你想要讓其他同一層級的 listener 也不要被執行,可以改用 e.stopImmediatePropagation()

  • preventDefault:取消瀏覽器的預設行為
    最常見的例子是阻止點擊<a>時新開分頁、跳轉或<form> submit action。
    preventDefault 跟事件傳遞「一點關係都沒有」,事件還是會繼續往下傳遞,只是阻止了現在和往後的預設行為。
    有一個特別值得注意的地方是 W3C 的文件裡面有寫到:Once preventDefault has been called it will remain in effect throughout the remainder of the event’s propagation. 意思就是說一旦 call 了 preventDefault,在之後傳遞下去的事件裡面也會有效果。
    用法舉例:

    1
    2
    3
    form.addEventListener('submit',e=>{
    e.preventDefault(); // 把原生的 Form submit 跳轉行為停掉
    })

Event Delegation 事件代理機制

將許多事件綁定在同一節點,透過該節點使下層的子節點都可以觸發事件,因為只要綁定一個地方不需要每個子節點或後來新增的節點都綁定所以叫做事件代理。

Reference

事件 (Event) 的註冊、觸發與傳遞
DOM 的事件傳遞機制:捕獲與冒泡

單冒號 (:) 是用在偽類
雙冒號 (::) 則是用在偽元素
偽類 (pseudo class) 就是在選已經存在的東西,比方說 a:hover 就是選了已經存在的 <a> 的某一個狀態
偽元素 (pseudo element) 就是在創造一個新的假元素,因為他不在 DOM 裡面,而是創造的了一個我們看不見的元素。比如說 ::first-line,第一行並沒有被任何的 tag 包住,所以在選取的過程就像是用了一個看不到的 tag 把第一行包起來,所以才選得到這行。

使用偽類元素,一方面可以減少頁面中的節點元素,加速頁面渲染速度,另一方面可以為設計動畫提供很多新思維。

pseudo class 偽類

Pseudo-classes are used to provide styles not for elements, but for various states of elements. The pseudo-class concept is introduced to permit selection based on information that lies outside of the document tree or that cannot be expressed using the other simple selectors.
用於定義元素的特殊狀態,為了選擇 DOM tree 之外的信息,或者使用其他簡單選擇器不能表達的信息,不會出現在 DOM tree。

Simple selectors includes:

  • type selector (h1, p, div)
  • universal selector (*)
  • attribute selector (img[alt])
  • class selector (.class)
  • ID selector (#id)
  • pseudo-class (:)
    • :active
    • :hover 設定滑鼠滑過的樣式
    • :visited 被訪問過的連結的樣式
    • :link
    • :nth-child()
    • :checked

狀態不存在 DOM 裡面,或是這幾種 simple selectors 選不到的東西,就是 pseudo-class 要去解決的問題。

pseudo element 偽元素

Pseudo elements differ from pseudo-classes in that they don’t select states of elements; they select parts of an element. 用於選擇元素的指定部分,創造一個關於 DOM tree 的抽象內容,提供一種方法來引用源文檔中不存在的內容,創造之後會出現在 DOM tree,偽元素也會「繼承」原本元素的屬性。

  • ::first-line 選取第一行
  • ::first-letter 選取第一個字
  • ::before 在原本的元素「之前」加入內容
  • ::after 在原本的元素「之後」加入內容
  • ::selection 選取文字反白後

Reference

偽元素 (pseudo element) 和偽類 (pseudo class) 差在哪?
css偽類元素的運用以及相應的hover的使用
CSS 偽類 與 偽元素
使用CSS3 :nth-child(n) 選取器教學
CSS 偽類 child 和 of-type
CSS 偽元素 ( before 與 after )

以下筆記如何在 react app 中引入 redux:
示範 code 專案:Tripper-app-redux

程式碼

in Tripper-app-redux’s index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { createLogger } from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
import { requestLogIn, routeChange, requestData } from './reducers';
import App from './containers/App';

const logger = createLogger();

const rootReducer = combineReducers({requestLogIn, routeChange, requestData});
const store = createStore(rootReducer, applyMiddleware(thunkMiddleware, logger));

ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>, document.getElementById('root'));

in Tripper-app-redux’s App.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { connect } from 'react-redux';
import { routeChange } from '../actions';
.
.
.
const mapStateToProps = (state) => {
return {
user: state.requestData.user,
journeys: state.requestData.journeys,
initialJourney: state.requestData.initialJourney,
journeyList: state.requestData.journeyList,
route: state.routeChange.route,
isSignedIn: state.requestLogIn.isSignedIn
}
}

const mapDispatchToProps = (dispatch) => {
return {
onRouteChange: (route) => dispatch(routeChange(route))
}
}
.
.
.

export default connect(mapStateToProps, mapDispatchToProps)(App);

Redux Store and Provider

<Provider></Provider>包住<App>他會把 store 用 props 傳給所有 App 包含的子組件。
The logger will catch the actions and console.log() the action that is going to go to the reducer.

Store binds together the 3 principles fo Redux:

  1. The entire state of the application will be represented by one JavaScript Object. -> Store holds the current application state object.
  2. The state is read only, and can only be modified by dispatching actions. -> Store allows you to dispatch actions.
  3. To specify how actions change state tree, the reducers should be pure functions. -> When you create the Store, you need to specify the Reducer that tells how states updated with actions.

Store 有三個重要的 methods:

  1. getState(): run console.log(store.getState()) => get the current state of the Redux Store.
  2. dispatch(): dispatch actions to change the state of the application.
  3. subscribe(): register a callback that the redux store will call any time an action has been dispatched. So you can update the UI of application to reflect the current application’s state.

運用 combineReducers() method 將多個 reducer 綁在一起變成一個,createStore()則負責建立 store,接受兩個參數:要監聽的 reducer、用 applyMiddleware() 加入要使用的 middleware。

Connect

connect() are those which one of these components we want to be smart or be aware that the redux library exists and they subscribe to the changes. The connect function is optimized in order for us to avoid using somthing called store.subscribe(). It is a higher order function, means that a function that return another function. Through connect function, App component now could listen to the states that mapStateToProps gives and interested in actions that mapDispatchToProps gives to the component.

Redux Middleware

Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store’s dispatch method for fun and profit. The key feature of middleware is that it is composable. Multiple middleware can be combined together, where each middleware requires no knowledge of what comes before or after it in the chain.
Middleware 就像一個 tunnel,在 action 到 reducer 的階段之間建立通道,在 dispatch actions 時適時採取相應的行為。 Middleware helps us to handle side effects, monitor each one of our actions. We can listen to what logging output each one of these actions happen. It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

Redux Pattern:

graph LR;
A(Action) -- Middleware --> B(Reducer);
B --> C(Store);
C --> D(Make changes);

在 Action 和 Reducer 之間的 Middleware 作用是輔助處理,當 dispatch action 的時候透過 middleware 進行額外動作。
常用的 Middleware 例如:

  • redux-logger:監聽 actions,在 console 印出 action type、以及更新前、後的狀態
  • redux-thunk:額外或是非同步的 dispatch
  • redux-observable、redux-saga:複雜邏輯 dispatch

使用的方法:

1
const store = createStore(rootReducer, applyMiddleware(thunkMiddleware, logger));

在建立 store 的時候一併設定,透過 applyMiddleware() 放入多個 middleware,表示當 action dispatch時會依序經過 thunkMiddleware、logger 這些 middleware,然後再到 reducer。

Asynchronous Redux

The redux-thunk package that provides a getState and dispatch functions that are passed on. Redux-thunk can handle asynchronous actions like AJAX calls. How does this middleware work? Thunk middleware is waiting for a function, it waits and sees if any actions return a function instead of an object. And if it is a function, give a dispatch so we can call the actions to run the function as we thought.

Asynchronous Action 可以寫作這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const editBudgets = (data, id, index) => (dispatch) => {
dispatch({ type: REQUEST_DATA_FAILED });
fetch(`${Url}/journeys_budgets/${id}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(journey => {
if (journey) {
dispatch({
type: REQUEST_UPDATE_BUDGETS_SUCCESS,
payload: {
data: journey,
index: index
}
})
} else {
alert('unable to edit budget')
}
})
.catch(err => dispatch({ type: REQUEST_DATA_FAILED, payload: err }));
}

其中 const editBudgets = (data, id, index) => (dispatch) => {...} 表示 A function that return another function, redux wouldn’t understand it. Because it’s not an object, as it expects for an action. With the redux-thunk middleware, now we’re listening to actions. Anytime the actions get triggered it’s going to return a function and trigger redux-thunk and redux-thunk is going to know whether it is a function or not then give it a dispatch to call action to run.

Reference

redux 官方

What is MVC framework? Why we need?

MVC 架構是為了讓複雜程式碼能夠根據各自負責的功能目的做區分而出現的開發設計模式,分別是 modle、view、controller 三個部分,在權責區分清楚的情況下,可以有更好的擴充性和維護性。

modle 代表邏輯處理,view 負責 UI 的呈現,controller 則串連 modle 和 view,作為程式的流程控制。

舉例:使用者造訪一個部落格網站,點擊文章列表頁面,瀏覽器發出 request 給伺服器,server side 處理 request;將請求丟給對應的 controller,controller 向 model 存取資料,model 回應傳回資料並 render html 交給 controller,controller 再回傳一整份 html 檔給瀏覽器,瀏覽器顯示包含資料內容的 html 畫面。

前後端分離

後端專注在提供資料,前端專注在顯示資料,把 data 跟 view 完全切開來,這樣要替換語言或框架時也比較方便,後端只要確保 API 接口一致,就不會影響前端,反之也是一樣。

假如 server 有一天掛了,API 也跟著掛了,使用者依然可以造訪網頁,只是看不到資料而已,或者是你可以顯示出一個錯誤的圖案。但如果是舊的那種綁在一起的架構,server 一旦掛掉,你連畫面都渲染不出來。

而這樣的做法會讓前端自己管理 URL 的狀態,去決定現在要顯示哪一個頁面。

SSR(Sever-side Rendering)

view 的畫面是在後端動態產生 html,render 完成再 response 給前端(瀏覽器),本質上是「每一個不同頁面就回傳一份不同的 html 檔案」,每一個頁面之間的狀態不會互相干擾。

CSR(Client-side Rendering)

view 的畫面當前端拿到資料以後,才用 JavaScript 動態的產生,把內容填到網頁上面,並且需要管理不同頁面,根據路徑或用 hashtag 管理畫面切換。
前端 render 的難題在:「要怎麼只更新部分畫面,而不是暴力的每次都砍掉重練」。

SPA(Single-Page-Application)

所有個內容都由前端用 JavaScript 動態產生,用 AJAX 拿資料並搭配 JavaScript 來做畫面上的處理。
與之對應的概念是 MPA,Multiple Page Application。

  • 前端、後端各自有 MVC 框架
    前端如果利用 SPA 來實作的話,會把原本應該是後端處理的一部份職責給搬到前端去,例如說狀態的管理跟路由;以往 Server 根據不同的路徑對應到不同的 Controller,進而渲染出不同的 View,可是現在 Server 無論什麼路徑都會輸出同一個檔案(index.html),所以你在前端也要判斷現在的網址是哪個,才能決定在前端應該渲染出哪個畫面,因此前端也開始需要 MVC 架構了,也就是由 router 去分類路徑並交給 controller 去分別連接 model 和 view。
    藉由前後端各自的架構,後端只負責輸出資料,前端來負責抓資料跟渲染畫面。把前後端完完全全的切開了,就算後端壞掉,前端還是看得到畫面(只是可能會顯示個錯誤畫面之類的);前端壞掉,後端還是能安穩的輸出資料供其他服務使用。

  • SEO 問題
    由於 SPA 是由前端的 JavaScript 動態產生內容,因此如果你對 SPA 的網站按下右鍵 -> 檢視原始碼,只會看到空蕩蕩的一片,只看得到一個 JavaScript 檔案跟一些最基本的 tag,這對於 SEO 的影響而言是不好的。
    強大的 Google 的爬蟲其實支援執行 JavaScript,所以他依然會 index 你在前端渲染之後的頁面,但還有其他很多搜尋引擎,有些可能沒有像 Google 這麼強大,碰到 SPA 就只能索引空蕩蕩的 HTML,內容幾乎空白。
    既然問題出在「第一次渲染」,在第一次渲染的時候把該輸出的資料都輸出(運用 server-side rendering),對使用者來說還是一個 SPA,差別在於使用者接收到 HTML 的時候,就已經有完整的資料了,第一個頁面由 Server side render,之後的操作還是由 Client side render。

Flux

Flux 是一種架構設計模式,幫助你撰寫有條理的前端架構,用於管理控制應用程式中資料的流向,確保資料是 one-way data flow,資料的定義不限於指來自 server 的邏輯資料,也包括 view 的變化等。 在 flux 的架構中,Store基本上是你唯一可以操作資料與儲存資料的地方。

Flux Pattern 是這樣的:

graph LR;
A(Action) --> B(Dispatcher);
B --> C(Store);
C --> D(View);
  • Flux 中有四位主要角色:
  1. Action:規範所有改變資料的動作,讓你可以快速掌握整個 App 的行為。
  2. Dispatcher:將目前發生的行為,告知給所有已註冊的 Store。
  3. Store:存放資料和業務邏輯,並且只提供 getter API 讓人取得資料。
  4. View:根據資料渲染 UI 和傾聽使用者的操作事件。

當 action 被觸發後,View 就會透過 Dispatcher dispatch 出一個 Action,該 Action 可以包含一個 payload,說明你想做什麼事情以及你需要操作什麼資料,在 Store 資料做完更新後,要告訴前端頁面去刷新視圖(view)。

  • Flux 的特性:
  1. 單向資料流:改變資料的行為都必須經過 Action、Dispatcher,再到 Store。
  2. Single Source of Truth:資料統一存放於 Store,View 要資料都需跟 Store 拿。
  • Flux 的優點:
    用更清晰的模式,規範資料和頁面複雜互動情境下的資料流。

事實上,MVC 跟 Flux 都只是一個概念,因此有各種不同的實作,加上MVC在資料流的處理上,並不像Flux一般有較為明確的定義,多數時候Model的更動與View的刷新可能會透過Controller來管理,讓Model單純存放data。

如此一來,假若今天 View 的操作更動了 Model,而 Model 的變化又刷新了 View,在系統龐大的時候,一來一往,就會讓資料與頁面狀態變得非常複雜,要追蹤某個頁面的變動到底是誰觸發的,或是哪個資料改變了,必須從 Controller 去慢慢 trace。而若是遵照 Flux 的流程,任何 View 的 update 都只要去追蹤其 State 的來源 Store 即可,有一個明確的 flow 可以遵循,並且每個 View 所需要監聽的資料來源,可以依照 Store 來區分,資料流不會互相干擾,而且 Flux 能夠更輕鬆的做出更小單元的 Unit test,這是複雜的 Controller 難以達成的。

React + Redux => 解決 MVC 架構問題

  • React 解決什麼問題?
    React 是專注在 MVC 架構中的 view,以 state 來管理介面變化的開發框架,透過 virtualDOM 在 state 改變時只重新 render 改變的地方,不需要在頁面切換或資料狀態更新、介面變化時重新 render 整個畫面,有效增進了應用程式的效能和使用者體驗。
    React 把更改 state 的邏輯寫在各自的 component,然而當項目的邏輯變得越來越複雜的時候,將難以釐清 state 和 view 之間的對應關係:一個 state 的變化可能引起多個 view 的變化,一個 view 上面觸發的事件可能引起多個 state 的改變,需要對所有引起 state 變化的情況進行統一管理,於是就需要 Redux。

  • Redux 解決什麼問題?
    Redux 受到幾個 Flux Pattern 重要特質的影響,Redux 透過單一的狀態樹將所有 state 存於一個物件中,並由 Store 去管理;一律透過 action 描述更新動作,經過 Reducer 來變更 state。
    Reducer 根據 action 物件和舊資料回傳新資料,因此你可以紀錄 action,並重新調用 reducer 函數來得到一樣的狀態,幫助我們做可預測的狀態管理,能夠追蹤 state 和資料變化,更好維護而且資料之間不會互相干擾。
    Redux Pattern:

    graph LR;
    A(Action) -- Middleware --> B(Reducer);
    B --> C(Store);
    C --> D(Make changes);

所以使用 Redux + React,可以促進維護性和可擴展性。

  • Redux 和 Flux pattern 不同的地方在於:
    1. Redux 只有一個 store,業務資料都存於一個狀態物件中,並由 Store 去管理;Flux 裡面會有多個 store,分別管理不同資料。
    2. Redux 中更新的邏輯不在 store 中執行而是由 reducer 負責執行;Flux 在 store 裡面執行更新邏輯,當 store 變化的時候再通知 controller-view 更新自己的數據。
      reducer 是一個純函數,這個函數被表述為(previousState, action) => newState,它根據應用的狀態和當前的 action 推導出新的 state 。 Redux 中有多個 reducer,每個 reducer 負責維護應用整體 state 樹中的某一部分,多個 reducer 可以通過 combineReducers 方法合成一個根 reducer,這個根 reducer 負責維護完整的 state,當一個 action 被觸發,store 會調用 dispatch 方法向某個特定的 reducer 傳遞該 action,reducer 收到 action 之後執行對應的更新邏輯然後返回一個新的 state,state 的更新最終會傳遞到根 reducer 處,返回一個全新的完整的 state,然後傳遞給 view。
    3. Redux 沒有 Dispatcher 的概念,它使用 reducer 來進行事件的處理,Store 提供 dispatch API 來傳遞 action 物件;Flux 的 Dispatcher 負責將 action 物件傳遞給每個 Store。
      Redux 和 Flux 之間最大的區別就是對 store/reducer 的抽象,Flux 中 store 是各自為陣的,每個 store 只對對應的 controller-view 負責,每次更新都只通知對應的 controller-view;而 Redux 中各子 reducer 都是由根 reducer 統一管理的,每個子 reducer 的變化都要經過根 reducer 的整合。

Reference

跟著小明一起搞懂技術名詞:MVC、SPA 與 SSR 2018
Node.js-Backend見聞錄(10):關於後端觀念(六)-關於MVC 2017
前後端分離與 SPA 2017
從 Flux 與 MVC 的差異來簡介 Flux 2016
MVC是一個巨大誤會 2015
浅谈 React、Flux 与 Redux 2016
深入淺出 Redux 2016

延伸閱讀

Connect Redux part to React

What is Knex.js?

Knex.js is a tool in the Database Tools category of a tech stack.
Knex.js is an open source tool with 10.8K GitHub stars and 1.3K GitHub forks.

Knex.js is a JavaScript query builder for relational databases including PostgreSQL, MySQL, SQLite3, Oracle, MSSQL, MariaDB and Amazon Redshift. It can be used with callbacks and promises. It supports transactions and connection pooling.

可以將操作資料庫的方式分成三種層次: low-level Database Drivers、middle-level Query Builders、high-level ORMs。

  1. Database Drivers
    This is basically as low-level as you can get — short of manually generating TCP packets and delivering them to the database. A database driver is going to handle connecting to a database (and sometimes connection pooling). At this level you’re going to be writing raw SQL strings and delivering them to a database, and receiving a response from the database. In the Node.js ecosystem there are many libraries operating at this layer. Here are three popular libraries:
  • mysql: MySQL (13k stars / 330k weekly downloads)
  • pg: PostgreSQL (6k stars / 520k weekly downloads)
  • sqlite3: SQLite (3k stars / 120k weekly downloads)

Each of these libraries essentially works the same way: take the database credentials, instantiate a new database instance, connect to the database, and send it queries in the form of a string and asynchronously handle the result.

  1. Query Builders
    This is the intermediary level between using the simpler Database Driver module vs a full-fledged ORM. The most notable module which operates at this layer is Knex. This module is able to generate queries for a few different SQL dialects. This module depends on one of the aforementioned libraries — you’ll need to install the particular ones you plan on using with Knex.
  • knex: Query Builder (8k stars / 170k weekly downloads)
  1. ORMs
    ORMs are powerful tools. The ORMs we’ll be examining in this post are able to communicate with SQL backends such as SQLite, PostgreSQL, MySQL, and MSSQL.
    This is the highest level of abstraction we’re going to consider. When working with ORMs we typically need to do a lot more configuration ahead of time. The point of an ORM, as the name implies, is to map a record in a relational database to an object (typically, but not always, a class instance) in our application. What this means is that we’re defining the structure of these objects, as well as their relationships, in our application code.
    Bust there are some reasons that you should be wary of using an ORM:

(1) Take time to learn but it is not SQL. An ORM is going to be written in the same language as the rest of the application, while SQL is a completely different syntax. A lot of people pick up an ORM because they don’t want to take the time to learn the underlying SQL (Structured Query Language).The syntax for a simple read operation varies greatly between these examples. As the operation you’re trying to perform increases in complexity, such as operations involving multiple tables, the ORM syntax will vary from between implementations even more.

(2) Complex ORM calls can be Inefficient: Recall that the purpose of an ORM is to take the underlying data stored in a database and map it into an object that we can interact within our application. This often comes with some inefficiencies when we use an ORM to fetch certain data.

(3) Not all queries can be represented as an ORM operation. When we need to generate these queries we have to fall back to generating the SQL query by hand. This often means a codebase with heavy ORM usage will still have a few handwritten queries strewn about it. A common situation which doesn’t work too well with ORMs is when a query contains a subquery.

Why use Knex.js?

與 SQL 原生語法相近,卻更簡潔、容易使用。語法設計讓參數和查詢字串分別傳遞給 database driver,保護查詢不會受到 SQL injection。

One nicety is that you’re able to programmatically generate dynamic queries in a much more convenient way than if you were to concatenate strings together to form SQL (which often introduces security vulnerabilities).

Parameters and query string are passed separately to database driver protecting query from SQL injection. Other query builder methods always uses binding format internally so they are safe too.

How to use?

When creating a Knex instance you provide the connection details, along with the dialect you plan on using and are then able to start making queries. The queries you write will closely resemble the underlying SQL queries.
詳細的使用說明可以查詢官方文件

Here is the example of using knex.js to connect postgreSQL:

1
2
3
4
5
6
7
8
// $ npm install pg knex

const knex = require('knex');
const connection = require('./connection.json');
const client = knex({
client: 'pg',
connection
});

要小心使用 knex.raw(),避免允許植入 SQL 語句。

What is PostgreSQL? compare with MySQL?

PostgreSQL 和 MySQL 兩個資料庫之間有許多相似之處和重疊,但也有非常明顯的差異,使用 PostgreSQL 的優點:

  • Open Source:

    • PostgreSQL 由 PostgreSQL 全球開發小組(PostgreSQL Global Development Group)開發,PostgreSQL 全球開發組由多個公司和個人貢獻者所組成。它是免費的開源軟體。PostgreSQL 是在 PostgreSQL 使用許可發布的,這是一個自由的開源許可,類似於 BSD 或 MIT 使用許可。
    • MySQL 開發專案根據 GNU GPL 的條款提供了它的原始碼以及各種各樣的專有協議。它現在由 Oracle 公司擁有,並且提供了幾個商業使用的付費版本。
  • ACID Compliance:

    • ACID(Atomicity,Consistency,Isolation,Durability)是一組資料庫交易安全的特性,ACID 要求確保在單一的資料交易中發生多個更新時也不會在整個系統中遺失數據或發生錯誤。
      PostgreSQL 完全符合 ACID 特性,並確保滿足所有要求。
    • MySQL 只有在使用 InnoDB 和 NDB 叢集儲存引擎時,才符合ACID 標準。
  • Performance:

    • PostgreSQL 被廣泛應用於讀寫速度至關重要且資料需要驗證的大型系統中。 此外,它在商業解決方案中,還支持各種效能優化,(如地理空間資料支持,不需要讀取鎖定的資料一致性支援,如 Oracle、SQL Server)。 整體來看,PostgreSQL 的效能在需要執行複雜查詢的系統中會得到了最好的展現。 PostgreSQL 在 OLTP / OLAP 系統中運行良好,尤其在需要讀/寫速度和需要大量的資料分析時。 PostgreSQL 也適用於商業智慧應用服務,但更適合需要快速讀/寫速度的資料倉庫和資料分析應用服務。
    • MySQL 被廣泛採用,是由於 Web 應用,只需要一個簡單的資料交易資料庫。然而,在一般的情況下,如果 MySQL 的表現不佳,都是當負載過重或試圖進行複雜查詢的時候。MySQL 在 OLAP / OLTP 系統只需要讀取速度時表現良好。MySQL + InnoDB為 OLTP 場景提供了非常好的讀/寫速度。 整體來說,MySQL在高度資料一致性需求的情況下表現良好。MySQL 是可靠的,並且適用於商業智慧應用服務,因為商業智慧應用服務通常是更重視讀取效能的。
  • Security:

    • PostgreSQL 具有角色並可繼承角色來設定和維護權限。 PostgreSQL 內建支援 SSL連線來加密客戶端/伺服器通訊。 它具有資料列級的安全性。除此之外,PostgreSQL 還附帶了一個名為 SE-PostgreSQL 的增強功能,可以根據 SELinux 安全策略提供額外的存取控制。
    • MySQL 為所有連線、查詢和用戶可能嘗試執行的其他操作實作了存取控制列表(ACL)的安全性。 對 MySQL 客戶端和伺服器之間的 SSL 加密連接也有支援。
  • NoSQL Features/JSON Support:

    • NoSQL 和 JSON 都非常流行,NoSQL資料庫變得越來越普及。JSON 是一種簡單的資料格式,它允許程式設計師儲存和傳遞跨系統的資料內容、資料列表和 key-value 對應。
      PostgreSQL 支援 JSON 和其他 NoSQL 的功能,如內建 XML 支援和 HSTORE 的 key-value 對應。 它還支援將 JSON 資料索引以加快存取速度。
    • MySQL 具有 JSON 資料類型支援,但沒有其他 NoSQL功能,也不支援 JSON 索引。
  • Programming Languages Support:

    • PostgreSQL 支援各種程式語言,包括:C / C ++,Java,JavaScript,.Net,R,Perl,Python,Ruby,Tcl 等等。 甚至可以在單獨的程序中執行用戶提供的程式(即作為背景執行程式)。
    • MySQL 一些支援伺服器端不可擴展的程式語言。
  • Extensible Type System:

    • PostgreSQL有幾個專用於延伸套件的功能,可以增加新的資料型別、新的函數功能、新的索引類型。
    • MySQL 不支援增加延伸功能。

更多比較可以查看 PostgreSQL官方文件:PostgreSQL vs MySQL

小結

  • PostgreSQL 相對於 MySQL 的優勢

  - 在 SQL 的標準實現上 PostgreSQL 要比 MySQL 完善,而且功能實現比較嚴謹。
  - 存儲過程的功能支持要比 MySQL 好,具備本地緩存執行計劃的能力。
  - 對表連接支持較完整,優化器的功能較完整,支持的索引類型很多,復雜查詢能力較強。
  - PostgreSQL 主表采用堆表存放,MySQL 采用索引組織表,能夠支持比MySQL更大的數據量。
  - PostgreSQL 的主備復制屬於物理復制,相對於 MySQL 基於 binlog 的邏輯復制,數據的一致性更加可靠,復制性能更高,對主機性能的影響也更小。
  6、MySQL 的存儲引擎插件化機制,存在鎖機制復雜影響並發的問題,而 PostgreSQL 不存在。

  • MySQL 相對於 PostgreSQL 的優勢:

  - innodb 的基於回滾段實現的 MVCC 機制,相對 PostgreSQL 新老數據一起存放的基於 XID 的 MVCC 機制,是占優的。新老數據一起存放,需要定時觸發 VACUUM,會帶來多余的 IO和數據庫對象加鎖開銷,引起數據庫整體的並發能力下降。而且 VACUUM 清理不及時,還可能會引發數據膨脹;
  - MySQL 采用索引組織表,這種存儲方式非常適合基於主鍵匹配的查詢、刪改操作,但是對表結構設計存在約束;
  - MySQL 的優化器較簡單,系統表、運算符、數據類型的實現都很精簡,非常適合簡單的查詢操作;
  - MySQL 分區表的實現要優於PG的基於繼承表的分區實現,主要體現在分區個數達到上千上萬後的處理性能差異較大。
  - MySQL 的存儲引擎插件化機制,使得它的應用場景更加廣泛,比如除了 innodb 適合事務處理場景外,myisam 適合靜態數據的查詢場景。

總體上來說,開源數據庫都不是很完善,商業數據庫 oracle 在架構和功能方面都還是完善很多的。從應用場景來說,PostgreSQL 更加適合嚴格的企業應用場景(比如金融、電信、ERP、CRM),而 MySQL 更加適合業務邏輯相對簡單、數據可靠性要求較低的互聯網場景(比如 Google、facebook、alibaba)。
雖然有不同的歷史、引擎與工具,不過並沒有明確的參考能夠表明這兩個數據庫哪一個能夠適用於所有情況。很多組織喜歡使用 PostgreSQL,因為它的可靠性好,在保護數據方面很擅長,而且是個社區項目,不會陷入廠商的牢籠之中。MySQL 更加靈活,提供了更多選項來針對不同的任務進行裁剪。很多時候對於一個組織來說,對某個軟件使用的熟練程度要比特性上的原因更重要。

Reference

Why you should avoid ORMs (with examples in Node.js)
Knex.js
PostgreSQL vs MySQL
PostgreSQL 與 MySQL的比較

How is the Internet work?

現代網路運作的基礎主要由瀏覽器、伺服器以及網路資料的傳遞組成,在網路世界中,最小的資訊單位是 bit,遍佈世界的光纖纜線以光速傳輸資訊,所以當我們用 wifi 上網時,就像用電台傳送廣播一樣,將資訊波段以無線的方式傳送 wifi路由器,再透過電纜運送連結到世界上的任何一處。
當使用者在自己電腦透過瀏覽器瀏覽網頁時,會從瀏覽器發出造訪某網頁網址的請求,而該網址所屬的網站伺服器接收到請求內容後,返回包含了 HTML、CSS、JavaScript 等的檔案,瀏覽器接到回應之後將網站呈現在畫面上,上述這個瀏覽器發出請求(request)、伺服器給予回應(response)的過程就是一般我們在瀏覽器輸入網址後,到瀏覽器渲染出頁面之間發生的事情,而這個過程其實仰賴許多機制共同建立,包括:

  • 域名系統 - DNS (Domain Name System):由於我們平常用的網址是 Domain,而電腦都是用 IP 在溝通的,DNS 會負責將網址分析為 IP 位址,回報給本地電腦,本地電腦獲得 IP 位址,就能成功連上對方主機並請求主機回傳網頁資料。

  • 資料傳輸的單位 - 封包 (packet):封包帶有起始和終點的 IP 位址資訊,網路傳輸時會將資料拆分成多個封包傳遞;若某網路定義的封包大小為 1500 bytes,一部影音為 500MB 的資料就會被拆為 35萬個左右的封包發送至目的地 IP 位址。(1MB = 1,048,576 bytes)

  • 指引封包走對的路線 - 路由器 (router):一種專門電腦,根據纜線的「交通狀況」幫封包選擇最易通行、最省時且風險最低的路徑,確保網路傳輸可靠度。

  • 確保資料完整抵達 - 傳輸控制協定 (TCP-Transmission Control Protocol):在資料送達時,根據 IP 封包裡的資訊檢查是否有全部送達,只有全數送達時才會簽收,若發現資料短少就會通知伺服器主機要求重新發送資料。

  • 通訊協定:伺服器和瀏覽器的對話 - HTTP:通訊協定事電腦之間互相索取資料的溝通語言,設計 HTTP 的最初目的是發布和接收 HTML 頁面。

透過以上的機制維持網路的運作,我們可以在自家電腦使用瀏覽器發送請求以連上世界各地的網站,獲得世界各地主機的資料。

事實上,網路除了瀏覽器和伺服器,根據網路的架構還可以將網路運作分為四個層級:應用層、傳送層、網路層、鏈結層,可以對應到 OSI 七層級,如下表:

OSI 七層級 TCP/IP 模型 相關通訊協定和標準
應用層、表現層、會談層 應用層 HTTP/FTP/SMTP/DNS/SSH
傳送層 傳送層 TCP
網路層 網路層 IP
資訊鏈結層、實體層 鏈結層 LAN/WAN

TCP/IP 模型由 OSI(open system interconnecttion)模型簡化而來,OSI是由 ISO組織制定,作為制定網路標準的參考,依網路運作方式將網路架構分為七個層次。
鏈結層將數位訊號組成符合邏輯的傳輸資料(資訊框 data frame),定義網路裝置之間的位元資料傳輸,主要跟硬體有關。
網路層定義網路路由、定址功能,IP(Internet Protocol)會將 IP 位址加入傳輸資料內,並將資料組成封包。
傳送層負責資料傳輸和控制,將大型資料切割成適合傳輸的大小型式,替應用層提供流量管制及錯誤控制。
應用層建立網路連線、將ASCII編碼轉成應用層可用資料,負責加密、解密資料等,連結瀏覽器和伺服器之間的溝通。

HTTP vs. HTTPS

現代網路資料傳遞的原則有:

  1. 標準化的內容格式
  2. 資料結構分為 header 和 body
  3. 使用狀態碼 HTTP Status Code 標準化傳輸結果
    • HTTP Status Code
      • 2XX 成功:
        • 200 ok
        • 204 no content
      • 3xx 重新導向:
        • 301 永久搬遷
        • 302 暫時搬遷
      • 4xx 用戶端錯誤:
        • 404 Not Found
        • 401 Unauthorized:未認證,表示 client-side 沒有必要的憑證或伺服器拒絕了 client-side 提供的憑證。
        • 403 Forbidden:伺服器理解請求,但是拒絕執行,表示 client-side 沒有權限。
      • 5xx 伺服器端錯誤
        502 Bad Gateway:表示伺服器的某個服務沒有正確執行,可能是處理請求的時間太長或伺服器主機問題。

常見與不常見的 HTTP Status Code

  1. 用動詞 HTTP Method 標準化動作: GET、POST、DELETE、PUT、PATCH

    • HTTP Method
      • GET:透過 API 把資料內容撈出來,這些內容通常是可以對外公布,不需要密碼帳號就可以取得的資料。
      • POST:新增一筆資料
      • DELETE:刪除資料
      • PUT:更新一筆資料,如果存在這筆資料就會覆蓋過去
      • PATCH:部分更新資料
  2. protocol 通訊協定:定義資料交換的格式,標準化促成規模化 ex: HTTP、HTTPS

    • HTTP(Hyper Text Transfer Protocol) 超文本傳輸協定
      In 1989 Tim Berners-Lee invented the World Wide Web and built the HTTP for transferring HTML document around the world. HTTP is a protocol or the rules that we use over the wires. It is a foundation of any data exchange on the web.

    • HTTPS(Hyper Text Transfer Protocol Secure) 超文本安全傳輸協定
      HTTPS(Hyper Text Transfer Protocol Secure) use TLS and SSL technology to encrypt the information that transported. Only the server or client web could have the secret key to read the message.

    主要是由網景公司(Netscape)開發並內建於其瀏覽器中,之後廣泛發展於網際網路上,用於對資料進行壓縮傳輸及傳輸後解壓縮回正確資訊的操作。隨著網路的發達,資訊技術越來越透明,HTTP 的開放明文顯得在網際網路中,不再是這麼安全的傳送協定,知名企業網站及全球級大型網站 Google、Facebook 等皆採用 HTTPS 加密,作為預設連線方式,因此有使用者登入系統或或存取到機密敏感資料頁面等有建立會員、購物車、金流、刷卡機制服務的網站選擇使用HTTPS的傳送協定。HTTPS 其實就是在原本的 HTTP 協定中延伸加入 SSL(Secure Sockets Layer,傳輸層安全協議) 或 TLS(Transport Layer Security,傳輸層安全) 憑證的安全連線技術。

    SSL/TLS 憑證是網頁伺服器和瀏覽器之間以加解密方式溝通的安全技術標準,透過憑證內的公開金鑰加密資料傳輸至伺服器端,伺服器端用私密金鑰解密來證明自己的身份,取得有效憑證後在網際網路上傳輸加密過的資料以達到資安的目的。當您在伺服器與連線至伺服器的瀏覽器上安裝 SSL 憑證時,SSL 憑證的存在會觸發 SLL (或 TLS) 協議,這將加密伺服器與瀏覽器之間 (或伺服器之間) 發送的資訊;詳細資料顯然更加複雜一些。

    運作流程:

    1. SSL 在 TCP 連線建立後開始運作,啟動 SSL信號交換。
    2. 伺服器發送憑證至使用者,同時附上大量規格 (包括哪個版本的 SSL/TLS 以及使用哪個加密方法等。)
    3. 使用者檢查憑證的有效性,選擇雙方都支援的最高等級的加密,並使用這些方法啟動安全的工作階段。有很多組方法可用優勢各異,被稱為密碼組。
    4. 為了確保所有傳輸訊息的完整性與真實性,SSL 與 TLS 協議也包括使用訊息驗證程式碼的驗證程序。
      這些方法聽上去冗長又複雜,但在現實生活中是瞬間實現的。
      SSL 直接在傳輸控制協議之上 (TCP) 運作,像安全感毛毯一樣有效。其允許最高的協議層不變,同時仍然提供安全連線。所以在 SSL 層下,其他協議層可以照常運作。若 SSL 憑證使用正確,所有攻擊者將看到哪個 IP 與通訊埠已連線以及大概在發送多少資料。他們或許可以終止連線,但是伺服器與使用者可以知道這是由第三方造成的。但是,他們無法攔截任何資料,所以使攻擊從根本上變得無效。

    取得SSL憑證的方法就是向憑證廠商購買,SSL憑證費用依照加密的等級、販售廠商而有所不同,可以依照網站性質與企業規模選擇SSL的加密程度以及衡量費用支出。一般來說,SSL憑證的發行廠商是不會影響搜尋排名,但是Google Chrome於2017年公布賽門鐵克(Symantec)發行的SSL視為無效化,並於2018/3/7公布Chrome 66連上賽門鐵克的SSL時呈現的警告畫面,被Chrome 視為無效的SSL憑證發行商名單:Thawte、VeriSign、Equifax、GeoTrust、RapidSSL、Symantec。

AJAX and JSON

AJAX (Asynchronous JavaScript And XML/JSON) 非同步的 JavaScript 與 XML 技術,這是在2005年由Jesse James Garrett所發明的術語,描述一個使用多種既有技術的新方法。這些技術包括 HTTPS/HTTP 通訊協定、發 HTTP Request 方法(例如 fetch api)、XHR(XMLHttpRequest)或 JSON。使用 AJAX 能使網頁應用程式更快速、即時的更動介面內容,而不需要刷新整個頁面,讓程式更快回應使用者的操作,可以在網頁操作過程當中,不用刷新網頁就可以得到新的資訊。

  • JavaScript:在 AJAX 技術裡,使用 JavaScript 將所有技術串連,當 server 回傳對應的資料給 client 時,我們可以透過 JavaScript 去解析和拆解這些資料,並使用 DOM 提供的方法把資料放進網頁。

  • 非同步的 JavaScript:使用非同步的請求和回應,這表示當使用者點擊網頁中的一個連結時,即使該操作還未結束,仍可以對網頁做其他操作,同步的話則無法。

非同步 Asynchronous:指可以同時進行多件任務,而不需等待前一任務結束。同步 Synchronous:必須等待上一任務完成才能進行下一個。

  • XML/JSON:
    • XML(Extensible Markup Language):可延伸標記式語言是一種標記式語言,設計用來傳輸和儲存 data。標記指電腦所能理解的資訊符號,通過此種標記,電腦之間可以處理包含各種資訊的文章等。如何定義這些標記,既可以選擇國際通用的標記式語言,比如HTML,也可以使用像XML這樣由相關人士自由決定的標記式語言,這就是語言的可延伸性。XML是從標準通用標記式語言中簡化修改出來的。
    • JSON(JavaScript Object Notation):是一種由道格拉斯·克羅克福特構想和設計、輕量級的資料交換語言,將結構化資料 (structured data) 呈現為 JavaScript 物件的標準格式,常用於網站上的資料呈現、傳輸 (例如將資料從伺服器送至用戶端,以利顯示網頁) JSON 可能是物件或字串,當你想從 JSON中讀取資料時,JSON可作為物件;當要跨網路傳送 JSON 時,就會是字串,因為 JSON 可以通過一個標準的 JS 函數解析(JSON.parse()將接收到的 JSON 內容解析成 JavaScript Object、JSON.stringfy()將 Object 轉成 sever 可以傳輸的 JSON string),我們可以將任何 Object 轉換成 JSON,並使用 JSON 和 server 溝通,再由 JSON 轉換收到的訊息成 JavaScript Object。

兩者都是以讓人閱讀的文字為基礎,可以被轉譯成程式語言使用,讓文件能夠很容易地讓人去閱讀,同時又很容易讓電腦程式去辨識的語言格式和語法,不過 XML 相較之下更難被解析(parse),須使用專屬的解析器,而 JSON 不須使用 end tag,JSON is parse into a ready-to-use JS Object,讀寫更簡潔、快速,可以使用 arrays(objects)格式來儲存資料,雖然 JSON 是以 JavaScript 語法為基礎,但可獨立使用,且許多程式設計環境亦可讀取 (剖析) 並產生 JSON。所以現在多使用 JSON 格式來傳輸資料。

  • XHR(XMLHttpRequest):是一個 JavaScript 物件,他的功能是用來向 server 發送非同步請求,在 XHR 技術剛被開發出來時,
    使用 XML 作為請求、回應的資料格式,因此被稱為 XMLHttpRequest。現今提到 XMLHttpRequest 時已經不限於使用 XML 資料格式,也可以傳遞 XML、JSON、String 等多種資料格式。

  • AJAX 進化史:

    • XHR(XMLHttpRequest) 大約10幾年前
    • XHR(XMLHttpRequest) v2 大約10年前
    • jQuery + XHR
    • 現在:Fetch API、Axios、Request、SuperAgent、Supertest等
      • Fetch:由 Mozilla 和 Google browser 在 2015年3月發佈實作的消息,它有不同於 XHR 思考角度的設計,基於 promise 的語法結構,而且設計足夠低階所以有更多彈性可擴展設定。目前不同瀏覽器版本還有不完全支援的,所以需要安裝 polyfill 當作暫時的解決方案。
      • Axios:Promise-based HTTP library for performing HTTP requests on both Node.js and Browser. It supports all mordern browser, even an included support for IE 8+. It’s built on top of XMLHttpRequest for making AJAX calls. It lets you make HTTP requests from both the browser and the server.
        • Intercept requests and responses before they are carried out.
        • Transform request and response data using promises.
        • Automatically transforms JSON data.
        • Cancel live requests.
        • 可以對 response setTimeout
        • Support protection against CSRF.
        • Has support for upload progress.
      • Request - A Simplified HTTP Client:The Request library is one of the simplest ways to make HTTP calls. The structure and syntax are very similar to that of how requests are handled in Node.js.
        1
        2
        3
        4
        5
        6
        var request = require('request');
        request('http://www.google.com', function (error, response, body) {
        console.log('error:', error); // Print the error if one occurred
        console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
        console.log('body:', body); // Print the HTML for the Google homepage.
        });

Making HTTP calls from the client-side wasn’t this easy a decade ago. A front-end developer would have to rely on XMLHttpRequest which was hard to use and implement. The modern libraries and HTTP clients make the front-end features like user interactions, animations, asynchronous file uploads, etc., easier.

RESTful API

  • API (Application Programming Interface) 應用程式介面
    是一種廣義的稱呼,主要是指 Application 與資料庫的互動介面。透過這個介面,開發者可以了解如何和提供這個介面的 Application 互動。
    其中一種 API 又可以稱作 Web API,也是現在最廣泛出現的,他是指程式開發者提供一個網址 url 來讓其他開發者訪問,並提供使用這個 API 的文件(說明書),這樣其他開發者就可以閱讀文件了解如何使用這個 API 來存取所需的資料。
    串接 API 指的就是在開發時透過程式介面操作資料;一個應用程式通常會開發其程式介面提供串接、讀取資料或應用服務,提供一套標準讓任何想接入應用程式服務的開發者,可以遵循規範、根據介面的設計撰寫程式碼,來得到想要的服務或資料。串接 API 的本質就是交換資料,通常我們會根據文件說明來使用他人寫好的 API,若要主動提供 API 時則需定義要給什麼資料。

  • Web API 或 HTTP API
    透過 HTTP 協定的 API,也就是用 HTTP 格式來提供資料的 API 可以稱為 Web API/HTTP API。根據 HTTP 規範,伺服器端和客戶端進行請求和回應時,會使用定義明確的請求格式(request format)和回應格式(response format),request format 指的是 client-side 需使用特定的 HTTP verb 對 url 發出請求,response format 則是指 server-side 回應時採用的資料格式,有可能是 XML、JSON等。
    HTTP 的組成元素:

    HTTP request HTTP response
    HTTP Method Response Status
    HTTP Headers Response Headers
    Request Body Response Body
    HTTP URL 組成:例如https://www.example.com/photos?page=1
    通訊協定 https://、網域名稱 DNS www.example.com、路徑 path /photos、參數 parameter?page=1(又稱 query string)
  • RESTful API
    RESTful 風格也是一種格式(format),RESTful API 就是指一種寫 API 格式的風格建議,目的是讓大家的寫法更有一致性。

    • REST
      REST 是 Representational State Transfer 的縮寫,由 Roy Fielding 博士在 2000 年的博士論文中所提出。他同時也是 HTTP 規範的主要作者之一。REST 是一種軟體架構風格(並非標準),目的是幫助在世界各地不同軟體、程式在網際網路中能夠互相傳遞訊息。而每一個網頁都可視為一個資源(resource)提供使用者使用,以資源操作的概念(指的是對某項資源,譬如 User、Post 等,指派 Show、Edit 等動作),結合 URL path 與 HTTP Method ,目的是使 URL path 更為簡潔、容易被理解,除了介面簡潔之外,尚有增加快取 cache 效率、提升 api 活用性等優點。換句話說, REST 風格可以單從 HTTP request 就能看出如何操作伺服器的資料。
      REST 的一個最重要的觀念就是 resources (特定資訊的資源),每一個 resource 由一個 global identifier (即 URL)所表示。為了操作這些 resources,網路的 components (client 跟 server) 透過標準化的介面 (HTTP) 來溝通並交換這些 resources 的 representations (實際上傳達資訊的文件)。
      任意數量的 connectors (如 clients, servers, caches, tunnels 等) 可以居中 request,但是都不可以 “seeing past” (不需要其他layer層)。這樣的應用程式跟一個 resource 互動根據兩件事情: resource 的 URL 跟要做的動作 — 它不需要知道是否有 caches, proxies, gateways, firewalls, tunnels, 或其他任何藏在 sever 之間的東西。這個應用程式只需要知道資訊的格式 (representation),通常是 HTML 或 XML 或圖片什麼的。

    • RESTful
      設計為 REST 的系統稱為 RESTful。RESTful 路由便是充分應用 REST 風格的路由設置。而你可以透過 HTTP URL(Uniform Resource Locator),也就是這些資源的地址(俗稱網址),來取得這些資源並在你的瀏覽器上使用。
      RESTful API 建議我們使用 Standard HTTP Verb 來撰寫 API,並根據 API 接口的提供目的規劃 URL,例如 /resouces、/resouces/:id 來代表 resource collection 以及 individual resource 的 path 來規劃 API 介面。
      路由(routing)是指定義 app 如何將 client requset 指向一個正確的接口(endpoint)並做出相應的後續處理(給予 response),一個 route 通常由下組成:
      app.METHOD(PATH, HANDLER)
      METHOD means HTTP request method, PATH is a path on the server and the HANDLER is the function executed when the routes is matched. PATH 是開放給 client 的 route,透過不同 path 導向不同動作。
      舉例:一個簡單的 route based on express.js,'/'表示根目錄(root route):

      1
      2
      3
      app.get('/', function(req, res){
      res.send('hello world');
      })

Reference

Top JavaScript Libraries for Making AJAX Calls
Fetch API
HTTPS的威力?「S」對SEO與網站經營的四大重要性
SSL 憑證
為何HTTPS憑證有貴有便宜還更可以免費?讓我們從CA原理開始講起。

什麼是 CORS?

Cross-Origin Resource Sharing 跨來源資源共享,這是由 W3C 頒布的一種瀏覽器技術規範,透過傳輸 HTTP Header 判斷阻擋或允許不同來源網域的資源存取。

當使用者請求一個不是目前文件來源,例如不同網域(domain/host)、通訊協定(protocol)或通訊埠(port)的資源時,會建立一個跨來源 HTTP 請求(cross-origin http request)。

而瀏覽器基於安全性考量,程式碼所發出的跨來源 HTTP 請求會受到限制,若不受限制的話,駭客可以透過撰寫程式碼跨域撈資料來進行跨站腳本攻擊(XSS),而這個限制只有在瀏覽器上,若在本機使用 node.js 則沒有限制。

同源政策(same-origin policy)

在同源政策(same-origin policy)中規範了那寫資源可以跨源存取,哪些會受到限制。
同源的定義簡單如下:

  • 不同網域(Domain)
  • 不同通訊協定:HTTP, HTTPS, FTP
  • 不同連接埠號(Port)

只要瀏覽器發的 request 與 server 所屬不同源,request 就會被瀏覽器阻擋。不過即便擋下來,其實 request 已經發出去,server 也確實給予瀏覽器 response,只是瀏覽器基於同源政策,因此不把拿到的回應給你的 JavaScript 去做進一步的處理。

一般來說跨來源寫(Cross-origin writes)、跨來源嵌入(Cross-origin embedding)是被允許的,而跨來源讀取(Cross-origin reads)是受限制的。

  1. 受同源政策管理的跨來源請求:
    XMLHttpRequest 及 Fetch 都遵守同源政策(same-origin policy),這代表網路應用程式所使用的 API 除非使用 CORS 標頭否則只能請求與應用程式相同網域的 HTTP 資源
    如果想開啟跨來源 HTTP 請求的話, server 必須在 response 的 header 加上 Access-Control-Allow-Origin: *表示接受所有不同來源的跨域請求,或者可以指定接受特定網址的跨域請求:
    1
    Access-Control-Allow-Origin: http://foo.example

其他方法還有:

  • Access-Control-Allow-Headers:指定哪些 HTTP 標頭可以於實際請求中使用。
  • Access-Control-Allow-Methods:GET, POST, PUT,存取資源所允許的方法,用來回應預檢請求。
  • Access-Control-Expose-Headers:瀏覽器能夠存取伺服器回應當中哪些標頭。
  • Access-Control-Max-Age:預檢請求之結果可以被快取的秒數。
  • Access-Control-Allow-Credentials:用於驗證請求中,用來告知瀏覽器是否允許 client-side 向 server-side 傳送 cookie,默認為 false:CORS 規範會阻止跨域 AJAX 向 server-side 發送 cookie。
  1. 不受同源政策管理的 html tags 包括:<script src='' /><img/><video/><audio/><iframe/><link rel='stylesheet' href /> 這些資源本來就能夠跨域存取。

JSON with Padding(JSONP)

JSONP 是一個利用 script tag 不受同源政策限制的特性,直接載入帶參數的 js 程式碼的跨來源請求實作方法,實務上在操作 JSONP 的時候,Server 通常會提供一個callback的參數讓 client 端帶過去。

Twitch API 有提供 JSONP 的版本,我們可以直接來看範例:

URL: https://api.twitch.tv/kraken/games/top?client_id=xxx&callback=receiveData&limit=1

1
receiveData({"_total":1067,"_links":{"self":"https://api.twitch.tv/kraken/games/top?limit=1","next":"https://api.twitch.tv/kraken/games/top?limit=1\u0026offset=1"},"top":[{"game":{"name":"Dota 2","popularity":63361,"_id":29595,"giantbomb_id":32887,"box":{"large":"https://static-cdn.jtvnw.net/ttv-boxart/Dota%202-272x380.jpg","medium":"https://static-cdn.jtvnw.net/ttv-boxart/Dota%202-136x190.jpg","small":"https://static-cdn.jtvnw.net/ttv-boxart/Dota%202-52x72.jpg","template":"https://static-cdn.jtvnw.net/ttv-boxart/Dota%202-{width}x{height}.jpg"},"logo":{"large":"https://static-cdn.jtvnw.net/ttv-logoart/Dota%202-240x144.jpg","medium":"https://static-cdn.jtvnw.net/ttv-logoart/Dota%202-120x72.jpg","small":"https://static-cdn.jtvnw.net/ttv-logoart/Dota%202-60x36.jpg","template":"https://static-cdn.jtvnw.net/ttv-logoart/Dota%202-{width}x{height}.jpg"},"_links":{},"localized_name":"Dota 2","locale":"zh-tw"},"viewers":65622,"channels":376}]})

結合起來就是:

1
2
3
4
5
6
<script src="https://api.twitch.tv/kraken/games/top?client_id=xxx&callback=receiveData&limit=1"></script>
<script>
function receiveData (response) {
console.log(response);
}
</script>

利用 JSONP 也可以存取跨來源的資料。但 JSONP 的缺點就是你要帶的那些參數永遠都只能用附加在網址上的方式(GET)帶過去,沒辦法用 POST。

Reference

輕鬆理解 Ajax 與跨來源請求
[JS] 同源政策與跨來源資源共用(CORS)