天天看點

weex-html5 元件進階前言元件基礎元件擴充實踐小結

先來回味一下前篇中提到的,在元件擴充過程中可能遇到的問題:

在元件的 constructor 裡需要幹些什麼?

在元件的其他方法中分别需要做哪些事情?

有哪些可以直接調用的父類的原型方法?

元件從注冊到渲染到頁面上的執行流程是怎樣的?

先不着急回答這幾個問題,我們先來了解一下 weex-html5 元件架構的基本原理。

weex-html5 的所有元件都是從一個最基礎的基類繼承而來,基類中包含基本的 渲染操作 和一些 __輔助方法__。每個元件都有一個 id 用于 jsfm (weex-jsframework) 對其進行索引。在web渲染端,管理這些索引,以及響應 jsfm 的操作指令,并做一些元件渲染周期的管理,這些事情是由元件的管理者 <code>componentmanager</code> 負責的。每一個 weex 的執行個體都包含一個 <code>componentmanager</code> 的執行個體。

每個元件在定義的時候都需要實作一個 <code>init</code> 方法,用于 weex 對該元件進行注冊。在 <code>weex.install(yourcomponent)</code> 的過程中會向這個方法裡注入 weex 這個類。你可以通過 <code>weex.component</code> 擷取到這個類的構造函數,也可以通過 <code>weex.atomic</code> 擷取 atomic 類的構造函數(其他暴露在 weex 上的靜态屬性還包括 <code>componentmanager</code>, <code>utils</code> 以及 <code>config</code>)。

component 是 weex-html5 自定義元件的始祖,一切元件包括 atomic 都是從這個基類繼承而來。 atomic 是 不包含任何子元件 的元件,相比 component 來說有更嚴格的限制,并且不需要重寫它的 <code>createchildren</code>、<code>appendchild</code>、<code>inserbefore</code>以及 <code>removechild</code> 等操作子節點的方法。簡單來說,如果你要定義一個可以有子元件的元件,那麼繼承 component 就可以,如果你要定義一個不應當包含任何子元件的元件(比如表單元件 input),那麼需要繼承 atomic.

weex-html5 元件進階前言元件基礎元件擴充實踐小結

下面對這些方法分别進行介紹:

<code>getweexinstance</code> 擷取目前的 weex 執行個體

<code>getcomponentmanager</code> 擷取目前 weex 執行個體對應的 componentmanager 執行個體

<code>getparent</code> 擷取父元件

<code>getparentscroller</code> 向上擷取最近的 scrollable 元件(滑動元件,目前有 list、scroller 等 ),如果不在 scrollable 元件内部,則傳回 null

<code>getrootscroller</code> 擷取最頂層的 scrollable 元件

<code>getrootcontainer</code> 擷取目前 weex 頁面的 root 節點,一般是 <code>document.body</code> 下 id 為 weex 的節點

<code>isscrollable</code> 目前元件是否是 scrollable 元件

<code>isinscrollable</code> 目前元件是否是其他 scrollable 元件的子孫元件

渲染操作相關方法是 weex 元件渲染執行流程中的重要環節。weex 元件的執行流程可以在 component 元件的構造函數中找到:

從代碼裡可以看出一個元件的基本構造流程為:

這個元件構造完畢後需要挂載到某個頁面中已經存在的父節點中,這時候就會(通過 componentmanager )調用父節點的 <code>appendchild</code> 或 <code>insertbefore</code> 方法,是以這兩個方法也非常重要,但是一般元件不需要重寫這兩個方法,除非需要在這裡做一些特殊的邏輯處理。

weex-container 和 weex-element 類的預設樣式如下:

<code>createchildren</code> 建立子節點。這裡僅限于建立 data.children 中的節點,如果目前節點的 append 方式 是 <code>append="node"</code> 這種 weex 的預設處理方式,那麼子節點不會被塞到 data.children 裡處理,如果目前節點的 append 方式是 <code>append="tree"</code> 方式,此時該節點的子節點都需要父節點通過處理 data.children 來建立。如果你不知道怎麼處理 data.children 裡的每個數組元素,可以直接丢給 <code>componentmanager.createelement(data.children[i])</code> 來處理。實際上 component 基類裡就是這麼做的,當然你也可以自己去做一些特殊處理

<code>appendchild</code> 添加一個子節點到子節點清單的末尾

<code>insertbefore</code> 添加一個子節點到指定的位置 (指定 <code>index</code>)

<code>removechild</code> 删除一個子節點

