React16-高级教程-四-

61 阅读18分钟

React16 高级教程(四)

原文:Pro React 16

协议:CC BY-NC-SA 4.0

八、SportsStore:认证和部署

在本章中,我向 SportsStore 应用添加了身份验证,以保护管理功能免受未经授权的使用。我还准备将 SportsStore 应用部署到 Docker 容器中,该容器可以在大多数托管平台上使用。

为本章做准备

为了准备本章,我将向提供 RESTful web 服务和 GraphQL 服务的简单服务器添加对认证和授权的支持。目前,任何客户端都可以执行任何操作,这意味着购物者可以改变价格,创建产品,以及执行其他应该仅限于管理员的任务。表 8-1 列出了应该可以公开访问的 HTTP 方法和 URL 的组合;其他一切都将受到保护,包括所有 GraphQL 查询和变异。

表 8-1

可公开访问的 HTTP 方法和 URL 组合

|

HTTP 方法

|

统一资源定位器

|

描述

| | --- | --- | --- | | GET | /api/products | 该组合用于为购物者请求产品页面。 | | GET | /api/categories | 该组合用于请求类别集,并用于为购物者提供导航按钮。 | | POST | /api/orders | 此组合用于提交订单。 | | POST | /login | 该组合将用于提交用户名和密码进行身份验证。 |

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

为了实现认证并提供授权的方法,我在sportsstore文件夹中添加了一个名为authMiddleware.js的文件,并添加了清单 8-1 中所示的代码。

const jwt = require("jsonwebtoken");

const APP_SECRET = "myappsecret", USERNAME = "admin", PASSWORD = "secret";

const anonOps = [{ method: "GET", urls: ["/api/products", "/api/categories"]},
                 { method: "POST", urls: ["/api/orders"]}]

module.exports = function (req, res, next) {
    if (anonOps.find(op => op.method === req.method
            && op.urls.find(url => req.url.startsWith(url)))) {
        next();
    } else if (req.url === "/login" && req.method === "POST") {
        if (req.body.username === USERNAME && req.body.password === PASSWORD) {
            res.json({
                success: true,
                token: jwt.sign({ data: USERNAME, expiresIn: "1h" }, APP_SECRET)
            });
        } else {
            res.json({ success: false });
        }
        res.end();
    } else {
        let token = req.headers["authorization"];
        if (token != null && token.startsWith("Bearer<")) {
            token = token.substring(7, token.length - 1);
            jwt.verify(token, APP_SECRET);
            next();
        } else {
            res.statusCode = 401;
            res.end();
        }
    }
}

Listing 8-1The Contents of the authMiddleware.js File in the sportsstore Folder

清单 8-1 中的代码将检查 HTTP 服务器收到的每个请求,该服务器提供 RESTful web 服务和 GraphQL 服务。如果请求不是针对 HTTP 方法和 URL 的不安全组合之一,则返回 401 未授权响应。/login URL 用于认证,硬连线凭证如表 8-2 所示。

表 8-2

SportsStore 应用使用的凭据

|

名字

|

描述

| | --- | --- | | name | admin | | password | secret |

警告

SportsStore 项目中的所有服务器端代码都可以用于实际项目,除了清单 8-1 ,它包含硬编码的凭证,不适合除基本开发和测试之外的任何事情。

为了将中间件添加到服务器,我将清单 8-2 中所示的语句添加到server.js文件中。

const express = require("express");
const jsonServer = require("json-server");
const chokidar = require('chokidar');
const cors = require("cors");
const fs = require("fs");
const { buildSchema } = require("graphql");
const graphqlHTTP = require("express-graphql");
const queryResolvers  = require("./serverQueriesResolver");
const mutationResolvers = require("./serverMutationsResolver");

const auth = require("./authMiddleware");

const fileName = process.argv[2] || "./data.js"
const port = process.argv[3] || 3500;

let router = undefined;
let graph = undefined;

const app = express();

const createServer = () => {
    delete require.cache[require.resolve(fileName)];
    setTimeout(() => {
        router = jsonServer.router(fileName.endsWith(".js")
                ? require(fileName)() : fileName);
        let schema =  fs.readFileSync("./serverQueriesSchema.graphql", "utf-8")
            + fs.readFileSync("./serverMutationsSchema.graphql", "utf-8");
        let resolvers = { ...queryResolvers, ...mutationResolvers };
        graph = graphqlHTTP({
            schema: buildSchema(schema), rootValue: resolvers,
            graphiql: true, context: { db: router.db }
        })
    }, 100)
}

createServer();

app.use(cors());
app.use(jsonServer.bodyParser)

app.use(auth);

app.use("/api", (req, resp, next) => router(req, resp, next));
app.use("/graphql", (req, resp, next) => graph(req, resp, next));

chokidar.watch(fileName).on("change", () => {
    console.log("Reloading web service data...");
    createServer();
    console.log("Reloading web service data complete.");
});

app.listen(port, () => console.log(`Web service running on port ${port}`));

Listing 8-2Adding Middleware in the server.js File in the sportsstore Folder

打开一个新的命令提示符,导航到sportsstore文件夹,运行清单 8-3 中所示的命令,启动 React 开发工具、RESTful web 服务和 GraphQL 服务。

npm start

Listing 8-3Starting the Development Tool and Web Services

项目编译完成后,将会打开一个新的浏览器窗口,显示 SportsStore 购物功能,如图 8-1 所示。

img/473159_1_En_8_Fig1_HTML.jpg

图 8-1

运行示例应用

为 GraphQL 请求添加身份验证

身份验证中间件的引入破坏了管理特性,这些特性依赖于不再公开访问的 HTTP 请求。如果您导航到http://localhost:3000/admin,您将看到服务器对 GraphQL HTTP 请求做出的 401-未授权响应的效果,如图 8-2 所示。

img/473159_1_En_8_Fig2_HTML.jpg

图 8-2

遇到错误

在接下来的部分中,我将解释 SportsStore 应用将如何对用户进行身份验证并实现所需的功能,以防止出现图中所示的错误,并为经过身份验证的用户恢复管理功能。

了解认证系统

当服务器对用户进行身份验证时,它将返回一个 JSON Web 令牌(JWT ),应用必须将该令牌包含在后续的 HTTP 请求中,以表明身份验证已经成功执行。您可以在 https://tools.ietf.org/html/rfc7519 阅读 JWT 规范,但是对于 SportsStore 项目来说,只要知道应用可以通过向/login URL 发送 POST 请求来验证用户就足够了,在请求体中包含一个 JSON 格式的对象,该对象包含名称和密码属性。在清单 8-1 中定义的验证码中只有一组有效凭证,我在表 8-3 中已经重复了。您不应该在实际项目中对凭证进行硬编码,但这是 SportsStore 应用需要的用户名和密码。

表 8-3

RESTful Web 服务支持的身份验证凭证

|

用户名

|

密码

| | --- | --- | | admin | secret |

如果正确的凭证被发送到/login URL,那么来自服务器的响应将包含一个 JSON 对象,如下所示:

{
  "success": true,
  "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiYWRtaW4iLCJleHBpcmVz
           SW4iOiIxaCIsImlhdCI6MTQ3ODk1NjI1Mn0.lJaDDrSu-bHBtdWrz0312p_DG5tKypGv6cA
           NgOyzlg8"
}

success属性描述认证操作的结果,而token属性包含 JWT,它应该包含在使用Authorization HTTP 头的后续请求中,格式如下:

Authorization: Bearer<eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiYWRtaW4iLC
                     JleHBpcmVzSW4iOiIxaCIsImlhdCI6MTQ3ODk1NjI1Mn0.lJaDDrSu-bHBtd
                     Wrz0312p_DG5tKypGv6cANgOyzlg8>

我配置了服务器返回的 JWT 令牌,使它们在一小时后过期。

如果向服务器发送了错误的凭证,那么响应中返回的 JSON 对象将只包含一个设置为falsesuccess属性,如下所示:

{
  "success": false
}

创建身份验证上下文

SportsStore 应用需要能够确定用户是否已经过身份验证,并跟踪必须包含在 HTTP 请求中的 web 令牌,确保只有在成功通过身份验证后才显示管理功能。

这是应用中多个地方经常需要的信息类型,以确保组件可以轻松协作。对于 SportsStore 应用,我将使用 React 上下文特性,该特性允许以一种简单和轻量级的方式在组件之间轻松共享功能,这在第十四章中有所描述。我创建了src/auth文件夹,并添加了一个名为AuthContext.js的文件,代码如清单 8-4 所示。

import React from "react";

export const AuthContext = React.createContext({
    isAuthenticated: false,
    webToken: null,
    authenticate: (username, password) => {},
    signout: () => {}
})

Listing 8-4The Contents of the AuthContext.js File in the src/auth Folder

React.createContext方法用于创建上下文,它接收的对象用于默认值,这就是为什么authenticatesignout函数是空的。上下文的真正功能由提供者组件提供,我通过在src/auth文件夹中创建一个名为AuthProviderImpl.js的文件并添加清单 8-5 中所示的代码来定义它。

import React, { Component } from "react";
import Axios from "axios";
import { AuthContext } from "./AuthContext";
import { authUrl } from "../data/Urls";

export class AuthProviderImpl extends Component {

    constructor(props) {
        super(props);
        this.state = {
            isAuthenticated: false,
            webToken: null
        }
    }

    authenticate = (credentials) => {
        return Axios.post(authUrl,  credentials).then(response => {
            if (response.data.success === true) {
                this.setState({
                    isAuthenticated: true,
                    webToken:response.data.token
                })
                return true;
            } else {
                throw new Error("Invalid Credentials");
            }
        })
    }

