1 创建React-APP
通过官方的create-react-app,找个喜欢的目录,执行:
npx create-react-app app-name
稍等片刻即可完成安装。安装完成后,可以使用npm或者yarn启动项目。或者直接在vscode中打开项目,执行start命令启动项目。项目文件结构如下图:
2 精简项目
2.1 删除不用的文件
删除后目录如下:
2.2 简化代码
逐个修改以下文件:
src/App.js代码简化如下:
import React from 'react'
function App() {
return (
<div className="App">
<h1>This is React App.</h1>
</div>
)
}
export default App
src/index.js代码简化如下:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))
public/index.html 代码简化如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<!-- <noscript>You need to enable JavaScript to run this app.</noscript> -->
<div id="root"></div>
</body>
</html>
3 项目目录结构
项目目录结构可根据项目实际灵活制定。这里分享下我常用的结构,仅供参考。
├─ /node_modules
├─ package.json
├─ /public
| ├─ favicon.ico <-- 网页图标
| └─ index.html <-- HTML页模板
├─ README.md
├─ /src
| ├─ /common <-- 全局公用目录
| | ├─ /fonts <-- 字体文件目录
| | ├─ /images <-- 图片文件目录
| | ├─ /js <-- 公用js文件目录
| | └─ /style <-- 公用样式文件目录
| | | ├─ frame.css <-- 全部公用样式(import其他css)
| | | ├─ reset.css <-- 清零样式
| | | └─ global.css <-- 全局公用样式
| ├─ /components <-- 公共模块组件目录
| | ├─ /Header <-- 头部导航模块
| | | ├─ index.js <-- header主文件
| | | └─ style.module.css <-- header样式文件
| | └─ ... <-- 其他模块
| ├─ /pages <-- 页面组件目录
| | ├─ /Home <-- home页目录
| | | ├─ index.js <-- home主文件
| | | └─ style.module.css <-- home样式文件
| | ├─ /Login <-- login页目录
| | | ├─ index.js <-- login主文件
| | | └─ style.module.css <-- login样式文件
| | └─ ... <-- 其他页面
| ├─ App.js <-- 项目主模块
| └─ index.js <-- 项目入口文件
└─ yarn.lock
3.1 支持jsx和sass
本人更喜欢用jsx,会把App.js 和 page , components的组件都改错jsx,直接改后缀名即可。
使用npm 或 yarn安装sass
npm install node-sass
安装好后,直接把项目里的.css 改成 .scss 就可以了。
此时项目目录结构如下:
3.2 使用classnames库
看个人习惯或项目要求,可用可不用
安装
npm install classnames
使用
import React from "react";
import styles from './style.module.scss'
import classNames from 'classnames/bind'
const Header = props => {
const cx = classNames.bind(styles);
return (
<div className={cx("header-test-1")}> header </div>
)
}
export default Header
4 路由
4.1 页面构建
根据自己的要求创建页面,这里新建了home和login做演示,页面自己创建和编写即可。
4.2 使用react-router-dom
这里是v6的版本,v5版本的这里就不展示了。
安装
npm install react-router-dom
src下新建: /routes/index.js, js内是所有页面,示例代码如下:
import React from 'react';
const Home = React.lazy(() => import('../pages/Home'))
const Login = React.lazy(() => import('../pages/Login'))
const routes = [
{
path: "/home",
component: Home,
},
{
path: "/login",
component: Login,
}
]
export default routes
然后在app.jsx中导入路由,直接上代码:
import React, { Suspense } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Header from './components/Header';
import routes from './routes'
import Home from './pages/Home'
function App() {
const routerRender = (routes) => {
return routes.map((item, index) => {
return (
<Route
key={index}
path={item.path}
element={ <item.element /> }
>
{item.children ? routerRender(item.children) : null}
</Route>
)
});
};
return (
<div className="App">
<Header />
<Suspense fallback={<div></div>}>
<BrowserRouter>
<Routes>
{/* 默认页面 */}
<Route path={"/"} element={<Home />} />
{routerRender(routes)}
{/* 匹配未定义的路由地址 */}
<Route path={"*"} element={<div>暂无此页面</div>} />
</Routes>
</BrowserRouter>
</Suspense>
</div>
);
}
export default App;
4.3 路由跳转
接下来,简单介绍下如果在页面之间进行路由跳转。
在Home页面添加一个用于跳转至Login页的按钮,代码修改如下:
import React, { Fragment, useEffect } from "react";
import styles from './style.module.scss'
import classNames from 'classnames/bind'
import { useNavigate } from 'react-router-dom'
const Home = props => {
let navigate = useNavigate();
const cx = classNames.bind(styles);
return (
<Fragment>
<div className={cx("home-test-1")}> Home </div>
<div onClick={() => navigate('login')}>click me go to Login</div>
</Fragment>
)
}
export default Home
现在,点击按钮进行页面路由跳转已经实现了。
5 Redux及相关插件
Redux是用来做什么的?简单通俗的解释,Redux是用来管理项目级别的全局变量,而且是可以实时监听变量的变化并改变DOM的。当多个模块都需要动态显示同一个数据,并且这些模块从属于不同的父组件,或者在不同的页面中,如果没有Redux,那实现起来就很麻烦了,问题追踪也很痛苦。因此Redux就是解决这个问题的。
redux涉及的内容较多,把各个依赖组件的官方文档都阅读一遍确实不容易消化。本次分享通过一个简单的Demo,把redux、react-redux、redux-thunk、immutable这些依赖组件的使用方法串起来,非常有利于理解。
5.1 安装redux
npm install redux
仅安装redux也是可以使用的,但是比较麻烦。redux里更新store里的数据,需要手动订阅(subscribe)更新,这里就不展开介绍了。可以借助另一个插件(react-redux)提高开发效率。
5.2 安装react-redux
npm install react-redux
react-redux允许通过connect方法,将store中的数据映射到组件的props,省去了store订阅。原state中读取store的属性改用props读取。
5.3 安装redux-thunk
npm install redux-thunk
redux-thunk允许在actionCreators里传递函数类型的数据。这样可以把业务逻辑(例如接口请求)集中写在actionCreator.js,方便复用的同时,可以使组件的主文件更简洁。
5.4创建store
安装以上各种插件后,可以store用来管理状态数据了。
如果项目比较简单,只有一两个页面,可以只创建一个总store管理整体项目。目录结构参考如下:
以下是各文件的代码:
src/store/index.js:
import { createStore, applyMiddleware, compose } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
// 这里让项目支持浏览器插件Redux DevTools
const composeEnhancers = typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose
const enhancer = composeEnhancers(
applyMiddleware(thunk)
);
const store = createStore(
reducer,
enhancer
)
export default store
以上是store的核心代码,支持了Redux DevTools。同时,利用redux的集成中间件(applyMiddleware)功能将redux-thunk集成进来,最终创建了store。
src/store/constants.js:
export const SET_DATA = 'SET_DATA'
创建这个定义常量的文件,是因为方便被下面的reducer.js和actionCreators.js同时引用,便于统一修改和管理。
src/store/actionCreators.js:
import * as constants from './constants'
export const getData = (data) => ({
type: constans.SET_DATA,
data
})
src/store/reducer.js:
import * as constants from './constants'
// 初始默认的state
const defaultState = {
myData: null
}
const reducer = (state = defaultState, action) => {
// 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。
let newState = Object.assign({}, state)
switch(action.type) {
case constants.SET_DATA:
newState.myData = action.data
return newState
default:
return state
}
}
export default reducer
以上代码,在store设置了一个myData。现在,state修改起来还是有点小麻烦,如何更好地解决这个问题,在5.7章节会提到。
到这里,你可能还是不知道Redux怎么用。实际项目中很少只用一个总store库来管理的。因此,在下面章节的分库内容中具体讲述Redux的使用方法。
5.6 复杂项目store分解
当项目的页面较多,如果数据都集中放在一个store里,维护成本将会变高。接下来分享下如何将store分解到各个组件中。
一般来说,每个组件有自己的store(分库),再由src/store作为总集,集成每个组件的store。以Home和Login两个组件为例,分别创建组件自己的store,文件结构跟store总集一致。
目录结构变动如下:
这里只贴出来Home组件的相关代码,其他组件写法相同。
src/pages/Home/store/index.js:
import reducer from './reducer'
import * as actionCreators from './actionCreators'
import * as constants from './constants'
export { reducer, actionCreators, constants}
其实就是把当前组件store(分库)下的其他文件集中起来作为统一输出口。
src/pages/Home/store/constants.js:
const ZONE = 'pages/Home' //ZONE是用来避免与其他组件的constants重名。
export const SET_DATA = ZONE + 'SET_DATA'
src/pages/Home/store/actionCreators.js:
import * as constants from './constants'
export const setData = (data) => ({
type: constants.SET_DATA,
data
})
src/pages/Home/store/reducer.js:
import * as constants from './constants'
// 初始默认的state
const defaultState = {
myHomeData: null
}
const reducer = (state = defaultState, action) => {
// 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。
let newState = Object.assign({}, state)
switch(action.type) {
case constants.SET_DATA:
newState.myHomeData = action.data
return newState
default:
return state
}
}
export default reducer
然后修改项目store总集,删除actionCreators.js和constants.js,index.js不变。
src/store/reducer.js重写如下:
import { combineReducers } from 'redux'
import { reducer as homeReducer } from '../pages/Home/store'
import { reducer as loginReducer } from '../pages/Login/store'
const reducer = combineReducers({
home: homeReducer,
login: loginReducer
})
export default reducer
以上代码的作用就是把Home和Login的store引入,然后通过combineReducers合并在一起,并分别加上唯一的对象key值。
这样的好处非常明显:
- 避免各组件的store数据互相污染。
- 组件独立维护自己的store,减少维护成本。
非常建议使用这种方式维护store。
5.7 安装使用immutable
在5.5章节,提到了store里不能直接修改state,因为state是引用类型,直接修改可能导致监测不到数据变化。
immutable.js从字面上就可以明白,immutable的意思是“不可改变的”。使用immutable创建的数据是不可改变的,对immutable数据的任何修改都会返回一个新的immutable数据,不会改变原始immutable数据。
immutable.js提供了很多方法,非常方便修改对象或数组类型的引用型数据。
安装immutable和redux-immutable,执行:
npm install immutable redux-immutable
然后对代码进行改造:
src/store/reducer.js:
用 import { combineReducers } from 'redux-immutable'
替换 :import { combineReducers } from 'redux'
把combineReducers换成redux-immutable里的。
然后修改src/pages/Home/store/reducer.js:
import * as constants from './constants'
import { fromJS } from 'immutable'
// 初始默认的state
const defaultState = fromJS({
myHomeData: null
})
const getData = (state, action) => {
return state.set('myHomeData', action.data)
}
const reducer = (state = defaultState, action) => {
// 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。
switch(action.type) {
case constants.SET_DATA:
return getData(state, action)
default:
return state
}
}
export default reducer
src/pages/Login/store/reducer.js也一样修改即可。
immutable的介入,就是利用fromJS方法,把原始的JS类型转化为immutable类型。
由于state已经是immutable类型了,可以使用immutable的set方法进行数据修改,并返回一个新的state。代码简洁很多,不需要手动通过Object.assign等方法去复制再处理了。
5.8 对接react-redux与store
下面来对接react-redux与store,让全部组件都能方便引用store。
修改src/index.jsx:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './common/style/frame.scss'
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
,
document.getElementById('root')
)
以上代码就是用react-redux提供的Provider,把store传给了整个App。
在需要使用store的组件中,要使用react-redux提供的connect方法对组件进行包装。
5.9 设置并实时读取Redux变量
以Home为例,修改src/pages/Home/index.jsx:
import React, { Fragment, useEffect } from "react";
import styles from './style.module.scss'
import classNames from 'classnames/bind'
import { useNavigate } from 'react-router-dom'
import { connect } from 'react-redux'
import * as actionCreators from './store/actionCreators'
const Home = props => {
const { myHomeData, setData } = props
let navigate = useNavigate();
const cx = classNames.bind(styles);
useEffect(() => {
}, [])
const goTo = (link) => {
navigate(link);
}
return (
<Fragment>
<div className={cx("home-test-1")}> Home </div>
<div onClick={() => goTo('login')}>click me go to Login</div>
<div className="ipt-con">home store: myData = {myHomeData}</div>
<div className="ipt-con">
<button onClick={() => { setData('123456') }}>更改home store的myData</button>
</div>
</Fragment>
)
}
// 把store中的数据映射到组件的props
const mapStateToProps = (state) => {
return {
// 数组第一个元素的login,对应的是src/store/reducer.js中定义的login分库名称
myHomeData: state.getIn(['home', 'myHomeData']),
}
}
// 把store的Dispatch映射到组件的props
const mapDispatchToProps = (dispatch) => ({
setData(data) {
const action = actionCreators.setData(data)
dispatch(action)
},
})
export default connect(mapStateToProps, mapDispatchToProps)(Home)
关键点说明:
-
注意代码最后一行,export的数据被connect方法包装了。
-
通过mapStateToProps和mapDispatchToProps方法,把store里的state和dispatch都映射到了组件的props。这样可以直接通过props进行访问了,store中数据的变化会直接改变props从而触发组件的视图更新。
-
state.getIn()方法是来自于redux-immutable的。
点击按钮后,可以看到页面中显示的myData发生了变化。
5.10 在Login组件实时读取Redux变量
接下来,要实现在Login组件中实时读取在Home页面设置的myHomeData。
修改src/page/Login/index.jsx:
import React, { Fragment } from "react";
import { connect } from 'react-redux'
import styles from './style.module.scss'
import classNames from 'classnames/bind'
const Login = props => {
const cx = classNames.bind(styles);
// 接收来自父组件及Redux的数据
const { myHomeData } = props
return (
<Fragment>
<div className={cx("login-test-1")}> Login </div>
<div className={cx("login-test-1")}> myHomeData:{myHomeData} </div>
</Fragment>
)
}
// 把store中的数据映射到组件的props
const mapStateToProps = (state) => {
return {
// 数组第一个元素的login,对应的是src/store/reducer.js中定义的login分库名称
myHomeData: state.getIn(['home', 'myHomeData']),
}
}
export default connect(mapStateToProps, null)(Login)
由于在Login中只用到了读取Redux的myLoginData,所以不需要mapDispatchToProps方法了。
这里是通过Redux实时获取的,而非通过父子组件传递方式。因此同样的方式可以在其他页面或者组件中直接使用,无需考虑组件的父子关系。
现在点击“更改home store的myData”,然后点击跳转到login页面后可以发现Login组件可以正常实时获取myHomeData了。
5.11 Redux开发小结
上述Redux相关内容较多,跟着操作一遍好像大概知道了,但又说不清为什么使用这些依赖包。这里做一下小结,便于消化理解。
其实react-redux、redux-thunk、immutable都是围绕如何简化redux开发的。
react-redux是为了简化redux通过订阅方式修改state的繁琐过程。
redux-thunk是为了redux的dispatch能够支持function类型的数据,请回顾8.9章节中login页面代码的mapDispatchToProps。
immutable是为了解决store中的数据不能被直接赋值修改的问题(引用类型数据的变化导致无法监测到数据的变化)
6 基于axios封装公用API库
axios是一款非常流行的API请求工具,先来安装一下。
npm install axios
6.1 封装公用API库
直接上代码。
src/api/request.js:
/**
* 网络请求配置
*/
import axios from "axios";
axios.defaults.timeout = 100000;
// axios.defaults.baseURL = "http://test.mediastack.cn/";
/**
* http request 拦截器
*/
axios.interceptors.request.use(
(config) => {
config.data = JSON.stringify(config.data);
config.headers = {
"Content-Type": "application/json",
};
return config;
},
(error) => {
return Promise.reject(error);
}
);
/**
* http response 拦截器
*/
axios.interceptors.response.use(
(response) => {
if (response.data.errCode === 2) {
console.log("过期");
}
return response;
},
(error) => {
console.log("请求出错:", error);
}
);
/**
* 封装get方法
* @param url 请求url
* @param params 请求参数
* @returns {Promise}
*/
export function get(url, params = {}) {
return new Promise((resolve, reject) => {
axios.get(url, {
params: params,
}).then((response) => {
landing(url, params, response.data);
resolve(response.data);
})
.catch((error) => {
msag(error);
reject(error);
});
});
}
/**
* 封装post请求
* @param url
* @param data
* @returns {Promise}
*/
export function post(url, data) {
return new Promise((resolve, reject) => {
axios.post(url, data).then(
(response) => {
//关闭进度条
resolve(response);
},
(error) => {
msag(error);
reject(error);
}
);
});
}
/**
* 封装patch请求
* @param url
* @param data
* @returns {Promise}
*/
export function patch(url, data = {}) {
return new Promise((resolve, reject) => {
axios.patch(url, data).then(
(response) => {
resolve(response);
},
(err) => {
msag(err);
reject(err);
}
);
});
}
/**
* 封装put请求
* @param url
* @param data
* @returns {Promise}
*/
export function put(url, data = {}) {
return new Promise((resolve, reject) => {
axios.put(url, data).then(
(response) => {
resolve(response.data);
},
(err) => {
msag(err);
reject(err);
}
);
});
}
//统一接口处理,返回数据
export default function (fecth, url, param) {
let _data = "";
return new Promise((resolve, reject) => {
switch (fecth) {
case "get":
console.log("begin a get request,and url:", url);
get(url, param)
.then(function (response) {
resolve(response);
})
.catch(function (error) {
console.log("get request GET failed.", error);
reject(error);
});
break;
case "post":
post(url, param)
.then(function (response) {
resolve(response);
})
.catch(function (error) {
console.log("get request POST failed.", error);
reject(error);
});
break;
default:
break;
}
});
}
//失败提示
function msag(err) {
if (err && err.response) {
switch (err.response.status) {
case 400:
alert(err.response.data.error.details);
break;
case 401:
alert("未授权,请登录");
break;
case 403:
alert("拒绝访问");
break;
case 404:
alert("请求地址出错");
break;
case 408:
alert("请求超时");
break;
case 500:
alert("服务器内部错误");
break;
case 501:
alert("服务未实现");
break;
case 502:
alert("网关错误");
break;
case 503:
alert("服务不可用");
break;
case 504:
alert("网关超时");
break;
case 505:
alert("HTTP版本不受支持");
break;
default:
}
}
}
/**
* 查看返回的数据
* @param url
* @param params
* @param data
*/
function landing(url, params, data) {
if (data.code === -1) {
}
}
6.2请求隔离
所有请求都在此文件里, 这里只已登录接口做示例
src/api/index.js:
import request from "./request"
// import qs from "qs"
let base = { public: "your url", detail: null}
export const login = data => request("post",`${base.public}/login`, data);
export default {
login
}
6.3 组件内使用
...
import { login } from "../../api"
...
let data = {
name: "test"
}
login(data).then(
(res) => {
console.log('zero login res.....')
},
(error) => {
console.log('zero login error.....')
}
)
...
7.其他
有一下需求,请参考本人其他文章
1.多语言切换
2.换肤
...