漫谈css模块化,postcss及css之未来

Posted 码上打卡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了漫谈css模块化,postcss及css之未来相关的知识,希望对你有一定的参考价值。

CSS作为前端三大基础技术之一,是前端技术一个重要的组成部分。对于开发者而言,加一些聪明的css技巧及优化,对于提升用户体验很重要。这篇博文中,我们将用一些实例来探索css模块化,postcss技术,及大胆揣测CSS技术的未来之路。

争议和热点

关于CSS热点讨论很多,一个有名的争论是,它作为一种语言,以现在的形式展现,本身是否是好的,是否有值得商榷的地方。我认为双方都有一些道理。例如,css初始设计是为了给一个文档加样式而不是应用的一个组成部分,即将来到的CSS新形式新特点可能会改变这个状况,许多CSS错误认识来源于把样式化作为一个事后的想法,而不是抽出时间学习它或者雇佣一些专业的人来完成它。

我不认为CSS工具本身是争议的源头;我们可能总是可以花最小的时间和精力做不同程度的事情。不过写在JS里的CSS是不同的,它修补了CSS脚本化缺点。这里要说的是,JS控制CSS不是唯一的办法;也不是最新的办法。记得我们经常有相同的讨论关于预处理,像Sass?Sass有很多特性,如混入类,它们不符合任何CSS建议的语法。Sass的产生在不同的背景下,如果我们还是从它自己本身的好坏来争论,这不公平。因为对象自身已经改变。因此我们从JS控制CSS开始讨论。

在编码中,我们应该使用一个能提示代码语法的编码工具。我们拿javascript的Pomise对象来类比。这个特性在IE中不被支持,因此使用它的时候加polyfill(腻子脚本)包。polyfill相当于一个补丁包,可以使不支持某特性的浏览器获得兼容性。还有些类似的语法转换器,像Babel。我们用它使语言的新语法转换成旧的语法以使旧的浏览器可以正常运行。这对我们很有好处,我们可以提前使用语言新的语法和技术来做可以支持当前浏览器的代码,用一些工具或工具包转换一下就可以了。像sass,less,将css技术向前推了一大步。

这边所说的css多样性是指我们应该用工具包或工具来用未来的css技术编码。

预处理器

我们已经讨论了一点Css预处理器,接下来我们将讨论更多细节和它们是怎么进行css和js之间会话的。我们有sass,less,postcss工具可以尝试css新特性。

下面的例子中,我们将看一下嵌套,预处理最常用的特性。我建议用postcss,因为它可以使我们对添加的新特征进行细粒度的控制,这是我们在该例子所真正需要的。我们要用到的postcss插件是postcss-nesting。

我们使用postcss的最佳编译工具就是webpack,在css-loader插件之后加上postcss-loader到配置中。在css-loader后加可选择的插件需要标明为几个,下例中为1:

{  test: /\.css$/,  use: [    
   'style-loader',    {      loader: 'css-loader',      options: {        importLoaders: 1,      },    },    
   'postcss-loader',  ], }

这样就保证从其它css文件也会被postcss-loader处理。

配置好postcss-loader,我们装一下postcss-nesting包,然后把它配置到postcss配置中:

yarn add postcss-nesting

有很多种方法配置postcss。在这个实例中,我们在工程的根目录下添加了一个postcss.config.js文件:

module.exports = {  plugins: {    
    "postcss-nesting": {},  }, }

现在,我们写一个css文件,例如给一个组件Photo用。我们命名为Photo.css:

.photo {  
 width: 200px;  &.rounded {    
    border-radius: 1rem;  } } @media (min-width: 30rem) {  
 .photo {    
    width: 400px;  } }

我们在添加一个css文件utils.css,里面包含一个样式类.visuallyHidden:

.visuallyHidden {  
   border: 0;  
   clip: rect(0 0 0 0);  
   height: 1px;  margin: -1px;  
   overflow: hidden;  
   padding: 0;  
   position: absolute;  
   width: 1px;  
   white-space: nowrap; }

由于我们的组件依赖这个utils.css,我们可以用@import把它导入到Photo.css中:

@import url('utils.css');

有了css-loader,通过webpack就能保证这个引用被编译。

接下来,我们把Photo.css导入到我们的Javascript文件中,然后可以在组件里使用我们定义的class样式:

import React from 'react'
import { getSrc, getSrcSet } from './utils'
import './Photo.css'

