小程序基础及原理
第一部分
小程序是个闭源的框架。
现在的前端开发,都不是纯H5开发了,一个网页一个网页的那种形式,而是嵌入到一个APP做 Hybrid(混合)开发。
小程序的本质还是一个Hybrid开发。
面临的一个问题
和客户端交流的过程中,比如微信,很多应用是直接嵌入到微信里面的,但是H5的能力比较弱(比如使用播放器),H5是很弱的,video标签在各个浏览器样式都不统一,缺少很多能力,像NFC 蓝牙 扫二维码的都没法调用(不过应该也有解决方案)
小程序的优点
丰富的客户端能力
微信的文档里,提供了很多API,都是在客户端直接调用的,前端用js,sdk的形式各种去调用。H5上有的也有,没有的小程序还有。
优良的离线性
小程序下载到本地手机上之后,再次打开的时候,不一定走网络
综述一下
小程序与普通网页开发的区别
在原始H5开发页面时,每次打开浏览器都会向服务器发请求,新打开一个tap页,就又是一个新的请求。
而使用小程序就不同了,
腾讯有个大的CDN服务器,上面存着数以万计的小程序,都是一些zip包,当一个用户,通过微信扫码打开一个小程序后(首次进入),微信会从CDN服务器中(通过每个用户的APP id 来区分CDN地址),将对应小程序的包下载到本地手机里,下载完之后会解压,然后就打开小程序。
下一次再运行小程序时,微信会check,先检查本地有没有,如果没有,会从CDN下载(根刚才的步骤一样),如果有的话,会优先打开本地的zip,(平时我们的二维码,其实就是记录一串我们appid的字符串,微信的一个协议,看到这个协议会解析出来微信的appid)。打开的旧的小程序,在后台会悄咪咪的跟服务端进行一次校验,看小程序是不是最新的(开发者有可能在不停的迭代),假如发现有更新的版本,会悄悄的把最新版下载到本地,然后等你下一次再点开的时候,就变成新版本的小程序了。
这就是为什么小程序具有优良的离线性的原因,它直接存在手机里了。(如果直接把小程序删掉的话,zip包也会从手机删除掉),每个小程序都是如此,所以在手机的内存卡里,微信会下很多小程序,为了离线性。
微信有个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都是这么做的)
总结
上述的这些种种,让微信里装载的这些网页,拥有了无限的能力。H5太弱了,要啥能力啥能力没有,比如文件操作等等。
第二部分
搞个例子看一下。
一个例子
新建一个项目 ,下面是目录结构
把该删的代码删一下,留个空壳
app.json
每一项的全局配置
app.js
全局的js文件,无论是切换多少个页面,这个js文件都是常驻后台的。永远不会被销毁,全局只有一个,就可以在上面挂载全局变量。
每个子页面都能引用到这个app.js
sitemap.json
微信小程序的爬取策略。配置允许微信爬取你的哪个页面。默认是都可以爬取。在微信的搜一搜进行相关的结果展示。
假如有一些身不由己的界面,可以不让微信抓。
app.wxss
全局的css,可以作用到每一个页面。
project.config.json
开发者工具的配置文件,配置开发工具需要的一些字段,开发时候用。
然后就照着文档规范写,view、text那些小组件。不要再写div那些了。
navigator
修改index.wxml
<!--index.wxml-->
<view class="container">
<view>
一条新闻
</view>
<navigator url="/pages/index/index">
跳转!
</navigator>
</view>
先来看一个小知识点,跳转,navigator,
每次跳转都是同一个页面,那data里的数据在下一个页面变更,不会影响到别的吗?
是不会的,微信再开发的时候,每次跳转到下一个页面其实都是用的一个data的克隆体。
每次都深拷贝一份。
循环一个列表
js文件
// index.js
// 获取应用实例
const app = getApp()
Page({
data: {
list: [
{
type: 'aa',
},
{
type: 'bb',
},
{
type: 'aa',
},
{
type: 'bb',
},
]
}
})
wxml文件
<!--index.wxml-->
<view class="container">
<view
wx:for="{{list}}"
wx:fo-itemr="item"
wx:key="index"
>
当前项目的类型 {{item.type}}
</view>
</view>
template
可以复用,template定义一个模板
然后使用template的is属性调用,动态的反射出来
<!--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定义就好了。
{
"usingComponents": {
"aa" : "/components/items/aa/aa",
"bb" : "/components/items/bb/bb"
}
}
如果像vue写在js中的components里,在编译的时候,如果想知道引用了哪些组件,就得提前解析js文件,但是有时候引用组件的路径很可能有运行时的变量,这就很难解析了,所以直接抽离出来,就很方便了。
其实默认是这样的"aa" : "/components/items/aa/aa.json"
,不用写json后缀了。
看一下声明的组件
// components/items/aa/aa.js
Component({
/**
* 组件的属性列表
*/
properties: {
},
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
}
})
用一下这个组件试试
<!--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
<!--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
- onLoad:生命周期回调—监听页面加载
- onShow:生命周期回调—监听页面显示
- onReady:生命周期回调—监听页面初次渲染完成
- onHide: 生命周期回调—监听页面隐藏
- onUnload: 生命周期回调—监听页面卸载
一些动作的回调
- onPullDownRefresh: 监听用户下拉动作
- onReachBottom:页面上拉触底事件的处理函数
- onShareAPPMessage:用户点击右上角转发
- onShareTimeLine:用户点击右上角转发到朋友圈
- onAddToFavorites: 用户点击右上角收藏
- onPageScroll:页面滚动触发事件的处理函数
- onResize:页面尺寸改变时触发,详见 响应显示区域变化
- onTabItemTap:当前是 tab 页时,点击 tab 时触发
- onSaveExitState:页面销毁前保留状态回调
发请求 wx.request
新建一个接口
// 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)
起开它,然后我们发请求
// 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)
}
})
},
})
记得开发的时候,关一下这个域名校验
就看到控制台打印出了结果
this.setData
直接显示的更新数据,vue是劫持了,这种调用方式更偏向于react风格。
修改wxml
<!--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
// 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
<!--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>
第三部分
小程序的基础架构
整个小程序分为多个渲染层和一个逻辑层,我们新建一个详情页,点击跳转进去。
修改wxml
<!--index.wxml-->
<view
bind:tap="skip"
>
跳转到详情页
</view>
修改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的原因了。他们都在一个线程里,只是写代码的时候不放在一起而已。
他们是怎么通信的
postmessage,他在后台创建的这个隐藏的service进程,填充了所有js,这也是为什么你在微信里,找不到碰dom接口的原因,因为你的js根本没和你的html运行在一个webview里,不在一个线程里,上哪碰dom呢。
唯一能拿dom的api是假的,wx.createSelectorQuery()
这个是假api,本身也是异步的,返回一些信息,通过通讯的方式,告诉他,
jsconst 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的时候,都要往返于两个进程来回通讯。
小程序分包
大招 查看微信小程序源代码
微信的开发者工具时用electro做的,打开调试模式,审查元素,
审查到webview标签,
我们可以调试他,$0
就是这个webview
$0.showDevTools(true)
,所有代码直接就裸奔了。
这时候又弹出了一个调试工具,本质还是一个html
rpx是怎么实现的
是微信再插入css的时候,会先处理一遍,使用js插入进来的,并且给所有的类都编译了一下,会把rpx转换为真正的px,这样浏览器就认识了。
view、text组件是怎么实现的
其实就是一个自定义组件,真正渲染到页面的就是一个span标签。
new Vue({
components: {
text: {
template: '<span> <slot></slot> </span>'
}
}
})
自定义组件创建了一些高阶的属性,所以重建了一套生态系统。其实就是一直在使用一套组件库。且所有的js和html、css是分开的。
第四部分
实现
service.html,覆盖webview,所有的js都在这个webview
view.js,主要负责渲染部分,也就是用的wxml
怎么传数据呢,会向对应的页面PostMessage
html页面接收通讯,当每次收到一次消息后,就更新一下试图,触使页面完成一次刷新。
所以一直生成新的webview吃内存,页面栈以前最多是5,现在是10了,所以小程序适合大场景刷新,小场景刷新还得通讯一次,所以感觉有点卡,但是整个页面刷新就比H5做的好,这样一层一层的网上盖webview的形式就显得很舒适。