react18从0到1搭建

546 阅读11分钟

一、环境搭建 1、安装node.js 官网地址:Node.js

2、安装webpack

npm install -g webpack

3、配置淘宝镜像(看个人需求) 如果在本地一直装不上npm的包,可以考虑用国内的淘宝镜像,使用淘宝定制的 cnpm (gzip 压缩支持) 命令行工具代替默认的 npm

npm install -g cnpm --registry=https://registry.npm.taobao.org
 
npm config set registry https://registry.npm.taobao.org

4、安装create-react-app

npm install -g create-react-app

5、创建项目

create-react-app my-project // 创建名为my-project的项目
 
cd my-project // 进入项目文件夹

6、本地服务启动

npm start // 启动本地server用于开发

二、项目框架结构

|- node_modules // 项目依赖包
|- public // 一般用于存放静态文件,打包时会被直接复制到输出目录(./buidle)
|- src // 项目源代码
  |  |- assets // 静态资源文件夹,用于存放静态资源,打包时会经过 webpack 处理
  |  |- components // 组件文件夹,存放 React 组件,一般是该项目公用的无状态组件
          |  |- MLayout
                  |  |- MLayout.js
                  |  |- MLayout.module.less
  |  |- containers // 页面视图文件夹
          |  |- Index
                  |  |- Index.js
                  |  |- Index.module.less
  |  |- redux // redux文件夹
          |  |- actions
                  |  |- actions.js
          |  |- reducers
                  |  |- reducers.js
          |  |- store
                  |  |- store.js
          |  |- state.js
  |  |- router // 路由配置文件夹
          |  |- router.js // 路由文件配置
  |  |- service // 服务请求文件夹
          |  |- http.js // 请求接口文件
          |  |- request.js // 请求配置文件
  |  |- App.js // 入口文件
  |  |- App.less
  |  |- index.js //注册路由与服务
  |  |- index.css
  |  |- serviceWorker //开发配置
|- .env // 环境配置文件
|- .env.development // dev环境配置文件
|- .env.test // test环境配置文件
|- .env.production // prd环境配置文件
|- craco.config.js // craco配置文件
|- package.json // 包管理代码
|- .gitignore // Git忽略文件

三、常用的集成和配置 1、React Router(路由配置管理) React Router 是一个基于 React 之上的强大路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。

1.1、安装react-router

npm install react-router@4.3.1 --save

Router下面只能包含一个盒子标签,类似这里的div。 Link代表一个链接,在html界面中会解析成a标签。作为一个链接,必须有一个to属性,代表链接地址。这个链接地址是一个相对路径。 Route,有一个path属性和一个组件属性(可以是component、render等等)。

1.2、基本使用:

render () {
    return (
          <Router>
             <div>
                <ul>
                   <li><Link to="/index">首页</Link></li>
                   <li><Link to="/other">其他页</Link></li>
                 </ul>
                 <Route path="/index" component={Index}/>
                 <Route path="/other" component={Other}/>
             </div>
          </Router>
    )
}

1.3 路由嵌套使用

const Test = () => (
   <div>
      <Switch>
         <Route
            path="/"
            render={props => (
               <App>
                  <Switch>
                     <Route path="/" exact component={Index} />
                     <Route path="/index" component={Index} />
                     <Route path="/other" component={Other} />
                     <Route path="/message/:id/:TestId" component={Message} />
                      {/*路由不正确时,默认跳回home页面*/}
                     <Route render={() => <Redirect to="/" />} />
                  </Switch>
               </App>
            )}
         />
      </Switch>
   </div>
);
 
export default Test;

1.4、把路由路径写在一个配置文件router.js里,示例代码如下:

import Login from '../containers/Login/Login.js';
import Index from '../containers/Index/Index.js';
import Home from '../containers/Home/Home.js';
import New from '../containers/New/New.js';
import NewOne from '../containers/NewOne/NewOne.js';

let routes = [
    {
        path: "/",
        component: Home,
        exact: true
    },
    {
        path: "/login",
        component: Login,
        exact: true
    },
    {
        path: "/index",
        component: Index,
        exact: true
    },
    {
        path: "/news",
        component: New,
        routes: [  // 嵌套路由
            {
                path: "/news/newsone",
                component: NewOne,
                exact: true
            }
        ]
    }
];

export default routes;

在App.js里导入使用

// import logo from './logo.svg';
import './App.css';

import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import routes from './router/router';
import Home from './containers/Home/Home.js';

