前言
- 大家好,我是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%;- VueCompositionApi;
- plain-ui-composition;
- plain-design-composition;
- 这三个属于一种包含的关系,也就是说puc比vca多出来2%的知识点,pdc比puc多出来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;