无服务器端渲染 React Next
使用无服务器框架在 AWS Lambda 上设置 Next 和 React 的示例 repo。
不久前,我开始探索服务器端渲染的单页应用。是的,试着快速说三遍。为初创公司构建产品的经历让我明白,如果你想要在线上有所作为,SEO 是必不可少的。但是,你也需要 SPA 所能提供的性能。
我们想要两全其美。既能提升服务器端渲染带来的 SEO 性能,又能保持单页应用的速度。今天,我将向大家展示这一切,同时在 AWS Lambda 上的无服务器环境中免费托管它。
让我们来看看本教程的内容。你可以快速浏览,直接跳到你感兴趣的部分。或者,你也可以像个书呆子一样继续阅读。*悄悄话* 请像个书呆子一样。
注意:我们将要编写的代码已经在 GitHub 上,如果您需要进一步参考或遗漏任何步骤,请随时查看。在我开始撰写本教程之前, Cube.js的团队简要介绍了 React。他们有一个可以完美嵌入 React 的无服务器分析框架。欢迎尝试一下。
当然是超快的 React 应用啦!不过,每个 SPA 的代价就是糟糕的 SEO 性能。所以我们需要以一种能够集成服务器端渲染的方式构建应用。听起来很简单。我们可以使用Next.js,这是一个轻量级的静态和服务端渲染React.js应用框架。
为了实现这一点,我们需要启动一个简单的 Express 服务器,并配置 Next 应用通过 Express 提供文件服务。其实操作起来比听起来简单得多。
然而,从标题就能看出,我们社区的人不太喜欢“服务器”这个词。解决方案是把整个应用程序部署到AWS Lambda上!毕竟,它只是一个很小的 Node.js 实例。
准备好了吗?快点行动吧!
与往常一样,我们从无聊的部分开始,设置项目并安装依赖项。
为了使无服务器开发不再是绝对的折磨,请继续安装无服务器框架。
$ npm i -g serverless
注意: 如果您使用的是 Linux 或 Mac,则可能需要以 的形式运行该命令sudo
。
一旦在您的计算机上全局安装,您就可以从终端的任何位置使用这些命令。但是,为了与您的 AWS 账户通信,您需要配置一个 IAM 用户。点击此处查看说明,然后返回并使用提供的密钥运行以下命令。
$ serverless config credentials \
--provider aws \
--key xxxxxxxxxxxxxx \
--secret xxxxxxxxxxxxxx
现在,你的 Serverless 安装已经知道在运行任何终端命令时要连接到哪个账户了。让我们开始实际操作吧。
创建一个新目录来存放您的无服务器应用服务。在那里启动一个终端。现在,您可以创建新服务了。
您可能会问,什么是服务?可以把它看作一个项目。但实际上并非如此。服务是定义 AWS Lambda 函数、触发这些函数的事件以及它们所需的任何 AWS 基础设施资源的地方,所有这些都包含在一个名为serverless.yml的文件中。
返回终端类型:
$ serverless create --template aws-nodejs --path ssr-react-next
create 命令会创建一个新的service。震惊!但有趣的部分来了。我们需要为函数选择一个运行时。这被称为template。传入 templateaws-nodejs
会将运行时设置为 Node.js。这正是我们想要的。path会为服务创建一个文件夹。
在终端中切换到ssr-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",
"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"
}
}
我们还需要添加两个脚本,一个用于构建,一个用于部署应用程序。您可以在scripts
部分中看到它们package.json
。
接下来,让我们在代码编辑器中打开项目。查看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'`
该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
文件。
添加一个secrets.json
文件并将其粘贴进去。这将阻止我们将密钥推送到 GitHub。
{
"NODE_ENV": "production",
"DOMAIN": "react-ssr.your-domain.com"
}
现在,只需更改这些值,您就可以将不同的环境部署到不同的阶段和域。太酷了。
为了构建一个服务器端渲染的 React.js 应用,我们将使用 Next.js 框架。它让你专注于编写应用,而无需担心 SEO。它的工作原理是在 JavaScript 发送到客户端之前对其进行渲染。一旦它在客户端加载完毕,它就会缓存并从客户端提供服务。它的速度绝对让你爱不释手!
让我们首先在服务器上编写 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
这很简单。我们抓取 Express 和 Next,创建一个静态路由,express.static
并将 Next 将要创建的 JavaScript 打包目录传递给它。路径是/_next
,它指向.next
文件夹。
我们还将设置服务器端路由并为客户端渲染器添加一个catch-all路由。
现在,需要将应用连接到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 的设置就到这里。让我们开始编写客户端代码吧!
在项目根目录中创建三个文件夹,分别名为components
、layouts
、pages
。进入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
将使我们更容易地将值注入到元标记中。现在,您可以继续在文件夹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
该index.js
文件将渲染到我们应用的根路径下。它调用一个狗狗 API,并显示一张可爱的狗狗图片。
让我们创建更多路由。创建一个名为 的子文件夹,并在其中dogs
创建一个index.js
文件和一个文件。 将在路由中渲染, 将在代表路由参数的位置渲染。_breed.js
index.js
/dogs
_breed.js
/dogs/:breed
:breed
将其添加到目录index.js
中dogs
。
// 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
_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
正如您在组件中看到的,Default
我们正在注入自定义元标记。它将在您的页面中添加自定义字段<head>
,从而提供适当的 SEO 支持!
注意:如果您遇到困难,请参阅 repo 中的代码。
让我们部署它并看看它是否有效。
一开始,我们在package.json
名为 的脚本中添加了一个名为 的脚本deploy
。它将构建 Next 应用并按照我们在 中指定的方式部署无服务器服务serverless.yml
。
您需要做的就是运行:
$ npm run deploy
终端将返回包含您应用端点的输出。我们还需要添加域名才能使其正常工作。我们已经在 中添加了配置,serverless.yml
但还需要运行一个命令。
$ sls create_domain
这将创建一个 CloudFront 发行版并将其连接到您的域。请确保您已将证书添加到您的 AWS 账户。AWS 通常需要大约 20 分钟来配置新的发行版。请稍事休息。
回来后,继续重新部署一切。
$ npm run deploy
现在它应该已经绑定到你的域名了。它看起来应该是这样的。
太棒了!应用已启动并运行。快来试用吧。
所有无服务器应用程序的首要问题是其分布式特性。简而言之,想要全面了解所有正在发生的事情极其困难。更不用说当出现问题时,调试起来有多困难了。
为了平息我的恐惧,我使用了Tracetest。它提供由 OpenTelemetry 支持的端到端测试和调试。
幸好,这里有详细的文档,让新手入门变得轻而易举。请按照快速入门指南操作。不过,别忘了回来这里看看。😄
这次演练就像坐过山车一样,带给你一种全新的视角,让你能够创建快速高效的单页应用,同时又能保留服务器渲染应用的 SEO 功能。不过,有一个问题:你无需担心服务器。一切都在 AWS Lambda 上的无服务器环境中运行。它易于部署,并可自动扩展。简直完美。
如果您在任何地方遇到困难,请查看GitHub repo以获取更多参考,如果您希望更多人在 GitHub 上看到它,请随意给它一颗星。
使用无服务器框架在 AWS Lambda 上设置 Next 和 React 的示例 repo。
如果您想阅读我之前的一些无服务器思考,请转到我的个人资料或加入我的时事通讯!
或者,立即查看我的几篇文章:
我还强烈建议您查看有关 Next.js 的这篇文章,以及有关无服务器域管理器的教程。
希望大家喜欢阅读这篇文章,就像我喜欢写这篇文章一样。如果你们喜欢,请点个小爱心,这样 dev.to 上会有更多人看到这篇教程。下次再见,保持好奇心,享受乐趣吧。
免责声明:Tracetest赞助了这篇博文。使用可观察性可以将测试创建和故障排除的时间和精力减少 80%。