设计

reactvue说明
渲染构建用户界面的 js 库渐进式框架react 侧重于 library,vue 侧重于 framework
编写方式setState 更新 state 的值来达到重新 render 视图响应式数据渲染,修改了响应式数据对应的视图也进行渲染react 需要考虑何时 setState,何时 render;vue 只需要考虑修改数据
编写方式jsxtemplatereact 是函数式,all in js;vue 区分 tempalte、script、style,提供语法糖,使用 vue-loader 编译

组件通信

  • React: 严格单向数据流

    • 向下: props
    • 向上: props func
    • 跨层级: context

    严格单向数据流,数据只能从父组件传递到子组件,子组件不能修改父组件传递过来的数据,只能通过回调函数的方式,让父组件修改数据,然后再传递给子组件。遵循万物皆可propsonChange/setState()

  • Vue: 单向数据流

    • 向下: props down
    • 向上: event up
    • 跨层级: provide/inject, $attrs/$listeners

    还有各种获取组件实例(VueComponent),如:$refs$parent$children;通过递归获取上级或下级组件,如:findComponentUpwardfindComponentDownward;高阶组件:provide/rejectdispatch/broadcast

reactvue说明
子组件数据传递propsprops都是声明式
组件状态机statedata管理组件的状态,react 使用 setState 更改,vue 直接赋值,新属性使用$set;vue 使用函数闭包特性,保证组件 data 的独立性,react 本就是函数

生命周期

生命周期reactvue说明
数据初始化constructorcreated
挂载componentDidMountmounteddom 节点已经生成
更新componentDidUpdateupdatedreact:组件更新完毕后,react 只会在第一次初始化成功会进入 componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到 prevProps 和 prevState,即更新前的 props 和 state。 vue:在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用
卸载componentWillUnmountdestroyed销毁事件

事件处理

  • React
    • React 事件的命名采用小驼峰式(camelCase),而不是纯小写
    • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
    • 不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault
    • 不能卸载非Element标签上,否则会当成props传下去
function Form() {
  function handleSubmit(e) {
    e.preventDefault()
    console.log('You clicked submit.')
  }
  return (
    <form onSubmit={handleSubmit}>
      <button type='submit'>Submit</button>
    </form>
  )
}
  • Vue

    • 用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件
//原生事件

<form v-on:submit.prevent="onSubmit"></form>
//自定义事件
<my-component @my-event="handleThis(123, $event)"></my-component>
  • vue 事件修饰符:
    • .stop - 调用 event.stopPropagation()。
    • .prevent - 调用 event.preventDefault()。
    • .capture - 添加事件侦听器时使用 capture 模式。
    • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    • .once - 事件将只触发一次。
    • .native - 监听组件根元素的原生事件。
    • .sync - 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。
    • .left - (2.2.0) 只当点击鼠标左键时触发。
    • .right - (2.2.0) 只当点击鼠标右键时触发。
    • .middle - (2.2.0) 只当点击鼠标中键时触发。
    • .passive - (2.3.0) 以 { passive: true } 模式添加侦听器
    • 事件修饰符的顺序:v-on:click.stop.prevent

class 和 style

  • class

    • ReactclassName
    render() {
      let className = 'menu';
      if (this.props.isActive) {
        className += ' menu-active';
      }
      return <span className={className}>Menu</span>
    }
    
    • Vueclass
    <div
      class="static"
      :class="{ active: isActive, 'text-danger': hasError }"
    ></div>
    <div :class="[{ active: isActive }, errorClass]"></div>
    
  • style

  • Reactstyle

<div style={{ color: 'red', fontWeight: 'bold' }} />

react 中,可以使用 CSS Modules 达到局部和模块功能,避免组件间 css 污染;注意,根据 CSS Modules 的官方规范,更推荐以驼峰式的命名方式定义类名,而非 kebab-casing

<h1 className={styles.appTitle}>
  • Vuestyle
<div :style="[baseStyles, overridingStyles]"></div>

