微信小程序自制脚手架

前段时间接触了下微信小程序,对于写了几个月RN的我,小程序的语法还是不太容易让我接受,于是我往里面加了点之前用得还挺顺手的东西(mobx、async、await),于是整理了下,出了这么一个微信小程序自制脚手架,分享出来共同探讨下。
项目地址:https://github.com/bbbond/wx-demo

写在前面

首先声明,本脚手架适合习惯小程序自带wxmlwxss方式写法的小伙伴们(我是不太喜欢这样的方式,写到想吐)

其次对于项目较大的小程序来说,不太推荐本框架,默认的写法下项目不太好管理,推荐wepyGitHub地址,不过本人还未使用过,只是看好多博客有推荐。之后有机会去体验下。言归正传开始介绍下这个框架。

脚手架结构

目录结构

首先根据习惯我的项目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
./
├── README.md
├── app.js
├── app.json
├── app.wxss
├── assets
├── constants
│   └── CONFIG.js
├── libs
│   ├── combine.js
│   ├── mobx.min.js
│   ├── moment.min.js
│   ├── observer.js
│   ├── runtime.js
│   └── storeCache.js
├── pages
│   └── index
│   ├── index.js
│   ├── index.wxml
│   ├── index.wxss
│   ├── indexStore.js
│   ├── request.js
│   └── search
│   ├── search.js
│   ├── search.wxml
│   └── search.wxss
├── project.config.json
├── store
│   └── stores.js
└── utils
├── baseRequest.js
└── fetchHelper.js

网络封装

为了统一请求风格,对请求框架进行了一次简单封装。(可自行根据业务进行修改)

  • 接口返回JSON结果风格:

    字段 类型 说明
    statusCode int 错误状态码,当值不等于200时表示返回异常结果
    data.code int 错误码,当值不等于0时表示返回异常结果
    data.message string 错误信息,错误所对应的
    data object 服务端返回数据
  • 底层封装(以GET方式为例)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // fetchHelper.js

    export const get = (url, headers) => {
    return new Promise((resolve, reject) => {
    logRequest(url);
    wx.request({
    url: url,
    header: headers || {},
    success: (res) => {
    let data = res.data;
    if (Number(res.statusCode) !== 200) {
    data = {
    ...data,
    code: res.statusCode,
    msg: res.data.message,
    };
    }
    logSuccess('GET', url, headers, undefined, data);
    // 将服务端返回的结果整理好抛给上层
    resolve(data);
    },
    fail: (error) => {
    logFailed('GET', url, headers, undefined, error);
    // 由于本机产生的问题直接异常抛出
    reject({code: 1, msg: "网络请求失败", ...error});
    }
    });
    });
    };
  • 上层封装(以GET为例)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // baseRequest.js

    export const baseGetRequest = (api, params, header) => {
    let requestHeader = {
    ...getBaseHeader(),
    ...header
    };
    params && Object.keys(params).map((key) => {
    api = api.replace(`{${key}}`, params[key])
    });
    return new Promise((resolve, reject) => {
    get(api, requestHeader)
    .then(async result => {
    if (result.code) {
    // 返回结果存在code则抛出异常信息,(针对不同错误类型进行不同的处理)
    reject(result.msg || '未知错误')
    } else {
    // 无错误正常返回结果
    resolve(result);
    }
    })
    .catch(error => {
    reject(error.msg)
    });
    })
    };

    这一层demo中只是简单的进行封装,在具体业务下需要自行进行处理,(例如token失效的处理)

  • 应用层使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // request.js

    const API = {
    IN_THEATERS: `${domain}/movie/in_theaters?city={city}&start={start}`,
    };

    export const getInTheatersReq = (city, start = 0) => baseGetRequest(
    API.IN_THEATERS, {city, start}
    );

    使用没什么好说的,看上面。

mobx状态管理及缓存

  • mobx介绍
    如果还没接触过mobx,可以去mobx GitHub了解下,这是一款连redux创始人多说好的状态管理框架。

  • mobx+cache
    首先得要对mobx的store进行处理,添加初始化值(initialState)和cache白名单(xxxWhiteList),并在mobx初始化的时候赋值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // indexStore.js

    /** ================== 初始化值 ================== **/
    const initialState = {
    subjects: [],
    };

    /** ================== cache白名单 ================== **/
    const indexWhiteList = [
    'subjects'
    ];

    class IndexStore {
    constructor() {
    extendObservable(this, {
    subjects: this.store && this.store.subjects || initialState.subjects,
    });
    }

    getInTeater = async (city, start = 0) => {
    let inTeater;
    // ...
    this.subjects = inTeater.subjects;
    };
    }

    module.exports = {
    IndexStore,
    indexWhiteList
    };

    之后还需要一个方法初始化Store,监听Store变化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // storeCache.js

    const settingStoreAutoRun = (key, store, whiteList) => {
    // 将缓存塞入store
    store.prototype.store = JSON.parse(wx.getStorageSync(key) || '{}') || {};
    let storeObj = new store();
    mobx.autorun(() => {
    let app = mobx.toJS(storeObj);
    let temp = {};
    whiteList.map((key) => {
    temp[key] = app[key];
    });
    wx.setStorage({
    key: key,
    data: JSON.stringify(temp)
    });
    });
    return storeObj;
    };

    然后找个地方中将所有Store都初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // stores.js

    const { settingStoreAutoRun, getCacheKey } = require('../libs/storeCache.js');
    let { IndexStore, indexWhiteList } = require('../pages/index/indexStore');

    const stores = {
    index: settingStoreAutoRun(getCacheKey('INDEX'), IndexStore, indexWhiteList),
    };

    module.exports = stores;

    最后在App.js中将初始化后的stores放入globalData中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //app.js

    const stores = require('./store/stores.js');

    App(observer({
    globalData: {
    ...stores
    },
    onLaunch: function () {
    },
    }));

    细心的小伙伴们一定发现上面突然乱入了一个observer,这也是mobx的一个用法,无论是App还是page都要包一层,这样才能接收到store的变化

    1
    2
    3
    App(observer(app));

    Page(observer(page));

