BI 数据可视化平台建设(1)—交叉表组件演变实战

news2024/11/13 9:42:42

作者:vivo 互联网大数据团队 - Zhu Jianchen

本文是vivo互联网大数据团队《BI数据可视化平台建设》系列文章第1篇 - 交叉表组件。

交叉表在数据分析里应用广泛,通过本文,你将了解到:

  • 交叉表的基本概念,以及BI可视化平台常见术语。

  • 我们的表格类组件的演化过程,以及如何通过技术调研和优化实现大数据量下渲染性能,一步一步从原先的~10s降低到3~4s。

  • 交叉表的一些特定场景,提供了一些技术实现简易描述,对这些场景有一些宏观认识。

  • Worker,虚拟滚动,微应用等关键技术的实现细节。

一、背景

表格和表单在前端里面是最复杂的两类需求,在BI工具平台上,这2类组件需求更多,并且需要实现一些特有的交互展示。目前在敏捷BI平台上进行报表配置,表格类组件的使用占比达到了1/3,在可视化组件库里使用范围很广。为了满足不同的数据分析场景,表格组件主要分为分组表、交叉表、明细表三种类型,其中又以交叉表功能最为丰富强大。随着敏捷BI的业务的发展,交叉表组件也经历了多次设计改版以支持高性能的数据渲染和个性化的展示配置。本文主要通过交叉表组件的升级实践给大家讲解一下如何设计开发高性能的表格组件。 

术语注解

  • 【敏捷BI】

专为 vivo 生态用户量身打造的 自助式 BI 平台,提供从数据接入、数据准备、到数据分析、可视化应用、数据管理的一站式数据解决方案,同Quick BI,FineBI。

  • 【图表类型】

图表是数据视觉化表示的特殊方式。表示数据的方法有很多,如使用不同的符号、形状和排列,我们把这些称之为图表的类型。一些图表类型你比较熟悉,如条形图、饼图、折线图,但其他类型你可能就很少见了,如桑基图、树图、等值线图的地图。

  • 【交互方式】

交互式可视化允许您修改,操作和探索计算机显示的数据。绝大多数交互式可视化系统在计算机网络上,但越来越多出现在平板电脑和智能手机上。相比之下,静态可视化只显示单一的、非交互数据,它通常是为了打印和在屏幕上显示。

  • 【度量值】

表示数值的规模和范围。度量通常以间隔表示(10、20、30等等),代表度数字的单位,如价格、距离、年,或百分比。

  • 【指标】

同度量,表示具体某项值,单个值本身没有任何业务意义,一般需要对应的指标口径解释,才会具有业务价值。

二、交叉表介绍

交叉表(Cross Tabulations)是一种常用的由 行、列、汇总字段 三个元素组成分类汇总表格。利用交叉表查询数据非常直观明了,在进行数据分析中也被广泛应用。这里牵涉到另外一个概念即分组报表,分组报表是所有报表当中最普通,最常见的报表类型,也是所有报表工具都支持的一种报表格式。从一般概念上来讲,分组报表就是只有纵向的分组,传统的分组报表制作方式是把报表划分为条带状,用户根据一个数据绑定向导指定分组,汇总字段,生成标准的分组报表。交叉表有多列查询能力、分类汇总、多角度排序、交互式分析等特性。

图片

三、架构演变历程

为了提高交叉表的数据渲染性能和功能扩展能力,敏捷BI的表格组件经历了三次的设计升级。最开始用的jQuery拼接表格方式实现,随着组件化方案的推行,采用了组件化的方式实现升级,随着业务的发展,用多维度、多指标交叉分析场景越来越多了,尤其是通过交叉表进行分析时,大数据量出现了渲染崩溃等问题,所以我们最后通过微前端方式实现。  下面我们从开发难度,性能,功能扩展性,学习成本等方面的调研来讲解对底层表格的升级实践。

图片

3.1 V1版表格

敏捷BI平台第一版表格,技术栈是基于jQuery+DIV的方式实现的。表格拼接属于jQuery时代的常见开发风格,这种方式,代码可维护性会非常差,很容易会出现标签不匹配的情况,不带缩进,调试起来也比较费劲。这个版本的表格组件支持的业务场景主要是数据的基本展现,无法满足用户对表格的数据分析的需求。

  • 架构设计

