Vue、React 也可以互相引用?| 8月更文挑战

4,787 阅读4分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

前言

Vuera 可以使 Vue 和 React 开发的组件相互混用。

当已经有成熟的 React 组件或某个开源组件不是用你使用的技术栈 Vue 开发的时候,你可以使用 Vuera 来导入这个 React 组件,从而低成本的复用 React 组件。

既然我们已经知道 Vuera 是用来混用 Vue、React 组件的,那么接下来我们来看看是怎么使用的。

举个例子

最简单的 demo :

<template> 
  <div> 
    <my-react-component :message="message" />
    </div>
</template>
<script>
import { ReactInVue } from 'vuera'
import MyReactComponent from './MyReactComponent'

export default {
  data () {
    message: 'Hello from React!',
  },
  components: { 
    'my-react-component': ReactInVue(MyReactComponent)
  }
}
</script>

可以看到,这段代码引入了一个 React 组件,并且通过 ReactInVue 包装了一下,然后这个组件就可以在 Vue 环境中跑起来了。

从这里可以看出来,Vuera 的使用方式还是很简单的,在 Vue 中使用 React 组件只需要使用 ReactInVue 包装一下。那么现在我们来看看 ReactInVue都做了什么。

前置知识

在 Vue 中,有两种⽅式可以创建⼀个组件。

// way 1
Vue.component('com-1', {
  template: '<div class="constainer">{{msg}}</div>',
  data() {
     return {
         msg: 'com-1'
     }
 },
})
// way 2
Vue.component('com-1', {
  data() {
     return {
       msg: 'com-1'
     }
   },
   render(h) {
     return h('div', { class: "constainer" }, this.msg)
   }
})

关键点在于 Vue支持配置 template 或者 手写 render 函数。

template:html + vue 语法糖的模板字符串

render 函数:render 的第一个参数是渲染函数,即 createElement。可以接收三个参数,分别是 标签名,标签的属性,子标签或文本。

在 Vue 中,支持 ref 属性获取元素渲染后的真实 DOM。

Vue.component('com-1', {
   template: '<div class="container" ref="container">{{msg}}</div>',
   data() {
      return {
        msg: 'com-1'
      }
   },
  mounted() {
    console.log(this.$refs.container)
  },
})

可以看到,在 container 上添加了一个 ref 属性,它的值是 container。这样在 mounted(页面渲染完毕后),就可以通过 this.$refs 获取到 container 的真实 DOM 节点。

在 Vue 中,可以通过 new Vue({ el: xx }) 渲染 Vue组件到 el 节点上

React 中,支持书写 JSX 节点。看下 JSX 编译前后的对比:

const App = (props) => (
    <div className="container"> {props.msg} </div>
)

// ->

const h = React.createElement
const App = () => h('div', {
    className: 'container'
}, props.msg)

这时可以看到,App 编译后也是 render 函数,这里的 h 也就是 createElement,这里和 Vue 几乎一致。

React 也支持 ref

class App extends React.Component {
  onRefMounted(element) {
      console.log(element)
  }
  render() {
      return (
        {/* <div className="container" ref={this.containerRef}> */} 
    	<div className="container" ref={this.onRefMounted}>
            {props.msg} 
	</div>
     )
  }
}

使用方式和 Vue 相同,ref 属性可以是一个函数或一个变量。如果是一个函数,那么在页面渲染完毕后,这个函数的形参上会有 ref 对应的 DOM 真实节点。

React 可以通过 ReactDOM.render 渲染 React 组件到节点上

ReactDOM.render(<App />, document.querySelector('#app'))

ReactInVue 的核心原理

我们先以上面的例子来解释,即在 Vue 中使用 React 组件:

<!-- App.vue -->
<template>
  <div>
  	<ReactComponent />
  </div>
</template>
<script>
  export default {
    components: { 
      'my-react-component': ReactInVue(MyReactComponent)
    }
  }
</script>

new Vue({
  el: '#app',
  render(h) {
    return h('div', {}, h('ReactComponent'))
  }
})

我们先把上面的代码转化为 render 函数(babel 编译后):

Vue.component('ReactComponent', {
  render(h) {
    return h('react-wrapper', {
      props: {
        component: MyReactComponent,
      }
    })
  }
})

new Vue({
  el: '#app',
  render(h) {
    return h('div', {}, h('ReactComponent'))
  }
})

可以看到这里的关键在于 react-wrapper。它接收一个 component ,值是我们传入的 React 组件。

这里的 react-wrapper 很明显是个 Vue 组件:

{
  render(h) {
    return h('div', { ref: 'react' }),
  },
  methods: {
    mountReactComponent() {
      // 渲染 React 组件在 ref="react" 的 DOM 节点上
    }
  },
  mounted() {
    this.mountReactComponent()
  }
}

上面代码就是 react-wrapper 最精简的版本。可以看到:这个 Vue 组件 绑定了一个 ref 叫 react。在这个div 渲染完毕后,会执行 mountReactComponent

