实现微信小程序编译和运行环境系列 (核心篇三)

动手实现微信小程序和小游戏编译打包和运行环境平台 (核心篇三)

本章节就带大家通过微信官方的创建项目部分代码来讲解一下这些对外 api 如何通过我们自己方式来实现和微信相同的功能操作,我们通过微信开发者工具来自动创建一个默认的小程序项目 一个首页和日志页。

image.png

这个项目大家应该都比较熟悉吧,应该第一次接触小程序开始时引入眼前的场景,具体的其他内容我们就不在这里啰嗦了,直接看下它的 app.js 文件,编辑器打开后可以看到里面写了这些。

 
 //app.js
App({
  onLaunch: function () {
    // 展示本地存储能力
    var logs = wx.getStorageSync('logs') || []
    logs.unshift(Date.now())
    wx.setStorageSync('logs', logs)

    // 登录
    wx.login({
      success: res => {
        // 发送 res.code 到后台换取 openId, sessionKey, unionId
      }
    })
    // 获取用户信息
    wx.getSetting({
      success: res => {
        if (res.authSetting['scope.userInfo']) {
          // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
          wx.getUserInfo({
            success: res => {
            }
          })
        }
      }
    })
  },
  globalData: {
    userInfo: null
  }
})

先给大家把接下来部分要说明的消息事件整理出来让大家可以看的清楚一些,后面的内容主要是围绕这个消息事件和代码来说明

image.png

上面这个图就是从微信开发者工具里面打开页面时候出现的事件和消息类型以及有关数据,分析一下可以看出:

1、首先调用了同步 api getSystemInfo,我们可以看到在我们的项目代码里面没有存在这个 api,但是开发者工具第一步就调用了,所以在项目编译初始化的时候就触发了,至于 Receive 的数据返回的就是工具界面左上角的一些配置数据,大家可以打开自己的开发者工具看看,一些参数是你可以直接从上面观察到的,如手机型号 屏幕占比 网络类型等,这个同步 api 是系统自动调用的不用我们做写什么操作,而且它会比较频繁的调用。(这点我也没理解,当我打开控制台的时候他会调用很多次)

  • 在前面的文章中讲到同步的 api 都是通过走 http 协议’/apihelper/assdk’链接过来的,里面携带了 api 名和一些参数,所以我们可以直接拦截’/apihelper/assdk’请求进行处理

实例代码展示


router.post('/apihelper/assdk', async (ctx, next) => {
const { api, args } = JSON.parse(ctx.request.body);
if (api === 'getSystemInfo') {
    ctx.body = {
      'errMsg': 'getSystemInfo:ok',
      'model': 'iPhone 7 Plus',
      'pixelRatio': 3,
      'windowWidth': 414,
      'windowHeight': 624,
      'system': 'iOS 10.0.1',
      'language': 'en',
      'version': '7.0.4',
      'screenWidth': 414,
      'screenHeight': 736,
      'SDKVersion': '2.7.1',
      'brand': 'devtools',
      'fontSizeSetting': 16,
      'benchmarkLevel': 1,
      'batteryLevel': 100,
      'statusBarHeight': 20,
      'safeArea': { 'right': 414, 'bottom': 736, 'left': 0, 'top': 20, 'width': 414, 'height': 716 }
    };
  }
})

ctx.body 的结果集可以直接复制开发者工具中的数据就可以,如果要实际使用的话需要把部分参数改成动态获取的。

2、当代码执行 wx.getStorageSync(‘logs’) wx.setStorageSync(‘logs’, logs)的时候(标签二)通过前面的日志描述可以知道它们都是 appservice sdk 调用的同步的 api,就是我们普通使用的 Storage 它们包装做了一层转换,因为这个页面是初始化的时候调用的所以这里面的数据都是空的。当点击 log 页面的时候,就可以看到调用了 APPSERVICE_PUBLISH 事件类型,此类型是 view 层发给 service 层的我们看到它的 webviewID 存在值。

image.png

看出 data 里面的数据就是 Storage 存储的 Date.now()时间, wx.setStorageSync wx.getStorageSync 在源码对应的地方。

image.png

image.png


var r = re.IS_IOS ? "setStorage" : "setStorageSync"
var r = re.IS_IOS ? "getStorage" : "getStorageSync"

看出如果是 ios 的话就只能调用 setStorage getStorage 方式。

模拟代码展示:


const storage = new Map();

const getStorage = function (key) {
  return storage.get(key);
};
const setStorage = function (key, value) {
  storage.set(key, value);
};
const removeStorage = function (key) {
  storage.delete(key);
};

module.exports = {
  getStorage,
  setStorage,
  removeStorage
};