图片

// 简单的拼接代码demo
function createTable() {
  var data = new Array();
     
 data.push('<table border=1><tbody>');
   for (var i = 0; i < 2000; i++) {
     data.push('<tr>');
     for (var j = 0; j < 5; j++) {
       data.push('<td>' + i + ',' + j + '</td>');
     }
     data.push('</tr>');
   }
   data.push('</tbody><table>');
      
   document.getElementById('table1').innerHTML = data.join('');
 }

3.2 V2版表格

随着系统整体架构升级,前后端分离的推进,我们从原生的table组件迁移到Vue组件化上,开发了V2版表格组件。  平台的整体架构全面迁移到vue+ant-design-vue上面。

1. 功能拓展

鉴于ant-design-vue上正好有table组件,对此我们对比了antd的table组件和element的table组件。2 种表格对比来看,ant-design-vue参照ant.design的React版开发出来,配置相对element更丰富,考虑到本身复杂场景支持性,更适合深度定制,最终选择了ant.design的vue版本。

图片

V2版的表格主要支持这几类场景配置(条件格式,合计行/列,单元格/行样式/内容定制等):

业务场景&具体实现

(1)数据展示

整体就是根据不同情况设置不同的column的字段,另外为了达到点击交互下,能够获取业务的数据,需要在column上挂一些冗余数据,这样会让column的数据信息很庞大。

columns是一个tree结构,这里采用的是dfs遍历,depth标识层级,item, itemType就是冗余的数据信息,在处理业务的时候会用到。

图片

图片

(2)数据排序

图片

(3)数据过滤

(4)单元格自定义渲染

图片

(5)多级表头定制

这个实现难点主要在于把已有的列如何放到新增的表头里,保持树形children结构具体实现代码也比较复杂,总共80行。

图片

图片

(6)条件格式渲染(条形图,热力图)

根据设定的条件,定制表格内单元格内容的样式

图片

(7)合计行/列配置

添加合计列和行,内置min,max,avg,sum表达式,支持自定义简易字段表达式运算这个功能难点在于合计列与行交叉的场景,也就是如何计算合计列的合计行。

图片

2. 架构设计

图片

3. 渲染优化

这个阶段的交叉表,在功能上已经能够满足绝大多数分析场景,但是一些数据量大的表格反馈渲染白屏时间过长,经常会出现浏览器崩溃,表格的性能面临新的挑战。另外表格在渲染时,CPU会占满,导致其他图表也会卡住等待,形成假死的现象。我们通过分析大数据表格渲染流程,发现有30%的时间会花销在数据适配,因此我们思考能不能把数据计算部分隔离出来,计算的时候,不阻塞渲染主进程,这样的话,浏览器渲染就可以处理其他的渲染任务。在做性能优化调研时,我们引入了service worker,worker在处理cpu密集型任务有独特的优势,所以我们把数据预处理的过程交给了Worker。之前没有使用worker时,我们前端逻辑会处理很多数据初始化和计算的操作,对于一个数据量很大的表格,会导致渲染卡顿2~3s,有些个别情况会导致浏览器崩溃的现象。

Worker原理和定义

W3C 组织早在 2014 年 5 月就提出过 Service Worker 这样的一个 HTML5 API ,主要用来做持久的离线缓存。service worker是浏览器的一个高级特性,本质是一个web worker,是独立于网页运行的脚本。

web worker这个api被造出来时,就是为了解放主线程。因为,浏览器中的JavaScript都是运行在单一个线程上,随着web业务变得越来越复杂,js中耗时间、耗资源的运算过程则会导致各种程度的性能问题。

而web worker由于独立于主线程,则可以将一些复杂的逻辑交由它来去做,完成后再通过postMessage的方法告诉主线程。service worker则是web worker的升级版本,相较于后者,前者拥有了持久离线缓存的能力。

3.3 V3版表格

