当我在头条写React时,我是如何考虑技术选型与设计的

发表于 3年以前  | 总阅读数:750 次

前言

最近在业务中开发了一套定制化的 C 端组件库,在这个过程中遇到了一些组件库技术选型和设计的问题,在参考公司内外的多个组件库后确定了最终的方案。本文希望通过向读者介绍技术选型的过程中的方案比较和组件库设计中的考量,让读者在组件库的技术选型和设计上有所启发。

一个完整的组件库方案的思路

组件库的技术选型

样式方案选择

事实上,这三种样式方案可以并存,但实际开发以其中一种为主。

Sass/Less

这是大家最熟悉的方式,它的优点是足够灵活、开发成本低(绝大多数工程师都熟悉它们)、 完全支持外部覆盖组件的样式,缺点是难以调试(需要到 runtime 才能知道命中的规则),以及难以实现静态分析。

Atomic CSS

在 UI 足够标准化的情况下,使用 Atomic CSS 能实现更小的包体积大小,对于单个组件,除了极少数无法抽象的样式以及自定义动画,不再需要声明其他样式。当然它的缺点是代码可读性稍稍降低。同时开发者需要先熟悉项目的原子样式,增加了一定的开发成本。

CSS-in-JS

CSS-in-JS 指包括 styled-component、Emotion、JSS 等在内的,在运行时通过 js 生成 css 样式的第三方库。CSS-in-JS 这种方案的优点在于能有效解决“组件样式随着数据变化”的问题。但是,它的缺点在于为了支持从外部覆盖内部元素的样式,需要给内部元素加上 className,同时不支持 postcss,取而代之的是特定 CSS-in-JS 库自己的 plugin 生态,少部分库(如 emotion)没有支持 rem 的工具库。另外在做 SSR 和流式渲染时,都需要在 node 层增加提取样式逻辑,增加了开发成本和额外的开销。

小结:在有成熟的 UI 规范的情况下,Atomic CSS 是一个不错的选择,其次,使用传统的 sass/less 来编写样式也利于维护(大部分前端开发者都熟悉它),在选用 CSS-in-JS 方案时则要考虑团队的开发习惯和上手成本。

icon 方案选择

在选择 icon 方案的时候,除了关注渲染质量,我们还应该关注它的灵活性,以便具有更好的适配能力。

iconfont

iconfont 这种方案的优点在于兼容性最好,支持 IE6 及以上版本。但是,由于 iconfont 方案是将 icon 作为文本来使用,在 webkit 内核的浏览器下由于对文字有抗锯齿,导致渲染失真。另外,由于将所有的 icon 打包成一个字体文件,不支持按需加载,包体积偏大。这样很容易导致在加载完成 icon font 后页面的重刷新

base64 引入

base64 也是一种常用的方法,但是由于将 svg 作为背景图引入,只能控制它的大小,不能覆盖它的颜色,也更不能修改 svg 内部的元素,不够灵活。对于常常采用 MPA 结构的端内 h5,不利于 icon 在不同 SPA 之间复用。同时 base64 字符串的长度是 svg 文件(优化后)的 1.3 倍左右。

React Component、SVGUseElement 和直接写入 svg 元素

这三种方式本质上都是将 svg 作为 html 元素进行渲染,但具体的使用方式不同。

svg 的基本能力的兼容性除了在 IE11 以下不支持动画和缩放,基本没问题,而 svg effect(主要是使用 transform、filter 等属性)在 android4.4 以上的支持良好。svg 的动画性能有瓶颈,幸运的是我们可以使用 css 动画来替代它。

直接写入 SVG 元素的方式缺点在于完全无法复用同一个 icon

而 SVGUseElement 的具体实现方式有使用元素、 元素和 SVG fragment identifiers 等方式,但总的来说,都是在顶部声明 svg 元素,在需要使用的地方使用元素引入。具体可以参考使用 SVGUseElement 插入 icon 的例子[1]。它的缺点在于不够灵活,icon 难以在不同页面复用,同时支持 SSR 也比较困难

目前调研的结果,最好的方式是使用 svgr[2] 将 svg 转换为 React Component 来使用,它支持按需加载、完全的样式覆盖能力。同时,它支持自定义 AST 模板,可以在转换时给 svg 元素加入自定义的 className 等,容易实现 icon 自动适配 RTL、Dark Mode(这部分下文会详细介绍)。

svgr 集成了 svgo 对 svg 文件进行优化,它可以抹去 svg 中无用的属性、隐藏元素等,具体的配置可以参考 svgo-github[3]。

