从零搭建乞丐版的小程序平台(1)-渲染和逻辑层的分离实现

简介

在小程序开发如火如荼的今天,生态越发的完整,各类开发框架的出现带来了开发上的便利,巨大的入口流量,让我们无法拒绝小程序。于此同时,了解小程序平台的底层原理也尤为重要,笔者也对于小程序在安全隔离,性能优化上颇为感兴趣,因此也尝试了实现了一些小程序平台的相关功能。本文将介绍笔者的一些思考和实际操作。

架构设计

在对于设计渲染和逻辑层面分离上,笔者也去调研了一些开源的作品的做法。
preact-worker-demo

image

如图 在构建和修改viabledom(undom) 时记录存储dom的id(顺序增长)和引用,undom是dom标准实现的一个库,可以让dom一些功能在任意地方去运行,preact--worker-demo 利用undom的相关接口,并利用
MutationObserver接口去监听dom的改动,并利用MutationRecords在主线程(渲染线程)去重放preact 对于undom创建和改动。

/** Observe DOM mutations and send MutationRecords to parent page. */
(new MutationObserver( mutations => {
    for (let i=mutations.length; i--; ) {
        let mutation = mutations[i];
        for (let j=TO_SANITIZE.length; j--; ) {
            let prop = TO_SANITIZE[j];
            mutation[prop] = sanitize(mutation[prop]);
        }
    }
    send({ type:'MutationRecord', mutations });
})).observe(document, { subtree:true });

在主线程重放构建的过程中,代理preact 对于window的监听方式,将捕获事件携带上产出事件的dom上的id一起发送到worker线程。
在worker线程上利用记录的map 索引到dom的引用,重构event 在dispatch 出去。

function proxyEvent(e) {
        if (e.type==='click' && touch) return false;
        let event = { type: e.type };
        if (e.target) event.target = e.target.__id;
        // ....
        worker.postMessage({
            type: 'event',
            event
        });
        // .....
    }

其实还有不少分离方案的实现,笔者也觉得非常不错,就不在此处介绍了

笔者并未采纳此种方案,再通过对支付宝和微信小程序编码后的前端程序研究后,给出了一下一个设计架构。

image

逻辑层

因为逻辑层的运行时一个纯v8运行环境,只能执行基础js代码。
因此我们在使用rollup 打包时 format选择了umd格式。
我们使用lerna管理我们的相关包管理

  • dtf
    • packages/
      • dtf-core
      • dtf-example //用户代码
      • dtf-web // web渲染方案

我们可以首先看一下dtf-example上最终开发者会开发的代码。


image

如上图,笔者借鉴了react component和jsx的相关写法和语法。
我们定义了component和初始化的state,并将组件带上路由利用Page进行注册.

那我们具体来看一下dtf-core 我们都做了一些什么吧

dtf-core 重要的一个作用就是去描述组件模板页成为一个virtual dom。
因为利用typescript 书写 所以我们还需要定义好JSX.IntrinsicElements接口 才能让typescript编译时把我们的模板利用我们暴露出去的createElement (简写成h)包裹好。

这里我们采用preact 的createElement相关写法,简单封装一下jsx语法传入参数成为一个描述结构,于此同时我们也顺序记录上相关绑定事件。

export const handlers = new Map<Number, (...args: any) => any>();

export function createElement(type: string,
                              props:
                                  | JSX.HTMLAttributes &
                                  JSX.SVGAttributes &
                                  Record<string, any>
                                  | null & { children: ComponentChildren[] },
                              children?: ComponentChildren): VNode<any> {
    const newProps = {...props};
    let newChildren: ComponentChildren[] = [];
    if(children){
        newChildren=[children]
    }
    if (arguments.length > 3) {
        // eslint-disable-next-line no-param-reassign
        for (let i = 3; i < arguments.length; i++) {
            newChildren.push(arguments[i]);
        }
    }
    if (newChildren.length) {
        newProps.children = newChildren;
    }
    for (let k in newProps) {
        if (k && k.substr(0, 2) === 'on') {
            const id=handlers.size + 1
            handlers.set(id, () => {
                return props[k]();
            })
            newProps[k]=id
        }
    }
    let ref = newProps.ref;
    let key = newProps.key;
    if (ref !== null) delete newProps.ref;
    if (key !== null) delete newProps.key;

    return createVNode(type, newProps, key, ref);

}

在App注册全局页面组件后,我们会向rendertime 发送一个initData的信息,
DtfWorker 是我们注入V8 runtime的一个全局变量。同时我们也开始接受,来自rendertim
的eventcall 和lifecycle事件的触发。

