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

写在前面

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

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

脚手架结构

目录结构

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

./
├── 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方式为例)

    // 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为例)

    // 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失效的处理)

  • 应用层使用

    // 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初始化的时候赋值。

    // 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变化

    // 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都初始化

    // 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中

    //app.js
    
    const stores = require('./store/stores.js');
    
    App(observer({
      globalData: {
        ...stores
      },
      onLaunch: function () {
      },
    }));
    

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

    App(observer(app));
    
    Page(observer(page));
    

组件化开发

组件开发

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

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

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

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

    props: {
      getInTeater: app.globalData.index.getInTeater,
      index: app.globalData.index,
    }
    

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

    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) {
      // ...
    },
    

    最后导出组件

    module.exports = {
      props,
      data,
      __onInputCity,
      __onSearch
    }
    

组件使用

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

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

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

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

    //index.js
    
    let { combine } = require('../../libs/combine');
    let search = require('./search/search');
    
    let page = {
      props,
      data,
    };
    
    combine(page, search);
    Page(observer(page));
    
  • combine方法
    合并方法参考了慕课的一片文章,原文链接

    // 方法来自 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 用了都说好,谁用谁知道,可惜小程序不支持,那我们只能自己引入了。
不过由于限制必须在每个使用的文件中都加入如下代码

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/微信小程序自制脚手架/