【Flutter混合开发】开发一个简单的快速启动框架

news2024/11/25 7:12:15

在这里插入图片描述

目录

  • 前言
  • 启动插件
    • Flutter代码
    • Android代码
    • IOS代码
  • 启动模块
  • 使用
    • android端
    • ios端

前言

因为在移动端中启动Flutter页面会有短暂空白,虽然官方提供了引擎预热机制,但是需要提前将所有页面都进行预热,这样开发成本较高,在研究了闲鱼的FlutterBoost插件后,我看看能不能自己实现一个简单的快速启动框架。

这篇文章用到的知识点都在《flutter混合开发:native与flutter交互》中详细讲解了,大家可以先读一下这篇文章再来看本文。本文不再赘述这些内容,直接上干货。

启动插件

创建一个Flutter Plugin项目,并添加git,然后编写三端代码:

Flutter代码

首先是flutter端的代码

1)RouteManager

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_boot/BasePage.dart';

class RouteManager{
  factory RouteManager() => _getInstance();

  static RouteManager get instance => _getInstance();

  static RouteManager _instance;

  RouteManager._internal(){

  }

  static RouteManager _getInstance(){
    if(_instance == null){
      _instance = new RouteManager._internal();
    }
    return _instance;
  }

  Map<String, BasePage> routes = Map();

  void registerRoute(String route, BasePage page){
    routes[route] = page;
  }

  RouteFactory getRouteFactory(){
    return getRoute;
  }

  MaterialPageRoute getRoute(RouteSettings settings){
    if(routes.containsKey(settings.name)){
      return MaterialPageRoute(builder: (BuildContext context) {
        return routes[settings.name];
      }, settings: settings);
    }
    else{
      return MaterialPageRoute(builder: (BuildContext context) {
        return PageNotFount();
      });
    }
  }

  BasePage getPage(String name){
    if(routes.containsKey(name)) {
      return routes[name];
    }
    else{
      return PageNotFount();
    }
  }
}

class PageNotFount extends BasePage{

  
  State<StatefulWidget> createState() {
    return _PageNotFount();
  }

}

class _PageNotFount extends BaseState<PageNotFount>{

  
  Widget buildImpl(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text("page not found"),
      ),
    );
  }
}

它的作用就是管理路由,是一个单例,用一个map来维护路由映射。其中三个函数比较重要:

  • registerRoute:注册路由。一般在启动时调用。
  • getRouteFactory:返回RouteFactory。将它赋值给MaterialApp的onGenerateRoute字段
  • getPage:通过route名称返回页面widget。

这里getRouteFactory和getPage共用一个路由map,所以不论是页面内切换还是页面切换都保持统一。

2)BaseApp

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_boot/RouteManager.dart';

abstract class BaseApp extends StatefulWidget{

  
  State<StatefulWidget> createState() {
    registerRoutes();
    return _BaseApp(build);
  }

  Widget build(BuildContext context, Widget page);

  void registerRoutes();

}

class _BaseApp extends State<BaseApp>{

  Function buildImpl;
  static const bootChannel = const BasicMessageChannel<String>("startPage", StringCodec());
  Widget curPage = RouteManager.instance.getPage("");

  _BaseApp(this.buildImpl){
    bootChannel.setMessageHandler((message) async {
      setState(() {
        var json = jsonDecode(message);
        var route = json["route"];
        var page = RouteManager.instance.getPage(route);
        page.args = json["params"];
        curPage = page;
      });
      return "";
    });
  }

  
  Widget build(BuildContext context) {
    return buildImpl.call(context, curPage);
  }

}

是一个抽象类,真正的flutter app需要继承它。主要是封装了一个BasicMessageChannel用来与android/ios交互,并根据收到的消息处理页面内的切换,实现快速启动。

继承它的子类需要实现registerRoutes函数,在这里使用RouteManager的registerRoute将每个页面注册一下即可。

3)BasePage

import 'package:flutter/material.dart';