这里可以看到,Vue 内嵌套 React 组件 首先通过 将 React组件作为参数传入一个 `react-wrapper`` 组件,然后 render 时渲染一个空的 div,这个div的作用是作为父亲,传入的react组件最终会渲染到这个 div上。

我们接着说 mountReactComponent 做了哪些事情。

{
  methods: {
    mountReactComponent (component) {
      const Component = makeReactContainer(component)
      const children = this.$slots.default !== undefined
      	? { children: this.$slots.default }
      : {}
      
      ReactDOM.render(
        <Component
          {...this.$props.passedProps}
          {...this.$attrs}
          {...this.$listeners}
          {...children}
          ref={ref => (this.reactComponentRef = ref)}
        />,
        this.$refs.react
      )
    }
  }
}

这里的 makeReactContainer 是用来处理 children 相关的逻辑,我们先暂时不看。

我们知道 this.$slots.default 就相当于 React 中的 children,然后这里 会把 this.$slots.default 包装成对象 赋给 children

然后会通过 ReactDOM.render 来将传入的 React 组件,渲染在 this.$refs.react,这就是刚刚 render 中渲染的 空 div 节点。

看到这里其实就可以总结出,先创建一个空的 div 节点,然后通过 ref 拿到真实 DOM,再通过 ReactDOM.render 来将React组件渲染到这个 DOM 上,至此就实现了 在 Vue 中使用 React 组件。

如果引入的 React 组件没有子节点,那么代码其实就到此为止了。但是实际上大多数的 React 组件都是层层嵌套的,它的子节点也是需要处理的。

上述代码 render 的 Component 是经过了 makeReactContainer 。所以这里我们接着再看一下 makeReactComponent 做了什么。

从它的用法可以看出,makeReactContainer 返回的肯定是一个 react 组件。

const makeReactContainer = Component => {
  return class ReactInVue extends React.Component {
    constructor(props) {
    	super(props)
      this.state = { ...props }
    }
    
    render() {
      const { children, ...rest } = this.state
      const wrappedChildren = this.wrapVueChildren(children)
      
      return (
        <Component {...rest}>
          {children && <VueWrapper component={wrappedChildren} />}
        </Component>
      )
    }
  }
}

我们可以看到 传给 Component 组件的 props 会被 ReactInVue 组件接收到,然后会将props绑定到 state上。

然后在 render 中,将 children子节点解构出来,然后传入了 wrapVueChildren 并作为 component 参数 传入到了 VueWrapper 组件中。

也就是说 React组件的子节点会包装一下 并传入 VueWrapper 中。

那么 VueWrapper 中又做了什么呢,我们展开看一下:

class VueWrapper extends React.Component {
  constructor(props) {
    super(props)
    this.currentVueComponent = props.component
  }
  
  createVueInstance (targetElement, reactThisBinding) {
    const { component, on, ...props } = reactThisBinding.props

    // `this` refers to Vue instance in the constructor
    reactThisBinding.vueInstance = new Vue({
      el: targetElement,
      data: props,
      ...config.vueInstanceOptions,
      render (createElement) {
        return createElement(
          VUE_COMPONENT_NAME,
          {
            props: this.$data,
            on,
          },
          [wrapReactChildren(createElement, this.children)]
        )
      },
      components: {
        [VUE_COMPONENT_NAME]: component,
        'vuera-internal-react-wrapper': ReactWrapper,
      },
    })
  }
  
  render () {
    return <div ref={this.createVueInstance} />
  }
} 

首先将传入的 component(嵌入Vue中 React组件的 子节点)保存到了 currentVueComponent 中。

然后在 render 函数中,将创建一个空的 div,然后绑定 ref 用来获取真实的 div DOM节点。

当div渲染完毕后,在 createVueInstance 可以拿到div 的 真实DOM 节点 targetElement。这里 reactThisBinding 就是当前的 React类的 this 实例(在 constructor中绑定,绑定过程省略)

然后可以看到 首先将传入的 props 中 component、on 解构出来。

显然 component 就是嵌入Vue中 React组件的 子节点,而 on 是在 Vue 中组件绑定的一些事件(onClick ....),然后会通过 new Vue 创建一个 Vue 节点。然后渲染到 targetElement 上。这里和 刚才的 ReactDOM.render() 渲染到一个 ref 节点上是完全一致的。

然后在 render 的部分,可以看到 会渲染传入的这个 component 组件,然后 render 函数的第二个参数是 { props, on }, 最后一个参数是关于子节点的部分,传入了 wrapReactChildren,来看看 wrapReactChildren 的代码:

const wrapReactChildren = (createElement, children) =>
  createElement('vuera-internal-react-wrapper', {
    props: {
      component: () => <div>{children}</div>,
    },
  })

这里会将 children 作为 component 的参数传入 vuera-internal-react-wrapper 组件中,而在上一段代码 new Vue 中可以看到:

components: {
  [VUE_COMPONENT_NAME]: component,
    'vuera-internal-react-wrapper': ReactWrapper,
}

vuera-internal-react-wrapper 其实就是 ReactWrapper。这这就我们刚才说的 react-wrapper.

所以到此为止可以看到,React 的所有子节点都会循环在 react-wrappervue-wrapper 中转换。

到这,所有的代码和原理已经讲解完毕。