使用 Nuxt.js 进行路由和路由保护

Avatar of Chris Nwamba
Chris Nwamba

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

本教程假设您具备 Vue 的基础知识。 如果您以前从未使用过它,则可能需要查看 CSS-Tricks 指南 中的入门指南。

您可能尝试过在服务器上渲染使用 Vue 构建的应用程序。 服务器端渲染 (SSR) 的概念和实现细节对于初学者和经验丰富的开发人员来说都具有挑战性。 当您需要执行数据获取、路由和保护已认证路由等操作时,这些挑战会变得更加艰巨。 本文将引导您了解如何使用 Nuxt.js 克服这些挑战。

您将学到什么

标题可能限制了本文的范围,因为您将学到不仅仅是路由和路由保护的内容。 以下是本文涵盖内容的摘要列表

  • 为什么进行服务器端渲染?
  • 服务器端渲染和 SEO
  • 从头开始设置 Nuxt.js 项目
  • 自定义布局
  • Webpack 和静态全局资产
  • 隐式路由和自动代码分割
  • 嵌套和参数化路由
  • 使用中间件保护路由

您可以从 Github 获取代码示例。

我为什么要渲染到服务器?

如果您已经知道为什么要进行服务器端渲染,并且只想了解路由或路由保护,则可以跳到 从头开始设置 Nuxt.js 应用程序 部分。

SSR,也称为通用渲染或同构渲染,是最近从 JavaScript 生态系统中产生的一个概念,旨在帮助缓解 JavaScript 框架的缺点。

当我们没有像 Angular、React 和 Vue 这样的 JS 框架或 UI 库时,构建网站的事实上的方法是从服务器发送一个 HTML(以及一些样式和 JS)字符串作为响应,然后由浏览器解析和渲染。 这意味着您的视图是服务器端渲染的。 页面渲染后,我们最多可以做的就是开始使用 JavaScript 或 jQuery 来操作其内容的繁琐工作。

使用这些模式构建交互式用户界面是一场噩梦。 除了您必须使用 JS 对 DOM 执行的工作量之外,您仍然需要执行戳 DOM、遍历 DOM 并强制内容和功能进入其中的繁琐工作。 更糟糕的是,这导致了许多糟糕的代码和性能不佳(缓慢)的 UI。

JavaScript 框架引入了一些概念,例如虚拟 DOM 和声明式 API,这使得使用 DOM 变得更快、更有趣。 它们的问题在于视图完全由 JavaScript 控制。 可以说它们是 JavaScript 渲染的。 含义是,与之前默认由服务器渲染视图的时代不同,JavaScript 是必需的,并且您必须等待它,然后用户才能看到任何内容。

以下是您应该从这段冗长的讨论中获得的收获

  1. 服务器渲染的应用程序速度更快,因为它们不依赖于 JavaScript 来开始使用内容绘制浏览器。
  2. JavaScript 渲染的应用程序更适合于提供更好的用户体验。 不幸的是,这仅在 JavaScript 已被解析和编译之后。

我们希望提高服务器渲染应用程序的首次绘制速度,并创造更好的 JS 渲染用户体验。 这就是 JavaScript 框架的 SSR 概念的由来。

SEO 问题

使用 Vue 构建应用程序时遇到的另一个大问题是如何使其对 SEO 友好。 目前,网络爬虫不会在 JavaScript 中查找要索引的内容。 它们只了解 HTML。 对于服务器端渲染的应用程序,情况并非如此,因为它们已经响应了爬虫所需的 HTML。

事情可能会出错,如下所示

上图显示了一个简单的前端应用程序,其中包含一些文本。 在其简单性中,检查页面源代码,您会失望地发现文本不在页面源代码中

用于服务器端渲染 Vue 应用的 Nuxt.js

