Flutter for Web 首次首屏优化——JS 分片优化

news2024/11/26 14:53:23

作者:马坤乐(坤吾)

Flutter for Web(FFW)从 2021 年发布至今,在国内外互联网公司已经得到较多的应用。作为 Flutter 技术在 Web 领域的有力扩充,FFW 可以让熟悉 Flutter 的客户端同学直接上手写 H5,复用 App 端代码高效支撑业务需求;在 App 侧 FFW 也可作为 Flutter 动态下发的兜底方案。总的来说在业务和技术上 FFW 都具有相当的价值。

然而在使用 FFW 时有一个明显的问题:其编译产物 main.dart.js 较大,初始的 Hello world 工程编译后产物 js 大小为 1.2 MB,添加业务代码后 js 的大小还会继续增加。在阿里卖家的内容外投业务中,3 个页面的工程 js 大小为 2.0 MB,js 文件过大直接的影响就是页面首次首屏加载的速度。针对 js 的大小有较多优化方法,本文主要记录 main.dart.js 分片优化方案的实现。

1.方案总览

图 1.  FFW js 分片示意

页面 js 加载速度提升一般从两个角度考虑:

  • 减少 js 文件大小
  • 提升 js 加载效率

对应到 js 分片方案,主要通过如下两点提升加载速度:

按需加载:在工程中存在多个页面时,不论打开哪个页面都需要加载完整的main.dart.js,而这里包含了很多不需要的页面代码。如果将各个页面的代码拆分只加载当前页面所需要的代码,则可减少 js 文件体积,而且当其他页面越多逻辑越复杂时,其提升的效果越明显。

并行加载:将 js 分片后会生成多个大小不一的 js 文件,在带宽充足的情况下如果使用并行加载则可以节省较小的分片加载时间。

注:js 文件压缩在线上部署的时候会自动处理,这里不做处理。

2. 工程实践

通过按需和并行加载提升加载速度,首先需要完成 js 的分片。分片和按需加载操作通常是绑定的,如在前端 Vue 开发中,可使用 webpack 的 code splitting 工具在定义好各类库的使用关系后实现文件分割和按需加载,类似的在 flutter 中则可使用 延迟加载组件 功能。

2.1 延迟加载组件

Flutter 为 App 设计的延迟组件加载功能同样适用于 FFW。在 dart 代码中通过关键字 deffered as 引入相关代码库并在使用时加载即可实现延迟加载功能。在官方的示例中可以通过如下的方式实现 box.dart 的延迟加载。

// box.dart
import 'package:flutter/material.dart';

/// 一个正常方式编写的 widget,后面会被延迟加载
class DeferredBox extends StatelessWidget {
  const DeferredBox({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 30,
      width: 30,
      color: Colors.blue,
    );
  }
}

在需要使用 box.dart 的地方通过 deferred as 关键字引入 box.dart

/// some_widget.dart
import 'package:flutter/material.dart';

/// 1. deferred as 引入
import 'box.dart' deferred as box;

class SomeWidget extends StatefulWidget {
  const SomeWidget({Key? key}) : super(key: key);

  @override
  State<SomeWidget> createState() => _SomeWidgetState();
}

之后调用延迟加载库的加载方法,加载完成后使用即可

/// some_widget.dart
class _SomeWidgetState extends State<SomeWidget> {
  late Future<void> _libraryFuture;

  @override
  void initState() {
    /// 2. 使用时加载延迟加载库
    _libraryFuture = box.loadLibrary();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<void>(
      future: _libraryFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasError) {return Text('Error: ${snapshot.error}');}
          /// 3. 延迟加载库加载完成后使用
          return box.DeferredBox();
        }
        return const CircularProgressIndicator();
      },
    );
  }
}

经过上述操作后,在 FFW 中编译后可生成类似如下的两个 js 文件:

├── [1.2M]  main.dart.js            /// FFW 引擎和主工程内容
├── [616B]  main.dart.js_1.part.js  /// 存放 box.dart 对应的内容

在多页面的工程中使用延迟组件加载即可完成多页面的分片,可进行接下来的改造工作。

2.2 延迟加载改造

在阿里卖家 FFW 工程中,为了尽可能的做到只加载必须内容,我们从路由跳转位置将各页面改造为延迟加载方式。

2.2.1 主工程代码

