Ray-D-Song's Blog

用 Mitosis 同时构建 React 和 Vue 组件

2024-08-09 7min

上次写了个UnoCSS icon 选择器, 希望写个库可以同时在 React 和 Vue 中使用.

首先想到的是 Web Component. 但缺点是在核心逻辑之外需要针对 Vue 和 React 分别编写一套适配代码.
对于一些比较小的需求, 写适配代码的成本可能比人工适配还要高.

然后我就发现了Mitosis这邪门东西.
Mitosis 的中文是有丝分裂, 顾名思义, 它可以将组件编译成多个框架的代码, 包括 React, Vue, Qwik, Solid, Angular, Svelte.

实现原理就是一个编译器, 将 Mitosis 的文件编译成 Vue 和 React 的有限子集. 和 UnoCSS 一样是个前端编译期的探索.

创建项目

npm create @builder.io/mitosis@latest

输入命令会有一些可配置的选项, 在选择输出目标时, 只有React, Svelte, Qwik. 但支持的框架远不止于此.
修改library/mitosis.config.cjstargets字段, 增加 Vue 支持:

"targets": [
  "react",
  "vue",
],

项目自带todo-app.lite.tsxautocomplete.lite.tsx两个例子, 试运行一下.

cd library
npm run build

# Mitosis: react: generated 2 components, 1 regular files.
# Mitosis: vue: generated 2 components, 1 regular files.
# Mitosis: generation completed.

/library/package会生成 React 和 Vue 的组件.

编写组件

Mitosis 的组件后缀是.lite.tsx, 和 React 近似的 jsx 语法, 但有一套自己的 API 和限制.

import { useState } from "@builder.io/mitosis";

export default function MyComponent(props) {
  const [name, setName] = useState("Steve");

  return (
    <div>
      <input
        css={{
          color: "red",
        }}
        value={name}
        onChange={(event) => setName(event.target.value)}
      />
      Hello! I can run natively in React, Vue, Svelte, Qwik, and many more frameworks!
    </div>
  );
}

例如由于编译器限制, 组件只支持上面这种 export default function 的写法, 不能用 const 定义箭头函数再导出.

状态管理

现代前端框架最重要的就是state.

useStore

mitosis组件状态维护通过useStore钩子, 返回值接收必须叫state(也是编译器限制).

export default function MyComponent() {
  // ...
  const state = useStore({
    iconList: [],
  })
}

React 组件中, useStore 会被转换成一个简单的useState定义.

store 的参数塞啥都可以, 但如果你想要的值需要经过表达式计算得到, 就得写个getter:

const state = useStore({
  iconList: [],
  get outlineIcon() {
    return this.iconList.filter(icon => icon.endsWith('-outline'))
  },
})

getter会转换成一个独立的方法:

const [iconList, setIconList] = useState(() => []);

function outlineIcon() {
  return iconList.filter((icon) => icon.endsWith("-outline"));
}

useState

mitosis 也支持useState api, 使用方式和 React 一样:

const [count, setCount] = useState(0)

在 Vue 中, useState 和 useStore 都会被编译成 options api 中的data选项.

顺带一提, 现在 mitosis 还不支持 Vue 3 的setup写法

export default defineComponent({
  data() {
    return { count: 0 };
  },
})

样式

Mitosis 通过标签上的css属性来编写camelCase的样式, 你甚至可以像这样写 css query.

<div
  css={{
    marginTop: '10px',
    '@media (max-width: 500px)': {
      marginTop: '0px',
    },
  }}
/>

而且编译后的结果还做了样式隔离. (React 本体到现在也没支持😂)

当然你也可以选择使用class属性(不需要 React 中的 className), 不过这样就没法做样式隔离了.

流程控制

为了屏蔽不同框架的差异, Mitosis 通过一标签实现了自己的流程控制, 以下是对照表, 详细可以看文档

MitosisReactVue
<Show>ifv-if
<For>mapv-for

集成其他库

因为 Mitosis 并不是一个前端框架, 而是一个编译器, 所以它没有 webpack 或者 vite 集成.
比如, 想要使用 UnoCSS, 只能使用uno cli, 监听文件变化然后编译出 css 文件, 再进行引入.

当然还有个更好的办法, 本质上 Mitosis 只是一个转化管道, 那我们只要设置产物的环境就行了, Mitosis 完成转换后,由其他工具接管, 进行打包.
最终我们编写组件的流程就变成了:

  1. 编写 Mitosis 组件
  2. 使用npm run build编译, 输出到 vite 环境中
  3. 使用vite之类的的打包器去打包最终的产物, npm 发布

你可以以这个项目为示例unocss-icon-viewer.

有一些关键点你可能需要注意, 比如mitosis的 tsconfig 将 jsxImportSource 设置为@builder.io/mitosis.
这会导致你 build react 或者 solid 之类的库时, jsx 类型报错.
解决方案是在 Vite 中配置 esbuild 和 ets 插件, 动态修改 compilerOptions.

export default defineConfig({
  plugins: [
    // ...
    dts({
      outDir: 'dist',
      include: 'packages/*/src/**',
      compilerOptions: {
        jsxImportSource: 'react',
      }
    }),
  ],
  // ...
  esbuild: {
    tsconfigRaw: {
      compilerOptions: {
        jsxImportSource: 'react',
      }
    }
  }
})

缺陷

Mitosis 算是不错的解决方案, 但也有缺陷, 比如: