家好,我卡颂。
近日,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小时内与您取得联系。