/// main.dart
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AliSupplier Headline',
      debugShowCheckedModeBanner: false,
      onGenerateRoute: RouteConfiguration.onGenerateRoute,
      onGenerateInitialRoutes: (settings) {
      return [RouteConfiguration.onGenerateRoute(RouteSettings(name: settings))];
    },
    );
  }
}

2.2.2 原路由代码

/// routes.dart
import 'package:alisupplier_content/business/distribution/page/sellerapp_page.dart';
import 'package:alisupplier_content/business/webmain/page/web_news_detail_page.dart';
import 'package:alisupplier_content/debug/page/debug_main_page.dart';

/// 路由和页面 builder 的 map
static Map<String, RouteWidgetBuilder?> builders = {
    '/debug': (context, params) {
      return DebugMainPage(title: 'Debug');
    },
    '/web_news_detail': (context, params) {
      return WebNewsDetailPage(
        courseCode: params?['courseCode'] ?? params?['c'] ?? '',
        sourceId: params?['sourceId'] ?? params?['s'] ?? '',
      );
    },
    '/sellerapp': (context, params) {
      return SellerAppPage(
        url: params?['url'] ?? '',
        sourceId: params?['sourceId'] ?? params?['s'] ?? '',
      );
    },
};
/// routes.dart
class RouteConfiguration {
  static Route<dynamic> onGenerateRoute(RouteSettings settings) {
    return NoAnimationMaterialPageRoute(
      settings: settings,
      builder: (context) {
        var uri = Uri.parse(settings.name ?? '');
        /// 根据 path 找页面的 builder
        var route = builders[uri.path];
        if (route != null) {
          return route(context, uri.queryParameters);
        } else {
          /// 404 页面
          return CommonPageNotFound(routeSettings: settings);
        }
      },
    );
  }
}

2.2.3 改造代码

创建 DeferredLoaderWidget 执行各页面加载操作

/// routes.dart
class RouteConfiguration {
  static Route<dynamic> onGenerateRoute(RouteSettings settings) {
    return NoAnimationMaterialPageRoute(
      settings: settings,
      builder: (context) {
        /// 承担路由和加载工作
        return DeferredLoaderWidget(
          settings: settings,
        );
      },
    );
  }
}

DeferredLoaderWidget 中将各页面通过 deferred as 方式引入

/// deferred_loader_widget.dart, 新添加的文件
import '../../business/distribution/page/sellerapp_page.dart' deferred as sellerapp;
import '../../business/webmain/page/web_news_detail_page.dart' deferred as web_news_detail;
import '../../debug/page/debug_main_page.dart' deferred as debug;
import '../../ability/common/page/common_page_not_found.dart' deferred as pageNotFound;
import 'package:flutter/material.dart';

typedef WidgetConstructer = Widget Function(Map? params);

/// 分包加载: library 加载 map
/// <页面地址,library加载方法>
var _loadLibraryMap = {
  '/sellerapp': sellerapp.loadLibrary,
  '/web_news_detail': web_news_detail.loadLibrary,
  '/debug': debug.loadLibrary,
};

/// 分包加载: 页面 widget 创建方法 map
/// <页面地址,widget 创建方法>
var _constructorMap = {
  '/sellerapp': () => sellerapp.widgetConstructor,
  '/web_news_detail': () => web_news_detail.widgetConstructor,
  '/debug': () => debug.widgetConstructor,
};

之后在需要的时候对页面进行加载,在 _DeferredLoaderWidgetState.initState 中执行加载操作:

/// deferred_loader_widget.dart
@override
void initState() {
  super.initState();

  /// 路由解析
  Uri uri = Uri.parse(widget.settings.name ?? '');
  path = uri.path;
  params = uri.queryParameters;

  /// 根据 path 找到 libraryLoad 方法
  Future Function()? loadLibrary = _loadLibraryMap[path];

  /// 未找到时使用 404 页面 loadLibrary
  if (loadLibrary == null) {
    loadLibrary = pageNotFound.loadLibrary;
    params = {'settings': widget.settings};
  }

  loadFuture = loadLibrary.call();
}

DeferredLoaderWidgetState.build 中进行 widget 的创建:

/// deferred_loader_widget.dart
@override
Widget build(BuildContext context) {
  return FutureBuilder(
    future: loadFuture,
    builder: (context, snapshot) {
      if (snapshot.connectionState == ConnectionState.done) {
        if (snapshot.hasError) {
          return Text('页面加载失败,请重试');
        }

        var constructor = _constructorMap[path];
        if (constructor == null) {
          /// 页面未找到
          constructor = () => pageNotFound.widgetConstructor;
        }

        return constructor().call(params);
      } else {
        return Container();
      }
    },
  );
}

其中对于每个页面在其头部定义构造统一的构造方法,以 sellerapp 为例:

/// sellerapp_page.dart

/// 页面构造方法
WidgetConstructer widgetConstructor = (params) {
  return SellerAppPage(
    url: params?['url'] ?? '',
    sourceId: params?['sourceId'] ?? params?['s'] ?? '',
  );
};

详情可见代码库:http://gitlab.alibaba-inc.com/algernon/alisupplier_content_web

在进行延迟加载改造时有两个需要注意的点:

  • 各页面构造方法封装一定要写到各页面的 dart 文件中,这样才能通过 deferred as 命名引用到
  • 各页面的 widgetConstructor 需要在相应的 library load 之后才能实际调用,在此之前引用的值会在使用时无效,如将 deferred_loader_widget_constructorMap 进行如下修改:

图 2. widgetConstructor 错误使用方式说明

则运行时会得到如下的报错信息

图 3. widgetConstructor 错误使用方式报错信息

2.2.4 分片效果

改造完成后即可进行编译调试,查看 js 分片和按需加载的效果。

产物对比

查看编译产物发现 main.dart.js 被拆分成了一个较小的 main.dart.js 和诸多小的 main.dart.js_xx.part.js

图 4. 分片前后编译产物对比

页面加载对比

在浏览器中查看页面 js 加载发现资讯页和下载页总的 js 大小均有减少,下载页因压缩问题传输 js 会比分包前稍大,但总大小有所减少,另外因为分包实现了部分的并行加载,总体耗时有所减少:

表1. 资讯页 js 加载情况对比

表2. 下载页 js 加载情况对比

在实验室环境经过多次测试后取平均时间,发现下载页耗时减少 15%,资讯页加载总加载耗时减少 9%。由于下载页 js 减少更多结果符合预期。

2.3 并行加载

经过延迟加载改造后,产物 js 分成了多个包,相关页面加载耗时也有所减少,但是在加载中发现一个问题,main.dart.js 和其他分片的 js 不是同时加载的:

图 5. 分片后 js 加载时序

main.dart.js_xx.part.js 是在 main.dart.js 加载完成之后过了相当一段时间才开始加载,这浪费了很多的加载时间,如果所有的分片 js 都在 main.dart.js 加载时同时加载,则加载耗时基本只会和 main.dart.js 加载耗时相同。

2.3.1 分片加载原理

为了让所有分片 js 同时加载,首先观察分片的加载过程。打开页面后检查页面发现情况如下,页面内被注入了分片 js 的加载代码:

图 6. FFW 自动注入的分片加载代码

main.dart.js 中查找相关分片的文件名,可发现如下内容:

图 7. 分片 main.dart.js 内的 js 加载信息

猜测 main.dart.js 内部包含的各页面所需 js 分片信息的相关字段含义如下:

  • deferredPartUris: 分片文件的列表
  • deferredLibraryParts: 每个组件所需分片在列表中的 index

考虑如果能将 main.dart.js 中注入分片的时间提前到 main.dart.js 加载时,则可实现理想的并行加载效果。由于 main.dart.js 还未加载相关注入的代码不可用,则只能在 index.html 中添加分片的加载代码。

2.3.2 并行加载实现

有了实现的思路,接下来就是进行操作和验证。我们使用构建脚本中解析延迟组件信息,并将解析处理后的信息写入 index.html 中的方案来实现 js 分片的并行加载。

首先在 index.html 中增加加载 js 分片的代码:

<!-- ffw 分包并行加载,根据页面 path 并行加载相关的 part.js,不用等到 ffw 执行时自己去加载 -->
<script id="flutterJsPatchLoad">
  // 使用脚本替换内容
  var deferredLibraryParts = {};
  // 使用脚本替换内容
  var deferredPartUris = [];
  // 使用脚本替换内容
  var base = "";
  
  // 根据页面路径加载所需 js 分片,为了方便要求 DeferredLoaderWidget 中 _loadLibraryMap key 的名称
  // 和延迟组件的名称相同
  var hash = window.location.hash.substring(2);
  var path = hash.split('?')[0];
  if (deferredLibraryParts[path]) {
    for (var index in deferredLibraryParts[path]) {
      loadScript(deferredPartUris[index])
    }
  }

  function loadScript(url) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = base + url;
    document.body.appendChild(script);
  }
