设计前端组件是最能考验开发者基本功的测试之一,因为调用Material design、Antd、iView 等现成组件库的 API 每个人都可以做到,但是很多人并不知道很多常用组件的设计原理。
能否设计出通用前端组件也是区分前端工程师和前端api调用师的标准之一,那么应该如何设计出一个通用组件呢?
下文中提到的组件库通常是指单个组件,而非集合的概念,集合概念的组件库是 Antd iView这种,我们所说的组件库是指集合中的单个组件,集合性质的组件库需要考虑的要更多.
我们在学习设计模式的时候会遇到很多种设计原则,其中一个设计原则就是单一职责原则,在组件库的开发中同样适用,我们原则上一个组件只专注一件事情,单一职责的组件的好处很明显,由于职责单一就可以最大可能性地复用组件,但是这也带来一个问题,过度单一职责的组件也可能会导致过度抽象,造成组件库的碎片化。
举个例子,一个自动完成组件(AutoComplete),他其实是由 Input 组件和 Select 组件组合而成的,因此我们完全可以复用之前的相关组件,就比如 Antd 的AutoComplete组件中就复用了Select组件,同时Calendar、 Form 等等一系列组件都复用了 Select 组件,那么Select 的细粒度就是合适的,因为 Select 保持的这种细粒度很容易被复用.
那么还有一个例子,一个徽章数组件(Badge),它的右上角会有红点提示,可能是数字也可能是 icon,他的职责当然也很单一,这个红点提示也理所当然也可以被单独抽象为一个独立组件,但是我们通常不会将他作为独立组件,因为在其他场景中这个组件是无法被复用的,因为没有类似的场景再需要小红点这个小组件了,所以作为独立组件就属于细粒度过小,因此我们往往将它作为 Badge 的内部组件,比如在 Antd 中它以ScrollNumber的名称作为Badge的内部组件存在。
所以,所谓的单一职责组件要建立在可复用的基础上,对于不可复用的单一职责组件我们仅仅作为独立组件的内部组件即可。
我们要设计的本身就是通用组件库,不同于我们常见的业务组件,通用组件是与业务解耦但是又服务于业务开发的,那么问题来了,如何保证组件的通用性,通用性高一定是好事吗?
比如我们设计一个选择器(Select)组件,通常我们会设计成这样
这是一个我们最常见也最常用的选择器,但是问题是其通用性大打折扣
当我们有一个需求是长这样的时候,我们之前的选择器组件就不符合要求了,因为这个 Select 组件的最下部需要有一个可拓展的条目的按钮
这个时候我们难道要重新修改之前的选择器组件,甚至再造一个符合要求的选择器组件吗?一旦有这种情况发生,那么只能说明之前的选择器组件通用性不够,需要我们重新设计.
Antd 的 Select 组件预留了dropdownRender
来进行自定义渲染,其依赖的 rc-select
组件中的代码如下
Antd 依赖了大量以rc-
开头的底层组件,这些组件被react-component团队(同时也就是Antd 团队)维护,其主要实现组件的底层逻辑,Antd 则是在此基础上添加Ant Design设计语言而实现的
当然类似的设计还有很多,通用性设计其实是一定意义上放弃对 DOM 的掌控,而将 DOM 结构的决定权转移给开发者,dropdownRender
其实就是放弃对 Select 下拉菜单中条目的掌控,Antd 的 Select 组件其实还有一个没有在文档中体现的方法getInputElement
应该是对 Input 组件的自定义方法,Antd整个 Select 的组件设计非常复杂,基本将所有的 DOM 结构控制权全部暴露给了开发者,其本身只负责底层逻辑和最基本的 DOM 结构.
这是 Antd 所依赖的 re-select 最终 jsx 的结构,其 DOM 结构很简单,但是暴露了大量自定义渲染的接口给开发者.
return (
<SelectTrigger
onPopupFocus={this.onPopupFocus}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
dropdownAlign={props.dropdownAlign}
dropdownClassName={props.dropdownClassName}
dropdownMatchSelectWidth={props.dropdownMatchSelectWidth}
defaultActiveFirstOption={props.defaultActiveFirstOption}
dropdownMenuStyle={props.dropdownMenuStyle}
transitionName={props.transitionName}
animation={props.animation}
prefixCls={props.prefixCls}
dropdownStyle={props.dropdownStyle}
combobox={props.combobox}
showSearch={props.showSearch}
options={options}
multiple={multiple}
disabled={disabled}
visible={realOpen}
inputValue={state.inputValue}
value={state.value}
backfillValue={state.backfillValue}
firstActiveValue={props.firstActiveValue}
onDropdownVisibleChange={this.onDropdownVisibleChange}
getPopupContainer={props.getPopupContainer}
onMenuSelect={this.onMenuSelect}
onMenuDeselect={this.onMenuDeselect}
onPopupScroll={props.onPopupScroll}
showAction={props.showAction}
ref={this.saveSelectTriggerRef}
menuItemSelectedIcon={props.menuItemSelectedIcon}
dropdownRender={props.dropdownRender}
ariaId={this.ariaId}
>
<div
id={props.id}
style={props.style}
ref={this.saveRootRef}
onBlur={this.onOuterBlur}
onFocus={this.onOuterFocus}
className={classnames(rootCls)}
onMouseDown={this.markMouseDown}
onMouseUp={this.markMouseLeave}
onMouseOut={this.markMouseLeave}
>
<div
ref={this.saveSelectionRef}
key="selection"
className={`${prefixCls}-selection
${prefixCls}-selection--${multiple ? 'multiple' : 'single'}`}
role="combobox"
aria-autocomplete="list"
aria-haspopup="true"
aria-controls={this.ariaId}
aria-expanded={realOpen}
{...extraSelectionProps}
>
{ctrlNode}
{this.renderClear()}
{this.renderArrow(!!multiple)}
</div>
</div>
</SelectTrigger>
);
那么这么多需要自定义的地方,这个 Select 组件岂不是很难用?因为好像所有地方都需要开发者自定义,通用性设计在将 DOM 结构决定权交给开发者的同时也保留了默认结构,在开发者没有显示自定义的时候默认使用默认渲染结构,其实 Select 的基本使用很方便,如下:
<Select defaultValue="lucy" style={{ width: 120 }} disabled>
<Option value="lucy">Lucy</Option>
</Select>
组件的形态(DOM结构)永远是千变万化的,但是其行为(逻辑)是固定的,因此通用组件的秘诀之一就是将 DOM 结构的控制权交给开发者,组件只负责行为和最基本的 DOM 结构
由于CSS 本身的众多缺陷,如书写繁琐(不支持嵌套)、样式易冲突(没有作用域概念)、缺少变量(不便于一键换主题)等不一而足。为了解决这些问题,社区里的解决方案也是出了一茬又一茬,从最早的 CSS prepocessor(SASS、LESS、Stylus)到后来的后起之秀 PostCSS,再到 CSS Modules、Styled-Components 等。
Antd 选择了 less 作为 css 的预处理方案,Bootstrap 选择了 Scss,这两种方案孰优孰劣已经争论了很多年了:
但是不管是哪种方案都有一个很烦人的点,就是需要额外引入 css,比如 Antd 需要这样显示引入:
import Button from 'antd/lib/button';
import 'antd/lib/button/style';
为了解决这种尴尬的情况,Antd 用 Babel 插件将这种情况 Hack 掉了
而material-ui
并不存在这种情况,他不需要显示引入 css,这个最流行的 React 前端组件库里面只有 js 和 ts 两种代码,并不存在 css 相关的代码,为什么呢?
他们用 jss
作为css-in-js 的解决方案,jsx 的引入已经将 js 和 html 耦合,css-in-js将 css 也耦合进去,此时组件便不需要显示引入 css,而是直接引用 js 即可.
这不是退化到史前前端那种写内联样式的时代了吗?
并不是,史前前端的内联样式是整个项目耦合的状态,当然要被抛弃到历史的垃圾堆中,后来的样式和逻辑分离,实际上是以页面为维度将 js css html 解耦的过程,如今的时代是组件化的时代了,jsx 已经将 js 和 html 框定到一个组件中,css 依然处于分离状态,这就导致了每次引用组件却还需要显示引入 css,css-in-js 正式彻底组件化的解决方案.
当然,我个人目前在用 styled-components,其优点引用如下:
选 Typescript ,因为巨硬大法好...
可以看看知乎问题下我的回答你为什么不用 Typescript
或者看此文TypeScript体系调研报告
组件的具体实现部分当然是组件库的核心,但是在现代前端库中其他部分也必不可少,我们需要一堆工具来辅助我们开发,例如编译工具、代码检测工具、打包工具等等。
市面上打包工具数不胜数,最火爆的当然是需要_配置工程师_专门配置的webpack,但是在类库开发领域它有一个强大的对手就是 rollup。
现代市面上主流的库基本都选择了 rollup 作为打包工具,包括Angular React 和 Vue, 作为基础类库的打包工具 rollup 的优势如下:
虽然上面部分功能已经被 webpack 实现了,但是 rollup 明显引入得更早,而Scope Hoisting更是杀手锏,由于 webpack 不得不在打包代码中构建模块系统来适应 app 开发(模块系统对于单一类库用处很小),Scope Hoisting将模块构建在一个函数内的做法更适合类库的打包.
由于 JavaScript 各种诡异的特性和大型前端项目的出现,代码检测工具已经是前端开发者的标配了,Douglas Crockford最早于2002创造出了 JSLint,但是其无法拓展,具有极强的Douglas Crockford个人色彩,Anton Kovalyov由于无法忍受 JSLint 无法拓展的行为在2011年发布了可拓展的JSHint,一时之间JSHint成为了前端代码检测的流行解决方案.
随后的2013年,Nicholas C. Zakas鉴于JSHint拓展的灵活度不够的问题开发了全新的基于 AST 的 Lint 工具 ESLint,并随着 ES6的流行统治了前端界,ESLint 基于Esprima进行 JavaScript 解析的特性极易拓展,JSHint 在很长一段时间无法支持 ES6语法导致被 ESLint 超越.
但是在 Typescript 领域 ESLint 却处于弱势地位,TSLint 的出现要比 ESLint 正式支持 Typescript 早很多,~目前 TSLint 似乎是 TS 的事实上的代码检测工具~.
注: 文章成文较早,我也没想到前阵子 TS 官方钦点了 ESLint,TSLint 失宠了,面向未来的官方标配的代码检测工具肯定是 ESLint 了,但是 TSLint 目前依然被大量使用,现在仍然可以放心使用
代码检测工具是一方面,代码检测风格也需要我们做选择,市面上最流行的代码检测风格应该是 Airbnb 出品的eslint-config-airbnb
,其最大的特点就是极其严格,没有给开发者任何选择的余地,当然在大型前端项目的开发中这种严格的代码风格是有利于协作的,但是作为一个类库的代码检测工具而言并不适合,所以我们选择了eslint-config-standard
这种相对更为宽松的代码检测风格.
以下两种 commit 哪个更严谨且易于维护?
最开始使用 commit 的时候我也经常犯下图的错误,直到看到很多明星类库的 commit 才意识到自己的错误,写好 commit message 不仅有助于他人 review, 还可以有效的输出 CHANGELOG, 对项目的管理实际至关重要.
目前流行的方案是 Angular 团队的规范,其关于 head 的大致规范如下:
当然规范人们不一定会遵守,我最初知道此类规范的时候也并没有严格遵循,因为人总会偷懒,直到用commitizen
将此规范集成到工具流中,每个 commit 就不得不遵循规范了.
我具体参考了这篇文章: 优雅的提交你的 Git Commit Message
业务开发中由于前端需求变动频繁的特性,导致前端对测试的要求并没有后端那么高,后端业务逻辑一旦定型变动很少,比较适合测试.
但是基础类库作为被反复依赖的模块和较为稳定的需求是必须做测试的,前端测试库也可谓是种类繁多了,经过比对之后我还是选择了目前最流行也是被三大框架同时选择了的 Jest 作为测试工具,其优点很明显:
当然以上是主要工具的选择,还有一些比如:
那么以上这么多配置难道要我们每次都自己写吗?组件的具体实现才是组件库的核心,我们为什么要花这么多时间在配置上面?
我们在建立 APP 项目时通常会用到框架官方提供的脚手架,比如 React 的 create-react-app,Angular 的 Angular-Cli 等等,那么能不能有一个专门用于组件开发的快速启动的脚手架呢?
有的,我最近开发了一款快速启动组件库开发的命令行工具--create-component
利用
create-component init <name>
来快速启动项目,我们提供了丰富的可选配置,只要你做好技术选型后,根据提示去选择配置即可,create-component 会自动根据配置生成脚手架,其灵感就来源于 vue-cli和 Angular-cli.
说了很多理论,那么实战如何呢?设计一个通用组件试试吧!
轮播图(Carousel),在 Antd 中被称为走马灯,可能是前端开发者最常见的组件之一了,不管是在 PC 端还是在移动端我们总能见到他的身影.
那么我们通常是如何使用轮播图的呢?Antd 的代码如下
<Carousel>
<div><h3>1</h3></div>
<div><h3>2</h3></div>
<div><h3>3</h3></div>
<div><h3>4</h3></div>
</Carousel>
问题是我们在Carousel
中放入了四组div
为什么一次只显示一组呢?
图中被红框圈住的为可视区域,可视区域的位置是固定的,我们只需要移动后面div
的位置就可以做到1 2 3 4四个子组件轮播的效果,那么子组件2目前在可视区域是可以被看到的,1 3 4应该被隐藏,这就需要我们设置overflow 属性为 hidden来隐藏非可视区域的子组件.
复制查看动图: https://images2015.cnblogs.com/blog/979044/201707/979044-20170710105934040-1007626405.gif
因此就比较明显了,我们设计一个可视窗口组件Frame
,然后将四个 div
共同放入幻灯片组合组件SlideList
中,并用SlideItem
分别将 div
包裹起来,实际代码应该是这样的:
<Frame>
<SlideList>
<SlideItem>
<div><h3>1</h3></div>
</SlideItem>
<SlideItem>
<div><h3>2</h3></div>
</SlideItem>
<SlideItem>
<div><h3>3</h3></div>
</SlideItem>
<SlideItem>
<div><h3>4</h3></div>
</SlideItem>
</SlideList>
</Frame>
我们不断利用translateX
来改变SlideList
的位置来达到轮播效果,如下图所示,每次轮播的触发都是通过改变transform: translateX()
来操作的
搞清楚基本原理那么实现起来相对容易了,我们以移动端的实现为例,来实现一个基础的移动端轮播图.
首先我们要确定可视窗口的宽度,因为我们需要这个宽度来计算出SlideList
的长度(SlideList
的长度通常是可视窗口的倍数,比如要放三张图片,那么SlideList
应该为可视窗口的至少3倍),不然我们无法通过translateX
来移动它.
我们通过getBoundingClientRect
来获取可视区域真实的长度,SlideList
的长度那么为:
slideListWidth = (len + 2) * width
(len 为传入子组件的数量,width 为可视区域宽度)
至于为什么要+2
后面会提到.
/**
* 设置轮播区域尺寸
* @param x
*/
private setSize(x?: number) {
const { width } = this.frameRef.current!.getBoundingClientRect()
const len = React.Children.count(this.props.children)
const total = len + 2
this.setState({
slideItemWidth: width,
slideListWidth: total * width,
total,
translateX: -width * this.state.currentIndex,
startPositionX: x !== undefined ? x : 0,
})
}
获取到了总长度之后如何实现轮播呢?我们需要根据用户反馈来触发轮播,在移动端通常是通过手指滑动来触发轮播,这就需要三个事件onTouchStart
onTouchMove
onTouchEnd
.
onTouchStart
顾名思义是在手指触摸到屏幕时触发的事件,在这个事件里我们只需要记录下手指触摸屏幕的横轴坐标 x 即可,因为我们会通过其横向滑动的距离大小来判断是否触发轮播
/**
* 处理触摸起始时的事件
*
* @private
* @param {React.TouchEvent} e
* @memberof Carousel
*/
private onTouchStart(e: React.TouchEvent) {
clearInterval(this.autoPlayTimer)
// 获取起始的横轴坐标
const { x } = getPosition(e)
this.setSize(x)
this.setState({
startPositionX: x,
})
}
onTouchMove
顾名思义是处于滑动状态下的事件,此事件在onTouchStart
触发后,onTouchEnd
触发前,在这个事件中我们主要做两件事,一件事是判断滑动方向,因为用户可能向左或者向右滑动,另一件事是让轮播图跟随手指移动,这是必要的用户反馈.
/**
* 当触摸滑动时处理事件
*
* @private
* @param {React.TouchEvent} e
* @memberof Carousel
*/
private onTouchMove(e: React.TouchEvent) {
const { slideItemWidth, currentIndex, startPositionX } = this.state
const { x } = getPosition(e)
const deltaX = x - startPositionX
// 判断滑动方向
const direction = deltaX > 0 ? 'right' : 'left'
this.setState({
direction,
moveDeltaX: deltaX,
// 改变translateX来达到轮播组件跟随手指移动的效果
translateX: -(slideItemWidth * currentIndex) + deltaX,
})
}
onTouchEnd
顾名思义是滑动完毕时触发的事件,在此事件中我们主要做一个件事情,就是判断是否触发轮播,我们会设置一个阈值threshold
,当滑动距离超过这个阈值时才会触发轮播,毕竟没有阈值的话用户稍微触碰轮播图就造成轮播,误操作会造成很差的用户体验.
/**
* 滑动结束处理的事件
*
* @private
* @memberof Carousel
*/
private onTouchEnd() {
this.autoPlay()
const { moveDeltaX, slideItemWidth, direction } = this.state
const threshold = slideItemWidth * THRESHOLD_PERCENTAGE
// 判断是否轮播
const moveToNext = Math.abs(moveDeltaX) > threshold
if (moveToNext) {
// 如果轮播触发那么进行轮播操作
this.handleSwipe(direction!)
} else {
// 轮播不触发,那么轮播图回到原位
this.handleMisoperation()
}
}
我们常见的轮播图肯定不是生硬的切换,一般在轮播中会有一个渐变或者缓动的动画,这就需要我们加入动画效果.
我们制作动画通常有两个选择,一个是用 css3自带的动画效果,另一个是用浏览器提供的requestAnimationFrame API
孰优孰劣?css3简单易用上手快,兼容性好,requestAnimationFrame
灵活性更高,能实现 css3实现不了的动画,比如众多缓动动画 css3都束手无策,因此我们毫无疑问地选择了requestAnimationFrame
.
双方对比请看张鑫旭大神的CSS3动画那么强,requestAnimationFrame还有毛线用?
想用requestAnimationFrame
实现缓动效果就需要特定的缓动函数,下面就是典型的缓动函数
type tweenFunction = (t: number, b: number, _c: number, d: number) => number
const easeInOutQuad: tweenFunction = (t, b, _c, d) => {
const c = _c - b;
if ((t /= d / 2) < 1) {
return c / 2 * t * t + b;
} else {
return -c / 2 * ((--t) * (t - 2) - 1) + b;
}
}
缓动函数接收四个参数,分别是:
通过这个函数我们能算出每一帧轮播图所在的位置, 如下:
在获取每一帧对应的位置后,我们需要用requestAnimationFrame
不断递归调用依次移动位置,我们不断调用animation
函数是其触发函数体内的 this.setState({ translateX: tweenQueue[0], })
来达到移动轮播图位置的目的,此时将这数组内的30个位置依次快速执行就是一个缓动动画效果.
/**
* 递归调用,根据轨迹运动
*
* @private
* @param {number[]} tweenQueue
* @param {number} newIndex
* @memberof Carousel
*/
private animation(tweenQueue: number[], newIndex: number) {
if (tweenQueue.length < 1) {
this.handleOperationEnd(newIndex)
return
}
this.setState({
translateX: tweenQueue[0],
})
tweenQueue.shift()
this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex))
}
但是我们发现了一个问题,当我们移动轮播图到最后的时候,动画出现了问题,_当我们向左滑动最后一个轮播图div4
时,这种情况下应该是图片向左滑动,然后第一张轮播图div1
进入可视区域,但是反常的是图片快速向右滑动div1
出现在可是区域..._
因为我们此时将位置4设置为了位置1,这样才能达到不断循环的目的,但是也造成了这个副作用,图片行为与用户行为产生了相悖的情况(用户向左划动,图片向右走).
目前业界的普遍做法是将图片首尾相连,例如图片1前面连接一个图片4,图片4后跟着一个图片1,这就是为什么之前计算长度时要+2
slideListWidth = (len + 2) * width
(len 为传入子组件的数量,width 为可视区域宽度)
当我们移动图片4时就不会出现上述向左滑图片却向右滑的情况,因为真实情况是:
图片4 -- 滑动为 -> 伪图片1
也就是位置 5 变成了位置 6
当动画结束之后,我们迅速把伪图片1
的位置设置为真图片1
,这其实是个障眼法,也就是说动画执行过程中实际上是图片4
到伪图片1
的过程,当结束后我们偷偷把伪图片1
换成真图片1
,因为两个图一模一样,所以这个转换的过程用户根本看不出来...
如此一来我们就可以实现无缝切换的轮播图了
我们实现了轮播图的基本功能,但是其通用性依然存在缺陷:
以上都是可以对轮播图进行拓展的方向,相关的还有性能优化方面
我们的具体代码中有一个相关实现,我们的轮播图其实是有自动轮播功能的,但是很多时候页面并不在用户的可视页面中,我们可以根据是否页面被隐藏来取消定时器终止自动播放.
github项目地址
以上 demo 仅供参考,实际项目开发中最好还是使用成熟的开源组件,要有造轮子的能力和不造轮子的觉悟
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/add-two-numbers
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
维护一个 进位 符,链表从左到右按位数 单个运算 保持是否进位的思路 即可。
两个链表若是长度不一样,短的链表的缺失位则用 0代替
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function (l1, l2) {
let needAdd = 0 //是否要进位
let result = new ListNode(0) //初始化返回的链表结构
let obj = result //copy指针 为下面提供操作
while (needAdd || l1 || l2) {
let tmp = (l1?.val != null? l1.val:0) + (l2?.val != null?l2.val:0) + needAdd
needAdd = tmp >= 10 ? 1 : 0 //判断是否大于10 进位
let cur = tmp % 10 //当前节点的值 也就是 取余10 的个位数
obj.next =new ListNode(cur) //当前节点的值
obj = obj.next//下一个循环的操作的 next 值
if(l1 != null) l1 = l1.next //下一个循环的 l1下一个节点
if(l2 != null) l2 = l2.next//下一个循环的 l2下一个节点
}
return result.next;
};
]]>晚上照常回到家,逗一下猫。葵依然是活泼好动 哈哈~
[vplayer url="https://hblyan.oss-cn-beijing.aliyuncs.com/blog/515e6287c888be67e523ebc06667c59b.mp4" pic="https://hblyan.oss-cn-beijing.aliyuncs.com/blog/20221129074517.png" /]
白天,就发现小富贵上厕所是窜稀,然后就是疯狂去厕所(还好自己不会乱拉),看的出来是非常难受。
一整天,小富贵就是找地方藏起来,蜷缩身体或者翻过来(可能这样肚子舒服一点),看着一天没吃东西,我中途还给他为了点毛条和鸡胸肉,但是不到10分钟就会吐出来。我在网上和抖音研究了一天,买了推荐的一些止吐和止泻的药(奥美拉挫、蒙脱石散、肽克痢等),白天和晚上 都为喂了药。
[scode type="red"]宠物生病,网上的只能参考,小病可以尝试一下,一两天无效就真的要去花钱治疗了![/scode]
今天早上一看,还是虚脱的躺在窝里(平时永远不会去的窝,可能感觉快要去世,怀旧),我又抓起一点 猫粮和毛条给他吃点。但是都是吐了,10分钟左右......
[album]
[/album]
出于要上班的缘故,我就又喂了一点肽克痢,晚上回来 再看。
看着这可怜的娃,哈哈就是想吃
[vplayer url="https://hblyan.oss-cn-beijing.aliyuncs.com/blog/a8f6868086ef041ac3ca1e1f652cf66a.mp4" pic="https://hblyan.oss-cn-beijing.aliyuncs.com/blog/20221129080203.png" /]
因为又吃了点猫粮,然后就又又又又吐了 = = !
[vplayer url="https://hblyan.oss-cn-beijing.aliyuncs.com/blog/759767c0f2504abb2fe329bbbfb81305.mp4" pic="https://hblyan.oss-cn-beijing.aliyuncs.com/blog/20221129080333.png" /]
唉~看着有点可怜。委屈巴巴~
[album]
[/album]
网上看的药物还是有点不靠谱(不能对症下药),还是去了附近的宠物医院,到了之后,肯定是老程序走一套:粪检、血检、体温等,等待半小时后,也没有查出什么。但是精神状态还是可以的,所以医生就给开了药,打了止吐针等。
[album]
[/album]
初步推断是吃的东西不太好,我就是买一些便宜的猫粮和毛条,但是吃了三四个月没有什么问题,另一只也没有问题。毕竟这种也不好说,出了问题肯定这些都是又嫌疑的。
[album]
[/album]
好贵,一颗药几十网上买也就是几十块一盒吧,天下人赚天下人钱,也没有太多抱怨~
回家后,看着感觉好多了,然后两小时后喂了东西也没有吐(果然多花钱还是比病急乱投医管用),接下来就是看看这两天的状况了。
[vplayer url="https://hblyan.oss-cn-beijing.aliyuncs.com/blog/516fc2383b1b5ad62b96b86669a667f3.mp4" pic="https://hblyan.oss-cn-beijing.aliyuncs.com/blog/20221129081727.png" /]
养宠物还是要慎重,毕竟肯定要花时间花精力的。我平时就是看便宜点,猫也不生病。以后还是得注意点,猫粮还是买稍微好点吧。平时宠物生病的时候,网上的方案还是参考一下,如果两天不起效果肯定要去医院,当然去医院随便一趟都是大几百多则几千(真的比我精贵),只能祈祷,少生病,多负责吧~ 🙇
]]>