跳到主要内容

前端首屏优化探索与方案:从指标到落地,大厂是怎么做到"秒开"的

· 阅读需 17 分钟

"首屏优化"是前端工程化里最常被问、也最难答好的话题——因为它不是一个技巧,而是一整套从指标定义 → 网络传输 → 资源加载 → 构建产物 → 渲染执行全链路的方法论。本文把这条链路完整走一遍:先讲清楚"快"到底指什么、慢在哪,再按层给出可落地的方案,最后用淘宝、美团、字节等大厂的真实实践收尾。


1. 为什么要优化首屏

首屏慢的代价是实打实的商业数字。美团技术团队在《前端黑科技:美团网页首帧优化实践》里引用过一组业界数据:

  • 57% 的用户更在乎网页在 3 秒内完成加载;
  • 52% 的在线用户认为网页打开速度影响其对网站的忠诚度;
  • 每慢 1 秒,页面 PV 降低 11%、用户满意度下降 16%
  • 半数移动用户因 10 秒内没打开页面而放弃。

所以"秒开"不是一个技术执念,而是直接关系收入指标(转化率、跳出率、留存)。这也是为什么大厂会把首屏性能做成性能预算(Performance Budget)——把它当"依赖"一样锁进 CI,超了就不让发版。

虚拟列表大厂方案调研:各场景的选型、量级判断与 fallback 策略

· 阅读需 14 分钟

一万条订单直接 map 渲染,浏览器会创建一万多个 DOM 节点——滚动掉帧、内存暴涨,甚至直接卡死。虚拟列表(Virtual List / Windowed Rendering)就是为了解决这个问题:不管数据有多少,始终只渲染视口附近的那几十个节点。但"上虚拟列表"只是第一步,真正难的是选型——不同场景(表格、feed、聊天、分组)该用哪个方案,不同数据量级该不该用、用什么级别的虚拟化,以及各种边界情况下的 fallback。

本文把主流方案和大厂实践完整调研一遍:原理 → 方案全景 → 场景选型 → 量级判断 → fallback。


1. 虚拟列表是什么:先看清问题的量级

不虚拟化的问题有多严重?一个直观对比:

直接渲染虚拟列表
10 万条数据10 万 + 个 DOM 节点约 50~80 个节点
首次渲染可能冻结 / 卡死毫秒级
滚动大量布局 + 绘制只重算视口窗口

为什么 DOM 多就慢:浏览器对每个节点都要维护数据结构、做布局计算、参与绘制。节点多 → 布局(Layout)慢、绘制(Paint)慢、内存占用高。50 万行直接渲染,浏览器基本会冻结甚至崩溃。

虚拟列表的核心思路三件事:

  1. 总高度占位:用一个"撑高"的容器模拟全部内容的高度,让滚动条长度正确;
  2. 只渲染窗口内:根据 scrollTop 算出当前视口应该显示哪些项(startIndex~endIndex),只渲染这些;
  3. 绝对定位 / translateY 偏移:把每一项按它在整条列表中的位置放好,视觉上无缝衔接。

overscan(缓冲区):视口外再多渲染几个,防止快速滚动时出现白屏——一般 5~10 个为宜。


2. 核心分水岭:定高 vs 不定高

这是选型的第一道分岔,也决定实现的复杂度和用哪个库。

维度定高(Fixed Height)不定高(Dynamic Height)
每项高度固定,index * height 直接算位置不一致,需要测量 / 预估
定位O(1)index * itemHeight需位置缓存:测量可见项 + 预估其余
实现成本低,几乎不踩坑高:高度缓存 + 预估修正 + ResizeObserver
滚动稳定性最稳测量偏差会导致滚动条跳动,需补偿
典型场景后台表格、结构规整的列表评论、feed 流、混合复杂布局

决策逻辑一句话:内容可控(表格、纯文本行)→ 定高;内容不可控(feed、评论、图文混排)→ 不定高。定高永远是最优解,不定高是业务逼出来的复杂实现。

不定高是怎么做的:只测量渲染过的,其余靠预估

所有主流实现用的都是同一套策略(react-window 的 VariableSizeList、TanStack 的 measureElement、react-virtuoso 内置的都是):

  1. 给每项一个预估高度 estimateSize(通常 100px 起步,其实不精确也没关系);
  2. 只在视口内实际渲染的项,用 ResizeObserver / getBoundingClientRect 测量真实高度,存进缓存;
  3. 位置由"已测量的精确值 + 未测量的预估值"累加得到——随着滚动,越来越多的项被测量,预估会被真实值逐步替换,位置越来越准;
  4. 当某项尺寸变化(图片加载、行展开)导致视口上方的项高度改变时,做滚动补偿scrollTop += 变化量,让用户看到的视觉位置不跳动。
// 量级化的实现思路:位置 = 已测量项的精确高度 + 未测量项的预估高度
function getItemOffset(index) {
let offset = 0;
for (let i = 0; i < index; i++) {
offset += measuredHeights.get(i) ?? estimateSize(i); // 测过的用真实值,没测的用预估
}
return offset;
}

这就是"只测量渲染过的,其余全靠预估,用多少测多少"的 fallback 思想——它在精度和性能之间取平衡,也是后面"各种量级 fallback"的雏形。


3. 主流方案全景(大厂 / 开源)

方案定位体积定高/不定高擅长
react-window轻量基础组件~4-7KB定高为主(不定高要手写测量)定高列表/表格,性能极致
react-virtuoso开箱即用全能方案~30KB+动态高度内置(ResizeObserver 自动测量)聊天/feed/动态内容
@tanstack/react-virtualheadless 底层 hook~15-18KB定高/不定高都支持,测量可控企业表格、跨框架、自定义渲染
rc-virtual-listAnt Design 内部组件~148KB定高为主antd 系表格/列表
ahooks useVirtualListReact Hook轻量支持动态高度蚂蚁生态快速接入
vue-virtual-scroller / Element PlusVue 生态支持动态高度Vue 项目