// 默认语言为 en-US,如果你需要设置其他语言,推荐在入口文件全局设置 locale
// import moment from 'moment';
import 'moment/locale/zh-cn';
import locale from 'antd/lib/locale/zh_CN';
import { ConfigProvider } from 'antd';

function App() {
  return (
      <ConfigProvider locale={locale}>
        <div className="App">
          <Router>
            <Routes>
              {
                routes.map((route, key) => {
                  console.log(route)
                  if (route.exact) {
                    return <Route key={key} exact path={route.path} element={<route.component/>}/>
                  } else {
                    return <Route key={key} path={route.path}
                                  render={props => <route.component {...props} routes={route.routes} />
                                  }/>
                  }
                })
              }
              <Route element={Home}/>
            </Routes>
          </Router>
        </div>
      </ConfigProvider>
  );
}

export default App;

new.js(父页面)

// import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { Route } from 'react-router-dom';

function New(props) {
    return <div>
        new
        {
            props.routes.map((route, key) => {
                console.log(route.path)
                return <Route key={key} exact path={route.path} component={route.component}/>
            })
        }
    </div>
}

export default New

newOne.js(嵌套的子页面)

function NewOne() {
    return <div>new one</div>
}

export default NewOne

2、redux(数据管理)

React是单向数据流,所以有些情况下单单依赖React自身无法实现组件之间的通信,故此需要redux

需要安装Redux相关依赖,由于不同版本之间可能存在兼容性问题,所以安装的时候最好制定版本,安装好之后,就可以在项目中引入redux了

npm install redux@4.2.0 --save

npm install redux-thunk@2.4.1 --save

npm install react-redux@8.0.2 --save

2.1 在函数组件里使用

state.js(声明变量默认值)

// state的默认值统一放置在state.js文件
 
// state.js
// 声明默认值
// 这里我们列举两个示例
// 同步数据:pageTitle
// 异步数据:infoList(将来用异步接口获取)
 
export default {
  pageTitle: '首页',
  infoList: []
}

actions.js(定义触发事件)

// actions.js
// action也是函数
 
export function increaseAction (data) {
  return (dispatch, state) => {
    dispatch({ type: 'increase', data: data })
  }
}
 
export function setInfoList (data) {
  return (dispatch, getState) => {
    // // 使用fetch实现异步请求
    // window.fetch('/api/getInfoList', {
    //     method: 'GET',
    //     headers: {
    //         'Content-Type': 'application/json'
    //     }
    // }).then(res => {
    //     return res.json()
    // }).then(data => {
    //     let { code, data } = data
    //     if (code === 0) {
    //         dispatch({ type: 'SET_INFO_LIST', data: data })
    //     }
    // })
  }
}

reducers.js(获取和处理数据)

// reducers.js
// 工具函数,用于组织多个reducer,并返回reducer集合
import { combineReducers } from 'redux'
// 默认值
import defaultState from '../state.js'
 
// 一个reducer就是一个函数
function pageTitle (state = defaultState.pageTitle, action) {
  // 不同的action有不同的处理逻辑
  switch (action.type) {
    case 'SET_PAGE_TITLE':
      return action.data
    default:
      return state
  }
}
 
function infoList (state = defaultState.infoList, action) {
  switch (action.type) {
    case 'SET_INFO_LIST':
      return action.data
    default:
      return state
  }
}
 
// 导出所有reducer
export default combineReducers({
    pageTitle,
    infoList
})

store.js(创建store实例)

// applyMiddleware: redux通过该函数来使用中间件
// createStore: 用于创建store实例
 
// 中间件,作用:如果不使用该中间件,当我们dispatch一个action时,
// 需要给dispatch函数传入action对象;但如果我们使用了这个中间件,那么就可
// 以传入一个函数,这个函数接收两个参数:dispatch和getState。这个dispatch可
// 以在将来的异步请求完成后使用,对于异步action很有用
 
import { applyMiddleware, createStore } from 'redux'
import thunk from 'redux-thunk'
// 引入reducer
import reducers from '../reducers/reducers.js'
 
 
// 创建store实例
let store = createStore(
  reducers,
  applyMiddleware(thunk)
)
 
export default store

在根文件index.js里使用,向目标元素渲染容器组件App

