likes
comments
collection
share

React&Vue 系列:CSS 知识要点

作者站长头像
站长
· 阅读数 24

React&Vue 系列:CSS 知识要点

背景:作为使用三年 react.js 的搬运工,目前正在积极学习 vue.js 语法,而在学习的过程中,总是喜欢和 react.js 进行对比,这里也不是说谁好谁坏,而是怀有他有我也有,他没我还有的思想去学习,去总结。

  • React18
  • Vue3

css 作为前端三剑客之一,任何框架都是离不开的,本篇就来聊聊 React 和 Vue 关于 css 的使用。

规则不变,先从 React 开始。

React:CSS

我相信最开始刚入手 React 的小伙伴,一定经受过 CSS 的痛苦。为什么呢?因为正常的 CSS 文件写法,都是全局性的,是会相互影响的。

很形象的公式:CSS 文件分离 != 样式分离

简单的理解,就是 React 项目的 CSS 文件无论怎么分离,都是通过 style 标签添加在全局的 head 标签中。

可以看看下面的截图( vite + react 创建的 demo):

React&Vue 系列:CSS 知识要点

app.csshome.cssabout.css 里面都存在一个类名 content,但是都是存在各自的文件夹中,并在对应的文件夹的 index.tsx 中导入,那么效果呢?

React&Vue 系列:CSS 知识要点

可以发现,文字的颜色都是蓝色的,也就说明 about.css 文件起效果了。

再来看看 dom 结构,发现每个 CSS 文件里面的内容添加到 style 标签中,然后依次添加到 head 标签中,我们都知道 style 标签的内容属性,都是后面内容的覆盖前面的内容,所以最终的效果都是蓝色。

猜测:这里的 style 标签添加 head 标签上的顺序取决于 ESModule 的静态分析,分析模块依赖顺序

那么既然存在这样的问题,那么该如何解决呢?我们可以采取下面的几种手段:

  1. 内联样式 style
  2. 命名规范
  3. css module
  4. css in js

内联样式

不推荐使用。

在 js 里面使用大量的 css,代码优雅度直接炸裂,而且还不能编写伪类等样式。

使用场景,内联样式只推荐针对动态样式在组件中使用

命名规范

如果项目中存在 less, sass 等预处理器,该方案可以采用,但是也不是很推荐。

// home.tsx
import "./home.less";

// 定义 CSS 前端,一般采取的方式就是文件路由的形式
const PREFIX = "home";

const Home = () => {
  return (
    <div className={PREFIX}>
      <div className={`${PREFIX}_top`}>
        <div className="color">top</div>
      </div>
      <div className={`${PREFIX}_content`}>
        <div className="color">content</div>
      </div>
      <div className={`${PREFIX}_bottom`}>
        <div className="color">bottom</div>
      </div>
    </div>
  );
};

export default Home;
// home.less
.home {
  &_top {
    .color {
      color: red;
    }
  }
  &_content {
    .color {
      color: orange;
    }
  }
  &_bottom {
    .color {
      color: blue;
    }
  }
}

类似于上面的写法,先定义一个前缀(一般采取文件路由的形式,很少有相同的),该文件下的所有样式都根据这个前缀来进行命名,也能是样式达到正确的效果。

该方案缺点:

  • 存在可能性的命名冲突(概率很小)
  • 开发者不按照规矩开发,就会存在问题。

有人可能想,使用了 less, 难道不能一直嵌套使用下去吗?这样 css 样式的层级更高,更加不容易有冲突,不是更好? 只能说,可以写,但不推荐,css 嵌套的层级最好不要超过 3 级,为什么呢?

  1. 嵌套过深,难以维护(重点)
  2. 浏览器解析 css 会因为层级过深,导致加载过慢(这里可能会存在吹毛求疵的感觉)

css module

比较推荐的方式,无论是 webpack 创建的项目,还是 vite 创建的项目,都是默认支持 css module 的。

当然如果想个性化的话,就需要自己动手配置一下,比如说支持 xxx.module.less 等。 还是偷偷说一句,vite 配置比 webpack 简单[哈哈],并不是说 webpack 不如 vite。

