Flutter WebView 性能优化,让 h5 像原生页面一样优秀

news2024/12/26 23:47:03

在这里插入图片描述

大家好,我是 17。

WebView 的文章分两篇

  1. 在 Flutter 中使用 webview_flutter 4.0 | js 交互
  2. Flutter WebView 性能优化,让 h5 像原生页面一样优秀

本篇和大家一起讨论下性能优化的问题。

WebView 页面的体验上之所以不如原生页面,主要是因为原生页面可以马上显示出页面骨架,一下子就能看到内容。WebView 需要先根据 url 去加载 html,加载到 html 后才能加载 css ,css 加载完成后才能正常显示页面内容,至少多出两步网络请求。有的页面是用 js 渲染的,这样时间会更长。要想让 WebView 页面能接近 Flutter 页面的体验,主要就是要省掉网络请求的时间。

做优化要考虑到很多方面,在成本与收益之间做平衡。如果不是新开项目,需要考虑项目当前的情况。下面分两种情况讨论一下。

服务端渲染

页面 html 已经在服务端拼接完成。只需要 html,css 就可以正常查看页面(主要内容不受影响)。如果你的项目的页面是这样的,那么我们已经有了一个好的起点。

WebView 要显示一个页面,需要串行下面的过程。通过 url 加载到 html 后再加载 css,css 加载完成后显示页面。

url -> html -> css -> 显示

我们可以对 css 的请求做一下优化。优化方案有两种

  1. 内联 css 到 html
  2. 把 css 缓存到本地。

第一种方案比较容易做,修改一下页面的打包方案即可。很容易实现一份代码打包出两个页面,一个外链 css ,一个内联css。但坏处也是很明显的,每次都加载同样的 css,会增加网络传输,如果网络不佳的话,对首屏时间可能会产生明显的影响。就算抛开首屏时间,也会对用户的流量造成浪费。

第二种方案可以解决 css 重复打包的问题。首先要考虑的问题是:css 放在本地的哪个地方?

css 放哪里

有两个地方可以放

  1. 放在 asset,和 app 一起打包发布,好处是简单可靠,坏处是不方便更新。
  2. 放在 文档目录,好处是可以随时更新,坏处是逻辑上会复杂一些。

文档目录用于存储只能由该应用访问的文件,系统不会清除该目录,只有在删除应用时才会消失。

从技术上来说,这两种方案都是可以的。先说下不方便更新的问题:既然 app 的其它页面都不能随便更新,为什么不能接受这个页面的样式不能随便更新?如果是害怕版本冲突,那也好解决,发一次版,更新一次页面地址,每个版本都有其对应的页面地址,这样就不会冲突了。根本原因是掌控的诱惑,即使你能控制住诱惑,你的老板也控制不住。所以还是老老实实选第二种方案吧。

放哪里的问题解决了,接下来要考虑的是如何更新 css 的问题。

更新 css

因为有可能 app 启动后第一个展示的就是这个页面,所以要在 app 启动后第一时间就更新 css。但又有一个问题,每次启动都更新同样的内容是在浪费流量。解决办法是加一个配置,每次启动后第一时间加载这个配置,通过配置信息来判断要不要更新 css。

这个配置一定要很小,比如可以用二进制 01 表示true false,当然了可能不需要这么极端,用一个 map 就好。

如何利用本地 css 快速显示页面

在 app 上启动一个本地 http server 提供 css。 我们可以在打包的时候把 css 的外链写成本地 http,比如 http://localhost:8080/index.css

除了 css,页面的重要图片,字体等静态资源也可以放在本地,只要加载到 html 就可以立即显示页面,省了一步需要串行的网络请求。

到这里服务端渲染页面的优化就完成了,还是很简单的吧,示例代码在后面。

浏览器渲染

近年来,随着 vue,react 的兴起,由 js 在浏览器中拼接 html 逐渐成为主流。虽然可以用同构的方案,但那样会增加成本,除非必须,一般都是只在浏览器渲染。可能你的页面正是这样的。我们来分析一下。

