在開發小程序的時候,咱們老是指望用以往的技術規範和語法特色來書寫當前的小程序,因此纔會有各色的小程序框架,例如 mpvue、taro 等這些編譯型框架。固然這些框架自己對於新開發的項目是有所幫助。而對於老項目,咱們又想要利用 vue 的語法特性進行維護,又該如何呢?
在此我研究了一下youzan的 vant-weapp。而發現該項目中的組件是如此編寫的。html
import { VantComponent } from '../common/component'; VantComponent({ mixins: [], props: { name: String, size: String }, // 可使用 watch 來監控 props 變化 // 其實就是把properties中的observer提取出來 watch: { name(newVal) { ... }, // 能夠直接使用字符串 代替函數調用 size: 'changeSize' }, // 使用計算屬性 來 獲取數據,能夠在 wxml直接使用 computed: { bigSize() { return this.data.size + 100 } }, data: { size: 0 }, methods: { onClick() { this.$emit('click'); }, changeSize(size) { // 使用set this.set(size) } }, // 對應小程序組件 created 週期 beforeCreate() {}, // 對應小程序組件 attached 週期 created() {}, // 對應小程序組件 ready 週期 mounted() {}, // 對應小程序組件 detached 週期 destroyed: {} });
竟然發現該組件寫法總體上相似於 Vue 語法。而自己卻沒有任何編譯。看來問題是出在了導入的 VantComponet 這個方法上。下面咱們開始詳細介紹一下如何利用 VantComponet 來對老項目進行維護。vue
小程序組件寫法這裏就再也不介紹。這裏咱們給出利用 VantComponent 寫 Page 的代碼風格。c++
import { VantComponent } from '../common/component'; VantComponent({ mixins: [], props: { a: String, b: Number }, // 在頁面這裏 watch 基本上是沒有做用了,由於只作了props 變化的watch,page不會出現 props 變化 // 後面會詳細說明爲什麼 watch: {}, // 計算屬性仍舊可用 computed: { d() { return c++ } }, methods: { onLoad() {} }, created() {}, // 其餘組件生命週期 })
這裏你可能感到疑惑,VantComponet 不是對組件 Component 生效的嗎?怎麼會對頁面 Page 生效呢。事實上,咱們是可使用組件來構造小程序頁面的。
在官方文檔中,咱們能夠看到 使用 Component 構造器構造頁面
事實上,小程序的頁面也能夠視爲自定義組件。於是,頁面也可使用 Component 構造器構造,擁有與普通組件同樣的定義段與實例方法。代碼編寫以下:小程序
Component({ // 可使用組件的 behaviors 機制,雖然 React 以爲 mixins 並非一個很好的方案 // 可是在某種程度該方案的確能夠複用相同的邏輯代碼 behaviors: [myBehavior], // 對應於page的options,與此自己是有類型的,而從options 取得數據均爲 string類型 // 訪問 頁面 /pages/index/index?paramA=123¶mB=xyz // 若是聲明有屬性 paramA 或 paramB ,則它們會被賦值爲 123 或 xyz,而不是 string類型 properties: { paramA: Number, paramB: String, }, methods: { // onLoad 不須要 option // 可是頁面級別的生命週期卻只能寫道 methods中來 onLoad() { this.data.paramA // 頁面參數 paramA 的值 123 this.data.paramB // 頁面參數 paramB 的值 ’xyz’ } } })
那麼組件的生命週期和頁面的生命週期又是怎麼對應的呢。通過一番測試,得出結果爲: (爲了簡便。只會列出 重要的的生命週期)緩存
// 組件實例被建立 到 組件實例進入頁面節點樹 component created -> component attched -> // 頁面頁面加載 到 組件在視圖層佈局完成 page onLoad -> component ready -> // 頁面卸載 到 組件實例被從頁面節點樹移除 page OnUnload -> component detached
固然 咱們重點不是在 onload 和 onunload 中間的狀態,由於中間狀態的時候,咱們能夠在頁面中使用頁面生命週期來操做更好。
某些時候咱們的一些初始化代碼不該該放在 onload 裏面,咱們能夠考慮放在 component create 進行操做,甚至能夠利用 behaviors 來複用初始化代碼。
某種方面來講,若是不須要 Vue 風格,咱們在老項目中直接利用 Component 代替 Page 也不失爲一個不錯的維護方案。畢竟官方標準,不用擔憂其餘一系列後續問題。app
此時,咱們對 VantComponent 開始進行解析框架
// 賦值,根據 map 的 key 和 value 來進行操做 function mapKeys(source: object, target: object, map: object) { Object.keys(map).forEach(key => { if (source[key]) { // 目標對象 的 map[key] 對應 源數據對象的 key target[map[key]] = source[key]; } }); } // ts代碼,也就是 泛型 function VantComponent<Data, Props, Watch, Methods, Computed>( vantOptions: VantComponentOptions< Data, Props, Watch, Methods, Computed, CombinedComponentInstance<Data, Props, Watch, Methods, Computed> > = {} ): void { const options: any = {}; // 用function 來拷貝 新的數據,也就是咱們能夠用的 Vue 風格 mapKeys(vantOptions, options, { data: 'data', props: 'properties', mixins: 'behaviors', methods: 'methods', beforeCreate: 'created', created: 'attached', mounted: 'ready', relations: 'relations', destroyed: 'detached', classes: 'externalClasses' }); // 對組件間關係進行編輯,可是page不須要,能夠刪除 const { relation } = vantOptions; if (relation) { options.relations = Object.assign(options.relations || {}, { [`../${relation.name}/index`]: relation }); } // 對組件默認添加 externalClasses,可是page不須要,能夠刪除 // add default externalClasses options.externalClasses = options.externalClasses || []; options.externalClasses.push('custom-class'); // 對組件默認添加 basic,封裝了 $emit 和小程序節點查詢方法,能夠刪除 // add default behaviors options.behaviors = options.behaviors || []; options.behaviors.push(basic); // map field to form-field behavior // 默認添加 內置 behavior wx://form-field // 它使得這個自定義組件有相似於表單控件的行爲。 // 能夠研究下文給出的 內置behaviors if (vantOptions.field) { options.behaviors.push('wx://form-field'); } // add default options // 添加組件默認配置,多slot options.options = { multipleSlots: true,// 在組件定義時的選項中啓用多slot支持 // 若是這個 Component 構造器用於構造頁面 ,則默認值爲 shared // 組件的apply-shared,能夠研究下文給出的 組件樣式隔離 addGlobalClass: true }; // 監控 vantOptions observe(vantOptions, options); // 把當前從新配置的options 放入Component Component(options); }
剛剛咱們談到 basic behaviors,代碼以下所示xss
export const basic = Behavior({ methods: { // 調用 $emit組件 其實是使用了 triggerEvent $emit() { this.triggerEvent.apply(this, arguments); }, // 封裝 程序節點查詢 getRect(selector: string, all: boolean) { return new Promise(resolve => { wx.createSelectorQuery() .in(this)[all ? 'selectAll' : 'select'](selector) .boundingClientRect(rect => { if (all && Array.isArray(rect) && rect.length) { resolve(rect); } if (!all && rect) { resolve(rect); } }) .exec(); }); } } });
小程序 watch 和 computed的 代碼解析函數
export function observe(vantOptions, options) { // 從傳入的 option中獲得 watch computed const { watch, computed } = vantOptions; // 添加 behavior options.behaviors.push(behavior); /// 若是有 watch 對象 if (watch) { const props = options.properties || {}; // 例如: // props: { // a: String // }, // watch: { // a(val) { // // 每次val變化時候打印 // consol.log(val) // } } Object.keys(watch).forEach(key => { // watch只會對prop中的數據進行 監視 if (key in props) { let prop = props[key]; if (prop === null || !('type' in prop)) { prop = { type: prop }; } // prop的observer被watch賦值,也就是小程序組件自己的功能。 prop.observer = watch[key]; // 把當前的key 放入prop props[key] = prop; } }); // 通過此方法 // props: { // a: { // type: String, // observer: (val) { // console.log(val) // } // } // } options.properties = props; } // 對計算屬性進行封裝 if (computed) { options.methods = options.methods || {}; options.methods.$options = () => vantOptions; if (options.properties) { // 監視props,若是props發生改變,計算屬性自己也要變 observeProps(options.properties); } } }
如今剩下的也就是 observeProps 以及 behavior 兩個文件了,這兩個都是爲了計算屬性而生成的,這裏咱們先解釋 observeProps 代碼
export function observeProps(props) { if (!props) { return; } Object.keys(props).forEach(key => { let prop = props[key]; if (prop === null || !('type' in prop)) { prop = { type: prop }; } // 保存以前的 observer,也就是上一個代碼生成的prop let { observer } = prop; prop.observer = function() { if (observer) { if (typeof observer === 'string') { observer = this[observer]; } // 調用以前保存的 observer observer.apply(this, arguments); } // 在發生改變的時候調用一次 set 來重置計算屬性 this.set(); }; // 把修改的props 賦值回去 props[key] = prop; }); }
最終 behavior,也就算 computed 實現機制
// 異步調用 setData function setAsync(context: Weapp.Component, data: object) { return new Promise(resolve => { context.setData(data, resolve); }); }; export const behavior = Behavior({ created() { if (!this.$options) { return; } // 緩存 const cache = {}; const { computed } = this.$options(); const keys = Object.keys(computed); this.calcComputed = () => { // 須要更新的數據 const needUpdate = {}; keys.forEach(key => { const value = computed[key].call(this); // 緩存數據不等當前計算數值 if (cache[key] !== value) { cache[key] = needUpdate[key] = value; } }); // 返回須要的更新的 computed return needUpdate; }; }, attached() { // 在 attached 週期 調用一次,算出當前的computed數值 this.set(); }, methods: { // set data and set computed data // set可使用callback 和 then set(data: object, callback: Function) { const stack = []; // set時候放入數據 if (data) { stack.push(setAsync(this, data)); } if (this.calcComputed) { // 有計算屬性,一樣也放入 stack中,可是每次set都會調用一次,props改變也會調用 stack.push(setAsync(this, this.calcComputed())); } return Promise.all(stack).then(res => { // 全部 data以及計算屬性都完成後調用callback if (callback && typeof callback === 'function') { callback.call(this); } return res; }); } } });