在开发V2表格时,我们意识到数据处理部分不应该交给前端,列拼接上掺杂了太多的业务场景处理,另外渲染性能和崩溃问题急需解决,对此我们进行了V3版本迭代,提前对表格版本进行了技术升级,为之后的新一批列汇总行汇总,分组小计等高级交叉分析需求做好技术储备。

1. 技术选型

我们对比了react,vue及canvas生态有代表性的表格组件。综合三者优劣势最终确定了基于react的table组件。

图片

S2:https://github.com/antvis/S2

ali-react-table:https://github.com/alibaba/ali-react-table

vxe-table:https://github.com/x-extends/vxe-table/tree/v2

vxe-table设计初衷是解决单元格编辑的问题,主要用于大量增删改查的场景,性能不是它唯一的目标;S2是建立在电子表格需求上的,对筛选、排序、搜索、复制、框选、聚合分析都有诉求。

同时需要在大数据量下保持高性能,解决之前商业软件版本实现的性能问题和拓展性问题,所以它覆盖的场景更全更复杂,但是它的缺点就是定制型不强,不太适合我们自身的业务。

所以最终我们选择了ali-react-table,它本身体积小,在基础能力都满足的情况下,扩展新功能也很容易,而且在大数据量渲染下有高性能的优势。

2. 架构设计

后端接口返回数据和配置部分,基于渲染模型:左树 + 上树 => 表格,根据配置生成左树leftTree和上树topTree,构造数据源,参照了ant-design的Table组件数据源构造的流程,与自身的pipeline插件机制结合,实现了表格的交互操作(排序,筛选,分页)。

由于本项目里接入了微前端架构,采用了loadApp的方式实现了异构应用混合开发:

图片

运作流程图如下:

图片

3. 升级实践

(1)架构升级:

图片

(2)底层渲染:

虚拟滚动:长列表渲染受制于浏览器本身限制,在大量DOM下,会达到浏览器本身的渲染瓶颈,在这种情况下,虚拟滚动可以解决这种渲染问题,它是一种按需渲染的理念的体现。所以虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。

大致原理如下图:

图片

我们发现长列表在展示时,用户只会关注可视区域,其他非可视区域部分,我们可以把已经渲染的DOM销毁,不需要立即渲染的DOM延后。所以优化策略就是只渲染可见区域的内容。

在滚动事件触发后,根据滚动 Offset 调整相应渲染的内容即可。在用户看来,还是一个完整的长列表。这种懒加载的方式,和早期页面图片资源懒加载和非必须资源异步加载属于同一种思路。

在新版版本里,ali-react-table自带了虚拟滚动的特性,在大列表下,框架会自动开启,可以明显提升表格渲染性能和滚动的性能。

四、同类产品对比

4.1 技术架构对比

1. Quick BI

① 架构设计

图片

② 技术实现

  • 使用原生div和flex布局,不使用原生table表格

  • 列宽,固定列,固定表头等表格不好实现的问题,都易实现,渲染性能也较好

  • 有2个版本的表格,旧版表格使用table,在这种情况下,性能,复杂交互,分组都存在瓶颈,这一点和我们类似,新老版本的表格同时在线上应用

  • 虚拟滚动支持横向,纵向滚动

③ 优劣势

  • ali-react-table不维护了,源码不太复杂,可以二次迭代开发;基本满足交叉表所有功能;大数据量下渲染高性能

  • 接口数据略冗余

④ 备注

  • 数据结构明确行维、列维、指标列数据;

  • 数据汇总和小计是存放在后端

2. 敏捷BI

① 架构设计

图片

② 技术实现

  • 使用table布局

  • 使用position:sticky实现固定列;固定表头使用独立的单表头表格模拟,这里需要强制table设置列宽,保证列对齐

  • 支持横向,纵向虚拟滚动,在10w列下依然可以正常渲染

  • 在ali-react-table基础上扩展了按维度合并,表头筛选等feature

③ 优劣势

  • flex布局灵活,不受表格本身布局限制,易实现固定列和表头,列宽;canvas开发成本较高,bug不好调试

  • 接口数据更精简

