MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Qwik 服务端渲染(SSR)实战:提升首屏加载速度

2022-03-317.2k 阅读

Qwik 服务端渲染(SSR)简介

Qwik 是一个新兴的前端框架,它致力于提供极致的性能体验,其中服务端渲染(SSR)是其提升首屏加载速度的重要手段之一。SSR 意味着在服务器端生成 HTML 页面,然后将其发送到客户端。这样,当用户首次访问页面时,浏览器可以直接呈现已经渲染好的内容,而无需等待客户端 JavaScript 加载并执行后才开始渲染。

在传统的客户端渲染(CSR)中,浏览器加载 HTML 文件,接着下载 JavaScript 代码,解析并执行 JavaScript 来获取数据,然后渲染页面。这个过程在网络较慢或者 JavaScript 代码量较大时,会导致首屏加载时间明显变长。而 SSR 提前在服务器端完成了数据获取和页面渲染,极大地减少了用户等待页面呈现的时间。

Qwik 中 SSR 的原理

Qwik 的 SSR 基于其独特的渲染模型。Qwik 采用了一种称为“岛屿架构”的设计理念。在 SSR 过程中,服务器端生成完整的 HTML 页面,其中包含了页面的初始状态和结构。这些页面中的某些部分(称为“岛屿”)可以是交互性的组件,它们在客户端激活时,只需要极小的 JavaScript 代码来增强交互功能。

Qwik 的 SSR 流程大致如下:

  1. 请求到达服务器:当客户端发起请求时,服务器接收到请求信息。
  2. 渲染页面:服务器根据请求的路由,在服务器端使用 Qwik 的渲染引擎渲染页面组件,获取数据并生成完整的 HTML 内容。
  3. 返回 HTML:服务器将生成的 HTML 发送回客户端。
  4. 客户端激活:客户端接收到 HTML 后,浏览器可以立即呈现页面内容。随后,Qwik 会根据需要激活页面中的“岛屿”组件,通过加载少量的 JavaScript 代码来添加交互功能。

实战准备

  1. 环境搭建
    • 确保你已经安装了 Node.js 和 npm。Node.js 是运行 Qwik 应用的基础,npm 用于管理项目依赖。
    • 创建一个新的 Qwik 项目。可以使用 Qwik CLI 来快速初始化项目。在命令行中执行以下命令:
npm create qwik@latest my - qwik - app
cd my - qwik - app

这将创建一个名为 my - qwik - app 的新 Qwik 项目,并进入项目目录。 2. 项目结构介绍 - 初始化后的 Qwik 项目结构如下:

my - qwik - app
├── node_modules
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── components
│   │   └── Layout.tsx
│   ├── entry.client.tsx
│   ├── entry.server.tsx
│   ├── routes
│   │   ├── index.tsx
│   │   └── about.tsx
│   └── styles.css
├── package.json
├── tsconfig.json
└── vite.config.ts
- `public` 目录包含静态资源,如 `index.html` 是应用的入口 HTML 文件。
- `src` 目录是项目的源代码目录。`components` 目录用于存放通用组件,`routes` 目录存放路由相关的页面组件。`entry.client.tsx` 是客户端入口文件,`entry.server.tsx` 是服务器端入口文件。
- `package.json` 管理项目的依赖和脚本。`tsconfig.json` 是 TypeScript 的配置文件,`vite.config.ts` 是 Vite(Qwik 使用 Vite 作为构建工具)的配置文件。

创建 SSR 页面

  1. 在路由中创建页面
    • 我们在 src/routes 目录下创建一个新的页面,例如 contact.tsx。这个页面将展示联系信息。
import { component$, useStore } from '@builder.io/qwik';
import Layout from '~/components/Layout';

export const Contact = component$(() => {
    const store = useStore({
        contactInfo: 'You can reach us at contact@example.com'
    });

    return (
        <Layout>
            <h1>Contact Us</h1>
            <p>{store.contactInfo}</p>
        </Layout>
    );
});
- 在上述代码中,我们使用 `component$` 定义了一个 Qwik 组件 `Contact`。通过 `useStore` 我们创建了一个简单的状态对象 `store`,包含联系信息。组件返回一个包含标题和联系信息的页面,并且使用了 `Layout` 组件来包裹页面内容。

2. 配置路由 - Qwik 使用约定式路由。在 src/routes 目录下创建的文件会自动成为路由。在 src/routes/index.tsx 中添加导航链接到新创建的 contact 页面:

import { component$, useNavigate } from '@builder.io/qwik';
import Layout from '~/components/Layout';