WebView 要显示一个页面,需要串行下面的过程。通过 url 加载到 html 后再加载 css、js,js 请求完数据后才能显示页面。

url -> html -> css,js -> js 去加载数据 -> 显示

和服务端渲染的页面相比,首次请求时间更长。多出了 js 加载数据的时间。除了要缓存 css,还要缓存 js 和数据。缓存 js 是必须的,缓存数据是可选的。好消息是 html 只有骨架,没有内容,可以连 html 也一起缓存。

缓存 js,html 的方案和缓存 css 的方案是一样的。缓存数据会面临数据更新的难题,所以只可以缓存少量不需要时时更新的少量重要数据,不需要所有数据都缓存。app 的原生页面也是需要加载数据的,也不是每种数据都要缓存。

数据更新之所以说是一个难题,是因为很多内容数据是需要即时更新的。但数据已经下发到客户端,已经缓存起来,客户端不再发起新的请求,如何通知客户端进行数据更新?虽然有轮询,socket,服务端推送等方案可以尝试,但开发成本都比较高,和获得的收益相比,代价太大。

当缓存了 html,css,js 等静态资源后,h5 就已经和原生页面站在同一起跑线上了,对于只读的页面,体验上相差无几。

加载数据后还有js 拼接 html 的时间,和加载的时间相比,只要硬件还可以的情况下,消耗的时间可以忽略

图片不适合用缓存 css 的方案,因为图片太大也太多。只能预加载少量最重要的图片,其它大量图片只能对二次加载做优化,我们会在后面讨论

浏览器渲染的页面也需要打包的配合,需要把所有的要缓存的静态资源地址都换成本地地址,这就要求发布的时候一份代码需要发布两个页面。一个是给浏览器用的,资源都通过网络加载。一个是给 WebView 用的,资源都从本地获取。

思路已经有了,具体实现就简单了。下面我给出关键环节的示例代码,供大家参考。

如何启动本地server

本地不需要 https,用 http 用行了,但是需要在 AndroidManifest.xml 的 applictation 中做如下配置 android:usesCleartextTraffic="true"

import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_static/shelf_static.dart';
import 'package:path_provider/path_provider.dart';

Future<void> initServer(webRoot) async {
  var documentDirectory = await getApplicationDocumentsDirectory();
  
  var handler =
      createStaticHandler('${documentDirectory.path}/$webRoot', defaultDocument: 'index.html');
  io.serve(handler, 'localhost', 8080);
}

createStaticHandler 负责处理静态资源。

如果要兼容 windows 系统,路径需要用 path 插件的 join 方法拼接

如何让 WebView 的页面请求走本地服务

两种方案:

  1. 打包的时候需要缓存的页面的地址都改成本地地址
  2. 对页面请求 在 WebView 中进行拦截,让已经缓存的页面走本地 server。

相比之下,第 2 种方案都好一些。可以通过配置文件灵活修改哪些页面需要缓存。

在下面的示例代码中 ,cachedPagePaths 存储着需要缓存的页面的 path。

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class MyWebView extends StatefulWidget {
  const MyWebView({super.key, required this.url, this.cachedPagePaths = const []});
  final String url;
  final List<String> cachedPagePaths;

  
  State<MyWebView> createState() => _MyWebViewState();
}

class _MyWebViewState extends State<MyWebView> {
  late final WebViewController controller;

  
  void initState() {
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(NavigationDelegate(
        onNavigationRequest: (request) async {
          var uri = Uri.parse(request.url);
          // TODO: 还应该判断下 host
          if (widget.cachedPagePaths.contains(uri.path)) {
            var url = 'http://localhost:8080/${uri.path}';
            Future.microtask(() {
              controller.loadRequest(Uri.parse(url));
            });
            return NavigationDecision.prevent;
          } else {
            return NavigationDecision.navigate;
          }
        },
      ))
      ..loadRequest(Uri.parse(widget.url));
    super.initState();
  }
  
  void didUpdateWidget(covariant MyWebView oldWidget) {
    
    if(oldWidget.url!=widget.url){
      controller.loadRequest(Uri.parse(widget.url));
    }
    
    super.didUpdateWidget(oldWidget);
  }
  
