Skip to content

4. 示例 [nuxt-01]:路由与导航

我们将构建一系列简单示例,逐步了解 [nuxt] 应用程序的工作原理。首先,我们将移植文档 |通过示例了解 VUE.JS 框架| 中的 [vuejs-11] 应用程序,以此初步了解 [nuxt] 应用程序与 [vue] 应用程序在代码组织上的区别。

4.1. 项目目录结构

[vuejs-11] 项目是一个涉及视图间导航的项目:

Image

[vuejs-11] 项目的源代码目录结构如下:

Image

  • [main.js] 是 [vue] 应用程序启动时执行的脚本;
  • [router.js] 定义了路由规则;
  • [App.vue] 是应用程序的结构视图,负责组织不同视图的布局;
  • [Component1、Component2、Component3、Layout、Navigation] 是该应用程序在各个视图中使用的组件;

在将 [Vue] 应用程序 [1] 移植为 [Nuxt] 应用程序 [2] 时:

  • 应用程序启动时执行的脚本必须在 [nuxt.config.js] 文件的 [plugins] 键下声明。此外,可以将针对 [nuxt] 服务端的脚本与针对 [nuxt] 客户端的脚本分开;
  • [App.vue] 视图必须放置在 [layouts] 文件夹中,并重命名为 [default.vue];
  • 作为路由目标的组件 [Component1, Component2, Component3] 必须移至 [pages] 文件夹。其中一个组件(作为首页的组件)必须重命名为 [index.vue]。在此,我们已将文件重命名为:
    • [Component1] --> [index]:显示文本 [Home];
    • [Component2] --> [page1]:显示文本 [Page 1];
    • [Component3] --> [page2]:显示文本 [Page 2];

[nuxt] 会利用 [pages] 文件夹中的内容动态生成以下路由:

1
2
3
{ name :’index’, ‘path’ :’/’}
{ name :’page1’, ‘path’ :’/page1’}
{ name :’page2’, ‘path’ :’/page2’}

因此,[Vue] 项目中使用的 [router.js] 文件在 [Nuxt] 项目中就不再需要了。

配置文件 [nuxt.config.js] 将如下所示:


export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    title: 'Introduction à [nuxt.js]',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: 'ssr routing loading asyncdata middleware plugins store'
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module'
  ],
  /*
   ** Nuxt.js modules
   */
  modules: [
    // Doc: https://bootstrap-vue.js.org
    'bootstrap-vue/nuxt',
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  // source code directory
  srcDir: 'nuxt-01',
  // router
  router: {
    // application URL root
    base: '/nuxt-01/'
  },
  // server
  server: {
    // service port, default 3000
    port: 81,
    // network addresses listened to, default localhost: 127.0.0.1
    // 0.0.0.0 = all the machine's network addresses
    host: '0.0.0.0'
  }
}
  • 第 62 行:指定包含项目源代码的文件夹 [dvp];
  • 第 66 行:指定 [dvp] 应用程序的根 URL(可自定义);
  • 第 43 行:请注意配置中引用了 [bootstrap-vue] 库;

4.2. 移植 [main.js] 文件

[vuejs-11] 项目的 [main.js] 文件如下:


// imports
import Vue from 'vue'
import App from './App.vue'
 
// plugins
import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);
 
// bootstrap
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
 
// routeur
import monRouteur from './router'
 
// configuration
Vue.config.productionTip = false
 
// instanciation projet [App]
new Vue({
  name: "app",
  // vue principale
  render: h => h(App),
  // routeur
  router: monRouteur,
}).$mount('#app')

除了 [导入] 之外,该代码执行以下操作:

  • 第 5–11 行:使用 [bootstrap-vue] 库。这现在由 [nuxt.config.js] 配置文件第 43 行中的 [bootstrap-vue/nuxt] 模块处理;
  • 第 14 行和第 25 行:使用路由文件 [router.js]。现在,[nuxt] 应用程序会根据 [pages] 文件夹的目录结构自动完成此操作;
  • 第 20–26 行:实例化应用程序的主视图。在 [nuxt] 应用程序中,[layouts/default.vue] 视图充当主视图;

[main.js] 文件已不再需要。如果需要,我们本应在 [nuxt.config.js] 配置文件的第 30 行 [plugins] 键下声明它;

4.3. 主视图 [default.vue]

