跳至内容

模块

本文档解释了 Meteor 使用的模块系统的使用方法和关键功能。

Meteor 1.2 引入了对 许多新的 ECMAScript 2015 功能的支持,其中最值得注意的遗漏之一是 ES2015 的 importexport 语法

Meteor 1.3 通过一个完全符合标准的模块系统填补了这一空白,该系统在客户端和服务器端均可使用。

Meteor 1.7 在 package.json 中引入了 meteor.mainModulemeteor.testModule,因此 Meteor 不再需要专门的文件夹来存放 js 资源。也不需要急切加载 js 资源。

根据设计,meteor.mainModule 仅影响 js 资源。对于非 js 资源,仍然有一些事情只能在导入中完成

  • 只有导入中的样式表可以动态导入
  • 如果样式表位于导入中,则只能通过在 js 中导入它们来控制样式表的加载顺序

导入之外(以及其他一些特殊文件夹)的任何非 js 资源仍然会被急切加载。

您可以在此 评论中阅读有关这些差异的更多信息。

启用模块

它默认安装在所有新的应用程序和包中。尽管如此,modules 包是完全可选的。

如果您想将其添加到现有的应用程序或包中

对于应用程序,这就像 meteor add modules 一样简单,或者(甚至更好)meteor add ecmascript,因为 ecmascript意味着 modules 包。

对于包,您可以通过将 api.use('modules') 添加到 package.js 文件的 Package.onUsePackage.onTest 部分来启用 modules

现在,您可能想知道在没有 ecmascript 包的情况下 modules 包有什么用,因为 ecmascript 启用了 importexport 语法。modules 包本身提供了 CommonJS 的 requireexports 原语,如果您曾经编写过 Node 代码,可能会很熟悉这些原语,而 ecmascript 包只是将 importexport 语句编译成 CommonJS。requireexport 原语还允许 Node 模块在无需修改的情况下在 Meteor 应用程序代码中运行。此外,保持 modules 的独立性使我们能够在使用 ecmascript 比较棘手的地方(例如 ecmascript 包本身的实现)使用 requireexports

虽然 modules 包本身很有用,但我们强烈建议使用 ecmascript 包(以及 importexport),而不是直接使用 requireexports。如果您需要说服,这里有一个 演示文稿解释了差异。

基本语法

ES2015

尽管 importexport 语法有很多不同的变体,但本节描述了每个人都应该知道的必要形式。

首先,您可以在声明变量的同时 export 任何命名声明

js
// exporter.js
export var a = ...;
export let b = ...;
export const c = ...;
export function d() { ... }
export function* e() { ... }
export class F { ... }

这些声明使变量 abc(等等)不仅在 exporter.js 模块的范围内可用,而且对从 exporter.js import 的其他模块也可用。

如果您愿意,您可以按名称 export 变量,而不是在其声明前加上 export 关键字

js
// exporter.js
function g() { ... }
let h = g();

// At the end of the file
export { g, h };

所有这些导出都是命名的,这意味着其他模块可以使用这些名称导入它们

js
// importer.js
import { a, c, F, h } from './exporter';
new F(a, c).method(h);

如果您希望使用不同的名称,您会很高兴知道 exportimport 语句可以重命名其参数

js
// exporter.js
export { g as x };
g(); // Same as calling `y()` in importer.js
js
// importer.js
import { x as y } from './exporter';
y(); // Same as calling `g()` in exporter.js

与 CommonJS module.exports 一样,可以定义单个默认导出

js
// exporter.js
export default any.arbitrary(expression);

然后,可以使用任何导入模块选择的名称在没有花括号的情况下导入此默认导出

js
// importer.js
import Value from './exporter';
// Value is identical to the exported expression

与 CommonJS module.exports 不同,使用默认导出不会阻止同时使用命名导出。以下是您可以组合它们的方式

js
// importer.js
import Value, { a, F } from './exporter';

事实上,默认导出在概念上只是另一个名为“default”的命名导出

js
// importer.js
import { default as Value, a, F } from './exporter';

这些示例应该让您开始使用 importexport 语法。要了解更多信息,这里有一个由 Axel Rauschmayer 提供的关于 importexport 语法每个变体的非常详细的 解释

CommonJS

您无需使用 ecmascript 包或 ES2015 语法即可使用模块。就像 ES2015 之前的 Node.js 一样,您可以使用 requiremodule.exports——无论如何,importexport 语句就是编译成这些的。

以下 ES2015 import

js
import { AccountsTemplates } from 'meteor/useraccounts:core';
import '../imports/startup/client/routes.js';

可以用 CommonJS 这样写

js
var UserAccountsCore = require('meteor/useraccounts:core');
require('../imports/startup/client/routes.js');

并且您可以通过 UserAccountsCore.AccountsTemplates 访问 AccountsTemplates

