MERN 项目初学者指南(三)
六、使用 MERN 构建一个受欢迎的社交网络
欢迎来到最后的 MERN 项目,在这里你将使用 MERN 框架构建一个令人敬畏的流行社交网络。后端托管在 Heroku,前端站点托管在 Firebase。Firebase 还处理身份验证功能。Material-UI 提供了该项目中的图标。您还可以使用样式化的组件和 CSS。
使用 Pusher 是因为 MongoDB 不是像 Firebase 那样的实时数据库,并且您希望帖子反映某人点击提交的时刻。
在这个项目中,您将构建一个具有 Google 身份验证的社交媒体应用。这款应用的外观和感觉类似于一个流行的社交网络。在这里,你可以贴一张图片和描述性的文字。最终托管的 app 如图 6-1 所示。
图 6-1
最终应用
转到您的终端并创建一个popular-social-mern文件夹。在里面,使用 create-react-app 创建一个名为 popular-social-frontend 的新应用。这些命令如下所示。
mkdir popular-social-mern
cd popular-social-mern
npx create-react-app popular-social-frontend
Firebase 托管初始设置
由于前端站点是通过 Firebase 托管的,所以可以在 create-react-app 创建 React app 的同时创建基本设置。按照第一章的设置说明,我在 Firebase 控制台中创建了 popular-social-mern 。
因为你也在使用认证功能,你需要做第四章中提到的额外配置,并获取firebaseConfig,你需要复制它。
在 Visual Studio Code (VSCode)中打开代码,在src文件夹中创建一个firebase.js文件,并将配置内容粘贴到那里。
const firebaseConfig = {
apiKey: "AIxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxY",
authDomain: "popular-xxxxxxxxxxxxxxxxxxxxxxx.com",
projectId: "popular-xxxxxxxxxxx",
storageBucket: "popular-xxxxxxxxxxxx",
messagingSenderId: "19xxxxxxx",
appId: "1:59xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};
React 基本设置
回到 React 项目,将cd转到popular-social-frontend目录。然后,用npm start启动 React app。
cd popular-social-frontend
npm start
在index.js、App.js和App.css中删除文件和基本设置就像在第二章中所做的一样。遵循这些指示。图 6-2 显示了该应用在 localhost 上的外观。
图 6-2
初始应用
添加样式化组件
您将使用著名的 CSS-in-JS 库 styled-components ( https://styled-components.com )来设计项目的样式。这是在 React 项目中使用 CSS 的最流行的替代方法之一。打开集成终端并将其安装在popular-social-frontend文件夹中。
npm i styled-components
然后,在App.js文件中导入样式化的组件。代替 div 的是AppWrapper组件。后有风格AppWrapper的功能。更新的内容用粗体标记。
import styled from 'styled-components'
function App() {
return (
<AppWrapper>
<h1>Popular Social Network MERN</h1>
</AppWrapper >
);
}
const AppWrapper = styled.div`
background-color: #f1f2f5;
`
export default App;
创建标题组件
让我们创建一个组件,在应用中显示一个漂亮的标题。为此,在src文件夹中创建一个components文件夹,然后在components文件夹中创建一个Header.js文件。
图标来自素材-UI ( https://material-ui.com )。您需要进行两次 npm 安装,然后在popular-social-frontend文件夹中安装核心和图标。
npm i @material-ui/core @material-ui/icons
Header.js里放了很多代码,但主要是静态代码,用的是素材 UI 图标。请注意,所有文件中都使用了样式化的组件。
样式化的组件就像 SCSS,可以在父元素中嵌套内部 div。例如,HeaderCenter样式组件包含了header__option div 的样式。另外,注意像悬停这样的伪元素是由&:hover给出的。
import React from 'react'
import styled from 'styled-components'
import SearchIcon from '@material-ui/icons/Search'
import HomeIcon from '@material-ui/icons/Home'
import FlagIcon from '@material-ui/icons/Flag'
import SubscriptionsOutlinedIcon from '@material-ui/icons/SubscriptionsOutlined'
import StorefrontOutlinedIcon from '@material-ui/icons/StorefrontOutlined'
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle'
import { Avatar, IconButton } from '@material-ui/core'
import AddIcon from '@material-ui/icons/Add'
import ForumIcon from '@material-ui/icons/Flag'
import NotificationsActiveIcon from '@material-ui/icons/NotificationsActive'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
const Header = () => {
return (
<HeaderWrapper>
<HeaderLeft>
<img src="logo192.png" alt="Popular" />
</HeaderLeft>
<HeaderInput>
<SearchIcon />
<input placeholder="Search Popular" type="text" />
</HeaderInput>
<HeaderCenter>
<div className="header__option header__option--active">
<HomeIcon fontsize="large" />
</div>
<div className="header__option">
<FlagIcon fontsize="large" />
</div>
<div className="header__option">
<SubscriptionsOutlinedIcon fontsize="large" />
</div>
<div className="header__option">
<StorefrontOutlinedIcon fontsize="large" />
</div>
<div className="header__option">
<SupervisedUserCircleIcon fontsize="large" />
</div>
</HeaderCenter>
<HeaderRight>
<div className="header__info">
<Avatar src="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg " />
<h4>Nabendu</h4>
</div>
<IconButton>
<AddIcon />
</IconButton>
<IconButton>
<ForumIcon />
</IconButton>
<IconButton>
<NotificationsActiveIcon />
</IconButton>
<IconButton>
<ExpandMoreIcon />
</IconButton>
</HeaderRight>
</HeaderWrapper>
)
}
const HeaderWrapper = styled.div`
display: flex;
padding: 15px 20px;
justify-content: space-between;
align-items: center;
position: sticky;
background-color: white;
z-index: 100;
top: 0;
box-shadow: 0px 5px 8px -9px rgba(0, 0, 0, 0.75);
`
const HeaderLeft = styled.div`
display: flex;
justify-content: space-evenly;
img {
height: 40px;
}
`
const HeaderInput = styled.div`
display: flex;
align-items: center;
background-color: #eff2f5;
padding: 10px;
margin-left: 10px;
border-radius: 33px;
input {
border: none;
background-color: transparent;
outline-width: 0;
}
`
const HeaderCenter = styled.div`
display: flex;
flex: 1;
justify-content: center;
.header__option{
display: flex;
align-items: center;
padding: 10px 30px;
cursor: pointer;
.MuiSvgIcon-root{
color: gray;
}
&:hover{
background-color: #eff2f5;
border-radius: 10px;
align-items: center;
padding: 0 30px;
border-bottom: none;
.MuiSvgIcon-root{
color: #2e81f4;
}
}
}
.header__option--active{
border-bottom: 4px solid #2e81f4;
.MuiSvgIcon-root{
color: #2e81f4;
}
}
`
const HeaderRight = styled.div`
display: flex;
.header__info {
display: flex;
align-items: center;
h4 {
margin-left: 10px;
}
}
`
export default Header
在App.js文件中包含Header组件。更新的内容用粗体标记。
import styled from 'styled-components'
import Header from './components/Header'
function App() {
return (
<AppWrapper>
<Header />
</AppWrapper >
);
}
const AppWrapper = styled.div`
background-color: #f1f2f5;
`
export default App;
图 6-3 显示,这个头在 localhost 上看起来棒极了。
图 6-3
漂亮的头球
创建侧栏组件
让我们创建组件来显示一个漂亮的包含用户头像和一些静态信息的左侧栏。在components文件夹中创建一个Sidebar.js文件,并将以下内容放入其中。内容是静态的,主要包含传递给另一个SidebarRow组件的材质 UI 图标。
import React from 'react'
import SidebarRow from './SidebarRow'
import LocalHospitalIcon from '@material-ui/icons/LocalHospital'
import EmojiFlagsIcon from '@material-ui/icons/EmojiFlags'
import PeopleIcon from '@material-ui/icons/People'
import ChatIcon from '@material-ui/icons/Chat'
import StorefrontIcon from '@material-ui/icons/Storefront'
import VideoLibraryIcon from '@material-ui/icons/VideoLibrary'
import ExpandMoreOutlined from '@material-ui/icons/ExpandMoreOutlined'
import styled from 'styled-components'
const Sidebar = () => {
return (
<SidebarWrapper>
<SidebarRow src="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg" title="Nabendu" />
<SidebarRow Icon={LocalHospitalIcon} title="COVID-19 Information Center" />
<SidebarRow Icon={EmojiFlagsIcon} title="Pages" />
<SidebarRow Icon={PeopleIcon} title="Friends" />
<SidebarRow Icon={ChatIcon} title="Messenger" />
<SidebarRow Icon={StorefrontIcon} title="Marketplace" />
<SidebarRow Icon={VideoLibraryIcon} title="Videos" />
<SidebarRow Icon={ExpandMoreOutlined} title="More" />
</SidebarWrapper>
)
}
const SidebarWrapper = styled.div``
export default Sidebar
在components文件夹中创建一个SidebarRow.js文件。注意MuiSvgIcon-root类在每个材质界面上都有。您的目标是添加自定义样式。
import React from 'react'
import { Avatar } from '@material-ui/core'
import styled from 'styled-components'
const SidebarRow = ({ src, Icon, title }) => {
return (
<SidebarRowWrapper>
{src && <Avatar src={src} />}
{Icon && <Icon />}
<p>{title}</p>
</SidebarRowWrapper>
)
}
const SidebarRowWrapper = styled.div`
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
&:hover {
background-color: lightgray;
border-radius: 10px;
}
p{
margin-left:20px;
font-weight: 600;
}
.MuiSvgIcon-root{
font-size:xx-large;
color: #2e81f4;
}`
export default SidebarRow
在App.js文件中,在app__body div 中添加一个侧边栏组件,并在样式化组件中为其添加样式。更新的内容用粗体标记。
import styled from 'styled-components'
import Header from './components/Header'
import Sidebar from './components/Sidebar'
function App() {
return (
<AppWrapper>
<Header />
<div className="app__body">
<Sidebar />
</div>
</AppWrapper >
);
}
const AppWrapper = styled.div`
background-color: #f1f2f5;
.app__body {
display: flex;
}
`
export default App;
图 6-4 显示了 localhost 上的侧边栏。
图 6-4
不错的侧边栏
创建提要组件
让我们看看应用中的中间部分,它添加并显示了所有的帖子。在components文件夹中创建一个Feed.js文件。把下面的内容放进去。一个FeedWrapper风格的组件正在包装一个Stories组件。
import React from 'react'
import Stories from './Stories'
import styled from 'styled-components'
const Feed = () => {
return (
<FeedWrapper>
<Stories />
</FeedWrapper>
)
}
const FeedWrapper = styled.div`
flex: 1;
padding: 30px 150px;
display: flex;
flex-direction: column;
align-items: center;
`
export default Feed
接下来,在components文件夹中创建一个Stories.js文件。在这里,您正在将image、profileSrc,和title传递给Story组件。
import React from 'react'
import Story from './Story'
import styled from 'styled-components'
const Stories = () => {
return (
<StoriesWrapper>
<Story
image="https://images.unsplash.com/photo-1602524206684-fdf6393c7d89?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80" profileSrc="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"
title="Nabendu"
/>
<Story
image="https://images.unsplash.com/photo-1602526430780-782d6b1783fa?ixid=MXwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80" profileSrc="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"
title="TWD"
/>
<Story
image="https://www.jonesday.com/-/media/files/publications/2019/05/when-coding-is-criminal/when-coding-is-criminal.jpg?h=800&w=1600&la=en&hash=5522AA91198A168017C511FCBE77E201" profileSrc="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"
title="Nabendu"
/>
</StoriesWrapper>
)
}
const StoriesWrapper = styled.div`
display: flex;
`
export default Stories
接下来,在components文件夹中创建Story.js文件。在这里,你展示道具。请注意,StoryWrapper在背景图像中使用了道具,这显示了样式化组件的威力。如果图像是在 props 中传递的,则使用三元运算符来显示图像。
import { Avatar } from '@material-ui/core'
import React from 'react'
import styled from 'styled-components'
const Story = ({ image, profileSrc, title }) => {
return (
<StoryWrapper imageUrl={`${image}`}>
<Avatar src={profileSrc} className='story__avatar' />
<h4>{title}</h4>
</StoryWrapper>
)
}
const StoryWrapper = styled.div`
background-image: url(${props => props.imageUrl ? props.imageUrl : ''});
position: relative;
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
width: 120px;
height: 200px;
box-shadow: 0px 5px 17px -7px rgba(0,0,0,0.75);
border-radius: 10px;
margin-right: 10px;
cursor: pointer;
transition: transform 100ms ease-in;
&:hover {
transform: scale(1.07);
}
.story__avatar {
margin: 10px;
border: 5px solid #2e81f4;
}
h4 {
position: absolute;
bottom: 20px;
left: 20px;
color: white;
}
`
export default Story
在App.js文件中,包含Feed组件。更新的内容用粗体标记。
import styled from 'styled-components'
import Header from './components/Header'
import Sidebar from './components/Sidebar'
import Feed from './components/Feed'
function App() {
return (
<AppWrapper>
<Header />
<div className="app__body">
<Sidebar />
<Feed />
</div>
</AppWrapper >
);
}
const AppWrapper = styled.div`
...
`
export default App;
图 6-5 显示这些故事在 localhost 上看起来很棒。
图 6-5
不错的形象
添加小部件
通过从脸书的页面插件中添加一个小部件来完成 web 应用的前端。将这个添加到右边栏,这样应用看起来就完整了。使用脸书开发者帐户( https://developers.facebook.com/docs/plugins/page-plugin/ )连接,这样你就可以在任何 web 应用中使用它。
您需要给出脸书页面的 URL、宽度和高度,然后向下滚动并单击 Get Code 按钮。我使用了我的盖茨比烹饪书页面,如图 6-6 所示。
图 6-6
添加小部件
将打开一个弹出窗口。你需要点击 iFrame 标签获取代码,如图 6-7 所示。
图 6-7
获取 iFrame
在components文件夹中创建一个Widget.js文件。包括早期的 IFrame,但稍有修改。
import styled from 'styled-components'
const Widget = () => {
return (
<WidgetWrapper>
<iframe src="https://www.facebook.com/plugins/page.php?href=https%3A%2F%2Fwww.facebook.com%2Fgatsbycookbook%2F&tabs=timeline&width=340&height=1500&small_header=false&adapt_container_width=true&hide_cover=true&show_facepile=true&appId=332535087157151"
width="340"
height="1500"
style={{ border: "none", overflow: "hidden" }}
scrolling="no"
frameborder="0"
allow="encrypted-media"
title="Facebook Widget"
>
</iframe>
</WidgetWrapper>
)
}
const WidgetWrapper = styled.div``
export default Widget
图 6-8 显示了本地主机上的一个漂亮的小部件。
图 6-8
显示的小部件
创建信使组件
接下来,让我们通过实现组件来完成Feed.js文件,用户可以通过该组件为文章编写描述并上传图像。这里又添加了两个组件。在components文件夹中创建一个新的Messenger.js文件。
首先将它包含在Feed.js文件中。更新的内容用粗体标记。
import React from 'react'
import Stories from './Stories'
import styled from 'styled-components'
import Messenger from './Messenger'
const Feed = () => {
return (
<FeedWrapper>
<Stories />
< Messenger />
</FeedWrapper>
)
}
const FeedWrapper = styled.div`
...
`
export default Feed
让我们创建Messenger.js文件。这里,你主要有MessengerTop和MessengerBottom组件。在MessengerTop中,你主要有一个文本框,一个文件,和一个按钮。你用 CSS 中的display: none使按钮不可见。一旦设置了后端,大部分功能都在其中了。
MessengerBottom组件主要是显示图标的静态组件。
import React, { useState } from 'react'
import { Avatar, Input } from '@material-ui/core'
import VideocamIcon from '@material-ui/icons/Videocam'
import PhotoLibraryIcon from '@material-ui/icons/PhotoLibrary'
import InsertEmoticonIcon from '@material-ui/icons/InsertEmoticon'
import styled from 'styled-components'
const Messenger = () => {
const [input, setInput] = useState('')
const [image, setImage] = useState(null)
const handleChange = e => {
if(e.target.files[0])
setImage(e.target.files[0])
}
const handleSubmit = e => {
e.preventDefault()
}
return (
<MessengerWrapper>
<MessengerTop>
<Avatar src=" https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg " />
<form>
<input
type="text"
className="messenger__input"
placeholder="What's on your mind?"
value={input}
onChange={e => setInput(e.target.value)}
/>
<Input
type="file"
className="messenger__fileSelector"
onChange={handleChange}
/>
<button onClick={handleSubmit} type="submit">Hidden</button>
</form>
</MessengerTop>
<MessengerBottom>
<div className="messenger__option">
<VideocamIcon style={{ color: 'red' }} />
<h3>Live Video</h3>
</div>
<div className="messenger__option">
<PhotoLibraryIcon style={{ color: 'green' }} />
<h3>Photo/Video</h3>
</div>
<div className="messenger__option">
<InsertEmoticonIcon style={{ color: 'orange' }} />
<h3>Feeling/Activity</h3>
</div>
</MessengerBottom>
</MessengerWrapper>
)
}
const MessengerWrapper = styled.div`
display: flex;
margin-top: 30px;
flex-direction: column;
background-color: white;
border-radius: 15px;
box-shadow: 0px 5px 7px -7px rgba(0,0,0,0.75);
width: 100%;
`
const MessengerTop = styled.div`
display: flex;
border-bottom: 1px solid #eff2f5;
padding: 15px;
form {
flex: 1;
display: flex;
.messenger__input {
flex: 1;
outline-width: 0;
border: none;
padding: 5px 20px;
margin: 0 10px;
border-radius: 999px;
background-color: #eff2f5;
}
.messenger__fileSelector{
width: 20%;
}
button {
display: none;
}
}
`
const MessengerBottom = styled.div`
display: flex;
justify-content: space-evenly;
.messenger__option{
padding: 20px;
display: flex;
align-items: center;
color: gray;
margin: 5px;
h3{
font-size: medium;
margin-left: 10px;
}
&:hover{
background-color: #eff2f5;
border-radius: 20px;
cursor: pointer;
}
}
`
export default Messenger
本地主机几乎完成了,Messenger 组件显示正确(见图 6-9 )。
图 6-9
信使组件
创建帖子组件
接下来,让我们在 web 应用中显示帖子。Post组件在Feed.js文件中。它现在是硬编码的,但很快就会来自后端。
更新的内容用粗体标记。
...
import Post from './Post'
const Feed = () => {
return (
<FeedWrapper>
<Stories />
< Messenger />
<Post profilePic="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg" message="Awesome post on CSS Animation. Loved it"
timestamp="1609512232424" imgName="https://res.cloudinary.com/dxkxvfo2o/image/upload/v1598295332/CSS_Animation_xrvhai.png"
username="Nabendu"
/>
<Post profilePic="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg" message="BookList app in Vanilla JavaScript"
timestamp="1509512232424" imgName="https://res.cloudinary.com/dxkxvfo2o/image/upload/v1609138312/Booklist-es6_sawxbc.png"
username="TWD"
/>
</FeedWrapper>
)
}
const FeedWrapper = styled.div`
...
`
export default Feed
在components文件夹中创建一个新的Post.js文件。在这里,PostTop部分显示了头像、用户名和时间。PostBottom显示消息和图像。
接下来,显示PostOptions中的图标。
import { Avatar } from '@material-ui/core'
import React from 'react'
import styled from 'styled-components'
import ThumbUpIcon from '@material-ui/icons/ThumbUp'
import ChatBubbleOutlineIcon from '@material-ui/icons/ChatBubbleOutline'
import NearMeIcon from '@material-ui/icons/NearMe'
import AccountCircleIcon from '@material-ui/icons/AccountCircle'
import ExpandMoreOutlined from '@material-ui/icons/ExpandMoreOutlined'
const Post = ({ profilePic, message, timestamp, imgName, username }) => {
return (
<PostWrapper>
<PostTop>
<Avatar src={profilePic} className="post__avatar" />
<div className="post__topInfo">
<h3>{username}</h3>
<p>{new Date(parseInt(timestamp)).toUTCString()}</p>
</div>
</PostTop>
<PostBottom>
<p>{message}</p>
</PostBottom>
{
imgName ? (
<div className="post__image">
<img src={imgName} alt="Posts" />
</div>
) : (
console.log('DEBUG >>> no image here')
)
}
<PostOptions>
<div className="post__option">
<ThumbUpIcon />
<p>Like</p>
</div>
<div className="post__option">
<ChatBubbleOutlineIcon />
<p>Comment</p>
</div>
<div className="post__option">
<NearMeIcon />
<p>Share</p>
</div>
<div className="post__option">
<AccountCircleIcon />
<ExpandMoreOutlined />
</div>
</PostOptions>
</PostWrapper>
)
}
const PostWrapper = styled.div`
width: 100%;
margin-top: 15px;
border-radius: 15px;
background-color: white;
box-shadow: 0px 5px 7px -7px rgba(0,0,0,0.75);
.post__image{
img{
width: 100%
}
}
`
const PostTop = styled.div`
display: flex;
position: relative;
align-items: center;
padding: 15px;
.post__avatar{
margin-right: 10px;
}
.post__topInfo{
h3{
font-size: medium;
}
p{
font-size: small;
color: gray;
}
}
`
const PostBottom = styled.div`
margin-top: 10px;
margin-bottom:10px;
padding: 15px 25px;
`
const PostOptions = styled.div`
padding: 10px;
border-top: 1px solid lightgray;
display: flex;
justify-content: space-evenly;
font-size: medium;
color: gray;
cursor: pointer;
padding: 15px;
.post__option {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
flex: 1;
p {
margin-left: 10px;
}
&:hover {
background-color: #eff2f5;
border-radius: 10px;
}
}
`
export default Post
图 6-10 显示了本地主机上的帖子。
图 6-10
显示的帖子
Google 身份验证设置
让我们来看看谷歌认证,它允许你登录应用并发布。在这里,您使用第四章中的流程,并将其添加到 Firebase 控制台。
在firebase.js文件中,初始化应用并使用auth, provider和数据库。更新的内容用粗体标记。
import firebase from 'firebase'
const firebaseConfig = {
...
};
const firebaseApp = firebase.initializeApp(firebaseConfig)
const db = firebaseApp.firestore()
const auth = firebase.auth()
const provider = new firebase.auth.GoogleAuthProvider()
export { auth, provider }
export default db
您还需要在终端中安装 Firebase 的所有依赖项。但是要确保你在popular-social-frontend文件夹中。
npm i firebase
创建登录组件
让我们在components文件夹中创建一个Login.js文件。Login.js文件是一个简单的功能组件,显示了一个徽标和一个登录按钮。和以前一样,您正在使用样式化的组件。
import React from 'react'
import styled from 'styled-components'
import { Button } from '@material-ui/core'
const Login = () => {
const signIn = () => {}
return (
<LoginWrapper>
<div className="login__logo">
<img src="logo512.png" alt="login" />
<h1>Popular Social</h1>
</div>
<Button type='submit' className="login__btn" onClick={signIn}>Sign In</Button>
</LoginWrapper>
)
}
const LoginWrapper = styled.div`
display: grid;
place-items: center;
height: 100vh;
.login__logo {
display: flex;
flex-direction: column;
img {
object-fit: contain;
height: 150px;
max-width: 200px;
}
}
.login__btn {
width: 300px;
background-color: #2e81f4;
color: #eff2f5;
font-weight: 800;
&:hover {
background-color: white;
color: #2e81f4;
}
}
`
export default Login
接下来,如果没有当前用户,显示Login组件。您创建一个临时状态变量来显示在App.js文件中。更新的内容用粗体标记。
...
import { useState } from 'react'
import Login from './components/Login'
function App() {
const [user, setUser] = useState(null)
return (
<AppWrapper>
{user ? (
<Header />
<div className="app__body">
<Sidebar />
<Feed />
<Widget />
</div>
) : (
<Login />
)}
</AppWrapper>
);
}
const AppWrapper = styled.div`...`
export default App;
图 6-11 显示了本地主机上的登录屏幕。
图 6-11
登录屏幕
在Login.js文件中,需要从本地 Firebase 文件中导入auth、provider。然后用一个signInWithPopup()的方法得到结果。更新的内容用粗体标记。
...
import { Button } from '@material-ui/core'
import { auth, provider } from '../firebase'
const Login = () => {
const signIn = () => {
auth.signInWithPopup(provider)
.then(result => console.log(result))
.catch(error => alert(error.message))
}
return (...)
}
const LoginWrapper = styled.div`...`
export default Login
点击 localhost 上的登录按钮,弹出 Gmail 认证窗口。点击 Gmail 用户名后,你会在控制台中看到所有登录用户的详细信息,如图 6-12 所示。
图 6-12
登录详细信息
使用 Redux 和上下文 API
让我们将用户数据分派到数据层,这里 Redux/Context API 开始发挥作用。
您希望用户信息存储在全局状态中。首先,创建一个新的StateProvider.js文件。使用 useContext API 创建一个StateProvider函数。以下是内容。再次,在 www.youtube.com/watch?v=oSqqs16RejM 了解更多关于我的 React hooks YouTube 视频中的useContext钩子。
import React, { createContext, useContext, useReducer } from 'react'
export const StateContext = createContext()
export const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
)
export const useStateValue = () => useContext(StateContext)
接下来,在src文件夹中创建一个Reducer.js文件。这是一个类似于 Redux 组件中的 reducer 的概念。还是那句话,你可以在 www.youtube.com/watch?v=m0G0R0TchDY 了解更多。
export const initialState = {
user: null,
}
export const actionTypes = {
SET_USER: 'SET_USER'
}
const reducer = (state, action) => {
console.log(action)
switch (action.type) {
case actionTypes.SET_USER:
return {
...state,
user: action.user
}
default:
return state
}
}
export default reducer
在index.js文件中,导入所需文件后,用StateProvider组件包装 app 组件。更新的内容用粗体标记。
...
import { StateProvider } from './StateProvider';
import reducer, { initialState } from './Reducer';
ReactDOM.render(
<React.StrictMode>
<StateProvider initialState={initialState} reducer={reducer}>
<App />
</StateProvider>
</React.StrictMode>,
document.getElementById('root')
);
在Login.js文件中,当你从 Google 取回用户数据时,你将它调度到 reducer,它存储在数据层。
这里,useStateValue是一个自定义钩子。更新的内容用粗体标记。
...
import { auth, provider } from '../firebase'
import { useStateValue } from '../StateProvider'
import { actionTypes } from '../Reducer'
const Login = () => {
const [{}, dispatch] = useStateValue()
const signIn = () => {
auth.signInWithPopup(provider)
.then(result => {
console.log(result)
dispatch({
type: actionTypes.SET_USER,
user: result.user
})
})
.catch(error => alert(error.message))
}
return (...)
}
const LoginWrapper = styled.div`...`
export default Login
返回到App.js文件并使用useStateValue钩子。从中提取全局用户,并以您的登录为基础。更新的内容用粗体标记。
...
import { useStateValue } from './StateProvider';
function App() {
const [{ user }, dispatch] = useStateValue()
return (...);
}
const AppWrapper = styled.div`...`
export default App;
如果你在 localhost 上登录,它会带你进入应用,如图 6-13 所示。
图 6-13
已登录
在其他组件中使用 Redux 数据
你可以访问用户的数据,所以你可以在任何地方使用它。让我们使用用户的 Google 图片作为头像和 Google 用户名,而不是在Header.js文件中硬编码的那个。更新的内容用粗体标记。
...
import { useStateValue } from '../StateProvider'
const Header = () => {
const [{ user }, dispatch] = useStateValue()
return (
<HeaderWrapper>
...
<HeaderCenter>
...
</HeaderCenter>
<HeaderRight>
<div className="header__info">
<Avatar src={user.photoURL} />
<h4>{user.displayName}</h4>
</div>
...
</HeaderRight>
</HeaderWrapper>
)
}
const HeaderWrapper = styled.div`...`
export default Header
还有,使用用户的谷歌图片作为Messenger.js文件中的头像。
...
import { useStateValue } from '../StateProvider'
const Messenger = () => {
const [input, setInput] = useState('')
const [image, setImage] = useState(null)
const [{ user }, dispatch] = useStateValue()
...
return (
<MessengerWrapper>
<MessengerTop>
<Avatar src={user.photoURL} />
<form>
...
</form>
</MessengerTop>
<MessengerBottom>
...
</MessengerBottom>
</MessengerWrapper>
)
}
const MessengerWrapper = styled.div`...`
export default Messenger
Sidebar.js文件包括用户的用户名和头像。
...
import { useStateValue } from '../StateProvider'
const Sidebar = () => {
const [{ user }, dispatch] = useStateValue()
return (
<SidebarWrapper>
<SidebarRow src={user.photoURL} title={user.displayName} />
<SidebarRow Icon={LocalHospitalIcon} title="COVID-19 Information Center" />
...
</SidebarWrapper>
)
}
const SidebarWrapper = styled.div`
`
export default Sidebar
图 6-14 显示了用户在本地主机上所有正确位置的谷歌图片和用户名。
图 6-14
登录详细信息
初始后端设置
让我们转到后端,从 Node.js 代码开始。打开一个新的终端窗口,在根目录下创建一个新的photo-social-backend文件夹。移动到photo-social-backend目录后,输入git init命令,这是 Heroku 稍后需要的。
mkdir popular-social-backend
cd popular-social-backend
git init
接下来,通过在终端中输入npm init命令来创建package.json文件。你被问了一堆问题;对于大多数情况,只需按下回车键。你可以提供描述和作者,但不是强制的。你一般在server.js做进入点,这是标准的(见图 6-15 )。
图 6-15
初始后端设置
一旦package.json被创建,您需要创建包含node_modules的.gitignore文件,因为您不想以后将 node_modules 推送到 Heroku。以下是.gitignore文件内容。
node_modules
接下来,打开package.json.需要在 Node.js 中启用类似 React 的导入,包括一个启动脚本来运行server.js文件。更新的内容用粗体标记。
{
"name": "popular-social-backend",
"version": "1.0.0",
"description": "Popular Social App Backend",
"main": "server.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"author": "Nabendu Biswas",
"license": "ISC"
}
开始之前你需要安装一些软件包。打开终端,在popular-social-backend文件夹中安装cors、express、gridfs-stream、mongoose、multer、multer-gridfs-storage、nodemon、path、body-parser,和pusher。
npm i body-parser cors express gridfs-stream mongoose multer multer-gridfs-storage nodemon path pusher
MongoDB 设置
MongoDB 的设置与第一章中的解释相同。按照这些说明,创建一个名为 popular-social-mern 的新项目。
初始路线设置
在photo-social-backend文件夹中创建一个server.js文件。在这里,您导入 Express 和 Mongoose 包。然后使用 Express 创建一个运行在端口 9000 上的port变量。
第一个 API 端点是一个由app.get()创建的简单 GET 请求,如果成功,它会显示文本 Hello TheWebDev 。
然后用app.listen()监听端口 9000。
//imports
import express from 'express'
import mongoose from 'mongoose'
import cors from 'cors'
import multer from 'multer'
import GridFsStorage from 'multer-gridfs-storage'
import Grid from 'gridfs-stream'
import bodyParser from 'body-parser'
import path from 'path'
import Pusher from 'pusher'
//app config
Grid.mongo = mongoose.mongo
const app = express()
const port = process.env.PORT || 9000
//middleware
app.use(bodyParser.json())
app.use(cors())
//DB Config
//api routes
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
//listen
app.listen(port, () => console.log(`Listening on localhost: ${port}`))
在终端输入 nodemon server.js 查看监听 localhost: 9000 控制台日志。为了检查路线是否正常工作,转到http://localhost:9000/查看终点文本,如图 6-16 所示。
图 6-16
路线测试
数据库用户和网络访问
在 MongoDB 中,您需要创建一个数据库用户并授予网络访问权限。该过程与第一章中的解释相同。遵循这些说明,然后获取用户凭证和连接 URL。
在server.js文件中,创建一个connection_url变量,并将 URL 粘贴到 MongoDB 的字符串中。您需要提供之前保存的密码和数据库名称。
更新后的代码用粗体标记。
//imports
...
//app config
Grid.mongo = mongoose.mongo
const app = express()
const port = process.env.PORT || 9000
const connection_url = 'mongodb+srv://admin:<password>@cluster0.quof7.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'
//middleware
...
在 MongoDB 中存储图像
您使用 GridFS 来存储图像。您之前通过multer-gridfs-storage包安装了它。gridfs-stream包负责读取并呈现给用户的流。
项目中使用了两个连接。第一个用于图片上传,第二个用于其他 GET 和 POSTs。server.js中更新的代码用粗体标记。
...
//middleware
app.use(bodyParser.json())
app.use(cors())
//DB Config
const connection = mongoose.createConnection(connection_url, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
})
mongoose.connect(connection_url, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
})
//api routes
...
完成上传图像的代码。首先,创建一个gfs变量,然后使用conn变量连接到数据库。接下来,使用Grid连接到数据库,然后创建一个图像集合来存储图片。
接下来,创建storage变量,它用一个对象调用一个GridFsStorage函数。这里使用了connection_url变量。在承诺中,通过附加当前日期来创建一个唯一的文件名。创建一个包含文件名和桶名的fileInfo对象,作为之前创建的集合图像。
使用multer包通过传递之前创建的变量来上传图像。
使用 POST 请求构建端点来上传图像,并上传之前创建的变量。server.js中更新的代码用粗体标记。
...
//DB Config
const connection = mongoose.createConnection(connection_url, {
...
})
let gfs
connection.once('open', () => {
console.log('DB Connected')
gfs = Grid(connection.db, mongoose.mongo)
gfs.collection('images')
})
const storage = new GridFsStorage({
url: connection_url,
file: (req, file) => {
return new Promise((resolve, reject) => {
const filename = `image-${Date.now()}${path.extname(file.originalname)}`
const fileInfo = {
filename: filename,
bucketName: 'images'
}
resolve(fileInfo)
})
}
})
const upload = multer({ storage })
mongoose.connect(connection_url, {
...
})
//api routes
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
app.post('/upload/image', upload.single('file'),(req, res) => {
res.status(201).send(req.file)
})
//listen
...
在 Postman 中检查端点。向http://localhost:9000/upload/image提交发布请求。
选择正文,然后选择表单数据。接下来,从文件下拉菜单中选择一个文件,然后点击选择文件。这将打开一个弹出窗口,您必须在其中选择一个图像文件(参见图 6-17 )。
图 6-17
发布请求
点击发送按钮。如果一切顺利,你会在 Postman 中看到图像细节,如图 6-18 所示。
图 6-18
张贴图像
也可以在 MongoDB 中查看,图片保存为images.chunks,细节在images.files,如图 6-19 。
图 6-19
图像块
创建获取文件的路径。为此,创建一 img/single GET route,它带有一个参数 filename。然后使用findOne`方法找到文件。
如果文件存在,使用gfs.createReadStream()读取文件。然后使用管道将 res 传递给这个读取流。server.js中更新的代码用粗体标记。
...
//api routes
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
app.post('/upload/image', upload.any('file'),(req, res) => {
res.status(201).send(req.file)
})
app.getimg/single',(req, res) => {
gfs.files.findOne({ filename: req.query.name }, (err, file) => {
if(err) {
res.status(500).send(err)
} else {
if(!file || file.length === 0) {
res.status(404).json({ err: 'file not found' })
} else {
const readstream = gfs.createReadStream(file.filename)
readstream.pipe(res)
}
}
})
})
//listen
...
接下来,让我们在 Postman 中测试 GET 路由以接收图像。
在 Postman 中,打开对http://localhost:90img/single的 GET 请求。在 Params 下,键是名,而值是来自 MongoDB 记录的图像。一旦按下发送按钮,图像就会返回(参见图 6-20 )。
图 6-20
获取请求
MongoDB 模式和路由
直到现在,这个过程都是获取图像并保存在 MongoDB 中。现在您已经有了图像细节,您可以将它与其他帖子细节一起保存在 MongoDB 中。
为此,您需要创建保存帖子的路径。首先,为文章创建模型。然后在popular-social-backend文件夹中创建一个postModel.js文件。
在这里,您用需要传递的参数创建一个模式,然后导出它。
import mongoose from 'mongoose'
const postsModel = mongoose.Schema({
user: String,
imgName: String,
text: String,
avatar: String,
timestamp: String
})
export default mongoose.model('posts', postsModel)
现在,您可以使用该模式来创建向数据库添加数据的端点。
在server.js中,创建一个到/upload端点的 POST 请求。负载在req.body到 MongoDB。然后用create()发送dbPost。如果成功,您会收到状态 201;否则,您会收到状态 500。
接下来,创建/sync的 GET 端点,从数据库中获取数据。你在这里用的是find()。如果成功,您将收到状态 200(否则,状态 500)。时间戳对帖子进行分类。
更新后的代码用粗体标记。
...
import Posts from './postModel.js'
...
app.post('/upload/post', (req, res) => {
const dbPost = req.body
Posts.create(dbPost, (err, data) => {
if(err)
res.status(500).send(err)
else
res.status(201).send(data)
})
})
app.get('/posts', (req, res) => {
Posts.find((err, data) => {
if(err) {
res.status(500).send(err)
} else {
data.sort((b,a) => a.timestamp - b.timestamp)
res.status(200).send(data)
}
})
})
//listen
app.listen(port, () => console.log(`Listening on localhost: ${port}`))
将后端与前端集成在一起
你希望在应用初始加载时获取所有消息,然后推送这些消息。您需要达到 GET 端点,为此您需要 Axios。打开photo-social-frontend文件夹并安装。
npm i axios
接下来,在src文件夹中创建一个新的axios.js文件,并创建一个axios的实例。基础 URL 是http://localhost:9000。
import axios from 'axios'
const instance = axios.create({
baseURL: "http://localhost:9000"
})
export default instance
在Feed.js文件中进行必要的导入。之后,你就有了一个postsData状态变量。接下来,从useEffect调用一次syncFeed函数。
syncFeed函数执行对 posts 端点的 GET 调用,并用setPostsData设置postsData和res.data。
...
import React, { useEffect, useState } from 'react'
import axios from '../axios'
const Feed = () => {
const [postsData, setPostsData] = useState([])
const syncFeed = () => {
axios.get('/posts')
.then(res => {
console.log(res.data)
setPostsData(res.data)
})
}
useEffect(() => {
syncFeed()
}, [])
return (
<FeedWrapper>
<Stories />
<Messenger />
{
postsData.map(entry => (
<Post
profilePic={entry.avatar}
message={entry.text}
timestamp={entry.timestamp}
imgName={entry.imgName}
username={entry.user}
/>
))
}
</FeedWrapper>
)
}
const FeedWrapper = styled.div`...`
export default Feed
在Messenger.js,中添加axios和FormData的导入,它们附加了新的图像。
更新handleSubmit()。在这里,检查您已经上传的图像,然后在表单中附加图像和图像名称。
使用axios.post将图像发送到/upload/image端点。在 then 部分,创建一个postData对象,从用户输入的输入中获取文本。imgName包含来自res.data.filename的图像名称。用户和头像取自 Firebase 数据,而时间戳来自Date.now()。
用postData对象调用savePost()。请注意,这里有一个else,在这里您没有将图像发送到savePost()。这适用于用户创建没有任何图像的帖子的情况。
在savePost()中,您使用postData并对/upload/post端点进行 POST 调用。更新的内容用粗体标记。
...
import axios from '../axios'
import FormData from 'form-data'
const Messenger = () => {
...
const handleSubmit = e => {
e.preventDefault()
if(image) {
const imgForm = new FormData()
imgForm.append('file',image, image.name)
axios.post('/upload/image', imgForm, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
'Content-Type': `multipart/form-data; boundary=${imgForm._boundary}`
}
}).then(res => {
const postData = {
text: input,
imgName: res.data.filename,
user: user.displayName,
avatar: user.photoURL,
timestamp: Date.now()
}
savePost(postData)
})
} else {
const postData = {
text: input,
user: user.displayName,
avatar: user.photoURL,
timestamp: Date.now()
}
savePost(postData)
}
setInput('')
setImage(null)
}
const savePost = async postData => {
await axios.post('/upload/post', postData)
.then(res => {
console.log(res)
})
}
return (...)
}
const MessengerWrapper = styled.div`...`
export default Messenger
下一个变化是在Post.js文件中,您通过将图像名称作为参数传递来显示从http://localhost:90img/single端点获得的图像。Post.js文件中更新的内容用粗体标出。
...
const Post = ({ profilePic, message, timestamp, imgName, username }) => {
return (
...
{
imgName ? (
<div className="post__image">
<img src={`http://localhost:90img/single?name=${imgName}`} alt="Posts" />
</div>
) : (
console.log('DEBUG >>> no image here')
)
}
...
</PostWrapper>
)
}
...
现在您有了一个可以上传图片和发布消息的应用。它存储在 MongoDB 中,并显示在主页上。但是你有一个问题,帖子没有实时反映。您必须刷新应用(参见图 6-21 )。
图 6-21
问题
配置推动器
由于 MongoDB 不是实时数据库,所以是时候给 app 添加一个推送器,用于实时数据。因为你已经完成了第四章的设置,按照同样的说明,创建一个名为的应用。
将推杆添加到后端
由于 Pusher 已经安装在后端,您只需要在server.js文件中为它添加代码。使用 Pusher 初始化代码,该代码可从 Pusher 网站获得。您可以通过在server.js中创建一个新的 Mongoose 连接来使用它。在这里,您使用changeStream来监控帖子的所有变化。如果有任何变化,触发推动器。
...
//App Config
...
const pusher = new Pusher({
appId: "11xxxx",
key: "9exxxxxxxxxxxxx",
secret: "b7xxxxxxxxxxxxxxx",
cluster: "ap2",
useTLS: true
});
//API Endpoints
mongoose.connect(connection_url, { ...})
mongoose.connection.once('open', () => {
console.log('DB Connected for pusher')
const changeStream = mongoose.connection.collection('posts').watch()
changeStream.on('change', change => {
console.log(change)
if(change.operationType === "insert") {
console.log('Trigerring Pusher')
pusher.trigger('posts','inserted', {
change: change
})
} else {
console.log('Error trigerring Pusher')
}
})
})
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
...
//Listener
app.listen(port, () => console.log(`Listening on localhost: ${port}`))
将推杆添加到前端
是时候移动到前端使用 Pusher 了。首先,你需要在photo-social-frontend文件夹中安装pusher-js包。
npm i pusher-js
将推杆导入Feed.js中,然后使用唯一代码。然后用useEffect订阅帖子。如果改变了,调用syncFeed(),它再次从/posts端点获取所有的帖子。更新后的代码用粗体标记。
...
import Pusher from 'pusher-js'
const pusher = new Pusher('e6xxxxxxxxxxxxxx', {
cluster: 'ap2'
});
const Feed = () => {
const [postsData, setPostsData] = useState([])
const syncFeed = () => {
axios.get('/posts')
.then(res => {
console.log(res.data)
setPostsData(res.data)
})
}
useEffect(() => {
const channel = pusher.subscribe('posts');
channel.bind('inserted', (data) => {
syncFeed()
});
},[])
useEffect(() => {
syncFeed()
}, [])
return (...)
}
const FeedWrapper = styled.div`...`
export default Feed
现在回到应用,你可以实时发布任何内容。
将后端部署到 Heroku
转到www.heroku.com部署后端。按照第一章的相同步骤,创建一个名为流行-社交-后台的应用。
部署成功后,进入 https://popular-social-backend.herokuapp.com 。图 6-22 显示了正确的文本。
图 6-22
后端已部署
回到axios.js,将端点改为 https://popular-social-backend.herokuapp.com 。如果一切正常,你的应用应该可以运行了。
import axios from 'axios'
const instance = axios.create({
baseURL: "https://popular-social-backend.herokuapp.com"
})
export default instance
将前端部署到 Firebase
是时候在 Firebase 中部署前端了。遵循与第一章相同的程序。
你需要更新Post.js文件。更新的内容用粗体标记。
...
{
imgName ? (
<div className="post__image">
<img src={` https://popular-social-backend.herokuapp.cimg/single?name=${imgName}`} alt="Posts" />
</div>
) : (
console.log('DEBUG >>> no image here')
)
}
...
在这个过程之后,站点就可以正常运行了。
将前端部署到 Firebase
是时候在 Firebase 中部署前端了。遵循与第一章相同的程序。完成此过程后,站点应处于活动状态并正常工作,如图 6-23 所示。
图 6-23
最终部署地点
摘要
在这一章中,你创建了一个简单而实用的社交网络。Firebase 在网上主办的。您学习了添加 Google 身份验证,通过它您可以使用 Google 帐户登录。您还了解了如何在 MongoDB 中存储图像,以及如何使用 Pusher 为 MongoDB 提供实时功能。