这里的组件Provider是一个react-redux中特殊的组件

  1. Provider中有且只有一个子组件(这里就是App容器组件,不一定是容器组件,根据自己的业务需求自己操作)

  2. 使用Provider组件的好处是,只需要给Provider组件设置属性,那么其子组件和其子组件中的子组件都可以直接使用其对应的属性

  3. 避免了组件嵌套之后一个一个传递的复杂操作

import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
 
// Provider是react-redux两个核心工具之一,作用:将store传递到每个项目中的组件中
// 第二个工具是connect,稍后会作介绍
import { Provider } from 'react-redux'
// 引入创建好的store实例
import store from './redux/store/store.js'
 
ReactDOM.render(
  <div>
    {/* 将store作为prop传入,即可使应用中的所有组件使用store */}
    <Provider store = {store}>
      <App />
    </Provider>
  </div>,
  document.getElementById('root')
);
 
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

在UI页面里使用

import { Layout, Button, Form, Input } from 'antd';
import style from './Login.module.less';
import { useRef } from 'react'
 
// connect方法的作用:将额外的props传递给组件,并返回新的组件,组件在该过程中不会受到影响
import { connect } from 'react-redux';
// 引入action
import { increaseAction, setInfoList } from '../../redux/actions/actions.js'
 
const { Header, Footer, Content } = Layout;
 
function Login(props) {
 
  const formRef = useRef(null);
 
  const onReset = () => {
    formRef.current.resetFields();
  };
 
  const onFinish = (values) => {
    console.log('Success:', values);
    props.history.push('/index')
  };
 
  const onFinishFailed = (errorInfo) => {
    console.log('Failed:', errorInfo);
  };
  
 
  return <Layout style={{height: '100vh'}}>
            ...省略代码
          </Layout>
}
 
// mapStateToProps:将state映射到组件的props中
const mapStateToProps = (state) => {
  return {
    pageTitle: state.pageTitle,
    infoList: state.infoList
  }
}
// mapDispatchToProps:将dispatch映射到组件的props中
const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    increaseAction (data) {
        // 如果不懂这里的逻辑可查看前面对redux-thunk的介绍
        dispatch(increaseAction(data))
        // 执行increaseAction会返回一个函数
        // 这正是redux-thunk的所用之处:异步action
        // 上行代码相当于
        /*dispatch((dispatch, getState) => {
            dispatch({ type: 'SET_PAGE_TITLE', data: data })
        )*/
    },
    setInfoList (data) {
        dispatch(setInfoList(data))
    }
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(Login)

2.2 在Class组件里使用

安装react-redux依赖包

npm i react-redux --save
npm i @types/react-redux --save-dev

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';


// Provider是react-redux两个核心工具之一,作用:将store传递到每个项目中的组件中
// 第二个工具是connect,稍后会作介绍
import { Provider } from 'react-redux'
// 引入创建好的store实例
import store from './redux/store/store.js'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    {/*<App />*/}
      {/* 将store作为prop传入,即可使应用中的所有组件使用store */}
      <Provider store = {store}>
          <App />
      </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Header组件中使用

import { Component } from 'react'
import { withRouter, RouteComponentProps } from 'react-router-dom'
import { MenuInfo } from 'rc-menu/lib/interface'
import { nanoid } from 'nanoid'
// 使用react-redux
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import store, { RootState } from 'redux/store'
import { addLanguageActionCreator, changeLanguageActionCreator } from 'redux/language/actionCreators'
 
/* connect 就是一个高阶hoc,只不过它的命名没有使用withxxxx来表示,class 类组件 使用 withxxx 高阶hoc组件
  使用了connect,就相当于store.subscribe, 即组件订阅了store中的数据
*/
 
 
/* mapStateToProps: 处理数据的流入。返回一个对象
 使用connect函数,传入mapStateToProps,完成store数据与组件的props绑定
 */
// 高阶hoc包裹的组件,可以获取到一些额外的props或state属性,connect函数传入参数 mapStateToProps, 组件的props会被注入一些额外的props属性
const mapStateToProps = (state: RootState) => {
  return {
    lng: state.lng,
    languageList: state.languageList
  }
}
 
/* mapDispatchToProps: 处理数据的流出。返回一个对象,对象中的每一个字段都是一个dispatch处理函数
   将dispatch绑定到props中
*/
const mapDispatchToProps = (dispatch: Dispatch) => {
  return {
    addLanguageDispatch: (language: { code: string, language: string }) => {
      dispatch(addLanguageActionCreator(language))
    }
    changeLanguageDispatch: (lng: 'en'|'zh') => {
      dispatch(changeLanguageActionCreator(lng))
    }
  }
}
 
