一個更高效的 CSS-IN-JS 方案

咱們先看看咱們最終的目標, 咱們能夠如何描述代碼:javascript

import React from "react";

export default () => (
  <button inlist="bg:#f00; @md:display:none; hover:radius:8px; radius:4px"> 我是一個按鈕 </button>
);
複製代碼

在這個文件中,咱們 "彷佛沒有引入任何庫",就好像寫內聯樣式同樣,把樣式描述、媒體查詢、僞類都實現了, 而且可自定義樣式名,如 bg、radius。css

cssin

cssin 是一個高度可定製的低級 CSS-In-JS 框架,它爲您提供構建定製設計所需的全部構建模塊,而無需任何使人討厭的樣式,你可使用內聯樣式的全部語法,和其餘擴展語法。html

大多數 CSS 框架都作得太多了。 它們帶有各類預先設計的組件,如按鈕,卡片和警報,這些組件可能會幫助您最初快速移動,可是當您的網站使用自定義設計脫穎而出時,會致使更多的痛苦。java

cssin 不同凡響。react

cssin 提供了低級實用程序類,而不是固定的預先設計的組件,使您能夠構建徹底自定義的設計而無需離開 JS。android

cssin 生成的每一個相同的樣式值能夠被重複引用,而不是從新建立。ios

理念

咱們在使用 cssin 以前作了很是多的嘗試,css\less\scss, tailwindCSS, styled-components 和其餘 css-in-js 方案。其中 tailwindCSS 是最符合生產須要的,咱們從中學到許多東西和理念;但是這些樣式方案對於做者來講並沒能真正解決問題:git

簡短高效的描述個人樣式,而且不離開 js;固然也不放棄 css 的任何一個特性github

css-in 就是爲了解決此類問題而存在npm

旨在定製

cssin 全部樣式都是經過定製而得,cssin 容許您自定義它。這包括顏色,邊框大小,字體粗細,間距實用程序,斷點,陰影和任何 css 樣式。

cssin 採用純 Typescript 編寫,而且無需對項目框架進行配置,這意味着您能夠輕鬆得到真正編程語言的所有功能。

cssin 至關於在內斂樣式上擴展了僞類和媒體查詢,而且支持自定義屬性名和設定組件。

cssin 不只僅是一個 CSS-IN-JS 框架,它仍是一個建立設計系統的引擎。

輕巧

  • 僅有 2kb (gzip)
  • 每條樣式會被緩存, 以更高的性能進行樣式處理
  • 能夠在任何框架中使用,如你喜歡的 React、Vue、Stencil

安裝

$ npm i cssin --save
複製代碼

先看看展示形式

example: navar.workos.top

在沒有進行任何配置以前,cssin 的語法和內斂樣式是一致的

import React from "react";
import cssin from "cssin";

// 設置一個全局的 css-value
document.body.style.setProperty("--button-color", "#fff");

export default () => {
  return (
    <div className={cssin`background-color:#f66; hover:background-color:#f33; padding:8px; color:#000; border:2px solid #f33; @md:border-radius:4px;`} > Button </div>
  );
};
複製代碼

看起來還不錯,有點像內聯樣式,可是又有些許不一樣,彷佛直接描述了僞類和媒體查詢,並且代碼不夠精簡。

好的,咱們最後會經過簡單的配置的讓樣式描述變成這樣:

import React from "react";
import cssin from "cssin";

export default () => {
  return (
    <div className={cssin`btn:#f33, 8px; hover:bg:#f33; @md:radius:4px;`}> Button </div>
  );
};
複製代碼

或者極限簡潔:

import React from "react";
import cssin from "cssin";

export default () => {
  return <div className={cssin`button`}>Button</div>;
};
複製代碼

更加極限極限簡潔, 連 cssin 的包裹都省略掉:

import React from "react";

export default () => {
  return <div inlist="button">Button</div>;
};
複製代碼

咱們會一步步來達到最後的步驟。

或許一段話就能夠描述清楚 cssin

咱們先回顧剛開始的代碼塊:

export default () => {
  return (
    <div className={cssin`background-color:#f66; hover:background-color:#f33; padding:8px; color:--button-color; border:2px solid #f33; @md:border-radius:4px;`} > Button </div>
  );
};
複製代碼

上述代碼有點像內聯樣式,可是又有一些不一樣,由於它能夠實現僞類及更好的自定義,咱們逐步分析:

  • 和編寫內聯樣式同樣的編寫 css 樣式, 如: background-color: #f66; padding: 4px;
  • 直接使用僞類, 僞類在屬性名以前,使用:分割如: hover:background-color=#f33
  • 能夠直接描述媒體查詢等功能, 媒體查詢對象使用@開頭, 如: @md:border-radius=4px