3.1 react-window:定高场景的性能标杆

  • 优点:包体积最小、纯组件性能最好、避免布局抖动(layout thrashing),是 2019-2022 年的行业标准,存量项目极多;
  • 缺点动态高度需要自己维护测量逻辑VariableSizeList 只给机制不给自动测量);不支持分组、sticky header、反向滚动、嵌套滚动;官方不建议新项目再用,基本停止新功能开发;
  • 适用:数据看板、定高表格这类"高度可预测 + 极致性能"的场景。

3.2 react-virtuoso:动态场景的开箱即用之王

  • 优点:动态高度全自动(ResizeObserver 测量 + 高度缓存,零手动维护);内置 TableVirtuoso(表格)、VirtuosoGrid(网格/瀑布流)、GroupedVirtuoso(分组 + sticky 组头);反向滚动 + followOutput(聊天消息流跟随最新消息);TypeScript 完整;
  • 生产案例Rocket.Chat 的消息列表就是用它——同时用"双向加载 + 动态高度 + 日期分隔符",是业界最严苛的虚拟化场景;
  • 适用:聊天、feed、评论这类"动态高度 + 内容不可控"的场景,中小团队起步首选。

3.3 @tanstack/react-virtual:headless 的底层基础设施

  • 优点框架无关(React/Vue/Solid 都行)、不绑定 DOM(可接 Canvas/WebGL)、TypeScript-first;底层用 Float64Array 连续数组做位置缓存(O(1) 访问、少 GC),能扛几十万条;与 @tanstack/react-table 组合是企业级数据表格的推荐模式;
  • 缺点:headless,要自己处理 ref、resize、scroll 同步,不是开箱即用;
  • 适用:企业数据表格、内部工具平台、需要自定义渲染管线的复杂布局。

3.4 蚂蚁系:rc-virtual-list + ahooks useVirtualList

  • rc-virtual-list:Ant Design(antd Table、Select 的虚拟滚动)底层的虚拟列表组件,周下载百万级;
  • ahooks useVirtualList:一行 hook 接入,支持动态高度,适合蚂蚁生态 / 快速原型。

4. 各场景的大厂选型

场景 1:后台数据表格(大量定高行)

固定列 + 海量行、每行高度基本一致 → 定高虚拟化。React 侧用 react-window@tanstack/react-virtual + @tanstack/react-table;antd 系直接用 rc-virtual-list 或 Table 的虚拟滚动。

真实案例(企业后台 10 万行 × 20 列表格):初始渲染 12s → 0.5s,CPU 80% → 15%

场景 2:feed / 评论流(动态高度)

图文混排、高度不可控 → 动态高度虚拟化,选 react-virtuoso(自动测量)或 @tanstack/react-virtual(自己接测量)。

真实案例(电商商品列表):首屏 3.2s → 0.8s,内存 400MB → 100MB,帧率 20 → 60 FPS

场景 3:聊天消息流(反向 + 动态 + 跟随)

这是最苛刻的场景:消息从底部往上排(反向滚动)、每条高度不同、还要跟随最新消息。react-virtuoso 是事实标准(Rocket.Chat 生产案例),followOutput 平滑滚动到最新。

场景 4:分组列表 / 树形(sticky 组头)

分组 + 吸顶组头 → react-virtuosoGroupedVirtuoso;树形展开/收起会改高度,属于"动态高度 + 增量测量"的复合场景。

场景 5:网格 / 瀑布流

VirtuosoGrid(多列)+ 每格动态高度。

场景 6:虚拟化 + 分页 / 无限滚动组合

服务端分页管"数据边界",客户端虚拟列表管"渲染边界"。注意一个坑:接口返回 total_count 和实际数据可能不一致,会导致滚动条过长/过短——用"已知高度 + 估算未加载部分"来对齐滚动条。

场景 7:Hybrid H5(移动端)

真实案例(Hybrid H5 饰品列表 1000+ 行):迁移前 DOM 节点 >20000、滚动 30fps 以下;虚拟化后显著改善。移动端选型优先 react-window(轻量)或动态场景的 react-virtuoso


5. 各种量级的 fallback:什么时候该虚拟化,用什么级别

"上不上虚拟列表"和"上哪种虚拟化",本质取决于数据量级。这一步做对,能省掉大量无谓的复杂度。

5.1 量级判断表

数据量级策略说明
< 1000不虚拟化,直接全渲染虚拟列表本身有实现复杂度 + 测量开销,这个量级收益趋近于零,别为了"显得专业"而上
1000 ~ 5000优先分页 / 无限滚动;或简单定高虚拟开始有卡顿风险(经验阈值),但还没到非虚拟不可
5000 ~ 10 万正式虚拟列表定高优先;动态高度必须上"测量 + 预估 + 补偿"
10 万 ~ 100 万高性能虚拟化二分定位(O(log n))、位置缓存、增量重算,避免 O(n) 扫描;注意浏览器像素高度上限
100 万 +特殊架构单容器几乎到极限,考虑数据聚合/分批、服务端、Canvas/WebGL 渲染

5.2 量级对应的实现强度

5k~10 万(普通虚拟化):线性累加位置可接受,overscan 5~10,定高走 O(1) 公式。

10 万~100 万(高性能虚拟化):这一步要跨过三道坎——

  1. 定位别用线性扫描:从 scrollTop 找起始项,用二分查找 O(log n),否则每次滚动都 O(n);
  2. 位置数据别用对象数组:TanStack 用连续 Float64Array 存 start/size,避免几十万个对象的内存与 GC 压力;
  3. 测量别全量做:只测视口内的项,其余用预估;增量地重算受影响位置,别每次滚动全量重算。

浏览器硬边界:DOM 元素/滚动容器高度有物理上限(Chrome 约 2^24 ≈ 1600 万 px,实用上滚动到百万像素级已是极限)。这意味着"百万条 × 每条 20px = 2000 万 px"这种组合,单靠一个滚动容器是放不下的——必须拆(分批加载、按需扩展高度)或换渲染载体。

5.3 fallback 的几种形态

"fallback"在这类系统里至少指三件事:

① 数据量级的降级(上面那张表):数据小到不值得虚拟化 → 直接全渲染。虚拟化不是免费的,它有测量开销、高度缓存维护、定位计算——数据只有几百条时,直接渲染更快更简单。

