浏览器渲染
入门
通过启用 hub.browser
选项,在你的 Nuxt 项目中启用浏览器渲染
export default defineNuxtConfig({
hub: {
browser: true
},
})
最后,通过运行以下命令安装所需的依赖项
npx nypm i @cloudflare/puppeteer puppeteer
用法
在你的服务器 API 路由中,你可以使用 hubBrowser
函数获取一个 Puppeteer 浏览器实例
const { page, browser } = await hubBrowser()
在生产环境中,该实例将来自 @cloudflare/puppeteer
,它是 Puppeteer 的一个分支,专门用于在 Cloudflare worker 中工作。
page
实例,并在需要时关闭或断开 browser
实例的连接。以下是一些在你的 Nuxt 应用中使用像 Puppeteer 这样的无头浏览器的用例
- 截取页面的屏幕截图
- 将页面转换为 PDF
- 测试 Web 应用
- 收集页面加载性能指标
- 抓取网页以获取信息检索(提取元数据)
限制
为了提高生产环境中的性能,NuxtHub 将重用浏览器会话。这意味着浏览器将在每次请求后保持打开状态(持续 60 秒),如果可用,新请求将重用同一个浏览器会话,否则将打开一个新的会话。
Cloudflare 的限制是
- 每个 Cloudflare 帐户每分钟 2 个新浏览器
- 每个帐户 2 个并发浏览器会话
- 如果在 60 秒内没有检测到活动(空闲超时),浏览器实例将被终止
在创建浏览器实例时,你可以通过提供 keepAlive
选项来延长空闲超时
// keep the browser instance alive for 120 seconds
const { page, browser } = await hubBrowser({ keepAlive: 120 })
最大空闲超时时间为 600 秒(10 分钟)。
屏幕截图捕捉
截取网站的屏幕截图是无头浏览器的一个常见用例。让我们创建一个 API 路由来截取网站的屏幕截图
import { z } from 'zod'
export default eventHandler(async (event) => {
// Get the URL and theme from the query parameters
const { url, theme } = await getValidatedQuery(event, z.object({
url: z.string().url(),
theme: z.enum(['light', 'dark']).optional().default('light')
}).parse)
// Get a browser session and open a new page
const { page } = await hubBrowser()
// Set the viewport to full HD & set the color-scheme
await page.setViewport({ width: 1920, height: 1080 })
await page.emulateMediaFeatures([{
name: 'prefers-color-scheme',
value: theme
}])
// Go to the URL and wait for the page to load
await page.goto(url, { waitUntil: 'domcontentloaded' })
// Return the screenshot as response
setHeader(event, 'content-type', 'image/jpeg')
return page.screenshot()
})
在应用程序端,我们可以创建一个简单的表单来调用我们的 API 端点
<script setup>
const url = ref('https://hub.nuxtjs.org.cn')
const image = ref('')
const theme = ref('light')
const loading = ref(false)
async function capture {
if (loading.value) return
loading.value = true
const blob = await $fetch('/api/browser/capture', {
query: {
url: url.value,
theme: theme.value
}
})
image.value = URL.createObjectURL(blob)
loading.value = false
}
</script>
<template>
<form @submit.prevent="capture">
<input v-model="url" type="url" />
<select v-model="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<button type="submit" :disabled="loading">
{{ loading ? 'Capturing...' : 'Capture' }}
</button>
<img v-if="image && !loading" :src="image" style="aspect-ratio: 16/9;" />
</form>
</template>
就是这样!你现在可以使用 Puppeteer 在你的 Nuxt 应用中截取网站的屏幕截图。
存储屏幕截图
你可以在 Blob 存储中存储屏幕截图
const screenshot = await page.screenshot()
// Upload the screenshot to the Blob storage
const filename = `screenshots/${url.value.replace(/[^a-zA-Z0-9]/g, '-')}.jpg`
const blob = await hubBlob().put(filename, screenshot)
元数据提取
另一个常见的用例是从网站提取元数据。
import { z } from 'zod'
export default eventHandler(async (event) => {
// Get the URL from the query parameters
const { url } = await getValidatedQuery(event, z.object({
url: z.string().url()
}).parse)
// Get a browser instance and navigate to the url
const { page } = await hubBrowser()
await page.goto(url, { waitUntil: 'networkidle0' })
// Extract metadata from the page
const metadata = await page.evaluate(() => {
const getMetaContent = (name) => {
const element = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`)
return element ? element.getAttribute('content') : null
}
return {
title: document.title,
description: getMetaContent('description') || getMetaContent('og:description'),
favicon: document.querySelector('link[rel="shortcut icon"]')?.href
|| document.querySelector('link[rel="icon"]')?.href,
ogImage: getMetaContent('og:image'),
origin: document.location.origin
}
})
return metadata
})
访问 /api/metadata?url=https://cloudflare.com
将返回网站的元数据
{
"title": "Connect, Protect and Build Everywhere | Cloudflare",
"description": "Make employees, applications and networks faster and more secure everywhere, while reducing complexity and cost.",
"favicon": "https://www.cloudflare.com/favicon.ico",
"ogImage": "https://cf-assets.www.cloudflare.com/slt3lc6tev37/2FNnxFZOBEha1W2MhF44EN/e9438de558c983ccce8129ddc20e1b8b/CF_MetaImage_1200x628.png",
"origin": "https://www.cloudflare.com"
}
要存储网站的元数据,你可以使用 键值存储。
或者直接利用此 API 路由上的 缓存
export default cachedEventHandler(async (event) => {
// ...
}, {
maxAge: 60 * 60 * 24 * 7, // 1 week
swr: true,
// Use the URL as key to invalidate the cache when the URL changes
// We use btoa to transform the URL to a base64 string
getKey: (event) => btoa(getQuery(event).url),
})
PDF 生成
你也可以使用 hubBrowser()
生成 PDF,这在你想生成发票或收据时很有用。
让我们创建一个 /_invoice
页面,使用 Vue 作为模板来生成 PDF
<script setup>
definePageMeta({
layout: 'blank'
})
// TODO: Fetch data from API instead of hardcoding
const currentDate = ref(new Date().toLocaleDateString())
const items = ref([
{ name: 'Item 1', quantity: 2, price: 10.00 },
{ name: 'Item 2', quantity: 1, price: 15.00 },
{ name: 'Item 3', quantity: 3, price: 7.50 }
])
const total = computed(() => {
return items.value.reduce((sum, item) => sum + item.quantity * item.price, 0)
})
</script>
<template>
<div style="font-family: Arial, sans-serif; padding: 20px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
<div>
<p><strong>Invoice To:</strong></p>
<p>John Doe</p>
<p>123 Main St</p>
<p>Anytown, USA 12345</p>
</div>
<div>
<p><strong>Invoice Number:</strong> INV-001</p>
<p><strong>Date:</strong> {{ currentDate }}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in items" :key="index">
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>${{ item.price.toFixed(2) }}</td>
<td>${{ (item.quantity * item.price).toFixed(2) }}</td>
</tr>
</tbody>
</table>
<div style="text-align: right; margin-top: 20px;">
<p><strong>Total: ${{ total.toFixed(2) }}</strong></p>
</div>
</div>
</template>
<style scoped>
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
为了避免出现任何样式问题,我们建议你的 app.vue
保持尽可能简洁
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
并将大多数头部管理、样式和 HTML 结构移至 layouts/default.vue
中。
最后,我们需要创建一个 layouts/blank.vue
,以避免在我们的 _invoice
页面上出现任何布局
<template>
<slot />
</template>
这将确保不会渲染任何头部、页脚或任何其他布局元素。
现在,让我们创建服务器路由来生成 PDF
export default eventHandler(async (event) => {
const { page } = await hubBrowser()
await page.goto(`${getRequestURL(event).origin}/_invoice`)
setHeader(event, 'Content-Type', 'application/pdf')
return page.pdf({ format: 'A4' })
})
你现在可以在你的页面中显示下载或打开 PDF 的链接
<template>
<a href="/invoice.pdf" download>Download PDF</a>
<a href="/invoice.pdf">Open PDF</a>
</template>