使用React.js、Next.js和AWS Lambda进行无服务器端渲染的速成班


不久前,我开始探索服务器端渲染的单页应用。是的,试着把这句话快速说三遍。为初创企业打造产品让我了解到,如果你想在网上立足,SEO是必须的。但是,你也想要SPA能够提供的性能。

我们想要两全其美。服务器端渲染提供的SEO提升,以及单页应用的速度。今天我将向你展示这一切,同时在AWS Lambda上的无服务器环境中基本免费托管。

TL;DR

让我们回顾一下本教程将涵盖的内容。你可以略过并跳到你感兴趣的部分。或者,作为一个书呆子,继续阅读。 *低声说 *请做个书呆子。

注意 :我们要写的 代码已经在GitHub 上,如果你需要进一步参考或错过任何步骤,可以随时查看。在我开始写这个教程之前, Cube.js 的人给了我一个关于React的快速介绍。他们有一个 s 无服务器分析框架 ,可以很好地插入React。请随意试一试。

我们在建造什么?

当然是一个快速的React应用程序!每个SPA的代价都是糟糕的SEO能力。但每个SPA的代价是糟糕的SEO能力。所以我们需要以一种结合服务器端渲染的方式来构建这个应用程序。听起来很简单。我们可以使用 Next.js ,一个用于静态和服务器渲染的轻量级框架 React.js 应用程序。

为了实现这一目标,我们需要启动一个简单的Express服务器,并配置Next应用程序,使其通过Express提供文件。这比听起来要简单得多。

然而,从标题上看,你可以认为我们不喜欢在我附近的 s erver 这个词。解决方案是将整个应用程序部署到 AWS Lambda ! 它毕竟是一个小小的Node.js实例。

准备好了吗?让我们开始破解吧!

配置和安装依赖

像往常一样,我们从无聊的部分开始,设置项目和安装依赖。

1. 安装无服务器框架

为了使无服务器开发不至于 遭受绝对的折磨,继续安装 无服务器框架

$ npm i -g serverless

注意:   如果你使用的是Linux或Mac,你可能需要以 sudo 的方式运行命令。

一旦在你的机器上全局安装,你就可以在终端的任何地方使用这些命令。但是,为了让它与你的AWS账户通信,你需要配置一个IAM用户。跳过 这里的解释 ,然后回来运行下面的命令,并使用提供的密钥。

$ serverless config credentials \ 
    --provider aws \ 
    --key xxxxxxxxxxxxxx \ 
    --secret xxxxxxxxxxxxxx

现在你的无服务器安装知道当你运行任何终端命令时要连接到什么账户。让我们跳进去看看它的运行情况。

2. 创建一个服务

创建一个新的目录来存放你的无服务器应用程序服务。在那里启动一个终端。现在你已经准备好创建一个新的服务了。

你问什么是服务?把它看作是一个项目。但不是真的。它是你定义AWS Lambda函数、触发它们的事件以及它们需要的任何AWS基础设施资源的地方,所有这些都在一个叫做 s erverless.yml 的文件中。

回到你的终端,输入。

$ serverless create --template aws-nodejs --path ssr-react-next 

create   命令将创建一个新的 s 服务 。震惊吧! 但这里是有趣的部分。我们需要为这个函数选择一个运行时间。这被称为 模板 。传入 aws-nodejs 将设置运行时间为Node.js。这正是我们想要的。 路径 将为该服务创建一个文件夹。

3. 安装npm模块

在你的终端中进入 ss r-react-next 文件夹。里面应该有三个文件,但现在,让我们首先初始化npm。

$ npm init -y

package.json 文件被创建后,你可以安装一些依赖性的东西。

$ npm i \
    axios \
    express \
    serverless-http \
    serverless-apigw-binary \
    next \
    react \
    react-dom \
    path-match \
    url \
    serverless-domain-manager

这些是我们的生产依赖项,我将在下文中更详细地解释它们的作用。最后一个被称为 serverless-domain-manager 的,将让我们把一个域与我们的端点联系起来。很好!

现在,你的 package.json 看起来应该是这样的。

  // package.json
{
  "name": "serverless-side-rendering-react-next"。
  "版本": "1.0.0",
  "描述": "",
  "main": "index.js",
  "scripts": { // 添加这些脚本
    "build": "Next build",
    "deploy": "下一个构建 && sls 部署"
  },
  "关键词"。[],
  "作者": "",
  "许可证": "ISC",
  "依赖性"。{
    "axios": "^0.18.0",
    "Express": "^4.16.4",
    "next": "^7.0.2",
    "path-match": "^1.2.4",
    "反应": "^16.6.3",
    "react-dom": "^16.6.3",
    "serverless-apigw-binary": "^0.4.4",
    "serverless-http": "^1.6.0",
    "url": "^0.11.0",
    "serverless-domain-manager": "^2.6.0"
  }
}  

