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

2025-05-24

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

无服务器端渲染 React Next

不久前,我开始探索服务器端渲染的单页应用。是的,试着快速说三遍。为初创公司构建产品的经历让我明白,如果你想要在线上有所作为,SEO 是必不可少的。但是,你也需要 SPA 所能提供的性能。

我们想要两全其美。既能提升服务器端渲染带来的 SEO 性能,又能保持单页应用的速度。今天,我将向大家展示这一切,同时在 AWS Lambda 上的无服务器环境中免费托管它。

TL;DR

让我们来看看本教程的内容。你可以快速浏览,直接跳到你感兴趣的部分。或者,你也可以像个书呆子一样继续阅读。*悄悄话* 请像个书呆子一样。

注意我们将要编写的代码已经在 GitHub 上,如果您需要进一步参考或遗漏任何步骤,请随时查看。在我开始撰写本教程之前, Cube.js的团队简要介绍了 React。他们有一个可以完美嵌入 React 的无服务器分析框架。欢迎尝试一下。

我们正在建造什么?

当然是超快的 React 应用啦!不过,每个 SPA 的代价就是糟糕的 SEO 性能。所以我们需要以一种能够集成服务器端渲染的方式构建应用。听起来很简单。我们可以使用Next.js,这是一个轻量级的静态和服务端渲染React.js应用框架。

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

然而,从标题就能看出,我们社区的人不太喜欢“服务器”这个词。解决方案是把整个应用程序部署到AWS Lambda上!毕竟,它只是一个很小的 ​​Node.js 实例。

准备好了吗?快点行动吧!

配置并安装依赖项

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

1.安装无服务器框架

为了使无服务器开发不再是绝对的折磨,请继续安装无服务器框架

$ npm i -g serverless
Enter fullscreen mode Exit fullscreen mode

注意: 如果您使用的是 Linux 或 Mac,则可能需要以 的形式运行该命令sudo

一旦在您的计算机上全局安装,您就可以从终端的任何位置使用这些命令。但是,为了与您的 AWS 账户通信,您需要配置一个 IAM 用户。点击此处查看说明,然后返回并使用提供的密钥运行以下命令。

$ serverless config credentials \ 
    --provider aws \ 
    --key xxxxxxxxxxxxxx \ 
    --secret xxxxxxxxxxxxxx
Enter fullscreen mode Exit fullscreen mode

现在,你的 Serverless 安装已经知道在运行任何终端命令时要连接到哪个账户了。让我们开始实际操作吧。

2.创建服务

创建一个新目录来存放您的无服务器应用服务。在那里启动一个终端。现在,您可以创建新服务了。

您可能会问,什么是服务?可以把它看作一个项目。但实际上并非如此。服务是定义 AWS Lambda 函数、触发这些函数的事件以及它们所需的任何 AWS 基础设施资源的地方,所有这些都包含在一个名为serverless.yml的文件中。

返回终端类型:

$ serverless create --template aws-nodejs --path ssr-react-next
Enter fullscreen mode Exit fullscreen mode

create 命令会创建一个新的service。震惊!但有趣的部分来了。我们需要为函数选择一个运行时。这被称为template。传入 templateaws-nodejs会将运行时设置为 Node.js。这正是我们想要的。path会为服务创建一个文件夹

3.安装 npm 模块

在终端中切换到ssr-react-next文件夹。里面应该有三个文件,但现在,我们先初始化 npm。

$ npm init -y
Enter fullscreen mode Exit fullscreen mode

创建文件后package.json,您可以安装一些依赖项。

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

这些是我们的生产依赖项,稍后我会更详细地解释它们的作用。最后一个依赖项,名为“serverless-domain-manager将域名绑定到我们的端点”。太棒了!

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

// package.json
{
  "name": "serverless-side-rendering-react-next",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": { // ADD THESE SCRIPTS
    "build": "next build",
    "deploy": "next build && sls deploy"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.18.0",
    "express": "^4.16.4",
    "next": "^7.0.2",
    "path-match": "^1.2.4",
    "react": "^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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

4.配置serverless.yml文件

接下来,让我们在代码编辑器中打开项目。查看serverless.yml文件,它包含此服务的所有配置设置。在这里,您可以指定常规配置设置和每个函数的配置。您的serverless.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'`
Enter fullscreen mode Exit fullscreen mode

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

反过来,这会将整个 Express 应用捆绑成一个 lambda 函数,并将其绑定到 API 网关端点。在 functions 属性下,您可以看到一个服务器函数,该函数将具有文件server中指定的处理程序index.js。API 网关会将所有请求代理到内部 Express 路由器,然后路由器会指示 Next 渲染我们的 React.js 页面。哇,听起来很复杂!但其实并不复杂。一旦我们开始编写代码,您就会发现它实际上非常简单。

我们还添加了两个插件,serverless-apigw-binary用于让更多 mime 类型通过 API 网关,以及serverless-domain-manager让我们毫不费力地将域名连接到我们的端点。

custom我们在底部还有一个部分。该secrets属性用于安全地将环境变量加载到我们的服务中。稍后可以使用它们来引用${self:custom.secrets.<environment_var>}它们,实际值保存在一个名为的简单文件中secrets.json

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

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

5.添加 secrets 文件

添加一个secrets.json文件并将其粘贴进去。这将阻止我们将密钥推送到 GitHub。

{
  "NODE_ENV": "production",
  "DOMAIN": "react-ssr.your-domain.com"
}
Enter fullscreen mode Exit fullscreen mode

现在,只需更改这些值,您就可以将不同的环境部署到不同的阶段和域。太酷了。

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

为了构建一个服务器端渲染的 React.js 应用,我们将使用 Next.js 框架。它让你专注于编写应用,而无需担心 SEO。它的工作原理是在 JavaScript 发送到客户端之前对其进行渲染。一旦它在客户端加载完毕,它就会缓存并从客户端提供服务。它的速度绝对让你爱不释手!

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

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

创建一个名为server.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
Enter fullscreen mode Exit fullscreen mode

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

我们还将设置服务器端路由并为客户端渲染器添加一个catch-all路由。

现在,需要将应用连接到serverless-httplambda 函数并将其导出。创建一个index.js文件并将其粘贴进去。

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

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

如你所见,我们还需要创建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'
]
Enter fullscreen mode Exit fullscreen mode

