来了!VueCompositionApi For React!

1,752 阅读7分钟

前言

  • 大家好,我是peryl,今天给大家带来的是 VueCompositionApi for React——plain-design-composition
  • 在前端社区中,关于VueCompositionApi与React Hook之间的讨论,那是异常激烈,有不同站队的,有保持中立的,也有在观望不知道学啥好的,也有像小编这样两边都学一点的,毕竟赚钱嘛,不磕碜;于是在这种背景之下,小编慢慢地琢磨出来了,能够在React开发中拥有一模一样VueCompositionApi开发体验的方式,当然前提是使用jsx,而不是sfc template。 废话不多说,直接上才艺;

安装

安装插件

在一个已有的React应用中使用plain-design-composition(以下简称为pdc),关键在于配置babel加上一个编译插件,如下示例所示:

const JSXModel = require('plain-design-composition/plugins/babel-plugin-react-model')
// babel.config.js 或者 .babelrc
module.exports = {
    plugins:[
        JSXModel,
    ],
}
  • 这个插件是用来编译jsx代码中的v-model,比如语法糖v-model={state.name}会编译成modelValue={state.name}以及onUpdateModelValue={val=>state.name=val},实际上onUpdateModelValue中的代码会复杂一点,这里只是做简洁示例;
  • 多值绑定,比如同时给一个组件写上:
    • v-model-start={ state.formData.startDate }
    • v-model-end={ state.formData.endDate }
  • 那么这个多值就会被编译成以下四个属性:
    • start={ state.formData.startDate }
    • onUpdateStart={ val => state.formData.startDate = val }
    • end={state.formData.end}
    • onUpdateEnd={ val => state.formData.endDate = val }

这个插件可以理解为vue jsx编译插件的超级缩小版,当然功能也是超级缩水…………;

在Vite中构建

本文以vite构建react应用中使用pdc为例,首先使用vite创建一个react工程

yarn create vite

// 1. 第一次随便输入项目名称
// 2. 选择react模板
// 3. 选择react-ts模板
// 4. 进入工程根目录安装依赖, npm i;
// 5. 启动服务: npm run dev

安装依赖:

yarn add plain-design-composition babel-plugin-syntax-jsx @babel/preset-react @babel/preset-typescript

在vite.config.ts 中配置插件

import {defineConfig} from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
// @ts-ignore
import JsxModelTransform from 'plain-design-composition/plugins/vite-plugin-react-jsx-model'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        JsxModelTransform(),
        reactRefresh()
    ]
})

接下来就可以使用pdc在应用中编写组件了。

其他构建

  • 关于使用其他构建工具,比如cra,umi,vue-cli(没错,vue-cli也能用来构建react应用)等等,可以参考这个环境搭建文档:plain-design 快速上手
  • 或者连一盏茶的功夫也没有,就想直接上手体验,你也可以直接新建一个html文件,粘贴这篇文档中的代码即可体验——plain-design 快速上手:静态构建

用法

基本用法

使用designComponent定义一个组,比如现在要实现一个能够支持双向绑定数字变量的步进器组件

const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup({props, event}) {

        const {emit} = event

        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue + 1
                emit.onAddNum(val)
                emit.onUpdateModelValue(val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                emit.onSubNum(val)
                emit.onUpdateModelValue(val)
            },
        }

        return () => (
            <div>
                <button onClick={handler.onClickSub}>-</button>
                <button>{props.modelValue == null ? 'N' : props.modelValue}</button>
                <button onClick={handler.onClickAdd}>+</button>
            </div>
        )
    },
})

// 在父组件中使用
export const DemoPage = designPage(() => {
    const state = reactive({
        count: 123
    })
    return () => <>
        <h1>Hello world:{state.count}</h1>
        <DesignNumber
            v-model={state.count}
            onAddNum={val => console.log('add', val)}
            onChange={val => console.log('get change value', val, val?.toFixed(0))}
        />
    </>
})

const App = () => <>
    <div className="App">
        <DemoPage/>
    </div>
</>

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)
  • 有印象的同学应该可以看出来,这个DesignNumber组件的源码,与小编之前讲的一篇文章给defineComponent附魔中的DesignNumber组件的源码一模一样。唯一的区别就是当前这个是React组件,另一篇文章中的是Vue3组件;

组件选项

以下列举了所有属性,具体请查看pdc的文档或者这篇掘金文章:给defineComponent附魔,这篇文章中有录制了一个视频具体讲解了每个属性的用法示例。目前所有的属性的功能与plain-ui-composition都是一样的,区别在于当前这些属性是运行在React应用中的;

  • name
  • provideRefer
  • inheritAttrs
  • props
  • emits
  • setup
  • expose
  • slots
  • scopeSlots

React Hook

  • designComponent最后返回的是一个React Hook组件;
  • 虽然在designComponent中推荐使用CompositionApi,但是CompositionApi并不能解决所有场景的问题,为此pdc暴露一个叫做useHookOnDesign的一个函数,可以实现在CompositionApi中同时使用Hook函数的骚操作;如下示例所示:
import {useEffect, useRef} from 'react'
import {designPage, onBeforeUpdate, onUpdated, reactive, useHooksOnDesign, useRefs} from 'plain-design-composition'

