Skip to content

小程序基础及原理

第一部分

小程序是个闭源的框架。

现在的前端开发,都不是纯H5开发了,一个网页一个网页的那种形式,而是嵌入到一个APP做 Hybrid(混合)开发。

小程序的本质还是一个Hybrid开发。

面临的一个问题

和客户端交流的过程中,比如微信,很多应用是直接嵌入到微信里面的,但是H5的能力比较弱(比如使用播放器),H5是很弱的,video标签在各个浏览器样式都不统一,缺少很多能力,像NFC 蓝牙 扫二维码的都没法调用(不过应该也有解决方案)

小程序的优点

  1. 丰富的客户端能力

    微信的文档里,提供了很多API,都是在客户端直接调用的,前端用js,sdk的形式各种去调用。H5上有的也有,没有的小程序还有。

  2. 优良的离线性

    小程序下载到本地手机上之后,再次打开的时候,不一定走网络

综述一下

image-20220220192356770

小程序与普通网页开发的区别

image-20220220192440681

在原始H5开发页面时,每次打开浏览器都会向服务器发请求,新打开一个tap页,就又是一个新的请求。

image-20220220192755236

而使用小程序就不同了,

腾讯有个大的CDN服务器,上面存着数以万计的小程序,都是一些zip包,当一个用户,通过微信扫码打开一个小程序后(首次进入),微信会从CDN服务器中(通过每个用户的APP id 来区分CDN地址),将对应小程序的包下载到本地手机里,下载完之后会解压,然后就打开小程序。

下一次再运行小程序时,微信会check,先检查本地有没有,如果没有,会从CDN下载(根刚才的步骤一样),如果有的话,会优先打开本地的zip,(平时我们的二维码,其实就是记录一串我们appid的字符串,微信的一个协议,看到这个协议会解析出来微信的appid)。打开的旧的小程序,在后台会悄咪咪的跟服务端进行一次校验,看小程序是不是最新的(开发者有可能在不停的迭代),假如发现有更新的版本,会悄悄的把最新版下载到本地,然后等你下一次再点开的时候,就变成新版本的小程序了。

这就是为什么小程序具有优良的离线性的原因,它直接存在手机里了。(如果直接把小程序删掉的话,zip包也会从手机删除掉),每个小程序都是如此,所以在手机的内存卡里,微信会下很多小程序,为了离线性。

image-20220220193518162

微信有个api,也是可以感知到更新的,悄悄下载完会给我们一个回调,UpdateManager.onUpdateReady(function callback)

这时候可以给用户弹出一个dialog,说版本更新,是否重启。

其实这就是一套典型的Hybrid。

客户端是怎么和前端通信的

js-bridge 桥。

在微信装在一个网页的时候,其实在H5发出的所有请求,客户端都是可以拦截的。

然后一些牛人,就发明了一些非http的请求,比如以wx://xxxx/xxxx形式的请求,当微信客户端拦截到这样的请求时,微信协议的开头,微信就知道你要调用一些客户端能力了。

比如:想调用相机能力时,就可以wx://xxxx/xxxx/camera

比如:想获取地理位置参数的能力,就可以wx://xxxx/xxxx/location

这样客户端在拦截的时候,就可以调起本地摄像头或者定位。

客户端调用之后,就告诉网页调用的结果(客户端有在网页执行函数的能力,可以直接在前端网页执行一段函数,超强能力),以此往复循环,就形成了微信小程序里的api。

当你调用微信的这些api的时候,微信的底层的js-sdk就会发送这种带微信协议的请求,然后被客户端拦到了,比如getSystemInfo,客户端就会拼一些系统信息,然后再返还给前端网页,返回之后,微信再调用一次你的回调函数,success、fail、complete,然后你就获取到信息了。(业界的Hybrid都是这么做的)

image-20220220195435650

总结

上述的这些种种,让微信里装载的这些网页,拥有了无限的能力。H5太弱了,要啥能力啥能力没有,比如文件操作等等。

第二部分

搞个例子看一下。

一个例子

新建一个项目 ,下面是目录结构

image-20220220202543487

把该删的代码删一下,留个空壳

  1. app.json

    每一项的全局配置

    全局配置

  2. app.js

    全局的js文件,无论是切换多少个页面,这个js文件都是常驻后台的。永远不会被销毁,全局只有一个,就可以在上面挂载全局变量。

    每个子页面都能引用到这个app.js

  3. sitemap.json

    微信小程序的爬取策略。配置允许微信爬取你的哪个页面。默认是都可以爬取。在微信的搜一搜进行相关的结果展示。

    假如有一些身不由己的界面,可以不让微信抓。

  4. app.wxss

    全局的css,可以作用到每一个页面。

  5. project.config.json

    开发者工具的配置文件,配置开发工具需要的一些字段,开发时候用。

然后就照着文档规范写,view、text那些小组件。不要再写div那些了。

修改index.wxml

html
<!--index.wxml-->
<view class="container">
  <view>
    一条新闻
  </view>
  <navigator url="/pages/index/index">
  跳转!
  </navigator>
</view>

先来看一个小知识点,跳转,navigator,

每次跳转都是同一个页面,那data里的数据在下一个页面变更,不会影响到别的吗?

是不会的,微信再开发的时候,每次跳转到下一个页面其实都是用的一个data的克隆体。

每次都深拷贝一份。

循环一个列表

js文件

js
// index.js
// 获取应用实例
const app = getApp()
Page({
  data: {
    list: [
      {
        type: 'aa',
      },
      {
        type: 'bb',
      },
      {
        type: 'aa',
      },
      {
        type: 'bb',
      },
    ]
  }
})

wxml文件

html
<!--index.wxml-->
<view class="container">
    <view 
        wx:for="{{list}}"
        wx:fo-itemr="item"
        wx:key="index"
    >
    当前项目的类型 {{item.type}}
    </view>
</view>

template

可以复用,template定义一个模板

然后使用template的is属性调用,动态的反射出来

html
<!--index.wxml-->
<template name="aa">AAAA</template>
<template name="bb">BBBBB</template>


<view class="container">
    <view 
        wx:for="{{list}}"
        wx:fo-itemr="item"
        wx:key="index"
    >
    <template is="{{item.type}}"></template>
    </view>
</view>

component

在微信小程序引用组件直接在json文件里的usingComponents定义就好了。

json
{
  "usingComponents": {
    "aa" : "/components/items/aa/aa",
    "bb" : "/components/items/bb/bb"
  }
}

如果像vue写在js中的components里,在编译的时候,如果想知道引用了哪些组件,就得提前解析js文件,但是有时候引用组件的路径很可能有运行时的变量,这就很难解析了,所以直接抽离出来,就很方便了。

其实默认是这样的"aa" : "/components/items/aa/aa.json",不用写json后缀了。

看一下声明的组件

js
// components/items/aa/aa.js
Component({
    /**
     * 组件的属性列表
     */
    properties: {

    },

    /**
     * 组件的初始数据
     */
    data: {

    },

    /**
     * 组件的方法列表
     */
    methods: {

    }
})

用一下这个组件试试

html
<!--index.wxml-->
<template name="aa">AAAA</template>
<template name="bb">BBBBB</template>


<view class="container">
    <view 
        wx:for="{{list}}"
        wx:fo-itemr="item"
        wx:key="index"
    >
    <!-- <template is="{{item.type}}"></template> -->
    <aa></aa> ---  <bb></bb> 
    </view>
</view>

template + component 完成反射

修改wxml

html
<!--index.wxml-->
<template name="aa">
    <aa></aa>
</template>
<template name="bb">
    <bb></bb> 
</template>


<view class="container">
    <view 
        wx:for="{{list}}"
        wx:fo-itemr="item"
        wx:key="index"
    >
    <template is="{{item.type}}"></template>
    </view>
</view>

小程序的声明周期

参考文档:https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html

  1. onLoad:生命周期回调—监听页面加载
  2. onShow:生命周期回调—监听页面显示
  3. onReady:生命周期回调—监听页面初次渲染完成
  4. onHide: 生命周期回调—监听页面隐藏
  5. onUnload: 生命周期回调—监听页面卸载

一些动作的回调

  1. onPullDownRefresh: 监听用户下拉动作
  2. onReachBottom:页面上拉触底事件的处理函数
  3. onShareAPPMessage:用户点击右上角转发
  4. onShareTimeLine:用户点击右上角转发到朋友圈
  5. onAddToFavorites: 用户点击右上角收藏
  6. onPageScroll:页面滚动触发事件的处理函数
  7. onResize:页面尺寸改变时触发,详见 响应显示区域变化
  8. onTabItemTap:当前是 tab 页时,点击 tab 时触发
  9. onSaveExitState:页面销毁前保留状态回调

发请求 wx.request

新建一个接口

js
// WeChat_App\server.js

let http = require('http')

let app = http.createServer((req, res) => {
    let data = [
        {
            name: 'cheny'
        },
        {
            name: 'xzz'
        },

    ]

    res.write(JSON.stringify(data))
    res.end()
})

app.listen(3001)

起开它,然后我们发请求

js
// index.js
// 获取应用实例
const app = getApp()
Page({
  data: {
    list: [
      {
        type: 'aa',
      },
      {
        type: 'bb',
      },
      {
        type: 'aa',
      },
      {
        type: 'bb',
      },
    ]
  },

  onLoad(){
    wx.request({
      url: 'http://localhost:3001',
      success(res){
        console.log(res)
      }
    })
  },
})

记得开发的时候,关一下这个域名校验

image-20220220213443066

就看到控制台打印出了结果

image-20220220213513277

this.setData

直接显示的更新数据,vue是劫持了,这种调用方式更偏向于react风格。

修改wxml

html
<!--index.wxml-->
<template name="cheny">
    <aa></aa>
</template>
<template name="xzz">
    <bb></bb> 
</template>


<view class="container">
    <view 
        wx:for="{{list}}"
        wx:fo-itemr="item"
        wx:key="index"
    >
    <template is="{{item.name}}"></template>
    </view>
</view>

修改js

js
// index.js
// 获取应用实例
const app = getApp()
Page({
  data: {
    list: []
  },

  onLoad(){
    wx.request({
      url: 'http://localhost:3001',
      success: ({data})=>{
        console.log(this.setData)
        this.setData({
          list: data
        })
      }
    })
  },
})

往template上传参

修改wxml

html
<!--index.wxml-->
<template name="cheny">
    <aa></aa>
</template>
<template name="xzz">
我是:::{{name}}
    <bb></bb> 
</template>


<view class="container">
    <view 
        wx:for="{{list}}"
        wx:fo-itemr="item"
        wx:key="index"
    >
    <template is="{{item.name}}" data="{{...item}}"></template>
    </view>
</view>

第三部分

小程序的基础架构

整个小程序分为多个渲染层和一个逻辑层,我们新建一个详情页,点击跳转进去。

image-20220220214923592

修改wxml

html
<!--index.wxml-->
<view 
bind:tap="skip"
>
跳转到详情页
</view>

修改js

js
// index.js
// 获取应用实例
const app = getApp()
Page({
  data: {
    list: []
  },

  skip(){
    wx.navigateTo({
      url: '/pages/detail/detail',
    })
  },
})

存在的问题

每次我们在点击浏览器的前进和后退按钮的时候,浏览器都会刷新当前页面,全部页面全部重新加载,这个体验其实是很不好的。

但是微信小程序在这方面就做的很好,每次切换回退,整个页面也不会卡顿,这是怎么做到的呢?

微信的解决办法

当每次微信初始化一个页面的时候,会创建一个webview在上面(就相当于chrom浏览器的一个tap),微信就是当要加载新页面的时候,就新建一个webview,然后滑进来,盖在之前的webview的上面,给人的感觉就很好,当退出当前页面的时候,这个栈顶的webview就销毁掉,体验非常好。

但是面临一个问题,整个小程序是使用一个app.js的,小程序是如何做到每次加载新的webview的时候,所有页面都共享一个app.js的呢?

微信小程序的开发人员做了一个很疯狂的事。