    signout = () => {
        this.setState({ isAuthenticated: false, webToken: null });
    }

    render = () =>
        <AuthContext.Provider value={ {...this.state,
                authenticate: this.authenticate, signout: this.signout}}>
            { this.props.children }
        </AuthContext.Provider>
}

Listing 8-5The Contents of the AuthProviderImpl.js File in the src/auth Folder

该组件在其 render 方法中使用 React 上下文特性来提供AuthContext属性和功能的实现,这是通过特殊的AuthContext.Provider元素的value属性来实现的。其效果是将对状态数据以及authenticatesignout方法的访问权直接共享给任何应用了相应的AuthContext.Consumer元素的后代组件,我很快就会用到这个元素。

authenticate方法的实现使用 Axios 包发送 POST 请求,以验证将从用户处获得的凭证。authenticate 方法的结果是一个Promise,当服务器响应确认凭证时将解析该结果,如果凭证不正确,将拒绝该结果。

为了定义用于执行身份验证的 URL,我添加了清单 8-6 中所示的 URL。

import { DataTypes } from "./Types";

const protocol = "http";
const hostname = "localhost";
const port = 3500;

export const RestUrls = {
    [DataTypes.PRODUCTS]: `${protocol}://${hostname}:${port}/api/products`,
    [DataTypes.CATEGORIES]: `${protocol}://${hostname}:${port}/api/categories`,
    [DataTypes.ORDERS]: `${protocol}://${hostname}:${port}/api/orders`
}

export const GraphQlUrl = `${protocol}://${hostname}:${port}/graphql`;

export const authUrl = `${protocol}://${hostname}:${port}/login`;

Listing 8-6Adding a URL in the Urls.js File in the src/data Folder

为了将上下文应用于 SportsStore 应用,我对文件App.js进行了清单 8-7 中所示的更改。

import React, { Component } from "react";
import { SportsStoreDataStore } from "./data/DataStore";
import { Provider } from "react-redux";
import { BrowserRouter as Router, Route, Switch, Redirect }
    from "react-router-dom";
import { ShopConnector } from "./shop/ShopConnector";
import { Admin } from "./admin/Admin";

import { AuthProviderImpl } from "./auth/AuthProviderImpl";

export default class App extends Component {

    render() {
        return <Provider store={ SportsStoreDataStore }>
            <AuthProviderImpl>

                <Router>
                    <Switch>
                        <Route path="/shop" component={ ShopConnector } />
                        <Route path="/admin" component={ Admin } />
                        <Redirect to="/shop" />
                    </Switch>
                </Router>
            </AuthProviderImpl>

        </Provider>
    }
}

Listing 8-7Adding a Context Provider to the App.js File in the src Folder

为了更容易使用由AuthContext定义的特性,我在src/auth文件夹中添加了一个名为AuthWrapper.js的文件,并定义了清单 8-8 中所示的高阶组件。

import React, { Component } from "react";
import { AuthContext } from "./AuthContext";

export const authWrapper = (WrappedComponent) =>
    class extends Component {
        render = () =>
            <AuthContext.Consumer>
                { context =>
                    <WrappedComponent { ...this.props } { ...context } />
                }
            </AuthContext.Consumer>
    }

Listing 8-8The Contents of the AuthWrapper.js File in the src/auth Folder

上下文功能依赖于 render prop 函数,该函数很难直接集成到组件中。使用authWrapper函数将允许组件接收由AuthContext定义的特性作为属性。(高阶组件和渲染属性功能都在第十四章中描述。)

创建身份验证表单

为了允许用户提供他们的凭证,我在src/auth文件夹中添加了一个名为AuthPrompt.js的文件,并用它来定义清单 8-9 中所示的组件。

import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import { authWrapper } from "./AuthWrapper";
import { ValidatedForm } from "../forms/ValidatedForm";

export const AuthPrompt = withRouter(authWrapper(class extends Component {

    constructor(props) {
        super(props);
        this.state = {
            errorMessage: null
        }
        this.defaultAttrs = { required: true };
        this.formModel = [
            { label: "Username", attrs: { defaultValue: "admin"}},
            { label: "Password", attrs: { type: "password"} },
        ];
    }

    authenticate = (credentials) => {
        this.props.authenticate(credentials)
            .catch(err => this.setState({ errorMessage: err.message}))
            .then(this.props.history.push("/admin"));
    }

    render = () =>
        <div className="container-fluid">
            <div className="row">
                <div className="col bg-dark text-white">
                    <div className="navbar-brand">SPORTS STORE</div>
                </div>
            </div>
            <div className="row">
                <div className="col m-2">
                    { this.state.errorMessage != null &&
                        <h4 className="bg-danger text-center text-white m-1 p-2">
                            { this.state.errorMessage }
                        </h4>
                    }
                    <ValidatedForm formModel={ this.formModel }
                        defaultAttrs={ this.defaultAttrs }
                        submitCallback={ this.authenticate }
                        submitText="Login"
                        cancelCallback={ () => this.props.history.push("/")}
                        cancelText="Cancel"
                    />
                </div>
            </div>
        </div>
}))

Listing 8-9The Contents of the AuthPrompt.js File in the src/auth Folder

该组件从withRouter函数接收路由特性,从authWrapper函数接收认证特性,这两个特性都将通过组件的 props 呈现。我在第六章中定义的ValidatedForm用于向用户显示用户名和密码字段,这两个字段都需要值。当提交表单数据时,authenticate方法会转发详细信息进行身份验证。如果认证成功,那么由 URL 路由系统提供的history对象(在章节 21 和 22 中描述)用于将用户重定向到/admin URL。如果身份验证失败,将显示一条错误消息。

保护身份验证功能

为了防止在用户通过身份验证之前访问管理特性,我对Admin组件进行了清单 8-10 中所示的更改。

import React, { Component } from "react";
import  ApolloClient from "apollo-boost";
import { ApolloProvider} from "react-apollo";
import { GraphQlUrl } from "../data/Urls";
import { OrdersConnector } from "./OrdersConnector"
import { Route, Redirect, Switch } from "react-router-dom";
import { ToggleLink } from "../ToggleLink";
import { ConnectedProducts } from "./ProductsConnector";
import { ProductEditor } from "./ProductEditor";
import { ProductCreator } from "./ProductCreator";

import { AuthPrompt } from "../auth/AuthPrompt";

import { authWrapper } from "../auth/AuthWrapper";

// const graphQlClient = new ApolloClient({

//     uri: GraphQlUrl

// });

export const Admin = authWrapper(class extends Component {

    constructor(props) {

        super(props);

        this.client = new ApolloClient({

            uri: GraphQlUrl,

            request: gqloperation => gqloperation.setContext({

                headers: {

                    Authorization: `Bearer<${this.props.webToken}>`

                },

            })

        })

    }

    render() {
        return <ApolloProvider client={ this.client }>

            <div className="container-fluid">
                <div className="row">
                <div className="col bg-info text-white">
                    <div className="navbar-brand">SPORTS STORE</div>
                </div>
            </div>
            <div className="row">
                <div className="col-3 p-2">
                    <ToggleLink to="/admin/orders">Orders</ToggleLink>
                    <ToggleLink to="/admin/products">Products</ToggleLink>
                    { this.props.isAuthenticated &&

                        <button onClick={ this.props.signout }

                            className=

                                "btn btn-block btn-secondary m-2 fixed-bottom col-3">

                            Log Out

                        </button>

                    }

                </div>
                <div className="col-9 p-2">
                    <Switch>
                        {

                            !this.props.isAuthenticated &&

                                <Route component={ AuthPrompt } />

                        }

                        <Route path="/admin/orders" component={ OrdersConnector } />
                        <Route path="/admin/products/create"
                            component={ ProductCreator} />
                        <Route path="/admin/products/:id"
                            component={ ProductEditor} />
                        <Route path="/admin/products"
                            component={ ConnectedProducts } />
                        <Redirect to="/admin/orders" />
                    </Switch>
                </div>
            </div>
        </div>
        </ApolloProvider>
    }
})

Listing 8-10Guarding Features in the Admin.js File in the src/admin Folder

Admin组件由authWrapper函数包装,因此它可以访问认证特性。在构造函数中创建了ApolloClient对象,这样我就可以添加一个函数来修改每个请求,为每个 GraphQL HTTP 请求添加一个Authorization头。

render方法中有两个新的代码片段。如果用户通过了身份验证,第一个会显示一个注销按钮。第二个片段检查身份验证状态,并生成一个显示AuthPrompt组件的Route组件,而不考虑 URL。(没有path属性的Route组件将始终显示其组件,并可与Switch一起使用,以防止其他Route组件被评估。)

为管理功能添加导航链接

为了更容易使用管理特性,我给CategoryNavigation组件添加了一个Link,如清单 8-11 所示。

import React, { Component } from "react";
import { ToggleLink } from "../ToggleLink";

import { Link } from "react-router-dom";

export class CategoryNavigation extends Component {

    render() {
        return <React.Fragment>
            <ToggleLink to={ `${this.props.baseUrl}/all` } exact={ false }>
                All
            </ToggleLink>
            { this.props.categories && this.props.categories.map(cat =>
                <ToggleLink key={ cat }
                    to={ `${this.props.baseUrl}/${cat.toLowerCase()}`}>
                    { cat }
                </ToggleLink>
            )}
            <Link className="btn btn-block btn-secondary fixed-bottom m-2 col-3"

                to="/admin">

                Administration

            </Link>

        </React.Fragment>
    }
}

Listing 8-11Adding a Link in the CategoryNavigation.js File in the src/shop Folder

