小程序組件化編程

在開發微信小程序時,發現缺乏了組件化開發體驗,在網上找了一波資源,發現都不是很好。其中,有用開發Vue的方式去開發小程序,好比,WePY,最後將源代碼編譯成小程序的官方文件模式。這種方式,開發感受爽,可是若是小程序版本升級變了以後,不在支持這種方式,那麼就得從新開發一套小程序官方支持的代碼了,成本代價很大。而且,此次項目時間很是緊,團隊成員不熟悉vue的狀況下,不敢用WePY。可是,小程序官方又對組件化支持不是很友好。因而,決定本身弄一套,既有組件化開發體驗,又是最大限度的接近小程序官方的開發模式。javascript

目前項目已經成功上線,小程序:會過精選
源碼地址以及實例地址vue

第一步,改寫Page

因爲小程序的頁面定義是經過Page方法去定義的,那麼,Page必定在小程序內能夠認爲是一個全局變量,我只須要改寫Page這個方法,去能夠引用組件,調用組件,觸發組件的生命週期方法,維持組件內部的數據狀態,那麼,是否是就能夠接近了組件化的編程體驗了,而且能夠抽離經常使用組件,達到複用的目的。java

//先保存原Page
const nativePage = Page;

/* 自定義Page */
Page = data => {
  //...改寫Page邏輯,增長本身的功能
  //最後必定得調用原Page方法,否則,小程序頁面沒法生成
  nativePage(c);
};

確保後面頁面調用的Page是咱們改寫的,那麼,必須在小程序啓動時引入這個文件,達到改寫Page的目的。git

//在app.js 頭部引入,假如咱們的文件名叫registerPage.js
import "./registerPage";

App();

第二步,引入組件

page的參數是一個對象,這個對象裏定義了頁面的data ,生命週期方法,等等。若是要引入組件,我得定一個字段,用來代表,須要引入的組件。我決定用componnets 這個字段去引入當前頁面須要引入的組件。components是一個數組,能夠同時引入多個不一樣的組件。es6

//在components中引入頁面須要的組件,咱們這裏引入了Toast和LifeCycle這2個組件
Page({
  components: ["Toast", "LifeCycle"],
  data: {
    motto: "Hello World",
    userInfo: {}
  }
});

經過components,代表了須要引入的組件。那麼,咱們須要注入組件的相關數據和方法到當前頁面,以保證當前頁面內能調用組件的方法,或更改組件的數據狀態,以達到頁面的更新。爲了實現這個,咱們須要定義規範組件的結構,這樣才能正確拿到組件的相關信息。咱們定義的組件格式爲github

//定義了一個初始化組件的方法initComponent,這個方法就是返回一個對象,跟page裏的參數相似,描述了組件了相關信息。
function initComponent() {
  return {
    timer: null,
    data: {
      content: ""
    },
    show: function(msg, options) {
    }
  };
}

export { initComponent };

第三步,注入組件

有了組件的相關信息,咱們須要把這些信息自動注入到頁面中,這樣,在頁面中才能與組件通訊,而且也須要把頁面的信息引入到組件內,這樣,在組件中也可與父級頁面通訊。其中,組件內部最爲重要的就是data 字段了,這個字段內的數據變化了,也要保證頁面自動刷新,跟頁面功能同樣。爲了隔離各個組件內部的數據,我對每一個組件默認定一個命名空間,這個命名空間就是組件的名字。把組件內部的數據掛在本身的命名空間下,再把這個命名空間掛到頁面的data 下。同時,把組件的方法和其餘熟悉以組件名.方法名或屬性名的方法掛到頁面下。這樣,組件的相關信息就都注入到頁面中了。編程

//掛載組件的data,以組件名爲命名空間掛載
if (v.data) {
  o.data = { ...o.data, [v.name]: v.data };
}
//掛載組件的方法,以【組件名.方法名】掛載
let fns = Object.keys(v).filter(
  vv => v.hasOwnProperty(vv) && typeof v[vv] === "function"
);
for (let fn of fns) {
  o[`${v.name}.${fn}`] = function() {
    let newThis = createComponentThis(v, this);
    let args = Array.from(arguments);
    args.length < 5
      ? v[fn].call(newThis, ...args)
    : v[fn].apply(newThis, args);
  };
}

第四步,隔離組件

爲了在組件內調用本身的方法,有本身的做用域,咱們必須爲每一個組件建立一個獨立的做用域,以隔離組件和父級頁面做用域。保證了,在組件內部更改this,不會對父級頁面有影響。同時,組件內部也必須有和父級頁面相似的setData方法,達到一樣的刷新頁面的目的。咱們定義一組保護的屬性名。小程序