请注意,如果像此示例中的 routes.js 一样,在没有赋值给任何变量的情况下进行 require,则文件不需要 module.exportsroutes.js 中的代码将简单地包含在上述 require 语句的位置并执行。

以下 ES2015 export 语句

js
export const insert = new ValidatedMethod({ ... });
export default incompleteCountDenormalizer;

可以重写为使用 CommonJS module.exports

js
module.exports.insert = new ValidatedMethod({ ... });
module.exports.default = incompleteCountDenormalizer;

如果您愿意,您也可以简单地编写 exports 而不是 module.exports。如果您需要从具有 default 导出的 ES2015 模块中 require,则可以使用 require('package').default 访问导出。

有一种情况您可能需要使用 CommonJS,即使您的项目有 ecmascript 包:如果您想有条件地包含模块。import 语句必须位于顶级作用域,因此不能位于 if 块内。如果您正在编写一个在客户端和服务器端都加载的通用文件,您可能只想在一个或另一个环境中导入一个模块

js
if (Meteor.isClient) {
  require('./client-only-file.js');
}

请注意,对 require() 的动态调用(其中要请求的名称可以在运行时更改)无法正确分析,并可能导致客户端捆绑包损坏。这也在 指南中进行了讨论。

CoffeeScript

从 Meteor 的早期开始,CoffeeScript 就一直是一种一流的支持语言。即使今天我们推荐 ES2015,我们仍然打算完全支持 CoffeeScript。

从 CoffeeScript 1.11.0 开始,CoffeeScript 本身支持 importexport 语句。确保您在项目中使用最新版本的 CoffeeScript 包以获得此支持。今天创建的新项目将通过 meteor add coffeescript 获取此版本。确保不要忘记包含 ecmascriptmodules 包:meteor add ecmascript。(modules 包由 ecmascript 暗示。)

CoffeeScript import 语法与您上面看到的 ES2015 语法几乎相同

coffee
import { Meteor } from 'meteor/meteor'
import SimpleSchema from 'simpl-schema'
import { Lists } from './lists.coffee'

您也可以将传统的 CommonJS 语法与 CoffeeScript 一起使用。

模块化应用程序结构

在应用程序的 package.json 文件中使用 meteor 部分。

这从 Meteor 1.7 开始可用

json
{
  "meteor": {
    "mainModule": {
      "client": "client/main.js",
      "server": "server/main.js"
    }
  }
}

指定后,这些入口点将定义 Meteor 将在哪些文件中开始针对每个架构(客户端和服务器)的评估过程。

这样,Meteor 就不会急切加载任何其他 js 文件。

还有一个用于 legacy 客户端的架构,如果您希望在导入现代客户端的主模块之前为旧浏览器加载 polyfill 或其他代码,这将非常有用。

除了 meteor.mainModule 之外,package.jsonmeteor 部分还可以指定 meteor.testModule 来控制 meteor testmeteor test --full-app 加载哪些测试模块

json
{
  "meteor": {
    "mainModule": {
      "client": "client/main.js",
      "server": "server/main.js"
    },
    "testModule": "tests.js"
  }
}

如果您的客户端和服务器测试文件不同,您可以使用与 mainModule 相同的语法扩展 testModule 配置

json
{
  "meteor": {
    "mainModule": {
      "client": "client/main.js",
      "server": "server/main.js"
    },
    "testModule": {
      "client": "client/tests.js",
      "server": "server/tests.js"
    }
  }
}

无论您是否使用 --full-app 选项,都将加载相同的测试模块。

任何需要检测 --full-app 的测试都应检查 Meteor.isAppTest

meteor.testModule指定的模块可以运行时导入其他测试模块,因此您仍然可以跨代码库分发测试文件;只需确保导入要运行的模块即可。

要禁用在给定架构上模块的急切加载,只需提供一个值为false的mainModule即可。

json
{
  "meteor": {
    "mainModule": {
      "client": false,
      "server": "server/main.js"
    }
  }
}

模块化应用程序结构的历史

如果您想了解在package.json中没有meteor.mainModule的情况下Meteor是如何工作的,请继续阅读本节,但我们不再推荐这种方法。

在Meteor 1.3发布之前,在应用程序中文件之间共享值的唯一方法是将它们分配给全局变量或通过共享变量(如Session)进行通信(这些变量虽然在技术上不是全局变量,但在语法上确实与全局变量非常相似)。随着模块的引入,一个模块可以精确地引用任何其他特定模块的导出,因此全局变量不再必要。

如果您熟悉Node中的模块,您可能会期望模块直到第一次导入时才会被评估。但是,由于早期版本的Meteor在应用程序启动时评估了所有代码,并且我们关心向后兼容性,因此急切评估仍然是默认行为。

如果您希望一个模块被延迟评估(换句话说:按需,在您第一次导入它时,就像Node那样),那么您应该将该模块放在imports/目录中(在您的应用程序中的任何位置,而不仅仅是根目录),并在导入模块时包含该目录:import {stuff} from './imports/lazy'。注意:node_modules/目录包含的文件也将被延迟评估(稍后详细介绍)。

