A Simple Pre-Rendered Web App Using Vue + Nuxt

Michael Lynch

使用 Vue + Nuxt 构建简单的预渲染 Web 应用

原文由 Michael Lynch 发布,订阅该博客

在这篇文章中,我将向你展示如何使用 Vue 和 Nuxt 预渲染页面。这种方式既能享受 Vue 带来的便捷开发体验,又不会牺牲社交分享或搜索引擎优化等关键功能。

本教程假设你没有任何 Vue 或 Nuxt 经验,接下来的内容我会一步步讲清楚。

Vue 存在的问题

与 Angular 和 React 一样,Vue 是一个用于构建单页应用(SPA)的框架。传统网站每当用户点击站内链接时,浏览器都要重新下载整个页面;而单页应用则把所有内容都放在同一个页面上。当用户在站内跳转时,JavaScript 只需重新绘制页面,而无需从服务器重新拉取所有内容。这样省去了用户浏览器与 Web 服务器之间缓慢的网络请求,带来了快速、流畅的用户体验。

Vue 这种响应速度的代价是,你对页面初始 HTML 的控制变少了。当浏览器从服务器获取 SPA 时,收到的 HTML 大致如下:

<html>
<head>
  <title>My Awesome Website</title>
</head>
<body>
  <div id="app"></div>
  <!-- app.js populates the rest of the page after the browser executes the script. -->
  <script type="text/javascript" src="app.js">
</body>
</html>

因为是单页应用,你网站上所有页面的 HTML 存根都是相同的。换句话说,无论用户访问 yoursite.com/about 还是 yoursite.com/contact,服务器返回的都是同一个 HTML 存根。JavaScript 会在浏览器中执行后,负责识别路径并绘制出对应的页面。

动态渲染页面是一项让站点导航更快的好创新,但当你把网站接入社交网络或搜索引擎时,它就会带来问题。

SPA 问题一:社交分享

我在 Twitter 上分享博客文章时,效果是这样的:

富媒体 Twitter 卡片示例

使用 Open Graph 标签,让 Twitter 为我的文章生成富媒体卡片。

Twitter 会根据页面中遵循 Open Graph 标准的 HTML 标签来生成这种卡片。例如,要指定卡片中的图片,我会添加这样一个标签:

<meta property="og:image" content="https://mtlynch.io/post-42/cover.jpg" />

如果你的网站是 SPA,那么所有页面共享同一个 HTML 骨架,也就共享同一套 Open Graph 标签。而 Twitter、Facebook 等主流社交网络都要求 Open Graph 标签在任何 JavaScript 执行之前就已存在。结果就是,你无法为网站上的不同页面创建各自独立的 Twitter 卡片或 Facebook 卡片。

SPA 问题二:搜索引擎优化(SEO)

与社交网站不同,搜索引擎确实会用 JavaScript 来渲染网站。问题在于它们并不能完美地做到这一点

许多网站会用 JavaScript 在用户浏览时持续更新页面内容。对 Google 来说,页面何时才算“渲染完成”并可以收录呢?对于普通的 SPA,Google 会尝试为页面建立索引,但你无法保证它能正确收录。

Nuxt 来救场

在现代 Web 上,社交网络和 SEO 都相当重要,如果使用 Vue 就意味着应用无法与这些服务充分集成,那会非常令人沮丧。

Nuxt.js 标志

Nuxt 正是解决这一问题的框架。它在 Vue 之上增加了一层,把原本由浏览器承担的部分工作移回服务端。Nuxt 不再是先下发一个空的 HTML 存根、再等待客户端 JavaScript 渲染全部内容,而是在服务端预先处理页面,生成渲染更完整的 HTML。

服务端渲染的问题

大多数人是在 Web 服务器上运行 Nuxt,这被称为“服务端渲染”。当用户向服务器请求页面时,Nuxt 会在服务端即时构建页面,然后再发送到用户浏览器。

服务端渲染降低了应用的首次加载时间,因为服务器分担了部分浏览器的工作。但如果你只是想为社交分享和 SEO 填充几个 HTML 标签,却要为此引入 Nuxt 和一整套 Node.js 服务端技术栈,那就有些小题大做了。

SPA 最大的优势之一就是它只是静态的 HTML、CSS 和 JavaScript,根本不需要应用服务器。像 Google Cloud Storage 和 Amazon S3 这样的简单文件托管服务就能托管标准的 SPA。而一旦使用服务端渲染,你就必须从静态文件托管升级到完整的应用服务器,成本更高,也更复杂。

幸运的是,服务端渲染还有一种替代方案:预渲染。Nuxt 不必在收到 HTTP 请求时按需渲染页面,而是提前把网站上的每个页面都渲染好。这个过程会生成静态文件,因此你仍然可以像托管标准 SPA 那样,把应用托管在任何支持静态文件的地方。