</script>

之后在构建脚本中解析组件信息,并替换到 deferredLibraryPartsdeferredPartUris 中,同时在线上发布时将分片 js 的 base 路径替换为实际的 cdn 地址:

# 从 main.dart.js 中获取 js 分包信息,写入 index.html 中预加载部分的变量中
def write_js_patch_info():
    # 从 main.dart.js 获取两个参数:deferredLibraryParts、deferredPartUris
    # 这个阶段在本地编译时执行
    parts = reg_find_file_content('./build/web/main.dart.js', r'deferredLibraryParts:{(.*?)},')[0]
    uris = reg_find_file_content('./build/web/main.dart.js', r'deferredPartUris:\[(.*?)\],')[0]

    str_replace_file_content('./build/web/index.html', r'deferredLibraryParts = {}', r'deferredLibraryParts = {' + parts + r'}')
    str_replace_file_content('./build/web/index.html', r'deferredPartUris = []', r'deferredPartUris = [{}]'.format(uris))
# 修改 index.html 中的 base 为实际的cdn地址
def change_base(version, publish_env):
    str_replace_file_content('./build/web/index.html', r'base = ""', r'base = "{}"'.format(get_base(version, publish_env)))

构建过程中经过脚本的替换,index.html 内容更新如下:

<!-- ffw 分包并行加载,根据页面 path 并行加载相关的 part.js,不用等到 ffw 执行时自己去加载 -->
<script id="flutterJsPatchLoad">
  // 使用脚本替换内容
  var deferredLibraryParts = {sellerapp:[0,1,2,3],web_news_detail:[0,4,1,5,2,6],debug:[0,4,1,7,5,8],pageNotFound:[0,4,7,9]};
  // 使用脚本替换内容
  var deferredPartUris = ["main.dart.js_3.part.js","main.dart.js_9.part.js","main.dart.js_7.part.js","main.dart.js_6.part.js","main.dart.js_4.part.js","main.dart.js_11.part.js","main.dart.js_10.part.js","main.dart.js_2.part.js","main.dart.js_12.part.js","main.dart.js_1.part.js"];
  // 使用脚本替换内容
  var base = "https://g.alicdn.com/algernon/alisupplier_content_web/2.0.5/";

  // 根据页面路径加载所需 js 分片,为了方便要求 DeferredLoaderWidget 中 _loadLibraryMap key 的名称
  // 和延迟组件的名称相同
  var hash = window.location.hash.substring(2);
  var path = hash.split('?')[0];
  if (deferredLibraryParts[path]) {
    for (var index in deferredLibraryParts[path]) {
      loadScript(deferredPartUris[index])
    }
  }

  function loadScript(url) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = base + url;
    document.body.appendChild(script);
  }
</script>

构建部署完成后测试加载过程如下,发现各分片 js 加载完成时间接近,基本与 main.dart.js 加载完成时间相同:

图 8. 并行加载改造后的 js 加载时序

同时检查页面发现,FFW 没有再额外注入分片 js 的加载代码,至此分片 js 并行加载达到了理想的效果。

图 9. 并行加载改造后 FFW 不再注入分片加载代码

2.3.3 异常说明

在实际使用中发现 deferredLibraryParts 中包含的信息与实际所需分片可能不完全相同,如在 main.dart.js 中资讯页面的deferredLibraryParts加载信息为 0,4,1,5,2,6 6 个分片,但在实际打开页面的时候发现还会加载 index 为 7 的分片:

图 10. FFW 额外需加载的 js 分片

简单的解析 deferredLibraryParts 不够精确,要做到更精确还需深入分析 main.dart.js 代码,这里目前采用人工修正的方式处理。

2.3.4 并行效果

经过并行加载改造后,资讯页面总加载耗时进一步减少,加载耗时由 -9% 变为 -15%。下载页则提升不明显,考虑原因为下载页多图片资源占比稍大,IO资源在非并行的状态下已经得到了较为充分的使用。

3. 效果分析

由于当前阿里卖家 FFW 页面访问量不够大,同时线上性能数据为初次启动和非初次启动的混合数据不易区分,这里使用多次实验取平均数方式分析效果。