Sarah Drasner 撰写了一篇关于 什么是 Nuxt.js 以及为什么要使用它 的精彩文章。 她还展示了使用此工具可以执行的一些惊人的操作,例如页面路由和页面转换。 Nuxt.js 是 Vue 生态系统中的一个工具,您可以使用它从头开始构建服务器端渲染的应用程序,而无需担心将 JavaScript 应用程序渲染到服务器的基本复杂性。

Nuxt.js 是 Vue 已提供内容 的一种选择。 它建立在 Vue SSR 和路由库的基础上,为您自己的应用程序提供了一个无缝的平台。 Nuxt.js 归根结底是一件事:简化您作为开发人员使用 Vue 构建 SSR 应用程序的体验。

我们已经谈了很多(俗话说,光说不练假把式);现在让我们动手实践吧。

从头开始设置 Nuxt.js 应用程序

您可以使用 Vue CLI 工具通过运行以下命令快速搭建一个新项目

vue init nuxt-community/starter-template <project-name>

但这并不是我们的目标,我们希望亲自动手实践。 通过这种方式,您将了解为 Nuxt 项目引擎提供支持的基本流程。

首先在您的计算机上创建一个空文件夹,打开您的终端指向此文件夹,然后运行以下命令以启动一个新的节点项目

npm init -y

# OR

yarn init -y

这将生成一个类似于以下内容的 package.json 文件

{
  "name": "nuxt-shop",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

name 属性与您正在使用的文件夹名称相同。

通过 npm 安装 Nuxt.js 库

npm install --save nuxt

# OR

yarn add nuxt

然后在 package.json 文件中配置一个 npm 脚本以启动 nuxt 构建过程

"scripts": {
  "dev": "nuxt"
}

然后,您可以通过运行您刚刚创建的命令来启动构建

npm run dev

# OR

yarn dev

构建失败是正常的。 这是因为 Nuxt.js 会在 pages 文件夹中查找内容,并将其提供给浏览器。 目前,此文件夹不存在

退出构建过程,然后在项目的根目录中创建 pages 文件夹,然后再次运行。 这次您的构建应该会成功

应用程序在端口 3000 上启动,但当您尝试访问它时会收到 404 错误

Nuxt.js 将页面路由映射到 pages 文件夹中的文件名。 这意味着,如果您的 pages 文件夹中有一个名为 index.vue 的文件和另一个名为 about.vue 的文件,它们将分别解析为 //about。 目前,/ 抛出 404 错误,因为 pages 文件夹中不存在 index.vue

使用以下简单代码段创建 index.vue 文件

<template>
  <h1>Greetings from Vue + Nuxt</h1>
</template>

现在,重新启动服务器,404 错误应该会被替换为显示问候消息的索引路由

项目范围的布局和资产

在深入研究路由之前,让我们花一些时间讨论如何构建项目,以便您拥有可重用的布局以及在所有页面上共享全局资产。 让我们从全局资产开始。 我们需要在项目中使用以下两个资产

  1. Favicon
  2. 基本样式

Nuxt.js 提供了两个根文件夹选项(取决于您正在执行的操作)来管理资产

  1. assets: 这里的文件将被 Webpack 打包(捆绑并通过 Webpack 转换)。 诸如 CSS、全局 JS、LESS、SASS、图像等文件应该放在这里。
  2. static: 这里的文件不会经过 Webpack。 它们按原样提供给浏览器。 对于 robot.txt、favicon、Github CNAME 文件等来说很有意义。

在我们的例子中,favicon 属于 static 文件夹,而基础样式则位于 assets 文件夹。因此,创建这两个文件夹并在 /assets/css/base.css 中添加 base.css 文件。同时,下载这个 favicon 文件 并将其放入 static 文件夹。我们需要 normalize.css,但我们可以通过 npm 安装它,而不是将其放在 assets 文件夹中。

yarn add normalize.css

最后,在配置文件中告诉 Nuxt.js 所有这些资源。此配置文件应位于项目的根目录,命名为 nuxt.config.js

module.exports = {
  head: {
    titleTemplate: '%s - Nuxt Shop',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt online shop' }
    ],
    link: [
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/css?family=Raleway'
      },
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  css: ['normalize.css', '@/assets/css/base.css']
};

我们刚刚定义了标题模板、页面元信息、字体、favicon 和所有样式。Nuxt.js 会自动将它们全部包含在页面头部。

将此代码添加到 base.css 文件中,让我们看看是否一切按预期工作。

html, body, #__nuxt {
  height: 100%;
}

