Skip to content

模块

¥Modules

本文档解释了 Meteor 使用的模块系统的用法和主要功能。

¥This document explains the usage and key features of the module system used by Meteor.

Meteor 1.2 引入了对 许多新的 ECMAScript 2015 功能 的支持,最明显的遗漏之一是 ES2015 importexport 语法

¥Meteor 1.2 introduced support for many new ECMAScript 2015 features, one of the most notable omissions was ES2015 import and export syntax.

Meteor 1.3 填补了这一空白,它使用完全符合标准的模块系统,可在客户端和服务器上运行。

¥Meteor 1.3 filled the gap with a fully standards-compliant module system that works on both the client and the server.

Meteor 1.7 将 meteor.mainModulemeteor.testModule 引入到 package.json,因此 Meteor 不再需要为 js 资源设置特殊文件夹。此外,也不需要预加载 js 资源。

¥Meteor 1.7 introduced meteor.mainModule and meteor.testModule to package.json so Meteor doesn't need special folders anymore for js resources. Also doesn't need to eager load js resources.

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

¥By design, meteor.mainModule only affect js resources. For non-js resources, there are still some things that can only be done within imports:

  • 只能动态导入导入中的样式表

    ¥only stylesheets within imports can be dynamically imported

  • 如果样式表在导入范围内,你只能通过在 js 中导入它们来控制样式表的加载顺序

    ¥you can only control the load order of stylesheets by importing them in js if the stylesheets are within imports

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

¥Any non-js resource outside of imports (and some other special folders) are still eagerly loaded.

你可以在此 comment 中阅读有关这些差异的更多信息。

¥You can read more about these differences in this comment.

启用模块

¥Enabling modules

它默认安装在所有新应用和软件包中。不过,modules 包是完全可选的。

¥It is installed by default for all new apps and packages. Nevertheless, the modules package is totally optional.

如果你想将其添加到现有的应用或软件包中:

¥If you want to add it to existent apps or packages:

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

¥For apps, this is as easy as meteor add modules, or (even better) meteor add ecmascript, since the ecmascript package implies the modules package.

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

¥For packages, you can enable modules by adding api.use('modules') to the Package.onUse or Package.onTest sections of your package.js file.

现在,你可能想知道如果没有 ecmascript 包,modules 包有什么用,因为 ecmascript 启用了 importexport 语法。modules 包本身提供了 CommonJS requireexports 原语,如果你曾经编写过 Node 代码,那么这些原语可能很熟悉,而 ecmascript 包只是将 importexport 语句编译为 CommonJS。requireexport 原语还允许 Node 模块在 Meteor 应用代码中运行而无需修改。此外,将 modules 分开允许我们在使用 ecmascript 很棘手的地方使用 requireexports,例如 ecmascript 包本身的实现。

¥Now, you might be wondering what good the modules package is without the ecmascript package, since ecmascript enables import and export syntax. By itself, the modules package provides the CommonJS require and exports primitives that may be familiar if you’ve ever written Node code, and the ecmascript package simply compiles import and export statements to CommonJS. The require and export primitives also allow Node modules to run within Meteor application code without modification. Furthermore, keeping modules separate allows us to use require and exports in places where using ecmascript is tricky, such as the implementation of the ecmascript package itself.

虽然 modules 包本身很有用,但我们非常鼓励使用 ecmascript 包(以及 importexport),而不是直接使用 requireexports。如果你需要说服力,这里有一个 presentation 可以解释差异。

¥While the modules package is useful by itself, we very much encourage using the ecmascript package (and thus import and export) instead of using require and exports directly. If you need convincing, here's a presentation that explains the differences.

基本语法

¥Basic syntax

ES2015

虽然 importexport 语法有许多不同的变体,但本节介绍了每个人都应该知道的基本形式。

¥Although there are a number of different variations of import and export syntax, this section describes the essential forms that everyone should know.

