想要开发组件库?那你一定要提前了解一下这个神器

尼伯特

共 27824字,需浏览 56分钟

 · 2023-07-19



转自:前端学不动


相信很多人都有过制作组件库的经历,也有一部分人做的组件库需要适配各种不同的框架,或者相同框架的不同版本(如: Vue2 Vue3 等) 此时很多人就会选择  Web Components  来作为组件库的技术栈。


国内用  Web Components  做组件库最知名的案例应该就是凹凸实验室的  Taro UI  了吧?凹凸团队曾说纯原生语法构建 Web Components 过于繁琐,希望能用声明式语法来构建 Web Components,所以选择了 Stencil 这个框架/库/编译器(我也不知道叫啥好,就先叫框架吧。大家能明白意思就行,不必过于钻牛角尖非要论证这个到底是个框架还是库还是编译器)

Stencil

但  Stencil  生成出来的组件有虚拟  DOM  及简易版  Diff  算法,编译出来的组件还是有点重。比方说我在 React 里用 Stencil 编译出来的组件库,React 本身就已经有一套完善的虚拟 DOM 和 Diff 算法了,然而引入的组件库却包含了另一套虚拟 DOM 和 Diff 算法,这就很是浪费。


而且  Web Components  也是有兼容性要求的:


b7eadc8f3a543e423a1e29b501ee75eb.webp


有些人公司的技术栈是  Vue 2 + 3 ,因为可能有些项目还是需要兼容低版本浏览器的,所以不得不用  Vue2 ,新项目可以用  Vue3 ,那么此时用  Web Components  就有点不太合适了。


Polyfill

当然有人可能会说不是有  Polyfill  么?首先  Polyfill  体积太大了,我们仅仅就是想用个组件库而已,不至于再引入一个  Polyfill 。其次  Safari  对  Web Components  的支持有一点瑕疵, 从上图可以看到  Safari  虽然也是绿色,但是不是那种绿意盎然的绿,而是一种脏绿色。


这种脏绿代表部分支持或者虽然支持但是有 bugSafari 属于前者,当然这个其实也有 Polyfill,张鑫旭从 GitHub 上 Fork 了一份然后进行了改进。

不过还是太麻烦了点,很多人用 Web Components 的目的仅仅只是为了能一套代码适配多个框架而已。那么有没有这样一种方案:同样也是只写一套代码,通过编译的手段来达成相同的目的。

Angular  之父:那必须有啊!我这方案可比  Web Components  好多了



Mitosis 有丝分裂


这个叫  Mitosis  的项目出自  Angular  之父之手,意思是有丝分裂:

124534b1c38bc61a144b67a6ea4b575f.webp


这个名字起的也非常符合它的职责,一个变俩、俩变四、四变八、八变十六… 我们只需要编写一套 DNA (代码)就能为我们分裂出一大堆细胞(组件):


9fc0a1916d7ffacfe96104d6292c2e66.webp


我们再来看看它  GitHub  上的标语


953400e8e97f495e562b2fb2df023330.webp

Web Components:Write components once, run everywhere 这不是我的口号么?你特么抢我台词啊!

Mitosis:我特么不仅要抢你台词,我还要把你的工作都给抢过来!

91efec919c8da7ba3fbdebf8a7c54512.webp


从口号中也可以看出,它与  Web Components  的作用高度重合,但一个是运行时方案,一个是编译方案。这两个方案各有利弊吧,随着最近重编译的框架迅速崛起,很多人都觉得这是一个更先进的方案。之前在某问答平台上看过这样一条回答(关于 Svelte 的):


60ec61557d3912380558c38d4b5952aa.webp


当然  Svelte  是编译成原生  DOM  操作,既然能编译成原生语法那为什么不能编译成框架语法呢? Angular  之父就是这么想的,我们一起来看一下  Mitosis  的语法。



简||介|绍