该不该使用预渲染?

预渲染并非适合所有场景。你需要根据应用的实际需求来决定,到底该用预渲染、服务端渲染,还是继续使用普通的 Vue。下面我列出了一些优缺点,帮你判断何时适合采用预渲染。

预渲染的优点

  • 可以为网站上的每个页面设置独立的社交分享卡片
  • 提升页面加载速度
    • 标准的 SPA 必须等浏览器下载并执行完 JavaScript 后才开始渲染页面。而使用预渲染,用户在浏览器执行任何 JavaScript 之前就能看到页面内容。

预渲染的缺点

  • 比标准 Vue 更复杂
    • 虽然预渲染比在 Node 服务器上运行 Nuxt 简单,但比完全在客户端运行的 Vue 应用要复杂。
    • 使用预渲染时,你需要在脑中区分代码是在服务端还是客户端执行,以及在代码执行时有哪些上下文可用。
  • 不支持用户生成页面
    • 预渲染要求在构建时就知道所有页面路由。它明确不支持动态路由
    • 如果你的网站包含用户生成的内容,并且希望用户注册后拥有自己的 URL(例如 yoursite.com/users/michael123),预渲染就无法实现。
    • 你可以通过把部分路由放到 URL 查询参数中来变通(例如 yoursite.com/users?id=michael123),然后在客户端拉取动态数据,但你仍然无法为这些页面生成独立的社交分享标签。

预渲染的 “Hello, world”

为了演示预渲染,我将用仅仅三个文件展示一个最基本的预渲染 “Hello, world!” 应用。

唯一的先决条件是 Node.js。我使用的是 Node v12.13.1,这是撰写本文时的最新稳定版本。

pages/index.vue

第一个文件定义了 Web 应用中的一个页面。pages/ 目录对 Nuxt 有特殊含义。Nuxt 会为在 pages/ 目录中找到的每个 .vue 文件预渲染出单独的页面。文件名 index.vue 表示这是一个根页面,也就是用户未指定任何路径时看到的页面。

index.vue 会生成一个简单的 “Hello, world!” 页面,显示一句欢迎语和一个按钮。为了展示 Vue 的部分客户端功能,按钮会在用户每次点击时更新文本。

<template>
  <div>
    <h1>Hello, world!</h1>
    <p>I'm an example of a pre-rendered Vue webpage.</p>
    <button v-on:click="count++">I have been clicked {{ count }} times</button>
  </div>
</template>

<script>
  export default {
    data: function () {
      return {
        count: 0,
      };
    },
  };
</script>

package.json

package.json 文件告诉 Node.js 如何构建这个应用:

{
  "name": "hello-world-vue-pre-rendered",
  "dependencies": {
    "nuxt": "latest"
  },
  "scripts": {
    "dev": "nuxt --port 3600",
    "generate": "nuxt generate"
  }
}

nuxt.config.js

最后,Nuxt 需要一个配置文件,即使它是空的:

// Even though we have no Nuxt settings, this file is required.

运行 “Hello, world”

你可以在 Codesandbox 上运行这个应用:

或者,你也可以在本地机器上通过以下命令运行应用:

git clone https://github.com/mtlynch/hello-world-vue-pre-rendered.git
cd hello-world-vue-pre-rendered
git checkout step-1

npm install
npm run dev

应用将在 http://localhost:3600 上运行。

预渲染你的应用

当你运行 npm run dev 时,你实际上是在使用服务端渲染。Node 会启动一个本地开发服务器,并在你请求时按需生成页面。

但我之前承诺过的是预渲染页面。使用预渲染页面时,你甚至不需要 Web 服务器,因为它就是一组静态文件。

要预渲染你的应用,请运行以下命令:

npm run generate

如果你查看 dist/ 目录,会看到 Nuxt 已经为你预渲染好了页面:

$ find ./dist/ -type f
./dist/.nojekyll
./dist/200.html
./dist/index.html
./dist/_nuxt/7cef7880379068a94897.js
./dist/_nuxt/b10d0692e6306468ee9f.js
./dist/_nuxt/cae55ee8b1125819f113.js
./dist/_nuxt/ee10340617a3beab9da2.js
./dist/_nuxt/LICENSES

你可以通过一个简单的 HTTP 服务器来查看这些文件,例如 Python 2 的 SimpleHTTPServer:

cd dist
python -m SimpleHTTPServer 8123

Python 随后会启动一个 Web 服务器,让你可以在 http://localhost:8123 查看预渲染好的应用。稍后我会向你展示如何将这个应用发布到静态文件托管服务上。

添加 About 页面

为了让例子更有趣,我再为应用添加第二个页面。

pages/about.vue

这个页面使用 Vue 钩子来显示页面是如何渲染的。我会在下文更详细地解释这段代码。

