Vue 3 CSS-in-JS方案探索

1. CSS-in-JS简介

1.1 什么是CSS-in-JS

CSS-in-JS是一种将CSS样式直接编写在JavaScript代码中的技术方案,允许开发者使用JavaScript的全部能力来编写和管理样式。

1.2 CSS-in-JS的核心特性

  • 组件级样式隔离:自动实现样式的局部作用域
  • 动态样式:可以根据组件状态动态生成样式
  • 主题支持:方便实现主题切换和管理
  • 代码复用:可以使用JavaScript的模块化和函数式编程
  • 类型安全:与TypeScript结合提供类型检查
  • 服务端渲染支持:良好的SSR支持

1.3 CSS-in-JS的优缺点

优点:

  • 解决了样式命名冲突问题
  • 动态样式实现更加灵活
  • 可以利用JavaScript的强大能力
  • 更好的组件封装性

缺点:

  • 运行时性能开销
  • 学习曲线较陡
  • 调试难度增加
  • 可能导致CSS体积增大

2. Vue中常用的CSS-in-JS方案

2.1 styled-components/vue-styled-components

虽然styled-components主要用于React,但也有Vue版本:

npm install vue-styled-components

基本使用:

<template>
  <div>
    <StyledButton primary @click="toggle">
      {{ buttonText }}
    </StyledButton>
    <StyledDiv>Hello Vue Styled Components</StyledDiv>
  </div>
</template>

<script>
import styled from 'vue-styled-components'

// 定义props类型
const buttonProps = {
  primary: Boolean
}

// 创建样式组件
const StyledButton = styled('button', buttonProps)`
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s ease;
  
  background-color: ${props => props.primary ? '#42b983' : '#f0f0f0'};
  color: ${props => props.primary ? 'white' : '#333'};
  
  &:hover {
    opacity: 0.8;
    transform: translateY(-2px);
  }
`

const StyledDiv = styled.div`
  background-color: #f5f5f5;
  padding: 20px;
  margin: 10px 0;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`

export default {
  components: {
    StyledButton,
    StyledDiv
  },
  data() {
    return {
      primary: true,
      buttonText: 'Primary Button'
    }
  },
  methods: {
    toggle() {
      this.primary = !this.primary
      this.buttonText = this.primary ? 'Primary Button' : 'Secondary Button'
    }
  }
}
</script>

2.2 Emotion

Emotion是一个高性能的CSS-in-JS库,支持Vue:

npm install @emotion/css @emotion/server

基本使用:

<template>
  <div>
    <button :class="buttonClass" @click="count++">
      Clicked {{ count }} times
    </button>
    <div :class="containerClass">
      Emotion in Vue
    </div>
  </div>
</template>

<script>
import { css } from '@emotion/css'

export default {
  data() {
    return {
      count: 0
    }
  },
  computed: {
    buttonClass() {
      return css`
        padding: 10px 20px;
        background-color: #42b983;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 16px;
        transition: all 0.3s ease;
        
        &:hover {
          background-color: #369a6e;
          transform: scale(1.05);
        }
        
        &:active {
          transform: scale(0.95);
        }
      `
    },
    containerClass() {
      return css`
        background-color: ${this.count % 2 === 0 ? '#f0f0f0' : '#e8f5e8'};
        padding: 20px;
        margin: 10px 0;
        border-radius: 8px;
        transition: background-color 0.3s ease;
      `
    }
  }
}
</script>

2.3 JSS

JSS是一个成熟的CSS-in-JS库,支持多种框架:

npm install jss jss-preset-default

基本使用:

<template>
  <div :class="classes.container">
    <h1 :class="classes.title">JSS in Vue</h1>
    <button :class="classes.button" @click="toggleTheme">
      Toggle Theme
    </button>
  </div>
</template>

<script>
import { create } from 'jss'
import preset from 'jss-preset-default'

// 创建JSS实例
const jss = create(preset())

export default {
  data() {
    return {
      isDark: false,
      classes: {}
    }
  },
  mounted() {
    this.updateStyles()
  },
  methods: {
    toggleTheme() {
      this.isDark = !this.isDark
      this.updateStyles()
    },
    updateStyles() {
      // 定义样式
      const styles = {
        container: {
          backgroundColor: this.isDark ? '#1a1a1a' : '#ffffff',
          color: this.isDark ? '#ffffff' : '#333333',
          padding: '20px',
          borderRadius: '8px',
          transition: 'all 0.3s ease'
        },
        title: {
          color: this.isDark ? '#42b983' : '#3498db',
          fontSize: '24px',
          marginBottom: '16px'
        },
        button: {
          backgroundColor: this.isDark ? '#42b983' : '#3498db',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          padding: '8px 16px',
          cursor: 'pointer',
          fontSize: '14px',
          transition: 'all 0.3s ease',
          '&:hover': {
            opacity: 0.8,
            transform: 'translateY(-2px)'
          }
        }
      }
      
      // 应用样式
      const { classes } = jss.createStyleSheet(styles).attach()
      this.classes = classes
    }
  }
}
</script>

2.4 Linaria