export const Index = component$(() => {
    const navigate = useNavigate();

    return (
        <Layout>
            <h1>Home Page</h1>
            <button onClick={() => navigate('/contact')}>Go to Contact</button>
        </Layout>
    );
});
- 这里我们引入了 `useNavigate` 来实现页面导航功能。当用户点击按钮时,会导航到 `/contact` 路由对应的页面。

数据获取与 SSR

  1. 模拟数据获取
    • 假设我们需要从服务器获取一些联系信息,而不是使用硬编码的数据。我们可以在服务器端进行数据获取。首先,创建一个简单的 API 模拟数据获取。在项目根目录下创建 api 目录,然后在其中创建 contact.js 文件:
const express = require('express');
const app = express();
const port = 3001;

app.get('/contact', (req, res) => {
    const contactData = {
        message: 'You can reach us at contact@example.com'
    };
    res.json(contactData);
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
- 上述代码使用 Express 框架创建了一个简单的 API 服务器,在 `/contact` 端点返回联系信息。

2. 在 Qwik 页面中获取数据 - 修改 src/routes/contact.tsx 文件,在服务器端获取数据并渲染到页面上:

import { component$, useStore } from '@builder.io/qwik';
import Layout from '~/components/Layout';
import { useLoader } from '@builder.io/qwik-city';

export const Contact = component$(() => {
    const store = useStore({
        contactInfo: ''
    });

    useLoader(async () => {
        const response = await fetch('http://localhost:3001/contact');
        const data = await response.json();
        store.contactInfo = data.message;
    });

    return (
        <Layout>
            <h1>Contact Us</h1>
            <p>{store.contactInfo}</p>
        </Layout>
    );
});
- 在这个版本中,我们引入了 `useLoader` 钩子。`useLoader` 会在服务器端渲染时执行其内部的异步函数。我们通过 `fetch` 从本地 API 获取数据,并更新 `store` 中的 `contactInfo`。这样,在服务器端渲染时,页面就会包含从 API 获取的实际联系信息。

优化 SSR 性能

  1. 代码拆分
    • Qwik 支持代码拆分,这对于 SSR 性能优化非常重要。通过代码拆分,我们可以将 JavaScript 代码按需加载,而不是一次性加载所有代码。在 Qwik 中,Vite 会自动进行代码拆分。例如,我们可以将一些不常用的组件或者功能模块进行拆分。假设我们有一个复杂的图表组件,只有在特定页面才需要。我们可以将其定义在一个单独的文件中:
// src/components/Chart.tsx
import { component$ } from '@builder.io/qwik';

export const Chart = component$(() => {
    return (
        <div>
            <p>This is a simple chart component</p>
        </div>
    );
});
- 然后在需要使用的页面中动态导入:
// src/routes/specialPage.tsx
import { component$, useStore } from '@builder.io/qwik';
import Layout from '~/components/Layout';

export const SpecialPage = component$(() => {
    const loadChart = async () => {
        const { Chart } = await import('~/components/Chart.tsx');
        return <Chart />;
    };

    return (
        <Layout>
            <h1>Special Page</h1>
            {loadChart()}
        </Layout>
    );
});
- 这样,只有当用户访问 `specialPage` 时,`Chart` 组件的代码才会被加载,减少了初始 JavaScript 包的大小,从而提升了 SSR 的性能。

2. 缓存策略 - 在 SSR 过程中,缓存数据可以显著提高性能。如果某些数据不经常变化,我们可以在服务器端缓存这些数据。例如,对于联系信息这种可能不经常变动的数据,我们可以使用内存缓存。修改 api/contact.js 文件,添加缓存功能:

const express = require('express');
const app = express();
const port = 3001;
let cachedContactData;

const getContactData = async () => {
    if (cachedContactData) {
        return cachedContactData;
    }
    const contactData = {
        message: 'You can reach us at contact@example.com'
    };
    cachedContactData = contactData;
    return contactData;
};

app.get('/contact', async (req, res) => {
    const data = await getContactData();
    res.json(data);
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
- 这里我们定义了一个 `cachedContactData` 变量来缓存联系数据。每次请求 `/contact` 时,先检查缓存中是否有数据,如果有则直接返回缓存数据,否则获取新数据并缓存。这样可以减少数据获取的时间,提高 SSR 的速度。

部署 SSR 应用

  1. 构建项目
    • 在部署之前,我们需要先构建项目。在项目根目录下执行以下命令:
npm run build
- 这会使用 Vite 构建项目,生成用于生产环境的优化代码。构建后的文件位于 `dist` 目录下。

2. 部署到服务器 - 对于 SSR 应用,我们需要将构建后的文件部署到服务器上。可以选择多种服务器,如 Node.js 服务器、云服务提供商(如 Vercel、Netlify 等)。 - 使用 Node.js 服务器部署: - 我们可以创建一个简单的 Node.js 服务器来部署 Qwik SSR 应用。在项目根目录下创建 server.js 文件:

const express = require('express');
const { createQwikCity } = require('@builder.io/qwik-city/express');
const app = express();
const { distDir } = require('./vite.config.ts');

app.use(express.static(distDir));
app.use(createQwikCity());

const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
    - 上述代码使用 Express 框架,将构建后的静态文件目录设置为静态资源目录,并使用 `createQwikCity` 中间件来处理 Qwik SSR 请求。然后启动服务器,监听指定端口。
- **使用 Vercel 部署**:
    - 确保你已经安装了 Vercel CLI。在项目根目录下执行 `vercel login` 登录到你的 Vercel 账户。
    - 然后执行 `vercel` 命令,Vercel 会自动检测项目类型为 Qwik,并进行部署。Vercel 会优化 SSR 应用的部署,提供良好的性能和可扩展性。

处理 SSR 中的 SEO

  1. 设置元数据
    • 对于 SEO 来说,设置正确的元数据非常重要。在 Qwik 中,我们可以在页面组件中设置元数据。例如,在 src/routes/about.tsx 页面中设置页面标题和描述:
import { component$, useMeta } from '@builder.io/qwik';
import Layout from '~/components/Layout';

export const About = component$(() => {
    useMeta({
        title: 'About Us - My Qwik App',
        description: 'Learn more about our company and mission'
    });

    return (
        <Layout>
            <h1>About Us</h1>
            <p>Here is some information about our company...</p>
        </Layout>
    );
});
- 通过 `useMeta` 钩子,我们可以设置页面的 `title` 和 `description` 等元数据。这些元数据会在服务器端渲染时添加到 HTML 页面的 `<head>` 标签中,有助于搜索引擎理解页面内容。

2. 优化 URL 结构 - Qwik 的约定式路由已经提供了一个简洁的 URL 结构。但是我们还可以进一步优化,确保 URL 能够准确反映页面内容并且易于理解。例如,将 src/routes/contact.tsx 对应的路由设置为 /contact - us 而不是 /contact,可以让用户和搜索引擎更清楚页面的主题。在 Qwik 中,可以通过修改文件名来实现类似效果,因为 Qwik 基于文件系统的路由约定。如果将 contact.tsx 重命名为 contact - us.tsx,对应的路由就会变为 /contact - us

处理 SSR 中的错误

  1. 错误处理在服务器端
    • 在 SSR 过程中,可能会遇到各种错误,如数据获取失败、组件渲染错误等。我们需要在服务器端进行适当的错误处理。在 entry.server.tsx 文件中添加全局错误处理:
import { renderToString } from '@builder.io/qwik/server';
import { createResponse } from '@builder.io/qwik-city';
import type { RequestEvent } from '@builder.io/qwik-city';
import type { PageProps } from './types';
import { Root } from './root';

export default async function handleRequest(
    event: RequestEvent<PageProps>
): Promise<Response> {
    try {
        const html = await renderToString(<Root {...event.data } />);
        return createResponse(html, event);
    } catch (error) {
        console.error('SSR error:', error);
        const errorMessage = 'An error occurred during server - side rendering';
        return createResponse(errorMessage, event, { status: 500 });
    }
}
- 在上述代码中,我们在 `try - catch` 块中包裹了 `renderToString` 操作。如果在渲染过程中发生错误,会捕获错误并记录到控制台,同时返回一个带有错误信息和 500 状态码的响应。

2. 客户端错误处理 - 客户端也可能会遇到错误,例如激活“岛屿”组件时的错误。Qwik 提供了一些机制来处理客户端错误。在组件内部,可以使用 try - catch 块来捕获错误。例如,在一个交互性组件中:

import { component$ } from '@builder.io/qwik';

export const InteractiveComponent = component$(() => {
    try {
        // Some code that might throw an error
        const result = 1 / 0;
        return <p>{result}</p>;
    } catch (error) {
        return <p>An error occurred: {error.message}</p>;
    }
});
- 这里我们模拟了一个可能会抛出错误的操作(除以零),并在 `catch` 块中处理错误,向用户显示友好的错误信息。

通过以上对 Qwik 服务端渲染(SSR)的实战介绍,我们详细了解了如何在 Qwik 项目中实现 SSR,从环境搭建、页面创建、数据获取到性能优化、部署以及 SEO 和错误处理等方面。通过合理运用这些技术,我们能够显著提升首屏加载速度,为用户提供更好的体验。在实际项目中,可以根据具体需求进一步优化和扩展这些技术,充分发挥 Qwik SSR 的优势。