abstract class BasePage extends StatefulWidget{
  dynamic args;
}

abstract class BaseState<T extends BasePage> extends State<T>{
  dynamic args;

  
  Widget build(BuildContext context) {
    if(ModalRoute.of(context).settings.arguments == null){
      args = widget.args;
    }
    else{
      args = ModalRoute.of(context).settings.arguments;
    }
    return buildImpl(context);
  }

  Widget buildImpl(BuildContext context);
}

同样是抽象类,每个flutter页面都需要继承它,它主要是处理两种启动方式传过来的参数,统一到args中,这样子类就可以直接使用而不需要考虑是如何启动的。

Android代码

接下来是plugin中的android的代码

1)BootEngine

package com.bennu.flutter_boot

import android.app.Application
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.StringCodec

object BootEngine {
    public var flutterBoot : BasicMessageChannel<String>? = null

    fun init(context: Application){
        var flutterEngine = FlutterEngine(context)
        flutterEngine.dartExecutor.executeDartEntrypoint(
            DartExecutor.DartEntrypoint.createDefault()
        )
        FlutterEngineCache.getInstance().put("main", flutterEngine)

        flutterBoot = BasicMessageChannel<String>(flutterEngine.dartExecutor.binaryMessenger, "startPage", StringCodec.INSTANCE)
    }
}

这个是单例,初始化并预热FlutterEngine,同时创建BasicMessageChannel用于后续交互。需要在Application的onCreate中调用它的init函数来初始化。

2)FlutterBootActivity

package com.bennu.flutter_boot

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import io.flutter.embedding.android.FlutterActivity
import org.json.JSONObject

class FlutterBootActivity : FlutterActivity() {

    companion object{
        const val ROUTE_KEY = "flutter.route.key"

        fun build(context: Context, routeName : String, params : Map<String, String>?) : Intent{
            var intent = withCachedEngine("main").build(context)
            intent.component = ComponentName(context, FlutterBootActivity::class.java)
            var json = JSONObject()
            json.put("route", routeName)

            var paramsObj = JSONObject()
            params?.let {
                for(entry in it){
                    paramsObj.put(entry.key, entry.value)
                }
            }
            json.put("params", paramsObj)
            intent.putExtra(ROUTE_KEY, json.toString())
            return intent
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
    }

    override fun onResume() {
        super.onResume()
        var route = intent.getStringExtra(ROUTE_KEY)
        BootEngine.flutterBoot?.send(route)
    }

    override fun onDestroy() {
        super.onDestroy()
    }
}

继承FlutterActivity,提供一个build(context: Context, routeName : String, params : Map<String, String>?)函数来启动,传递路由名称和参数。在onResume的时候通过BasicMessageChannel将这两个数据send给flutter处理。

IOS代码

ios与android类似

1)FlutterBootEngine

FlutterBootEngine.h

#ifndef FlutterBootEngine_h
#define FlutterBootEngine_h

#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>

@interface FlutterBootEngine : NSObject

+ (nonnull instancetype)sharedInstance;

- (FlutterBasicMessageChannel *)channel;
- (FlutterEngine *)engine;
- (void)initEngine;
@end

#endif /* FlutterBootEngine_h */
FlutterBootEngine.m
#import "FlutterBootEngine.h"
#import <Flutter/Flutter.h>

@implementation FlutterBootEngine

static FlutterBootEngine * instance = nil;

FlutterEngine * engine = nil;
FlutterBasicMessageChannel * channel = nil;

+(nonnull FlutterBootEngine *)sharedInstance{
    if(instance == nil){
        instance = [self.class new];
    }
    return instance;
}

+(id)allocWithZone:(struct _NSZone *)zone{
    if(instance == nil){
        instance = [[super allocWithZone:zone]init];
    }
    return instance;
}

- (id)copyWithZone:(NSZone *)zone{
    return instance;
}

- (FlutterEngine *)engine{
    return engine;
}

