微信号:frontshow

介绍:InfoQ大前端技术社群:囊括前端、移动、Node全栈一线技术,紧跟业界发展步伐。

利用Nuxt.js创建服务端渲染的Vue.js应用程序

2018-05-23 17:56 张健欣 译

作者|Ben Jones
译者|张健欣
编辑|覃云
Nuxt.js 是一个 Vue 同构应用程序开发框架。本文将介绍为什么要选择 Nuxt、如何创建一个 Nuxt 项目、Nuxt 项目结构、Nuxt 的强化组件、使用服务端渲染时的考量、Nuxt 在各种环境的部署以及涉及的一些基本概念。希望能够鼓励你尝试 Nuxt 来进行快速开发,并利用 Nuxt 提供的强大功能创造出交互丰富的卓越用户体验。

像 Vue 之类的 JavaScript 框架(或库)可以在浏览你的网站时提供奇妙的用户体验。大多数框架都提供了一种动态改变页面内容,而无需每次都向服务端发送一个请求。

然而,这种方案有一个问题。刚开始加载你的网站时,浏览器不会接收到完整的显示页面。相反地,浏览器收到一堆用来构建页面的代码片段(HTML、CSS 和其它文件)和如何将这些代码片段组装起来的指令(一个 JavaScript 框架或库)。在浏览器真正有可以显示的东西之前,需要先花费一段时间来将这些信息拼装起来,这有点像收到一堆书和一个扁平包装的书柜。

解决这个问题的方案很巧妙:在服务端放一个能够构建出随时显示的页面的框架(或库)版本,然后将这个完整页面发送给浏览器,附带进一步改变的能力和动态页面内容(框架 / 库),就像发送一个现有的书柜和一些书,当然,你仍然需要将这些书放到书柜中,但是你已经得到了一些立即可用的东西。

除了这个可能不太巧妙的比喻,Nuxt 还有许多其它优势。举个例子,一个很少改变的页面,比如一个 关于我们 页面,并不需要每次在用户访问它的时候重新创建。因此,服务器可以只创建它一次,然后将它缓存或存储在某个地方以备将来使用。这种速度提升可能看起来很小,但是在一个响应时间以毫秒(或更少)来衡量的环境下,每一点小的提升都很重要。

如果你想了解关于 Vue 环境下的服务端渲染(Server-Side Rendering,SSR)优势的更多信息,可以查看 Vue 自己的关于服务端渲染的文章。虽然有许多实现服务端渲染的可选方案,但是 Nuxt 是最流行的方案,也是 被 Vue 团队所推荐的方案。

为什么选 Nuxt.js

Next 是一个针对流行的 React 库的服务端渲染实现。在看到了这种设计的优势之后,以它为基础,设计了一个针对 Vue 的类似实现,即 Nuxt。熟悉 React+Next 组合的人,会在设计和应用程序布局方面发现许多相似之处。然而,Nuxt 提供了一些 Vue 特有的功能来针对 Vue 创建一个功能强大而灵活的服务端渲染解决方案。

Nuxt 在 2018 年 1 月份 更新到 1.0 生产环境就绪版本,并且成为活跃的受到广泛支持的社区的一部分。其中最重要的一点是,使用 Nuxt 构建一个项目和构建其它 Vue 项目没有太多不同。事实上,它提供了一堆特性,使你能够以更少的时间创建结构良好的代码库。

另外值得一提的是,Nuxt 不一定非要用于服务端渲染。它被推动作为一个 Vue.js 同构应用程序开发库,包含了一个使用相同代码创建静态生成的 Vue 应用程序的命令(nuxt generate)。因此,如果你对于深入研究服务端渲染顾虑重重,不要惊慌。你仍然可以利用 Nuxt 的特性创建一个静态站点。

我们可以创建一个简单的项目来深入理解 Nuxt。这个项目的最终源码 托管在 GitHub 上,你可以随便查看,或者,你可以查看托管在 Netlify 上的使用nuxt generate创建的 实时版本。

创建一个 Nuxt 项目

首先,让我们使用一个名为vue-cli的 Vue 项目生成器快速创建一个示例项目:

# install vue-cli globally
npm install -g vue-cli

# create a project using a nuxt template
vue init nuxt-community/starter-template my-nuxt-project

经过几个选项之后,就会在my-nuxt-project目录或者你指定的其它目录创建一个项目。然后我们只需要安装依赖,运行服务器:

cd my-nuxt-project
npm install # Or yarn
npm run dev

这样做之后,确保项目正在运行,然后打开浏览器,访问localhost:3000。这跟创建一个 Vue Webpack 项目没有太多不同。然而,当我们查看应用程序的实际结构时,并没有太多文件,特别是和类似 Vue Webpack 模版之类的文件比较起来的话。

package.json也显示我们只有一个依赖——Nuxt 它本身。这是因为 Nuxt 的每个版本都是针对特定版本的 Vue、Vue-router 和 Vuex 进行定制并将它们打包在一起的。

在项目的根目录还有一个nuxt.config.js文件。这使得你能够自定义许多 Nuxt 提供的功能。默认情况下,它会为你设置头部标签、加载条颜色以及 ESLint 规则。如果你着急想看看你能够进行哪些配置,这里有一个文档链接;我们将在本文介绍到其中的一些配置项。

那么,这些目录有什么特别的呢?

项目结构

如果你浏览一遍创建出来的目录,它们都会有一个附带的 Readme 文件。这个 Readme 文件通常以一段介绍这个目录是干什么的简短总结和一个文档链接开头。

使用 Nuxt 的一个好处是:一个默认的应用程序结构。任何 优秀的前端开发者 都会类似这样来安排应用程序结构,但是关于应用程序结构存在许多不同的想法,而团队协作时,不可避免地会投入一些时间来讨论或选择应用程序结构。Nuxt 给你提供了一个,(通常就可以避免再花费这些时间)。

Nuxt 会寻找特定的目录,然后基于它找到的目录来为你构建你的应用程序。让我们逐个看看这些目录。

Pages

这是唯一的 必需 目录。这个目录中的任何 Vue 组件都会基于它们的文件名称和目录结构自动添加到vue-router中。这点真是超级方便。通常,我会有一个独立的 Pages 目录,然后必需手动将其中的每个组件注册到另一个路由文件中。对于比较大型的项目,这个路由文件会变得复杂,可能需要进行拆分来维持可读性。相反地,Nuxt 会为你处理所有这些逻辑。

为了演示,我们可以在 Pages 目录创建一个名为about.vue的 Vue 组件。让我们添加一个简单的模板:

<template>
 <h1>About Page</h1>
</template>

当你保存的时候,Nuxt 会为你重新生成对应的路由。可以看到,因为我们将组件命名为about.vue,如果你导航到/about,你应该就会看到那个组件。很简单。

有一个文件名很特别。将一个文件命名为index.vue会为那个目录创建一个根路由。当项目生成的时候,在 Pages 目录已经有一个index.vue组件,它关联到你站点的主页或着陆页(landing page)。(在这个开发示例中,指的是localhost:3000。)

更深的路由会怎么样呢?Pages 目录的子目录可以帮你构建自己的路由结构。因此,如果我们想要一个 View Product 页面,我们可以将 Pages 目录构建成如下的结构:

/pages
--| /products
----| index.vue
----| view.vue

现在,如果我们导航到/products/view,我们会看到 products 目录中的view.vue组件。如果导航到products,我们会看到 products 目录中的index.vue目录。

你可能会问,我们为什么不在 Pages 目录创建一个products.vue组件,就像我们创建/about页面那样。你可能会认为结果是相同的,但是这两种结构有一点区别。让我们通过添加另一个新页面来演示这个区别:

假如说,我们想要为每个员工创建一个单独的 About 页面。例如,为我创建一个 About 页面。这个页面应该位于/about/ben-jones。一开始,我们可能像下面这样构建 Pages 目录结构:

/pages
--| about.vue
--| /about
----| ben-jones.vue

当我们访问/about/ben-jones,会得到about.vue组件,和访问/about时一样。这里发生了什么?

有意思的是,Nuxt 在这里构建了一个嵌套的路由。这种结构表示,你想要一个永久的/about路由,并且那个路由中的任何东西都应该嵌套在它自己的视图区域。在 vue-router 中,这会通过在about.vue组件中指定一个<router-view />组件来表示。在 Nuxt 中,概念相同,但我们用<nuxt />而不是<router-view />。让我们更新about.vue组件来实现嵌套路由:

<template>
 <div>
   <h1>About Page</h1>
   <nuxt />
 </div>
</template>

现在,当我们导航到/about,我们得到了之前的about.vue组件,只有一个标题。然而,当我们导航到/about/ben-jones时,我们会看到那个标题和渲染在<nuxt/>占位符所在位置的ben-jones.vue组件。

这并不是我们最初想要的,但是这个想法(存在一个人物列表,每个人物有一个 About 页面,当点击的时候,将对应人物信息填充到页面的某个区域)是一个有意思的概念,我们目前可以先把它放到一边。如果你确实想要其它选项,那么我们要做的就是重新构建我们的目录结构。我们只需要将about.vue组件移动到/about目录,然后将它重命名为index.vue,因此,得到的目录结构会是:

/pages
--| /about
----| index.vue
----| ben-jones.vue

最终,假如说,我们想要使用路由参数来获取某个具体的产品。例如,我想要通过导航到/products/edit/64来编辑某个产品,而 64 是product_id。我们可以实现如下:

/pages
--| /products
----| /edit
------| _product_id.vue

注意_product_id.vue组件开头的下划线,这表示一个路由参数,可以稍后在$route.params对象或者 Nuxt 的 Context 中的params对象(稍后再作更多介绍)上访问这个参数。注意,这个参数的键是组件名称去掉开始的下划线,在这个例子中就是product_id,因此使它们在整个项目中保持唯一。因此,在_product_id.vue中,我们会有如下代码:

<template>
 <h1>Editing Product {{ $route.params.product_id }}</h1>
</template>

你可以想象更复杂的布局,那些布局使用 vue-router 配置会非常痛苦。例如,我们可以将上述内容都组合到一个如下路由中:

/pages
--| /categories
----| /_category_id
------| products.vue
------| /products
--------| _product_id.vue

不难推理出/categories/2/products/3会显示什么。我们会得到products.vue组件和一个嵌套的_product_id.vue组件,附带 2 个路由参数:category_idproduct_id。进行推理时,这比同等的路由配置要简单得多。

当我们谈到这个话题时,我想要在路由配置中做的一件事是设置路由拦截。由于 Nuxt 为我们构建路由,因此我们可以通过组件本身的beforeRouterEnter设置路由拦截。如果你想要验证路由参数,Nuxt 提供了一个名为validate的组件方法。因此,如果你想要在渲染组件之前检查product_id是否是一个数字,你需要在_product_id.vue的 script 标签中增加如下代码:

export default {
 validate ({ params }) {
   // Must be a number
   return /^\d+$/.test(params.product_id)
 }
}

现在,导航到/categories/2/products/someproduct会返回一个 404 页面,因为someproduct不是一个正确的数字。

这就是 Pages 目录的内容。了解如何在这个目录中恰当构建你的路由结构是很重要的,因此,一开始花一些时间来学习它是很重要的。如果你正在寻找一份概要,参考 路由相关文档 通常会有所帮助。

如果你担心路由不受控制,那大可不必。这个默认设置对于各种各样的项目来说都运行得很好,只要它们结构良好。然而,有一些情况下,你可能需要增加一些 Nuxt 自动为你生成的路由之外的路由,或者需要重构它们。Nuxt 提供了一种在配置中自定义路由实例的方法,使你能够新增路由并自定义生成的路由。你还可以编辑路由实例的核心功能,包括 Nuxt 增加的额外选项。因此,如果你确实遇到了一个极端例子,你还可以灵活地找到合适的解决方案。

Store

和 Pages 目录有点类似,Nuxt 可以基于 Store 目录结构构建你的 Vuex store。如果你不需要 store,只需要删除这个目录就可以了。有两种模式的 store:经典模式和模块模式。

经典模式需要在 Store 目录有一个index.js文件。在这个文件中,你需要导出一个返回了一个 Vuex 实例的函数:

import Vuex from 'vuex'

const createStore = () => {
 return new Vuex.Store({
   state: ...,
   mutations: ...,
   actions: ...
 })
}

export default createStore

这使你能够创建任何你想要的 store,这与在一个普通 Vue 项目中使用 Vuex 非常像。

模块模式也需要你在 Store 目录创建一个index.js文件。然而,这个文件只需要为你的 Vuex store 导出根 state/mutations/actions。下面的例子指定了一个空的根 state:

export const state = () => ({})

然后,store 目录中的每个文件都会以它自己的命名空间或模块增加到 store 中。例如,让我们创建某个 store 来存储当前产品。如果我们在 store 目录创建一个名为product.js的文件,那么可以在$store.product访问 store 下对应命名空间的部分。下面是一个简单的例子:

export const state = () => ({
 _id: 0,
 title: 'Unknown',
 price: 0
})

export const actions = {
 load ({ commit }) {
   setTimeout(
     commit,
     1000,
     'update',
     { _id: 1, title: 'Product', price: 99.99 }
   )
 }
}

export const mutations = {
 update (state, product) {
   Object.assign(state, product)
 }
}

在 load action 中的setTimeout模拟了某种类型的 API 调用,会在响应中更改 store;在这个例子中,它需要 1 秒钟(才会响应)。现在,让我们在products/view页面中使用它:

<template>
 <div>
   <h1>View Product {{ product._id }}</h1>
   <p>{{ product.title }}</p>
   <p>Price: {{ product.price }}</p>
 </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
 created () {
   this.$store.dispatch('product/load')
 },
 computed: {
   ...mapState(['product'])
 }
}
</script>

需要注意:这里,我们是在组件创建时,调用了我们假的 API。你可以看到,我们正在调度的product/load action 是在 Product 命名空间下。这样,我们正在处理 store 的哪个部分就显得非常清楚。然后,通过将 state 映射到一个本地的 computed 属性,我们可以在模版中轻松使用它。

有一个问题:在 API 运行的时候,我们可以持续看到原始状态 1 秒钟。稍后,我们会使用 Nuxt 提供的一种解决方案来修复这个问题(称为fetch)。

再次强调,我们从来不需要运行npm install vuex,因为它已经包含在 Nuxt 包中了。当你在 store 目录增加一个index.js文件时,所有这些方法都会向你自动打开。

其中主要的 2 个目录已经解释了;剩下的目录就简单多了。

Components

Components 目录用来包含你的可复用组件,例如导航条、图片夹、分页、数据表格等。由于 Pages 目录中的组件会被转换为路由,因此你需要在其它地方存储这些组件类。可以通过引用这些组件而在页面或其它组件中访问它们:

import ComponentName from ~/components/ComponentName.vue
Assets

这个目录包含未编译的资源,与 Webpack 如何加载和处理文件有更多关系,而与 Nuxt 如何工作没有太多关系。如果你对此感兴趣,可以阅读 Readme 中的指南。

Static

这个目录包含一些映射到你的站点的根目录的静态文件。例如,将一个名为logo.png的图片放到这个目录,就可以在/logo.png访问它。这很适合一些诸如 robots.txt、favicon.ico 和其它你需要可被访问的元文件。

Layouts

通常,在一个 Vue 项目中,你有某种根组件,通常名为App.vue。在这里,你可以设置你的(通常是静态的)应用程序的布局。这个布局可能会包含一个导航栏、页脚和一个用于 vue-router 的内容区。default布局就是这样的,在 layouts 目录中提供了这种默认布局。最初,它只有一个 div 和一个<nuxt />组件(等同于<router-view />),但它可以任你调整。例如,我向示例项目中增加了一个简单的导航栏,用于导航各种演示页面。

你可能会想让你的 App 的特定区域有一种不同的布局。也许,你有某种看起来不太一样的内容管理系统(CMS)或者管理看板。为了解决这个问题,可以在 Layouts 目录创建一个新的布局。举个例子,让我们创建一个admin-layout.vue布局,只有一个额外的 header 标签而没有导航条:

<template>
 <div>
   <h1>Admin Layout</h1>
   <nuxt />
 </div>
</template>

然后,我们可以在 Pages 目录创建一个admin.vue页面,然后使用 Nuxt 提供的一个名为layout的属性来指定我们想要为那个组件使用的布局名称(一个字符串)。

<template>
 <h1>Admin Page</h1>
</template>

<script>
export default {
 layout: 'admin-layout'
}
</script>

这就是所有代码了。Page 组件将使用默认布局除非指定了布局,但是当你导航到/admin,它现在使用admin-layout.vue布局。当然,如果你想的话,这个布局可以在几个管理屏幕之间共享。需要牢记的是,布局必须包含一个<nuxt />元素

关于布局,还有最后一点需要注意。你可能已经在实验时注意到了,如果你输入一个不正确的 URL,会显示一个错误页面。事实上,这个错误页面是另外一种布局。Nuxt 有它自己的错误页面布局(源代码链接),但是如果你想要编辑它,只需要创建一个error.vue布局,然后 Nuxt 就会使用这个布局代替默认的布局。这里需要提醒的是,错误页面布局一定不要有一个<nuxt />元素。你还可以访问组件上的一个error对象,它包括一些要显示的基本信息。(这会打印在运行 Nuxt 的终端中,如果你想要检查这点的话。)

Middleware

中间件(Middleware)是一些可以在渲染一个页面或布局之前被执行的函数。有各种各样的原因你想要这样做。路由拦截是一种流行用途,其中你可以通过检查 Vuex store 来校验登录是否合法,或者校验一些参数(而不是在组件本身上使用validate方法)。我最近从事的一个项目使用中间件来基于路由和参数生成动态面包屑导航(breadcrumbs)。

这些函数可以是异步的;需要小心,因为直到中间件处理完毕之前不会向用户显示任何东西。这些函数还可以访问到 Nuxt 的 Context,我稍后会解释这一点。

Plugins

这个目录使你能够在应用程序被创建之前注册 Vue 插件。这使得插件可以在你的应用程序上的 Vue 实例中共享,并可以在任何一个组件中使用。

大部分主流插件都有 Nuxt 版本,通过遵循它们的文档,可以轻松将它们注册到 Vue 实例。然而,你也可以开发一个插件或者对一个现有插件进行适配。我从 Nuxt 文档中借用的一个例子展示了如何对vue-notifications进行适配。首先,我们需要安装这个包:

npm install vue-notifications --save

然后,在 plugins 目录创建一个名为vue-notifications的文件,包含如下内容:

import Vue from 'vue'
import VueNotifications from 'vue-notifications'

Vue.use(VueNotifications)

这与你向普通的 Vue 环境注册一个插件非常相似。然后在你项目的根目录编辑nuxt.config.js文件,并将下面的内容添加到 module.exports 对象中:

plugins: ['~/plugins/vue-notifications']

这就行了。现在,你可以在你的应用程序中使用vue-notifications。在示例项目的/plugin有一个例子。

这样就逐个介绍完了 Nuxt 目录结构。这可能看起来有很多东西要学,但是如果你正在开发一个 Vue 应用程序,你其实已经在设置相同类型的逻辑。Nuxt 只是帮助简化了设置,帮你将精力集中在应用程序构建上。

其实,Nuxt 做的不仅仅是辅助开发。它通过提供的额外功能,强化了你的组件。

Nuxt 的强化组件

当我刚开始研究 Nuxt 的时候,我一直在看 Page 组件是如何被强化的。强化组件虽然听起来很棒,但是它究竟意味着什么以及它能带来什么好处并不能立即显现出来。

强化组件意味着,Nuxt 给所有的 Page 组件绑定了额外的方法,Nuxt 可以使用这些方法来提供额外的功能。事实上,我们先前使用validate方法来检查参数和重定向非法用户时,已经看过其中一个强化组件了。

在一个 Nuxt 项目中被使用的 2 个主要方法是asyncDatafetch方法。它们在概念上非常相似,在组件生成之前异步运行,而且它们可以用来获取组件和 sotre 的数据。它们还能够使页面在发送到客户端之前在服务端完全渲染,即使我们必须等待一些数据库或 API 调用。

asyncDatafetch之间有什么不同?

  • asyncData是用来获取 Page 组件的数据。当你返回一个对象,它会在渲染之前,与输出数据进行合并。

  • fetch是用来获取 Vuex Store。如果你返回一个 promise,Nuxt 会在渲染之前一直等待 promise 处理完毕。

那么,让我们好好利用这些功能。还记得在/products/view页面,我们有一个问题,即在我们调用假 API 时,store 的最初状态被会短暂显示?修复这个问题的一个方法是,在组件或 Store 上存储一个布尔值,例如loading = true,然后在调用 API 时显示一个加载中组件。API 调用完成之后,我们设置loading = false并显示数据。

作为替代,我们可以在渲染之前使用fetch获取 Store。在一个名为/products/view-async的新页面,我们将created方法改为fetch;这样应该可行,对吧?

export default {
 fetch () {
   // Unfortunately the below line throws an error
   // because 'this.$store' is undefined...
   this.$store.dispatch('product/load')
 },
 computed: {...}
}

这里有个问题:这些“强化”方法在组件被创建之前运行,因此不会指向对应的组件,而且不能访问组件上的任何东西。那么,我们这里如何访问 Store?

The Context API

当然是有方案的。在所有 Nuxt 方法中,会被提供一个包含一个 Context 对象的参数(通常是第一个参数),这个对象非常有用。这个对象中,就是你就会跨应用程序引用到的任何东西。这意味着,我们不需要等待 Vue 预先在对应组件上创建那些引用。

我强烈推荐查看 Context 文档 来看看有什么可用的内容。比较方便的有app,你可以通过它访问你的所有插件;redirect,可以用来改变路由;error用来显示错误页面;以及一些一目了然的属性,例如routequerystore等。

因此,为了访问 Store,我们可以解构 Context 并从中提取 Store。我们还需要确保返回了一个 promise,这样 Nuxt 可以在渲染组件之前等待这个 promise 解决,因此我们还需要对我们的 Store action 做一些小的调整。

// Component
export default {
 fetch ({ store }) {
   return store.dispatch('product/load')
 },
 computed: {...}
}

// Store Action
load ({ commit }) {
 return new Promise(resolve => {
   setTimeout(() => {
     commit('update', { _id: 1, title: 'Product', price: 99.99 })
     resolve()
   }, 1000)
 })
}

你可以根据自己的编码风格,使用 async/await 或者其它方法,但是概念是相同的——我们让 Nuxt 在尝试渲染组件之前确保 API 已经调用结束并且 Store 已经用返回结果更新了。如果你导航到/products/view-async,不会再闪现处于原始状态的产品内容。

你可以想象,即使没有服务端渲染,这在任何 Vue 应用程序中会多么有用。Context 还可以在所有中间件以及其它 Nuxt 方法,例如nuxtServerInit中访问。nuxtServerInit是一个特殊的 store action,在 Store 初始化之前运行(下个章节中有一个关于它的例子)。

使用服务端渲染时的考量

我确信,许多开始使用 Nuxt 之类技术的人(包括我自己),在将它当作任何其它 Vue 项目的时候,最后都会碰壁——我们通常认为可以起效的事情在 Nuxt 中看起来是不可能的。随着更多的这些警告被记录,这会变得更容易克服,但是在开始调试时主要需要考虑的是,客户端和服务端是两个独立的实体。

当你刚开始访问一个页面的时候,会向 Nuxt 发送一个请求,服务端会尽可能多地构建那个页面和应用程序的剩余部分,然后将它发送给你。然后,客户端负责继续导航并按需加载对应的部分。

我们想要服务端在开始的时候尽可能多地做事情,但是有时候,服务端并没有它所需要的信息,从而导致相应的工作要在客户端完成。更糟糕的是,当客户端展示的最终内容与服务端预期的内容不同时,会让客户端重头开始重新构建。这是一个很大的迹象表明应用程序的逻辑在某些地方出了问题。谢天谢地,(在开发环境),如果应用程序的逻辑发生了错误,在你的浏览器控制台会生成一个错误。

让我们以如何解决会话管理这个常见问题为例。想象你有一个 Vue 应用程序,你可以登录一个账户,然后使用一个 token(例如 JWT)存储你的会话,你可能决定将这个 token 保持在localStorage。当你一开始访问站点的时候,你想要通过一个 API 来验证那个 token,如果 token 合法,那个 API 会返回一些基本的用户信息并将这些信息放到 Store 中。

读完 Nuxt 的文档之后,你可以看到,有一个方便的方法nuxtServerInit可以使你在刚开始加载的时候获取 Store 数据。这听起来很完美!因此,你在 Store 中创建了你的用户模块,并在 Store 目录的index.js文件中增加适当的 action:

export const actions = {
 nuxtServerInit ({ dispatch }) {
   // localStorage should work, right?
   const token = localStorage.getItem('token')
   if (token) return dispatch('user/load', token)
 }
}

当你刷新页面,你会得到一个错误——localStorage is not defined(localStorage 未被定义)。想一想这个错误是在哪里发生的,一切就清楚了。这个方法运行在服务端,而服务端不清楚客户端上的localStorage中存储了什么;事实上,服务端甚至不知道“localStorage“是什么!因此,上述方法是行不通的。

那么,解决方案是什么呢?事实上,还是有一些解决方案的。你可以让客户端来初始化 Store,但会丧失服务端渲染的好处,因为客户端最终做了所有事情。你可以在服务端建立会话,然后用它来验证用户,但那又是另一个层次的事情了。最类似于localStorage的替代方法是使用 cookies。

Nuxt 可以访问 cookies,因为它们会随着请求一起从客户端发送到服务端。和其它 Nuxt 方法一样,nuxtServerInit可以访问 Context,这次是作为第二个参数,因为第一个参数是保留给 store 的。在 Context 上,我们可以访问req对象,它存储了所有的请求头和客户端请求的其它信息。(如果你用过 Node.js,就会对这些感到特别熟悉。)

因此,在一个 cookie 中存储 token(本例中,称为“token”)之后,让我们在服务端访问它。

import Cookie from 'cookie'

export const actions = {
 nuxtServerInit ({ dispatch }, { req }) {
   const cookies = Cookie.parse(req.headers.cookie || '')
   const token = cookies['token'] || ''
   if (token) return dispatch('user/load', token)
 }
}

这是一个简单的解决方案,但可能不是能够让人立即明白。学着去思考,特定的活动发生在什么地方(客户端、服务端或者同时这两端),以及它们访问的哪些内容虽然会需要一些时间但其好处是值得的。

  部 署  

部署 Nuxt 非常简单。使用同样的代码,你可以创建一个服务端渲染应用程序、单页应用程序或者静态页面。

服务端渲染应用程序(SSR App)

这可能就是你使用 Nuxt 的目的。部署的基本概念是,在你选择的任何平台运行build进程并设置一些配置。我将使用 文档 中的 Heroku 例子:

首先,在package.json中设置好 Heroku 的脚本:

"scripts": {
 "dev": "nuxt",
 "build": "nuxt build",
 "start": "nuxt start",
 "heroku-postbuild": "npm run build"
}