<code>updateattrs</code> 更新屬性值,不推薦直接重寫此方法,後面會介紹如何配置屬性的 setter

<code>updatestyle</code> 更新樣式,不推薦直接重寫此方法,後面會介紹如何配置樣式的 setter

<code>bindevents</code> 綁定事件,不推薦直接重寫此方法,後面會介紹如何配置事件的額外參數以及 updator

<code>unbindevents</code> 解綁事件,一般不需要重寫此方法

<code>onappend</code> 元件被挂載到頁面時的執行勾子,基類已經在這裡做了一些處理,不要直接重寫這個方法

<code>addappendhandler</code> 為元件添加挂載時的執行勾子,如果想要在元件被挂載時執行一些代碼,可以調用這個方法

<code>dispatchevent</code> 在目前元件的 node 上觸發一個事件(如果 dsl 開發者綁定了這個事件類型的監聽器,那麼這個監聽器會被觸發)

<code>enablelazyload</code> 為目前元件的 node 節點指定懶加載的屬性 <code>img-src</code> 為某個指定的 src,這樣懶加載控制器會識别目前節點為 image 并對目前節點進行懶加載控制,這個一般不會用到,開發者比較常用的是下一個方法

<code>firelazyload</code> 用于手動觸發某個元件或者節點内部的 image 元件進行懶加載。之是以會有這個方法是由于某些特殊情況下元件進入了視口,其中的圖檔節點應該進行加載時,懶加載因為某些原因卻沒有正确執行,這種情況下就可以手動調用此方法

配置資訊是友善元件對屬性、樣式和事件進行定制的一種手段。通過配置可以簡化代碼,規範元件行為,避免不必要的代碼備援,提高代碼複用度,友善開發者進行擴充。

<code>attr</code> 屬性 setter 配置,基類的為空對象

<code>style</code> 基類配置兩組樣式的 setter, 一組和 <code>positon</code> 相關,即除了基本的 <code>relative</code> 和 <code>absolute</code>,更支援了 position 的 <code>fixed</code>, <code>sticky</code> 值。另外對 <code>flex</code> 相關的樣式做了歸一化的處理,開發者不用去寫多套 flex 降級名稱。歸一化後的 flex 樣式及其支援的值為:

樣式名

支援的值

flex

number

align-items

flex-start, flex-end, stretch, center

justify-content

flex-start, flex-end, center, space-between

<code>event</code> 事件配置,基類的為空對象

在後面的 元件擴充實踐 小節裡對如何做元件配置做了詳細解釋。

componentmanager 是元件的大管家,不僅僅需要管理目前注冊的元件類型,管理目前 weex 執行個體的所有元件以及它們的 ref id,還要負責監聽元件的 appear 事件,判斷目前的渲染狀态,并串聯處理元件生命周期的各個階段。

weex-html5 元件進階前言元件基礎元件擴充實踐小結

componentmanager 包含幾個靜态方法,可以直接通過 <code>componentmanager.xxx</code> 調用。

<code>getinstance(id)</code> 擷取對應 id 的 componentmanager 執行個體

<code>registercomponent</code> 注冊元件,我們自定義的元件在内部就是通過這個方法注冊進來的

<code>getscrollabletypes</code> 擷取 scrollable 元件類型(比如 <code>list</code>, <code>scroller</code> 等)的數組

每個 componentmanager 執行個體都實作了 jsfm 裡 vdom 的 listener 的裡的方法,在 jsfm 裡的 listener 負責建立對應虛拟 dom 操作的指令發送給 native 端的 <code>callnative</code> 橋接器。而在 weex-html5 裡 componentmanager 接管了這個接口,并将 dom 操作的指令轉換為真實的元件增添删除以及其他操作。

這些元件操作相關的方法,在 native 平台以及舊版的 weex-html5(&lt; v0.3.0) 中是通過 bridge 的 <code>callnative</code> 接收 <code>dom</code> 子產品的 api 調用實作的。在新版本的 weex-html5 (&gt;= v0.3.0)中 componentmanager 接管了 <code>dom</code> 子產品的幾乎所有方法(除了 <code>scrolltoelement</code> 這個比較特殊的方法)。

這些方法包括:

<code>createbody</code> 建立頁面根節點

<code>addelement</code> 添加一個元件

<code>removeelement</code> 移除一個元件

<code>moveelement</code> 移動一個元件

<code>setattr</code> 更新屬性值

<code>setstyle</code> 更新樣式

<code>setstyles</code> 更新多個樣式