Image

主视图 [layouts/default.vue] 如下所示:


<template>
  <div class="container">
    <b-card>
      <!-- a message -->
      <b-alert show variant="success" align="center">
        <h4>[nuxt-01] : routage et navigation</h4>
      </b-alert>
      <!-- the current routing view -->
      <nuxt />
    </b-card>
  </div>
</template>
 
<script>
export default {
  name: 'App'
}
</script>
  • 在第 9 行,[vuejs-11] 项目中我们使用的是 <router-view /> 标签,而不是这里使用的 <nuxt /> 标签。两者似乎都能正常工作。我尝试了这两种写法,但没有发现任何变化。我保留了 <nuxt /> 标签,因为这是推荐的写法。它会显示当前视图,即当前路由的目标页面;

4.4. 组件

Image

与 [vuejs-11] 项目相比,[布局、导航] 组件保持不变:

[components / layout.vue]


<!-- view layout -->
<template>
  <!-- line -->
  <div>
    <b-row>
      <!-- two-column zone -->
      <b-col v-if="left" cols="2">
        <slot name="left" />
      </b-col>
      <!-- ten-column zone -->
      <b-col v-if="right" cols="10">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>
 
<script>
export default {
  // paramètres
  props: {
    left: {
      type: Boolean
    },
    right: {
      type: Boolean
    }
  }
}
</script>

此组件用于将应用程序的页面划分为两列:

  • 第 7–9 行:左侧栏占 2 个 Bootstrap 列;
  • 第 11–13 行:右侧栏占 10 个 Bootstrap 列;

[navigation.vue]


<template>
  <!-- bootstrap menu with three options -->
  <b-nav vertical>
    <b-nav-item to="/" exact exact-active-class="active">
      Home
    </b-nav-item>
    <b-nav-item to="/page1" exact exact-active-class="active">
      Page 1
    </b-nav-item>
    <b-nav-item to="/page2" exact exact-active-class="active">
      Page 2
    </b-nav-item>
  </b-nav>
</template>

此组件显示三个导航链接:

Image

要确定第 4、7 和 10 行中 [to] 属性的取值,请查看 [pages] 文件夹 [2]:

  • [index] 页面的 URL 为 [/];
  • [page1] 页面的 URL 为 [/page1];
  • [page2] 页面的 URL 为 [/page2];

[navigation] 组件也可以写成如下形式:


<template>
  <!-- bootstrap menu with three options -->
  <b-nav vertical>
    <nuxt-link to="/" exact exact-active-class="active">
      Home
    </nuxt-link>
    <nuxt-link to="/page1" exact exact-active-class="active">
      Page 1
    </nuxt-link>
    <nuxt-link to="/page2" exact exact-active-class="active">
      Page 2
    </nuxt-link>
  </b-nav>
</template>

<b-nav-item> 标签已被 <nuxt-link> 标签取代,后者表示路由链接。实际上,我并未察觉到任何显著差异——没有任何因素足以让其中一个标签明显优于另一个。

4.5. 页面

Image

[index.vue] 页面显示以下视图:

Image

该页面的代码如下:


<!-- page principale -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">
      Home
    </b-alert>
  </Layout>
</template>
 
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
 
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
 
export default {
  name: 'Home',
  // components used
  components: {
    Layout,
    Navigation
  },
  // life cycle
  beforeCreate(...args) {
    console.log('[home beforeCreate]', 'process.server=', process.server,
 'process.client=', process.client, "nombre d'arguments=", args.length)
  },
  created(...args) {
    console.log('[home created]', 'process.server=', process.server,
 'process.client=', process.client, "nombre d'arguments=", args.length)
  },
  beforeMount(...args) {
    console.log('[home beforeMount]', 'process.server=', process.server,
 'process.client=', process.client, "nombre d'arguments=", args.length)
  },
  mounted(...args) {
    console.log('[home mounted]', 'process.server=', process.server,
 'process.client=', process.client, "nombre d'arguments=", args.length)
  }
}
</script>
  • 第 5 行:导航组件放置在左侧栏中;
  • 第7–9行:在右侧栏中放置一个提示框;