要查看认证功能,导航至http://localhost:3000并点击新管理按钮。警卫将确保显示验证表格。在密码栏中输入 secret ,点击登录按钮进行认证,然后显示管理功能,如图 8-3 所示。单击“注销”按钮返回到未验证状态。

img/473159_1_En_8_Fig3_HTML.jpg

图 8-3

认证以使用管理功能

为部署准备应用

在接下来的小节中,我准备了 SportsStore 应用,以便可以对其进行部署。

为管理功能启用延迟加载

部署应用时,各个 JavaScript 文件将合并成一个文件,浏览器可以更高效地下载该文件。大多数用户将是购物者,这意味着他们不太可能需要管理功能。为了防止他们下载不太可能用到的代码,我在将顶层管理组件合并到应用其余部分的import语句上启用了延迟加载,如清单 8-12 所示。

import React, { Component, lazy, Suspense } from "react";

import { SportsStoreDataStore } from "./data/DataStore";
import { Provider } from "react-redux";
import { BrowserRouter as Router, Route, Switch, Redirect }
    from "react-router-dom";
import { ShopConnector } from "./shop/ShopConnector";
//import { Admin } from "./admin/Admin";
import { AuthProviderImpl } from "./auth/AuthProviderImpl";

const Admin = lazy(() => import("./admin/Admin"));

export default class App extends Component {

    render() {
        return <Provider store={ SportsStoreDataStore }>
            <AuthProviderImpl>
                <Router>
                    <Switch>
                        <Route path="/shop" component={ ShopConnector } />
                        <Route path="/admin" render={
                            routeProps =>
                                <Suspense fallback={ <h3>Loading...</h3> }>

                                    <Admin { ...routeProps } />

                                </Suspense>

                            } />
                        <Redirect to="/shop" />
                    </Switch>
                </Router>
            </AuthProviderImpl>
        </Provider>
    }
}

Listing 8-12Using Lazy Loading in the App.js File in the src Folder

Suspense组件用于表示仅在需要时才加载的内容,并与lazy功能结合使用。这些共同确保了Admin组件在需要时才会被加载。延迟加载特性是 React 最近新增的功能,在撰写本文时,它还不支持从文件中延迟加载命名导出。为了适应这个需求,我修改了清单 8-13 中所示的Admin组件的定义。

import React, { Component } from "react";
import  ApolloClient from "apollo-boost";
import { ApolloProvider} from "react-apollo";
import { GraphQlUrl } from "../data/Urls";
import { OrdersConnector } from "./OrdersConnector"
import { Route, Redirect, Switch } from "react-router-dom";
import { ToggleLink } from "../ToggleLink";
import { ConnectedProducts } from "./ProductsConnector";
import { ProductEditor } from "./ProductEditor";
import { ProductCreator } from "./ProductCreator";
import { AuthPrompt } from "../auth/AuthPrompt";
import { authWrapper } from "../auth/AuthWrapper";

export default authWrapper(class extends Component {

    // ...constructor and render method omitted for brevity...

})

Listing 8-13Changing the Export in the Admin.js File in the src/admin Folder

创建数据文件

RESTful 和 GraphQL 服务使用的数据文件使用 JavaScript 在服务器每次启动时生成相同的数据。这在开发过程中很有用,因为它使返回到已知状态变得容易,但是它不适合生产应用。

json-server包在提供 JSON 文件时会创建一个持久数据库,所以我在sportstore文件夹中添加了一个名为productionData.json的文件,内容如清单 8-14 所示。

{
    "products": [
        { "id": 1, "name": "Kayak", "category": "Watersports",
            "description": "A boat for one person", "price": 275 },
        { "id": 2, "name": "Lifejacket", "category": "Watersports",
            "description": "Protective and fashionable", "price": 48.95 },
        { "id": 3, "name": "Soccer Ball", "category": "Soccer",
            "description": "FIFA-approved size and weight", "price": 19.50 },
        { "id": 4, "name": "Corner Flags", "category": "Soccer",
            "description": "Give your playing field a professional touch",
            "price": 34.95 },
        { "id": 5, "name": "Stadium", "category": "Soccer",
            "description": "Flat-packed 35,000-seat stadium", "price": 79500 },
        { "id": 6, "name": "Thinking Cap", "category": "Chess",
            "description": "Improve brain efficiency by 75%", "price": 16 },
        { "id": 7, "name": "Unsteady Chair", "category": "Chess",
            "description": "Secretly give your opponent a disadvantage",
            "price": 29.95 },
        { "id": 8, "name": "Human Chess Board", "category": "Chess",
            "description": "A fun game for the family", "price": 75 },
        { "id": 9, "name": "Bling Bling King", "category": "Chess",
            "description": "Gold-plated, diamond-studded King", "price": 1200 }
    ],
    "categories": ["Watersports", "Soccer", "Chess"],
    "orders": []
}

Listing 8-14The Contents of the productionData.json File in the sportsstore Folder

配置请求 URL

当我部署应用时,我将把 React development HTTP 服务器替换为一个结合了提供静态 HTML 和 JavaScript 文件以及 RESTful 和 GraphQL 服务的服务器。为了准备在一个端口上组合所有的服务,我改变了 SportsStore 使用的 URL 的格式,如清单 8-15 所示。

import { DataTypes } from "./Types";

// const protocol = "http";

// const hostname = "localhost";

// const port = 3500;

export const RestUrls = {
    [DataTypes.PRODUCTS]: `/api/products`,

    [DataTypes.CATEGORIES]: `/api/categories`,

    [DataTypes.ORDERS]: `/api/orders`

}

export const GraphQlUrl = `/graphql`;

export const authUrl = `/login`;

Listing 8-15Changing URLs in the Urls.js File in the src/data Folder

构建应用

为了创建适合生产使用的应用的优化版本,打开一个新的命令提示符,导航到sportsstore文件夹,并运行清单 8-16 中所示的命令。

npm run build

Listing 8-16Building the Application for Deployment

构建过程可能需要一些时间来完成,结果是在build文件夹中得到一组优化的文件。

创建应用服务器

React 开发 HTTP 服务器不适合生产。在清单 8-17 中,我扩展了一直提供 RESTful 和 GraphQL 服务的服务器,这样它也将服务于来自build文件夹的文件。

const express = require("express");
const jsonServer = require("json-server");
const chokidar = require('chokidar');
const cors = require("cors");
const fs = require("fs");
const { buildSchema } = require("graphql");
const graphqlHTTP = require("express-graphql");
const queryResolvers  = require("./serverQueriesResolver");
const mutationResolvers = require("./serverMutationsResolver");
const auth = require("./authMiddleware");

const history = require("connect-history-api-fallback");

const fileName = process.argv[2] || "./data.js"
const port = process.argv[3] || 3500;

let router = undefined;
let graph = undefined;

const app = express();

const createServer = () => {
    delete require.cache[require.resolve(fileName)];
    setTimeout(() => {
        router = jsonServer.router(fileName.endsWith(".js")
                ? require(fileName)() : fileName);
        let schema =  fs.readFileSync("./serverQueriesSchema.graphql", "utf-8")
            + fs.readFileSync("./serverMutationsSchema.graphql", "utf-8");
        let resolvers = { ...queryResolvers, ...mutationResolvers };
        graph = graphqlHTTP({
            schema: buildSchema(schema), rootValue: resolvers,
            graphiql: true, context: { db: router.db }
        })
    }, 100)
}

createServer();

app.use(history());

app.use("/", express.static("./build"));

app.use(cors());
app.use(jsonServer.bodyParser)
app.use(auth);
app.use("/api", (req, resp, next) => router(req, resp, next));
app.use("/graphql", (req, resp, next) => graph(req, resp, next));

chokidar.watch(fileName).on("change", () => {
    console.log("Reloading web service data...");
    createServer();
    console.log("Reloading web service data complete.");
});

app.listen(port, () => console.log(`Web service running on port ${port}`));

Listing 8-17Configuring the Server in the server.js File in the sportsstore Folder

connect-history-api-fallback包用index.html文件的内容响应任何 HTTP 请求。这对于使用 URL 路由的应用很有用,因为这意味着用户可以直接导航到应用使用 HTML5 历史 API 导航到的 URL。

测试生产版本和服务器

为了确保生产构建正在工作,并且服务器已经被正确配置,运行在sportsstore文件夹中的清单 8-18 中显示的命令。

node server.js ./productionData.json 4000

Listing 8-18Testing the Production Build

一旦服务器启动,打开一个新的浏览器窗口并导航到http://localhost:4000;你会看到熟悉的内容如图 8-4 所示。

img/473159_1_En_8_Fig4_HTML.jpg

图 8-4

测试应用

容器化 SportsStore 应用

为了完成本章,我将为 SportsStore 应用创建一个容器,以便将其部署到生产中。在撰写本文时,Docker 是创建容器最流行的方式,它是 Linux 的精简版,功能仅够运行应用。大多数云平台或托管引擎都支持 Docker,其工具运行在最流行的操作系统上。

安装 Docker

第一步是在你的开发机器上下载并安装 Docker 工具,可以从 www.docker.com/products/docker 获得。有适用于 macOS、Windows 和 Linux 的版本,也有一些适用于 Amazon 和 Microsoft 云平台的专门版本。对于这一章,免费的社区版已经足够了。

警告

