使用Webtask.io构建无服务器的MERN Story应用程序-部署零:2

This is a continuation of the previous article where we discussed Webtask. We shared few Webtask concepts and built a RESTful API using Express and Node. Our data is persisted in a MongoDB database provisioned by Mongolab.

这是我们讨论Webtask的上一篇文章的延续。 我们共享了一些Webtask概念,并使用Express和Node构建了RESTful API。 我们的数据保存在Mongolab提供的MongoDB数据库中。

In this part of the article, we will consume the REST API in a React-based UI app. At the end, we will deploy the app to Github Pages so as to have both our API and Frontend available remotely.

在本文的这一部分中,我们将在基于React的UI应用程序中使用REST API。 最后,我们将应用程序部署到Github Pages,以便可以远程使用我们的API和Frontend。

创建一个React应用 ( Creating a React App )

For maintenance purposes and task distribution among teams, it's always preferable to split the entire application into different projects. We have our API ready in a project directory, it's not much of good practice to build our React app right in the same directory. Rather, we will create an independent React project that will communicate with the API via endpoints.

为了维护目的和团队之间的任务分配,始终最好将整个应用程序拆分为不同的项目。 我们已经在项目目录中准备好了我们的API,在同一个目录中构建React应用程序并不是一个好习惯。 相反,我们将创建一个独立的React项目,该项目将通过端点与API进行通信。

Facebook makes creating a React project easy by providing a CLI tool for that:

通过提供以下方面的CLI工具,Facebook使创建React项目变得容易:

# 1. Install CLI tool
npm install -g create-react-app
# 2. Create a React app, "wt-mern-ux"
create-react-app wt-mern-ux
# 3. cd into app
cd wt-mern-ux
# 4. Launch app
npm start

组件结构 (Component Structure)

React is a component-based tool, therefore, it is easier to visualize what is expected from an app when the app's components hierarchical structure is analyzed. Let's have a look:

React是基于组件的工具,因此,在分析应用程序的组件层次结构时,更容易可视化应用程序的预期。 我们来看一下:

The App component is the container component as well as the 1st in the hierarchy. This makes it the entry point of our app thereby serving as the control unit for all other presentation components.

App组件是容器组件 ,也是层次结构中的第一个组件 。 这使其成为我们应用程序的切入点,从而充当所有其他演示组件的控制单元。

The obvious components are the presentation components because they paint the browser with contents and visuals. In that regards, we will build the app starting with presentation components and when a logic in App is needed, we will discuss that as well.

显而易见的组件是表示组件,因为它们用内容和视觉效果绘制浏览器。 在这方面,我们将从演示组件开始构建应用程序,并且当需要App的逻辑时,我们也会对此进行讨论。

故事清单 ( Story List )

We need to read a list of stories from the API endpoint and display them on the webpage. This should be some pretty basic stuff:

我们需要从API端点读取故事列表,并将其显示在网页上。 这应该是一些非常基本的东西:

// ./src/Components/StoryList/StoryList.js
import React from 'react'
// FlipMove for list animations
import FlipMove from 'react-flip-move';

import StoryItem from '../StoryItem/StoryItem'
import './StoryList.css'

export default ({stories, handleEdit, handleDelete}) => (
    <div className="StoryList clearfix">
        <FlipMove duration={350} easing="ease-in-out" enterAnimation="accordionHorizontal">
            {stories.map(story => <StoryItem
                story={story}
                key={story._id}
                handleEdit={handleEdit}
                handleDelete={handleDelete}
            />)}
        </FlipMove>
    </div>
)

And there it is; A functional component that receives a list of stories, and some event handlers from the App component. It iterates over this stories and passes each of the items down to a child StoryItem component. The event handlers, handleEdit and handleDelete are also passed down to StoryItem.

在那里; 一个功能组件,可从App组件接收故事列表以及一些事件处理App 。 它遍历此stories并将每个项目向下传递到子StoryItem组件。 事件处理程序handleEdithandleDelete也将传递给StoryItem

Let's see how App fetches stories:

让我们看看App如何获取stories

import React, { Component } from 'react';
import Axios from 'axios';

import StoryList from './Components/StoryList/StoryList';
import './App.css';

class App extends Component {

  constructor() {
    super();

    this.state = {
      stories: [],
    };

    this.apiUrl = 'https://wt-<WEBTASK-ACCOUNT>-0.run.webtask.io/wt-mern-api/stories'

    this.handleEdit = this.handleEdit.bind(this);
    this.handleDelete = this.handleDelete.bind(this)
  }

  componentDidMount() {
    // Fetch stories from API and
    // and update `stories` state
    Axios.get(this.apiUrl).then(({data}) => {
      this.setState({stories: data});
    })
  }