小结:目前看来使用 svgr 将 svg 转换生成 React Component 来构建 icon 是最佳的方式,能很方便地按需加载、复用,适配能力也最强。我们可以将 icon 专门做成一个 npm 包,供组件库使用,也可以在业务仓库中直接使用。

组件库的核心设计

深色模式(Dark Mode)适配

事实上,本小节讨论的是业务上使用组件库的 Dark Mode 能力时会遇到的兼容性问题和实际业务场景。但组件库本身就是服务于业务的,从这个角度讲本小节的内容也属于组件库相关的一部分,它指导组件库如何去提供更好的 Dark Mode 适配能力。

多主题能力

深色模式本质上是一种运行时的多主题问题,主要是在运行时支持切换不同的主题色。我们可以使用 CSS 变量来定义颜色,然后在 Sass/Less/Css 中约定使用它:

:root{
    --bg-default: #fff;
}
:root[theme="dark"]{
    --bg-default: #000;
}
.button{
    background-color: var(--bg-default);
}

这样,只要我们在元素中设置自定义属性 theme 的值为 dark,颜色就会自动切换。且我们只要定义好颜色变量,并约定使用它,则开发组件的时候只写一次就可以支持多个主题。

可惜的是 CSS 变量在 android4、IE11 及以下等有兼容性问题。我们有如下三种方案:

我们可以实现一个 postcss plugin 来生成兜底属性,做法类似于:

// 处理前
.button{
    background-color: var(--bg-default);
}

// 处理后
.button{
    background-color: #fff; // 对于不支持css变量的浏览器这行会生效
    background-color: var(--bg-default); // 对于支持css变量的浏览器这行会覆盖上一行属性
}

它最大的优点在于增大的包大小几乎可以忽略不计,缺点在于对于不支持 CSS 变量的颜色实际上变成了强制展示一套兜底主题色。对于移动端内页面来说,不支持 css 变量的环境可以等同于没有深色模式的环境,可以使用浅色模式的主题色兜底。

我们还有另一种方式来实现兼容,比如下面这样:

.button{
    background-color: #fff;
}
.theme-a .button{
    background-color: #000;
}
.thema-b .button{
    background-color: #ccc;
}

然后在某个根元素上(例如 html)增加 theme-a 这个 class 即可,这样的优点在于完全不会有兼容性问题,缺点在于增加了开发成本,幸运的是,我们可以使用postcss-css-variables[4]来很方便地从 css 变量的写法生成这种声明。它的另一个缺点是随着主题色的增多,会成倍地产生额外的 CSS 包大小。

css-vars-ponyfill 能完美支持多主题色,缺点是会产生固定的额外包大小。

小结:支持运行时多主题色主要使用 css 变量,而业务仓库的解决兼容性问题,可以根据具体情况选择。如果是端内 h5 且只需要深浅色模式,可以考虑使用 postcss plugin 生成兜底属性,否则可以使用 css-vars-ponyfill 或者 postcss-css-variables。

判断 Dark Mode

媒体查询

我们可以很容易的利用 prefers-color-scheme 这个媒体特性来检测 Dark Mode,结合我们 css 变量的使用,就像这样:

:root{
    --bg-default: #fff;
}
@media (prefers-color-scheme: dark) {
    :root{
        --bg-default: #000;
    }
}
// 支持白名单逃逸,再写一次:root下的属性
:root[theme="light"]{
    --bg-default: #fff;
}

白名单逃逸是指在我们的业务中,可能有一部分页面,如活动页、抽奖页等不支持 Dark Mode,我们可以通过在 html 上增加一个 theme 属性来强制为浅色模式。

媒体查询的优点是使用方便,媒体查询会自动监听系统设置的变化(是否开启深色模式)不用在 html 中增加额外代码。缺点在于对需要逃逸的情况,书写比较繁琐

JS API 监听媒体查询

使用 JS API 的例子如下:

<body>
    <script>
        const mql = window.matchMedia('(prefers-color-scheme: dark)');
        function matchMode(e) {
           const $root = document.documentElement;
            if (e.matches) {
                $root.setAttribute('theme', 'dark');
            } else {
                $root.removeAttribute('theme');
            }
        }
        mql.addListener(matchMode);
    </script>
    <div id="root"></div>
</body>

样式的部分就像我们一开始介绍 CSS 变量的例子:

:root{
    --bg-default: #fff;
}
:root[theme="dark"]{
    --bg-default: #000;
}