export default designPage(() => {

  const {refs, onRef} = useRefs({button: HTMLButtonElement})
  const state = reactive({
    num: 0,
  })

  onBeforeUpdate(() => {
    console.log('before update:', !refs.button ? 'button not mounted' : refs.button.innerText)
  })
  onUpdated(() => {
    console.log('updated:', !refs.button ? 'button not mounted' : refs.button.innerText)
  })

  const hookData = useHooksOnDesign(() => {
    const count = useRef(0)
    console.log('on hook:', !refs.button ? 'button not mounted' : refs.button.innerText)
    useEffect(() => {
      count.current++
      console.log('useEffect:', !refs.button ? 'button not mounted' : refs.button.innerText)
    })
    /*hook 中返回的数据也可以用来渲染*/
    return {
      count
    }
  })
  console.log(hookData)
  return () => (
    <div>
      <h4>hook data count: {String(hookData.current.count.current)}</h4>
      <button ref={onRef.button} onClick={() => state.num++}>计数器:{String(state.num)}</button>
    </div>
  )
})

自定义Composition钩子函数

以taro中使用pdc为例,taro暴露了一个叫做usePullDownRefresh的函数,用来在hook组件中监听页面的下拉刷新事件;那么现在可以基于这个hook函数封装一个Composition钩子函数,示例如下所示:

import {usePullDownRefresh, stopPullDownRefresh} from '@tarojs/taro'

const onPullDownRefresh = (fn: () => void) => {
  // eslint-disable-next-line
  useHooksOnDesign(() => {
    usePullDownRefresh(fn)
  })
}

export default designPage(() => {
  const state = reactive({
    count: 100
  })
  onPullDownRefresh(() => {
    console.log('下拉刷新', Button, View)
    stopPullDownRefresh()
  })
  return () => <>
    <View>
      Hello world
    </View>
  </>
})

最后附上 onMounted 钩子函数的实现源码:

export const onMounted = createHookFunction('onMounted', (cb: () => (void | (() => void) | any)) => {
    useHooksOnDesign(() => {
        useEffect(() => {
            const returnValue = cb()
            return typeof returnValue === "function" ? returnValue : undefined
        }, [])
    })
})

其他

在本文末尾所提到的录制的视频中,还演示了如何使用reactivity api创建一个超简单的状态共享机制;以及使用不到30行代码在React中实现一个简易的国际化插件;以下是使用ReactivityApi实现一个多语言国际化Api的示例代码:

import {reactive} from "plain-design-composition";

export const intl = (() => {
    const state = reactive({locale: {} as Record<string, any>})
    const get = (keyString: string) => {
        return ({
            d: (defaultString: string): string => {
                const keys = keyString.split('.')
                const len = keys.length
                let obj = state.locale
                for (let i = 0; i < keys.length; i++) {
                    const key = keys[i];
                    if (i === len - 1 && !!obj && typeof obj[key] === "string") {
                        return obj[key]
                    }
                    if (!obj) {
                        return defaultString
                    }
                    obj = obj[key]
                }
                return defaultString
            },
        })
    }
    const setLocal = (language: any) => state.locale = language
    return {
        get, setLocal
    }
})()

// 使用intl多语言的示例代码
export const DemoIntl = designPage(() => {
    return () => <>
        <h1>{intl.get('order.detail.title').d('默认订单详情大标题')}</h1>
        <h1>{intl.get('order.detail.subTitle').d('默认订单详情福标题')}</h1>

        <button onClick={() => intl.setLocal({
            order: {
                detail: {
                    title: '中文标题',
                },
            },
        })}>中文
        </button>
        <button onClick={() => intl.setLocal({
            order: {
                detail: {
                    title: 'title',
                    subTitle: 'sub title',
                },
            },
        })}>English
        </button>
    </>
})
  • 示例中有中文英文两种语言包,页面初始化的时候没有使用任何语言包,所以标题和副标题位置都是显示默认的文本:
    • 默认订单详情大标题
    • 默认订单详情副标题
  • 切换到英文之后,显示为:
    • title
    • sub title
  • 切换到中文之后,显示为:
    • 中文标题
    • 默认订单详情副标题(因为中文语言包中没有匹配到order.detail.subTitle,所以会使用默认的显示文本)

结语

  • 小编录制了一个视频对本文的一些示例的说明:来了!VueCompositionApi For React!
  • pdc的可用性目前是有组件库做支撑的,plain-design就是基于pdc实现的一套React组件库,其中涵盖了大部分常见的组件及其功能,下一篇文章小编将介绍这个React组件库;有意思的是,plain-design组件库的源码与Vue3组件库plain-ui的源码相似度高达95%;
  • 整个VueCompositionApi For React的学习过程可以分为三部分,第一部分是学会使用VueCompositionApi,占比大概为97%,然后是学习使用plain-ui-composition,学习成本占比2%,最后是plain-design-composition,学习成本占比1%;
  • 有的同学可能会问,我已经会用Vue了,那需要久才能上手React开发?那么恭喜你同学,现在有了pdc之后,学会了Vue开发=学会了React开发,虽然说不能保证成为一名React大牛,但是至少常见的React功能以及组件都可以比原来更轻松地完成;从现在开始就可以去卷其他的React开发者了。
  • 各位学习React的同学,也可以尝试一下VueCompositionApi,体验一下React Class、React Hook以外的不一样的开发体验;
  • 小编在做这个库之前,也有在调研其他的ReactCompositionApi方案,但是没能找到一款心目中合适的库。理想中的ReactCompositionApi应该与VueCompositionApi大致相同,这样不但能够减少学习成本,也能够减少Vue组件与React组件互相转换的成本;相信在座的各位也曾看到过或者正在研发类似的ReactCompositionApi库,就别藏着掖着了,有亮眼的功能赶紧拿出来共同探讨一下(让小编抄一下^_^!)。
  • 目前正在筹备做taro的Vue3以及React版本的组件库,以充分验证CompositionApi的可行性;后续会有计划支持在ReactNative中使用VueCompositionApi,如果进展顺利,那么最后可以考虑使用sfc来开发React应用;所以,最终目的其实是使用Vue sfc来开发ReactNative;