页面预览
该实战项目是(不怎么严谨的)电子商务网站。
首页
商品列表页面
登录页面
注册页面
购物车列表
支付完成页面
Dashboard 页面
普通用户页面
购买历史页面
资料更新页面
管理员页面
创建分类页面
创建商品页面
订单列表页面
显示所有用户的订单
客户端技术栈介绍
- 脚本:TypeScript
- 前端框架:React
- 路由管理:react-router-dom
- 用户界面:Ant Design
- 全局状态管理:Redux
- 一部状态更新:redux-saga
- 路由状态同步:connected-react-router
- 网络请求:Axios
- 日期处理工具:Moment
- 调试工具:redux-devtools-extension
创建客户端项目
创建使用 TypeScript 的项目
npx create-react-app ecommerce-front --template typescript
cd ecommerce-front
# 启动项目
npm start
安装项目依赖
npm i @types/react react-router-dom @types/react-router-dom antd redux react-redux @types/react-redux redux-saga connected-react-router axios redux-devtools-extension moment
引入 Antd 样式表
可以引入项目本地模块中的样式表文件,也可以从 CDN 平台引入。
本地文件方式
在 src/index.tsx
中引入
import 'antd/dist/antd.css'
CDN 方式
以 cdnjs 平台为例,修改 public/index.html
,添加 link
标签:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.16.8/antd.min.css" />
删除不需要的文件
├─ src
│ ├─ App.css
│ ├─ App.test.tsx
│ ├─ index.css
│ ├─ logo.svg
│ ├─ reportWebVitals.ts
│ └─ setupTests.ts
删除不需要的代码
// src\index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import '~antd/dist/antd.css'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
// src\App.tsx
function App() {
return <div>App works</div>
}
export default App
配置服务器端 API 请求地址
create-react-app 脚手架内置了 dotenv,允许开发者在 React 项目中配置环境变量,环境变量的名称要求以 REACT_APP_
开头,在项目中通过 process.env.REACT_APP_<name>
访问。
在项目根目录下新建 .env
文件:
# .env
# 以 `#` 开头的行被视为注释
# 生产环境的服务器端 API 地址 应该在 `npm run build` 构建项目时使用
REACT_APP_PRODUCTION_API_URL = http://xxx.com/api
# 开发环境的服务器端 API 地址 应该在 `npm start` 启动项目时使用
REACT_APP_DEVELOPMENT_API_URL = http://localhost/api
直接使用 process.env
访问 API 地址会将环境写死,为了使其根据环境决定使用哪个 API 地址,可以将 API 地址写入配置中。
新建 src/config.ts
文件:
// src\config.ts
export let API: string
if (process.env.NODE_ENV === 'development') {
// 在值后添加 `!` 后缀断言值不会是 `null` 或 `undefined` 省略检查
// https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#non-null-assertion-operator-postfix-
API = process.env.REACT_APP_DEVELOPMENT_API_URL!
} else {
API = process.env.REACT_APP_PRODUCTION_API_URL!
}
安装 chrome 扩展
安装扩展
- React Developer Tools:检查 React 组件层次结构、props、hooks 等信息,在页面上显示 React 组件
- Redux DevTools:监测 Store 中状态的变化
Chrome 网上应用店访问受限,可以使用 Microsoft Edge 浏览器,不需要翻墙。
React Developer Tools
Redux DevTools
配置 Redux DevTools
安装完扩展,还需要修改代码,在创建 store 时用 Redux DevTools 的 composeWithDevTools
包裹下 applyMiddleware
,该方法来自安装的 redux-devtools-extension
模块。
import { composeWithDevTools } from 'redux-devtools-extension'
export const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(...middlewares))
)
页面组件初始化和路由初始化
创建文件和文件夹
文件命名规范建议(参考 Taro):
- 普通 TS 文件以
.ts
作为后缀 - 组件文件以
.tsx
作为后缀
在 src
目录下添加:
├─ components
│ ├─ admin # 存放登录后访问页面的文件夹
│ └─ core # 存放前台页面组件的文件夹
│ ├─ Layout.tsx # 布局组件
│ ├─ Home.tsx # 首页
│ └─ Shop.tsx # 商品列表页
└─ Routes.tsx # 路由组件
布局组件
// src\components\core\Layout.tsx
import React, { FC } from 'react'
// 定义 Layout 组件参数类型的接口
interface Props {
children: React.ReactNode
}
// FC 表示函数型组件类型
const Layout: FC<Props> = ({ children }) => {
return <div>Layout {children}</div>
}
export default Layout
首页
// src\components\core\Home.tsx
import Layout from './Layout'
const Home = () => {
return <Layout>Home</Layout>
}
export default Home
商品列表页
// src\components\core\Shop.tsx
import Layout from './Layout'
const Shop = () => {
return <Layout>Shop</Layout>
}
export default Shop
路由组件
// src\Routes.tsx
import { HashRouter, Route, Switch } from 'react-router-dom'
import Home from './components/core/Home'
import Shop from './components/core/Shop'
const Routes = () => {
return (
<HashRouter>
<Switch>
<Route path="/" component={Home} exact />
<Route path="/shop" component={Shop} />
</Switch>
</HashRouter>
)
}
export default Routes
修改入口文件
// src\index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import 'antd/dist/antd.css'
import Routes from './Routes'
ReactDOM.render(
<React.StrictMode>
<Routes />
</React.StrictMode>,
document.getElementById('root')
)
访问页面
npm start
运行,访问:
http://localhost:3000/
http://localhost:3000/#/shop
全局 Store 初始化
创建文件和文件夹
在 src
下添加:
├─ store
│ ├─ reducers
│ │ ├─ index.ts
│ │ └─ test.reducer.ts # 测试 reducer
│ └─ index.ts
// src\store\reducers\test.reducer.ts
export default function testReducer(state: number = 0) {
return state
}
// src\store\reducers\index.ts
import { combineReducers } from 'redux'
import testReducer from './test.reducer'
const rootReducer = combineReducers({
test: testReducer
})
export default rootReducer
// src\store\index.ts
import { createStore } from 'redux'
import rootReducer from './reducers'
const store = createStore(rootReducer)
export default store
注入全局
// src\index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import 'antd/dist/antd.css'
import Routes from './Routes'
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<Routes />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
测试
// src\components\core\Home.tsx
import { useSelector } from 'react-redux'
import Layout from './Layout'
const Home = () => {
const state = useSelector(state => state)
return <Layout>Home {JSON.stringify(state)}</Layout>
}
export default Home
访问首页显示:Layout Home {"test":0}
将路由状态同步到全局 Store
connected-react-router 文档
connected-react-router 用于将路由状态同步到 Store。
第一步
在 Root Reducer 文件中,
- 将 rootReducer 更改为创建 rootReducer 的函数(createRootReducer),将历史记录(history)作为参数接收。
- 内部通过向
connectRouter
函数传递 history 实例对象创建 routerReducer ,并添加到返回的 rootReducer 中。 - key 必须是
router
// reducers.js
import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'
// 导出的是一个创建 rootReducer 的函数
const createRootReducer = (history) => combineReducers({
// connectRouter 返回一个 routerReducer
router: connectRouter(history),
// 其余的 reducers
... // rest of your reducers
})
export default createRootReducer
第二步
当创建 Store 时,
- 创建一个 history 实例对象,并导出
- 通过调用
createBrowserHistory/createHashHistory
方法创建 history 实例对象 createBrowserHistory/createHashHistory
是 history 模块提供的 API。- history 模块是 react-router-dom (除了 React 本身)仅有的两个主要依赖项之一
- 它提供了用于在 JavaScript 中各种环境下管理 history 的实现。
- 官方文档:ReactTraining/history
- React Router 文档:history
- 通过调用
- 向
createRootReducer
函数传递 history 对象,创建的 rootReducer 传递给createStore
方法 - 配置用于派发 history actions 的中间件
routerMiddleware()
- 该中间件通过调用
routerMiddleware
生成,方法来自connected-react-router
- 传递 history 对象
- 中间件的作用是监听路由状态,当路由状态更改的时候 dispatch 一个 action
- 该中间件通过调用
// configureStore.js
...
import { createBrowserHistory } from 'history'
import { applyMiddleware, compose, createStore } from 'redux'
import { routerMiddleware } from 'connected-react-router'
import createRootReducer from './reducers'
...
export const history = createBrowserHistory()
export default function configureStore(preloadedState) {
const store = createStore(
// 第一步编写的追加了 routerReducer 的 rootReducer
createRootReducer(history), // root reducer with router state
preloadedState,
compose(
applyMiddleware(
// 用于派发历史记录操作的中间件
routerMiddleware(history), // for dispatching history actions
// ... 其它中间件 ...
),
),
)
return store
}
第三步
- 用
ConnectedRouter
组件包裹根组件,并将第二步创建的 history 对象传递给组件- 该组件用于让内部组件可以获取路由状态
- 记得删除
BrowserRouter
、HashRouter
或NativeRouter
。 - 将
ConnectedRouter
组件作为 react-redux 的Provider
子级放置 - 注意:如果进行服务器端渲染,仍然应该使用
StaticRouter
// index.js
...
import { Provider } from 'react-redux'
import { Route, Switch } from 'react-router' // react-router v4/v5
import { ConnectedRouter } from 'connected-react-router'
import configureStore, { history } from './configureStore'
...
const store = configureStore(/* provide initial state if any */)
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
<> { /* your usual react-router v4/v5 routing */ }
<Switch>
<Route exact path="/" render={() => (<div>Match</div>)} />
<Route render={() => (<div>Miss</div>)} />
</Switch>
</>
</ConnectedRouter>
</Provider>,
document.getElementById('react-root')
)
注意:提供给 routerReducer、routerMiddleware 和 ConnectedRouter 组件的 history 对象必须是同一个对象。
修改代码
第一步
// src\store\reducers\index.ts
import { connectRouter } from 'connected-react-router'
import { History } from 'history'
import { combineReducers } from 'redux'
// import testReducer from './test.reducer'
// 定义一个包含 router 的 store 类型接口 供外部使用
export interface AppState {
router: RouterState
}
const createRootReducer = (history: History) =>
combineReducers({
// test: testReducer,
router: connectRouter(history)
})
export default createRootReducer
第二步
// src\store\index.ts
import { applyMiddleware, createStore } from 'redux'
import createRootReducer from './reducers'
import { createHashHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'
export const history = createHashHistory()
const store = createStore(createRootReducer(history), applyMiddleware(routerMiddleware(history)))
export default store
第三步
// src\index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import 'antd/dist/antd.css'
import Routes from './Routes'
import { Provider } from 'react-redux'
import store, { history } from './store'
import { ConnectedRouter } from 'connected-react-router'
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<ConnectedRouter history={history}>
<Routes />
</ConnectedRouter>
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
测试
在 Shop 页面也输入 Redux 状态:
// src\components\core\Shop.tsx
import { useSelector } from 'react-redux'
import Layout from './Layout'
const Shop = () => {
const state = useSelector(state => state)
return <Layout>Shop {JSON.stringify(state)}</Layout>
}
export default Shop
# 访问 `http://localhost:3000/` 输出:
Layout Home {"test":0,"router":{"location":{"pathname":"/","search":"","hash":"","query":{}},"action":"POP"}}
# 访问 `http://localhost:3000/#/shop?id=1` 输出:
Layout Shop {"test":0,"router":{"location":{"pathname":"/shop","search":"?id=1","hash":"","query":{"id":"1"}},"action":"POP"}}
配置 Redux DevTools 插件
Redux DevTools 插件需要使用composeWithDevTools
包裹 applyMiddleware
:
// src\store\index.ts
import { applyMiddleware, createStore } from 'redux'
import createRootReducer from './reducers'
import { createHashHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'
import { composeWithDevTools } from 'redux-devtools-extension'
export const history = createHashHistory()
const store = createStore(createRootReducer(history), composeWithDevTools(applyMiddleware(routerMiddleware(history))))
export default store