react18+vite

906 阅读23分钟

github.com/hejiyun/vit…

创建 vite-react

pnpm create vite@latest vite-react -- --template react-ts 
npm install @types/node --save-dev //node不支持ts, 所以需要安装插件
npm install npm run dev
  1. 配置 resolve.alias
// vite.config.ts 
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path';
 
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
})


// tsconfig.json 
{ 
    "compilerOptions": { 
            "paths": { 
                "@/*": ["./src/*"], 
            } 
    } 
}
  1. 去除默认的严格模式下标签StrictMode,避免开发环境下重复渲染
在react渲染组件中 , 初始化的index.tsx文件里,存在一个<React.StrictMode>标签, 这个是react的一个用来突出显示应用程序中潜在问题的工具(严格模式) 有一项检测意外的副作用,严格模式不能自动检测到你的副作用,但它可以帮助你发现它们,使它们更具确定性。通过故意重复调用以下函数来实现的该操作。 注意:这仅适用于开发模式。生产模式下生命周期不会被调用两次。 所以只需要把这个标签去掉就可以了 
// main.tsx 
import React from 'react'
import ReactDOM from 'react-dom/client' 
import App from './App' 
import './index.css' 
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 
    // <React.StrictMode> 
    <App /> 
    // </React.StrictMode>,
)

集成react-router, 添加路由缓存react-activation

路由配置与缓存

npm i react-router-dom npm install react-activation
  1. 在src目录下分别创建page 和routes文件夹, routes存放路由相关文件,并在其下创建index.tsx。 page存放页面相关文件,并在其下创建error文件夹和redirect文件夹存放路由错误时跳转页面
特别注意的是: 1. KeepAlive标签一定一定要加id 2. 要缓存的路由不能懒加载 3. 如果懒加载的话第一次点击不能渲染出组件,要缓存的路由不能懒加载 const App = lazy(() => import("../App")) //懒加载路由 import App from '../App' //非懒加载路由

// routes ----- > index.tsx 
import {useRoutes} from "react-router-dom";
import { Suspense, lazy } from 'react'
import KeepAlive from 'react-activation'
import App from '../App' 
// KeepAlive标签一定一定要加id!!! 
// 要缓存的路由不能懒加载!!! 
//如果懒加载的话第一次点击会出现空白不能渲染出组件,要缓存的路由不能懒加载!!
// const App = lazy(() => import("../App")) 
// 这里暂定为有name的需要缓存
const routes = [
  {
    name: 'fdjj',
    path: '/',
    auth:false,
    component: App
  },
  { 
    path: '*',
    auth:false,
    component:lazy(() => import('@/page/error/NotFound'))
  },
 { 
    path: '/redirect',
    auth:false,
    component: lazy(() => import('@/page/Redirect/Redirect')),
  }
]
 
// 路由处理方式
const generateRouter = (routers:any) => {
  return routers.map((item:any) => {
    if (item.children) {
      item.children = generateRouter(item.children)
    }
   
    item.element = <Suspense fallback={
      <div>加载中...</div>
    }>
      {/* 把懒加载的异步路由变成组件装载进去 */}
      {
        item.name ? ( <KeepAlive id={item.name}>
            <item.component />
        </KeepAlive>) : (<item.component />)
      }
    </Suspense>
    return item
  })
}
const Router = () => useRoutes(generateRouter(routes))

// page --->error --->NotFound.tsx 
// 当前设置后, 除了/123的路由会被引导至notfount, 其他的都会返回主页

import {useEffect} from 'react'
import {useNavigate,useLocation } from "react-router-dom";

export default function NotFound(): any {
    // 获取操作路由跳转对象
    const navigate = useNavigate()
    //获取路由对象
    const location = useLocation()
    console.log(location)
    useEffect(()=>{
        if(location.pathname == '/123'){
            // navigate('/123')  // 跳转到指定路由
        }else{
            navigate('/')  // 跳转到主页
        }
      },[])
    return (
        <div className="NotFount">
            您输入的地址栏未找到匹配的页面
        </div>
    );
}

// page ----> Redirect ----->Redirect.tsx 
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
const Redirect=(props:any)=> {
  let navigate = useNavigate();
  useEffect(() => {
    navigate("/");
  });
  return <></>;
}
 
export default Redirect
  1. 将设置好的routes逻辑引入到main.tsx, BrowserRouter和hash是两种路由模式, 区别在于是否有/#
import ReactDOM from 'react-dom/client'
import './index.css'
import {BrowserRouter} from "react-router-dom"; 
import {Router} from './routes/index' 
import { AliveScope } from 'react-activation' 
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 
// <React.StrictMode> 
    <BrowserRouter>
    <AliveScope> 
    // 添加上路由缓存 注册路由时,将需要缓存的页面使用<KeepAlive>包裹,这个组件必须设置id,否则缓存不生效。id要是唯一的,因为这个缓存时依据id实现的。 
    <Router/> </AliveScope>
    </BrowserRouter>
    // </React.StrictMode>, 
)

image.png 3. 测试当前缓存逻辑中app.tsx页面缓存, 在page目录下创建test.tsx文件,在routes-index.tsx中添加test路由, 且不做缓存