<code>addevent</code> 添加事件監聽

<code>removeevent</code> 移除事件監聽

上述方法對于我們元件開發者來說其實是透明的,一般不會被用到。另一些方法則是你可能會用到的:

<code>getcomponent</code> weex 裡每個元件都有一個唯一的 id, 在 weex 裡這個 id 叫做 ref. componentmanager 内部存儲了一個元件的 ref 和執行個體映射的 map. 一些針對某個元件進行的操作,比如 <code>setattr</code>, <code>setstyle</code>, <code>removeelement</code> 等都會根據這個 map 去查找對應 ref 的元件。componentmanager 同時提供了這個專門通過 ref 擷取對應元件的方法。 在元件中可以通過 <code>this.getcomponentmanager().getcomponent(ref)</code> 擷取某個元件。

<code>createelement</code> 如果你要自己實作元件的 <code>appendchild</code> 或者 <code>createchildren</code> 等方法,你得到的入參一般是元件的 data,這時調用 componentmanager 的 <code>createelement(data)</code> 傳回的就是對應 <code>data.type</code> 指定類型的元件的執行個體。這時這個方法是必須調用的,因為隻有 componentmanager 中有注冊的元件類型資訊。

另一些你不會直接調用,但是在元件裡可能會間接用到的方法:

<code>rendering</code> weex 元件基于優化的考慮會在元件做頻繁 dom 操作期間做一些提高可用性減少頁面阻塞的操作。componentmanager 通過這個方法向 global (window) 對象注冊了兩個事件,<code>renderbegin</code> 和 <code>renderend</code>. 某個固定時間間隔内(預設為 800ms)沒有任何 dom 操作時會出發 <code>renderend</code> 事件,一旦有 dom 操作就會觸發 <code>renderbegin</code>. 這裡的 dom 操作特指 <code>addelement</code>, <code>removeelement</code> 和 <code>moveelement</code> 這三個操作。如果你的元件需要在'頻繁 dom 操作期間'做一些優化操作,比如關閉某些特性,可以考慮監聽這兩個事件。

<code>handleappend</code> 在元件挂載之後調用 <code>onappend</code> 方法。你不會直接用到這個方法,但是元件的 <code>onappend</code> 的執行依賴這個方法。另外元件的 <code>appear</code> 和 <code>disappear</code> 事件也是在這裡綁定的,圖檔的懶加載也是在這裡觸發。

前面已經介紹了在基類 component 的構造函數裡的執行流程。這裡跳出單個元件的構造過程,我們來看整個頁面是如何被構造并渲染出來的。這個過程涉及到 jsfm 裡如何編譯模闆、綁定資料、監聽變化并構造虛拟 dom 等等,這些原理限于篇幅這裡就不多做介紹了,展開會是個很大的話題。我們把 jsfm 看作一個實體,來看 jsfm 和 render 之間的通信過程,以及 componentmanager 執行個體和各個元件之間的協作過程。

weex-html5 元件進階前言元件基礎元件擴充實踐小結

componentmanager 做為 listener 挂載在 jsfm 的 vdom(虛拟 dom) 裡,在 jsfm 的 document 執行個體裡包含它的引用。vdom 的所有添加删除元素的操作,都會觸發 componentmanager 的對應方法,轉變為真實的元件操作。

在 vdom 裡有個 documentelement 的概念,類似 html 裡的 documentelement,相當于整個頁面的根标簽。在這個根标簽裡添加的節點,被稱為 body. 在 append body 的過程中會調用 componentmanager 的 <code>createbody</code> 方法,這時 weex 頁面的根節點就是 <code>createbody</code> 這個方法建立出來的。

當 vdom 裡需要添加一個元素,首先觸發其父元素的 appendchild (<code>element.prototype.appendchild</code>) 或者 insertbefore (<code>element.prototype.insertbefore</code>) 方法,這個操作被翻譯到 listener 中,也就是 componentmanager 的 <code>addelement</code> 方法。在 componentmanager 中會根據傳入的 index 判斷是調用自己的 <code>appendchild</code> (在末尾添加元素) 還是 <code>insertbefore</code> (在中間插入元素)。

在 componentmanager 的 <code>appendchild</code> 和 <code>insertbefore</code> 方法中首先會根據 parentref 找到父元件,然後調用父元件的對應的方法(如果沒有 override 的話就是基類 component 的對應方法)。在這些方法中又會調用 componentmanager 的 <code>createelement</code> 方法建立要替添加的子元件。

