@layer 样式降权之 Vue CLI <style> @import 外部 css 文件 layer() 具名不生效的问题

126 阅读6分钟

背景

标题有点模糊,简单说说想做什么。

项目引入了多个外部 css 文件(跨域的),引入时机不定,可能会存在样式冲突,要求是外部 css 文件的优先级权重低于项目内的,且外部 css 文件之间可以排列优先级,2022 年的一个 CSS 新特性 @layer 进入了方案,级联层 layer 是干嘛的,后面介绍。

按如下方式使用:

<!-- src/App.vue -->
<style>
@layer baidu, example;
@import 'https://www.example.com/style.css' layer(example);
</style>
<!-- src/views/Page.vue -->
<style>
@import 'https://www.baidu.com/style.css' layer(baidu);
</style>

Vue 单文件组件的 <style> 标签引入(@import) 外部 css 文件(跨域),并声明具名 Layer layer(example) (即名字叫 example 的级联层)。

预期是达到这样的效果:layer(example)layer(baidu) 级联层的样式优先级权重低于页面开发者定义的普通样式,且 layer(example) 的优先级高于 layer(baidu),如下

image.png

然而,

在 Vue CLI 4 项目中,css 文件直接没有生效。

在 Vue CLI 5 项目中,css 文件生效了,但 Layer 是匿名的,如下

image.png

顺便提下,<style> @import 外部 css 文件,默认就是匿名的 Layer(支持 Layer 的浏览器),不支持 Layer 的浏览器就是普通样式。

<!-- any.html -->
<style>
@import 'https://www.example.com/style.css';
</style>

显然,Vue CLI 4 和 Vue CLI 5 都没有达到预期效果,但表现不一样,接下来一探究竟。

Vue CLI 4 & css-loader@3.6.0

特意提到 css-loader 的版本,这是关键。

vue cli CSS 相关 中引用静态资源有说明:所有编译后的 CSS 都会通过 css-loader 来解析其中的 url() 引用,并将这些引用作为模块请求来处理。

说明原因大概就在 css-loader。

首先在 App.vue 引入,声明具名 Layer layer(example)

<!-- src/App.vue -->
<style>
@import 'https://www.example.com/style.css' layer(example);
</style>

从 Element 找找 css 文件没有生效的原因