  Widget build(BuildContext context) { 
    return Column(
      children: [Expanded(child: WebViewWidget(controller: controller))],
    );
  }
}

优化图片请求

如果页面中有很多图片,你会发现,体验上还是不如 Flutter 页面,为什么呢?原来 Flutter Image Widget 使用了缓存,把请求到的图片都缓存了起来。 要达到相同的体验,h5 页面也需要实现相同的缓存功能。

关于 Flutter 图片请参见 快速掌握 Flutter 图片开发核心技能

代码实现

要如何实现呢?只需要两步。

  1. 打包的时候需要把图片的外链请求改成本地请求
  2. 本地 server 对图片请求进行拦截,优先读缓存,没有再去请求网络。

第 1 条我举个例子,比如图片的地址为 https://juejin.com/logo.png ,打包的时候需要修改为 http://localhost:8080/logo.png

第 2 条的实现上,我们取个巧,借用 Flutter 中的 NetworkImage,NetworkImage 有缓存的功能。

下面给出完整示例代码,贴到 main.dart 中就能运行。运行代码后看到一段文字和一张图片。

注意先安装相关的插件,插件的名字 import 里有。

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_static/shelf_static.dart';
import 'dart:ui' as ui;
import 'package:webview_flutter/webview_flutter.dart';

const htmlString = '''
<!DOCTYPE html>
<head>
<title>webview demo | IAM17</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, 
  maximum-scale=1.0, user-scalable=no,viewport-fit=cover" />
<style>
*{
  margin:0;
  padding:0;
}
body{
   background:#BBDFFC;  
   text-align:center;
   color:#C45F84;
   font-size:20px;
}
img{width:90%;}
p{margin:30px 0;}
</style>
</head>
<html>
<body>
<p>大家好,我是 17</p>
<img src='http://localhost:8080/tos-cn-i-k3u1fbpfcp/
c6208b50f419481283fcca8c44a2e3af~tplv-k3u1fbpfcp-watermark.image'/>
</body>
</html>
''';
void main() async {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  WebViewController? controller;
  
  void initState() {
    init();
    super.initState();
  }

  init() async {
    var server = Server17(remoteHost: 'p6-juejin.byteimg.com');
    await server.init();

    var filePath = '${server.webRoot}/index.html';
    var indexFile = File(filePath);
    await indexFile.writeAsString(htmlString);
    setState(() {
      controller = WebViewController()
        ..loadRequest(Uri.parse('http://localhost:${server.port}/index.html'));
    });
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      body: SafeArea(
        child: controller == null
            ? Container()
            : WebViewWidget(controller: controller!),
      ),
    ));
  }
}

class Server17 {
  Server17(
      {this.remoteSchema = 'https',
      required this.remoteHost,
      this.port = 8080,
      this.webFolder = 'www'});
  final String remoteSchema;
  final String remoteHost;

  final int port;
  final String webFolder;
  String? _webRoot;
  String get webRoot {
    if (_webRoot == null) throw Exception('请在初始化后读取');
    return _webRoot!;
  }

  init() async {
    var documentDirectory = await getApplicationDocumentsDirectory();
    _webRoot = '${documentDirectory.path}/$webFolder';
    await _createDir(_webRoot!);
    var handler = Cascade()
        .add(getImageHandler)
        .add(createStaticHandler(_webRoot!, defaultDocument: 'index.html'))
        .handler;

    io.serve(handler, InternetAddress.loopbackIPv4, port);
  }

  _createDir(String path) async {
    var dir = Directory(path);
    var exist = dir.existsSync();
    if (exist) {
      return;
    }
    await dir.create();
  }

  Future<Uint8List?> loadImage(String url) async {
    Completer<ui.Image> completer = Completer<ui.Image>();
    ImageStreamListener? listener;
    ImageStream stream = NetworkImage(url).resolve(ImageConfiguration.empty);
    listener = ImageStreamListener((ImageInfo frame, bool sync) {
      final ui.Image image = frame.image;
      completer.complete(image);
      if (listener != null) {
        stream.removeListener(listener);
      }
    });
    stream.addListener(listener);
    var uiImage = await completer.future;
    var pngBytes = await uiImage.toByteData(format: ui.ImageByteFormat.png);
    if (pngBytes != null) {
      return pngBytes.buffer.asUint8List();
    }
    return null;
  }

