整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

为什么我推荐使用JSX开发Vue3

很长的一段时间中,Vue 官方都以简单上手作为其推广的重点。这确实给 Vue 带来了非常大的用户量,尤其是最追求需求开发效率, 往往不那么在意工程代码质量的国内中小企业中,Vue 占据的份额极速增长。但是作为开发者自身,我们必须要认清一个重点,简单易用从来不应该在技术选型中占据很大的份额,可维护性才是。

以防万一有的同学实在不看官方文档,我先提一嘴,SFC 就是写 Vue 组件的时候写的.vue文件,这一个文件就是一个 SFC,全称 Single File Component,也即单文件组件。

在开始说我个人的观点之前,我们先来看几个事实:

一是:Vue3 的定义原生支持 JSX,并且 Vue3 源码中有jsx.d.ts来便于使用 JSX。 不知道同学们看到这里会想到什么, 我的第一反应是:社区对于 JSX 的需求声音是不小的,所以会反向推动 Vue3 官方对于 JSX 的支持。

二是:AntDesign 的 vue3 版本,基本全部都是用 JSX 开发的,而且 Vue3 现在官方的 babel-jsx 插件就是阿里的人一开始维护的, 虽然我向来不喜欢阿里系的 KPI 推动技术方式,而且现在的 JSX 语法支持也不是很符合我的期望,但至少在使用 JSX 开发是更优秀的选择这点上,我还是很认可 AntDesign 团队的。

OK,说这些呢,主要是先摆出一些事实作为依据,让有些同学可以不需要拿什么:

  • 啊,这都是你空想的,你太自以为是了
  • 你再怎么想都没用,咱们 Vue 就是应该用 SFC 开发

这些观点来批斗我,首先我都会从客观的角度来分析为什么,至少是我是能讲出优劣势的理由的。

OK,前言差不多到这里,接下来咱给您分析分析,为什么你应该选择 JSX 来开发 Vue。


TypeScript 支持

其实第一点就已经是杀手了,对于想要使用 TypeScript 来开发 Vue3 应用的同学来说,这简直就是 SFC 无法克服的世界难题。

一句话概括:TypeScript 原生支持 JSX 语法,而基本无望 TS 官方能支持 SFC 的 template 语法

TS 毫无疑问在前端社区的重要性越来越大,但凡未来对于代码质量有一定要求的前端团队,都应该会选择使用 TS 来进行开发。 而且现在基本上在 NPM 上都能看到包你都能找到对应的 TS 定义,现在使用 TS 开发成本已经只剩下你是不是会 TS 语法了,在这种情况下是否支持 TS 则是开发模式在未来走不走的远的重要原因。

目前 SFC 只能通过shim让 TS 可以引入.vue文件,但是对于所有 SFC 的组件的定义都是一样的:

declare module '*.vue' {
    import { DefineComponent } from 'vue'
    const component: DefineComponent<{}, {}, {}, any>
    export default component
}

也就是说你引入的 SFC 组件,TS 是不知道这个组件的 Props 应该接收什么的。所以你无法享受到这些 TS 的优势:

  • 开发时的自动提示
  • 编译时的 TS 校验,让你尽早发现问题
  • 编译组件生成你的组件定义(对于类库开发尤其重要)

当然你会说既然 Vue 官方能开发处 SFC 的语法,自然会支持这些特性。我表示这当然有可能,但是这个难度是非常大的,需要很多方面的支持,甚至可能需要 TS 官方团队愿意协助, 但是我想不到 TS 官方有什么理由来支持 SFC,因为这只是 Vue 自己创建的方言,在其他场景下是没有使用的,TS 是面向全社区的,我觉得他们不会考虑主动来支持 SFC。

那么有同学要问了,JSX 不也是非原生的 JS 语法么,他怎么就能让 TS 官方支持了呢,是不是 FB 和微硬之间有什么 PY 交易?

这就涉及第二点了,JSX 和静态模板的灵活性区别。

JSX 其实并不是方言

很多人弄错了一个问题,就是觉得 SFC 的模板语法和 JSX 是一样的,都是一种别人发明的语法,并不是 JS 原生的。这是事实,但又有一些区别,这个区别主要是体现在对于 JSX 的认知上。

一句话概括:JSX 并没有扩展 JS 的语法,他只是缩略了 JS 的写法!其本质就是 JS 的语法糖

就像 es6 给增加的语法糖,比如

const a = 1
const b = 2

const obj = { a, b }