// page ----> test.tsx 
import {useState} from 'react'
import {Link} from "react-router-dom";
export default function Test(): any { 
    const [count, setCount] = useState(0)
    return ( 
        <div className="Test"> 
            <h1>
            <Link to="/">Vite + React</Link>
            </h1> 
            <button onClick={() => setCount((count) => count + 1)}> count is {count} </button> 
        </div> 
    );
} 

// routes --> index.tsx 
const routes = [
    ... 
    { 
    path: '/test',
    auth:false, 
    component:lazy(() => import('@/page/test')) },
    ... 
] 


// app.tsx 
import {Link} from 'react-router-dom'
    .... 
    return (
        ... 
        <h1>
            <Link to="/test">Vite + React</Link>
        </h1>
        ...
    )
  1. 先点击app.tsx中的count进行计数, 然后点击vite+react图标跳转到test,点击test中count进行计数, 然后点击vite+react图标跳转返回到app, 继续点击返回test, 观察是否app count数值不变, 而test被重置

image.png

image.png
5. 通过测试, 缓存功能逻辑实现。

路由传参与获取

  1. react router v6 获取传参需要用两个 hook,分别是 useParams(params)和 useSearchParams(search)
常规的传参方式有三种, params, search state
// params传参就是常见的传参方式 即 : url/:id/:name ... 
//params传参,特点在于在路由定义时需要声明是否需要传递参数, 如, 需要通过params方式给test传递一个id 

// routes --> index.tsx
const routes = [ 
    ... 
    { 
        path: '/test/:id?', 
        auth:false, 
        component:lazy(() => import('@/page/test'))
    },
    ... 
] 

//传递参数 
// app.tsx 
import { Link } from 'react-router-dom';
{/* 路由定义 /test/:id? */} 
<Link to={`/test/1`}>vite+react</Link> 

// 获取参数
// test.tsx 
import { useParams } from 'react-router-dom'
/* params */ 
const params = useParams(); 
const { id } = params;

image.png

// search传参与params传参略有区别,格式为 : url?id = **&name = ** ... 
//search传参,也不需要在路由中特别声明, 

//传递参数 
// app.tsx
import { Link } from 'react-router-dom'; 
<h1><Link to="/test?cid=3&name=6">Vite + React</Link></h1> 

// 获取参数 , search方式通过searchParams 上的get/getall方法来获取对应参数, 两者都是一个行参, 区别是一个返回数组, 一个返回字符串 
// test.tsx 
import { useParams, useSearchParams } from 'react-router-dom' 
/* search */ 
let [searchParams, setSearchParams] = useSearchParams(); 
const cid = searchParams.getAll('cid'); 
const name = searchParams.get('name');

image.png

// state传参类似与vue中的query传参方式, 参数不会在路由地址中显示 ,通过state属性进行传递,
// 特别注意的是, state传参拥有和query传参同样的弊端, 即无缓存的情况下刷新页面(刷新并清除缓存)参数丢失的问题 
// 即:<Link to="/test" state={{mid:234}}></Link> 如, 需要通过state方式给test传递一个mid 

//传递参数 
// app.tsx 
import { Link } from 'react-router-dom'; 
<h1><Link to="/test" state={{mid:234}}>Vite + React</Link></h1> 

// 获取参数 , state方式通过useLocation方式获取 
// test.tsx 
import { useParams, useSearchParams,useLocation } from 'react-router-dom' 
/* state */ 
let location = useLocation(); 
const { mid } = location.state;

image.png

  1. 编程式路由跳转 useNavigate, 对于js而言, 只是更换了js控制跳转的方法名, 其他的没有什么区别
// useHistory 已废弃,而是使用 useNavigate
import { useNavigate } from 'react-router-dom'; 
const navigate = useNavigate();
// 正常跳转 
<Button type="primary" onClick={() => navigate('/test/1', { replace: true })}>params</Button>
<Button type="primary" onClick={() => navigate('/test?cid=1', { replace: true })}>search</Button> 
<Button type="primary" onClick={() => navigate('/test', { replace: true, state: { mid: 1 } })}>state</Button>
参数1 跳转页面 参数2 传递的参数 params 
// 如果页面返回的时候需要回调 则
navigate({ 
    name: "test", 
    params:{addname:"2344"},
    merge:true 
    // 通过navigate方法返回到前面一个页,如果增加了新的参数,merge 表示合并参数,如果不设置该参数或者为false,则原来的参数会被替换,而不是合并,这个需要注意!
});

路由嵌套outlet

  1. outlet的意义可以看做是一个占位符, 它所在的位置,代表了嵌套路由展示时所插入的位置,类似于在outlet所在位置预留出一个坑位供子组件展示用
  2. 先在page目录下创建test1,test2,test3三个子组件, 然后在路由routes-index.tsx中添加进test的children中,其中test1设定为缓存子路由,修改test中的代码并添加outlet
// page --->test1 
import {useState} from 'react' 
import {Link} from "react-router-dom";
export default function Test(): any { 
const [count, setCount] = useState(0)
return ( 
<div className="Test">
<h1><Link to="/">children + React</Link></h1>
<button onClick={() => setCount((count) => count + 1)}> 1count is {count} </button> 
</div> 
);
} 

