小程式元件化程式設計

snayan發表於2017-09-17

在開發微信小程式時,發現缺少了元件化開發體驗,在網上找了一波資源,發現都不是很好。其中,有用開發Vue的方式去開發小程式,比如,WePY,最後將原始碼編譯成小程式的官方檔案模式。這種方式,開發感覺爽,但是如果小程式版本升級變了之後,不在支援這種方式,那麼就得重新開發一套小程式官方支援的程式碼了,成本代價很大。並且,這次專案時間非常緊,團隊成員不熟悉vue的情況下,不敢用WePY。但是,小程式官方又對元件化支援不是很友好。於是,決定自己弄一套,既有元件化開發體驗,又是最大限度的接近小程式官方的開發模式。

目前專案已經成功上線,小程式:會過精選
原始碼地址以及例項地址

第一步,改寫Page

由於小程式的頁面定義是通過Page方法去定義的,那麼,Page一定在小程式內可以認為是一個全域性變數,我只需要改寫Page這個方法,去可以引用元件,呼叫元件,觸發元件的生命週期方法,維持元件內部的資料狀態,那麼,是不是就可以接近了元件化的程式設計體驗了,並且可以抽離常用元件,達到複用的目的。

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

/* 自定義Page */
Page = data => {
  //...改寫Page邏輯,增加自己的功能
  //最後一定得呼叫原Page方法,不然,小程式頁面無法生成
  nativePage(c);
};

確保後面頁面呼叫的Page是我們改寫的,那麼,必須在小程式啟動時引入這個檔案,達到改寫Page的目的。

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

App();

第二步,引入元件

page的引數是一個物件,這個物件裡定義了頁面的data ,生命週期方法,等等。如果要引入元件,我得定一個欄位,用來表明,需要引入的元件。我決定用componnets 這個欄位去引入當前頁面需要引入的元件。components是一個陣列,可以同時引入多個不同的元件。

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

通過components,表明了需要引入的元件。那麼,我們需要注入元件的相關資料和方法到當前頁面,以保證當前頁面內能呼叫元件的方法,或更改元件的資料狀態,以達到頁面的更新。為了實現這個,我們需要定義規範元件的結構,這樣才能正確拿到元件的相關資訊。我們定義的元件格式為

//定義了一個初始化元件的方法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還是得手動引入。

相關文章