- (FlutterBasicMessageChannel *)channel{
    return channel;
}

- (void)initEngine{
    engine = [[FlutterEngine alloc]initWithName:@"flutter engine"];
    channel = [FlutterBasicMessageChannel messageChannelWithName:@"startPage" binaryMessenger:engine.binaryMessenger codec:[FlutterStringCodec sharedInstance]];
    [engine run];
}

@end

这是也是一个单例,初始化并启动FlutterEngine,并创建一个FlutterBasicMessageChannel与flutter交互。

需要在ios项目的AppDelegate初始化时调用它的initEngine函数。

2)FlutterBootViewController

FlutterBootViewController.h

#ifndef FlutterBootViewController_h
#define FlutterBootViewController_h

#import <Flutter/FlutterViewController.h>

@interface FlutterBootViewController : FlutterViewController

- (nonnull instancetype)initWithRoute:(nonnull NSString*)route
                       params:(nullable NSDictionary*)params;

@end

#endif /* FlutterBootViewController_h */
FlutterBootViewController.m
#import "FlutterBootViewController.h"
#import "FlutterBootEngine.h"

@implementation FlutterBootViewController

NSString * mRoute = nil;
NSDictionary * mParams = nil;

- (nonnull instancetype)initWithRoute:(nonnull NSString *)route params:(nullable NSDictionary *)params{
    self = [super initWithEngine:FlutterBootEngine.sharedInstance.engine nibName:nil bundle:nil];
    mRoute = route;
    mParams = params;
    return self;
}

//viewDidAppear时机有点晚,会先显示一下上一个页面才更新到新页面,所以换成viewWillAppear
- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    if(mParams == nil){
        mParams = [[NSDictionary alloc]init];
    }
    NSDictionary * dict = @{@"route" : mRoute, @"params" : mParams};
    NSData * jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:NULL];
    NSString * str = [[NSString alloc]initWithData:jsonData encoding:NSUTF8StringEncoding];
    NSLog(@"%@", str);
    [FlutterBootEngine.sharedInstance.channel sendMessage:str];
}

@end

同样新增一个使用路由名和参数的构造函数,然后在viewWillAppear时通知flutter。

注意这里如果改成viewDidAppear时机有点晚,会先显示一下上一个页面才更新到新页面,所以换成viewWillAppear。

3)FlutterBoot.h

#ifndef FlutterBoot_h
#define FlutterBoot_h

#import "FlutterBootEngine.h"
#import "FlutterBootViewController.h"

#endif /* FlutterBoot_h */

这个是swift的桥接文件,通过它swift就可以使用我们上面定义的类。

这样我们的plugin就开发完成了,可以发布到pub上。我这里是push到git仓库中,通过git的方式依赖使用。

启动模块

创建一个flutter module,然后引入我们的plugin,在pubspec.yaml中:

dependencies:
  flutter:
    sdk: flutter
  ...
  flutter_boot:
    git: https://gitee.com/chzphoenix/flutter-boot.git

然后我们开发两个页面用于测试。

1)FirstPage.dart

import 'package:flutter/material.dart';
import 'package:flutter_boot/BasePage.dart';

class FirstPage extends BasePage{

  
  State<StatefulWidget> createState() {
    return _FirstPage();
  }
}

class _FirstPage extends BaseState<FirstPage>{

  void _goClick() {
    Navigator.of(context).pushNamed("second", arguments: {"key":"123"});
  }

  
  Widget buildImpl(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter Demo Home Page"),
      ),
      body: Center(
        child: ...,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _goClick,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

继承BasePage和BaseState即可,点击按钮可以跳转到页面2

2)SecondPage.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_boot/BasePage.dart';

class SecondPage extends BasePage{

  
  State<StatefulWidget> createState() {
    return _SecondPage();
  }

}

class _SecondPage extends BaseState<SecondPage>{

  
  Widget buildImpl(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("test"),
        ),
        body:Text("test:${args["key"]}")
    );
  }
}

