【Flutter_Web】Flutter编译Web第四篇(微信小程序篇):Flutter直接运行在微信小程序环境?全程不废话,全是干货!_flutter 微信小程序
前言
这是目前为止,Flutter编译成为web之后最大胆的尝试,我们不是以编译成微信小程序代码的形式,而是直接以webview
的形式尝试在微信小程序中运行,当然,市面上也有一个类似的框架mpflutter
,他们提供了将flutter代码直接运行在微信小程序的方式,但是没有提供将现有的flutter项目转为mpflutter进而运行在微信小程序的方式,且不完全开源和商用付费
,因此我们决定不走这条路,而是直接尝试编译成web之后,在微信小程序提供的webview中去跑。
试验和结论
先说我在前期做的一些试验和结论:
- flutter支持多种渲染模式,是否全都可以在微信小程序正常渲染?
答案是否定的,目前的结果来看,按flutter的编译模式进行编译,仅html的渲染模式可以在微信小程序正常渲染,并且不支持wasm。
- 是否可以进行文件选择、录音、上传文件等系统层级的操作呢?
答案也是否定的,操作文件系统的web api在微信小程序都被修改过,不调用微信小程序提供的api或者不使用微信 js sdk去操作文件相关的是不现实的。
- webview和微信小程序环境是否一致呢,是否可以共享数据呢?
答案也是不可以,他们完全是隔离的。哪怕你拿到微信小程序的文件路径,在webview也是操作不了的,因为给你的是沙盒路径,只有微信自己能操作。
- webview是否可以跟微信小程序进行通信呢?
答案是有,但是不是完美的通信方式,并且数据格式有限制,方法有限制,原因是微信小程序官方提供的通信方式要求太苛刻了,完全不适合实时通信的业务场景。
简单来说,就是目前完美的实时数据通信是不可能做到的(2025年1月2日记录,如果未来微信小程序官方有提供方法的话)。
- webview可以使用微信小程序的api吗?
答案是不可以,webview仅能使用微信js sdk提供的有限的api能力,但是你可以依靠通信的方式去通知微信小程序调用能力。
前期准备
企业类型的微信小程序
如果你需要运行在微信小程序中的webview环境,首先你需要以企业的身份
去创建微信小程序。
原因是只有企业级才支持webview这个组件,个人的是不支持使用的。
添加请求域名
微信小程序规定要求,所有接口、网页都必须走https,并且所有接口域名都必须提前提交到后台。
生成密钥
生成密钥,这个密钥是需要开发的时候使用的,并且你需要把自己添加进开发者成员中,否则无法运行在微信小程序编译器中。
数据通信解决方案
我们先聊聊官方提供的数据通信方案吧,然后我们再讲讲为什么不适用于业务场景?
官方文档:
https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html
重点在这里:
网页向小程序 postMessage 时,会在以下特定时机触发并收到消息:小程序后退、组件销毁、分享、复制链接。data是多次 postMessage 的参数组成的数组。
首先,触发收到消息,仅在这四个时机才能收到,并且收到的数据是一个数组。
分享、复制链接是需要用户主动操作的,做不到无感
。
后退、组件销毁其实是一个意思,就是当前页面关闭的时候,当你只有一个webview的时候,你的所有数据都在webview中,页面关闭的时候还需要通信吗
?
所以我说,官方提供的方案压根就不适用于当前的业务场景,换句话来说,微信官方根本就不希望我们用webview做太多的事情。
因此,当你明白了这件事情之后,很多事情你都能弄清楚。
第一,为什么我使用postMessage之后,在编译器中有反应,但是实际上没有任何数据,原因在于接受数据的时机并不在上述四个时机的其中一个。
第二,为什么数据不能共享,假如数据能够共享,那么他们同处一个环境中,完全不需要postMessgae了,我把数据都放在一个地方,都从一个地方拿就行了,因此微信官方是把数据隔离了,只留下了wx.miniProgram这一个入口(指不定哪天又没了),给你们去做路由跳转。
因此我们完成通信的本质
实际上就是借助于路由跳转能够携带参数
这一个特点去做数据传递。
当然,这种数据传递也有局限性,数据格式就有局限,数据大小也有局限。
图解
我们画张图来描述做法:
- 我们调用
wx.miniProgram.navigateTo
方法跳转微信小程序中间页面并携带参数 - 同时监听页面hash值的变化,因为hash值的改变不会让页面刷新
- 中间页拿到数据之后,经过处理返回接口,将返回的数据改变当前的hash值
- Flutter监听到hash值的改变之后做出处理,关闭中间页,关闭监听。
具体实践
首先,必须确保Flutter web使用的是hash路由
,hash路由的本质是通过监听hash值的变化去动态响应页面内容,也就是我们说的spa应用,无论是vue或者react其实都有这样的做法。
其次,必须确保微信小程序的路由有中间页面。
携带数据跳转
调用wx.miniProgram.navigateTo
方法携带数据,这里我使用serialize方法去序列化,这样可以让我的数据到微信小程序端可以正常解析,其次保留初始hash值,用于拆解数据,最后跳转中间页面并携带数据。
//跳转去微信小程序中间过渡页面 async jumpToWeChatMini(params) { //保留初始hash值 WeChatMiniPlugin.backupHash = window.location.hash; console.log(\'初始hash值为:\', window.location.hash) /** * 对象序列化 * @param {Object} data 要序列化的对象 * @returns {String} */ let serialize = function (data) { var s = \"\"; for (var p in data) s += \"&\" + p + \"=\" + encodeURIComponent(data[p]).replace(/\\+/gm, \"%2B\"); s = s.length > 0 ? s.substring(1) : s; return s; }; return new Promise((resolve, reject) => { wx.miniProgram.navigateTo({ url: \"/pages/index/transition?\" + serialize({ cmd: JSON.stringify({ ...params, backupSrc: window.location.href }) }), success: function (res) { resolve(res) }, fail: function (err) { reject(err) }, }); }); }
中间页接受数据
我使用uniapp,如果你使用微信官方小程序开发也是如此,在onload生命周期的options中接收参数。
onLoad(options) {let result = JSON.parse(decodeURIComponent(options.cmd));console.log(\"获取到信号\", result)this.backSrc = result.backupSrcthis.handleAction(result)},
处理数据并返回
这里我返回webview页面的同时,通过emit通知数据变化,你可以自己定义通信格式
这里我定义
- actionType操作类型
- data数据
- code 成功或者失败代码
- msg 消息
- id
uni.navigateBack()uni.$emit(\'callbackToH5\', {\'actionType\': actionType,\'data\': data,\'code\': code,\'msg\': msg,\'backSrc\': that.backSrc,\'id\': id})
webview接收数据
这里有几点需要注意的:
- flutter的hash模式,可能是**#/home**,也可能带有参数**#/home?a=1**,我们的目的是不能刷新当前页面去改变hash值。
因此如果你是没有search参数的,那么直接拼接数据即可。
比如#/homea=1&b=2,这样是不会刷新页面的。
如果你有search参数,必须拼接在他们中间
比如#/homec=1&b=2?a=1,这样也不会刷新页面
- 你很可能多次发出请求,但是如果你的src没有变化的话,那么src是不会改变的,hash值就不会变,你也就拿不到hash值变化了,因此我这里必须多传一个id,并且id每次发出请求都不一样,这样组装的链接就不一样,每一次都会发出请求。
onLoad() {//监听处理好的数据let that = thisuni.$on(\'callbackToH5\', function(msg) {console.log(\"原始路径\", msg.backSrc)console.log(\"处理好的数据\", msg)let src = msg.backSrc;let id = msg.id ? msg.id : that.id//如果当前的链接带有?号,那么就要把数据放入?前面去,这样会改变hash值,但是不会当前刷新页面if (src.indexOf(\"?\") != -1) {let main = src.split(\'?\')[0] + \'actionType=\' + msg.actionType + \'&code=\' + msg.code +\'&data=\' +encodeURIComponent(JSON.stringify(msg.data)) + \'&msg=\' + encodeURIComponent(msg.msg) + \'&id=\' + id + \'?\' + src.split(\'?\')[1]that.webview_url = main//没有就直接拼接就好} else {that.webview_url = src + \'actionType=\' + msg.actionType + \'&code=\' + msg.code + \'&data=\' +encodeURIComponent(JSON.stringify(msg.data)) + \'&msg=\' + encodeURIComponent(msg.msg) + \'&id=\' + id}that.id++console.log(that.webview_url)})},
flutter web接收到数据
flutter web通过监听hash值的变化获取到数据,我通过保留初始hash值,去做数据拆解,然后还原回原来的hash值,页面不会刷新,但是数据的改变我已经获取到了。
//监听hash值的变化 const listenerWrapper = new EventListenerWrapper(\'hashchange\', (event) => { if (window.location.hash !== WeChatMiniPlugin.backupHash) { // WeChatMiniPlugin.backupHash可能是#/home,也可以是#/home?a=1 if (WeChatMiniPlugin.backupHash.includes(\'?\')) { let main = WeChatMiniPlugin.backupHash.split(\'?\')[0] let part = WeChatMiniPlugin.backupHash.split(\'?\')[1] console.log(\"hash值发生了变化\"); let url = window.location.hash.split(main)[1].split(part)[0] const paramsObject = parseURLParams(\'?\' + url); listenerWrapper.removeListener(); //todo 恢复初始hash值,flutter会自动检查并恢复 // window.location.hash = WeChatMiniPlugin.backupHash //todo 最终在这里返回结果 console.log(\"恢复链接,传递数据\", paramsObject) resolve(paramsObject); } else { console.log(\"hash值发生了变化\"); let url = \'?\' + window.location.hash.split(WeChatMiniPlugin.backupHash)[1] const paramsObject = parseURLParams(url); listenerWrapper.removeListener(); //todo 恢复初始hash值,flutter会自动检查并恢复 // window.location.hash = WeChatMiniPlugin.backupHash //todo 最终在这里返回结果 console.log(\"恢复链接,传递数据\", paramsObject) resolve(paramsObject); } } });
封装插件
最后我把这一套交互封装成了插件的形式。
定义好js层和dart层的通信模型,声明要操作的类和方法,并完成数据转换。
//微信通信模型 js层extension type WeChatActionType._(JSObject _) implements JSObject { external JSString actionType; external JSString code; external JSString data; //todo 注意:data需要json decode external JSString msg; external JSString id; //js to dart,转成map Map<String, dynamic> toDartMap() { return { \'actionType\': actionType.toDart, \'code\': code.toDart, \'data\': data.toDart, \'msg\': msg.toDart, \'id\': id.toDart }; }}//文件下载参数类型,js层extension type DownloadFileType._(JSObject _) implements JSObject { external JSString url; external JSString type; //类型 视频video 图片image external JSString? id; //随机数id //dart to js factory DownloadFileType.toJSMap(Map param) { final obj = JSObject(); return DownloadFileType._(obj) ..url = param[\'url\'] ..type = param[\'type\'] ..id = param[\'id\']; }}//声明要操作的类extension type WeChatMiniPlugin._(JSObject _) implements JSObject { external WeChatMiniPlugin(); external JSPromise<JSBoolean> getEnv(); external JSPromise<WeChatActionType> downloadFile(DownloadFileType param);}@JS(\'window.WeChatMiniPlugin\') //标识全局对象external WeChatMiniPlugin get weChatMiniPlugin;
最后,在dart层,只需要发起请求,等待结果即可。
Future<AiWebWxMessageModel> downloadFile(Map param) async { WeChatActionType result = await weChatMiniPlugin .downloadFile(DownloadFileType.toJSMap(param)) .toDart; return AiWebWxMessageModel.fromJson(result.toDartMap()); }
这是一个webview使用微信小程序api下载视频并保存到相册的功能
AiWebWxMessageModel result = await AiWebWxminiUtil().downloadFile({ \'url\': \'xxxxx.mp4\', \'type\': \'video\', }); print(\"获取到了下载结果${result.msg}\");
注意事项
保存到相册需要更新微信小程序开发者后台的隐私清单,才能使用能力
总结
尽管这种通信方式不算优雅,但是确实能够满足业务需求,因为是唯一的方法了,当然我也很期待微信团队能够给出更好的解决方案。Flutter编译成web之后在微信小程序的体验远不及在app端那么出色,但是我们目前的确做到了ios、安卓、web、微信小程序公用一份代码。
如果你有更好的方案或者你目前有相同的业务需求,欢迎在下方评论,我也会在这里持续更新我在微信小程序遇到的问题和解决方案,共同努力探索。