使用 Docker 的一个缺点是,生产该软件的公司因做出突破性的改变而获得了声誉。这可能意味着后面的示例在以后的版本中可能无法正常工作。如果您有问题,请查看这本书的资源库以获取更新( https://github.com/Apress/pro-react-16 )或通过adam@adam-freeman.com联系我。

准备应用

第一步是为 NPM 创建一个配置文件,该文件将用于下载应用在容器中使用所需的附加包。我在sportsstore文件夹中创建了一个名为deploy-package.json的文件,内容如清单 8-19 所示。

{
    "name": "sportsstore",
    "description": "SportsStore",
    "repository": "https://github.com/Apress/pro-react-16",
    "license": "0BSD",

    "devDependencies": {
      "graphql": "¹⁴.0.2",
      "chokidar": "².0.4",
      "connect-history-api-fallback": "¹.5.0",
      "cors": "².8.5",
      "express": "⁴.16.4",
      "express-graphql": "⁰.7.1",
      "json-server": "⁰.14.2",
      "jsonwebtoken": "⁸.1.1"
    }
}

Listing 8-19The Contents of the deploy-package.json File in the sportsstore Folder

devDependencies部分将运行应用所需的包分类到容器中。浏览器中使用的所有包都包含在由build c命令生成的 JavaScript 文件中。其他字段描述应用,它们的主要用途是防止在创建容器时出现警告。

创建 Docker 容器

为了定义容器,我在sportsstore文件夹中添加了一个名为Dockerfile(没有扩展名)的文件,并添加了清单 8-20 中所示的内容。

FROM node:10.14.1

RUN mkdir -p /usr/src/sportsstore

COPY build /usr/src/sportsstore/build

COPY authMiddleware.js /usr/src/sportsstore/
COPY productionData.json /usr/src/sportsstore/
COPY server.js /usr/src/sportsstore/
COPY deploy-package.json /usr/src/sportsstore/package.json

COPY serverQueriesSchema.graphql /usr/src/sportsstore/
COPY serverQueriesResolver.js /usr/src/sportsstore/
COPY serverMutationsSchema.graphql /usr/src/sportsstore/
COPY serverMutationsResolver.js /usr/src/sportsstore/

WORKDIR /usr/src/sportsstore

RUN echo 'package-lock=false' >> .npmrc

RUN npm install

EXPOSE 80

CMD ["node", "server.js", "./productionData.json", "80"]

Listing 8-20The Contents of the Dockerfile File in the sportsstore Folder

Dockerfile的内容使用已经用 Node.js 配置的基本映像,并复制运行应用所需的文件,包括包含应用的包文件和将用于安装在部署中运行应用所需的 NPM 包的文件。

为了加快容器化过程,我在sportsstore文件夹中创建了一个名为.dockerignore的文件,其内容如清单 8-21 所示。这告诉 Docker 忽略node_modules文件夹,这在容器中是不需要的,并且需要很长的处理时间。

node_modules

Listing 8-21The Contents of the .dockerignore File in the sportsstore Folder

sportsstore文件夹中运行清单 8-22 中所示的命令,创建一个包含 SportsStore 应用及其所需的所有包的映像。

docker build . -t sportsstore  -f  Dockerfile

Listing 8-22Building the Docker Image

图像是容器的模板。当 Docker 处理 Docker 文件中的指令时,将下载并安装 NPM 包,并将配置和代码文件复制到映像中。

运行应用

一旦创建了映像,使用清单 8-23 中所示的命令创建并启动一个新的容器。

docker run -p 80:80 sportsstore

Listing 8-23Starting the Docker Container

您可以通过在浏览器中打开http://localhost来测试应用,这将显示运行在容器中的 web 服务器提供的响应,如图 8-5 所示。

img/473159_1_En_8_Fig5_HTML.jpg

图 8-5

运行容器化 SportsStore 应用

要停止容器,运行清单 8-24 中所示的命令。

docker ps

Listing 8-24Listing the Containers

您将看到一个正在运行的容器列表,如下所示(为简洁起见,我省略了一些字段):

CONTAINER ID        IMAGE               COMMAND             CREATED
ecc84f7245d6        sportsstore         "node server.js"    33 seconds ago

使用容器 ID 列中的值,运行清单 8-25 中所示的命令。

docker stop ecc84f7245d6

Listing 8-25Stopping the Container

该应用可以部署到任何支持 Docker 的平台上。

摘要

本章完成了 SportsStore 应用,展示了如何准备 React 应用以进行部署,以及将 React 应用放入 Docker 之类的容器中是多么容易。这部分书到此结束。在第二部分中,我开始深入研究细节,并向您展示我用来创建 SportsStore 应用的特性是如何深入工作的。

九、了解 React 项目

在本书的第一部分中,我创建了 SportsStore 应用来演示如何将不同的 React 特性与其他包结合起来创建一个真实的应用。在本书的这一部分,我深入探讨了内置 React 特性的细节。在本章中,我将描述 React 项目的结构,并解释为开发人员提供的工具,以及编译、打包代码和内容并将其发送到浏览器的过程。表 9-1 将本章放在上下文中。

表 9-1

将 React 项目放在上下文中

|

问题

|

回答

| | --- | --- | | 它们是什么? | create-react-app包用于创建项目和设置有效的 React 开发所需的工具。 | | 它们为什么有用? | 用create-react-app包创建的项目是为复杂应用的开发而设计的,并且提供了一整套用于开发、测试和部署的工具。 | | 它们是如何使用的? | 使用npx create-react-app包创建一个项目,使用npm start命令启动开发工具。 | | 有什么陷阱或限制吗? | create-react-app包是“开放的”,这意味着它提供了一种使用很少配置选项的特定工作方式。如果你习惯了不同的工作流程,这可能会令人沮丧。 | | 有其他选择吗? | 你不必使用create-react-app来创建项目。本章后面会提到一些可供选择的包。 |

表 9-2 总结了本章内容。

表 9-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 创建新的 React 项目 | 使用create-react-app包并添加可选包 | 1–3 | | 将 HTML 转换为 JavaScript | 使用 JSX 格式混合 HTML 和代码语句 | six | | 包括静态内容 | 将文件添加到src文件夹,并使用import关键字将它们合并到应用中 | 9–10 | | 包括开发工具之外的静态内容 | 将文件添加到公共文件夹,并使用PUBLIC_URL属性定义引用 | 11–13 | | 禁用林挺消息 | 向 JavaScript 文件添加注释 | 15–19 | | 配置 React 开发工具 | 创建一个.env文件并设置配置属性 | Twenty | | 调试 React 应用 | 使用 React Devtools 浏览器扩展或使用浏览器调试器 | 22–26 |

为本章做准备

为了创建本章的示例项目,打开一个新的命令提示符,导航到一个方便的位置,并运行清单 9-1 中所示的命令。

npx create-react-app projecttools

Listing 9-1Creating the Project

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

注意

创建新项目时,您可能会看到有关安全漏洞的警告。React 开发依赖于大量的包,每个包都有自己的依赖关系,不可避免的会发现安全问题。对于本书中的示例,使用指定的包版本以确保获得预期的结果是很重要的。对于您自己的项目,您应该查看警告并更新到解决问题的版本。

运行清单 9-2 中所示的命令,导航到项目文件夹,并将引导包添加到项目中。

cd projecttools
npm install bootstrap@4.1.2

Listing 9-2Adding the Bootstrap CSS Framework

为了在应用中包含引导 CSS 样式表,将清单 9-3 中所示的语句添加到index.js文件中,该文件可以在src文件夹中找到。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import 'bootstrap/dist/css/bootstrap.css';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

Listing 9-3Including Bootstrap in the index.js File in the src Folder

使用命令提示符,运行projecttools文件夹中清单 9-4 所示的命令来启动开发工具。

警告

注意,开发工具是使用npm命令启动的,而不是清单 9-1 中使用的npx命令。

npm start

Listing 9-4Starting the Development Tools

一旦项目的初始准备工作完成,一个新的浏览器窗口将打开并显示 URL http://localhost:3000并显示如图 9-1 所示的占位符内容。

img/473159_1_En_9_Fig1_HTML.jpg

图 9-1

运行示例应用

了解 React 项目结构

当您创建一个新项目时,您将从一组基本的 React 应用文件、一些占位符内容和一套完整的开发工具开始。图 9-2 显示了projecttools文件夹的内容。

img/473159_1_En_9_Fig2_HTML.jpg

图 9-2

新项目的内容

注意

您不必使用create-react-app包来创建 React 项目,但这是最常用的方法,它负责配置支持本章所述特性的构建工具。如果您愿意,您可以创建所有文件并直接配置工具,或者使用其他可用于创建项目的技术之一,在 https://reactjs.org/docs/create-a-new-react-app.html 中有所描述。

表 9-3 描述了项目中的每一个文件,我在接下来的章节中提供了关于最重要文件的更多细节。

表 9-3

项目文件和文件夹

|

名字

|

描述

| | --- | --- | | node_modules | 该文件夹包含应用和开发工具所需的包,如“了解包文件夹”一节中所述。 | | public | 该文件夹用于静态内容,包括用于响应 HTTP 请求的index.html文件,如“理解静态内容”一节所述。 | | src | 该文件夹包含应用代码和内容,如“了解源代码文件夹”一节中所述。 | | .gitignore | 这个文件用于从 Git 版本控制包中排除文件和文件夹。 | | package.json | 该文件夹包含项目的顶层包依赖项集,如“了解包文件夹”一节中所述。 | | package-lock.json | 该文件包含项目的包依赖项的完整列表,如“了解包文件夹”一节中所述。 | | README.md | 该文件包含项目工具的信息,相同的内容可以在 https://github.com/facebook/create-react-app 找到。 |

了解源代码文件夹

src文件夹是项目中最重要的,因为它是放置应用代码和内容文件的地方,也是您定义项目所需的定制特性的地方。create-react-app包向跳转开发添加文件,如表 9-4 所述。

表 9-4

src 文件夹中的文件

|

名字

|

描述

| | --- | --- | | index.js | 这个文件负责配置和启动应用。 | | index.css | 该文件包含应用的全局 CSS 样式。有关使用 CSS 文件的详细信息,请参见“理解静态内容”一节。 | | App.js | 该文件包含顶级 React 组件。第章 10 和第章 11 对部件进行了描述。 | | App.css | 该文件包含新项目的占位符 CSS 样式。有关详细信息,请参见“理解静态内容”一节。 | | App.test.js | 该文件包含顶级组件的单元测试。有关单元测试的详细信息,请参见第十七章。 | | registerServiceWorker.js | 该文件由渐进式 web 应用使用,这些应用可以脱机工作。我没有在本书中描述渐进式应用,但你可以在 https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 中找到细节。 | | logo.svg | 该图像文件包含 React 徽标,并由创建时添加到项目中的占位符组件显示。请参见“理解静态内容”一节。 |

了解包文件夹

JavaScript 应用开发依赖于丰富的包生态系统,从包含将被发送到浏览器的代码的包,到在特定任务的开发过程中在后台使用的小包。React 项目中需要大量的包:例如,本章开始时创建的示例项目需要 900 多个包。

这些包之间有一个复杂的依赖层次结构,手工管理起来太困难了,只能用一个包管理器来处理。React 项目可以使用两个不同的包管理器来创建:NPM,它是节点包管理器,在第一章中与 Node.js 一起安装 Yarn,它是最近的竞争对手,旨在改进包管理。为简单起见,我在本书中通篇使用 NPM。

小费

你应该按照书中的例子使用 NPM,但如果你想在自己的项目中使用它,你可以在 https://yarnpkg.com 找到纱的细节。

当创建一个项目时,包管理器会得到一个 React 开发所需的包的初始列表,对每个包进行检查以获得它所依赖的包的集合。再次执行该过程以获得那些包的依赖性,并且重复该过程,直到构建了完整的包列表。包管理器下载并安装所有的包,并将它们安装到node_modules文件夹中。

使用dependenciesdevDependencies属性在package.json文件中定义初始的一组包。dependencies部分用于列出应用运行所需的包。devDependencies部分用于列出开发所需的包,但这些包不是作为应用的一部分部署的。

您可能会在项目中看到不同的细节,但这里是我的示例项目中的package.json文件的dependencies部分:

...
"dependencies": {
    "bootstrap": "⁴.1.2",
    "react": "¹⁶.7.0",
    "react-dom": "¹⁶.7.0",
    "react-scripts": "2.1.2"
},
...

React 项目的dependencies部分只需要三个包:react包包含主要特性,react-dom包包含 web 应用所需的特性,react-scripts包包含我在本章中描述的开发工具命令。第四个包是 Bootstrap CSS 框架,添加到清单 9-2 的项目中。对于每个包,package.json文件包括可接受版本号的详细信息,使用表 9-5 中描述的格式。

表 9-5

软件包版本编号系统

|

格式

|

描述

| | --- | --- | | 16.7.0 | 直接表示版本号将只接受具有精确匹配版本号的包,例如 16.7.0。 | | * | 使用星号表示接受要安装的任何版本的软件包。 | | >16.7.0 >=16.7.0 | 在版本号前面加上>或> =接受任何大于或等于给定版本的软件包版本。 | | <16.7.0 <=16.7.0 | 在版本号前加上 | | ~16.7.0 | 在版本号前加一个波浪号(字符)接受要安装的版本,即使修补程序级别号(三个版本号中的最后一个)不匹配。例如,指定16.7.0 将接受版本 16.7.1 或 16.7.2(将包含版本 16.7.0 的修补程序),但不接受版本 16.8.0(将是新的次要版本)。 | | ¹⁶.7.0 | 在版本号前加一个插入符号(^字符)将接受版本,即使次要版本号(三个版本号中的第二个)或补丁号不匹配。例如,指定¹⁶.7.0 将允许 16.8.0 和 16.9.0 版本,但不允许 17.0.0 版本。 |

package.json文件的dependencies部分指定的版本号将接受较小的更新和补丁。

了解全球和本地包

软件包管理员可以安装软件包,使它们特定于单个项目(称为本地安装)或者可以从任何地方访问它们(称为全局安装)。很少有软件包需要全局安装,但有一个例外,那就是我在第一章安装的create-react-app软件包,它是本书准备工作的一部分。create-react-app包需要全局安装,因为它用于创建新项目。项目所需的单个包被本地安装到node_modules文件夹中。

当你创建一个 React 项目时,开发所需的所有包都被自动下载并安装到node_modules文件夹中,但是表 9-6 列出了一些你可能会发现在开发过程中有用的 NPM 命令。所有这些命令都应该在项目文件夹中运行,这个文件夹包含了package.json文件。

表 9-6

有用的 NPM 命令

|

命令

|

描述

| | --- | --- | | npx create-react-app <name> | 该命令创建一个新的 React 项目。 | | npm install | 该命令执行在package.json文件中指定的包的本地安装。 | | npm install package@version | 该命令执行包的特定版本的本地安装,并更新package.json文件以将包添加到dependencies部分。 | | npm install --save-dev package@version | 该命令执行包的特定版本的本地安装,并更新package.json文件以将包添加到devDependencies部分,该部分用于将包添加到项目中,这些包是开发所需的,但不是应用的一部分。 | | npm install --global package@version | 此命令将对特定版本的软件包执行全局安装。 | | npm list | 该命令将列出所有本地包及其依赖项。 | | npm run | 该命令将执行在package.json文件中定义的脚本之一,如下所述。 |

表 9-6 中描述的最后一个命令很奇怪,但是包管理器传统上包含了对运行命令的支持,这些命令在package.json文件的scripts部分中定义。在 React 项目中,该特性用于提供对工具的访问,这些工具在开发过程中使用,并为应用的部署做准备。下面是示例项目中package.json文件的scripts部分:

...
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},
...

这些命令总结在表 9-7 中,我将在后面的章节中演示它们的用法。

表 9-7

package.json 文件的脚本部分中的命令

|

名字

|

描述

| | --- | --- | | start | 该命令启动开发工具,如“使用 React 开发工具”一节所述。 | | build | 此命令执行构建过程。 | | test | 该命令运行单元测试,如第十七章所述。 | | eject | 该命令将所有开发工具的配置文件复制到项目文件夹中。这是一种单向操作,只有当开发工具的默认配置不适合某个项目时才应该使用。 |

表 9-7 中的命令通过使用npm run后跟您需要的命令名来执行,这必须在包含package.json文件的文件夹中完成。因此,如果您想在示例项目中运行build命令,您可以导航到projecttools文件夹并键入npm run build。例外是使用npm start执行的start命令。

使用 React 开发工具

添加到项目中的开发工具会自动检测到src文件夹中的更改,编译应用,并将文件打包以备浏览器使用。这些任务可以手动执行,但是自动更新会带来更愉快的开发体验。如果它们还没有运行,通过打开命令提示符,导航到projecttools文件夹,并运行清单 9-5 中所示的命令来启动开发工具。

npm start

Listing 9-5Starting the Development Tools

开发工具使用的关键包是 webpack ,它是许多 JavaScript 开发工具和框架的主干。Webpack 是一个模块捆绑器,这意味着它打包了在浏览器中使用的 JavaScript 模块——尽管这对于一个重要功能来说是一个平淡无奇的描述,并且它是开发 React 应用时您将依赖的关键工具之一。

当您运行清单 9-5 中的命令时,当 webpack 准备运行示例应用所需的包时,您会看到一系列消息。Webpack 从index.js文件开始,加载所有有import语句的模块,以创建一组依赖关系。对index.js依赖的每个模块重复这个过程,webpack 继续在应用中工作,直到它拥有整个应用的一组完整的依赖项,然后这些依赖项被组合成一个文件,称为

绑定过程可能需要一点时间,但是只需要在启动开发工具时执行。一旦完成了初始准备,您将会看到一条类似这样的消息,它告诉您应用已经被编译和绑定了:

...
Compiled successfully!

You can now view projecttools in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://192.168.0.77:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.
...

当初始过程完成时,将为http://localhost:3000打开一个新的浏览器窗口,显示图 9-3 中的占位符内容。

img/473159_1_En_9_Fig3_HTML.jpg

图 9-3

使用开发工具

了解编译和转换过程

Webpack 负责构建过程,其中一个关键步骤是由 Babel 包执行的代码转换。在 React 项目中,Babel 有两个重要的任务:转换 JSX 内容,以及将使用最新 JavaScript 特性的 JavaScript 代码转换成可以由旧浏览器执行的代码。

了解 JSX 转型

正如我在第三章中解释的,JSX 格式是 JavaScript 的超集,允许 HTML 与常规代码语句混合使用。JSX 并不完全支持标准 HTML,最明显的区别是纯 HTML 中的属性如class在 JSX 文件中被表示为className。出现这些奇怪现象的原因是,在构建过程中,JSX 文件的内容被 Babel 转换成对 React API 的调用,因此每个 HTML 元素都被转换成对React.createElement方法的调用。在清单 9-6 中,我用一个组件替换了App.js文件中的占位符内容,该组件的render方法返回一些简单的 HTML 元素。

import React, { Component } from "react";

export default class extends Component {

    render = () =>
        <h4 className="bg-primary text-white text-center p-3">
            This is an HTML element
        </h4>
}

Listing 9-6Replacing the Placeholder Content in the App.js File in the src Folder

在转换过程中,h4元素被替换为对React.createElement方法的调用,产生的结果完全是 JavaScript,不需要浏览器对 JSX 有特别的理解。作为一个简单的演示,清单 9-7 直接使用React.createElement方法来实现相同的结果。

import React, { Component } from "react";

export default class extends Component {

    render = () => React.createElement("h4",
                    { className: "bg-primary text-white text-center p-3" },
                    "This is an HTML element")
}

Listing 9-7Using the React API Directly in the App.js File in the src Folder

清单 9-6 和清单 9-7 产生相同的结果,当 Babel 处理清单 9-6 中App.js文件的内容时,它产生清单 9-7 中的代码。当 React 在浏览器中执行 JavaScript 代码时,它使用 DOM API 创建 HTML 元素,如第三章所示。这似乎是一种循环的方法,但是 JSX 转换只在构建过程中执行,目的是使编写 React 特性更容易。

理解 JavaScript 语言转换

经过多年的停滞之后,JavaScript 语言已经重新焕发了活力,并通过简化开发和提供其他编程语言中常见的功能(如第四章中描述的那些功能)实现了现代化。并非所有浏览器都支持所有最新的语言功能,尤其是较旧的浏览器或在企业环境中使用的浏览器,在这些环境中,更新通常很慢(如果有的话)。Babel 通过将现代功能翻译成代码来解决这个问题,这些代码使用了更广泛的浏览器支持的功能,包括 JavaScript 文艺复兴之前的浏览器。

在清单 9-8 中,我返回了App.js文件以使用 HTML 元素,并使用最新的 JavaScript 特性来设置h4元素的内容。

import React, { Component } from "react";

let name = "Adam";

const city = "London";

export default class extends Component {

    message = () => `Hello ${name} from ${city}`;

    render = () =>
        <h4 className="bg-primary text-white text-center p-3">
            { this.message() }
        </h4>
}

Listing 9-8Using Modern JavaScript Features in the App.js File in the src Folder

这个组件依赖于几个最新的 JavaScript 特性:定义类的关键字classextends,定义变量和常量的关键字letconst,以及message方法中的 lambda 函数和模板字符串。当您保存更改时,React 开发工具将自动编译并捆绑 JavaScript 代码,并将其发送到浏览器,产生如图 9-4 所示的内容。

img/473159_1_En_9_Fig4_HTML.jpg

图 9-4

使用现代语言功能

要查看 Babel 是如何处理现代 JavaScript 特性的,打开 F12 开发者工具,选择 Sources 选项卡,在窗口左侧的树中找到main.chunk.js项,如图 9-5 所示。对于编写本文时的 Chrome 版本,该文件位于树的localhost:3000 > static/js部分下。

img/473159_1_En_9_Fig5_HTML.jpg

图 9-5

定位编译后的源代码

小费

谷歌 Chrome 开发者工具经常变化,你可能不得不四处寻找 Babel 产生的代码。使用 Ctrl+F 并搜索伦敦是找到你要找的代码的好方法。另一种方法是将清单 9-8 中的代码粘贴到解释器的 https://babeljs.io/repl 处,这将产生类似的结果。

如果你向下滚动——或者搜索 London ,如前所述——那么你会看到巴别塔生成的代码。旧浏览器不支持的所有功能都被替换为向后兼容的代码,如下所示:

...
var name = "Adam";
var city = "London";

var App = function (_Component) {
    _inherits(App, _Component);

    function App() {
        var _ref;

        var _temp, _this, _ret;

        _classCallCheck(this, App);

        for (var _len = arguments.length, args = Array(_len), _key = 0;
                _key < _len; _key++) {
            args[_key] = arguments[_key];
        }

        return _ret = (_temp = (_this = _possibleConstructorReturn(this,
            (_ref = App.__proto__ || Object.getPrototypeOf(App)).call.apply(_ref,
                [this].concat(args))), _this), _this.message = function () {
                   return "Hello " + name + " from " + city;
                }, _temp), _possibleConstructorReturn(_this, _ret);
    }

    _createClass(App, [{
        key: "render",
        value: function render() {
            return __WEBPACK_IMPORTED_MODULE_0_react___default.a.createElement(
                "div",
                { className: "h1 bg-primary text-white text-center p-3", __source: {
                        fileName: _jsxFileName,
                        lineNumber: 12
                    },
                    __self: this
                },
                this.message()
            );
        }
    }]);
    return App;
}
...

您不需要详细了解这些代码是如何工作的,尤其是因为其中一些代码非常复杂,难以阅读。重要的是如何处理在App.js文件中使用的特性,例如letconst关键字,它们被传统的var关键字所取代。

...
var name = "Adam";
var city = "London";
...

您还可以看到模板字符串已被替换为字符串串联,如下所示:

...
return "Hello " + name + " from " + city;
...

一些特性,比如类,是使用 Babel 添加到发送给浏览器的包中的函数来处理的。JSX 的 HTML 片段被翻译成对React.createElement方法的调用。

现代特性的翻译是复杂的,但是 JavaScript 语言最近增加的内容很大程度上是语法糖,旨在使开发人员更喜欢编码。翻译这些功能剥夺了这些租赁功能的代码,需要一些扭曲来创建一个老浏览器可以执行的等效效果。

理解巴别塔的极限

Babel 是一个优秀的工具,但是它只处理 JavaScript 语言特性。Babel 不能在不支持最新 JavaScript APIs 的浏览器上增加对这些 API 的支持。您仍然可以使用这些 API——正如我在第一部分中使用本地存储 API 时所演示的那样——但是这样做限制了可以运行应用的浏览器的范围。

了解开发 HTTP 服务器

为了简化开发过程,该项目包含了webpack-dev-server包,这是一个与 webpack 集成的 HTTP 服务器。服务器被配置为在初始绑定过程完成后立即开始侦听端口 3000 上的 HTTP 请求。当接收到 HTTP 请求时,开发 HTTP 服务器返回public/index.html文件的内容。在处理index.html文件时,开发服务器做了一些重要的添加,您可以通过在浏览器窗口中右键单击并从弹出菜单中选择 View Page Source 来查看。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,
        initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="/manifest.json">
    <link rel="shortcut icon" href="/favicon.ico">
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
    <script src="/static/js/0.chunk.js"></script>
    <script src="/static/js/main.chunk.js"></script>
    <script src="/main.a5f0dcc648ccc4241725.hot-update.js"></script>
  </body>
</html>

开发服务器添加了script元素,告诉浏览器加载包含 React 框架、应用代码、静态内容(如 CSS)和一些支持开发工具的附加功能的文件,并在检测到更改时自动重新加载浏览器。

理解静态内容

有两种方法可以在 React 应用中包含静态内容,比如图像或 CSS 样式表。在大多数情况下,最好的方法是将您需要的文件添加到src文件夹中,然后在代码文件中使用import语句声明对它们的依赖。

为了演示如何处理src文件夹中的静态内容,我用清单 9-9 中所示的 CSS 样式替换了App.css文件的内容,该文件是在创建时添加到项目中的。

img {
  background-color: lightcyan;
  width: 50%;
}

Listing 9-9Replacing the Styles in the App.css File in the src Folder

我定义的样式选择img元素并设置背景颜色和宽度。在清单 9-10 中,我向App组件的src文件夹中的两个静态文件添加了依赖项,包括我在前面的清单中更新的 CSS 文件和创建项目时添加到项目中的占位符图像。

小费

index.css文件由index.js文件导入,后者是负责启动 React 应用的 JavaScript 文件。您可以在 CSS 文件中定义全局样式,它们将包含在发送到浏览器的内容中。

import React, { Component } from "react";

import "./App.css";

import reactLogo from "./logo.svg";

let name = "Adam";
const city = "London";

export default class extends Component {

    message = () => `Hello ${name} from ${city}`;

    render = () =>
        <div className="text-center">
            <h4 className="bg-primary text-white text-center p-3">
                { this.message() }
            </h4>
            <img src={ reactLogo } alt="reactLogo" />
        </div>
}

Listing 9-10Declaring a Static Dependency in the App.js File in the src Folder

要导入在使用时不需要引用的内容,如 CSS 样式表,import关键字后接文件名,文件名必须包括文件扩展名,如下所示:

...
import "./App.css";
...

要导入将在 HTML 元素中引用的内容,如图像,则必须使用为导入的特性指定名称的import语句的形式,如以下语句:

...
import reactLogo from "./logo.svg";
...

这个statement导入了logo.svg文件,并给它命名为reactLogo,然后我可以在一个img元素的表达式中使用它,就像这样:

...
<img src={ reactLogo } alt="reactLogo" />
...

当您使用import关键字声明对静态内容的依赖时,如何处理内容的决定就留给了开发工具。对于小于 10Kb 的文件,内容将包含在bundle.js文件中,以及将内容添加到 HTML 文档所需的 JavaScript 代码。这就是清单 9-10 中导入的App.css文件的情况:CSS 文件的内容将包含在bundle.js文件中,以及创建style元素所需的代码。

对于较大的文件,以及任何大小的 SVG 文件,在单独的 HTTP 请求中请求导入的文件。由import语句指定的相对路径被自动替换为定位文件的 URL,并且文件名被更改为包含校验和,这确保了过时的数据不会被浏览器缓存。

您可以看到清单 9-10 中使用的静态内容的效果,方法是保存对App.js文件的更改,等待浏览器重新加载,然后使用 F12 开发人员的工具检查 Elements 选项卡,这将显示以下 HTML(尽管为了简洁起见,我省略了大量的 Bootstrap CSS 样式):

<html lang="en">
  <head>
    <meta charset="utf-8">
    <link rel="shortcut icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width,
         initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="/manifest.json">
    <title>React App</title>
    <style type="text/css">
      img { background-color: lightcyan; width: 50% }
     </style>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root">
        <div class="text-center">
          <h4 class="bg-primary text-white text-center p-3">
            Hello Adam from London
           </h4>
           <img src="/static/media/logo.5d5d9eef.svg" alt="reactLogo">
         </div>
    </div>
    <script src="/static/js/bundle.js"></script>
    <script src="/static/js/1.chunk.js"></script>
    <script src="/static/js/main.chunk.js"></script>
    <script src="/main.00ec8a0c115561c18137.hot-update.js"></script>
  </body>