class HeaderComponent extends Component<RouteComponentProps & ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>>{
  
  render(){
    const { history, lng, languageList, addLanguageDispatch, changeLanguageDispatch } = this.props
    
    /* meun 的点击事件 */
    const oprateLanguage = ( e: MenuInfo ) => {
      if(e.key !== 'new'){
        changeLanguageDispatch(e.key)
      }else{
       addLanguageDispatch({code: `lng${nanoid()}`, language: '新语种'})
      }
    }
    
    const menu = (
      <Menu>
        <Menu.Item key='new' onClick={oprateLanguage}>
          添加新语言
        </Menu.Item>
        {languageList.map(language => (
          <Menu.Item key={language.code} onClick={oprateLanguage}>
            {language.language}
          </Menu.Item>
        ))}
      </Menu>
    )
    
    return (
      <div>
        <Typography.Text className='mr40'>让旅游更幸福</Typography.Text>
        <Dropdown overlay={menu}>
        	<Button>
        		{languageList.find(language => language.code === lng)?.language}
          	<GlobalOutlined />
          </Button>
 				</Dropdown>
        <Button.Group>
          <Button onClick={() => history.push('/signIn')}>登录</Button>
          <Button onClick={() => history.push('/register')}>注册</Button>
        </Button.Group>
		</div>
    )
  }
}
 
export const Header = connect(mapStateToProps, mapDispatchToProps)(withRouter(HeaderComponent))

store数据封装

  • 新建目录:src/reduxsrc/redux/language
  • 新建文件:src/redux/store.tssrc/redux/language/actionCreators.tssrc/redux/language/reducers.ts

store.ts

// applyMiddleware: redux通过该函数来使用中间件
// createStore: 用于创建store实例

// 中间件,作用:如果不使用该中间件,当我们dispatch一个action时,
// 需要给dispatch函数传入action对象;但如果我们使用了这个中间件,那么就可
// 以传入一个函数,这个函数接收两个参数:dispatch和getState。这个dispatch可
// 以在将来的异步请求完成后使用,对于异步action很有用

import { applyMiddleware, createStore } from 'redux'
import thunk from 'redux-thunk'
// 引入reducer
import reducers from '../reducers/reducers.js'

// 引入languageReducer
import languageReducer from './language/reducer.ts'


// 创建store实例
let store = createStore(
    reducers,
    languageReducer,
    applyMiddleware(thunk)
)

export default store

工厂模式创建action- actionCreators.ts

/* 用常量定义action.type,减少代码敲错 */
export const ADD_LANGUAGE = 'language/add'
export const CHANGE_LANGUAGE = 'language/change'

/* action的类型申明 */
type AddActionProps = {
    type: typeof ADD_LANGUAGE,
    payload: { code: string, language: string }
}
type ChangeActionProps = {
    type: typeof CHANGE_LANGUAGE,
    payload: 'zh' | 'en'
}

export type LanguageActionProps = AddActionProps | ChangeActionProps

/* 用工厂模式创建action */
// export const addLanguageActionCreator = (language: {code: string, language: string}):ADD_LANGUAGE  => {
//     return {
//         type: ADD_LANGUAGE,
//         payload: language
//     }
// }
// export const changeLanguageActionCreator = (lng: 'zh' | 'en'):CHANGE_LANGUAGE  => {
//     return {
//         type: CHANGE_LANGUAGE,
//         payload: lng
//     }
// }

reducer.ts

import { ADD_LANGUAGE, CHANGE_LANGUAGE, LanguageActionProps } from './actionCreators'

export interface LanguageState {
    lng: 'zh' | 'en',
    languageList: {code: string, language: string}[]
}

const defaultStoreState: LanguageState = {
    lng: 'zh',
    languageList: [{ code: 'zh', language: '中文'}, { code: 'en', language: 'English'}]
}


export default (state = defaultStoreState, action:LanguageActionProps) => {
    switch (action.type) {
        case CHANGE_LANGUAGE:
            return { ...state, lng: action.payload }
        case ADD_LANGUAGE:
            return { ...state, languageList: [...state.languageList, action.payload] }
        default:
            return state
    }
}

3、axios

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

安装

npm install axios

示例代码

request.js

import axios from 'axios'

const BASE_URL = process.env.BASE_URL;
const TIMEOUT = 5000;