// page --->test2 
import {useState} from 'react'
import {Link} from "react-router-dom"; 
export default function Test(): any { 
const [count, setCount] = useState(0)
return ( 
<div className="Test"> 
<h1><Link to="/">children + React</Link></h1>
<button onClick={() => setCount((count) => count + 1)}> 2count is {count} </button> 
</div> 
); 
}

// page --->test3 
import {useState} from 'react'
import {Link} from "react-router-dom";
export default function Test(): any { 
const [count, setCount] = useState(0) 
return ( 
<div className="Test"> 
<h1><Link to="/">children + React</Link></h1>
<button onClick={() => setCount((count) => count + 1)}> 3count is {count} </button> 
</div> 
);
} 

// routes -->index.tsx
import test1 from '@/page/test1' 
//缓存路由不能懒加载 在嵌套关系中, 默认路由用path:''指代,即只访问父路由的情况下, outlet处子路由会显示默认子路由 children中的子路由起始不需要加/, 通过 父路由地址/子路由地址可访问对应子页面 
const routes = [ 
    。。。 
    { 
        path: '/test', 
        auth:false, 
        component:lazy(() => import('@/page/test')),
        children:[ 
            { 
                name:'123444', 
                path: 'test1',
                auth:false, 
                component:test1, 
            },
            { 
                path: 'test2',
                auth:false, 
                component:lazy(() => import('@/page/test2')),
            },
            { 
                path: 'test3', 
                auth:false,
                component:lazy(() => import('@/page/test3')),
            },
            // 默认路由 
            {
                path: '', 
                auth:false, 
                component:test1,
            }, 
         ] 
    },
    。。。 
] 

// page --->test 
import {useState} from 'react'
import {Link} from "react-router-dom"; 
import { useParams, useSearchParams,useLocation } from 'react-router-dom'
import { Outlet,useNavigate } from 'react-router-dom'
export default function Test(): any {
    const navigate = useNavigate();
    const [count, setCount] = useState(0)
    return ( 
        <div className="Test">
            <button onClick={() => navigate('/test/test1')}> 分页1 </button> 
            <button onClick={() => navigate('/test/test2')}> 分页2 </button> 
            <button onClick={() => navigate('/test/test3')}> 分页3 </button> 
            <Outlet></Outlet> 
            <h1><Link to="/">Vite + React</Link></h1>
            <button onClick={() => setCount((count) => count + 1)}> count is {count} </button>
        </div> 
    ); 
}

image.png

  1. 测试子路由显示位置, 及默认子路由是否正确, 以及test1子路由是否正确缓存。 通过

image.png

  1. 路由鉴权,实际情况实际分析,下面示例, 先在routes下创建一个RouterBefore.tsx文件,作为鉴权路由,然后在routes->index.tsx中定义鉴权方法
// routes ---> RouterBefore.tsx 
import {useNavigate,useLocation,useResolvedPath } from "react-router-dom";
import { Outlet } from 'react-router-dom'
import {urlAuth} from './index'
import {useEffect,useState} from 'react'
const RouterBefore = ()=>{
  const navigate = useNavigate()
  const location = useLocation()
  const [auth,setAuth] = useState(false)
  useEffect(()=>{
    // 先获取当前路由的对象属性
    let curRoute = urlAuth(location.pathname)
    // 然后获取当前状态是否登录
    let Login =sessionStorage.getItem('login')
    // 然后就是进行判断当前路由是否满足登录和权限状态, 满足就继续,不满足就跳转主页或登录页
    if(curRoute && curRoute.auth && Login === 'false'){
      setAuth(false)
      navigate('/')
    }else{
      setAuth(true)
    }
  },[])
  // 如果最后通过了验证, 那么就放置一个子路由占位outlet标签,否则什么都不放。
  return auth?<Outlet/>:null
} 
 
export default RouterBefore


// 添加鉴权方法urlAuth 
// routes -->index.tsx 
... 
//根据路径获取路由
const getUrlAuth = (routers:any, path:String)=>{
  for (const item of routers) {
    if (item.path === path) return item
    if (item.children) {
      const target:any = getUrlAuth(item.children, path)
      if (target) return target
    }
  }
  return null
}
const urlAuth = (path:String)=>{
  let auth = null
  auth = getUrlAuth(routes,path)
  return auth
}

export{ Router,urlAuth}
  1. 然后在需要使用鉴权子路由的父组件里地方,引入RouterBefore, 将outlet使用位置替换成RouterBefore即可。

react-redux

安装redux

npm install @reduxjs/toolkit react-redux 
redux-toolkit是目前redux官方推荐的编写redux逻辑的方法,针对redux的创建store繁琐、样板代码太多、
依赖外部库等问题进行了优化, 官方总结了四个特点是简易的/有想法的/强劲的/高效的,
总结来看,就是更加的方便简单了 Redux Toolkit包旨在成为编写Redux逻辑的标准方式。
它最初的创建是为了帮助解决关于 Redux 的三个常见问题:
1. 配置 Redux 存储太复杂了 
2. 必须添加很多包才能让 Redux 做任何有用的事情 
3. Redux 需要太多样板代码

