回想当年,在高考结束后我的分数并不高,然后被调剂到了工业设计,再到后来感觉对计算机更感兴趣,于是对了很久的线努力转专业到了计算机,之后废了九牛二虎之力在大二一年修完了计算机专业大一大二两年的课程,到了大三开始搞事就开始做了一些项目。我发现项目做多了就很容易将项目串联起来,这点非常有意思。
本着分享与开源的精神,我与大家分享我的项目经验,我也就是凭借着这些项目成功在23
届的秋招混进了字节,老大后来跟我说我是23
届唯一一个双非进入我们大团队的,虽然我可能是当时秋招最菜的一个,但是最终还是混进去了,所以也算是希望分享一下为之付出的努力,与君共勉。计算机还是非常有意思的,我也希望能够保持对于技术的好奇心,同样也希望能做出一些有意思的项目来获得一丝成就感。
实际上我也只是昨天突发奇想想总结分享一下我做过的项目,因为写过的东西还是比较多的,所以也总结了好久。其实这其中我觉得最重要的问题是为什么要做这些项目,有些是想解决平时遇到的问题,比如学校App
不好用就做个好用的、网页不让复制我就做个解除复制限制的,也有些是想提升技术水平做的,比如我写过的Webpack
相关的、用Canvas
实现的简历编辑器,还有一些是希望深入研究的,比如在线文档协同系列、富文本编辑器相关的项目。下面介绍的都是我的开源项目,内容基本也是按照时间线来的,也欢迎关注我的 GitHub:WindrunnerMax 。
此外需要声明的是,这很像是一篇论文的综述,文章的信息密度非常高,特别是很多与项目相关的链接,如果从零开始接受相关的内容还是需要一些时间的。
首先说说为什么要做这个小程序,这其中有个背景是当时学校有个查各类信息的软件叫智校园,但是这个App
很不好用,经常崩溃,在某一天我上午第一节课下课后这个App
就打不开了,导致我想查个自习室都查不了,当时我在J7
教学楼爬了四层楼都未找到自习室,当时就觉得很烦,于是就想自己做一个,不用这个了。
当时我还算是个计算机小白,毕竟在课上老师们大都还是照本宣科,书本上的东西也都比较死板,我就只能自己研究。回去之后我大概研究了两种方案,分别是搞App
逆向看是否有什么逻辑问题,抓包看是否是数据问题。首先我是逆向了这个包没看出什么逻辑的毛病,不过也不是没有收获,这时候我拿到了接口请求的地址,然后我就测试请求是否有问题,这一测我就发现了问题所在,仅仅是一个拿初始化数据的接口失败了而已,而这个数据在我这里完全可以跳过,毕竟我只需要手动在我自己的服务器上维护这部分数据即可,于是配合之前逆向拿到的接口,一个全新的小程序就上线了,并且还集成了很多数据源扩展了很多功能。
最开始的时候只是我自己在用,第一版本是我在上课期间不务正业一个上午写完的,做的也比较粗糙,在更新几版之后,几乎全校的同学都用上了,数据大家可以看下。
再到后期因为很多原因比如多场景、小程序认证等问题,我又更新了很多个小程序以及App
版本 GitHub:SHST-SDUST 。我个人觉得如果能够做一个大家都用的项目,并且能够不断将学习到的新知识应用到其中,比如我曾经尝试过迁移TS
山科小站小程序,以及做过组件库的封装 微信小程序校历组件 GitHub:Campus,以及后期我从UniApp
框架迁移到了Taro
,同样也是从Vue
迁移到了React
实现。这样的项目会比纯技术的项目比如各种学习写mini
框架会更有业务价值一些,不过这是两个方向上的价值,如果能结合技术和业务价值那是最好的。此外我觉得面试官同样也希望能够希望候选人能够在生活中运用自己的技术解决问题,能够保持对于技术的热爱与好奇心,这样更容易跟面试官聊到一起。也可以参考下当时我秋招时简历上对于项目的描述,PS
: 数据并不是最新的。
实际上从本质来说这个小程序就是单纯的爬虫项目,在我对爬虫比较了解之后,我还做了一些有意思的事情,大家可能或多或少都受到过学校在各种平台/App
上刷任务的通知,为了减少我自己的工作量,在当时我还做了个爬虫来帮助我们全班同学刷评论 GitHub:Younger ,帮了团支书一个大忙。还有一个是一个小游戏的辅助 GitHub:EliminVirus ,当时在朋友的带领下玩了消灭病毒这个小游戏,这个小游戏真的有意思,但是凭借我风骚的走位打了十几次硬是过不去230
关,于是便有了这个辅助脚本,这个脚本不仅仅是一个爬虫,这其中还涉及到逆向微信小程序、微信ROOT
抓包等知识,并且还写了文章 手机抓包HTTPS (Fiddler & Packet Capture) Recover刷机简介,此外还有一些诸如校园网认证的自动登录 GitHub:GIWIFI 之类的就不过多介绍了。
在实现山科小站小程序的时候,其中还有一个很大的问题是验证码需要识别,当时对于验证码识别的方法不是很懂,于是边学边查,最开始的时候就是用的最基本的OpenCV
来处理像素,即使这个验证码很简单,但是同样是因为我对像素处理的不好导致识别率很低,经常要刷新几次才能成功识别。
到了后来不断完善了识别的能力,于是将其抽离出来独立出了验证码识别的仓库。在做上边介绍的刷评论 GitHub:Younger 脚本时,验证码变得很复杂,此时通过像素来处理就变的很吃力,于是学习了通过CNN
来处理验证码 GitHub:Example,并且到了后期搞了脚本的脚本用来实现自动训练的能力,并且也将其应用到了强智的验证码识别当中。
再到后来,因为在多种语言上的需要,将其封装了Js
脚本的实现,帮助我在网页上登录强智的时候可以自动识别验证码而不需要我再手动输入了,再到后来因为学校的教务系统开启了公网访问,并且部署了HTTPS
证书,国内的公网网站通常都是需要备案的,所以在此基础上我通过小程序 GitHub:SHST-ULTRA 实现了直接请求教务系统的方案,这意味着只要网络环境允许,小程序可以一直运行而不需要我的服务器介入。
这是之前跟@FubinSama
一起做的小学期作业,C++(MFC)
实现的飞机大战,虽然代码写的很烂但是我觉得还是很有意思的,也算是在前端领域之外的技术扩展。
这个项目应该很多同学都用过,在GreasyFork
上的下载安装量已经超过260
万了,这其中还不包括其他下载渠道比如GitHub
、脚本猫等等。最开始做这个项目的原因是老师让我们写某个报告,然后我就准备先“参考”着看看网上的内容,发现很多类似的文库网站都不让复制,所以便有了这个脚本 文本选中复制。
虽然这仅仅是一个解除网页复制限制的脚本,但是其中却涉及到很多技术细节,比如在调试的时候我应该怎么确定我剪贴板里有什么数据 Canvas图形编辑器-我的剪贴板里究竟有什么数据 、我应该怎么写脚本 深入浅出脚本管理器与油猴脚本。如果遇到的都是基本的DOM
文本还好说,毕竟无论如何文本都是下载到本地并且在DOM
树上,无论我怎么处理都可以,但是如果是Canvas
绘制的文本应该如何处理、如何设计更加通用的方案、如何适配的各个网站并且管理各个模块、如何将数据写到剪贴板等等都是需要考虑的,当然最难的还是调试这个网站为什么不能复制。之前我遇到的一个最离谱的是,浏览器在选中文本时焦点需要在文本上,而某网站有个按钮一直在抢焦点,这样的话用户就不能复制了,非常的离谱。在这里也可以参考下当时我秋招时简历上对于项目的描述,PS
: 数据并不是最新的。
在后期我发现有人做了个浏览器扩展,其中嵌入了我的代码而且还是我打包过后的GPL
协议的代码,最离谱的是其中还加了广告,此外再加上我也想学习一下浏览器拓展,于是我也就做了一个浏览器拓展,首先从零使用Rspack
搭建了Chrome
扩展的开发环境 从零实现的Chrome扩展,并且研究了脚本管理器的实现 从油猴脚本管理器的角度审视Chrome扩展 ,毕竟我是相当于从脚本管理器的基础上再做一套浏览器拓展的实现 GitHub:ForceCopy 。再之后因为Chrome
主推V3
版本的浏览器扩展实现,而FireFox
主推V2
版本的浏览器扩展实现,我还实现了一套兼容方案 初探webpack之单应用多端构建。实际上我的浏览器插件同样也在这个仓库里,这个仓库是个MonoRepo
,所有写过的脚本都在其中。
我的博客已经写了非常久了,在博客分支中已有455
篇文章,仓库已有1.5k
的star
,这是切切实实从前端小白开始慢慢积攒写起来的文章,仓库的简介是“前端基础 个人博客 学习笔记”,所以如果按照目录一点点开始看的话,最开始的时候能够感受到特别简单,都是基础的HTML CSS JavaScript
,然后越往后会越发的上强度,因为这实际上就是我的学习历程,都是这几年来由浅入深慢慢学习的过程。
写到这里我想起来之前我在群里说的暴论,把我的博客学明白了进字节应该问题不大,当然在这里也仅供参考,还有个例子是有位同学把我所有的文章都打印出来学习,最后也是双非进了字节,当然能进字节跟他自己的努力是分不开的,但是我的博客肯定也是起到了一点作用,要不谁会闲的没事画上百大洋去打印文章,毕竟一百大洋还是能吃不少好东西的。此外很多同学去买一些这种课那种课,有我免费的博客不看而非得去花钱买课是何必呢,我写的内容也都是由浅入深,特别是我的行文风格就是把问题写的特别详细,如果从头开始看基本不会出现理解不了的情况,因为我写文章同样也还是要给自己看的,时间太长我怕我自己都会忘所以对于问题的描述以及相关思考都写的非常详细,基本都会配有DEMO
可以帮助理解。
这里还要引用一下我README
的内容,这是一个前端小白的学习历程,如果只学习而不记录点什么那基本就等于白学了。这个版本库的名字EveryDay
就是希望激励我能够每天学习,下面的文章就是从2020.02.25
开始积累的文章,都是参考众多文章归纳整理学习而写的,文章包括了HTML
基础、CSS
基础、JavaScript
基础与拓展、Browser
浏览器相关、Vue
使用与分析、React
使用与分析、Plugin
插件相关、Patterns
设计模式、Linux
命令、LeetCode
题解等类别,内容都是比较基础的,毕竟我也还是个小白,此外基本上每个示例都是本着能够即时运行为目标的,新建一个html
文件复制之后即可在浏览器运行或者直接可以在console
中运行。如果想按照我写的顺序进行阅读的话可以 查看目录 ,另外如果想更条理地查看的话可以访问 我的博客,博客同时也是本版本库的gh-pages
分支,是作为纯静态页面搭建在Git Pages
上的,使用jsdelivr
以及cloudflare
作为缓存缓解国内访问速度问题。后期还在gh-pages-ssg
分支上部署了SSG
版本的 新版博客,并且借助ChatGPT
提供了英文翻译版本,分支是部署在Vercel
上来缓解国内访问速度问题。在博客中的内容就相对比较多了,除了学习笔记之外还有一些做项目时的记录以及遇到的坑等。
初探富文本之富文本概述
初探富文本之编辑器引擎
初探富文本之OT协同算法
初探富文本之CRDT协同算法
初探富文本之OT协同实例
初探富文本之CRDT协同实例
初探富文本之React组件实时预览
初探富文本之富文本diff算法与文档对比视图的实现
在实习的时候,在偶然的机会上我接触到了富文本编辑器,并且感觉对在线文档产生了比较大的兴趣,本来我想着在公司里实践一下,但是因为业务线不同并且本身不赚钱的或者说收益比较低的变更,在商业排序上是比较靠后的,所以我就想自己做一个并且实践一下。在这其中学习了很多富文本的知识 基于slate构建文档编辑器,对于Rollup
打包工具也有更深的了解 Rollup的基本使用,学习并发布了NPM
包以便复用富文本能力,同样也有部署在GitPages
的 在线DEMO。
在现在看来,当时很多设计都是有问题的,插件化这部分设计不是很完善,Core
模块也没有真正完整抽离出来,并且代码写的也并没有那么完善,但是这个编辑器为我以后做的工作打下了基础,并且不知道大家是否看到在文中第一段我说的“我发现项目做多了就很容易将项目串联起来,这点非常有意思”,这个文档编辑器就在我很多项目中集成。
同样引用一下README
的内容,基于slate.js
构建的文档编辑器,slate
提供了控制富文本的core
,简单来说他本身并不提供各种富文本编辑功能,所有的富文本功能都需要自己来通过其提供的API
来实现,甚至他的插件机制也需要通过自己来拓展,所以在插件的实现方面就需要自己制定一些策略。在交互与ui
方面对于飞书文档的参考比较多,整体来说坑也是比较多的,尤其是在做交互策略方面,不过做好兜底以后实现基本的文档编辑器功能是没有问题的。
依旧先说说为什么要做这个项目,在这里主要是遇到了一个问题,当时正值秋招时期,我也为怎么更新简历发愁,于是我借了学长的某简历平台账号想做来个简历,但是模版嘛你懂的,很多细节都不满意,限制太多,经常要么就是某个模块的内容必须要按照固定格式写,还有诸如行间距等内容都不能调,要么就是写的内容比较多,本来预览的时候还挺好,导出的时候PDF
就超过了一页。
在某个夜晚我正在洗澡,然后灵光一闪我为什么不自己做一个简历编辑器呢,我设想的简历编辑器是什么样子的,那一定是自由拖拽调整位置、自由调整大小、导出的简历保证是一页,那么这不就是一个很好的低代码产品嘛!在专有领域上低代码还是有价值的,并且还能够串联我上边的文档编辑器项目!简直是一举多得,于是这个项目便被确立了起来 基于NoCode构建简历编辑器。
实际上我秋招的简历就是用我的这个简历编辑器实现的 在线DEMO,前文中项目介绍的简历截图也就是用这个项目生成的,这同样也是我说的项目串联中的一环,我自己的简历是用我自己的项目实现的,并且这个项目还串联了我的富文本项目,这可以称作项目的自举。
初探webpack之编写plugin
初探webpack之编写loader
初探webpack之从零搭建Vue开发环境
初探webpack之单应用多端构建
作为专业搞前端的,不理解Webpack
的打包实现显然是不合适的,所以我也学习过一些Webpack
的实践,并且写了相关博客和DEMO
。目前看起来在工作中学会这些暂时是足够的,如果有再深入的使用的话肯定还需要继续学习并输出文章。
此外,我还是觉得我们学习了东西最重要的还是解决问题,例如我学习了Webpack
的plugin
和loader
之后,确确实实帮我解决了实现浏览器扩展的时候兼容Chrome
和FireFox
的问题 GitHub:Script 。
这个项目是与工作内容有关系的,在实现某个需求的时候,我们需要实现一种类似于动态编译的能力,当时敲定的实现是基于MarkDown
的方案,后边恰逢周末我就趁着这两天时间调研并开始写了这个博客 初探富文本之React组件实时预览(React Playground),后边也成功说服了大家,项目的方案也更换为基于这个方案的类似实现。
大家可能也注意到了上边我引用的博客还是与富文本相关的,那么也就是说这个项目又成功串联了我的富文本项目,实际上在ReactLive
的 在线DEMO 以及文档编辑器的 在线DEMO 中都有实现,在ReactLive
中是与博客内容完整相关的DEMO
,其中还涉及到了多个编译器性能测试的相关部分。
实际上这个项目在文档站中的应用场景会比较契合一些,在一些场景中比如组件库的文档编写时,我们希望能够有实时预览的能力,也就是用户可以在文档中直接编写代码,然后在页面中实时预览,这样可以让用户更加直观的了解组件的使用方式,这也是很多组件库文档中都会有的一个功能。
初探富文本之OT协同算法
初探富文本之CRDT协同算法
初探富文本之OT协同实例
初探富文本之CRDT协同实例
实际上我们聊了很多文档编辑器的内容,但是大都是本地实现的独立的编辑器,涉及到多人协作的时候,独立的编辑器就会出现问题,例如用户A
和B
同时在写文档,很明显在两位同学进行保存的时候就会出现文档覆盖的问题,在这种情况下我们就需要文档协同来调度。
当然协同的成本还是比较高的,如果成本不能接受的情况下使用悲观锁的方式也是可以接受的,顾名思义是基于一种以悲观的态度类来防止一切数据冲突的方式,其以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在其释放锁之前其他的任何人都不能对数据进行操作。
但是有个场景非常特殊,那就是划词评论的能力,设想一个场景,我们的在线文档系统是由线上状态和草稿状态组成的,当然这也是常见的解决方案,此时我线上的文档文本是xxxx
,草稿被添加过文字了此时的状态为yyyyxxxx
,如果此时用户在线上的xxxx
四个字也就是索引为0-4
上添加了评论,那么此时在草稿的0-4
为yyyy
,这样的话添加评论的位置就产生了错误,导致下次发版本到线上之后评论的位置就错了。
为了解决上述的问题实际上就必须要引入协同算法来解决,协同本质上就是引入了合并与冲突的解决算法,所以在离线状态引入协同算法来解决实际问题也是有很大价值的,是做在线文档功能必不可少的一环能力。
所以我研究了大概得一个月的协同方案,并且又用了一个月的时间写了上述的四篇文章,当时是在实习的过程中写的,只能靠周末的时候学习,所以还是比较费劲的,尤其是我希望本着简单的原则提供DEMO
出来,这就更耗费了时间,所以这几篇文档真的是很有质量的内容。
在之前看到了某个项目,其流程图编辑器的UI
跟我印象中很久之前用的ProcessOn
特别像,然后我就比较好奇这件事,后来慢慢了解到ProcessOn
可能是基于mxGraph
实现的(现在可能是自研SVG+Canvas
方案),又因此重新看到了DrawIO
这个项目,于是我就想既然ProcessOn
能够做出集成方案,那整体来说方案应该是可行的,于是开始调研这个项目。
因为我的目标是纯前端的NPM
包的集成方案,在研究了一段时间后,我将整个项目迁移了过来,并且完成了 在线DEMO,并且将能力归类整理出文章 基于drawio构建流程图编辑器,并且在这里我依然尝试着将项目串联起来,将流程图编辑器这个项目作为插件嵌入到了我的文档编辑器 在线DEMO 当中。实际上实现整个嵌入功能的时候并不简单,特别是将其作为独立的NPM
包发布的这部分,我提交了60+
次来做项目的兼容与BUG
处理,mxGraph
是比较老的项目了,代码都是纯Js
并且都是基于原型链的修改,或者我这么说吧,即使是在我精简之后,Graph.js 这一个文件的代码就有10637
行。
在前一段时间,我想在手机上向电脑发送文件,因为要发送的文件比较多,所以我想直接通过USB
连到电脑上传输,等我将手机连到电脑上之后,我发现手机竟然无法被电脑识别,能够充电但是并不能传文件,因为我的电脑是Mac
而手机是Android
,所以无法识别设备这件事就变得合理了起来。那么接着我想用WeChat
去传文件,但是一想到传文件之后我还需要手动将文件删掉否则会占用我两份手机存储并且传输还很慢,我就又开始在网上寻找其他软件,这时候我突然想起来了AirDrop
也就是隔空投送,就想着有没有类似的软件可以用,然后我就找到了Snapdrop
这个项目,我觉得这个项目很神奇,不需要登录就可以在局域网内发现设备并且传输文件,于是在好奇心的驱使下我也学习了一下,并且基于WebRTC/WebSocket
实现了类似的文件传输方案,并且在实现的过程中解决了如下问题:
IP
地址等信息。WebRTC
的P2P
方案,无需服务器中转数据。NAT
穿越受限的情况下,基于WebSocket
服务器中转传输。通过这种方式,任何拥有浏览器的设备都有传输数据的可能,不需要借助数据线传输文件,也不会受限于Apple
全家桶才能使用的隔空投送,并且在实现的过程中我还拓展了多文件发送、文本消息、尝试公网连接等能力,总结起来通过这种方式我们可以获得如下的收益:
IOS/Android/Mac
设备向PC
台式设备传输文件的场景等等。AP
隔离功能,即使因为各种原因比如合租时房东对路由器设备开启了AP
隔离,我们的服务依旧可以正常交换数据,这样避免了在路由器不受我们控制的情况下通过WIFI
传输文件的掣肘,但是这通常不适用于大型公司的对称型NAT
,至于原因我们后边也会聊到。P2P
数据交换,在现在IPv6
的普及下,如果设备支持IPv6
并且拥有公网IPv6
地址的话,是可以直接进行数据传输的,点对点的数据交换不会受限于服务器的数据转发,并且隐私性会更高一些,当然因为国内网络环境的复杂性以及运营商对于UDP
数据包的支持受限、IPv6
地址只出不进等等限制,公网传输的实用性还是差一些的。在耗费了两个周末的时间之后,将整个功能完善了出来 仿照AirDrop(隔空投送)优雅地在局域网中传输文件,并且可以尝试使用 在线DEMO,因为本身数据不会经由服务器转发,所以部署后大概占用20MB
左右的内存,使用效果可以查看README
的 视频。
掘金老给我推Canvas,于是我也学习Canvas做了个简历编辑器
Canvas图形编辑器-数据结构与History(undo/redo)
Canvas图形编辑器-我的剪贴板里究竟有什么数据
最开始本着学习的态度以及对技术的好奇心来做的,因为暂时找不到可以用Canvas
实现的比较有意思的场景,所以才选择了继续做简历编辑器 在线DEMO。因此除了一些工具类的包例如 ArcoDesign
、ResizeObserve
、Jest
等包之外,关于 数据结构packages/delta
、插件化packages/plugin
、核心模块packages/core
等都是手动实现的,项目中的富文本内容还是用我的文档编辑器实现的,又将项目串了起来,并且整个项目还是用Rspack
打包的基准环境,前边学习的Webpack
又派上了用场帮我解决了一个打包问题。
实际上这也是本着 自己学习的项目能自己写就自己写,公司/商业化项目能有已有包就用已有包 的原则来的,在这里的目标是学习而不是做产品,自己学习肯定是希望能够更多地接触相对底层一些的能力,自己可以多踩一些坑会对相关能力有更深的理解,如果是公司的项目那肯定是成熟的产品优先,成熟的产品对于边界case
的处理以及积攒的issue
也不是轻易能够比拟的。
此外,除了想学习的理由之外,针对于市面上简历编辑器本身的问题与痛点,也做了相关的解决方案:
Canvas
实现的简历编辑器都是图形,完全依靠画布绘制图形,在给定的基础图形上可以任意绘制,不会有排版问题。PDF
,所以在设置页面大小后,导出的PDF
总会是保持一页,看起来会更美观。虽然这只是一个小小的简历编辑器,但是针对编辑器涉及的能力还有很多,整体来看会涉及到很多技术问题,例如数据结构、History
模块、复制粘贴模块、画布分层、事件管理、无限画布、按需绘制、性能优化、焦点控制、参考线、富文本、快捷键、层级控制、渲染顺序、事件模拟、PDF
排版等等,目前来说想要做好还是有很大难度的,也是一个可以深入研究的项目。
感觉还是做了一些比较有意思的项目的,我个人觉得如果是搞技术的话,保持对技术的好奇心是一件很重要的事,能用技术来解决平时遇到的问题也是非常重要的一点,昨天听群友说他做了个项目用算法来下棋,面试官问他这个项目有什么用,我觉得这就是个很有意思的项目,过年回家的时候跟大爷下棋能跟大爷下的有来有回可不是有用吗,提供情绪价值也是很重要的。
再说回来我做的这些项目,最开始我的目的就非常简单,我觉得这件事有意思我就去做了,并且在做的过程中不断解决了很多问题,并且也不断提升了技术水平,还能写很多博客,何乐而不为。其实在这里我还思考过一个问题,什么是好项目,这个问题就很宽泛,我觉得在这其中很重要的是将做过的项目联系起来,或者真的将其用到了自己的生活中,这对于自己来说就是好项目,或许对于面试来说这个项目的价值并不大,但是如果能对自己有所帮助,无论是生活上还是技术上,那就很重要。
还有一个问题我觉得也是值得讨论的,内卷和努力的区别究竟是什么,我目前的理解是内卷一定是有多个人一起在恶性的竞争,比如两个人在比谁工作的时间长,谁加班加得多,这种就是纯纯的内卷,努力的话是一个人也可以做的事,当然多个人也可以,大家没有利益冲突,相互学习相互帮助,一个人的话就是做着自己感兴趣的事,比如每天下班后学习英语。但是话又说回来了,个人的努力实际上是在更大范围上来说还是竞争,大家肯定也体会到整个环境越来越卷了,从另一个角度上讲,这或许也是社会发展的动力所在吧。
那么搞了这么多项目,那么我的朋友,代价是什么呢?代价就是到现在都还没有对象,过年回家差点就要去相亲了,我很难绷。