我们点击进入 Mitosis 的官网 mitosis.builder.io,映入眼帘的是这样的一幅界面:


accf861960aeb8594277f30cda12989c.webp


左边就是我们写的代码(他们写的 咱们啥也没写呢还),一股浓烈的  React  味道扑鼻而来。右侧就是编译后的代码,默认是 Vue2,你看 Angular 他爸多体贴,不仅给咱们提供了 TSJS 选项,甚至还能选择编译成 Options API 还是 Composition API


afa3ee9f6676432fcd42787348c91b0d.webp


Vue 的地位

从这一个简单的小案例就能侧面反映出如今  Vue  的地位,身处 C 位啊家人们!那么多框架凭什么就  Vue  排第一?我知道有人会说这肯定不是按照流行程度或者使用人数来排的名,因为如果按照这个标准来排名的话肯定是  React  排第一。


这么说确实没毛病,但我觉得  React  之所以没有排在  Vue  前面的原因是因为左侧编辑器代码写的本来就是浓浓的  React  风,如果右侧编译区默认还是  React  的话那就是从  React  编译为  React


7ed7fb4e60663dfd22a76003ad89cd4a.webp


这有什么意思啊?就是多引入了一个  Styled JSX  呗,这怎么能体现出  Mitosis  的技术含量呢?所以才会把  React  往后排一位,咱就说能编译的框架有这么老多,在排除  React  之后为啥就  Vue  能排在最前面:


170c601325e49afe7b22f74c43f00f88.webp


我知道肯定又会有人说:你光在那凸显右侧编译区  Vue  的地位,怎么不说说左侧编辑区没有  Vue  呢?人家可不光是只支持  JSX ,甚至还支持  Svelte


63171a572612d5c03c4ddf90408645c9.webp


如果  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  会变成什么鬼样子!随之而来的争议也越来越大:


25a0de3b0b16130d5a02b87d052dd5af.webp


而  Svelte  则要简洁的多,语法糖就一个  $: ,触发更新也好记,只有等号  =  才会触发更新,对使用者而言不需要多么了解  Svelte ,只需要记住几个简单的规则就可以迅速上手。


JSX 同理

而  JSX  也是同理, JSX  也不需要你多么了解  React ,大部分都是  JS ,你只需要了解一下在  JS  里面写  XML  的规则就行,所以这才是左侧编辑区仅支持  JSX  与  Svelte  的原因之一。


当然个人认为这同样也是  Svelte  比  Vue  更受(国外)新手欢迎的原因之一, Svelte  在国外已经抢走了原本属于  Vue2  的一部分市场份额。

培训班

之前  Vue  正是凭借简洁易用等优势在各大培训机构里获得了老师们的青睐, 老师讲啥底下的学生就学啥,一批批 “ Vue ” 开发者大批量的涌入市场,进一步提升了  Vue  的市场份额。但目前明显是  Svelte  更简单,只不过在中国没人会招只会  Svelte  的前端,所以为了就业,培训班们估计也不会教。


但如果  Vue  的复杂程度超过了某个临界点达到了培训班里大多数学生都学不会的那种程度或是学习周期过长的话,就很有可能会给  Svelte  、 Solid  这类新秀一定的可乘之机。

Vue2 也是 Vue

当然也有可能只教  Vue2      毕竟  Vue2  也是  Vue ,而且好学的多。学会了  Vue2  差不多也就“毕业”了,开始找工作了,到时候再让他们在工作中摸索  Vue3  吧。


言归正传,我们先从  Mitosis  的  README  看起:


6c2f7cb208cfa2a84177b239cbe89d5a.webp