html {
  font-size: 62.5%;
}

body {
  font-size: 1.5em;
  line-height: 1.6;
  font-weight: 400;
  font-family: 'Raleway', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  color: #222;
}

您应该会看到问候语的字体已更改为反映 CSS 样式。

现在我们可以讨论布局了。Nuxt.js 已经有一个默认布局,您可以自定义它。在根目录下创建一个 layouts 文件夹,并在其中添加一个名为 default.vue 的文件,内容如下所示。

<template>
  <div class="main">
    <app-nav></app-nav>
    <!-- Mount the page content here -->
    <nuxt/>
    
  </div>
</template>
<style>
/* You can get the component styles from the Github repository for this demo */
</style>

<script>
import nav from '@/components/nav';
export default {
  components: {
    'app-nav': nav
  }
};
</script>

我省略了 style 标签中的所有样式,但您可以从代码仓库中获取它们。为了简洁起见,我省略了它们。

布局文件也是一个组件,但它包裹了 nuxt 组件。此文件中的所有内容在所有其他页面之间共享,而每个页面的内容都替换了 nuxt 组件。说到共享内容,文件中的 app-nav 组件应该显示一个简单的导航。

通过创建 components 文件夹并在其中添加 nav.vue 文件来添加 nav 组件。

<template>
  <nav>
    <div class="logo">
      <app-h1 is-brand="true">Nuxt Shop</app-h1>
    </div>
    <div class="menu">
      <ul>
        <li>
           <nuxt-link to="/">Home</nuxt-link>
        </li>
        <li>
           <nuxt-link to="/about">About</nuxt-link>
        </li>
      </ul>
    </div>
  </nav>
</template>
<style>
/* You can get the component styles from the Github repository for this demo */
</style>
<script>
import h1 from './h1';
export default {
  components: {
    'app-h1': h1
  }
}
</script>

该组件显示品牌文本和两个链接。请注意,为了让 Nuxt 正确处理路由,我们没有使用 <a> 标签,而是使用了 <nuxt-link> 组件。品牌文本使用可重用的 <h1> 组件呈现,该组件包装并扩展了 <h1> 标签。此组件位于 components/h1.vue 中。

<template>
  <h1 :class="{brand: isBrand}">
    <slot></slot>
  </h1>
</template>
<style>
/* You can get the component styles 
from the Github repository for this demo
*/
</style>
<script>
export default {
  props: ['isBrand']
}
</script>

这是添加了布局和这些组件的首页输出。

当您检查输出时,您应该会看到内容已渲染到服务器。

隐式路由和自动代码分割

如前所述,Nuxt.js 使用其文件系统生成路由。pages 目录中的所有文件都映射到服务器上的 URL。因此,如果我有这种目录结构

pages/
--| product/
-----| index.vue
-----| new.vue
--| index.vue
--| about.vue

…那么我将自动获得一个具有以下结构的 Vue 路由对象。

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'about',
      path: '/about',
      component: 'pages/about.vue'
    },
    {
      name: 'product',
      path: '/product',
      component: 'pages/product/index.vue'
    },
    {
      name: 'product-new',
      path: '/product/new',
      component: 'pages/product/new.vue'
    }
  ]
}

这就是我更喜欢称之为“隐式路由”的原因。