组件样式的时候你可以在 style 标签上声明一个 scoped 来作为组件样式隔离标注,如:<style lang="sass" scoped></style>。最后打包时其实样式都加入一个 hash 的唯一值,避免组件间 css 污染

条件渲染

  • Reactifswitch三元表达式&&, return nullreturn false表示不渲染
function UserGreeting(props) {
  return <h1>Welcome back!</h1>
}

function GuestGreeting(props) {
  return <h1>Please sign up.</h1>
}

function Greeting(props) {
  const isLoggedIn = props.isLoggedIn
  if (isLoggedIn) {
    return <UserGreeting />
  }
  return <GuestGreeting />
}
  • Vuev-ifv-else-ifv-elsev-showreturn ture 表示被渲染
<template>
  <div v-if="type === 'A'">A</div>
  <div v-else-if="type === 'B'">B</div>
  <div v-else-if="type === 'C'">C</div>
  <div v-else>Not A/B/C</div>

  <div v-show="isShow">show</div>
</template>

列表渲染

  • ReactmapfilterreduceforEachfor infor of, 一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key
<ul>
  {props.posts.map((post) => (
    <li key={post.id}>{post.title}</li>
  ))}
</ul>
  • Vuev-forkey 是为 Vue 中的虚拟 DOM 提供一个唯一标识,以便它能够跟踪每个节点的身份,从而重用和重新排序现有元素
<ul>
  <li v-for="item in items" :key="item.id"></li>
</ul>

组件嵌套

  • React:默认插槽
<div className={'FancyBorder FancyBorder-' + props.color}>{props.children}</div>
  • React: 具名插槽
<div className="SplitPane">
  <div className="SplitPane-left">
    {props.left}
  </div>
  <div className="SplitPane-right">
    {props.right}
  </div>
</div>

<SplitPane left={<Contacts />} right={<Chat />} />

  • Vue:默认插槽
<main>
  <slot></slot>
</main>
  • Vue:具名插槽
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<!-- 使用 -->
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>

</base-layout>

获取 DOM

  • Reactref
class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }
  render() {
    return <div ref={this.myRef} />
  }
}
  • Vueref
<template>
  <div ref="myRef"></div>
</template>

<script>
export default {
  mounted() {
    console.log(this.$refs.myRef.offsetWidth)
  },
}
</script>

项目文档结构

  • ReactUmi 为例
├── config                   # umi 配置,包含路由,构建等配置
│   ├── config.ts            # 项目配置.umirc.ts优先级更高,使用此方式需要删除.umirc.ts
│   ├── routes.ts            # 路由配置
│   ├── defaultSettings.ts   # 系统配置
│   └── proxy.ts             # 代理配置
├── mock                     # 此目录下所有 js 和 ts 文件会被解析为 mock 文件
├── public                   # 此目录下所有文件会被copy 到输出路径,配置了hash也不会加
├── src
│   ├── assets               # 本地静态资源
│   ├── components           # 业务通用组件
│   ├── e2e                  # 集成测试用例
│   ├── layouts              # 约定式路由时的全局布局文件
│   ├── models               # 全局 dva model
│   ├── pages                # 所有路由组件存放在这里
│   │   └── document.ejs     # 约定如果这个文件存在,会作为默认模板
│   ├── services             # 后台接口服务
│   ├── utils                # 工具库
│   ├── locales              # 国际化资源
│   ├── global.less          # 全局样式
│   ├── global.ts            # 全局 JS
│   └── app.ts               # 运行时配置文件,比如修改路由、修改 render 方法等
├── README.md
└── package.json
  • VueVue-cli 为例

├── mock                       # 项目mock 模拟数据
├── public                     # 静态资源
│   └── index.html             # html模板
├── src                        # 源代码
│   ├── api                    # 所有请求
│   ├── assets                 # 主题 字体等静态资源
│   ├── components             # 全局公用组件
│   ├── directive              # 全局指令
│   ├── filters                # 全局 filter
│   ├── layout                 # 全局 layout
│   ├── router                 # 路由
│   ├── store                  # 全局 store管理
│   ├── utils                  # 全局公用方法
│   ├── views                  # views 所有页面
│   ├── App.vue                # 入口页面
│   └── main.js                # 入口文件 加载组件 初始化等
├── tests                      # 测试
├── vue.config.js              # vue-cli 配置如代理,压缩图片
└── package.json               # package.json

