如今,社交媒体已成为 web 不可或缺的一部分,我们构建的许多以用户为中心的 web 应用最终都需要社交组件来推动用户参与。
对于我们的第一个真实世界的 MERN 应用,我们将修改和扩展上一章中开发的 MERN 骨架应用,以构建一个简单的社交媒体应用。
在本章中,我们将介绍以下社交媒体特色的实现:
- 带有说明和照片的用户配置文件
- 用户相互跟踪
- 谁来遵循这些建议
- 发布带有照片的消息
- 带有跟踪用户帖子的新闻提要
- 按用户列出帖子
- 喜欢的帖子
- 评论帖子
MERN Social 是一款社交媒体应用,其基本功能源自 Facebook 和 Twitter 等现有社交媒体平台。此应用的主要目的是演示如何使用 MERN 堆栈技术实现允许用户通过内容连接和交互的功能。您可以根据需要进一步扩展这些实现,以实现更复杂的功能:
Code for the complete MERN Social application is available on GitHub in the repository at github.com/shamahoque/mern-social. You can clone this code and run the application as you go through the code explanations in the rest of this chapter.
MERN 社交应用所需的视图将通过扩展和修改 MERN 骨架应用中现有的 React 组件来开发。我们还将添加新的自定义组件来组合视图,包括一个 Newsfeed 视图,用户可以在其中创建新帖子,还可以浏览他们关注 MERN Social 的人的所有帖子的列表。下面的组件树显示了组成 MERN Social 前端的所有自定义 React 组件,还公开了我们将用于构建其余部分中的视图的合成结构第章:
骨架应用只支持用户名、电子邮件和密码。但在 MERN Social 中,我们允许用户添加关于自己的描述,并在注册后编辑个人资料时上传个人资料照片:
为了存储用户在about
字段中输入的描述,我们需要在server/models/user.model.js
中的用户模型中添加一个about
字段:
about: {
type: String,
trim: true
}
然后,为了从用户那里获得作为输入的描述,我们在EditProfile
表单中添加了一个多行TextField
,并以与用户名输入相同的方式处理值更改。
mern-social/client/user/EditProfile.js
:
<TextField
id="multiline-flexible"
label="About"
multiline
rows="2"
value={this.state.about}
onChange={this.handleChange('about')}
/>
最后,为了显示添加到用户配置文件页面about
字段的描述文本,我们可以将其添加到现有的配置文件视图中。
mern-social/client/user/Profile.js
:
<ListItem> <ListItemText primary={this.state.user.about}/> </ListItem>
通过对 MERN 骨架代码中用户特性的修改,用户现在可以添加和更新关于他们自己的描述,以显示在他们的配置文件中。
允许用户上传个人资料照片需要我们存储上传的图像文件,并根据请求将其检索到视图中加载。考虑到不同的文件存储选项,有多种实现此上载功能的方法:
- 服务器文件系统:将文件上传并保存到服务器文件系统,并将 URL 存储到 MongoDB
- 外部文件存储:将文件保存到 Amazon S3 等外部存储,并将 URL 存储在 MongoDB 中
- 在 MongoDB 中存储为数据:将小文件(小于 16MB)作为缓冲区类型的数据保存到 MongoDB 中
对于 MERN Social,我们将假设用户上传的照片文件大小较小,并演示如何将这些文件存储在 MongoDB 中,以实现个人资料照片上传功能。在第 8 章、构建流媒体应用中,我们将讨论如何使用 GridFS 在 MongoDB 中存储较大的文件。
为了将上传的个人资料照片直接存储在数据库中,我们将更新用户模型,添加一个photo
字段,该字段将文件存储为Buffer
类型的data
及其contentType
。
mern-social/server/models/user.model.js
:
photo: {
data: Buffer,
contentType: String
}
用户可以在编辑配置文件时从本地文件上载图像文件。我们将使用上传照片选项更新client/user/EditProfile.js
中的EditProfile
组件,然后将用户选择的文件附加到提交给服务器的表单数据中。
我们将利用 HTML5 文件输入类型,让用户从本地文件中选择图像。当用户选择文件时,文件输入将在更改事件中返回文件名。
mern-social/client/user/EditProfile.js
:
<input accept="image/*" type="file"
onChange={this.handleChange('photo')}
style={{display:'none'}}
id="icon-button-file" />
为了将此文件input
与物料 UI 组件集成,我们应用display:none
从视图中隐藏input
元素,然后在标签内添加物料 UI 按钮,用于此文件输入。这样,视图将显示 Material UI 按钮,而不是 HTML5 文件输入元素。
mern-social/client/user/EditProfile.js
:
<label htmlFor="icon-button-file">
<Button variant="raised" color="default" component="span">
Upload <FileUpload/>
</Button>
</label>
当Button
的组件属性设置为span
时,Button
组件呈现为label
元素内部的span
元素。点击Upload
跨距或标签时,文件输入会以与标签相同的 ID 注册,因此,文件选择对话框会打开。一旦用户选择了一个文件,我们可以在调用handleChange(...)
时将其设置为状态,并在视图中显示名称。
mern-social/client/user/EditProfile.js
:
<span className={classes.filename}>
{this.state.photo ? this.state.photo.name : ''}
</span>
与上一个实现中发送的stringed
对象不同,使用表单将文件上载到服务器需要提交多部分表单。我们将修改EditProfile
组件,使用FormData
API 以编码类型multipart/form-data
所需的格式存储表单数据。
首先,我们需要在componentDidMount()
中初始化FormData
。
mern-social/client/user/EditProfile.js
:
this.userData = new FormData()
接下来,我们将更新输入handleChange
函数,将文本字段和文件输入的输入值存储在FormData
中。
mern-social/client/user/EditProfile.js
:
handleChange = name => event => {
const value = name === 'photo'
? event.target.files[0]
: event.target.value
this.userData.set(name, value)
this.setState({ [name]: value })
}
然后在提交时,this.userData
与 fetch API 调用一起发送,以更新用户。由于发送到服务器的数据的内容类型不再是'application/json'
,我们还需要修改api-user.js
中的update
fetch 方法,将Content-Type
从fetch
调用的头中删除。
mern-social/client/user/api-user.js
:
const update = (params, credentials, user) => {
return fetch('/api/users/' + params.userId, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: user
}).then((response) => {
return response.json()
}).catch((e) => {
console.log(e)
})
}
现在,如果用户在编辑配置文件时选择上载配置文件照片,服务器将收到一个请求,其中包含附加的文件以及其他字段值。
Learn more about the FormData API at developer.mozilla.org/en-US/docs/Web/API/FormData.
在服务器上,为了处理对更新 API 的请求,现在可能包含一个文件,我们将使用formidable
npm 模块:
npm install --save formidable
强大将允许我们读取multipart
表单数据,允许访问字段和文件(如果有的话)。如果有文件,formidable
将临时存储在文件系统中。我们将从文件系统中读取它,使用fs
模块检索文件类型和数据,并将其存储到用户模型中的 photo 字段中。formidable
代码将进入user.controller.js
中的update
控制器中,如下所示。
mern-social/server/controllers/user.controller.js
:
import formidable from 'formidable'
import fs from 'fs'
const update = (req, res, next) => {
let form = new formidable.IncomingForm()
form.keepExtensions = true
form.parse(req, (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Photo could not be uploaded"
})
}
let user = req.profile
user = _.extend(user, fields)
user.updated = Date.now()
if(files.photo){
user.photo.data = fs.readFileSync(files.photo.path)
user.photo.contentType = files.photo.type
}
user.save((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
user.hashed_password = undefined
user.salt = undefined
res.json(user)
})
})
}
这将上传的文件作为数据存储在数据库中。接下来,我们将设置文件检索,以便能够在前端视图中访问和显示用户上传的照片。
要检索存储在数据库中的文件并在视图中显示它,最简单的方法是设置一个路由,该路由将获取数据并将其作为图像文件返回给请求的客户端。
我们将为每个用户设置一个到数据库中存储的照片的路由,并且还将添加另一个路由,如果给定用户没有上传个人资料照片,该路由将获取默认照片。
mern-social/server/routes/user.routes.js
:
router.route('/api/users/photo/:userId')
.get(userCtrl.photo, userCtrl.defaultPhoto)
router.route('/api/users/defaultphoto')
.get(userCtrl.defaultPhoto)
我们将在photo
控制器方法中查找照片,如果找到,则在照片路由的请求响应中发送,否则调用next()
返回默认照片。
mern-social/server/controllers/user.controller.js
:
const photo = (req, res, next) => {
if(req.profile.photo.data){
res.set("Content-Type", req.profile.photo.contentType)
return res.send(req.profile.photo.data)
}
next()
}
从服务器的文件系统检索并发送默认照片。
mern-social/server/controllers/user.controller.js
:
import profileImage from './../../client/iimg/profile-pic.png'
const defaultPhoto = (req, res) => {
return res.sendFile(process.cwd()+profileImage)
}
通过设置照片 URL 路由来检索照片,我们只需在img
元素的src
属性中使用这些路由即可将照片加载到视图中。例如,在Profile
组件中,我们从 state 获取用户 ID,并使用它来构建照片 URL。
mern-social/client/user/Profile.js
:
const photoUrl = this.state.user._id
? `/api/users/photo/${this.state.user._id}?${new Date().getTime()}`
: '/api/users/defaultphoto'
为了确保在编辑中更新照片后,img
元素重新加载到Profile
视图中,我们还向照片 URL 添加了一个时间值,以绕过浏览器的默认图像缓存行为
然后,我们可以将photoUrl
设置为材质 UIAvatar
组件,该组件在视图中渲染链接图像:
<Avatar src={photoUrl}/>
MERN Social 中更新的用户配置文件现在可以显示用户上传的配置文件照片和about
描述:
在 MERN Social 中,用户将能够相互跟踪。每个用户都会有一个追随者列表和他们关注的人列表。用户还可以看到他们可以关注的用户列表;换句话说,MERN Social 中的用户还没有开始关注。
为了跟踪哪个用户在跟踪哪个其他用户,我们必须为每个用户维护两个列表。当一个用户跟随或取消跟随另一个用户时,我们将更新一个用户的following
列表和另一个用户的followers
列表。
为了在数据库中存储following
和followers
列表,我们将使用两个用户引用数组更新用户模型。
mern-social/server/models/user.model.js
:
following: [{type: mongoose.Schema.ObjectId, ref: 'User'}],
followers: [{type: mongoose.Schema.ObjectId, ref: 'User'}]
这些引用将指向集合中由给定用户跟随或跟随的用户。
当从后端检索单个用户时,我们希望user
对象包含following
和followers
数组中引用的用户的名称和 ID。要检索这些详细信息,我们需要更新userByID
控制器方法来填充返回的用户对象。
mern-social/server/controllers/user.controller.js
:
const userByID = (req, res, next, id) => {
User.findById(id)
.populate('following', '_id name')
.populate('followers', '_id name')
.exec((err, user) => {
if (err || !user) return res.status('400').json({
error: "User not found"
})
req.profile = user
next()
})
}
我们使用 Mongoosepopulate
方法指定查询返回的用户对象应该包含following
和followers
列表中引用的用户的名称和 ID。当我们使用 read API 调用获取用户时,这将为我们提供followers
和following
列表中用户引用的名称和 ID。
当一个用户跟随或从视图中取消跟随另一个用户时,数据库中两个用户的记录都将更新,以响应follow
或unfollow
请求。
我们将在user.routes.js
中设置follow
和unfollow
路线,如下所示。
mern-social/server/routes/user.routes.js
:
router.route('/api/users/follow')
.put(authCtrl.requireSignin, userCtrl.addFollowing, userCtrl.addFollower)
router.route('/api/users/unfollow')
.put(authCtrl.requireSignin, userCtrl.removeFollowing, userCtrl.removeFollower)
用户控制器中的addFollowing
控制器方法将通过将后续用户的引用推送到数组中来更新当前用户的'following'
数组。
mern-social/server/controllers/user.controller.js
:
const addFollowing = (req, res, next) => {
User.findByIdAndUpdate(req.body.userId, {$push: {following: req.body.followId}}, (err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
next()
})
}
成功更新后续数组后,执行addFollower
方法将当前用户的引用添加到后续用户的'followers'
数组中。
mern-social/server/controllers/user.controller.js
:
const addFollower = (req, res) => {
User.findByIdAndUpdate(req.body.followId, {$push: {followers: req.body.userId}}, {new: true})
.populate('following', '_id name')
.populate('followers', '_id name')
.exec((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
result.hashed_password = undefined
result.salt = undefined
res.json(result)
})
}
对于 unfollowing,实现类似。removeFollowing
和removeFollower
控制器方法通过使用$pull
而不是$push
删除用户引用来更新相应的'following'
和'followers'
阵列。
mern-social/server/controllers/user.controller.js
:
const removeFollowing = (req, res, next) => {
User.findByIdAndUpdate(req.body.userId, {$pull: {following: req.body.unfollowId}}, (err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
next()
})
}
const removeFollower = (req, res) => {
User.findByIdAndUpdate(req.body.unfollowId, {$pull: {followers: req.body.userId}}, {new: true})
.populate('following', '_id name')
.populate('followers', '_id name')
.exec((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
result.hashed_password = undefined
result.salt = undefined
res.json(result)
})
}
为了访问视图中的这些 API 调用,我们将使用follow
和unfollow
获取方法更新api-user.js
。follow
和unfollow
方法将类似,使用当前用户的 ID 和凭证以及跟随或未跟随的用户 ID 调用各自的路由。follow
方法将如下所示。
mern-social/client/user/api-user.js
:
const follow = (params, credentials, followId) => {
return fetch('/api/users/follow/', {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: JSON.stringify({userId:params.userId, followId: followId})
}).then((response) => {
return response.json()
}).catch((err) => {
console.log(err)
})
}
unfollow
fetch 方法与此类似,它获取未跟随的用户 ID 并调用unfollow
API
mern-social/client/user/api-user.js
:
const unfollow = (params, credentials, unfollowId) => {
return fetch('/api/users/unfollow/', {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: JSON.stringify({userId:params.userId, unfollowId: unfollowId})
}).then((response) => {
return response.json()
}).catch((err) => {
console.log(err)
})
}
允许用户follow
或unfollow
其他用户的按钮将根据当前用户是否已经跟随该用户有条件地出现:
我们将为 follow 按钮创建一个名为FollowProfileButton
的单独组件,该组件将添加到Profile
组件中。此组件将显示Follow
或Unfollow
按钮,具体取决于当前用户是否已经是配置文件中用户的跟随者。FollowProfileButton
部分如下所示。
mern-social/client/user/FollowProfileButton.js
:
class FollowProfileButton extends Component {
followClick = () => {
this.props.onButtonClick(follow)
}
unfollowClick = () => {
this.props.onButtonClick(unfollow)
}
render() {
return (<div>
{ this.props.following
? (<Button variant="raised" color="secondary" onClick=
{this.unfollowClick}>Unfollow</Button>)
: (<Button variant="raised" color="primary" onClick=
{this.followClick}>Follow</Button>)
}
</div>)
}
}
FollowProfileButton.propTypes = {
following: PropTypes.bool.isRequired,
onButtonClick: PropTypes.func.isRequired
}
当FollowProfileButton
被添加到配置文件中时,将确定'following'
值,并将其作为道具从Profile
组件发送到FollowProfileButton
,以及将要调用的特定follow
或unfollow
获取 API 作为参数的点击处理程序:
在Profile
视图中,只有当用户查看其他用户的配置文件时才会显示FollowProfileButton
,所以我们需要修改查看配置文件时显示Edit
和Delete
按钮的条件,如下所示:
{auth.isAuthenticated().user && auth.isAuthenticated().user._id == this.state.user._id
? (edit and delete buttons)
: (follow button)
}
在Profile
组件中,在componentDidMount
上成功抓取用户数据后,我们会检查登录用户是否已经跟随配置文件中的用户,并将following
值设置为状态。
mern-social/client/user/Profile.js
:
let following = this.checkFollow(data)
this.setState({user: data, following: following})
为了确定在following
中设置的值,checkFollow
方法会检查被抓取用户的追随者列表中是否存在登录用户,如果找到则返回match
,否则如果没有找到匹配则返回undefined
。
mern-social/client/user/Profile.js
:
checkFollow = (user) => {
const jwt = auth.isAuthenticated()
const match = user.followers.find((follower)=> {
return follower._id == jwt.user._id
})
return match
}
Profile
组件还将定义FollowProfileButton
的点击处理程序,因此Profile
的状态可以在后续或取消后续操作完成时更新。
mern-social/client/user/Profile.js
:
clickFollowButton = (callApi) => {
const jwt = auth.isAuthenticated()
callApi({
userId: jwt.user._id
}, {
t: jwt.token
}, this.state.user._id).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
this.setState({user: data, following: !this.state.following})
}
})
}
单击处理程序定义将 fetch API 调用作为参数,并在将其添加到Profile
视图时作为道具与following
值一起传递给FollowProfileButton
。
mern-social/client/user/Profile.js
:
<FollowProfileButton following={this.state.following} onButtonClick={this.clickFollowButton}/>
在每个用户的个人资料中,我们将添加他们的追随者和他们关注的人的列表:
following
和followers
列表中引用的用户的详细信息已经在加载概要文件时使用read
API 获取的用户对象中。为了呈现这些单独的关注者和关注者列表,我们将创建一个名为FollowGrid
的新组件
FollowGrid
组件将用户列表作为道具,显示用户的头像及其姓名,并链接到每个用户的个人资料。我们可以根据需要将该组件添加到Profile
视图中以显示followings
或followers
。
mern-social/client/user/FollowGrid.js
:
class FollowGrid extends Component {
render() {
const {classes} = this.props
return (<div className={classes.root}>
<GridList cellHeight={160} className={classes.gridList} cols={4}>
{this.props.people.map((person, i) => {
return <GridListTile style={{'height':120}} key={i}>
<Link to={"/user/" + person._id}>
<Avatar src={'/api/users/photo/'+person._id} className=
{classes.bigAvatar}/>
<Typography className={classes.tileText}>{person.name}
</Typography>
</Link>
</GridListTile>
})}
</GridList>
</div>)
}
}
FollowGrid.propTypes = {
classes: PropTypes.object.isRequired,
people: PropTypes.array.isRequired
}
要将FollowGrid
组件添加到Profile
视图中,我们可以根据需要将其放置在视图中,并将followers
或followings
列表作为people
道具传递:
<FollowGrid people={this.state.user.followers}/>
<FollowGrid people={this.state.user.following}/>
如前所示,在 MERN Social 中,我们选择在Profile
组件的选项卡中显示FollowGrid
组件。我们使用材质 UI 选项卡组件创建了一个单独的ProfileTabs
组件,并将其添加到Profile
组件中。此ProfileTabs
组件包含两个FollowGrid
组件,其中包含以下和关注者列表,以及一个PostList
组件,该组件显示用户发布的帖子。这将在本章后面讨论。
“关注谁”功能将向登录用户显示 MERN Social 中当前未关注的人的列表,并提供关注他们或查看其个人资料的选项:
我们将在服务器上实现一个新的 API,以查询数据库并获取当前用户未跟踪的用户列表。
mern-social/server/routes/user.routes.js
:
router.route('/api/users/findpeople/:userId')
.get(authCtrl.requireSignin, userCtrl.findPeople)
在findPeople
控制器方法中,我们将查询数据库中的用户集合,找到不在当前用户following
列表中的用户。
mern-social/server/controllers/user.controller.js
:
const findPeople = (req, res) => {
let following = req.profile.following
following.push(req.profile._id)
User.find({ _id: { $nin : following } }, (err, users) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(users)
}).select('name')
}
要在前端使用此用户列表,我们将更新api-user.js
以添加此 find people API 的获取。
mern-social/client/user/api-user.js
:
const findPeople = (params, credentials) => {
return fetch('/api/users/findpeople/' + params.userId, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
}).then((response) => {
return response.json()
}).catch((err) => console.log(err))
}
为了显示跟随者功能,我们将创建一个名为FindPeople
的组件,它可以添加到任何视图中,也可以自己渲染。在这个组件中,我们将首先获取用户,然后调用componentDidMount
中的findPeople
方法。
mern-social/client/user/FindPeople.js
:
componentDidMount = () => {
const jwt = auth.isAuthenticated()
findPeople({
userId: jwt.user._id
}, {
t: jwt.token
}).then((data) => {
if (data.error) {
console.log(data.error)
} else {
this.setState({users: data})
}
})
}
获取的用户列表将被迭代并呈现在材质 UIList
组件中,每个列表项包含用户的化身、名称、到配置文件页面的链接和Follow
按钮。
mern-social/client/user/FindPeople.js
:
<List>{this.state.users.map((item, i) => {
return <span key={i}>
<ListItem>
<ListItemAvatar className={classes.avatar}>
<Avatar src={'/api/users/photo/'+item._id}/>
</ListItemAvatar>
<ListItemText primary={item.name}/>
<ListItemSecondaryAction className={classes.follow}>
<Link to={"/user/" + item._id}>
<IconButton variant="raised" color="secondary"
className={classes.viewButton}>
<ViewIcon/>
</IconButton>
</Link>
<Button aria-label="Follow" variant="raised"
color="primary"
onClick={this.clickFollow.bind(this, item, i)}>
Follow
</Button>
</ListItemSecondaryAction>
</ListItem>
</span>
})
}
</List>
点击Follow
按钮将调用 follow API,并通过拼接出新跟踪的用户来更新要跟踪的用户列表。
mern-social/client/user/FindPeople.js
:
clickFollow = (user, index) => {
const jwt = auth.isAuthenticated()
follow({
userId: jwt.user._id
}, {
t: jwt.token
}, user._id).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
let toFollow = this.state.users
toFollow.splice(index, 1)
this.setState({users: toFollow, open: true, followMessage:
`Following ${user.name}!`})
}
})
}
我们还将添加一个 Material UISnackbar
组件,当用户被成功跟踪时,该组件将临时打开,告诉用户他们开始跟踪这个新用户。
mern-social/client/user/FindPeople.js
:
<Snackbar
anchorOrigin={{ vertical: 'bottom', horizontal: 'right'}}
open={this.state.open}
onClose={this.handleRequestClose}
autoHideDuration={6000}
message={<span className={classes.snack}>{this.state.followMessage}</span>}
/>
Snackbar
将在页面右下角显示消息,并在设置的持续时间后自动隐藏:
MERN 社交用户现在可以互相关注,查看每个用户的关注者和追随者列表,还可以查看他们可以关注的人列表。在 MERN Social 中跟踪另一个用户的主要目的是跟踪他们的社交帖子,因此接下来我们将研究帖子功能的实现。
MERN Social 中的发布功能允许用户在 MERN Social 应用平台上共享内容,并通过评论或喜欢帖子的方式在内容上相互交流:
为了存储每个帖子,我们将首先在server/models/post.model.js
中定义 Mongoose 模式。帖子模式将存储帖子的文本内容、照片、对发布用户的引用、创建时间、用户对帖子的喜好以及用户对帖子的评论:
- 帖子文本:
text
将是用户在新建帖子时从以下视图提供的必填字段:
text: {
type: String,
required: 'Name is required'
}
- 帖子照片:
photo
将在帖子创建过程中从用户本地文件上传,并存储在 MongoDB 中,类似于用户档案照片上传功能。每个帖子的照片都是可选的:
photo: {
data: Buffer,
contentType: String
}
- 发帖人:创建发帖需要用户先登录,因此我们可以在
postedBy
字段中存储对发帖用户的引用:
postedBy: {type: mongoose.Schema.ObjectId, ref: 'User'}
- 创建时间:在数据库中创建后期时自动生成
created
时间:
created: { type: Date, default: Date.now }
- 喜欢:对喜欢特定帖子的用户的引用将存储在
likes
数组中:
likes: [{type: mongoose.Schema.ObjectId, ref: 'User'}]
- 评论:帖子上的每条评论都将包含文本内容、创建时间以及对发布评论的用户的引用。每个帖子将有一个
comments
数组:
comments: [{
text: String,
created: { type: Date, default: Date.now },
postedBy: { type: mongoose.Schema.ObjectId, ref: 'User'}
}]
这个模式定义将使我们能够在 MERN Social 中实现所有与 post 相关的特性。
在深入研究 MERN Social 中发布功能的实现之前,我们将查看 Newsfeed 视图的组成,以展示如何设计共享状态的嵌套 UI 组件的基本示例。Newsfeed
组件将包含两个主要子组件—一个新的帖子表单和来自以下用户的帖子列表:
Newsfeed
组件的基本结构如下,包括NewPost
组件和PostList
组件。
mern-social/client/post/Newsfeed.js
:
<Card>
<Typography type="title"> Newsfeed </Typography>
<Divider/>
<NewPost addUpdate={this.addPost}/>
<Divider/>
<PostList removeUpdate={this.removePost} posts={this.state.posts}/>
</Card>
作为父组件,Newsfeed
将控制子组件中呈现的帖子数据的状态。它将提供一种在子组件内修改 post 数据时跨组件更新 post 状态的方法,例如在NewPost
组件中添加新 post,或从PostList
组件中删除 post。
具体来说,Newsfeed
中的loadPosts
函数最初会调用服务器,从当前登录用户关注的人那里获取帖子列表,并将其设置为PostList
组件中呈现的状态。Newsfeed
组件为NewPost
和PostList
提供addPost
和removePost
功能,当创建新帖子或删除现有帖子时,将使用该功能更新处于Newsfeed
状态的帖子列表,并最终反映在PostList
中
Newsfeed
组件中定义的addPost
函数将获取NewPost
组件中创建的新帖子,并将其添加到状态中的帖子中。
mern-social/client/post/Newsfeed.js
:
addPost = (post) => {
const updatedPosts = this.state.posts
updatedPosts.unshift(post)
this.setState({posts: updatedPosts})
}
Newsfeed
组件中定义的removePost
功能将从PostList
中的Post
组件中获取已删除的帖子,并将其从状态中的帖子中移除。
mern-social/client/post/Newsfeed.js
:
removePost = (post) => {
const updatedPosts = this.state.posts
const index = updatedPosts.indexOf(post)
updatedPosts.splice(index, 1)
this.setState({posts: updatedPosts})
}
由于帖子以这种方式更新为Newsfeed
的状态,PostList
将向查看者呈现更改后的帖子列表。这种将状态更新从父组件转发到子组件并返回的机制将应用于其他功能,例如帖子中的注释更新,以及为Profile
组件中的单个用户呈现PostList
时。
在 MERN Social 中,我们将在Newsfeed
和每个用户的个人资料中列出帖子。我们将创建一个通用的PostList
组件,它将呈现提供给它的任何帖子列表,我们可以在Newsfeed
和Profile
组件中使用它。
mern-social/client/post/PostList.js
:
class PostList extends Component {
render() {
return (
<div style={{marginTop: '24px'}}>
{this.props.posts.map((item, i) => {
return <Post post={item} key={i}
onRemove={this.props.removeUpdate}/>
})
}
</div>
)
}
}
PostList.propTypes = {
posts: PropTypes.array.isRequired,
removeUpdate: PropTypes.func.isRequired
}
PostList
组件将遍历作为道具从Newsfeed
或Profile
传递给它的帖子列表,并将每个帖子的数据传递给Post
组件,该组件将呈现帖子的细节。PostList
还将把作为道具从父组件发送到Post
组件的removeUpdate
功能传递给Post
组件,因此删除单个帖子时可以更新状态
我们将在服务器上设置一个 API,用于查询帖子集合,并返回指定用户关注的人的帖子。所以这些帖子可能会显示在Newsfeed
的PostList
中。
此特定于新闻源的 API 将通过server/routes/post.routes.js
中定义的以下路径接收请求:
router.route('/api/posts/feed/:userId')
.get(authCtrl.requireSignin, postCtrl.listNewsFeed)
我们在这个路由中使用:userID
参数来指定当前登录的用户,我们将使用user.controller
中的userByID
控制器方法来获取用户详细信息,就像我们之前做的那样,并将它们附加到listNewsFeed
post 控制器方法中访问的请求对象中。因此,在mern-social/server/routes/post.routes.js
中也添加以下内容:
router.param('userId', userCtrl.userByID)
post.routes.js
文件将非常类似于user.routes.js
文件,要在 Express 应用中加载这些新路由,我们需要在express.js
中装载 post 路由,就像我们对 auth 和 user 路由所做的那样。
mern-social/server/express.js
:
app.use('/', postRoutes)
post.controller.js
中的listNewsFeed
控制器方法将查询数据库中的帖子集合,以获得匹配的帖子。
mern-social/server/controllers/post.controller.js
:
const listNewsFeed = (req, res) => {
let following = req.profile.following
following.push(req.profile._id)
Post.find({postedBy: { $in : req.profile.following } })
.populate('comments', 'text created')
.populate('comments.postedBy', '_id name')
.populate('postedBy', '_id name')
.sort('-created')
.exec((err, posts) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(posts)
})
}
在对帖子集合的查询中,我们找到了所有具有postedBy
用户引用的帖子,这些用户引用与当前用户的以下内容和当前用户匹配。
为了在前端使用此 API,我们将在client/post/api-post.js
中添加一个获取方法:
const listNewsFeed = (params, credentials) => {
return fetch('/api/posts/feed/'+ params.userId, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
}).then(response => {
return response.json()
}).catch((err) => console.log(err))
}
这是将加载在PostList
中呈现的帖子的 fetch 方法,该帖子作为子组件添加到Newsfeed
组件中。因此需要在Newsfeed
组件中的loadPosts
方法中调用此提取。
mern-social/client/post/Newsfeed.js
:
loadPosts = () => {
const jwt = auth.isAuthenticated()
listNewsFeed({
userId: jwt.user._id
}, {
t: jwt.token
}).then((data) => {
if (data.error) {
console.log(data.error)
} else {
this.setState({posts: data})
}
})
}
将在Newsfeed
组件的componentDidMount
中调用loadPosts
方法,以初始加载状态,并在PostList
组件中呈现帖子:
获取特定用户创建的帖子列表并在Profile
中显示的实现类似于上一节的讨论。我们将在服务器上设置一个 API,用于查询帖子集合,并将特定用户的帖子返回到Profile
视图。
mern-social/server/routes/post.routes.js
中增加接收特定用户回帖查询的路由:
router.route('/api/posts/by/:userId')
.get(authCtrl.requireSignin, postCtrl.listByUser)
post.controller.js
中的listByUser
控制器方法将查询帖子集合,以查找在postedBy
字段中与路由中userId
参数中指定的用户具有匹配引用的帖子。
mern-social/server/controllers/post.controller.js
:
const listByUser = (req, res) => {
Post.find({postedBy: req.profile._id})
.populate('comments', 'text created')
.populate('comments.postedBy', '_id name')
.populate('postedBy', '_id name')
.sort('-created')
.exec((err, posts) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(posts)
})
}
为了在前端使用此 API,我们将在mern-social/client/post/api-post.js
中添加一个获取方法:
const listByUser = (params, credentials) => {
return fetch('/api/posts/by/'+ params.userId, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
}
}).then(response => {
return response.json()
}).catch((err) => console.log(err))
}
此fetch
方法将加载添加到Profile
视图的PostList
所需的帖子。我们将更新Profile
组件以定义一个调用listByUser
获取方法的loadPosts
方法。
mern-social/client/user/Profile.js
:
loadPosts = (user) => {
const jwt = auth.isAuthenticated()
listByUser({
userId: user
}, {
t: jwt.token
}).then((data) => {
if (data.error) {
console.log(data.error)
} else {
this.setState({posts: data})
}
})
}
在Profile
组件中,在init()
函数中从服务器获取用户详细信息后,将使用加载配置文件的用户的用户 ID 调用loadPosts
方法。为特定用户加载的帖子设置为状态,并在添加到Profile
组件的PostList
组件中呈现。Profile
组件还提供了removePost
功能,类似于Newsfeed
组件,作为PostList
组件的支柱,因此如果删除帖子,可以更新帖子列表:
“创建新帖子”功能将允许登录用户发布消息,并可以通过从本地文件上载图像来选择性地向帖子添加图像。
在服务器上,我们将定义一个 API 在数据库中创建 post,首先在mern-social/server/routes/post.routes.js
中的/api/posts/new/:userId
处声明一条接受 post 请求的路由:
router.route('/api/posts/new/:userId')
.post(authCtrl.requireSignin, postCtrl.create)
post.controller.js
中的create
方法将使用formidable
模块访问字段和图像文件(如果有),就像我们对用户配置文件照片更新所做的那样。
mern-social/server/controllers/post.controller.js
:
const create = (req, res, next) => {
let form = new formidable.IncomingForm()
form.keepExtensions = true
form.parse(req, (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Image could not be uploaded"
})
}
let post = new Post(fields)
post.postedBy= req.profile
if(files.photo){
post.photo.data = fs.readFileSync(files.photo.path)
post.photo.contentType = files.photo.type
}
post.save((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(result)
})
})
}
为了检索上传的照片,我们还将设置一个photo
路由 URL,返回带有特定帖子的照片。
mern-social/server/routes/post.routes.js
:
router.route('/api/posts/photo/:postId').get(postCtrl.photo)
photo
控制器将返回存储在 MongoDB 中的photo
数据作为图像文件
mern-social/server/controllers/post.controller.js
:
const photo = (req, res, next) => {
res.set("Content-Type", req.post.photo.contentType)
return res.send(req.post.photo.data)
}
由于 photo route 使用了:postID
参数,我们将设置一个postByID
控制器方法,在返回 photo 请求之前,通过其 ID 获取特定帖子。我们将把 param 调用添加到post.routes.js
。
mern-social/server/routes/post.routes.js
:
router.param('postId', postCtrl.postByID)
postByID
将类似于userByID
方法,它将从数据库检索到的 post 附加到请求对象,通过next
方法访问。此实现中附带的 post 数据还将包含postedBy
用户引用的 ID 和名称。
mern-social/server/controllers/post.controller.js
:
const postByID = (req, res, next, id) => {
Post.findById(id).populate('postedBy', '_id name').exec((err, post) => {
if (err || !post)
return res.status('400').json({
error: "Post not found"
})
req.post = post
next()
})
}
我们将更新api-post.js
以添加create
方法来调用fetch
创建 API。
mern-social/client/post/api-post.js
:
const create = (params, credentials, post) => {
return fetch('/api/posts/new/'+ params.userId, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: post
}).then((response) => {
return response.json()
}).catch((err) => {
console.log(err)
})
}
此方法与用户edit
fetch 一样,将使用一个FormData
对象发送一个多部分表单提交,该对象可以包含文本字段和图像文件。
Newsfeed
组件中添加的NewPost
组件将允许用户编写包含文本消息和可选图像的新帖子:
NewPost
组件将是一个标准表单,具有EditProfile
中实现的物料 UITextField
和文件上传按钮,该按钮获取值并将其设置在FormData
对象中,以便在提交后调用create
获取方法时传递。
mern-social/client/post/NewPost.js
:
clickPost = () => {
const jwt = auth.isAuthenticated()
create({
userId: jwt.user._id
}, {
t: jwt.token
}, this.postData).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
this.setState({text:'', photo: ''})
this.props.addUpdate(data)
}
})
}
NewPost
组件作为子组件添加到Newsfeed
中,并作为道具给出addUpdate
方法。成功创建帖子后,表单视图被清空并执行addUpdate
,因此Newsfeed
中的帖子列表将用新帖子更新。
每个帖子中的帖子细节将在Post
组件中呈现,该组件将从PostList
组件接收作为道具的帖子数据,以及删除帖子时要应用的onRemove
道具。
Post
组件布局将有一个标题,显示海报的详细信息、帖子的内容、一个带有喜欢和评论计数的操作栏,以及评论部分:
标题将包含诸如姓名、头像、发布用户的个人资料链接以及发布日期等信息。
mern-social/client/post/Post.js
:
<CardHeader
avatar={<Avatar src={'/api/users/photo/'+this.props.post.postedBy._id}/>}
action={this.props.post.postedBy._id ===
auth.isAuthenticated().user._id &&
<IconButton onClick={this.deletePost}>
<DeleteIcon />
</IconButton>
}
title={<Link to={"/user/" + this.props.post.postedBy._id}>
{this.props.post.postedBy.name}
</Link>}
subheader={(new Date(this.props.post.created)).toDateString()}
className={classes.cardHeader}
/>
如果登录用户正在查看自己的帖子,标题也会有条件地显示一个delete
按钮。
内容部分将显示文章的文本和图像(如果文章包含照片)。
mern-social/client/post/Post.js
:
<CardContent className={classes.cardContent}>
<Typography component="p" className={classes.text}>
{this.props.post.text}
</Typography>
{this.props.post.photo &&
(<div className={classes.photo}>
<img className={classes.media}
src={'/api/posts/photo/'+this.props.post._id}/>
</div>)
}
</CardContent>
“操作”部分将包含一个交互式"like"
选项,其中包含帖子上的喜欢总数,以及一个评论图标,其中包含帖子上的评论总数。
mern-social/client/post/Post.js
:
<CardActions>
{ this.state.like
? <IconButton onClick={this.like} className={classes.button}
aria-label="Like" color="secondary">
<FavoriteIcon />
</IconButton>
:<IconButton onClick={this.like} className={classes.button}
aria-label="Unlike" color="secondary">
<FavoriteBorderIcon />
</IconButton>
} <span> {this.state.likes} </span>
<IconButton className={classes.button}
aria-label="Comment" color="secondary">
<CommentIcon/>
</IconButton> <span>{this.state.comments.length}</span>
</CardActions>
comments 部分将包含Comments
组件中所有与评论相关的元素,并将获得props
数据,如postId
和comments
数据,以及state
更新方法,在Comments
组件中添加或删除评论时可以调用该更新方法。
mern-social/client/post/Post.js
:
<Comments postId={this.props.post._id}
comments={this.state.comments}
updateComments={this.updateComments}/>
delete
按钮仅在登录用户和postedBy
用户对于呈现的特定帖子相同时可见。对于要从数据库中删除的帖子,我们必须设置一个 delete post API,该 API 在前端也将有一个 fetch 方法,以便在单击delete
时应用。
mern-social/server/routes/post.routes.js
:
router.route('/api/posts/:postId')
.delete(authCtrl.requireSignin,
postCtrl.isPoster,
postCtrl.remove)
删除路由在调用 post 上的remove
之前会检查授权,确保认证用户和postedBy
用户是相同的用户。isPoster
方法在执行next
方法之前会检查登录用户是否是 post 的原始创建者。
mern-social/server/controllers/post.controller.js
:
const isPoster = (req, res, next) => {
let isPoster = req.post && req.auth &&
req.post.postedBy._id == req.auth._id
if(!isPoster){
return res.status('403').json({
error: "User is not authorized"
})
}
next()
}
使用remove
控制器方法的 delete API 的其余实现和前端的 fetch 方法与其他 API 实现相同。这里的重要区别在于 delete post 特性,当 delete 成功时,调用Post
组件中的onRemove
更新方法。onRemove
方法作为道具从Newsfeed
或Profile
发送,以在删除成功时更新状态中的帖子列表。
当点击帖子上的delete
按钮时,将调用Post
组件中定义的以下deletePost
方法。
mern-social/client/post/Post.js
:
deletePost = () => {
const jwt = auth.isAuthenticated()
remove({
postId: this.props.post._id
}, {
t: jwt.token
}).then((data) => {
if (data.error) {
console.log(data.error)
} else {
this.props.onRemove(this.props.post)
}
})
}
此方法对 delete post API 进行 fetch 调用,并在成功时通过执行从父组件作为道具接收的onRemove
方法来更新处于该状态的帖子列表。
Post
组件的操作栏部分中的 like 选项将允许用户喜欢或不喜欢某篇文章,并显示该文章的喜欢总数。要记录一个 like,我们必须设置可以在视图中调用的 like 和 inspect API。
like API 将是更新Post
文档中likes
数组的 PUT 请求。该请求将在路由api/posts/like
处接收。
mern-social/server/routes/post.routes.js
:
router.route('/api/posts/like')
.put(authCtrl.requireSignin, postCtrl.like)
在like
控制器方法中,请求体中接收到的 post ID 将被用于查找 post 文档,并通过将当前用户的 ID 推送到likes
数组进行更新。
mern-social/server/controllers/post.controller.js
:
const like = (req, res) => {
Post.findByIdAndUpdate(req.body.postId,
{$push: {likes: req.body.userId}}, {new: true})
.exec((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(result)
})
}
要使用此 API,将在api-post.js
中添加一个名为like
的获取方法,当用户单击like
按钮时将使用该方法。
mern-social/client/post/api-post.js
:
const like = (params, credentials, postId) => {
return fetch('/api/posts/like/', {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: JSON.stringify({userId:params.userId, postId: postId})
}).then((response) => {
return response.json()
}).catch((err) => {
console.log(err)
})
}
unlike
API 的实现方式与同类 API 类似,在mern-social/server/routes/post.routes.js
有自己的路由:
router.route('/api/posts/unlike')
.put(authCtrl.requireSignin, postCtrl.unlike)
控制器中的unlike
方法将通过其 ID 找到帖子,并通过使用$pull
而不是$push
删除当前用户的 ID 来更新likes
数组。
mern-social/server/controllers/post.controller.js
:
const unlike = (req, res) => {
Post.findByIdAndUpdate(req.body.postId, {$pull: {likes: req.body.userId}}, {new: true})
.exec((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(result)
})
}
与之不同的 API 还将具有与api-post.js
中的like
方法类似的相应获取方法。
当呈现Post
组件时,我们需要检查当前登录的用户是否喜欢该帖子,以便显示相应的like
选项。
mern-social/client/post/Post.js
:
checkLike = (likes) => {
const jwt = auth.isAuthenticated()
let match = likes.indexOf(jwt.user._id) !== -1
return match
}
在Post
组件的componentDidMount
和componentWillReceiveProps
期间可以调用checkLike
函数,在检查当前用户是否在 post 的likes
数组中被引用后,为 post 设置like
状态:
在使用checkLike
方法的状态下设置的like
值可用于渲染心脏轮廓按钮或完整心脏按钮。如果用户不喜欢该帖子,则会显示一个心形轮廓按钮,单击该按钮将调用like
API,显示完整的心形按钮,并增加likes
计数。全心按钮将表示当前用户已经喜欢此帖子,单击此按钮将调用unlike
API,呈现心脏轮廓按钮,并减少likes
计数。
通过将likes
值设置为this.props.post.likes.length
状态,在Post
组件安装和接收道具时,也会初始设置likes
计数。
mern-social/client/post/Post.js
:
componentDidMount = () => {
this.setState({like:this.checkLike(this.props.post.likes),
likes: this.props.post.likes.length,
comments: this.props.post.comments})
}
componentWillReceiveProps = (props) => {
this.setState({like:this.checkLike(props.post.likes),
likes: props.post.likes.length,
comments: props.post.comments})
}
当发生相似或不相似的操作时,likes
相关值将再次更新,更新后的 post 数据将从 API 调用返回。
为了处理对like
和unlike
按钮的点击,我们将设置一个like
方法,该方法将根据是否是相似操作调用相应的获取方法,并更新帖子的like
和likes
计数状态。
mern-social/client/post/Post.js
:
like = () => {
let callApi = this.state.like ? unlike : like
const jwt = auth.isAuthenticated()
callApi({
userId: jwt.user._id
}, {
t: jwt.token
}, this.props.post._id).then((data) => {
if (data.error) {
console.log(data.error)
} else {
this.setState({like: !this.state.like, likes:
data.likes.length})
}
})
}
每个帖子中的评论部分将允许登录用户添加评论、查看评论列表以及删除自己的评论。对注释列表的任何更改(如新添加或删除)都将更新注释以及Post
组件的操作栏部分中的注释计数:
当用户添加注释时,数据库中的 post 文档将使用新注释进行更新。
为了实现 addcomment API,我们将设置如下的PUT
路径来更新帖子。
mern-social/server/routes/post.routes.js
:
router.route('/api/posts/comment')
.put(authCtrl.requireSignin, postCtrl.comment)
comment
控制器方法将通过其 ID 找到要更新的相关帖子,并将请求正文中接收到的评论对象推送到帖子的comments
数组中。
mern-social/server/controllers/post.controller.js
:
const comment = (req, res) => {
let comment = req.body.comment
comment.postedBy = req.body.userId
Post.findByIdAndUpdate(req.body.postId,
{$push: {comments: comment}}, {new: true})
.populate('comments.postedBy', '_id name')
.populate('postedBy', '_id name')
.exec((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(result)
})
}
在响应中,更新后的 post 对象将被发回,其中包含在 post 和评论中填充的postedBy
用户的详细信息。
为了在视图中使用此 API,我们将在api-post.js
中设置一个获取方法,该方法从视图中获取当前用户 ID、post ID 和comment
对象,并与添加注释请求一起发送。
mern-social/client/post/api-post.js
:
const comment = (params, credentials, postId, comment) => {
return fetch('/api/posts/comment/', {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + credentials.t
},
body: JSON.stringify({userId:params.userId, postId: postId,
comment: comment})
}).then((response) => {
return response.json()
}).catch((err) => {
console.log(err)
})
}
Comments
组件中的添加注释部分将允许登录用户键入注释文本:
它将包含一个带有用户照片的化身和一个文本字段,当用户按下回车键时,该字段将添加注释。
mern-social/client/post/Comments.js
:
<CardHeader
avatar={<Avatar className={classes.smallAvatar}
src={'/api/users/photo/'+auth.isAuthenticated().user._id}/>}
title={<TextField
onKeyDown={this.addComment}
multiline
value={this.state.text}
onChange={this.handleChange('text')}
placeholder="Write something ..."
className={classes.commentField}
margin="normal"/>}
className={classes.cardHeader}
/>
当值改变时,文本将以状态存储,在onKeyDown
事件中,如果按下Enter键,addComment
方法将调用comment
fetch 方法。
mern-social/client/post/Comments.js
:
addComment = (event) => {
if(event.keyCode == 13 && event.target.value){
event.preventDefault()
const jwt = auth.isAuthenticated()
comment({
userId: jwt.user._id
}, {
t: jwt.token
}, this.props.postId, {text: this.state.text}).then((data) => {
if (data.error) {
console.log(data.error)
} else {
this.setState({text: ''})
this.props.updateComments(data.comments)
}
})
}
}
Comments
组件从Post
组件接收updateComments
方法(在上一节中讨论)作为道具。这将在添加新注释时执行,以便更新 Post 视图中的注释和注释计数。
Comments
组件从Post
组件接收特定帖子的评论列表作为道具,然后迭代各个评论以呈现评论人的详细信息和评论内容。
mern-social/client/post/Comments.js
:
{this.props.comments.map((item, i) => {
return <CardHeader
avatar={
<Avatar src=
{'/api/users/photo/'+item.postedBy._id}/>
}
title={commentBody(item)}
className={classes.cardHeader}
key={i}/>
})
}
commentBody
呈现内容,包括链接到其个人资料的评论人的姓名、评论文本和评论创建日期。
mern-social/client/post/Comments.js
:
const commentBody = item => {
return (
<p className={classes.commentText}>
<Link to={"/user/" + item.postedBy._id}>{item.postedBy.name}
</Link><br/>
{item.text}
<span className={classes.commentDate}>
{(new Date(item.created)).toDateString()} |
{auth.isAuthenticated().user._id === item.postedBy._id &&
<Icon onClick={this.deleteComment(item)}
className={classes.commentDelete}>delete</Icon> }
</span>
</p>
)
}
如果注释的postedBy
引用与当前登录的用户匹配,commentBody
还将为注释提供删除选项。
点击评论中的删除按钮将通过从comments
数组中删除评论来更新数据库中的帖子:
我们将在下面的 PUT 路径上实现一个uncomment
API。
mern-social/server/routes/post.routes.js
:
router.route('/api/posts/uncomment')
.put(authCtrl.requireSignin, postCtrl.uncomment)
uncomment
控制器方法将通过 ID 找到相关帖子,然后从帖子中的comments
数组中提取带有已删除评论 ID 的评论。
mern-social/server/controllers/post.controller.js
:
const uncomment = (req, res) => {
let comment = req.body.comment
Post.findByIdAndUpdate(req.body.postId, {$pull: {comments: {_id: comment._id}}}, {new: true})
.populate('comments.postedBy', '_id name')
.populate('postedBy', '_id name')
.exec((err, result) => {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
res.json(result)
})
}
更新后的帖子将在回复中返回,就像在 commentapi 中一样。
为了在视图中使用此 API,我们还将在api-post.js
中设置一个 fetch 方法,类似于 addcomment
fetch 方法,该方法使用当前用户 ID、post ID 和删除的comment
对象与uncomment
请求一起发送。
当注释者点击注释的删除按钮时,Comments
组件将调用deleteComment
方法获取uncomment
API,并在注释成功从服务器上删除时更新注释和注释计数。
mern-social/client/post/Comments.js
:
deleteComment = comment => event => {
const jwt = auth.isAuthenticated()
uncomment({
userId: jwt.user._id
}, {
t: jwt.token
}, this.props.postId, comment).then((data) => {
if (data.error) {
console.log(data.error)
} else {
this.props.updateComments(data.comments)
}
})
}
updateComments
方法在Post
组件中定义,并作为道具传递给Comments
组件,该方法将在添加或删除注释时更新comments
和注释计数。
mern-social/client/post/Post.js
:
updateComments = (comments) => {
this.setState({comments: comments})
}
此方法将更新的注释列表作为参数,并更新保存视图中呈现的注释列表的状态。当Post
组件挂载时,设置 Post 组件中注释的初始状态,并将 Post 数据作为道具接收。此处设置的注释作为道具发送到Comments
组件,还用于在帖子布局的操作栏中呈现 likes 操作旁边的注释计数,如下所示。
mern-social/client/post/Post.js
:
<IconButton aria-label="Comment" color="secondary">
<CommentIcon/>
</IconButton> <span>{this.state.comments.length}</span>
Post
组件中的注释计数与Comments
组件中呈现和更新的注释之间的这种关系,再次简单演示了如何在 React 中的嵌套组件之间共享不断变化的数据,以创建动态的交互式用户界面
MERN 社交应用包含了我们之前为该应用定义的一组功能。用户可以用照片和描述更新他们的个人资料,在应用上相互跟踪,用照片和文本创建帖子,以及对帖子进行评论。这里显示的实现可以进一步调整和扩展,以添加更多特性,利用所揭示的使用 MERN 堆栈的机制。
本章中开发的 MERN 社交应用演示了如何将 MERN 堆栈技术结合起来,构建一个具有社交媒体功能的功能齐全的 web 应用。
我们首先更新了 skeleton 应用中的用户功能,允许在 MERN Social 上拥有帐户的任何人添加关于自己的描述,并从本地文件上传个人资料图片。在上传配置文件图片的实现中,我们探索了如何从客户端上传多部分表单数据,然后在服务器上接收数据,将文件数据直接存储在 MongoDB 数据库中,然后能够检索回来进行查看。
接下来,我们进一步更新了用户功能,以允许用户在 MERN 社交平台上相互关注。在用户模型中,我们添加了维护用户引用数组的功能,以表示每个用户的关注者和关注者列表。为了扩展这一功能,我们在视图中加入了 follow 和 unfollow 选项,并显示了关注者列表、关注者列表,甚至还显示了尚未关注的用户列表。
然后,我们添加了允许用户发布内容的功能,并通过喜欢或评论文章来与内容进行交互。在后端,我们建立了 Post 模型和相应的 api,能够存储可能包含或不包含图像的 Post 内容,并维护任何用户对 Post 的喜欢和评论记录。
最后,在实现发布、喜欢和评论功能的视图时,我们探索了如何使用组件组合并在组件之间共享不断变化的状态值,以创建复杂的交互式视图。
在下一章中,我们将进一步扩展 MERN 堆栈中的这些功能,并在通过扩展 MERN 骨架应用开发在线市场应用时释放新的可能性。