另一方面,这些页面中的每一个都没有捆绑在一个
bundle.js 文件中。这将是使用 webpack 时预期的结果。在普通的 Vue 项目中,这就是我们得到的结果,我们会手动将每个路由的代码拆分为自己的文件。使用 Nuxt.js,您可以开箱即用地获得此功能,它被称为自动代码分割。

当您在 pages 文件夹中添加另一个文件时,您可以看到整个过程。将此文件命名为 about.vue,内容如下所示。

<template>
  <div>
    <app-h1>About our Shop</app-h1>
    <p class="about">Lorem ipsum dolor sit amet consectetur adipisicing ...</p>
    <p class="about">Lorem ipsum dolor sit amet consectetur adipisicing ...</p>
    <p class="about">Lorem ipsum dolor sit amet consectetur adipisicing ...</p>
    <p class="about">Lorem ipsum dolor sit amet consectetur adipisicing ...</p>
    ...
  </div>
</template>
<style>
...
</style>
<script>
import h1 from '@/components/h1';
export default {
  components: {
    'app-h1': h1
  }
};
</script>

现在,点击导航栏中的“关于”链接,它应该会带您到 /about 页面,页面内容如下所示。

查看 DevTools 中的网络选项卡,您会发现没有加载 pages/index.[hash].js 文件,而是加载了 pages/about.[hash].js 文件。

您应该从这里得出一点:路由 === 页面。因此,您可以在服务器端渲染的世界中自由地互换使用它们。

数据获取

这就是游戏发生变化的地方。在普通的 Vue 应用中,我们通常会等待组件加载,然后在 created 生命周期方法中发出 HTTP 请求。不幸的是,当您也渲染到服务器时,服务器的准备时间远远早于组件的准备时间。因此,如果您坚持使用 created 方法,则无法将获取的数据渲染到服务器,因为已经太晚了。

出于这个原因,Nuxt.js 公开了另一个类似于 created 的实例方法,称为 asyncData。此方法可以访问两个上下文:客户端和服务器。因此,当您在此方法中发出请求并返回数据有效负载时,有效负载会自动附加到 Vue 实例。

让我们看一个例子。在根目录下创建一个 services 文件夹,并在其中添加一个 data.js 文件。我们将通过从此文件中请求数据来模拟数据获取。

export default [
  {
    id: 1,
    price: 4,
    title: 'Drinks',
    imgUrl: 'http://res.cloudinary.com/christekh/image/upload/v1515183358/pro3_tqlsyl.png'
  },
  {
    id: 2,
    price: 3,
    title: 'Home',
    imgUrl: 'http://res.cloudinary.com/christekh/image/upload/v1515183358/pro2_gpa4su.png'
  },
  // Truncated for brevity. See repo for full code.
]

接下来,更新首页以使用此文件。

<template>
  <div>
    <app-banner></app-banner>
    <div class="cta">
      <app-button>Start Shopping</app-button>
    </div>
    <app-product-list :products="products"></app-product-list>
  </div>
</template>
<style>
...
</style>
<script>
import h1 from '@/components/h1';
import banner from '@/components/banner';
import button from '@/components/button';
import productList from '@/components/product-list';
import data from '@/services/data';
export default {
  asyncData(ctx, callback) {
    setTimeout(() => {
      callback(null, { products: data });
    }, 2000);
  },
  components: {
    'app-h1': h1,
    'app-banner': banner,
    'app-button': button,
    'app-product-list': productList
  }
};
</script>

暂时忽略导入的组件,专注于 asyncData 方法。我正在使用 setTimeout 模拟异步操作,并在两秒后获取数据。回调方法使用您希望公开给组件的数据进行调用。

现在回到导入的组件。您已经看到了 <h1> 组件。我还创建了一些其他组件来作为我们应用程序的 UI 组件。所有这些组件都位于 components 目录中,您可以从 Github 仓库中获取它们的代码。请放心,它们主要包含 HTML 和 CSS,因此您应该能够理解它们的功能。

