本文会围绕小程序的基础原理进行介绍。主要包括小程序的基础结构、编译、加载、通讯等几个方面。旨在阅读完毕后可以对小程序有一个基本的印象。
对于用户来讲,小程序无需下载、用完即走、体验良好。
对于开发者来讲,小程序主要是区别于端内的内嵌纯Web页面。
微信小程序开发指南
DSL:Domain Specific Language(领域特定语言)
所谓DSL,是指利用为特定领域(Domain)所专门设计的词汇和语法,简化程序设计过程,提高生产效率的技术,同时也让非编程领域专家的人直接描述逻辑成为可能。DSL
的优点是,可以直接使用其对象领域中的概念,集中描述“想要做到什么”(What)的部分,而不必对“如何做到”(How)进行描述。摘自:《代码的未来》 — [日]
松本行弘
目的:限定问题边界和控制复杂度,提高编程效率,更加的抽象化。
举例:1. 内部:jQuery。
我们需要使用WXML、WXSS、WXS结合基础组件和事件系统来构建小程序页面。
WXML
<view> {{message}} view>
<view wx:for="{{array}}"> {{item}} view>
<view wx:if="{{view == 'WEBVIEW'}}"> WEBVIEW view>
<view wx:elif="{{view == 'APP'}}"> APP view>
<view wx:else="{{view == 'MINA'}}"> MINA view>
<template name="staffName">
<view>
FirstName: {{firstName}}, LastName: {{lastName}}
view>
template>
<template is="staffName" data="{{...staffA}}">template>
<template is="staffName" data="{{...staffB}}">template>
<template is="staffName" data="{{...staffC}}">template>
WXSS
.padding {
padding: 20rpx;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uMOyCt2Q-1668755624647)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/87ae7f449c8c4dc2a44bb349ae0cd9c5~tplv-k3u1fbpfcp-zoom-1.image)]
WXS WXS 语法参考 WXS响应事件
WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML
,可以构建出页面的结构。
WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。
特点:
常用场景:
过滤器
// /pages/tools.wxs
var foo = "five";
var bar = function (d) {
return `${d} apples`;
}
module.exports = {
FOO: foo,
bar: bar,
};
module.exports.msg = "some msg";
<wxs src="./../tools.wxs" module="tools" />
<view> {{tools.msg}} view>
<view> {{tools.bar(tools.FOO)}} view>
some msg
five apples
拖拽效果
<wxs module="test" src="./movable.wxs">wxs>
<view wx:if="{{show}}" class="area" style='position:relative;width:100%;height:100%;'>
<view
data-index="1"
data-obj="{{dataObj}}"
bindtouchstart="{{test.touchstart}}"
bindtouchmove="{{test.touchmove}}"
bindtouchend='{{test.touchmove}}'
class="movable"
style="position:absolute;width:100px;height:100px;background:{{color}};left:{{left}}px;top:{{top}}px"
>view>
view>
// pages/movable/movable.js
Page({
data: {
left: 50,
top: 50,
color: 'red',
taptest: 'taptest',
show: true,
dataObj: {
obj: 1
}
},
onLoad: function (options) {
},
onReady: function () {
},
testCallmethod(params) {
console.log(params);
this.setData({
color: params.c,
})
}
})
// movable.wxs
var startX = 0
var startY = 0
var lastLeft = lastTop = 50
function touchstart(event, ins) {
var touch = event.touches[0] || event.changedTouches[0]
startX = touch.pageX
startY = touch.pageY
ins.callMethod('testCallmethod', {
c: 'blue',
})
}
function touchmove(event, ins) {
var touch = event.touches[0] || event.changedTouches[0]
var pageX = touch.pageX
var pageY = touch.pageY
var left = pageX - startX + lastLeft
var top = pageY - startY + lastTop
startX = pageX
startY = pageY
lastLeft = left
lastTop = top
ins.selectComponent('.movable').setStyle({
left: left + 'px',
top: top + 'px'
})
}
module.exports = {
touchstart: touchstart,
touchmove: touchmove,
}
使用 DSL 的原因:HTML的自由度过高,通过DSL来约束管控。也增加了一定的扩展性,小程序可以在DSL转换的过程中去添加一些自己的兼容。例如:input输入框focus时被键盘遮挡的问题,小程序可以自己通过覆盖原生组件的方式来解决。
整个小程序框架系统分为两部分:逻辑层(App Service)和 视图层(View)。小程序提供了自己的视图层描述语言 WXML
和 WXSS
,以及基于 JavaScript
的逻辑层框架。
通常我们常见的渲染界面的方式有以下三种
小程序是运行在端内的,比如微信小程序的宿主是微信,字节小程序的宿主是头条APP、抖音等。使用原生技术来渲染小程序,很明显的优势是可以获得比较好的用户体验和丰富的原生能力。
但同时也会存在一个问题,完全使用原生来渲染小程序,那么小程序就和宿主绑定在了一起了,完全属于宿主的一部分,小程序的发布也需要依赖端的发版,这种迭代发版的节奏是肯定是不对的。
小程序需要像Web应用一样,可以随时的,直接拉取下载最新的资源包在本地运行。但纯粹的web技术无法直接调用原生能力并且也无法保证良好的用户体验。
因为在一些复杂的交互场景下,逻辑执行会阻塞UI的渲染(单线程),页面也可能会出现一些性能问题。
最终,小程序选择了Hybrid 方案,微信在2015年就推出了JS-SDK,其为开发者提供了丰富的微信原生能力,解决了移动端页面能力不足的问题。
同时每个页面由单独的Webview渲染,降低了每个Webview的渲染负担(多Webview的结构),定义了一系列的内置组件来统一体验。
hybrid方案
由于web技术的灵活性,我们可以使用JavaScript随意直接Window的API去更改页面内容,随意使用eval执行代码,或者直接去跳转至新的页面。
这些对于小程序来说都会导致内容不可控,存在一定的安全合规风险。
并且小程序提供了内置组件来统一体验,如果页面直接跳转至其他的网址上,那小程序就无法再保证统一体验了。
小程序提供一个沙箱环境来给开发者使用。沙箱内只提供JavaScript的解释执行环境,不再提供任何浏览器相关的接口,这样就将开发者的JavaScript运行环境和浏览器环境隔离开了。
所以我们看到架构图中,逻辑层是单独运行在JsCore中的,并没有在浏览器的环境中运行。
不同的运行环境
通讯的延时
因为小程序的JavaScript的环境不在Webview内了,所以它所有的通信都需要端的转发,这会带来一些延时,所以我们在涉及setData操作时,需要注意一些性能问题。
渲染层多Webview
下图是当小程序执行过两次navigateTo
之后的页面栈 [ pageA, pageB, pageC ]。
在小程序中我们路由跳转的API有 navigateTo、navigateBack、redirectTo、switchTab
四种。我们以上图状态为例,来看下四个API执行之后对页面栈的影响。
当我们调用wx.navigateTo({url: ‘pageD’}) 时,此时页面栈会变成 [ pageA, pageB, pageC, pageD ]。
当我们调用wx.navigateBack()时,此时页面栈会变成 [ pageA, pageB ]。
当我们调用wx.redirectTo({url: ‘pageD’})时, 此时页面栈会变成 [ pageA, pageB, pageD ]。
当然这个页面栈的长度不会无限增加,目前已知的是最多限制在10条。当页面栈达到10条之后,继续执行navigateTo
的话,小程序会自动将其转换为redirectTo
执行。
小程序开发者工具测试结果 10条
小程序开发者工具调试Element中也可以看到多个Webview
实际在小程序开发者工具中获取Webview节点,会发现超过了10个。
XXML 在WebView中是无法直接使用的,小程序这里做了一层编译转换。(各平台实现有差别)
这块有一个公式来描述编译前后的结果 :
$$ V i e w = r e n d e r ( s t a t e ) View = render(state) View=render(state)$$
小程序将编译就是将XXML 转换成一系列的render函数,放在视图层中。等到传入路由信息后,找到对应的render函数,再将其渲染为真正的dom节点。
以 123 为例
第一步:使用 HTML Parser 2 对 XXML 进行解析生成DOM树。
第二步:使用 Babel的能力去处理DOM节点,将其转换成抽象语法树。
第三步:使用 babel-generator 将抽象语法树转换成 js (render函数)。
第一步
使用 htmlparser2 对其进行解析 htmlparser2
主要拿到三部分内容
const { Parser } = require('htmlparser2');
const parser = new Parser({
onopentag(name, attributes) {
console.log('open tag name ->', name)
console.log('open tag attributes ->', attributes)
},
ontext(text) {
console.log("text -->", text);
},
onclosetag(tagname) {
console.log('close tag name ->', tagname)
},
});
parser.write(
"123 "
);
parser.end();
第二步 & 第三步
主要使用babel相关能力,去生成对应的语法树
参考: AST Explorer @babel/types 指南
这块主要用到了 @babel/types 、 @babel/generator、@babel/template
const template = require('@babel/template').default;
const types = require('@babel/types');
const generate = require("@babel/generator").default;
const ast = types.expressionStatement(
types.assignmentExpression(
"=",
types.memberExpression(
types.identifier('exprots'),
types.identifier('render'),
false
),
types.functionExpression(
null,
[types.identifier('data')],
types.blockStatement([
types.returnStatement(
types.arrayExpression([
types.jsxElement(
types.jsxOpeningElement(types.jsxIdentifier('div'),[
types.jsxAttribute(
types.JSXIdentifier('class'),
types.StringLiteral('red'),
)
]),
types.jsxClosingElement(types.jsxIdentifier('div')),
[types.JSXText('123')]
)
])
)
])
),
)
)
console.log('-------AST树-------')
console.log(ast)
console.log('-------代码-------')
console.log(generate(ast).code)
const codeTemp = template(`exprots.render = function (data) {
return [JSX];
};`);
let templateAst = codeTemp({
JSX: types.jsxElement(
types.jsxOpeningElement(types.jsxIdentifier('div'),[]),
types.jsxClosingElement(types.jsxIdentifier('div')),
[types.JSXText('123')]
),
})
console.log('-------AST templateAst-------')
console.log(templateAst)
console.log('-------代码 templateAst-------')
console.log(generate(templateAst).code)
字节小程序通过schema打开小程序,解析schema的参数拿到对应小程序的id等信息去请求对应的小程序资源包包。小程序在下载之前会查看代码包是否已经被缓存,如果有缓存则会跳过下载。
当两侧加载完毕后,向端发送onDocumentReady事件,端记录双方状态。
常见场景说明
流程
建议
data 应只包括渲染相关的数据
控制 setData 的频率
选择合适的 setData 范围
setData 应只传发生变化的数据
this.setData({ 'obj.a.b.c': 'change' });
详细请参考 微信:合理使用 setData
小程序做的工作