  FutureOr<Response> getImageHandler(Request request) async {
    if (RegExp(
      r'\.(png|image)$',
    ).hasMatch(request.url.path)) {
      var url = '$remoteSchema://$remoteHost/${request.url.path}';
      var imageData = await loadImage(url);
      //TODO: 如果 imageData 为空,改成错误图片
      return Response.ok(imageData);
    } else {
      return Response.notFound('next');
    }
  }
}

代码逻辑

  1. 在本地文档目录的 www 文件夹中准备了一个 index.html 文件
  2. 启动本地 server,通过访问 http://localhost:8080/index.html 请求本地页面。
  3. server 收到请求后,对图片请求进行拦截,通过 NetworkImage 返回图片。

第 2 条。本例中是直接访问的 localhost,实际应用中,页面地址是外链地址,通过拦截的方式请求本地。如何做页面地址拦截前面已经给出示例了。

第 3 条。打包后的时候对所有图片地址都写成了本地地址,改成本地地址的目的就是为了让图片请求都由本地 server 响应。本地 server 拿到 图片地址后,再改回网络地址,通过 NetworkImage 请求图片。NetworkImage 会首先判断有没有缓存,有直接用,没有就发起网络请求,然后再缓存。

可能你觉得有点绕,既然最后还要用网络地址,为什么还要先写成本地地址,象拦截页面请求那样拦截图片请求不香吗?答案是不可以。两个原因。

  1. webview_flutter 只能拦截页面请求。
  2. 本地 server 不方便拦截 443 端口。

对比于拦截 443 端口,修改打包方案要容易的多。

关于图片类型

在示例代码中,用 RegExp( r'\.(png|image)$',) 判断是否要响应请求。从正则可以看出,以 png 或 image 结果的图片都能响应请求。判断 image 是因为示例中的图片地址是以 image 结尾的。

示例代码只能支持 png 格式的图片,示例图片虽然是 image 结尾,但格式也是 png 格式。如果要支持更多格式的图片,需要用到第三方库。

关于图片地址

如果图片地址失改,可以自行换一个,随使在网上找个 png 图片 地址就行。

把图片缓存到磁盘。

我们演示了把图片缓存到内存,当 app 被杀掉,缓存都没了,除非缓存到磁盘。这项工作已经有插件帮我们做了。
用 cached_network_image 替换 NetworkImage,稍加改动就可以实现磁盘缓存了。

总结一下

服务端染页面方案

  1. 打包的时候需要打出两个页面,一个页面的 css 外链接是外网,一个页面的 css 链接是本地。
  2. 在 App 启动的时候根据配置信息预加载 css 存到文档目录。
  3. 启动本地 server 响应 css 的请求。

浏览器渲染方案

  1. 打包的时候需要打出两个页面,一个页面的 css,js 链接是外网,一个页面的 css,js 链接是本地。
  2. 在 App 启动的时候根据配置信息预加载 html,css,js 存到文档目录。
  3. 根据配置信息拦截页面请求,已经缓存的页面改走本地 server。
  4. 启动本地 server 响应 html,css,js 的请求

图片缓存

如果不做图片缓存,通过前面两个方案,h5 速度就已经得到大大提高了。如果有余力,可以做图片缓存。图片缓存是可选的,是对前面两种方案的加强。

  1. 给 app 用的页面打包的时候把图片地址换成本地地址。
  2. 启动本地 server 响应图片请求,有缓存就读缓存,没有缓存走网络。

可能你的项目不同,有不同的方案,欢迎一起讨论。

本文到这里就结束了,谢谢观看。

番外

