应用结构
阅读本文后,你将了解
- 从文件结构方面,Meteor 应用与其他类型的应用相比有何不同。
- 如何组织你的应用,以适应小型和大型应用。
- 如何以一致且可维护的方式格式化代码并命名应用的各个部分。
通用 JavaScript
Meteor 是一个用于构建 JavaScript 应用的**全栈**框架。这意味着 Meteor 应用与大多数应用不同,因为它包含在客户端(在 Web 浏览器或 Cordova 移动应用内)运行的代码、在服务器(在 Node.js 容器内)运行的代码,以及在两种环境中都运行的**通用**代码。 Meteor 构建工具 允许你使用 ES2015 的 import
和 export
以及 Meteor 构建系统 默认文件加载顺序 规则,指定在每个环境中运行哪些 JavaScript 代码,包括任何支持的 UI 模板、CSS 规则和静态资源。
ES2015 模块
从 1.3 版本开始,Meteor 完全支持 ES2015 模块。ES2015 模块标准替代了 CommonJS 和 AMD,它们是常用的 JavaScript 模块格式和加载系统。
在 ES2015 中,你可以使用 export
关键字使变量在文件外部可用。要在其他地方使用这些变量,你必须使用源路径将其 import
。导出一些变量的文件称为“模块”,因为它们表示一个可重用代码单元。显式导入你使用的模块和包有助于你以模块化的方式编写代码,避免引入全局符号和“远距离操作”。
由于这是 Meteor 1.3 中引入的新功能,因此你会在网上找到很多使用旧的、更集中的约定构建的代码,这些约定围绕着包和应用声明全局符号。这个旧系统仍然有效,因此要选择加入新的模块系统,代码必须放在应用中的 imports/
目录中。我们预计 Meteor 的未来版本将默认为所有代码启用模块,因为这更符合更广泛的 JavaScript 社区开发人员编写代码的方式。
你可以在 modules
包的 README 中详细了解模块系统。此包作为 ecmascript
元包 的一部分自动包含在每个新的 Meteor 应用中,因此大多数应用无需执行任何操作即可立即开始使用模块。
使用 `import` 和 `export` 的简介
Meteor 允许你不仅可以 import
应用中的 JavaScript,还可以 import
CSS 和 HTML 来控制加载顺序。
import '../../api/lists/methods.js'; // import from relative path
import '/imports/startup/client'; // import module with index.js from absolute path
import './loading.html'; // import Blaze compiled HTML from relative path
import '/imports/ui/style.css'; // import CSS from absolute path
有关导入样式的更多方法,请参阅 构建系统 文章。
Meteor 还支持标准的 ES2015 模块 export
语法。
export const listRenderHold = LaunchScreen.hold(); // named export
export { Todos }; // named export
export default Lists; // default export
export default new Collection('lists'); // default export
从包中导入
在 Meteor 中,使用 import
语法在客户端或服务器上加载 npm 包并访问包导出的符号就像使用任何其他模块一样简单直观。你也可以从 Meteor Atmosphere 包中导入,但导入路径必须以 meteor/
为前缀,以避免与 npm 包命名空间冲突。例如,要从 npm 导入 moment
,从 Atmosphere 导入 HTTP
import moment from 'moment'; // default import from npm
import { HTTP } from 'meteor/http'; // named import from Atmosphere
有关使用 imports
和包的更多详细信息,请参阅 Meteor 指南中的 使用包。
使用 `require`
在 Meteor 中,import
语句编译为 CommonJS require
语法。但是,按照约定,我们鼓励你使用 import
。
也就是说,在某些情况下,你可能需要直接调用 require
。一个值得注意的例子是从公共文件需要客户端或服务器端代码。由于 import
必须位于顶层作用域,因此你不能将它们放在 if
语句中,因此你需要编写如下代码
if (Meteor.isClient) {
require('./client-only-file.js');
}
请注意,对 require()
的动态调用(其中需要调用的名称可以在运行时更改)无法正确分析,并可能导致客户端包损坏。
如果需要从具有 default
导出的 ES2015 模块中 require
,可以使用 require("package").default
获取导出。
使用 CoffeeScript
请参阅文档:模块 » 语法 » CoffeeScript
// lists.coffee
export Lists = new Collection 'lists'
import { Lists } from './lists.coffee'
文件结构
为了充分利用模块系统并确保我们的代码仅在我们要求时运行,我们建议所有应用代码都应放在 imports/
目录中。这意味着 Meteor 构建系统仅在使用 import
(也称为“延迟求值或加载”)从另一个文件中引用该文件时才会捆绑和包含该文件。
Meteor 将使用 默认文件加载顺序 规则(也称为“急切求值或加载”)加载应用中任何名为 imports/
的目录之外的所有文件。建议你创建两个精确加载的文件,client/main.js
和 server/main.js
,以便为客户端和服务器定义明确的入口点。Meteor 确保任何名为 server/
的目录中的任何文件都只能在服务器上使用,类似地,任何名为 client/
的目录中的文件只能在客户端使用。这也排除了尝试从任何名为 client/
的目录中 import
要在服务器上使用的文件,即使它嵌套在 imports/
目录中,反之亦然,从 server/
中导入客户端文件。
这些 main.js
文件本身不会执行任何操作,但它们应该导入一些启动模块,这些模块将在应用加载时分别在客户端和服务器上立即运行。这些模块应执行使用应用中包所需的任何配置,并导入应用其余代码。
示例目录布局
首先,让我们看一下我们的 Todos 示例应用,这是一个构建应用结构的绝佳示例。以下是其目录结构的概述。你可以使用命令 meteor create appName --full
生成具有此结构的新应用。
imports/
startup/
client/
index.js # import client startup through a single index entry point
routes.js # set up all routes in the app
useraccounts-configuration.js # configure login templates
server/
fixtures.js # fill the DB with example data on startup
index.js # import server startup through a single index entry point
api/
lists/ # a unit of domain logic
server/
publications.js # all list-related publications
publications.tests.js # tests for the list publications
lists.js # definition of the Lists collection
lists.tests.js # tests for the behavior of that collection
methods.js # methods related to lists
methods.tests.js # tests for those methods
ui/
components/ # all reusable components in the application
# can be split by domain if there are many
layouts/ # wrapper components for behaviour and visuals
pages/ # entry points for rendering used by the router
client/
main.js # client entry point, imports all client code
server/
main.js # server entry point, imports all server code
构建导入
现在我们已将所有文件放在 imports/
目录中,让我们考虑一下如何最好地使用模块组织代码。将所有在应用启动时运行的代码放在 imports/startup
目录中是有意义的。另一个好主意是将数据和业务逻辑与 UI 渲染代码分开。我们建议为此逻辑拆分使用名为 imports/api
和 imports/ui
的目录。
在 imports/api
目录中,根据代码提供的 API 域拆分代码是有意义的——通常这对应于你在应用中定义的集合。例如,在 Todos 示例应用中,我们有 imports/api/lists
和 imports/api/todos
域。在每个目录中,我们定义用于操作相关域数据的集合、发布和方法。
注意:在一个较大的应用中,鉴于待办事项本身是列表的一部分,将这两个域组合到一个更大的“列表”模块中可能更有意义。Todos 示例足够小,我们只需要将它们分开以演示模块化。
在 imports/ui
目录中,通常根据它们定义的 UI 侧代码类型将文件分组到目录中,例如顶层 pages
、包装 layouts
或可重用 components
。
对于上面定义的每个模块,将各种辅助文件与基本 JavaScript 文件放在一起是有意义的。例如,Blaze UI 组件应在其同一目录中包含模板 HTML、JavaScript 逻辑和 CSS 规则。具有某些业务逻辑的 JavaScript 模块应与该模块的单元测试放在一起。
启动文件
你的一些代码不会成为业务逻辑或 UI 单元,它是一些需要在应用启动时在应用上下文中运行的设置或配置代码。在 Todos 示例应用中,imports/startup/client/useraccounts-configuration.js
文件配置 useraccounts
登录模板(有关 useraccounts
的更多信息,请参阅 账户 文章)。imports/startup/client/routes.js
配置所有路由,然后导入客户端需要的所有其他代码
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { AccountsTemplates } from 'meteor/useraccounts:core';
// Import to load these templates
import '../../ui/layouts/app-body.js';
import '../../ui/pages/root-redirector.js';
import '../../ui/pages/lists-show-page.js';
import '../../ui/pages/app-not-found.js';
// Import to override accounts templates
import '../../ui/accounts/accounts-templates.js';
// Below here are the route definitions
然后,我们在 imports/startup/client/index.js
中导入这两个文件。
import './useraccounts-configuration.js';
import './routes.js';
这样就可以轻松地从我们主要急切加载的客户端入口点 client/main.js
中将所有客户端启动代码作为模块导入。
import '/imports/startup/client';
在服务器端,我们使用相同的技术在 imports/startup/server/index.js
中导入所有启动代码。
// This defines a starting set of data to be loaded if the app is loaded with an empty db.
import '../imports/startup/server/fixtures.js';
// This file configures the Accounts package to define the UI of the reset password email.
import '../imports/startup/server/reset-password-email.js';
// Set up some rate limiting and other important security settings.
import '../imports/startup/server/security.js';
// This defines all the collections, publications and methods that the application provides
// as an API to the client.
import '../imports/api/api.js';
然后,我们的主要服务器入口点 server/main.js
导入此启动模块。你可以在这里看到我们实际上没有从这些文件中导入任何变量——我们导入它们是为了按此顺序执行它们。
导入 Meteor 的“伪全局变量”
为了向后兼容,Meteor 1.3 仍然为 Meteor 核心包以及应用中包含的其他 Meteor 包提供 Meteor 的全局命名空间。你也可以像在 Meteor 的早期版本中一样,直接调用 Meteor.publish
等函数,而无需先导入它们。但是,建议你首先使用 import { Name } from 'meteor/package'
语法加载所有 Meteor 的“伪全局变量”,然后再使用它们。例如
import { Meteor } from 'meteor/meteor';
import { EJSON } from 'meteor/ejson';
默认文件加载顺序
尽管建议您编写应用程序以使用 ES2015 模块和imports/
目录,但 Meteor 1.3 继续支持文件的急切求值,使用这些默认加载顺序规则,以提供与为 Meteor 1.2 及更早版本编写的应用程序的后向兼容性。有关急切求值、延迟求值和延迟加载之间区别的说明,请参阅此 Stack Overflow 文章。
您可以在单个应用程序中组合使用急切求值和延迟加载,使用import
。任何导入语句在文件加载并使用这些规则进行求值时,都按其在文件中列出的顺序进行求值。
有几个加载顺序规则。它们按顺序应用于应用程序中的所有适用文件,并按以下给出的优先级应用。
- HTML 模板文件始终在其他所有内容之前加载。
- 以
main.
开头的文件最后加载。 - 任何
lib/
目录内的文件接下来加载。 - 路径更深的接下来加载。
- 然后按整个路径的字母顺序加载文件。
nav.html
main.html
client/lib/methods.js
client/lib/styles.js
lib/feature/styles.js
lib/collections.js
client/feature-y.js
feature-x.js
client/main.js
例如,上面的文件按正确的加载顺序排列。main.html
加载第二,因为 HTML 模板始终首先加载,即使它以main.
开头,因为规则 1 优先于规则 2。但是,它将在nav.html
之后加载,因为规则 2 优先于规则 5。
client/lib/styles.js
和lib/feature/styles.js
在规则 4 之前具有相同的加载顺序;但是,由于client
在字母顺序上位于lib
之前,因此它将首先加载。
您还可以使用Meteor.startup来控制何时在服务器和客户端上运行代码。
特殊目录
默认情况下,Meteor 应用程序文件夹中的所有 JavaScript 文件都将在客户端和服务器上捆绑并加载。但是,项目内部的文件和目录名称会影响它们的加载顺序、加载位置以及其他一些特性。以下是 Meteor 特殊处理的文件和目录名称列表。
imports
任何名为
imports/
的目录都不会在任何地方加载,并且必须使用import
导入文件。node_modules
任何名为
node_modules/
的目录都不会在任何地方加载。安装到node_modules
目录中的 node.js 包必须使用import
或在package.js
中使用Npm.depends
导入。client
任何名为
client/
的目录都不会在服务器上加载。类似于将您的代码包装在if (Meteor.isClient) { ... }
中。所有在客户端加载的文件在生产模式下都会自动连接和缩小。在开发模式下,JavaScript 和 CSS 文件不会被缩小,以方便调试。为了确保生产和开发之间的一致性,CSS 文件仍会合并到一个文件中,因为更改 CSS 文件的 URL 会影响其中 URL 的处理方式。Meteor 应用程序中的 HTML 文件与服务器端框架中的 HTML 文件处理方式大不相同。Meteor 会扫描目录中的所有 HTML 文件,查找三个顶级元素:
<head>
、<body>
和<template>
。头部和主体部分分别连接到单个头部和主体,并在初始页面加载时传输到客户端。server
任何名为
server/
的目录都不会在客户端加载。类似于将您的代码包装在if (Meteor.isServer) { ... }
中,只是客户端甚至不会接收代码。任何您不想提供给客户端的敏感代码,例如包含密码或身份验证机制的代码,都应保存在server/
目录中。Meteor 收集所有 JavaScript 文件(排除
client
、public
和private
子目录下的所有内容),并将它们加载到 Node.js 服务器实例中。在 Meteor 中,您的服务器代码在每个请求中运行在一个线程中,而不是使用 Node 典型的异步回调样式。public
顶级目录
public/
内的所有文件都按原样提供给客户端。引用这些资源时,不要在 URL 中包含public/
,请像它们都在顶级一样编写 URL。例如,将public/bg.png
引用为<img src='/bg.png' />
。这是favicon.ico
、robots.txt
和类似文件的最佳位置。private
顶级目录
private/
内的所有文件只能从服务器代码访问,并且可以通过Assets
API 加载。这可用于私有数据文件以及项目目录中您不想从外部访问的任何文件。client/compatibility
此文件夹用于与依赖于在顶级声明的变量(使用 var)作为全局变量导出的 JavaScript 库兼容。此目录中的文件在不包装在新的变量作用域中执行。这些文件在其他客户端 JavaScript 文件之前执行。
建议使用 npm 作为第三方 JavaScript 库,并使用
import
来控制文件加载时间。tests
任何名为
tests/
的目录都不会在任何地方加载。将其用于您希望使用 Meteor 内置测试工具之外的测试运行程序运行的任何测试代码。Meteor 的内置测试工具
以下目录也不作为应用程序代码的一部分加载。
- 名称以点开头的文件/目录,例如
.meteor
和.git
。 packages/
:用于本地包。cordova-build-override/
:用于高级移动构建自定义。programs
:出于遗留原因。
特殊目录之外的文件
特殊目录之外的所有 JavaScript 文件都将在客户端和服务器上加载。Meteor 提供变量Meteor.isClient
和Meteor.isServer
,以便您的代码可以根据它是在客户端还是服务器上运行来更改其行为。
特殊目录之外的 CSS 和 HTML 文件仅在客户端加载,无法从服务器代码使用。
拆分为多个应用程序
如果您正在编写一个足够复杂的系统,可能会出现将代码拆分为多个应用程序的时机。例如,您可能希望为管理 UI 创建一个单独的应用程序(而不是在整个站点的管理部分都检查权限,您可以检查一次),或者将移动版和桌面版应用程序的代码分开。
另一个非常常见的用例是从主应用程序中分离出一个工作进程,以便昂贵的作业不会通过锁定单个 Web 服务器来影响访问者的用户体验。
以这种方式拆分应用程序有一些优势。
如果您将特定类型的用户永远不会使用的代码分离出来,则您的客户端 JavaScript 包可以显著减小。
您可以使用不同的扩展设置部署不同的应用程序,并以不同的方式保护它们(例如,您可以将对管理应用程序的访问限制为防火墙后面的用户)。
您可以允许组织中的不同团队独立地处理不同的应用程序。
但是,以这种方式拆分代码也有一些挑战,在开始之前应考虑这些挑战。
共享代码
主要挑战是在您构建的不同应用程序之间正确共享代码。解决此问题的最简单方法是在不同的 Web 服务器上部署相同的应用程序,并通过不同的设置控制行为。此方法允许您部署具有不同扩展行为的不同版本,但无法享受上面列出的其他大多数优势。
如果您想创建具有单独代码的 Meteor 应用程序,那么您将有一些模块希望在它们之间共享。如果这些模块是更广泛的世界可以使用的东西,您应该考虑将它们发布到包系统,具体取决于代码是特定于 Meteor 的还是其他方式,可以是 npm 或 Atmosphere。
如果代码是私有的,或者对其他人没有兴趣,通常有意义的是在两个应用程序中都包含相同的模块(您可以使用私有 npm 模块这样做)。有几种方法可以做到这一点。
一种简单的方法是将公共代码作为两个应用程序的git 子模块包含在内。
或者,如果您将两个应用程序包含在一个存储库中,则可以使用符号链接将公共模块包含在两个应用程序中。
共享数据
另一个重要考虑因素是如何在不同的应用程序之间共享数据。
最简单的方法是将两个应用程序指向相同的MONGO_URL
,并允许两个应用程序直接读取和写入数据库。这得益于 Meteor 通过数据库对反应性的支持,可以很好地工作。当一个应用程序更改 MongoDB 中的一些数据时,连接到数据库的任何其他应用程序的用户将立即看到这些更改,这要归功于 Meteor 的 livequery。
但是,在某些情况下,最好允许一个应用程序成为主应用程序,并通过 API 控制其他应用程序对数据的访问。如果您想在不同的时间表上部署不同的应用程序,并且需要谨慎处理数据更改,这将有所帮助。
提供服务器到服务器 API 的最简单方法是直接使用 Meteor 内置的 DDP 协议。这与您的 Meteor 客户端从服务器获取数据的方式相同,但您也可以使用它在不同的应用程序之间进行通信。您可以使用DDP.connect()
从“客户端”服务器连接到主服务器,然后使用返回的连接对象进行方法调用并从发布中读取。
共享帐户
如果您有两个访问相同数据库的服务器,并且希望经过身份验证的用户在两者之间进行 DDP 调用,则可以使用一个连接上设置的恢复令牌在另一个连接上登录。
如果您的用户已连接到服务器 A,则可以使用DDP.connect()
打开到服务器 B 的连接,并将服务器 A 的恢复令牌传递进来以在服务器 B 上进行身份验证。由于两个服务器都使用相同的数据库,因此在两种情况下相同的服务器令牌都将起作用。身份验证代码如下所示。
// This is server A's token as the default `Accounts` points at our server
const token = Accounts._storedLoginToken();
// We create a *second* accounts client pointing at server B
const app2 = DDP.connect('url://of.server.b');
const accounts2 = new AccountsClient({ connection: app2 });
// Now we can login with the token. Further calls to `accounts2` will be authenticated
accounts2.loginWithToken(token);
您可以在示例存储库中看到此架构的概念验证。