这个方案的好处是灵活,可以很容易地在脚本里加入其它逻辑支持白名单逃逸。缺点是为了支持 SSR,需要单独将这部分脚本写在 html 模板的 body 元素内最上方,对于组件库的使用方增加接入成本。

小结:从实际业务可能出现的白名单逃逸问题以及业务的变化来看,虽然使用 JS API 监听媒体查询判断 Dark Mode 的方式会少许增加接入组件库的成本。但是和带来的灵活性收益相比来说是值得的,建议使用这种方式。

RTL 适配

组件库如果支持国际化,那么 RTL 是一个必不可少的部分。RTL(right to left) 是指部分语言,例如阿拉伯语是从右往左阅读的,由此带来 UI 上需要左右相反(大部分情况下,有些例外),一些 icon 也需要镜像,手势也是从右往左滑动的,input 输入框从右到左输入,更多细节具体可以参考 《bidirectionality - Material》[5]。

布局适配

我们可以利用原生的 dir 属性[6]来支持大部分的 rtl 能力,即在 html 上设置属性 dir='rtl'。在浏览器环境下可以通过 NavigatorLanguage API 来获取页面语言,进而根据当前语言是否是 rtl 来设置 dir 的值。在 node 环境下可以通过请求头 Accept-Language 获取页面语言,判断得到 dir 的值后注入到返回的页面中。设置 dir='rtl'后,全局的 flex 水平布局会自动反向,文本也会自动右对齐(除非显示声明 text-align)。但包括 marin-left、left、border-left 这类属性(其他方向类似)无法自动适配,解决这个问题有多种方式,我们可以很直观地来看代码:

// 方法1: 样式覆盖方式,ant design使用此方式
.button{
    margin-left: 16px;
}
html[dir='rtl'] .button{
    margin-left: 0;// 要覆盖掉,否则左右都是16px的margin
    margin-right: 16px;
}


// 方法2: 另一种方式,虽然不用覆盖,但是需要将方位属性拆出来
.button{
   // 与方位无关的属性
}
html[dir='ltr'] .button{
    margin-left: 16px;
}
html[dir='rtl'] .button{
    margin-right: 16px;
}

// 方法3: 方法2 + Atomic CSS
html[dir='ltr'] .ms-16{
    margin-left: 16px;
}
html[dir='rtl'] .ms-16{
    margin-right: 16px;
}

我们可以看到方法 1 和方法 2 都不是很方便,而方法 3 需要 UI 非常的规范化(将 margin、padding 收敛到可枚举的状态),也不能覆盖所有的情况。幸运的是,我们可以使用 margin-inline-start 这类 RTL 敏感的属性来解决(更多属性见CSS Logical Properties[7]) :

.button{
   // 其他属性
   margin-inline-start: 16px;
}

在实际使用中还存在一些兼容性问题,我们可以使用 postcss-bidirection[8] 处理,会把上述声明转化为:

.button{
    // 其他属性
}
html[dir="ltr"] .button {
    margin-left: 16px;
}
html[dir="rtl"] .button {
    margin-right: 16px;
}

小结:RTL 的布局适配我们可以使用 RTL 敏感属性,它与 Atomic CSS 不冲突,合适的情况下可以结合起来使用。

icon 适配

在 RTL 下,部分 icon 需要镜像。前面我们已经介绍,icon 的最佳方式是使用 svgr 将 svg 转换为 React Component。这样,我们可以在转换时为需要 RTL 翻转的 icon 增加一个 class,例如 flip-rtl,然后组件库提供以下 CSS 声明供业务使用:

[dir="rtl"] .flip-rtl {
  transform: scaleX(-1);
}

icon 是否镜像可能是偏设计侧的事情,如果我们将 icon 的设计稿托管在 figma 平台上,我们可以和设计师约定需要 RTL 下需要翻转的 icon 的命名,然后实现一个自动下载 svg 源文件、 svgo 处理、 使用 svgr 转换成 React Component 的脚本,并且在转换过程中根据命名自动判断是否需要加上 flip-rtl 这个 class。这样,在组件库和业务开发过程中,研发都不需要关心 icon 的镜像问题,减少沟通和验收成本

手势适配

一些组件,如进度条组件,在传统 LTR 下是从左向右滑动,但是在 RTL 下则是从右向左滑动。我们可以简单地给这类组件增加一个 isRTL 这种 props,但是这显然不是一种很好的做法,使用的时候都要计算并传入 props 值。由此思考,我们可以为整个组件库抽象一些通用能力,全局注入。

全局化配置

