• 小程序基础原理


    前言

    本文会围绕小程序的基础原理进行介绍。主要包括小程序的基础结构、编译、加载、通讯等几个方面。旨在阅读完毕后可以对小程序有一个基本的印象。

    一、基础

    对于用户来讲,小程序无需下载、用完即走、体验良好。

    对于开发者来讲,小程序主要是区别于端内的内嵌纯Web页面

    • 第一点是开发体验上的区别,首先需要注册账号,下载开发者工具,然后使用小程序定制的DSL和JavaScript来进行开发,在小程序开发者工具上运行小程序来查看小程序的效果,最后提交代码审核通过后进行发布。

    👉 微信小程序开发指南

    DSL:Domain Specific Language(领域特定语言)

    所谓DSL,是指利用为特定领域(Domain)所专门设计的词汇和语法,简化程序设计过程,提高生产效率的技术,同时也让非编程领域专家的人直接描述逻辑成为可能。DSL
    的优点是,可以直接使用其对象领域中的概念,集中描述“想要做到什么”(What)的部分,而不必对“如何做到”(How)进行描述。

    摘自:《代码的未来》 — [日]
    松本行弘

    目的:限定问题边界和控制复杂度,提高编程效率,更加的抽象化。

    举例:1. 内部:jQuery。

    1. 外部:JSX、Sass、SQL、WXML、WXSS、WXS。

    我们需要使用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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    WXSS

    .padding {
        padding: 20rpx;
    }
    
    • 1
    • 2
    • 3

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 一致。

    特点:

    1. 运行在视图层,不依赖基础库。
    1. 不能直接setData,只能修改组件的class和style,以及一些格式化处理。(可以间接通过callMethod通知逻辑层设置数据)
    1. 可以进行简单的逻辑运算,能力有限。
    1. 支持绑定事件,可以处理一些用户交互事件。

    常用场景:

    1. 数据格式处理,如日期、文本等,可以实现过滤器的功能。
    1. 用户频繁交互且需要改变样式的场景下:页面滚动菜单吸顶、拖拽跟随、侧边栏抽屉等。

    过滤器

    // /pages/tools.wxs
    
    var foo = "five";
    var bar = function (d) {
      return `${d} apples`;
    }
    module.exports = {
      FOO: foo,
      bar: bar,
    };
    module.exports.msg = "some msg";
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    
    <wxs src="./../tools.wxs" module="tools" />
    <view> {{tools.msg}} view>
    <view> {{tools.bar(tools.FOO)}} view>
    
    
    some msg
    five apples
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    拖拽效果

    
    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    // 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,
        })
      }
    })
    
    • 1
    • 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
    // 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,
    }
    
    • 1
    • 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

    使用 DSL 的原因:HTML的自由度过高,通过DSL来约束管控。也增加了一定的扩展性,小程序可以在DSL转换的过程中去添加一些自己的兼容。例如:input输入框focus时被键盘遮挡的问题,小程序可以自己通过覆盖原生组件的方式来解决。

    • 第二点基础架构上的区别,小程序将逻辑层和渲染层分离,逻辑层不再置于浏览器环境中运行,逻辑侧和视图侧的通讯会经过端的转发。
    • 第三点是渲染侧多页面,逻辑侧只有一个逻辑实例

    二、架构

    整个小程序框架系统分为两部分:逻辑层(App Service)和 视图层(View)。小程序提供了自己的视图层描述语言 WXMLWXSS,以及基于 JavaScript 的逻辑层框架。
    在这里插入图片描述

    • 业务逻辑侧用来执行开发者的业务逻辑代码,运行在JsCore中。
    • 渲染侧有多个Webview。
    • 渲染侧和逻辑侧不能直接通讯,而是通过的jsBridge来中转。

    「 为什么选择这样的架构 」

    技术选型

    通常我们常见的渲染界面的方式有以下三种

    原生渲染

    小程序是运行在端内的,比如微信小程序的宿主是微信,字节小程序的宿主是头条APP、抖音等。使用原生技术来渲染小程序,很明显的优势是可以获得比较好的用户体验丰富的原生能力

    但同时也会存在一个问题,完全使用原生来渲染小程序,那么小程序就和宿主绑定在了一起了,完全属于宿主的一部分,小程序的发布也需要依赖端的发版,这种迭代发版的节奏是肯定是不对的。

    纯Web渲染

    小程序需要像Web应用一样,可以随时的,直接拉取下载最新的资源包在本地运行。但纯粹的web技术无法直接调用原生能力并且也无法保证良好的用户体验。

    因为在一些复杂的交互场景下,逻辑执行会阻塞UI的渲染(单线程),页面也可能会出现一些性能问题。

    Hybrid 渲染

    最终,小程序选择了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 ]。

    当我们调用switchTab 时,看下图。
    在这里插入图片描述

    当然这个页面栈的长度不会无限增加,目前已知的是最多限制在10条。当页面栈达到10条之后,继续执行navigateTo的话,小程序会自动将其转换为redirectTo执行。
    在这里插入图片描述

    小程序开发者工具测试结果 10条

    小程序开发者工具调试Element中也可以看到多个Webview

    实际在小程序开发者工具中获取Webview节点,会发现超过了10个。

    三、编译

    3.1 文件编译

    1. JS编译:Babel 转换后 模块化处理注入一些不希望开发者使用的变量(window、document、alert等)。
    1. XXSS编译:通过postCSS转换为CSS,rpx在运行时转换所以,最后CSS会转换成JS。
    1. XXML编译:XXML解析生成DOM树,再通过Babel转换成JS。
    1. JSON编译:是一个将多个JSON合并的操作。

    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节点。

    3.2 流程

    在这里插入图片描述

    以 123 为例

    第一步:使用 HTML Parser 2 对 XXML 进行解析生成DOM树。

    第二步:使用 Babel的能力去处理DOM节点,将其转换成抽象语法树。

    第三步:使用 babel-generator 将抽象语法树转换成 js (render函数)。

    第一步

    使用 htmlparser2 对其进行解析 👉 htmlparser2

    主要拿到三部分内容

    • Opentag 开标签 包括开标签名 属性
    • Text 文本内容
    • Closetag 闭合标签 包括闭合标签名
    
    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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    运行结果
    在这里插入图片描述

    第二步 & 第三步

    主要使用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)
    
    • 1
    • 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
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    运行结果
    在这里插入图片描述

    3.3 产物

    在这里插入图片描述

    • 各页面逻辑和路由打包。
    • 将所有页面的模版打包成渲染函数并放在一个大的渲染函数里。
    • 将所有的配置放在同一个配置文件里。
    • 最终生成一个应用入口。

    四、加载过程

    4.1 加载过程

    • 字节小程序通过schema打开小程序,解析schema的参数拿到对应小程序的id等信息去请求对应的小程序资源包包。小程序在下载之前会查看代码包是否已经被缓存,如果有缓存则会跳过下载。
      在这里插入图片描述

    • 端下载并接收到小程序的内容后,分别由渲染侧去加载渲染函数、逻辑侧去加载应用入口。
      在这里插入图片描述

    • 当两侧加载完毕后,向端发送onDocumentReady事件,端记录双方状态。

    • 然后端告知逻辑侧执行页面的生命周期事件,随后逻辑侧执行后会携带一些初始化后的数据,主动去告知渲染侧可以进行页面渲染
    • 等页面渲染完毕后主动向逻辑侧发送onDomReady事件,逻辑侧执行onReady
      在这里插入图片描述

    4.2 小程序运行时

    在这里插入图片描述

    • 当小程序启动后,通过锁屏右上角关闭安卓/IOS 返回操作home键切换等操作后,小程序会进入后台状态。
    • 5秒后如果小程序未返回前台,将会被挂起(后台音乐、后台地理除外),JS停止执行,宿主内小程序内存状态保留,事件和接口回调会在再次切回前台后触发。
    • 如果30分钟后(字节小程序为5分钟),未切回前台,小程序被销毁。或当小程序占用内存过高时,也会被宿主主动回收。

    五、通讯方式

    5.1 三种通信方式

    • 逻辑侧、渲染侧主动监听端上的事件:app冷热启动,前后台切换
    • 逻辑侧、渲染侧互相通信:Data、事件传输
    • 逻辑侧、渲染侧调用端上能力:存储、文件

    5.2 jsb设计

    常见场景说明

    • 主动通知端:用户调用showToast时,会做一些前置的校验,回调的封装,将回调放入事件map中,然后告知端我们要调用showToast的命令,等端处理完toast的事件之后,会通过 全局暴露的handler事件去找到map去执行里面的事件。
    • 两侧通讯:接到setData命令后,首先会更新内部维护的数据,然后将数据格式化之后传输给端上,端上拿到数据之后再转发给渲染侧。
    • 监听端事件:我们无法直接去监听端的前后台切换事件,主要依赖端在事件发生后,主动通知我们去执行提前注册的相关回调。

    5.3 setData使用建议

    流程

    • 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
    • 将 data 从逻辑层传输到视图层;
    • 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新。

    建议

    1. data 应只包括渲染相关的数据

      1. 与渲染无关的可以挂在data之外的字段上,this.xxx = xxx。
      2. 与渲染间接相关的可以使用纯数据字段,通过observers监听。
    1. 控制 setData 的频率

      1. 合并多次setData且仅在更新视图时使用。
    1. 选择合适的 setData 范围

      1. 可以将需要频繁更新的部分,抽离成单独的组件。
    1. setData 应只传发生变化的数据

      1. 比如使用数据路径的方式 this.setData({ 'obj.a.b.c': 'change' });

    详细请参考 👉 微信:合理使用 setData

    六、基础小结

    小程序做的工作

    在这里插入图片描述

  • 相关阅读:
    JavaScript-ECMAScript编程
    2022年各大企业java面试题解析,堪称全网最详细的java面试指南
    fork仓库的代码如何同步主仓库代码
    通过WiSE-FT 大模型微调 (“灾难性遗忘”catastrophic forgetting)
    关于《web课程设计》网页设计 用html css做一个漂亮的网站 仿新浪微博个人主页
    图片水印怎么加?图片加水印方法分享
    39.B树,B+树(王道第7章查找补充知识)
    Salesforce ServiceCloud考证学习(3)
    MySQL理解-下载-安装
    JVM是什么?Java程序为啥需要运行在JVM中?
  • 原文地址:https://blog.csdn.net/lunhui1994_/article/details/127923278