const Photo = ({ publicId, alt, rounded }) => (  
  <figure>    <img      className={rounded ? 'photo rounded' : 'photo'}      
     src={getSrc({ publicId, width: 200 })}      
     srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })}      
     sizes="(min-width: 30rem) 400px, 200px"    />
   <figcaption className="visuallyHidden">{alt}</figcaption>  </figure>) Photo.defaultProps = {  rounded: false, } export default Photo

我们看到我们用的class名字太过简单,容易与其它和该组件不相关的样式形成冲突。一个解决办法是可以用一个命名方法学来重命名我们的样式名,比如BEM(边界元法)。不过这样做导致组件变复杂且样式类名字太长,从而使整个项目变得复杂。

下面看一下CSS模块

CSS模块

顾名思义,css模块就是一个css文件,把所有的类样式和动画封闭在一个本地域下。它们看起来和一般css没什么区别。例如:不用修改,我们还用Photo.css和utils.css文件作为一个CSS模块,简单的为css-loader设置一个可选属性:

{  
 loader:
'css-loader',  
 options:
{    
   importLoaders:
1,    
   modules:
true,  },
}

css模块看起来和普通的css非常像,但用的时候很不同。它们被导入js的时候做为一个对象,这时候他们的类名,被自动编译成限制在本组件范围的css代码:

import React from 'react'
import { getSrc, getSrcSet } from './utils'
import styles from './Photo.css'
import stylesUtils from './utils.css'

const Photo = ({ publicId, alt, rounded }) => (  
<figure>    <img      className={rounded        ? `${styles.photo} ${styles.rounded}`        
       : styles.photo}      
     src={getSrc({ publicId, width: 200 })}      
     srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })}      
     sizes="(min-width: 30rem) 400px, 200px"    />
   <figcaption className={stylesUtils.visuallyHidden}>{alt}</figcaption>  </figure>) Photo.defaultProps = {  rounded: false, } export default Photo

因为我们把utils.css作为一个CSS模块使用,所以我们可以把@import声明在Photo.css中移除了。同时,注意样式类使用驼峰命名的格式,这样在Javascript中使用更容易。

CSS模块还有其它的特性,像composition。现在我们把utils.css导入到Photo.js组件中,我们只想把照片说明的样式放到Photo.css中。这样,只要我们的jsx代码执行,那么styles.caption就是另一个样式类名;它只是碰巧在视觉上隐藏了元素,但是将来它的样式可能会有所不同。无论哪种方式,Photo.css来决定。

那么我们在Photo.css加一个照片说明的样式来扩展visualHidden的属性的功能,就用composes:

.caption {  
   composes: visuallyHidden from './utils.css'; }

我们也可以添加更多的规则到该样式中,这个例子中我们就需要这些。现在,我们不用再导utils.css到Photo.js代码中;我们可以简单的用styles.caption代替那种做法:

<figcaption className={styles.caption}>{alt}</figcaption>

这是怎么工作的?是visuallyHidden样式拷贝了一份到caption中吗?让我们检查一下style.caption的值。哇,有两个样式类!这就对了,一个来自visuallyHidden,另一个能应用到任何我们在caption添加的样式中。这种做法就更容易了避免了样式被覆盖,不过CSS模块鼓励拒绝使用已存在的样式。没有必要创建一个新的VisaullyHidden React组件为了使用几个CSS规则。

下面我们测验一下用的不舒服的类成分模式:

rounded  ? `${styles.photo} ${styles.rounded}`  : styles.photo