在 <script> 部分,我们在页面生命周期函数 [beforeCreate, created, beforeMount, beforeMounted] 中放置代码。我们需要了解哪些函数由服务器 []nuxt 执行,哪些由客户端 [nuxt] 执行。让我们回顾两点:

  • 当页面被请求时——无论是应用程序启动时(如 [index] 页面),还是用户手动刷新浏览器页面或手动输入 URL——该页面首先由 [nuxt] 服务器返回。服务器会解析上述代码并执行其中包含的 JavaScript;
  • 当 [nuxt] 服务器发送的页面到达浏览器时,会随附 [nuxt] 客户端代码。随后,客户端代码会重新执行上述代码;
  • 我们通过日志记录来追踪各环节的执行情况,以便更好地理解这一过程;
  • 第 30–31 行:我们在函数中使用了一个全局对象 [process],该对象在服务器和客户端均存在:
    • 若代码由服务器执行,[process.server] 为 true,否则为 false;
    • 若代码由客户端执行,[process.client] 为 true,否则为 false;
    • 由于代码中未声明 [process] 变量,我们需要添加第 14 行以满足 [eslint] 的要求。第 16 行是必要的,否则 [eslint] 会因 [process] 变量而报告另一种类型的错误。第 15 行是必要的,以便在生命周期函数中使用 [console];
  • 第 29 行:我们还想知道生命周期函数是否接收参数。我们确实会发现 [nuxt] 会向某些函数传递信息。我们需要确认生命周期函数是否包含在其中;
  • 我们将相同的代码应用到所有四个函数中;

4.6. [nuxt.config.js] 文件

该文件控制 [dvp] 项目的执行。相关内容已在第 33 页进行过说明。

4.7. 运行项目

我们运行该项目:

Image

显示的页面如下:

Image

在浏览器中加载后,[nuxt] 应用便成为一个标准的 [vue] 应用。因此,我们将不再讨论 [nuxt-01] 应用的客户端行为。相关内容已在文档《通过示例了解 VUE.JS 框架》中的 [vuejs-11] 项目中进行过讲解。

[nuxt] 应用与 [vue] 应用仅在两个方面有所不同:

  • 应用程序的初始启动,此时会渲染首页;
  • 用户以任何方式触发浏览器刷新时;

在这两种情况下:

  • 服务器提供所请求的页面;
  • 接收到的页面由客户端进行处理;

让我们查看应用程序的启动日志(在浏览器中按 F12):

Image

  • 在 [1] 中,是服务器日志(process.server=true)。这些日志前缀为 [Nuxt SSR](SSR = 服务器端渲染);
  • 在 [2] 中,是浏览器中的客户端日志(process.client=true);

根据这些日志,我们可以推断出:

  • 服务器执行 [beforeCreate, created] 生命周期函数;
  • 客户端执行 [beforeCreate, created, beforeMount, mounted] 生命周期函数;
  • 服务器在客户端之前处理了该页面;
  • 在两种情况下,执行的函数均不接收任何参数;

现在让我们查看接收到的页面的源代码(浏览器中的 [页面源代码] 选项):


<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-01/">
  ....
  <link rel="preload" href="/nuxt-01/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/app.js" as="script">
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
              <h4>[nuxt-01] : routage et navigation</h4>
            </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-01/" target="_self" class="nav-link active nuxt-link-active">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-warning">
                    Home
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
    window.__NUXT__ = (function (a, b, c, d, e, f, g, h, i, j) {
      return {
        layout: "default", data: [{}], error: null, serverRendered: true,
        logs: [
          { date: new Date(1574069600078), args: [a, b, c, d, e, f, g, "(repeated 1 times)"], type: h, level: i, tag: j },
          { date: new Date(1574070938091), args: [a, b, c, d, e, f, g], type: h, level: i, tag: j }
        ]
      }
    }("[home beforeCreate]", "process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0", "log", 2, ""));
  </script>
  <script src="/nuxt-01/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-01/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-01/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-01/_nuxt/app.js" defer></script>
</body>
</html>

评论

  • 你可能会首先注意到,收到的 HTML 代码准确地反映了用户所看到的页面。而在 [Vue] 应用中并非如此,当时显示的源代码是一个几乎为空的 HTML 文件的源代码。那正是浏览器接收到的内容。随后 [Vue] 客户端接管并构建了用户所期望的页面。 当时,你必须进入浏览器开发者工具(F12)中的 [检查] 选项卡,才能查看显示页面的 HTML 代码;
  • 第 57–67 行:这是显示带有 [Nuxt SSR] 标签日志的脚本。这些日志在服务器端生成,结果被嵌入到发送页面中包含的脚本中;
  • 第 68–71 行:构成在浏览器中执行的客户端代码的脚本;

