初学react17到项目实战(附开源代码)

3,687 阅读7分钟

项目背景

学习契机

说起react,一直拖到现在也不是说难学,可能是惰性使然,觉得用vue躺着挺舒服就懒得捣腾,每次想咸鱼翻身,但是看了看JSX语法,也就只是翻了个身继续躺着。

受各位卷王们的影响,我也焦虑起来了,所以定制了一系列的学习计划,争取今年能不再做API工程师,不然就要回去继承家产了。

这个react项目也是用的我们后台管理系统为模板写的,所以如果看过我之前的 VUE3项目,可能会觉得 这个react项目怎么V里V气的

附上github开源项目地址:
vite-react

学习渠道

我个人不喜欢看视频,觉得太慢容易走神,看文档反而更能引人入胜,可能是对学习没耐心。所以直接从react官方文档入手。

在官方文档中发现看到官方对初学者推荐:

如果你觉得 React 官方文档节奏太快,不太适应,可以先去看看这篇 Tania Rascia 的 React 概览。它以新手友好的方式详细介绍了最重要的 React 概念,看完这篇概览,再回来试试看官方文档吧!

于是我就对号入座先去看了官方推荐的这篇博客,确实读解起来比较轻松,虽然我反复看了三遍,接着看了这位作者写的用 Hooks 在 React 中构建一个 CRUD 应用程序,接着就是回头去读react官方文档,期间有难以理解的地方就结合百度一步步解析,比如context, 比如hooks,这整个阅读理解过程用了一周时间,当然是这个一周是工作之余。

在读完这些后最大的感受就是,之前让我看不下去的JSX结构,现在略微清爽了一些,没那么抗拒了(在后面写过一遍之后,彻底不排斥了,反而觉得写起来挺有意思的)

项目实践

接下来就是实践阶段

项目构建

我这里脚手架直接用的vite2,根据文档流程正常走一遭。

使用npm构建vite项目:

$ npm create vite@latest

选择react => react-ts image.png

然后一个初始的react模板项目就构建完成了(蒸的C) image.png

接着下载依赖包:

$ npm install

然后启动开发环境:

$ npm run dev

image.png

这样一个最简洁的react的项目就跑起来了,然后在一步步配置项目所需要用的插件:

路由:react-router
状态管理:react-redux
HTTP库:axios
CSS预处理器:less
日期格式化组件库:moment
UI组件库:Ant Design

删除scr目录下生成的模板页面文件,创建项目所需的目录结构:

image.png

路由配置

路由我这边用的是 react-router V6 貌似没找到官方的文档,这个版本新增了一个API useRoutes ,能读取路由配置数组,生成相应的路由组件列表,配方跟 vue-router 差不多,当然标签形式还是可以正常使用,只不过我习惯这种API模式:

创建路由管理文件:src/router/index.tsx

import LoginPage from "./../views/login";
import LayoutPage from "./../views/layout";
import HomePage from "./../views/home";
import LandPage from "./../views/api/land";
import IndustryPage from "./../views/api/industry";
import RolePage from "./../views/sys/role";
import MenuPage from "./../views/sys/menu";

const routes:any = [
  {path: "/login", element: <LoginPage />, isHome: true},
  { 
      path: "/", 
      element: <LayoutPage />,
      // 设置子路由
      children: [
          {path: "/home", element: <HomePage />},
          {path: "/land", element: <LandPage />},
          {path: "/industry", element: <IndustryPage />},
          {path: "/role", element: <RolePage />},
          {path: "/menu", element: <MenuPage />}
      ]
  }
]
export default routes

path: '/'为默认路由,配置好路由列表后,到项目入口文件mian.tsx中引入路由配置:

修改入口文件:src/main.tsx

import ReactDOM from 'react-dom'
import routes from "./router";
import { useRoutes } from 'react-router-dom';
import {BrowserRouter as Router} from 'react-router-dom'

import './assets/css/index.css'
import 'antd/dist/antd.css';
import zhCN from 'antd/lib/locale/zh_CN';
import { ConfigProvider } from 'antd';

function App() {
  return useRoutes(routes) 
}

const renderApp = () => {
  ReactDOM.render(
    <ConfigProvider locale={zhCN}>
      <Router>
        <App />
      </Router>
    </ConfigProvider>,
    document.getElementById('root')
  )
}
renderApp();

创建一个函数组件,将路由数组传入useRoutes中,useRoutes只能作用于router context中,所以useRoutes需要写组件BrowserRouter里。

ConfigProviderAnt Design国际化组件。

状态管理

状态管理器集成的react-redux,但是最终没有使用这种方式,而是用 context 组件树全局共享数据的方式,这个后续描述。

创建入口文件:src/store/index.tsx

import { configureStore, combineReducers } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux';

import slice from './slices';

const reducer = combineReducers({
  slice
});