其餘規則:

  • 若是隻有屬性名,那麼它將是一個組件, 如 button;
  • 若是值是一個單一的 css 變量, 如 color:--button-color; 等效於 color:var(--button-color);
  • 使用!表示!important, 如 color: #f00!; 等效於 color: #f00 !important
  • 若是隻有屬性名,而且以 . 開頭, 那麼就是對原生 css 樣式的引用, 如 .button;
  • 若是包含 {}, 表示這是一個純 css, 它會被插入至全局樣式中, 如 body { margin:0px; }

以上就是 cssin 的全部規則

下面是完整屬性的表達式: @[媒體查詢]:[僞類名]:[屬性名]:[屬性值];

下面這句完整的語法描述:

// 當媒體查詢大於 760px 時、鼠標移入時、描邊等於 #f00;
cssin`@md:hover:border:1px solid #f00;`;
複製代碼

爲何不直接編寫 style 內聯樣式?

  1. style 樣式沒法徹底描述 css 的功能,如媒體查詢、僞類等等 style;
  2. 樣式沒法自定義更簡短的樣式集、樣式集的組合、嵌套;
  3. 內聯樣式沒法直接引用 className,這樣咱們一般須要編寫 css 文件,設置 className 和 style;
  4. 而且默認優先級比 css 高,css 和 內聯樣式混合使用須要注意優先級;

cssin 最後生成的仍是 css 樣式,因此不會有以上的問題

若是更喜歡編寫 style 屬性

有的朋友更喜歡編寫 style 屬性,可是 style 中的一個痛點是沒法實現僞類或媒體查詢。

cssin 足夠輕量,咱們也能夠僅僅使用它的僞類或媒體查詢特性,來配合 style 屬性進行項目樣式的編寫.

不過咱們要注意,style 中編寫的屬性權重默認高於 className 中的樣式,因此須要添加 !important:

import React from "react";
import cssin from "cssin";

export default () => {
  return (
    <div className={cssin`hover:background:#f00 !important;`} style={{ background: "#00f", fontSize: "20px" }} > Button </div>
  );
};
複製代碼

因爲這個模式很常見,因此在 cssin 中,它可使用 ! 直接表示 !important:

...
export default () => {
  return (
    <div className={cssin`hover:background:#f00!;`} style={{ background: "#00f", fontSize: "20px" }} > Button </div>
  );
};Z
複製代碼

訂製自定義樣式

和衆多 css 框架同樣,cssin 容許你自定義樣式集,這樣能夠用更簡短的聲明來描述樣式

cssin 有一個 addSheets 屬性用來添加樣式映射表

咱們如今達成剛剛的約定,將:

background-color:#f66; hover:background-color:#f33; padding:4px; color:--button-color; border:2px solid #f33; @md:border-radius:8px;

變成:

btn:#f33, 4px; hover:bg:#f33; @md:radius:8px;

import React from 'react';
import cssin, { addSheets } from 'cssin';

// 添加自定義樣式集
addSheets({
  bg: (v) => `{ background-color: ${v}; }`,
  radius: (v) => `{ border-radius: ${v}; }`,
  btn: (v) => {
    const values = v.split(';');
    return {
      `{ background-color: ${values[0]}; padding:${values[1]}; color:var(--button-color); }`
    }
  },
});

// 使用自定義的樣式
export default () => {
  return <div className={cssin`btn:#f33, 4px; hover:bg:#f33; @md:radius:8px;`}>Button</div>;
};
複製代碼

因爲使用 cssin , 咱們不會須要有 css 代碼,因此能夠下降項目首屏的資源請求。

自定義樣式除了能夠簡化開發,還能夠減小 js 代碼量,從而最終達到相對更少的打包資源。

訂製媒體查詢

cssin 默認配置了 4 個尺寸級別的媒體查詢,和基於設備媒體查詢,咱們能夠覆蓋它或者建立新的規則

注意,咱們約定,只有以 @ 開頭的纔是媒體查詢對象

