###购物车来啦
2020年疫情刚刚结束,好久没写React了,有点慌,为了不遗漏知识点,查漏补缺,写个简单的购物车,基本的功能有列表、新增、修改、删除
开发用的框架React+Antd+Koa+Mysql,开发工具hbuilder-x,上传代码gitee
好啦,开始写啦
1:新建一个文件夹,起名shopWeb,里面新建shop文件夹,存放前台所有代码,新建shopServer文件夹,存放后台接口api所有文件
2:先讲解前台部分吧,进入shop文件夹,安装项目,用官方提供的脚手架初始化一下
create-react-app shop
等待安装完成后,基本的脚手架就搭建完了
下面安装所有依赖
cnpm i antd axios react-router-dom babel-plugin-import --save
这个就是需要的安装包
这里antd最好用按需加载的方式动态引入css,我这里偷懒就不用了
3:要实现的效果基本如下图
我们先把路由定义好了在说,
src下面新建一个router文件夹,下面新建router.js
import React from 'react'
//导入路由所有需要的包 必须这样写 否则报错
import {BrowserRouter as Router,Link,Route} from 'react-router-dom';
import './router.css';
import Shop from '../page/shop.js';
import User from '../page/user.js'
import Add from '../page/add.js';
import Edit from '../page/edit.js'
var router = ()=>{
return(
<Router>
<ul className="router">
<li><Link to="/shoplist" >去购物车</Link></li>
<li><Link to="/user" >去个人中心</Link></li>
</ul>
<div className="linkTag">
<Link to="/shopAdd" className="add" >新增</Link>
<Link to="/shopEdit/66" className="edit">修改测试</Link>
</div>
<Route exact path='/shoplist' component={Shop}></Route>
<Route path='/user' component={User}></Route>
<Route path='/shopAdd' exact component={Add}></Route>
<Route path='/shopEdit/:id' exact component={Edit}></Route>
</Router>
)
}
export default router;
先配置好路由才可以哦,这样才可以切换呀~~~
记得要在index.js里面引入这个路由js哦,否则无法生效哦
当然也可以直接在app.js中直接映入也是可以的
4:src下面新建page文件夹,存放所有页面js
新建一个shop.js这就是我们的主页面,列表页面
基本代码如下
import React,{Component} from 'react';
import { BrowserRouter as Router,Route,Link,withRouter} from 'react-router-dom'
import '../assets/page/shop.css';
import axios from 'axios';
import { Table, Tag ,message,Modal,Button} from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons'
//注意 setState({}) 我在这里浪费一个小时 = 导致浪费了1个小时找错误
const { confirm } = Modal;
let api = "http://localhost:9090/";
//import historyRouter from '../history/index.js';
class Shop extends Component{
constructor(props){
super(props);
this.state={
isShow:false,
username:"",
list:[],
columns: [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '价格',
dataIndex: 'price',
key: 'price'
},
{
title: '数量',
dataIndex: 'number',
key: 'number'
},
{
title: '操作',
key: 'action',
render: (text, record) => (
<span className="actionBtn">
<Button type="primary" className="btn editBtn" onClick={this.goEdit.bind(this,record)}>编辑</Button>
<Button type="primary" danger className="btn deleteBtn" onClick={this.deleteOne.bind(this,record)} >删除</Button>
</span>
),
}
]
};
}
//初始化查询列表
async getData(){
let res = await axios.get(api+"list");
this.setState({
list:res.data.data
})
}
goEdit(event){
let id = event.id;
//let { history } = this.props;
//history.push("/shopEdit/"+id);
this.props.history.push({pathname:'/shopEdit/'+id});
}
//删除
deleteOne(event){
let id = event.id;
var that = this;
confirm({
title: '确定删除这条记录么?',
icon: <ExclamationCircleOutlined />,
content: '删除后不可恢复哦~~~',
onOk() {
console.log('OK');
axios.get(api+"delete/"+id).then(res=>{
var result = res.data.data;
if(res.data.code==1){
message.success('删除成功');
//再次查询
that.getData();
}else{
message.success('删除失败');
};
});
},
onCancel() {
console.log('Cancel');
},
});
}
//组件初始化
componentDidMount(){
this.getData();
}
componentWillReceiveProps(nextProps){
if (nextProps.location.pathname != this.props.location.pathname) {
this.getData();
}
}
render(){
return (
<div>
<Table dataSource={this.state.list} columns={this.state.columns} />
</div>
)
}
}
export default withRouter(Shop);
分享我遇到的坑:
1:antd的table展示,最好有key,不然会一直提醒你
2:列表跳转到Add路由时候,最好用路由,不要用变量显示隐藏,不然刷新就没有了,如果用BrowserRouter就需要后台配合
3:路由最好单独提出去写,否则会乱七八糟。
4:在点击修改的时候,路由跳转遇到问题,
this.props.history.push({pathname:'/shopEdit/'+id});
提示找不到props,最后用了withRouter才解决,当然也可以用别的办法,比如自定义history路由
—好了,接下来讲解新增吧,新建add.js
import React,{Component,useEffect,useState} from 'react'
import axios from 'axios'
import { Modal, Button,Input,message } from 'antd';
import '../assets/page/add.css';
import { withRouter } from 'react-router-dom'
class Add extends Component{
constructor(props) {
super(props);
this.state={
ModalText: 'Content of the modal',
visible: true,
confirmLoading: false,
username:'',
price:'',
number:''
}
};
showModal = () => {
this.setState({
visible: true
});
};
handleOk = () => {
console.log(this.username.value);
console.log(this.price.value);
console.log(this.number.value);
let name = this.username.value;
let price = this.price.value;
let number= this.number.value;
let key = null;
if(!name){
message.error('必须输入名称');
return;
}else if(!price){
message.error('必须输入价格')
return;
}else if(!number){
message.error('必须输入数量')
return;
}else{
}
let { history } = this.props
//var that = this;
let api = "http://localhost:9090/";
/*
axios.get(api+"add?name="+name+"&price="+price+"&number="+number).then(res=>{
let code = res.data.code;
if(code ==1){
message.success('添加成功');
this.setState({
ModalText: 'The modal will be closed after two seconds',
confirmLoading: true,
});
setTimeout(() => {
this.setState({
visible: true,
confirmLoading: false
});
window.location.href="/shoplist";
//history.push({pathname: '/shoplist'})
}, 2000);
}
});
*/
let postData={"name":name,"price":price,"number":number};
axios.post(api+"add",postData).then(res=>{
let code = res.data.code;
if(code ==1){
message.success('添加成功');
this.setState({
ModalText: 'The modal will be closed after two seconds',
confirmLoading: true,
});
setTimeout(() => {
this.setState({
visible: true,
confirmLoading: false
});
window.location.href="/shoplist";
//history.push({pathname: '/shoplist'})
}, 2000);
}
});
};
handleCancel = () => {
console.log('Clicked cancel button');
this.setState({
visible: false
});
};
render(){
const { visible, confirmLoading, ModalText } = this.state;
return(
<div className="dialog">
<Modal
title="Title"
visible={visible}
onOk={this.handleOk}
confirmLoading={confirmLoading}
onCancel={this.handleCancel}
>
<form>
<input type='text' placeholder="商品名称" defaultValue={this.state.username} ref={input => this.username = input} />
<input type='text' placeholder="商品价格" defaultValue={this.state.price} ref={input => this.price = input} />
<input type='text' placeholder="商品数量" defaultValue={this.state.number} ref={input => this.number = input} />
</form>
</Modal>
</div>
)
}
}
export default Add;
新增这里注意的是,表单的可控组件和不可控,注意要defaultValue的使用,获取值我用的是ref
接下来是修改,新建edit.js
代码如下
import React,{Component,useEffect,useState} from 'react'
import axios from 'axios'
import { Modal, Button,Input,message } from 'antd';
import '../assets/page/add.css';
import { withRouter } from 'react-router-dom'
class Edit extends Component{
constructor(props) {
super(props);
this.state={
sendId:0,
ModalText: 'Content of the modal',
visible: true,
confirmLoading: false,
username:'',
price:'',
number:''
}
};
async initGetData(){
let api = "http://localhost:9090/";
let sendId=this.props.match.params.id ;
//query 传值
//let {location} = this.props;
//let {id} =location.query;
let res = await axios.get(api+"select/"+sendId);
if(res.data.code ==1){
let data = res.data.data[0];
let name = data.name;
let price = data.price;
let number= data.number;
this.setState({
sendId:sendId,
username:name,
price:price,
number:number
})
};
}
componentDidMount(){
this.initGetData()
}
showModal = () => {
this.setState({
visible: true
});
};
handleOk = () => {
let name = this.state.username;
let price = this.state.price;
let number= this.state.number;
let key = null;
if(!name){
message.error('必须输入名称');
return;
}else if(!price){
message.error('必须输入价格')
return;
}else if(!number){
message.error('必须输入数量')
return;
}else{
}
console.log(name,price,number);
let { history } = this.props
//var that = this;
let api = "http://localhost:9090/";
/*
axios.get(api+"add?name="+name+"&price="+price+"&number="+number).then(res=>{
let code = res.data.code;
if(code ==1){
message.success('添加成功');
this.setState({
ModalText: 'The modal will be closed after two seconds',
confirmLoading: true,
});
setTimeout(() => {
this.setState({
visible: true,
confirmLoading: false
});
window.location.href="/shoplist";
//history.push({pathname: '/shoplist'})
}, 2000);
}
});
*/
let postData={"id":this.state.sendId * 1,
"name":name,
"price":price,
"number":number,
"mark":Math.floor(Math.random()*100)};
axios.post(api+"update",postData).then(res=>{
let code = res.data.code;
if(code ==1){
message.success('修改成功');
this.setState({
ModalText: 'The modal will be closed after two seconds',
confirmLoading: true,
});
setTimeout(() => {
this.setState({
visible: true,
confirmLoading: false
});
history.push({pathname: '/shoplist'})
}, 2000);
}
});
};
handleCancel = () => {
console.log('Clicked cancel button');
this.setState({
visible: false
});
};
//值改变输入
changeValue(event){
let name =event.target.name;
let value =event.target.value
this.setState({
[name]:value
})
}
render(){
const { visible, confirmLoading, ModalText } = this.state;
return(
<div className="dialog">
<Modal
title="请输入内容"
visible={visible}
onOk={this.handleOk}
confirmLoading={confirmLoading}
onCancel={this.handleCancel}
>
<form>
<Input placeholder="输入名称" value={this.state.username} name="username" onChange={this.changeValue.bind(this)} />
<Input placeholder="输入价格" value={this.state.price} name="price" onChange={this.changeValue.bind(this)} />
<Input placeholder="输入数量" value={this.state.number} name="number" onChange={this.changeValue.bind(this)} />
</form>
</Modal>
</div>
)
}
}
export default Edit;
修改我用的是antd的Input组件,然后用的onChange结合value实现,初始化填值,输入改变值存储来实现的,这里为了方便用name,动态传递参数
<form>
<Input placeholder="输入名称" value={this.state.username} name="username" onChange={this.changeValue.bind(this)} />
<Input placeholder="输入价格" value={this.state.price} name="price" onChange={this.changeValue.bind(this)} />
<Input placeholder="输入数量" value={this.state.number} name="number" onChange={this.changeValue.bind(this)} />
</form>
这里绑定事件,注意this的指向
-this指向绑定有3中方法哦
1:点击的时候绑定
onChange={this.changeValue.bind(this)}
2:构造器里面绑定
this.changeValue=this.changeValue.bind(this)
3:点击的时候箭头函数绑定
onChange={()=>this.changeValue}
好了,接着讲表单输入取值,这里name注意是动态的
//值改变输入
changeValue(event){
let name =event.target.name;
let value =event.target.value
this.setState({
[name]:value
})
}
对啦,忘记讲解一下修改页面,如何动态路由传递参数啦
A:首先得再路由中定义一下呗,看图
这里用的params传递参数
B:然后再shop.js点击修改的时候,方法里面
当然也可以用简单写法
history.push("/shopEdit/"+id);
或者query也可以哦
this.props.history.push({pathname:'/shopEdit',query:{id:id}});
看你自己的喜欢啦~~~~
然后再edit.js里面就是获取路由传递过来的参数呗
如果是pamrms传递过来就是这么取值
let sendId=this.props.match.params.id ;
如果是query传递过来就这么取值
//query 传值
let {location} = this.props;
let {id} =location.query;
贴图说明
这里基本就完成啦
然后就是获取id,请求后台接口,获取后台数据,填充到页面就好啦
#####现在来讲解后台啦
1:当然先安装mysql数据库啦,然后phpstudy管理工具
建立好shop表,字段如上,明白人一看就懂了吧~~~
2:进入shopServer文件夹,安装所有包
cnpm i koa koa-bodyparser koa-router koa2-cors mysql nodemon --save
说明一下:
nodemon是一个自动修改配置重启生效的工具很好用
koa2-cors是跨域会用到的,前端访问后端会遇到跨域问题
类似上面这样,等会儿我们来解决吧~~~
安装好所有依赖后,先写个接口试试呗
3:好就先列表接口呗
src下面新建index.js
//列表
router.get("/list",async (ctx)=>{
let results= await mysqlData.query();
if(results.length > 0){
ctx.body={
"code":1,
"msg":"ok",
"data":results
}
}else{
ctx.body={
"code":0,
"msg":"fail",
"data":"获取失败"
}
}
});
这里就是连接数据库,查询数据啦
这里连接数据库,启动端口9090
var mysql = require('mysql')
var mysqlConfig = require('../config/config.js')
//列表查询
query(){
return new Promise((resolve,reject)=>{
mysqlConfig.getConnection((err,connection)=>{
const listSql = "select * from shop";
connection.query(listSql,(err,results,fields)=>{
if(err){
throw err
};
resolve(results);
})
})
})
}
启动后效果图
看到启动成功~~~~
虽然启动成功啦,但是前台访问后台跨域问题还没解决,怎么办了?
如果有webpack当然,跨域配置proxy,但是我们这里没有,我们用koa2-cors这个中间来处理下
var cors = require('koa2-cors');
// 配置跨域
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With')
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET');
ctx.set('Access-Control-Allow-Credentials', true);
ctx.set('Access-Control-Max-Age', 3600 * 24);
await next();
})
app.use(cors({
// origin: function(ctx) {
// if (ctx.url === '/test') {
// return false;
// }
// return '*';
// },
origin:'http://127.0.0.1:3000',
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5,
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));
app.use(router.routes()); /*启动路由*/
app.use(router.allowedMethods());
app.listen("9090");
console.log('app started ...');
最重要的origin,这里我们的跨域就配置成功啦!!
查询接口完成,接下来就是删除mysql文件下index.js
//删除
delete(id){
return new Promise((resolve,reject)=>{
mysqlConfig.getConnection((err,connection)=>{
const deleteSql = "delete from shop where id =" +id;
connection.query(deleteSql ,(err,results,fields)=>{
if(err){
throw err
};
console.log(results);
resolve(results)
})
})
})
}
删除调用
//删除
router.get("/delete/:id",async (ctx)=>{
let id = ctx.params.id;
let data2 = await mysqlData.delete(id);
let affectedRows = data2.affectedRows;
if(affectedRows==1){
ctx.body={
"code":1,
"msg":"ok",
"data":"删除成功"
}
}else{
ctx.body={
"code":0,
"msg":"fail",
"data":"删除失败"
}
}
});
注意因为所有的数据库操作都是异步,所以我们必须用promise异步处理下,然后返回处理结果才可以
新增接口
//新增数据
add(name,price,number,key){
return new Promise((resolve,reject)=>{
mysqlConfig.getConnection((err,connection)=>{
const addSql = `insert into shop values(null,${name},${price},${number},${key},null)`;
connection.query(addSql,(err,results, fields)=>{
if(err){
throw err
}
console.log("result", results)//表中数据
resolve(results)
connection.release()
})
});
})
}
新增调用
//用post请求
router.post("/add",async (ctx)=>{
let body=ctx.request.body;
let name=ctx.request.body.name;
let price=ctx.request.body.price;
let number=ctx.request.body.number;
let key= Math.floor(Math.random()*100);
//调取数据库插入方法
let isSuccess = await mysqlData.add(name,price,number,key);
let insertId = isSuccess.insertId;
if(insertId){
ctx.body={
"code":1,
"msg":"ok",
"data":"商品添加成功"
}
}else{
ctx.body={
"code":0,
"msg":"fail",
"data":"商品失败"
}
}
});
注意这里的新增是post提交请求,获取数据必须用到koa-bodyparser这个中间件才可以哦
修改前查询接口
//修改查询
select(id){
return new Promise((resolve,reject)=>{
mysqlConfig.getConnection((err,connection)=>{
let selectSql = 'select * from shop where id='+id;
connection.query(selectSql,(err,results,fields)=>{
if(err){
throw err
};
//console.log(results)
resolve(results);
})
})
})
}
修改前查询调用
//修改 先根据id查询结果 1
router.get("/select/:id",async (ctx)=>{
let id = ctx.params.id;
let result = await mysqlData.select(id);
if(result.length > 0){
ctx.body={
"code":1,
"msg":"ok",
"data":result
}
}else{
ctx.body={
"code":0,
"msg":"faile",
"data":"查询失败"
}
}
});
修改更新接口
//修改更新数据
update(sendId,name,price,number,mark){
return new Promise((resolve,reject)=>{
mysqlConfig.getConnection((err,connection)=>{
const updateSql = `update shop set name = "${name}",price = ${price},number = ${number},mark=${mark} where id = ${sendId}`;
connection.query(updateSql,(err,results, fields)=>{
if(err){
throw err
}
console.log("result", results)//表中数据
resolve(results)
connection.release()
})
});
})
}
修改更新接口调用
router.post("/update",async (ctx)=>{
//body中间件解析post提交数据
let body=ctx.request.body;
let sendId = ctx.request.body.id * 1 ;
let name=ctx.request.body.name;
let price=ctx.request.body.price;
let number=ctx.request.body.number;
let key= Math.floor(Math.random()*100);
let mark= Math.floor(Math.random()*100);
let isSuccess = await mysqlData.update(sendId,name,price,number,mark);
if(isSuccess.affectedRows == 1){
ctx.body={
"code":1,
"msg":"ok",
"data":"商品修改成功"
}
}else{
ctx.body={
"code":0,
"msg":"fail",
"data":"商品修改失败"
}
}
});
注意接口写完了,可以用postmon先测试下,注意我碰到过修改语句一直报错的问题
1:新增必须键名值对应,顺序很重要,null都可以,否则添加失败,也可以手动字段名称,防止出错,key很关键
const addSql = `insert into shop values(null,${name},${price},${number},${key},null)`;
2:修改更新时候,老是sql异常,找了很久百度才知道name只能是string类型才可以,单独加个“”就可以啦
const updateSql = `update shop set name = "${name}",price = ${price},number = ${number},mark=${mark} where id = ${sendId}`;
最后上几个效果图
新增
删除
修改