Linaria是一个零运行时的CSS-in-JS库,编译时将CSS提取为单独的文件:

npm install linaria

基本使用:

<template>
  <div>
    <h1 :class="title">Linaria in Vue</h1>
    <button :class="button" @click="count++">
      Count: {{ count }}
    </button>
  </div>
</template>

<script>
import { css } from 'linaria'

export default {
  data() {
    return {
      count: 0
    }
  },
  computed: {
    title() {
      return css`
        color: #3498db;
        font-size: 24px;
        margin-bottom: 16px;
      `
    },
    button() {
      return css`
        padding: 10px 20px;
        background-color: #42b983;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 16px;
        transition: all 0.3s ease;
        
        &:hover {
          opacity: 0.8;
        }
      `
    }
  }
}
</script>

2.5 Vue 3内置的CSS-in-JS方案

Vue 3的组合式API提供了一种原生的CSS-in-JS方案,使用useCssModule和动态样式绑定:

<template>
  <div :style="dynamicStyles">
    <h1 :style="titleStyle">Vue 3 Dynamic Styles</h1>
    <button :style="buttonStyle" @click="count++">
      Count: {{ count }}
    </button>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const count = ref(0)

const dynamicStyles = computed(() => ({
  backgroundColor: count.value % 2 === 0 ? '#f0f0f0' : '#e8f5e8',
  padding: '20px',
  borderRadius: '8px',
  transition: 'background-color 0.3s ease'
}))

const titleStyle = computed(() => ({
  color: '#42b983',
  fontSize: '24px',
  marginBottom: '16px'
}))

const buttonStyle = computed(() => ({
  padding: '10px 20px',
  backgroundColor: '#3498db',
  color: 'white',
  border: 'none',
  borderRadius: '4px',
  cursor: 'pointer',
  fontSize: '14px',
  transition: 'all 0.3s ease',
  transform: `scale(${1 + count.value * 0.01})`
}))
</script>

3. 自定义CSS-in-JS方案

我们也可以使用Vue 3的组合式API创建自己的CSS-in-JS方案:

<template>
  <div :class="$style.container">
    <h1 :class="$style.title">Custom CSS-in-JS</h1>
    <DynamicComponent :style="dynamicStyle" />
  </div>
</template>

<script>
import { ref, computed } from 'vue'
import DynamicComponent from './DynamicComponent.vue'

export default {
  components: {
    DynamicComponent
  },
  data() {
    return {
      theme: {
        primary: '#42b983',
        secondary: '#3498db',
        background: '#f0f0f0'
      }
    }
  },
  provide() {
    return {
      theme: this.theme
    }
  }
}
</script>

<style module>
.container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.title {
  color: v-bind('theme.primary');
  font-size: 24px;
  margin-bottom: 20px;
}
</style>

DynamicComponent.vue:

<template>
  <div :style="computedStyle">
    <p>Dynamic Component with Themed Styles</p>
  </div>
</template>

<script setup>
import { inject, computed } from 'vue'

const theme = inject('theme')

const computedStyle = computed(() => ({
  backgroundColor: theme.background,
  border: `1px solid ${theme.secondary}`,
  borderRadius: '4px',
  padding: '16px',
  color: theme.primary,
  transition: 'all 0.3s ease'
}))
</script>

4. CSS-in-JS与其他样式方案的比较

特性 CSS-in-JS scoped CSS CSS Modules
样式隔离 自动实现 基于属性选择器 基于唯一类名
动态样式 强大支持 有限支持 有限支持
主题管理 方便 较复杂 较复杂
类型安全 良好支持 不支持 部分支持
性能开销 运行时开销 编译时处理 编译时处理
学习曲线 较陡 平缓 平缓
调试难度 较难 容易 容易
SSR支持 良好 良好 良好

5. CSS-in-JS的最佳实践

5.1 性能优化

  • 避免频繁更新样式:减少样式的重新计算和应用
  • 使用memoization:缓存计算结果,避免重复计算
  • 优先使用编译时方案:如Linaria,减少运行时开销
  • 合理使用动态样式:只对必要的样式进行动态化
  • 避免过度使用嵌套:保持样式结构扁平

5.2 样式组织

  • 组件级样式:每个组件对应自己的样式
  • 主题样式:集中管理主题变量
  • 工具类样式:提取通用样式为工具类
  • 模块化设计:将样式拆分为多个模块

5.3 与TypeScript结合

  • 使用支持TypeScript的CSS-in-JS库
  • 为样式对象添加类型定义
  • 使用类型安全的主题变量
  • 利用TypeScript的类型检查

5.4 调试技巧

  • 使用浏览器开发者工具的Elements面板
  • 利用CSS-in-JS库提供的调试工具
  • 添加调试信息到样式中
  • 使用Source Maps

6. 实际应用场景

6.1 主题切换

<template>
  <div :class="classes.container">
    <h1 :class="classes.title">Theme Switcher</h1>
    <div class="theme-buttons">
      <button :class="[classes.themeButton, isDark && classes.active]" @click="setTheme('dark')">
        Dark
      </button>
      <button :class="[classes.themeButton, !isDark && classes.active]" @click="setTheme('light')">
        Light
      </button>
    </div>
  </div>
