终究还是vue拯救了我
遇到的麻烦
最近在公司React项目里写了一个库,全局modal api,使用方式如下:
import modals from "/@/src/service/global"
import { Settings } from "/@/components/modals"
// 任意位置
// 当只是简单的确认框时
// 等价于 modals.show("Confirm", {
// title: "提示",
// description: "描述内容"
// })
modals.Confirm("提示", "描述内容", options).then(res => res ? "confirm" : "cancel")
// 当需要自定义Content时,Content可以是任何复杂的大型组件
// Content 在components/modals里, 使用 Content: lazy(() => import("ComplexContent")) 懒加载
// 下面这个api就会复用Confirm的title和actions,将Content替换为Settings
modals.show("Confirm", {
title: "设置",
Content: Settings
})
// 如果是electron单机版,show方法会有分支语句 打开新的BrowserWindow,通过特殊的url参数仅显示这个Content
modals.show("Settings", {
asWindow: true // 实际里是在设置里做了本地化,在show方法里主动进行了处理
})
// 此时的show实际上会开一个新窗口 url 为 host/modal?name=Settings
if (route === "modal") {
ReactDOM.render( // modal context
//...
<Component render={query.name} />
//...
,
dom
)
} else {
ReactDOM.render( // app context
// App
)
}
这样保证了一个入口和host能处理多窗口不同的各种显示需求,代码通过split chunk 和懒加载 处理当入口唯一而显示复杂的需求。
但同样,modal作为全局任意地方可调用的api,用Provider控制就必须写在这一条context里,那么多窗口里写无数个context就会很恶心。同时,弹窗何其多!那干脆做得绝一点,另外起一个react实例的context。原理就和ant-design的message、notification组件一样(保存了一份另一个context的react实例,利用这个实例和ref调用内部组件的方法修改state,使得你可以在不同的context也能弹窗)。这个都不是重点就不细说有兴趣的同学可以去看ant-design的实现。
既然不同context,那么react就麻烦了,provider和reducer都是基于context的实现。跨context更新个毛啊!
试了redux/rxjs等各种方案后,还是vue香啊,直接上手@vue/reactivity,provider/context 或者 reducer 贼麻烦,而且还不能跨context,reactivity完全轻量级嘛。
用@vue/reactivity 拯救我react项目的全局数据共享
核心思想,vue3的reactivity是和vue主框架分离的,也就是响应式系统是纯原生js,同时可独立用于web或者node。gzip才11kb左右。
所以,我们只需要写一个globalState监听state的变化,每次变化时设置react的state就OK了。
在此之前,@vue/reactivity时没有watch的,因为watch api 在compilar-dom里深度绑定了生命周期。所以我们要稍微的实现一个简易版的watch:
// 这里面一个比较重要的api 就是effect
// 第一个参数getter返回一个响应式对象(监听目标)的函数,Ref或者Reactive都可以
// 第二参数配置项里的scheduler是一个方法,可以理解为监听目标change之后的副作用函数。
// effect返回一个runner,可以通过stop(runner)(reactivity提供的api)停止监听。
function traverse(value, seen = new Set()) { /* 遍历收集响应式 */ }
const watch = (source, fn, { deep = true, lazy } = {}) => {
let getter = isRef(source)
? () => source.value
: isReactive(source)
? () => source
: source
if (deep) {
let _source = getter()
getter = () => traverse(_source)
}
const runner = effect(getter, {
lazy,
scheduler: () => {
fn()
stop(runner)
}
})
}
参考:antfu watch with @vue/reactivity
// somepaht/store.js
const globalState = reactive({
count: 0
})
export function useCount(num) {
const [count, setCount] = useState(num)
const observe = (v) => {
globalState.count = v
}
useEffect(() => {
watch(globalState, () => {
setCount(globalState.count) // 副作用去修改react组件state
})
})
return [
count,
observe
]
}
// some component.jsx
import { useCount } from "somepath/store";
export default function TheCount() {
const [count, setCount] = useCount(2)
console.log("Info count", count)
return <p style={{textAlign: "center"}}>
<button type="button" onClick={() => {
setCount(count + 1)
}}>
count in Info context is: {count}
</button>
</p>
}
就这么简单,实现了一个context无关,全局响应式,轻量级,扩展方便的全局数据管理。再也不用为store、主题同步(之前的modals主题同步太麻烦了,用provider还需要通知update另一个context)烦恼了。
另外,我写了一个demo 对比reducer和@vue/reactivity CodeSandbox
如果你感兴趣,可以支持一下 github antfu/reactivue