目录创建和挂载store及同步数据测试

  1. 先在page目录下创建store文件夹, 然后先创建index.tsx作为store的出口,再创建modules文件夹作为存放各页面数据的区分模块, modules中index.tsx文件作为导出所有模块的出口, 然后在main.tsx挂载redux, 跟旧版本redux挂载没有区别
// store --->index.tsx 
import { configureStore } from "@reduxjs/toolkit";
import * as reducer from './modules';

// configureStore 创建一个 redux 数据
const store = configureStore({
  // 合并多个Slice
  reducer: {
      ...reducer,
  },
});

export default store;


// store ---> modules--> index.tsx 

export { default as test } from './test'
export { default as async } from './async'
// .....

// main.tsx 
import ReactDOM from 'react-dom/client' 
import './index.css' 
import {BrowserRouter} from "react-router-dom"; 
import {Router} from './routes/index'
import { AliveScope } from 'react-activation'
import { Provider } from 'react-redux'; 
import store from '@/store';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  // <React.StrictMode>
  <Provider store={store}>
    <BrowserRouter>
      <AliveScope> 
        <Router/>
      </AliveScope>
    </BrowserRouter>
  </Provider>
  // </React.StrictMode>,
)

image.png

  1. 在modules文件夹下, 创建test.tsx文件, 来测试store功能
// store --> modules ---> test.tsx 

import { createSlice } from '@reduxjs/toolkit';

// 创建一个接口类型, 方便指定store数据的类型定义及各属性的type.
// 可以看做是为你即将设定的对象和对象里的各项数据设置一个可以被ts精准辨识的类型,防止使用时发生类型错误的问题
export interface testState {
  count: number; // 设定即将被创建的对象拥有count属性,指代计数次数 并且该属性只接收数字类型的数据
  opreateName: Array<string>  // 设定即将被创建的对象拥有opreateName属性,指代操作类型,并且该属性是一个
  // 由且只能是字符串组成的数组
}

// 创建需要存储在redux中的数据对象,
const initialState: testState = {
    count: 0,
    opreateName: [ "add", "del", 'edit', 'search']
};

// 创建一个 Slice
export const test = createSlice({
  // 命名空间
  name: 'test',

  // 初始化状态值
  initialState,

  // 定义 reducers 并生成关联的操作
  reducers: {
    //创建修改count的方法
    setCount(state, { payload }){
      console.log(payload, '方法传递的修改值');
      state.count = payload.count;
    }
  },
});

// 导出 reducers 方法
export const { setCount } = test.actions;

// 默认导出
export default test.reducer;
  1. 然后在page/test3.tsx中使用redux进行数据管理, 测试未缓存页面是否能通过redux进行数据缓存
// page ---> test3.tsx 
import {useState, useEffect} from 'react'
import {Link} from "react-router-dom";
import { useSelector, useDispatch } from 'react-redux';
import { setCount } from '@/store/modules/test';

export default function Test(): any {

     // 通过useDispatch 派发事件
     const dispatch = useDispatch();

     // 通过useSelector直接拿到store中定义的value
     const { count, opreateName } = useSelector((store: any) => store.test);
    console.log(opreateName)
     const [value, setValue] = useState(count);
 
     useEffect(() => {
         // 监听 counter 变化
         console.log(count,'count变化了');
     }, [count])
    return (
        <div className="Test">
            {opreateName}
            <h1><Link to="/">children + React</Link></h1>
            <button onClick={() => {setValue(value + 1); dispatch(setCount({ count: value + 1 }))}}>
            3count is {count}
            </button>
        </div>
    );
}

image.png

  1. 到此, 一个最简单的redux同步数据模块的缓存读取和修改功能便实现了。

异步actions --- createAsyncThunk

  1. 在store-modules目录下创建async.tsx文件, 通过createAsyncThunk方法创建异步actions数据模块, 然后modules下index.tsx导出async
npm i --save @types/lodash 
// lodash 可以对数据进行一些便捷处理 用不用都行。 这里主要用它的get方法 _.get(object, path, [defaultValue])​ 根据 object对象的path路径获取值。 如果解析 value 是 undefined 会以 defaultValue 取代。 添加版本 3.7.0 
参数 object (Object): 要检索的对象。
path (Array|string): 要获取属性的路径。
[defaultValue] (*): 如果解析值是 undefined ,这值会被返回。 

// store --> modules --> async.tsx
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import _ from 'lodash';

// 模拟一个正常数据列表请求,获取到结果之后, 将数据填充到redux数据模块中
// 异步 actions
export const getList = createAsyncThunk('async/getList',
    async ({ currentPage = 1, pageSize = 5 }: { currentPage?: number, pageSize?: number, type?: string }) => {
        // 这里不使用await, 模拟异步请求等待时间
        console.log('进来')
        const promise = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve({ data: { list: [1, 2, 3, 4, 5], total: 5 } });
            }, 3000);
        });

        try {
            var [res] = await Promise.all([promise]);
        } catch (error) {
            console.error(error);
        }
        const payload = _.get(res, 'data', { list: [], total: 0 });
        console.log(payload);
        return payload;
    },
);