如果想模拟全局的存储简单的可以直接使用 map 进行操作,然后在调用的时候获取对应的 api 进行获取和设置操作,完善的话存储可以采用浏览器的 LocalStorage,SessionStorage 或者一些 npm 包进行处理。

3、我们比较常用的一个 api 通过 wx.login 换取 code 可以看出(标签三)中,是通过逻辑层 service 自发自收的,发起一个请求通过 service 逻辑层处理后在回调回去,通过微信 api 源码可以看到调用的地方

image.png

这一块要想要弄的很好不是很容易,涉及到用户体系就要和服务端彻底打通,好在登录授权这块微信也是采用 OAuth2 协议实现的。

如果有朋友对 OAuth2 还不是很了解明白的话,建议看下这个文档(https://tools.ietf.org/html/rfc6749)比市面上 90%的相关内容讲的更彻底通透。

4、当在小程序中打开页面时触发了 onAppRoute 事件,通过日志看出发送了 APPSERVICE_ON_EVENT 事件,path 表示当前页面,openType 表示操作类型,openType 如果不是很明显的话我们试着点击一下,从日志页面返回首页的后退操作。

image.png

看到"openType":"navigateBack"这个应该比较好理解解释了。

到这里我们会有一些不一样的地方了,因为这里的操作的是事件处理不是简单的 api 处理就 ok 了,所有前端页面操作的控制器都是一个整体,首先我们要先搭建这个载体容器存放各个部分。

模拟代码实例:

构建一个总管理处理的信息


constructor(wxConfig = {}, socketPort) {
  super();
  this.wxConfig = wxConfig;
  this.systemManager = new SystemManager(this, wxConfig);
  this.navigatorManager = new NavigatorManager(this, wxConfig);
  this.pageManager = new PageManager(wxConfig, socketPort);
  this.tabbarManager = new TabbarManager(this, wxConfig);
  this._render();
  this._launch();
  window.socketClient.setEmitter(this);
}

然后 render 页面信息,结合 node 后台服务渲染前端展示


this.domElement = document.createElement('div');
this.domElement.id = 'container';
this.domElement.style = ` height: ${global.simulator.height}px; width: ${global.simulator.width}px;`
// system
this.domElement.appendChild(this.systemManager.domElement);
// navigator
this.domElement.appendChild(this.navigatorManager.domElement);
// pages
this.domElement.appendChild(this.pageManager.domElement);
// tabbar
this.domElement.appendChild(this.tabbarManager.domElement);

后面就是监听各个事件控制和各种业务处理,核心还是要按照开发者工具的消息顺序和内容来实现。

例如上面我们提到的 navigateBack 我们自己这边实现按照平常的业务写法就 ok。


navigateBack (path, query) {
  let currentWebview = this.domElement.children.item(0);
  let currentIndex = currentWebview.style['z-index'];
  this.domElement.removeChild(currentWebview);

  let targetPage = null;
  for (let i = 0; i < this.domElement.children.length; i++) {
    let webview = this.domElement.children.item(i);
    if (webview.style['z-index'] === `${currentIndex - 1}`) {

      let viewId = +webview.getAttribute('data-view-id');
      let path = webview.getAttribute('data-view-path');
      let query = JSON.parse(webview.getAttribute('data-view-query'));
      window.socketClient.send(WebsocketMessage.onAppRoute(viewId, path, query, 'navigateBack'));
      window.socketClient.send(WebsocketMessage.onAppRouteDone(viewId, path, query, 'navigateBack'));
      targetPage = path;
      break;
    }
  }
  return targetPage;
}

重要的还是 socketClient.send 消息的正确传递才可以和基础库正确的交互

其他的很多对外的 api 实现方式都是大同小异,主要是在接收到消息后怎么处理设计

我们知道了核心的流程,下面要做的就是模仿设计,模仿它的消息格式和返回结构,设计自己的各系统模块的关联

对于小游戏而言大致是一样的,主要有几个点不同:

  • 小游戏是通过根目录下的 game.json 来对小游戏进行全局配置,决定相关界面渲染和属性设置等;
  • 在小游戏的运行环境里面不存在 BOM 和 DOM API,只有 wx API 对它们进行了包装,所以无法直接使用;
  • 小游戏的运行层只有一层在 view 里面跑;
  • 小游戏的大部分 api 主要都是对文件系统和网络的处理。

上面的一些总结主要是根据一些 api 的实现来描述扩展了一些,看完后希望大家对此有所了解,后面我打算从全局来讲下怎么从代码设计方面来设计整个浏览器运行环境实现方案。

本文作者:风逝

(完)