社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
前端依然使用 react-create-app
来创建。
这次用到的依赖有如下的
"classnames": "^2.2.5",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0"
偷懒直接从package.json粘的,嘿嘿
这里有一个陌生的,classnames是一个可以用表达式的形式来给DOM加class
的
其他的都是常用的。
backend 是后台的文件夹,后台的代码就在server.js 中
public
下的index.html
是网页的起始。剩下的都是关于icon的
src 中
index.js 是项目的起始,页面的框架和路由在这里
registerServerWorker.js 在这个项目中并不需要
components 中是所有的页面组件
reducers ,,,就是reducer了~~~
actions 中,就是action ~~~
constants 中是变量,防止写错字符串类型但又报错报不到地方的问题
这个小Demo只有三个tab,分别是home
页,其实啥都没有。中间是list
页,将提交的内容以卡片的形式展示出来。最后一个是form
页,提交数据用的。
这个小项目体验了前端配合node的后台,对数据库增删查改,所以我会以增删查改这四块来介绍这个小项目的逻辑。
这个小项目的重心并不在样式,所以样式使用了Semantic UI
库,是用Boot CDN来引用的。
使用方法也非常简单,在public/index.html 中引用,就可以在所有页面都出现效果,但是编辑代码并不会有代码提示,像我这脑子不太好使的,基本只能看文档了,哈哈哈。下面是引用的代码
<link href="https://cdn.bootcss.com/semantic-ui/2.3.1/semantic.min.css" rel="stylesheet">
在其他页面使用的时候和 Bootstrap 非常相似,都是通过 className
来实现的,比如下面
<button className="ui primary button">Save</button>
<div className="ui three item menu">
<NavLink exact activeClassName="active" className="item" to="/">Home</NavLink>
<NavLink exact activeClassName="active" className="item" to="/games">Games</NavLink>
<NavLink activeClassName="active" className="item" to="/games/new">Add New Game</NavLink>
</div>
这是样式,然后设置不同的路由,便可以进行tab切换了
<Route exact path="/" component={ Home } />
<Route exact path="/games" component={ GamesPage } />
···
自然,这些组件是要引入的,我这里就不写了。
exact
作用:让路径变得唯一,如果不加,会将两个组件的内容都渲染到后面的路由页面中。
例如:Home中有句“hello”,GamesPage中有句“world”,这样,在/games
中会出现 hello world。不过是两行显示。加入exact
后,每个路由只会渲染自己的组件。
在项目根目录创建一个后台的文件夹backend,里面的server.js 就是后台的入口文件。
这里使用的是node.js搭建,框架是express,数据库为Mongodb,语法为ES6, 所以肯定会用到Babel,另外使用nodemon,改变后台代码后会自动重启服务器。
Babel的用法大家还是看官网了,我用的版本比较老,是6.X,就不做介绍了,推荐使用较新的版本。
首先在server.js 中创建一个实例,并监听在某个端口,然后初始化后台,就能创建这个后台了。
import express from 'express';
const app = express();
app.listen(8180, () => console.log('Server is runing on localhost:8180'))
这个端口是可以任意设置的,但是尽量不要用一些常见的软件或者进程用的端口,不然可能经常由于端口占用而无法启动后台服务。
初始化,express、nodemon、babel安装
npm init -y
npm install express --save
npm install nodemon --save-dev
npm install --save-dev babel-cli babel-preset-env
然后在package.json 中修改start
的方法
"start": "nodemon --exec babel-node -- ./server.js"
最后,在终端运行就可以了,如果终端出现了我们 app.listen()
的回调函数的那句话就说明成功了
npm run start
安装Mongodb,我用的是windows电脑(说实话相对 MAC 麻烦一些的),去官网下载可执行文件,下载后常规安装即可,最后有一个Install MongoDB Compass
可以不用勾选,这是一个可视化程序,勾选上安装太慢了,可以后面再安装或者根据自己喜好安装其他的。
以下方法都是基于Windows的方法,如果大家是Mac或者Linux可以去官方文档或者其他文档看一看,因为我没有验证过,所以也不敢随便写。
新建一个数据库存放的文件夹,其中存放数据。
mkdir E:mongodbdata
当然,在文件管理器中建立也是一样的。
命令行运行MongoDB服务器
C:mongodbbinmongod --dbpath E:mongodbdata
C:mongodbbinmongod
是默认安装位置的mongodb启动程序
如果执行成功,就会输出一些日志信息。**这种方法虽然可以启动mongodb服务器,但是每次启动都要输入这段程序有些太麻烦了,下面介绍一下更加简洁的方法。**由于MongoDB v4.0之后,安装后会自动将mongodb服务添加到windows的服务中,并可以自启动。我们需要做的就会特别简单。
首先需要建立一个日志文件夹
mkdir E:mongodblog
当然也可以在文件管理中建立。
然后将日志信息定位到这个文件夹中即可,方法是 打开安装目录下C:mongodbbinmongod.cfg
storage:
下dbpath
改为 E:mongodbdata
。systemLog:
下path
改为 E:mongodblogmongod.log
。mp:
有的话就删了,原因不懂,但是装在C盘好像就会出现这个,不删掉服务器跑不起来最后贴一下修改后的配置文件
这时候可以打开 Windows的服务,重新启动MongoDB Server
Express 会在程序中后端创建后再初始化安装,就放到后面再说了。下面直接开始这个项目
首先需要一个库,常见的有mongodb,mongoose等等,我其实都没用过。
这个项目使用的是mongodb,算是最底层的一个。
贴一段代码
import mongodb from 'mongodb';
const dbUrl = "mongodb://localhost";
mongodb.MongoClient.connect(dbUrl, (err, client) => {
if (err) throw err;
const db = client.db('crud')
app.get('/api/games', (req, res) => {
db.collection('games').find({}).toArray((err, games) => {
res.json({ games });
});
});
app.listen(8180, () => console.log('Server is running on localhost:8180'));
})
首先引入mongodb这个库,dbUrl
是数据库的地址,这里就是本地了。
后面一句是连接方法,具体看看文档,由于某些原因,我一直打不开官方网站,就不给大家放网址了。放一个菜鸟教程的,可能由于版本更新,会有变化。
然后判断是否有错,有错就抛出来,然后指定数据库为 crud
。
之后是一个对api
的处理,作用是找到数据库中所有数据,转换成数组,加一个回调函数,输出json
格式。
后面监听的那句也放到连接数据库的方法里面,因为只有数据库连接了再监听才有意义。
最后注意一下 ,前端要跟后台交流,前端需要设置一个代理,代理到后台的地址和端口8180,方法是在最外层的 package.json 中加这样一句
"proxy": "http://localhost:8180"
后台的搭建,数据库连接基本就这些内容,下面就看一下前端是如何对后台数据库进行增删查改的。
首先将redux建立起立,新建一个reducers文件夹,其中的index.js 使用了辅助函数 combineReducers
在 Redux 中,只有一个 store,但是
combineReducers
可以让你拥有多个 reducer,同时保持各自负责逻辑块的独立性
combineReducers
可以把多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数。但是这个小项目其实只有 games
一个reducer,是不是这样写都可以。
详细的关于 combineReducers
的了解可以看看这里
下面贴一下代码
import { combineReducers } from 'redux';
import games from './games';
export default combineReducers({ games });
引入的games
就是一个普通的reducer
了
const games = (state = []) => {
return state;
}
export default games;
可以看到,这里state就是一个数组
在components 中新建 GamesPage.js ,这里主要用来接收 store
中的数据,将其返回到页面,连接后台,会有查,和删的内容。
先贴一下代码
class GamesPage extends Component {
componentDidMount() {
this.props.fetchGames();
}
render() {
return (
<div>
<GamesList games={ this.props.games }/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
games: state.games
};
};
export default connect(mapStateToProps)(GamesPage);
为了方便测试,这里会有 propTypes
的内容,限于篇幅,就不写了,关于propTypes
的相关知识可以看这里。好多引入也就不写了,嘿嘿。
可以看到,接收的 games
被传入到 GamesList
组件中,我们后面再说这个组件。
获取数据我们用 AJAX来获取,使用 componentDidMount()
这个生命周期函数,可以等组件挂载之后调用,这也是获取异步数据最常用的做法。可以看到,这里使用了一个叫 jfetchGames()
的方法,这个方法就是异步获取数据的方法了,这个内容写在 action/index.js 中
代码如下
export const setGames = (games) => {
return {
type: SET_GAMES,
games
}
}
export const fetchGames = () => {
return dispatch => {
fetch('/api/games')
.then(res => res.json())
.then(data => dispatch(setGames(data.games)))
}
};
这里 fetch()
方法是浏览器自带的异步获取的方法,通过调用一个接口,就能获取数据。
当然,使用异步,就需要一个中间件,可以是 redux-thunk
也可以是 redux-saga
,我们这里使用了前者。直接在 src/index.js 中引入即可。
也就是 GameList.js
的组件,这一页相对简单,主要是会查看数据库中是否有数据,没有的话给一句提示,如果有的话就将其显示出来,另外再加个删除和重新编辑(改)的按钮。
这些数据都是以数组的形式来存在的。这里使用了propTypes
来检测数据格式。
代码
const GamesList = ({ games }) => {
const emptyMessage = (
<p>There are no games yet in your collection.</p>
);
const gamesList = (
<div className="ui four cards">
{ games.map(game => <GameCard game={ game } key={ game._id } />) }
</div>
);
return (
<div>
{ games.length === 0 ? emptyMessage : gamesList }
</div>
)
}
GamesList.propTypes = {
games: PropTypes.array.isRequired,
}
export default GamesList;
这是一个无状态组件,组件首先接收games
,组件返回值会根据gams
是否有值,即 games.length
是否为0,决定在页面显示什么,如果为0,就显示 emptyMessage
的内容,如果有值,就显示 gamelist
中的内容。
gamelist
引入了一个 GameCard
组件,这个组件在后面删和改的地方会有介绍。
前面有介绍,增 的操作需要在form页,这个页面需要两个输入框,分别输入Tiele
(图片的标题)和 Cover Url
(图片的路径) ,这也是这个小项目唯一前后台交互的数据(emmmmm…太小了)。然后是一个粘贴完 Cover Url
后显示图片的 <div>
。最后就是一个<butten>
用来提交数据。这其中还需要有一个区域是用来显示报错的,如果前端给后台的数据接口有问题,后台便会显示这个的错误。
title: '', //也就是第一个输入框的内容
cover: '', //即Cover Url
errors: {}, //报错信息
loading: false //loading页面的控制数据,默认不显示
输入框作用就是获取数据了,直接贴一段代码
<input
type="text"
name="title"
value={ this.state.title }
onChange={ this.handleChange }
/>
每个属性的意义相信大家已经知道了,那我就直接贴一段handleChange
的代码。
handleChange = (e) => {
if (!!this.state.errors[e.target.name]) {
let errors = Object.assign({}, this.state.errors);
delete errors[e.target.name];
this.setState({
[e.target.name]: e.target.value,
errors
});
} else {
this.setState({
[e.target.name]: e.target.value,
});
}
}
这段代码首先会判断是否有错误(这个错误后面会讲到,有两部分,一部分是如果输入框没填写东西就提交会出现的错误,另一部分是数据接口的错误),如果有错误,就先将错误删除,再用e.target.value
来获取输入框的内容。如果没有出现错误,就直接获取输入框的内容。
这里面有两个小知识点,这里列举一下
!!
这是JS中强制转换为
Boolean
值得方法,往往用来判断某个元素是否存在。具体的过程是先进行一次
!
的操作,这样已经可以得到一个Boolean
值,但是得到的是其取反后的值,于是再进行一次!
操作,就得到了真正的Boolean
值。在这段代码里,如果后面的
this.state.errors[e.target.name]
是有值的,那么这里的值为true
。Object.assign()
属于浅拷贝,或者一级深拷贝。在这里拷贝的好处就是如果改变了
errors
的值,但是被拷贝的this.state.errors
中的值不会变化。还有一种拷贝是深拷贝,这里只需要得到
errors
的value
值,就不需要用到深拷贝了,至于浅拷贝与深拷贝的区别我会在以后的某篇文章说一下,而且我用的次数也不算多,好多都并不是很熟悉,不敢随便写。
这样的输入框有说法是“有约束的输入框”,它只有在 的 onChange
可以正确获取键盘输入的时候,才会在输入框中显示出来。还有一种是“无约束的输入框”,其实就是不加任何属性的
先贴代码
<div className="field">
{ this.state.cover !== '' && <img src={ this.state.cover } alt="cover" className="ui small bordered image" /> }
</div>
我个人感觉这么写不是太好理解,可能是功浅的缘故吧,哈哈哈。
这段代码的意思就是在cover
有值的时候,就执行 &&
符号后面的内容。
先贴代码
components/GameForm.js
<form *className* = {classnames('ui', 'form', { loading: this.state.loading })} *onSubmit*={ this.handleSubmit }>
handleSubmit = (e) => {
e.preventDefault();
// 先判断输入是否为空
let errors = {};
if (this.state.title === '') errors.title = "Can't be empty";
if (this.state.cover === '') errors.cover = "Can't be empty";
this.setState({ errors });
const isValid = Object.keys(errors).length === 0
// 点击保存后先显示loading,然后传入输入的内容,并捕获服务端的反馈,出错或者上传完成就取消loading
if (isValid) {
const { _id, title, cover } = this.state;
this.setState({ loading: true });
this.props.saveGame({ _id, title, cover })
.catch((err) => err.response.json()
.then(({ errors }) => {
this.setState({ errors, loading: false })
}
))
}
}
actions/index.js
const handleResponse = (response) => {
if (response.ok) {
return response.json();
} else {
let error = new Error(response.statusText);
error.response = response
throw error;
}
}
export const addGame = (game) => {
return {
type: ADD_GAME,
game
}
};
export const saveGame = (data) => {
return dispatch => {
return fetch('/api/games', {
method: 'post',
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json"
}})
// 判断请求结果
.then(handleResponse)
// 直接让新的记录显示出来,而不是等页面从数据库取数据
.then(data => dispatch(addGame(data.game)));
}
};
首先要注意一下,这个 是包裹所有, ,
tihs.state.errors
,然后在页面中提示出来。
若不为空,并且前端与后台连接正常,也就是errors
中没有值,或者说 errors
的长度为0,则会去保存这个title
和 cover
。
保存的过程是这样的,首先会从 state
中获得 title, cover
的值,显示loading
(这个loading在页面没有报错,也没有上传成功的时候会一直存在)。随后调用saveGame()
saveGame
是action中的方法,用来将数据保存到数据库。这里的逻辑是这样的:
首先发送保存的请求,使用dispatch
派发数据,这里定义了接口为/api/games
,方式为 post
,数据为 json
格式,有头部格式。然后接了一个 then
,调用 handleResponse()
方法,用来查看后台返回的数据,如果有返回值,就将该值返回到 saveGame()
,如果有错误码,即statusText
不为200,就抛出错误。之后再接一个 then
,将我们输入的数据送到后台。
我们先看这样一个问题,如果前端的接口写错了,或者这个接口在后端是没有的,那么我们是不是应该从后端给前端一个提示?
思路是这样的,如果后端收到一个没有的接口,那么就返回一个 404
的状态码,然后返回一句话,用来提示前端,当然,这里完全可以不只是一句话,我们日常生活中的网站很多时候会返回一个新的页面或者一个比较友好的界面来。
这里贴一下代码
app.use((req, res) => {
res.status(404).json({
errors: {
global: "Still working on it. Please try again later than when we implement it"
}
})
})
这时候就可以和文章前面有对应了,这就是那个后台传来的错误,前端的 form
页经常会用来判断是否可以提交。前端是怎么接收的呢,就是 action
中 handleResponse()
方法,上面已经有说过了,我这里就不说了,原理就是判断返回的值,再根据返回的值给页面返回信息。
这个信息到前端页面之后,我们就需要做一些操作了。首先这个信息会放在 GameForm.js 的 this.state.errors
中,这个数据会在 handleSubmit()
中使用,如果有错误,就去掉 loading
的标志。在页面上,这个数据会显示一个提示。
{ !!this.state.errors.global && <div className="ui negative message">{ this.state.errors.global }</div> }
首先需要安装一个库,用来解析前端过来的数据,在 server.js 中引入
npm install --save body-parser
使用方法很简单,只需要在主体函数前面加一句
app.use(bodyParser.json());
提交操作后台的代码是这样的
const validate = (data) => {
let errors = {};
if (data.title === '') errors.title = "Can't be empty";
if (data.cover === '') errors.cover = "Can't be empty";
const isValid = Object.keys(errors).length === 0
return { errors, isValid };
}
// 接口
app.post('/api/games', (req, res) => {
const { errors, isValid } = validate(req.body);
// 如果 为真,就取出body中数据,放到数据库,否在返回 400
if (isValid) {
const { title, cover } = req.body;
db.collection(
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/sunshine_kevin/article/details/90341821
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!