想要开发组件库?那你一定要提前了解一下这个神器
转自:前端学不动
相信很多人都有过制作组件库的经历,也有一部分人做的组件库需要适配各种不同的框架,或者相同框架的不同版本(如:
Vue2
、
Vue3
等)
此时很多人就会选择
Web Components
来作为组件库的技术栈。
国内用
Web Components
做组件库最知名的案例应该就是凹凸实验室的
Taro UI
了吧?凹凸团队曾说纯原生语法构建 Web Components
过于繁琐,希望能用声明式语法来构建 Web Components
,所以选择了 Stencil
这个框架/库/编译器(我也不知道叫啥好,就先叫框架吧。大家能明白意思就行,不必过于钻牛角尖非要论证这个到底是个框架还是库还是编译器)
但
Stencil
生成出来的组件有虚拟
DOM
及简易版
Diff
算法,编译出来的组件还是有点重。比方说我在 React
里用 Stencil
编译出来的组件库,React
本身就已经有一套完善的虚拟 DOM
和 Diff
算法了,然而引入的组件库却包含了另一套虚拟 DOM
和 Diff
算法,这就很是浪费。
而且
Web Components
也是有兼容性要求的:
有些人公司的技术栈是
Vue 2 + 3
,因为可能有些项目还是需要兼容低版本浏览器的,所以不得不用
Vue2
,新项目可以用
Vue3
,那么此时用
Web Components
就有点不太合适了。
当然有人可能会说不是有
Polyfill
么?首先
Polyfill
体积太大了,我们仅仅就是想用个组件库而已,不至于再引入一个
Polyfill
。其次
Safari
对
Web Components
的支持有一点瑕疵,
从上图可以看到
Safari
虽然也是绿色,但是不是那种绿意盎然的绿,而是一种脏绿色。
这种脏绿代表部分支持或者虽然支持但是有 bug
,Safari
属于前者,当然这个其实也有 Polyfill
,张鑫旭从 GitHub
上 Fork
了一份然后进行了改进。
不过还是太麻烦了点,很多人用 Web Components
的目的仅仅只是为了能一套代码适配多个框架而已。那么有没有这样一种方案:同样也是只写一套代码,通过编译的手段来达成相同的目的。
Angular
之父:那必须有啊!我这方案可比Web Components
好多了
Mitosis 有丝分裂
这个叫
Mitosis
的项目出自
Angular
之父之手,意思是有丝分裂:
这个名字起的也非常符合它的职责,一个变俩、俩变四、四变八、八变十六… 我们只需要编写一套
DNA
(代码)就能为我们分裂出一大堆细胞(组件):
我们再来看看它
GitHub
上的标语
Web Components
:Write components once, run everywhere 这不是我的口号么?你特么抢我台词啊!
Mitosis
:我特么不仅要抢你台词,我还要把你的工作都给抢过来!
从口号中也可以看出,它与
Web Components
的作用高度重合,但一个是运行时方案,一个是编译方案。这两个方案各有利弊吧,随着最近重编译的框架迅速崛起,很多人都觉得这是一个更先进的方案。之前在某问答平台上看过这样一条回答(关于
Svelte
的):
当然
Svelte
是编译成原生
DOM
操作,既然能编译成原生语法那为什么不能编译成框架语法呢?
Angular
之父就是这么想的,我们一起来看一下
Mitosis
的语法。
简 介 简|单|介|绍
我们点击进入 Mitosis
的官网 mitosis.builder.io,映入眼帘的是这样的一幅界面:
左边就是我们写的代码(他们写的 咱们啥也没写呢还),一股浓烈的
React
味道扑鼻而来。右侧就是编译后的代码,默认是 Vue2
,你看 Angular
他爸多体贴,不仅给咱们提供了 TS
、JS
选项,甚至还能选择编译成 Options API
还是 Composition API
:
从这一个简单的小案例就能侧面反映出如今
Vue
的地位,身处
C
位啊家人们!那么多框架凭什么就
Vue
排第一?我知道有人会说这肯定不是按照流行程度或者使用人数来排的名,因为如果按照这个标准来排名的话肯定是
React
排第一。
这么说确实没毛病,但我觉得
React
之所以没有排在
Vue
前面的原因是因为左侧编辑器代码写的本来就是浓浓的
React
风,如果右侧编译区默认还是
React
的话那就是从
React
编译为
React
:
这有什么意思啊?就是多引入了一个
Styled JSX
呗,这怎么能体现出
Mitosis
的技术含量呢?所以才会把
React
往后排一位,咱就说能编译的框架有这么老多,在排除
React
之后为啥就
Vue
能排在最前面:
我知道肯定又会有人说:你光在那凸显右侧编译区
Vue
的地位,怎么不说说左侧编辑区没有
Vue
呢?人家可不光是只支持
JSX
,甚至还支持
Svelte
:
如果
Vue
真像你说的那么地位显赫,那为什么只有
MITOSIS JSX
、
SVELTOSIS
?怎么没有
VUETOSIS
?也就是说为什么只能把
React
编译成
Vue
却不能反过来把
Vue
编译成
React
呢?
首先我觉得
Vue
的单文件组件语法糖有点过多了,写法也越来越多:
<template>
<div/>
<template>
<script lang="ts" setup generic="T extends { name: string }">
...
</script>
看上去就没有
Svelte
简洁,概念太多,需要你对
Vue
有一定的了解,而且还有很多莫名其妙的全局变量语法糖:
<template>
<div/>
<template>
<script lang="ts" setup generic="T extends { name: string }">
defineOptions({
name: 'Foo'
})
defineExpose({ a, b })
const props = defineProps<{
a: string,
b?: number
}>()
const emit = defineEmits<{
(evt: 'update:modelValue', value: number): void
}>()
const slots = defineSlots<{
default(props: { foo: string; bar: number }): any
}>()
</script>
反正就是越来越复杂,而且
Vue
还在不停的更新迭代中,每次大更新就又往里塞语法糖,鬼知道
Vue3.4
、
3.5
、
3.6
、甚至是
4.0
会变成什么鬼样子!随之而来的争议也越来越大:
而
Svelte
则要简洁的多,语法糖就一个
$:
,触发更新也好记,只有等号
=
才会触发更新,对使用者而言不需要多么了解
Svelte
,只需要记住几个简单的规则就可以迅速上手。
而
JSX
也是同理,
JSX
也不需要你多么了解
React
,大部分都是
JS
,你只需要了解一下在
JS
里面写
XML
的规则就行,所以这才是左侧编辑区仅支持
JSX
与
Svelte
的原因之一。
当然个人认为这同样也是
Svelte
比
Vue
更受(国外)新手欢迎的原因之一,
Svelte
在国外已经抢走了原本属于
Vue2
的一部分市场份额。
之前
Vue
正是凭借简洁易用等优势在各大培训机构里获得了老师们的青睐,
老师讲啥底下的学生就学啥,一批批 “
Vue
” 开发者大批量的涌入市场,进一步提升了
Vue
的市场份额。但目前明显是
Svelte
更简单,只不过在中国没人会招只会
Svelte
的前端,所以为了就业,培训班们估计也不会教。
但如果
Vue
的复杂程度超过了某个临界点达到了培训班里大多数学生都学不会的那种程度或是学习周期过长的话,就很有可能会给
Svelte
、
Solid
这类新秀一定的可乘之机。
当然也有可能只教
Vue2
,
毕竟
Vue2
也是
Vue
,而且好学的多。学会了
Vue2
差不多也就“毕业”了,开始找工作了,到时候再让他们在工作中摸索
Vue3
吧。
言归正传,我们先从
Mitosis
的
README
看起:
翻译:我们正在积极寻找有兴趣成为
Mitosis
贡献者的人。鉴于它的编译的范围很广,任何工程师都有很大的空间对项目产生巨大而持续的影响。我们投入大量时间帮助新手入职,并教他们如何更改代码库(参见以下示例:#847、#372、#734)。如果有兴趣,请查看我们的优秀首期列表或联系我们的Discord
确实他们步子迈的很大,编译的可不仅仅只是那几个常见框架而已。现在正是缺人的时候,想在自己简历上增添亮点的小伙伴们可得抓紧了!我们来看一下它到底能编译成多少种技术:
-
VUE
-
REACT
-
QWIK
-
ANGULAR
-
SVELTE
-
REACT NATIVE
-
RSC
-
SWIFT
-
SOLID
-
STENCIL
-
MARKO
-
PREACT
-
LIT
-
ALPINE.JS
-
WEB COMPONENTS
-
HTML
-
LIQUID
-
TEMPLATE
-
MITOSIS
-
JSON
-
BUILDER
真不是我吹
Vue
的地位,如果刨除掉
React
这一特殊因素的话,
Vue
甚至都排在私心之前。什么是私心呢?给自己造出来的东西一个比较靠前的排序就叫私心,
Angular
之父对吧?
当然 Angular
本身就是三大框架之一,排序靠前也是应该的。Qwik
这个就能看出私心来了,虽然是个很有潜力的超新星,但总体实力是比不上 Angular
的。Qwik
同样也是 Angular
之父的最新力作(Angular
之父牛逼),所以排在了 Angular
的前面。
我们发现
Mitosis
居然还可以编译为
JSON
,编译成其他框架其实也是根据生成的
JSON
数据来进行编译的。
Mitosis
还支持插件功能,你可以根据
JSON
来生成任何你想要的东西。
这对于最近几年很火的低代码平台是个福音,Mitosis
所在的 Builder
就是做低代码的,Mitosis
正是他们在做低代码时所创造的,目前也正在为他们的低代码产品提供支持(所以不用担心会出现赚不到钱不维护了的情况)
Mitosis
采用的是静态
JSX
来限制灵活性,不然的话根本没法编译:
-
高情商:静态
JSX
-
低情商:阉割版
JSX
阉割版
静态版
JSX
灵感源自于
Solid.js
,一个正常的组件将会被编译为如下
JSON
:
除了编写组件库之外,
Mitosis
同样也可以用于编写
SDK
。他们的新一代
SDK
就是用
Mitosis
写的,并且还因为交付速度很快而受到了赞扬:
心动不如马上行动,接下来我们就按照官方文档来创建一个
Mitosis
项目。
官网里没有类似于
create-react-app
或
create-vue
这样的工具,而是稍显繁琐,既然不好用那就是机会!大家可以立刻去给
create-vite
提
PR
,让它支持
Mitosis
,相信我,这玩意日后一定会火🔥
它 README
写的第一步就是:
这真的很不人性化,因为这一看就是在一个已有项目里的操作,我们还得
npm init
一个项目:
然后
npm i @builder.io/mitosis-cli @builder.io/mitosis
,紧接着再在根目录下创建一个
mitosis.config.js
:
在
mitosis.config.js
里面写上:
/** @type {import('@builder.io/mitosis').MitosisConfig} */
module.exports = {
files: 'src/**',
targets: ['vue3', 'solid', 'svelte', 'react'],
};
然后再来安装一下
TS
:
npm i -D typescript
,随即在根目录下创建一个
tsconfig.json
:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"jsx": "preserve",
"jsxImportSource": "@builder.io/mitosis"
}
}
接下来咱们再来安装一个
ESLint
插件,由于
Mitosis
的
JSX
是
被阉割过
被静态化过的
JSX
,所以必须要用
ESLint
来限制你的灵活性。
我们来看一下它都有哪些规则,首先就是
css-no-vars
来限制你在
css
属性里写表达式,哦对了,在
React
里写的
style
属性到了这里变成
css
属性了:
<button css={{ color: a ? 'red' : 'green' }} />
这段代码在 React
里没问题,但请记住,Mitosis
是一个编译型框架,React
之所以能那么灵活就是因为它更依仗运行时,我们必须写的比较静态一点才能被编译出来一个具体值:
<button css={{ color: 'red' }} />
说实话其实还是蛮不爽的,
style
的值是动态的这是一个很常见的需求,但在这里却只能写静态值,这也是作为一个编译型框架所不得不承受的缺点。再接下来的规则是
jsx-callback-arg-name
,这个规则可能会让人比较费解:
<button onClick={e => console.log(e)} />
这段代码有毛病么?在
Mitosis
里还真就有毛病!这个参数不能写成
e
,要写成别的(如
event
):
<button onClick={event => console.log(event)} />
写成
e
会影响编译么?我不知道,我只想问一句
Why
:
接下来这个规则还算好接受一点,叫 jsx-callback-arrow-function
,顾名思义,跟 jsx
的回调函数以及箭头函数有关,只接受箭头函数来作为回调函数,其它值都不行:
// 这些都是反例:
<button onClick={ function(event) {} }/>
<button onClick={ null }/>
<button onClick={ "string" }/>
<button onClick={ 1 }/>
<button onClick={ true }/>
<button onClick={ {} }/>
<button onClick={ [] }/>
<button onBlur={ [] }/>
<button onChange={ [] }/>
必须箭头函数:
<button onClick={ event => doSomething(event) }/>
<button onClick={ event => doSomething() }/>
<button onClick={ event => {} }/>
<button onClick={ () => doSomething() }/>
再然后的条规则叫
no-assign-props-to-state
,一看名字就明白了,不能把
props
分配给
state
:
import { useStore } from '@builder.io/mitosis';
export default function MyComponent(props) {
const state = useStore({ text: props.text });
}
在 Mitosis
中用 useStore
来表示响应式值,你可以把它简单的理解为 Vue
的 reactive
:
import { reactive } from 'vue';
const props = defineProps(...)
const state = reactive({ text: props.text })
下一条规则是不能在 state
里写异步函数(no-async-methods-on-state
):
// 反例:
export default function MyComponent() {
const state = useStore({
async doSomethingAsync(event) {
return;
},
});
}
那我们需要异步操作的话要怎么办?可以用一个立即执行函数来包裹一下:
export default function MyComponent() {
const state = useStore({
doSomethingAsync(event) {
void (async function () {
const response = await fetch();
})();
},
});
}
接下来这条规则会严重限制我们写
jsx
的灵活性,不能在
return
出去的
jsx
里写条件判断(
no-conditional-logic-in-component-render
)我们之前可能会经常写出这样的代码:
// 反例:
export default function MyComponent(props) {
if (x > 10) return <a />;
else if (x < 5) return <b />;
else return <c />
}
在
Mitosis
里可不行,你就只能有一个
return
:
// 反例:
export default function MyComponent(props) {
// 这个 return 必须是静态的:
return <a />
}
接下来是
state
不能解构(
no-state-destructuring
)跟
Vue
一样,因为最终也会编译成
Vue
嘛:
export default function MyComponent() {
const state = useStore({ foo: '1' });
// 不能解构:
const { foo } = state;
}
下面这个规则就比较令人费解了,不能在
JSX
里声明变量(
no-var-declaration-in-jsx
):
export default function MyComponent(props) {
return (
<div>
{a.map((x) => {
// 不能在这声明变量:
const foo = 'bar';
return <span>{x}</span>;
})}
</div>
);
}
是不是有点扯蛋?但也不是说完全不能声明变量,比方说解构声明就可以:
export default function MyComponent(props) {
return (
<div
someProp={a.find((b) => {
// 解构声明的话可以:
const { x } = b;
return x < 1;
})}
/>
);
}
接下来这条规则也挺操蛋的,在组件里不能把一个变量赋值给另外一个变量:
// 以下全部都是反例:
export default function MyComponent(props) {
const a = b;
return <div />;
}
let a;
export default function MyComponent(props) {
a = b;
return <div />;
}
export default function MyComponent(props) {
let a;
a = b;
return <div />;
}
下面这条规则对词汇量不高的人来说很不友好,不能声明一个与
state
属性同名的变量(
no-var-name-same-as-state-property
):
import { useStore } from '@builder.io/mitosis';
export default function MyComponent(props) {
const state = useStore({
a: 1,
});
// 这个 a 和 stata.a 里的属性重名了:
const a = 1;
return <div />;
}
export function MyComponent(props) {
const state = useStore({
a: 1,
});
function myFunction() {
// 哪怕说你声明在其他作用域里也不行:
const a = 1;
}
return <div />;
}
接下来这条规则是想把
JSX
给变成一个单文件组件,因为除了可以导出类型之外就只能用
export default
默认导出了(
only-default-function-and-imports
):
//
export default function MyComponent(props) {
return <div />;
}
// 这么写不行 只能 export default
export const x = y;
// 当然如果你导出的是一个 TS 类型的的话还是可以的:
export type a = 1;
接下来就是和
Solid.js
差不多的规则了,不要用三元表达式来进行
JSX
的条件判断,取而代之的是
<Show>
组件(
prefer-show-over-ternary-operator
):
export default function MyComponent(props) {
// 这么写可不行:
return <div>{foo ? <bar /> : <baz />}</div>;
// 要写成这样:
return (
<div>
<Show when={foo}>
<bar />
</Show>
<Show when={!foo}>
<baz />
</Show>
</div>
);
}
下面的规则是为了纠正你从
React
那里带过来的习惯,
ref
不需要
.current
属性(
ref-no-current
):
import { useRef } from '@builder.io/mitosis';
export default function MyComponent(props) {
const inputRef = useRef();
const myFn = () => {
// 不需要 .current
inputRef.current.focus();
};
return <div />;
}
最后一条规则是
useStore
的返回值必须叫
state
(
use-state-var-declarator
)叫别的不行:
export default function MyComponent(props) {
// useStore() 的返回值起名不是 state:
const a = useStore();
// 必须写成这样:
const state = useStore();
return <div />;
}
还有就是
useState
的返回值必须解构,也就是我们常用的一种写法:
export default function MyComponent(props) {
const [name, setName] = useState();
return <div />;
}
写成这样是万万不行的:
export default function MyComponent(props) {
const a = useState();
return <div />;
}
了解完这几条规则之后我们就来安装
ESLint
插件吧:
npm i -D eslint @builder.io/eslint-plugin-mitosis
安装完成后再运行一下
npx eslint --init
,接下来就是根据提示选择自己想要的
eslint
规范:
运行完成后所生成的 .eslintrc.js
长这样:
我们给它改成这样:
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'xo',
'plugin:@builder.io/mitosis/recommended',
],
overrides: [{
extends: ['xo-typescript'],
files: ['*.ts', '*.tsx'],
}],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {jsx: true},
},
rules: {'@builder.io/mitosis/css-no-vars': 'error'},
plugins: ['@builder.io/mitosis'],
};
接下来再来新建一个 src
文件夹,文件夹里新建一个 Component.lite.tsx
,记住中间一定要有 .lite
,如果你写成 Component.tsx
那是万万不行的。
import {useState} from '@builder.io/mitosis';
export default function MyComponent() {
const [name, setName] = useState('Alex');
return (
<div>
<input
css={{
color: 'red',
}}
value={name}
onChange={event => {
setName(event.target.value);
}}
/>
Hello! I can run in React, Vue, Solid, or Liquid!
</div>
);
}
想要生成组件的话我们就要运行一下:
npm exec mitosis build
,接下来我们就可以看到命令行里:
定睛一看,根目录下多了个 output
文件夹:
我们挨个来看下:
// React:
import * as React from "react";
import { useState } from "react";
function MyComponent(props) {
const [name, setName] = useState(() => "Alex");
return (
<>
<div>
<input
className="input"
value={name}
onChange={(event) => {
setName(event.target.value);
}}
/>
Hello! I can run in React, Vue, Solid, or Liquid!
</div>
<style jsx>{`
.input {
color: red;
}
`}</style>
</>
);
}
export default MyComponent;
// Solid:
import { createSignal } from "solid-js";
import { css } from "solid-styled-components";
function MyComponent(props) {
const [name, setName] = createSignal("Alex");
return (
<div>
<input
class={css({
color: "red",
})}
value={name()}
onInput={(event) => {
setName(event.target.value);
}}
/>
Hello! I can run in React, Vue, Solid, or Liquid!
</div>
);
}
export default MyComponent;
<!-- Svelte -->
<script>
let name = "Alex";
</script>
<div>
<input class="input" bind:value={name} />
Hello! I can run in React, Vue, Solid, or Liquid!
</div>
<style>
.input {
color: red;
}
</style>
<!-- Vue3 -->
<template>
<div>
<input class="input" :value="name" @input="name = $event.target.value" />
Hello! I can run in React, Vue, Solid, or Liquid!
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "my-component",
data() {
return { name: "Alex" };
},
});
</script>
<style scoped>
.input {
color: red;
}
</style>
是不是还挺智能的?不过有一点我很好奇,就是 React
的函数组件是每次更新都会执行一次,也就是说如果我们写成这样:
那么每次组件更新时都会 console.log
一遍,换算成 Vue
的话就相当于:
// 伪代码
export default {
updated () {
console.log(this.name);
}
};
那么会被编译成这样吗?我们再来运行一遍 npm exec mitosis build
试试。神奇的是,这次编译把这个 console.log
直接编译没了,无论哪个框架的组件都没有这段代码,这也可以理解,为了让所有框架行为一致,这些东西必须写在生命周期里,那我们就:
import {useState, onMount} from '@builder.io/mitosis';
export default function MyComponent() {
const [name, setName] = useState('Alex');
onMount(() => {
console.log(name);
});
return (
<div>
<input
css={{
color: 'red',
}}
value={name}
onChange={event => {
setName(event.target.value);
}}
/>
Hello! I can run in React, Vue, Solid, or Liquid!
</div>
);
}
最终编译(只截取生命周期部分):
// React:
useEffect(() => {
console.log(name);
}, []);
// Solid:
onMount(() => {
console.log(name);
});
// Svelte:
onMount(() => {
console.log(name);
});
// Vue
mounted() {
console.log(name);
}
Vue
和 Solid
这里就有点毛病了,因为按理来说应该是这样才对:
-
Vue::
console.log(this.name)
-
Solid:
console.log(name())
但它们全部都被编译成了 console.log(name)
,实话实说,我觉得还是有点坑,哪怕说 Vue
这边没有这个 bug
,但我在组件里除了声明变量以外什么都不能写,想写逻辑的话就只能写在生命周期内,不然就全给你删了。
缺陷就是语法限制的有点过多了,毕竟想要写一套代码编译成差异巨大的各个框架组件的话,还是很难做到的。
不过随着这个项目的持续迭代,说不定这些问题都会有对应的解决方案,不过目前来看不太推荐大家拿它来写组件库,文档写的也语焉不详,有点坑…
- EOF -
加主页君微信,不仅前端技能+1
主页君日常还会在个人微信分享前端开发学习资源和技术文章精选,不定期分享一些有意思的活动、岗位内推以及如何用技术做业余项目
加个微信,打开一扇窗
3、看了 9 个开源的 Vue3 组件库,发现了这些前端的流行趋势
觉得本文对你有帮助?请分享给更多人
推荐关注「前端大全」,提升前端技能
点赞和在看就是最大的支持 ❤️