子元件也可能會有子元素,如果 dsl 裡指定了一個元件的 append 屬性為 <code>append=tree</code> ,那麼添加這個元件的時候,它的子元件的資訊都放在了 <code>data.children</code> 裡。這時子元件的建構過程中會調用 <code>createchildren</code> 方法建立子元件。反之如果 dsl 不指定 append 或者指定其為 <code>append=node</code> (預設方式),此時 <code>data.children</code> 一般為空,而它的子元件會通過下一次的 <code>addelement</code> (<code>componentmanager.prototype.addelement</code>) 調用被建立。

整個頁面就是從 <code>createbody</code> 開始,接着向 body 節點裡 <code>addelement</code> 添加新的元件,并在這個新的元件裡 <code>addelement</code> 添加它自己的子元件,這樣不斷疊代構造出來的。每個元件再通過自身的構造渲染流程(<code>create</code>, <code>createchildren</code>, <code>updateattrs</code>, <code>updatestyle</code>, <code>bindevents</code> 等等),把自己按照一定的模闆和樣式渲染到頁面中。

基本原理是很簡單的,但是實際擴充元件的過程中,需要關注一些最佳實踐和注意事項。

一個元件執行個體化的入口總是它的構造函數。在基類 component 和 atomic 的構造函數裡已經做了大部分的函數調用,前面已經提過,它們的執行順序是:

在自定義元件的構造函數裡就不需要重複去做這些事情了,隻需要調用基類的構造函數即可:

這樣這個元件就可以跑起來。當然你可以向其中添加一些其他的邏輯,比如存儲 data 裡的屬性,做一些初始化的操作,這取決于你元件所承載的功能。建議元件内部抽象的以及資料相關的初始化邏輯放到元件的構造函數裡,而涉及到具體 dom 建構和操作的初始化邏輯放到 <code>create</code> 方法裡。

weex-html5 元件進階前言元件基礎元件擴充實踐小結

這裡 <code>value</code> 是一個 setter,接受新的屬性值(<code>val</code>)做為參數,你所要做的事情就是定義這個 setter,每次這個屬性更新的時候這個 setter 都會被執行。在 setter 裡的 <code>this</code> 會綁定為目前的 component 執行個體。

這裡 <code>txtcolor</code> 的 setter 的參數接受的是 <code>txt-color</code> 的樣式值。同理 <code>bgcolor</code> 對應的是元件的樣式 <code>bg-color</code>. 你所要做的事是定義這兩個 setter 的内容。在這個例子裡僅僅是将 <code>this.inner</code> 這個 dom 元素的對應樣式更新為指定的值,複雜元件可能需要你做更多事情。和 attr 的 setter 一樣,函數體裡的 <code>this</code> 會綁定為目前的 component 執行個體。

事件配置相比屬性和樣式的配置更複雜一些。首先需要指定對哪個事件類型做配置,例子裡需要定制的事件類型隻有一個 <code>click</code>。一個事件可以進行三種配置,分别為 <code>extra</code>、<code>updator</code> 和 <code>setter</code>.

<code>extra</code> 配置事件參數傳遞的額外資訊,比如在上面的例子裡需要往 event 對象裡增加一個 <code>value</code> 值,dsl 開發者可以通過 <code>evt.value</code> 得到這個值:

注意 <code>extra</code> 是一個函數,需要傳回一個額外資料對象,這個函數的 <code>this</code> 也是綁定為目前 component 的執行個體的。

<code>updator</code> weex 目前由于自身的限制無法做到資料雙向綁定,使用者操作導緻的資料變更需要 dsl 開發者在事件監聽裡擷取并進行手動更新,而手動更新資料可能導緻 jsfm 發送備援的更新操作消息。updator 可以認為是 weex 的一種資料靜默更新機制,當使用者操作導緻某個 attr 或者 style 的值發生變更時,會把對應的值傳給 jsfm ,這樣當 dsl 開發者手動更新資料時 jsfm 已經将該值更新過了,不會再發備援的消息

<code>setter</code> 直接替換掉事件監聽函數,并不推薦使用這種方式進行事件綁定

定義了 attr, style 和 event,還需要把它們綁定到 prototype 上。這裡也有一些技巧,并不是直接把 prototype 加上這些屬性就可以了。我們來看這段代碼:

這裡的 extend 就是簡化版的 <code>object.assign</code>,這段代碼很好了解。需要注意的是,style 不是直接挂載到 prototype 上面的,因為基類(這個例子是 atomic)已經包含 style 屬性,即 <code>position</code> 的 setter 以及 <code>flex</code> 規範化的 setter. 是以需要把原來的 style 都繼承下來,再用 extend 添加新的 style 進來。