// 其实就等价于
const obj = { a: a, b: b }

这种写法并没有扩展 JS 的能力,只是简便了写法,JSX 也是一样的。

JSX 其实就是方法调用,他和 JS 是有一对一对应关系的,我们来看一个例子:

const element = <div id="root">Hello World</div>

这里的 JSX 语法编译之后其实就是:

const element = createElement('div', { id: 'root' }, 'Hello World')

而 JSX 就是这些了,没有什么更多的内容,所以说 JSX 只是方便我们写嵌套的函数调用的语法糖,而其本身没有扩展任何其他的内容。

但是 SFC 就不一样了。

SFC 定义的不仅是语法,更是文件。

SFC 的具体定义是单文件组件,它本身就是把一个文件看作一个单位,所以他的约束性是要大很多的,你必须具有固定的文件结构才能使用 SFC,这做了很多的限制:

  • 一个文件只能写一个组件
  • 节点片段只能写在 template 里面,非常不灵活
  • 变量绑定只能获取this上面的内容,不能使用全局变量(很多时候我们都要把全局变量先挂载到this上)

我们一点点来讲

一个文件只能写一个组件

这个说实话非常非常不方便,很多时候我们写一个页面的时候其实经常会需要把一些小的节点片段拆分到小组件里面进行复用(如果你现在没有这个习惯可能就是因为 SFC 的限制让你习惯了全部写在一个文件内)。

React 生态中丰富的 css-in-js 方案就是很好的例子,我们可以通过:

const StyledButton = styled('button', {
    color: 'red',
})

如果我们这个页面需要使用特定样式的按钮,通过这种方式在页面文件里面封装一下是非常常见的。因为没必要把这个组件拆分出去,他也不是一个可复用的组件,拆分出去了还要多一次import。

Vue 生态基本没有 css-in-js 的成熟方案其实跟这个限制也很有关系。

再来一个例子,比如我们封装了一个 Input 组件,我们希望同时导出 Password 组件和 Textarea 组件来方便用户根据实际需求使用,而这两个组件本身内部就是用的 Input 组件,只是定制了一些 props:

const Input = { ... }

export default Input

export const Textarea = (props) => <Input multiline={true} {...props} />

export const Password = (props) => <Input type="password" {...props} />

在 JSX 中可以非常简单地实现,但是如果通过 SFC,你可能就要强行拆成三个文件,另外为了方便,你可能还要增加一个index.js来导出这三个组件,你能想象这多了多少工作量么。

节点片段只能写在 template 里面,非常不灵活

我不知道有多少同学看过 Vue 的 template 编译出来之后的代码,以我的经验来说看过的可能不会超过 50%(乐观估计),建议同学们如果还不了解的,可以去尝试看一下。

为什么要看这个呢?因为你看了之后你会发现,你在 template 里面写的类似 HTMl 的内容,其实跟 HTML 根本没啥关系,他们也会被编译成类似 JSX 编译出来的结果。

{
    render(h) {
        return h('div', {on: {}, props: {}}, h('span'))
    }
}

类似这样的结果,而这里面h函数调用的结果就是一个 VNode,是 Vue 中的节点的基础单元。那么既然这些单元就是一个对象,其实理所当然的,他们是可以作为参数传递的。 也就是说,理论上他们是可以通过props把节点当作参数传递给其他组件的。

这个做法在 React 中非常常见,叫做renderProps,并且其非常灵活:

const Comp = () => <Layout header={<MyHeader />} footer={<MyFooter />} />

但是因为 SFC 模板的限制,我们很难在 SFC 里面的 props 上写节点:

<template>
    <Layout :header="<MyHeader/>"></Layout>
</template>

这样写是不行的,因为 SFC 定义了:header绑定接受的只能是 js 表达式,而<MyHeader/>显然不是。

因为通过 props 传递不行,所以 Vue 才发明了 slot 插槽的概念

虽然我们一直再说 Vue 简单,但是事实上ScopedSlots一度成为新手理解 Vue 的噩梦,很多同学都被这个绕来绕去的作用域整的死去活来。

我们看一个ScopedSlots的例子:

<template>
    <Comp>
        <template v-slot:scope="ctx">
            <div>{{ctx.name}}</div>
        </template>
    </Comp>
</template>

这里ctx是Comp里面的属性,通过这种方式传递出来,让我们在当前组件可以调用父组件里面的属性。这简直就是理解的噩梦,但是如果用 JSX 实现类似功能就非常简单:

<Comp scope={name => <div>{name}</div>} />

我们只是给一个叫做scope的 props 传递来一个函数,这个函数接受一个name属性,在Comp里面会调用这个函数并传入name。 简单来说我们传入的就是一个构建节点片段的函数,就是这么简单。

这就是因为 SFC 的模板的限制,导致灵活性不足,Vue 需要去创造概念,创造关键字来抹平这些能力的不足,而创造的概念自然就引入了学习成本。

所以其实我一直不认可 Vue 比 React 好学的说法的,如果你真的认真研究所有用法,并且总是尝试用最合理的方式实现功能,那么 Vue 绝对不会比 React 简单。

变量绑定只能获取this上面的内容,不能使用全局变量

这个体现在两个方面,一个是我们定义在全局的一些固定数据如果要在组件内使用的话,就要通过this挂载到组件上。

比如我们缓存了一份城市数据,这种数据基本上是不会改的,所以也没必要挂载到组件上让其能够响应式。但是在 SFC 里面这是做不到的, 因为模板的执行上下文是在编译时绑定。你在模板里面访问的变量,都会在编译时自动绑定到this上,因为模板需要编译,其本身也是字符串不具有作用域的概念。

而这在 JSX 中则不复存在:

const citys = []

const Comp = () => {
    return citys.map(c => <div>{c}</div>)
}

另外一个方面则是在组件使用上,在 SFC 中,组件必须事先注册,因为我们在模板里面写的只能是字符串而不能是具体某个组件变量。 那么模板中的组件和真实的组件对象只能通过字符串匹配来实现绑定。这带来了以下问题:

  • 多了注册组件这个步骤,增加代码量
  • 通过字符串名注册自然就会出现可能的冲突问题
  • 模板解析组件支持不同的样式,比如<MyComp>和<my-comp>,容易导致风格不一的问题

在 JSX 中则没有这些问题,因为 JSX 里面直接使用组件引用作为参数:

const Comp = {...}

const App = () => <Comp />

需要通过directive来扩展能力

其实上面能看出来,除了 SFC 本身的问题之外,Vue 使用字符串模板也会带来很多的灵活性问题。 最直接的证据,就是 Vue 使用了directive来扩展功能(当然这不是 Vue 发明的,老早的模板引擎就有类似问题)。

为什么说directive是不得已的选择呢?因为静态模板缺失逻辑处理的能力。我们拿列表循环举例,在 JS 中我们可以非常方便地通过map函数来创建列表:

const list = arr.map(name => <span key={name}>{name}</span>)

而因为 JSX 本身就是函数调用,所以上面的代码和 JSX 结合起来也非常自然:

const App = () => (
    <div>
        <Header />
        {arr.map(name => (
            <span key={name}>{name}</span>
        ))}
    </div>
)

上面的例子对应到 JS 如下:

const App = () =>
    createElement('div', {}, [
        <Header />,
        arr.map(name => createElement('span', { key: name }, name)),
    ])

这仍然是因为 JSX 只是 JS 的语法糖的原因,所有能在 JS 中实现的在 JSX 里面都能实现。

而 SFC 的模板是基于字符串编译的,其本身就是一段字符串,我们不能直接在模板里面写map来循环节点,(当然我们可以在可以接收表达式的地方写,比如v-on里面)。

那么我们不能循环节点,有需要这样的功能来渲染列表,怎么办呢?就是发明一个标志来告诉编译器这里需要循环,在 Vue 中的体现就是v-for指令。

同学们可能要问了,既然 Vue 能实现v-for,为什么不直接实现表达式循环列表呢?他当然也可以实现,但是他肯定不会这么选,因为成本太高了。 他要这么做就相当于他要实现一个 JS 引擎,而其实里面很多内容又是不必须的,一个v-for其实就能够适用大部分情况了。

但有了v-for就需要v-if,那么后面还会需要其他各种能力,这就是一种方言的产生和发展的过程。

当然指令也不仅仅是 JS 表达式的代替品,其本身也是增加了一些其他能力的,比如它能够让我们更方便地访问 DOM 节点, 但是嘛,我们用框架的理由不就是为了能够尽可能的屏蔽 DOM 操作嘛~

总结