export const store = configureStore({
  reducer,
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

export default store;

创建数据中心文件:src/store/slices.tsx

import { createSlice } from '@reduxjs/toolkit';
import { RootState } from '../store';

export interface IGlobalState {
  pageTabActive: any;
  tabsList: Array<[]>;
}

const initialState: IGlobalState = {
  pageTabActive: -1, // 当前menu页面ID
  tabsList: [], // 当前已打开的tab页列表
  collapsed: window.innerWidth < 1000 // 菜单闭合
};

const globalSlice = createSlice({
  name: "global",
  initialState,
  reducers: {
    setPageTab: (state, action) => {
      state.pageTabActive = action.payload || -1
    },
    setTabsList: (state, action) => {
      state.tabsList = action.payload || []
    }
  }
});

export const selectGlobal = (state: RootState) => state.slice;

export const {
  setPageTab,
  setTabsList
} = globalSlice.actions;

export default globalSlice.reducer;

修改入口文件:src/main.tsx

...
import { Provider } from 'react-redux';
import store from './store/index';

const renderApp = () => {
  ReactDOM.render(
    <Provider store={store}>
      ...
        <App />
      ...
    </Provider>,
    document.getElementById('root')
  )
}
...

组件中使用:src/views/layout/components/c-nav/index.tsx

import { useAppDispatch, useAppSelector } from './store';
import { selectGlobal, setPageTab, setTabsList } from './store/slices';
function LayoutNav() {
    const globalState:any = useAppSelector(selectGlobal); // 属性
    const dispatch = useAppDispatch(); // 方法
    dispatch(setPageTab("-1"))
    dispatch(setTabsList([]))
    
    return <div>{globalState.pageTabActive}</div>
}
export default LayoutNav

我这里用了reduxjs/toolkit库,它内将redux一些繁琐配置都进行了封装,具体还待探究。

redux三大原则:
1)单一数据源
2)state 只读
3)使用函数来执行修改

redux上面没做过多纠结,因为最终我采用了context代替,所以更多的去研究context了,当然项目中还是保留了了redux方案。

Hooks

// 函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// class组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

react 组件分为函数组件class组件,它们的区别在于:
类组件有this,函数组件没有。
类组件有生命周期,函数组件没有。
类组件有state状态,函数组件没有。

hooks出现之前,react中的函数组件通常只考虑负责UI的渲染,没有自身的状态没有业务逻辑代码,是一个纯函数,下面看看有hooks之前的函数组件:

import {useState} from 'react'
function Welcome() {
  const [name, setName] = useState('小明')
  
  useEffect(() => {
    setName('小南')
  },[])

  return <h1>Hello, {props.name}</h1>;
}
export default Welcome

hooks为函数组件提供了:useState状态useEffect副作用useCallback缓存函数等,让一个应用程序可以不用写任何类组件。

useState状态只能通过解构函数进行修改。
useEffect副作用可看做class组件中三个生命周期函数的组合,第二个参数表示需要监听的状态变化,如果没设置则表示监听所以状态变化执行。

Context

react是单向数据流,数据通过props自上由下传递,但这种做法在组件树较深较复杂的时候,多个组件都需要的时候使用极其繁琐,Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props

上面说过我项目使用Context代替redux做全局状态管理,因为Context对于一个组件树而言就是全局的,虽然它的设计目的是为了简化props逐层传递,但在所有路由组件都集中在一个主路由下的时候,那么这个主路由组件的状态就可以通过Context做到全局状态管理的效果,这是目前的个人浅见~~。

import {createContext, useState} from 'react'
import style from './index.module.less'
import LayoutNav from "./components/c-nav/index";
import LayoutMain from "./components/c-main/index";

interface IGlobalState {
  pageTabActive: String;
  tabsList: Array<[]>;
  collapsed: boolean;
}

const initialState: IGlobalState = {
  pageTabActive: "-1", // 当前menu页面ID
  tabsList: [] // 当前已打开的tab页列表
};

export const MyContext = createContext({})

function App() {
  const [state, setState] = useState(initialState)

  return (
    <MyContext.Provider value={{state, setState}}>
      <div className={style.layoutContainer}>
        <LayoutNav />
        <LayoutMain />
      </div>
    </MyContext.Provider>
  )
}
export default App

通过createContext创建一个名为MyContextContext对象,组件树会将Context匹配到离自己最近的Provider中。

MyContext.Provider为生产者,value属性为需要传递的属性,供内部组件使用。

在组件内如何使用Context,我这里使用的是useContextAPI获取导入的context对象值,可以跟自身组件中的状态一样使用。

import { MyContext } from "./../../index";
function LayoutNav() {
    const {state, setState}:any = useContext(MyContext)
    setState({...state, tabsList: []});
    console.log(state)
}

还有其他用法待探究

最后

因为react还未学习全面,上述见解有不对的地方望见谅。
后续通过更多的项目去锤炼。

不积蛙步,无以至千里
不积小流,无以成江海