注:本文将import与export译为导入和导出,也可译为加载/载入和输出等等。
引言
尽管 Meteor 1.2 引入了许多 ECMAScript 2015 特性 的支持, 但是还是缺了最重要的一个就是 ES2015 import and export syntax. Meteor 1.3 填补了这个空缺,它提供了一个同时在客户端和服务端符合标准的模块系统,解决了长久以来Meteor应用存在的问题(如控制文件加载顺序),同时还兼容你之前的Meteor代码。这篇文章介绍了新的模块系统的使用方式和关键特性。
开启 modules 功能
我们认为你会爱上新的模块系统,并且模块系统在创建app或者package的时候默认安装。当然,modules
包完全是可选的,你可以自行选择是否添加它到你现有的app或package中。
对于app来说,只需要通过meteor add modules
来添加即可,更好的做法是使用meteor add ecmascript
,因为ecmascript
包涵盖了modules
包。
对于package来说,你可以这样使用modules
,通过添加api.use("modules")
到package.js
中的Package.onUse
或是Package.onTest
代码段。
现在,你可能会想知道单独使用modules
而不使用ecmascript
的好处是什么呢?因为ecmascript
也提供了import
和export
语法。modules
包自身提供了CommonJS的require和exports原语,如果你写过Node代码会对此熟悉。ecmascript
包则仅仅编译了import和export语句到CommonJS。
require
和export
的原语也允许你的Node模块在Meteor应用代码中运行而不用做出修改。更重要是,把modules
独立出来是为了让我们能够在不方便添加ecmascript
包的时候使用require
和export
,比如在实现ecmascript
包本身的时候。
基本语法
尽管import
和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
的模块中有效。
如果你想,你可以通过名称来导出这些变量,而不用使用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(); // 与在 importer.js 中调用 y() 相同
// importer.js
import {x as y} from "./exporter";
y(); // 与在 exporter.js 中调用 g() 相同
与CommonJS中的module.exports
一样,可以定义一个default的导出:
// exporter.js
export default any.arbitrary(expression);
默认的导出可以在不加大括号进行导入,并且使用任何导入的模块中使用的名称:
// importer.js
import Value from "./exporter";
// Value 与导出的表达式是等价的
与CommonJS中module.exports
不同的是,default导出并没有阻值你同时使用带名称的导出。你可以既用default,也用带名称的导出,也就是组合使用:
// importer.js
import Value, {a, F} from "./exporter";
事实上,default导出在概念上就是一个名为"default"的带名称的导出:
// importer.js
import {default as Value, a, F} from "./exporter";
上面的例子应该能使你开始使用import
和export
语法了。进一步阅读可以看Axel Rauschmayer的这篇详细的解释,它给出了import
和export
语法的每一个变种。
模块化应用结构
在发布Meteor 1.3之前,唯一的在不同文件中共享数据的方式就是让它们成为全局变量,或是使用共享变量例如Session
来交换它们的值。通过模块的介绍,一个模块可以引用任何特定模块导出的变量,所以全局变量再无必要。
如果你对Node中的模块很熟悉,你可能不希望模块被执行(evaluated)直到你第一次导入它们。但是,由于早期Meteor会在应用启动时执行所有代码,我们考虑到向前兼容,所以即刻执行(eager evaluation)是默认行为。
如果你希望一个模块被延时执行,也就是说在你第一次加载的时候再执行的话,就像Node中一样,你应该把它放在imports/
目录下,它可以是应用的任何地方,不仅仅是根目录下,并且当你导入模块的时候包含这个目录:import {stuff} from "./imports/lazy"
。注意:node_modules/
目录下的文件也会被延时执行。
延时执行(Lazy evaluation)可能会成为今后Meteor的默认行为,如果你想现在开始全面使用这个特性,那么我们推荐你将模块放在client/imports/
或server/imports/
目录下,并且只保留一个入口文件client/main.js
和server/main.js
。两个main.js
文件会被即刻执行,以使你的应用能够导入imports/
下的模块。
模块化扩展包结构
如果你是扩展包开发者,除了在package.js
中Package.onUse
代码段中使用api.use("modules")
或是api.use("ecmascript")
之外,你还可以使用新的API也就是api.mainModule
来指定你扩展包的入口文件:
Package.describe({
name: "my-modular-package"
});
Npm.depends({
moment: "2.10.6"
});
Package.onUse(function (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
时,main module的导出以Package["my-modular-package"]
全局暴露。此外任何通过api.export
暴露的符号,都可以在任何导入这个包的代码中使用。也就是说,main module需要决定api.export
中导出的Foo
的值,同时也提供了其他可以显式地从包中导入的值。
// 在使用 my-modular-package 的应用中:
import {Foo as ExplicitFoo, bar} from "meteor/my-modular-package";
console.log(Foo); // 自动导入,由于使用 api.export.
console.log(ExplicitFoo); // 显式导入, 但是和 Foo 等价.
console.log(bar); // 由 server.js 或 client.js 导出, 但不是自动导入的.
注意到import
是from "meteor/my-modular-package"
,而不是from "my-modular-package"
。这是因为Meteor的包标识符必须有前缀meteor/...
,这样才能与npm包进行区分。
最后,由于这个包使用了最新的modules
包,并且这个扩展包Npm.depends
于"moment"这个npm包,这个包中的模块可以在client和server端import moment from "moment"
。这是个好消息,因为之前Meteor只允许你在server端通过Npm.require
使用npm包。
本地 node_modules
在Meteor 1.3之前,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
当你运行这个app时,moment
会被加载到client和server端,然后两个console都会有这样的输出:Today at 7:51 PM
。我们希望的是直接在app中安装Node模块可以使那些封装npm模块的扩展包减少,就比如https://atmospherejs.com/momentjs/moment。
注意到:在Meteor 1.3的beta发布中,Node模块在client和server端使用相同的入口文件(不管是模块中以package.json
中main字段定义的,或是index.js
)。为此,仅当你确定导入的npm库会在浏览器环境运行时才能将它导入到客户端。
文件加载顺序
在Meteor 1.3之前,应用的文件加载顺序是口述的一套规则,我们可以在Structuring Your Application中的File Load Order小节中看到。这些规则会阻碍到你,当你的一个文件依赖于另一个文件中定义的变量,但是这个文件又先于另外一个文件加载时。
感谢modules功能,任何依赖加载顺序都可以由import
语句来解决了。所以如果a.js
由于文件字母顺序在b.js
之前加载,但是a.js
又使用到b.js
中定义的某些内容的话,那么a.js
可以简单地导入b.js
定义的值:
// 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");
有时候一个模块(模块c)并不需要导入另一个模块(模块a)的任何东西,但你仍希望确保另一个模块(模块a)先执行。这种情况下,你可以使用一个更为简单的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");
参考
- https://github.com/meteor/meteor/blob/release-1.3/packages/modules/README.md
- https://github.com/meteor/meteor/pull/5475
- https://themeteorchef.com/blog/meteor-1-3-from-a-20-000-foot-view/
- https://egoist.github.io/2016/01/29/node-js-module-style-guide/
- http://es6.ruanyifeng.com/#docs/module
广告
极客学院春节期间(2016.02.01—2016.02.14)通过App学习免费!欢迎观看Meteor开发指南系列课程!
欢迎关注微信公众号「Meteor全栈开发」!
关注时下最流行的框架和技术栈,让你成为全栈开发大师!
Meteor, Node, React, GraphQL, Phoenix, Elixir and More !