<template>
  <div>
    <h1>About this Build</h1>
    <p v-if="buildTime">
      Nuxt pre-rendered this page at
      <b>{{ buildTime }}</b> (before the browser ever saw it).
    </p>
    <template v-else>
      <p>
        Vue generated this page client-side because you navigated here from
        another route on the same site.
      </p>
      <p>
        <a href="/about">Refresh the page</a> to see the pre-rendered version.
      </p>
    </template>
    <p>
      The browser loaded this page at
      <b>{{ loadTime }}</b>.
    </p>
    <p><nuxt-link to="/">Home</nuxt-link></p>
  </div>
</template>

<script>
  export default {
    asyncData() {
      // Don't re-evaluate buildTime when the client loads this page in the
      // browser.
      if (!process.client) {
        return {
          buildTime: new Date().toUTCString(),
        };
      }
    },
    // Vue evaluates data variables at page render time and again every time the
    // browser loads this page.
    data: function () {
      return {
        loadTime: new Date().toUTCString(),
      };
    },
  };
</script>

这是 About 页面的在线版本

理解 About 页面的两个版本

About 页面展示了 Nuxt 和 Vue 如何协同工作来创建预渲染页面。根据你在站内的导航方式,你会看到两个不同版本的页面。

About 页面不同版本的截图

About 页面会根据你进入页面的方式显示不同的信息。

如果你直接打开 /about 页面,应该会看到左侧的版本。如果你先打开 首页,再点击“about page”链接,则会看到右侧的版本。

为什么会看到两个不同版本的页面?答案就在 asyncData 钩子中。这个函数会在两个时机执行:

  1. (服务端)当 Nuxt 预渲染页面时
  2. (客户端)当浏览器从站内其他页面导航到该页面时

定义如下:

asyncData() {
  // Don't re-evaluate buildTime when the client loads this page in the
  // browser.
  if (!process.client) {
    return {
      buildTime: new Date().toUTCString(),
    };
  }
},

当 Nuxt 预渲染站点时,服务器会执行 asyncData 方法。在服务端环境中,process.client 为空,因此它会把 buildTime 设为当前时间,并用该变量来预渲染页面的 HTML。

当你从站内其他页面导航到 /about 路径时,浏览器会在页面加载时执行 asyncData 方法。此时 process.client 不为空,因为代码是在客户端运行,所以该方法不会定义 buildTime,Vue 会在 buildTime 未定义时渲染对应的页面模板:

<p v-if="buildTime">...</p>
<template v-else>
  <p>
    Vue generated this page client-side because you navigated here from another
    route on the same site.
  </p>
  <p><a href="/about">Refresh the page</a> to see the pre-rendered version.</p>
</template>

预渲染仅针对首个页面

About 页面展示了预渲染的一个微妙之处:Nuxt 只会预渲染用户访问的第一个页面。此后,Vue 会像普通的 SPA 那样,在用户于站内导航时在客户端重新绘制页面。这其实是好事,它意味着你的应用既保留了 Vue 瞬时页面切换的体验,又不会牺牲对需要服务端渲染的服务的兼容性。

在本地运行 About 页面

要体验 About 页面,请运行以下命令

git clone https://github.com/mtlynch/hello-world-vue-pre-rendered.git
cd hello-world-vue-pre-rendered

npm install
npm run dev

你会注意到,当你访问 https://localhost:3600/about 时,构建时间和加载时间大致相同。这是因为当你运行 npm run dev 时,Nuxt 使用服务端渲染来即时创建页面。

使用服务端渲染的 About 页面截图

npm run dev 会在用户请求时渲染页面,因此构建时间和加载时间是一致的。

与只生成一次并持续提供同一页面的预渲染不同,服务端渲染会在用户每次访问时都生成页面的全新版本。

发布你的应用

使用预渲染时,你不需要 Node.js 服务器来托管应用。你只需要一个支持静态文件托管的服务即可。

以下是几家主流服务商发布静态文件的说明:

源代码

本示例的所有代码均已在 GitHub 上以 MIT 许可证开源:

功能更丰富的示例

如果你要用 Vue 和 Nuxt 构建真实的应用,仅有两个预渲染页面肯定不够。我创建了一个模板项目 pre-vue,其中包含了 SEO 和社交分享所需的所有样板代码:

它具有以下功能:

  • 生成 robots.txt 文件
  • 生成站点地图
  • 为每个页面支持独立的 <title> 标签和其他与 SEO 相关的 <meta> 标签
  • 为每个页面添加独立的 Open Graph 标签
  • 添加 Google Analytics 支持
  • 添加 favicon
  • 处理 404

我使用 pre-vue 模板重写了 Zestful 演示站点,它之前是一个 Angular 单页应用。README 中说明了 pre-vue 的使用方法,如果大家感兴趣,我会再写一篇详细的博文来讲解细节。

本文章由 muse-spark-1.2-contributor 进行翻译

评论