// 默認的媒體查詢
addSheets({
  "@sm": (v: string) => `@media (min-width: 640px) {${v}}`,
  "@md": (v: string) => `@media (min-width: 768px) {${v}}`,
  "@lg": (v: string) => `@media (min-width: 1024px) {${v}}`,
  "@xl": (v: string) => `@media (min-width: 1280px) {${v}}`,
  "@ios": (v: string) =>
    `@media (min-width: ${device.isIos ? "0px" : "9999px"}) {${v}}`,
  "@android": (v: string) =>
    `@media (min-width: ${device.isAndroid ? "0px" : "9999px"}) {${v}}`,
  "@native": (v: string) =>
    `@media (min-width: ${device.isNative ? "0px" : "9999px"}) {${v}}`,
  "@pc": (v: string) =>
    `@media (min-width: ${device.isPc ? "0px" : "9999px"}) {${v}}`
});
// 咱們覆蓋 @md 以及建立一個 @xxl
addSheets({
  "@md": v => `@media (min-width: 800px) {${v}}`,
  "@xxl": v => `@media (min-width: 1920px) {${v}}`
});
複製代碼

使用媒體查詢,如下例子是屏幕寬度大於 800px,button 寬度爲 200px,而且在 native 端隱藏

import React from "react";
// 最終只須要包裹一個單詞的聲明
export default () => {
  return (
    <div inlist="width:100px; height:50px; @md:width:200px; @native:display:none;"> Button </div>
  );
};
複製代碼

訂製組件

咱們但願把剛剛的代碼簡寫成更精巧的組件, 組件實際上是一組樣式集

設置自定義組件, 由於 sheets 是一個簡單的對象表,請注意不要和其餘自定義樣式重名致使覆蓋

它和自定義樣式或媒體查詢的區別是它的值是一個單純的字符串:

import React from "react";
import cssin, { addSheets } from "cssin";

addSheets({
  // 區別於自定義樣式,組件的值是一個字符串,它遵循 cssin 語法,能夠調用其餘組件和自定義樣式
  button: "bgc:#f66; hover:bgc:#f22; padding:8px; color:--button-color;"
});

// 最終只須要包裹一個單詞的聲明
export default () => {
  return <div className={cssin`button;`}>Button</div>;
};
複製代碼

注意,組件不能夠和僞類或者媒體查詢進行組合,由於組件內部就已經包含了僞類或媒體查詢

覆蓋 setAttribute

這裏涉及一些魔法,請辯證的使用。

做者在編寫代碼的時候不但願每次都引用 cssin,這對做者來講太過繁瑣了,若是你也有這種感受,可使用 cssin 的 coverAttribute

index.js

import React from "react";
import { coverAttribute } from "cssin";

// 這裏咱們覆蓋inlist對象,它會模擬 className={cssin`...`}
coverAttribute("inlist");

// 請確保 coverAttribute 在 ReactDOM.render 以前執行
ReactDOM.render(<App />, document.getElementById("root")); 複製代碼

App.js

import React from "react";

// 最終只須要一個單詞的聲明,就像原生聲明同樣
export const App = () => {
  return (
    <div inlist="full; m:20px;"> <div inlist="button">Button</div> </div>
  );
};
複製代碼

inlint 能夠和 className 一塊兒使用,前提是 className 必須在 inlist 以前聲明

import React from "react";

// 最終只須要一個單詞的聲明,就像原生聲明同樣
export const App = () => {
  return (
    <div> <div className="app-box" inlist="button"> Button </div> </div>
  );
};
複製代碼

咱們須要注意,覆蓋 setAttribute 並非很是正確的作法,由於它帶來了其餘開發人員的不友好,其餘人員並不知道咱們作了這些黑魔法的前提下,會很是困惑。

另外,請不要覆蓋 className 等經常使用屬性,這樣會讓其餘組件庫失效

使用 css 原生功能在 javascript 中

使用 {} 編寫單純的 css 片斷

有時候,咱們會須要編寫單純的 css 片斷,咱們約定若字符串中包含 {}

此時傳入的字符串只會被當成單純的 css 樣式進行注入至 html 中

import cssin from "cssin";

cssin` body { margin: 0px; background-color: #f5f5f5; } @media (min-width: 640px) { .box { background: #f00; } } `;
複製代碼

使用 . 引用原生的 css

其餘地方定義的原生的 css 能夠和 cssin 混合使用,只須要在屬性名前面增長 .:

import React from "react";
import cssin from "cssin";

// 使用 .box 引用 css 樣式
export default () => {
  return <div inlist="margin:4px; .box">Button</div>;
};
複製代碼

使用預設自定義樣式、組件、 css-values

cssin 提供了一整套預設的自定義樣式集合及 css-value 集合,它精心設計、開箱即用,亦能夠做爲一個自定義樣式集合的參照標本

默認狀況下 cssin 並未配置它,若是咱們須要能夠以下配置:

import "cssin/commonSheets"; // 引入 sheets集合
import "cssin/commonCSSValues"; // 引入 css-value 集合
複製代碼

commonSheets 中的內容:

自定義樣式名 映射樣式 使用
dis display dis: flex;
items align-items items: 20px;
justify justify-content justify: start;
self align-self self: center;
content align-content content: end;
z z-index z: 10;
p padding p: 5rem;
px pading-left, padding-right px: 5rem;
py padding-top, padding-bottom py: 5rem;
pl padding-left pl: 5rem;
pt padding-top pt: 5rem;
pr padding-right pr: 5rem;
pb padding-bottom pb: 5rem;
m margin m: 5rem;
mx margin-left, margin-right mx: 5rem;
my margin-top, margin-bottom my: 5rem;
ml margin-left ml: 5rem;
mt margin-top mt: 5rem;
mr margin-right mr: 5rem;
mb margin-bottom mb: 5rem;
w width w: 5rem;
w-min min-width w-min: 5rem;
w-max max-width w-max: 5rem;
w-min-max min-width, max-width w-min-max: 5rem;
h height h: 5rem;
h-min min-height h-min: 5rem;
h-max max-height h-max: 5rem;
h-min-max min-height, max-height h-min-max: 5rem;
b border: ${v} solid; b: 5rem;
bl border-left: ${v} solid; bl: 5rem;
bt border-top: ${v} solid; bt: 5rem;
br border-right: ${v} solid; br: 5rem;
bb border-bottom: ${v} solid; bb: 5rem;
bc border-color bc: #f00;
radius border-radius radius: 2rem;
font font-size font: 1.25rem;
bg background background: #f00;
bgc background-color bgc: #f00;
linear transition: all ${v} linear; linear: 0.3s;
ease transition: all ${v} ease; ease: 0.3s;
ease-in transition: all ${v} ease-in; ease-in: 0.3s;
ease-out transition: all ${v} ease-out; ease-out: 0.3s;
ease-in-out transition: all ${v} ease-in-out; ease-in-out: 0.3s;
move-x transform: translateX(${v}); move-x: 50%;
move-y transform: translateY(${v}); move-y: 50%;
move-z transform: translateZ(${v}); move-z: 50%;
rotate transform: rotate(${v}deg); rotate: 180;
scale transform: scale(${v}, ${v}); scale: 0.7;
如下均爲組件 組件不須要設置值
col dis:flex; flex-direction:column; col;
row dis:flex; flex-direction:row; row;
center col; justify:center; items:center; center;
fixed position:fixed; fixed;
static position:static; static;
absolute position:absolute; absolute;
relative position:relative; relative;
sticky position:sticky; sticky;
left left:0px; left;
top top:0px; top;
right right:0px; right;
bottom bottom:0px; bottom;
bold font-weight: bold; bold;

commonCSSValues 設置了一些 css-value, 其中的顏色、尺寸分類、投影均取自於 tailwindCSS 的配置:

使用預設的示例:

import React from "react";
import cssin from "cssin";

// 使用預設的自定義樣式和 css-value 配合使用
export default () => {
  return (
    <div className={cssin` bg:--gray-200; p:--2; font:--font-sm; box-shadow:--shadow-xl `} > Button </div>
  );
};
複製代碼

咱們能夠查看這兩個文件,它們只是使用 cssin API 的簡單配置,也歡迎有朋友提供更好的自定義樣式及組件:

commonSheets.ts

commonCSSValues.ts

性能開銷

cssin 雖然是運行時建立 css 樣式,可是它有着極低的性能開銷。

咱們能夠看到,建立重複執行 500 次,每次大約建立 20 條樣式,只消耗了 1.6ms, 這是由於 cssin 會對總體屬性作緩存,還會對子屬性建立 css 樣式作緩存:

console.time(t);
for (let i = 0; i < 500; i++) {
  cssin(
    `transition:all 0.1s ease-in; box-shadow:--shadow-1lg; hover:box-shadow:--shadow-1md; active:box-shadow:--shadow-sm1;`
  );
  cssin(
    `transition:all 0.2s ease-in; box-shadow:--shadow-2lg; hover:box-shadow:--shadow-2md; active:box-shadow:--shadow-sm2;`
  );
  cssin(
    `transition:all 0.3s ease-in; box-shadow:--shadow-3lg; hover:box-shadow:--shadow-3md; active:box-shadow:--shadow-sm3;`
  );
  cssin(
    `transition:all 0.4s ease-in; box-shadow:--shadow-4lg; hover:box-shadow:--shadow-4md; active:box-shadow:--shadow-sm4;`
  );
}
console.timeEnd(t); // 1.60009765625ms
複製代碼

如今開始使用它:

$ npm i cssin --save
複製代碼

盼望 Star 或提出貢獻,倉庫地址:

github.com/ymzuiku/css…

相關文章
相關標籤/搜索