为了给自己一点压力,上一篇 在 Flutter 中使用 webview_flutter 4.0 | js 交互 中我就预告说今天要发这篇性能优化的文章。结果压力是有的了,但却没能按时完工(理想情况是周日下午完工,这样可以休息一下)。一个原因是 升级 flutter 报错,浪费了一个上午,再有就是写了一版后,并不满意,又重写了一版,最后才定稿。一直写到深夜才把主要内容写完。早上起来又做了补充修改。

由于时间紧,有不妥之处,还请各位大佬雅正。

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

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

相关文章

c#数据结构-有序列表和有序字典

有序列表和有序字典 有序列表和有序字典都是是一个键值对容器&#xff0c;像字典一样。 从习惯和描述推测&#xff0c; 列表控制一个数组有序列表使用比有序字典更少的内存如果一次性添加一堆数据&#xff0c;且这堆数据有序。那么有序列表比有序字典更快 有序列表大概长这样 …

最后一个单词的长度-力扣58-java

一、题目描述给你一个字符串 s&#xff0c;由若干单词组成&#xff0c;单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。示例 1&#xff1a;输入&#xff1a;s "Hello World"输出&#x…

聊聊火出圈的ChatGPT

前言 OpenAI 近期发布聊天机器人模型 ChatGPT&#xff0c;迅速火爆各大技术网站&#xff0c;就像一个突然激起的巨浪打破了沉寂已久的水面。它的出现无论是对人工智能的资本圈还是技术圈都感受到了春风回暖&#xff0c;前兔似锦的未来。 尤记得2015年谷歌开源的Tensorflow&am…

2.19、读者-写者问题

有读者和写者两组并发进程&#xff0c;共享一个文件&#xff0c;当两个或两个以上的读进程同时访问共享数据时不会产生副作用&#xff0c; 但若某个写进程和其他进程&#xff08;读进程或写进程&#xff09;同时访问共享数据时则可能导致数据不一致的错误。 因此要求: ①允许…

新专利:苹果笔可充当VR游戏控制器,智能笔与Find My结合将成为趋势

根据美国商标和专利局&#xff08;USPTO&#xff09;公示的清单&#xff0c;苹果获得了一项关于 Apple Pencil 的技术专利。根据专利描述&#xff0c;Apple Pencil 可以和混合现实头显、iPad、Mac 显示器等设备进行交互和操作&#xff0c;可以充当 VR 游戏的控制器。 该专利的…

Three.js 3D建模必备基础

在 three.js 中&#xff0c;可见对象由几何体和材质构成。 我们已经了解了如何创建适用于点和线图元的简单几何图形&#xff0c;并且遇到了各种标准网格几何图形&#xff0c;例如 THREE.CylinderGeometry 和 THREE.IcosahedronGeometry。 在本节中&#xff0c;我们将了解如何从…

【Unity 代码相关】实现脚本之间动态变量传递

这是我在做作品的过程中遇到的问题&#xff0c;大概阐述一下遇到的问题及解决思路。 1 问题阐述 因为是做天空盒&#xff0c;有很多变量需要控制&#xff0c;shader之间又想实现颜色、变量的传递&#xff0c;所以整个项目的所有暴露出来的可调整变量&#xff0c;我都汇总在了…

算法训练营DAY53|392.判断子序列、115.不同的子序列

392. 判断子序列 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/is-subsequence/ 判断子序列这道题目&#xff0c;和上一期的题解法几乎完全相同&#xff0c;只是递推公式有一点差别&#xff0c;但是要是完全用之前的代码也是可行的。…

模拟信号隔离放大器直流信号转换0-20mA/ 4-20mA/2-10V/0-±5V/0-±10V

概述&#xff1a;导轨安装DIN11 IPO EM系列模拟信号隔离放大器是一种将输入信号隔离放大、转换成按比例输出的直流信号混合集成电路。产品广泛应用在电力、远程监控、仪器仪表、医疗设备、工业自控等需要电量隔离测控的行业。该模块内部嵌入了一个高效微功率的电源&#xff0c;…

【Linux系统编程】02:文件操作

文件IO 系统调用&#xff08;不带缓冲的IO操作&#xff09;库函数&#xff08;默认带用户缓冲的IO操作&#xff09; 一、非缓冲IO 系统调用&#xff1a;即为不带缓冲的IO 1.打开文件open 2.读取文件read NAMEread - read from a file descriptorSYNOPSIS#include <unist…