首先,你可以在声明的同一行上 export 任何命名声明:

¥First, you can export any named declaration on the same line where it was declared:

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 等其他模块中也可用。

¥These declarations make the variables a, b, c (and so on) available not only within the scope of the exporter.js module, but also to other modules that import from exporter.js.

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

¥If you prefer, you can export variables by name, rather than prefixing their declarations with the export keyword:

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

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

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

¥All of these exports are named, which means other modules can import them using those names:

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

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

¥If you’d rather use different names, you’ll be glad to know export and import statements can rename their arguments:

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 一样,可以定义单个默认导出:

¥As with CommonJS module.exports, it is possible to define a single default export:

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

然后可以使用导入模块选择的任何名称导入此默认导出,而无需使用大括号:

¥This default export may then be imported without curly braces, using any name the importing module chooses:

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

与 CommonJS module.exports 不同,使用默认导出不会阻止同时使用命名导出。下面是你可以如何组合它们:

¥Unlike CommonJS module.exports, the use of default exports does not prevent the simultaneous use of named exports. Here is how you can combine them:

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

事实上,默认导出在概念上只是另一个命名导出,其名称恰好是 "default":

¥In fact, the default export is conceptually just another named export whose name happens to be "default":

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

这些示例应该可以帮助你开始使用 importexport 语法。如需进一步阅读,这里有一份非常详细的 explanation by Axel Rauschmayer,其中包含 importexport 语法的每个变体。

¥These examples should get you started with import and export syntax. For further reading, here is a very detailed explanation by Axel Rauschmayer of every variation of import and export syntax.

CommonJS

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

¥You don’t need to use the ecmascript package or ES2015 syntax in order to use modules. Just like Node.js in the pre-ES2015 days, you can use require and module.exports—that’s what the import and export statements are compiling into, anyway.

ES2015 import 行如下:

¥ES2015 import lines like these:

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

可以用 CommonJS 像这样编写:

¥can be written with CommonJS like this:

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

你可以通过 UserAccountsCore.AccountsTemplates 访问 AccountsTemplates

¥and you can access AccountsTemplates via UserAccountsCore.AccountsTemplates.

请注意,如果文件像本例中的 routes.js 一样需要 module.exports,则不需要分配给任何变量。routes.js 中的代码将简单地包含并执行,代替上面的 require 语句。

¥Note that files don’t need a module.exports if they’re required like routes.js is in this example, without assignment to any variable. The code in routes.js will simply be included and executed in place of the above require statement.

ES2015 export 语句如下:

¥ES2015 export statements like these:

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

可以使用 CommonJS module.exports 重写:

¥can be rewritten to use CommonJS module.exports:

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

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

¥You can also simply write exports instead of module.exports if you prefer. If you need to require from an ES2015 module with a default export, you can access the export with require('package').default.

在某些情况下,即使你的项目有 ecmascript 包,也可能需要使用 CommonJS:如果你想有条件地包含模块。import 语句必须位于顶层范围,因此它们不能位于 if 块内。如果你正在编写一个通用文件,并在客户端和服务器上加载,你可能只想在一个或另一个环境中导入模块:

¥There is a case where you might need to use CommonJS, even if your project has the ecmascript package: if you want to conditionally include a module. import statements must be at top-level scope, so they cannot be within an if block. If you’re writing a common file, loaded on both client and server, you might want to import a module in only one or the other environment:

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

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

¥Note that dynamic calls to require() (where the name being required can change at runtime) cannot be analyzed correctly and may result in broken client bundles. This is also discussed in the guide.

CoffeeScript

自 Meteor 早期以来,CoffeeScript 一直是一流的支持语言。尽管我们今天推荐 ES2015,但我们仍打算完全支持 CoffeeScript。

¥CoffeeScript has been a first-class supported language since Meteor’s early days. Even though today we recommend ES2015, we still intend to support CoffeeScript fully.

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