④ 备注

  • 数据结构不明确,需要对二维数组转换,存在一定的预处理逻辑;

  • 数据结构存在冗余现象

4.2 应用场景对比

以实际测试为准

1. Quick BI

业务场景

(1)字段配置

  • 行:数据集的维度字段拖拽到行选择区

  • 列:数据集里的维度或者度量字段拖拽到列选择区

  • 过滤器:数据集字段拖拽到过滤器选择区,对字段进行筛选

(2)样式配置

  • 标题与卡片:设置标题样式

  • 备注和尾注:设置图表备注和尾注内容

  • 组件容器:设置内边距和背景色,圆角

  • 展示型配置:设置主题,表头样式,内容样式,冻结列,序号等配置

  • 功能型配置:条件格式配置,针对字段满足特定条件下突出显示配置的样式

  • 总计配置:支持列汇总和行汇总,行总计和行小计,列总计和列小计

(3)高级配置

  • 联动:图表里的字段与其他图标关联

  • 跳转:图表字段跳转传值

 技术实现

  • 实现使用原生,未使用第三方库

  • 自定义主题使用主色编辑

  • 拖拽方式交互

2. 网易有数

业务场景

  • 没有复杂的交叉表场景,只支持普通明细表

  • 配置方式主要包括主题,表头,内容的字体样式,背景,对齐等样式

  • 支持下钻,字段跳转

  • 数据集字段支持维度层级和组的概念

  • 没有虚拟滚动

 技术实现

  • 内部使用table表格实现

  • 主题配置支持上传主题json文件

3. 敏捷BI

(1)字段配置

  • 行维:数据集维度字段放置区

  • 列维:数据集维度字段放置区

  • 指标:数据集指标字段

(2)图表属性和图表样式配置

  • 支持条件格式,自定义代码样式嵌入,主题配置

(3)字段过滤

  • 使用字段过滤数据

 技术实现

  • 最开始使用smooth-dnd库,来实现从数据集字段拖拽到行列、指标区

  • 因为smooth-dnd有性能问题,不再维护等问题,就废弃掉了,使用原生的拖拽实现

  • 主题使用在线代码编辑主题,基于codemirror在线代码,接入css variables,实时应用,不需要刷新

4.3 部分核心代码实现

应用场景① :表头筛选

代码实现:

图片

应用场景②:按维度合并

代码实现:

图片

4.4 渲染性能对比

1. Quick BI

(1)数据量级 <50列

  • 接口耗时300ms 接口大小<5kb

  • 渲染耗时 < 1s

注:数据量不是很大的情况下,数据加载忽略不计,合入到数据渲染时间,差别不大

(2)数据量级 ≥ 200列

  • 接口耗时1.88s 接口大小<10kb

  • 表格渲染 < 3s

注:数据量很大的情况下,数据加载需要单独计入时间

(3)数据量级 > 1W列

极端情况下,表格渲染崩溃

图片

2. 敏捷BI

(1)数据量级 <50列

  • 接口耗时250ms 接口大小~100kb

  • 渲染耗时 < 1s

(2)数据量级 ≥ 200列

  • 接口耗时300ms 接口大小~300kb

  • 渲染耗时 <3s

(3)数据量级 > 1W列

  • 接口耗时2s 数据大小2M

  • 渲染耗时~10s

4.5 总结

网易有数表格组件较为简单,只有简单的数据展示和排序筛选,适用于明细数据展示场景。

Quick BI表格和敏捷BI在交互,可视化能力,业务场景上都保持着同样的功能,底层实现 Quick BI采用原生DIV+Flex布局模拟表格实现,在渲染上比表格会有渲染的优势,这点是浏览器自身渲染机制决定,我们内部实现需要满足极端数据量下数据展示,所以特定做了横向的虚拟列表优化,这种场景看业务需求,否则表格会过于复杂,得不偿失。

表格渲染性能基本与Quick BI性能相当,极端情况下,敏捷BI依旧可以正常渲染,这点优于Quick BI。

五、规划

  • 数据预处理部分不由前端处理,交给后端,和后端协调好返回的数据结构,直接返回;

  • 表格扩展的功能与表格耦合严重,表格渲染不够纯净;

  • 开发一个Headless UI,不依赖渲染框架,提供一个数据适配层,同时支持在Vue3生态上使用。