如果想两个js都共享一个变量的话,那么就让这两个js放在一个进程里,js不要和html放在一个进程里,

我们传统的网页是js、css放在一个webview里的,但是小程序就剑走偏锋,比较诡异,他把所有页面的js文件放在了同一个webview里,这个webview我们看不到,不负责页面展示,俗称jscall,就是一个线程,类似于一个worker里,事业群的人把他称之为一个service。

相当于他做了什么事呢,他创建了一个空的webview,在里面跑js,然后把他最小化了,微信做的事,把他隐藏到后台了,其实后台还能跑jscall,前台不停的创建试图,一个webview、两个webview、等等等,他们都听后台这个js的操作和指挥,相当于一个傀儡,就像火影忍者里的那样,前面的所有页面都是后台app.js,别的js的傀儡,你在js里写的setData,其实是对前台傀儡做的一次刷新操作,所以说,这就是为什么他们所有的js都可以访问到同一个app.js的原因了。他们都在一个线程里,只是写代码的时候不放在一起而已。

image-20220220225109791

他们是怎么通信的

postmessage,他在后台创建的这个隐藏的service进程,填充了所有js,这也是为什么你在微信里,找不到碰dom接口的原因,因为你的js根本没和你的html运行在一个webview里,不在一个线程里,上哪碰dom呢。

唯一能拿dom的api是假的,wx.createSelectorQuery()

这个是假api,本身也是异步的,返回一些信息,通过通讯的方式,告诉他,

js
const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect()
query.selectViewport().scrollOffset()
query.exec(function(res){
  res[0].top       // #the-id节点的上边界坐标
  res[1].scrollTop // 显示区域的竖直滚动位置
})

你告诉他,说要查#the-id,然后service发个请求,传到视图层,视图层查完之后,再把结果返回回来,所以根本就没有碰到dom,这就是你没发碰dom的原因,且唯一能碰dom的属性的api也只能是个假的,在有限的范围给你返回top、scrollTop的属性。

底层架构是这样的,所以碰不了dom。

写代码的时候虽然是写在一起,但是编译的时候,会把所有的js采集起来,放到service里,html、css采集起来放到前台,你的每次setData或者点击操作,都是一次通讯。

为什么setData不能频繁掉

为什么不能传大量的数据

就是因为在每次setData的时候,都要往返于两个进程来回通讯。

image-20220220222615090

小程序分包

image-20220220222914688

大招 查看微信小程序源代码

微信的开发者工具时用electro做的,打开调试模式,审查元素,

审查到webview标签,

image-20220220223246555

我们可以调试他,$0就是这个webview

image-20220220223325593

$0.showDevTools(true),所有代码直接就裸奔了。

这时候又弹出了一个调试工具,本质还是一个html

image-20220220223454325

rpx是怎么实现的

是微信再插入css的时候,会先处理一遍,使用js插入进来的,并且给所有的类都编译了一下,会把rpx转换为真正的px,这样浏览器就认识了。

image-20220220223734532

view、text组件是怎么实现的

其实就是一个自定义组件,真正渲染到页面的就是一个span标签。

js
new Vue({
    components: {
        text: {
            template: '<span> <slot></slot> </span>'
        }
    }
})

image-20220220224002675

自定义组件创建了一些高阶的属性,所以重建了一套生态系统。其实就是一直在使用一套组件库。且所有的js和html、css是分开的。

第四部分

实现

  1. service.html,覆盖webview,所有的js都在这个webview

    image-20220220224404505

  2. view.js,主要负责渲染部分,也就是用的wxml

    image-20220220224559082

  3. 怎么传数据呢,会向对应的页面PostMessage

    image-20220220224737847

  4. html页面接收通讯,当每次收到一次消息后,就更新一下试图,触使页面完成一次刷新。

    image-20220220224919037

所以一直生成新的webview吃内存,页面栈以前最多是5,现在是10了,所以小程序适合大场景刷新,小场景刷新还得通讯一次,所以感觉有点卡,但是整个页面刷新就比H5做的好,这样一层一层的网上盖webview的形式就显得很舒适。