  handleEdit(id) {
     // Open a modal to update a story
     // Uncomment this line later
    // this.openModal(this.state.stories.find(x => x._id === id))
  }

  handleDelete(id) {
    // Delete story from API
    Axios.delete(`${this.apiUrl}?id=${id}`).then(() => {
       // Remove story from stories list
      const updatedStories = this.state.stories.findIndex(x => x._id === id);
      this.setState({states: [...this.state.stories.splice(updatedStories, 1)]})
    })
  }

  render() {

    return (
      <div className="App">
        <div className="col-md-4 col-md-offset-4 Story">

          <div className="StoryHeader">
            <h2>Stories</h2>
          </div>
          {/* pass stories and 
          event handlers down to StoryList*/}
          <StoryList
              stories={this.state.stories}
              handleEdit={this.handleEdit}
              handleDelete={this.handleDelete}
          />

          <div className="StoryFooter">
            <p>Thank you!</p>
          </div>

        </div>
      </div>
    );
  }
}

export default App;
  • componentDidMount is a lifecycle method. It is called when your component is ready. This feature makes it a great place to fetch bootstrap data. In that case, we are requesting a list of stories from our server and setting the stories state to whatever is returned.

    componentDidMount是生命周期方法。 当组件准备就绪时,将调用它。 此功能使其成为获取引导程序数据的好地方。 在这种情况下,我们需要从服务器中获取故事列表,并将stories状态设置为返回的内容。
  • handleEdit method is meant to pop up a modal with a form and an existing story to be updated. /*(Don’t be scared :), we’ll take a look at that soon) */ We will see about that soon.

    handleEdit方法旨在弹出一个带有表单和要更新的现有故事的模式。 / *(不要害怕:),我们很快会看的)* /我们很快就会看到。
  • handleDelete makes a DELETE request for a single resource. If that was successful, rather than re-fetch the whole list, we just remove the item from the stories list.

    handleDelete对单个资源发出DELETE请求。 如果成功,则无需重新获取整个列表,我们只需从stories列表中删除该项目即可。
  • The lost <StoryList /> receives the stories and event handlers as props. Functions are first class objects so it's possible to pass them around.

    丢失的<StoryList />故事和事件处理程序作为道具。 函数是一流的对象,因此可以传递它们。
  • <FlipMove /> is an animation component that helps us apply different animation effects to the list when adding and removing items from the list.

    <FlipMove />是一个动画组件,可帮助我们在列表中添加和删除项目时将不同的动画效果应用于列表。

故事项 ( Story Item )

StoryItem is yet another presentation component. It takes the iteration values passed down from StoryList and displays each of them. It also receives the event handlers and binds them to some buttons.

StoryItem是另一个演示组件。 它采用从StoryList传递的迭代值并显示每个值。 它还接收事件处理程序并将它们绑定到某些按钮。

// ./src/Components/StoryItem/StoryItem.js
import React from 'react'
import './StoryItem.css'

export default class StoryItem extends React.Component {
  render() {
    const {
      story,
      handleEdit,
      handleDelete
    } = this.props;
    return (
        <div className="StoryItem clearfix">
          <div className="col-sm-9 StoryItem__content">
            <h4>{story.author}</h4>
            <p>{story.content}</p>
          </div>
          <div className="col-sm-3 StoryItem__control">
            <span
                className="glyphicon glyphicon-edit"
                onClick={handleEdit.bind(this, story._id)}
            />
            <span
                className="glyphicon glyphicon-remove"
                onClick={handleDelete.bind(this, story._id)}
            />
          </div>
        </div>
    )
  }
}

This component doesn't have any direct relationship with App container component, so we don't have to worry about that. It's also a class component rather than functional component because FlipMove uses React refs for list items which functional components do not support.

该组件与App容器组件没有任何直接关系,因此我们不必为此担心。 它也是类组件而不是功能组件,因为FlipMove使用React refs来列出功能组件不支持的列表项。

故事按钮和情态 ( Story Button and Modal )

We need to add a button which when clicked, launches a Modal to create a new story. /Nothing strange here!/ Just a stateless functional component that returns a HTML button:

我们需要添加一个按钮,单击该按钮将启动模态以创建新故事。 / 这里没什么奇怪的! /只是一个返回HTML按钮的无状态功能组件:

// ./src/Components/StoryButton/StoryButton.js
import React from 'react';
import './StoryButton.css'

export default ({handleClick}) =>
   <button className="StoryButton" onClick={handleClick}> + </button>

It's housed by the App components:

它位于App组件中:

import React, { Component } from 'react';

import StoryButton from './Components/StoryButton/StoryButton';
...

class App extends Component {