翻译:我们正在积极寻找有兴趣成为 Mitosis 贡献者的人。鉴于它的编译的范围很广,任何工程师都有很大的空间对项目产生巨大而持续的影响。我们投入大量时间帮助新手入职,并教他们如何更改代码库(参见以下示例:#847、#372、#734)。如果有兴趣,请查看我们的优秀首期列表或联系我们的Discord

确实他们步子迈的很大,编译的可不仅仅只是那几个常见框架而已。现在正是缺人的时候,想在自己简历上增添亮点的小伙伴们可得抓紧了!我们来看一下它到底能编译成多少种技术:


69c981e486845a416aa5cd160554091f.webp


  • 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


721bbbc7213268e6e923f293fc46d0a0.webp


阉割版 静态版  JSX  灵感源自于  Solid.js ,一个正常的组件将会被编译为如下  JSON


2f7548c0873fc629372e26210bfa093d.webp


除了编写组件库之外, Mitosis  同样也可以用于编写  SDK 。他们的新一代  SDK  就是用  Mitosis  写的,并且还因为交付速度很快而受到了赞扬:


46d31d6948795dfdd5b8202fde7ebfc4.webp


心动不如马上行动,接下来我们就按照官方文档来创建一个  Mitosis  项目。



4dfe522938b41c30535cbe5954bfeef3.webp创建项目

官网里没有类似于  create-react-app  或  create-vue  这样的工具,而是稍显繁琐,既然不好用那就是机会!大家可以立刻去给  create-vite  提  PR ,让它支持  Mitosis ,相信我,这玩意日后一定会火🔥


它 README 写的第一步就是:


e78e9f22eeb71d083b1be7802c62ee2d.webp


这真的很不人性化,因为这一看就是在一个已有项目里的操作,我们还得  npm init  一个项目:


0027dcefccdb155f8eb9fbd9c64e1420.webp

然后  npm i @builder.io/mitosis-cli @builder.io/mitosis ,紧接着再在根目录下创建一个  mitosis.config.js


49cc00c1925ae8e4b602f94fa25d5380.webp


在  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


b4586c2ff1e51c4d121c121c5d2ab52c.webp

接下来这个规则还算好接受一点,叫 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 > 10return <a />;
  else if (x 5return <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({
    a1,
  });

  // 这个 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  规范:


02f1075f548dbadf8cbb3ac617a788ce.webp


运行完成后所生成的 .eslintrc.js 长这样:


e3c5a2017af33679f16fab5ea50c9596.webp

我们给它改成这样:

          module.exports = {
  env: {
    browsertrue,
    es2021true,
    nodetrue,
  },
  extends: [
    'xo',
    'plugin:@builder.io/mitosis/recommended',
  ],
  overrides: [{
    extends: ['xo-typescript'],
    files: ['*.ts''*.tsx'],
  }],
  parserOptions: {
    ecmaVersion'latest',
    sourceType'module',
    ecmaFeatures: {jsxtrue},
  },
  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 ,接下来我们就可以看到命令行里:


2cf72bbb3b3d8178f4c6da8f47687cb2.webp


定睛一看,根目录下多了个 output 文件夹:


f2bc4327cc943295bb706dcdf15ec668.webp


我们挨个来看下:

          // 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 的函数组件是每次更新都会执行一次,也就是说如果我们写成这样:


b139f838d69a14da4eca4b8048f16075.webp


那么每次组件更新时都会 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 -


4f4059035f29efe53815e68b43d625c3.webp

加主页君微信,不仅前端技能+1

da5c30fe5bff28a75c8b13ce6da118a8.webpe7bf46de46976bc2f5f6054cb6c0cc63.webp

主页君日常还会在个人微信分享前端开发学习资源技术文章精选,不定期分享一些有意思的活动岗位内推以及如何用技术做业余项目

加个微信,打开一扇窗



推荐阅读  点击标题可跳转

1、React 拖拽排序组件库对比研究

2、从零开始搭建一个属于你自己的组件库!

3、看了 9 个开源的 Vue3 组件库,发现了这些前端的流行趋势


觉得本文对你有帮助?请分享给更多人

推荐关注「前端大全」,提升前端技能

点赞和在看就是最大的支持 ❤️

浏览 37
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报