想写React, 公司技术选型却限制用Vue, 怎么办?我来教你!

1,107 阅读4分钟

学习本篇文章,你将收获:

  • 快速接入 Vue3.0 的学习开发
  • 体验书写 React 的快感

vue-hooks

秘诀: 换汤不换药

我们知道,一个 Vue 项目想要改造成 React 项目,实属难于登天,不单单仅限于语法上的差异,背后涉及的生态才是痛点,怎么办呢?试想一下,我们可不可以只改变 Vue 的写法,以 React jsx 的形式来写呢?答案是肯定的,我们今天就来体验一下 React 版本的 Vue 实现,莫急,精彩马上开始!

前置条件: 让 Vue 支持 JSX

  • 一个令人振奋的好消息就是:@vue/cli 3.x 脚手架创建的 Vue 项目直接是支持 JSX 的:

    // xxx.vue
    <script>
        export default {
            name: 'xxx',
            ...,
            render() {
                return <h3>JSX 渲染</h3>
            }
        }
    </script>
    

    运行后,直接能够渲染。

  • 如果是以前版本构建的项目则需要安装依赖: npm install babel-helper-vue-jsx-merge-props babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx babel-preset-env --save-dev

    // .babelrc
    {
      "presets": ["env"],
      "plugins": ["transform-vue-jsx"]
    }
    

    具体可查看:npmjs

我们继续

  1. 我们在项目 pages 目录里面新建一个组件 Hooks.js(x),既然是 React 一样写代码,自然后缀名肯定要改正过来。

  2. 我们需要安装一个包:vue-hooks,不知道有没有小伙伴使用过。

  3. 我们从 node_modules 里面找到这个包:我们可以发现,里面其实就只有一个有效的 index.js,从 package.json 中也可以看到它没有引入其它任何第三方依赖,其代码量也不足 200 行,这是个好消息(伏笔)。

  4. 我们可以看一下它导出的所有方法:

    {
        withHooks, // 通过使用 vue-hooks 包装成 Vue 组件的函数 
        useState, // 类比:React useState
        useEffect, // 类比:React useEffect
        useRef, // 类比:React useEffect
        useData, // 类比: Vue state
        useMounted, // 类比: Vue useMounted
        useDestroyed, // 类比: Vue useDestroyed
        useUpdated, // 类比: Vue useUpdated
        useWatch, // 类比: Vue useWatch
        useComputed, // 类比: Vue useComputed
        hooks // .vue 文件中使用的钩子
    }
    
  5. 如何使用?我们看一下包里面的 README.md 。从里面我们可以看到,vue-hooks 提供了三种使用方式。我们首先简单看一下第三种使用方式:

    • main.js 中,
      import { hooks } from 'vue-hooks'
      Vue.use(hooks)
      
    • .vue 文件中,
      <script>
          export default {
              name: 'xxx',
              ...,
              hooks() {
                  const data = useData({
                    count: 0
                  })
                  const double = useComputed(() => data.count * 2)
                  return {
                    data,
                    double
                  }
              }
          }
      </script>
      

    return 出来的变量是可以直接作用在 <template> 上面的,使用起来是不是很简单? 偷偷告诉你 hooks(props) {} 这里是可以接收传递的属性参数的。这里我不多赘述,有兴趣的同学可以自己去研究一下,已经超出了本文要讨论的范围哦。我们要重点来看第一种和第二种的写法,毕竟要是 React 风格的嘛~

  6. 说到这里,有同学就会问了:为什么要学习两种方式,React style 不是只需要第一种就可以了吗?说的没错!如果是从零开发项目,真的只需要第一种方式就行了,但是,用到组件怎么办?自己造轮子? 不打算使用第三方 UI 组件?这不现实吧!首先要特别强调的一点是:使用 vue-hooks React style 写代码,只是形势上的变化,实质还是在写 Vue Components ,千万不要混淆了概念,如我开头所说,换汤不换药,切记!

  7. 由于 vue-hooks 现在已经没人维护,不再更新了,对于暴露的接口还有一点点欠缺,为了满足我们的开发需求,兼容 Vue 的 api ,我们来稍加改造一下,找到 function withHooks(render)

    export function withHooks(render) {
      return {
        data() {
          return {
            _state: {}
          }
        },
        created() {
          this._effectStore = {}
          this._refsStore = {}
          this._computedStore = {}
        },
        render(h) {
          callIndex = 0
          currentInstance = this
          isMounting = !this._vnode
          const ret = render(
            h,
            Object.assign(
              {},
              this.$attrs,
              this.$props, {
                $store: this.$store,
                $refs: this.$refs,
                $router: this.$router,
                $on: this.$on.bind(this),
                $emit: this.$emit.bind(this),
                children: this.$slots
              }
            )
          )
          currentInstance = null
          return ret
        }
      }
    }
    

    现在就大功告成了。

  8. 还有一点值得注意的是,因为我们改的是 node_modules 的文件夹里面的文件, 对于协同开发,其他同学拉取代码得到下载的包依然是未更改过的,这就是个问题, 我们可以把这个改过的文件夹拷贝到工程目录 src 下,或者根目录下就 ok 了。(回答前面留下的伏笔)。 当然,如果有同学能把更改过的包发布到 npm 仓库就非常 nice 了。

上手体验一下哈~

且看我实现的一个 demo 案例,稍后作解释:

// Hooks.jsx
import {
  withHooks,
  useState,
  useEffect,
  useRef,
  useData,
  useWatch,
  useComputed
} from '@/vue-hooks'
import { Tree, Input } from 'element-ui'

const Hooks = withHooks((h, props) => {
  // console.log(props)
  // state
  const [prop, setProp] = useState('')
  const tree = useRef('tree')

  const data = useData({
    value: ''
  })

  // effect
  useEffect(() => {
    props.$store.dispatch('getUser', {
      name: '小明',
      age: 30
    })
  }, [])

  useWatch(() => data.value, (val, oldVal) => {
    console.log(val)
  })

  useComputed(() => {
    setProp(props.$store.getters.getUser.name)
    props.$refs.tree && props.$refs.tree.$on('node-click', (value, node) => {
      console.log(node)
    })
  })

  const clickHandle = (e) => {
    console.log('点我干什么', e.target)
  }

  const filterNode = (value, data) => {
    if (!value) return true
    return data.label.indexOf(value) !== -1
  }

  const watchValue = (val) => {
    data.value = val
    props.$refs.tree.filter(val)
  }

  return (
    <div>
      <h3 onClick={clickHandle}>{ props.aa } This is Hooks Page! {prop}</h3>
      <div>
        <Input
          placeholder="输入关键字进行过滤"
          value={data.value}
          onInput={watchValue}
          style="width: 250px"
        />
        <Tree
          data={props.dataTree}
          show-checkbox
          node-key="id"
          filter-node-method={filterNode}
          indent={35}
          check-strictly
          ref={tree.current}
        />
      </div>
    </div>
  )
})
Hooks.name = 'Hooks' // 给组件命名,避免出现 <Anonymous Component />

export default Hooks
// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: {}
  },
  getters: {
    getUser(state) {
      return state.user
    }
  },
  mutations: {
    getUser(state, payload) {
      state.user = payload
    }
  },
  actions: {
    getUser({ commit }, payload) {
      setTimeout(() => {
        commit('getUser', payload)
      }, 1000)
    }
  }
})
// vue.config.js
module.exports = {
  publicPath: '/',
  outputDir: 'dist',
  configureWebpack (config) {},
  css: {
    requireModuleExtension: true // 开启css modules => xxx.modules.css
  },
  devServer: {
    // proxy: {},
    before: app => {}
  }
}

解释:

  • store.js 就是存了一个非常简单的 Vue store 数据

  • Hooks.jsx 从第三方 UI 库引入了 element-ui 这么个包,引入了 { Tree, Input } 两个组件

  • 使用方式与 React 一模一样,放在末尾用 return 返回

  • withHooks((h, props) => {}) 可以看作是 React 函数组件,props 作为第二个参数

  • useStateuseEffect 使用与 React Hooks 的 useStateuseEffect 完全一致

  • useRef 使用上有些许区别,

    const treeRef = useRef('tree')
    ref={treeRef.current}
    或者:ref="tree"  ^_^
    

    React 直接是 ref={treeRef}, Vue 版的 useRef 要给它一个名字,之后才能通过 props.$refs.tree 使用

  • useData 类似于 Vue data 提供的值

  • useWatch 类似于 Vue watch 接收2个回调函数,第一个为监听的属性,第二个为提供前后变化值的回调函数

  • useComputed 类似于 Vue computed,接收一个回调函数

  • 注意:useDatauseState 的值都可以直接作用于 DOM 节点,触发变更会自动更新,但是 useState 的值不能够被 useWatchuseComputed 监听到

  • 属性的变化可以被 useWatchuseComputed 监听到

  • 组件指令属性改造:

    v-model ==> value={} onInput={() => {}}
    v-* ==> { props.xx ? a : b }
    !!!【class => className; for => htmlFor】✘✘✘    // 不需要改造
    v-bind ==> {}
    @ ==> 
        1. 原生事件: `onClick={}` 
        2. `$emit` 事件:`{props.$refs.xxx.$on}`
    
  • Vue 常用的 api 都可以通过 propsprops.$storeprops.$refsprops.$routerprops.$onprops.$emit 来完成业务逻辑

  • props.children 用来表示 vue $slots,匿名插槽通过 props.children.default 访问, 命名插槽通过 props.children.xxx访问

  • 有兴趣的同学可以研究一下 vue-hooks 各个 api 的源码,代码量不多,总共不到 200 行

  • 还有疑问的同学可以留言一起探讨!么么哒~

补充

前文提到修改 node_mudules 代码需要拷贝一份到 src 目录,这里需要纠正一下,原因有: ① 包有其他依赖项,拷贝不现实 ② 拷贝的代码无法迭代更新 。

经查阅,解决方案如下:

// vue.config.js
chainWebpack: config => {
  config.module
    .rule('custom-loader')
    .test(/需要改动的文件名.js$/)
    .use('custom-loader')
    .loader(resolve('loaders/custom-loader'))
    .end()
}   
// loaders/custom-loader.js
module.exports = function (source) {
  const source = '修改的内容';
  return source
}

写到最后

怎么样? React 版 Vue 写法是否 get 到了呢?是不是跃跃欲动蠢蠢欲试了呢?赶紧行动起来吧!我爱 React !

本文纯手敲,觉得写得还不错点个赞关注一波呗,如有错别字,或者知识存疑,还请各位大佬指正!