likes
comments
collection
share

Vue 终于也有属于自己的 CSSInJs 原子库啦!一款动静结合,极致体积,融合了原子 css 优点的样式库 @usacss/vue

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

Vue 终于也有属于自己的 CSSInJs 原子库啦!一款动静结合,极致体积,融合了原子 css 优点的样式库 @usacss/vue

简介

纯动态的 css-in-js 库大家都见过

0js 无运行时的 css-in-js 库大家都见过

atom 原子样式库大家都见过

那么 3 者结合,还是搭配 Vue 的大家是否见过呢?

@usacss/vue 就是这么一个怪兽,将 css-in-js 的灵活与 atom 原子样式的体积优势相结合,目标是带给用户项目一个优秀的样式解决方案

为什么选择它?它有什么优点?

市面上的 css 方案有很多,Vue 本身自带的也不失为一个很棒的样式方案,可 @usacss/vue 是结合了多方的特点,权衡利弊下得出的产物

关于各种 css 方案的优劣势对比的文章有很多,这里只介绍自身的优点,是否真的值得一试有你来决定~

  1. 为原子样式赋予动态能力

大家常见的原子样式哭,缺点也很明显。比较致命的一点就是,我的项目中用了 ui 库,因某些业务原因我需要修改组件样式,自己写 css 文件就是破坏人家的原则,不这样又很难办到

@usacss/vue 提供深度选择器来支持修改,但内部没有用编译器,所以性能会纯 css-in-js 库要高很多

  1. 动静结合下的极致体积

体积可以说是原子样式库最大的卖点之一,同样也是 @usacss/vue 最大的卖点

自身体积小。大一个全包的 js 体积为 9.6kbgzip 压缩后的体积为 3.7kb,支持 treeshaking,最终体积可能会更小

样式继承。内部的样式全是以 json 存储的样式规则,允许把一个项目或者一个库(例如组件)的样式导出为 json,使用的项目则可以进行规则继承,然后统一生成,这就可以做到跨项目的样式共享,总体项目的样式体积会骤降

原子样式的复用能力。对于任意地方使用的 color:red 这种同样内容的样式,都只会生成 1 条,项目体积会大幅度下降

0 js,可选的静态生成模式。静态模式下会移除动态创建新样式的能力(依然能深度选择,只是不能创建新的),其他的优点则会全部保留

哈希样式。样式类名都是以哈希的形式使用,避免全局样式污染

  1. 其他的一些能力,相比之下就没什么亮点了

支持热更新

支持服务端渲染

支持多种自定义主题切换方式

普遍的样式写法基本都支持:一般写法,伪类,动画,媒体查询,自定义昂是变量,important!

下载 & 配置

首先请创建一个基于 Vite 的 Vue 项目,目前只支持 vite 项目

下载必要的依赖

npm install @usacss/vue @usacss/vite-vue

@usacss/vite-vue 辅助插件

@usacss/vue 样式库

配置 vite 配置文件

import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import { UsacssPlugin } from "@usacss/vite-vue"

export default defineConfig({
  plugins: [vue(), UsacssPlugin()]
})

配置应用入口文件

import { createUsacssProvide } from "@usacss/vue"
import { createApp } from "vue"
import App from "./App.vue"

createUsacssProvide({ app: App }).then(({ UsacssProvide }) => {
  createApp(UsacssProvide).mount("#app")
})

通过 createUsacssProvide 创建一个上下文组件,内部会使用 provide 依赖注入一些必要的东西

整体的使用流程

高性能写法

通过样式文件创建样式,插件自动编译成计算好的样式规则,组件内使用

一般性能写法

直接在组件内裸写,没有插件的编译,那么计算哈希和拼接样式的步骤就到了浏览器中进行

样式文件

样式文件必须是以,.style.[js|jsx|ts|tsx] 结尾的文件,插件会自动编译导出的内容

atomStyle 用于创建原子 css 样式,keyframes 用于创建动画,内容允许重复,重复内容会自动去重

动画

import { keyframes } from "@usacss/vue"
//写法1
export const route = keyframes("route", {
  "0%": {
    transform: "rotate(0deg)"
  },
  "100%": {
    transform: "rotate(360deg)"
  }
})
//写法2
export const route = keyframes("route", {
  "from": {
    transform: "rotate(0deg)"
  },
  "to": {
    transform: "rotate(360deg)"
  }
})

