概述
组成项
配置项vue | 函数式vue | 函数式react | ||
数据 组件内定义 | data $ref computed template||render | reactive ref computed template||render | useState useRef useMemo,useCallback JSX | |
数据 父组件传入 | props,attr,slot provider-inject 自定义事件 | defineProps,attr,slot provider-inject 自定义事件 | props useContext | |
数据 import引入 | 配置components | 无需配置 | 无需配置 | |
向上暴露 | ref+defineExpose | forwardRef+useImperativeHandle | ||
方法 | methods | function | function | |
钩子 | 生命周期 watch $nextTick | 生命周期 watch $nextTick | 组件函数 useEffect | |
插件 | router(路由) pinia(仓库) | router(路由) pinia(仓库) | route(路由) redux(仓库) | |
Vue的ref能指向普通标签实例、组件实例 React的useRef能指向普通标签实例 React的createRef能指向普通标签实例、类组件实例 React函数式组件,不会生成实例 |
用法对比
Vue | React |
ref,reactive | useState |
模版语法:{{}},v-bind | {} |
v-bind:style="{k1:v1}" | style={{k1:v1}} |
v-bind:class="样式名" | className={样式名} |
v-model="textValue" | value={textValue} onChange=(e)=>{...} |
v-if | show?< 标签名/>:<></> show&&< 标签名/> |
v-show | style={{display:'none'}} |
v-for | obj.map((el,index)=>{ return ... }) |
watch,生命周期 | useEffect |
props=defineProps(["变量1","变量2"]) | props通过函数参数传递 |
Test组件的内容: <div> <slot name='one' /> </div> Test组件的使用 <Test> <template v-slot:'one' > .... </template> </Test> | Test组件的内容: <div> props.childen.find( child=>child.key=='one' ) </div> Test组件的使用: <Test> <Fragement key={'one'} > ... </Fragement> </Test> |
事件绑定
HTML部分 | 实际转化 | |
原生 | <div οnclick="test()" /> <div οnclick="age++" /> div.οnclick=(e)=>{} | div.οnclick=(event)=>{ test() } div.οnclick=(event)=>{ age++ } div.οnclick=(e)=>{} |
Vue | <div @click="fun1()" /> <div @click="age++" /> <div @click="fun2" /> | div.οnclick=($event)=>{ fun1() } div.οnclick=($event)=>{ age++ } div.οnclick=fun2 |
React | <div onClick={fun1} /> onClick={ fun2("hi") } /> <div onClick={(e)=>{ fun3("hi") }} /> | div.οnclick=fun1 div.οnclick=fun2("hi") div.οnclick=(e)=>{ fun3("hi") } |
uniapp | <view bindtap="test" data-age="xx" data-sex="yy" /> | function test(e){ let { sex,age }=e.target.dataset } view.bindtap=test |
模版解析
XML/JSON写法 | 虚拟DOM对象 | |
Vue | <div> <div>标题</div> <div>学校</div> <div>年级</div> </div> | { ... |
h('div',[ h('div',"标题"), h('div',"学校"), h('div',"年级") ]) | ||
React | <div age={12} sex={'男'} > | { ... |
createElement( 'div', { age: 12, sex: '男' }, createElement('p', null, '内容') |
样式处理
vue scope模块化 | css_class=>css_class[data-v-xxx] |
react module模块化 | css_class=>_css_class_xxx_xx |
组合式函数
React | 普通函数fn中,能调用useState() 组件函数中:能调用useState()、fn() 事件处理程序、定时器、异步操作中,无法使用函数API和组合式函数 |
Vue | 普通函数fn中,能调用ref() setup函数中,能调用ref()、fn() |
性能优化
- 懒加载路由组件
- diff算法
- 分页请求数据
- TreeShaking第三方库
- 防抖和节流减少事件触发频率
- 压缩打包的代码
- 缓存计算属性
- keep-alive缓存Vue的路由组件实例
Vue的父组件重新渲染
- props不变,子组件不重新渲染
- props改变,子组件重新渲染
React的父组件重新渲染
- 不用memo包裹子组件,子组件重新渲染
- 用memo包裹子组件,isEqual=false,子组件重新渲染
- 用memo包裹子组件,isEqual=true,子组件不重新渲染
memo(
Child,
(oldProps,newProps):boolean=>isEqual
)
数据注入
祖先组件 | 后代组件 | |
Vue | provide(key,value) | const value=inject(key) |
React | let Context=createContext(null) value={data} > | let data=useContext(Context) |
特殊标签
- Vue:组件标签、router-view、slot、template
- React:组件标签、Outlet、Fragment,<></>
Vue的响应式
定义
ref定义
let age=ref(0)
age.value++ //触发set派发更新
age=2 //age变为普通数字,不再具有响应式
//组件mount时,age.value指向div实例
<div ref='age'></div>
//组件mount时,items.value是一个数组,数组中有list.length个li实例
<li v-for="item in list" ref="items"></li>
reactive定义
let info=reactive({
age:12,
simpleInfo:{
age:14
}
})
info.age=22 //响应式对象修改属性值、添加属性、移除属性,触发set派发更新
info={age:22} //info变为普通对象,不再具有响应式
//age不具有响应式
//simpleInfo不具有响应式
//simpleInfo的属性仍具有响应式
let {age,simpleInfo}=info
- const name=computed(()=>{}):name是ref对象
- pinia仓库数据:具有响应式
- router和route上的数据:具有响应式
watch监听
定义
watch([age,sex],(newVal,oldVal)=>{
....
},{immediate:true,deep:true})
immediate:true ===> setup中watch监听就会执行一次
依赖收集
响应式数据age、sex收集了watch的回调函数
派发更新
- 修改响应式数据age、sex
- 把age、sex收集的watch回调,放入异步队列
- 运行异步队列,调用watch回调
数据源
watch(数据源,回调函数,配置项)
- ref对象
- 响应式对象:默认开启深度监听
- getter函数:深度监听需要手动开启;函数的返回值可以是响应式对象或响应式对象属性
页面渲染函数
依赖收集
- 页面中,使用到了响应式数据age、sex
- 页面渲染时,读取响应式数据age、sex的值
- 响应式数据sex、age收集到渲染函数
派发更新
- 修改响应式数据age、sex
- 把age、sex收集的渲染函数,放入异步队列
- 运行异步队列,调用渲染函数,重新渲染页面
注意点
- 页面渲染时,会读取需要的响应式数据和非响应式数据
- 页面渲染,组件为单位;父组件重新渲染&&props改变==>子组件也会重新渲染
- 异步队列中,只会执行一次页面渲染函数
- 渲染函数==beforeUpdate+页面渲染+updated
生命周期
配置项出现时间
el | data-method | props-$attrs | $ref | |
beforeCreated | 无 | 无 | 有 已接收props 已接收attrs | 无 |
created | 无 | 有 已完成数据响应式 | 有 | 无 |
beforeMount | 无 | 有 | 有 已编译模版 已创建虚拟DOM | 无 |
mounted | 有 已确定挂载点 | 有 | 有 | 有 已替换模版的{{xx}}占位符 已创建真实DOM 已挂载组件 |
父子组件生命周期执行顺序
挂载阶段 | (父beforeCreated、父created) || (父setup)、父beforeMounted、 (子beforeCreated、子created) || (父setup)、子beforeMounted、子mouted、 父mounted |
更新阶段 | 父beforeUpdate、 子beforeUpdate、子updated、 父updated |
解绑阶段 | 父beforeCreated、 子beforeCreated、子destoryed、 父destoryed |
计算属性
定义计算属性
const name=computed(()=>{
return first_name+second_name
})
回调中响应式数据的处理
- 修改响应式数据first_name、second_name,同步把计算属性name标记为"脏"
name的处理
- 读取响应式数据name
- 如果标记不是"脏",返回缓存的值
- 如果标记是"脏",同步运行计算属性回调,返回最新的值
可写计算属性
const name = computed({
get() {
return firstName.value + ' ' + lastName.value
},
set(newValue) {
[first_name.value, last_name.value] = newValue.split(' ')
}
})
React的响应式
四种数据
- useState数据
- useRef数据
- 函数组件中的非响应式数据
- 函数组件外的非响应式数据
useState
手写
let init=true
let isPutQueueMicrotask=false
let cacheValue
function useState(initialValue){
//数据初始化
if(init){
if(initialValue typeOf "function"){
cacheValue=initialValue()
}else{
cacheValue=initialValue
}
init=false
}
//更新函数
function updateValue(newVal){
if(newVal typeOf "function"){
cacheValue=newVal(cacheValue)
}else{
cacheValue=newVal
}
//把组件函数放入异步队列
queueMicrotask(组件函数)
isPutQueueMicrotask=true
}
//返回数据
return [cacheValue,updateValue]
}
useState是浅层次的
使用
useState初始化数据
let {age,setAge}=useState(initialValue)
let {age,setAge}=useState(()=>{return initialValue})
useState更新数据
setAge(age+1)
setAge((age)=>age+1)
加深理解
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
console.log('APP渲染',count)
function clickMe(){
new Promise((res,rej)=>{
res('promise1')
}).then(res=>{
console.log(res)
})
setCount(count+1)
new Promise((res,rej)=>{
res('promise2')
}).then(res=>{
console.log(res)
})
setTimeout(()=>{
console.log('setTimeout')
},0)
}
return (
<button onClick={clickMe}>点我</button>
)
}
export default App
点击按钮后,打印结果:
promise1
APP渲染 2
promise2
setTimeout
从运行结果上得出:渲染函数放到了一个微任务队列中
useEffect
挂载阶段
- 组件函数
- 组件挂载、DOM更新
- useRef指向对应DOM元素
- useLayoutEffect回调
- 页面绘制
- useEffect回调
更新阶段
- 组件函数
- 组件挂载、DOM更新
- useRef指向对应DOM元素
- useLayoutEffect副作用
- useLayoutEffect回调
- 页面绘制
- useEffect副作用
- useEffect回调
解绑阶段
- useEffect副作用
手写
let init=true
const effectFnArr=[]
const cleanUpArr=[]
let oldArr
function useEffect(setUp,newArr){
//初始化
if(init){
//异步调用监听函数,收集副作用函数和监听数组
effectFnArr.push(()=>{
cleanUpArr.push(setUp())
oldArr=newArr
init=false
})
return
}
if(oldArr===undefined){
//没写监听数组,直接把监听函数放入异步队列
effectFnArr.push(()=>{
//调用监听函数,收集新的副作用函数
cleanUpArr.push(setUp())
})
}else{
for(let i=0;i<oldArr.length;i++){
if(oldArr[i]!=newArr[i]){
//监听的数据被修改,把监听函数放入异步队列
effectFnArr.push(()=>{
//调用监听函数,收集新的副作用函数
cleanUpArr.push(setUp())
})
break
}
}
}
//更新监听数组
oldArr=newArr
}
useEffect的监听是浅层次的
使用
useEffect(function setUp() {
console.log(age,name)
return function cleanUp() => {
console.log(age,name)
};
}, [age, name]);
组件函数初次运行
- 运行useEffect的setUp函数,接收cleanUp函数
监听数据更新 || 没有配置数组
- 运行收集的cleanUp
- 运行setUp,接收新的cleanUp
组件卸载
- 运行cleanUp
useRef
手写
let cacheValue={current:undefined}
let init=true
function useRef(initialValue){
if(init){
cacheValue.current=initialValue
init=false
}
return cacheValue
}
使用
const num=useRef(0)
num.current++ //定义缓存数据
const btnRef=useRef(0)
btnRef.current++ //组件渲染前,btnRef.current依旧是数字
<button ref={btnRef}/> //组件渲染时,btnRef.current会指向button实例
const btnRef=[useRef(),useRef(),useRef()]
<button ref={btnRef[0]}/> //组件渲染时,btnRef[0].current会指向button1实例
<button ref={btnRef[1]}/> //组件渲染时,btnRef[1].current会指向button2实例
<button ref={btnRef[2]}/> //组件渲染时,btnRef[2].current会指向button3实例
useCallback
手写
let init=true
let oldArr
let cacheFun
function useCallback(newFun,newArr){
if(init){
cacheFun=newFun
oldArr=newArr
init=false
}
//没写监听数组||监听数据改变,cacheFun指向新函数
if(oldArr==undefined){
cacheFun=newFun
}else{
for(let i=0;i<oldArr.length;i++){
if(oldArr[i]!=newArr[i]){
cacheFun=newFun
break
}
}
}
return cacheFun
}
使用
- const fun=useCallback(()=>{},[age,sex])
- 监听数组的值没变化:fun的指向不变
- 没写监听数组||监听数组的值变化:fun的指向改变
useMemo
手写
let cacheValue
let oldArr
let init=true
function useMemo(getValue,newArr){
if(init){
oldArr=newArr
cacheValue=getValue()
}
if(newArr==undifined){
cacheValue=getValue()
}else{
for(let i=0;i<oldArr.length;i++){
if(oldArr[i]!=newArr[i]){
cacheValue=getValue()
break
}
}
}
return cacheValue
}
使用
- const num=useMemo(()=>{},[age,sex])
- 监听数组的值没变化,使用缓存数据
- 没写监听数组||监听数组的值变化,重新计算新的值
useId
let cache
let init=true
function useId(){
if(init){
init=false
//num∈[0,1,2,3.....]
cache=`:r${num}:`
}
return cache
}
虚拟DOM
elementType
- class xxx:类组件
- div:普通标签
- function xxx:函数组件
memoizeState
- 类组件存放:state数据
- 函数组件存放:useXxx的缓存数据
- 普通标签存放:null
memoizedProps、pendingProps
- 类组件存放:组件标签属性
- 函数组件存放:组件标签属性
- 普通标签存放:普通标签属性,children子标签
Other
- child:首个子标签
- sibling:下一个兄弟标签
- return:父标签
- ref:指向标签实例的ref属性
父子传值
概述
- React的props <=> Vue的props+slot+attrs+style+class
- props传递的数据不建议修改/不能修改
- props对象的作用域<=>父组件的作用域
Vue广义的props
- props:主动接收的组件标签上的属性
- attr:未主动接收的组件标签上的属性
- class、style:样式属性;分配给子组件的独生子标签
- slot:组件内的children
样式属性处理
- 路由组件标签直接丢弃children,样式属性放到attr上
- 建议组件标签书写的样式:position、width、height;
- 建议独生子标签书写的样式:display、text-align、其他
React的props
- 组件标签上的属性
- 组件内的children
父组件传递fn函数
- 父组件定义传递函数fn,fn的作用域是父组件的作用域
- 子组件调用props.fn,并传递参数,可以实现子向父传值
父组件传递空对象obj
- 父组件传递空对象obj
- 子组件向父组件上放入fn函数,即obj.fn=fn
- fn由子组件定义,fn的作用域是子组件的作用域
- 父组件可以调用obj.fn,运行子组件定义的函数fn
Vue插槽
- 父组件提供插槽内容,子组件提供插槽出口
- 模版内容和插槽出口依靠插槽名称对应起来
- 插槽内容的变量作用域是父组件的变量作用域
- 子组件能向父组件提供变量,父组件能接收变量并使用
Child组件
<div>
<header>
//name属性确定插槽名称是header;content属性是向父组件传递的值
<slot name="header" content="headercontent"><slot>
</header>
<main>
//没有name属性,是默认插槽;content属性是向父组件传递的值
<slot content="maincontent"><slot>
</main>
<footer>
//name属性确定插槽名称是footer,content属性是向父组件传递的值
<slot name="footer" content="footercontent"><slot>
</footer>
</div>
Parent组件
<div>
<Child>
//v-slot:header 简写 #header;接收子组件的content属性
<template v-slot:header="{content}">
header-{{content}}
</template>
//如果不用接收子组件传递的值,默认插槽可以不用template包裹
<template v-slot:default="{content}">
main-{{content}}
</template>
//接收子组件的content属性
<template v-slot:footer="{content}">
footer-{{content}}
</template>
</Child>
</div>
React插槽
JSX解析组件
<Child age={12},sex={'男'}>
<Fragment key={'插槽111'}>
<h3>插槽111</h3>
</Fragment>
<Fragment key={'插槽222'}>
<h3>插槽222</h3>
</Fragment>
<Fragment key={'插槽333'}>
<h3>插槽333</h3>
</Fragment>
</Child>
====>
{
type:'Child',
props:{ //这个props将传给Child组件函数
age:12,
sex:'男',
children:[
{type:'Fragement',props:{key:'插槽111',children:[...]}},
{type:'Fragement',props:{key:'插槽222',children:[...]}},
{type:'Fragement',props:{key:'插槽333',children:[...]}}
]
}
}
App.tsx
import { Fragment } from 'react'
import Child from '@/components/Child'
function App() {
return (
<>
<h3>父组件</h3>
<Child>
<Fragment key={'插槽111'}>
<h3>插槽111</h3>
</Fragment>
<Fragment key={'插槽222'}>
<h3>插槽222</h3>
</Fragment>
<Fragment key={'插槽333'}>
<h3>插槽333</h3>
</Fragment>
</Child>
<h3>父组件</h3>
</>
)
}
export default App
Child.tsx
import React from 'react';
export default function(props) {
const {children}=props
return (
<>
<h3>子组件</h3>
{children.find((value)=>{
return value.key=="插槽333"
})}
{children.find((value)=>{
return value.key=="插槽222"
})}
{children.find((value)=>{
return value.key=="插槽111"
})}
<h3>子组件</h3>
</>
);
}
Vue的更强封装
全局API
app.use
- app.use(obj,args)会调用参数obj的install(app,args)方法
- install的参数app:app实例
- install的参数args:app.use方法的args
app.use=function(obj,args){
...
obj.install(app,args)
...
}
strengthObj={
install:function(app,args){
.....
}
}
app=createApp({})
app.use(strengthObj,args)
Other
- app.component
- app.directive
- app.provide
- app.mount
- app. unmount
指令
自定义指令
标签的生命周期
<p v-my-directive:参数名.修饰符1.修饰符2="属性名"></p>
let 属性名="属性值"
const vMyDirective={
created(el,binding,vnode,prevVnode){},
beforeMount(el,binding,vnode,prevVnode){},
mounted(el,binding,vnode,prevVnode){},
beforeUpdate(el,binding,vnode,prevVnode){},
updated(el,binding,vnode,prevVnode){},
beforeUnmount(el,binding,vnode,prevVnode){},
unmounted(el,binding,vnode,prevVnode){},
}
el:p标签指向的真实DOM;created也能访问p标签指向的真实DOM
binding:{
arg:"参数名",
modifiers:{修饰符1:true,修饰符2:true},
value:"属性值", //value=属性名="属性名"
oldValue:undefined
...
}
内置指令
v-for、v-if、v-show
- vue3.2.47中,同一标签上优先级,v-if>v-for>v-show
- v-if:销毁真实DOM,保留虚拟DOM
- v-show:隐藏真实DOM
v-for和v-if在同一标签上
- v-if先执行,导致无法使用v-for的数据
- v-if=false,标签销毁,v-for不会再执行
- v-if=true,v-for创建多个标签,但是每个标签上的v-if都等于true
动态class
动画
<video class="video_screen" :class="{ fold: fold }" controls src="#"></video>
const fold = ref(false)
const openComment = () => {
fold.value = true
}
const closeCommentDrawer = () => {
fold.value = false
}
.video_screen {
height: 100%;
width: 100%;
border-radius: 10px;
transition-duration:2s;
&.fold {
width: calc(100% - 300px);
transition-duration:2s;
}
}
暗黑模式+背景图切换
const {dark} =ref(dark)
<div :class="{dark:dark}">
......
<el-switch v-model="dark" />
......
</div>
<div class="header-banner"
:style="{background:dark?
'url(src/assets/moon.jpg)':
'url(src/assets/sun.jpg)'}"
>
</div>
.dark{
background-color: black;
}
路由
何为路由
- v-show、v-if:实现页面切换最简单的方式
- 路由:在v-show、v-if之上,进行了更好的封装
<script setup>
import { ref, computed } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import NotFound from './NotFound.vue'
const routes = {
'/': Home,
'/about': About
}
const currentPath = ref(window.location.hash)
window.addEventListener('hashchange', () => {
currentPath.value = window.location.hash
})
const currentView = computed(() => {
return routes[currentPath.value.slice(1) || '/'] || NotFound
})
</script>
<template>
<a href="#/">Home</a> |
<a href="#/about">About</a> |
<a href="#/non-existent-path">Broken Link</a>
<component :is="currentView" />
</template>
路由使用
概述
- 创建routes路由表,路由组件放入路由表
- 创建router对象,routes放入router
- router放入根组件
- 设置路由出口
Vue使用路由
创建routes路由表 routes路由表添加组件 | const routes=[ //path:''表示此子路由是默认子路由 //path:'yy'<=>path:'/xx/yy' path:'/:pathMatch(.*)*', redirect:'/404' } |
创建router对象 确定router类型 | const router=createRouter({ routes, history:xxx }) |
把router注册到app 设置一级出口 | import { RouterView } from 'vue-router'; import { createApp } from 'vue'; import router from './router'; const app=createApp(RouterView) app.use(router) app.mount('#app') |
设置二级出口 | <RouterView /> |
React使用路由
创建routes路由表 routes路由表添加组件 | let routes=[ //path:''表示此子路由是默认子路由 //path:'yy'<=>path:'/xx/yy' path:'/xx', element:<xxx />, children:[...] }, path:'/*', element:<Navigate to='/xx' /> } |
创建router对象 确定router类型 | let router=createBrowserRouter(routes) |
router放入根组件 设置一级出口 | ReactDOM .createRoot(document .getElementById("root")) .render( |
设置二级出口 | {Outlet} |
ReactDOM.createRoot(document.getElementById("root")).render(
//确定router类型、router注入根组件
<BrowserRouter>
//确定路由出口、路由组件放入route、route放入router
<Routes>
<Route path="/home" element={<Home/>}></Route>
<Route path="/login" element={<Login/>}></Route>
<Route path="/*" element={<Navigate to="/home" />}></Route>
</Routes>
</BrowserRouter>
);
let element=useRoutes([
{
path:'/xx',
Component:xxx,
children:[
{path:'',Component:xxx}, //path:''表示此子路由是默认子路由
{path:'yy',Component:xxx}, //path:'yy'<==>path:'/xx/yy'
]
},
{path:'/xx',element:<xxx />,children:[...]},
{path:'/*',element:<Navigate to='/xx' />},
])
ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter>
{element}
</BrowserRouter>
);
简单对比
vue | react | |
标签导航 | <router-link to="/" /> | <Link to='/sing' /> |
编程导航 | const $router=useRouter() $router.push('') | const navigate=useNavigate() navigate('') |
出口 | <router-view /> | <RouterProvider router={router}/> {Outlet} |
传参方式 | params query | params search(类似query) state:对应window.history.state |
标签传参 | <router-link to='/sing/a/b' /> <router-link to='/dance?a1=a&a2=b' /> | <Link to='/sing/a/b' /> to='/dance?a1=a&a2=b' /> to='/rap' state={{a1:'a',a2:'b'}} /> |
<router-link :to="{ path/name:"...", query:{a1:'',a2:''} }" /> <router-link :to="{ name:'', params:{a1:'',a2:''} }" /> | ||
编程传参 | $router.push{ path:''(或name:''), query:{a1:'',a2:''} } $router.push{ name:'', params:{a1:'',a2:''} } | navigate('/sing/a/b') navigate('/dance?a1=a&a2=b') navigate('/rap',{state:{a1:'a',a2:'b'}}) |
接参方式 | $route.params $route.query | let {name,song}=useParams() let [search,setSeach]=useSearchParams() let name=search.get('name') let song=search.get('song') let {name,song}=useLocation().state |
懒加载 | { path:'xxx', component()=>import('xxx') } | { path:'xxx', async lazy(){ let Home = await import("xxx") return { Component: Home.default }; } } |
xxx.tsx文件 const Home=lazy(()=>import('xxx')) { path:'xxx', element:<Home /> } | ||
xxx.tsx文件 { path:'xxx', .Component:lazy(()=>import('xxx')) } | ||
匹配顺序 | 从前向后 通配路由建议放到最后 通配路由放到最后和最前效果相同 | 从前向后 通配路由建议放到最后 |
动态路由 | addRoute、addRoutes |
路由鉴权
Vue 内置鉴权
全局前置
router.beforeEach((to,from,next)=>{...})
全局后置
router.afterEach((to,from)=>{...})
独享前置
beforeEnter((to,from,next)=>{...})
next()
next(false)
next('xxx')
next({path:'xxx',query:'xxx'})
React 高阶组件(Hoc)实现鉴权
import React from 'react';
import { Navigate } from "react-router"
const withAuth = (WrappedComponent) => {
return function(props) {
const isLoggedIn = checkUserAuth(); // 检查用户是否已登录,可根据实际需求实现
if (!isLoggedIn) return <Navigate to="/home" />;
return <WrappedComponent {...props} />;
};
};
// 使用withAuth高阶组件包裹需要进行鉴权的路由组件
const AuthHome = withAuth(Home);
多级路由
- 一级路由占有部分可支配页面,其余分给二级路由。建议写法:/xxx
- 二级路由占有部分可支配页面,其余分给三级路由。建议写法:/xxx/xxx
- 三级路由占有部分可支配页面,其余分给四级路由。建议写法:/xxx/xxx/xxx
- 一级路由的可支配页面是整个浏览器的页面
- /yy也可以是/xx的子路由,但是这种写法可读性极差,建议把/yy写成/xx/yy。
//标准写法
[
{
//通过/xx访问
path:"/xx"
children:[
//通过/xx/yy访问
{path:"/xx/yy"},
//通过/xx/zz访问
{path:"/xx/zz"}
]
}
]
//简略写法
[
{
path:"/xx"
children:[
{path:"yy"}, //等价于{path:"/xx/yy"}
{path:"zz"}, //等价于{path:"/xx/zz"}
]
}
]
//不建议写法
[
{
path:"/xx"
children:[
//通过/yy访问
{path:"/yy"},
//通过/zz访问
{path:"/zz"},
]
}
]
目录结构
vue | react |
src assets utils api index.ts request.ts store index.ts store1.ts store2.ts router index.ts routes.ts pages | src assets utils request.ts store.ts router index.ts routes.ts pages services.ts model.ts PageB services.ts model.ts components ComA index.jsx |
一级路由可以提取到src目录下 react子组件函数、父组件函数,能写在同一文件下 |
路由模式
- http://ip:port/pathname/?key=value#/123
- http://ip:port是域名
- pathname是前端向后端请求的资源,
- ?key=value携带query参数信息
- #携带其他信息
hash模式 | 前端路由信息写在#后面 前端路由信息更改,不会向后端请求新的资源 |
history模式 | 前端路由信息直接写在域名后面 前端路由信息更改,会向后端请求新的资源 这时后端需要加工处理 |
内存模式 | 不再使用浏览器导航栏的url 框架维护一个内置的路由表 |
仓库
自定义仓库
创建仓库
//store.js
export const miniStore={
age:10,
sex:"男",
name:"Tom"
}
export const store={
state:{……},
getters:{……},
actions:{……},
reducers:{……},
modules:{
module1:{},
module2:{},
……
}
}
使用仓库
//组件.js
import {miniStore,store} from 'store.js'
实现响应式
- Vue,用proxy或defineProperty处理仓库数据,以依赖收集和派发更新的方式更新组件
- React,给仓库数据注册event监听事件,数据变化就全局重新渲染
数据注入
在根组件上,把仓库数据传递进去
Redux
概述
function todoApp(state=initialState,action){}
//初始化仓库,第一次调用todoApp
let action=null
let cacheState=undefined
cacheState=todoApp(cacheState,action)
//更新仓库数据,第二次及以上调用todoApp
action=接收的action
cacheState=todoApp(cacheState,action)
action的作用
- reducer是纯函数,负责直接修改数据
- action可以是副作用函数,负责数据修改前的逻辑
同步使用&对象
- const actionSynch={type:"",payload:{...}}
- dispatch(actionSynch)
- 同步修改仓库数据
异步使用&函数
- function actionAsync(dispatch){...}
- dispatch(actionAsync)
- 异步修改仓库数据
原生创建仓库
-------------大仓库管理一个小仓库-----------------
function todoApp(state = initialState, action) {
switch (action.type) {
case xxx:
...
return xxx
case xxx:
...
return xxx
default:
return state
}
}
let store = createStore(todoApp,applyMiddleware(thunk))
-------------大仓库管理多个小仓库-----------------
function todoHome(state=initialState1,action) {
switch (action.type) {
case xxx:
...
return xxx
case xxx:
...
return xxx
default:
return state
}
}
function todoLogin(state=initialState2,action) {
switch (action.type) {
case xxx:
...
return xxx
case xxx:
...
return xxx
default:
return state
}
}
function todoApp(state={},action) {
return {
todoHome:todoHome(state.todoHome,action),
todoLogin:todoLogin(state.todoLogin,action)
}
}
//redux需要注册中间件redux-thunk,才支持异步action
const store = createStore(todoApp,applyMiddleware(thunk))
store.getStore():获取仓库数据
store.dispatch(action):更新仓库数据
const unsubscribe=store.subscribe(func):仓库数据更新,运行监听函数func
unsubscribe:取消函数func的监听功能
原生使用redux仓库
- react组件中注册redux的subscribe监听
- react组件更新redux中的数据
- redux中的数据改变,触发subscribe监听函数
- subscribe监听函数调用useState("")
- react组件重新渲染
@reduxjs/toolkit创建仓库
import { createSlice,configureStore } from '@reduxjs/toolkit'
//@reduxjs/toolkit,默认注册了redux-thunk
--------------------草稿----------------------
const counterSlice = createSlice({
name: 'counter', //name属性对应action对象的前缀
initialState: 0,
reducers: {
increment(state){
state+= 1
},
incrementByAmount(state, action){
state+= action.payload
}
}
})
const nameSlice = createSlice({
name: 'name',
initialState: 'Tom'
})
-----------------生成actions----------------------
export const { increment,incrementByAmount,xxx } = counterSlice.actions
function xxx(payload){
return {
type:counterSlice.name+"/"+xxx.name,
payload
}
}
-----------------生成reducer----------------------
const couter=counterSlice.reducer
const name=nameSlice.reducer
-----------------生成大仓库----------------------
const store=configureStore({
reducer: {
//counter、name属性是useSelector获取属性时用的到
counter,
name
},
middleware(getDefaultMiddleware){
return getDefaultMiddleware({
serializableCheck:false
})
}
})
react-redux使用仓库
//react-redux的Provider组件,把仓库store注入最外层组件App
import { useSelector, useDispatch, Provider } from 'react-redux'
<Provider store={store}>
<App>...</App>
</Provider>
const dispatch=useDispatch()
dispatch(xxx(payload))
const value=useSelector((state)=>{ //state={counter:0,name:'Tom'}
return state.xxx.yyy...
})
Pinia
创建仓库
function defineStore(仓库名,配置项){
return function(大仓库=app?.pinia)
}
--------------小仓库的创建----------------------
const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
}
}
})
--------------大仓库的创建----------------------
const pinia=createPinia()
-----全局注册&&使用小仓库&&小仓库注入大仓库-------
app.use(pinia)
const counterStore=useCounterStore()
--------使用小仓库&&小仓库注入大仓库-------
const counterStore=useCounterStore(pinia)
使用仓库数据
const counter = useCounterStore()
counter.count++
counter.$patch({ count: counter.count + 1 })
counter.increment()
仓库数据解构
- xxxStore.yyy=zzz
- {yyy}=storeToRefs(xxxStore)&&yyy=zzz
- {yyy}=xxxStore&&yyy=zzz
-
1、2、3都可以修改仓库属性;1、2是响应式的,3不是
Umi
目录结构
├── package.json
├── .umirc.ts
├── .env
├── dist
├── mock //约定mock文件夹下,所有文件都当成mock文件解析
├── public
└── src
├── .umi
├── layouts/index.tsx
├── pages
├── index.less
├── index.tsx
└── document.ejs //约定默认模板
├── app.ts //约定运行时配置文件
└── global.less //约定全局样式文件
样式
- umi3默认支持less
- import style from 'xxx.less':识别出CSS Modules文件
- import 'xxx.less':识别成普通css文件
图片
- <img src={require("./xxx.png")} />:相对路径引入
- <img src={require("@/xxx.png")} />:使用@代指src
- import logo from './logo.svg';<img src={logo} />:url式引入
- import {ReactComponent as Logo} from './logo.svg';<Logo />:组件式引入
- .logo{ background : url(./xxx.png) }:相对路径引入
- .logo{ background : url(~@/xxx.png)}:使用~@代指src
前端路由
routes配置 | .umirc.ts文件 export default defineConfig({ .... routes:[ {path:/xx,component:"@/pages/xx" exact:true} { path:/yy,component:"@/pages/yy",exact:false routes:[ {path:/yy/zz,component:"@/pages/yy/pages/zz",exact:true}, {path:/yy/gg,component:"@/pages/yy/pages/gg",exact:true} ] } ] .... }) |
编程式导航 | history.push('/url?k1=v1&k2=v2') history.push({ pathname: 'url', query: {k1: v1,k2:v2}, }) |
组件式导航 | <Link to="/url"></Link> |
history对象 | 获取方式一:props.history 获取方式二:import { history } from "umi" |
history跳转方法:push、replace、go、goBack、goForward history属性: location对象:包含pathname、search、query、hash、state属性 action:当前路由的action,PUSH/REPLACE/POP三种 length:history栈的实体数 | |
props对象 | props children history对象 location对象 route当前路由信息 routes所有路由信息 match staticContext |
路由懒加载dynamic
dynamic({
loader:()=>import("/xxx"), //按需加载的模块
loading:loadingComponent //加载模块时,显示的组件
delay:2000 //模块2s后开始加载
timeout:5000, //模块开始加载后,5s内没能完成加载,则加载失败
error:errorComponent //加载模块失败后,显示的组件
})
网路请求
request请求API
request('url', {
method: "xxx",
params: {...},
data:{...},
headers:{...},
timeout:5000,
credentials:"omit/include"
mode:"cors/no-cors"
})
request全局配置
//采用@umijs/plugin-request
//配置文件src/app.tsx
import { RequestConfig } from 'umi';
export const request: RequestConfig = {
timeout: 5000, //超时时间
errorConfig: {},
middlewares: [],
requestInterceptors: [], //请求拦截器
responseInterceptors: [], //响应拦截器
};
mockjs拦截请求和模拟数据
import mockjs from 'mockjs' //可以使用mockjs模拟数据
export default {
//请求方法,请求url,响应数据
'GET /api/users': { users: [1, 2] },
//请求方法默认是GET
'/api/users/1': { id: 1 },
//请求方法,请求url,响应函数(同express)
'POST /api/users/create': (req, res) => {
res.end('ok');
},
}
dva仓库
小仓库(model)位置
- 约定src/models下,xxx.ts文件
- 约定src/pages下,models/xxx.ts文件
- 约定src/pages下,model.ts 文件
仓库属性
- namespace:仓库名称;唯一标识
- state:仓库数据
- reducers:同步更新仓库数据
- effects:运行异步操作;
- subscriptions:dva仓库注册时,自动调用subscriptions中的函数
仓库API
- useSelector(state=>state):返回大仓库数据
- const dispatch=useDispatch();dispatch({type:'小仓库namespace/方法名',...arg})
- connect:绑定数据到组件
- getDvaApp():获取dva实例
- useStore():不知道什么用法
示例
import React from "react";
import {useSelector,useDispatch} from 'umi'
export default function PageLayout() {
const list=useSelector(state=>state.list)
const dispatch=useDispatch()
function addAgeFn(){
dispatch({type:"list/addAge"}) //需要加仓库名前缀
}
function asyncAddAgeFn(){
dispatch({type:"list/asyncAddAge"})
}
return (
<>
<button onClick={addAgeFn}>同步增加list仓库年龄{list.age}</button>
<button onClick={asyncAddAgeFn}>同步增加list仓库年龄{list.age}</button>
</>
);
}
export default {
namespace: 'list',
state: {age: 11},
reducers: {
addAge(state, action) {
return {
...state,
age: ++state.age
}
}
},
effects: {
*asyncAddAge(_, { call, put }) {
console.log("计时开始")
yield call(delay, 1000)
console.log("计时结束")
yield put({ type: 'addAge' }) //不需要加仓库名前缀
},
},
subscriptions: {
subscription1({ dispatch, history }) {
dispatch({ type: 'addAge' }) //不需要加仓库名前缀
},
subscription2({ dispatch, history }) {
dispatch({ type: 'addAge' }) //不需要加仓库名前缀
},
}
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
useModel仓库
仓库创建
约定src/models下,xxx.ts文件
import { useState,useCallback } from "react"
export default function useAuthModel() {
let [user,updateUser] = useState("")
const setUser = (newUser) => {
updateUser(newUser)
}
const clearUser = () => {
updateUser("")
}
return {user,setUser,clearUser}
}
仓库的使用
import { useModel } from "umi";
export default function Home(props) {
let useAuthModel=useModel(
//仓库名称,即仓库文件名称
'xxx',
//参数可选,默认返回xxx仓库所有数据;也可以主动设置仓库数据
model=>({user:model.user,setUser:model.setUser})
)
function showModel(){
console.log(useAuthModel)
}
function changeModel(){
useAuthModel.setUser("Home")
}
return (
<>
<button onClick={showModel}>showModel</button>
<button onClick={changeModel}>changeModel</button>
</>
);
}
useModal仓库手写
import { useState } from "react"
const store = {}
function useModel(modalName) {
return store[modalName]
}
function modal1() {
const [info, setInfo] = useState({
one: 'one',
two: 'two',
three: 'three'
})
return {
...info,
setInfo
}
}
function modal2() {
const [info, setInfo] = useState({
a: 'a',
b: 'b',
c: 'c'
})
return {
...info,
setInfo
}
}
export default function InjectApp() {
store[modal1.name] = modal1()
store[modal2.name] = modal2()
return (
<>
<div>app</div>
<Child1 />
<Child2 />
</>
)
}
function Child1() {
console.log('child1重新渲染')
const modal1 = useModel('modal1')
const modal2 = useModel('modal2')
return (
<>
<div onClick={()=>{
modal1.setInfo(
{'one':modal1.one+1,
'two':modal1.two+1,
'three':modal1.three+1}
)
modal2.setInfo(
{'a':modal2.a+1,
'b':modal2.b+1,
'c':modal2.c+1}
)
}}>Child1</div>
<div>{modal1.one} {modal1.two} {modal1.three}</div>
<div>{modal2.a} {modal2.b} {modal2.c}</div>
</>
)
}
function Child2() {
console.log('child2重新渲染')
const modal1 = useModel('modal1')
const modal2 = useModel('modal2')
return (
<>
<div onClick={()=>{
modal1.setInfo(
{'one':modal1.one+2,
'two':modal1.two+2,
'three':modal1.three+2}
)
modal2.setInfo(
{'a':modal2.a+2,
'b':modal2.b+2,
'c':modal2.c+2}
)
}}>Child2</div>
<div>{modal1.one} {modal1.two} {modal1.three}</div>
<div>{modal2.a} {modal2.b} {modal2.c}</div>
</>
)
}
@umijs/plugin-initial-state
src/app.tsx
export async function getInitialState() { //生产@@initialState数据
return {name:"LiHua",age:32}
}
xxx组件
export default function xxx(){
let initData=useModel("@@initialState") //使用@@initialState数据
return (...)
}
src/app.ts默认方法
//修改 clientRender 参数
export function modifyClientRenderOpts(fn){}
//修改路由
export function patchRoutes({ routes }){}
//覆写 render
export function render(oldRender: Function){}
//在初始加载和路由切换时做一些事情
export function onRouteChange({ routes, matchedRoutes, location, action }){}
//根组件外包一个Provider;
//args包括routes、plugin、history
export function rootContainer(LastRootContainer, args){}
.umirc.ts配置文件
initUmiConfig方法
initUmiConfig({
mf: "development",
ngBuild: true,
routes: [
{path: "/", component: "@/pages/layout", routes: []}
]
}
========>
{
publicPath: './',
base: '/',
nodeModulesTransform: { type: 'none' },
ignoreMomentLocale: true,
history: { type: 'hash' },
theme: {
'@layout-body-background': '#f0f2f5',
'@body-background': '#f0f2f5',
'@layout-header-background': '#fff',
'@layout-sider-background': '#001529',
'@menu-dark-bg': '#001529',
'@tooltip-color': '#333',
'@tooltip-bg': '#fff',
'@modal-header-bg': '#FAFAFA',
'@tabs-horizontal-gutter': '16px',
'@line-height-base': '1.5',
'@checkbox-size': '14px',
'@font-size-base': '12px',
'@height-base': '28px',
'@steps-title-line-height': '32px',
'@form-label-width': '86px',
'@form-item-margin-bottom': '20px',
'@tabs-bar-margin': '0px',
'@modal-border-radius': '12px',
'@table-header-bg': '#f4f4f4',
'@border-color-split': '#ECECEC',
'@wait-icon-color': '#00000040',
'@disabled-color': '#666666',
'@disabled-bg': '#F7F8FA',
'@select-multiple-item-disabled-color': '#666666',
'@select-multiple-item-disabled-border-color': '#F7F8FA',
'@root-entry-name': 'variable'
},
antd: { disableBabelPluginImport: true },
dva: {
skipModelValidate: true,
disableModelsReExport: true,
lazyLoad: true
},
dynamicImport: { loading: '@/components/pageLoading' },
terserOptions: {},
routes: [ { path: '/', component: '@/pages/layout', routes: [] } ],
mfsu: {},
webpack5: {}
}
默认别名项目
- @:src 目录
- @@:临时目录,通常是 src/.umi目录
Other
React18新特性
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
- 开发模式("development")且使用了严格模式("Strict Mode"),函数组件会渲染两次
- 生产环境("production")模式下和原来一样,仅执行一次。
双向数据绑定
非受控
input框的输入内容,直接赋值给input的value属性
受控
- input框的输入内容,通过触发input的onchange事件
- 进而修改组件的响应式数据
- 最后赋值input的value属性
e.target属性
文本框 | value:input输入框的值 默认check:false |
多选框 | 默认value:‘on' check:选中输入框?true:false |
单选框 | 默认value:‘on' check:选中输入框?true:false name:相同的name分入一组 |
自定义选择框 | select-value:选中option的value属性值 select-selectedIndex:选择了第几个option元素(从0开始) option-value:设置的value值 |
text文本框
:value="textValue"
@input="()=>{
textValue=$event.target.value"
}"
checkbox多选框
//单一checkbox
:checked="isChecked"
@change="()=>{
isChecked=$event.target.checked
}"
//多个checkbox
<template>
<span>Tom</span><input type="checkbox" value="Tom" @change="change" :checked="isCheck(input1)" ref="input1">
<span>Jack</span><input type="checkbox" value="Jack" @change="change" :checked="isCheck(input2)" ref="input2">
<span>black</span><input type="checkbox" value="black" @change="change" :checked="isCheck(input3)" ref="input3">
<span>white</span><input type="checkbox" value="white" @change="change" :checked="isCheck(input4)" ref="input4">
</template>
<script lang="ts" setup>
import {ref} from 'vue'
let boxArr=ref([])
const change=($event)=>{
if($event.target.checked==true){
boxArr.value.push($event.target.value)
}else{
boxArr.value.splice(boxArr.value.indexOf($event.target.value),1)
}
}
let input1=ref(null)
let input2=ref(null)
let input3=ref(null)
let input4=ref(null)
const isCheck=(target)=>{
let result=boxArr.value.find(item=>{
return item==target.value
})
if(result==undefined){
return false
}else{
return true
}
}
</script>
父选框-子选框
- 父选框选中<=>子选框全部选中
- 父选框半选中<=>子选框部分选中
- 父选框未选中<=>子选框都未选中
radio单选框
<template>
<span>Tom</span>
<input type="radio" name="name" value="Tom"
@change="change" :checked="isCheck(input1)" ref="input1"
>
<span>Jack</span>
<input type="radio" name="name" value="Jack"
@change="change" :checked="isCheck(input2)" ref="input2"
>
<span>black</span>
<input type="radio" name="name" value="black"
@change="change" :checked="isCheck(input3)" ref="input3"
>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
let choice=ref('')
const change=($event)=>{
choice.value=$event.target.value
}
let input1=ref(null)
let input2=ref(null)
let input3=ref(null)
let isCheck=(target)=>{
if(target?.value==choice.value){
return true
}else{
return false
}
}
</script>
select自定义选择框
:value="choice"
@change="()=>{
choice=$event.target.value
}"