对于这种情况,有可用的插件,像classnames(https://github.com/JedWatson/classnames),它在更复杂的类成分中很好用。在我们的例子中,我们可以继续使用componses,重命名.rounded为.roundedPhoto:

.photo {  
   width: 200px; }
.roundedPhoto {  
   composes: photo;  
   border-radius: 1rem; } @media (min-width: 30rem) {  
   .photo {    
       width: 400px;     } }
.caption {  
   composes: visuallyHidden from './utils.css'; }

现在我们就可以在我们的组件中使用这些类名,且以一种更可读的形式:

rounded ? styles.roundedPhoto : styles.photo

等等,万一我们不小心把.roundedPhoto规则放在.photo之前,其它使用.photo规则的被.roundedPhoto重写了怎么办?不用担心,CSS模块会阻止我们在当前样式类之后构建样式类,会报一个错误:

referenced class name "photo" in composes not found (2:3)
 1 | .roundedPhoto {> 2 |   composes: photo;    |   ^  3 |   border-radius: 1rem;  4 | }

注意,对CSS模块使用文件命名约定通常是一个好主意,例如扩展名.module.css。

动态样式

到目前为止,我们已经有条件地应用了样式的预定义集,被称为条件样式。比如如果我们想做成可以调整照片的圆角怎么办?这叫做动态造型,因为我们起初不知道设定的值是多少;当应用在运行,就可以动态地改变它,动态地调整它。

动态样式不常使用,通常我们会选择使用条件样式,在一些场景中,假如我们不得不使用,方法是怎样的呢?我们可以使用内联样式的同时,一种native的解决方案是定义属性(也叫css变量)。这种特性的真正有价值的地方是浏览器会以定制属性的方式更新样式,当Javascript代码改变它们的时候。我们可以通过内联样式的方法给一个元素设置定制属性,这就意味着它只对该元素有用:

style={typeof borderRadius !== 'undefined' ? {  '--border-radius': borderRadius,} : null
}

在Photo.css中,我们可以使用定制属性,通过var(),传一个默认的值作为第二参数:

.roundedPhoto {  
   composes: photo;
   border-radius: var(--border-radius, 1rem); }

只要Javascript用到这些了,它只是传了一个动态的参数给到CSS,然后CSS接管,它就照原来的样子应用了这个值,通过cals()方法计算一个新的值,等等。

反馈

浏览器对css-variables的支持情况,可以通过【https://caniuse.com/#feat=css-variables】自己查,不支持的浏览器,在应用中可能也遇不到问题,因为不会使用这些,不过记住,有些样式不那么重要。在这个例子中,如果IE中的边角总是1rem也不是一个大问题。应用在每个浏览器中没有必要看起来完全一模一样。

有相关的插件可以自动反馈所有自定义属性的问题,可以安装postcss-custom-properties,然后在postcss配置文件中配置一下就可以了:

yarn add postcss-custom-properties

module.exports = {  plugins: {  
    'postcss-nesting': {},    
    'postcss-custom-properties': {},  }, }

这样,就会对我们的border-radius规则进行反馈:

.roundedPhoto {  
   composes: photo;  
   border-radius: 1rem;
   border-radius: var(--border-radius, 1rem); }

浏览器如果不识别var(),它就会忽略,然后使用前一个。但不要太偏信插件;它仅仅部分改善了自定义属性的支持情况,然后给出一个静态的反馈。对于动态方面,是没有办法打腻子补丁以寻求支持的。

对Javascript暴露值

在前面的部分,我们探索了在css和js之间可以分享任何东西,对于用媒体查询,我们是不是不可能做到?

以前是不行了,多亏了一些开发者,现在可以做到了。

首先,认识下这个包postcss-preset-env,cssnex【http://cssnext.io/】的继承者。它是一个postcss插件,和@babel/preset-env相似。它包含一些插件,像postcss-nesting,postcss-custom-properties,autoprefixer等等。多亏了它们,我们才可以提前使用未来的css技术。它分离插件为四个层级标准【https://cssdb.org/#staging-process】。我这边要展示的一些特性在默认的级别2+中是没有的,因此我们要明确的在配置中标出哪些我们需要:

yarn add postcss-preset-env

module.exports = {  
     plugins:
{    
         'postcss-preset-env':
{      
            features:
{        
               'nesting-rules':
true,        
               'custom-properties':
true, // already included in stage 2+        
                'custom-media-queries':
true, // oooh, what's this? :)             },           },      },
}

注意,我们覆盖了我们之后安装的插件,因为postcss-preset-env中已经配置了它们,这也代表我们的代码会和之前一样有效的工作。

在媒体查询(media queries)使用自定义属性是无效的,因为设计如此。不过可以使用custome-media-queries【https://preset-env.cssdb.org/features#custom-media-queries】代替:

@custom-media --photo-breakpoint (min-width: 30em);
.photo {  
   width: 200px; } @media (--photo-breakpoint) {  
 .photo {  
    width: 400px;  } }

尽管它还在实验阶段,因此没有浏览器支持,但多亏了postcss-preset-env,它可以工作!一个问题是PostCSS是基于每个文件进行操作的,因此这种方式仅在Photo.css可用--photo-breakpoint。

最近,postcss-preset-env的作者往里面加了一个importFrom【https://github.com/csstools/postcss-preset-env/blob/f53ea9b859fa4d1f57894a6e845a815cf8b4562f/README.md#importfrom】属性,它可以传自定义属性到其它插件,像postcss-custome-propertis和postcss-custom-media。它的值可能意味着很多东西,但在我们的例子中,它就是一个文件路径,会被导入到postcss可以处理的文件中。我们姑且称这个可以处理的文件为global.css,然后把我们自定义的媒体查询移到这边:

@custom-media --photo-breakpoint (min-width: 30em);

下面是我们定义importFrom配置,提供路径给global.css:

module.exports = {  plugins: {    
     'postcss-preset-env': {      importFrom: 'src/global.css',      features: {        
         'nesting-rules': true,        
         'custom-properties': true,        
         'custom-media-queries': true,      },    },  }, }

现在定义了这个配置,我们文件中的@custom-media标识就可以删除了。删除之后--photo-breakpoint依旧可以工作,因为postcss-preset-env会从global.css编译它。自定义属性和自定义选择器是一样的。

现在,我们怎么把这些提供给Javascript呢?当这些实验特性,像自定义媒体查询被标准化且在大部分浏览器都提供支持时,我们将可以在本地的Css文件中找到他们。例如,这是我们如何访问一个定义在:root上的--font-family自定义属性的:

const rootStyles = getComputedStyle(document.body)
const fontFamily = rootStyles.getPropertyValue('--font-family')

如果自定义媒体查询可以标准化,我们将可能可以用相似的方法使用。但同时我们不得不找一个替代选择。我们可以用exportTo【https://github.com/csstools/postcss-preset-env/blob/f53ea9b859fa4d1f57894a6e845a815cf8b4562f/README.md#exportto】选项把它构建成Javascript文件或者Json文件,这文件就可以导入到Javascript代码中。不过,问题是webpack要求先require进来,才生成它。即便我们在webpack之前生成它,每个对global.css的更新就会引起webpack重复编译两次,一次生成输出文件,一次导入它。我需要一个没有阻碍的实行这些过程的解决方案。

为了实现这些,有一个loader包叫做css-customs-loader【https://github.com/silvenon/css-customs-loader】。它可以很容易的解决这个问题:我们要做的就是把它加进我们的webpack配置,并且是在css-loader之间:

{  test: /\.css$/,  use: [    
   'style-loader',    
   'css-customs-loader',    {      loader: 'css-loader',      options: {        importLoaders: 1,      },    },    
   'postcss-loader',  ], }

和自定义属性一样,这样就把自定义媒体查询暴露给Javascript。引入global.css,我们很简单的就可以访问到它们:

import React from 'react'
import { getSrc, getSrcSet } from './utils'
import styles from './photo.module.css'
import { customMedia } from './global.css'

const Photo = ({ publicId, alt, rounded, borderRadius }) => (  
<figure>    <img      className={rounded ? styles.roundedPhoto : styles.photo}      
     style={        typeof borderRadius !== 'undefined'          ? { ['--border-radius']: borderRadius } : null      }      
     src={getSrc({ publicId, width: 200 })}      
     srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })}      
     sizes={`${customMedia['--photo-breakpoint']} 400px, 200px`}    />
   <figcaption className={styles.caption}>{alt}</figcaption>  </figure>) Photo.defaultProps = {  rounded: false, } export default Photo

就这样。

总结

CSS模块化、PostCss以及将到来的CSS新特性,来应付日常的CSS的诸多挑战与难点是可以胜任的,这对我们来说是安全的。

我有很强在js里操作css的背景,不过我很容易被别人的代码所感染,因此这对我来说很困难。虽然有了下一代的更简明样式写法,但也容易弄混这两种不同的语言。CSS和Javascript相比,前者更冗长。这使我更容易选择用less写样式,因为这样可以避免使一个css文件太拥挤。这可能是一个个人喜好,但我真的不希望那会成为一个问题。css分开文件的写法能给我的代码一些新鲜空气,更加简明。

掌握这种方式可能不像css in js那么直接,我相信长期坚持肯定是值得的。它将提高你的CSS技能以使你为未来做好准备。

以上是关于漫谈css模块化,postcss及css之未来的主要内容,如果未能解决你的问题,请参考以下文章

css CSS模块+ PostCSS + Webpack

带有 IE 11 后备的 css 模块、postcss + webpack

【Webpack4】CSS 配置之 postcss-loader

具有 PostCSS / CSS 模块的全局实用程序类

如何结合gulp使用postcss

深入浅出的webpack构建工具---PostCss