然后使用heroku-cli安装好 Heroku 环境(安装指令如下:

# set Heroku variables
heroku config:set NPM_CONFIG_PRODUCTION=false
heroku config:set HOST=0.0.0.0
heroku config:set NODE_ENV=production

# deploy
git push heroku master

这就可以了。现在,你的服务端渲染 Vue 应用程序已经上线了,全世界都看得到。其它平台的安装步骤不同,但是过程类似。目前其官方部署方法列举如下:

  • Now:https://nuxtjs.org/faq/now-deployment

  • Dokku:Digital Ocean :https://nuxtjs.org/faq/dokku-deployment

  • Nginx :https://nuxtjs.org/faq/nginx-proxy

单页应用程序(SPA)

如果你想要利用 Nuxt 提供的其它特性,但是避免服务端渲染页面,那么你可以将它作为一个 SPA 部署。

首先,最好关闭服务端渲染来测试你的应用程序,因为默认地,npm run dev运行时会开启服务端渲染。可以编辑nuxt.config.js文件并增加如下配置项来改变这点:

mode: 'spa',

现在,当你运行npm run dev,服务端渲染会被关闭,而应用程序会作为一个单页应用程序运行来供你测试。这个设置也会确保未来的构建都不会包含服务端渲染。

如果看起来一切正常,那么其部署过程和一个服务端渲染应用程序其实是相同的。只要记住,你需要先设置mode: 'spa'来让构建进程知道你想要一个单页应用程序。

静态页面

如果你一点儿也不想和服务器打交道,而只想要生成一些页面,用于静态托管服务,例如 Surge 或 Netlify,那么你也可以用 Nuxt。只需要牢记,没有服务器,你就不能在 Context 中访问reqres,因此,如果你的代码依赖这些,最好确保对它进行适配。例如,在生成示例项目时,nuxtServerInit函数因为试图从请求头的 cookies 中获取 token 而抛出了一个错误。在这个项目中,这没什么影响,因为那些数据在任何地方都不会被用到,但是在一个真实的应用程序中,就会需要有一种备选方法来获取那些数据。

一旦就绪,部署就很简单了。你可能首先需要改变的是,增加一个配置项,从而使nuxt generate命令也会创建一个反馈文件。这个文件会提示托管服务让 Nuxt 处理路由而不是托管服务处理路由,抛出一个 404 错误。为了实现这点,要向nuxt.config.js中增加如下一行代码:

generate: { fallback: true },

这里有一个使用 Netlify 的例子,目前不在 Nuxt 文档中。只要牢记,如果你是第一次使用netlify-cli,你会被提示进行身份验证:

# install netlify-cli globally
npm install netlify-cli -g

# generate the application (outputs to dist/ folder)
npm run generate

# deploy
netlify deploy dist

就是这么简单!正如本文开头说的,这里 有一个这个项目的版本。下述服务也有各自的官方部署文档:

  • Surge:https://nuxtjs.org/faq/surge-deployment

  • GitHub Pages:https://nuxtjs.org/faq/github-pages

了解更多

Nuxt 正迅速更新,这只是挑选出来的一小部分它提供的特性。我希望本文能够鼓励你去尝试它,并看看它是否能够帮助提升你的 Vue 应用程序的能力,使你能够更快地开发并利用它的强大功能。

如果你想要获取更多信息,那么没有比 Nuxt 的官方链接更透彻的了:

  • Documentation:https://nuxtjs.org

  • Playground:https://glitch.com/edit/#!/nuxt-hello-world

  • GitHub:https://github.com/nuxt/nuxt.js

  • FAQ:https://nuxtjs.org/faq

  英文原文

https://www.toptal.com/vue-js/server-side-rendered-vue-js-using-nuxt-js

前端之巅

「前端之巅」是 InfoQ 旗下关注大前端技术的垂直社群。紧跟时代潮流,共享一线技术,欢迎关注。

  活动推荐

PWA、Web 框架、UI 与动画、Node... 大前端的下一站在哪里?前端工程师的价值和成长路径是什么?GMTC2018 上,来自 Google、Facebook、BAT 等 60+ 国内外一线前端大牛,将与你面对面探讨大前端领域最新技术趋势和实践,想要升职加薪就快来吧!扫描下方二维码或点击“阅读原文”了解更多大会详情!

目前大会 8 折热销中,团购更优惠,购票咨询:18514549229(同微信)


 
前端之巅 更多文章 为什么说Flutter让移动开发变得更好? 利用深度学习进行前端自动化开发 前端每周清单:Redux 4.0,用Kotlin重构Java应用 七行JSON代码将你的网站变成移动应用 一文带你快速了解现代的CSS
猜您喜欢 Rails4升级第二弹 折腾了几个月,终于调教出了一架可以抢车位的无人机。然而… PLA 1.2 的解决方案 当我们在谈“高性能高可用可扩展安全稳定”时,我们到底在谈什么? 2017年投资比特币和比特币现金,该考虑什么?