</template>

<script>
import { create } from 'jss'
import preset from 'jss-preset-default'

const jss = create(preset())

export default {
  data() {
    return {
      isDark: false,
      classes: {}
    }
  },
  mounted() {
    this.setTheme('light')
  },
  methods: {
    setTheme(theme) {
      this.isDark = theme === 'dark'
      
      const styles = {
        container: {
          backgroundColor: this.isDark ? '#1a1a1a' : '#ffffff',
          color: this.isDark ? '#ffffff' : '#333333',
          padding: '20px',
          borderRadius: '8px',
          transition: 'all 0.3s ease'
        },
        title: {
          color: this.isDark ? '#42b983' : '#3498db',
          fontSize: '24px',
          marginBottom: '20px'
        },
        themeButton: {
          padding: '8px 16px',
          marginRight: '10px',
          border: '1px solid',
          borderColor: this.isDark ? '#42b983' : '#3498db',
          borderRadius: '4px',
          backgroundColor: 'transparent',
          color: this.isDark ? '#42b983' : '#3498db',
          cursor: 'pointer',
          transition: 'all 0.3s ease',
          '&:hover': {
            backgroundColor: this.isDark ? '#42b983' : '#3498db',
            color: 'white'
          }
        },
        active: {
          backgroundColor: this.isDark ? '#42b983' : '#3498db',
          color: 'white'
        }
      }
      
      const { classes } = jss.createStyleSheet(styles).attach()
      this.classes = classes
    }
  }
}
</script>

6.2 动态组件样式

<template>
  <div>
    <StyledCard v-for="item in items" :key="item.id" :item="item">
      <h3>{{ item.title }}</h3>
      <p>{{ item.description }}</p>
      <button @click="item.expanded = !item.expanded">
        {{ item.expanded ? 'Collapse' : 'Expand' }}
      </button>
    </StyledCard>
  </div>
</template>

<script>
import styled from 'vue-styled-components'

const cardProps = {
  item: Object
}

const StyledCard = styled('div', cardProps)`
  background-color: #ffffff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
  margin: 10px 0;
  transition: all 0.3s ease;
  
  h3 {
    color: #333;
    margin-bottom: 8px;
  }
  
  p {
    color: #666;
    margin-bottom: 12px;
    max-height: ${props => props.item.expanded ? '200px' : '60px'};
    overflow: hidden;
    transition: max-height 0.3s ease;
  }
  
  button {
    padding: 6px 12px;
    background-color: #42b983;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
  }
  
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
`

export default {
  data() {
    return {
      items: [
        {
          id: 1,
          title: 'Item 1',
description: 'This is the first item with some detailed description.',
          expanded: false
        },
        {
          id: 2,
          title: 'Item 2',
description: 'This is the second item with a longer description that will be truncated when collapsed.',
          expanded: false
        },
        {
          id: 3,
          title: 'Item 3',
description: 'This is the third item with some additional information that can be expanded.',
          expanded: false
        }
      ]
    }
  }
}
</script>

7. CSS-in-JS的未来趋势

7.1 编译时CSS-in-JS

越来越多的CSS-in-JS库开始采用编译时方案,如Linaria、Astroturf等,以减少运行时开销。

7.2 CSS Modules + CSS变量

结合CSS Modules和CSS变量的方案,提供了更好的性能和开发体验。

7.3 浏览器原生支持

CSS Houdini等浏览器原生API的发展,可能会改变CSS-in-JS的实现方式。

7.4 零运行时方案

零运行时的CSS-in-JS方案将成为趋势,在编译时生成纯CSS文件。

8. 总结

CSS-in-JS是一种强大的样式解决方案,特别适合需要动态样式和复杂主题管理的应用。在Vue中,我们有多种CSS-in-JS方案可以选择,每种方案都有其优缺点:

  1. styled-components/vue-styled-components:React生态成熟,Vue版本功能有限
  2. Emotion:性能较好,API简洁
  3. JSS:成熟稳定,功能强大
  4. Linaria:零运行时,性能优秀
  5. Vue 3内置方案:原生支持,无需额外依赖

在选择CSS-in-JS方案时,需要考虑以下因素:

  • 应用的性能要求
  • 开发团队的熟悉程度
  • 项目的复杂度
  • 对动态样式的需求
  • 服务端渲染的需求

CSS-in-JS不是银弹,对于简单应用,传统的scoped CSS或CSS Modules可能更加合适。但对于复杂应用,特别是需要大量动态样式和主题管理的应用,CSS-in-JS可以提供更好的开发体验和灵活性。

9. 练习

  1. 使用不同的CSS-in-JS库实现一个主题切换功能
  2. 对比CSS-in-JS与scoped CSS在性能上的差异
  3. 实现一个动态样式的组件,根据props和状态变化
  4. 使用CSS-in-JS库创建一个可复用的样式组件库
  5. 探索CSS-in-JS在服务端渲染中的应用

10. 进一步阅读

« 上一篇 深度选择器原理与应用 下一篇 » 过渡动画基础类名