Vue

如何使用 Vue 前端渲染库与 Meteor 结合。

阅读完本指南后,您将了解

  1. 什么是 Vue,以及为什么您应该考虑将其与 Meteor 一起使用
  2. 如何在您的 Meteor 应用中安装 Vue,以及如何正确使用它
  3. 如何将 Vue 与 Meteor 的实时数据层集成
  4. 如何使用 Vue 构建 Meteor 应用结构
  5. 如何使用 Meteor 进行 Vue 的服务器端渲染 (SSR)

Vue 已经有了一份优秀的指南,涵盖了许多高级主题。其中一些是 SSR(服务器端渲染)路由代码结构和风格指南 以及 使用 Vuex 进行状态管理

本指南纯粹专注于将其与 Meteor 集成。

Meteor 有一个 Vue 脚手架,它会为您准备一个基本的 Vue Meteor 应用。您可以通过运行 meteor create vue-meteor-app --vue 来创建一个。还有一个 Vue 教程 涵盖了本节的基础知识。

简介

Vue(发音为 /vjuː/,类似于 view)是一个用于构建用户界面的渐进式框架。与其他单体框架不同,Vue 从一开始就被设计为可增量采用。核心库仅专注于视图层,易于上手并与其他库或现有项目集成。另一方面,当与 现代工具支持库 结合使用时,Vue 也完全能够为复杂的单页应用程序提供动力。

Vue 有一个优秀的 指南和文档。本指南介绍了如何将其与 Meteor 集成。

为什么将 Vue 与 Meteor 一起使用

Vue 是一个前端库,就像 React、Blaze 和 Angular 一样。

一些非常棒的框架是围绕 Vue 构建的。例如,Nuxt.js 旨在创建一个足够灵活的框架,您可以将其用作主要项目基础或作为基于 Node.js 的当前项目的补充。尽管 Nuxt.js 是全栈的并且具有很强的可插拔性,但它缺乏与服务器之间通信数据的 API。此外,与 Meteor 不同,Nuxt 仍然依赖于配置文件。

Meteor 的构建工具和 Pub/Sub API(或 Apollo)为 Vue 提供了这个 API,而您通常需要自己集成,从而大大减少了您需要编写的样板代码量。

将 Vue 与 Meteor 集成

创建 vue3 应用

meteor create --vue

要启动一个新项目

meteor create vue-meteor-app

要在 Meteor 中安装 Vue,您应该将其添加为 npm 依赖项

meteor npm install --save vue

为了支持 Vue 的单文件组件 以及 .vue 文件扩展名,请安装由 Vue 核心开发者 Akryum (Guillaume Chau) 创建的以下 Meteor 包。

meteor add akryum:vue-component

您最终将至少得到 3 个文件

  1. 一个 /client/App.vue 作为应用的根组件
  2. 一个 /client/main.js 在 Meteor 启动时初始化 Vue 应用
  3. 一个 /client/main.html 包含带有 #app div 的 body

我们需要一个包含 app id 的基本 HTML 文档。如果您从 meteor create . 创建了一个新项目,请将其放入您的 /client/main.html 中。

<body>
  <div id="app"></div>
</body>

您现在可以使用以下格式在您的应用中开始编写 .vue 文件。如果您从 meteor create . 创建了一个新项目,请将其放入您的 /client/App.vue 中。

<template>
  <div>
    <p>This is a Vue component and below is the current date:<br />{{date}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      date: new Date(),
    };
  }
}
</script>

<style scoped>
  p {
    font-size: 2em;
    text-align: center;
  }
</style>

您可以通过在客户端启动文件中使用以下代码段将 Vue 组件层次结构渲染到 DOM。如果您从 meteor create . 创建了一个新项目,请将其放入您的 /client/main.js 中。

import Vue from 'guide/site/content/vue';
import App from './App.vue';
import './main.html';

Meteor.startup(() => {
  new Vue({
    el: '#app',
    ...App,
  });
});

使用此命令运行新的 Vue+Meteor 应用:NO_HMR=1 meteor

使用 Meteor 的数据系统

Meteor 最大的优势之一绝对是它的实时数据层。它允许所谓的全栈反应性和 乐观 UI 功能。为了实现全栈反应性,Meteor 使用 Tracker。在本节中,我们将解释如何将 Meteor Tracker 与 Vue 集成以利用这两个工具的优势。

  1. 从 NPM 安装 vue-meteor-tracker
meteor npm install --save vue-meteor-tracker

接下来,需要将该包作为插件插入 Vue。将以下内容添加到您的 /client/main.js

import Vue from 'vue';
import VueMeteorTracker from 'vue-meteor-tracker'; // import the integration package!
import App from './App.vue';
import './main.html';

Vue.use(VueMeteorTracker);                         // Add the plugin to Vue!

Meteor.startup(() => {
  new Vue({
    el: '#app',
    ...App,
  });
});

示例应用

如果您已遵循 集成指南,则您的 Vue 应用程序会显示其加载时间。

让我们添加一些使这部分动态化的功能。为了灵活地使用 Meteor 的管道,我们将创建

  1. 一个名为 TimeMeteor 集合,其中包含一个 currentTime 文档。
  2. 一个名为 TimeMeteor 发布,用于发送所有文档
  3. 一个名为 UpdateTimeMeteor 方法,用于更新 currentTime 文档。
  4. 一个到 TimeMeteor 订阅
  5. Vue/Meteor 反应性 用于更新 Vue 组件

前 3 个步骤是基本的 Meteor

1) 在 /imports/collections/Time.js

Time = new Mongo.Collection("time");

2) 在 /imports/publications/Time.js

Meteor.publish('Time', function () {
  return Time.find({});
});

3) 在 /imports/methods/UpdateTime.js

Meteor.methods({
  UpdateTime() {
    Time.upsert('currentTime', { $set: { time: new Date() } });
  },
});

现在,让我们将这些添加到我们的服务器中。首先 删除 autopublish,以便我们的发布生效

meteor remove autopublish

为了好玩,让我们创建一个 settings.json 文件

{ "public": { "hello": "world" } }

现在,让我们更新我们的 /server/main.js 以使用我们的新内容

import { Meteor } from 'meteor/meteor';

import '/imports/collections/Time';
import '/imports/publications/Time';
import '/imports/methods/UpdateTime';

Meteor.startup(() => {
  // Update the current time
  Meteor.call('UpdateTime');
  // Add a new doc on each start.
  Time.insert({ time: new Date() });
  // Print the current time from the database
  console.log(`The time is now ${Time.findOne().time}`);
});

启动您的 Meteor 应用,您应该会看到一条从 Mongo 拉取数据的消息。我们还没有对客户端进行任何更改,因此您应该只会看到一些启动消息。

meteor

4) 和 5) 太好了,让我们使用 Vue Meteor Tracker 将其与 Vue 集成,并更新我们的 /client/App.vue 文件

<template>
  <div>
    <div v-if="!$subReady.Time">Loading...</div>
    <div v-else>
      <p>Hello {{hello}},
        <br>The time is now: {{currentTime}}
      </p>
      <button @click="updateTime">Update Time</button>
      <p>Startup times:</p>
      <ul>
        <li v-for="t in TimeCursor">
          {{t.time}}  -  {{t._id}}
        </li>
      </ul>
      <p>Meteor settings</p>
      <pre><code>
        {{settings}}
      </code></pre>
    </div>
  </div>
</template>

<script>
import '/imports/collections/Time';

export default {
  data() {
    console.log('Sending non-Meteor data to Vue component');
    return {
      hello: 'World',
      settings: Meteor.settings.public,   // not Meteor reactive
    }
  },
  // Vue Methods
  methods: {  
    updateTime() {
      console.log('Calling Meteor Method UpdateTime');
      Meteor.call('UpdateTime');          // not Meteor reactive
    }
  },
  // Meteor reactivity
  meteor: {
    // Subscriptions - Errors not reported spelling and capitalization.
    $subscribe: {
      'Time': []
    },
    // A helper function to get the current time
    currentTime () {
      console.log('Calculating currentTime');
      var t = Time.findOne('currentTime') || {};
      return t.time;
    },
    // A Minimongo cursor on the Time collection is added to the Vue instance
    TimeCursor () {
      // Here you can use Meteor reactive sources like cursors or reactive vars
      // as you would in a Blaze template helper
      return Time.find({}, {
        sort: {time: -1}
      })
    },
  }
}
</script>