② 测量机制的降级

异常 / 边界fallback
某项还没被渲染 / 没测到estimateSize 预估值(测量后自动替换)
图片异步加载导致高度变化ResizeObserver 监听 + 增量重算受影响区间
视口上方高度变化导致跳动滚动补偿scrollTop += 高度差值 保持视觉稳定
不支持 ResizeObserver(老环境)降级为滚动事件 + getBoundingClientRect 采样
快速滚动出现白屏加大 overscan 缓冲区(5~10 个)

③ 需求不满足的降级:定高方案在遇到动态内容时,如果不想引入大库,可以退化为"预估高度 + 出现内容后再补测修正"(react-window VariableSizeList 就是让你自己接这套);极端情况(无法测量、必须逐条真实高度)甚至放弃虚拟化、退化为分批渲染 + 懒加载


6. 工程实践:性能归因与常见坑

6.1 先做性能归因,别盲目上虚拟列表

虚拟列表只解决"渲染性能(DOM 规模)"问题,不解决数据获取、状态管理问题。先用 Chrome Performance 归因:

  • DOM 数量过多(经验阈值:<1000 通常没问题;1000~5000 有卡顿风险;>5000 基本要虚拟化)→ 虚拟列表是核心解法;
  • Layout 时间长(不定高元素、图片、复杂 CSS)→ 重点是控制高度、降低布局复杂度,虚拟列表帮不上;
  • Scripting 时间长(频繁 render/diff)→ 应做 memo、分层渲染、状态拆分。

6.2 五个高频坑

  1. key={index}:滚动后状态漂移(第 100 行展开,滚动后变第 98 行)——用 item 的业务 id 做 key
  2. 状态存在 item 组件内部:虚拟列表的项会反复卸载/重建,内部 state 会丢——状态提到父级或全局 store,用 Map<itemId, state> 缓存;
  3. overscan 太小:快速滚动出现白屏——缓冲区放到 5~10 个
  4. 滚动回调里做复杂计算:应使用 requestAnimationFrame 或防抖,别阻塞主线程;
  5. 服务端分页 + 虚拟列表混用total_count 与实际数据不一致会导致滚动条失真——用"已知部分精确高度 + 未加载部分估算高度"对齐。

6.3 一个性能对照

场景优化前优化后
10 万行 × 20 列表格初始渲染 12s,CPU 80%0.5s,CPU 15%
电商商品列表首屏 3.2s,内存 400MB,20 FPS0.8s,内存 100MB,60 FPS
Hybrid H5 饰品列表DOM >20000,滚动 <30fps显著改善

7. 一句话总结

虚拟列表的本质是"总高度占位 + 只渲染视口窗口 + 定位偏移",它只解决 DOM 规模问题,不背数据获取和状态的锅。选型的第一分水岭是定高 vs 不定高:内容可控走定高(react-window / TanStack + Table,O(1) 定位、最稳),内容不可控走动态高度(react-virtuoso 自动测量,或 TanStack 自接测量),聊天/feed/分组等特殊场景用对应内置能力。而上不上虚拟、上到什么强度,看数据量级:千条以内直接渲染、万级上正式虚拟、十万到百万上二分 + 位置缓存 + 增量重算、百万以上得换架构。各种 fallback(预估高度、滚动补偿、overscan 加大、ResizeObserver 降级)本质都是"在精度、性能、稳定性之间取平衡"——先归因性能,再决定手段,永远别为了"用上虚拟列表"而虚拟化。


参考

webassembly及兼容性分析

· 阅读需 11 分钟

相关工具安装

WABT

WABT(WebAssembly Binary Toolkit)是一个开源的WebAssembly编译器和工具集,可以将WebAssembly二进制文件转换为文本格式,反汇编,分析,优化,以及生成二进制文件。

  • wat2wasm:将WebAssembly文本格式转换为二进制格式
  • wasm2wat:将WebAssembly二进制格式转换为文本格式
  • wasm-objdump:主要功能包括反汇编、查看文件结构、导入和导出分析等

安装

sudo apt-get install -y cmake make curl git
git clone https://github.com/WebAssembly/wabt
cd wabt
mkdir build
cd build
cmake ..
make
sudo make install

或者ubuntu可以直接使用apt安装

sudo apt-get install -y wabt

兼容性分析的处理办法和思路

WebAssembly是一种新兴的技术,兼容性分析是WebAssembly的重要组成部分。WebAssembly兼容性分析的处理办法和思路如下:

概要

首先,要确定浏览器是否支持WebAssembly。目前,主流浏览器都支持WebAssembly;其次,要确定WebAssembly的运行环境。WebAssembly运行环境包括运行时环境和编译环境。运行时环境包括浏览器、Node.js、Deno等,编译环境包括Emscripten、Binaryen、WABT等;第三,要确定WebAssembly的编译工具链。WebAssembly编译工具链包括编译器、链接器、调试器等;最后,要确定WebAssembly的兼容性问题。

赖于主流浏览器都支持WebAssembly、编辑环境支持,WebAssembly的语法兼容性、语义兼容性、性能兼容性、体积兼容性问题基本上不存在。

具体处理办法