这个页面获取传递过来的参数key,并展示。

3)main.dart

import 'package:flutter/material.dart';
import 'package:flutter_boot/BaseApp.dart';
import 'package:flutter_boot/RouteManager.dart';

import 'FirstPage.dart';
import 'SecondPage.dart';

void main() => runApp(MyApp());

class MyApp extends BaseApp {
  
  Widget build(BuildContext context, Widget page) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: page,
      onGenerateRoute: RouteManager.instance.getRouteFactory(),
    );
  }

  
  void registerRoutes() {
    RouteManager.instance.registerRoute("main", FirstPage());
    RouteManager.instance.registerRoute("second", SecondPage());
  }
}

入口继承BaseApp,并实现registerRoutes,注册这两个页面。

注意这里的onGenerateRoute使用RouteManager.instance.getRouteFactory(),这样一次注册就可以了,不必自己去实现。

使用

module开发完后,就可以在andorid/ios上使用了。

android端

在android上比较简单,在android项目中引入刚才的module即可,然后需要在android的主module(一般是app)的build.gradle中引入module和plugin,如下:

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    ...
    implementation project(path: ':flutter')  //module
    provided rootProject.findProject(":flutter_boot") //plugin
}

注意plugin的名称是之前在module中的pubspec.yaml定义的。

然后就可以在android中使用了,首先要初始化,如下:

import android.app.Application
import com.bennu.flutter_boot.BootEngine

public class App : Application() {

    override fun onCreate() {
        super.onCreate()
        BootEngine.init(this)
        ...
    }
}

然后合适的时候启动flutter页面即可,启动代码如下:

button.setOnClickListener {
    startActivity(FlutterBootActivity.build(this, "main", null))
}
button2.setOnClickListener {
    var params = HashMap<String, String>()
    params.put("key", "123")
    startActivity(FlutterBootActivity.build(this, "second", params))
}

一个启动无参的页面1,一个启动有参的页面2。

测试可以发现无论打开哪个页面都非常快,几乎没有加载时间。这样就实现了快速启动。

ios端

ios端稍微复杂一些,需要先了解一下ios如何加入flutter,见《flutter混合开发:在已有ios项目中引入flutter》

我选用的是framework的方式引入,所以在flutter module项目下通过命令编译打包framework

flutter build ios-framework --xcframework --no-universal --output=./Flutter/

然后引入到ios项目中,与上一篇文章不同的是,因为这个module中加入了plugin,所以framework产物是四个:

  • App.xcframework
  • flutter_boot.xcframework (这个就是我们的plugin中的ios部分)
  • Flutter.xcframework
  • FlutterPluginRegistrant.xcframework

这四个都需要引入到ios项目中。

然后AppDelegate需要继承FlutterAppDelegate(如果无法继承,则需要处理每个生命周期,见https://flutter.cn/docs/development/add-to-app/ios/add-flutter-screen?tab=engine-swift-tab) 。

然后在AppDelegate中初始化,如下:

import UIKit
import Flutter
import flutter_boot

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FlutterBootEngine.sharedInstance().initEngine()
        return true
    }

    override func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }
}

然后在合适的地方启动flutter页面即可,如下:

@objc func showMain() {
    let flutterViewController =
        FlutterBootViewController(route: "main", params: nil)
    present(flutterViewController, animated: true, completion: nil)
  }

@objc func showSecond() {
    let params : Dictionary<String, String> = ["key" : "123"]
    let flutterViewController =
        FlutterBootViewController(route: "second", params: params)
    present(flutterViewController, animated: true, completion: nil)
  }

同样分别打开两个页面,可以看到启动几乎没有加载时间,同时参数也正确传递。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/614619.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通过点引导掩码表示的弱半监督实例分割

文章目录 The Devil is in the Points: Weakly Semi-Supervised Instance Segmentation via Point-Guided Mask Representation摘要本文方法Weakly Semi-Supervised Instance Segmentation using Point LabelsMask Refinement Network 实验结果消融实验 The Devil is in the Po…

