微信小程序架构分析 (下)-成都创新互联网站建设

关于创新互联

多方位宣传企业产品与服务 突出企业形象

公司简介 公司的服务 荣誉资质 新闻动态 联系我们

微信小程序架构分析 (下)

【引自第九程序的博客】这一篇拖了一段时间,原因是实现一个可以运行微信小程序的 web 环境比我想象中要困难一些, 这一方面是因为微信对于代码进行了压缩混淆,另一方面主要原因是开发者工具内部逻辑调用比较复杂(难怪 bug 不少),完全无法拿出来重用。

小程序实时运行工具 wept 的开发已经基本完成了, 你可以通过我的代码对小程序的 web 环境实现有更全面的认识。下面我将介绍它的实现过程以及实时更新的原理。

小程序 web 服务实现

我在 wept 的开发中使用 koa 提供 web 服务,以及 et-improve 提供模板渲染。

***步: 准备页面模板

我们需要三个页面,一个做为控制层 index.html,一个做为 service 层service.html,还有一个做为 view 层的 view.html

index.html:

 
 
 
  1.  
 
  •  
  •  
  •  
  •  
  •  
  •   
  • service.html:

     
     
     
    1.  
    2.    
    3.    
    4.    
    5.    
    6.    
    7.   {{each _.utils as util}} 
    8.    
    9.   {{/}} 
    10.    
    11.   {{each _.routes as route}} 
    12.    
    13.    
    14.   {{/}} 
    15.  
    16.  
    17.    
    18.  

    view.html:

     
     
     
    1.  
    2.    
    3.    
    4.    
    5.    
    6.    
    7.    
    8.    
    9.    
    10.    
    11.    
    12.    
    13.  
    14.  
    15.   
       
    16.   

    第二步: 实现 http 服务

    用 koa 实现的代码逻辑非常简单:

    server.js

     
     
     
    1. // 日志中间件 
    2. app.use(logger()) 
    3. // gzip 
    4. app.use(compress({ 
    5.   threshold: 2048, 
    6.   flush: require('zlib').Z_SYNC_FLUSH 
    7. })) 
    8. // 错误提醒中间件 
    9. app.use(notifyError) 
    10. // 使用当前目录下文件处理 404 请求 
    11. app.use(staticFallback) 
    12. // 各种 route 实现 
    13. app.use(router.routes()) 
    14. app.use(router.allowedMethods()) 
    15. // 对于 public 目录启用静态文件服务 
    16. app.use(require('koa-static')(path.resolve(__dirname, '../public'))) 
    17. // 创建启动服务 
    18. let server = http.createServer(app.callback()) 
    19. server.listen(3000)  

    router.js

     
     
     
    1. router.get('/', function *() { 
    2.   // 加载 index.html 模板和数据,输出 index 页面 
    3. }) 
    4.  
    5. router.get('/appservice', function *() { 
    6.   // 加载 service.html 模板和数据,输出 service 页面 
    7. }) 
    8.  
    9. // 让 `/app/**` 加载小程序所在目录文件 
    10. router.get('/app/(.*)', function* () { 
    11.   if (/\.(wxss|js)$/.test(file)) { 
    12.     // 动态编译为 css 和相应 js 
    13.   } else if (/\.wxml/.test(file)) { 
    14.     // 动态编译为 html 
    15.   } else { 
    16.     // 查找其它类型文件, 存在则返回 
    17.     let exists = util.exists(file) 
    18.     if (exists) { 
    19.       yield send(this, file) 
    20.     } else { 
    21.       this.status = 404 
    22.       throw new Error(`File: ${file} not found`) 
    23.     } 
    24.   } 
    25. })  

    第三步:实现控制层功能

    实现完上面两步,就可以访问 view 页面了,但是你会发现它只能渲染,并不会有任何功能,因为 view 层功能依赖于控制层进行的通讯, 如果控制层收不到消息,它不会响应任何事件。

    控制层是整个实现过程中最复杂的一块,因为官方工具的代码与 nwjs 以及 react 等第三方组件耦合过高,所以无法拿来直接使用。 你可以在 wept 项目的 src 目录下找到控制层逻辑的所有代码,总体上控制层要负责以下几个功能:

    wept 里面 iframe 之间的通讯是通过 message.js 模块实现的,控制页面(index.html)代码如下:

     
     
     
    1. window.addEventListener('message', function (e) { 
    2.   let data = e.data 
    3.   let cmd = data.command 
    4.   let msg = data.msg 
    5.   // 没有跟 contentscript 握手阶段,不需要处理 
    6.   if (data.to == 'contentscript') return 
    7.   // 这是个遗留方法,基本废弃掉了 
    8.   if (data.command == 'EXEC_JSSDK') { 
    9.     sdk(data) 
    10.   // 直接转发 view 层消息到 service,主要是各种事件通知 
    11.   } else if (cmd == 'TO_APP_SERVICE') { 
    12.     toAppService(data) 
    13.   // 除了 publish 发送消息给 view 层以及控制层可以处理的逻辑(例如设置标题), 
    14.   // 其它全部转发 service 处理,所有控制层的处理结果统一先返回 service 
    15.   } else if (cmd == 'COMMAND_FROM_ASJS') { 
    16.     let sdkName = data.sdkName 
    17.     if (command.hasOwnProperty(sdkName)) { 
    18.       command[sdkName](data) 
    19.     } else { 
    20.       console.warn(`Method ${sdkName} not implemented for command!`) 
    21.     } 
    22.   } else { 
    23.     console.warn(`Command ${cmd} not recognized!`) 
    24.   } 
    25. })  

    具体实现逻辑可以查看 src/command.js src/service.jssrc/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 层:

     
     
     
    1. view.postMessage({ 
    2.   msg: { 
    3.     data: { 
    4.       data: { path } 
    5.     }, 
    6.     eventName: 'reload' 
    7.   }, 
    8.   command: 'CUSTOM' 
    9. })  

    view/service 层监听 reload 事件:

     
     
     
    1. WeixinJSBridge.subscribe('reload', function(data) { 
    2.   // data 即为上面的 msg.data 
    3. })  

    第二步: 前端响应不同文件变化

    前端需要对 4 种(wxml wxss json javascript)不同类型文件进行 4 种不同的热更新处理,其中 wxss 和 json 相对简单。

     
     
     
    1. o.subscribe('reload', function(data) { 
    2.     if (/\.wxss$/.test(data.path)) { 
    3.     var p = '/app/' + data.path 
    4.     var els = document.getElementsByTagName('link') 
    5.     ;[].slice.call(els).forEach(function(el) { 
    6.       var href = el.getAttribute('href').replace(/\?(.*)$/, '') 
    7.       if (p == href) { 
    8.         console.info('Reload: ' + data.path) 
    9.         el.setAttribute('href', href + '?id=' + Date.now()) 
    10.       } 
    11.     }) 
    12.   } 
    13. })  
     
     
     
    1. socket.onmessage = function (e) { 
    2.   let data = JSON.parse(e.data) 
    3.   let p = data.path 
    4.   if (data.type == 'reload'){ 
    5.     if (p == 'app.json') { 
    6.       redirectToHome() 
    7.     } else if (/\.json$/.test(p)) { 
    8.       let win = window.__wxConfig__['window'] 
    9.       win.pages[p.replace(/\.json$/, '')] = data.content 
    10.       // header 通过全局 __wxConfig__ 获取 state 进行渲染 
    11.       header.reset() 
    12.       console.info(`Reset header for ${p.replace(/\.json$/, '')}`) 
    13.     } 
    14.   } 
    15. }  
     
     
     
    1. router.get('/generateFunc', function* () { 
    2.   this.body = yield loadFile(this.query.path + '.wxml') 
    3.   this.type = 'text' 
    4. }) 
    5.  
    6. function loadFile(p, throwErr = true) { 
    7.   return new Promise((resolve, reject) => { 
    8.     fs.stat(`./${p}`, (err, stats) => { 
    9.       if (err) { 
    10.         if (throwErr) return reject(new Error(`file ${p} not found`)) 
    11.         // 文件不存在有可能是文件被删除,所以不能使用 reject 
    12.         return resolve('') 
    13.       } 
    14.       if (stats && stats.isFile()) { 
    15.         // parer 函数调用 exec 命令执行 wcsc 文件生成 wxml 对应的 javascript 代码 
    16.         return parser(`${p}`).then(resolve, reject) 
    17.       } else { 
    18.         return resolve('') 
    19.       } 
    20.     }) 
    21.   }) 
    22. }  
     
     
     
    1. // curr 为当前的 VirtualDom 树 
    2. if (!curr) return 
    3. var xhr = new XMLHttpRequest() 
    4. xhr.onreadystatechange = function() { 
    5.   if (xhr.readyState === 4) { 
    6.     if (xhr.status === 200) { 
    7.       var text = xhr.responseText 
    8.       var func = new Function(text + '\n return $gwx("./' +__path__+ '.wxml")') 
    9.       window.__generateFunc__ = func() 
    10.       var oldTree = curr 
    11.       // 获取当前 data 生成新的树 
    12.       var o = m(p.default.getData(), false), 
    13.       // 进行 diff apply 
    14.       a = oldTree.diff(o); 
    15.       a.apply(x); 
    16.       document.dispatchEvent(new CustomEvent("pageReRender", {})); 
    17.       console.info('Hot apply: ' + __path__ + '.wxml') 
    18.     } 
    19.   } 
    20. xhr.open('GET', '/generateFunc?path=' + encodeURIComponent(__path__)) 
    21. xhr.send()  
     
     
     
    1. router.get('/generateJavascript', function* () { 
    2.   this.body = yield loadFile(this.query.path) 
    3.   this.type = 'text' 
    4. })  

    然后我们在 window 对象上加入 Reload 函数执行具体的更换逻辑:

     
     
     
    1. window.Reload = function (e) { 
    2. var pages = __wxConfig.pages; 
    3. if (pages.indexOf(window.__wxRoute) == -1) return 
    4. // 替换原来的构造函数 
    5. f[window.__wxRoute] = e 
    6. var keys = Object.keys(p) 
    7. // 判定是否当前使用中页面 
    8. var isCurr = s.route == window.__wxRoute 
    9. keys.forEach(function (key) { 
    10.   var o = p[key]; 
    11.   key = Number(key) 
    12.   var query = o.__query__ 
    13.   var page = o.page 
    14.   var route = o.route 
    15.   // 页面已经被创建 
    16.   if (route == window.__wxRoute) { 
    17.     // 执行封装后的 onHide 和 onUnload 
    18.     isCurr && page.onHide() 
    19.     page.onUnload() 
    20.     // 创建新 page 对象 
    21.     var newPage = new a.default(e, key, route) 
    22.     newPage.__query__ = query 
    23.     // 重新绑定当前页面 
    24.     if (isCurr) s.page = newPage 
    25.     o.page = newPage 
    26.     // 执行 onLoad 和 onShow 
    27.     newPage.onLoad() 
    28.     if (isCurr) newPage.onShow() 
    29.     // 更新 data 数据 
    30.     window.__wxAppData[route] = newPage.data 
    31.     window.__wxAppData[route].__webviewId__ = key 
    32.     // 发送更新事件, 通知 view 层 
    33.     u.publish(c.UPDATE_APP_DATA) 
    34.     u.info("Update view with init data") 
    35.     u.info(newPage.data) 
    36.     // 发送 appDataChange 事件 
    37.     u.publish("appDataChange", { 
    38.       data: { 
    39.         data: newPage.data 
    40.       }, 
    41.       option: { 
    42.         timestamp: Date.now() 
    43.       } 
    44.     }) 
    45.     newPage.__webviewReady__ = true 
    46.   } 
    47. }) 
    48. u.info("Reload page: " + window.__wxRoute) 
    49. }  

    以上代码需要添加到 t.pageHolder 函数后才可运行

    ***在 view 层初始化后把 Page 函数切换到 Reload 函数(当然你也可以在请求返回 javascript 前把 Page 重命名为 Reload) 。

     
     
     
    1.  
    2.  
    3.   

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


    网页标题:微信小程序架构分析 (下)
    本文路径:http://kswsj.cn/article/cdpjhig.html

    其他资讯