  constructor() {
    super();

    this.state = {
      ...
    };

    ...
    this.openModal = this.openModal.bind(this);
  }

  ...

  openModal(story) {
    // Launches Modal. We will un-comment later
    /* this.setState({modalIsOpen: true});
    if(story) {
      this.setState({story});
    } */
  }

  render() {

    return (
      <div className="App">
        <div className="col-md-4 col-md-offset-4 Story">

          ...

        </div>

        <StoryButton handleClick={this.openModal.bind(this, null)} />
      </div>
    );
  }
}

export default App;

The handleClick property holds an event handler to open a modal. Now this Modal is not a mystery, let's have a look at its component:

handleClick属性持有一个事件处理程序以打开模式。 现在这个模态已经不是什么谜,让我们看一下它的组成部分:

import React from 'react';
import Modal from 'react-modal';

import './StoryModal.css'

// Modal custom styles
// Basically centering stuff
const customStyles = {
  content : {
    top                   : '50%',
    left                  : '50%',
    right                 : 'auto',
    bottom                : 'auto',
    marginRight           : '-50%',
    transform             : 'translate(-50%, -50%)'
  }
};

export default class ModalComponent extends React.Component {

  constructor(props) {
    super(props)
    // Internal state
    this.state = {
      author: '',
      content: '',
      _id: ''
    }
    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(e) {
    // Re-binding author and content values
    if(e.target.id === 'author') {
      this.setState({author: e.target.value})
    }
    if(e.target.id === 'content') {
      this.setState({content: e.target.value})
    }
  }

  componentWillReceiveProps({story}) {
    // Update story state anytime
    // a new props is passed to the Modal
    // This is handy because the component
    // is never destroyed but it's props might change
    this.setState(story)
  }

  render() {
    const {
        modalIsOpen,
        closeModal
    } = this.props;
    // Use React's Modal component
    return (
        <Modal
            isOpen={modalIsOpen}
            onRequestClose={closeModal.bind(this, null)}
            style={customStyles}
            shouldCloseOnOverlayClick={false}
            contentLabel="Story Modal"
        >

          <div className="Modal">
            <h4 className="text-center">Story Form</h4>
            <div className="col-md-6 col-md-offset-3">
              <form>
                <div className="form-group">
                  <label>Name</label>
                  <input type="text" value={this.state.author} onChange={this.handleInputChange} id="author" className="form-control"/>
                </div>
                <div className="form-group">
                  <label>Content</label>
                  <textarea value={this.state.content} onChange={this.handleInputChange} cols="30" id="content" className="form-control"></textarea>
                </div>
                <div className="form-group">
                  <button
                      className="ModalButton"
                      onClick={closeModal.bind(this, this.state)}
                  >Save</button>
                  <button
                      className="ModalButton ModalButton--close"
                      onClick={closeModal.bind(this, null)}
                  >Cancel</button>
                </div>
              </form>
            </div>
          </div>
        </Modal>
    )
  }
}
笔记 (Notes)
  • We are using the Modal component from the react-modal library.

    我们正在使用react-modal库中的Modal组件。
  • /Because it has a form to keep track of, StoryModal possesses an internal state. For this reason, it’s not entirely a presentation component./ has a form to keep track of, for that reason, it's not entirely a presentation component because of it's internal state.

    / 因为StoryModal具有跟踪的形式,所以它具有内部状态。 因此,它并不完全是表示组件。 /具有某种形式的跟踪记录,因此,由于其内部状态,它并不完全是表示组件。
  • The StoryModal component can be shown or hidden but not created/mounted /n/or destroyed. Therefore, if its props changes, we update the story state with the new story props. This is why instead of using componentDidMount, we are using componentWillReceiveProps. /A/ Possible occurrence of such /a/ situation is when story state changes from empty property values to values that need to be updated.

    所述StoryModal部件可以显示或隐藏的,但不创建/安装/ N /或破坏。 因此,如果其道具发生变化,我们将使用新的story道具更新story状态。 这就是为什么我们不使用componentWillReceiveProps而是使用componentDidMount原因。 这样的/ 一个 /情形/ A /可能发生的是,当story的状态,从空的属性值更改为值,需要进行更新。

Next, we need to uncomment openModal and handleEdit logics in App:

接下来,我们需要在App取消注释openModalhandleEdit逻辑:

// ./src/App.js
...
constructor() {
 super();

 this.state = {
   modalIsOpen: false,
   }
 };
openModal(story) {
  this.setState({modalIsOpen: true});
   if(story) {
     this.setState({story});
   }
 }
handleEdit(id) {
  this.openModal(this.state.stories.find(x => x._id === id))
 }
 ...

If openModal is passed a story, we will set the state's story object to its content. This is passed down to the Modal for us to edit. If no story is passed, we just create a new story via the form.

如果openModal传递了一个故事,则将状态的story对象设置为其内容。 这被传递给模态供我们编辑。 如果未传递任何故事,则仅通过表单创建一个新故事。

Let's now complete the Modal wire by writing logic for what happens when the Modal is closed:

现在,通过编写关闭模态时发生的逻辑来完成模态连线:

import React, { Component } from 'react';
import Axios from 'axios';

import StoryModal from './Components/StoryModal/StoryModal';
import './App.css';

class App extends Component {