一般原子样式

import { atomStyle } from "@usacss/vue"
export const imgStyle = atomStyle({
  width: "120px",
  height: "120px",
  animation: "route 3s linear infinite"
})

伪类和元素伪类

: 开头会被当成伪类使用

export const inputStyle = atomStyle({
  width: "100%",
  color: {
    "::placeholder": "red",
    ":hover": "blue"
  }
})

媒体查询

@media+空格 开头的会被当成媒体查询

export const containerStyle = atomStyle({
  background: {
    "@media screen and (max-width: 600px) ": "slateblue"
  }
})

媒体查询+伪类

满足媒体查询的开头,以 空格+&:+内容 结尾的,会被当做伪类

export const containerStyle = atomStyle({
  background: {
    "@media screen and (max-width: 600px) &:hover": "black"
  }
})

组件内使用样式文件

使用 useAtomStyle 函数包裹样式文件的导出内容,因为编译出来的是样式规则,该函数用来插入样式到 Dom 并返回对应的类名

<script setup lang="ts">
import { useAtomStyle } from "@usacss/vue"
import { style1, style2, style3 } from "app.style"
</script>
<template>
	<div :class="boolean ? useAtomStyle(style1) : useAtomStyle(style2, style3)">
  	<h1>hello</h1>
 	</div>
</template>

当然动态切换也是能做到的,只需要根据自己的情况选择 useAtomStyle 包裹的内容即可

深度选择器

深度选择器使用 useDeepStyle 函数创建,要注意的是,它只支持单层的样式对象,并不支持样式嵌套,深度嵌套样式必须要引入编译器

对于一个原则上仍然是原子样式库来说,这是笔非常夸张的体积开销

深度选择器只是给原子样式做不到的某些情况下,提供一个方便

基本写法

<scirpt setup lang="ts">
  import {useDeepStyle}from "@usacss/vue"
const [css1, setStyle] = useDeepStyle({
  select: ".el-input__inner",
  border: "1px solid red"
})
</scirpt>
<template>
<div :class="css1" />
</template>

编译出来格式类似于 .hash .el-input__inner{border: "1px solid red"}

select 是放在哈希类名后的选择器

其他样式会被简单的拼接后放入节点中

返回内容是一个数组,第一个是哈希类名,第二个是个修改器

修改器参数和 useDeepStyle 一样

伪类

const [css1, setStyle] = useDeepStyle({
  select: ".el-input__inner",
  ":hover": {
    border: "1px solid red"
  }
})

: 开头的会被当做是伪元素,值是伪类下的多条样式

这里也可以使用 css 变量

import { useDeepStyle } from "@usacss/vue"
const randomNum = () => (Math.random() * 255) >>> 0
const [css1, setStyle] = useDeepStyle()
const setInputDeepStyle = () => {
  setStyle({
    select: ".el-input__inner",
    color: `var(--c, rgb(${randomNum()},${randomNum()},${randomNum()}))`,
  })
}

自定义主题

实现主题切换的方式有多钟,可以选择一种自己喜欢的

基于内部的主题适配能力

在样式文件中

@mode+空格 开头会被当做是要适配主题,空格后的内容则是主题

export const containerStyle = atomStyle({
  boxShadow: {
    "@mode light": "0 0 20px 30px rgba(255, 255, 0, 0.5),inset 0 0 20px 30px rgba(255, 255, 0, 0.5)",
    "@mode dark": "0 0 20px 30px rgba(255, 0, 0, 0.5),inset 0 0 20px 30px rgba(0, 0, 0, 0.4)"
  }
})

满足适配主题的开头,以 空格+&:+内容 结尾的,会被当做伪类

export const containerStyle = atomStyle({
  boxShadow: {
    "@mode dark &:hover": "0 0 20px 30px rgba(255, 0, 0, 0.5),inset 0 0 20px 30px rgba(0, 0, 0, 0.4)"
  }
})

@mode 后,&: 前,的内容就是自定义主题类名

它们会被编译成 .自定义的样式名 .生成的哈希类名 {} 的形式

所以只要在它们的祖先节点上挂上那个自定义类名即可生效

可是

大部分时候我们希望主题是全局的,所以可以把主题类名挂载 body/html 上,可是在组件内部操作会比价麻烦,使用辅助函数 useThemeMode 可以轻松做到