我们还需要添加两个脚本,一个用于构建,一个用于部署应用程序。你可以在 package.jsonscripts 部分看到它们。

4. 配置serverless.yml文件

继续,最后让我们在代码编辑器中打开该项目。查看 s erverless.yml 文件,它包含这个服务的所有配置设置。在这里,你既要指定一般的配置设置,也要指定每个功能的设置。你的 s erverless.yml 将充满了模板代码和注释。请随意将其全部删除,然后将这个粘贴进来。

service: ssr-react-next

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${self:custom.secrets.NODE_ENV}
  region: us-east-1
  environment: 
    NODE_ENV: ${self:custom.secrets.NODE_ENV}

functions:
  server:
    handler: index.server
    events:
      - http: ANY /
      - http: ANY /{proxy+}

plugins:
  - serverless-apigw-binary
  - serverless-domain-manager

custom:
  secrets: ${file(secrets.json)}
  apigwBinary:
    types:
      - '*/*'
  customDomain:
    domainName: ${self:custom.secrets.DOMAIN}
    basePath: ''
    stage: ${self:custom.secrets.NODE_ENV}
    createRoute53Record: true
    # endpointType: 'regional'
    # if the ACM certificate is created in a region except for `'us-east-1'` you need `endpointType: 'regional'`

functions 属性列出了服务中的所有功能。我们只需要一个函数,因为它将运行Next应用程序并渲染React页面。它的工作原理是启动一个小小的Express服务器,在Express路由器旁边运行Next渲染器,并将服务器传递给 s erverless-http 模块。

反过来,这将把整个Express应用程序捆绑到一个单一的lambda函数中,并将其与API网关的端点联系起来。在函数属性下,你可以看到一个 s erver 函数,它将在 index.js 文件中有一个名为 server 的处理器。API Gateway将代理任何和每一个请求到内部的Express路由器,然后告诉Next来渲染我们的React.js页面。Woah,这听起来很复杂! 但其实并不复杂。一旦我们开始写代码,你会发现它真的很简单。

我们还添加了两个插件, serverless-apigw-binary 用于让更多的mime类型通过API Gateway, serverless-domain-manager 可以让我们毫不费力地将域名与我们的端点挂钩。

我们在底部也有一个 custom 部分。 secrets 属性作为一种安全地将环境变量加载到我们的服务中的方式。它们后来通过使用 ${self:custom.secrets.<environment_var>} 来引用,实际的值被保存在一个叫做 secrets.json 的简单文件中。

除此之外,我们还让API网关二进制插件知道我们想让所有的类型通过,并为我们的端点设置一个自定义域。

配置就这样了,让我们添加 secrets.json 文件。

5. 添加秘密文件

添加一个 secrets.json 文件并将其粘贴进去。这将使我们不会向GitHub推送秘密密钥。

{
  "NODE_ENV": "production",
  "DOMAIN": "react-ssr.your-domain.com"
}

现在,只有通过改变这些值,才能将不同的环境部署到不同的阶段和领域。相当酷。

使用无服务器框架和Next.js构建应用程序

为了构建一个服务器端渲染的React.js应用程序,我们将使用Next.js框架。它让你专注于编写应用程序,而不是担心SEO。它的工作原理是在将JavaScript发送到客户端之前进行渲染。一旦它被加载到客户端,它将缓存它,并从那里提供服务。你一定会喜欢它的速度!

我们先在服务器上编写Next.js的设置。

1. 设置Next.js服务器(less)端渲染

创建一个名为 s erver.js的文件 。真的很直观,我知道。

// server.js
const express = require('express')
const path = require('path')
const dev = process.env.NODE_ENV !== 'production'
const next = require('next')
const pathMatch = require('path-match')
const app = next({ dev })
const handle = app.getRequestHandler()
const { parse } = require('url')

const server = express()
const route = pathMatch()
server.use('/_next', express.static(path.join(__dirname, '.next')))
server.get('/', (req, res) => app.render(req, res, '/'))
server.get('/dogs', (req, res) => app.render(req, res, '/dogs'))
server.get('/dogs/:breed', (req, res) => {
  const params = route('/dogs/:breed')(parse(req.url).pathname)
  return app.render(req, res, '/dogs/_breed', params)
})
server.get('*', (req, res) => handle(req, res))

module.exports = server