第 68–71 行的脚本执行后会对接收到的页面进行转换。要查看最终显示给用户的页面,请转到浏览器开发者工具(F12)中的 [检查器] 选项卡:

Image

展开 <html> 标签 [3] 后,您将看到以下内容:


<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-01/">
  ...
  <link rel="preload" href="/nuxt-01/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/app.js" as="script">
 
  <script charset="utf-8" src="/nuxt-01/_nuxt/pages_index.js"></script>
  <script charset="utf-8" src="/nuxt-01/_nuxt/pages_page1.js"></script>
  <script charset="utf-8" src="/nuxt-01/_nuxt/pages_page2.js"></script>
</head>
<body>
  <div id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-success" align="center">
              <h4>[nuxt-01] : routage et navigation</h4>
            </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-01/" target="_self" class="nav-link active nuxt-link-active">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div>
                <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-warning">
                    Home
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
    window.__NUXT__ = (function (a, b, c, d, e, f, g, h, i) {
      return {
        layout: "default", data: [{}], error: null, serverRendered: true,
        logs: [
          { date: new Date(1574068674481), args: ["[home beforeCreate]", a, b, c, d, e, f], type: g, level: h, tag: i },
          { date: new Date(1574068674482), args: ["[home created]", a, b, c, d, e, f], type: g, level: h, tag: i }
        ]
      }
    }("process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0", "log", 2, ""));
  </script>
  <script src="/nuxt-01/_nuxt/runtime.js" defer=""></script>
  <script src="/nuxt-01/_nuxt/commons.app.js" defer=""></script>
  <script src="/nuxt-01/_nuxt/vendors.app.js" defer=""></script>
  <script src="/nuxt-01/_nuxt/app.js" defer=""></script>
 
  <iframe id="mc-sidebar-container" ...></iframe>
  <iframe id="mc-topbar-container"...>  </iframe>
  <iframe id="mc-toast-container" ...></iframe>
  <iframe id="mc-download-overlay-container"...></iframe>
</body>

评论

  • 乍看之下,第19至59行显示的页面似乎与接收到的页面相同;
  • 第14–16行:出现了三个新脚本,每个应用程序页面各有一个;
  • 第76–79行:出现了四个[iframe]元素;

第 33、37 和 42 行:这些链接存在问题。它们看似普通的链接,点击后应向服务器发送请求。然而,实际执行时我们发现并非如此:并未向服务器发送任何请求。要理解原因,我们需要返回浏览器的 [检查] 标签页:

Image

我们看到链接上已绑定了 [1, 2] 事件。正是第 71–74 行的脚本为这些链接绑定了事件处理程序。因此:

  • 客户端显示的页面在视觉上与服务器发送的页面完全一致;
  • 页面中的动态行为是由客户端添加的;

现在,让我们手动输入 URL [http://192.168.1.128:81/nuxt-01/page1] 来请求 [page1] 页面。日志将变为如下所示:

Image

我们得到了与[index]页面相同的结果,但这次是针对[page1]。收到的页面的源代码如下:


<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success"><h4>[nuxt-01] : routage et navigation</h4></div> <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-01/" target="_self" class="nav-link">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page1" target="_self" class="nav-link active nuxt-link-active">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-primary">
 
                    Page 1
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>window.__NUXT__ = { layout: "default", data: [{}], error: null, serverRendered: true, logs: [{ date: new Date(1573917721122), args: ["[page1 beforeCreate]", "process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0"], type: "log", level: 2, tag: "" }] };</script>
  <script src="/nuxt-01/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-01/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-01/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-01/_nuxt/app.js" defer></script>
</body>

我们得到的页面与 [index] 页面类型相同,但带有 [Page 1] 视图提示(第 30 行)。 第 41–44 行:客户端代码已随页面一同发送。归根结底,手动请求一个 URL 相当于重启应用程序。唯一的区别在于,显示的页面不一定是首页,而是被请求的那个页面。一旦页面接收完成,客户端便接管控制。除非用户另有决定,否则将不再与服务器进行通信。