¥As of CoffeeScript 1.11.0, CoffeeScript supports import and export statements natively. Make sure you are using the latest version of the CoffeeScript package in your project to get this support. New projects created today will get this version with meteor add coffeescript. Make sure you don’t forget to include the ecmascript and modules packages: meteor add ecmascript. (The modules package is implied by ecmascript.)

CoffeeScript import 语法与你在上面看到的 ES2015 语法几乎相同:

¥CoffeeScript import syntax is nearly identical to the ES2015 syntax you see above:

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

你还可以将传统的 CommonJS 语法与 CoffeeScript 结合使用。

¥You can also use traditional CommonJS syntax with CoffeeScript.

模块化应用结构

¥Modular application structure

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

¥Use in your application package.json file the section meteor.

从 Meteor 1.7 开始,此功能可用

¥This is available since Meteor 1.7

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

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

¥When specified, these entry points will define in which files Meteor is going to start the evaluation process for each architecture (client and server).

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

¥This way Meteor is not going to eager load any other js files.

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

¥There is also an architecture for the legacy client, which is useful if you want to load polyfills or other code for old browsers before importing the main module for the modern client.

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

¥In addition to meteor.mainModule, the meteor section of package.json may also specify meteor.testModule to control which test modules are loaded by meteor test or meteor test --full-app:

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

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

¥If your client and server test files are different, you can expand the testModule configuration using the same syntax as mainModule:

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

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

¥The same test module will be loaded whether or not you use the --full-app option.

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

¥Any tests that need to detect --full-app should check Meteor.isAppTest.

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

¥The module(s) specified by meteor.testModule can import other test modules at runtime, so you can still distribute test files across your codebase; just make sure you import the ones you want to run.

要在给定架构上禁用模块的预先加载,只需提供 mainModule 值 false:

¥To disable eager loading of modules on a given architecture, simply provide a mainModule value of false:

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

模块化应用结构背后的历史

¥Historic behind Modular application structure

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

¥If you want to understand how Meteor works without meteor.mainModule on package.json keep reading this section, but we don't recommend this approach anymore.

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

¥Before the release of Meteor 1.3, the only way to share values between files in an application was to assign them to global variables or communicate through shared variables like Session (variables which, while not technically global, sure do feel syntactically identical to global variables). With the introduction of modules, one module can refer precisely to the exports of any other specific module, so global variables are no longer necessary.

如果你熟悉 Node 中的模块,你可能希望在第一次导入模块之前不会对其进行评估。但是,由于早期版本的 Meteor 在应用启动时评估了你的所有代码,并且我们关心向后兼容性,因此预评估仍然是默认行为。

¥If you are familiar with modules in Node, you might expect modules not to be evaluated until the first time you import them. However, because earlier versions of Meteor evaluated all of your code when the application started, and we care about backwards compatibility, eager evaluation is still the default behavior.

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

¥If you would like a module to be evaluated lazily (in other words: on demand, the first time you import it, just like Node does it), then you should put that module in an imports/ directory (anywhere in your app, not just the root directory), and include that directory when you import the module: import {stuff} from './imports/lazy'. Note: files contained by node_modules/ directories will also be evaluated lazily (more on that below).

模块化包结构

¥Modular package structure

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

¥If you are a package author, in addition to putting api.use('modules') or api.use('ecmascript') in the Package.onUse section of your package.js file, you can also use a new API called api.mainModule to specify the main entry point for your package:

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 函数添加。

¥Now server.js and client.js can import other files from the package source directory, even if those files have not been added using the api.addFiles function.

当你使用 api.mainModule 时,主模块的导出将作为 Package['my-modular-package'] 全局公开,以及 api.export 导出的任何符号,因此可供导入包的任何代码使用。换句话说,主模块可以决定 api.export 将导出 Foo 的哪个值,并提供可以从包中明确导入的其他属性:

¥When you use api.mainModule, the exports of the main module are exposed globally as Package['my-modular-package'], along with any symbols exported by api.export, and thus become available to any code that imports the package. In other words, the main module gets to decide what value of Foo will be exported by api.export, as well as providing other properties that can be explicitly imported from the package:

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 包区分开来。

¥Note that the import is from 'meteor/my-modular-package', not from 'my-modular-package'. Meteor package identifier strings must include the prefix meteor/... to disambiguate them from npm packages.

最后,由于此包使用的是新的 modules 包,而 "moment" npm 包上的包是 Npm.depends,因此包内的模块可以在客户端和服务器上 import moment from 'moment'。这是个好消息,因为以前的 Meteor 版本仅允许通过 Npm.require 在服务器上进行 npm 导入。

¥Finally, since this package is using the new modules package, and the package Npm.depends on the "moment" npm package, modules within the package can import moment from 'moment' on both the client and the server. This is great news, because previous versions of Meteor allowed npm imports only on the server, via Npm.require.

从包中延迟加载模块

¥Lazy loading modules from a package

包还可以指定惰性主模块:

¥Packages can also specify a lazy main module:

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

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

¥This means the client.js module will not be evaluated during app startup unless/until another module imports it, and will not even be included in the client bundle if no importing code is found.

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

¥To import a method named exportedPackageMethod, simply:

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

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

¥Note: Packages with lazy main modules cannot use api.export to export global symbols to other packages/apps. Also, prior to Meteor 1.4.4.2 it is necessary to explicitly name the file containing the module: import "meteor/<package name>/client.js".

本地 node_modules

¥Local node_modules

在 Meteor 1.3 之前,Meteor 应用代码中 node_modules 目录的内容被完全忽略。当你启用 modules 时,那些无用的 node_modules 目录突然变得无比有用:

¥Before Meteor 1.3, the contents of node_modules directories in Meteor application code were completely ignored. When you enable modules, those useless node_modules directories suddenly become infinitely more useful:

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 模块的可能性将减少对 https://atmospherejs.com/momentjs/moment 等 npm 封装器包的需求。

¥When you run this app, the moment library will be imported on both the client and the server, and both consoles will log output similar to: Today at 7:51 PM. Our hope is that the possibility of installing Node modules directly within an app will reduce the need for npm wrapper packages such as 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 如何安装这些文件的细节。

¥A version of the npm command comes bundled with every Meteor installation, and (as of Meteor 1.3) it's quite easy to use: meteor npm ... is synonymous with npm ..., so meteor npm install moment will work in the example above. (Likewise, if you don't have a version of node installed, or you want to be sure you're using the exact same version of node that Meteor uses, meteor node ... is a convenient shortcut.) That said, you can use any version of npm that you happen to have available. Meteor's module system only cares about the files installed by npm, not the details of how npm installs those files.

文件加载顺序

¥File load order

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

¥Before Meteor 1.3, the order in which application files were evaluated was dictated by a set of rules described in the Application Structure - Default file load order section of the Meteor Guide. These rules could become frustrating when one file depended on a variable defined by another file, particularly when the first file was evaluated after the second file.

由于模块的存在,你可能想到的任何加载顺序依赖都可以通过添加 import 语句来解决。因此,如果 a.js 由于文件名而在 b.js 之前加载,但 a.js 需要由 b.js 定义的内容,则 a.js 可以简单地从 b.jsimport 该值:

¥Thanks to modules, any load-order dependency you might imagine can be resolved by adding an import statement. So if a.js loads before b.js because of their file names, but a.js needs something defined by b.js, then a.js can simply import that value from b.js:

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 语法:

¥Sometimes a module doesn’t actually need to import anything from another module, but you still want to be sure the other module gets evaluated first. In such situations, you can use an even simpler import syntax:

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

无论首先导入哪个模块,console.log 调用的顺序始终为:

¥No matter which of these modules is imported first, the order of the console.log calls will always be:

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