2017/03/29 Update: Fixed the versions of react-router and react-hot-loader. 2017/03/20 Update: Webpack 2 configuration.
2017/03/29更新 :修复了react-router和react-hot-loader的版本。 2017/03/20 更新 :Webpack 2配置。
The Javascript stacks, MEAN and MERN on top, are definitely the hottest tech in the web development community nowadays. In fact all the Javascript ecosystem is continuosly expanding with updates and new packages on a daily basis. Finding the direction may be seen as an overwhelming tasks sometimes, especially for beginners, but luckily communites like Scotch.io strive to provide the right direction with always up-to-date tutorials and articles.
Javascript堆栈(位于MEAN和MERN之上)绝对是当今Web开发社区中最热门的技术。 实际上,所有Javascript生态系统每天都在不断扩展,包括更新和新软件包。 有时,找到方向可能会被认为是一项繁重的任务,特别是对于初学者而言,但是幸运的是,像Scotch.io这样的社区努力通过始终提供最新的教程和文章来提供正确的方向。
In this tutorial we are going to write an archive for retrogames by using Javascript in both backend and frontend:
在本教程中,我们将通过在后端和前端中使用Javascript来编写复古游戏档案:
We will combine express built on top of Node.js with React and Redux to demonstrate how easy is to write a single page app.
我们将结合在Node.js之上的express与React和Redux结合起来,以演示编写一个单页面应用程序有多么容易。
To persist data we are using Mongo.db which integrates pretty well with Node.js thanks to its Mongoose ODM.
为了持久化数据,我们使用Mongo.db,由于其Mongoose ODM,它与Node.js集成得很好。
In addition, to upload pictures with no hassle we are gonna integrate Filestack in our app which returns a CDN url for us:
此外,要轻松上传图片,我们将Filestack集成到我们的应用中,该应用会为我们返回CDN网址:
Filestack hosts our pictures saving the burden to make sure our machines has the space to store them as well as avoid us all the uploading related security.
Filestack托管我们的图片,从而减轻了负担,以确保我们的机器具有存储它们的空间,并避免了我们所有与上传有关的安全性。
Last but not least, the free account is enough to implement all the functionalities we need for the app.
最后但并非最不重要的一点是,免费帐户足以实现我们为应用程序所需的所有功能。
Our archive allows users to view, create and delete games of the past that made history. I am a huge fan of games like Super Mario Bros., Street Fighter, Sonic, King of Fighters so I really enjoyed writing this app, I hope you too!
我们的档案库使用户可以查看,创建和删除过去创造历史的游戏。 我是超级马里奥兄弟,街头霸王,索尼克,拳皇之类的游戏的忠实粉丝,所以我真的很喜欢编写这个应用程序,希望你也喜欢!
I separated the tutorials in different parts to make it easier to digest all the information. In this very first tutorial we are going to setup the Node.js, connect it to Mongo.db, write the games API and test it with postman. Then, we will write it using React and serve it with webpack-dev-server. In the second part of the tutorial we are going to include Redux state container and Redux-saga to perform asynchronous HTTP requests to our real server. Finally, I may add a third part, a bonus one, to show some simple authentication and improve the UI.
我将教程分为不同的部分,以使消化所有信息变得更加容易。 在这第一个教程中,我们将设置Node.js,将其连接到Mongo.db,编写游戏API并使用邮递员对其进行测试。 然后,我们将使用React编写它,并将其与webpack-dev-server一起使用。 在本教程的第二部分中,我们将包含Redux状态容器和Redux-saga,以对实际服务器执行异步HTTP请求。 最后,我可以添加第三部分,即奖金部分,以显示一些简单的身份验证并改进UI。
I suggest to follow the tutorial and build the app step-by-step, however you can also find the app on github:
我建议按照教程并逐步构建应用程序,但是您也可以在github上找到该应用程序:
Once you cloned/forked the repo, just checkout to tutorial/part1 branch.
克隆/创建仓库后,只需签出到tutorial / part1分支即可。
git checkout tutoral/part1
--app
----models
------game.js
----routes
------game.js
--client
----dist
------css
--------style.css
------fonts
--------PressStart2p.ttf
------index.html
------bundle.js
----src
------components
--------About.jsx
--------Archive.jsx
--------Contact.jsx
--------Form.jsx
--------Game.jsx
--------GamesListManager.jsx
--------Home.jsx
--------index.js
--------Modal.jsx
--------Welcome.jsx
------containers
--------AddGameContainer.jsx
--------GamesContainer.jsx
------index.js
------routes.js
--.babelrc
--package.json
--server.js
--webpack-loaders.js
--webpack-paths.js
--webpack.config.js
--yarn.lock
Notice the two files /client/src/components/index.js
and /client/src/containers/index.js
:
请注意两个文件/client/src/components/index.js
和/client/src/containers/index.js
:
I am using them to export all the components and containers in a single file to write the import more easily. Take a look at this example:
我正在使用它们将所有组件和容器导出到一个文件中,以便更轻松地编写导入。 看一下这个例子:
import c1 from './c1.jsx';
import c2 from './c2.jsx';
import c3 from './c3.jsx';
export {
c1, c2, c3 };
And then we can include them within a single line:
然后我们可以将它们包含在一行中:
import {
c1, c2, c3 } from '../components';
So we are going to write our server API! let's define the routes first:
因此,我们将编写服务器API! 让我们先定义路线:
Routes Table | |
---|---|
GET /games | Get all the games. |
POST /games | Create a game. |
GET /games/:id | Get a single game. |
DELETE /games/:id | Delete a game. |
路线表 | |
---|---|
GET /游戏 | 获取所有游戏。 |
POST /游戏 | 创建一个游戏。 |
GET / games /:id | 取得一场比赛。 |
删除/ games /:id | 删除游戏。 |
Nothing exotic up here, we just defined some common routes to edit our archive. Before we start creating the project, you should have already asked yourself where are we going to save the data... Are we gonna persist it? Yes, we are gonna use Mongoose ODM to persist data on a Mongo database.
在这里没有什么异国情调的地方,我们只是定义了一些编辑存档的常用路线。 在开始创建项目之前,您应该已经问过自己,我们将在哪里保存数据...我们将坚持下去吗? 是的,我们将使用Mongoose ODM将数据持久存储在Mongo数据库中。
In the newly created project folder we first initialize the package.json
:
在新创建的项目文件夹中,我们首先初始化package.json
:
yarn init
So now let's start adding our dependencies. For the server part of the project we just need a few, Express (definitely), Mongoose, Body-parser, Morgan and Babel transpiler to use ES6 syntax throughout our app.
现在开始添加依赖项。 对于项目的服务器部分,我们只需要一些Express (绝对), Mongoose , Body-parser , Morgan和Babel Transpiler即可在整个应用程序中使用ES6语法。
NB: Babel is great but not suggested for production as it slows down the server while transpiling from ES6 to ES5.
注意 :Babel很棒,但不建议用于生产环境,因为它会降低从ES6到ES5的编译过程中的服务器速度。
We need to run two commands as babel is a dev-dependency:
我们需要运行两个命令,因为babel是一个dev-dependency:
yarn add express mongoose morgan body-parser
yarn add babel-core babel-cli babel-preset-es2015 --dev
Now we are able to run our server with babel-node server.js
. however It's good practice to create a specific command inside the package.json
under "scripts". So, open the package.json
file and add
现在,我们可以使用babel-node server.js
运行服务器。 但是,最好在package.json
“脚本”下创建特定命令。 因此,打开package.json
文件并添加
"scripts": {
"api": "babel-node server.js"
}
So now we can just run
所以现在我们可以运行
yarn api
At the end your package.json
file should be similar to mine:
最后,您的package.json
文件应与我的类似:
{
"name": "tutorial",
"version": "1.0.0",
"main": "server.js",
"author": "Sam",
"license": "MIT",
"scripts": {
"api": "babel-node server.js"
},
"dependencies": {
"body-parser": "^1.15.2",
"express": "^4.14.0",
"mongoose": "^4.7.2",
"morgan": "^1.7.0"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-core": "^6.20.0",
"babel-preset-es2015": "^6.18.0"
}
}
In addition to this, to effectively take advantage of Babel transpiler we have to create a file .babelrc
in the root folder, then paste the following code:
除此之外,为了有效利用Babel transpiler,我们必须在根文件夹中创建一个文件.babelrc
,然后粘贴以下代码:
{
"presets": ["es2015"]
}
NB: According to the documentation you can specify your config within the package.json
file too.
注意 :根据文档,您也可以在package.json
文件中指定配置。
At this point it's really time to code! We need a server file where we configure our express server, connect the body-parser and morgan middlewares as well as mongoose, write our routes and so on. Create the server.js
file in the root folder and past the following code:
现在是时候编写代码了! 我们需要一个服务器文件,在其中配置快速服务器,连接正文解析器和morgan中间件以及猫鼬,编写路由等。 在根文件夹中并通过以下代码创建server.js
文件:
import express from 'express';
import bodyParser from 'body-parser';
import mongoose from 'mongoose';
import morgan from 'morgan';
// We gotta import our models and routes
import Game from './app/models/game';
import {
getGames, getGame, postGame, deleteGame } from './app/routes/game';
const app = express(); // Our express server!
const port = process.env.PORT || 8080;
// DB connection through Mongoose
const options = {
server: {
socketOptions: {
keepAlive: 1, connectTimeoutMS: 30000 } },
replset: {
socketOptions: {
keepAlive: 1, connectTimeoutMS : 30000 } }
}; // Just a bunch of options for the db connection
mongoose.Promise = global.Promise;
// Don't forget to substitute it with your connection string
mongoose.connect('YOUR_MONGO_CONNECTION', options);
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
// Body parser and Morgan middleware
app.use(bodyParser.urlencoded({
extended: true}));
app.use(bodyParser.json());
app.use(morgan('dev'));
// We tell express where to find static assets
app.use(express.static(__dirname + '/client/dist'));
// Enable CORS so that we can make HTTP request from webpack-dev-server
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE');
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
// API routes
app.route('/games')
// create a game
.post(postGame)
// get all the games
.get(getGames);
app.route('/games/:id')
// get a single game
.get(getGame)
// delete a single game
.delete(deleteGame);
// ...For all the other requests just sends back the Homepage
app.route("*").get((req, res) => {
res.sendFile('client/dist/index.html', {
root: __dirname });
});
app.listen(port);
console.log(`listening on port ${
port}`);
The code is pretty straightforward:
该代码非常简单:
Before writing the functions in /app/routes/game.js
let's define our Game model!
在/app/routes/game.js
编写函数之前,我们先定义游戏模型!
The Game model is very simple, we need the game name, a description, the year it was published and a picture. Plus, postDate to track the time it was created.
游戏模型非常简单,我们需要游戏名称 , 描述 ,发布年份和图片 。 另外, postDate可以跟踪创建时间。
Paste the following code in /app/models/game.js
:
将以下代码粘贴到/app/models/game.js
:
// Dependencies
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
// Our schema definition
const gameSchema = new Schema(
{
name: String,
year: Number,
description: String,
picture: String,
postDate : {
type: Date, default: Date.now } // Timestamp
}
);
// We export the schema to use it anywhere else
export default mongoose.model('Game', gameSchema);
Notice I did not mark any field as required:
注意,我没有按要求标记任何字段:
Although I recommend to do it in your personal projects, for the purpose of the tutorial I wanted to be as concise as possible.
尽管我建议在您的个人项目中进行此操作,但出于教程的目的,我希望尽可能简洁。
Next, let's create the callback functions to handle the requests and responses and we can test our server.
接下来,让我们创建回调函数来处理请求和响应,然后我们可以测试服务器。
Create the game.js
file in /client/app/routes
and paste the following code:
在/client/app/routes
创建game.js
文件,然后粘贴以下代码:
// We import our game schema
import Game from '../models/game';
// Get all the games sorted by postDate
const getGames = (req, res) => {
// Query the db, if no errors send all the games to the client
Game.find(null, null, {
sort: {
postDate : 1 } }, (err, games) => {
if (err) {
res.send(err);
}
res.json(games); // Games sent as json
});
}
// Get a single game filtered by ID
const getGame = (req, res) => {
const {
id } = req.params;
// Query the db for a single game, if no errors send it to the client
Game.findById(id, (err, game) => {
if (err) {
res.send(err);
}
res.json(game); // Game sent as json
});
}
// Get the body data and create a new Game
const postGame = (req, res) => {
// We assign the game info to a empty game and send a message back if no errors
let game = Object.assign(new Game(), req.body);
// ...Then we save it into the db
game.save(err => {
if (err) {
res.send(err);
}
res.json({
message: 'game created' }); // A simple JSON answer to inform the client
});
};
// Delete a game by the given ID
const deleteGame = (req, res) => {
// We remove the game by the given id and send a message back if no errors
Game.remove(
{
_id: req.params.id },
err => {
if (err) {
res.send(err);
}
res.json({
message: 'successfully deleted' }); // A simple JSON answer to inform the client
}
);
};
// We export our functions to be used in the server routes
export {
getGames, getGame, postGame, deleteGame };
The four functions take care of the user requests: They all gonna communicate with the database through the Game model and return a defined response to the client.
这四个功能处理用户的请求:它们都将通过游戏模型与数据库通信,并向客户端返回定义的响应。
Our server is complete, let's give it a try!
我们的服务器已完成,请尝试一下!
For this simple test we first create a new game and doublecheck whether it gets really added to the archive. We consequently gonna delete it and make sure it really disappeared from the archive! By doing so we test all the routes we previously defined.
对于这个简单的测试,我们首先创建一个新游戏并仔细检查它是否真的添加到了存档中。 因此,我们将其删除,并确保它确实从存档中消失了! 通过这样做,我们测试了先前定义的所有路由。
I personally use Postman browser extension to achieve this result but feel free to use your favorite tools.
我个人使用Postman浏览器扩展来实现此结果,但是可以随时使用自己喜欢的工具。
Let's start our server with
让我们启动服务器
yarn api
My database is already populated with a few games, here is the result:
我的数据库已经填充了一些游戏,结果如下:
Let's try to add a game with random information since we are going to delete it soon!
让我们尝试添加包含随机信息的游戏,因为我们将很快将其删除!
As counterproof let's make another GET request to /games and see whether it was really added to the archive.
作为反证,让我们向/ games发出另一个GET请求,看看它是否真的添加到了存档中。
Cool it was really added!
真的很酷!
Let's try to filter the games by id, trivial test but we want to cover all the endpoints.
让我们尝试通过id,琐碎的测试来筛选游戏,但我们希望涵盖所有端点。
Seems to be working smoothly.
似乎工作顺利。
Now, given the id, let's try to delete it:
现在,给定ID,让我们尝试将其删除:
Finally, let's doublecheck if it was really deleted:
最后,让我们仔细检查它是否确实被删除:
Awesome the server is ready, time to work on the client-side!
很棒的服务器已经准备好了,该在客户端上工作了!
All of you familiar with React already know we need a few steps before really coding the client. While there are some solutions like react-create-app which aims to avoid the initial configuration hassle, I still prefer to manually install all the packages which also has an educational value for the tutorial. For anyone interested in digging into Webpack, I suggest to take a look at survive.js. It's a valuable resource for learning Webpack and React, plus the e-books can be read online for free.
熟悉React的所有人都已经知道,在真正编码客户端之前,我们需要一些步骤。 虽然有一些解决方案(例如react-create-app)旨在避免初始配置的麻烦,但我仍然更喜欢手动安装所有软件包,这些软件包对于本教程也具有教育意义。 对于有兴趣研究Webpack的任何人,我建议看一下Surviv.js 。 这是学习Webpack和React的宝贵资源,并且可以免费在线阅读电子书。
Let's start installing some packages now:
现在开始安装一些软件包:
yarn add webpack webpack-dev-server webpack-merge --dev
We are obviously installing webpack to help us create the bundle along with webpack-dev-server for serving us the client during development.
显然,我们正在安装webpack,以帮助我们与webpack-dev-server一起创建捆绑包,以便在开发过程中为我们的客户提供服务。
Perhaps not everyone is familiar with the latter one: webpack-merge helps to merge pieces of configurations together.
也许不是每个人都熟悉后一种: webpack-merge有助于将配置片段合并在一起。
Other than this, we need a few loaders:
除此之外,我们需要一些装载机:
yarn add babel-preset-react babel-loader react-hot-loader@next style-loader css-loader file-loader --dev
These helps in severals tasks like transpiling (guess which one!), include the css and the fonts in our bundle as well as avoid to refresh the page while changing the code in React. As there are continuous updates on these packages, make sure that react-hot-loader is up-to-date because the syntax to include it in .babelrc
changed, I am currently using the version 3.0.0-beta.6 thanks to @next
. In case you receive a webpack error stating that it cannot find load the plugin, just run
这些帮助完成了一些任务,例如转译(猜测是哪一个!),将css和字体包含在我们的软件包中,以及避免在React中更改代码时刷新页面。 由于这些软件包会不断更新,因此请确保react-hot-loader是最新的,因为将其包含在.babelrc
的语法.babelrc
更改,由于@next
,我目前正在使用3.0.0-beta.6版本@next
。 如果您收到一个webpack错误消息,指出找不到插件,请运行
yarn add [email protected] --dev
To organize our code for better readability, webpack-config.js
will require some data from other files.
为了组织我们的代码以提高可读性, webpack-config.js
将需要其他文件中的一些数据。
Let's create webpack-paths.js
and paste the following code:
让我们创建webpack-paths.js
并粘贴以下代码:
"use strict";
const path = require('path');
// We define some paths to be used throughout the webpack config
module.exports = {
src: path.join(__dirname, 'client/src'),
dist: path.join(__dirname, 'client/dist'),
css: path.join(__dirname, 'client/dist/css')
};
As you can see we want to export some paths we are using inside the webpack configuration. Let's move on and create the webpack.config.js
file and paste the following code:
如您所见,我们想导出在webpack配置中使用的一些路径。 让我们继续创建webpack.config.js
文件,然后粘贴以下代码:
"use strict";
const merge = require('webpack-merge');
const PATHS = require('./webpack-paths');
const loaders = require('./webpack-loaders');
const common = {
entry: {
// The entry file is index.js in /client/src
app: PATHS.src
},
output: {
// The output defines where the bundle output gets created
path: PATHS.dist,
filename: 'bundle.js'
},
module: {
rules: [
loaders.babel, // Transpiler
loaders.css, // Our bundle will contain the css
loaders.font, // Load fonts
]
},
resolve: {
extensions: ['.js', '.jsx'] // the extensions to resolve
}
};
let config;
// The switch defines the different configuration as development requires webpack-dev-server
switch(process.env.NODE_ENV) {
case 'build':
config = merge(
common,
{
devtool: 'source-map' } // SourceMaps on separate file
);
break;
case 'development':
config = merge(
common,
{
devtool: 'eval-source-map' }, // Default value
loaders.devServer({
host: process.env.host,
port: 3000
})
);
}
// We export the config
module.exports = config;
The loaders are imported from another file, webpack-loaders.js
. Let's create it and paste the following code:
加载程序是从另一个文件webpack-loaders.js
导入的。 让我们创建它并粘贴以下代码:
"use strict";
const webpack = require('webpack');
const PATHS = require('./webpack-paths');
exports.devServer = function(options) {
return {
devServer:{
historyApiFallback: true,
hot: true, // Enable hot module
inline: true,
stats: 'errors-only',
host: options.host, // http://localhost
port: options.port, // 3000
contentBase: './client/dist',
},
// Enable multi-pass compilation for enhanced performance
plugins: [ // Hot module
new webpack.HotModuleReplacementPlugin({
multistep: true
})
]
};
}
// the css loader
exports.css = {
test: /\.css$/,
use: ['style-loader', 'css-loader'],
include: PATHS.css
}
// The file loader
exports.font = {
test: /\.ttf$/,
use: ['file-loader']
}
// Babel loader
exports.babel = {
test: /\.jsx?$/,
exclude: /node_modules/,
use: ['babel-loader']
};
The file just exports the loaders and webpack-dev-server with the hot-reload plugin.
该文件仅使用热重载插件导出加载程序和webpack-dev-server。
We also have to edit .babelrc
:
我们还必须编辑.babelrc
:
{
"presets": [
"es2015",
"react"
],
"plugins": [
"react-hot-loader/babel"
]
}
We added the preset for react and react-hot-loader plugin.
我们为react和react-hot-loader插件添加了预设。
Finally, we also gotta edit package.json
to include new scripts commands:
最后,我们还必须编辑package.json
以包含新的脚本命令:
"start": "NODE_ENV=development webpack-dev-server",
"build": "NODE_ENV=build webpack"
We set the NODE_ENV variable to switch between the two configurations we defined before in webpack.config.js
.
我们将NODE_ENV变量设置为在之前在webpack.config.js
定义的两个配置之间切换。
NB: If you are a windows user you may add a & to concatenate the commands:
注意 :如果您是Windows用户,则可以添加&来连接命令:
"start": "NODE_ENV=development & webpack-dev-server",
"build": "NODE_ENV=build & webpack"
First, let's create the index file served by the server which includes all our assets, bundle.js
included. In /client/dist
create a file index.html
and paste the following code:
首先,让我们创建服务器提供的索引文件,其中包含我们所有的资产,包括bundle.js
。 在/client/dist
创建一个文件index.html
并粘贴以下代码:
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Retrogames Archivetitle>
<link rel="icon" href="https://cdn.filestackcontent.com/S0zeyXxRem6pL6tHq9pz">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
head>
<body>
<div id="content">div>
<script src="https://code.jquery.com/jquery-3.1.1.min.js">script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js">script>
<script src="https://api.filestackapi.com/filestack.js">script>
<script src="./bundle.js">script>
body>
html>
Regarding the css, I have just customized two templates that you can find on the boostrap website and included some cool fonts I found. Just take copy them from my project.
关于CSS,我刚刚定制了两个模板,您可以在boostrap网站上找到它们,并包含一些我发现的漂亮字体。 只需从我的项目中复制它们即可。
Before diggin' into the React components it's better to setup a Filestack account, so on their website just click on the try it free button and follow the instructions:
在深入研究React组件之前,最好设置一个Filestack帐户,因此在其网站上只需单击免费试用按钮并按照说明进行操作:
Once in the developer portal you are immediately proposed to add their snippet in the project which is great because it would save us time but we want to customize the uploader right? So just skip it and instead grab the API key (click on "New application" in case) as we are needing it later!
进入开发人员门户后,系统会立即建议您在项目中添加其摘要,这很不错,因为这可以节省我们的时间,但我们想自定义上传器,对吗? 因此,只需跳过它,而是获取API密钥(以防万一,单击“ New application”),以备稍后使用!
It's finally time to write our components! Our archive will welcome user with a nice UI:
现在是时候编写我们的组件了! 我们的归档文件将以良好的用户界面欢迎用户:
We provide the user some simple informations about us as well as the app in three different views. Once they click on Browse! they are redirected to the real archive list where they can add new games as well as view the details and delete them:
我们以三种不同的视图向用户提供有关我们以及应用程序的一些简单信息。 一旦他们单击浏览! 它们将被重定向到实际的存档列表,在其中可以添加新游戏以及查看详细信息并将其删除:
Our routes configuration is composed by two main routes with their children routes:
我们的路线配置由两条主要路线及其子路线组成:
Notice I named them* Homepage* and Games to help you guys understand the structure but in the code they actually don't carry any name themselves.
请注意,我为它们*主页*和游戏命名是为了帮助你们了解结构,但是在代码中它们实际上并不携带任何名称。
Let's install a few packages:
让我们安装一些软件包:
yarn add react react-dom [email protected]
NB: I am using the version 3 of react-router in the tutorial.
注意 :我在本教程中使用的是react-router的版本3。
We can start by creating a file index.js
in /client/src
and past the following code:
我们可以通过在/client/src
创建文件index.js
并通过以下代码开始:
import '../dist/css/style.css';
import React from 'react';
import ReactDOM from 'react-dom';
import Routes from './routes';
// Don't forget to add your API key
filepicker.setKey("YOUR_API_KEY");
// Our views are rendered inside the #content div
ReactDOM.render(
Routes,
document.getElementById('content')
);
We included react-dom so we can render the routes in the div element with id content! Also, this is where you should set Filestack's API key.
我们包含了react-dom,因此我们可以在div元素中使用id内容渲染路由! 同样,在这里应该设置Filestack的API密钥。
Well, our routes configuration is in the same folder so create routes.js
in /client/src
and paste the following code:
好吧,我们的路由配置位于同一文件夹中,因此routes.js
在/client/src
创建routes.js
并粘贴以下代码:
import React from 'react';
import {
Router, Route, hashHistory, IndexRoute } from 'react-router';
import {
Home, Welcome, About, Contact } from './components';
// Use hashHistory for easier development
const routes = (
<Router history={
hashHistory}>
<Route path="/" component={
Home}>
<IndexRoute component={
Welcome} />
<Route path="/about" component={
About} />
<Route path="/contact" component={
Contact} />
</Route>
</Router>
);
export default routes;
We imported a few components from react-router and defined our first URL paths structure:
我们从react-router导入了一些组件,并定义了我们的第一个URL路径结构:
Url | Component |
---|---|
/ | Home -> Welcome |
/about | Home -> About |
/contact | Home -> Contact |
网址 | 零件 |
---|---|
/ | 首页->欢迎光临 |
/关于 | 首页->关于 |
/联系 | 首页->联系方式 |
We are using hashHistory so we don't need any server configuration in case of page refresh. Moreover, notice the four components we are going to write are stateless, they are just presentational components that are not going to touch the state so they are very easy to write. Let's do it!
我们正在使用hashHistory,因此在刷新页面时不需要任何服务器配置。 此外,请注意,我们将要编写的四个组件是无状态的,它们只是表示形式的组件,不会涉及状态,因此它们非常容易编写。 我们开始做吧!
NB: React 15.3.0 introduced PureComponent to replace pure-render-mixin which does not work with ES6 classes so we can actually extends it for our stateless components.
注意: React 15.3.0引入了PureComponent来代替不能与ES6类一起使用的pure-render-mixin,因此我们实际上可以将其扩展为无状态组件。
This component is basically the skeleton for the others, in /client/src/components
create a file Home.jsx
and paste the following code:
此组件基本上是其他组件的框架,在/client/src/components
创建文件Home.jsx
并粘贴以下代码:
import React, {
PureComponent } from 'react';
import {
Link } from 'react-router';
export default class Home extends PureComponent {
active (path) {
// Returns active when the path is equal to the current location
if (this.props.location.pathname === path) {
return 'active';
}
}
render () {
return (
<div className="main">
<div className="site-wrapper">
<div className="site-wrapper-inner">
<div className="cover-container">
<div className="masthead clearfix">
<div className="inner">
<nav>
<img className="header-logo" src="https://cdn.filestackcontent.com/nLnmrZQaRpeythR4ezUo"/>
<ul className="nav masthead-nav">
<li className={
this.active('/')}><Link to="/">Home</Link></li>
<li className={
this.active('/about')}><Link to="/about">About</Link></li>
<li className={
this.active('/contact')}><Link to="/contact">Contact</Link></li>
</ul>
</nav>
</div>
</div>
{
this.props.children}
</div>
</div>
</div>
</div>
);
}
}
{this.props.children}
is where we render the three children components. {this.props.children}
是我们渲染三个子组件的地方。 active()
function which checks the pathname against the path parameter we pass. If we had to change the class of the Link component we wouldn't need any function but unfortunately the theme I grabbed from Bootstrap applies "active" on the li element instead. 单击时需要将类更改为li元素,这很容易通过active()
函数来实现,该函数根据传递的path参数检查路径名。 如果我们必须更改Link组件的类,则不需要任何功能,但是不幸的是,我从Bootstrap抓取的主题在li元素上应用了“ active”。 This component welcomes our user and provides the link to navigate to the games archive, create the file Welcome.jsx
in /client/src/component
and paste the following code:
该组件欢迎我们的用户,并提供链接导航到游戏存档,在/client/src/component
创建文件Welcome.jsx
并粘贴以下代码:
import React, {
PureComponent } from 'react';
import {
Link } from 'react-router';
export default class Welcome extends PureComponent {
render () {
return (
<div className="inner cover">
<h1 className="cover-heading">Welcome</h1>
<p className="lead">Click on browse to start your journey into the wiki of games that made history.</p>
<p className="lead">
<Link className="btn btn-lg" to="/games">Browse!</Link>
</p>
</div>
);
}
}
It doesn't require any explanation, just a welcome message and the browse! link to view the games archive.
它不需要任何解释,只需欢迎消息和浏览即可! 链接以查看游戏存档。
Create About.jsx
in /client/src/components
and paste the following code:
在/client/src/components
创建About.jsx
并粘贴以下代码:
import React, {
PureComponent } from 'react';
export default class About extends PureComponent {
render () {
return (
<div className="inner cover">
<h1 className="cover-heading">Javascript Everywhere</h1>
<p className="lead">This archive is made with Node.js and React. The two communicate through async HTTP requests handled by Redux-saga... Yes we love Redux here!</p>
</div>
);
}
}
Even simplier, just a simple explanation on how we wrote the app!
甚至更简单,只需简单解释一下我们如何编写该应用程序即可!
Create Contact.jsx
in /client/src/components
and paste the following code:
在/client/src/components
创建Contact.jsx
并粘贴以下代码:
import React, {
PureComponent } from 'react';
export default class About extends PureComponent {
render () {
return (
<div className="inner cover">
<h1 className="cover-heading">Any Questions?</h1>
<p className="lead">Don't hesitate to contact me: zaza.samuele@gmail.com</p>
</div>
);
}
}
Feel free to change the text!
随时更改文本!
We need this file to export all the components, let's create it and paste the following code:
我们需要此文件来导出所有组件,让我们创建它并粘贴以下代码:
import About from './About';
import Contact from './Contact';
import Home from './Home';
import Welcome from './Welcome';
// We export all the components at once
export {
About, Contact, Home, Welcome };
At this point we can already see the client in action, just run
至此,我们已经可以看到客户端正在运行,只需运行
yarn start
and open http://localhost:3000 in the browser. Though we haven't completed the app we can already see the welcome page as well as the other links on the top-right of the page.
并在浏览器中打开http:// localhost:3000 。 尽管我们尚未完成应用程序的安装,但我们已经可以在页面的右上角看到欢迎页面以及其他链接。
Let's now work on the interactive pages.
现在让我们在交互式页面上工作。
We already discussed about the url, we need to update our route configuration: We have other two views, one for the games list and one which is basically the form users upload games.
我们已经讨论了有关url的内容,我们需要更新路由配置:我们还有另外两个视图,一个用于游戏列表,一个基本上是用户上传游戏的形式。
Open routes.js and replace the code with the following:
打开routes.js并将代码替换为以下代码:
import React from 'react';
import {
Router, Route, hashHistory, IndexRoute } from 'react-router';
import {
AddGameContainer, GamesContainer } from './containers';
import {
Home, Archive, Welcome, About, Contact } from './components';
const routes = (
<Router history={
hashHistory}>
<Route path="/" component={
Home}>
<IndexRoute component={
Welcome} />
<Route path="/about" component={
About} />
<Route path="/contact" component={
Contact} />
</Route>
<Route path="/games" component={
Archive}>
<IndexRoute component={
GamesContainer} />
<Route path="add" component={
AddGameContainer} />
</Route>
</Router>
);
export default routes;
Let's take a look at the definitive configuration:
让我们看一下最终配置:
Url | Component |
---|---|
/ | Home -> Welcome |
/about | Home -> About |
/contact | Home -> Contact |
/games | Archive -> GamesContainer |
/games/add | Archive -> AddGameContainer |
网址 | 零件 |
---|---|
/ | 首页->欢迎光临 |
/关于 | 首页->关于 |
/联系 | 首页->联系方式 |
/游戏 | 存档-> GamesContainer |
/游戏/添加 | 存档-> AddGameContainer |
As we did for the components we create an index file to export the containers. Create it in /client/src/containers
and paste the following code:
正如我们对组件所做的那样,我们创建了一个索引文件来导出容器。 在/client/src/containers
创建它,然后粘贴以下代码:
import AddGameContainer from './AddGameContainer';
import GamesContainer from './GamesContainer';
// We export all the containers at once
export {
AddGameContainer,
GamesContainer
};
as Home
component, it just provides the layour and render the children. Create Archive.jsx
in /client/src/components
and paste the following code:
作为Home
组件,它仅提供布局并渲染子级。 在/client/src/components
创建Archive.jsx
并粘贴以下代码:
import React, {
PureComponent } from 'react';
import {
Link } from 'react-router';
export default class Layout extends PureComponent {
render () {
return (
<div className="view">
<nav className="navbar navbar-inverse">
<div className="container">
<div className="navbar-header">
<button type="button" className="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span className="sr-only">Toggle navigation</span>
<span className="icon-bar" />
<span className="icon-bar" />
<span className="icon-bar" />
</button>
<Link className="navbar-brand" to="/">
<img src="https://cdn.filestackcontent.com/nLnmrZQaRpeythR4ezUo" className="header-logo" />
</Link>
</div>
</div>
</nav>
{
this.props.children}
<footer className="text-center">
<p>© 2016 Samuele Zaza</p>
</footer>
</div>
);
}
}
This is the container for the archive list where we are writing all the functions to manipulate the state. Let's first create the file and then comment it! Create GamesContainer.jsx
in /client/src/containers
and paste the following code:
这是存档列表的容器,我们在其中编写了所有操作状态的功能。 首先创建文件,然后对其进行注释! 在/client/src/containers
创建GamesContainer.jsx
并粘贴以下代码:
import React, {
Component } from 'react';
import {
Modal, GamesListManager } from '../components';
export default class GamesContainer extends Component {
constructor (props) {
super(props);
// The initial state
this.state = {
games: [], selectedGame: {
}, searchBar: '' };
// Bind the functions to this (context)
this.toggleModal = this.toggleModal.bind(this);
this.deleteGame = this.deleteGame.bind(this);
this.setSearchBar = this.setSearchBar.bind(this);
}
// Once the component mounted it fetches the data from the server
componentDidMount () {
this.getGames();
}
toggleModal (index) {
this.setState({
selectedGame: this.state.games[index] });
// Since we included bootstrap we can show our modal through its syntax
$('#game-modal').modal();
}
getGames () {
fetch('http://localhost:8080/games', {
headers: new Headers({
'Content-Type': 'application/json'
})
})
.then(response => response.json()) // The json response to object literal
.then(data => this.setState({
games: data }));
}
deleteGame (id) {
fetch(`http://localhost:8080/games/${
id}`, {
headers: new Headers({
'Content-Type': 'application/json',
}),
method: 'DELETE',
})
.then(response => response.json())
.then(response => {
// The game is also removed from the state thanks to the filter function
this.setState({
games: this.state.games.filter(game => game._id !== id) });
console.log(response.message);
});
}
setSearchBar (event) {
// Super still filters super mario thanks to toLowerCase
this.setState({
searchBar: event.target.value.toLowerCase() });
}
render () {
const {
games, selectedGame, searchBar } = this.state;
return (
<div>
<Modal game={
selectedGame} />
<GamesListManager
games={
games}
searchBar={
searchBar}
setSearchBar={
this.setSearchBar}
toggleModal={
this.toggleModal}
deleteGame={
this.deleteGame}
/>
</div>
);
}
}
componentDidMount()
we call game() which make an HTTP call for the games and set them into the state. Notice the new fetch()
function. 在componentDidMount()
我们调用game(),它对游戏进行HTTP调用并将其设置为状态。 注意新的fetch()
函数。 toggleModal()
is passed as props to the GamesListManager
component to set the current game in the state and toggle the modal. toggleModal()
作为道具传递给GamesListManager
组件,以将当前游戏设置为状态并切换模式。 setSearchBar()
updates the state with the current keyword. toLowerCase()
guarantees our search is not case-sensitive. setSearchBar()
使用当前关键字更新状态。 toLowerCase()
保证我们的搜索不区分大小写。 Modal
and GamesListManager
components. 最后,我们渲染Modal
和GamesListManager
组件。 NB: At the present time thinking about refactoring isn't necessary as our code will substantially change with Redux. In fact we should be just focusing on making things work now!
注意 :目前,无需考虑重构,因为我们的代码将随着Redux的改变而发生很大变化。 实际上,我们现在应该只专注于使事情工作!
This is another stateless component, just create it in /client/src/components
and paste the following code:
这是另一个无状态组件,只需在/client/src/components
创建它,然后粘贴以下代码:
import React, {
PureComponent } from 'react';
export default class Modal extends PureComponent {
render () {
const {
_id, img, name, description, year, picture } = this.props.game;
return(
<div className="modal fade" id="game-modal" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 className="modal-title" id="myModalLabel">{
`${
name} (${
year})`}</h4>
</div>
<div className="modal-body">
<div>
<img src={
picture} className="img-responsive img-big" />
</div>
<hr />
<p>{
description}</p>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-warning" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
);
}
}
There is nothing special here, we simply shows the game information in a fancy modal.
这里没有什么特别的,我们只是以精美的方式显示游戏信息。
Though stateless it is a more meaningful component. Create it in /client/src/components
and paste the following code:
尽管无状态,它是一个更有意义的组件。 在/client/src/components
创建它,并粘贴以下代码:
import React, {
PureComponent } from 'react';
import {
Link } from 'react-router';
import Game from './Game';
export default class GamesListManager extends PureComponent {
render () {
const {
games, searchBar, setSearchBar, toggleModal, deleteGame } = this.props;
return (
<div className="container scrollable">
<div className="row text-left">
<Link to="/games/add" className="btn btn-danger">Add a new Game!</Link>
</div>
<div className="row">
<input
type="search" placeholder="Search by Name" className="form-control search-bar" onKeyUp={
setSearchBar} />
</div>
<div className="row">
{
// A Game is only shown if its name contains the string from the searchBar
games
.filter(game => game.name.toLowerCase().includes(searchBar))
.map((game, i) => {
return (
<Game {
...game}
key={
game._id}
i={
i}
toggleModal={
toggleModal}
deleteGame={
deleteGame}
/>
);
})
}
</div>
<hr />
</div>
);
}
}
Game
component and we do some basic filtering: We make sure the game name contains the keyword from the search bar 在渲染功能中,我们将游戏映射到Game
组件,并进行一些基本过滤:确保游戏名称包含搜索栏中的关键字 The game container is pretty immediate as well, create it into /client/src/components
and paste the following code:
游戏容器也非常即时,将其创建到/client/src/components
并粘贴以下代码:
import React, {
PureComponent } from 'react';
import {
Link } from 'react-router';
export default class Game extends PureComponent {
render () {
const {
_id, i, name, description, picture, toggleModal, deleteGame } = this.props;
return (
<div className="col-md-4">
<div className="thumbnail">
<div className="thumbnail-frame">
<img src={
picture} alt="..." className="img-responsive thumbnail-pic" />
</div>
<div className="caption">
<h5>{
name}</h5>
<p className="description-thumbnail">{
`${
description.substring(0, 150)}...`}</p>
<div className="btn-group" role="group" aria-label="...">
<button className="btn btn-success" role="button" onClick={
() => toggleModal(i)}>View</button>
<button className="btn btn-danger" role="button" onClick={
() => deleteGame(_id)}>Delete</button>
</div>
</div>
</div>
</div>
);
}
}
The buttons triggers the functions we wrote in GamesContainer
: These were passed as props from GamesContainer
to GamesListManager
and finally to Game
.
这些按钮触发了我们在GamesContainer
编写的功能:这些作为道具从GamesContainer
传递给GamesListManager
,最后传递给Game
。
The container is gonna render a form where our users can create games for the archive. Create the AddGameContainer.jsx
in /client/src/containers
and paste the following code:
该容器将呈现一个表单,供我们的用户为存档创建游戏。 在/client/src/containers
创建AddGameContainer.jsx
并粘贴以下代码:
import React, {
Component } from 'react';
import {
hashHistory } from 'react-router';
import {
Form } from '../components';
export default class AddGameContainer extends Component {
constructor (props) {
super(props);
// Initial state
this.state = {
newGame: {
}};
// Bind this (context) to the functions to be passed down to the children components
this.submit = this.submit.bind(this);
this.uploadPicture = this.uploadPicture.bind(this);
this.setGame = this.setGame.bind(this);
}
submit () {
// We create the newGame object to be posted to the server
const newGame = Object.assign({
}, {
picture: $('#picture').attr('src') }, this.state.newGame);
fetch('http://localhost:8080/games', {
headers: new Headers({
'Content-Type': 'application/json'
}),
method: 'POST',
body: JSON.stringify(newGame)
})
.then(response => response.json())
.then(data => {
console.log(data.message);
// We go back to the games list view
hashHistory.push('/games');
});
}
uploadPicture () {
filepicker.pick (
{
mimetype: 'image/*', // Cannot upload other files but images
container: 'modal',
services: ['COMPUTER', 'FACEBOOK', 'INSTAGRAM', 'URL', 'IMGUR', 'PICASA'],
openTo: 'COMPUTER' // First choice to upload files from
},
function (Blob) {
console.log(JSON.stringify(Blob));
$('#picture').attr('src', Blob.url);
},
function (FPError) {
console.log(FPError.toString());
}
);
}
// We make sure to keep the state up-to-date to the latest input values
setGame () {
const newGame = {
name: document.getElementById('name').value,
description: document.getElementById('description').value,
year: document.getElementById('year').value,
picture: $('#picture').attr('src')
};
this.setState({
newGame });
}
render () {
return <Form submit={
this.submit} uploadPicture={
this.uploadPicture} setGame={
this.setGame} />
}
}
setGame()
we create its values whenever the user edit one of the inputs from the form (you will see it later). 在构造函数中,我们在状态中定义一个空白的新游戏。 多亏了setGame()
我们每当用户编辑来自表单的输入之一时,我们就创建它的值(稍后您将看到它)。 submit()
sends the new game to the server through POST request. submit()
通过POST请求将新游戏发送到服务器。 What about the upload()
function?
那么upload()
函数呢?
Inside we run the pick()
function from Filestack which prompts a modal a picture. If you take a look a the documentation for the function, we may have noticed that the first parameter is an option object for customizing our uploader: For example, if you don't want users to upload non-image files, well Filestack allows you to restrict the mimetype! I love the fact I can create in few minutes my uploader with custom options to fit my needs. For the current tutorial, I defined the option objects as following:
在内部,我们运行Filestack中的pick()
函数,该函数会提示模态图片。 如果您看一下该函数的文档 ,我们可能已经注意到第一个参数是用于自定义我们的上传器的选项对象:例如,如果您不希望用户上传非图像文件, 那么Filestack可以让您限制模仿! 我喜欢这样的事实,我可以在几分钟内创建带有自定义选项的上传器,以满足我的需求。 对于当前教程,我定义了选项对象,如下所示:
Finally, there are two functions, one for onSuccess
and one for onError
. Notice the Blob
object parameter on onSuccess
: This is returned by Filestack, it contains a bunch of information among which the image url!
最后,有两个函数,一个用于onSuccess
,一个用于onError
。 注意onSuccess
上的Blob
对象参数:这是Filestack返回的,它包含一堆信息,其中包括图像URL!
Let me show you an example:
让我给你看一个例子:
{
"url":"https://cdn.filestackcontent.com/CLGctDtSZiFbm4AKYTSX",
"filename":"background.jpg",
"mimetype":"image/jpeg",
"size":609038,
"id":1,
"key":"w53urmDSga10ndZsOiE5_background.jpg",
"container":"filestack-website-uploads",
"client":"computer",
"isWriteable":true
}
For more information don't hesitate to take a look at the documentation, the guys made a big effort to write very clear instructions.
欲了解更多信息,请随时阅读文档,这些家伙付出了巨大的努力来编写非常清晰的说明。
Our last component is Form, let's create it in /client/src/components
(used to it yet?!) and paste the following code:
我们的最后一个组件是Form,让我们在/client/src/components
创建它(是否使用过?!)并粘贴以下代码:
import React, {
PureComponent } from 'react';
import {
Link } from 'react-router';
export default class Form extends PureComponent {
render () {
return (
<div className="row scrollable">
<div className="col-md-offset-2 col-md-8">
<div className="text-left">
<Link to="/games" className="btn btn-info">Back</Link>
</div>
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title text-center">
Add a Game!
</h2>
</div>
<div className="panel-body">
<form name="product-form" action="" onSubmit={
() => this.props.submit()} noValidate>
<div className="form-group text-left">
<label htmlFor="caption">Name</label>
<input id="name" type="text" className="form-control" placeholder="Enter the title" onChange={
() => this.props.setGame()} />
</div>
<div className="form-group text-left">
<label htmlFor="description">Description</label>
<textarea id="description" type="text" className="form-control" placeholder="Enter the description" rows="5" onChange={
() => this.props.setGame()} ></textarea>
</div>
<div className="form-group text-left">
<label htmlFor="price">Year</label>
<input id="year" type="number" className="form-control" placeholder="Enter the year" onChange={
() => this.props.setGame()} />
</div>
<div className="form-group text-left">
<label htmlFor="picture">Picture</label>
<div className="text-center dropup">
<button id="button-upload" type="button" className="btn btn-danger" onClick={
() => this.props.uploadPicture()}>
Upload <span className="caret" />
</button>
</div>
</div>
<div className="form-group text-center">
<img id="picture" className="img-responsive img-upload" />
</div>
<button type="submit" className="btn btn-submit btn-block">Submit</button>
</form>
</div>
</div>
</div>
</div>
);
}
}
Pretty straightforward! Whenever a users edit any form input, the onChange function update the state.
非常简单! 每当用户编辑任何表单输入时,onChange函数都会更新状态。
The components were all created but we have to update /client/src/components/index.js
to export them all. Replace its code with the following:
所有组件均已创建,但我们必须更新/client/src/components/index.js
才能全部导出。 将其代码替换为以下代码:
import About from './About';
import Contact from './Contact';
import Form from './Form';
import Game from './Game';
import GamesListManager from './GamesListManager';
import Home from './Home';
import Archive from './Archive';
import Modal from './Modal';
import Welcome from './Welcome';
// We export all the components at once
export {
About,
Contact,
Form,
Game,
GamesListManager,
Home,
Archive,
Modal,
Welcome
};
And now we can run the app! We first start the api server:
现在我们可以运行该应用程序了! 我们首先启动api服务器:
yarn api
And if you haven't, webpack-dev-server:
如果没有,请使用webpack-dev-server:
yarn start
This should work smoothly however we are still not serving the bundle from Node.js. We need to run another command:
这应该可以顺利进行,但是我们仍然不提供Node.js的捆绑软件。 我们需要运行另一个命令:
yarn build
This will create the bundle.js
in the /dist
folder... Now connect to http://localhost:8080 and the client is served from our real server instead.
这将在/dist
文件夹中创建bundle.js
...现在连接到http:// localhost:8080 ,该客户bundle.js
我们的真实服务器提供。
Congratulations for finishing the first part of the tutorial!
恭喜您完成了本教程的第一部分!
In this first part of the tutorial we went through the initial project configuration.
在本教程的第一部分中,我们完成了初始项目配置。
We first built the backend of the app, an API server with Node.js and Express. We also made a preliminary test with postman to doublecheck that everything works as expected. For a real-world app this is not exhaustive, if you a curious about testing, take a look at my previous post on testing Node.js with Mocha and Chai here on Scotch!
我们首先构建了应用程序的后端,即具有Node.js和Express的API服务器。 我们还与邮递员进行了初步测试,以仔细检查一切是否按预期进行。 对于真实世界的应用程序,这还不是很详尽,如果您对测试感到好奇,请查看我以前在苏格兰语上有关使用Mocha和Chai测试Node.js的文章 !
Then we spent some time configuring Webpack to include javascript and css inside the same bundle file. Eventually we wrote React components to see the app in action.
然后,我们花了一些时间将Webpack配置为在同一捆绑文件中包含javascript和CSS。 最终,我们编写了React组件以查看应用程序的运行情况。
In the next tutorial we are going to include Redux and related packages, we will see how easily we can manage the state if we separate it into a container. We will include lots of new packages, not just redux but redux-saga, redux-form... We will work with immutable data structure as well.
在下一个教程中,我们将包括Redux和相关软件包,我们将看到如果将状态分离到容器中,我们可以多么轻松地管理状态。 我们将包括很多新软件包,不仅包括redux,还包括redux-saga,redux-form ...我们还将处理不可变的数据结构。
Stay tuned!
敬请关注!
翻译自: https://scotch.io/tutorials/retrogames-library-with-node-react-and-redux-1-server-api-and-react-frontend