序
今年大前端的概念一而再再而三的被提及,那麼大前端時代究竟是什麼呢?大前端這個詞最早是因為在阿里內部有很多前端開發人員既寫前端又寫 Java 的 Velocity 模板而得來,不過現在大前端的範圍已經越來越大了,包含前端 + 移動端,前端、CDN、Nginx、Node、Hybrid、Weex、React Native、Native App。筆者是一名普通的全職 iOS 開發者,在接觸到了前端開發以後,發現了前端有些值得移動端學習的地方,於是便有了這個大前端時代系列的文章,希望兩者能相互借鑑優秀的思想。談及到大前端,常常被提及的話題有:元件化,路由與解耦,工程化(打包工具,腳手架,包管理工具),MVC 和 MVVM 架構,埋點和效能監控。筆者就先從元件化方面談起。網上關於前端框架對比的文章也非常多(對比 React,Vue,Angular),不過跨端對比的文章好像不多?筆者就打算以前端和移動端(以 iOS 平臺為主)對比為主,看看這兩端的不同做法,並討論討論有無相互借鑑學習的地方。
本文前端的部分也許前端大神看了會覺得比較基礎,如有錯誤還請各位大神不吝賜教。
Vue 篇
一. 元件化的需求
為了提高程式碼複用性,減少重複性的開發,我們就把相關的程式碼按照 template、style、script 拆分,封裝成一個個的元件。元件可以擴充套件
HTML 元素,封裝可重用的 HTML 程式碼,我們可以將元件看作自定義的 HTML 元素。在 Vue 裡面,每個封裝好的元件可以看成一個個的 ViewModel。
二. 如何封裝元件
談到如何封裝的問題,就要先說說怎麼去組織元件的問題。
如果在簡單的 SPA 專案中,可以直接用 Vue.component 去定義一個全域性元件,專案一旦複雜以後,就會出現弊端了:
-
全域性定義(Global definitions) 強制要求每個 component 中的命名不得重複
-
字串模板(String templates) 缺乏語法高亮,在 HTML 有多行的時候,需要用到醜陋的
-
不支援 CSS(No CSS support) 意味著當 HTML 和 JavaScript 元件化時,CSS 明顯被遺漏
-
沒有構建步驟(No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用前處理器,如 Pug (formerly Jade) 和 Babel
而且現在公司級的專案,大多數都會引入工程化的管理,用包管理工具去管理,npm 或者 yarn。所以 Vue 在複雜的專案中用 Vue.component 去定義一個元件的方式就不適合了。這裡就需要用到單檔案元件,還可以使用 Webpack 或 Browserify 等構建工具。比如下面這個Hello.vue元件,整個檔案就是一個元件。
在單檔案元件中,整個檔案都是一個 CommonJS 模組,裡面包含了元件對應的 HTML、元件內的處理邏輯 Javascript、元件的樣式 CSS。
在元件的 script 標籤中,需要封裝該元件 ViewModel 的行為。
-
data
元件的初始化資料,以及私有屬性。 -
props
元件的屬性,這裡的屬性專門用來接收父子元件通訊的資料。(這裡可以類比 iOS 裡面的 @property ) -
methods
元件內的處理邏輯函式。 -
watch
需要額外監聽的屬性(這裡可以類比 iOS 裡面的 KVO ) -
computed
元件的計算屬性 -
components
所用到的子元件 -
lifecycle hooks
生命週期的鉤子函式。一個元件也是有生命週期的,有如下這些:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、activated、deactivated、beforeDestroy、destroyed等生命週期。在這些鉤子函式裡面可以加上我們預設的處理邏輯。(這裡可以類比 iOS 裡面的 ViewController 的生命週期 )
如此看來,在 Vue 裡面封裝一個單檔案元件,和在 iOS 裡面封裝一個 ViewModel 的思路是完全一致的。接下來的討論無特殊說明,針對的都是單檔案元件。
三. 如何劃分元件
一般劃分元件分可以按照以下標準去劃分:
-
頁面區域:
header、footer、sidebar……
-
功能模組:
select、pagenation……
這裡舉個例子來說明一起前端是如何劃分元件的。
1. 頁面區域
還是以 objc中國 的首頁頁面為例
我們可以把上面的頁面按照佈局,先抽象圖片中間的樣子,然後接著按照頁面的區域劃分元件,最後可以得到最右邊的元件樹。
在 Vue 例項的根元件,載入 layout。
import Vue from `vue`;
import store from `./store`;
import router from `./router`;
import Layout from `./components/layout`;
new Vue({
el: `#app`,
router,
store,
template: `<Layout/>`,
components: {
Layout
}
});
根據抽象出來的元件樹,可以進一步的向下細分各個小元件。
layout 下一層的元件是 header、footer、content,這三部分就組成了 layout.vue 單檔案元件的全部部分。
上圖就是我們的 layout.vue 的全部實現。在這個單檔案元件中裡面引用了三個子元件,navigationBar、footerView、content。由於 content 裡面是又各個路由頁面組成,所以這裡宣告成 router-view。
至於各個子元件的具體實現這裡就不在贅述了,具體程式碼可以看這裡navigationBar.vue、footerView、layout.vue
2. 功能模組
一般專案裡面詳情頁的內容最多,我們就以以 objc中國 的詳情頁面為例
上圖左邊是詳情頁,右圖是按照功能區分的圖,我們把整個頁面劃分為6個子元件。
從上往下依次展開,見上圖。
經過功能上的劃分以後,整個詳情頁面的程式碼變的異常清爽,整個頁面就是6個單檔案的子元件,每個子元件的邏輯封裝在各自的元件裡面,詳情頁面就是把他們都組裝在了一起,程式碼可讀性高,後期維護也非常方便。
詳情頁面具體的程式碼在這裡https://github.com/halfrost/vue-objccn/blob/master/src/pages/productsDetailInfo.vue
6個子元件的程式碼在這裡https://github.com/halfrost/vue-objccn/tree/master/src/components/productsDetailInfo,具體的程式碼見連結,這裡就不在贅述了。
綜上可以看出,前端 SPA 頁面抽象出來就是一個大的元件樹。
四. 元件化原理
舉個例子:
<!DOCTYPE html>
<html>
<body>
<div id="app">
<parent-component>
</parent-component>
</div>
</body>
<script src="js/vue.js"></script>
<script>
var Child = Vue.extend({
template: `<p>This is a child component !</p>`
})
var Parent = Vue.extend({
// 在Parent元件內使用<child-component>標籤
template :`<p>This is a Parent component !</p><child-component></child-component>`,
components: {
// 區域性註冊Child元件,該元件只能在Parent元件內使用
`child-component`: Child
}
})
// 全域性註冊Parent元件
Vue.component(`parent-component`, Parent)
new Vue({
el: `#app`
})
</script>
</html>
在上面的例子中,在 <parent-component>
父元件裡面宣告瞭一個 <child-component>
,最終渲染出來的結果是:
This is a Parent component !
This is a child component !
上述程式碼的執行順序如下:
-
子元件先在父元件中的 components 中進行註冊。
-
父元件利用 Vue.component 註冊到全域性。
-
當渲染父元件的時候,渲染到
<child-component>
,會把子元件也渲染出來。
值得說明的一點是,Vue 進行模板解析的時候會遵循以下 html 常見的限制:
-
a 不能包含其它的互動元素(如按鈕,連結)
-
ul 和 ol 只能直接包含 li
-
select 只能包含 option 和 optgroup
-
table 只能直接包含 thead, tbody, tfoot, tr, caption, col, colgroup
-
tr 只能直接包含 th 和 td
五. 元件分類
元件的種類可分為以下4種:
-
普通元件
-
動態元件
-
非同步元件
-
遞迴元件
1. 普通元件
之前講的都是普通的元件,這裡就不在贅述了。
2. 動態元件
動態元件利用的是 is
的特性,可以設定多個元件可以使用同一個掛載點,並動態切換。
var vm = new Vue({
el: `#example`,
data: {
currentView: `home`
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
<component v-bind:is="currentView">
<!-- 元件在 vm.currentview 變化時改變! -->
</component>
現在 <component>
元件的具體型別用 currentView 來表示了,我們就可以通過更改 currentView 的值,來動態載入各個元件。上述例子中,可以不斷的更改 data 裡面的 currentView ,來達到動態載入 home、posts、archive 三個不同元件的目的。
3. 非同步元件
Vue允許將元件定義為一個工廠函式,在元件需要渲染時觸發工廠函式動態地解析元件,並且將結果快取起來:
Vue.component("async-component", function(resolve, reject){
// async operation
setTimeout(function() {
resolve({
template: `<div>something async</div>`
});
},1000);
});
動態元件可配合 webpack 實現程式碼分割,webpack 可以將程式碼分割成塊,在需要此塊時再使用 ajax 的方式下載:
Vue.component(`async-webpack-example`, function(resolve) {
// 這個特殊的 require 語法告訴 webpack
// 自動將編譯後的程式碼分割成不同的塊,
// 這些塊將通過 ajax 請求自動下載。
require([`./my-async-component`], resolve)
});
4. 遞迴元件
如果一個元件設定了 name 屬性,那麼它就可以變成遞迴元件了。
遞迴元件可以利用模板裡面的 name 不斷的遞迴呼叫自己。
name: `recursion-component`,
template: `<div><recursion-component></recursion-component></div>`
上面這段程式碼是一個錯誤程式碼,這樣寫模板的話就會導致遞迴死迴圈,最終報錯 “max stack size exceeded”。解決辦法需要打破死迴圈,比如 v-if 返回 false。
六. 元件間的訊息傳遞和狀態管理
在 Vue 中,元件訊息傳遞的方式主要分為3種:
-
父子元件之間的訊息傳遞
-
Event Bus
-
Vuex 單向資料流
1. 父子元件之間的訊息傳遞
父子元件的傳遞方式比較單一,在 Vue 2.0 以後,父子元件的關係可以總結為 props down, events up 。父元件通過 props 向下傳遞資料給子元件,子元件通過 events 給父元件傳送訊息。
父向子傳遞
舉個例子:
Vue.component(`child`, {
// 宣告 props
props: [`msg`],
// prop 可以用在模板內
// 可以用 `this.msg` 設定
template: `<span>{{ msg }}</span>`
})
<child msg="hello!"></child>
在 child 元件的 props 中宣告瞭一個 msg 屬性,在父元件中利用這個屬性把值傳給子元件。
這裡有一點需要注意的是,在非字串模板中, camelCased (駝峰式) 命名的 prop 需要轉換為相對應的 kebab-case (短橫線隔開式) 命名。
上面這個例子是靜態的繫結,Vue 也支援動態繫結,這裡也支援 v-bind 指令進行動態的繫結 props 。
父向子傳遞是一個單向資料流的過程,prop 是單向繫結的:當父元件的屬性變化時,將傳導給子元件,但是不會反過來。這是為了防止子元件無意修改了父元件的狀態——這會讓應用的資料流難以理解。
另外,每次父元件更新時,子元件的所有 prop 都會更新為最新值。這意味著你不應該在子元件內部改變 prop。Vue 建議子元件的 props 是 immutable 的。
這裡就會牽涉到2類問題:
-
由於單向資料流的原因,會導致子元件的資料或者狀態和父元件的不一致,為了同步,在子元件裡面反資料流的去修改父元件的資料或者資料。
-
子元件接收到了 props 的值以後,有2種原因想要改變它,第一種原因是,prop 作為初始值傳入後,子元件想把它當作區域性資料來用;第二種原因是,prop 作為初始值傳入,由子元件處理成其它資料輸出。
這兩類問題,開發者強行更改,也都是可以實現的,但是會導致不令人滿意的 “後果” 。第一個問題強行手動修改父元件的資料或者狀態以後,導致資料流混亂不堪。只看父元件,很難理解父元件的狀態。因為它可能被任意子元件修改!理想情況下,只有元件自己能修改它的狀態。第二個問題強行手動修改子元件的 props 以後,Vue 會在控制檯給出警告。
如果優雅的解決這2種問題呢?一個個的來說:
(1)第一個問題,換成雙向繫結就可以解決。
在 Vue 2.3.0+ 以後的版本,雙向繫結有2種方式
第一種方式:
利用 .sync
修飾符,在 Vue 2.3.0+ 以後作為一個編譯時的語法糖存在。它會被擴充套件為一個自動更新父元件屬性的 v-on 偵聽器。
// 宣告一個雙向繫結
<comp :foo.sync="bar"></comp>
// 上面一行程式碼會被會被擴充套件為下面這一行:
<comp :foo="bar" @update:foo="val => bar = val"></comp>
// 當子元件需要更新 foo 的值時,它會顯式地觸發一個更新事件:
this.$emit(`update:foo`, newValue)
第二種方式:
自定義事件可以用來建立自定義的表單輸入元件,使用 v-model 來進行資料雙向繫結。
<input :value="value" @input="updateValue($event.target.value)" >
在這種方式下進行的雙向繫結必須滿足2個條件:
-
接受一個 value 屬性
-
在有新的值時觸發 input 事件
官方推薦的2種雙向繫結的方式就是上述2種方法。不過還有一些隱性的雙向繫結,可能無意間就會造成bug的產生。
pros 是單向資料傳遞,父元件把資料傳遞給子元件,需要尤其注意的是,傳遞的資料如果是引用型別(比如陣列和物件),那麼預設就是雙向資料繫結,子元件的更改都會影響到父元件裡面。在這種情況下,如果人為不知情,就會出現一些莫名其妙的bug,所以需要注意引用型別的資料傳遞。
(2)第二個問題,有兩種做法:
-
第一種做法是:定義一個區域性變數,並用 prop 的值初始化它:
props: [`initialCounter`],
data: function () {
return { counter: this.initialCounter }
}
-
第二種做法是:定義一個計算屬性,處理 prop 的值並返回。
props: [`size`],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
父向子傳遞還可以傳遞模板,使用 slot 分發內容。
slot 是 Vue 的一個內建的自定義元素指令。slot 在 bind 回撥函式中,根據 name 獲取將要替換插槽的元素,如果上下文環境中有所需替換的內容,則呼叫父元素的 replaceChild 方法,用替換元素講 slot 元素替換;否則直接刪除將要替換的元素。如果替換插槽元素中有一個頂級元素,且頂級元素的第一子節點為 DOM 元素,且該節點有 v-if 指令,且 slot 元素中有內容,則替換模板將增加 v-else 模板放入插槽中的內容。如果 v-if 指令為 false,則渲染 else 模板內容。
子向父傳遞
子元件要把資料傳遞迴父元件,方式很單一,那利用自定義事件!
父元件使用 $on(eventName) 監聽事件
子元件使用 $emit(eventName) 觸發事件
舉個簡單的例子:
// 在子元件裡面有一個 button
<button @click="emitMyEvent">emit</button>
emitMyEvent() {
this.$emit(`my-event`, this.hello);
}
// 在父元件裡面監聽子元件的自定義事件
<child @my-event="getMyEvent"></child>
getMyEvent() {
console.log(` i got child event `);
}
這裡也可以通過父子之間的關係進行傳遞資料(直接修改資料),但是不推薦這種方法,例如 this.$parent 或者 this.$children 直接呼叫父或者子元件的方法,這裡類比iOS裡面的ViewControllers方法,在這個陣列裡面可以直接拿到所有 VC ,然後就可以呼叫他們暴露在.h裡面的方法了。但是這種方式相互直接耦合性太大了。
2. Event Bus
Event Bus 這個概念對移動端的同學來說也比較熟悉,因為在安卓開發中就有這個概念。在 iOS 開發中,可以類比訊息匯流排。具體實現可以是通知 Notification 或者 ReactiveCocoa 中的訊號傳遞。
Event Bus 的實現還是藉助 Vue 的例項。新建一個新的 Vue,專門用來做訊息匯流排。
var eventBus = new Vue()
// 在 A 元件中引入 eventBus
eventBus.$emit(`myEvent`, 1)
// 在要監聽的元件中監聽
eventBus.$on(`id-selected`, () => {
// ...
})
3. Vuex 單向資料流
由於本篇文章重點討論元件化的問題,所以這裡 Vuex 只是說明用法,至於原理的東西之後會單獨開一篇文章來分析。
這一張圖就描述了 Vuex 是什麼。Vuex 專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
上圖中箭頭的指向就描述了資料的流向。資料的流向是單向的,從 Actions 流向 State,State 中的資料改變了從而影響到 View 展示資料的變化。
從簡單的 Actions、State、View 三個角色,到現在增加了一個 Mutations。Mutations 現在變成了更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。Vuex 中的 mutations 非常類似於事件:每個 mutation 都有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)。
一般在元件中進行 commit 呼叫 Mutation 方法
this.$store.commit(`increment`, payload);
Actions 和 Mutations 的區別在於:
-
Action 提交的是 mutation,而不是直接變更狀態。
-
Action 可以包含任意非同步操作,而 Mutations 必須是同步函式。
一般在元件中進行 dispatch 呼叫 Actions 方法
this.$store.dispatch(`increment`);
Vuex 官方針對 Vuex 的最佳實踐,給出了一個專案模板結構,希望大家都能按照這種模式去組織我們的專案。
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API請求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我們組裝模組並匯出 store 的地方
├── actions.js # 根級別的 action
├── mutations.js # 根級別的 mutation
└── modules
├── cart.js # 購物車模組
└── products.js # 產品模組
關於這個例子的詳細程式碼在這裡
七. 元件註冊方式
元件的註冊方式主要就分為2種:全域性註冊和區域性註冊
1. 全域性註冊
利用 Vue.component 指令進行全域性註冊
Vue.component(`my-component`, {
// 選項
})
註冊完的元件就可以在父例項中以自定義元素 <my-component></my-component>
的形式使用。
// 註冊
Vue.component(`my-component`, {
template: `<div>A custom component!</div>`
})
// 建立根例項
new Vue({
el: `#example`
})
<div id="example">
<my-component></my-component>
</div>
2. 區域性註冊
全域性註冊元件會拖慢一些頁面的載入速度,有些元件只需要用的到時候再載入,所以不必在全域性註冊每個元件。於是就有了區域性註冊的方式。
var Child = {
template: `<div>A custom component!</div>`
}
new Vue({
// ...
components: {
// <my-component> 將只在父模板可用
`my-component`: Child
}
})
iOS 篇
一. 元件化的需求
在 iOS Native app 前期開發的時候,如果參與的開發人員也不多,那麼程式碼大多數都是寫在一個工程裡面的,這個時候業務發展也不是太快,所以很多時候也能保證開發效率。
但是一旦專案工程龐大以後,開發人員也會逐漸多起來,業務發展突飛猛進,這個時候單一的工程開發模式就會暴露出弊端了。
-
專案內程式碼檔案耦合比較嚴重
-
容易出現衝突,大公司同時開發一個專案的人多,每次 pull 一下最新程式碼就會有很多衝突,有時候合併程式碼需要半個小時左右,這會耽誤開發效率。
-
業務方的開發效率不夠高,開發人員一多,每個人都只想關心自己的元件,但是卻要編譯整個專案,與其他不相干的程式碼糅合在一起。除錯起來也不方便,即使開發一個很小的功能,都要去把整個專案都編譯一遍,除錯效率低。
為了解決這些問題,iOS 專案就出現了元件化的概念。所以 iOS 的元件化是為了解決上述這些問題的,這裡與前端元件化解決的痛點不同。
iOS 元件化以後能帶來如下的好處:
-
加快編譯速度(不用編譯主客那一大坨程式碼了,各個元件都是靜態庫)
-
自由選擇開發姿勢(MVC / MVVM / FRP)
-
方便 QA 有針對性地測試
-
提高業務開發效率
iOS 元件化的封裝性只是其中的一小部分,更加關心的是如何拆分元件,如何解除耦合。前端的元件化可能會更加註重元件的封裝性,高可複用性。
二. 如何封裝元件
iOS 的元件化手段非常單一,就是利用 Cocoapods 封裝成 pod 庫,主工程分別引用這些 pod 即可。越來越多的第三方庫也都在 Cocoapods 上釋出自己的最新版本,大公司也在公司內部維護了公司私有的 Cocoapods 倉庫。一個封裝完美的 Pod 元件,主工程使用起來非常方便。
具體如果用 Cocoapods 打包一個靜態庫 .a 或者 framework ,網上教程很多,這裡給一個連結,詳細的操作方法就不再贅述了。
最終想要達到的理想目標就是主工程就是一個殼工程,其他所有程式碼都在元件 Pods 裡面,主工程的工作就是初始化,載入這些元件的,沒有其他任何程式碼了。
三. 如何劃分元件
iOS 劃分元件雖然沒有一個很明確的標準,因為每個專案都不同,劃分元件的粗粒度也不同,但是依舊有一個劃分的原則。
App之間可以重用的 Util、Category、網路層和本地儲存 storage 等等這些東西抽成了 Pod 庫。還有些一些和業務相關的,也是在各個App之間重用的。
原則就是:要在App之間共享的程式碼就應該抽成 Pod 庫,把它們作為一個個元件。不在 App 間共享的業務線,也應該抽成 Pod,解除它與工程其他的檔案耦合性。
常見的劃分方法都是從底層開始動手,網路庫,路由,MVVM框架,資料庫儲存,加密解密,工具類,地圖,基礎SDK,APM,風控,埋點……從下往上,到了上層就是各個業務方的元件了,最常見的就類似於購物車,我的錢包,登入,註冊等。
四. 元件化原理
iOS 的元件化是藉助 Cocoapods 完成的。關於 Cocoapods 的具體工作原理,可以看這篇文章《CocoaPods 都做了什麼?》。
這裡簡單的分析一下 pod 進來的庫是什麼載入到主工程的。
pod 會依據 Podfile 檔案裡面的依賴庫,把這些庫的原始碼下載下來,並建立好 Pods workspace。當程式編譯的時候,會預先執行2個 pod 設定進來的指令碼。
在上面這個指令碼中,會把 Pods 裡面的打包好的靜態庫合併到 libPods-XXX.a 這個靜態庫裡面,這個庫是主工程依賴的庫。
上圖就是給主專案載入 Pods 庫的指令碼。
Pods 另外一個指令碼是載入資源的。見下圖。
這裡載入的資源是 Pods 庫裡面的一些圖片資源,或者是 Boudle 裡面的 xib ,storyboard,音樂資源等等。這些資源也會一起打到 libPods-XXX.a 這個靜態庫裡面。
上圖就是載入資源的指令碼。
五. 元件分類
iOS 的元件主要分為2種形式:
-
靜態庫
-
動態庫
靜態庫一般是以 .a 和 .framework 結尾的檔案,動態庫一般是以 .dylib 和 .framework 結尾的檔案。
這裡可以看到,一個 .framework 結尾的檔案僅僅通過檔案型別是無法判斷出它是一個靜態庫還是一個動態庫。
靜態庫和動態庫的區別在於:
-
.a檔案肯定是靜態庫,.dylib肯定是動態庫,.framework可能是靜態庫也可能是動態庫;
-
靜態庫在連結其他庫的情況時,它會被完整的複製到可執行檔案中,如果多個App都使用了同一個靜態庫,那麼每個App都會拷貝一份,缺點是浪費記憶體。類似於定義一個基本變數,使用該基本變數是是新複製了一份資料,而不是原來定義的;靜態庫的好處很明顯,編譯完成之後,庫檔案實際上就沒有作用了。目標程式沒有外部依賴,直接就可以執行。當然其缺點也很明顯,就是會使用目標程式的體積增大。
-
動態庫不會被複制,只有一份,程式執行時動態載入到記憶體中,系統只會載入一次,多個程式共用一份,節約了記憶體。而且使用動態庫,可以不重新編譯連線可執行程式的前提下,更新動態庫檔案達到更新應用程式的目的。
六. 元件間的訊息傳遞和狀態管理
之前我們討論過了,iOS 元件化十分關注解耦性,這算是元件化的一個重要目的。iOS 各個元件之間訊息傳遞是用路由來實現的。關於路由,筆者曾經寫過一篇比較詳細的文章,感興趣的可以來看這篇文章《iOS 元件化 —— 路由設計思路分析》。
七. 元件註冊方式
iOS 元件註冊的方式主要有3種:
-
load方法註冊
-
讀取 plist 檔案註冊
-
Annotation註解方式註冊
前兩種方式都比較簡單,容易理解。
第一種方式在 load 方法裡面利用 Runtime 把元件名和元件例項的對映關係儲存到一個全域性的字典裡,方便程式啟動以後可以隨時呼叫。
第二種方式是把元件名和元件例項的對映關係預先寫在 plist 檔案中。程式需要的時候直接去讀取這個 plist 檔案。plist 檔案可以從伺服器讀取過來,這樣 App 還能有一定的動態性。
第三種方式比較黑科技。利用的是 Mach-o 的資料結構,在程式程式設計連結成可執行檔案的時候,就把相關注冊資訊直接寫入到最終的可執行檔案的 Data 資料段內。程式執行以後,直接去那個段內去讀取想要的資料即可。
關於這三種做法的詳細實現,可以看筆者之前的一篇文章《BeeHive —— 一個優雅但還在完善中的解耦框架》,在這篇文章裡面詳細的分析了上述3種註冊過程的具體實現。
總結
經過上面的分析,我們可以看出 Vue 的元件化和 iOS 的元件化區別還是比較大的。
兩者平臺上開發方式存在差異
主要體現在單頁應用和類多頁應用的差異。
現在前端比較火的一種應用就是單頁Web應用(single page web application,SPA),顧名思義,就是隻有一張Web頁面的應用,是載入單個HTML 頁面並在使用者與應用程式互動時動態更新該頁面的Web應用程式。
瀏覽器從伺服器載入初始頁面,以及整個應用所需的指令碼(框架、庫、應用程式碼)和樣式表。當使用者定位到其他頁面時,不會觸發頁面重新整理。通過 HTML5 History API 更新頁面的 URL 。瀏覽器通過 AJAX 請求檢索新頁面(通常以 JSON 格式)所需的新資料。然後, SPA 通過 JavaScript 動態更新已經在初始頁面載入中已經下載好的新頁面。這種模式類似於原生手機應用的工作原理。
但是 iOS 開發更像類 MPA (Multi-Page Application)。
往往一個原生的 App ,頁面差不多應該是上圖這樣。當然,可能有人會說,依舊可以把這麼多頁面寫成一個頁面,在一個 VC 裡面控制所有的 View,就像前端的 DOM 那樣。這種思路雖然理論上是可行的,但是筆者沒有見過有人這麼做,頁面一多起來,100多個頁面,上千個 View,都在一個 VC 上控制,這樣開發有點蛋疼。
兩者解決的需求也存在差異
iOS 的元件化一部分也是解決了程式碼複用性的問題,但是更多的是解決耦合性大,開發效率合作性低的問題。而 Vue 的元件化更多的是為了解決程式碼複用性的問題。
兩者的元件化的方向也有不同。
iOS 平臺由於有 UIKit 這類蘋果已經封裝好的 Framework,所以基礎控制元件已經封裝完成,不需要我們自己手動封裝了,所以 iOS 的元件著眼於一個大的功能,比如網路庫,購物車,我的錢包,整個業務塊。前端的頁面佈局是在 DOM 上進行的,只有最基礎的 CSS 的標籤,所以控制元件都需要自己寫,Vue 的元件化封裝的可複用的單檔案元件其實更加類似於 iOS 這邊的 ViewModel。
所以從封裝性上來講,兩者可以相互借鑑的地方並不多。iOS 能從前端借鑑的東西在狀態管理這一塊,單向資料流的思想。不過這一塊思想雖然好,但是如何能在自家公司的app上得到比較好的實踐,依舊是仁者見仁智者見智的事了,並不是所有的業務都適合單向資料流。
Reference:
Vue.js 官方文件
GitHub Repo:Halfrost-Field
Follow: halfrost · GitHub