  constructor() {
    super();

    this.state = {
      modalIsOpen: false,
      stories: [],
      story: {
        author: '',
        content: '',
        _id: undefined
      }
    };

    ...
  }

 ...

  closeModal(model) {
    this.setState({modalIsOpen: false});
    if(model) {
      if(!model._id) {
        Axios.post(this.apiUrl, model).then(({data}) => {
          this.setState({stories: [data, ...this.state.stories]});
          this.setState({isLoading: false})
        })
      } else {
        Axios.put(`${this.apiUrl}?id=${model._id}`, model).then(({data}) => {
          const storyToUpdate = this.state.stories.find(x => x._id === model._id);
          const updatedStory = Object.assign({}, storyToUpdate, data)
          const newStories = this.state.stories.map(story => {
            if(data._id === story._id) return updatedStory;
            return story;
          })
          this.setState({stories: newStories});
        })
      }
    }
    this.setState({story: {
      author: '',
      content: '',
      _id: undefined
    }})
  }

  ...

  render() {

    return (
      <div className="App">

        <StoryModal
            modalIsOpen={this.state.modalIsOpen}
            story={this.state.story}
            closeModal={this.closeModal}
        />
      </div>
    );
  }
}

export default App;
笔记 (Notes)

Three possible outcomes:

三种可能的结果:

  1. A story model/data from the form does NOT exist. This means no argument was sent to closeModal when calling it. If that's the case, nothing should happen. A typical example is the Modal's Cancel button.

    表单中的故事模型/数据不存在。 这意味着在调用它时没有将参数发送给closeModal 。 如果是这样,则什么也不会发生。 一个典型的例子是“模态”的“取消”按钮。

  2. If model._id is NOT undefined, it means the model existed before, so we just need to make an update write rather than creating a new entry entirely. We do this by using axios's put method to send a PUT request with the payload to a single resource. The response contains the updated record which we can shove into the array.

    如果没有定义model._id ,则表示该模型以前存在过,因此我们只需要进行更新写入即可,而不必完全创建一个新条目。 为此,我们使用axios的put方法将带有有效负载的PUT请求发送到单个资源。 响应包含更新的记录,我们可以将其推送到数组中。

  3. In a situation where model._id is undefined, then we create a new story using a POST request and adding the new story to the top of our array.

    在未定义model._id的情况下,我们使用POST请求创建一个新故事,并将新故事添加到数组的顶部。

行使 ( Exercise )

Extend the app a little bit to show a loading spinner for every HTTP request that's fired.

稍微扩展一下应用程序,以显示每个触发的HTTP请求的加载微调器。

部署到Github页面 ( Deploy to Github Pages )

To deploy the React app to GitHub pages, we need to carefully follow the steps below:

要将React应用程序部署到GitHub页面,我们需要仔细遵循以下步骤:

  • Build the app to generate production bundle:

    生成应用以生成生产包:
npm run build

This will generate a production bundle in the build directory.

这将在build目录中生成生产捆绑包。

  • Create a Github repository for the app and add the following line in your package.json:

    为应用程序创建一个Github存储库,并在package.json添加以下行:
"homepage": "https://<GH-USERNAME>.github.io/<REPO-NAME>",
  • Install gh-pages:

    安装gh-pages
npm install --save gh-pages
  • Add a script to deploy the build directory:

    添加脚本以部署构建目录:
"scripts": {
   ...
   "deploy": "gh-pages -d build"
 }
  • Run the deploy script

    运行部署脚本
npm run deploy

结论 ( Conclusion )

Hopefully, I have proven to you that you do not need to be a backend expert before you can make your UI come to life. Tools like Webtask and even Node with a little bit of digging docs will provide a server for you while you focus on writing the ever awesome JavaScript. The frontend can be anything; not necessarily React.

希望我已经向您证明,您无需成为后端专家,即可使UI栩栩如生。 诸如Webtask甚至带有少量挖掘文档的Node之类的工具将为您提供服务器,而您将专注于编写超赞JavaScript。 前端可以是任何东西; 不一定是React。

翻译自: https://scotch.io/tutorials/build-a-serverless-mern-story-app-with-webtask-io-zero-to-deploy-2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值