preface

Based on WeChat applet cloud was before he wrote a small weekly program, suddenly thought of a few days ago can you write a job in a cloud function, check the documents, also have write timing triggers, but cloud function one by one to write function of cloud, there are a large number of redundant code, and is feeling very headache, again see small procedures, the development of the cloud cloud function routing senior play this article, I just want to implement a cloud function framework including routing, job and event, and encapsulate the small program side

In fact, I have not learned much about wechat small program cloud development. This framework is mainly used for practice. Now it only realizes cloud function routing, simple parameter verification, and encapsulation of small program side wx.cloud.callFunction()

Route distribution on the cloud side

In the cloud function, I am a new router cloud function, now all cloud functions will be written in the cloud function, of course, later can also be divided into modules to create multiple cloud functions, here is my directory structure

These are wrapped in this way for the time being, and we will continue to modify them to make the directory structure a little clearer

What happens to our index.js file

So what we’re doing here is we’re defining the option object, and we’re going to use the option object to instantiate the LeRouter object, which contains our configuration information, and we’re going to define some of the objects that we use a lot in cloud functions

I have been struggling with the configuration object here for a long time. For example, it would be more reasonable to define a config.json file separately for the configuration information of config, but that would make the logic more complicated and waste the performance

cloudfunctions\router\index.js

const { LeRouter } = require('./common/index')
const options = {
  config: {
    env: 'xxx',},routers: {
    "project.GetById": 'controller/project/GetById'."project.Upset": 'controller/project/Upset',}}const app = new LeRouter(options)
exports.main = app.run()
Copy the code

And then let’s see what the hell is LeRouter

LeRouter essentially initializes the Cloud and wraps the configuration object we passed in into this, adding DB objects to this for later use.

The DB object will not be created when the LeRouter object is instantiated. It will only be created when the DB object is used, because some cloud functions may not perform database operations

cloudfunctions\router\common\index.js

class LeRouter {
  constructor(options) {
    initAttr.call(this, options);
    initCloud.call(this);
  }
  run() {
    return async (event, context) => {
      const Controller = require(this.routers[event.$url]);
      // checkRequestParams(event, Controller.rules);
      return await Controller.main.call(this, event, context); }}}function initAttr(options) {
  this._cloud = require('wx-server-sdk');
  this.routers = options.routers || {};
  this.config = options.config || {};
  setDB.call(this);
}
function setDB() {
  Object.defineProperty(this.'DB', {
    configurable: false.get() {
      this._DB || (this._DB = require('./DB') (this));
      return this._DB; }})}function initCloud() {
  this._cloud.init({
    env: this.config.env,
  });
}
Copy the code

Then there’s our DB file

The DB file basically exposes an initDB factory method for producing DB objects

cloudfunctions\router\common\DB.js

function initDB(leOptions) {
  const config = leOptions.config
  return leOptions._cloud.database({
    env: config && config.env,
    throwOnNotFound: config && config.DBThrowOnNotFound,
  });
}
module.exports = initDB
Copy the code

Let’s look at a controller. Controllers now mainly have rules and main properties. Rules is used to automatically validate request parameters, and main is the actual controller method

cloudfunctions\router\controller\project\GetById.js

const { Project } = require('model/index')
module.exports = class GetById {
  static rules = {
    id: ['required'.'string'],}static main = async function (event, context) {
    const project = new Project(this.DB);
    const _ = this.DB.command
    const where = {
      _id: event.id,
      softDelete: _.neq(true),}return {
      data: await project.getProject(where)
    } 
  }
}
Copy the code

The Controler and model have a clearer division of labor. I am still thinking about whether to add another layer of service or make the Controler and model more clear

And then there’s a little bit of code in there and a little bit of common methods that need to be abstracted as the case may be

cloudfunctions\router\Model\Project.js

const COLLECTION_NAME = 'project';

class Project {
  constructor(DB) {
    this.db = DB
  }
  async getProject(where) { 
    return await this.db.collection(COLLECTION_NAME).where(where).get()
  }
  async upset(id, project) {
    // ...
    // ...
  }
  getServerDate() {
    return this.db.serverDate(); }}module.exports = Project
Copy the code

The small program end

The small program side mainly encapsulates wx.cloud.callFunction, and then manages the request module