Spring Boot HTTP 400排查

背景 前段时间朋友咨询他们公司某个HTTP接口偶现400错误&#xff0c;有没有什么好的分析方法和解决方案&#xff0c;使用的是Spring Cloud体系。最近有时间总结下这个问题的处理过程。 为了分析问题&#xff0c;笔者使用 Spring Boot 3.0.2还原报错场景进行讲解。 问题分析 …

OM | 强化学习 + 约束规划求解组合优化问题

组合优化在航空航天、交通规划以及经济学等众多学科领域中有广泛应用&#xff0c;其目标是在有限集中寻找最优解。然而状态空间过大的问题让目前组合优化变得棘手。在过去的几年中&#xff0c;使用深度强化学习&#xff08;deep reinforcement learning&#xff0c;DRL&#xf…

SharpSCCM:一款利用SCCM实现横向渗透的强大工具

关于SharpSCCM SharpSCCM是一款功能强大的横向渗透和凭证收集工具&#xff0c;该工具主要利用了微软终端配置管理器&#xff08;SCCM&#xff09;来实现其功能&#xff0c;并能够通过访问SCCM管理终端GUI来实现横向渗透和凭证收集。 功能介绍 1、后渗透功能&#xff0c;支持横…

QT入门Input Widgets之QSlider

目录 一、界面布局功能 1、界面位置介绍 2、控件界面基本属性 2.1horizontalSlider界面属性 2.2设置步进 2.3打开或关闭滑块跟踪&#xff08;setTracking&#xff09; 二、属性功能介绍 1、常用信号 2、调用信号 3、鼠标点击QSlider滑块定在点击位置 三、Demo展示 …

2.8、调度算法的评价指标

1、CPU 利用率 由于早期的 CPU 造价极其昂贵&#xff0c; 因此人们会希望让CPU尽可能多地工作\color{red}希望让 \texttt{CPU} 尽可能多地工作希望让CPU尽可能多地工作 CPU利用率\color{red}\texttt{CPU}利用率CPU利用率&#xff1a;指 CPU “忙碌” 的时间占总时间的比例。 利…

动态规划之01背包问题和完全背包问题

01背包的问题描述&#xff1a;&#xff08;内容参考代码随想录&#xff09;有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品只能用一次&#xff0c;求解将哪些物品装入背包里物品价值总和最大。问题示例&#…

Echarts 设置折线图拐点的颜色,边框等样式,hover时改变颜色

第014个点击查看专栏目录上一篇文章我们讲到了如何设置拐点大小,图形类型&#xff0c;旋转角度&#xff0c;缩放同比&#xff0c;位置偏移等&#xff0c;这篇文章介绍如何设置拐点的颜色、边框大小颜色等样式。hover轴线时候&#xff0c;拐点的填充颜色改变文章目录示例效果示例…

Zookeeper安装部署

文章目录Zookeeper安装部署Zookeeper安装部署 将Zookeeper安装包解压缩&#xff0c; [rootlocalhost opt]# ll 总用量 14032 -rw-r--r--. 1 root root 12392394 10月 13 11:44 apache-zookeeper-3.6.0-bin.tar.gz drwxrwxr-x. 6 root root 4096 10月 18 01:44 redis-5.0.4 …

什么时候用MQ、MQ 的作用、延迟消息

本文主要参考沈剑大佬的消息队列系列的四篇博文和博文评论&#xff0c;以及刘海丰老师的《架构设计面试精讲》&#xff0c;文末是完整参考。 1、什么时候用MQ、MQ 的作用 MQ是一个互联网架构中常见的解耦利器。 1.1 MQ 的组成 Producer&#xff1a;消息的生产者&#xff1b; Br…

浅谈函数式编程和命令式编程的区别

一、函数式编程 ------------------------------------------------------------------------------------------------------------------------------------------ 函数式编程用一个英文单词来说的话就是“What?” 它关注结果 定义 把某个功能的具体实现&#xff0c;封装…