從 VantComponent 談 小程序維護

在開發小程序的時候,咱們老是指望用以往的技術規範和語法特色來書寫當前的小程序,因此纔會有各色的小程序框架,例如 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

TLDR (很少廢話,先說結論)

小程序組件寫法這裏就再也不介紹。這裏咱們給出利用 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&paramB=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 源碼解析

VantComponent

此時,咱們對 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);
}

內置behaviors
組件樣式隔離異步

basic behaviors

剛剛咱們談到 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();
      });
    }
  }
});

observe

小程序 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

如今剩下的也就是 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

最終 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;
      });
    }
  }
});

寫在後面

  • js 是一門靈活的語言(手動滑稽)
  • 自己 小程序 Component 在 小程序 Page 以後,就要比Page 更加成熟好用,有時候新的方案每每藏在文檔之中,每次多看幾遍文檔毫不是沒有意義的。
  • 小程序版本 版本2.6.1 Component 目前已經實現了 observers,能夠監聽 props data 數據監聽器,目前 VantComponent沒有實現,固然自己而言,Page 不須要對 prop 進行監聽,由於進入頁面壓根不會變,而data變化自己就無需監聽,直接調用函數便可,因此對page而言,observers 無關緊要。
  • 該方案也只是對 js 代碼上有vue的風格,並沒在 template 以及 style 作其餘文章。
  • 該方案性能必定是有所缺失的,由於computed是每次set都會進行計算,而並不是根據set 的 data 來進行操做,在刪減以後我認爲自己是能夠接受。若是自己對於vue的語法特性需求不高,能夠直接利用 Component 來編寫 Page,選擇不一樣的解決方案實質上是須要權衡各類利弊。若是自己是有其餘要求或者新的項目,仍舊推薦使用新技術,若是自己是已有項目而且須要維護的,同時又想擁有 Vue 特性。可使用該方案,由於代碼自己較少,並且自己也能夠基於自身需求修改。
  • 同時,vant-weapp是一個很是不錯的項目,推薦各位能夠去查看以及star。
相關文章
相關標籤/搜索