export interface asyncState {
    total: number;
    list: Array<any>
  }

// 创建需要存储在redux中的数据对象,
const initialState: asyncState = {
    total: 0,
    list: []
};

export const async = createSlice({
    // 命名空间
    name: "async",
    // state
    initialState,
    // 同步 actions
    reducers: {},
    // redux数据模块的监听器, 可以监听这些异步action的状态
    extraReducers: (builder) => {
        builder.addCase(getList.fulfilled, (state, { payload }) => {
            console.log(payload, 'zheli')
            state.list = payload.list;
            state.total = payload.total;
        })
    }
});

console.log(async)
// 导出 reducers 方法
export const { } = async.actions;

// 默认导出
export default async.reducer;

  1. 在test2.tsx中测试异步actions的数据缓存
//page ---> test2.tsx 
import {useState, useEffect} from 'react'
import {Link} from "react-router-dom";
import { useDispatch, useSelector } from 'react-redux';
import { getList } from "@/store/modules/async"; 
import _ from 'lodash'; 
import type { AnyAction } from "@reduxjs/toolkit"; 
export default function Test(): any { 
// 获取store中的方法 
const dispatch = useDispatch(); 
useEffect(() => {
dispatch(getList({ currentPage: 2, pageSize: 10}) as unknown as AnyAction); }, []) 
const { list, total } = useSelector((store: any) => store.async); 
return (
    <div className="Test"> 
        <h1><Link to="/">children + React</Link></h1> 
        <p>测试异步actions</p> 
        <div>total: {total}</div>
        <ul style={{ padding: 0 }}>
        list: {_.map(list, (item: number, key: number) => <li key={key}>{item}</li>)} 
        </ul>
    </div> 
); 
}

image.png

  1. 异步数据action获取及组件更新完成。

父子组件通信

父组件向子组件传值 --- outlet(动态,多) / 引入式子组件(指定, 单)

  1. 引入式子组件(指定, 单)即为常规传值,
// 父 
function Father (props,ref){ 
const [page, setPage] = useState("首页");
//将page传给子组件
function changePage() { 
setPage('改变了的首页');
} 
return ( 
<div className="Home"> 
{page} 
<button onClick={changePage}>改变page值</button>
<Son page={page} /> 
</div> 
) 
} 
export default Father

// 子 
function Son(props, ref) { 
return ( 
<div id="footerBox"> 
{props.page}底部 
</div> ) 
} 
export default Son
  1. outlet(动态,多), 如果父组件需要往outlet上传递数据, 则可以 outlet的context属性进行传递, 子组件通过useOutletContext()方法获取outlet传递参数
  2. 在test中定义需要传递的数据obj, 将它通过outlet的context属性传递给子组件
// page test.tsx
import {useState} from 'react' 
import {Link, Outlet,useNavigate} from "react-router-dom"; 
import { useParams, useSearchParams,useLocation } from 'react-router-dom' 
export default function Test(): any { 
const navigate = useNavigate(); 
const [count, setCount] = useState(0) 
// 定义需要向子组件传递的对象,并且提供修改对象的方法 
const [obj, setObj] = useState({id:123,list: [1,2,3,4]})
// const params = useParams(); 
// const { id } = params; 
// let [searchParams, setSearchParams] = useSearchParams(); 
// const cid = searchParams.getAll('cid'); 
// const name = searchParams.get('name');
// let location = useLocation(); 
// const { mid } = location.state; 
// console.log(id, 'params传值') 
// console.log(cid,name, 'search传值') 
// console.log(mid, 'state传值') 
return ( 
<div className="Test">
    <p>{obj.id}/{obj.list}</p> 
    <button onClick={() => navigate('/test/test1')}> 分页1 </button> 
    <button onClick={() => navigate('/test/test2')}> 分页2 </button>
    <button onClick={() => navigate('/test/test3')}> 分页3 </button> 
    {/* 使用outlet的context属性传递obj */} 
    <Outlet context={[obj, setObj]} /> 
    <h1><Link to="/">Vite + React</Link></h1> 
    <button onClick={() => setCount((count) => count + 1)}> count is {count} </button> 
</div> 
); 
}
  1. 在test2中使用useOutletContext()接收传递的数据obj, 需要注意的是, 在定义接收时,如果使用解构赋值则需要定义接收值的属性类型,useOutletContext<[object, Function]>()
