flutter web其实出来也有一段时间了,最近在用flutter做一个跨端的APP,由于又加入了小程序端,所以在探索怎么通过flutter web来做小程序。
一、实现原理
通过flutter web将程序打包成手机浏览器能访问的网页,然后用uni中的webview来展示这个页面,最后通过uni打包成小程序发布,这样一个通过flutter实现的小程序基本就处理完毕了。
二、小程序登录问题
由于项目需要获取微信用户的openid,所以需要在小程序端做小程序登录处理,这里就涉及到小程序跟flutter交互的问题。目前我的处理办法是,使用uni创建一个登陆页面用于微信用户在小程序端登录:
<template>
<view class="content">
<image src="../../static/dog_logo.png" mode="aspectFill" style="width: 200rpx;height: 200rpx;margin-top: 200rpx;"></image>
<text style="margin-top: 30rpx;">恋爱狗</text>
<button class="login" type="primary" @tap="wxLogin">
授权登录
</button>
</view>
</template>
<script>
import {
wxLogin,
} from '@/api/network.js';
export default {
data() {
return {
errordata:'',
}
},
onShow() {
},
onLoad() {
},
methods: {
message(event) {
console.log("发的东西呢")
console.log(event.detail.data);
},
wxLogin() {
// 获取code 小程序专有,用户登录凭证。
uni.getUserProfile({
desc: "获取用户基本资料",
lang: 'zh_CN',
success: (user) => {
//获取成功基本资料后开启登录,因为基本资料首先要授权
uni.login({
provider: 'weixin',
success: function(code_res) {
if (code_res.errMsg == "login:ok") {
let code = code_res.code;
uni.navigateTo({
url:"./home?code="+code+"&nickName="+user.userInfo.nickName+"&avatarUrl="+user.userInfo.avatarUrl
})
}
}
});
},
fail: (res) => {
uni.showModal({
title:"用户拒绝授权",
showCancel:false
})
}
});
},
}
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.login {
height: 100rpx;
width: 500rpx;
margin-top: 400rpx;
margin-left: auto;
margin-right: auto;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>
通过uni提供的小程序登录功能获取到code以及用户的相关信息,然后跳转到集成webview的页面,通过页面传参的方式把用户信息传到flutter端,home.vue代码如下:
<template>
<view class="content">
<web-view :src="url" @message="message"></web-view>
</view>
</template>
<script>
import {
wxLogin,
} from '@/api/network.js';
export default {
data() {
return {
title: 'Hello',
url: ''
}
},
onShow() {
},
onLoad(e) {
this.url="https://url/#/SplashPage?code="+e.code+"&nickName="+e.nickName+"&avatarUrl="+e.avatarUrl;
},
methods: {
}
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>
在flutter端接收传递过来的信息通过接口将该用户在系统的信息获取过来用以将微信号跟系统账户绑定。
关于flutter端接收链接传参的操作如下,在main.dart中做如下更改,由于我flutter端是用的fish_redux做的路由管理,所以该方法可能仅对fish_redux有用:
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:tree_flutter/app_route.dart';
import 'package:tree_flutter/cache/local_storage.dart';
import 'package:tree_flutter/cache/userinfo_cache_manager.dart';
import 'package:tree_flutter/constants/global_events.dart';
import 'package:tree_flutter/constants/global_theme_styles.dart';
import 'package:tree_flutter/generated/l10n.dart';
import 'package:tree_flutter/net/api_work.dart';
import 'package:tree_flutter/utils/base_tools.dart';
import 'package:tree_flutter/utils/toast_tools.dart';
void main() {
// println(name+"---------");
LocalStorage.checnLocalThemeResources().then((e) =>runApp(MyApp()));
// runApp((MyApp()));
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver{
// This widget is the root of your application.
@override
void initState() {
// TODO: implement initState
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print("--" + state.toString());
switch (state) {
case AppLifecycleState.inactive: // 处于这种状态的应用程序应该假设它们可能在任何时候暂停。
break;
case AppLifecycleState.resumed:// 应用程序可见,前台
// GlobalThemeStyles.instan/ce.setStatusBarWhiteForeground(false);
// eventBus.fire("authtoken");
break;
case AppLifecycleState.paused: // 应用程序不可见,后台
break;
case AppLifecycleState.detached:
// TODO: Handle this case.
break;
}
}
@override
Widget build(BuildContext context) {
return ScreenUtilInit(builder: ()=>MaterialApp(
title: 'title',
showSemanticsDebugger: false,
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
locale: GlobalThemeStyles.themeLocale,
supportedLocales: <Locale>[
const Locale('en', 'US'), // 美国英语
const Locale('zh', 'CN'), // 中文简体
],
onGenerateRoute: (RouteSettings settings) {
if(BaseTools.IS_WEB){
Map<String, dynamic> map = BaseTools.urlToMap(settings.name);
if (!BaseTools.isEmpty(settings.arguments)) {
println(settings.arguments.toString());
}
if (!BaseTools.isEmpty(map)) {
return MaterialPageRoute<Object>(builder: (BuildContext context) {
return AppRoute.global.buildPage(
settings.name.replaceAll("/", "").split("?")[0], map);
});
} else {
return MaterialPageRoute<Object>(builder: (BuildContext context) {
return AppRoute.global.buildPage(
settings.name.replaceAll("/", "").split("?")[0],
settings.arguments);
});
}
}
return MaterialPageRoute<Object>(builder: (BuildContext context) {
return AppRoute.global.buildPage(settings.name, settings.arguments);
});
},
home: AppRoute.global.buildPage(RoutePath.SPLASH_PAGE, null),
// initialRoute: RoutePath.SplashPage,
),
designSize: Size(375, 667),
);
}
}
其中核心部分在:
if(BaseTools.IS_WEB){
Map<String, dynamic> map = BaseTools.urlToMap(settings.name);
if (!BaseTools.isEmpty(settings.arguments)) {
println(settings.arguments.toString());
}
if (!BaseTools.isEmpty(map)) {
return MaterialPageRoute<Object>(builder: (BuildContext context) {
return AppRoute.global.buildPage(
settings.name.replaceAll("/", "").split("?")[0], map);
});
} else {
return MaterialPageRoute<Object>(builder: (BuildContext context) {
return AppRoute.global.buildPage(
settings.name.replaceAll("/", "").split("?")[0],
settings.arguments);
});
}
}
urlToMap方法具体代码如下:
static Map<String,dynamic> urlToMap(String url){
Map<String,dynamic > map=new HashMap();
if(isEmpty(url)||!url.contains("?")){
return null;
}else{
List<String> urlList=url.split("?");
urlList=urlList[urlList.length-1].split("&");
for(int i=0;i<urlList.length;i++){
map[urlList[i].split("=")[0]]=urlList[i].split("=")[1];
}
println(map.toString());
}
return map;
}
三、关于微信小程序支付问题
由于flutter无法直接调用小程序的支付,所以要使用小程序支付功能稍微有些复杂,具体实现还是要通过uni让小程序跟flutter端建立连接。
1.首先在flutter端做好生成订单以及通过接口获取微信小程序支付需要的相关的数据,然后通过flutter端打开uni页面,具体实现如下:
首先导入:
import 'dart:js' as js;
然后:
js.context.callMethod("testPay",["./orderPay?timeStamp="+paydata["timestamp"].toString()+"&nonceStr="+paydata["nonce_str"].toString()+"&package="+paydata["prepay_id"].toString()+"&paySign="+paydata["sign"].toString()]);
通过dart.js提供的callMethod来与uni页面进行通信,其中testPay代表函数名称,这个函数怎么定义下面会说,然后后面数组里面的是传参。
在flutter下面的web文件夹下面有一个index.html文件,上面的testPay需要定义在这个文件下面:
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="tree_flutter">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<title>恋爱树</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script type="text/javascript"
src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>
<script type="text/javascript"
src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
<script>
function testPay(timeStamp) {
uni.navigateTo({
url:timeStamp
});
}
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js';
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
}
if ('serviceWorker' in navigator) {
// Service workers are supported. Use them.
window.addEventListener('load', function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener('statechange', () => {
if (serviceWorker.state == 'activated') {
console.log('Installed new service worker.');
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing || reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
</script>
</body>
</html>
其中
<script type="text/javascript"
src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>
<script type="text/javascript"
src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
需要导入,具体的实现就是下面这个函数:
function testPay(timeStamp) {
uni.navigateTo({
url:timeStamp
});
}
uni.navigateTo({url:timeStamp});这个就是uni打开页面所调用的方法,这样就可以通过flutter生成的web页面在uni中通过webview加载后打开uni的页面。
最后uni端的实现就是一个简单的uni实现微信小程序支付的功能:
<template>
<view>
支付页面
</view>
</template>
<script>
import {
wxPay,
} from '@/api/network.js';
export default {
data() {
return {
timeStamp: '',
nonceStr:'',
package:'',
paySign:'',
}
},
onLoad(e) {
this.timeStamp = e.timeStamp
this.nonceStr = e.nonceStr
this.package = e.package
this.paySign = e.paySign
console.log(JSON.stringify(e))
// console.log(this.ordernumber)
uni.showLoading({
mask: true,
title: "支付中"
})
this.wxPay()
},
methods: {
wxPay() {
// let openid = uni.getStorageSync('openid');
uni.requestPayment({
provider: 'wxpay',
timeStamp: this.timeStamp,
nonceStr: this.nonceStr,
package: 'prepay_id=' + this.package,
signType: 'MD5',
paySign: this.paySign,
success: function(res) {
console.log('success:' + JSON.stringify(res));
uni.showToast({
title: "支付成功",
icon: 'none',
})
setTimeout(function() {
uni.navigateBack();
}, 200);
},
fail: function(err) {
console.log('fail:' + JSON.stringify(err));
uni.showToast({
title: "支付取消",
icon: 'none',
})
setTimeout(function() {
uni.navigateBack();
}, 200);
}
});
}
}
}
</script>
<style>
</style>
其实flutter做小程序并没有多大优势,尤其是导航栏不像uni那样可以适配小程序的源生导航栏,所以能不用最好不要用flutter搞什么小程序开发,flutter的真正优势还是在Android很iOS的跨端,目前web端也没有太大优势。