太好了,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>
)
Enter fullscreen mode Exit fullscreen mode

默认视图将包含一个<Meta />用于动态设置元标记的组件和一个<Navbar/>组件。{ children }将从使用此布局的组件进行渲染。

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

// 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>
)
Enter fullscreen mode Exit fullscreen mode

这是一个非常简单的导航,用于在一些可爱的狗狗之间导航。一旦我们在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>
)
Enter fullscreen mode Exit fullscreen mode

meta.js将使我们更容易地将值注入到元标记中。现在,您可以继续在文件夹index.js中创建一个文件pages。粘贴以下代码。

// 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
Enter fullscreen mode Exit fullscreen mode

index.js文件将渲染到我们应用的根路径下。它调用一个狗狗 API,并显示一张可爱的狗狗图片。

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

将其添加到目录index.jsdogs

// 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
Enter fullscreen mode Exit fullscreen mode

_breed.js并且,文件夹中的文件中还有另一个片段dogs

// 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
Enter fullscreen mode Exit fullscreen mode

正如您在组件中看到的,Default我们正在注入自定义元标记。它将在您的页面中添加自定义字段<head>,从而提供适当的 SEO 支持!

注意:如果您遇到困难,请参阅 repo 中的代码

让我们部署它并看看它是否有效。

将应用程序部署到 AWS Lambda

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

您需要做的就是运行:

$ npm run deploy
Enter fullscreen mode Exit fullscreen mode

终端将返回包含您应用端点的输出。我们还需要添加域名才能使其正常工作。我们已经在 中添加了配置,serverless.yml但还需要运行一个命令。

$ sls create_domain
Enter fullscreen mode Exit fullscreen mode

这将创建一个 CloudFront 发行版并将其连接到您的域。请确保您已将证书添加到您的 AWS 账户。AWS 通常需要大约 20 分钟来配置新的发行版。请稍事休息。

回来后,继续重新部署一切。

$ npm run deploy
Enter fullscreen mode Exit fullscreen mode

现在它应该已经绑定到你的域名了。它看起来应该是这样的。

太棒了!应用已启动并运行。快来试用吧。

如何深入了解您的系统?

所有无服务器应用程序的首要问题是其分布式特性。简而言之,想要全面了解所有正在发生的事情极其困难。更不用说当出现问题时,调试起来有多困难了。

为了平息我的恐惧,我使用了Tracetest。它提供由 OpenTelemetry 支持的端到端测试和调试。

幸好,这里有详细的文档,让新手入门变得轻而易举。请按照快速入门指南操作。不过,别忘了回来这里看看。😄

总结

这次演练就像坐过山车一样,带给你一种全新的视角,让你能够创建快速高效的单页应用,同时又能保留服务器渲染应用的 SEO 功能。不过,有一个问题:你无需担心服务器。一切都在 AWS Lambda 上的无服务器环境中运行。它易于部署,并可自动扩展。简直完美。

如果您在任何地方遇到困难,请查看GitHub repo以获取更多参考,如果您希望更多人在 GitHub 上看到它,请随意给它一颗星。

GitHub 徽标 adnanrahic /无服务器端渲染 React-next

使用无服务器框架在 AWS Lambda 上设置 Next 和 React 的示例 repo。

无服务器端渲染 React Next

使用无服务器框架在 AWS Lambda 上设置 Next 和 React 的示例 repo。






如果您想阅读我之前的一些无服务器思考,请转到我的个人资料加入我的时事通讯!

或者,立即查看我的几篇文章:

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

希望大家喜欢阅读这篇文章,就像我喜欢写这篇文章一样。如果你们喜欢,请点个小爱心,这样 dev.to 上会有更多人看到这篇教程。下次再见,保持好奇心,享受乐趣吧。


免责声明:Tracetest赞助了这篇博文。使用可观察性可以将测试创建和故障排除的时间和精力减少 80%。


文章来源:https://dev.to/adnanrahic/a-crash-course-on-serverless-side-rendering-with-reactjs-nextjs-and-aws-lambda-13ed
PREV
从 DevOps 角度来看容器与无服务器
NEXT
使用 Express 和 MongoDB 的无服务器 API 速成课程 使用 Express 和 Mongodb 的无服务器 API 速成课程 使用 Express 和 Mongodb 的无服务器 API 速成课程