这很简单。我们正在抓取Express和Next,用 express.static 创建一个静态路由,并把Next将创建的捆绑JavaScript的目录传给它。路径是 /_next ,它指向 .next 文件夹。

我们还将设置服务器端的路由,并为客户端的渲染器添加一个 总括性的 路由。

现在,应用程序需要与 serverless-http 挂钩,并作为一个lambda函数导出。创建一个 index.js 文件,把这个粘贴进去。

// index.js
const sls = require('serverless-http')
const binaryMimeTypes = require('./binaryMimeTypes')

const server = require('./server')
module.exports.server = sls(server, {
  binary: binaryMimeTypes
}

正如你所看到的,我们还需要创建 binaryMimeTypes.js 文件来保存我们想要启用的所有mime类型。这只是一个简单的数组,我们把它传给 serverless-http 模块。

// binaryMimeTypes.js
module.exports = [
  'application/javascript',
  'application/json',
  'application/octet-stream',
  'application/xml',
  'font/eot',
  'font/opentype',
  'font/otf',
  'image/jpeg',
  'image/png',
  'image/svg+xml',
  'text/comma-separated-values',
  'text/css',
  'text/html',
  'text/javascript',
  'text/plain',
  'text/text',
  'text/xml'
]

很好,关于Next.js的设置就这样了。让我们跳到客户端的代码中去吧!

2. 编写客户端React.js

在你项目的根目录下创建三个文件夹,分别命名为 componentslayoutspages 。一旦进入 layouts 文件夹,创建一个名称为 default.js 的新文件,并将此粘贴进去。

// layouts/default.js
import React from 'react'
import Meta from '../components/meta'
import Navbar from '../components/navbar'
export default ({ children, meta }) => (
  <div>
    <Meta props={meta} />
    <Navbar />
    { children }
  </div>
)

默认视图将有一个用于动态设置元标签的 <Meta /> 组件和一个 <Navbar/> 组件。 { children } 将从使用这个布局的组件中渲染出来。

现在再添加两个文件。一个 navbar.js 和一个 meta.js 文件在 components 文件夹中。

// components/navbar.js
import React from 'react'
import Link from 'next/link'

export default () => (
  <nav className='nav'>
    <ul>
      <li>
        <Link href='/'>Home</Link>
      </li>
      <li>
        <Link href='/dogs'>Dogs</Link>
      </li>
      <li>
        <Link href='/dogs/shepherd'>Only Shepherds</Link>
      </li>
    </ul>
  </nav>
)

这是一个非常简单的导航,将用于在一些可爱的狗之间进行导航。一旦我们在 pages 文件夹中添加一些东西,它就有意义了。

// components/meta.js
import Head from 'next/head'
export default ({ props = { title, description } }) => (
  <div>
    <Head>
      <title>{ props.title || 'Next.js Test Title' }</title>
      <meta name='description' content={props.description || 'Next.js Test Description'} />
      <meta name='viewport' content='width=device-width, initial-scale=1' />
      <meta charSet='utf-8' />
    </Head>
  </div>
)

meta.js 将使我们更容易向我们的元标签注入价值。现在你可以继续在 pages 文件夹中创建一个 index.js 文件。粘贴下面的代码。

// pages/index.js
import React from 'react'
import Default from '../layouts/default'
import axios from 'axios'
const meta = { title: 'Index title', description: 'Index description' }

class IndexPage extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      loading: true,
      dog: {}
    }
    this.fetchData = this.fetchData.bind(this)
  }
  async componentDidMount () {
    await this.fetchData()
  }
  async fetchData () {
    this.setState({ loading: true })
    const { data } = await axios.get(
      'https://api.thedogapi.com/v1/images/search?limit=1'
    )
    this.setState({
      dog: data[0],
      loading: false
    })
  }
  render () {
    return (
      <Default meta={meta}>
        <div>
          <h1>This is the Front Page.</h1>
          <h3>Random dog of the day:</h3>
          <img src={this.state.dog.url} alt='' />
        </div>
      </Default>
    )
  }
}

export default IndexPage

index.js 文件将在我们应用程序的根路径上呈现。它调用一个狗的API,将显示一个可爱的狗的图片。

让我们创建更多的路由。创建一个名为 dogs 的子文件夹,并在其中创建一个 index.js 文件和一个 _breed.js 文件。 index.js 将在 /dogs 路由处渲染,而 _breed.js 将在 /dogs/:breed 处渲染,其中 :breed 代表一个路由参数。

将此添加到 dogs 目录中的 index.js

// pages/dogs/index.js
import React from 'react'
import axios from 'axios'
import Default from '../../layouts/default'
const meta = { title: 'Dogs title', description: 'Dogs description' }

