简介
在小程序开发如火如荼的今天,生态越发的完整,各类开发框架的出现带来了开发上的便利,巨大的入口流量,让我们无法拒绝小程序。于此同时,了解小程序平台的底层原理也尤为重要,笔者也对于小程序在安全隔离,性能优化上颇为感兴趣,因此也尝试了实现了一些小程序平台的相关功能。本文将介绍笔者的一些思考和实际操作。
架构设计
在对于设计渲染和逻辑层面分离上,笔者也去调研了一些开源的作品的做法。
如 preact-worker-demo
如图 在构建和修改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
});
// .....
}
其实还有不少分离方案的实现,笔者也觉得非常不错,就不在此处介绍了
笔者并未采纳此种方案,再通过对支付宝和微信小程序编码后的前端程序研究后,给出了一下一个设计架构。
逻辑层
因为逻辑层的运行时一个纯v8运行环境,只能执行基础js代码。
因此我们在使用rollup 打包时 format选择了umd格式。
我们使用lerna管理我们的相关包管理
- dtf
- packages/
- dtf-core
- dtf-example //用户代码
- dtf-web // web渲染方案
- packages/
我们可以首先看一下dtf-example上最终开发者会开发的代码。
如上图,笔者借鉴了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;
}
在解析模板页信息的时候其实又更多的处理和适配去,这里就不多做了。
总结:
本文主要记录了笔者在实现小程序平台时渲染和逻辑分离的想法和实践方法。思路仅供参考。笔者后期也会不断优化。适当完善代码后会进行分享。