import {useState, useEffect} from 'react' 
import {Link, useOutletContext} from "react-router-dom";
import { useDispatch, useSelector } from 'react-redux'; 
import { getList } from "@/store/modules/async"; 
import _ from 'lodash'; 
import type { AnyAction } from "@reduxjs/toolkit";
export default function Test(): any { 
// 如果是直接获取context不需要定义类型,
// const context = useOutletContext(); 
// 如果要解构获取对应参数, 则必须添加数据类型,
const [obj, setObj] = useOutletContext<[object, Function]>();
// console.log(context, '传递下来的参数') 
console.log(obj, '传递过来的值',setObj) 
// 获取store中的方法
const dispatch = useDispatch();
useEffect(() => { 
dispatch(getList({ currentPage: 2, pageSize: 10}) as unknown as AnyAction); }, []) 
const { list, total } = useSelector((store: any) => store.async); 
return ( 
    <div className="Test"> 
        <button onClick={() => setObj((obj:object) => obj={id: 344 , list: [123,213,444]})}>设置传值</button>
        <h1><Link to="/">children + React</Link></h1> 
        <p>测试异步actions</p> 
        <div>total: {total}</div> 
        <ul style={{ padding: 0 }}>
        list: {_.map(list, (item: number, key: number) => <li key={key}>{item}</li>)} 
        </ul> 
    </div>
); 
}
  1. 测试子组件是否接收父组件传值, 并且子组件修改值是否为父组件响应式

image.png

父组件调用子组件方法 -- function组件(没有生命周期)

  1. 默认情况下,不能在函数组件上使用 ref 属性,因为它们没有实例。解决办法就是使用 forwardRef 和 useImperativeHandle。 不过在函数的内部是可以使用 useRef 钩子来获取组件内的 DOM 元素。

  2. 引入式子组件(指定, 单)

// 父 
import React, { useRef } from 'react' ; 
import Child from './Child' ; 
const Parent = () => {
// 通过 Hooks 创建 Ref 
const childRef = useRef( null ) 
const handleClick = () => { 
childRef.current.sendMessage() 
} 
return ( 
<> 
<Child ref={childRef} />
<button onClick={handleClick}>Trigger Child Event</button> 
</> 
);
}
export default Parent; 

// 子 
import React, { forwardRef, useImperativeHandle } from 'react' ;
const Child = forwardRef((props, ref) => { 
//将子组件的方法 暴露给父组件 
useImperativeHandle(ref, () => ({ sendMessage })) 
const sendMessage = () => { console.log( 'sending message' ) }
return ( <div>Child</div> ); }) 
export default Child;
  1. outlet(动态,多), 虽然无法通过添加ref, 但可以同样利用context传值原理, 传递一个方法, 进行回调即可在父组件中调用子组件方法, 需要注意的是, 父组件传递给outlet的参数是所有子组件共享的
// 在 test.tsx 中添加一个方法childef, 行参是func, 内部逻辑只为 func(), 这样只要在子组件中调用这个方法时 , 把需要给父组件调用的方法作为形参放到其中,
// 即可完成对outlet类型标签 的子组件进行方法调用 
//父组件 test.tsx 
... 
export default function Test(): any { 
...
const childef = (func:Function) => { 
func() 
} 
...
return ( 
<div className="Test">
<p>{obj.id}/{obj.list}</p> 
<button onClick={() => navigate('/test/test1')}> 分页1 </button> 
<button onClick={() => navigate('/test/test2')}> 分页2 </button> 
<button onClick={() => navigate('/test/test3')}> 分页3 </button> 
{/* 使用outlet的context属性传递obj */} 
<Outlet context={[obj, setObj, childef]} />
<h1><Link to="/">Vite + React</Link></h1>
<button onClick={() => setCount((count) => count + 1)}> count is {count} </button>
</div> 
); 
} 

// 子组件 test2.tsx 
。。。。 
export default function Test(): any { 
// const context = useOutletContext(); 
// 这里注意, 同样需要声明参数类型 
const [obj, setObj,childef] = useOutletContext<[object, Function,Function]>();
// console.log(context, '传递下来的参数') 
console.log(obj, '传递过来的值',setObj)
。。。。 
const toParrentclick = () => { 
console.log('提供给父元素调用的方法') } 
return ( 
<div className="Test">
<button onClick={() => setObj((obj:object) => obj={id: 344 , list: [123,213,444]})}>设置传值</button> 
<h1><Link to="/">children + React</Link></h1>
<p>测试异步actions</p> 
{/* 将子组件中需要被调用的toParrentclick作为形参传递给父组件即可 */} 
<p onClick={ () => childef(toParrentclick)}>测试父组件调用子组件方法</p> 
<div>total: {total}</div> 
<ul style={{ padding: 0 }}>
list: {_.map(list, (item: number, key: number) => <li key={key}>{item}</li>)} 
</ul>
</div> 
); 
} 


// 子组件test1.tsx 
import {useState} from 'react'
import {Link,useOutletContext} from "react-router-dom";
export default function Test(): any { 
const [obj, setObj,childef] = useOutletContext<[object, Function,Function]>();
const [count, setCount] = useState(0)
const toParrentclick = () => { 
console.log('提供给父元素调用的方法child1') }
return ( 
<div className="Test"> 
<p onClick={ () => childef(toParrentclick)}>测试父组件调用子组件方法</p>
<h1><Link to="/">children + React</Link></h1> 
<button onClick={() => setCount((count) => count + 1)}> 1count is {count} </button>
</div> 
); 
}
  1. 测试结果, 1.由outlet-context传递的参数, 子组件可以共享获取, 2. outlet传值和调用子组件方法, 都是通过context传递参数, 调用子组件方法时, 实现原理通过context传递一个回调函数并在子组件将需要调用的方法以形参的形式传递即可完成调用。(该方法普通子组件也可以使用)

