作者:赵启明
链接: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>

总算是把这个坑填上了。希望通过这一系列的分析带给前端开发者更多思路。

微信小程序架构分析 (下)相关推荐

  1. 微信小程序架构分析 (上)

    作者:赵启明 链接:https://zhuanlan.zhihu.com/p/22754296 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 相信不少上手试用了微信 ...

  2. 微信小程序架构分析 (中)

    2019独角兽企业重金招聘Python工程师标准>>> 本文探讨一下小程序的 view 模块和 service 模块是如何构成的. 你可以在app.nw/app/dist/weapp ...

  3. android 微信小程序原理,微信小程序架构原理

    前言 昨日看朋友圈,据说有人花钱买star,不知道真的假的?以前以为只是大家开玩笑.今日早读文章由百度EUX@田光宇投稿分享. 正文从这开始~~ 微信小程序 微信小程序包含下面四种文件: js jso ...

  4. 微信小程序下拉框插件_微信小程序自定义select下拉选项框组件的实现代码_清玖_前端开发者...

    知识点:组件,animation,获取当前点击元素的索引与内容 微信小程序中没有select下拉选项框,所以只有自定义.自定义的话,可以选择模板的方式,也可以选择组件的方式来创建. 这次我选择了组件, ...

  5. 微信小程序自定义select下拉选择组件

    微信小程序自定义select下拉选择组件 微信小程序原生开发中,常用到的是从底部弹起的滚动选择器(picker),而有些项目需要用到下拉选择,话不多说,下面就可以把下拉选择封装成一个自定义组件: 1. ...

  6. 微信小程序框架分析思维导图

    微信小程序框架分析思维导图

  7. 关于微信小程序wepy框架下wx-charts的使用

    wx-charts在微信小程序wepy框架下的使用 wx-charts 微信小程序主流的图表工具 基于 Canvas,体积小 支持图表类型 饼图 pie 圆环图 ring 线图 line,scroll ...

  8. 微信小程序之select下拉框

    wxml: <view class="classify-kuangjia"><view class="classify-kuangjia2"& ...

  9. 微信小程序开发分析总结

    本文主要记录一下初次开发小程序一些注意事项,小程序开发语言,对小程序的审核要求,小程序的特点,开发特点,后面会再写下是否有三方好用的工具,及开发中遇到的js,html,css 相关的问题及解决方法.毕 ...

最新文章

  1. C++ 中的左值(Lvalues)和右值(Rvalues)
  2. vim折叠的使用方法
  3. 使用ajax将数据显示在指定位置_AJAX学习主题之一
  4. JavaScript实现数据分页
  5. 数据结构上机实践第三周项目4(2)—顺序表应用
  6. linux ubuntu/centos git 客户端编译安装升级
  7. matlab 使用uci数据集,如何使用UCI数据集
  8. 天思软件常见问题汇总
  9. 安装Eclipse完整版 配上百度云链接
  10. Windows Server 2016 AD父子域环境搭建手册
  11. Instructions on EINT configuration for SIM hot swap for MT6575/MT6577 platforms
  12. 送书【新书】 |《python数据科学实战》
  13. Itext5生成Pdf报表
  14. 来篇鸡汤文吧,教你如何七周内从小菜鸟成长为一名合格的数据分析师
  15. HDMI 收发器简化家庭影院系统设计
  16. 强化学习适合解决什么样的问题
  17. 随手记_英语_学术写作_英文科技论文
  18. 【2022最新版】ENSP安装教程
  19. OpenAL编程手册 - (1)
  20. 数据防泄漏(DLP)

热门文章

  1. Rootkit原理——ROOTKIT ON LINUX X86 V2.6
  2. HAP框架学习之——LOV使用
  3. [递推] 洛谷P1010
  4. win7 计算机管理的命令,win7 cmd命令大全_windows7的cmd命令有哪些
  5. 霍邱一中2021高考成绩查询入口,六安2021高考成绩查询入口
  6. Element UI for Vue 3.0 来了!
  7. JS笔记 (四)数组,json对象,数据类型 ,if 判断,swich 判断,三目运算 判断
  8. Android source code gerrit
  9. VS2013+OBS开发
  10. Vue 实现世界地图展示,根据国家中数量的多少区分(类似世界疫情地图)