# 不要在 Angular 中複製後端的 model ###### 2020.08.30 ###### tags: `自主學習` `翻譯` `Angular` --- 原始文章 [Don’t clone Back-End models in Angular](https://itnext.io/dont-clone-back-end-models-in-angular-f7a749bdc1b0) --- 在前端中, Model-View-Controller paradigm has been definitely discarded in favour of Model-Component: 所有的現代前端框架都是如此,但如果我們觀察 AngularJS 到 Angular 2+ 的發展,會發現這在 Angular 的世界中特別明顯。 MVC 架構在前端開發中不適用的主要原因,一般認為是 Controller 的角色不明確。但作者在本文想要把焦點放在另一個不足之處: model 在前後端中所扮演的角色不同。 <br> ## 本文的路線圖 本篇文章的目標在於展示,為什麼將後端的 model 複製到 Angular app 會導致架構上的問題,在前端中套用後端的開發方式為什麼會導致這些問題。作者會凸顯缺點,展示設計模式,並提出解決方法。 以下是本文的路線圖: 1. 介紹前後端一般性的架構差異。 2. 在 Angular 的特定案例中,展示在不同層級抽象化的區別,從綁定給 DOM 的屬性到後端的 model,展示為什麼避免這些錯誤最好的的方式是將這些抽象化保持明確的區分。 3. 用一些簡單的例子來展示,將不必要的後端變數帶到前端,會導致多餘的程式並且 Anngular app 容易出錯。 4. 呈現典型的案例,後端回傳的巢狀 JSON,AngularJS 對於這種 JSON 的實作,隱藏預期外的結構問題。 5. 提出一個巢狀 JSON 的不同實作方式,基於 Angular 2+ 的 component pattern,並展現這樣做的優點。 6. 聚焦在將從 API 取得的資料,傳遞給 component 的最好方式,並展現決定前端架構而不要考慮從後端取得的資料結構這件事有多重要。 7. 提出一種將 API 資料放入 Angular 2+ components 的不同方式:藉由在 component 層級初始化的 service 來達成。 8. 簡單談論在保持前後端完全分離的狀況下,發送 /post 呼叫 API 的最佳方式。 9. 簡單展示前端 model 完全沒有對應到後端資料的的例子。 <br> # 前後端架構本質上的差異 現在大家都知道,MVC 不適用在前端。許多人認為原因在於 controller。 (關於 controller 的討論,作者推薦[這篇文章](https://medium.freecodecamp.org/is-mvc-dead-for-the-frontend-35b4d1fe39ec))。 但是更深層的原因可能在於:**我們嘗試將後端的架構套用到前端**。 前後端真的這麼不同嗎? 作者認為:是的。**在後端中解決問題的方式,跟在前端中解決問題的方式,有本質上的不同。** - 後端開發必須達成關於 entity 的邏輯抽象表現,為了能夠在不同的執行環境中,對這些 entity 進行操作,例如外部使用者以及外部應用程式。 - 前端開發必須達成關於 view 的邏輯抽象表現,不只為了讓使用者的能有順暢的互動,也為了確保實作方式是**優化過**(因為程式必須執行在瀏覽器中),並且**可擴展**(因為比起後端來說,前端客戶端的需求改變的比較快)。 這個差異直到大約 2010 年 SPA 開始流行的時候都沒有那麼重要,在這之前,前端是由簡單的 CSS、HTML、以及一些零星的 JavaScript 的構成,而前端開發被認為不需要特殊設計模式或開發方式。 如果某人想要用前端的開發方式來開發後端將會發生什麼事?他在設計 entity 的邏輯結構時,會只考慮在 view 上怎麼使用到這些 entity。他只會建立一些他需要做的功能集合,而不考慮這些功能在 entity 的抽象邏輯上是否有較深層的後果。這會導致後端naif and unstable。 **如果我們想用開發後端的方式來開發前端,也會有一樣的結果。** One side of this problem is of course the unclear role of controllers in MVC, as already mentioned; but in my opinion a deeper and more hidden architectural lack is when a developer with Back-End mentality tries to define a structure of JavaScript models which are the exact clone of their Back-End counterparts. <br> ## 前後端的 model 在 Angular 中有什麼差異 前端和後端的 model 都是一種抽象化,但這並不表示他們必須一樣:**因為他們是對於不同事情的抽象化。** 一個能明顯看出差異的例子是,後端 model 會包含前端沒有用到的內部屬性,反過來說,前端 model 也可能包含跟後端無關的屬性。 在 Angular 的世界中,當 DOM 會因為 app 的狀態改變而更新時,事情就變得重要了,*"app 狀態改變"* 指的是什麼?這種狀態怎麼儲存?他怎麼關連到 model? 在 Angular 2+ 中,app 狀態儲存在 **components**,但是 components 實際上是什麼?他們怎麼與 DOM 互動?[Angular 2+ 的官方文件對於資料綁定機制的說明:](https://angular.io/guide/template-syntax#html-attribute-vs-dom-property): > 使用資料綁定,只是在處理綁定對象物件的屬性和事件。[...]資料綁定的對象是 DOM 中的某個東西,根據綁定類型不同,對象可能是一個 ( element | component | directive ) 的屬性,一個 ( element | component | directive ) 的事件,或是一個 attribute 名稱。 > 所以,**Angular 2+ 的 component 只是 DOM 的擴充**:DOM 可以被視為是一個由巢狀原生 element 和 component 組合的階層樹。看一個例子: ``` javascript=1 @Component({ selector: 'text-in-paragraph', template: ` <p>{{text}}</p> ` }) export class TextInParagraphComponent { @Input() text:string; } ``` 當我們想要使用上面的 component,我們簡單地在 HTML 中使用 tag 來識別,並傳入 *"text"* 變數,就像普通的 HTML attribute: ``` javascript=1 <text-in-paragraph text="Mary had a little lamb"> </text-in-paragraph> ``` 當我們使用 *"text-in-paragraph"* 這個 tag,看起來就像一個通常的 DOM 元素,並且它的確可以被認為是一個 DOM 元素,因為他表現的就是同樣的方式,加上一些擴充。 舉例來說,客製 component 比 DOM 多出一個功能,就是*資料綁定*: ``` javascript=1 <text-with-paragraph [text]="someVariable"> </text-with-paragraph> ``` 當我們將 *"text"* attribute 用括號包起來,意思是表達式會對應到父層元件中的變數:當變數的值改變時,Angular 2+ 會更新子層元件並且重新讀取它的 template。 這個 DOM 和 component 的類比是個簡單的例子,核心概念是 component 可以被認為是 DOM 的擴充。 一般而言,一個由巢狀原生 DOM 元素、component、directive 和他們的公開屬性所組成的樹,就是一個 Angular 2+ 提供的對於 DOM 的抽象化。 回到前後端 model 的差異,Angular 中對於 DOM 的擴充,可以被視為是一個第三方的抽象化,一種與前後端 model 都不同的 **DOM model**。 在 AngularJS 中概念一樣,但是有不同的實作方式:負責處理綁定給 DOM 變數的 entity,叫做 **controller**。即便 AngularJS 中的 controller 實際上並不是 DOM 的擴充,最好的實踐方式仍然是將他們跟 (前後端) model 分開。 <br> ## 清楚區分後端、前端、DOM model 我們區分三個不同層的抽象化: 1. 後端 model,處理 entity 的邏輯和一般性的抽象化。 2. 前端 model,表現前端中的 entity, 引用自 [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) : > 閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合。 在 [Vue.js](https://css-tricks.com/building-renderless-vue-components/) 中有 <code>Renderless Components</code>。 在 [React](https://reactjs.org/docs/render-props.html) 中有 <code>Render Props</code> 作者用兩個例子,對照上述兩個前端框架,實作 Angular 中無渲染的功能性元件 <br> ## 使用結構型指令 對照 Vue.js 的 [toggle](https://css-tricks.com/building-renderless-vue-components/) 例子: ``` html=1 <toggle> <div slot-scope="{ on, setOn, setOff }"> <button @click="click(setOn)">Blue pill</button> <button @click="click(setOff)">Red pill</button> <div> <span v-if="on">It's all a dream, go back to sleep.</span> <span v-else> I don't know how far the rabbit hole goes, I'm not a rabbit, neither do I measure holes. </span> </div> </div> </toggle> ``` <code>toggle</code> 元件負責提供 API 用來作為畫面渲染的開關。元件本身並不關注畫面的渲染或樣式。 Angular 中的結構型指令,藉由新增和移除 DOM 元素來改變 DOM 版面。並且能夠提供 <code>context</code> 物件讓其他元件使用。 下面是一個藉由 <code>context</code>,暴露 API 給外部的 <code>toggle</code> 元件: ``` javascript=1 type Toggle = { on: boolean; setOn: Function; setOff: Function; toggle: Function; } @Directive({ selector: '[toggle]' }) export class ToggleDirective implements OnInit { on = true; @Input('toggleOn') initialState = true; constructor(private tpl: TemplateRef<{ $implicit: Toggle }>, private vcr: ViewContainerRef) { } ngOnInit() { this.on = this.initialState; this.vcr.createEmbeddedView(this.tpl, { $implicit: { on: this.on, setOn: this.setOn, setOff: this.setOff, toggle: this.toggle, } }); } setOn() { this.on = true } setOff() { this.on = false } toggle() { this.on = !this.on } } ``` 藉由 <code>this.vcr.createEmbeddedView()</code>,我們創造畫面(第一個參數) 並且將 API 藉由 <code>context</code> (第二個參數) 暴露給外部。其中 <code>TemplateRef</code> 的泛型會做為暴露給外部的 <code>context</code> 的類型,IDE 會做型別檢查。 使用 <code>toggle</code> 元件: ``` html=1 <div *toggle="let controller; on: false"> <button (click)="controller.setOn()">Blue pill</button> <button (click)="controller.setOff()">Red pill</button> <div> <span *ngIf="controller.on">...</span> <span *ngIf="!controller.on">...</span> </div> </div> ``` ![IDE 檢查](https://miro.medium.com/max/875/1*guK7ENQ91LzKcQs40Ai3QQ.gif) <br> ## 使用 ExportAs 對照 React 的 [Render Props](https://reactjs.org/docs/render-props.html) 例子: ``` javascript=1 class Mouse extends React.Component { state = { x: 0, y: 0 }; handleMouseMove = (event) { this.setState({ x: event.clientX, y: event.clientY }); } render() { return ( <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}> {this.props.children(this.state)} </div> ); } } ``` React 的 <code>Render Props</code> 提供了一種方式,讓封裝在元件內的狀態和行為能夠分享給外部元件。上述例子中,<code>Mouse</code> 元件用來追蹤並保存滑鼠的位置。 因此可以像這樣使用 <code>Mouse</code> 元件: ``` javascript=1 class MouseTracker extends React.Component { render() { return ( <div> <h1>Move the mouse around!</h1> <Mouse> {mouse => ( <p>The mouse position is {mouse.x}, {mouse.y}</p> )} </Mouse> </div> ); } } ``` 在 Angular 中可以用 <code>exportAs</code> 來實現這樣的功能性元件: ``` javascript=1 @Component({ selector: 'mouse', exportAs: 'mouse', template: ` <div (mousemove)="handleMouseMove($event)"> <ng-content></ng-content> </div> ` }) export class MouseComponent { private _state = { x: 0, y: 0 }; get state() { return this._state; }; handleMouseMove(event) { this._state = { x: event.clientX, y: event.clientY }; } } ``` <code>MouseComponent</code> 並不關注畫面內容,而是藉由 <code>@Component()</code> 裝飾器中 <code>exportAs</code> 屬性,暴露出自己的 API,讓外部元件可以在範本中使用他的 API: ``` javascript=1 @Component({ selector: 'mouse-tracker', template: ` <mouse #mouse="mouse"> <p>The mouse position is {{ mouse.state.x }}, {{ mouse.state.y }}</p> </mouse> ` }) export class MouseTrackerComponent {} ``` 上面的範本中的 <code>#mouse="mouse"</code> 即是接取到 <code>MouseComponent</code> 的實例。 也可以用這種方式實作第一個例子中的 <code>toggle</code> 功能。 <br> ## 總結 使用結構型指令的好處,在於可以藉由定義 <code>context</code> 的型別,而明確定義哪些是要暴露給外部的 API,並且可以簡單地控制是否要根據狀態渲染畫面。 如果像 <code>MouseComponent</code> 的例子,還是必須具有部分的畫面(用來定義追蹤滑鼠移動的區塊)時,就可以使用 <code>@Component()</code> 的 <code>exportAs</code> 功能。