由于WebAssembly及js胶水语言大多是第三方厂商开发,不同浏览器厂商对WebAssembly的支持情况各不相同,因此,兼容性分析的处理办法和思路如下:

  1. 浏览器兼容性测试:
  • 浏览器:Chrome、Firefox、Edge、Safari、Opera、Brave、QQ浏览器、UC浏览器、360浏览器、搜狗浏览器
  • 运行时环境:浏览器
  • 编译环境:Emscripten、Binaryen、WABT
  • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
  • 性能:WebAssembly的运行性能测试
  • 体积:WebAssembly的体积大小测试
  • 兼容性:兼容性测试结果汇总
  1. 对于兼容性问题的处理,可以采用以下策略:

    • polyfill:如果浏览器不支持WebAssembly,可以使用polyfill。polyfill是指模拟浏览器的WebAssembly运行环境,使得WebAssembly代码可以在浏览器中运行。

    • fallback:如果浏览器支持WebAssembly,但运行时环境不支持,可以使用fallback。fallback是指在运行时环境不支持WebAssembly时,使用备用方案,比如asm.js。

    • 降级处理:如果WebAssembly的兼容性问题影响到业务,可以考虑降级处理,例如,使用兼容性更好的JavaScript或asm.js。

    • 策略一:降级处理。如果WebAssembly的兼容性问题影响到业务,可以考虑降级处理,例如,使用兼容性更好的JavaScript或asm.js。

    • 策略二:提前通知。如果WebAssembly的兼容性问题影响到业务,可以提前通知业务,并提供降级处理的方案。

    • 策略三:兼容性测试。如果WebAssembly的兼容性问题影响到业务,可以进行兼容性测试,并及时修复。

    • 策略四:持续跟进。如果WebAssembly的兼容性问题影响到业务,可以持续跟进,并及时更新。

  2. 对于兼容性报告,可以采用以下格式:

    • 兼容性报告标题:WebAssembly兼容性分析报告

    • 兼容性报告日期:2021年10月1日

    • 兼容性报告版本:v1.0

    • 兼容性报告作者:sumshare

    • 兼容性报告目的:汇总测试结果,并提供详细的兼容性分析报告。

    • 兼容性报告内容:

      • 浏览器兼容性测试:

        • 浏览器:Chrome、Firefox、Edge、Safari、Opera、Brave、QQ浏览器、UC浏览器、360浏览器、搜狗浏览器
        • 运行时环境:浏览器
        • 编译环境:Emscripten、Binaryen、WABT
        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 运行时兼容性测试:

        • 运行时环境:Node.js、Deno
        • 编译环境:Emscripten、Binaryen、WABT
        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 编译环境兼容性测试:

        • 编译环境:Emscripten、Binaryen、WABT
        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的编译性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 功能兼容性测试:

        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 性能兼容性测试:

        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 体积兼容性测试:

        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 兼容性矩阵测试:

        • 浏览器:Chrome、Firefox、Edge、Safari、Opera、Brave、QQ浏览器、UC浏览器、360浏览器、搜狗浏览器
        • 运行时环境:Node.js、Deno
        • 编译环境:Emscripten、Binaryen、WABT
        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
    • 兼容性报告结论:

      • 浏览器兼容性测试:

        • 浏览器:Chrome、Firefox、Edge、Safari、Opera、Brave、QQ浏览器、UC浏览器、360浏览器、搜狗浏览器
        • 运行时环境:浏览器
        • 编译环境:Emscripten、Binaryen、WABT
        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 运行时兼容性测试:

        • 运行时环境:Node.js、Deno
        • 编译环境:Emscripten、Binaryen、WABT
        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 编译环境兼容性测试:

        • 编译环境:Emscripten、Binaryen、WABT
        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的编译性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 功能兼容性测试:

        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 性能兼容性测试:

        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 体积兼容性测试:

        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总
      • 兼容性矩阵测试:

        • 浏览器:Chrome、Firefox、Edge、Safari、Opera、Brave、QQ浏览器、UC浏览器、360浏览器、搜狗浏览器
        • 运行时环境:Node.js、Deno
        • 编译环境:Emscripten、Binaryen、WABT
        • 功能:WebAssembly的语法、语义、性能、体积兼容性测试
        • 性能:WebAssembly的运行性能测试
        • 体积:WebAssembly的体积大小测试
        • 兼容性:兼容性测试结果汇总

其一、浏览器兼容性测试

测试结果可归档,并提供详细的兼容性分析报告。比如某些特性的关键版本

WebAssembly Feature Test 是一个库,用来检测浏览器或环境是否支持特定的 WebAssembly 特性。你可以在 JavaScript 中使用这个库来测试环境对不同 WebAssembly 特性的支持情况。

WebAssembly Feature Test:可以通过这个库检测Wasm模块是否使用了特定的Wasm特性,比如多线程、SIMD等。

其二、分析具体WASM文件

Webssembly2.0是2019年发布的,目前各大浏览器厂商都已经支持,但由于各个浏览器厂商对WebAssembly的支持情况各不相同,因此,需要重点分析Wasm文件是否采用2.0的新特性,以及是否兼容。如果你使用了 WebAssembly 2.0 的新特性(如 SIMD、多线程、异常处理、Reference Types 等),这些特性在仅支持 WebAssembly 1.0 的运行环境中将无法运行。WebAssembly 1.0 环境不会识别这些新指令或特性,可能导致模块加载失败或运行时错误。

要分析一个WebAssembly(Wasm)文件中使用了哪些Wasm特性以及它的兼容性,你可以采取以下几种方法:

  1. 使用wasm-objdump wasm-objdump 是一个WebAssembly工具,可以显示Wasm文件的内容。你可以通过以下命令查看模块的功能:
wasm-objdump -x <filename>.wasm

这将显示模块中的各个部分,包括导入、导出、类型、表、内存、全局变量等信息。

  1. 使用wasm2wat wasm2wat 可以将Wasm字节码转换为WebAssembly文本格式(Wat),从而更容易分析模块中使用的特性。命令如下:
wasm2wat <filename>.wasm -o <output>.wat

你可以查看生成的.wat文件,了解里面使用的指令和功能特性。

其三、模块兼容性

即使理论性测试不通过,你也应该实际测试模块的兼容性。模块设计者可能会在设计时就考虑了回退机制,因此,需要实际是否可以正常运行,并且兼容性是否达到要求。

以下是使用 WebAssembly Feature Test 的基本步骤:

  1. 安装库 如果你在 Node.js 环境中使用,可以通过 npm 安装:
npm install wasm-feature-detect

如果你在浏览器中使用,可以直接通过 CDN 加载:

<script src="https://unpkg.com/wasm-feature-detect"></script>
  1. 使用库检测特性 安装或加载库后,你可以通过以下方式检测各种 WebAssembly 特性是否在当前环境中得到支持:
const wasmFeatureDetect = require('wasm-feature-detect');

// 检测 WebAssembly 的支持
wasmFeatureDetect.simd().then(supported => {
console.log("SIMD supported:", supported);
});

