Meteor开发指南 — 使用JavaScript模块

注:本文将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。

requireexport的原语也允许你的Node模块在Meteor应用代码中运行而不用做出修改。更重要是,把modules独立出来是为了让我们能够在不方便添加ecmascript包的时候使用requireexport,比如在实现ecmascript包本身的时候。

基本语法

尽管importexport有许多不同语法,这节还是阐述了每个人都应该知道的最基本的用法形式。

首先,你可以导出任何有名称的声明,并且就在它声明语句的同一行:

// 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);

如果你想用其他名称,那么你会高兴的发现exportimport 语句可以重命名它们的参数:

// 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";

上面的例子应该能使你开始使用importexport语法了。进一步阅读可以看Axel Rauschmayer的这篇详细的解释,它给出了importexport语法的每一个变种。

模块化应用结构

在发布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.jsserver/main.js。两个main.js文件会被即刻执行,以使你的应用能够导入imports/下的模块。

模块化扩展包结构

如果你是扩展包开发者,除了在package.jsPackage.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.jsclient.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 导出, 但不是自动导入的.

注意到importfrom "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 !

Meteor全栈开发

你可能感兴趣的:(Meteor开发指南 — 使用JavaScript模块)