【JavaEE】HTTP状态码-HTTP数据报的构造

HTTP状态码HTTP数据报的构造 文章目录 JavaEE & HTTP状态码 & HTTP数据报的构造1. HTTP状态码1.1 200 - OK1.2 404 - Not Found1.3 403 - Forbidden1.4 500 - Internal Server Error1.5 504 - Gateway Timeout1.6 302/301 重定向 2. 构造HTTP请求2.1 浏览器搜索栏输入u…

Express应用之记账本项目总结

前言 在学完nodejs相关知识后第一个实践就是这个记账本项目&#xff0c;本篇文章是对项目遇到的问题的总结。 先聊聊技术栈&#xff1a; 前端技术&#xff1a;h5结合bootstrap框架&#xff1b; 后端技术&#xff1a;nodejsExpress框架lowdb数据库。 gitee地址&#xff1a;ht…

FinalShell界面左侧为什么能够监测系统指标动态变化的原理并用python实现

前言&#xff1a; 我们可以看出FinalShell是用Java写的&#xff0c;具体怎么看出来的&#xff0c;不能光看界面logo是Java的logo&#xff0c;还要进它的安装目录下进行查看是否真是用Java编写的&#xff01;&#xff01;&#xff01; 具体查看如下&#xff1a; 查看finalshe…

软件外包开发在线监测工具

软件系统上线后需要在线网络工具监测系统的运行&#xff0c;这样在系统出现故障时第一时间通知到系统维护人员&#xff0c;对于软件系统的稳定运行是必不可少的监测工具。今天和大家分享一些常用的在线监测工具&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#…

Vue.js 中的 TypeScript 支持是什么?如何使用 TypeScript?

Vue.js 中的 TypeScript 支持 Vue.js 是一款流行的前端框架&#xff0c;它提供了一种简单、灵活的方式来构建用户界面。随着 TypeScript 的普及&#xff0c;Vue.js 也开始支持 TypeScript&#xff0c;使得开发者可以使用类型检查等 TypeScript 特性来提高代码质量和可维护性。…

看完这篇卡尔曼滤波原理,我被惊到了!

在网上看了不少与卡尔曼滤波相关的博客、论文&#xff0c;要么是只谈理论、缺乏感性&#xff0c;或者有感性认识&#xff0c;缺乏理论推导。能兼顾二者的少之又少&#xff0c;直到我看到了国外的一篇博文&#xff0c;真的惊艳到我了&#xff0c;不得不佩服作者这种细致入微的精…

Vue-- 锚点实现左右两栏联动--scrollIntoView方法

官网&#xff1a;scrollintoView 通过滚动浏览器窗口或某个容器元素&#xff0c;调用元素就可以出现在视窗中。 利用scrollIntoView() 函数 默认是true document.getElementById(需要滚动的div的id).scrollIntoView() 如果给该方法传入true作为参数&#xff0c;或者不传入任何参…

什么是 Vue.js 中的 computed 属性?

什么是 Vue.js 中的 computed 属性&#xff1f; 在 Vue.js 中&#xff0c;computed 属性是一个非常重要的概念。它提供了一种方便的方式来计算和监听 Vue.js 实例中的属性&#xff0c;从而使得代码更加简洁、易于维护。在本文中&#xff0c;我们将详细介绍 computed 属性的原理…

【SVN】设置ubuntu下SVN服务开机自启

目录 0.背景环境 1.开机自启步骤 0.背景环境 1&#xff09;ubuntu下&#xff0c;已搭建好svn版本库&#xff0c;具体搭建方法参考文末的其他博客链接 2&#xff09;在搭svn服务器的过程中&#xff0c;发现ubuntu重启后&#xff0c;svn服务就关闭了 svn正常开启时见下图 所以…

ps技术的革命创新-photoshop beta版