const instance = axios.create({    // 创建axios实例,在这里可以设置请求的默认配置
    timeout: TIMEOUT, // 设置超时时间
    baseURL: BASE_URL // 根据自己配置的反向代理去设置不同环境的baeUrl
})

// 文档中的统一设置post请求头。下面会说到post请求的几种'Content-Type'
instance.defaults.headers.post['Content-Type'] = 'application/json'

let httpCode = {        //这里我简单列出一些常见的http状态码信息,可以自己去调整配置
    400: '请求参数错误',
    401: '权限不足, 请重新登录',
    403: '服务器拒绝本次访问',
    404: '请求资源未找到',
    500: '内部服务器错误',
    501: '服务器不支持该请求中使用的方法',
    502: '网关错误',
    504: '网关超时'
}

/** 添加请求拦截器 **/
instance.interceptors.request.use(config => {
    config.headers['token'] = sessionStorage.getItem('token') || ''
    // hide = message.loading({content: 'Loading...', duration: 0});
    // 在这里:可以根据业务需求可以在发送请求之前做些什么:例如我这个是导出文件的接口,因为返回的是二进制流,所以需要设置请求响应类型为blob,就可以在此处设置。
    if (config.url.includes('pur/contract/export')) {
        config.headers['responseType'] = 'blob'
    }
    // 我这里是文件上传,发送的是二进制流,所以需要设置请求头的'Content-Type'
    if (config.url.includes('pur/contract/upload')) {
        config.headers['Content-Type'] = 'multipart/form-data'
    }
    return config
}, error => {
    // 对请求错误做些什么
    return Promise.reject(error)
})

/** 添加响应拦截器  **/
instance.interceptors.response.use(response => {
    // hide()
    if (response.statusText === 'ok') {     // 响应结果里的statusText: ok是我与后台的约定,大家可以根据实际情况去做对应的判断
        return Promise.resolve(response.data)
    } else {
        message.error('响应超时')
        return Promise.reject(response.data.message)
    }
}, error => {
    // hide()
    if (error.response) {
        // 根据请求失败的http状态码去给用户相应的提示
        // let tips = error.response.status in httpCode ? httpCode[error.response.status] : error.response.data.message
        // message.error(tips)
        if (error.response.status === 401) {    // token或者登陆失效情况下跳转到登录页面,根据实际情况,在这里可以根据不同的响应错误结果,做对应的事。这里我以401判断为例
            // 针对框架跳转到登陆页面
            this.props.history.push(LOGIN);
        }
        return Promise.reject(error)
    } else {
        message.error('请求超时, 请刷新重试')
        return Promise.reject('请求超时, 请刷新重试')
    }
})

/* 统一封装get请求 */
export const get = (url, params, config = {}) => {
    return new Promise((resolve, reject) => {
        instance({
            method: 'get',
            url,
            params,
            ...config
        }).then(response => {
            resolve(response)
        }).catch(error => {
            reject(error)
        })
    })
}

/* 统一封装post请求  */
export const post = (url, data, config = {}) => {
    return new Promise((resolve, reject) => {
        instance({
            method: 'post',
            url,
            data,
            ...config
        }).then(response => {
            resolve(response)
        }).catch(error => {
            reject(error)
        })
    })
}

http.js

import {get, post} from './request'

export const requestIndex = () => {
    return get('api/index').then((res) => {
        // return res.data
        console.log(res)
    })
}

4、UI组件 

基于React的UI组件在这里推荐两个,一个是蚂蚁金服的Ant Design;另外一个是Material-UI

两个都很不错,这里使用的是Ant Design。

安装

npm install antd --save

修改 src/App.js,引入 antd 的组件

import React from 'react';
import { Button } from 'antd';
import './App.css';
 
const App = () => (
  <div className="App">
    <Button type="primary">Button</Button>
  </div>
);
 
export default App;

修改 src/App.css,在文件顶部引入 antd/dist/antd.css。

5、使用代理proxy(http-proxy-middleware)

安装

npm install http-proxy-middleware

建立 src/setupProxy.js 文件,可配置多个代理

const proxy = require("http-proxy-middleware");

module.exports = function(app) {
    app.use(
        proxy("/base/**", {
            target: "http://45.32.15.21:8090/",
            changeOrigin: true
        })
    );
    app.use(
        proxy("/fans/**", {
            target: "https://easy-mock.com/mock/5c0f31837214cf627b8d43f0/",
            changeOrigin: true
        })
    );
};

参考网址: REACT实战项目从0到1搭建

gitee地址