使用

/* home: index.module.css */

.content {
  color: orange;
}
// home.tsx

import styles from "./index.module.css";

const Home = () => {
  return <div className={styles.content}>我是 home 页面的 content</div>;
};

export default Home;

上面是 home 页面的结构和样式,about 页面也是一样的 ,就不多做展示,最终的效果就是:

React&Vue 系列:CSS 知识要点

可以发现:

  • 界面展示的样式不在相互影响。
  • 其 dom 结构中的类名发生了变化,添加了 hash 值。

也可以了解一下 额外知识:React 配置 css module 支持 less

css in js

就是把 css 代码也写在 js 里面,针对 react 来说,也可以称为 all in jshtml, css, js 前端代码全写在一个文件里面)。 该方案在社区里面也比较流行,比如 antd5 针对 css 就从 less 转变为了 css in js 方案

社区里面有很多 css in js 库:

  • jss
  • emotion
  • styled-components
  • ...

这里就以 styled-components 进行演示,在使用之前,你必须得掌握一个知识额外知识:ES6 的标签模板字符串,不然就不用进行下一步了。

先说明,在实际项目中,我是没有使用过 css in js 方案的,只是学习过,哈哈。

到了这里,希望你已经理解了 ES6 的标签模板字符串了,不然涉及到函数调用,确实看的很懵逼。

先安装:

pnpm add styled-components     # 6.0.7

使用

import styled from "styled-components";

// 这里的 div 是一个函数,返回一个组件(原生标签,在 styled 都存在对应的函数)
const HomeDiv = styled.div`
  color: red;

  .txt {
    font-size: 30px;
  }
`;

const Home = () => {
  return (
    // HomeDiv 组件: 类似 div 标签,只是里面添加了样式
    <HomeDiv>
      <span className="txt">我是 home 页面的 content</span>
    </HomeDiv>
  );
};

export default Home;

基本使用,就大致是这样的。其解析出来,还是一个类似 hash 的类名。

当然,css in js 肯定不只这点功能,还有:

  • 支持嵌套
  • 支持伪类,伪元素
  • 支持 css 继承
  • 支持 props 传递,看成一个组件即可。

上面这些功能,可以自行研究。

虽然 styled-components 功能齐全,但是个人感觉书写麻烦,脱离了常规的 css 模式开发。

结合以上几点,个人比较推荐 css module 的形式,但是其他方案也是可以采用的,react css 写法没有唯一标准。

Vue:CSS

vue 中写 css,简直不要太爽。

  • vue 文件中写个 style 标签即可。
  • 考虑样式隔离,加个 scoped 即可。
  • 考虑 css 预处理器,加个 lang 属性指定即可。

虽然 vue 的写法简单,但是其中涉及的知识可是并不少,vue 内部帮我们实现了一个完整的黑盒。接下来,就来看看理论知识吧。

scoped 实现原理

先来看看不使用 scoped,现象是怎么样的。

<script setup lang="ts">
import Son from "./components/Son.vue";
</script>

<template>
  <div class="content">
    我是 App 的 content
    <Son />
  </div>
</template>

<!-- 没有添加 scoped -->
<style>
.content {
  color: red;
}
</style>
<script setup lang="ts"></script>

<template>
  <div class="content">我是 Son 的 content</div>
</template>

<!-- 没有添加 scoped -->
<style>
.content {
  color: blue;
}
</style>

看效果:

React&Vue 系列:CSS 知识要点

发现,当没有添加 scoped 时,也都是添加 style 标签到 head 标签中,都是产生的全局样式,会相互影响(跟 react 表现形式一样)。

当个 Son 组件加上一个 scoped 之后

React&Vue 系列:CSS 知识要点

组件 div 标签多了一个 data-v-xxxxxxx 这个东西(以组件为单位,唯一标识),这个就是 vue 替我们加上的,并在解析 css 时,在选择器上也会拼接上这个唯一标识,这样就形成了样式的唯一性,不会产生冲突。