image.png

父组件调用子组件方法 -- class组件

  1. 引入式子组件(指定, 单)---1. 自定义事件
// 父组件
import React, { Component } from 'react' ;
import Child from './Child' ;
class Parent extends Component { 
componentDidMount () { console.log(this.childRef) }
handleChildEvent = (ref) => { 
// 将子组件的实例存到 this.childRef 中, 这样整个父组件就能拿到 
this.childRef = ref
} 
//按钮事件处理 
handleClick = () => { 
// 通过子组件的实例调用组组件中的方法
this.childRef.sendMessage() 
} 
render () { 
return ( 
<> 
<Child onChildEvent={ this .handleChildEvent} /> 
<button onClick={this.handleClick}>Trigger Child Event</button> 
</>);
} 
} 
export default Parent; 

// 子组件 
import React, { Component } from 'react' ; 
class Child extends Component { 
//子组件完成挂载时, 将子组件的方法 this 作为参数传到父组件的函数中 
componentDidMount () {
// 在子组件中调用父组件的方法,并把当前的实例传进去 
this.props.onChildEvent(this) 
} 
// 子组件的方法, 在父组件中触发 
sendMessage = () => { 
console.log('sending message') 
} 
render () { return ( <div>Child</div> ); } }
export default Child;
  1. 引入式子组件(指定, 单)2. 使用 React.createRef()
// 父组件
import React, { Component } from 'react'; 
import ChildCmp from './ChildCmp'; 
export default class ParentCmp extends Component { 
constructor(props) {
super(props) 
// 创建Ref 
this.childRef = React.createRef()
} 
// 按钮事件 
handleClick = () => {
// 直接通过 this.childRef.current 拿到子组件实例 
this.childRef.current.sendMessage()
} 
render () { 
return ( 
<> 
<ChildCmp ref={this.childRef} /> 
<button onClick={this.handleClick}>Trigger Child Event</button> 
</> 
);
} 
} 

// 子组件
import React, { Component } from 'react'; 
export default class ChildCmp extends Component { 
sendMessage = () => { console.log('sending message') } 
render () { return 'Child'; }
}
  1. 引入式子组件(指定, 单)3. 使用回调Refs
// 父组件 
import React, { Component } from 'react';
import ChildCmp from './ChildCmp';
export default class ParentCmp extends Component {
constructor(props) { 
super(props) 
// 创建 Ref,不通过 React.createRef()
this.childRef = null 
} 
// 设置 Ref
setChildRef = (ref) => { this.childRef = ref } 
// 按钮事件 
handleClick = () => { 
// 直接通过 this.childRef 拿到子组件实例
this.childRef.sendMessage(`Trigger Child Event from Parent`)
} 
render () { 
return ( 
<> 
<ChildCmp ref={this.setChildRef} /> 
<button onClick={this.handleClick}>Trigger Child Event</button>
</> 
);
}
} 

// 子组件 
import { Component } from 'react'; 
export default class ChildCmp extends Component { 
sendMessage = (message) => { console.log('sending message:', message) } 
render () { return 'Child'; } 
}

子组件向父组件传值 ---Class- outlet(动态,多) / 引入式子组件(指定, 单)

  1. 引入式子组件(指定, 单)-1.在父组件中定义方法,并绑定在子组件上
// 在子组件中调用父组件中的方法
import React,{Component} from 'react'; 
import Child from './child' 
class Parent extends Component{ 
constructor(props){ super(props); 
this.fun=this.fun.bind(this); 
} 
fun(){ console.log('你调用了父组件的方法') }
render(){ 
return ( 
<div>
<Child getFun={this.fun}></Child> 
</div>
) 
} 
} 
export default Parent; 

// 在子组件中调用父组件中的方法
import React,{Component} from 'react';
class Child extends Component{
constructor(props){ 
super(props);
console.log(this.props,'0000000000000000')
} 
render(){ 
return( 
<div> 
child 
<button onClick={()=>{console.log('你点击了按钮');this.props.getFun()}}>点击</button>
</div> 
) 
} 
} 
export default Child;
  1. outlet(动态,多) , 方法一样, 无论是函数组件还是类组件, 都可以通过传值和方法回调去相互调用

子组件向父组件传值 ---Function- outlet(动态,多) / 引入式子组件(指定, 单)

  1. 引入式子组件(指定, 单)-将父组件的方法作为值传递给子组件即可
// 父 
import React, { useRef } from 'react' ; 
import Child from './Child' ; 
const Parent = () => { 
const handleClick = () => { console.log('子组件调用') 
} 
return ( 
<> 
<Child getfunc={handleClick} /> 
</>
); }
export default Parent; 

// 子 
import React, { forwardRef, useImperativeHandle } from 'react' ;
const Child = forwardRef((props, ref) => { 
const fatherfunc = () => { props.getfunc() 
} 
return ( <div>Child</div> ); })
export default Child;
  1. outlet(动态,多) , 方法一样, 无论是函数组件还是类组件, 都可以通过传值和方法回调去相互调用

useContext

  1. 先创建createContex
  2. Provider 指定使用的范围
  3. 最后使用useContext
