应用程序开发公司
软件开发

针对您的项目需求及预算规划量身制定方案

个体/中小企业/集团/政府机构/行业组织 了解详情 了解详情

微信小程序架构分析《三》:实现过程以及实时更新

发布时间:2024-01-01 00:00 浏览次数:23

小程序实时运转工具 wept 的研发已经基本顺利完成了, 你可以通过我的代码对小程序的 web 环境同时实现存有更全面的重新认识。下面我将了解它的同时实现过程以及实时更新的原理。


小程序 web 服务同时实现


我在 wept 的研发中采用 koa 提供更多 web 服务,以及 et-improve 提供更多模板图形。


第一步: 准备工作页面模板


我们须要三个页面,一个作为掌控层 index.html,一个作为 service 层service.html,除了一个作为 view 层的 view.html


index.html:



service.html:


{{each _.utils as util}} {{/}} {{each _.routes as route}} {{/}}


view.html:



第二步: 同时实现 http 服务


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


server.js


// 日志中间件 app.use(logger()) // gzip app.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 = 404 throw 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.data let cmd = data.command let 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.sdkName if (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.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 USB留言消息给 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 USB)对应页面(如果就是 app.wxss 则就是所有 view 页面)展开创下,view 层接到消息后只须要修改对应 css 文件的时间撕就可以了,代码如下:


o.subscribe('reload', function(data) { if (/\.wxss$/.test(data.path)) { var p = '/app/' + data.path var 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.path if (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 展开处置。首先须要一个USB以获取代莱 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`)) // 文件不存有有可能就是文件被删掉,所以无法采用 reject return resolve('') } if (stats && stats.isFile()) { // parer 函数调用 exec 命令继续执行 wcsc 文件分解成 wxml 对应的 javascript 代码 return parser(`${p}`).then(resolve, reject) } else { return resolve('') } }) }) }


存有了USB就可以命令USB,然后继续执行回到函数展开 diff apply:


// curr 为当前的 VirtualDom 一棵 if (!curr) return var xhr = new XMLHttpRequest() xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { var text = xhr.responseText var func = new Function(text + '\n return $gwx("./' +__path__+ '.wxml")') window.__generateFunc__ = func() var oldTree = curr // 以获取当前 data 分解成代莱一棵 var o = m(p.default.getData(), false), // 展开 diff apply a = 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 更新逻辑相对繁杂一些, 首先依然就是一个USB去以获取代莱 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] = e var keys = Object.keys(p) // 认定与否当前采用中页面 var isCurr = s.route == window.__wxRoute keys.forEach(function (key) { var o = p[key]; key = Number(key) var query = o.__query__ var page = o.page var route = o.route // 页面已经被建立 if (route == window.__wxRoute) { // 继续执行PCB后的 onHide 和 onUnload isCurr && page.onHide() page.onUnload() // 建立崭新 page 对象 var newPage = new a.default(e, key, route) newPage.__query__ = query // 再次存取当前页面 if (isCurr) s.page = newPage o.page = newPage // 继续执行 onLoad 和 onShow newPage.onLoad() if (isCurr) newPage.onShow() // 更新 data 数据 window.__wxAppData[route] = newPage.data window.__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) 。



总算就是把这个坑填入了。期望通过这一系列的分析领略到前端开发者更多思路。

TAG标签:
阅读推荐