vue 是怎么实现的呢?

  • 构建工具 webpack 利用 vue-loader 解析 .vue 文件,vue-loader 默认使用 css 后处理器 postcss 来实现 scoped css,原理就是给声明了 scoped 的样式中选择器命中的元素添加一个自定义属性,再通过属性选择器实现作用域隔离样式的效果。

  • vite 则是利用插件的解析代码

这里自己也不是很清楚,如果写错了,请喷然后告诉我正确的说法

小结一下:

  1. 添加了 scoped, 在解析组件的时候,就会给 dom 节点加一个不重复 data 属性(形如:data-v-xxx)来表示他的唯一性(组件为单位)
  2. 如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的 data 属性(注意:只会针对单节点加上当前组件的 data 属性,如果是多节点,则不会加 data 属性)
  3. 给每句 css 选择器的末尾(编译后的生成的 css 语句)加一个当前组件的 data 属性选择器来实现样式私有化。

样式穿透

什么时候使用样式穿透呢? 在上面已经提及到,当组件内部包含其他组件时,只会在其他组件的的根节点上加上 data 属性,并不会对其他组件的更深层次的标签加上 data 属性(就以 element-plus 为例)

<template>
  <div class="content">
    我是 App 的 content
    <el-input class="my-input">按钮</el-input>
  </div>
</template>

看 dom 结构

React&Vue 系列:CSS 知识要点

发现更深层次的 dom 标签是没有添加 data 属性。

那么这时候想要修改到组件更深层的样式该怎么写呢?就比如上面的 input 标签,你也许会这样写?

<style scoped>
.my-input .el-input__inner {
  background-color: red;
}
</style>

但是这样真的行吗?

React&Vue 系列:CSS 知识要点

看效果是没有作用的。然后再看 css, 发现 data 属性添加到了.el-input__inner 上,但是呢,深层次的标签根本都没有绑定 data 属性,所以样式肯定不会匹配上。那么正确的写法呢?这时候就需要使用到了样式穿透了。

<style scoped>
.content {
}

/* 使用样式穿透 :deep */
.my-input :deep(.el-input__inner) {
  background-color: red;
}
</style>

vue3 已经废弃了 /deep>>>,样式穿透使用 :deep(.child-class) 来替代 ::v-deep

看最后的效果,nice。

React&Vue 系列:CSS 知识要点

my-input 根标签上绑定了 data 属性,然后再寻找 .el-input__inner,结果显而易见。

css 中 v-bind

vue3 新增了在 css 中使用 v-bind,来动态绑定数据,极大的方便了动态样式编写。

<script setup lang="ts">
import { ref } from "vue";
const width = ref(100);

const btn = () => {
  width.value += 10;
};
</script>

<template>
  <div>
    <div class="content"></div>
    <button @click="btn">+</button>
  </div>
</template>

<style scoped>
.content {
  height: 30px;
  /* 动态绑定 width 属性 */
  width: v-bind(width + "px");
  background-color: red;
}
</style>

vue3 也可以使用 css module

其实 vue3 中的 scoped 样式隔离已经很好了,但是 vue3 也是可以支持 css module,也能生成唯一 hash 类名。

<script setup lang="ts">
import { useCssModule } from "vue";
const style = useCssModule();
const copyerStyle = useCssModule("copyer");
</script>

<template>
  <div>
    <div :class="$style.abc">abc</div>
    <div :class="style.abc">abc</div>

    <div :class="copyerStyle.abc">abc</div>
  </div>
</template>

<style module>
.abc {
  color: red;
}
</style>

<style module="copyer">
.abc {
  color: blue;
}
</style>

使用要点:

  1. style 标签上使用 module 来开启 css module;可以指定值,也可以不指定。
  2. 利用 vue 提供的 useCssModule 来动态拿取样式。不传递参数,拿取 module 没有赋值的样式。传递参数(module 的属性值),拿取对应的属性。
  3. 针对没有给 module 赋值的样式,也可以直接通过 $style 来获取。