</html>

您可以看到 CSS 样式已经从 JavaScript 包中解包,并通过一个style元素添加到 HTML 文档中,而图像文件是通过 URL /static/media/logo.5d5d9eef.svg访问的。在构建过程中,大型文件会自动复制到应用代码中包含的 URL 所指定的位置,这意味着您不必担心它们是否可用。清单 9-10 中的变化产生如图 9-6 所示的结果。

img/473159_1_En_9_Fig6_HTML.jpg

图 9-6

src 文件夹中的静态内容

将公共文件夹用于静态内容

src文件夹用于静态内容有几个好处,但是您可能会发现它并不总是适合每个项目,尤其是在静态内容在构建时不可用并且不能被 React 开发工具处理的情况下。在这些情况下,您可以将静态内容放在public文件夹中,尽管这意味着您有责任确保应用拥有它所需要的文件。为了演示public文件夹的使用,我给它添加了一个名为static.css的文件,内容如清单 9-11 所示。

img {
    border: 8px solid black;
}

Listing 9-11The Contents of the static.css File in the public Folder

打开一个新的命令提示符,导航到projecttools文件夹,运行清单 9-12 所示的命令,将logo.svg文件从src文件夹复制到public文件夹。

cp src/logo.svg public/

Listing 9-12Copying an Image File into the Public Folder

在清单 9-13 中,我为public文件夹中的图像和样式表向由App组件呈现的内容添加了 HTML 元素。

import React, { Component } from "react";
import "./App.css";
import reactLogo from "./logo.svg";

let name = "Adam";
const city = "London";

export default class extends Component {

    message = () => `Hello ${name} from ${city}`;

    render = () =>
        <div className="text-center">
            <h4 className="bg-primary text-white text-center p-3">
                { this.message() }
            </h4>
            <img src={ reactLogo } alt="reactLogo" />
            <link rel="stylesheet"
                    href={ process.env.PUBLIC_URL + "/static.css"} />
            <img src={ process.env.PUBLIC_URL + "/logo.svg" } alt="reactLogo" />
        </div>
}

Listing 9-13Accessing Static Files in the App.js File in the src Folder

为了指定静态文件的 URL,process.env.PUBLIC_URL属性与表达式中的文件名结合在一起。注意,我已经为样式表添加了一个link元素,因为我不能依靠bundle.js文件中的代码来自动创建样式。添加元素到组件的结果如图 9-7 所示。

img/473159_1_En_9_Fig7_HTML.jpg

图 9-7

公共文件夹中的静态内容

了解错误显示

自动重新加载特性提供的即时性的一个影响是,在开发过程中,您会倾向于停止观察控制台输出,因为您的注意力会自然地被吸引到浏览器窗口。风险在于,当代码包含错误时,浏览器显示的内容保持不变,因为编译过程无法生成新的模块通过 HMR 功能发送给浏览器。为了解决这个问题,webpack 开发的包中包含了一个集成的错误显示,可以在浏览器窗口中显示问题的详细信息。为了演示处理错误的方式,我将清单 9-14 中所示的语句添加到了App.js文件中。

import React, { Component } from "react";
import "./App.css";
import reactLogo from "./logo.svg";

let name = "Adam";
const city = "London";

not a valid statement

export default class extends Component {

    message = () => `Hello ${name} from ${city}`;

    render = () =>
        <div className="text-center">
            <h4 className="bg-primary text-white text-center p-3">
                { this.message() }
            </h4>
            <img src={ reactLogo } alt="reactLogo" />
            <link rel="stylesheet"
                    href={ process.env.PUBLIC_URL + "/static.css"} />
            <img src={ process.env.PUBLIC_URL + "/logo.svg" } alt="reactLogo" />
        </div>
}

Listing 9-14Creating an Error in the App.js File in the src Folder

加法不是有效的 JavaScript 语句。保存对文件的更改后,构建过程会尝试编译代码,并在命令提示符下生成以下错误信息:

...
Failed to compile.

./src/App.js
  Line 8:  Parsing error: Unexpected token, expected ";"

   6 | const city = "London";
   7 |
>  8 | not a valid statement
     |     ^
   9 |
  10 | export default class extends Component {
  11 |
...

浏览器窗口中会显示相同的错误消息,因此即使您没有注意命令行消息,您也会意识到存在问题。如果你点击栈跟踪,那么浏览器会向开发服务器发送一个 HTTP 请求,开发服务器会试图找出你使用的是哪个代码编辑器,并突出显示问题,如图 9-8 所示。

img/473159_1_En_9_Fig8_HTML.jpg

图 9-8

在源代码文件出现错误后

小费

您可能需要配置 React 开发工具来指定您的编辑器,如“配置开发工具”一节中所述,并且不是所有的编辑器都受支持。图 9-8 显示了 Visual Studio 代码,这是提供支持的编辑器之一。

了解棉绒

React 开发工具包括一个 linter,它负责检查项目中的代码和内容是否符合一组规则。当您使用create-react-app包创建一个项目时,ESLint 包被用作 linter,带有一组规则,旨在帮助程序员避免常见错误。作为演示,我在App.js文件中添加了一个变量,如清单 9-15 所示。(此更改还会删除上一节中导致编译器错误的语句)。

import React, { Component } from "react";
import "./App.css";
import reactLogo from "./logo.svg";

let name = "Adam";
const city = "London";

let error = "not a valid statement";

export default class extends Component {

    message = () => `Hello ${name} from ${city}`;

    render = () =>
        <div className="text-center">
            <h4 className="bg-primary text-white text-center p-3">
                { this.message() }
            </h4>
            <img src={ reactLogo } alt="reactLogo" />
            <link rel="stylesheet"
                    href={ process.env.PUBLIC_URL + "/static.css"} />
            <img src={ process.env.PUBLIC_URL + "/logo.svg" } alt="reactLogo" />
        </div>
}

Listing 9-15Adding a Variable in the App.js File in the src Folder

保存文件时,您会在命令行和浏览器的 JavaScript 控制台中看到以下警告:

...
Compiled with warnings.

./src/App.js
  Line 8:  'error' is assigned a value but never used  no-unused-vars
...

linter 不能被禁用或重新配置,这意味着您将收到一组固定规则的林挺警告,包括清单 9-15 破坏的no-unused-vars规则。您可以在 https://github.com/facebook/create-react-app/tree/master/packages/eslint-config-react-app .看到 React 项目中应用的一组规则

当您收到警告时,搜索规则名称将为您提供问题的描述。在这种情况下,搜索 no-unused-vars 会将您带到 https://eslint.org/docs/rules/no-unused-vars ,这解释了变量不能被定义和不被使用。

禁用单个语句和文件的林挺

虽然 linter 不能被禁用,但是您可以向文件添加注释来防止警告。在清单 9-16 中,我通过添加注释禁用了单个语句的no-unused-var规则。

import React, { Component } from "react";
import "./App.css";
import reactLogo from "./logo.svg";

let name = "Adam";
const city = "London";

// eslint-disable-next-line no-unused-vars

let error = "not a valid statement";

export default class extends Component {

    message = () => `Hello ${name} from ${city}`;

    render = () =>
        <div className="text-center">
            <h4 className="bg-primary text-white text-center p-3">
                { this.message() }
            </h4>
            <img src={ reactLogo } alt="reactLogo" />
            <link rel="stylesheet"
                    href={ process.env.PUBLIC_URL + "/static.css"} />
            <img src={ process.env.PUBLIC_URL + "/logo.svg" } alt="reactLogo" />
        </div>
}

Listing 9-16Disabling a Single Linting Rule in the App.js File in the src Folder

如果您想禁用下一条语句的所有规则,那么您可以省略规则名称,如清单 9-17 所示。

...

// eslint-disable-next-line

let error = "not a valid statement";
...

Listing 9-17Disabling All Linting Rules in the App.js File in the src Folder

如果您想要禁用整个文件的规则,那么您可以在文件的顶部添加一个注释,如清单 9-18 所示。

/* eslint-disable no-unused-vars */

import React, { Component } from "react";
import "./App.css";
import reactLogo from "./logo.svg";

let name = "Adam";
const city = "London";

let error = "not a valid statement";

export default class extends Component {

    message = () => `Hello ${name} from ${city}`;

    render = () =>
        <div className="text-center">
            <h4 className="bg-primary text-white text-center p-3">
                { this.message() }
            </h4>
            <img src={ reactLogo } alt="reactLogo" />
            <link rel="stylesheet"
                    href={ process.env.PUBLIC_URL + "/static.css"} />
            <img src={ process.env.PUBLIC_URL + "/logo.svg" } alt="reactLogo" />
        </div>
}

Listing 9-18Disabling a Single Rule for a File in the App.js File in the src Folder

如果您想对单个文件的所有规则禁用林挺,那么您可以在注释中省略规则名称,如清单 9-19 所示。

...

/* eslint-disable */

import React, { Component } from 'react';
import "./App.css";
import reactLogo from "./logo.svg";

let name = "Adam";
const city = "London";
...

Listing 9-19Disabling All Rules for a File in the App.js File in the src Folder

linter 将忽略 App.js 文件的内容,但仍然会检查项目中其他文件的内容。

使用类型脚本或流

林挺并不是检测常见错误的唯一方法,一种很好的补充技术是静态类型检查,其中您将变量和函数结果的数据类型的细节添加到您的代码中,以创建由编译器强制执行的策略。例如,您可以指定函数总是返回一个字符串,或者它的第一个参数只能是一个数字。编译应用时,会检查使用该函数的代码,以确保它只将数值作为参数传递,并且只将结果作为字符串处理。

向 React 项目添加静态类型检查有两种常见的方法。首先是使用 TypeScript,它是微软生产的 JavaScript 的超集。TypeScript 使得使用 JavaScript 更像 C#或 Java,并且包括对静态类型检查的支持。如果要使用 TypeScript,那么在创建项目时使用- scripts-version 参数,如下所示:

...
npx create-react-app projecttools --scripts-version=react-scripts-ts
...

react-scripts-ts值产生一个用 TypeScript 工具和特性建立的项目。你可以在 https://www.typescriptlang.org 了解更多关于 TypeScript 的知识。

一个替代方案是一个名为 Flow 的包,它只关注类型检查,不具备 TypeScript 的更广泛的特性。您可以在 https://flow.org 了解更多心流知识

配置开发工具

React 开发工具提供了少量的配置选项,尽管在大多数项目中并不需要。可用选项如表 9-8 所述。

表 9-8

React 开发工具配置选项

|

名字

|

描述

| | --- | --- | | 浏览器 | 此选项用于指定开发工具完成初始生成过程时打开的浏览器。您可以通过指定路径来指定浏览器,或者使用none来禁用此功能。 | | 宿主 | 该选项用于指定开发 HTTP 服务器绑定的主机名,默认为localhost。 | | 港口 | 该选项用于指定开发 HTTP 服务器使用的端口,默认为3000。 | | 安全超文本传输协议 | 当设置为true时,该选项为开发 HTTP 服务器启用 SSL,生成自签名证书。默认为false。 | | PUBLIC_URL | 该选项用于更改用于从public文件夹请求内容的 URL,如理解静态内容一节所述。 | | 海峡群岛 | 当设置为true时,该选项将所有警告视为构建过程中的错误。默认值为false。 | | REACT _ 编辑器 | 如了解错误显示部分所述,该选项用于指定当您点击浏览器中的栈跟踪时打开代码文件的特性的编辑器。 | | CHOKIDAR_USEPOLLING | 当开发工具无法检测到对src文件夹的更改时,该选项应该设置为true,如果您在虚拟机或容器中工作,这种情况可能会发生。 | | 生成 _ 源地图 | 将此选项设置为false会禁用源映射的生成,浏览器在调试过程中使用源映射将捆绑的 JavaScript 代码与项目中的源文件相关联。默认为true。 | | 节点路径 | 此设置用于指定将搜索 Node.js 模块的位置。 |

这些选项要么通过设置环境变量来指定,要么通过创建一个.env文件来指定,这是我认为最可靠的方法。为了演示配置过程,我在projecttools文件夹中添加了一个名为.env的文件,并添加了清单 9-20 中所示的配置语句。

PORT=3500
HTTPS=true

Listing 9-20The Contents of the .env File in the projecttools Folder

我使用了PORT选项来指定用于接收请求的端口 3500,并使用了HTTPS选项来启用开发服务器中的 SSL。要查看更改的效果,停止开发工具并运行清单 9-21 中所示的命令来再次启动它们。

npm start

Listing 9-21Starting the React Development Tools

当初始构建过程完成时,打开的浏览器窗口将导航至https://localhost:3500。大多数浏览器会显示一个关于自签名证书的警告,然后在你点击高级链接(或其等效物)并告诉浏览器继续时显示 web 应用,如图 9-9 所示。

img/473159_1_En_9_Fig9_HTML.jpg

图 9-9

配置开发工具

调试 React 应用

并不是所有的问题都能被编译器或 linter 检测到,能够完美编译的代码可能会以意想不到的方式运行。有两种方法可以理解您的应用的行为,如下面几节所述。为了帮助演示调试特性,我在src文件夹中添加了一个名为Display.js的文件,并用它来定义清单 9-22 中所示的组件。

import React, {Component } from "react";

export class Display extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 1
        }
    }

    incrementCounter = () => {
        this.setState({ counter: this.state.counter + 1 });
    }

    render() {
        return (
            <div>
                <h2 className="bg-primary text-white text-center p-2">
                    <div>Props Value: { this.props.value }</div>
                    <div>Local Value: { this.state.counter } </div>
                </h2>
                <div className="text-center">
                    <button className="btn btn-primary m-2"
                            onClick={ this.props.callback }>
                        Parent
                    </button>
                    <button  className="btn btn-primary m-2"
                            onClick={ this.incrementCounter }>
                        Local
                    </button>
                </div>
            </div>
        )
    }
}