class DogsPage extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      loading: true,
      dogs: []
    }
    this.fetchData = this.fetchData.bind(this)
  }
  async componentDidMount () {
    await this.fetchData()
  }
  async fetchData () {
    this.setState({ loading: true })
    const { data } = await axios.get(
      'https://api.thedogapi.com/v1/images/search?size=thumb&limit=10'
    )
    this.setState({
      dogs: data,
      loading: false
    })
  }
  renderDogList () {
    return (
      <ul>
        {this.state.dogs.map((dog, key) =>
          <li key={key}>
            <img src={dog.url} alt='' />
          </li>
        )}
      </ul>
    )
  }
  render () {
    return (
      <Default meta={meta}>
        <div>
          <h1>Here you have all dogs.</h1>
          {this.renderDogList()}
        </div>
      </Default>
    )
  }
}

export default DogsPage

而且,在 dogs 文件夹中的 _breed.js 文件中还有一个片段。

// pages/dogs/_breed.js
import React from 'react'
import axios from 'axios'
import Default from '../../layouts/default'

class DogBreedPage extends React.Component {
  static getInitialProps ({ query: { breed } }) {
    return { breed }
  }
  constructor (props) {
    super(props)
    this.state = {
      loading: true,
      meta: {},
      dogs: []
    }
    this.fetchData = this.fetchData.bind(this)
  }
  async componentDidMount () {
    await this.fetchData()
  }
  async fetchData () {
    this.setState({ loading: true })
    const reg = new RegExp(this.props.breed, 'g')

    const { data } = await axios.get(
      'https://api.thedogapi.com/v1/images/search?size=thumb&has_breeds=true&limit=50'
    )
    const filteredDogs = data.filter(dog =>
      dog.breeds[0]
        .name
        .toLowerCase()
        .match(reg)
    )
    this.setState({
      dogs: filteredDogs,
      breed: this.props.breed,
      meta: { title: `Only ${this.props.breed} here!`, description: 'Cute doggies. :D' },
      loading: false
    })
  }
  renderDogList () {
    return (
      <ul>
        {this.state.dogs.map((dog, key) =>
          <li key={key}>
            <img src={dog.url} alt='' />
          </li>
        )}
      </ul>
    )
  }
  render () {
    return (
      <Default meta={this.state.meta}>
        <div>
          <h1>Dog breed: {this.props.breed}</h1>
          {this.renderDogList()}
        </div>
      </Default>
    )
  }
}

export default DogBreedPage

正如你在 Default 组件中看到的,我们正在注入自定义元标签。它将在你的页面的 <head> 中添加自定义字段,给予它适当的SEO支持!

注意 :如果你被卡住了, 这里是代码在repo中的样子

让我们部署它,看看它是否工作。

将应用程序部署到AWS Lambda

在一开始,我们在我们的 package.json 中添加了一个脚本,名为 deploy 。它将按照我们在 serverless.yml 中的指定,构建Next应用并部署无服务器服务。

你所需要做的就是运行。

$ npm run deploy

终端将返回输出,显示你的应用的端点。我们还需要添加域以使其正常工作。我们已经在 serverless.yml 中添加了配置,但还有一条命令需要运行。

$ sls create_domain

这将创建一个CloudFront分布,并将其与你的域挂钩。确保你已经将证书添加到你的AWS账户。通常AWS需要20分钟左右的时间来配置一个新的分布。让你的眼睛休息一会儿。

一旦你回来了,就去重新部署这一切。

$ npm run deploy

现在它应该与你的域名捆绑起来。下面是它应该有的样子。

很好! 该应用程序已经开始运行。来吧,试试它。

收尾

这次演练是一次情感的过山车!它让你对创建一个新的视角。它为你提供了一个新的视角,让你在创建快速和高性能的单页应用程序的同时,保持服务器渲染的应用程序的SEO能力。然而,有一个问题。你不需要担心服务器的问题。这一切都在AWS Lambda的无服务器环境中运行。它很容易部署并自动扩展。没有比这更好的了。

如果你在哪里卡住了,可以看看 GitHub repo 以进一步参考,如果你想让更多人在GitHub上看到它,请随时给它一个星星。

如果你想阅读我以前的一些无服务器的想法,请到 我的个人资料 加入我的通讯!

或者,马上看看我的几篇文章吧。

我还强烈建议查看 这篇关于Next.js的文章 ,以及 这篇关于无服务器域管理器的教程

希望你们这些家伙和姑娘们喜欢读这个,就像我喜欢写这个一样。直到下一次,要有好奇心,要有乐趣。