weex 是如何适配不同大小螢幕的?這個問題涉及到元件在頁面上的最終展現。如果你擴充的元件有自定義的屬性或者樣式,涉及到尺寸大小的,需要非常注意這一塊。每個元件在被建立之前,會由 componentmanager 将目前螢幕的 scale 值注入元件的 data (在除了 constructor 以外的任何元件方法中都可以通過 <code>this.data.scale</code> 通路到)中。那麼這個 scale 到底是什麼?

weex 中的設計尺寸是 __750px__,也就是說 weex 認為所有螢幕的寬度都是歸一化的 __750px__. 當真實螢幕不是 750px,weex 會自動将設計尺寸映射到真實尺寸中去,這個 <code>scale</code> 就是這種映射的比例。它的計算公式是 <code>目前螢幕尺寸 / 750</code>.

是以在擴充元件的時候,如果使用者傳入一個尺寸值,比如說 <code>375</code>,這個值是相對于 <code>750</code> 的設計尺寸來說的。你隻需要将這個值乘以 scale, 就是适配目前螢幕的真實尺寸:<code>value = 375 * this.data.scale</code>. 它應該占據真實螢幕一半的大小。

一個元件的生命周期包括初始化、建構、挂載以及移除等。元件開發者可以在其中各個階段進行控制。

__初始化__:通過元件的構造函數實作初始化的邏輯控制

__建構__:通過重寫元件的 <code>create</code> 方法實作建構階段的邏輯控制

__挂載__:基類 component 在 onappend 方法中已經做了一些處理(檢測 <code>appear</code> 事件的觸發條件,如果滿足條件則觸發該事件),如果需要在挂載階段做一些自己的處理,可以在初始化的邏輯裡調用 <code>addappendhandler</code> 方法向 onappend 中添加代碼:

這個調用操作也可以放在元件繼承完成以後的任何時刻進行:

__移除__:在基類的代碼裡可能找不到這個函數,因為基類沒有做額外的處理。如果你需要在元件被移除時做一些操作,比如連接配接斷開、資源釋放等,可以為你的元件添加一個 <code>onremove</code> 方法,元件在被移除時會自動調用這個方法。

和 scale 類似,元件在被建構之前就已經在 componentmanager 的 createelement 方法裡,将 id 注入到元件的 data 裡。在元件的生命周期的任何時刻都可以通過 <code>this.data.instanceid</code> 擷取到目前 weex 執行個體的 id.

在元件的生命周期任何時刻都可以通過 <code>this.getweexinstance</code> 擷取到目前 weex 執行個體。這個方法隻是 <code>this.getcomponentmanager().getweexinstance()</code> 的簡化版,一般也不太會用到。

componentmanager 包含靜态方法和執行個體方法。在 <code>init</code> 方法裡可以通過 <code>weex.componentmanager</code> 擷取到 componentmanager 這個類,并在接下來的代碼裡調用它的靜态方法。在自定義元件的方法實作中,可以通過 <code>this.getcomponentmanager()</code> 擷取到目前 componentmanager 的執行個體。一般可能用到的是 <code>createelement</code>, <code>getcomponent</code> 這兩個方法。

utils 是 weex-html5 内部一套工具函數,不是很推薦業務上直接使用。後續可能進行相關的重構,但是有幾個使用比較頻繁的方法應該不會有大的變動:

<code>extend(target, ...src)</code> 其實就是 <code>object.assign</code>,将後面傳入的對象的鍵值對拷貝給 target,相當于 mixin. 需要注意越靠後的對象的鍵值對具有越優先的覆寫權

<code>detectwep</code> 判讀目前裝置是否支援 <code>webp</code> 格式圖檔

<code>detectsticky</code> 判斷目前裝置是否支援原生的 <code>position: sticky</code>

<code>throttle(func, wait)</code> 函數限流,<code>func</code> 為要限流的函數,<code>wait</code> 為調用時間最小間隔,機關為 ms

<code>isplainobject</code> 是否是 <code>[object object]</code>

<code>gettype</code> 傳回對象的真實類型,如 <code>string</code>, <code>number</code>, <code>object</code>, <code>date</code>, 等等

可以通過 <code>init</code> 方法裡注入的 <code>weex</code> 上挂載的 <code>weex.utils</code> 擷取到 utils 的方法:

繼續閱讀