模块
本文档解释了 Meteor 使用的模块系统的使用方法和关键功能。
Meteor 1.2 引入了对 许多新的 ECMAScript 2015 功能的支持,其中最值得注意的遗漏之一是 ES2015 的
import
和export
语法。
Meteor 1.3 通过一个完全符合标准的模块系统填补了这一空白,该系统在客户端和服务器端均可使用。
Meteor 1.7 在
package.json
中引入了meteor.mainModule
和meteor.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.onUse
或 Package.onTest
部分来启用 modules
。
现在,您可能想知道在没有 ecmascript
包的情况下 modules
包有什么用,因为 ecmascript
启用了 import
和 export
语法。modules
包本身提供了 CommonJS 的 require
和 exports
原语,如果您曾经编写过 Node 代码,可能会很熟悉这些原语,而 ecmascript
包只是将 import
和 export
语句编译成 CommonJS。require
和 export
原语还允许 Node 模块在无需修改的情况下在 Meteor 应用程序代码中运行。此外,保持 modules
的独立性使我们能够在使用 ecmascript
比较棘手的地方(例如 ecmascript
包本身的实现)使用 require
和 exports
。
虽然 modules
包本身很有用,但我们强烈建议使用 ecmascript
包(以及 import
和 export
),而不是直接使用 require
和 exports
。如果您需要说服,这里有一个 演示文稿解释了差异。
基本语法
ES2015
尽管 import
和 export
语法有很多不同的变体,但本节描述了每个人都应该知道的必要形式。
首先,您可以在声明变量的同时 export
任何命名声明
// exporter.js
export var a = ...;
export let b = ...;
export const c = ...;
export function d() { ... }
export function* e() { ... }
export class F { ... }
这些声明使变量 a
、b
、c
(等等)不仅在 exporter.js
模块的范围内可用,而且对从 exporter.js
import
的其他模块也可用。
如果您愿意,您可以按名称 export
变量,而不是在其声明前加上 export
关键字
// exporter.js
function g() { ... }
let h = g();
// At the end of the file
export { g, h };
所有这些导出都是命名的,这意味着其他模块可以使用这些名称导入它们
// importer.js
import { a, c, F, h } from './exporter';
new F(a, c).method(h);
如果您希望使用不同的名称,您会很高兴知道 export
和 import
语句可以重命名其参数
// exporter.js
export { g as x };
g(); // Same as calling `y()` in importer.js
// importer.js
import { x as y } from './exporter';
y(); // Same as calling `g()` in exporter.js
与 CommonJS module.exports
一样,可以定义单个默认导出
// exporter.js
export default any.arbitrary(expression);
然后,可以使用任何导入模块选择的名称在没有花括号的情况下导入此默认导出
// importer.js
import Value from './exporter';
// Value is identical to the exported expression
与 CommonJS module.exports
不同,使用默认导出不会阻止同时使用命名导出。以下是您可以组合它们的方式
// importer.js
import Value, { a, F } from './exporter';
事实上,默认导出在概念上只是另一个名为“default”的命名导出
// importer.js
import { default as Value, a, F } from './exporter';
这些示例应该让您开始使用 import
和 export
语法。要了解更多信息,这里有一个由 Axel Rauschmayer 提供的关于 import
和 export
语法每个变体的非常详细的 解释。
CommonJS
您无需使用 ecmascript
包或 ES2015 语法即可使用模块。就像 ES2015 之前的 Node.js 一样,您可以使用 require
和 module.exports
——无论如何,import
和 export
语句就是编译成这些的。
以下 ES2015 import
行
import { AccountsTemplates } from 'meteor/useraccounts:core';
import '../imports/startup/client/routes.js';
可以用 CommonJS 这样写
var UserAccountsCore = require('meteor/useraccounts:core');
require('../imports/startup/client/routes.js');
并且您可以通过 UserAccountsCore.AccountsTemplates
访问 AccountsTemplates
。
请注意,如果像此示例中的 routes.js
一样,在没有赋值给任何变量的情况下进行 require
,则文件不需要 module.exports
。routes.js
中的代码将简单地包含在上述 require
语句的位置并执行。
以下 ES2015 export
语句
export const insert = new ValidatedMethod({ ... });
export default incompleteCountDenormalizer;
可以重写为使用 CommonJS module.exports
module.exports.insert = new ValidatedMethod({ ... });
module.exports.default = incompleteCountDenormalizer;
如果您愿意,您也可以简单地编写 exports
而不是 module.exports
。如果您需要从具有 default
导出的 ES2015 模块中 require
,则可以使用 require('package').default
访问导出。
有一种情况您可能需要使用 CommonJS,即使您的项目有 ecmascript
包:如果您想有条件地包含模块。import
语句必须位于顶级作用域,因此不能位于 if
块内。如果您正在编写一个在客户端和服务器端都加载的通用文件,您可能只想在一个或另一个环境中导入一个模块
if (Meteor.isClient) {
require('./client-only-file.js');
}
请注意,对 require()
的动态调用(其中要请求的名称可以在运行时更改)无法正确分析,并可能导致客户端捆绑包损坏。这也在 指南中进行了讨论。
CoffeeScript
从 Meteor 的早期开始,CoffeeScript 就一直是一种一流的支持语言。即使今天我们推荐 ES2015,我们仍然打算完全支持 CoffeeScript。
从 CoffeeScript 1.11.0 开始,CoffeeScript 本身支持 import
和 export
语句。确保您在项目中使用最新版本的 CoffeeScript 包以获得此支持。今天创建的新项目将通过 meteor add coffeescript
获取此版本。确保不要忘记包含 ecmascript
和 modules
包:meteor add ecmascript
。(modules
包由 ecmascript
暗示。)
CoffeeScript import
语法与您上面看到的 ES2015 语法几乎相同
import { Meteor } from 'meteor/meteor'
import SimpleSchema from 'simpl-schema'
import { Lists } from './lists.coffee'
您也可以将传统的 CommonJS 语法与 CoffeeScript 一起使用。
模块化应用程序结构
在应用程序的 package.json
文件中使用 meteor
部分。
这从 Meteor 1.7 开始可用
{
"meteor": {
"mainModule": {
"client": "client/main.js",
"server": "server/main.js"
}
}
}
指定后,这些入口点将定义 Meteor 将在哪些文件中开始针对每个架构(客户端和服务器)的评估过程。
这样,Meteor 就不会急切加载任何其他 js 文件。
还有一个用于 legacy
客户端的架构,如果您希望在导入现代客户端的主模块之前为旧浏览器加载 polyfill 或其他代码,这将非常有用。
除了 meteor.mainModule
之外,package.json
的 meteor
部分还可以指定 meteor.testModule
来控制 meteor test
或 meteor test --full-app
加载哪些测试模块
{
"meteor": {
"mainModule": {
"client": "client/main.js",
"server": "server/main.js"
},
"testModule": "tests.js"
}
}
如果您的客户端和服务器测试文件不同,您可以使用与 mainModule 相同的语法扩展 testModule 配置
{
"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即可。
{
"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来指定包的主入口点。
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.js
和client.js
可以从包源目录导入其他文件,即使这些文件没有使用api.addFiles
函数添加。
当您使用api.mainModule
时,主模块的导出将作为Package['my-modular-package']
全局公开,以及api.export
导出的任何符号,因此任何导入该包的代码都可以使用它们。换句话说,主模块可以决定api.export
将导出Foo
的什么值,以及提供可以从包中显式导入的其他属性。
// 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.
请注意,import
是from 'meteor/my-modular-package'
,而不是from 'my-modular-package'
。Meteor包标识符字符串必须包含前缀meteor/...
以将其与npm包区分开来。
最后,由于此包正在使用新的modules
包,并且包Npm.depends
于“moment” npm包,因此包中的模块可以在客户端和服务器上都import moment from 'moment'
。这是一个好消息,因为之前版本的Meteor只允许在服务器上通过Npm.require
进行npm导入。
从包中延迟加载模块
包还可以指定一个延迟主模块。
Package.onUse(function (api) {
api.mainModule("client.js", "client", { lazy: true });
});
这意味着除非/直到另一个模块导入它,否则client.js
模块不会在应用程序启动期间被评估,并且如果找不到导入代码,甚至不会包含在客户端包中。
要导入名为exportedPackageMethod
的方法,只需
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
目录突然变得更有用。
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.js
import
该值即可。
// a.js
import { bThing } from './b';
console.log(bThing, 'in a.js');
// b.js
export var bThing = 'a thing defined in b.js';
console.log(bThing, 'in b.js');
有时模块实际上不需要从另一个模块导入任何内容,但您仍然希望确保另一个模块先被评估。在这种情况下,您可以使用更简单的import
语法。
// c.js
import './a';
console.log('in c.js');
无论这些模块中的哪一个首先被导入,console.log
调用的顺序始终为
console.log(bThing, 'in b.js');
console.log(bThing, 'in a.js');
console.log('in c.js');