<style scoped>
  p {
    font-size: 2em;
  }
</style>

重新启动服务器以使用 settings.json 文件。

meteor --settings=settings.json 

然后刷新您的浏览器以重新加载客户端。

您应该会看到

  • 当前时间
  • 一个更新当前时间的按钮
  • 服务器的启动时间(在启动时添加到 Time 集合中)
  • 来自您的设置文件的 Meteor 设置

太棒了!这是一个关于 Meteor 一些功能以及如何与 Vue 集成的概述。有更好的方法吗?请发送 PR。

风格指南和文件结构

代码风格检查和风格指南是使代码更易于使用和更有趣的工具。

这些是达到实用目的的实用手段。

  1. 利用现有工具
  2. 利用现有配置

Meteor 的风格指南Vue 的风格指南 可以像这样重叠

  1. 配置您的编辑器
  2. 为 Meteor 配置 eslint
  3. 查看 Vue 风格指南
  4. 根据需要打开 ESLint 规则

应用结构在此处记录

  1. Meteor 的应用结构 是默认的起点。
  2. Vuex 的应用结构 可能也很有趣。

SSR 和代码分割

Vue 有 关于如何在服务器上渲染 Vue 应用程序的优秀指南。它包括代码分割、异步数据获取以及大多数需要此功能的应用程序中使用的许多其他实践。

基本示例

使 Vue SSR 与 Meteor 一起工作并不比例如与 Express 一起工作更复杂。但是,Meteor 没有定义通配符路由,而是使用自己的 server-render 包,该包公开了一个 onPageLoad 函数。每次对服务器端进行调用时,都会触发此函数。这就是我们应该放置代码的地方,就像 VueJS SSR 指南 中描述的那样。

要添加包,请运行

meteor add server-render
meteor npm install --save vue-server-renderer

然后在 /server/main.js 中连接到 Vue

import { Meteor } from 'meteor/meteor';
import Vue from 'vue';
import { onPageLoad } from 'meteor/server-render';
import { createRenderer } from 'vue-server-renderer';

const renderer = createRenderer();

onPageLoad(sink => {
  console.log('onPageLoad');
  
  const url = sink.request.url.path;
  
  const app = new Vue({
    data: {
      url
    },
    template: `<div>The visited URL is: {{ url }}</div>`
  });

  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error');
      return
    }
    console.log('html', html);
    
    sink.renderIntoElementById('app', html);
  })
})

幸运的是,Akryum 为我们提供了帮助,并为我们提供了 Meteor 包:akryum:vue-ssr 允许我们像下面这样编写服务器端代码

import { VueSSR } from 'meteor/akryum:vue-ssr';
import createApp from './app';

VueSSR.createApp = function () {
  // Initialize the Vue app instance and return the app instance
  const { app } = createApp(); 
  return { app };
}

服务器端路由

很棒,但是大多数应用程序都具有一些路由功能。我们可以为此使用 VueSSR 上下文参数。它只是传递 Meteor 服务器渲染请求 URL,我们需要将其推送到我们的路由器实例中

import { VueSSR } from 'meteor/akryum:vue-ssr';
import createApp from './app';

VueSSR.createApp = function (context) {
  // Initialize the Vue app instance and return the app + router instance
  const { app, router } = createApp(); 
  
  // Set router's location from the context
  router.push(context.url);
  
  return { app };
}

异步数据和水合

水合是指在服务器端将状态加载到组件中,然后在客户端重用这些数据。这允许组件在服务器上完全渲染其标记,并在加载 bundle 时防止客户端“重新渲染”。

Nuxt 使用名为 asyncData 的功能优雅地解决了此问题。

Meteor 的 pub/sub 系统目前不适合 SSR,这意味着如果我们想要相同的功能,就必须自己实现。如何实现正是我们接下来要解释的!

这里需要提醒的是,服务器端渲染本身就值得一篇指南 - 这正是 Vue 团队所做的。大多数代码在任何平台上都是需要的,除了 Nuxt(基于 Vue)和 Next(基于 React)。我们只是描述了在 Meteor 中实现此功能的最佳方法。要真正理解发生了什么,请阅读 Vue 的 SSR 指南。