wasmFeatureDetect.threads().then(supported => {
console.log("Threads supported:", supported);
});

wasmFeatureDetect.bulkMemory().then(supported => {
console.log("Bulk memory supported:", supported);
});

wasmFeatureDetect.multiValue().then(supported => {
console.log("Multi-value returns supported:", supported);
});

// 更多特性检测... 如果你在浏览器中使用了 CDN,可以直接访问 wasmFeatureDetect 作为全局变量:

wasmFeatureDetect.simd().then(supported => {
console.log("SIMD supported:", supported);
});
  1. 常见的特性检测 WebAssembly Feature Test 提供了多种常见的 WebAssembly 特性检测,包括但不限于以下内容:

simd(): 检测 SIMD (Single Instruction, Multiple Data) 支持。 threads(): 检测多线程支持。 bulkMemory(): 检测 Bulk Memory Operations 支持。 multiValue(): 检测多值返回支持。 referenceTypes(): 检测 Reference Types 支持。 exceptions(): 检测 Exception Handling 支持。 memory64(): 检测 64 位内存支持。

  1. 处理检测结果 这些检测返回的是一个 Promise 对象,所以你可以根据结果执行不同的操作。例如:
wasmFeatureDetect.simd().then(supported => {
if (supported) {
console.log("This environment supports SIMD.");
} else {
console.log("SIMD is not supported in this environment.");
}
});
  1. 进一步使用 你可以根据检测的结果决定是否加载某些 WebAssembly 模块,或者选择回退到其他解决方案。

示例代码 下面是一个完整的示例,检测几个常见的 WebAssembly 特性:

(async function() {
const supportsSIMD = await wasmFeatureDetect.simd();
console.log("SIMD support:", supportsSIMD);

const supportsThreads = await wasmFeatureDetect.threads();
console.log("Threads support:", supportsThreads);

const supportsBulkMemory = await wasmFeatureDetect.bulkMemory();
console.log("Bulk Memory support:", supportsBulkMemory);
})();

这样,你可以检测当前环境对不同 WebAssembly 特性的支持情况,并根据结果采取不同的策略。

https://github.com/evanw/polywasm

降级处理

在实际生成过程中,我们不能指望第三方库的作者会提供完美的兼容性。因此,我们需要考虑降级处理。

针对flutter web的完整处理方案

首先检查默认项目的兼容性

将初始化项目构建为web,然后运行项目,查看是否正常运行。

  • 检查不同渲染引擎的兼容性

分析示例

常见兼容性问题分析

  • ios14下,canvas无法正常渲染

参考资料

快速吃透 MySQL 索引:B+Tree、聚簇索引、最左前缀、覆盖索引、索引失效

· 阅读需 25 分钟

面试问 MySQL,十有八九会落到索引上。但很多人对索引的理解是碎片化的:知道"建索引能提速""最左前缀""不要前模糊 LIKE",却说不清索引为什么快、B+Tree 好在哪里、二级索引为什么要回表、EXPLAIN 里的 Using index / Using index condition 到底差在哪

这篇文章把 MySQL 索引从头到尾串一遍:从数据结构出发,到物理存储,再到联合索引与优化器行为。看完能自己判断"这个 SQL 该建什么索引、会不会命中"。


1. 索引是什么:从"全表扫描"说起

在没有索引的 MySQL 表上查一行数据,InnoDB 只能从第一个数据页开始,把整张表的所有页依次读进内存,逐行比对条件。这就是全表扫描(full scan)——复杂度 O(n),行数一多就慢。

索引做的事情,本质上和书的目录、字典的拼音检字表一样:额外维护一份"有序的、缩小范围的查找路径",用少量 IO 定位到目标,而不是翻遍全书

索引能提速的本质是把"线性查找"变成"树形查找":读多少次磁盘,取决于树的高度,而不是表的行数。所以下文的一切,都围绕"把树做矮、把路径做窄"展开。

利用Stream实现Flutter的组件解耦

· 阅读需 5 分钟

Stream 是什么?

本质上,Stream 是一个异步数据队列,具有先进先出(First In First Out,FIFO)的特性。借助它,组件之间可以彻底解耦——数据生产者只管往流里推数据,消费者只关心如何订阅,两者互不感知,数据流动因此更加灵活、可控。

Stream 和 Future 的区别

简单说:Future 只返回一次结果,Stream 可以连续返回多个结果。

Stream 的分类

Stream 分为两种:

  • 单订阅流(single-subscription):只能有一个订阅者,也就是只能有一个消费者。
  • 多订阅流(broadcast):可以有多个订阅者,也就是可以有多个消费者。用 StreamController.broadcast() 创建,或对已有的流调用 .asBroadcastStream() 转换。

Stream 的使用

单订阅流的构造

import 'dart:async';

void main() {
createSingleStream();
}

Future<void> createSingleStream() async {
// 1) Stream.periodic:每隔一秒产生一个递增整数。
// 它是无限流,永远不会自己结束,这里用 take(3) 截取前三个。
final Stream<int> periodic = Stream<int>.periodic(
const Duration(seconds: 1),
(i) => i,
).take(3);

// 2) Stream.fromFuture:把一个 Future 包装成 Stream
final Future<int> future = Future.delayed(const Duration(seconds: 1), () => 1);
final Stream<int> fromFuture = Stream<int>.fromFuture(future);

// 3) Stream.fromFutures:把多个 Future 的结果依次推入同一个 Stream
final Future<String> hello = Future.delayed(const Duration(seconds: 1), () => 'Hello');
final Future<String> world = Future.delayed(const Duration(seconds: 2), () => 'World');
final Stream<String> fromFutures = Stream.fromFutures([hello, world]);

// 4) Stream.fromIterable + Stream.merge:把两个有限流合并成一条
final Stream<int> left = Stream.fromIterable([1, 2, 3]);
final Stream<int> right = Stream.fromIterable([4, 5, 6]);
final Stream<int> merged = Stream.merge([left, right]);

// await for 按到达顺序逐个取出合并后的数据并打印
await for (final int i in merged) {
print(i);
}
}
为什么使用 await for?