参考资料:

  1. ali-react-table 站在巨人肩上,可惜不维护了

  2. ant-design table组件源码实现

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

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

相关文章

canal实操应用

一、MySQL的binlog日志 1.1、binlog的分类 binlog一般分为三类&#xff1a;statement语句级&#xff0c;记录一条一条的SQL&#xff0c;一条SQL可能更改多行&#xff0c;且SQL语句中如果用到now()函数或者random()函数&#xff0c;会存在数据不一致的问题。row行级&#xff0…

大容量疯了!居然想把磁带放到硬盘,100TB+是否可以实现?

1.引言 上一篇关于大容量硬盘的文章&#xff08;HDD最后的冲刺&#xff1a;大容量硬盘的奋力一搏&#xff09;中&#xff0c;我们针对大容量硬盘研发状态&#xff0c;小编最近又有了新发现。WDC希望可以通过HDD和磁带结合&#xff0c;把盘的容量提升到100TB。 2.数据大爆炸的…

C# Socket通信从入门到精通(7)——单个异步TCP服务器监听单个客户端C#代码实现

前言: 我们在开发TCP服务器程序的时候,有的时候需要一些异步的应用,比如我读取客户端发送的数据,但是服务器程序不能一直等待客户端数据发送过来,服务器要先做一些别的事情,这个时候C# Socket通信从入门到精通(5)——单个同步TCP服务器监听一个客户端C#代码实现这篇文…

低代码平台受欢迎度排行榜:揭秘市场热门之选

对于企业而言&#xff0c;低代码平台不仅仅是一个开发工具&#xff0c;它更是一个加速器&#xff0c;推动了企业的数字化转型进程。传统的开发模式下&#xff0c;业务部门与IT部门之间常常存在沟通障碍&#xff0c;导致需求难以实现或实现速度缓慢。而低代码平台打破了这种障碍…

C++学习贴---C++预处理器

文章目录 前言预处理器#define预处理条件编译#ifdef#ifndef#if、#elif、#else 和 #endif #和##运算符 预定义宏 前言 预处理器 预处理器是指一些指示编译器在实际编译之前所需要完成的指令。 预处理器负责处理以**井号&#xff08;#&#xff09;**开头的预处理指令&#xff0…

为啥$p(w|D)=p(y|X,w)$?

为啥 p ( w ∣ D ) p ( y ∣ X , w ) p(w|D)p(y|X,w) p(w∣D)p(y∣X,w)&#xff1f; p ( w ∣ X , y ) p ( w ∣ D ) p(w|X,y)p(w|D) p(w∣X,y)p(w∣D), p ( w ∣ D ) p ( D , w ) / p ( D ) p(w|D)p(D,w)/p(D) p(w∣D)p(D,w)/p(D)为啥 p ( D ∣ w ) p ( y ∣ X , w ) p(D|…

PLC开放式以太网通信网络状态查看工具netstat

在进行PLC的开放式以太网通信时,为了查看网络状态我们可以利用ping这个强有力的工具,还可以使用netstat这个工具。 博途PLC开放式以太网通信 UDP通信 博途PLC 1200/1500PLC开放式以太网通信TSEND_C通信(UDP)_RXXW_Dor的博客-CSDN博客文章浏览阅读1.7k次。开放式TSEND_C通信…

大数据毕业设计选题推荐-污水处理大数据平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

【Java】记一次服务内实现排队消费模式

主要是记录一下实现过程和实现的过程中遇到的坑。 我的业务 系统中有一个接口&#xff0c;是从大数据那边拉数据&#xff0c;之前的做法是&#xff0c;开个线程池&#xff0c;让SQL去执行&#xff0c;可是如果大量的慢SQL同时&#xff0c;请求数据库的话会适得其反。并且还有…

Python语法基础(字符串 列表 元组 字典 集合)