/* 受保護的屬性 */
const protectedProperty = ["name", "parent", "data", "setData"];

name是組件的名稱,parent是對父級頁面的引用,data 是組件內部數據狀態,setData是跟父級頁面相似的方法,用來更改組件內部本身的數據。微信小程序

建立組件做用域數組

/* 建立一個新的Component做用域 */
const createComponentThis = (component, page) => {
  let name = component.name;
  if (page[`__${name}.this__`]) {
    return page[`__${name}.this__`];
  }
  let keys = Object.keys(component);
  let newThis = Object.create(null);
  let protectedKeys = protectedProperty.concat(protectedEvent);
  let otherKeys = keys.filter(v => !~protectedKeys.indexOf(v));
  for (let key of otherKeys) {
    if (typeof component[key] === "function") {
      Object.defineProperty(newThis, key, {
        get() {
          return page[`${name}.${key}`];
        },
        set(val) {
          page[`${name}.${key}`] = val;
        }
      });
    } else {
      Object.defineProperty(newThis, key, {
        get() {
          return component[`${key}`];
        },
        set(val) {
          component[`${key}`] = val;
        }
      });
    }
  }
  Object.defineProperty(newThis, "name", {
    configurable: false,
    enumerable: false,
    get() {
      return name;
    }
  });
  Object.defineProperty(newThis, "data", {
    configurable: false,
    enumerable: false,
    get() {
      return page.data[name];
    }
  });
  Object.defineProperty(newThis, "parent", {
    configurable: false,
    enumerable: false,
    get() {
      return page;
    }
  });
  Object.defineProperty(newThis, "setData", {
    value: function(data) {
      page.setData(parseData(name, this.data, data));
    },
    enumerable: false,
    configurable: false
  });
  page[`__${name}.this__`] = newThis;
  return newThis;
};

第五步,觸發組件的生命週期方法

每一個組件必須均可以定義本身的生命週期方法,這些生命週期方法與父級頁面的同樣。由於,組件的生命週期方法必須是在父級頁面的生命週期方法內觸發的。必須是小程序官方支持的。

/* 受保護的頁面事件 */
const protectedEvent = [
  "onLoad",
  "onReady",
  "onShow",
  "onHide",
  "onUnload",
  "onPullDownRefreash",
  "onReachBottom",
  "onPageScroll"
];

咱們必須把組件的生命週期方法掛在父級頁面的對應的生命週期方法內,這樣,才能在觸發父級頁面的生命週期方法時,自動觸發組件對應的生命週期方法。其中,先是觸發完全部的組件的方法,再最後觸發父級頁面的方法

/* 綁定子組件生命週期鉤子函數 */
const bindComponentLifeEvent = page => {
  let components = page.components;
  for (let key of protectedEvent) {
    let symbols = page[Symbol["for"](key)];
    let pageLifeFn = page[key];
    if (Array.isArray(symbols) && symbols.length > 0) {
      if (typeof pageLifeFn === "function") {
        symbols.push({
          fn: pageLifeFn,
          type: "page",
          context: page
        });
      }
      page[key] = function() {
        let pageThis = this;
        let args = Array.from(arguments);
        for (let ofn of symbols) {
          let currentThis;
          if (ofn.type === "component") {
            currentThis = createComponentThis(ofn.context, pageThis);
          } else {
            currentThis = pageThis;
          }
          args.length < 5
            ? ofn.fn.call(currentThis, ...args)
            : ofn.fn.apply(currentThis, args);
        }
      };
    }
  }
};

經過上述這些步驟改寫Page以後,那麼我就能夠快速開始了個人小程序組件化編程體驗了。

其實原理以下:

  • 在小程序啓動時劫獲小程序的Page函數,在自定義的Page函數中注入子組件的相關數據到父級頁面中。
  • 將組件的data注入到父級頁面的data下,可是組件的data會以組件name爲命名空間,以隔離父級data或其餘組件的data
  • 將組件的通常方法(非生命週期方法)注入到父級頁面的方法中,方法名變成了{組件name.方法名}
  • 在組件內部的方法都會生成一個新的組件this,隔離父級this,組件this中都是定義了一系列的getter,setter方法,實際操做的是注入到父級頁面中的方法。

注意點

  • 組件裏的方法必須是es5的函數聲明模式,不能是es6的箭頭函數,由於使用es6的箭頭函數會丟失組件this。
  • 組件的js達到了自動化注入,可是wxml和wxss仍是得手動引入。
相關文章
相關標籤/搜索