模块化包结构

如果您是包作者,除了在package.js文件的Package.onUse部分中放入api.use('modules')api.use('ecmascript')之外,您还可以使用一个名为api.mainModule的新API来指定包的主入口点。

js
Package.describe({
  name: 'my-modular-package'
});

Npm.depends({
  moment: '2.10.6'
});

Package.onUse((api) => {
  api.use('modules');
  api.mainModule('server.js', 'server');
  api.mainModule('client.js', 'client');
  api.export('Foo');
});

现在server.jsclient.js可以从包源目录导入其他文件,即使这些文件没有使用api.addFiles函数添加。

当您使用api.mainModule时,主模块的导出将作为Package['my-modular-package']全局公开,以及api.export导出的任何符号,因此任何导入该包的代码都可以使用它们。换句话说,主模块可以决定api.export将导出Foo的什么值,以及提供可以从包中显式导入的其他属性。

js
// In an application that uses 'my-modular-package':
import { Foo as ExplicitFoo, bar } from 'meteor/my-modular-package';
console.log(Foo); // Auto-imported because of `api.export`.
console.log(ExplicitFoo); // Explicitly imported, but identical to `Foo`.
console.log(bar); // Exported by server.js or client.js, but not auto-imported.

请注意,importfrom 'meteor/my-modular-package',而不是from 'my-modular-package'。Meteor包标识符字符串必须包含前缀meteor/...以将其与npm包区分开来。

最后,由于此包正在使用新的modules包,并且包Npm.depends于“moment” npm包,因此包中的模块可以在客户端和服务器上都import moment from 'moment'。这是一个好消息,因为之前版本的Meteor只允许在服务器上通过Npm.require进行npm导入。

从包中延迟加载模块

包还可以指定一个延迟主模块。

js
Package.onUse(function (api) {
  api.mainModule("client.js", "client", { lazy: true });
});

这意味着除非/直到另一个模块导入它,否则client.js模块不会在应用程序启动期间被评估,并且如果找不到导入代码,甚至不会包含在客户端包中。

要导入名为exportedPackageMethod的方法,只需

js
import { exportedPackageMethod } from "meteor/<package name>";

注意:具有lazy主模块的包不能使用api.export将全局符号导出到其他包/应用程序。此外,在Meteor 1.4.4.2之前,有必要显式命名包含模块的文件:import "meteor/<package name>/client.js"

本地node_modules

在Meteor 1.3之前,Meteor应用程序代码中node_modules目录的内容完全被忽略。当您启用modules时,这些无用的node_modules目录突然变得更有用。

sh
meteor create modular-app
cd modular-app
mkdir node_modules
npm install moment
echo "import moment from 'moment';" >> modular-app.js
echo 'console.log(moment().calendar());' >> modular-app.js
meteor

当您运行此应用程序时,moment库将在客户端和服务器上都被导入,并且两个控制台都将记录类似于以下内容的输出:Today at 7:51 PM。我们希望在应用程序中直接安装Node模块的可能性将减少对npm包装器包(例如https://atmospherejs.com/momentjs/moment)的需求。

每个Meteor安装都捆绑了一个版本的npm命令,并且(从Meteor 1.3开始)它非常易于使用:meteor npm ...npm ...同义,因此meteor npm install moment将在上面的示例中起作用。(同样,如果您没有安装版本的node,或者您想确保您使用的是与Meteor使用的完全相同的node版本,meteor node ...是一个方便的快捷方式。)也就是说,您可以使用任何碰巧可用的npm版本。Meteor的模块系统只关心npm安装的文件,而不关心npm如何安装这些文件的细节。

文件加载顺序

在Meteor 1.3之前,应用程序文件评估的顺序由Meteor指南的应用程序结构 - 默认文件加载顺序部分中描述的一组规则决定。当一个文件依赖于另一个文件中定义的变量时,这些规则可能会令人沮丧,尤其是在第一个文件在第二个文件之后被评估时。

借助模块,您可以通过添加import语句来解决您可能想到的任何加载顺序依赖关系。因此,如果a.js由于文件名而先于b.js加载,但a.js需要b.js定义的内容,那么a.js只需从b.jsimport该值即可。

js
// a.js
import { bThing } from './b';
console.log(bThing, 'in a.js');
js
// b.js
export var bThing = 'a thing defined in b.js';
console.log(bThing, 'in b.js');

有时模块实际上不需要从另一个模块导入任何内容,但您仍然希望确保另一个模块先被评估。在这种情况下,您可以使用更简单的import语法。

js
// c.js
import './a';
console.log('in c.js');

无论这些模块中的哪一个首先被导入,console.log调用的顺序始终为

js
console.log(bThing, 'in b.js');
console.log(bThing, 'in a.js');
console.log('in c.js');