<!-- dist/index.html -->
<style type="text/css" media="layer(example)">
@import url(https://www.example.com/style.css);
</style>

可以看出,media="layer(example)" 这个属性,把 layer(example) 当成 media 了,从 MDN @import 可知,媒体查询条件列表,决定通过 URL 引入的 CSS 规则 在什么条件下应用。如果浏览器不支持列表中的任何一条媒体查询条件,就不会引入 URL 指明的 CSS 文件。所以,css 文件没有生效。

显然这是因为 css-loader 没有正确解析 layer(example),把它当成媒体查询条件了。看了下我的 Vue CLI 4 项目中 css-loader 的版本是 3.6.0,2020 年发布的版本,难怪,@layer 是个比较新的特性,当然不支持。

也是有办法处理的,查看 css-loader@3.6.0 的官方文档,能帮助我们的就是 import 属性。

import

Type: Boolean|Function Default: true

启用/禁用 @import 规则的处理。控制 @import 的解析。

针对不同形式的 url 采取不同的处理方式:

@import 'style.css' => require('./style.css')
@import url(style.css) => require('./style.css')
@import url('style.css') => require('./style.css')
@import './style.css' => require('./style.css')
@import url(./style.css) => require('./style.css')
@import url('./style.css') => require('./style.css')
@import url('http://dontwritehorriblecode.com/style.css') => @import url('http://dontwritehorriblecode.com/style.css') in runtime

// To import styles from a `node_modules` path (include `resolve.modules`) and for `alias`, prefix it with a `~`:
@import url(~module/style.css) => require('module/style.css')
@import url('~module/style.css') => require('module/style.css')
@import url(~aliasDirectory/style.css) => require('otherDirectory/style.css')

import 是函数类型时可以过滤 @import 的处理,返回 true 没被过滤的就正常解析,返回 false 被过滤的 @import 就不会被解析。

vue.config.js 中添加如下配置,parsedImport.media 的值是 layer(example),这也是为什么么构建后的 style 标签有 media="layer(example)" 这个属性,那么我们就可以判断 parsedImport.media 是否包含 'layer' 标识来判断要不要过滤当前 @import 不做解析,所以,包含 'layer' 就返回 false

  css: {
    loaderOptions: {
      css: {
        import: (parsedImport, resourcePath) => {
          // parsedImport.url - url of `@import`
          // parsedImport.media - media query of `@import`
          // resourcePath - path to css file

          console.log(parsedImport.url) // https://www.example.com/style.css
          console.log(parsedImport.media) // layer(example)
          console.log(resourcePath) // /home/user/Desktop/learn/vue-cli-4-proj/src/App.vue

          // Don't handle layer
          if (parsedImport.media.includes('layer')) return false

          return true
        }
      }
    }
  }

跑起来看看效果,达成目的!

image.png

再看看 Element 中 css 文件是怎么引入的,原样引入,没有问题。

<style type="text/css">
@import 'https://www.example.com/style.css' layer(example);
</style>

Vue CLI 5 & css-loader@6.8.1

后续,css-loader 在 6.3.0 版本支持了 layer()

css-loader/releases/tag/v6.3.0 : supported supports() and layer() functions in @import at-rules (#1377) (bce2c17)

原以为 Vue CLI 5 项目不加任何配置像之前这样使用就能达到想要的效果。

<!-- src/App.vue -->
<style>
@import 'https://www.example.com/style.css' layer(example);
</style>

但并没有,这次外部 css 文件生效了,但是 Layer 是匿名的,Layer 的名称 example 没生效。

image.png

此时再来看看 css-loader 最新(6.8.1)的文档,重点也是 import 属性。

import

Type:

type importFn =
  | boolean
  | {
      filter: (
        url: string,
        media: string,
        resourcePath: string,
        supports?: string,
        layer?: string
      ) => boolean;
    };

Default: true

可以看出,新版本的 css-loader import 属性的类型和 3.6.0 版本很不一样了,类型是 object 时只有一个 filter 属性,filter 属性是一个函数,打印参数得知,第五个参数 layer 的值就是 'example',那么就可以通过判断 layer 参数有值来返回 false 过滤当前 @import 不做解析,Vue CLI 5 的 vue.config.js 添加如下配置:

  css: {
    loaderOptions: {
      css: {
        import: {
          filter: (url, media, resourcePath, supports, layer) => {
            console.log(url) // https://www.example.com/style.css
            console.log(media) // undefined
            console.log(resourcePath) // /home/user/Desktop/learn/vue-cli-5-proj/src/App.vue
            console.log(supports) // undefined
            console.log(layer) // example

            // Don't handle layer
            return !layer
          }
        }
      }
    }
  }

跑起来看看效果,达成目的!

image.png

<style type="text/css">
@import 'https://www.example.com/style.css' layer(example);
</style>

Layer 是什么

先上图

图源自大漠老师的这篇文章

CSS中有两个重要的基础规则,一个是继承,一个是级联。

继承

指的是类似 color,font-family,font-size,line-height 等属性父元素设置后,子元素会继承的特性。

级联

可以简单理解为是CSS 用来解决要应用于元素的具体样式的算法。也就是基于一些优先级排序输出给给定元素上属性值一个级联值。级联值是级联的结果。

当前级联的排序标准:

  1. 起源和重要性(Origin and Importance)
  2. 上下文(Context)
  3. 样式属性(Element-Attached Styles)
  4. 层(Layers)
  5. 特异性(Specificity)
  6. 出场顺序(又名源代码顺序)(Order of Appearance)

浏览器在确定最终元素样式呈现的时候,会依据这些准则按照优先权从高到低排序,并且会一个一个的检查,直到确定最终样式。

CSS @layer 从 CSS Cascading and Inheritance Level 5 被规范定义。

CSS @规则 中的@layer 用于声明级联层,也可用于定义多个级联层的优先顺序。

通过 @layer 级联层管理样式优先级,单个级联层内的规则级联在一起,不与层外的样式规则交错,级联层引入的外部样式优先级低于页面开发者定义的样式(达到样式降权的效果),级联层引入的外部样式又可以通过名称排序定义优先顺序,这样就可以不用担心引入样式文件的先后顺序不一样而导致的样式优先级权重问题,可以用于避免样式冲突。

CSS Cascade Layers

Baseline 2022 Newly available across major browsers

当然这个特性很新,Chrome 和 Edge 都要 99 以上版本才支持,对于不支持的浏览器直接不会引入对应的 css 文件,因为会把 layer(name) 当作媒体查询,所以谨慎使用。

参考

详细了解推荐阅读以下文章:

前端开发如何更好的避免样式冲突?级联层(CSS@layer)

2022 年最受瞩目的新特性 CSS @layer 到底是个啥?

MDN @layer

A Complete Guide to CSS Cascade Layers

番外

顺便提一下,Vue 单文件组件的 <style scoped> 标签内 @import 引入项目内的 css 文件,作用域仍是全局的。

<style scoped>
@import '~@/assets/style/reset.css';
</style>

image.png

应该使用 <style scoped src=""></style> 的方式引入:

<style scoped src="@/assets/style/reset.css"></style>

image.png