Let’s look at the directory structure first

First take a look at the encapsulation of wx.cloud.callFunction

CallFunction () and request() are usually used. If you have special requirements, you can consider callFunction(). At the same time, it also encapsulates the pop-up “loading” box in the request. Request failed The Request Failed dialog box is displayed

miniprogram\engine\util\Service.js

import { Util } from '.. /index'
export default class ServiceUtil {
  static CLOUD_NAME = 'router'
  static request = (url, data = {}, complete = (res) => {}) = > {
    return new Promise((resolve, reject) = > {
      ServiceUtil.callFunction(
        url,
        data,
        ((result) = >{ resolve(result? .result? .data); }),(reuslt) = > {
          reject(reuslt);
        }),
        complete,
      )
    })
  }
  static callFunction = (url, data = {}, success = () => {}, fail = () => {}, complete = (res) => {}) = > {
    wx.showLoading({
      title: 'Loading',
    })
    wx.cloud.callFunction({
      name: ServiceUtil.CLOUD_NAME,
      data: {
        $url: url, ... data, },success: (res) = > {
        success(res);
      },
      fail: (res) = > {
        Util.logAndToast(res);
        fail(res);
      },
      complete: (res) = >{ wx.hideLoading(); complete(res); }}}})Copy the code

Then let’s look at the encapsulated request

There’s nothing more to this encapsulation than unified management of request modules. Right

miniprogram\engine\Project.js

import ServiceUtil from './util/Service.js'
export default class Project {
  static getById = async (id) => {
    return await ServiceUtil.request('project.GetById', { id });
  }
  static upset = async (params) => {
    return await ServiceUtil.request('project.Upset', params); }}Copy the code

Cloud side request parameter validation

The verification of our request parameters is actually carried out during route distribution. When we get the Controller module object, we can get the rules defined in it, and then verify the request parameters according to rules

If the type of the field is an object, we only verify whether it is an object, not the field in the object

If validation fails, an exception is thrown directly

cloudfunctions\router\common\index.js

class LeRouter {
  constructor(options) {
    // ...
  }
  run() {
    return async (event, context) => {
      const Controller = require(this.routers[event.$url])
      checkRequestParams(event, Controller.rules)
      return await Controller.main.call(this, event, context)
    }
  }
}
function checkRequestParams(params, rules = null) {
  if(! rules) {return params;
  }
  if (typeofrules ! = ='object') {
    throw Error('rules must be object! ')}return checkRequestRules(params, rules)
}
function checkRequestRules(params, rules, keyString = 'event') {
  const keys = Object.keys(rules);
  for(let i = keys.length - 1; i + 1 ; i--) {
    const key = keys[i];
    keyString = `${keyString}.${key}`
    // Determine whether the message is required
    if (rules[key].includes('required') && !params[key]) {
      throw Error(`The ${keyString} argument is required`);
    }
    // use typeof to determine the typeof the parameter
    if(! rules[key].includes(typeof params[key])) {
      throw Error(`The '${keyString}' argument mast be of type ${rules[key]}`); }}return true;
}
Copy the code

In Controller we define the rules object, which defines the validation rules for the request parameters

cloudfunctions\router\controller\project\GetById.js

const { Project } = require('model/index')

module.exports = class GetById {
  static rules = {
    id: ['required'.'string'],}static main = async function (event, context) {
    // ...
    // ...}}Copy the code

conclusion

The cloud function side is to create only one cloud function (or multiple cloud functions can be created by modules), and then distribute routes to find the corresponding Controller

The applet side is to encapsulate the request, fix the name field that wx.cloud.callFunction() needs to pass, and encapsulate our own route name into the request’s data

The request parameter verification is to define the verification rules, and then obtain the verification rules can be verified

feeling

Although the whole thing is not complicated and my implementation is still so simple, IT has made me entangled in the process of encapsulation. When I learned other frameworks, I just followed others’ ideas and went down, including the encapsulation of the construction project in the company’s project, but now I write it myself, so I don’t care about the end

Later, I will try to achieve the encapsulation of Page and Component on the small program side, as well as the encapsulation of Job and event in cloud functions. First, the main functions can be realized and run, and then I will consider optimization

Finally, take a look at the implementation

Run successfully! I just need it to work. I don’t need a bike