useContext就是上下文 什么是上下文呢? 全局变量就是全局的上下文,全局都可以访问到它;上下文就是你运行一段代码,所要知道的所有变量 
import React, { createContext, useContext, useReducer, useState } from 'react'
import ReactDOM from 'react-dom' 
// 创造一个上下文
const C = createContext(null);
function App(){ 
const [n,setN] = useState(0)
return( 
// 指定上下文使用范围,使用provider,并传入读数据和写入据 
<C.Provider value={{n,setN}}> 这是爷爷 <Baba></Baba> </C.Provider>
) } 

function Baba(){ 
return( <div> 这是爸爸 <Child></Child> </div> ) } 

function Child(){ 
// 使用上下文,因为传入的是对象,则接受也应该是对象 
const {n,setN} = useContext(C) 
const add=()=>{ setN(n=>n+1) };
return( <div> 这是儿子:n:{n} <button onClick={add}>+1</button> </div> ) } ReactDOM.render(<App />,document.getElementById('root'));

总结: outlet的通信使用和引入式通信

1.  无论在函数组件亦或是类组件中, outlet的通信方式基本一致, 都是通过context进行传值, 通过useOutletContext()获取。 相互调用方法也是通过传递函数方法,或者是传递回调函数方法并以子组件方法作为形参来进行通信。
1.  对于引入式组件而言, 无论在函数组件亦或是类组件中, 父组件传值给子组件都是一样的,区别在于方法的调用上
1.  而对于引入式组件而言, 因为有明确的针对性, 所以在函数组件和类组件中传递方式略有区别

引入式组件在函数组件格式下的传递方式为 1. 父传值给子, 通过attr属性传值, 子组件通过props获取即可 2. 父调用子方法, 通过用forwardRef创建子组件, 然后使用useImperativeHandle将需要被调用的方法暴露出去, 然后父组件用useRef来获取子组件dom, 并使用ref绑定子组件对象 
// 父 
import Child from './Child' ; 
const Parent = () => { 
// 通过 Hooks 创建 Ref 
const childRef = useRef(null) 
const handleClick = () => { childRef.current.sendMessage() 
} 
... 
<Child ref={childRef} /> 
...
export default Parent; 
引入式组件在类组件格式下的传递方式为 
1.父调用子方法, 同样是通过ref绑定获取子元素, 然后进行调用 子调用父组件方法, 
1.引入式组件通过父传值传递父组件方法, 子组件通过props中调用

组件类型-函数组件与类组件

  1. 类组件
所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件。 特点 
1.为了避免代码冗余,提高代码利用率,组件可以重复调用 
2.组件的属性props是只读的,调用者可以传递参数到props对象中定义属性,
调用者可以直接将属性作为组件内的属性或方法直接调用。 
往往是组件调用方调用组件时指定props定义属性,往往定值后就不改边了,注意组件调用方可赋值被调用方。 
3.通过props的方式进行父子组件交互,通过传递一个新的props属性值使得子组件重新render,从而达到父子组件通讯。 
4.{...this.props}可以传递属性集合,...为属性扩展符 
5.组件必须返回了一个 React 元素 
6.组件中state为私有属性,是可变的,一般在construct()中定义,使用方法:不要直接修改 state(状态) 
7.修改子组件还有一种方式,通过 ref属性,表示为对组件真正实例引用,其实就是ReactDOM.render()返回的组件实例
class DemoClass extends React.Component { 
// 初始化类组件的
state state = { text: "" }; 
componentDidMount() { // 省略业务逻辑 }
changeText = (newText) => { 
// 更新 state
this.setState({ text: newText }); };
// 编写生命周期方法 
render render() {
return ( <div> <p>{this.state.text}</p> 
<button onClick={()=>this.changeText("newText")}>点我修改</button>
</div> ); } }
  1. 函数组件, 自从React推出Hooks之后,函数组件写法相比于类组件写法更为便捷, 大部分普通组件都可以使用函数式组件格式,某些特殊用途(如错误边界)组件只能写成类式组件。
函数组件顾名思义,就是以函数的形态存在的 React 组件。早期并没有 React-Hooks 的加持,
函数组件内部无法定义和维护 state,因此它还有一个别名叫“无状态组件”。 特点
1.只负责接收 props,渲染 DOM 
2. 没有 state 
3.返回了一个 React 元素 
4.不能访问生命周期方法 
5.不需要声明类:可以避免 extends 或 constructor 之类的代码,语法上更加简洁。
6.不会被实例化:因此不能直接传 ref(可以使用 React.forwardRef 包装后再传 ref)。 
7.不需要显示声明 this 关键字:在 ES6 的类声明中往往需要将函数的 this 关键字绑定到当前作用域,
而因为函数式声明的特性,我们不需要再强制绑定。 
8.更好的性能表现:因为函数式组件中并不需要进行生命周期的管理与状态管理,
因此React并不需要进行某些特定的检查或者内存分配,从而保证了更好地性能表现。
function DemoFunction(props) { 
const { text } = props 
return ( 
<div className="demoFunction">
<p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p> 
</div> ); }

类组件的生命周期

juejin.cn/post/709613…