精通 React 全栈 Web 开发(五)
原文:
zh.annas-archive.org/md5/bf262aae6f171acba71b02c33c95aa23译者:飞龙
第六章:AWS S3 用于图像上传和关键应用程序功能的封装
目前我们有一个可以工作但缺少一些关键功能的应用程序。本章我们的重点将包括以下功能的实现/改进:
-
打开一个新的 AWS 账户
-
为您的 AWS 账户创建身份和访问管理(IAM)
-
设置 AWS S3 存储桶
-
添加上传文章照片的功能(添加和编辑文章封面)
-
添加设置标题、副标题和“覆盖副标题”的功能(在添加/编辑文章视图中)
仪表板上的文章目前在内容中有 HTML;我们需要改进:
我们需要完成这些事情。在完成这些改进后,我们将进行一些重构。
AWS S3-简介
亚马逊的 AWS S3 是亚马逊服务器上用于静态资产(如图像)的简单存储服务。它帮助您在云中安全、可靠、高度可扩展地托管对象(如图像)。
在线存储静态资产的这种方法非常方便和简单-这就是为什么我们将在整本书中使用它。
我们将在我们的应用程序中使用它,因为它为我们提供了许多可扩展性功能,在我们自己的 Node.js 服务器上托管图像资产时不容易访问。
一般来说,Node.js 不应该用于托管比我们现在使用的更大的资产。甚至不要考虑在 Node.js 服务器上实现图像上传机制(根本不推荐)-我们将使用亚马逊的服务来实现。
生成密钥(访问密钥 ID 和秘密密钥)
在我们开始添加新的 S3 存储桶之前,我们需要为我们的 AWS 账户生成密钥(accessKeyId和secretAccessKey)。
我们在 Node.js 应用程序中需要保留的一组示例详细信息如下:
const awsConfig = {
accessKeyId: 'EXAMPLE_LB7XH_KEY_BGTCA',
secretAccessKey: 'ExAMpLe+KEY+FYliI9J1nvky5g2bInN26TCU+FiY',
region: 'us-west-2',
bucketKey: 'your-bucket-name-'
};
在亚马逊 S3 中,什么是存储桶?存储桶是 Amazon S3 中文件的一种命名空间。您可以有几个与不同项目相关联的存储桶。正如您所看到的,我们接下来要做的是创建与您的accountDefine和bucketKey(文章图片的一种命名空间)相关联的accessKeyId和secretAccessKey。定义一个您希望在其中保留文件的区域。如果您的项目为位置指定了目标,它将加快图像的加载速度,并且通常限制延迟,因为图像将更接近我们发布应用程序的客户/用户。
要创建 AWS 账户,请访问aws.amazon.com/:
创建一个帐户或登录到您的帐户:
下一步是创建 IAM,在下一节中详细描述。
关于 AWS 创建 在为特定区域创建帐户后,如果要创建 S3 存储桶,您需要选择与您的帐户分配的相同区域;否则,在设置 S3 时可能会遇到问题。
IAM
让我们准备我们的新的 accessKeyId 和 secretAccessKey。您需要访问您的 Amazon 控制台中的 IAM 页面。您可以在服务列表中找到它:
IAM 页面如下(console.aws.amazon.com/iam/home?#home):
转到 IAM 资源 | 用户:
在下一页上,您将看到一个按钮;点击它:
点击后,您将看到一个表格。至少填写一个用户,就像这个屏幕截图中一样(即使 AWS 的 UX 在此期间已经更改,屏幕截图也会给您确切的步骤):
单击“创建”按钮后,将密钥复制到安全位置(我们将在稍后使用它们):
不要忘记复制密钥(访问密钥 ID 和秘密访问密钥)。您将在本书后面学习在代码中放置它们以后使用 S3 服务。当然,屏幕截图中的密钥是不活跃的。它们只是示例;您需要拥有自己的密钥。
为用户设置 S3 权限
最后一件事是使用以下步骤添加 AmazonS3FullAccess 权限:
- 转到权限选项卡:
- 单击附加策略,选择 AmazonS3FullAccess。附加后,它将列在以下示例中:
现在我们将继续创建一个新的存储桶用于图像文件。
- 您已经完成了密钥,并且已经为密钥授予了 S3 策略;现在,我们需要准备将保存图像的存储桶。首先,您需要转到 AWS 控制台的主页,如下所示(
console.aws.amazon.com/console/home):
- 您将看到类似 AWS 服务显示所有服务的东西(或者,从服务列表中找到它,就像 IAM 一样):
- 单击 S3 - 云中的可扩展存储(如上一截图中所示)。之后,您将看到类似于此的视图(我有六个存储桶;当您有一个新帐户时,您将看到零个):
在那个存储桶中,我们将保存文章的静态图像(您将在接下来的页面中学习确切的方法)。
- 通过单击“创建存储桶”按钮来创建存储桶:
- 选择 publishing-app 名称(或其他适合您的名称)。
在截图中,我们选择了 Frankfurt。但是,例如,当您创建帐户并且您的 URL 显示
"?region=us-west-2"时,请选择 Oregon。在分配帐户时,重要的是在正确的区域创建 S3 存储桶。
- 创建存储桶后,从存储桶列表中点击它:
- 具有 publishing-app 名称的空存储桶将如下所示:
- 当您在此视图中时,浏览器中的 URL 会告诉您确切的区域和存储桶(因此您以后可以在后端执行配置时使用):
// just an example link to the bucket
https://console.aws.amazon.com/s3/home?region=eu-central-
1&bucket=publishing-app&prefix=
- 最后一件事是确保 publishing-app 存储桶的 CORS 配置正确。在该视图中,单击“属性”选项卡,您将获得详细视图:
- 然后,单击“添加 CORS”按钮:
- 然后,将以下内容粘贴到文本区域中(以下是跨域资源共享定义;它定义了 Pub 应用程序在一个域中加载并与 AWS 服务中不同域中的资源进行交互的方式):
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com
/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
- 现在它看起来像以下示例:
- 单击“保存”按钮。完成所有步骤后,我们可以开始编写图像上传功能。
在 AddArticleView 中编写图像上传功能
在继续之前,您需要拥有在上一页中创建的 S3 存储桶的访问详细信息。AWS_ACCESS_KEY_ID来自上一小节,在该小节中我们创建了一个用户:
AWS_SECRET_ACCESS_KEY与 AWS 访问密钥相同(从名称中就可以猜到)。AWS_BUCKET_NAME是您的存储桶名称(在我们的书中,我们称之为 publishing-app)。对于AWS_REGION_NAME,我们将使用eu-central-1。
找到AWS_BUCKET_NAME和AWS_REGION_NAME的最简单方法是在该视图中查看 URL(在上一小节中描述)!
检查该视图中的浏览器 URL:https://console.aws.amazon.com/s3/home?region=eu-central-1#&bucket=publishing-app&prefix=
区域和存储桶名称清楚地显示在 URL 中(我想要非常清楚地说明,因为您的区域和存储桶名称可能会有所不同,这取决于您所在的位置)。
还要确保您的 CORS 设置正确,并且您的权限/附加策略与上述完全相同。否则,您可能会遇到以下各小节中描述的所有问题。
Node.js 中的环境变量
我们将通过节点的环境变量传递所有四个参数(AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_BUCKET_NAME和AWS_REGION_NAME)。
首先,让我们安装一个节点库,它将从文件中创建环境变量,以便我们能够在本地主机中使用它们:
npm i -save node-env-file@0.1.8
这些环境变量是什么?一般来说,我们将使用它们来传递一些敏感数据的变量给应用程序--我们在这里特别谈论 AWS 秘钥和 MongoDB 的登录/密码信息,用于当前环境设置(无论是开发还是生产)。
你可以通过访问它们来读取这些环境变量,就像以下示例中所示:
// this is how we will access the variables in
//the server.js for example:
env.process.AWS_ACCESS_KEY_ID
env.process.AWS_SECRET_ACCESS_KEY
env.process.AWS_BUCKET_NAME
env.process.AWS_REGION_NAME
在我们的本地开发环境中,我们将保留该信息在服务器目录中,因此请从命令提示符中执行以下操作:
$ [[you are in the server/ directory of your project]]
$ touch .env
您已经创建了server/.env文件;下一步是在其中放入内容(当我们的应用程序运行时,node-env-file将读取环境变量):
AWS_ACCESS_KEY_ID=_*_*_*_*_ACCESS_KEY_HERE_*_*_*_*_
AWS_SECRET_ACCESS_KEY=_*_*_*_*_SECRET_KEY_HERE_*_*_*_*_
AWS_BUCKET_NAME=publishing-app
AWS_REGION_NAME=eu-central-1
在这里,您可以看到节点环境文件的结构。每一行都有一个键和一个值。在那里,您需要粘贴在阅读本章时创建的键。用您自己的值替换这些值:*_*_ACCESS_KEY_HERE_*_和_*_SECRET_KEY_HERE_**_。
创建了server/.env文件后,在项目目录中使用npm安装所需的依赖项,以在图像上传时抽象整个巨大工作:
npm i --save react-s3-uploader@3.0.3
react-s3-uploader组件非常适合我们的用例,并且它很好地抽象了aws-sdk的功能。这里的主要问题是我们需要正确配置.env文件(具有正确的变量),react-s3-uploader将在后端和前端为我们完成工作(很快您将看到)。
改进我们的 Mongoose 文章模式
我们需要改进模式,这样我们的文章集合中就会有一个存储图片 URL 的位置。编辑旧的文章模式:
// this is old codebase to improve:
var articleSchema = new Schema({
articleTitle: String,
articleContent: String,
articleContentJSON: Object
},
{
minimize: false
}
);
将其更改为新的、改进的版本:
var articleSchema = new Schema({
articleTitle: String,
articleContent: String,
articleContentJSON: Object,
articlePicUrl: { type: String, default:
'/static/placeholder.png' }
},
{
minimize: false
}
);
如您所见,我们引入了articlePicUrl,默认值为/static/placeholder.png。现在,我们将能够在文章对象中保存带有图片 URL 变量的文章。
如果您忘记更新 Mongoose 模型,那么它将不允许您将该值保存到数据库中。
为 S3 的上传添加路由
我们需要将一个新的库导入到server/server.js文件中:
import s3router from 'react-s3-uploader/s3router';
我们最终会得到类似以下的东西:
// don't write it, this is how your server/server.js
//file should look like:
import http from 'http';
import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import falcor from 'falcor';
import falcorExpress from 'falcor-express';
import FalcorRouter from 'falcor-router';
import routes from './routes.js';
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { renderToStaticMarkup } from 'react-dom/server'
import ReactRouter from 'react-router';
import { RoutingContext, match } from 'react-router';
import * as hist from 'history';
import rootReducer from '../src/reducers';
import reactRoutes from '../src/routes';
import fetchServerSide from './fetchServerSide';
import s3router from 'react-s3-uploader/s3router';
var app = express();
app.server = http.createServer(app);
// CORS - 3rd party middleware
app.use(cors());
// This is required by falcor-express middleware
// to work correctly with falcor-browser
app.use(bodyParser.json({extended: false}));
app.use(bodyParser.urlencoded({extended: false}));
我把所有这些放在这里,这样您就可以确保您的server/server.js文件与此匹配。
还有一件事要做,就是修改server/index.js文件。找到这个:
require('babel-core/register');
require('babel-polyfill');
require('./server');
将其更改为以下改进版本:
var env = require('node-env-file');
// Load any undefined ENV variables form a specified file.
env(__dirname + '/.env');
require('babel-core/register');
require('babel-polyfill');
require('./server');
只是为了澄清,env(__dirname + '/.env');告诉我们在我们的结构中.env文件的位置(您可以从console.log中找到__dirname变量是服务器文件的系统位置--这必须与真实的.env文件的位置匹配,以便系统找到它)。
下一步是将此添加到我们的server/server.js文件中:
app.use('/s3', s3router({
bucket: process.env.AWS_BUCKET_NAME,
region: process.env.AWS_REGION_NAME,
signatureVersion: 'v4',
headers: {'Access-Control-Allow-Origin': '*'},
ACL: 'public-read'
}));
如您在这里所见,我们已经开始使用我们在server/.env文件中定义的环境变量。对我来说,process.env.AWS_BUCKET_NAME等于publishing-app,但如果您定义了不同的值,那么它将从server/.env中检索另一个值(感谢我们刚刚定义的env express 中间件)。
基于后端配置(环境变量和使用import s3router from 'react-s3-uploader/s3router'设置s3router),我们将能够使用 AWS S3 存储桶。我们需要准备前端,首先将在添加文章视图上实现。
在前端创建 ImgUploader 组件
我们将创建一个名为ImgUploader的组件。该组件将使用react-s3-uploader库,该库用于将上传抽象到 Amazon S3。在回调中,您将收到information:onProgress,并且可以使用该回调找到百分比的进度,以便用户可以查看uploadonError的状态。当发生错误时,将触发此回调:当完成时,此回调将向我们发送已上传到 S3 的文件的位置。
您将在本章中进一步了解更多细节;让我们先创建一个文件:
$ [[you are in the src/components/articles directory of your
project]]
$ touch ImgUploader.js
你已经创建了src/components/articles/ImgUploader.js文件,下一步是准备导入。所以在ImgUploader文件的顶部添加以下内容:
import React from 'react';
import ReactS3Uploader from 'react-s3-uploader';
import {Paper} from 'material-ui';
class ImgUploader extends React.Component {
constructor(props) {
super(props);
this.uploadFinished = this.uploadFinished.bind(this);
this.state = {
uploadDetails: null,
uploadProgress: null,
uploadError: null,
articlePicUrl: props.articlePicUrl
};
}
uploadFinished(uploadDetails) {
// here will be more code in a moment
}
render () {
return <div>S3 Image uploader placeholder</div>;
}
}
ImgUploader.propTypes = {
updateImgUrl: React.PropTypes.func.isRequired
};
export default ImgUploader;
正如你在这里所看到的,我们在render函数中用div初始化了ImgUploader组件,返回一个临时占位符。
我们还准备了一个带有必需属性updateImgUrl的propTypes。这将是一个回调函数,将发送最终上传的图片位置(必须保存在数据库中--我们将在稍后使用updateImgUrl属性)。
在ImgUploader组件的状态下,我们有以下内容:
// this is already in your codebase:
this.state = {
uploadDetails: null,
uploadProgress: null,
uploadError: null,
articlePicUrl: props.articlePicUrl
};
在这些变量中,我们将根据当前状态和props.articlePicUrl存储所有组件的状态,并将 URL 详细信息发送到AddArticleView组件(我们将在本章后面完成ImgUploader组件后进行)。
结束ImgUploader组件
下一步是改进我们ImgUploader中的uploadFinished函数,找到旧的空函数:
uploadFinished(uploadDetails) {
// here will be more code in a moment
}
用以下内容替换:
uploadFinished(uploadDetails) {
let articlePicUrl = '/s3/img/'+uploadDetails.filename;
this.setState({
uploadProgress: null,
uploadDetails: uploadDetails,
articlePicUrl: articlePicUrl
});
this.props.updateImgUrl(articlePicUrl);
}
正如你所看到的,uploadDetails.filename变量来自于我们在ImgUploader文件顶部导入的ReactS3Uploader组件。成功上传后,我们将uploadProgress设置回null,设置我们上传的详细信息,并通过this.props.updateImgUrl(articlePicUrl)回调发送详细信息。
下一步是改进我们ImgUploader中的render函数:
render () {
let imgUploadProgressJSX;
let uploadProgress = this.state.uploadProgress;
if(uploadProgress) {
imgUploadProgressJSX = (
<div>
{uploadProgress.uploadStatusText}
({uploadProgress.progressInPercent}%)
</div>
);
} else if(this.state.articlePicUrl) {
let articlePicStyles = {
maxWidth: 200,
maxHeight: 200,
margin: 'auto'
};
imgUploadProgressJSX = <img src={this.state.articlePicUrl}
style={articlePicStyles} />;
}
return <div>S3 Image uploader placeholder</div>;
}
这个渲染是不完整的,但让我们描述一下我们到目前为止添加了什么。这段代码简单地是关于通过this.state获取uploadProgress的信息(第一个if语句)。else if(this.state.articlePicUrl)是关于在上传完成后渲染图片。好的,但我们将从哪里获取这些信息呢?这就是剩下的部分:
let uploaderJSX = (
<ReactS3Uploader
signingUrl='/s3/sign'
accept='image/*'
onProgress={(progressInPercent, uploadStatusText) => {
this.setState({
uploadProgress: { progressInPercent,
uploadStatusText },
uploadError: null
});
}}
onError={(errorDetails) => {
this.setState({
uploadProgress: null,
uploadError: errorDetails
});
}}
onFinish={(uploadDetails) => {
this.uploadFinished(uploadDetails);
}} />
);
uploaderJSX变量与我们的react-s3-uploader库完全相同。从代码中可以看出,对于进度,我们使用uploadProgress: { progressInPercent, uploadStatusText }来设置状态,并设置uploadError: null(以防用户收到错误消息)。在出现错误时,我们设置状态,以便告知用户。完成后,我们运行uploadFinished函数,该函数之前已经详细描述过。
ImgUploader的完整render函数如下所示:
render () {
let imgUploadProgressJSX;
let uploadProgress = this.state.uploadProgress;
if(uploadProgress) {
imgUploadProgressJSX = (
<div>
{uploadProgress.uploadStatusText}
({uploadProgress.progressInPercent}%)
</div>
);
} else if(this.state.articlePicUrl) {
let articlePicStyles = {
maxWidth: 200,
maxHeight: 200,
margin: 'auto'
};
imgUploadProgressJSX = <img src={this.state.articlePicUrl}
style={articlePicStyles} />;
}
let uploaderJSX = (
<ReactS3Uploader
signingUrl='/s3/sign'
accept='image/*'
onProgress={(progressInPercent, uploadStatusText) => {
this.setState({
uploadProgress: { progressInPercent,
uploadStatusText },
uploadError: null
});
}}
onError={(errorDetails) => {
this.setState({
uploadProgress: null,
uploadError: errorDetails
});
}}
onFinish={(uploadDetails) => {
this.uploadFinished(uploadDetails);
}} />
);
return (
<Paper zDepth={1} style={{padding: 32, margin: 'auto',
width: 300}}>
{imgUploadProgressJSX}
{uploaderJSX}
</Paper>
);
}
正如你所看到的,这是整个 ImgUploader 的渲染。我们使用了内联样式的 Paper 组件(来自 material-ui ),所以整个东西看起来对文章的最终用户/编辑者更好。
AddArticleView 的改进
我们需要将 ImgUploader 组件添加到 AddArticleView 中;首先,我们需要将其导入到 src/views/articles/AddArticleView.js 文件中,就像这样:
import ImgUploader from '../../components/articles/ImgUploader';
接下来,在 AddArticleView 的构造函数中,找到这段旧代码:
// this is old, don't write it:
class AddArticleView extends React.Component {
constructor(props) {
super(props);
this._onDraftJSChange = this._onDraftJSChange.bind(this);
this._articleSubmit = this._articleSubmit.bind(this);
this.state = {
title: 'test',
contentJSON: {},
htmlContent: '',
newArticleID: null
};
}
将其改为以下改进版本:
class AddArticleView extends React.Component {
constructor(props) {
super(props);
this._onDraftJSChange = this._onDraftJSChange.bind(this);
this._articleSubmit = this._articleSubmit.bind(this);
this.updateImgUrl = this.updateImgUrl.bind(this);
this.state = {
title: 'test',
contentJSON: {},
htmlContent: '',
newArticleID: null,
articlePicUrl: '/static/placeholder.png'
};
}
正如你所看到的,我们将这个绑定到 updateImgUrl 函数,并添加了一个新的状态变量叫做 articlePicUrl(默认情况下,如果用户没有选择封面,我们将指向 /static/placeholder.png)。
让我们改进一下这个组件的功能:
// this is old codebase, just for your reference:
async _articleSubmit() {
let newArticle = {
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON
}
let newArticleID = await falcorModel
.call(
'articles.add',
[newArticle]
).
then((result) => {
return falcorModel.getValue(
['articles', 'newArticleID']
).then((articleID) => {
return articleID;
});
});
newArticle['_id'] = newArticleID;
this.props.articleActions.pushNewArticle(newArticle);
this.setState({ newArticleID: newArticleID});
}
将这段代码改为以下内容:
async _articleSubmit() {
let newArticle = {
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON,
articlePicUrl: this.state.articlePicUrl
}
let newArticleID = await falcorModel
.call(
'articles.add',
[newArticle]
).
then((result) => {
return falcorModel.getValue(
['articles', 'newArticleID']
).then((articleID) => {
return articleID;
});
});
newArticle['_id'] = newArticleID;
this.props.articleActions.pushNewArticle(newArticle);
this.setState({ newArticleID: newArticleID });
}
updateImgUrl(articlePicUrl) {
this.setState({
articlePicUrl: articlePicUrl
});
}
正如你所看到的,我们在 newArticle 对象中添加了 articlePicUrl: this.state.articlePicUrl。我们还引入了一个名为 updateImgUrl 的新函数,它只是一个回调函数,用来设置一个新的状态,其中包含 articlePicUrl 变量(在 this.state.articlePicUrl 中,我们保存了即将保存到数据库中的当前文章的图片 URL)。
src/views/articles/AddArticleView.js 中唯一需要改进的是我们当前的渲染。以下是旧的渲染:
// your current old codebase to improve:
return (
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
<h1>Add Article</h1>
<WYSIWYGeditor
name='addarticle'
title='Create an article'
onChangeTextJSON={this._onDraftJSChange} />
<RaisedButton
onClick={this._articleSubmit}
secondary={true}
type='submit'
style={{margin: '10px auto', display: 'block',
width: 150}}
label={'Submit Article'} />
</div>
);
}
我们需要使用 ImgUploader 来改进这段代码:
return (
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
<h1>Add Article</h1>
<WYSIWYGeditor
name='addarticle'
title='Create an article'
onChangeTextJSON={this._onDraftJSChange} />
<div style={{margin: '10px 10px 10px 10px'}}>
<ImgUploader
updateImgUrl={this.updateImgUrl}
articlePicUrl={this.state.articlePicUrl} />
</div>
<RaisedButton
onClick={this._articleSubmit}
secondary={true}
type='submit'
style={{margin: '10px auto', display: 'block',
width: 150}}
label={'Submit Article'} />
</div>
);
}
你可以看到,我们使用属性发送当前的 articlePicUrl(这将在以后很方便,也给我们提供了默认的 placeholder.png 位置),以及更新 img URL 的回调函数,称为 updateImgUrl。
如果你访问 http://localhost:3000/add-article ,你将会看到一个新的图片选择器,位于所见即所得框和提交文章按钮之间(查看截图):
当然,如果你按照所有的说明正确操作,点击“选择文件”后,你将能够上传一个新的图片到 S3 存储桶,缩略图中的图片将被替换,就像下面的例子一样:
正如你所看到的,我们可以上传一张图片。下一步是取消模拟查看它们,这样我们就可以看到我们的文章封面上有一只狗(狗的图片来自我们在数据库中的文章集合)。
对 PublishingApp、ArticleCard 和 DashboardView 进行一些剩余的调整
我们可以添加一篇文章。我们需要在视图中取消模拟图像 URL,这样我们就可以看到来自数据库的真实 URL(而不是在img src属性中模拟的)。
让我们从src/layouts/PublishingApp.js开始,改进旧的_fetch函数:
// old codebase to improve:
async _fetch() {
let articlesLength = await falcorModel.
getValue('articles.length').
then((length) => length);
let articles = await falcorModel.
get(['articles', {from: 0, to: articlesLength-1},
['_id','articleTitle', 'articleContent',
'articleContentJSON']]).
then((articlesResponse) => {
return articlesResponse.json.articles;
}).catch(e => {
return 500;
});
if(articles === 500) {
return;
}
this.props.articleActions.articlesList(articles);
}
用以下代码替换这段代码:
async _fetch() {
let articlesLength = await falcorModel.
getValue('articles.length').
then((length) => length);
let articles = await falcorModel.
get(['articles', {from: 0, to: articlesLength-1},
['_id','articleTitle', 'articleContent',
'articleContentJSON', 'articlePicUrl']]).
then((articlesResponse) => {
return articlesResponse.json.articles;
}).catch(e => {
console.debug(e);
return 500;
});
if(articles === 500) {
return;
}
this.props.articleActions.articlesList(articles);
}
正如您所看到的,我们已经开始通过falcorModel.get方法获取articlePicUrl。
接下来,在PublishingApp文件中,也是改进render函数,所以您需要改进以下代码:
// old code:
this.props.article.forEach((articleDetails, articleKey) => {
let currentArticleJSX = (
<div key={articleKey}>
<ArticleCard
title={articleDetails.articleTitle}
content={articleDetails.articleContent} />
</div>
);
添加一个新的属性,将传递图像 URL:
this.props.article.forEach((articleDetails, articleKey) => {
let currentArticleJSX = (
<div key={articleKey}>
<ArticleCard
title={articleDetails.articleTitle}
content={articleDetails.articleContent}
articlePicUrl={articleDetails.articlePicUrl} />
</div>
);
正如您所看到的,我们正在将获取的articlePicUrl传递给ArticleCard组件。
改进 ArticleCard 组件
在我们通过属性传递articlePicUrl变量之后,我们需要改进以下内容(src/components/ArticleCard.js):
// old code to improve:
render() {
let title = this.props.title || 'no title provided';
let content = this.props.content || 'no content provided';
let paperStyle = {
padding: 10,
width: '100%',
height: 300
};
let leftDivStyle = {
width: '30%',
float: 'left'
}
let rightDivStyle = {
width: '60%',
float: 'left',
padding: '10px 10px 10px 10px'
}
return (
<Paper style={paperStyle}>
<CardHeader
title={this.props.title}
subtitle='Subtitle'
avatar='/static/avatar.png'
/>
<div style={leftDivStyle}>
<Card >
<CardMedia
overlay={<CardTitle title={title}
subtitle='Overlay subtitle' />}>
<img src='/static/placeholder.png' height='190' />
</CardMedia>
</Card>
</div>
<div style={rightDivStyle}>
<div dangerouslySetInnerHTML={{__html: content}} />
</div>
</Paper>);
}
将其更改为以下内容:
render() {
let title = this.props.title || 'no title provided';
let content = this.props.content || 'no content provided';
let articlePicUrl = this.props.articlePicUrl ||
'/static/placeholder.png';
let paperStyle = {
padding: 10,
width: '100%',
height: 300
};
let leftDivStyle = {
width: '30%',
float: 'left'
}
let rightDivStyle = {
width: '60%',
float: 'left',
padding: '10px 10px 10px 10px'
}
return (
<Paper style={paperStyle}>
<CardHeader
title={this.props.title}
subtitle='Subtitle'
avatar='/static/avatar.png'
/>
<div style={leftDivStyle}>
<Card >
<CardMedia
overlay={<CardTitle title={title}
subtitle='Overlay subtitle' />}>
<img src={articlePicUrl} height='190' />
</CardMedia>
</Card>
</div>
<div style={rightDivStyle}>
<div dangerouslySetInnerHTML={{__html: content}} />
</div>
</Paper>);
}
在render的开始,我们使用let articlePicUrl = this.props.articlePicUrl || '/static/placeholder.png';,然后在我们的图片的 JSX 中使用它(img src={articlePicUrl} height='190')。
在这两个更改之后,您可以看到文章有一个真正的封面,就像这样:
改进 DashboardView 组件
让我们通过封面来改进仪表板,所以在src/views/DashboardView.js中找到以下代码:
// old code:
render () {
let articlesJSX = [];
this.props.article.forEach((articleDetails, articleKey) => {
let currentArticleJSX = (
<Link
to={`/edit-article/${articleDetails['_id']}`}
key={articleKey}>
<ListItem
leftAvatar={<img src='/static/placeholder.png'
width='50'
height='50' />}
primaryText={articleDetails.articleTitle}
secondaryText={articleDetails.articleContent}
/>
</Link>
);
articlesJSX.push(currentArticleJSX);
});
// below is rest of the render's function
用以下代码替换它:
render () {
let articlesJSX = [];
this.props.article.forEach((articleDetails, articleKey) => {
let articlePicUrl = articleDetails.articlePicUrl ||
'/static/placeholder.png';
let currentArticleJSX = (
<Link
to={`/edit-article/${articleDetails['_id']}`}
key={articleKey}>
<ListItem
leftAvatar={<img src={articlePicUrl} width='50'
height='50' />}
primaryText={articleDetails.articleTitle}
secondaryText={articleDetails.articleContent}
/>
</Link>
);
articlesJSX.push(currentArticleJSX);
});
// below is rest of the render's function
正如您所看到的,我们已经用真实的封面照片替换了模拟的占位符,所以在我们的文章仪表板(在登录后可用)中,我们将在缩略图中找到真实的图像。
编辑文章的封面照片
关于文章的照片,我们需要在src/views/articles/EditArticleView.js文件中进行一些改进,比如导入ImgUploader:
import ImgUploader from '../../components/articles/ImgUploader';
在导入ImgUploader之后,改进EditArticleView的构造函数。找到以下代码:
// old code to improve:
class EditArticleView extends React.Component {
constructor(props) {
super(props);
this._onDraftJSChange = this._onDraftJSChange.bind(this);
this._articleEditSubmit = this._articleEditSubmit.bind(this);
this._fetchArticleData = this._fetchArticleData.bind(this);
this._handleDeleteTap = this._handleDeleteTap.bind(this);
this._handleDeletion = this._handleDeletion.bind(this);
this._handleClosePopover =
this._handleClosePopover.bind(this);
this.state = {
articleFetchError: null,
articleEditSuccess: null,
editedArticleID: null,
articleDetails: null,
title: 'test',
contentJSON: {},
htmlContent: '',
openDelete: false,
deleteAnchorEl: null
};
}
用新的、改进后的构造函数替换它:
class EditArticleView extends React.Component {
constructor(props) {
super(props);
this._onDraftJSChange = this._onDraftJSChange.bind(this);
this._articleEditSubmit = this._articleEditSubmit.bind(this);
this._fetchArticleData = this._fetchArticleData.bind(this);
this._handleDeleteTap = this._handleDeleteTap.bind(this);
this._handleDeletion = this._handleDeletion.bind(this);
this._handleClosePopover =
this._handleClosePopover.bind(this);
this.updateImgUrl = this.updateImgUrl.bind(this);
this.state = {
articleFetchError: null,
articleEditSuccess: null,
editedArticleID: null,
articleDetails: null,
title: 'test',
contentJSON: {},
htmlContent: '',
openDelete: false,
deleteAnchorEl: null,
articlePicUrl: '/static/placeholder.png'
};
}
正如您所看到的,我们已经将其绑定到新的updateImgUrl函数(这将是ImgUploader的回调),并为articlePicUrl创建了一个新的默认状态。
下一步是改进当前的_fetchArticleData函数:
// this is old already in your codebase:
_fetchArticleData() {
let articleID = this.props.params.articleID;
if(typeof window !== 'undefined' && articleID) {
let articleDetails = this.props.article.get(articleID);
if(articleDetails) {
this.setState({
editedArticleID: articleID,
articleDetails: articleDetails
});
} else {
this.setState({
articleFetchError: true
})
}
}
}
用以下改进后的代码替换它:
_fetchArticleData() {
let articleID = this.props.params.articleID;
if(typeof window !== 'undefined' && articleID) {
let articleDetails = this.props.article.get(articleID);
if(articleDetails) {
this.setState({
editedArticleID: articleID,
articleDetails: articleDetails,
articlePicUrl: articleDetails.articlePicUrl,
contentJSON: articleDetails.articleContentJSON,
htmlContent: articleDetails.articleContent
});
} else {
this.setState({
articleFetchError: true
})
}
}
}
在这里,我们在初始获取中添加了一些新的this.setState变量,比如articlePicUrl,contentJSON和htmlContent。文章获取在这里是因为我们需要在ImgUploader中设置当前可能会更改的图片的封面。contentJSON和htmlContent在用户没有在所见即所得编辑器中编辑任何内容时会用到,我们需要从数据库中获取默认值(否则,编辑按钮会将空值保存到数据库中并破坏整个编辑体验)。
让我们改进_articleEditSubmit函数。这是旧代码:
// old code to improve:
async _articleEditSubmit() {
let currentArticleID = this.state.editedArticleID;
let editedArticle = {
_id: currentArticleID,
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON
// striped code for our convience
更改为以下改进版本:
async _articleEditSubmit() {
let currentArticleID = this.state.editedArticleID;
let editedArticle = {
_id: currentArticleID,
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON,
articlePicUrl: this.state.articlePicUrl
}
// striped code for our convenience
下一步是向EditArticleView组件添加一个新函数:
updateImgUrl(articlePicUrl) {
this.setState({
articlePicUrl: articlePicUrl
});
}
完成文章编辑封面的最后一步是改进旧的渲染:
// old code to improve:
let initialWYSIWYGValue =
this.state.articleDetails.articleContentJSON;
return (
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
<h1>Edit an existing article</h1>
<WYSIWYGeditor
initialValue={initialWYSIWYGValue}
name='editarticle'
title='Edit an article'
onChangeTextJSON={this._onDraftJSChange} />
<RaisedButton
onClick={this._articleEditSubmit}
secondary={true}
type='submit'
style={{margin: '10px auto',
display: 'block', width: 150}}
label={'Submit Edition'} />
<hr />
用以下内容替换它:
let initialWYSIWYGValue =
this.state.articleDetails.articleContentJSON;
return (
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
<h1>Edit an existing article</h1>
<WYSIWYGeditor
initialValue={initialWYSIWYGValue}
name='editarticle'
title='Edit an article'
onChangeTextJSON={this._onDraftJSChange} />
<div style={{margin: '10px 10px 10px 10px'}}>
<ImgUploader updateImgUrl={this.updateImgUrl}
articlePicUrl={this.state.articlePicUrl} />
</div>
<RaisedButton
onClick={this._articleEditSubmit}
secondary={true}
type='submit'
style={{margin: '10px auto',
display: 'block', width: 150}}
label={'Submit Edition'} />
<hr/>
如您所见,我们已经添加了ImgUploader并将其样式与AddArticleView完全相同。ImgUploader的其余部分会帮助我们允许用户编辑文章照片。
在这个截图中,您可以看到所有最近改进后编辑视图应该是什么样子。
添加编辑文章标题和副标题的能力
总的来说,我们将在server/configMongoose.js文件中改进文章的模型。首先找到以下代码:
// old codebase:
var articleSchema = new Schema({
articleTitle: String,
articleContent: String,
articleContentJSON: Object,
articlePicUrl: { type: String, default:
'/static/placeholder.png' }
},
{
minimize: false
}
);
用改进后的代码替换它,如下所示:
var defaultDraftJSobject = {
'blocks' : [],
'entityMap' : {}
}
var articleSchema = new Schema({
articleTitle: { type: String, required: true, default:
'default article title' },
articleSubTitle: { type: String, required: true, default:
'default subtitle' },
articleContent: { type: String, required: true, default:
'default content' },
articleContentJSON: { type: Object, required: true, default:
defaultDraftJSobject },
articlePicUrl: { type: String, required: true, default:
'/static/placeholder.png' }
},
{
minimize: false
}
);
如您所见,我们在我们的模型中添加了许多必需的属性;这将影响保存不完整对象的能力,因此,总的来说,我们的模型将在我们发布应用程序的整个生命周期中更加一致。
我们还向我们的模型添加了一个名为articleSubTitle的新属性,我们将在本章后面使用它。
AddArticleView 改进
总的来说,我们将添加两个DefaultInput组件(标题和副标题),整个表单将使用formsy-react,所以在src/views/articles/AddArticleView.js中,添加新的导入:
import DefaultInput from '../../components/DefaultInput';
import Formsy from 'formsy-react';
下一步是改进async _articleSubmit,所以更改旧代码:
// old code to improve:
async _articleSubmit() {
let newArticle = {
articleTitle: articleModel.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON,
articlePicUrl: this.state.articlePicUrl
}
let newArticleID = await falcorModel
.call(
'articles.add',
[newArticle]
).
// rest code below is striped
用以下内容替换它:
async _articleSubmit(articleModel) {
let newArticle = {
articleTitle: articleModel.title,
articleSubTitle: articleModel.subTitle,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON,
articlePicUrl: this.state.articlePicUrl
}
let newArticleID = await falcorModel
.call(
'articles.add',
[newArticle]
).
如您所见,我们在_articleSubmit参数中添加了articleModel;这将来自formsy-react,我们在LoginView和RegisterView中实现的方式相同。我们还向newArticle对象添加了articleSubTitle属性。
旧的render函数返回如下:
// old code below:
return (
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
<h1>Add Article</h1>
<WYSIWYGeditor
name='addarticle'
title='Create an article'
onChangeTextJSON={this._onDraftJSChange} />
<div style={{margin: '10px 10px 10px 10px'}}>
<ImgUploader updateImgUrl={this.updateImgUrl}
articlePicUrl={this.state.articlePicUrl} />
</div>
<RaisedButton
onClick={this._articleSubmit}
secondary={true}
type='submit'
style={{margin: '10px auto',
display: 'block', width: 150}}
label={'Submit Article'} />
</div>
);
将其更改为以下内容:
return (
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
<h1>Add Article</h1>
<Formsy.Form onSubmit={this._articleSubmit}>
<DefaultInput
onChange={(event) => {}}
name='title'
title='Article Title (required)' required />
<DefaultInput
onChange={(event) => {}}
name='subTitle'
title='Article Subtitle' />
<WYSIWYGeditor
name='addarticle'
title='Create an article'
onChangeTextJSON={this._onDraftJSChange} />
<div style={{margin: '10px 10px 10px 10px'}}>
<ImgUploader updateImgUrl={this.updateImgUrl}
articlePicUrl={this.state.articlePicUrl} />
</div>
<RaisedButton
secondary={true}
type='submit'
style={{margin: '10px auto',
display: 'block', width: 150}}
label={'Submit Article'} />
</Formsy.Form>
</div>
);
在这段代码片段中,我们添加了Formsy.Form,方式与LoginView中一样,所以我不会详细描述它。最重要的是要注意,通过onSubmit,我们调用了this._articleSubmit函数。我们还添加了两个DefaultInput组件(标题和副标题):这两个输入框中的数据将在async _articleSubmit(articleModel)中使用(根据本书中先前的实现,您已经知道这一点)。
根据 Mongoose 配置和AddArticleView组件中的更改,您现在可以向新文章添加标题和副标题,就像以下截图中一样:
我们仍然缺少编辑标题和副标题的能力,所以现在让我们实现它。
编辑文章标题和副标题的能力
转到src/views/articles/EditArticleView.js文件,并添加新的导入(类似于add视图):
import DefaultInput from '../../components/DefaultInput';
import Formsy from 'formsy-react';
改进当前版本中的旧_articleEditSubmit函数:
// old code:
async _articleEditSubmit() {
let currentArticleID = this.state.editedArticleID;
let editedArticle = {
_id: currentArticleID,
articleTitle: this.state.title,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON,
articlePicUrl: this.state.articlePicUrl
}
// rest of the function has been striped below
将其更改为以下内容:
async _articleEditSubmit(articleModel) {
let currentArticleID = this.state.editedArticleID;
let editedArticle = {
_id: currentArticleID,
articleTitle: articleModel.title,
articleSubTitle: articleModel.subTitle,
articleContent: this.state.htmlContent,
articleContentJSON: this.state.contentJSON,
articlePicUrl: this.state.articlePicUrl
}
// rest of the function has been striped below
正如您所看到的,我们在AddArticleView中做了与之相同的事情,所以您应该对此很熟悉。要做的最后一件事是更新render,以便我们能够输入标题和副标题,并将它们作为回调发送到articleModel中的_articleEditSubmit函数。render函数中的旧返回值如下:
// old code:
return (
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
<h1>Edit an existing article</h1>
<WYSIWYGeditor
initialValue={initialWYSIWYGValue}
name='editarticle'
title='Edit an article'
onChangeTextJSON={this._onDraftJSChange} />
<div style={{margin: '10px 10px 10px 10px'}}>
<ImgUploader updateImgUrl={this.updateImgUrl}
articlePicUrl={this.state.articlePicUrl} />
</div>
<RaisedButton
onClick={this._articleEditSubmit}
secondary={true}
type='submit'
style={{margin: '10px auto',
display: 'block', width: 150}}
label={'Submit Edition'} />
<hr />
{/* striped below */}
render函数中的新改进返回值如下:
return (
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
<h1>Edit an existing article</h1>
<Formsy.Form onSubmit={this._articleEditSubmit}>
<DefaultInput
onChange={(event) => {}}
name='title'
value={this.state.articleDetails.articleTitle}
title='Article Title (required)' required />
<DefaultInput
onChange={(event) => {}}
name='subTitle'
value={this.state.articleDetails.articleSubTitle}
title='Article Subtitle' />
<WYSIWYGeditor
initialValue={initialWYSIWYGValue}
name='editarticle'
title='Edit an article'
onChangeTextJSON={this._onDraftJSChange} />
<div style={{margin: '10px 10px 10px 10px'}}>
<ImgUploader updateImgUrl={this.updateImgUrl}
articlePicUrl={this.state.articlePicUrl} />
</div>
<RaisedButton
onClick={this._articleEditSubmit}
secondary={true}
type='submit'
style={{margin: '10px auto',
display: 'block', width: 150}}
label={'Submit Edition'} />
</Formsy.Form>
{/* striped below */}
我们在这里做的与AddArticleView中所做的事情相同。我们引入了Formsy.Form,当用户点击提交按钮(提交编辑)时,它会回调文章的标题和副标题。
以下是应该看起来的样子:
ArticleCard 和 PublishingApp 的改进
改进ArticleCard中的render函数,以便它也显示副标题(目前是模拟的)。src/components/ArticleCard.js文件的旧内容如下:
// old code:
render() {
let title = this.props.title || 'no title provided';
let content = this.props.content || 'no content provided';
let articlePicUrl = this.props.articlePicUrl ||
'/static/placeholder.png';
let paperStyle = {
padding: 10,
width: '100%',
height: 300
};
let leftDivStyle = {
width: '30%',
float: 'left'
}
let rightDivStyle = {
width: '60%',
float: 'left',
padding: '10px 10px 10px 10px'
}
return (
<Paper style={paperStyle}>
<CardHeader
title={this.props.title}
subtitle='Subtitle'
avatar='/static/avatar.png'
/>
<div style={leftDivStyle}>
<Card >
<CardMedia
overlay={<CardTitle title={title}
subtitle='Overlay subtitle' />}>
<img src={articlePicUrl} height='190' />
</CardMedia>
</Card>
</div>
<div style={rightDivStyle}>
<div dangerouslySetInnerHTML={{__html: content}} />
</div>
</Paper>);
}
让我们将其更改为以下内容:
render() {
let title = this.props.title || 'no title provided';
let subTitle = this.props.subTitle || '';
let content = this.props.content || 'no content provided';
let articlePicUrl = this.props.articlePicUrl ||
'/static/placeholder.png';
let paperStyle = {
padding: 10,
width: '100%',
height: 300
};
let leftDivStyle = {
width: '30%',
float: 'left'
}
let rightDivStyle = {
width: '60%',
float: 'left',
padding: '10px 10px 10px 10px'
}
return (
<Paper style={paperStyle}>
<CardHeader
title={this.props.title}
subtitle={subTitle}
avatar='/static/avatar.png'
/>
<div style={leftDivStyle}>
<Card >
<CardMedia
overlay={<CardTitle title={title}
subtitle={subTitle} />}>
<img src={articlePicUrl} height='190' />
</CardMedia>
</Card>
</div>
<div style={rightDivStyle}>
<div dangerouslySetInnerHTML={{__html: content}} />
</div>
</Paper>);
}
正如您所看到的,我们已经定义了一个新的subTitle变量,并在CardHeader和CardMedia组件中使用它,所以现在它也会显示副标题。
另一件事是让PublishingApp也获取在本章中引入的副标题,因此我们需要改进以下旧代码:
// old code:
async _fetch() {
let articlesLength = await falcorModel.
getValue('articles.length').
then((length) => length);
let articles = await falcorModel.
get(['articles', {from: 0, to: articlesLength-1},
['_id','articleTitle', 'articleContent',
'articleContentJSON', 'articlePicUrl']]).
then((articlesResponse) => {
return articlesResponse.json.articles;
}).catch(e => {
console.debug(e);
return 500;
});
// no changes below, striped
将其替换为以下内容:
async _fetch() {
let articlesLength = await falcorModel.
getValue('articles.length').
then((length) => length);
let articles = await falcorModel.
get(['articles', {from: 0, to: articlesLength-1}, ['_id',
'articleTitle', 'articleSubTitle','articleContent',
'articleContentJSON', 'articlePicUrl']]).
then((articlesResponse) => {
return articlesResponse.json.articles;
}).catch(e => {
console.debug(e);
return 500;
});
正如您所看到的,我们已经开始使用falcorModel.get来获取articleSubTitle属性。
当然,我们需要将这个subTitle属性传递给PublishingApp类的render函数中的ArticleCard组件。
// old code:
this.props.article.forEach((articleDetails, articleKey) => {
let currentArticleJSX = (
<div key={articleKey}>
<ArticleCard
title={articleDetails.articleTitle}
content={articleDetails.articleContent}
articlePicUrl={articleDetails.articlePicUrl} />
</div>
);
最终,我们将得到以下结果:
this.props.article.forEach((articleDetails, articleKey) => {
let currentArticleJSX = (
<div key={articleKey}>
<ArticleCard
title={articleDetails.articleTitle}
content={articleDetails.articleContent}
articlePicUrl={articleDetails.articlePicUrl}
subTitle={articleDetails.articleSubTitle} />
</div>
);
在主页上所有这些更改之后,你可以找到一个编辑过的文章,包括标题、副标题、封面照片和内容(由我们的所见即所得编辑器创建):
仪表板改进(现在我们可以剥离剩余的 HTML)
本章的最后一步是改进仪表板。它将从 props 中提取 HTML,以便在用户浏览我们的应用程序时获得更好的外观和感觉。找到以下代码:
// old code:
this.props.article.forEach((articleDetails, articleKey) => {
let articlePicUrl = articleDetails.articlePicUrl ||
'/static/placeholder.png';
let currentArticleJSX = (
<Link to={`/edit-article/${articleDetails['_id']}`}
key={articleKey}>
<ListItem
leftAvatar={<img src={articlePicUrl} width='50'
height='50' />}
primaryText={articleDetails.articleTitle}
secondaryText={articleDetails.articleContent}
/>
</Link>
);
用以下代码替换:
this.props.article.forEach((articleDetails, articleKey) => {
let articlePicUrl = articleDetails.articlePicUrl ||
'/static/placeholder.png';
let articleContentPlanText =
articleDetails.articleContent.replace(/</?[^>]+(>|$)/g,
'');
let currentArticleJSX = (
<Link to={`/edit-article/${articleDetails['_id']}`}
key={articleKey}>
<ListItem
leftAvatar={<img src={articlePicUrl} width='50'
height='50' />}
primaryText={articleDetails.articleTitle}
secondaryText={articleContentPlanText}
/>
</Link>
);
如你所见,我们只是从 HTML 中剥离 HTML 标签,这样我们将获得更好的secondaryText,而不带有 HTML 标记,就像这个例子中一样:
总结
我们已经实现了书中涵盖的所有功能。下一步是开始着手部署这个应用程序。
如果你想提高编码技能,最好自己完全实现一些功能。以下是一些我们发布应用程序中仍然缺少的功能的想法。
我们可以有一个单独的链接指向某篇文章,这样你可以与朋友分享。如果你想在数据库中创建一个与某篇文章相关的易读唯一标识,这可能会很有用。因此,用户可以分享类似于reactjs.space/an-article-about-a-dog这样的链接,而不是链接到类似于reactjs.space/570b6e26ae357d391c6ebc1d(reactjs.space是我们在生产服务器上将使用的域名)。
可能有一种方法将一篇文章与发布它的编辑关联起来。目前是模拟的。你可以取消模拟。
用户在登录状态下无法更改其用户详细信息--这可能是练习更全面的全栈开发的好方法。
用户无法设置他们的头像图片--你可以以类似的方式添加这个功能,就像我们实现封面图片一样。
创建一个更强大的 Draft.JS 所见即所得编辑器与插件。强大的插件易于实现提及、贴纸、表情符号、标签、撤销/重做等功能。访问www.draft-js-plugins.com/了解更多详情。实现你最喜欢的一个或两个。
在下一章中,我们将开始使用www.mLab.com在线部署我们的 MongoDB 实例,这是一个作为服务提供商,可以帮助我们轻松构建可扩展的 MongoDB 节点。
让我们开始部署的乐趣吧!
不要忘记休息一下哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享