export function Page(componetInstance:DTFComponent<any,any>,url:string):PageInfo{
                                                                                                                                               
    return {
        component:{
            vitrualDom:componetInstance.render(),
            state:componetInstance.state
        },
        url
    }

}

export function App(Pages:PageInfo[]){
    try{
        DtfWorker.onMessage=(message)=>{
            if(message.type && message.type.substr(0,2)==='on' && message.handerId){
                const func=handlers.get(message.handerId);
                func && func()
            }
            //.... 
        }
        DtfWorker.sendMessage({
            type:'initData',
            data:JSON.stringify(Pages)
    
        })
        DtfWorker.Pages=Pages;
    }catch(error){
    }
    
    return Pages
}

在android开发层面,我们利用j2v8 来快速搭建一个v8 runtime。

implementation 'com.eclipsesource.j2v8:j2v8:6.0.0@aar'

runtime=V8.createV8Runtime();
runtime.executeVoidScript("var DtfWorker={}");
dtfWorker=runtime.getObject("DtfWorker");
dtfWorker.registerJavaMethod(callback,"sendMessage");

new Thread(()->{
    String content= null;
    try {
        content = readFile(this.resourceStream);
    } catch (IOException e) {
        e.printStackTrace();
    }
    String finalContent = content;
    handler.post(()->{
       runtime.executeVoidScript(finalContent);
   });
}).start();

渲染层

渲染层,我们依旧采用react native 原声模块提供的通信方案来进行通信。

import {NativeModules} from 'react-native';
export const DtfWorker = NativeModules.dtfWorker;

//index.js
// 先运行react native 加载完成后,开始运行v8 runtime
DtfWorker.runMinProgram();
AppRegistry.registerComponent(appName, () => App);

同时我们在react native 中开始监听相关信息,并根据type进行一些事件分发。

DeviceEventEmitter.addListener('dtfWorkerSendMessage', data => {
      const message = JSON.parse(data.message);
      switch (message.type) {
        case 'initData':
          //....
          break;
        case 'setState':
          //....
          break;

      }
    });

在设计全局state store时采用了类似dva的分module的方法。同时也利用create-react-class 来设计生成我们的react native 组件。

export function createComponents(page) {
  const component = page.component;
  // ...
  const renderComponent = createReactClass({
    render: function() {
      return renderReactNode(component.vitrualDom, this.state);
    },
  、//...
  });
  return renderComponent;
}


function renderReactNode(node, state) {
  const children = node.props.children || [];
  if (node.type === 'data') {
    return React.createElement(
      Text,
      renderProps(node),
      children.map(item =>
        state[node] !== undefined ? state[node] : renderReactNode(node, state),
      ),
    );
  }
  if (typeof node !== 'object') {
    return React.createElement(
      Text,
      null,
      state[node] !== undefined ? state[node] : node,
    );
  }
  return React.createElement(
    renderType(node.type),
    renderProps(node),
    Array.isArray(children)
      ? children.map(item => renderReactNode(item, state))
      : children,
  );
}

function renderType(type) {
  if (type === 'div') {
    return View;
  }
  if (type === 'button') {
    return Button;
  }
  if (type === 'data') {
    return Text;
  }
  return type;
}

function renderProps(node) {
  const {type, props} = node;
  if (type === 'button') {
    props.title = props.children.join(',');
  }
  for (let k in props) {
    if (k && k.substr(0, 2) === 'on') {
      const id = props[k];
      // ~~ 其实又很多监听事件需要匹配映射好。这里就简单写个onPress了
      props.onPress = (event) => {
        DtfWorker.sendEventMessage(
          JSON.stringify({
            type: k,
            handerId: id,
            event
          }),
        );
      };
    }
  }
  return props;
}

在解析模板页信息的时候其实又更多的处理和适配去,这里就不多做了。

总结:

本文主要记录了笔者在实现小程序平台时渲染和逻辑分离的想法和实践方法。思路仅供参考。笔者后期也会不断优化。适当完善代码后会进行分享。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,813评论 1 18
  • 1 基本概念:Component(组件)、instance(组件实例)、 element、jsx、dom Comp...
    水落斜阳阅读 1,571评论 1 3
  • 前言 在Jq,原生javascript时期,在写页面时,往往强调的是内容结构,层叠样式,行为动作要分离,三者之间分...
    itclanCoder阅读 713评论 0 2
  • 微信小程序 项目结构 上图为微信小程序的项目结构,pages下面包含了小程序中的每一个页面,每一个页面由页面结构,...
    Jensen95阅读 8,886评论 0 19
  • 因个人精力有限,暂停简书的维护,欢迎大家关注我的知乎https://www.zhihu.com/people/we...
    尾尾阅读 1,172评论 3 13