一、项目介绍
1、界面
2、功能业务
- 登录、注册
- 商品检索:模糊搜索、属性筛选(多选)、价格区间筛选
- 购物流程:购物车 - 订单 - 支付 - 查看订单状态
- 管理员:创建分类、创建商品、订单列表
3、技术栈
⑴、 客户端
- 脚本:TypeScript
- 前端框架:React
- 路由管理:React-router-dom
- 用户界面:Antd
- 全局状态管理:Redux
- 异步状态更新:redux-saga
- 路由状态同步:connected-react-router
- 网络请求:Axios
- 调试工具:redux-devtools-extension
⑵、 服务端
- 脚本:Node.js
- 数据库:Mongodb
- 数据库可视化:Robo 3T
二、基础配置
1、安装 mongodb 数据库(Mac)
⑴、安装 homebrew
Homebrew 是mac系统中的软件包管理器
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
推荐:国内的镜像地址
$ /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"
// 镜像选择推荐选择清华大学TUNA镜像源
⑵、添加 mongodb 仓库源
$ brew tap mongodb/brew
⑶、安装 mongodb
安装前确保系统已经安装 xcode 命令行编译开发工具
$ brew install mongodb-community
$ xcode-select --install
⑷、启动 mongodb
$ brew services run mongodb-community
⑸、启动 mongodb
brew services stop mongodb-community
⑹、文件位置
- 数据库配置文件:/usr/local/etc/mongod.conf
- 数据库文件默认存放位置:/usr/local/var/mongodb
- 日志存放位置:/usr/local/var/log/mongodb/mongo.log
2、安装 数据库可视化 Robo 3T
3、安装 谷歌浏览器(Chrome) 扩展插件
- React Developer Tools - React开发调试工具
- Redux DevTools
三、创建项目
1、打开终端、创建项目
$ npx create-react-app ecommerce-front --template typescript
// 安装依赖
$ npm install antd axios moment redux react-redux react-router-dom redux-saga connected-react-router redux-devtools-extension @types/react-redux @types/react-router-dom
2、用VScode将项目打开
3、将antd的CSS 使用 CDN引入
public文件下的index.html文件
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
// !!!下面添加
<!-- 引用css样式 -->
<link
rel="stylesheet"
href="https://cdn.bootcdn.net/ajax/libs/antd/4.8.3/antd.min.css"
/>
4、初始化目录结构
⑴、删除多余文件
⑵、src文件夹下的 App.tsx
import React from 'react';
function App() {
return <div>App works</div>
}
export default App;
⑶、src文件夹下的 index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'))
5、打开新终端(VScode)、运行项目
$ npm start
6、界面效果
四、配置基础环境
1、配置服务器端API请求地址
⑴、在项目的根目录下新建 .env 文件:
// 生产环境的服务器端 API 地址
REACT_APP_PRODUCTION_API_URL=http://fullstack.net.cn/api
// 开发环境的服务器端 API 地址
REACT_APP_DEVLOPMENT_API_URL=http://localhost/api
在项目中可以通过 process.env.REACT_APP_DEVLOPMENT_API_URL
方式进行访问,但是这样会有弊端,其一是代码过长写起来不方便,其二是如果在代码中将环境写死,当切换环境时改起来也不方便。
解决方案就是将 API 地址写入配置中,根据环境决定使用哪个 API 地址
⑵、在src目录下新建 config.ts 文件:
// 根据环境变量决定使用哪一个值
export let API: string
if (process.env.NODE_ENV === "development") {
API = process.env.REACT_APP_DEVLOPMENT_API_URL!
} else {
API = process.env.REACT_APP_PRODUCTION_API_URL!
}
⑶、src文件夹下的 index.tsx
测试是否配置成功
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// 引入API
import { API } from './config'
// 测试 配置服务器端API请求地址 是否配置成功
console.log(API)
ReactDOM.render(<App />, document.getElementById('root'))
⑷、打印结果(开发环境)
⑸、测试生产环境
按 Ctrl + C 打断项目
// 构建项目
$ npm run build
// 启动构建好的项目
$ serve -s build
检查
2、页面组件初始化和路由初始化
- 在src目录下新建 components 文件夹, 用来放置公共组件
- 在components 文件夹下, 新建 core 文件夹, 用来放置 前台核心组件
- 在components 文件夹下, 新建 admin 文件夹, 用来放置 和管理员相关的页面
⑴、核心组件 - 在core文件夹下创建
生成函数组件的快捷键
// 快捷键生成函数
// 插件 ES7 React/Redux/GraphQL/React-Native snippets
// fafce =>
import React from 'react'
const test = () => {
return (
<div>
</div>
)
}
export default test
①、Home.tsx - 主页组件
// 引入Layout组件
import Layout from './Layout'
const Home = () => {
return <Layout>Home</Layout>
}
export default Home
②、Shop.tsx - 商品列表页面组件
// 引入Layout组件
import Layout from './Layout'
const Shop = () => {
return <Layout>Shop</Layout>
}
export default Shop
③、Layout.tsx - 布局组件
其他页面的组件要作为Layout 的 children 属性存在, 例如:菜单栏
import React, { FC } from 'react'
// Props需要自己定义
interface Props {
// children为react元素
children: React.ReactNode
}
// const layout = ({children}) => {
// Layout是函数性组件, 用 FC表示, 其后跟的是Props的数据类型
const layout: FC<Props> = ({children}) => {
return (
<div>
{/* 标识一下Layout */}
Layout
{children}
</div>
)
}
export default layout
⑵、初始化路由
在 src 文件夹下新建 Routes.tsx
import React from 'react'
import { HashRouter, Switch, Route } from "react-router-dom"
// 引入components组件
import Home from './components/core/Home'
import Shop from './components/core/Shop'
const Routes = () => {
return (
// 调用HashRouter
<HashRouter>
{/* 调用Switch组件 */}
<Switch>
{/* 配置路由规则 */}
{/* 通过Route配置路由, exact为精准匹配 */}
<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 App from './App';
import Routes from "./Routes"
// 引入API
// import { API } from './config'
// 测试 配置服务器端API请求地址 是否配置成功
// console.log(API)
ReactDOM.render(<Routes />, document.getElementById('root'))
⑷、页面效果
npm start运行项目
⑸、运行可能缺少的依赖
npm start运行项目
$ npm i --save-dev @types/react-router-dom
$ npm install --save react-redux
$ npm add connected-react-router
2、全局store初始化
- 在src目录下新建 store 文件夹
- 在store 文件夹下, 新建 index.ts 文件 和 reducers 文件夹
- 在 reducers 文件夹下, 新建 index.ts 和 test.reducer.ts 文件
⑴、test.reducer.ts - 测试的ruducer
// 默认导出一个名为 estReducer 的reducer函数
// reducer有一个参数state, 类型是number, 初始值是0
export default function testReducer(state: number = 0) {
// 将state返回
return state
}
⑵、index.ts(reducers文件夹下) - root reducer
import { combineReducers } from 'redux';
import testReducer from './test.reducer';
// 使用const这个关键词 声明 rootReducer这个常量, 它的值是combineReducers调用的结果
const rootReducer = combineReducers({
// 在调用combineReducers的时候,需要传入一个对象,这个对象有一个属性叫test, test的值是testReducer
test: testReducer,
})
// 通过export default关键字 导出 rootReducer
export default createRootReducer
⑶、index.ts(store文件夹下) - store
import { createStore } from 'redux';
import rootReducer from './reducers/index';
// 这个store就是createStore方法的返回结果
// 在调用createStore方法的时候,需要传入rooReducer
const store = createStore(rootReducer)
// 初始化完毕,导出store
export default store
⑷、src文件夹下的 index.tsx
让其他组件能从store中获取状态
import React from 'react';
import ReactDOM from 'react-dom';
// import App from './App';
import Routes from "./Routes"
// 引入API
// import { API } from './config'
// 测试 配置服务器端API请求地址 是否配置成功
// console.log(API)
// ReactDOM.render(<App />, document.getElementById('root'))
// 改为以Routes为首页
ReactDOM.render(
// 调用Provider组件,其他组件才能从store中获取状态
// 将刚刚创建的store赋值给这个store
<Provider store={store}>
<Routes/>
</Provider>
, document.getElementById('root'))
⑸、Home.tsx - src文件夹 下的components文件夹 下的core文件夹
测试能否获取状态
// 首页组件
// import React from 'react'
import { useSelector } from "react-redux"
// 引入Layout
import Layout from './Layout'
const Home = () => {
// 通过钩子函数获取store的状态,直接返回
const state = useSelector(state => state)
return (
<Layout>
Home
{/* 状态是一个对象,不能直接显示,需要转化成字符串 */}
{ JSON.stringify(state) }
</Layout>
)
}
export default Home
⑹、页面展示
3、将路由状态同步到全局store
⑴、connected-react-router的使用方法
①、在您的root reducer文件中
- 创建一个history以参数为参数并返回根减速器的函数
- router传递history给,将减速器添加到根减速器中connectRouter
- 注意:密钥必须为router
// reducers.js
import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'
const createRootReducer = (history) => combineReducers({
router: connectRouter(history),
... // rest of your reducers
})
export default createRootReducer
②、创建Redux存储区时
- 创建一个history对象
- 将创建的提供history给root reducer创建者
- 如果你想要调度历史动作(例如用push(’/path/to/somewhere’)来改变URL),可以使用routerMiddleware(history)
// 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(
createRootReducer(history), // root reducer with router state
preloadedState,
compose(
applyMiddleware(
routerMiddleware(history), // for dispatching history actions
// ... other middlewares ...
),
),
)
return store
}
③、第三步
- 将您的react-router v4 / v5路由包装起来ConnectedRouter,并将该history对象作为道具传递。切记删除的任何用法BrowserRouter或将NativeRouter其保留会导致 同步状态时出现问题
- 将ConnectedRouter作为react-redux的Provider的子节点
- 注意:如果执行服务器端渲染,则仍应在服务器上使用StaticRouterfrom react-router
// 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')
)
⑵、index.ts(reducers文件夹下)
import { connectRouter } from 'connected-react-router';
import { combineReducers } from 'redux';
import testReducer from './test.reducer';
import { History } from 'history';
// 使用const这个关键词 声明 rootReducer这个常量, 它的值是combineReducers调用的结果
// const rootReducer = combineReducers({
// 这个history需要传递过来, 当前的值会变成一个方法, 这个方法会接收一个名为history的参数, 参数的类型就是History
const createRootReducer = (history: History) =>
combineReducers({
// 在调用combineReducers的时候,需要传入一个对象,这个对象有一个属性叫test, test的值是testReducer
test: testReducer,
// 添加属性touter, 它的值是connectRouter方法的调用
// 调用这个方法的时候需要传入一个history
router: connectRouter(history)
})
// 通过export default关键字 导出 rootReducer
export default createRootReducer
⑶、index.ts(store文件夹下)
// store
import { createStore, applyMiddleware } from 'redux';
// import rootReducer from './reducers/index';
import createRootReducer from './reducers';
import { createHashHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
//通过export const关键字 导入 history, 它的值就是createHashHistory方法的调用
export const history = createHashHistory()
// 这个store就是createStore方法的返回结果
// 在调用createStore方法的时候,需要传入rooReducer
// 传入参数history
const store = createStore(
createRootReducer(history),
// 传入applyMiddleware
// routerMiddleware的作用就是监听路由状态, 当路由状态更改的时候, 去description一个action
applyMiddleware(routerMiddleware(history))
)
// 初始化完毕,导出store
export default store
⑷、index.tsx(src文件夹下)
// import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from "react-redux"
import Routes from "./Routes"
import store from "./store/index"
import { ConnectedRouter } from "connected-react-router"
import { history } from './store'
// import App from './App';
// 引入API
import { API } from './config'
console.log(API)
// ReactDOM.render(<App />, document.getElementById('root'))
// 改为以Routes为首页
ReactDOM.render(
// 调用Provider组件,其他组件才能从store中获取状态
// 将刚刚创建的store赋值给这个store
<Provider store={store}>
{/* 添加一个组件, 在组件中传入history */}
<ConnectedRouter history={history}>
<Routes/>
</ConnectedRouter>
</Provider>
, document.getElementById('root'))
⑸、页面展示
⑹、测试 - 改变请求头
http://localhost:3000/#/ => http://localhost:3000/#/?name=zhangsan
⑺、路由发生改变能否更新
Shop.tsx - core文件夹下
// import React from 'react'
import { useSelector } from 'react-redux'
// 引入Layout组件
import Layout from './Layout'
const Shop = () => {
// 测试路由切换的时候,状态是不是能更新
const state = useSelector(state => state)
return (
<Layout>
Shop
{/* 状态是一个对象,不能直接显示,需要转化成字符串 */}
{ JSON.stringify(state) }
</Layout>
)
}
export default Shop