vue 中的 css module 一般用于 tsx/ jsx 组件中,但是也一般采用 xxx.module.css 文件形式吧 缺少实践,不敢说话。

额外知识

React 配置 css module 支持 less

在上面已经提及到了构建工具(webpack, vite)是默认支持 css module 的,但是在实际项目开发中,大多数还是使用 css 的预处理器( less, sass 等),这里就以 less 为例。

webpack 配置

针对 webpack 肯定不会采取 npm run eject 直接暴露配置信息,而是采用其他的工具(cracocustomize-cra)来覆盖配置信息,这里就以 craco 工具为例(至于关于 craco 是什么,怎么使用,可以具体去学习一下)

npm install @craco/craco craco-less -D
const CracoLess = require("craco-less");

module.exports = {
  plugins: [
    {
      plugin: CracoLess,
      options: {
        lessLoaderOptions: {
          lessOptions: { javascriptEnabled: true },
        },
        modifyLessRule: function () {
          // webpack loader 执行顺序,你需要理解
          return {
            test: /.module.less$/,
            exclude: /node_modules/,
            use: [
              { loader: "style-loader" },
              {
                loader: "css-loader",
                options: {
                  modules: {
                    // 设置生成的类名
                    localIdentName: "[local]_[hash:base64:6]",
                  },
                },
              },
              { loader: "less-loader" },
            ],
          };
        },
      },
    },
  ],
};

这样配置之后,在项目中就可以使用 xxx.module.less 文件形式了。

vite

vite 天生支持 css module; 在项目中,如果安装了 less,vite 也就天生支持了 less。 那么两者结合呢?肯定支持

vite 真的内部做了很多配置,很多东西都不需要自己动手。

虽然 vite 支持 xxx.module.less,但是也是可以修改里面的默认配置的。

export default defineConfig({
  plugins: [react()],
  css: {
    // 配置 css module 的
    modules: {
      // 默认local,生成 hash类名,global: 全局的,不会转化类名
      scopeBehaviour: "local",

      // 配置 hash 类名的生成规则
      generateScopedName: "[local]-[hash:5]", // 字符串形式

      // 使生产的hash值更加乱,不容易重名
      hashPrefix: "copyer",

      // 采用驼峰式 还是短横线
      localsConvention: "dashes", // 默认值
    },
  },
});

ES6 的标签模板字符串

你认为的函数调用?

function foo() {}

foo(); // ??? 你以为的调用?

其实还可以这样调用

foo``;

不信的话,你试试,是不是会发现很怪异,但是事实就是这样。

参数传递呢?

function foo(num) {
  console.log(num); // ['123']
}

foo`123`;

疑问一:传递参数怎么又成为数组了?。

// 传递动态参数
let count;
function foo(num) {
  console.log(num); // ['', '']
}

foo`${count}`;

疑问二:你又会发现,参数怎么是两个空字符串了?

说出最后的结论:

对于 ES6 的模版字符串很熟悉:

  • 使用 `` 来进行包裹。
  • 遇到变量,使用 ${} 进行包裹。

那么对于标签模版字符串就是对模板字符串的另一种解析(自我理解):

  • 遇到 `` 就是进行函数调用
  • `` 里面包裹的内容,就是参数。当遇到内容为 ${} 就需要开始字符串分割,把字符串保存在一个数组中,把解析的变量保存在接下来的参数中。

React&Vue 系列:CSS 知识要点

不知道这张图是否好理解呢? 案例检验结果:

let age = 18;
let name = "copyer";

function foo() {
  console.log(arguments);
  /**
   * [Arguments] {
   * '0': [ 'my name is ', ', age is ', '' ],
   * '1': 'copyer',
   * '2': 18
   * }
   */
}

foo`my name is ${name}, age is ${age}`;

跟上面的图,是一致的。

总结

无论是 react 还是 vue, css 都是必要的一环,虽然比较简单,但是不能忽视。

css 也开始疲倦了起来:tailwindcss 可以了解一下,不需要你写样式,只需要你写类名。

转载自:https://juejin.cn/post/7264503433948086308
评论
请登录