目录 字符串(str)字符串的创建特殊情况字符串的转义字符字符串的运算符字符串常用方法求字符串长度去掉多余空格是否包含某子串分割字符串合并字符串替换字符串统计统计字符串出现的次数 练习&#xff1a;判断字符串是否为回文串 列表(list)列表的创建列表常用方法遍历列表列表…

小程序如何设置下单提示语句

下单提示会展示在购物车和提交订单页面&#xff0c;它可以帮助商家告知客户事项&#xff0c;提高用户体验和减少错误操作。例如提示&#xff1a;商品是否包邮、某些区域是否发货、商品送达时间等等。 在小程序管理员后台->配送设置处&#xff0c;填写下单提示。在设置下单提…

基于ssm的高校失物招领管理系统

基于ssm的高校失物招领管理系统 摘要 失物招领管理系统是一种利用现代信息技术&#xff0c;为高校提供高效、便捷的失物招领服务的平台。本系统基于SSM框架&#xff08;Spring SpringMVC MyBatis&#xff09;&#xff0c;充分利用了各框架的优势&#xff0c;实现了系统的稳定…

1.微服务与SpringCloud

微服务和SpringCloud 文章目录 微服务和SpringCloud1.什么是微服务2.SpringCloud3. 微服务 VS SpringCloud4. SpringCloud 组件5.参考文档6.版本要求 1.什么是微服务 微服务是将一个大型的、单一的应用程序拆分成多个小型服务&#xff0c;每个服务实现特定的业务功能&#xff…

C#上位机序列10: Winform上位机通用框架

C#上位机序列1: 多线程&#xff08;线程同步&#xff0c;事件触发&#xff0c;信号量&#xff0c;互斥锁&#xff0c;共享内存&#xff0c;消息队列&#xff09; C#上位机序列2: 同步异步(async、await) C#上位机序列3: 流程控制&#xff08;串行&#xff0c;并行&#xff0c…

防火防盗防小人 使用 Jasypt 库来加密配置文件

⚔️ 项目配置信息存放在哪&#xff1f; 在日常开发工作中&#xff0c;我们经常需要使用到各种敏感配置&#xff0c;如数据库密码、各厂商的 SecretId、SecretKey 等敏感信息。 通常情况下&#xff0c;我们会将这些敏感信息明文放到配置文件中&#xff0c;或者放到配置中心中。…

原厂监视综合控制继电器 ZZS-7/1 AC220V 凸出端子固定安装

ZZS-7/11分闸、合闸、电源监视综合控制装置&#xff1b; ZZS-7/12分闸、合闸、电源监视综合控制装置&#xff1b; ZZS-7/13分闸、合闸、电源监视综合控制装置&#xff1b; ZZS-7/14分闸、合闸、电源监视综合控制装置&#xff1b; ZZS-7/102分闸、合闸、电源监视综合控制装置…

GIT的安装与常见命令

Git的介绍 Git是一个开源的分布式版本控制系统&#xff0c;最初由Linus Torvalds在2005年创建用于管理Linux内核的开发&#xff0c;现在已成为全球最流行的版本控制工具之一。 Git可以跟踪代码的修改&#xff0c;记录开发历程&#xff0c;保证多人合作开发时代码的一致性&…

关于maven读取settings.xml文件的优先级问题

今天在IDEA中配置maven的setting.xml文件路径指向的.m2路径下的setting_a.xml文件&#xff0c;同时&#xff0c;我的maven3.6.3也放在.m2中。 [1] .m2文件夹 [2] apache-maven-3.6.3文件夹 然后&#xff0c;在IDEA中打包发布时发现&#xff0c;无论如何都读取不到指定的setti…

【Linux】Linux常用命令—磁盘管理、压缩包管理

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

猫罐头哪家好?宠物店自用的5款猫罐头推荐!猫咪嘎嘎炫~

亲爱的铲屎官们&#xff0c;你们是否会为猫咪选购猫罐头而感到烦恼&#xff1f;你们是否渴望了解哪些猫罐头在宠物界有着良好的口碑&#xff1f;猫罐头&#xff0c;作为猫咪日常饮食中的重要组成部分&#xff0c;其品质直接影响到猫咪的健康和幸福。 猫罐头哪家好&#xff1f;作…