以上就是我对应该选择使用 JSX 还是 SFC 进行开发的分析,其实归根到底 SFC 的问题在于其没有拥抱 JS, 他的语法是自己发明的,他需要有一个 JS 实现的 compiler 来让其最终能在 JS 环境中运行,这本质上就是一种发明, 我们不能否认发明确实有优点,但我们也不能只看有点不看问题,没能拥抱 JS 自然就很难完全复用 JS 社区的优势 而 JS 社区一直在蓬勃发展,好用的工具一直在涌现,而 SFC 想要使用 JS 社区的这些工具还要自己再实现一份,我们可以细数以下 SFC 做了哪些兼容

  • vue-loader 之于 webpack
  • eslint-plugin-vue 之于 eslint
  • rollup-plugin-vue 之于 rollup
  • vue-jest 之于 jest
  • Vetur 用来做代码提醒

基本上常用的工具我们都需要等待 Vue 社区或者官方开发了插件之后才能运行。而 JSX 因为有 babel 和 typescript 的官方支持, 基本上所有新的 JS 生态工具原生都是支持的。

在这 Vue3 开始预备发力的阶段,我们还是希望 Vue 社区能够使用更优秀更规范的方式来进行开发, 其实如果我们直接使用 JSX 开发 Vue3,我们会发现很多时候我们都不需要用到emit、attrs这些概念, 甚至如果 Vue3 的 JSX 插件支持,我们甚至能够抛弃slots。

但是因为 Vue3 一定要考虑兼容 Vue2,导致本身潜力很好的 Vue3 总是显得缩手缩脚,这不得不说是一种遗憾。

学习如何做一个网站,却又不知道从哪里下手,但是又不想花钱去找外包公司,那么自己到底该怎么做一个网站呢?

今天我来给大家说明一下想自己做个网站需要学会什么东西。

1:html

想自己做个网站,首先得把网站的模板做出来,而html就是就是做网站要学习的。比如网页上要显示内容,需要html;网站上需要显示图片,需要html;网站上需要文字,需要html。

2:css

光有文字还是不行,还需要考虑布局、排版和颜色,这时候就需要学习css了。虽然现在有css3可以制作动图和特效,但是css的基本用处还是布局、排版和颜色。

3:js

弄完内容和排版布局,很多人认为网站的一个页面就这么搞定了,但是大错特错,我们还需要js来做网页的一些特效。比如鼠标停在哪个位置会变形,或者哪个图片不停地变换,再或者跳转出一个广告视频,这些都是需要js来做的。

4:后台制作

这个是作用于网站的管理用的,反正除了自己其他的人看不到,就不需要太大的要求,直接制作个简单好管理的界面,再搭建个信息管理系统,后台基本就完成了。

5:美工设计

说到美工,大家一定会想到图片,没错,就是做图片,不过网站的美工不只是做图片,还涉及到摄影,毕竟网站上不只是有图片,有时候还要插入一些动图或视频,做不能每次需要图片或视频就到网站上找吧,所以至今动手很重要。

6:域名选购

这些基本和开发技术没有多大的关系,就是注册个域名,但是域名注册不好那么就是关系到网站的整体。就像网站上的那些二手域名,一个个简直就像是在淘宝一样,万一搞到的是一个有缺陷的域名,到时候网站搭建完后就要唱一首凉凉了。

7:服务器购买

光有域名还不行,当服务器是在国内的时候,是需要备案的。 备案流程现在简单多了,基本上7天就可以做完,不过其中有一个挺麻烦的地方,需要自己手写签字,然后再扫描出来发过去

以上就是自己做网站需要掌握的东西,不得不说的是,想自己总一个网站真的是非一日之功,那是需要下本钱的,就像html、css、js,这些单独学起来可能不怎么样,但是学到后面就越是麻烦,就像组合使用。

所以说,想自己学习开发技术在自己搭建网站,真的非一日之功,哪怕你天赋异禀,没有几个月的时间是不可能的。

所以像我这种天赋异禀的天才还是找个现成的网站模板就改下变成自己的比较实惠。就比如66好用源码网,里面有无数模板,随便改都可以,只要您懂点技术,怎么盗版都可以,不过带后台的需要把模板下载下来再修改。


SS in JS

CSS in JS是一种解决css问题想法的集合,而不是一个指定的库。从CSS in JS的字面意思可以看出,它是将css样式写在JavaScript文件中,而不需要独立出.css.less之类的文件。将css放在js中使我们更方便的使用js的变量模块化tree-shaking。还解决了css中的一些问题,譬如:更方便解决基于状态的样式更容易追溯依赖关系生成唯一的选择器来锁定作用域。尽管CSS in JS不是一个很新的技术,但国内的普及程度并不高。由于Vue和Angular都有属于他们自己的一套定义样式的方案,React本身也没有管用户怎样定义组件的样式[1],所以CSS in JS在React社区的热度比较高。