await for 是 Dart 2.0 引入的异步迭代语法:它从 Stream 里逐个取数据,取到一次就执行一次循环体;没有数据时就挂起等待,数据到达才继续。它把「订阅 → 收数据 → 处理 → 完成」的整个过程,写成一段像普通 for 循环一样线性的代码。

把它和基于回调的 listen() 对比更好理解:

  • stream.listen(onData):数据到达时触发回调,回调里再套回调,逻辑一多就不太好读。
  • await for:把同样的逻辑展开成顺序代码,读起来、改起来都更直观。底层依旧是非阻塞的,等待数据时不会卡住 UI 线程。

一个容易踩的坑:Stream 上的 error 事件不会被 await for 吞掉,而是作为异常重新抛出。需要处理错误时,用 try/catch 包住循环体捕获即可。

Stream 的基本使用流程

一个 Stream 的完整生命周期分三步:

  1. 创建 Stream:用 StreamController() 创建一个可控制的流,或用 Stream.periodic()Stream.fromIterable() 等从已有数据直接构造。
  2. 订阅 Stream:用 Stream.listen() 监听数据;在 Flutter 里则常把流交给 StreamBuilder,让它在数据到达时自动重建 UI。
  3. 发布数据:通过 StreamController.sink.add(data) 往流里推数据,所有订阅者都会收到并作出响应。

综合示例:CounterBloc + StreamBuilder

// 数据源:对外暴露只读 stream,通过 sink 发布数据
class CounterBloc {
final _controller = StreamController<int>();

Stream<int> get stream => _controller.stream;

void increment() => _controller.sink.add(1);

void dispose() => _controller.close();
}
// 页面:用 StreamBuilder 订阅流,数据到达时自动重建
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = CounterBloc();