类型声明如下

type useThemeMode(
	selector: string,  //document.querySelector 的参数
	mode?: string | null | undefined
	unMount?: boolean  //默认是 false,组件卸载时是否清除
): ShallowRef<string | null>

内部会在 nextTick 时获取 dom 节点,并设置主题为 mode

返回一个 ref,当修改内容时会自动同步到节点,赋值成 null 表示移除

使用

import { useThemeMode } from "@usacss/vue"

//获取但不立即创建
const mode = useThemeMode("body")
//获取 并且立即创建
const mode = useThemeMode("body", "dark")、

mode.value = "light" //修改
mode.value = null //移除

Css 变量

对于原子样式的处理就是简单的字符串拼接

export const imgStyle = atomStyle({
  width: "120px"
})

这里的 width 会被拼成 width:120px,非常的简单粗暴

所以自定义样式变量也是如此

export const imgStyle = atomStyle({
	"--c": "red",
  "color": "var(--parentDefinedColor, red)"
})

媒体查询

这东西对于大部分项目来说并不常用,略

样式继承

所有的样式都是以 js 对象的形式存储在内部,继承则意味着合并 js 对象

在配置应用入口文件中我们使用 createUsacssProvide 创建了上下文组件,内部还会返回一个叫 sheet 的对象,它就是内部操作样式的底层实例

import { createUsacssProvide } from "@usacss/vue"
import { createApp } from "vue"
import App from "./App.vue"

createUsacssProvide({ app: App }).then(({ UsacssProvide, sheet }) => {
  createApp(UsacssProvide).mount("#app")
})

使用它我们可以导出样式,也可以继承样式

导出

const rules = sheet.toJson()
//返回规则对象{atomRules: [], deepRules: [] 

导入到项目

import {rules} from "lib"
createUsacssProvide({ app: App }).then(({ UsacssProvide, sheet }) => {
	sheet.insertAtomRules(rules.atomRules)
	sheet.insertDeepRules(rules.deepRules)

  const app = createApp(UsacssProvide)
  app.mount("#app")
})

静态模式,开启 0 js 打包

静态模式需要进行配置

它类似于把服务端渲染的功能直接搬到了插件中

配置 Vite 插件

export default defineConfig({
  rules: [UsacssPlugin({ static: true })]
})

配置应用入口文件

import { createApp } from "vue"
import App from "./App.vue"
import "virtual:usacss"
createApp(App).mount("#app")

这里就不需要 createUsacssProvide 来创建上下文了。此时插件会将用到的样式文件编译成样式,替换掉 virtual:usacss

样式文件

既然是 0 js,那就不能用 useDeepStyle 动态创建深度样式了

此时我们可以在样式文件中用 deepStyle 来做下位替代

import { atomStyle, deepStyle, keyframes } from "@usacss/vue"
export const route = keyframes("route", {
  "0%": {
    transform: "rotate(0deg)"
  },
  "100%": {
    transform: "rotate(360deg)"
  }
})
export const imgStyle = atomStyle({
  width: "120px",
  height: "120px",
  animation: "route 3s linear infinite"
})

export const deepInputStyle = deepStyle({
  select: ".el-input__inner",
  color: `red`
})

组件内使用

<script setup>
import { route, imgStyle, deepInputStyle } from "./xxx.style"
</script>
<template>
<img src :class="[route, imgStyle, deepInputStyle]" />
</template>

配置静态生成后,样式文件的导出就是,已经生成好的,样式的哈希类名,所以直接绑定即可

实际的样式文件内容都在虚拟文件 virtual:usacss

总结

详细的使用和完整文档可以查看 npm点击跳转

从书写使用上看,体验差了裸写 css 文件一些

从灵活度上看,体验差了纯动态的 css-in-js 一些

可与它带来的优点相比,这都是可以接受的,另外值得一提的是,它并不像一般的原子样式库,会导致模版的臃肿,以及不适合团队使用的问题。因为团队中只要有一个打破原子样式的规则开始自己随便写样式文件了,那就相当于破功了,究其原因还是因为用起来不方便,有很多时候发现样式不生效,或者传不过去导致的

@usacss/vue 可以减缓这些种种的问题,是个可以尝试在团队尝试一下的方案~