目前为止实现CSS in JS的第三方库有很多:(http://michelebertoli.github.io/css-in-js/)。像JSS[2]styled-components[3]等。在这里我们就不展开赘述了(相关链接已放在下方),这篇文章的重点是JS in CSS

JS in CSS又是什么

在上面我们提到CSS in JS就是把CSS写在JavaScript中,那么JS in CSS我们可以推断出就是可以在CSS中使用JavaScript脚本,如下所示。可以在CSS中编写Paint API的功能。还可以访问:ctx,geom。甚至我们还可以编写自己的css自定义属性等。这些功能的实现都基于CSS Houdini[4]

.el {
  --color: cyan;
  --multiplier: 0.24;
  --pad: 30;
  --slant: 20;
  --background-canvas: (ctx, geom) => {
    let multiplier = var(--multiplier);
    let c = `var(--color)`;
    let pad = var(--pad);
    let slant = var(--slant);

    ctx.moveTo(0, 0);
    ctx.lineTo(pad + (geom.width - slant - pad) * multiplier, 0);
    ctx.lineTo(pad + (geom.width - slant - pad) * multiplier + slant, geom.height);
    ctx.lineTo(0, geom.height);
    ctx.fillStyle = c;
    ctx.fill();
  };
  background: paint(background-canvas);
  transition: --multiplier .4s;
}
.el:hover {
  --multiplier: 1;
}

Houdini 解决了什么问题

CSS 与 JS的标准制定流程对比

在如今的Web开发中,JavaScript几乎占据了项目代码的大部分。我们可以在项目开发中使用ES 2020、ES2021、甚至提案中的新特性(如:Decorator[5]),即使浏览器尚未支持,也可以编写Polyfill或使用Babel之类的工具进行转译,让我们可以将最新的特性应用到生产环境中(如下图所示)。

JavaScript标准制定流程.png

而CSS就不同了,除了制定CSS标准规范所需的时间外,各家浏览器的版本、实战进度差异更是旷日持久(如下图所示),最多利用PostCSS、Sass等工具來帮我们转译出浏览器能接受的CSS。开发者们能操作的就是通过JS去控制DOMCSSOM来影响页面的变化,但是对于接下來的LayoutPaintComposite就几乎没有控制权了。为了解决上述问题,为了让CSS的魔力不在受到浏览器的限制,Houdini就此诞生。

CSS 标准制定流程.png

CSS Polyfill

我们上文中提到JavaScript中进入提案中的特性我们可以编写Polyfill,只需要很短的时间就可以讲新特性投入到生产环境中。这时,脑海中闪现出的第一个想法就是CSS Polyfill,只要CSS的Polyfill 足够强大,CSS或许也能有JavaScript一样的发展速度,令人可悲的是编写CSS Polyfill异常的困难,并且大多数情况下无法在不破坏性能的情况下进行。这是因为JavaScript是一门动态脚本语言[6]。它带来了极强的扩展性,正是因为这样,我们可以很轻松使用JavaScript做出JavaScript的Polyfill。但是CSS不是动态的,在某些场景下,我们可以在编译时将一种形式的CSS的转换成另一种(如PostCSS[7])。如果你的Polyfill依赖于DOM结构或者某一个元素的布局、定位等,那么我们的Polyfill就无法编译时执行,而需要在浏览器中运行了。不幸的是,在浏览器中实现这种方案非常不容易。

页面渲染流程.png

如上图所示,是从浏览器获取到HTML到渲染在屏幕上的全过程,我们可以看到只有带颜色(粉色、蓝色)的部分是JavaScript可以控制的环节。首先我们根本无法控制浏览器解析HTML与CSS并将其转化为DOMCSSOM的过程,以及Cascade,Layout,Paint,Composite我们也无能为力。整个过程中我们唯一完全可控制的就是DOM,另外CSSOM部分可控。

CSS Houdini草案中提到,这种程度的暴露是不确定的、兼容性不稳定的以及缺乏对关键特性的支持的。比如,在浏览器中的 CSSOM 是不会告诉我们它是如何处理跨域的样式表,而且对于浏览器无法解析的 CSS 语句它的处理方式就是不解析了,也就是说——如果我们要用 CSS polyfill让浏览器去支持它尚且不支持的属性,那就不能在 CSSOM 这个环节做,我们只能遍历一遍DOM,找到 <style><link rel="stylesheet"> 标签,获取其中的 CSS 样式、解析、重写,最后再加回 DOM 树中。令人尴尬的是,这样DOM树全部刷新了,会导致页面的重新渲染(如下如所示)。

即便如此,有的人可能会说:“除了这种方法,我们也别无选择,更何况对网站的性能也不会造成很大的影响”。那么对于部分网站是这样的。但如果我们的Polyfill是需要对可交互的页面呢?例如scrollresizemousemovekeyup等等,这些事件随时会被触发,那么意味着随时都会导致页面的重新渲染,交互不会像原本那样丝滑,甚至导致页面崩溃,对用户的体验也极其不好。

综上所述,如果我们想让浏览器解析它不认识的样式(低版本浏览器使用grid布局),然而渲染流程我们无法介入,我们也只能通过手动更新DOM的方式,这样会带来很多问题,Houdini的出现正是致力于解决他们。

Houdini API

Houdini是一组底层API,它公开了CSS引擎的各个部分,如下图所示展示了每个环节对应的新API(灰色部分各大浏览器还未实现),从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。Houdini是一群来自Mozilla,Apple,Opera,Microsoft,HP,Intel和Google的工程师组成的工作小组设计而成的。它们使开发者可以直接访问CSS对象模型(CSSOM),使开发人员可以编写浏览器可以解析为CSS的代码,从而创建新的CSS功能,而无需等待它们在浏览器中本地实现。

CSS Houdini-API

Properties & Values API

尽管当前已经有了CSS变量,可以让开发者控制属性值,但是无法约束类型或者更严格的定义,CSS Houdini新的API,我们可以扩展css的变量,我们可以定义CSS变量的类型,初始值,继承。它是css变量更强大灵活。

CSS变量现状:

.dom {
  --my-color: green;
  --my-color: url('not-a-color'); // 它并不知道当前的变量类型
  color: var(--my-color);
}

Houdini提供了两种自定义属性的注册方式,分别是在js和css中。

CSS.registerProperty({
  name: '--my-prop', // String 自定义属性名
  syntax: '<color>', // String 如何去解析当前的属性,即属性类型,默认 *
  inherits: false, // Boolean 如果是true,子节点将会继承
  initialValue: '#c0ffee', // String 属性点初始值
});

我们还可以在css中注册,也可以达到上面的效果

@property --my-prop {
  syntax: '<color>';
  inherits: false;
  initial-value: #c0ffee;
}

这个API中最令人振奋人心的功能是自定义属性上添加动画,像这样:transition: --multiplier 0.4s;,这个功能我们在前面介绍什么是js in css那个demo[8]用使用过。我们还可以使用+使syntax属性支持一个或多个类型,也可以使用|来分割。更多syntax属性值:

属性值描述<length>长度值<number>数字<percentage>百分比<length-percentage>长度或百分比,calc将长度和百分比组成的表达式<color>颜色<image>图像<url>网址<integer>整数<angle>角度<time>时间<resolution>分辨率<transform-list>转换函数<custom-ident>ident

Worklets

Worklets是渲染引擎的扩展,从概念上来讲它类似于Web Workers[9],但有几个重要的区别:

  1. 设计为并行,每个Worklets必须始终有两个或更多的实例,它们中的任何一个都可以在被调用时运行
  2. 作用域较小,限制不能访问全局作用域的API(Worklet的函数除外)
  3. 渲染引擎会在需要的时候调用他们,而不是我们手动调用

Worklet是一个JavaScript模块,通过调用worklet的addModule方法(它是个Promise)来添加。比如registerLayout,registerPaint, registerAnimator 我们都需要放在Worklet中

//加载单个
await demoWorklet.addModule('path/to/script.js');

// 一次性加载多个worklet
Promise.all([
  demoWorklet1.addModule('script1.js'),
  demoWorklet2.addModule('script2.js'),
]).then(results => {});

registerDemoWorklet('name', class {

  // 每个Worklet可以定义要使用的不同函数
  // 他们将由渲染引擎在需要时调用
  process(arg) {
    return !arg;
  }
});

Worklets的生命周期

Worklets lifecycle

  1. Worklet的生命周期从渲染引擎内开始
  2. 对于JavaScript,渲染引擎启动JavaScript主线程
  3. 然后他将启动多个worklet进程,并且可以运行。这些进程理想情况下是独立于主线程的线程,这样就不会阻塞主线程(但它们也不需要阻塞)
  4. 然后在主线程中加载我们浏览器的JavaScript
  5. 该JavaScript调用 worklet.addModule 并异步加载一个worklet
  6. 加载后,将worklet加载到两个或多个可用的worklet流程中
  7. 当需要时,渲染引擎将通过从加载的Worklet中调用适当的处理函数来执行Worklet。该调用可以针对任何并行的Worklet实例。

Typed OM

Typed OM是对现有的CSSOM的扩展,并实现 Parsing APIProperties & Values API相关的特性。它将css值转化为有意义类型的JavaScript的对象,而不是像现在的字符串。如果我们尝试将字符串类型的值转化为有意义的类型并返回可能会有很大的性能开销,因此这个API可以让我们更高效的使用CSS的值。

现在读取CSS值增加了新的基类CSSStyleValue,他有许多的子类可以更加精准的描述css值的类型:

子类描述CSSKeywordValueCSS关键字和其他标识符(如inherit或grid)CSSPositionValue位置信息 (x,y)CSSImageValue表示图像的值属性的对象CSSUnitValue表示为具有单个单位的单个值(例如50px),也可以表示为没有单位的单个值或百分比CSSMathValue比较复杂的数值,比如有calc,min和max。这包括子类 CSSMathSum, CSSMathProduct, CSSMathMin,CSSMathMax, CSSMathNegate 和 CSSMathInvertCSSTransformValue由CSS transforms组成的CSSTransformComponent列表,其中包括CSSTranslate, CSSRotate, CSSScale, CSSSkew, CSSSkewX, CSSSkewY, CSSPerspective 和 CSSMatrixComponent

使用Typed OM主要有两种方法:

  1. 通过attributeStyleMap设置和获取有类型的行间样式
  2. 通过computedStyleMap获取元素完整的Typed OM样式

使用attributeStyleMap设置并获取

myElement.attributeStyleMap.set('font-size', CSS.em(2));
myElement.attributeStyleMap.get('font-size'); // CSSUnitValue { value: 2, unit: 'em' }

myElement.attributeStyleMap.set('opacity', CSS.number(.5));
myElement.attributeStyleMap.get('opacity'); // CSSUnitValue { value: 0.5, unit: 'number' };

在线demo[10]

使用computedStyleMap

.foo {
  transform: translateX(1em) rotate(50deg) skewX(10deg);
  vertical-align: baseline;
  width: calc(100% - 3em);
}
const cs = document.querySelector('.foo').computedStyleMap();

cs.get('vertical-align');
// CSSKeywordValue {
//  value: 'baseline',
// }

cs.get('width');
// CSSMathSum {
//   operator: 'sum',
//   length: 2,
//   values: CSSNumericArray {
//     0: CSSUnitValue { value: -90, unit: 'px' },
//     1: CSSUnitValue { value: 100, unit: 'percent' },
//   },
// }

cs.get('transform');
// CSSTransformValue {
//   is2d: true,
//   length: 3,
//   0: CSSTranslate {
//     is2d: true,
//     x: CSSUnitValue { value: 20, unit: 'px' },
//     y: CSSUnitValue { value: 0, unit: 'px' },
//     z: CSSUnitValue { value: 0, unit: 'px' },
//   },
//   1: CSSRotate {...},
//   2: CSSSkewX {...},
// }

Layout API

开发者可以通过这个API实现自己的布局算法,我们可以像原生css一样使用我们自定义的布局(像display:flex, display:table)。在Masonry layout library[11] 上我们可以看到开发者们是有多想实现各种各样的复杂布局,其中一些布局光靠 CSS 是不行的。虽然这些布局会让人耳目一新印象深刻,但是它们的页面性能往往都很差,在一些低端设备上性能问题犹为明显。

CSS Layout API 暴露了一个registerLayout方法给开发者,接收一个布局名(layout name)作为后面在 CSS中使用的属性值,还有一个包含有这个布局逻辑的JavaScript类。

my-div {
  display: layout(my-layout);
}
// layout-worklet.js
registerLayout('my-layout', class {
  static get inputProperties() { return ['--foo']; }
  
  static get childrenInputProperties() { return ['--bar']; }
  
  async intrinsicSizes(children, edges, styleMap) {}

  async layout(children, edges, constraints, styleMap) {}
});
await CSS.layoutWorklet.addModule('layout-worklet.js');

目前浏览器大部分还不支持

Painting API

我们可以在CSS background-image中使用它,我们可以使用Canvas 2d上下文,根据元素的大小控制图像,还可以使用自定义属性。

await CSS.paintWorklet.addModule('paint-worklet.js');
registerPaint('sample-paint', class {
  static get inputProperties() { return ['--foo']; }

  static get inputArguments() { return ['<color>']; }

  static get contextOptions() { return {alpha: true}; }

  paint(ctx, size, props, args) { }
});

Animation API

这个API让我们可以控制基于用户输入的关键帧动画,并且以非阻塞的方式。还能更改一个 DOM 元素的属性,不过是不会引起渲染引擎重新计算布局或者样式的属性,比如 transform、opacity 或者滚动条位置(scroll offset)。Animation API的使用方式与 Paint APILayout API略有不同我们还需要通过new一个WorkletAnimation来注册worklet。

// animation-worklet.js
registerAnimator('sample-animator', class {
  constructor(options) {
  }
  animate(currentTime, effect) {
    effect.localTime = currentTime;
  }
});
await CSS.animationWorklet.addModule('animation-worklet.js');

// 需要添加动画的元素
const elem = document.querySelector('#my-elem');
const scrollSource = document.scrollingElement;
const timeRange = 1000;
const scrollTimeline = new ScrollTimeline({
  scrollSource,
  timeRange,
});

const effectKeyframes = new KeyframeEffect(
  elem,
  // 动画需要绑定的关键帧
  [
    {transform: 'scale(1)'},
    {transform: 'scale(.25)'},
    {transform: 'scale(1)'}
  ],
  {
    duration: timeRange,
  },
);
new WorkletAnimation(
  'sample-animator',
  effectKeyframes,
  scrollTimeline,
  {},
).play();

关于此API的更多内容:(https://github.com/w3c/css-houdini-drafts/tree/main/css-animation-worklet-1)

Parser API

允许开发者自由扩展 CSS 词法分析器。

解析规则:

const background = window.cssParse.rule("background: green");
console.log(background.styleMap.get("background").value) // "green"

const styles = window.cssParse.ruleSet(".foo { background: green; margin: 5px; }");
console.log(styles.length) // 5
console.log(styles[0].styleMap.get("margin-top").value) // 5
console.log(styles[0].styleMap.get("margin-top").type) // "px"

解析CSS:

const style = fetch("style.css")
        .then(response => CSS.parseStylesheet(response.body));
style.then(console.log);

Font Metrics API

它将提供一些方法来测量在屏幕上呈现的文本元素的尺寸,将允许开发者控制文本元素在屏幕上呈现的方式。使用当前功能很难或无法测量这些值,因此该API将使开发者可以更轻松地创建与文本和字体相关的CSS特性。例如:

  • flex布局: align-items baseline特性。需要知道每一个flex盒子中第一个元素的基线位置。
  • 首字母: 需要知道每个字母的基线高度和字母最大的高度,以及换行内容的基线长度。
  • 单个字形的前进和后退。
  • 换行: 需要访问字体数据,文本的所有样式输入以及布局信息(可用的段落长度等)。
  • 元素中的每一个line boxes都需要一个基线。(line boxes代表包含众多inline boxes的这行)

Houdini 目前进展

Is Houdini ready yet

(https://ishoudinireadyyet.com/)

Houdini 的蓝图

了解到这里,部分开发者可能会说:“我不需要这些花里胡哨的技术,并不能带收益。我只想简简单单的写几个页面,做做普通的Web App,并不想试图干预浏览器的渲染过程从而实现一些实验性或炫酷的功能。”如果这样想的话,我们不妨退一步再去思考。回忆下最近做过的项目,用于实现页面效果所使用到的技术,grid布局方式在考虑兼容老版本浏览器时也不得不放弃。我们想控制浏览器渲染页面的过程并不是仅仅为了炫技,更多的是为了帮助开发者们解决以下两个问题:

  1. 统一各大浏览器的行为
  2. JavaScript一样,在推出新的特性时,我们可以通过Polyfill的形式快速的投入生产环境中。

几年过后再回眸,当主流浏览器完全支持Houdini的时候。我们可以在浏览器上随心所欲的使用任何CSS属性,并且他们都能完美支持。像今天的grid布局在旧版本浏览器支持的并不友好的这类问题,那时我们只需要安装对应的Polyfill就能解决类似的问题。