家好,我卡頌。
近日,Meta開源了一款「CSS-in-JS庫」 —— StyleX。看命名方式,Style - X是不是有點像JS - X,他們有關系么?當然有。
JSX是一種「用JS描述HTML」的語法規范,廣泛應用于前端框架中(比如React、SolidJS...),由Meta公司提出。
同樣的,按照Meta的設想,StyleX是一種「用JS描述CSS」的語法規范。
早在React Conf 2019[1],Meta工程師「Frank」就介紹了這種Meta內部使用的「CSS-in-JS庫」。
從Meta內部使用,到大會對外宣傳,這期間肯定已經經歷大量內部項目的洗禮。而從做完宣傳到最終開源,又經歷了快5年時間。
那么,這款Meta出品、打磨這么長時間的「CSS-in-JS庫」,到底有什么特點呢?
本文讓我們來聊聊。
市面上有非常多「CSS解決方案」,比如:
為什么需要這些方案?原生CSS哪里不好?在這里,我們舉個小例子(例子來源于「React Conf 2019」)。考慮如下代碼:
CSS文件如下:
.blue {color: blue;}
.red {color: red;}
HTML文件如下:
<p class="red blue">我是什么顏色?</p>
請問p標簽是什么顏色的?
從class來看,blue在red后面,p應該是藍色的么?
實際上,樣式取決于他們在樣式表中定義的順序,.red的定義在.blue后面,所以p應該是紅色的。
是不是已經有點暈了?再增加點難度。如果.red和.blue分別在兩個文件中定義呢?
# css文件1
.blue {color: blue;}
# css文件2
.red {color: red;}
那p的樣式就取決于最終打包代碼中樣式文件的加載順序。
上面只是原生CSS中「選擇器優先級相關的一個缺陷」(除此外還有其他缺陷,比如「作用域缺失」...)。隨著項目體積增大、項目維護時間變長、項目維護人員更迭,這些缺陷會被逐漸放大。
正是由于這些原因,才出現了各種「CSS解決方案」。
StyleX的API很少,掌握下面兩個就能上手使用:
比如:
import * as stylex from 'stylex';
// 創建樣式
const styles = stylex.create({
red: {color: 'red'},
});
// 定義props
const redStyleProps = stylex.props(styles.red);
使用時:
<div {...redStyleProps}>文字顏色是紅色</div>
stylex是如何解決上面提到的red blue優先級問題呢?其實很簡單,考慮如下代碼:
import * as stylex from 'stylex';
// 創建樣式
const styles = stylex.create({
red: {color: 'red'},
blue: {color: 'blue'}
});
// 使用
<p {...styles.props(styles.red, styles.blue)}></p>
樣式的優先級只需要考慮styles.props中的定義順序(blue在red后面,所以顏色為blue),不需要考慮樣式表的存在。
有些同學會說,看起來和常見的CSS-in-JS沒啥區別啊。那stylex相比于他們的優勢是啥呢?
首先要明確,stylex雖然以CSS-in-JS的形式存在,但本質上他是一種「用JS描述CSS的規范」。文章開頭也提到,他的定位類似JSX。
既然是規范,那他就不是對CSS的簡單封裝、增強,而是一套「自定義的樣式編寫規范」,只不過這套規范最終會被編譯為CSS。
作為對比,Less、Sass這樣的「CSS預處理器」就是對CSS語法的封裝、增強
那么,stylex都有哪些規范呢?
比如,stylex鼓勵將樣式與組件寫在同一個文件,類似Vue的SFC(單文件組件)。這么做除了讓組件的樣式與邏輯更方便維護,也減少了stylex編譯的實現難度。
再比如,CSS中各種選擇器的復雜組合增強了選擇器的靈活性。但同時也增強了不確定性。舉個例子,考慮如下三個選擇器:
這些對.className應用的選擇器將影響.className的某些后代。當這樣的選擇器多了后,很可能會在開發者不知道的情況下改變某些后代元素的樣式。
遇到這種情況我們一般會怎么處理呢?正確的選擇當然是找到上述影響后代的選擇器,再修改他。
但大家工作都這么忙,遇到這種問題,多半就是用新的選擇器覆寫樣式,必要的時候還會加!important后綴。久而久之,這代碼就沒法維護了。
為了規避這種情況,在stylex中,除了「可繼承樣式」(指「當父元素應用后,子孫元素默認會繼承的樣式」,比如color)外,不支持這些「可以改變子孫后代樣式的選擇器」。
那我該如何讓子孫組件獲得父組件同樣的樣式呢?通過props透傳啊~
也就是說,stylex禁用了CSS中可能造成混淆的選擇器,用JS的靈活性彌補這部分功能的缺失。
有些同學可能會說:這些功能,其他「CSS-in-JS庫」也能做啊。
這就要談到「CSS-in-JS庫」最大的劣勢 —— 為了計算出最終樣式,在運行時會造成額外的樣式計算開銷。
stylex通過編譯來減少運行時的開銷。比如對于上面提到過的stylex的代碼:
import * as stylex from 'stylex';
// 創建樣式
const styles = stylex.create({
red: {color: 'red'},
});
// 定義props
const redStyleProps = stylex.props(styles.red);
編譯后的產物包括如下兩部分:
JS的編譯產物:
import * as stylex from 'stylex';
const redStyleProps = {className: 'x1e2nbdu'};
CSS的編譯產物:
.x1e2nbdu {
color: red;
}
所以,運行時實際運行的代碼始終為:
<div {...{className: 'x1e2nbdu'}}>...</div>
對于再復雜的樣式,stylex都會通過編譯生成「可復用的原子類名」。
即使是跨文件使用樣式,比如我們在另一個文件也定義個使用color: 'red'樣式的stylex屬性foo:
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
foo: {
color: 'red',
},
bar: {
backgroundColor: 'blue',
},
});
會得到如下編譯結果,其中x1e2nbdu是一個原子類名,他是上一個文件中styles.red的編譯產物:
import * as stylex from '@stylexjs/stylex';
const styles = {
foo: {
color: 'x1e2nbdu',
$$css: true,
},
bar: {
backgroundColor: 'x1t391ir',
$$css: true,
},
};
隨著項目體積增大,樣式表的體積也能控制在合理的范圍內。這種對原子類名的控制粒度是其他「CSS-in-JS庫」辦不到的。
stylex相比TailwindCSS這樣的原子CSS有什么優勢呢?
這就要談到原子CSS的一個特點 —— 使用約定好的字符串實現樣式。比如,使用TailwindCSS定義圖片的樣式:
<img class="w-24 h-24 rounded-full mx-auto" src="/sarah-dayan.jpg" alt="" width="384" height="512">
效果如下:
由于樣式都是由不同的「原子類名字符串」組合而成,TS沒法分析,這就沒法實現「樣式的類型安全」。
什么叫「樣式的類型安全」?通俗的講,如果我實現一個組件,組件通過style props定義樣式,我只希望使用者能夠改變color與fontSize兩個樣式屬性,不能修改其他屬性。如果能實現這一點,就是「樣式的類型安全」。
「樣式的類型安全」有什么意義呢?舉個例子:設想開發基礎組件庫的團隊使用stylex。那么當業務團隊使用該組件庫時,就只能自定義組件的一些樣式(由組件庫團隊約束)。
當基礎組件庫升級時,組件庫團隊能很好對組件樣式向下兼容(因為知道只有哪些樣式允許被修改)。
在stylex中,由于stylex.create的產物本質是對象,所以我們可以為每個產物定義類型聲明。比如在如下代碼中,我們限制了組件style props只能接受如下stylex樣式:
import type {StyleXStyles} from '@stylexjs/stylex';
type Props = {
// ...
style?: StyleXStyles<{
color?: string;
backgroundColor?: string;
borderColor?: string;
borderTopColor?: string;
borderEndColor?: string;
borderBottomColor?: string;
borderStartColor?: string;
}>;
};
我猜想,當更多人知道stylex后,他會收到比當初TailwindCSS火時更多的兩級分化的評價。
畢竟,stylex的設計初衷是為了解決Meta內部復雜應用的樣式管理。如果:
那大概率是不能接受stylex設計理念中的這些約束。
對于stylex,你怎么看?
[1]
React Conf 2019: https://www.youtube.com/watch?v=9JZHodNR184&t=270s
前端開發中,Vue 一直以其簡單、高效的框架而備受開發者青睞。然而,隨著 React 在市場上的流行,許多開發者開始對 JSX(JavaScript XML)這種聲明式編程風格產生興趣。本文將探討 JSX 在 Vue3 中的應用,并對其是否成為 Vue3 前端開發的未來進行論證。
在開始之前,我們先來了解一下 Vue 本身的模版語法和 JSX 分別是什么。
Vue3 模版語法是 Vue.js 中常用的一種聲明式模板語法,使用 HTML 語法來描述視圖。在模板語法中,可以通過插值、指令和事件綁定等方式來將數據與視圖關聯起來。這是其簡單易用和上手快的主要原因之一。
<template>
<div>
<h1>{{ title }}</h1>
<p v-if="showText">{{ text }}</p>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
title: 'Vue3 Template Syntax',
text: 'This is a demo text.',
showText: true,
list: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
],
};
},
};
</script>
Vue3 的模板語法使用雙花括號({{ }})進行數據插值,使用v-if和v-for等指令處理條件和循環邏輯。
JSX 是一種 JavaScript 的語法擴展,它允許在 JavaScript 代碼中編寫類似于 XML 的結構。React 是第一個廣泛使用 JSX 的框架,它將組件的結構和邏輯封裝在 JSX 中,用于描述 UI 組件的層次結構。隨著 React 的成功,也隨著時間的推移,JSX 逐漸成為了一種通用的模式,被許多其他框架和庫所采用支持。
React示例:
import React, { useState } from 'react';
function JSXComponent() {
const [title, setTitle] = useState('JSX in React');
const [showText, setShowText] = useState(true);
const list = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
];
return (
<div>
<h1>{title}</h1>
{showText && <p>This is a demo text.</p>}
<ul>
{list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
export default JSXComponent;
Vue3示例:
import { defineComponent, ref } from 'vue';
const JSXComponent = defineComponent({
setup() {
const title = ref('JSX in Vue3');
const showText = ref(true);
const list = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
];
return {
title,
showText,
list,
};
},
render() {
return (
<div>
<h1>{this.title}</h1>
{this.showText && <p>This is a demo text.</p>}
<ul>
{this.list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
},
});
export default JSXComponent;
從上面不難看出,在 JSX 中,我們可以直接使用 JavaScript 表達式(如{title}),也可以使用條件語句(如{showText && <p>This is a demo text.</p>})和數組的map方法來處理循環邏輯。
這些只是舉例,有很多的 JavaScript 語法和方法等,都可以在這里使用。總之,使用 JSX 開發,可以很大程度上利用好 JavaScript,開發更加方便。
在 Vue3 中,我們可以通過 Vue 官方提供的項目腳手架工具 create-vue 來新建一個支持JSX的項目。首先,確保你安裝了最新版本的 Node.js(我用的是 16+ 版本),然后執行以下命令:
npm init vue@latest
這個命令將會安裝并執行 create-vue 工具。在執行過程中,你會看到一些可選的功能配置提示,其中會有如下內容:
Vue.js - The Progressive JavaScript Framework
? Project name: … vue-project
? Add TypeScript? … No / Yes
? Add JSX Support? … No / Yes
? Add Vue Router for Single Page Application development? … No / Yes
? Add Pinia for state management? … No / Yes
? Add Vitest for Unit Testing? … No / Yes
? Add an End-to-End Testing Solution? ? No
? Add ESLint for code quality? … No / Yes
? Add Prettier for code formatting? … No / Yes
Scaffolding project in ...
執行到? Add JSX Support?時選擇Yes,這樣 JSX 就會自動安裝。如果不確定是否要開啟某個功能,你可以直接按下回車鍵選擇 No。
當然,你也可以在已有的Vue項目中配置JSX。
在已有項目中配置JSX,首先需要安裝相關依賴:
npm install --save-dev @vue/babel-plugin-jsx
然后,在項目的vite.config.ts文件中進行配置。具體的配置內容如下圖所示:
image.png
配置完成后,現在我們就可以在項目中使用 JSX 語法來開發了。這樣,我們可以根據具體的場景選擇使用 Vue 模板或 JSX 語法。
總的來說,配置 JSX 是非常簡單的,通過上述步驟,我們可以輕松地在 Vue 項目中使用 JSX,發揮其簡潔、易讀的優勢,讓我們的代碼更加優雅和高效。
現在,我們來對比一些具體的代碼示例,看看 Vue3 模板語法和 JSX 之間的差異。
1000.webp
Vue3 模板語法使用雙花括號{{}}進行數據插值,而 JSX 使用花括號{}。
模板示例:
<template>
<p>{{ message }}</p>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const message = ref('Hello, JSX!');
return { message };
},
};
</script>
JSX示例:
import { defineComponent } from 'vue';
const DynamicData = defineComponent({
setup() {
const message = ref('Hello, JSX!');
return { message };
},
render() {
return <p>{this.message}</p>;
},
});
在 Vue3 中,我們可以使用v-if指令進行條件渲染,而在 JSX 中,我們使用 JavaScript 的條件表達式。
模板示例:
<template>
<div>
<p v-if="showContent">Content is visible.</p>
<p v-else>Content is hidden.</p>
<button @click="toggleContent">Toggle</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const showContent = ref(true);
function toggleContent() {
showContent.value = !showContent.value;
}
return { showContent, toggleContent };
},
};
</script>
JSX示例:
import { defineComponent } from 'vue';
const ConditionalRender = defineComponent({
setup() {
const showContent = ref(true);
return { showContent };
},
render() {
return (
<div>
{this.showContent ? <p>Content is visible.</p> : <p>Content is hidden.</p>}
<button onClick={() => this.showContent = !this.showContent}>Toggle</button>
</div>
);
},
}};
在 Vue3 中,我們使用v-for指令來處理列表渲染,而在 JSX 中,我們使用 JavaScript 的map方法。
模板示例:
<template>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const items = ref(['Apple', 'Banana', 'Orange']);
return { items };
},
};
</script>
JSX示例:
import { defineComponent } from 'vue';
const ListRendering = defineComponent({
setup() {
const items = ref(['Apple', 'Banana', 'Orange']);
return { items };
},
render() {
return (
<ul>
{this.items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
},
});
接下來,我們將對 Vue 模板和 JSX 進行對比,從多個方面分析它們的優勢與劣勢。
Vue 模板使用 HTML 語法,更加接近常規 HTML 結構,因此對于前端開發者來說比較容易上手。然而,「隨著項目的復雜性增加,模板中可能會包含大量的指令和插值,導致代碼變得冗長。」 例如,條件渲染、循環遍歷等情況都需要使用 Vue 特定的指令。「相比之下,JSX 在 JavaScript 語法的基礎上,使用類似 XML 的結構,使得代碼更加緊湊和直觀。」
模板示例:
<template>
<div>
<h1 v-if="showTitle">{{ title }}</h1>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
JSX示例:
const MyComponent = () => {
return (
<div>
{showTitle && <h1>{title}</h1>}
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
從上面的對比可以看出,JSX語法更加簡潔,尤其在條件渲染和循環遍歷方面更加靈活。
Vue.js 本身就支持組件化開發,但是在 Vue 模板中,組件的定義與使用是通過特定的 HTML 標簽和 Vue 指令實現的。這種方式雖然簡單易懂,但是在代碼重用和維護方面可能會有一些限制。相比之下,JSX 在 React 中的組件化開發非常強大,組件可以直接在 JavaScript 中定義,并且支持更加靈活的組合方式。
模板示例:
<template>
<div>
<my-component :prop1="value1" :prop2="value2" />
</div>
</template>
JSX示例:
const MyComponentWrapper = () => {
return (
<div>
<MyComponent prop1={value1} prop2={value2} />
</div>
);
};
從上面的對比可以看出,JSX 允許在JavaScript中直接定義組件,并且組件的傳參更加直觀。
由于 Vue 模板使用了特定的指令和 HTML 語法,IDE 對于代碼的支持可能相對有限。而 JSX 是 JavaScript 的擴展語法,因此可以與類型檢查工具(如TypeScript)完美結合,同時得到更好的 IDE 支持,例如自動補全、代碼跳轉等。
Vue 模板的語法是固定的,不能隨意擴展。而 JSX 作為 JavaScript 的一部分,不需要額外的指令或語法,可以通過編寫函數和組件來擴展其語法,也使得處理邏輯、計算和條件渲染變得更加靈活和方便。
JSX 作為 React 的核心特性,擁有龐大的社區和豐富的生態系統,為開發者提供了海量的工具和庫。
同時 JSX 在多個框架中得到了廣泛支持,開發者們可以更輕松地在不同的項目和技術棧之間切換,而無需學習不同的模板語法。如文章上面“如何配置JSX?”中對已有項目的配置,將 JSX 作為插件寫到 Vue Plugin 即可配置完成。
Vue3 的模板語法和 JSX 各有優劣,因此它們在不同的場景下有不同的適用性。
無論是從開發體驗、技術生態還是未來趨勢來看,JSX它使得組件的模板結構更加清晰、聲明性,提供了更強大的 JavaScript 表達能力,同時也增強了與其他技術棧的互通性。雖然傳統的 Vue2 模板語法在一定程度上仍然適用,但通過引入 JSX,Vue3 在前端開發領域擁有更廣闊的發展前景。開發者們可以更加便利地構建復雜的交互界面,同時又能享受到 Vue3 帶來的性能優勢。
看一眼 API,乍一看仿佛和 fard 類似的 API,仿佛又寫了個跨端小程序框架?
然而并不是……
voe 是一個底層小程序框架
意思是它實現了小程序的雙線程,利用“沙箱” 隔離 web 環境,屏蔽 dom 能力
接下來結合 微信小程序 介紹一下主要思路:
目標
絕對的控制力,意思是用戶不能操作 dom,不能使用 web API,不能使用完整的 jsx 和 html,不能……反正就是啥都不能!
就好像 sm 角色一樣,s 對 m 的絕對控制與虐待,m 只能服從命令與受虐
所以我把小程序的雙線程架構又稱為 【主奴架構】
沙箱
小程序中不能操作 dom,不是因為它屏蔽了 dom API 或者屏蔽了事件,這樣做是不切實際的
大家都是尋找一個非 dom 環境作為沙箱,比如 v8,比如 worker,比如 native,比如 wasm
以上都是 OK 的,我猜微信小程序等也是用的類似的沙箱
voe 使用 web worker 作為沙箱
為什么不使用 v8 或 native?
這就是純粹的個人選擇了,不選擇 v8 或 native 應該是,但是偏偏我個人更偏前一點,web worker 的計算力默認是優于 v8 或 native 的(同等硬件水平),但是 v8 也有好處,比如 node 可以使用 cookie,然后擁有一些先進的 API
將框架拆到兩個不同的線程中
重點來了,兩個線程中,如何安排框架工作呢?
有幾個原則:
于是乎,就變成下面這個樣子:
然后,困難如約而至,沙箱與主線程之間的鴻溝來自 dom 元素和 事件函數,這兩者無法傳遞
我絞盡腦汁,想了一個完全之策
將不能傳遞的東西保存到自己線程中并建立映射,將索引傳到另一個線程
比如,事件是這樣傳遞的:
let handlers = new WeakSet() if (props) { for (const k in props) { if (k[0] === 'o' && k[1] === 'n') { let e = props[k] let id = handlers.size + 1 handlers.set({ id: e }) props[k] = id } } }
將事件放到 map 中存起來,然后將 id 傳給主線程,主線程事件觸發的時候,將 id 和 event 參數交還給 worker
for (const k in props) { if (k[0] === 'o' && k[1] === 'n') { let id = props[k] props[k] = event => { // 不能傳太多,此處省略對事件的簡化操作 worker.postMessage({ type: EVENT, event, id }) } } }
然后在 worker 中,根據索引映射,找到對應的事件并觸發
是的沒錯就是這樣,這個方法是萬能的,比如我們的 diff 方法
既然 diff 無法將 dom 傳出去,那么我們就傳 dom 的 index
if (oldVNode ==null||oldVNode.type!==newVNode.type) { parent.insertBefore(createNode(newVNode), node) }
比如這個方法,parent 和 node 都是 dom 元素,是沒辦法傳遞的,我們就……傳他們的索引,may be 長這樣:
[ [0,'insertBefore',1] ]
dom 是這樣的:
<div id="root" index="0"> <ul index="1"> <li index="2" /> <li index="3" /> </ul> </div>
如果此時我們要刪除 index 為 3 的節點,那對應生成的結構,應該是這樣:
[ [1,'removeChild',3] ]
刺不刺激,我們成功找到了一個思路,能夠實現不同的 diff 算法啦
要知道,微信小程序就沒有找到類似的思路,他們的 diff 還是 virtual-dom 的那套古老的深度遍歷,代碼多性能差……
綜上所述,上面介紹了雙線程的主要思路,這些思路不僅僅適用于這個框架,同樣適用于其他跨端的場景
vue 3
這里簡單說一下 vue 3,嗯大家看到,voe 的名字和 API 神似 vue 3,事實上我確實將 vue 3 的核心抄過來了,包括依賴收集,響應式,狀態更新,組合函數……
這只是順手的事兒,其實比起 voe 的核心思路,API 是沒什么所謂的
因為幾乎所有的公司,如果想要搞自己的小程序,都只能過來參考思路,然后 API 很可能就和微信保持一致了
所以我覺得 vue 3 的 API 足夠簡單,正好可以弱化 API
就抄過來了……
大家可以可以將 voe 作為 vue 3 的最小實現,用于協助閱讀源碼也是很 OK 的哈!
雙線程 vs 異步渲染
題外話,大家應該都知道我之前寫的框架 fre.js,也應該對 concurrent(時間切片)、suspense 等異步渲染的機制有所了解
如今我又來搞 web worker,是因為它倆的思路是類似的,場景也是一樣的
兩者的場景同樣都是可視化,高幀率動畫,大量數據與計算……
可惜本身這種場景需求實在太少了,所以 preact 和 vue 團隊紛紛發聲,表示不想搞也不需要搞::>_<::
但是到我這來說的話,其實我不在意框架有沒有人用,也不在于業務的,我更加傾向于一種技術創新,所以從這個方面,只要是新的思路,總歸有它的價值
總結
呼~終于寫完了,在掘金發文,必須要長啊
最后放上 voe 的 github:
github.com/132yse/voe
作者:132
鏈接:https://juejin.im/post/5dd1edf3e51d4561ea3fb3cd
*請認真填寫需求信息,我們會在24小時內與您取得聯系。