Listing 9-22The Contents of the Display.js File in the src Folder

该组件显示自己的状态属性和从其父组件接收的属性值。它显示两个button元素,其中一个更改 state 属性,另一个调用作为 prop 提供的回调。在清单 9-23 中,我替换了App组件的现有内容,为调试部分做准备。

import React, { Component } from "react";
import { Display } from "./Display";

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            city: "London"
        }
    }

    changeCity = () => {
        this.setState({ city: this.state.city === "London" ? "New York" : "London"})
    }

    render() {
        return (
            <Display value={ this.state.city } callback={ this.changeCity } />
        );
    }
}

Listing 9-23Replacing the Contents of the App.js File in the src Folder

当您保存对 JavaScript 文件的更改时,应用将被编译,您将看到如图 9-10 所示的内容。

img/473159_1_En_9_Fig10_HTML.jpg

图 9-10

向示例应用添加功能

注意

您可能会发现,当.env文件中的HTTPS选项设置为 true 时,浏览器不会自动更新。您可以手动重新加载浏览器以查看更改,或者禁用此选项并重新启动开发工具。

探索应用状态

React Devtools 浏览器扩展是探索 React 应用状态的优秀工具。谷歌 Chrome 和 Mozilla Firefox 都有可用的版本,项目的细节——包括对其他平台的支持和独立版本的细节——可以在 https://github.com/facebook/react-devtools 找到。安装完扩展后,您将在浏览器的“开发人员工具”窗口中看到一个附加选项卡,可通过按 F12 按钮访问该选项卡(这也是这些工具也称为 F12 工具的原因)。

F12 工具窗口中的 React 选项卡允许您浏览和更改应用的结构和状态。您可以看到提供应用功能的一组组件,以及它们的状态数据和属性。

对于示例应用,如果您打开 React 选项卡并在左窗格中展开应用结构,您将在左窗格中看到AppDisplay组件,它们与应用呈现的 HTML 元素的视图一起显示。当您在左侧页面中选择一个组件时,其属性和状态数据会显示在右侧窗格中,如图 9-11 所示。

img/473159_1_En_9_Fig11_HTML.jpg

图 9-11

使用 React Devtools 探索组件

如果您单击浏览器窗口中的按钮,您将看到 React Devtools 显示的值发生变化,反映了应用的实时状态。您还可以单击一个状态数据值,并通过 React Devtools 更改其值,这允许直接操作应用的状态。

小费

Redux 数据存储包也有调试工具,我在第十九章描述了它,它通常用于管理复杂项目的数据。

使用浏览器调试器

现代浏览器包括复杂的调试器,可用于控制应用的执行并检查其状态。React 开发工具包括对创建源映射的支持,这允许浏览器将它正在执行的缩小和捆绑的代码与高效调试所需的开发人员友好的源代码相关联。

有些浏览器允许您使用这些源代码映射来浏览应用的源代码,并创建断点,当到达断点时,断点将暂停应用的执行,并将控制权交给调试器。当我写这篇文章时,创建断点的能力是一个脆弱的功能,在 Chrome 上不起作用,在其他浏览器上也有混合的可靠性。因此,将应用控制权传递给调试器的最可靠方式是使用 JavaScript debugger关键字,如清单 9-24 所示。

import React, { Component } from "react";
import { Display } from "./Display";

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            city: "London"
        }
    }

    changeCity = () => {
        debugger
        this.setState({ city: this.state.city === "London" ? "New York" : "London"})
    }

    render() {
        return (
            <Display value={ this.state.city } callback={ this.changeCity } />
        );
    }
}

Listing 9-24Triggering the Debugger in the App.js File in the src Folder

为了有效地使用调试器,禁用.env文件中的 HTTPS 选项,如清单 9-25 所示。如果您不禁用此选项,您将只能看到由 Babel 生成的代码,而不能看到您的原始源代码。

PORT=3500

HTTPS=false

Listing 9-25Disabling Secure Connections in the .env File in the projecttools Folder

停止开发工具,并通过运行projecttools文件夹中清单 9-26 所示的命令再次启动它们。

npx start

Listing 9-26Starting the Development Tools

应用将正常执行,但是当单击父按钮并调用changeCity方法时,浏览器将遇到debugger关键字并暂停应用的执行。然后你可以使用 F12 工具窗口中的控件来检查执行停止点的变量及其值,并手动控制执行,如图 9-12 所示。浏览器正在执行由开发工具创建的缩小和捆绑的代码,但是显示来自源地图的相应代码。

img/473159_1_En_9_Fig12_HTML.jpg

图 9-12

使用浏览器调试器

小费

大多数浏览器会忽略debugger关键字,除非 F12 工具窗口是打开的,但是在调试会话结束时删除它是一个好习惯。

摘要

在本章中,我描述了用create-react-app包创建的 React 项目的结构,并解释了 React 开发中使用的文件和文件夹的用途。我还解释了如何使用 React 开发工具,如何在浏览器中捆绑应用,错误显示和 linter 如何帮助避免常见问题,以及如何在没有收到预期结果时调试应用。在下一章中,我将介绍组件,它们是 React 应用的关键构件。