图 11. 资讯页下载页分片及并行改造结果对比

分析结论如下:

  • 资讯页:从分片到并行耗时分别减少 9% 和减少 15%,资讯页主要包括 js 加载和数据请求,受益于 domContentLoaded 时间减少数据请求可以更快进行,并行化处理后提速明显。
  • 下载页:从分片到并行耗时维持在减少 15% 左右,下载页主要受益于 js 按需加载,而包含多个图片带宽在非理想的并行情况下也得到了较为充分的使用,所以并行化处理效果不明显。

4. 未来展望

分片之后 main.dart.js 还有 1.3 MB 的体积,还有优化空间,另外延迟加载信息的解析还未做到完全精确。总体来说在加载提速上未来可做的事情还有:

  • FFW 引擎功能及代码精简,继续减少 main.dart.js 大小
  • 延迟加载信息精确分析,做到延迟加载信息的完全精确
  • 非当前页面分片预加载,提升多页面切换速度

FFW 在生产环境使用的条件已经成熟,在当前开发人员存量的情况,FFW 是端技术同学的一大利器。FFW 当前与前端体系的分离是影响其在前端推广使用的一大阻力,如果能做好 FFW 和现有前端体系的融合,相信会更加的繁荣。

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

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

相关文章

2023年有哪些半入耳蓝牙耳机?半入耳式蓝牙耳机排行榜

工作生活中最常用的真无线蓝牙耳机来说&#xff0c;各式各样、价格悬殊的产品&#xff0c;很多人不知道该如何选择&#xff0c;半入耳式的佩戴舒适度一直都是公认的好&#xff0c;下面小编分享几个性能表现、续航时间都非常优秀的半入耳式蓝牙耳机 TOP1:南卡小音舱蓝牙耳机 音…

Java IO流补充(字符流)

字符 那么在Java中的字符用char来表示&#xff0c;char存储字符。Java使用Unicode来表示字符。Unicode可以表示在所有人类语言中找到的所有字符。Java char是16位类型 字符的范围是 0 ~ 65536 ,没有负字符。字符可以是文字、字母数字、符号等等。 字符流 尽管Java中字节流的…

【Spring】——17、@Resource注解和@Inject注解?

&#x1f4eb;作者简介&#xff1a;zhz小白 公众号&#xff1a;小白的Java进阶之路 专业技能&#xff1a; 1、Java基础&#xff0c;并精通多线程的开发&#xff0c;熟悉JVM原理 2、熟悉Java基础&#xff0c;并精通多线程的开发&#xff0c;熟悉JVM原理&#xff0c;具备⼀定的线…

使用 systemd 管理多个 MySQL 服务器实例

使用 systemd 管理多个 MySQL 服务器实例 文章目录使用 systemd 管理多个 MySQL 服务器实例先决条件支持 systemd 的操作系统每个实例配置独立的目录和参数部署多实例环境1. 配置选项文件使用 systemd 管理 MySQL 多实例总结本文仅讲述使用 RPM 包安装的多个 MySQL 实例如何使用…

IronXL 2022.12.10926 Crack

关于适用于 .NET 的 IronXL 在 C# 中阅读和编辑 Excel 电子表格&#xff0c;无需 MS Office 或 Excel Interop。 IronXL for .NET 允许开发人员在 .NET 应用程序和网站中读取、生成和编辑 Excel&#xff08;和其他电子表格文件&#xff09;。您可以读取和编辑 XLS/XLSX/CSV/TSV…

操作系统-内存管理(内存的分配与回收(非连续分配方式,基本分页存储管理方式,基本地址变换机构))

文章目录1. 基本分页存储管理基本地址变换机构1. 基本分页存储管理 分页存储&#xff1a; 将内存空间分为一个个大小相等的分区&#xff08;eg&#xff1a;每个分区4KB&#xff09;&#xff0c;每个分区就是一个页框 每个页框有一个编号&#xff0c;即页框号&#xff0c;页框…

C++ 重要笔记与题 (循环-嵌套-数组)

文章目录十 循环10.1 字符与循环10.2 数与循环十一 循环嵌套——不拘一格11.2数学与循环——脉脉相通十 循环 10.1 字符与循环 题1.1&#xff1a;循环输出26个字母&#xff0c;从A-Z。 for(char i A;i<Z;i){cout<<i<<" ";}题1.2&#xff1a;输入某…