对于 direction(LTR/RTL)、 prefixCls(类名前缀)等一些全局配置,我们可以使用 React 的 Context 来注入,例如应用的根节点外面包裹一个 ConfigProvider:

import ConfigProvider from 'myComponent';
// ...
export default () => (
  <ConfigProvider direction="rtl">
    <App />
  </ConfigProvider>);

在组件中使用 hooks 获取:

import { useConfigContext } from 'myComponent';
export default () => {
    const { rtl, prefixCls, platform } = useConfigContext();
    //...
}

使用 Context 甚至可以实现局部的配置和全局不同,它非常灵活,后期可以很方便地扩展全局配置的能力,也解决了我们反复将一些全局通用的属性作为 props 传入各个组件的痛点,缺点在于不利于代码的静态测试。

组件分层

在组件库开发之前,应该先规划好组件库的层级,以增加组件库的代码复用性和使用的灵活性。

我们应该先规划一些基础组件,避免后续的重构。Switch、Checkbox、Radio(它们在逻辑上区别仅仅在于点击激活态后是取消还是依旧激活)可以抽象出一个 BaseSwitch,在它的基础上实现这三个组件。对于 Button,在弹窗组件等其他组件中也会出现,我们可以抽象出一个 BaseButton 或者在其他组件中使用 ConfigProvier 的 prefixCls 重写它的样式。对于表单相关的组件,可以先实现一些原子的 input、textarea,再实现 Form 中带有 lable、 校验状态等和 UI 跟相关的 Form.input 等。对于弹层组件,可以封装一个 Portal 组件提供能力等等。在 Metrial UI 中还抽象了一个 Box 组件,所有的组件都基于 Box 组件编写,实现全局布局和样式的控制。

样式

样式上,如果没有使用 Atomic CSS,我们可以将 UI 规范(字重、文本大小和行高的组合)封装成 sass/less 中的 mixin,降低出错的可能性。还可以封装一些常用的能力,比如文本溢出显示省略号、 0.5px 边框的伪元素实现等。这些封装的变量和 mixin 不仅可以在组件库内部使用,还可以提供给业务方使用(尤其在定制组件库中)。同时要和 UI 约定组件库不同组件的 z-index,以避免不符合预期的层级。

其他

组件库中用到的一些 hooks(比如弹层组件用到的冻结页面的滚动)可以使用 react-use 等主流开源库,也可以定制开发。如果组件库期望支持 preact(一个和 react 语法基本一致但更轻量的库),可以参考 switching-to-preact[9] 来避免在开发过程中使用不支持 preact 的语法。同时,组件库中用到的 utils(一些函数能力)也要考虑兼容 node 环境,以支持 SSR。在不会引入特别大的成本的前提下,组件库应该充分地去考虑业务方可能的技术选型,以避免限制业务上的技术实现。

组件库构建一般使用 tsc 或者 rollup,动画库则根据具体需求选择是否使用(使用 CSS 动画更轻量)。

组件库的其他细节

质量保障

组件库的质量保障从流程上来说,主要是 code review 和严格的 UI 验收、QA 测试等流程。从技术层面来说可以收敛发包权限,结合 semantic-release[10] 在 CI/CD 中实现自动发包,杜绝研发过程中在非 master 分支上随意发包的危险操作。还有单元测试、快照测试、e2e 测试等常用的技术手段,限于本文篇幅不再详细阐述。

规范

制定规范的目的在于保证质量、 方便业务方使用和增加组件库的可扩展性。比如上文提到的对于样式的封装、常用 mixin 封装,强制使用颜色变量等。还有设计统一的组件库 API 风格规范,能降低业务方的使用成本。

提效

组件库一般有一个演示站点,主流的技术选型有 stylegudist、storybook 等,可以根据团队习惯选用。对于移动端组件库,可以通过 webpack 别名的方法重写它们的组件,以支持移动端预览,方便 UI 验收。对于国际化的组件,可以提供类似 vconsole 形式的 devtools,可视化切换 dark/light Mode、rtl/lrt 等能力,提高开发和测试流程中的效率。

一些思考

组件库的开发是一个强依赖 UI 的事情,我们需要和 UI 进行充分的沟通。同时我们不能局限于组件库本身,而要考虑到开发、测试过程中的效率,业务中接入的难易,以及是否能良好地应对业务的变化等,从更全局的视角去思考。另外,如果是通用组件库,则组件库的推广是一个重中之重的事情,有更多的业务方接入,才能推动组件库的进一步迭代,形成良性循环。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/7GDmCxbwVPTNBAHom1BgyA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录