输出应该如下所示。

猜猜怎么了?获取的数据仍然渲染到了服务器上!

参数化(动态)路由

有时您在页面视图中显示的数据由路由的状态决定。Web 应用中的一种常见模式是在 URL 中使用动态参数。此参数用于查询数据或数据库以获取给定资源。参数可以采用以下形式

https://example.com/product/2

URL 中的值 2 可以是 34 或任何值。最重要的是,您的应用程序将获取该值并针对数据集运行查询以检索相关信息。

在 Nuxt.js 中,您在 pages 文件夹中具有以下结构

pages/
--| product/
-----| _id.vue

这解析为

router: {
  routes: [
    {
      name: 'product-id',
      path: '/product/:id?',
      component: 'pages/product/_id.vue'
    }
  ]
}

要了解其工作原理,请在
pages 目录中创建一个名为 product 的文件夹,并在其中添加一个名为 _id.vue 的文件。

<template>
  <div class="product-page">
    <app-h1>{{product.title}}</app-h1>
    <div class="product-sale">
      <div class="image">
        <img :src="product.imgUrl" :alt="product.title">
      </div>
      <div class="description">
        <app-h2>${{product.price}}</app-h2>
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
      </div>
    </div>
  </div>
</template>
<style>

</style>
<script>
import h1 from '@/components/h1';
import h2 from '@/components/h2';
import data from '@/services/data';
export default {
  asyncData({ params }, callback) {
    setTimeout(() => {
       callback(null,{product: data.find(v => v.id === parseInt(params.id))})
    }, 2000)
  },
  components: {
    'app-h1': h1,
    'app-h2': h2
  },
};
</script>

重要的是再次使用 asyncData。我们正在使用 setTimout 模拟异步请求。请求使用通过上下文对象的 params 接收的 id 来查询我们的数据集以查找第一个匹配的 id。其余部分只是组件呈现 product

使用中间件保护路由

您很快就会意识到需要保护网站某些内容免受未经授权的用户访问。是的,数据源可能是安全的(这很重要),但用户体验要求您阻止用户访问未经授权的内容。您可以通过显示友好的错误消息或将他们重定向到登录页面来做到这一点。

在 Nuxt.js 中,您可以使用中间件来保护您的页面(进而保护您的内容)。中间件是在访问路由之前执行的一段逻辑。此逻辑可以完全阻止访问路由(可能通过重定向)。

在项目的根目录下创建一个 middleware 文件夹,并在其中添加一个名为 auth.js 的文件。

export default function (ctx) {
  if(!isAuth()) {
    return ctx.redirect('/login')
  }
}
function isAuth() {
  // Check if user session exists somehow
  return false;
}

中间件检查方法 isAuth 是否返回 false。如果是这种情况,则表示用户未经身份验证,并将用户重定向到登录页面。出于测试目的,isAuth 方法默认返回 false。通常,您会检查会话以查看用户是否已登录。

不要依赖 localStorage,因为服务器不知道它的存在。

您可以使用此中间件通过将其作为值添加到 middleware 实例属性来保护页面。您可以将其添加到我们刚刚创建的 _id.vue 文件中。

export default {
  asyncData({ params }, callback) {
    setTimeout(() => {
       callback(null,{product: data.find(v => v.id === parseInt(params.id))})
    }, 2000)
  },
  components: {
   //...
  },
  middleware: 'auth'
};

这会在我们每次访问此页面时自动将其关闭。这是因为 isAuth 方法始终返回 false

长话短说

我可以肯定地假设您已经了解了什么是 SSR 以及为什么您应该对使用它感兴趣。您还学习了一些基本概念,如路由、布局、安全性以及异步数据获取。不过,还有更多内容。您应该深入了解 Nuxt.js 指南 以了解更多功能和用例。如果您正在进行 React 项目并需要此类工具,那么我认为您应该尝试 Next.js