第二证券|定增市场回暖 机构争抢优质项目

今年下半年以来&#xff0c;定增商场持续回暖。最新数据显现&#xff0c;到12月13日&#xff0c;下半年以来A股商场实施定增计划的上市公司近200家&#xff0c;比较上半年的133家显着增加。持续回暖的定增商场吸引了不少组织的目光&#xff0c;不只百亿级私募活跃捡拾筹码&…

大学生个人博客网页设计模板 学生个人博客网页成品 简单个人网站作品下载 静态HTML CSS个人网页作业源代码

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

[附源码]Python计算机毕业设计SSM基于java网上心理咨询系统数据分析(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

docke+gitlab+jenkins+springboot

安装Gitlab 开放防火墙端口80和配置映射文件夹 firewall-cmd --zonepublic --add-port80/tcp --permanent firewall-cmd --reload mkdir -p /docker_data/gitlab/{data,logs,config}启动Gitlab容器&#xff08;启动容器之前确保80&#xff0c;443端口没用被占用&#xff0c;被…

通话类型转换流程之AudioCall到VideoCall

目录 概述时序图关键代码关键log总结 一、概述 这里的通话类型指的是语音通话和视频通话&#xff0c;转换包括upgrade/ downgrade升降级&#xff0c;就是语音通话升级为视频通话、视频通话降级为语音通话。升级为视频通话一般就是包括如下图示的4步&#xff0c;MO发起请求&…

Microsoft SharePoint Online 更新功能可能是下一次勒索攻击的目标

Microsoft SharePoint Online是被使用最广泛的内容管理平台之一。但令人担忧的是&#xff0c;最近几年我们发现大部分攻击者可以滥用 SharePoint Online 和 OneDrive for Business 中的某项功能来加密您的所有文件并以此来勒索赎金。 SharePoint Online 据观察发现可能存在潜在…

四旋翼无人机学习第15节--PCB Editor简单绘制封装-手动绘制封装

文章目录1 前言2 class与sub class3 手动绘制3.1 芯片手册分析3.2 手动绘制1 前言 上一篇博客我们学习了获取封装的几种途径&#xff0c;分别是下载&#xff0c;软件生成与软件转化。本次博客开始讲手动绘制封装。 2 class与sub class 参考博客&#xff1a;第11讲、Allegro …

前端高频手写面试题集锦

手写深度比较isEqual 思路&#xff1a;深度比较两个对象&#xff0c;就是要深度比较对象的每一个元素。> 递归 递归退出条件&#xff1a; 被比较的是两个值类型变量&#xff0c;直接用“”判断被比较的两个变量之一为null&#xff0c;直接判断另一个元素是否也为null 提前结…

对受控组件和非受控组件的理解,以及应用场景?

一、受控组件 受控组件&#xff0c;简单来讲&#xff0c;就是受我们控制的组件&#xff0c;组件的状态全程响应外部数据 举个简单的例子&#xff1a; class TestComponent extends React.Component {constructor (props) {super(props);this.state { username: lindaidai }…

从事生活垃圾(含粪便)经营性清扫、收集、运输服务许可证

《城市生活垃圾管理办法》&#xff08;2007年4月28日建设部令第157号公布2015年5月4日修正本&#xff09;第十七条从事城市生活垃圾经营性清扫、收集、运输的企业&#xff0c;应当取得城市生活垃圾经营性清扫、收集、运输服务许可证。 未取得城市生活垃圾经营性清扫、收集、运输…

安卓玩机搞机技巧综合资源-----查看手机硬件全部参数 隐藏参数 多个软件【十七】

接上篇 安卓玩机搞机技巧综合资源------如何提取手机分区 小米机型代码分享等等 【一】 安卓玩机搞机技巧综合资源------开机英文提示解决dm-verity corruption your device is corrupt. 设备内部报错 AB分区等等【二】 安卓玩机搞机技巧综合资源------EROFS分区格式 小米红…

绿色消费积分,共建开放生态,让消费变投资

随着市场的逐渐饱和&#xff0c;不断推出新产品、开拓推广渠道、增加客户量是商家想要实现可持续发展的生存之道。商家为了刺激消费&#xff0c;可以说是无所不用&#xff0c;但还是面临着缺少新用户&#xff0c;推广难&#xff0c;客户活跃度低&#xff0c;复购率低等痛点。 商…