带有Node,React和Redux 1的Retrogames库:服务器API和React前端

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配置。

介绍 ( Introduction )

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.

最后但并非最不重要的一点是,免费帐户足以实现我们为应用程序所需的所有功能。

应用程式

( 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

先决条件 (Prerequisites)

      • In general it would be better to have basic knowledege of the technologies discussed throughout the tutorial (Node.js, React, Mongo.db, Webpack2...).

        通常,最好对整个教程中讨论的技术有基本的了解(Node.js,React,Mongo.db,Webpack2 ...)。
      • ES6 syntax, guys we are at the end of 2016 so let's start using it!

        ES6语法,到2016年底,大家好,让我们开始使用它吧!
      • Yarn, the new package manager is out and I fell in love with it. I especially like the intrinsic reliability guaranteed by yarn.lock, this makes our install works on different systems. For the record, Scotch.io released a very cool tutorial for Yarn.

        Yarn ,新的包装经理不在了 ,我爱上了它。 我特别喜欢yarn.lock保证的固有可靠性,这使我们的安装可在不同的系统上进行。 作为记录,Scotch.io为Yarn发布了一个非常酷的教程
      • 目录 ( Table of Contents )

        资料夹结构 ( Folder Structure )

        --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';

        服务器 ( The Server )

        路线表 (Routes Table)

        So we are going to write our server API! let's define the routes first:

        因此,我们将编写服务器API! 让我们先定义路线:

        Routes Table
        GET /gamesGet all the games.
        POST /gamesCreate a game.
        GET /games/:idGet a single game.
        DELETE /games/:idDelete 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数据库中。

        建立 (Setup)

        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 (绝对), MongooseBody-parserMorganBabel 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文件中指定配置。

        Server.js (Server.js)

        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:

        该代码非常简单:

        • We connect to our Mongo.db database through Mongoose.

          我们通过Mongoose连接到我们的Mongo.db数据库。
        • We set Body-parser and Morgan middlewares for parsing request bodies and output useful logs in the console.

          我们设置了Body-parser和Morgan中间件来解析请求正文,并在控制台中输出有用的日志。
        • We enable CORS to allow HTTP requests from the webpack-dev-server on the same machine, this makes our development easier later on, it won't be necessary once we serve our client from node.

          我们使CORS允许来自同一台机器上的webpack-dev-server的HTTP请求,这使我们以后的开发更加容易,一旦从节点为客户端提供服务,就不再需要。
        • ...And then our routes with a specific callback function.

          ...然后我们的路由带有特定的回调函数。

        Before writing the functions in /app/routes/game.js let's define our Game model!

        /app/routes/game.js编写函数之前,我们先定义游戏模型!

        游戏模型 (Game Model)

        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.

        接下来,让我们创建回调函数来处理请求和响应,然后我们可以测试服务器。

        路由回叫 (Routes Callbacks)

        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!

        我们的服务器已完成,请尝试一下!

        邮递员简单测试 (Postman Simple Testing)

        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

        / GET游戏 (/GET games)

        My database is already populated with a few games, here is the result:

        我的数据库已经填充了一些游戏,结果如下:

        / POST游戏 (/POST games)

        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!

        真的很酷!

        / GET游戏/:id (/GET games/:id)

        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.

        似乎工作顺利。

        /删除游戏/:id (/DELETE games/:id)

        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!

        很棒的服务器已经准备好了,该在客户端上工作了!

        客户端 ( The Client )

        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 react-hot-loader@3.0.0-beta.6 --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;
        
        • I created a common configuration, common, where I define the common properties for both development (using webpack-dev-server) and build (the bundle is served by Node.js).

          我创建了一个通用配置common ,在其中定义了开发 (使用webpack-dev-server)和构建 (该包由Node.js提供)的通用属性。
        • As you may have noticed, the entry point, the output, the loaders and resolve are common among the configurations.

          您可能已经注意到,入口点,输出,装载程序和解析器在配置中是常见的。
        • The switch discriminates between the two configurations, the main difference is that in development we run webpack-dev-server and get better debug informations inside our sourceMap thanks to eval-source-map. Finally, the merged configuration goes through the validate function and gets exported.

          开关区分了两种配置,主要区别在于,在开发过程中,我们运行webpack-dev-server并借助eval-source-map在sourceMap中获得更好的调试信息。 最后,合并的配置将通过验证功能并被导出。

        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"

        资产 (Assets)

        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并粘贴以下代码:

        <!DOCTYPE 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 Archive</title>
            <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>
        • Notice the div with id content, this is where ReactDOM renders our components

          注意具有id内容的div,这是ReactDOM渲染组件的地方
        • In this tutorial we won't use packages like react-bootsrap but just include the CDN url for both the css and javascript sources.

          在本教程中,我们将不使用诸如react-bootsrap之类的软件包,而仅包括css和javascript源的CDN URL。
        • Finally we included Filestack!

          最后,我们包含了Filestack!

        CSS (CSS)

        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网站上找到它们,并包含一些我发现的漂亮字体。 只需从我的项目中复制它们即可。

        文件栈 (Filestack)

        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”),以备稍后使用!

        React (React)

        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:

        我们的路线配置由两条主要路线及其子路线组成:

        1. The Homepage is a route with three children routes in charge to render the components related to home, features and contacts links.

          主页是一条包含三个子路线的路线,负责呈现与房屋,地图项和联系人链接相关的组件。
        2. The Games route handles the children routes to list the games and add a new one.

          游戏路线处理子路线以列出游戏并添加新游戏。

        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 react-router@3.0.0

        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密钥。

        路线 (Routes)

        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路径结构:

        UrlComponent
        / 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,因此我们实际上可以将其扩展为无状态组件。

        Home.jsx (Home.jsx)

        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}是我们渲染三个子组件的地方。
        • We need to change the class to the li element when clicked, this is easy to achieve through the 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”。

        Welcome.jsx (Welcome.jsx)

        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.

        它不需要任何解释,只需欢迎消息和浏览即可! 链接以查看游戏存档。

        About.jsx (About.jsx)

        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!

        甚至更简单,只需简单解释一下我们如何编写该应用程序即可!

        Contact.jsx (Contact.jsx)

        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!

        随时更改文本!

        /components/index.js (/components/index.js)

        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.

        现在让我们在交互式页面上工作。

        更新Routes.js (Update Routes.js)

        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:

        让我们看一下最终配置:

        UrlComponent
        / Home -> Welcome
        /about Home -> About
        /contact Home -> Contact
        /games Archive -> GamesContainer
        /games/add Archive -> AddGameContainer
        网址 零件
        / 首页->欢迎光临
        /关于 首页->关于
        /联系 首页->联系方式
        /游戏 存档-> GamesContainer
        /游戏/添加 存档-> AddGameContainer

        /containers/index.js (/containers/index.js)

        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
        };
        

        Archive.jsx (Archive.jsx)

        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>
            );
          }
        }
        

        GamesContainer.jsx (GamesContainer.jsx)

        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>
            );
          }
        }
        
        • In the constructor we defined an inital state with an empty array of games that will be soon populated. selectedGame is the specific game to show in the bootstrap modal and searchBar is the search keyword to filter the games.

          在构造函数中,我们定义了一个初始状态,其中包含将很快填充的空游戏数组。 selectedGame是要在引导程序模式中显示的特定游戏,searchBar是用于过滤游戏的搜索关键字。
        • In 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()保证我们的搜索不区分大小写。
        • Finally, we render Modal and GamesListManager components.

          最后,我们渲染ModalGamesListManager组件。

        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.

        这里没有什么特别的,我们只是以精美的方式显示游戏信息。

        GamesListManager.jsx (GamesListManager.jsx)

        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>
        
            );
          }
        }
        
        • We could actually move out the search bar and create another component for it, however we won't use it anywhere else so there is no reusability involved.

          我们实际上可以移出搜索栏并为其创建另一个组件,但是我们不会在其他任何地方使用它,因此不会涉及可重用性。
        • In the render function we map the games to a Game component and we do some basic filtering: We make sure the game name contains the keyword from the search bar

          在渲染功能中,我们将游戏映射到Game组件,并进行一些基本过滤:确保游戏名称包含搜索栏中的关键字

        Game.jsx (Game.jsx)

        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

        AddGameContainer.jsx (AddGameContainer.jsx)

        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} />
          }
        }
        
        • In the constructor we define an empty new game in the state. Thanks to 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可以让您限制模仿! 我喜欢这样的事实,我可以在几分钟内创建带有自定义选项的上传器,以满足我的需求。 对于当前教程,我定义了选项对象,如下所示:

        • The mimetype equal to image/* limits the upload to image files.

          等于image / *的模仿类型限制了上传到图像文件。
        • We can choose to show either a modal or dialog uploading interfaces, I personally prefer the modal but you guys could try to customize it the way you like!

          我们可以选择显示模式界面或对话框上传界面,我个人更喜欢该模式,但你们可以尝试按照自己喜欢的方式自定义它!
        • What are the sources to upload from? Not just the user's device but there are plenty of other choices. In our case we define an array of all the allowed sources.

          有哪些上传源? 不仅是用户的设备,还有很多其他选择。 在我们的例子中,我们定义了所有允许的源的数组。
        • Finally, among these choices above, we choose the computer as the default one.

          最后,在上述选择中,我们选择计算机作为默认计算机。

        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.

        欲了解更多信息,请随时阅读文档,这些家伙付出了巨大的努力来编写非常清晰的说明。

        Form.jsx (Form.jsx)

        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函数都会更新状态。

        更新/components/index.js (Update /components/index.js)

        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!

        恭喜您完成了本教程的第一部分!

        结论 ( Conclusions )

        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

      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值