百度小程序原理
整体框架
百度小程序的开发Api,加构设计与微信小程序基本一致。
为了提升整体性能,充分利用手机的多CPU性能:
- 把逻辑层与渲染层分离,分别位于不同的运行容器
- 异步请求都由native来执行
- 逻辑层
- 逻辑层就是对开发者所暴露的Api,有App,Page,布局文件,其中的App,Page都是两个函数
- App()函数的处理:直接创建App对象,全局唯一对象
- Page()函数的处理:保存到Map中,不会马上构建Page对象,当导航到页面时,才会真正创建Page对象
- 渲染层
- 使用MVVM框架san来渲染界面
- 在编译期间把小程序标签转化为san框架所支持的标签
- 为每个小程序页面,创建对应的san框架下Page组件,PageComponent的template就是swan.xml转译后的内容
- 渲染层与逻辑层交互
- 渲染层接收用户的交互事件,由统一的函数处理后,通过消息总线传递到逻辑层的Page对象,再调用对应的函数
- 逻辑层依据用户操作,执行业务操作,修改data数据,通过消息总线传递到渲染层的组件里,San.Page组件会自动更新界面
开发流程
编译
一个简单的百度小程序项目:
- 目录结构:
- App.js的源码
js
App({
onLaunch(event) {
console.log('onLaunch');
},
onShow(event) {
console.log('onShow');
},
globalData: {
userInfo: 'user'
}
});
- index.js的源码:
js
var p = [];
Page({
data: {
text: "这是一段文字."
},
add: function(e) {
p.push("其他文字");
this.setData({
text: "这是一段文字." + p.join(",")
})
},
remove: function(e) {
if(p.length > 0){
p.pop();
this.setData({
text: "这是一段文字." + p.join(",")
});
}
}
});
- index.swan的源码:
js
<view>
<view class="text-px text-{{text}}">{{text}}</view>
<button class="btn" type="primary" bind:tap="add">add text</button>
<button class="btn" type="primary" bind:tap="remove">remove text</button>
</view>
以上的小程序代码,经过编译后的情况:
- 目录结构:
- App.js的源码:
js
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
window.define("138",
function(t, e, n, o, a, i, s, c, r, u, d, l, g, w, f, h) {
var p = [];
Page({
data: {
text: "这是一段文字."
},
add: function(t) {
p.push("其他文字");
this.setData({
text: "这是一段文字." + p.join(",")
})
},
remove: function(t) {
p.length > 0 && (p.pop(), this.setData({
text: "这是一段文字." + p.join(",")
}))
}
})
});
window.define("193",
function(t, e, n, o, a, i, s, c, r, u, d, l, g, w, f, h) {
App({
onLaunch: function(t) {
console.log("Lifecycle App onLaunch")
},
onShow: function(t) {
console.log("Lifecycle App onShow")
},
globalData: {
userInfo: 'user'
}
})
});
window.__swanRoute = "app";
window.usingComponents = [];
require("193");
window.__swanRoute = "pages/text/text";
window.usingComponents = [];
require("138");
- index.swan.js的源码:
js
// 注意,做了一些简化
((global)=>{
global.errorMsg = [];
var templateComponents = Object.assign({}, {});
var param = {};
var filterArr = JSON.parse("[]");
try {
filterArr && filterArr.forEach(function (item) {
param[item.module] = eval(item.module)
});
var pageContent = `
<view>
<view>{{text}}</view>
<button class=\"btn\" type=\"primary\" on-bindtap=\"eventHappen('tap', $event, 'add', '', 'bind')\">
add text
</button>
<button class=\"btn\" type=\"primary\" on-bindtap=\"eventHappen('tap', $event, 'remove', '', 'bind')\">
remove text
</button>
</view>`;
var renderPage = function (filters, modules) {
// 路径与该组件映射
// var customAbsolutePathMap = (global.componentFactory.getAllComponents(), {});
// 当前页面使用的自定义组件
// const pageUsingComponentMap = JSON.parse("{}");
// 生成该页面引用的自定义组件
// const customComponents = Object.keys(pageUsingComponentMap).reduce((customComponents, customName) => {
// customComponents[customName] = customAbsolutePathMap[pageUsingComponentMap[customName]];
// return customComponents;
// }, {});
global.pageRender(pageContent, templateComponents)
};
renderPage(filterArr, param);
} catch (e) {
global.errorMsg['execError'] = e;
throw e;
}
})(window);
编译总结:
- 对template进行转换:
- 标签转换:bind:tap ===> on-bindtap
- 事件包装:eventHappen(‘tap’, $event, ‘add’, ‘’, ‘bind’)
- 对App.js进行包装,提升效率,减少逐一加载流程
- 通过渲染模板,生成index.swan.js文件,提升渲染效率
加载,启动,渲染
用户点击跳转到小程序后:
- Native的任务:
- 下载小程序.zip文件
- 启动两个web运行容器:
- 渲染层webview加载slaves.html
- 逻辑层jscore加载master.html
- 解析小程序app.json,发送’AppReady’事件
- 逻辑层master.js:
- 监听’AppReady’事件,执行小程序的调起逻辑
js
/**
* 监听客户端的调起逻辑
*/
listenAppReady() {
this.swaninterface.bind('AppReady', event => {
console.log('master listener AppReady ', event);
swanEvents('masterActiveStart');
// 给三方用的,并非给框架用,请保留
this.context.appConfig = event.appConfig;
// 初始化master的入口逻辑
this.initRender(event);
// this.preLoadSubPackage();
});
}
- 初始化master的入口逻辑,小程序的每个界面,对应一个Slave对象(与渲染层的slave.js不一样),依据用户打开多个页面,构建一个导航栈,保存在navigator对象里
js
/**
* 初始化渲染
*
* @param {Object} initEvent - 客户端传递的初始化事件对象
* @param {string} initEvent.appConfig - 客户端将app.json的内容(json字符串)给前端用于处理
* @param {string} initEvent.appPath - app在手机上的磁盘位置
* @param {string} initEvent.wvID - 第一个slave的id
* @param {string} initEvent.pageUrl - 第一个slave的url
*/
initRender(initEvent) {
// 设置appConfig
this.navigator.setAppConfig({
...JSON.parse(initEvent.appConfig),
...{
appRootPath: initEvent.appPath
}
});
swanEvents('masterActiveInitRender');
// 压入initSlave
this.navigator.pushInitSlave({
pageUrl: initEvent.pageUrl,
slaveId: +initEvent.wvID,
root: initEvent.root,
preventAppLoad: initEvent.preventAppLoad
});
this.appPath = initEvent.appPath;
swanEvents('masterActivePushInitslave');
}
- 创建初始化页面的slave后,如果没有预加载,就加载小程序里的app.js文件(注意:是编译后的app.js文件),并发送’slaveLoaded’事件,通知渲染层开始渲染
js
/**
* 初始化第一个slave
* @param {Object} [initParams] - 初始化的参数
*/
pushInitSlave(initParams) {
....
// 创建初始化slave
this.initSlave = this.createInitSlave(initParams.pageUrl, this.appConfig);
// slave的init调用
this.initSlave
.init(initParams)
.then(initRes => {
swanEvents('masterActiveCreateInitslaveEnd');
// 入栈
this.history.pushHistory(this.initSlave);
swanEvents('masterActivePushInitslaveEnd');
// 调用slave的onEnqueue生命周期函数
this.initSlave.onEnqueue();
swanEvents('masterActiveOnqueueInitslave');
});
}
/**
* 初始化为第一个页面
*
* @param {Object} initParams 初始化的配置参数
* @return {Promise} 返回初始化之后的Promise流
*/
Slave.init(initParams) {
this.isFirstPage = true;
return Promise
.resolve(initParams)
.then(initParams => {
swanEvents('masterActiveInitAction');
if (!!initParams.preventAppLoad) {
return initParams;
}
// const loadCommonJs = this.appConfig.splitAppJs
// && !this.appConfig.subPackages
// ? 'common.js' : 'app.js';
const loadCommonJs = 'app.js';
return loader
.loadjs(`${this.appRootPath}/${loadCommonJs}`, 'masterActiveAppJsLoaded')
.then(() => {
return this.loadJs.call(this, initParams);
});
})
.then(initParams => {
this.uri = initParams.pageUrl.split('?')[0];
this.accessUri = initParams.pageUrl;
this.slaveId = initParams.slaveId;
// init的事件为客户端处理,确保是在slave加载完成之后,所以可以直接派发
this.swaninterface.communicator.fireMessage({
type: `slaveLoaded${this.slaveId}`,
message: {slaveId: this.slaveId}
});
return initParams;
});
}
- 执行slave入栈后的生命周期函数this.initSlave.onEnqueue(); 在此函数里,会真正Page Instance,同时监听到渲染层准备好后,发送’initData’事件
js
/**
* 入栈之后的生命周期方法
*
* @return {Object} 入栈之后,创建的本slave的页面实例对象
*/
onEnqueue() {
return this.createPageInstance();
}
/**
* 创建页面实例,并且,当slave加载完成之后,向slave传递初始化data
*
* @return {Promise} 创建完成的事件流
*/
createPageInstance() {
if (this.isCreated()) {
return Promise.resolve();
}
swanEvents('masterActiveCreatePageFlowStart', {
uri: this.uri
});
const userPageInstance = createPageInstance(this.accessUri, this.slaveId, this.appConfig);
const query = userPageInstance.privateProperties.accessUri.split('?')[1];
this.setUserPageInstance(userPageInstance);
try {
swanEvents('masterPageOnLoadHookStart');
userPageInstance._onLoad(getParams(query));
swanEvents('masterPageOnLoadHookEnd');
}
catch (e) {
// avoid empty state
}
this.status = STATUS_MAP.CREATED;
console.log(`Master 监听 slaveLoaded 事件,slaveId=${this.slaveId}`);
return this.swaninterface.invoke('loadJs', {
uri: this.uri,
eventObj: {
wvID: this.slaveId
},
success: params => {
swanEvents('masterActiveCreatePageFlowEnd');
swanEvents('masterActiveSendInitdataStart');
userPageInstance.privateMethod
.sendInitData.call(userPageInstance, this.appConfig);
swanEvents('masterActiveSendInitdataEnd');
}
});
}
- 渲染层slave.js
- 监听’PageReady’事件,加载对应页面的文件:app.css,index.css,index.swan.js文件
js
/**
* 监听pageReady,触发整个入口的调起
* @param {Object} [global] 全局对象
*/
listenPageReady(global) {
swanEvents('slavePreloadListened');
// 控制是否开启预取initData的开关
let advancedInitDataSwitch = false;
this.swaninterface.bind('PageReady', event => {
swanEvents('slaveActiveStart', {
pageInitRenderStart: Date.now() + ''
});
...
const appPath = event.appPath;
const pagePath = event.pagePath.split('?')[0];
const onReachBottomDistance = event.onReachBottomDistance;
...
let loadUserRes = () => {
// 设置页面的基础路径为当前页面本应所在的路径
// 行内样式等使用相对路径变成此值
// setPageBasePath(`${appPath}/${pagePath}`);
swanEvents('slaveActivePageLoadStart');
// 加载用户的资源
Promise.all([
loader.loadcss(`${appPath}/app.css`, 'slaveActiveAppCssLoaded'),
loader.loadcss(`${appPath}/${pagePath}.css`, 'slaveActivePageCssLoaded')
])
.catch(() => {
console.warn('加载css资源出现问题,请检查css文件');
})
.then(() => {
// todo: 兼容天幕,第一个等天幕同步后,干掉
swanEvents('slaveActiveCssLoaded');
swanEvents('slaveActiveSwanJsStart');
loader.loadjs(`${appPath}/${pagePath}.swan.js`, 'slaveActiveSwanJsLoaded');
});
};
// (event.devhook === 'true' ? loadHook().then(loadUserRes).catch(loadUserRes) : loadUserRes());
loadUserRes();
});
}
- 在每个页面编译后的xxx.swan.js文件里,会执行pageRender()函数,进行界面渲染,如此demo里的index.swan.js文件:
js
((global)=>{
global.errorMsg = [];
var templateComponents = Object.assign({}, {});
var param = {};
var filterArr = JSON.parse("[]");
try {
filterArr && filterArr.forEach(function (item) {
param[item.module] = eval(item.module)
});
var pageContent = `
<div class=\"wrap\">
<div>{{text}}</div>
<button class=\"btn\" type=\"primary\" v-on:click=\"eventHappen('tap', $event, 'add', '', 'bind')\">
add text
</button>
<button class=\"btn\" type=\"primary\" v-on:click=\"eventHappen('tap', $event, 'remove', '', 'bind')\">
remove text
</button>
</div>`;
var renderPage = function (filters, modules) {
...
global.pageRender(pageContent, templateComponents)
};
renderPage(filterArr, param);
} catch (e) {
global.errorMsg['execError'] = e;
throw e;
}
})(window);
- global.pageRender()函数是在slave.js文件里定义的方法,其内部的逻辑就是创建对应的san框架里的Page组件,等待初始化数据过来后,再绑定到界面上
js
/**
* 注册所有components(也包括顶层components -- page)
*/
registerComponents() {
...
global.pageRender = (pageTemplate, templateComponents, customComponents, filters, modules) => {
...
// 定义当前页面的组件
componentFactory.componentDefine(
'page',
{
template: `<swan-page tabindex="-1">${pageTemplate}</swan-page>`,
superComponent: 'super-page'
},
{
classProperties: {
components: {...componentFactory.getComponents(), ...templateComponents, ...customComponents},
filters: {
...filtersObj
}
}
}
);
swanEvents('slaveActiveDefineComponentPage');
// 获取page的组件类
const Page = global.componentFactory.getComponents('page');
// 初始化页面对象
const page = new Page();
swanEvents('slaveActiveConstructUserPage');
// 调用页面对象的加载完成通知
page.slaveLoaded();
swanEvents('slaveActiveUserPageSlaveloaded');
// 用于记录用户模板代码在开始执行到监听initData事件之前的耗时
global.FeSlaveSwanJsInitEnd = Date.now();
// 监听等待initData,进行渲染
page.communicator.onMessage('initData', params => {
swanEvents('slaveActiveReceiveInitData');
try {
// 根据master传递的data,设定初始数据,并进行渲染
page.setInitData(params);
swanEvents('slaveActiveRenderStart');
// 真正的页面渲染,发生在initData之后
// 此处让页面真正挂载处于自定义组件成功引用其他自定义组件之后,
// 引用其它自定义组件是在同一时序promise.resolve().then里执行, 故此处attach时, 自定义组件已引用完成
setTimeout(() => {
page.attach(document.body);
// 通知master加载首屏之后的逻辑
page.communicator.sendMessage(
'master', {
type: 'slaveAttached',
slaveId: page.slaveId
}
);
swanEvents('slaveActivePageAttached');
}, 0);
}
catch (e) {
console.log(e);
global.errorMsg['renderError'] = e;
}
}, {listenPreviousEvent: true});
...
};
...
}
- 当界面渲染后,发送’slaveAttached’事件,逻辑层执行onShow()生命周期函数
交互
- 当用户点击界面上的button按钮时,会触发san.Page组件里的eventHappen()函数,发送’event’事件
js
/**
* 执行用户绑定的事件
*
* @param {string} eventName 事件名称
* @param {Object} $event 事件对象
* @param {Function} reflectMethod 用户回调方法
* @param {boolean} capture 是否事件捕获
* @param {boolean} catchType 是否终止事件执行
* @param {Object} customEventParams 用户绑定的事件集合
*/
eventHappen: function(eventName, $event, reflectMethod, capture, catchType, customEventParams) {
swanEvents('slaveEventHappen', {
eventName: eventName,
});
if ($event && catchType === 'catch') {
$event.stopPropagation && $event.stopPropagation();
(eventName === 'touchstart' || eventName === 'touchmove')
&& $event.preventDefault && $event.preventDefault();
}
this.$communicator.sendMessage(
'master',
{
type: 'event',
value: {
eventType: eventName,
reflectMethod,
e: $event, //eventProccesser(eventName, $event)
},
slaveId: this.slaveId,
customEventParams
}
);
}
- 逻辑层master.js监听’event’事件后,执行对应Page里的对应函数
js
/**
* 绑定开发者绑定的events
*/
bindDeveloperEvents() {
this.slaveCommunicator.onMessage('event', event => {
swanEvents('masterListenEvent', event);
const eventOccurredPageObject = this.history.seek(event.slaveId).getUserPageInstance();
// if (event.customEventParams) {
// const nodeId = event.customEventParams.nodeId;
// const reflectComponent = eventOccurredPageObject
// .privateProperties.customComponents[nodeId];
// if (reflectComponent[event.value.reflectMethod]) {
// reflectComponent[event.value.reflectMethod]
// .call(reflectComponent, event.value.e);
// }
// }
// else
if (eventOccurredPageObject[event.value.reflectMethod]) {
eventOccurredPageObject[event.value.reflectMethod]
.call(eventOccurredPageObject, event.value.e);
}
});
}
- 当逻辑层master,执行this.setData()函数更新界面时,会发送’setData’事件,渲染层slave.js会监听此事件,进行界面更新
js
// 逻辑层Page对象
/**
* 页面中挂载的setData操作方法,操作后,会传到slave,对视图进行更改
*
* @param {string|Object} [path] - setData的数据操作路径,或setData的对象{path: value}
* @param {*} [value] - setData的操作值
* @param {Function} [cb] - setData的回调函数
*/
setData(path, value, cb) {
this.sendDataOperation({
type: 'set',
path,
value,
cb
});
},
// 渲染层
/**
* 初始化事件绑定
* @private
*/
initMessagebinding: function() {
this.$communicator.onMessage(
['setData', 'pushData', 'popData', 'unshiftData', 'shiftData', 'removeAtData', 'spliceData'],
params => {
swanEvents('slaveDataEvent', params);
const setObject = params.setObject || {};
const operationType = params.type.replace('Data', '');
if (operationType === 'set') {
// TODO-ly 此处可以优化,使用Vue效率最高的方案
for(var key in setObject){
this[key] = setObject[key];
}
}
}
);
},