微信小程序架构分析 (下)
作者:赵启明
链接:https://zhuanlan.zhihu.com/p/22932309
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这一篇拖了一段时间,原因是实现一个可以运行微信小程序的 web 环境比我想象中要困难一些, 这一方面是因为微信对于代码进行了压缩混淆,另一方面主要原因是开发者工具内部逻辑调用比较复杂(难怪 bug 不少),完全无法拿出来重用。
小程序实时运行工具 wept 的开发已经基本完成了, 你可以通过我的代码对小程序的 web 环境实现有更全面的认识。下面我将介绍它的实现过程以及实时更新的原理。
小程序 web 服务实现
我在 wept 的开发中使用 koa 提供 web 服务,以及 et-improve 提供模板渲染。
第一步: 准备页面模板
我们需要三个页面,一个做为控制层 index.html,一个做为 service 层service.html,还有一个做为 view 层的 view.html
index.html:
<div class="head"></div><div class="scrollable"></div><div class="tabbar-root"></div><script>var __wxConfig__ = {{= _.config}}var __root__ = '{{= _.root}}'</script><script src="/script/build.js"></script>
service.html:
<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><link href="https://res.wx.qq.com/mpres/htmledition/images/favicon218877.ico" rel="Shortcut Icon"><script>var __wxAppData = {}var __wxRoutevar __wxRouteBeginglobal = {}var __wxConfig = {{= _.config}}</script><script src="/script/bridge.js" type="text/javascript"></script><script src="/script/service.js" type="text/javascript"></script>{{each _.utils as util}} <script src="/app/{{= util}}" type="text/javascript"></script>{{/}} <script src="/app/app.js" type="text/javascript"></script>{{each _.routes as route}} <script> var __wxRoute = '{{= route | noext}}', __wxRouteBegin = true;</script><script src="/app/{{= route}}" type="text/javascript"></script>{{/}}</head><body><script>window._____sendMsgToNW({sdkName: 'APP_SERVICE_COMPLETE'})</script></body>
view.html:
<head><link href="https://res.wx.qq.com/mpres/htmledition/images/favicon218877.ico" rel="Shortcut Icon"><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" /><link rel="stylesheet" type="text/css" href="/css/default.css"><link rel="stylesheet" type="text/css" href="/app/app.wxss"><link rel="stylesheet" type="text/css" href="/app/{{= _.path}}.wxss"><script> var __path__ = '{{= _.path}}'</script><script src="/script/ViewBridge.js" async type="text/javascript"></script><script src="/script/view.js" type="text/javascript"></script><script>{{= _.inject_js}}</script><script>document.dispatchEvent(new CustomEvent("generateFuncReady", {detail: {generateFunc: $gwx('./{{= _.path}}.wxml')}}))</script></head><body><div></div></body>
第二步: 实现 http 服务
用 koa 实现的代码逻辑非常简单:
server.js
// 日志中间件app.use(logger())// gzipapp.use(compress({threshold: 2048,flush: require('zlib').Z_SYNC_FLUSH}))// 错误提醒中间件app.use(notifyError)// 使用当前目录下文件处理 404 请求app.use(staticFallback)// 各种 route 实现app.use(router.routes())app.use(router.allowedMethods())// 对于 public 目录启用静态文件服务app.use(require('koa-static')(path.resolve(__dirname, '../public')))// 创建启动服务let server = http.createServer(app.callback())server.listen(3000)
router.js
router.get('/', function *() {// 加载 index.html 模板和数据,输出 index 页面})router.get('/appservice', function *() {// 加载 service.html 模板和数据,输出 service 页面})// 让 `/app/**` 加载小程序所在目录文件router.get('/app/(.*)', function* () {if (/\.(wxss|js)$/.test(file)) {// 动态编译为 css 和相应 js} else if (/\.wxml/.test(file)) {// 动态编译为 html} else {// 查找其它类型文件, 存在则返回let exists = util.exists(file)if (exists) {yield send(this, file)} else {this.status = 404throw new Error(`File: ${file} not found`)}}})
第三步:实现控制层功能
实现完上面两步,就可以访问 view 页面了,但是你会发现它只能渲染,并不会有任何功能,因为 view 层功能依赖于控制层进行的通讯, 如果控制层收不到消息,它不会响应任何事件。
控制层是整个实现过程中最复杂的一块,因为官方工具的代码与 nwjs 以及 react 等第三方组件耦合过高,所以无法拿来直接使用。 你可以在 wept 项目的 src 目录下找到控制层逻辑的所有代码,总体上控制层要负责以下几个功能:
实现 service 层,view 层以及控制层之间的通讯逻辑
依据路由指令动态创建 view (wept 使用 iframe 实现)
根据当前页面动态渲染 header 和 tabbar
实现原生 API 调用,返回结果给 service 层
wept 里面 iframe 之间的通讯是通过 message.js 模块实现的,控制页面(index.html)代码如下:
window.addEventListener('message', function (e) {let data = e.datalet cmd = data.commandlet msg = data.msg// 没有跟 contentscript 握手阶段,不需要处理if (data.to == 'contentscript') return// 这是个遗留方法,基本废弃掉了if (data.command == 'EXEC_JSSDK') {sdk(data)// 直接转发 view 层消息到 service,主要是各种事件通知} else if (cmd == 'TO_APP_SERVICE') {toAppService(data)// 除了 publish 发送消息给 view 层以及控制层可以处理的逻辑(例如设置标题),// 其它全部转发 service 处理,所有控制层的处理结果统一先返回 service} else if (cmd == 'COMMAND_FROM_ASJS') {let sdkName = data.sdkNameif (command.hasOwnProperty(sdkName)) {command[sdkName](data)} else {console.warn(`Method ${sdkName} not implemented for command!`)}} else {console.warn(`Command ${cmd} not recognized!`)}})
具体实现逻辑可以查看 src/command.js src/service.j***c/sdk/*.js。对于 view/service 页面只需把原来 bridge.js 的window.postMessage 改为 window.top.postMessage 即可。
view 层的控制逻辑由 src/view.js 以及 src/viewManage.js 实现,viewManage 实现了 navigateTo, redirectTo 以及 navigateBack 来响应 service 层通过名为 publish 的 command 传来的对应页面路由事件。
header.js 和 tabbar.js 包含了基于 react 实现的 header 和 tabbar 模块(原计划是使用 vue,但是没找到与原生 js 模块通讯的 API)
sdk 目录下包含了 storage,录音,罗盘模块,其它比较简单一些的原生底层调用我直接写在 command.js 里面了。
以上就是实现运行小程序所需 webserver 的全部逻辑了,其实现并不复杂,主要困难在与理解微信这一整套通讯方式。
实现小程序实时更新
第一步: 监视文件变化并通知前端
wept 使用了 chokidar 模块监视文件变化,变化后使用 WebSocket 告知所有客户端进行更新操作。 具体实现位于 lib/watcher.js 和 lib/socket.js, 发送内容是 json 格式的字符串。
前端控制层收到 WebSocket 消息后再通过 postMessage 接口转发消息给 view/service 层:
view.postMessage({msg: {data: {data: { path }},eventName: 'reload'},command: 'CUSTOM'})
view/service 层监听 reload 事件:
WeixinJSBridge.subscribe('reload', function(data) {// data 即为上面的 msg.data})
第二步: 前端响应不同文件变化
前端需要对 4 种(wxml wxss json javascript)不同类型文件进行 4 种不同的热更新处理,其中 wxss 和 json 相对简单。
wxss 文件变化后前端控制层通知(postMessage 接口)对应页面(如果是 app.wxss 则是所有 view 页面)进行刷新,view 层收到消息后只需要更改对应 css 文件的时间戳就可以了,代码如下:
o.subscribe('reload', function(data) {if (/\.wxss$/.test(data.path)) {var p = '/app/' + data.pathvar els = document.getElementsByTagName('link');[].slice.call(els).forEach(function(el) {var href = el.getAttribute('href').replace(/\?(.*)$/, '')if (p == href) {console.info('Reload: ' + data.path)el.setAttribute('href', href + '?id=' + Date.now())}})}})
json 文件变化首先需要判断,如果是 app.json 我们无法热更新,所以目前做法是刷新页面,对于页面的 json, 我们只需要在控制层上对 header 设置相应状态就可以了 (渲染工作由 react 帮我们处理):
socket.onmessage = function (e) {let data = JSON.parse(e.data)let p = data.pathif (data.type == 'reload'){if (p == 'app.json') {redirectToHome()} else if (/\.json$/.test(p)) {let win = window.__wxConfig__['window']win.pages[p.replace(/\.json$/, '')] = data.content// header 通过全局 __wxConfig__ 获取 state 进行渲染header.reset()console.info(`Reset header for ${p.replace(/\.json$/, '')}`)}}}
wxml 使用 VirtualDom API 提供的 diff apply 进行处理。首先需要一个接口获取新的 generateFunc 函数(用于生成 VirtualDom), 添加 koa 的 router:
router.get('/generateFunc', function* () {this.body = yield loadFile(this.query.path + '.wxml')this.type = 'text'})function loadFile(p, throwErr = true) {return new Promise((resolve, reject) => {fs.stat(`./${p}`, (err, stats) => {if (err) {if (throwErr) return reject(new Error(`file ${p} not found`))// 文件不存在有可能是文件被删除,所以不能使用 rejectreturn resolve('')}if (stats && stats.isFile()) {// parer 函数调用 exec 命令执行 wcsc 文件生成 wxml 对应的 javascript 代码return parser(`${p}`).then(resolve, reject)} else {return resolve('')}})})}
有了接口就可以请求接口,然后执行返回函数进行 diff apply:
// curr 为当前的 VirtualDom 树if (!curr) returnvar xhr = new XMLHttpRequest()xhr.onreadystatechange = function() {if (xhr.readyState === 4) {if (xhr.status === 200) {var text = xhr.responseTextvar func = new Function(text + '\n return $gwx("./' +__path__+ '.wxml")')window.__generateFunc__ = func()var oldTree = curr// 获取当前 data 生成新的树var o = m(p.default.getData(), false),// 进行 diff applya = oldTree.diff(o);a.apply(x);document.dispatchEvent(new CustomEvent("pageReRender", {}));console.info('Hot apply: ' + __path__ + '.wxml')}}}xhr.open('GET', '/generateFunc?path=' + encodeURIComponent(__path__))xhr.send()
javascript 更新逻辑相对复杂一些, 首先依然是一个接口来获取新的 javascript 代码:
router.get('/generateJavascript', function* () {this.body = yield loadFile(this.query.path)this.type = 'text'})
然后我们在 window 对象上加入 Reload 函数执行具体的更换逻辑:
window.Reload = function (e) {var pages = __wxConfig.pages;if (pages.indexOf(window.__wxRoute) == -1) return// 替换原来的构造函数f[window.__wxRoute] = evar keys = Object.keys(p)// 判定是否当前使用中页面var isCurr = s.route == window.__wxRoutekeys.forEach(function (key) {var o = p[key];key = Number(key)var query = o.__query__var page = o.pagevar route = o.route// 页面已经被创建if (route == window.__wxRoute) {// 执行封装后的 onHide 和 onUnloadisCurr && page.onHide()page.onUnload()// 创建新 page 对象var newPage = new a.default(e, key, route)newPage.__query__ = query// 重新绑定当前页面if (isCurr) s.page = newPageo.page = newPage// 执行 onLoad 和 onShownewPage.onLoad()if (isCurr) newPage.onShow()// 更新 data 数据window.__wxAppData[route] = newPage.datawindow.__wxAppData[route].__webviewId__ = key// 发送更新事件, 通知 view 层u.publish(c.UPDATE_APP_DATA)u.info("Update view with init data")u.info(newPage.data)// 发送 appDataChange 事件u.publish("appDataChange", {data: {data: newPage.data},option: {timestamp: Date.now()}})newPage.__webviewReady__ = true}})u.info("Reload page: " + window.__wxRoute)}
以上代码需要添加到 t.pageHolder 函数后才可运行
最后在 view 层初始化后把 Page 函数切换到 Reload 函数(当然你也可以在请求返回 javascript 前把 Page 重命名为 Reload) 。
<body><script>window._____sendMsgToNW({sdkName: 'APP_SERVICE_COMPLETE'})</script></body>
总算是把这个坑填上了。希望通过这一系列的分析带给前端开发者更多思路。
微信小程序架构分析 (下)相关推荐
- 微信小程序架构分析 (上)
作者:赵启明 链接:https://zhuanlan.zhihu.com/p/22754296 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 相信不少上手试用了微信 ...
- 微信小程序架构分析 (中)
2019独角兽企业重金招聘Python工程师标准>>> 本文探讨一下小程序的 view 模块和 service 模块是如何构成的. 你可以在app.nw/app/dist/weapp ...
- android 微信小程序原理,微信小程序架构原理
前言 昨日看朋友圈,据说有人花钱买star,不知道真的假的?以前以为只是大家开玩笑.今日早读文章由百度EUX@田光宇投稿分享. 正文从这开始~~ 微信小程序 微信小程序包含下面四种文件: js jso ...
- 微信小程序下拉框插件_微信小程序自定义select下拉选项框组件的实现代码_清玖_前端开发者...
知识点:组件,animation,获取当前点击元素的索引与内容 微信小程序中没有select下拉选项框,所以只有自定义.自定义的话,可以选择模板的方式,也可以选择组件的方式来创建. 这次我选择了组件, ...
- 微信小程序自定义select下拉选择组件
微信小程序自定义select下拉选择组件 微信小程序原生开发中,常用到的是从底部弹起的滚动选择器(picker),而有些项目需要用到下拉选择,话不多说,下面就可以把下拉选择封装成一个自定义组件: 1. ...
- 微信小程序框架分析思维导图
微信小程序框架分析思维导图
- 关于微信小程序wepy框架下wx-charts的使用
wx-charts在微信小程序wepy框架下的使用 wx-charts 微信小程序主流的图表工具 基于 Canvas,体积小 支持图表类型 饼图 pie 圆环图 ring 线图 line,scroll ...
- 微信小程序之select下拉框
wxml: <view class="classify-kuangjia"><view class="classify-kuangjia2"& ...
- 微信小程序开发分析总结
本文主要记录一下初次开发小程序一些注意事项,小程序开发语言,对小程序的审核要求,小程序的特点,开发特点,后面会再写下是否有三方好用的工具,及开发中遇到的js,html,css 相关的问题及解决方法.毕 ...
最新文章
- C++ 中的左值(Lvalues)和右值(Rvalues)
- vim折叠的使用方法
- 使用ajax将数据显示在指定位置_AJAX学习主题之一
- JavaScript实现数据分页
- 数据结构上机实践第三周项目4(2)—顺序表应用
- linux ubuntu/centos git 客户端编译安装升级
- matlab 使用uci数据集,如何使用UCI数据集
- 天思软件常见问题汇总
- 安装Eclipse完整版 配上百度云链接
- Windows Server 2016 AD父子域环境搭建手册
- Instructions on EINT configuration for SIM hot swap for MT6575/MT6577 platforms
- 送书【新书】 |《python数据科学实战》
- Itext5生成Pdf报表
- 来篇鸡汤文吧,教你如何七周内从小菜鸟成长为一名合格的数据分析师
- HDMI 收发器简化家庭影院系统设计
- 强化学习适合解决什么样的问题
- 随手记_英语_学术写作_英文科技论文
- 【2022最新版】ENSP安装教程
- OpenAL编程手册 - (1)
- 数据防泄漏(DLP)
热门文章
- Rootkit原理——ROOTKIT ON LINUX X86 V2.6
- HAP框架学习之——LOV使用
- [递推] 洛谷P1010
- win7 计算机管理的命令,win7 cmd命令大全_windows7的cmd命令有哪些
- 霍邱一中2021高考成绩查询入口,六安2021高考成绩查询入口
- Element UI for Vue 3.0 来了!
- JS笔记 (四)数组,json对象,数据类型 ,if 判断,swich 判断,三目运算 判断
- Android source code gerrit
- VS2013+OBS开发
- Vue 实现世界地图展示,根据国家中数量的多少区分(类似世界疫情地图)