组件化开发

组件开发

组件化的好处就不多说了,在开发过程中,不但能减少很多开发时间,还能让代码更清晰明了(其实更能应对需求变动)。
编写一个组件需要准备三个文件.wxml.wxss.js

  • .wxml
    组件的wxml和其他界面的wxml没什么区别,就不具体说明了。

  • .wxss
    组件的样式文件,与其他样式文件无异,需要注意的是避免由于类选择器重名而造成的影响。

  • .js(以demo中的search为例)
    props为mobx传入的属性,用于接收不可直接改变的值。
    在.wxml中通过使用。
    注意:需要接收store的实例,若直接接收store的某个属性,那么该属性变化后不会触发界面重新渲染

    1
    2
    3
    4
    props: {
    getInTeater: app.globalData.index.getInTeater,
    index: app.globalData.index,
    }

    data为mobx中组件的状态,类似于React的state。
    注意:由于组件的属性、方法最后将会和调用处属性、方法合并,因此注意不要和调用处重名
    建议:对于data将组件所需要的状态存在同一个对象中(入demo中的search),对于组件内的方法,我的做法是在方法名前加上__,对于组件抛出的方法正常使用驼峰命名即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    data: {
    search: {
    currentCity: '',
    city: app.globalData.index.city,
    title: app.globalData.index.title,
    }
    },
    __onInputCity: function(e) {
    this.setData({
    search: {
    ...this.data.search,
    currentCity: e.detail.value
    }
    })
    },
    __onSearch: function(e) {
    // ...
    },

    最后导出组件

    1
    2
    3
    4
    5
    6
    module.exports = {
    props,
    data,
    __onInputCity,
    __onSearch
    }

组件使用

组件的使用也需要在.wxml.wxss.js三个地方声明。

  • .wxml
    wxml中引入组件界面,这没什么好说的。

    1
    <include src="./search/search.wxml" />
  • .wxss
    wxss中引入组件样式,这也没什么好说的。

    1
    @import "./search/search.wxss";
  • .js
    组件的使用方式如下:
    其中关键是将组件的属性、方法和自身的属性、方法进行合并。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //index.js

    let { combine } = require('../../libs/combine');
    let search = require('./search/search');

    let page = {
    props,
    data,
    };

    combine(page, search);
    Page(observer(page));
  • combine方法
    合并方法参考了慕课的一片文章,原文链接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 方法来自 https://www.imooc.com/article/19908
    export const combine = (target, ...source) => {
    source.forEach(function (arg) {
    if ('object' === typeof arg) {
    for (let p in arg) {
    if ('object' === typeof arg[p]) {
    // 对于对象,直接采用 Object.assign
    target[p] = target[p] || {};
    Object.assign(target[p], arg[p])
    } else if ('function' === typeof arg[p]) {
    // 函数进行融合,先调用组件事件,然后调用父页面事件
    let fun = target[p] ? target[p] : function () {
    };
    delete target[p];
    target[p] = function () {
    arg[p].apply(this, arguments);
    fun.apply(this, arguments)
    }
    } else { // 基础数据类型,直接覆盖
    target[p] = target[p] || arg[p]
    }
    }
    }
    })
    };

其他注意点

async/await的引用

async/await 用了都说好,谁用谁知道,可惜小程序不支持,那我们只能自己引入了。
不过由于限制必须在每个使用的文件中都加入如下代码

1
const regeneratorRuntime = require('../../libs/runtime');

小程序的一些限制

  • 代码体积限制
    由于小程序的理念,其代码体积必须小于2M。经试验,若代码体积大于2M在微信Android版8.5.3中无法打开,会报内部异常。

  • 最低版本库设置

    若用户的基础库版本低于要求,则提示更新微信版本。此设置需要在iOS 6.5.8或安卓6.5.7及以上微信客户端版本生效

    以上为微信原话,看到这句话瞬间感觉头皮发麻,也就是说对于微信6.5.7以下(iOS 6.5.8)的版本我们得要手动判断是否支持,并作相应处理。
    虽然有 wx.canIUse 可以进行API可用性的判断,但是这个方法也是之后的基础库才加入的,因此有一个断层,让人没法好好玩耍。最后索性使用 wx.getSystemInfo 进行版本判断,对过低版本直接屏蔽,显示不可用,并提示更新,wx.getSystemInfo具体说明点这里

  • 其他限制
    嗯,等我想到再补充。

最后

这是我第一次写脚手架,一定会有不足之处,感兴趣的小伙伴们可以一起来完善它。

脚手架项目地址:https://github.com/bbbond/wx-demo

转载请注明来源:http://blog.bbbond.cn/2018/02/04/微信小程序自制脚手架/