return Scaffold(
appBar: AppBar(title: const Text('Counter Page')),
body: StreamBuilder<int>(
stream: bloc.stream,
builder: (context, snapshot) {
return Text(
snapshot.hasData ? '${snapshot.data}' : 'Waiting for data...',
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: bloc.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

这个例子把前面的三步串了起来:CounterBloc 内部用一个私有的 StreamController<int> 作为数据源,对外暴露只读的 stream getter;increment() 通过 sink.add(1) 发布数据。CounterPageStreamBuilder 订阅 bloc.stream——每当数据到达,builder 就会拿最新的 snapshot 重新构建 UI,计数立刻刷新;右下角的按钮点击后触发 increment(),完成一次「发布 → 订阅 → 更新」的闭环。

简化起见,示例把 CounterBloc 直接建在了 build() 里;真实项目中应由外层管理它的生命周期(例如放进 StatefulWidget 或依赖注入框架),并在页面销毁时调用 dispose() 关闭 StreamController,避免资源泄漏。

总结

Stream 是 Flutter 中非常重要的异步编程工具:生产者只管发布数据,消费者只关心订阅,两者互不感知,组件因此实现了解耦。利用这一特性,可以让代码更模块化、状态流转更可控——消息通知、收藏列表、表单校验这类「一处产生、多处响应」的场景,都是 Stream 的用武之地。

Flutter Web Service Worker

· 阅读需 3 分钟

Flutter Web Service Worker

在官方文档中,Flutter Web Service Worker的介绍如下:

Service workers are a new feature in web browsers that enable developers to create reliable and fast web applications. Service workers can intercept and handle network requests, cache responses, and manage push notifications. Service workers can also be used to create progressive web applications (PWA) that work offline and provide a seamless user experience.

Flutter Web supports Service workers, which can be used to cache data and provide a seamless user experience. This can help improve the performance of your web application and provide a better user experience.

To use Service workers in your Flutter Web application, you need to follow these steps:

  1. Enable Service worker support in your Flutter Web application.
  2. Define a Service worker file.
  3. Register the Service worker file in your index.html file.
  4. Use the Service worker to cache data and provide a seamless user experience.

Enable Service worker support in your Flutter Web application

To enable Service worker support in your Flutter Web application, you need to add the following code to your pubspec.yaml file:

flutter:
assets:
- assets/

Define a Service worker file

To define a Service worker file, you need to create a new file named service_worker.dart in the lib folder of your Flutter Web application.

Here's an example of a Service worker file:

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

void main() {
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
'flutter_web_worker',
(int viewId) => new ServiceWorkerController(),
);
}

class ServiceWorkerController {
ServiceWorkerController() {
print('Service worker initialized.');
}

Future<void> fetchHandler(FetchEvent event) async {
// Handle fetch events
}

Future<void> messageHandler(MessageEvent event) async {
// Handle message events
}

Future<void> pushHandler(PushEvent event) async {
// Handle push events
}

Future<void> syncHandler(SyncEvent event) async {
// Handle sync events
}
}

In this example, we define a ServiceWorkerController class that handles different types of events. For example, the fetchHandler method is called when a fetch event is triggered, and it can be used to handle network requests.

Register the Service worker file in your index.html file

To register the Service worker file in your index.html file, you need to add the following code to the <head> section of your index.html file:

<script type="application/javascript" src="service_worker.dart.js"></script>

Make sure to replace service_worker.dart.js with the actual name of your Service worker file.

Use the Service worker to cache data and provide a seamless user experience

To use the Service worker to cache data and provide a seamless user experience, you need to call the appropriate methods in your code.

For example, to cache data, you can use the cache.addAll method:

Future<void> cacheData() async {
final cache = await caches.open('my-cache');
await cache.addAll([
'https://example.com/data.json',
'https://example.com/images/logo.png',
]);
}

To provide a seamless user experience, you can use the skipWaiting method:

Future<void> updateUserExperience() async {
final controller = ServiceWorkerController();
await controller.skipWaiting();
}

In this example, we call the skipWaiting method on the ServiceWorkerController class to activate the new version of the Service worker. This ensures that the new version of the Service worker takes over and handles all future requests.

在二级域名下,service worker失效的问题

dart与js的互操作(一)

· 阅读需 1 分钟

https://dart.dev/interop/js-interop/past-js-interop


@js.JSExport()
class _MyAppState extends State<MyApp> {
final _streamController = StreamController<void>.broadcast();
DemoScreen _currentDemoScreen = DemoScreen.counter;
int _counterScreenCount = 0;

@override
void initState() {
super.initState();
final export = js.createJSInteropWrapper(this);
js.globalContext['_appState'] = export;
js.globalContext.callMethod('_stateSet'.toJS); // 调用js方法,将Dart实例化
}

Dart 平台适配

· 阅读需 8 分钟

本文主要探讨Dart平台相关的适配问题

  • 分平台导入与导出
  • 多平台适配

如何使用条件导入和导出来实现支持多个平台

通过查看Conditionally importing and exporting library files基本写法如下:

export 'src/hw_none.dart' // Stub implementation
if (dart.library.io) 'src/hw_io.dart' // dart:io implementation
if (dart.library.js_interop) 'src/hw_web.dart'; // package:web implementation
  • 在可以使用 dart:io 的应用程序(例如命令行应用程序)中,导出 src/hw_io.dart
  • 在可以使用 dart:js_interop (Web 应用程序)的应用程序中,导出 src/hw_web.dart
  • 在其他情况下,导出 src/hw_none.dart 作为空实现,以防止编译错误。
提示
  • dart.library.io 表示当前平台是Dart VM的IO平台,即支持Dart VM的命令行、命令行参数、文件系统等功能。
  • dart.library.js_interop 表示当前平台是JavaScript平台,即支持Dart VM的JavaScript运行时。
  • 你或许会看到别的写法,比如dart.library.html 请修改为 dart.library.js_interop

基于条件导出的基本实现

实际案例:drift数据库实现跨平台

例如,在 src/hw_io.dart 中,我们可以定义一个 printMessage() 函数,该函数在命令行中输出一条消息:

void printMessage() {

print('Hello from IO platform');
}

src/hw_web.dart 中,我们可以定义一个 printMessage() 函数,该函数在 Web 页面中输出一条消息:

void printMessage() {

print('Hello from Web platform');
}

然后,我们在 lib/hw.dart 中导入这两个实现:

export 'src/hw_none.dart'
if (dart.library.io) 'src/hw_io.dart'
if (dart.library.js_interop) 'src/hw_web.dart';

最后,我们在 main() 函数中调用 printMessage() 函数,并传入不同的参数:

void main() {
printMessage();
}

基于条件导入的多态实现

参考这篇如何实现多平台导入适配

其核心思路是:

  • stub 实现,即在不支持的平台上,提供一个空实现。
  • 平台实现,即在支持的平台上,提供具体的实现。
  • 条件导入
import 'key_finder_stub.dart'
// ignore: uri_does_not_exist
if (dart.library.io) 'package:flutter_conditional_dependencies_example/storage/shared_pref_key_finder.dart'
// ignore: uri_does_not_exist
if (dart.library.html) 'package:flutter_conditional_dependencies_example/storage/web_key_finder.dart';

其基本要求是各自实现中需要同名类或者同名函数,然后通过条件导入,导入对应的实现。

在此基础上,我们可以进一步思考,是否可以将不同平台的实现分离,并通过一个统一的接口来访问,从而实现跨平台的功能。

由于Dart 并不存在接口,只存在抽象类,所以我们可以借助抽象类来实现。当然抽象类实现的本质也是借助于各自实现的同名“工厂函数”

step1: 创建接口

import 'key_finder_stub.dart'
// ignore: uri_does_not_exist
if (dart.library.io) 'package:flutter_conditional_dependencies_example/storage/shared_pref_key_finder.dart'
// ignore: uri_does_not_exist
if (dart.library.html) 'package:flutter_conditional_dependencies_example/storage/web_key_finder.dart';

abstract class KeyFinder {

// some generic methods to be exposed.

/// returns a value based on the key
String getKeyValue(String key) {
return "I am from the interface";
}

/// stores a key value pair in the respective storage.
void setKeyValue(String key, String value) {}

/// factory constructor to return the correct implementation.
factory KeyFinder() => getKeyFinder();
}

step2: web实现

import 'dart:html';

import 'package:flutter_conditional_dependencies_example/storage/key_finder_interface.dart';

Window windowLoc;

class WebKeyFinder implements KeyFinder {

WebKeyFinder() {
windowLoc = window;
print("Widnow is initialized");
// storing something initially just to make sure it works. :)
windowLoc.localStorage["MyKey"] = "I am from web local storage";
}

String getKeyValue(String key) {
return windowLoc.localStorage[key];
}

void setKeyValue(String key, String value) {
windowLoc.localStorage[key] = value;
}
}

KeyFinder getKeyFinder() => WebKeyFinder();

step3: 原生实现

import 'package:flutter_conditional_dependencies_example/storage/key_finder_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SharedPrefKeyFinder implements KeyFinder {
SharedPreferences _instance;

SharedPrefKeyFinder() {
SharedPreferences.getInstance().then((SharedPreferences instance) {
_instance = instance;
// Just initializing something so that it can be fetched.
_instance.setString("MyKey", "I am from Shared Preference");
});
}

String getKeyValue(String key) {
return _instance?.getString(key) ??
'shared preference is not yet initialized';
}

void setKeyValue(String key, String value) {
_instance?.setString(key, value);
}

}

KeyFinder getKeyFinder() => SharedPrefKeyFinder();

step4: 空实现

import 'key_finder_interface.dart';

KeyFinder getKeyFinder() => throw UnsupportedError(
'Cannot create a keyfinder without the packages dart:html or package:shared_preferences');

基于插件实现多平台适配

Flutter插件是一种扩展Flutter功能的机制。通过插件,你可以将自己的代码打包成可供其他开发者使用的库。

插件可以帮助你实现跨平台适配,例如,你可以编写一个插件,它可以帮助你实现不同平台的适配。

插件的基本结构如下:

  • 一个pubspec.yaml文件,用于定义插件的名称、版本、依赖等信息。
  • 一个lib/文件夹,用于存放插件的源代码。
  • 一个example/文件夹,用于存放插件的示例代码。
  • 一个android/文件夹,用于存放Android平台的实现。
  • 一个ios/文件夹,用于存放iOS平台的实现。
  • 一个macos/文件夹,用于存放macOS平台的实现。
  • 一个linux/文件夹,用于存放Linux平台的实现。
  • 一个windows/文件夹,用于存放Windows平台的实现。
// my_plugin.dart
import 'my_plugin_platform_interface.dart';

class MyPlugin {
Future<String?> getPlatformVersion() {
return MyPluginPlatform.instance.getPlatformVersion();
}
}

// my_plugin_platform_interface.dart
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'my_plugin_method_channel.dart';

abstract class MyPluginPlatform extends PlatformInterface {
/// Constructs a MyPluginPlatform.
MyPluginPlatform() : super(token: _token);

static final Object _token = Object();

static MyPluginPlatform _instance = MethodChannelMyPlugin();

/// The default instance of [MyPluginPlatform] to use.
///
/// Defaults to [MethodChannelMyPlugin].
static MyPluginPlatform get instance => _instance;

/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [MyPluginPlatform] when
/// they register themselves.
static set instance(MyPluginPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}

Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}

// my_plugin_method_channel.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'my_plugin_platform_interface.dart';

/// An implementation of [MyPluginPlatform] that uses method channels.
class MethodChannelMyPlugin extends MyPluginPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('my_plugin');

@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}
// my_plugin_web.dart
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:web/web.dart' as web;

import 'my_plugin_platform_interface.dart';

/// A web implementation of the MyPluginPlatform of the MyPlugin plugin.
class MyPluginWeb extends MyPluginPlatform {
/// Constructs a MyPluginWeb
MyPluginWeb();

static void registerWith(Registrar registrar) {
MyPluginPlatform.instance = MyPluginWeb();
}

/// Returns a [String] containing the version of the platform.
@override
Future<String?> getPlatformVersion() async {
final version = web.window.navigator.userAgent;
return version;
}
}
name: my_plugin
description: "A new Flutter plugin project."
version: 0.0.1
homepage:

environment:
sdk: '>=3.4.1 <4.0.0'
flutter: '>=3.3.0'

dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter
web: ^0.5.1
plugin_platform_interface: ^2.0.2

dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.example.my_plugin
pluginClass: MyPlugin
ios:
pluginClass: MyPlugin
web:
pluginClass: MyPluginWeb
fileName: my_plugin_web.dart

# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware

# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

可以看到,基本实现是通过调用MyPluginPlatform.instance.getPlatformVersion()来实现具体的功能,而我们只需要将instance设置为不同的实现即可。 从web 实现中可以看出registerWith方法是替换instance的关键步骤。

plugin:
platforms:
android:
package: com.example.my_plugin
pluginClass: MyPlugin
ios:
pluginClass: MyPlugin
web:
pluginClass: MyPluginWeb
fileName: my_plugin_web.dart

有了初步的了解,我们可以继续深入探索插件的实现。


插件的实现方式有两种:

  • 基于平台的实现:插件可以提供不同的实现,例如,你可以提供一个Android实现和一个iOS实现。
  • 联合实现:插件可以提供一个通用实现,然后通过插件的依赖关系,将不同的实现提供给不同的平台。

基于平台的实现

基于平台的实现,即插件可以提供不同的实现,例如,你可以提供一个Android实现和一个iOS实现。

例如,你有一个插件,它可以帮助你实现不同平台的适配。

插件的pubspec.yaml文件如下:

name: platform_adapter
description: A new Flutter plugin project.
version: 0.0.1
author: Flutter Team <<EMAIL>>
homepage: https://flutter.dev

environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.20.0"


dependencies:
flutter:
sdk: flutter


dev_dependencies:
flutter_test:
sdk: flutter

endorsed federated implementations

Writing custom platform-specific code

components

· 阅读需 3 分钟

组件设计理念1

在计算机中分层处理是常见的一种设计理念,这种设计理念可以将复杂的问题分解成多个简单的问题,然后再将这些简单的问题组合起来解决复杂的问题。在前端开发中,组件化的设计理念也是如此,将一个复杂的页面分解成多个简单的组件,然后再将这些组件组合起来构成一个复杂的页面。

组件的定义

组件是一个独立的、可复用的、可组合的、可交互的 UI 单元,组件将 UI 分割成一些独立的、可复用的部件,每个部件都包含自己的 HTML、CSS、JS 代码,组件可以被多次使用。

组件的优点

  1. 提高开发效率
  2. 便于维护
  3. 提高代码复用率
  4. 便于协同开发
  5. 便于测试

组件的分类

按照组件的复用性

  1. 基础组件:基础组件是指那些可以被多个业务组件使用的组件,例如:按钮、输入框、单选框、复选框、下拉框等。
  2. 业务组件:业务组件是指那些只能被某个业务模块使用的组件,例如:登录组件、注册组件、商品列表组件、商品详情组件等。

按照组件的复用方式

  1. 页面组件:页面组件是指那些可以独立存在的组件,例如:登录页面、注册页面、商品列表页面、商品详情页面等。
  2. 区块组件:区块组件是指那些不能独立存在的组件,例如:商品列表组件、商品详情组件等。

按照组件的复用范围

  1. 全局组件:全局组件是指那些可以在任何组件中使用的组件,例如:按钮、输入框、单选框、复选框、下拉框等。
  2. 局部组件:局部组件是指那些只能在某个组件中使用的组件,例如:登录组件、注册组件、商品列表组件、商品详情组件等。

组件的设计原则

  1. 单一职责原则:一个组件只做一件事情。
  2. 开闭原则:一个组件应该对扩展开放,对修改关闭。
  3. 里氏替换原则:一个组件可以被它的子组件所替换。
  4. 接口隔离原则:一个组件不应该依赖它不需要的接口。
  5. 依赖倒置原则:一个组件不应该依赖它的实现细节,而应该依赖它的抽象。

组件的设计模式

  1. 单例模式:单例模式是指一个组件只能被实例化一次。
  2. 工厂模式:工厂模式是指通过工厂方法来创建组件。

组件的设计原则