SSR 遵循几个步骤,这些步骤对于任何前端库(React、Vue 或 Angular)来说几乎总是相同的。

  1. 使用路由器解析 URL
  2. 从路由器中获取任何匹配的组件
  3. 过滤掉没有 asyncData 的组件
  4. 通过返回 asyncData 方法的结果,将组件映射到一个 Promise 列表中
  5. 解析所有 Promise
  6. 将结果数据存储在 HTML 中,以便稍后客户端 bundle 进行水合
  7. 客户端水合

在代码中记录得更好

VueSSR.createApp = function (context) {

  // Wait with sending the app to the client until the promise resolves (thanks Akryum)
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp({
      ssr: true,
    });

    // 1. Resolve the URL with the router
    router.push(context.url);
    
    router.onReady(async () => {
      // 2, Fetch any matching components from the router
      const matchedComponents = router.getMatchedComponents();
      
      const route = router.currentRoute;
    
      // No matched routes
      if (!matchedComponents.length) {
        reject(new Error('not-found'));
      }
      
      // 3. Filter out components that have no asyncData
      const componentsWithAsyncData = matchedComponents.filter(component => component.asyncData);

      // 4. Map the components into a list of promises 
      // by returning the asyncData method's result
      const asyncDataPromises = componentsWithAsyncData.map(component => (
        component.asyncData({ store, route })
      ));
      
      // You can have the asyncData methods resolve promises with data. 
      // However to avoid complexity its recommended to leverage Vuex
      // In our case we're simply calling Vuex actions in our methods 
      // that do the fetching and storing of the data. This makes the below 
      // step really simple
      
      // 5. Resolve all promises. (that's it)
      await Promise.all(asyncDataPromises);
      
      // From this point on we can assume that all the needed data is stored 
      // in the Vuex store. Now we simply need to grap it and push it into 
      // the HTML as a "javascript string"
      
      // 6. Store the data in the HTML for later hydration of the client bundle
      const js = `window.__INITIAL_STATE__=${JSON.stringify(store.state)};`;
      
      // Resolve the promise with the same object as the simple version
      // Push our javascript string into the resolver. 
      // The VueSSR package takes care of the rest
      resolve({
        app,
        js, 
      });      
    });
  }); 
};

太棒了。当我们在浏览器中加载我们的应用程序时,您应该会看到一个奇怪的效果。应用程序似乎加载正确了。这是服务器端渲染发挥作用的结果。但是,在一秒钟后,应用程序突然又变空了。

这是因为当客户端 bundle 接管时,它还没有获取到数据。它将用一个空的应用程序覆盖 HTML!我们需要使用 HTML 中的 JSON 数据来水合 bundle。

如果您通过源代码视图检查 HTML,您将看到应用程序的 HTML 源代码以及填充了 JSON 字符串的 __INITIAL_STATE=""。我们需要使用它来水合客户端。幸运的是,这相当容易,因为我们只有一个需要水合的地方:Vuex 存储!

import { Meteor } from 'meteor/meteor';
import createApp from './app';

Meteor.startup(() => {
  const { store, router } = createApp({ // Same function as the server
    ssr: false,
  });

  // Hydrate the Vuex store with the JSON string
  if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__);
  }
});

现在,当我们加载我们的 bundle 时,组件应该从存储中获取数据。一切正常。但是还有一件事要做。如果我们导航,我们新渲染的客户端组件将再次没有任何数据。这是因为 asyncData 方法尚未在客户端调用。我们可以使用如下所示的 mixin 来修复此问题,如 Vue SSR 指南 中所述。

Vue.mixin({
  beforeMount () {
    const { asyncData } = this.$options
    if (asyncData) {
      // assign the fetch operation to a promise
      // so that in components we can do `this.dataPromise.then(...)` to
      // perform other tasks after data is ready
      this.dataPromise = asyncData({
        store: this.$store,
        route: this.$route
      })
    }
  }
})

现在,我们拥有了一个完全正常运行且服务器端渲染的 Meteor 中的 Vue 应用程序!

在 GitHub 上编辑
// 搜索框