正文从这开始~
就算是专业的Web开发者,可能也很难管理好CSS。在这个系列里,你将了解十四条关于CSS的最佳实践。在第一部分,你将学习如何掌握CSS特殊的层次结构,你会发现!important声明和ID选择器的危险,也会学习到过高优先级选择器带来的代价,最后你还会学习如何规避浏览器带来的不一致性。让我们开始学习吧。
第一条:掌握CSS特殊的层次结构
让我们从最艰难的一步开始。如果你想要成为一个CSS忍者,那么这是你必须掌握的,即便你不想成为忍者,也至少应该了解它。如果你只是想在同事朋友面前吹嘘一下,那么这块知识你一定要懂。有大量的人自称他们是Web开发者或者Web设计人员。
经常被忽略的基础知识
有趣的是,很多人对这个话题一无所知。这些人搭建了网站或者Web应用却不知道CSS是怎么工作的。他们知道一些层叠的知识,或者浏览器如何阅读选择器的(从右到左)。想象一下你认识一个对汽车的认知还不错的人,但你会让这个人给你造一辆车吗?恐怕不会。
不过如果你需要让人给你搭一个网站,你就需要寻找一个专业人士了。如果你同意我的话,那么真正专业的人士是会想去掌握好他的手艺的人。这种人会着迷于任何一个细节。如果CSS层次就是其中一个细节呢?或者是一个CSS最佳实践呢?那么,忽略这个细节的人就不能称之为专业人士了。
如果你想变得专业,你必须学习它,而如果你想成为忍者,你必须掌握它。你需要了解CSS的任何一个特性。当一个小孩问你,你需要能解释得出来,虽然不太可能发生。当然,你潜在的客户或者老板可能会问你关于这方面的东西。这个话题就是一个测试知识点的很好方式。
正如讨论过的,很多开发者对CSS特殊的层次结构知之甚少。因此这个问题可以说是帮助雇主们用来找到人才的好方法。若在网上向自由职业者提出这个问题,有多少人能够回答呢?
CSS特殊层次结构的困难之处
那么既然它如此重要,为什么它是CSS最容易被忽略的部分之一呢,而且为什么极少的CSS实践中会提及到?大概是因为你可能需要做一些数学运算。在这个层次结构中,每个CSS选择器都有一些特定的值。而且当你组合起选择器时,这些值会增加。因此简单的元素选择器(如a)比两个元素的选择器(如a span)的层次低。
通配符选择器它的值是最低的,几乎等于零。可以说所有的选择器都优先于它。排在后面的是元素选择器,它的值是1。然后是带有伪元素的元素,这个结合起来值是2。一直如此下去。最高值是元素选择器、ID选择器、class选择器再加上一个元素选择器的组合,这个组合的值是112。一个ID选择器的值差不多就是100了。(这段不大懂得话详见下表)
这里提供一个CSS特殊层次结构的概述。下边有个简单的表格可以帮助你快速找到层次,不用你自己去计算。如果你想要更深入了解的话,可以参考下这篇我写的短文,不然你经常会需要回看这个表格。
CSS层次特性表:
第二条:避免使用!important声明
第二条实践和层次结构也有关系。接下来两条也是如此。我希望这会让你知道特殊的层次结构的重要性。言归正传,这条实践就是永远不要使用!improtant声明。为什么我会把杜绝!improtant放在CSS的最佳实践里呢。原因是因为它会导致你不能避免的恶性循环。
惹祸上身
想象下你和一个用!improtant声明的人工作在一块。你不得不再次用!imprtant声明来覆盖样式。你能怎么办呢?你有几个方案可供选择。第一你可以冒险移除你同事用的!improtant声明,后果是可能会破坏掉一些东西,你需要测试保证移除完所有东西依旧运行良好。但是这会花费很多时间。那么需要考虑下值不值得这样去做。第二个选择是运用你的CSS层次结构知识。解决这个声明的办法就是提高选择器的层级。假设你的同事是用一个span元素加上一个类名,这意味着你需要的是比一个class选择器更高的级别,这个值是10。你可以有几种选择实现。第一种是用两个元素选择器和一个class选择器。第二种是用一个ID选择器。第三种是元素选择器+ID选择器+类选择器+元素选择器。但你在接下来会认识到用ID不是CSS的最佳实践,所以你会排除这个选项,所以这意味着你不得不用两个元素选择器加上一个类选择器,否则你会违背其他的CSS最佳实践。
解决!improtant问题
我所举的例子可能看起来不算什么大问题。但当发现你不得不再次覆盖!improtant时会发生什么状况?这就是使用improtant的问题所在。前一个总是会传染给下一个。这就是我所说的恶性循环。你用了一个!important后很快你会不得已使用另一个。所以我相信唯一的解决办法就是不要使用!improtant。
处理这个声明的方式类似于对待毒品。最好的办法就是坚持CSS最佳实践然后永远不要起这个头。否则就是自找麻烦。这也是我鼓励你使用 CSSlinter的原因。因为你可能很容易又会忘了这条实践。所以使用一些工具来测试你的代码是有效良好的。而且如果你使用Gulp,你可以使用gulp-csslint或者gulp-sass-lint,只要力所能及都应该实现自动化。
第三条:不要使用ID
样式化元素时千万不要用ID。为什么说它属于CSS最佳实践呢?第一,ID是不可复用的,你只能在一个页面中用一个特定的ID元素。所以如果你想要在同一个页面样式化更多的元素,你需要使用更多的ID和更多行的CSS。第二,这是恶性循环的开始,我又要扯到CSS层次结构了。我们在前边的!improtant中有提到这个。第三,对于ID的争议是它通常用来指特定的元素,那么你就不可能让你的CSS变得简短又可复用。让样式太过特制化不是个好主意。相反的你应该让样式更具抽象和通用性。这可以帮助你重复地使用同一个样式。如果你在每个页面都用了超过一个特定的ID,那么可复用的CSS这个目标就不可能实现。
那么性能呢?
确实ID选择器的运行速度比class选择器要快。如果你定义了1000条规则,ID的性能会快一毫秒。这是在你只在一个页面定义了一个ID选择器的情况下发生的,所以这并不是什么显著的性能优势。而且你只有在至少定义了1000规则之后才能获得这个优势,对于小项目来说这通常是一个还蛮大的数字。
我们还必须考虑的另一件事是额外的ID选择器会带来什么性能影响。==理论上当你添加任何ID选择器时,性能优势其实已经没有了。==而且如果你想获得任何性能优势的话,你不能在ID后边添加其他选择器。如果添加了像class或者元素选择器会发生什么呢?浏览器会从右到左阅读CSS,这意味着后边添加的选择器会首先被读取到,ID的速度优势也就被冲击掉了。
结论就是性能提升的唯一途径必须通过纯ID的选择器。同时你需要有足够多的CSS规则让优势显现。既然1000条规则可以有一毫秒的优势,那么你应该把目标放在100,000条规则上。这个大概可以给到100毫秒的优势。另外的事就是浏览器扫描渲染这么多CSS规则需要的时间。所以你应该避免使用ID为妙。
什么时候适合使用ID
在进入下一条实践前我先再说多一点。我是主张从你的CSS或者SASS中摆脱掉ID,但这不意味着你需要完全地从HTML标记中移除掉它。ID有一个具体的用例,就是使用到JS。正如讨论过的ID更快,所以能用来提升你的JS速度,同样的JS有个getElementByID()方法。
这个方法用来获取带有ID的元素,你可以使用ID作为唯一标识符来赋给需要额外功能的元素。这样做的另一个好处就是分离CSS和JS。当你从一个元素中去掉class时,你不用担心会破坏掉JS。所有你的JS只需要关联到ID即可。但是我还是想说我并不喜欢使用ID。
当我用到JS绑定时,我会使用带有”js-“前缀的class。这帮我区分哪些类是用于样式哪些是用于JS。现在我只会在一种状况下使用JS,在通过href属性给页面中某个元素绑定锚标记的时候。这样用户点击锚标记时页面会滚动到特定的区域。除了这个例子我不认为其他地方有需要使用到ID。
第四条:避免层次过高的元素
我保证这是最后一条关于CSS层次结构的最佳实践。层次过高的元素是什么意思?就是你给一个元素赋予了过多复杂的选择器。假设你想要一个锚标记表现得像一个按钮,符合最佳实践的做法是使用像btn类的东西,你的CSS或者SASS样式表可能就会包含像.btn {...}的代码。
当你使用了这个选择器,可能就会有类似a.btn{...}的代码,更极端点之后会变成a.btn.btn-big.btn-primary{...}。重点就是使用元素选择器变得没有必要。这同样适用于其他附加的类。你完全可以只用.btn类。所以避免使用层次过高的选择器可以有三个理由,一是你会想压缩的CSS大小写更少的CSS,和你要想要提高选择器的性能。
使用层次过高的元素的问题
我多次提到浏览器阅读CSS是从右到左。同时浏览器也在选择位于每个选择器后的元素。比如aricle h1{...},浏览器会先寻找所有h1标签,然后寻找所有内嵌在article标签里的h1标签。那么按钮的例子呢,浏览器会先寻找带有.btn-primary的元素,然后是同时带有.btn-primary和.btn-big的元素。a和.btn选择器同理。
综上所述,浏览器需要四个周期来呈现一个按钮。这高效得了吗?想象下你为了买些小东西会跑四趟超市吗?你当然会一次性买齐所有的吧。因此浏览器若在一个周期内完成,按钮的渲染才不会浪费时间。
网站变得更快可以提升用户体验,避免层次过高的元素会让浏览器渲染CSS更快些。更快的CSS渲染意味着你的站点会加载得更快。这就是你的目标。所以避开层级过高的元素意味着实现良好的用户体验。
第五条:reset还是normalize
思考下这个问题,当你开始一个新项目,你会先重置所有的元素的默认样式吗?如果没有的话我建议你把这部分加入你的工作流程里去。为什么呢?不同的浏览器会试图以不同的方式渲染一些CSS样式。这导致了或大或小的不一致设计。结果就是你的网站在Chrome,Firefox,Edge都会看起来有点不一样。试想下当你在客户端打开你的站点时发现布局乱套了。
你可以通过让浏览器渲染一致来解决这个问题。有两件事是你可以做的,一,自己手动重置所有元素,这可能会花费一些时间。如果每个项目都这样,可能并不高效。那么第二种方式就是使用预制的CSS样式表,把重置的样式表放在你主要的样式表前边即可。
reset还是normalize
当你选择第二种方式时,你需要考虑多一件事。你会采用哪种样式表来使浏览器呈现一致?目前有两种样式表可选,Nicolas Gallagher和Jonathan Neal写的Normalize或Eric A. Meyer写的Reset。
这两种样式表是不同的,它们遵循不同的方法来移除浏览器的不一致性。Normalize只着眼于常规正确的样式;同时旨在保留默认样式,没有完全移除它们。它包含了一些小的修正来提高可用性。它也相对来说更模块化些,你可以提取或删除你不需要的部分。
Reset样式表则不太一样。它通过移除所有的样式来达成同质化的视觉样式,给几乎所有元素设定了广泛性的默认样式。它除去了所有的样式像粗体斜体等等。结果就是strong、em、span看起来都差不多。从这个角度讲,reset就像一个大锤子,normalize则像一把 刀。reset坚决地重置所有东西,normalize则是重置需要重置的。
因此这些样式表的代码和文件大小也是不同的。当然这些区别并不是很重要,但是它们是确实存在的。不管怎样,你应该选择哪个呢?看你更喜欢哪种方式了。如果你想要一个彻彻底底的重置,就选择reset。如果不需要那么极端的话就选择normalize。别忘了在normalize里添上些可用性的改进。我个人是偏爱Normalize的。
第六条:别重置所有东西
假定你同意重置了,但不要急于下载运行。这并不和上一条矛盾。你需要考虑清楚你是不是真的需要这个东西。无论是normalize还是reset,都包含了大量你可能不会运用到你的项目里的元素。因此没有必要让这个代码扩大你的CSS。
我的建议同时也是CSS最佳实践的第六条,是去定制预制的样式表。你需要选择你会用到的样式。当然,少数的额外几行CSS并不会对性能或者文件大小有什么太大影响。但是这不代表你可以浪费资源。如果想要用户体验良好,你需要利用一切机会去优化你的网站,这就是其中一个。
要记得所有这些样式表是会在很多种不同状况下工作的,这意味着你项目中一半的代码是没有用的。同时你个人的编码风格可能会覆盖掉一些这样的代码,这些重复性的东西是可以删除的。另一个解剖和定制样式表的原因是能得到更好地理解,当你分离reset或者normalize时,你可以清楚看到原本是什么东西。
结果就是你会知道它们是怎么工作的。这些样式表是由专业的Web开发者维护的。光是阅读源码和进行运用你就可以学习到很多东西。也可能帮助你找到更适合自己的样式表。
不要想CSS最佳实践了
这就是今天我带给你的所有CSS最佳实践。我相信这六条会帮助你提升你的CSS代码。让我们快速回顾一下,第一,掌握CSS特殊的层次结构。这是CSS经常被忽略的一部分。但是你能看到今天许多最佳实践都和它有关。第二,避免使用improtant声明,同时不要使用ID进行样式书写。这两件事可以避免恶性循环。
之后是避免层级过高的元素。它会让浏览器执行过多渲染你样式的不必要周期。这会降低性能和带来不佳的用户体验。最后是使用reset或者normalize样式表来避免浏览器的不一致。但要确保根据你具体的状况来量身定制。不然的话就会让你的CSS变得冗杂。
关于本文
译者:@安生
原文:
每天早读,三万同行相伴成长
欢迎投稿:181422448@qq.com
【第1533期】Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)
前言
这波操作很秀。今日早读文章由趣店@蔡珉星投稿分享。
正文从这开始~~
结合趣店 FED 在过去小半年的实践经验,我们开发了首个 Taro 三端统一应用:taro-yanxuan(高仿网易严选微信小程序),用以探讨 Taro 多端开发的正确姿势。
趣店 FED 早在去年 10 月份就已全面使用 Taro 框架开发小程序(当时版本为 1.1.0-beta.4),至今也上线了 2 个微信小程序、2 个支付宝小程序。
之所以选用 Taro,解决微信小程序原生开发的痛点是一方面,另一方面团队也有多端统一开发的诉求,Taro 无疑是当时支持最好的。另外 React 也符合团队的整体技术栈,可显著降低团队学习成本。
可以说,Taro 在小程序端、H5 端支持程度已经不错,也有不少上线实例可以查看,但在 React Native 的支持上,Github 中公开的项目在 RN 这块均未适配:
这种现况可以理解,毕竟要做到多端统一是有一定难度的,需准确把握各端差异,并做出合理取舍,而 Taro 虽以多端为设计目标,可重心在小程序端,没有对多端做出一定的开发约束,无从下手也便正常。笔者曾在 2018 iWeb 峰会 - 厦门站做过《多端统一开发实践》的分享,提到用 Taro 开发 RN 端的坑与大体思路,并加以实践。
结合趣店 FED 在过去小半年的实践经验,我们开发了首个 Taro 三端统一应用:taro-yanxuan(高仿网易严选微信小程序),用以探讨本文的重点:Taro 开发多端应用的正确姿势。
在线预览
可在线预览 H5、RN 端(直接调用了网易严选接口,若要体验登录、购物车功能,请使用网易邮箱账号登录):
在线预览:
如下是 React Native 的运行截图:
样式管理
样式管理是多端开发的首要挑战,因为 React Native 与一般 Web 样式支持度差异较大,上述几个未适配 RN 的多端项目多数已栽在样式上了,用到了大量 RN 不支持的样式,这种情况再要去兼容 RN 无异于重写页面,想必也是有心无力了。这也是本文所强调的,需把握正确的多端开发姿势。
样式上 H5 最为灵活,小程序次之,RN 最弱,统一多端样式即是对齐短板,也就是要以 RN 的约束来管理样式,同时兼顾小程序的限制,核心可以用三点来概括:
使用 Flex 布局
在进一步阐述之前,需先了解 RN 端几个影响样式方案的主要差异:
使用 Flex 布局,不单单是因为 RN 的 View 标签有默认样式 display: flex; flex-direction: column,更重要的是 Flex 可以解决幽灵空白问题:
// View 标签高度不会是 100px,图片下方会有几像素空白,称为幽灵空白
const imgStyle = { height: '100px' }
<View>
<Image src={...} style={imgStyle}
View>
常规解决方案是在 View 标签上设置 font-size / line-height: 0, 或 Image 标签 display: inline-block 等,但这些在 RN 中都不支持,给 View 标签设置 display: flex 算是唯一可靠方案了。
何况 Flex 布局能力强大,为啥不用呢?只需要注意一点,RN 中 View 标签默认主轴方向是 column,如果不将其他端改成与 RN 一致,就需要在所有用到 display: flex 的地方都显式声明主轴方向。
基于 BEM 写样式
RN 实际上只支持一种样式声明方式,即声明 style 属性:
const viewStyle = { height: '100%' }
<View style={viewStyle}
这也导致 Taro 在 RN 端基本只支持 class 选择器这一种写法(最终编译成对象字面量),BEM(Block Element Modifier)在此处就恰如其分的发挥了作用:
例如每行 2 个元素的列表,每行最后 1 个元素有特定样式,用伪元素选择器 :nth-child(even) 很容易实现,在 RN 中就需要自行计算了:
{list.map((item, index) => (
<View className={classNames('block__element',
index % 2 === 1 && 'block__element--even'
)} />
)}
基于 BEM 写 class 样式,不依赖其他选择器,虽然会让代码稍显繁琐,但也能保证多端都是行得通的,不存在支持问题。
采用 style 属性覆盖组件样式
小程序、RN 在页面、组件间传递样式时均有问题:
// 目前 Taro RN 端还未实现往组件传递 className 对应样式
<CompA compClass='my-style' />
// CompA,样式不生效
<View className={this.props.compClass} />
上述场景小程序虽可通过组件外部样式 externalClasses 实现,但官网文档有强调 “在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此最好避免这种情况”;用全局样式倒是可以,但这样样式就不好维护了。
那么,通过 style 传递、覆盖组件样式也就成了唯一可选方案了。需要注意一点,样式文件是会经过编译处理兼容多端的,但 style 方式需要运行时兼容:
style={postcss({ background: '#fff' })} />
// 简单演示,如 RN 不支持 background,需改成 background-color
function postcss(style) {
const { background, ...restStyle } = style
const newStyle = {}
if (background) {
newStyle.backgroundColor = background
}
return { ...newStyle, ...restStyle }
}
从这个角度看,styled-components 或许是多端开发的最佳样式方案,然而 Taro 还不支持,且微信小程序官方文档中也提到 “尽量避免将静态的样式写进 style 中,以免影响渲染速度”,全部样式都用写到 style 中恐怕就不靠谱了,但只用来覆盖少量样式不见得会有太大影响。
样式兼容
即便是把握了如上样式管理思路,多端样式差异的问题依然存在,例如 white-space: nowrap 这个样式在 RN 端会报错,Taro 有提供解决方案:
.text {
/*postcss-pxtransform rn eject enable*/
white-space: nowrap;
/*postcss-pxtransform rn eject disable*/
}
但项目中不止一处会有这个问题,都这样写实在不太美观,可以用 Sass mixins 稍微封装下:
@mixin eject($attr, $value) {
/*postcss-pxtransform rn eject enable*/
#{$attr}: $value;
/*postcss-pxtransform rn eject disable*/
}
.text {
@includes eject(white-soace, nowrap);
}
Sass mixins 并不能解决差异,但对于部分各端不兼容的样式,通过 Sass mixins 统一处理是比较合理的方式,代码相对美观也方便维护。
端能力差异
相较于样式,端能力的差异倒是还好,各端差异是客观存在的,更不用说 RN 在 iOS 与 Android 上就已存在大量差异。
应对端能力差异,要么改变实现思路,例如 RN 端还不支持 Taro.(get/set)StorageSync,那就改用 async / await + Taro.(get/set)Storage 实现,要么就得使用环境判断方式了。
Taro 提供 process.env.TARO_ENV 用于环境判断,多数小的差异都可以用这种方式来解决:
function foo() {
if (process.env.TARO_ENV === 'weapp') {
// 微信小程序逻辑
}
if (process.env.TARO_ENV === 'h5') {
// H5 逻辑
}
if (process.env.TARO_ENV === 'rn') {
// RN 逻辑
}
}
这个时候也比较考验开发者的封装能力了,一般是建议将这些差异逻辑的判断统一起来,例如在 src/utils 中进行封装,对外提供一致的接口,尽量不要在业务页面中杂糅太多的判断。
而对于简单的环境判断处理不了的问题,就只能动用原生开发了,例如 Taro 还不支持 RN 端的 WebView 组件,就需要自己用原生 RN 实现:
// Taro 页面,根据环境引入 RN 原生页面
import { WebView } from '@tarojs/components'
const WebViewRN = process.env.TARO_ENV === 'rn' ? require('./rn').default : null
export default class extends Component {
render() {
return process.env.TARO_ENV === 'rn' ?
<WebViewRN src={this.url} /> :
<WebView src={this.url} />
}
}
// 原生 RN 页面,从 react-native 引入 WebView
import Taro, { Component } from '@tarojs/taro'
import { WebView } from 'react-native'
export default class WebViewRN extends Component {
render() {
const source = { uri: this.props.src }
return <WebView source={source} />
}
}
process.env.TARO_ENV 的处理是编译时而不是运行时,也就是说若不是编译 RN,上述用原生写的 RN 页面不会被打包,保证了编译成其他端时不会引入不支持的内容。
原生页面能够引入,多端问题也就有了基本的实现保障。
Taro RN 端的坑
Taro RN 端目前小问题还是不少的,本项目开发过程中也顺带解了几个 bug:
除此之外还有好几个问题,时间关系还未提 pr 解决,暂且先绕过,但其中有两个坑还是值得一说的。
onClick
RN 的 View 标签不支持 onClick ,但这又是很通常的需求,原生解决方式是套一层 Touchable 组件,如:
onPress={this.handlePress}>
{...}
而 Taro 是引入 PanResponder 响应用户操作:
{...PanResponder.carete({ ...})}
style={wrapperStyle}
>
style={innerStyle} />
问题在于这样多嵌套了一层 View,并把样式拆分成 wrapperStyle、innerStyle 分别应用,但样式拆分有问题,导致绑定 onClick 之后元素的样式错乱了,这点在开发过程中还是相当坑的。
宽高自适应
onClick 的问题也还好,改改样式能绕过去,宽高自适应的坑就比较尴尬了。
小程序、H5 可用 rpx / em 实现自适应,而 RN 的自适应方案麻烦些,一般需通过 Dimensions 获取宽高再进行换算。Taro.pxTransform() 可解决该问题,但编译 RN 端样式文件时并没有考虑这点,即 width: 100px 会被编译成 width: 50,而不是 width: Taro.pxTransform(100),无法适配屏幕不同的屏幕尺寸。
因此,目前 Taro RN 端还不好做到自适应,要么非百分比的宽高都用 style + Taro.pxTransform(),要么就得自己写个脚本去处理编译后的样式文件。
这两个问题都提了 issue 2204 2205,有需要的可以关注下解决进度
其他
要做到多端统一,能说的细节点实在太多,上述实现思路虽然简单,但背后也都是隐含着对各端差异的斗争与取舍,本文也仅是列出最基本的几点,用于阐述 Taro 多端开发的核心思路。
本项目代码没有做过多封装,方便阅读,也实现了足够多的样式细节进行踩坑,具体涉及的踩坑点、注意事项都在代码中以注释 // TODO(Taro 还未支持的)、// NOTE(开发技巧、注意事项)注明了,更多内容就有待各位去实践、体会了。
总结
如前言所说,Taro 虽然是以多端为设计目标,但重心是小程序端,RN 端目前的支持情况不算特别理想。但充分理解多端差异、掌握正确的多端开发姿势(特别是样式管理方面,避免项目成型后再去兼容需要大动刀斧)之后,在简单的项目上是完全可以一展拳脚的。
若说 2 个礼拜开发一个小程序,是稀疏平常的事,但 2 个礼拜即搞定了小程序端(微信、支付宝、百度等等),还搞定了 H5、React Native 端,后续更新也只要改一处地方,这产出、维护效率就实在太惊人了,这大抵也就是 “Write once, run anywhere” 的魅力所在(虽然在前端领域极容易发展成 “Write once, debug everywhere” )
相信随着小程序热度不断上升,还会有更多优秀的开源框架、解决方案涌现。而我们不倾向于造轮子,更关注基于现有方案如何更好地去开发多端应用。
项目开源地址:
关于本文