Photoshop 2023 Beta功能介绍 全新的Generative Fill功能现已加入Photoshop Beta桌面应用程序&#xff0c;可以让用户在非破坏性的基础上轻松地添加、扩展或删除图像内容&#xff0c;实现令人惊喜、高兴和震撼的真实效果&#xff0c;仅需数秒。使用此功能&#xff0c;只需要选…

Java编程中必须掌握的抽象类和接口

抽象类与接口是J--a中两个重要的概念&#xff0c;都用于实现多态性和代码重用。在本文中&#xff0c;将会介绍这两个概念的特点以及如何使用它们来实现继承父类同时实现接口&#xff0c;还有接口的多继承。 老规矩&#xff0c;思维导图我已经整理好放在文末&#xff0c;需要自…

【ARM AMBA AXI 入门 1 - AXI 握手协议】

文章目录 1.1 AXI 双向握手机制简介1.1.1 信号列表1.1.2 双向握手目的1.1.3 握手过程 1.2 数据通路的握手要求1.2.1 读数据通路1.2.2 读地址通路1.2.3 写数据通路1.2.4 写地址通路1.2.5 写回复通路1.2.6 全信号 1.3 不同数据通路间的约束关系1.3.1 读操作约束关系1.3.2 写操作约…

怎么将pdf文件免费转为扫描件

推荐两个工具&#xff0c;也算是给自己记一下 1、手机&#xff1a;扫描全能王APP 太好使了&#xff0c;可以直接拍照并转换为扫描件 不开会员的话会出现水印&#xff0c;因为我都是自己用或者交作业就没开 支持读取相册&#xff0c;一次一张、多张都可以 如果不想要水印也…

Grafana 如何监控容器指标(五)

Grafana 如何监控容器指标(五) 1、添加cadvisor CAdvisor 是用于收集有关我们容器信息的常用工具。它是普罗米修斯和格拉法纳用来抓取信息和可视化图表、图表、时间序列和其他各种形式的信息的代理人。CAdvisor从我们的容器中收集各种指标&#xff0c;这些信息被普罗米修斯刮擦…

SQL了解之复制(二)

&#xff08;续&#xff09;从另一个master初始化slave 前面讨论的假设你是新安装的master和slave&#xff0c;所以&#xff0c;slave与master有相同的数据。但是&#xff0c;大多数情况却不是这样的&#xff0c;例如&#xff0c;你的master可能已经运行很久了&#xf…

Pads和AD画图哪个更好?

Mentor Pads和Altium Designer&#xff08;简称&#xff1a;AD&#xff09;是两种常用的电子设计软件&#xff0c;用于PCB设计&#xff0c;它们拥有各自的优势及适用场景&#xff0c;但如何根据项目来选择工具&#xff1f;下面将比对Pads和AD在不同方面的比较&#xff0c;希望对…

chatgpt赋能python:Python安装包制作入门教程

Python 安装包制作入门教程 Python 作为一门简单易学且适用范围广的编程语言&#xff0c;不仅可以运行在各种操作系统上&#xff0c;还能够通过制作安装包方便地分享给其他人使用。本文将介绍如何使用 Python 的一些工具来制作安装包&#xff0c;以帮助 Python 开发者更加便捷…

VFP下载公众号消息图片,小白入门DAY2

封面张终于放正了。 VFP接收到的消息如下 <xml><ToUserName><![CDATA[gh_63145a34e897]]></ToUserName> <FromUserName><![CDATA[oljsK6OgHA9ftJxuCUWg7cFylj6Y]]></FromUserName> <CreateTime>1685926602</CreateTime>…

设置float后,按钮遇上position: relative点击失效

1 问题&#xff1a;设置button浮动到右边之后&#xff0c;button上的onClick事件无法触发 "点击"按钮点不了 2 原因&#xff1a;button浮动到右边之后&#xff0c;下面的div填补上来&#xff0c;但是下面的div设置了position: relative&#xff0c;结果下面的div直接…