路由

  • Reactreact-router-dom

  • 动态路由 & 路由传参

    • history.push(/list?id=${id})
    • history.push({pathname: '/list', query: {id}})
    • history.push(/list/id=${id})
    • history.push({pathname: '/list', params: {id}})

    获取 props.match.query / props.match.params

  • Vuevue-router

  • 动态路由 & 路由传参

    • this.$router.push({path: '/list', query: {id}})
    • this.$router.push({path: '/list', params: {id}})

    获取 this.$route.query / this.$route.params

嵌套路由

  • Reactreact-router-dom
{
  path: '/',
  component: '@/layouts/index',
  routes: [
    { path: '/list', component: 'list' },
    { path: '/admin', component: 'admin' },
  ],
}

<div style={{ padding: 20 }}>{ props.children }</div>

使用 props.children 渲染子路由

  • Vuevue-router
{
  path: '/',
  component: '@/layouts/index',
  children: [
    { path: '/list', component: 'list' },
    { path: '/admin', component: 'admin' },
  ],
}

<template>
  <div>
    <router-view></router-view>
  </div>
</template>

使用 vue 原生组件<router-view/>组件渲染子路由

路由跳转

  • ReactUmi 为例
;<NavLink exact to='/profile' activeClassName='selected'>
  Profile
</NavLink>

history.push(`/list?id=${id}`)
  • Vuevue-router 为例
<router-link to="/about">About</router-link>

this.$router.push({path: '/list', query: {id}})

路由守卫(登录验证,特殊路由处理)

  • ReactUmi 为例

// 路由配置
{
  path: '/login',
  component: '@/pages/login',
  wrappers: ['@/wrappers/auth'],
}

// /wrappers/auth.js

import { Redirect } from 'umi'

export default (props) => {
  const isLogin = true
  if (isLogin) {
    return <div>{props.children}</div>
  } else {
    return <Redirect to="/login" />
  }
}

  • React: react-router-dom 为例
import React from 'react'
import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom'

const isAuthenticated = true

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={(props) =>
      isAuthenticated === true ? (
        <Component {...props} />
      ) : (
        <Redirect to='/login' />
      )
    }
  />
)

const HomePage = () => {
  return (
    <div>
      <h1>Welcome to the home page!</h1>
    </div>
  )
}

const LoginPage = () => {
  return (
    <div>
      <h1>Please log in to continue.</h1>
    </div>
  )
}

const App = () => {
  return (
    <Router>
      <div>
        <PrivateRoute exact path='/' component={HomePage} />
        <Route path='/login' component={LoginPage} />
      </div>
    </Router>
  )
}

export default App

状态管理

dvavuex说明
模块namespacemodules解决应用的所有状态会集中到一个比较大的对象,store 对象可能变得相当臃肿
单一状态树statestate唯一数据源
提交状态机reducermutations用于处理同步操作,唯一可以修改 state 的地方
处理异步操作effectsaction调用提交状态机更改状态树
  • Reactdva 为例

// new model:models/products.js
export default {
  namespace: 'products',
  state: [],
  reducers: {
    'delete'(state, { payload: id }) {
      return state.filter(item => item.id !== id);
    },
  },
};
//connect model
export default connect(({ products }) => ({
  products,
}))(Products);

//dispatch model reduce
dispatch model reduce({
  type: 'products/delete',
  payload: id,
})

如有异步操作,如 ajax 请求,dispath model effects,然后 effects 调用 model reduce

  • Vuevuex 为例

// new module
const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})
//bind UI
<input v-model="$store.state[modelesName].name"/>
//commit module mutation
store.commit('increment')

如有异步操作,如 ajax 请求,dispath module actions,然后 actions 调用 module mutations

// 异步action
store.dispatch({
  type: 'incrementAsync',
  amount: 10,
})

参考