应用程序的结构
在阅读完以下内容后,你将学习到:
- 如何比较一个Meteor应用在文件结构上与其他应用的差别。
- 如何组织你的应用,无论是小型还是大型的应用。
- 如何去格式化你的代码以及用持久且可维护的方式来命名应用的组成部分。
通用的JavaScript
Meteor is a full-stack framework for building JavaScript applications. This means Meteor applications differ from most applications in that they include code that runs on the client, inside a web browser or Cordova mobile app, code that runs on the server, inside a Node.js container, and common code that runs in both environments. The Meteor build tool allows you to easily specify what JavaScript code, including any supporting UI templates, CSS rules, and static assets, to run in each environment using a combination of ES2015 import
and export
and the Meteor build system default file load order rules.
Meteor是一个用来创建JavaScript应用的全栈框架。这意味着Meteor应用与大部分应用不同,它包括运行在客户端、web浏览器及Cordova内核的移动应用的代码,运行在服务端、某个Node.js容器中的代码,或者同时运行在两者环境之间的通用代码。Meteor构建工具允许你极为简单地制定
ES2015 模块
As of version 1.3, Meteor ships with full support for ES2015 modules. The ES2015 module standard is the replacement for CommonJS and AMD, which are commonly used JavaScript module format and loading systems.
In ES2015, you can make variables available outside a file using the export
keyword. To use the variables somewhere else, you must import
them using the path to the source. Files that export some variables are called "modules", because they represent a unit of reusable code. Explicitly importing the modules and packages you use helps you write your code in a modular way, avoiding the introduction of global symbols and "action at a distance".
Since this is a new feature introduced in Meteor 1.3, you will find a lot of code online that uses the older, more centralized conventions built around packages and apps declaring global symbols. This old system still works, so to opt-in to the new module system code must be placed inside the imports/
directory in your application. We expect a future release of Meteor will turn on modules by default for all code, because this is more aligned with how developers in the wider JavaScript community write their code.
You can read about the module system in detail in the modules
package README. This package is automatically included in every new Meteor app as part of the ecmascript
meta-package, so most apps won't need to do anything to start using modules right away.
Importing from packages
In Meteor, it is also simple and straightforward to use the import
syntax to load npm packages on the client or server, and access the package's exported symbols as you would with any other module. You can also import from Atmosphere packages, but the import path must be prefixed with meteor/
to avoid conflict with the npm package namespace. For example, to import HTTP
you could use import { HTTP } from 'meteor/http'
.
`require`的使用
In Meteor, import
statements compile to CommonJS require
syntax. However, as a convention we encourage you to use import
.
With that said, in some situations you may need to call out to require
directly. One notable example is when requiring client or server-only code from a common file. As import
s must be at the top-level scope, you may not place them within an if
statement, so you'll need to write code like:
if (Meteor.isClient) {
require('./client-only-file.js');
}
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.
If you need to require
from an ES2015 module with a default
export, you can grab the export with require("package").default
.
Another situation where you'll need to use require
is in CoffeeScript files. As CS doesn't support the import
syntax yet, you should use require
:
{ FlowRouter } = require 'meteor/kadira:flow-router'
React = require 'react'
Exporting with CoffeeScript
When using CoffeeScript, not only the syntax to import variables is different, but also the export has to be done in a different way. Variables to be exported are put in the exports
object:
exports.Lists = ListsCollection 'Lists'
文件结构
To fully use the module system and ensure that our code only runs when we ask it to, we recommend that all of your application code should be placed inside the imports/
directory. This means that the Meteor build system will only bundle and include that file if it is referenced from another file using an import
(also called "lazy evaluation or loading").
Meteor will load all files outside of any directory named imports/
in the application using the default file load order rules (also called "eager evaluation or loading"). It is recommended that you create exactly two eagerly loaded files, client/main.js
and server/main.js
, in order to define explicit entry points for both the client and the server. Meteor ensures that any file in any directory named server/
will only be available on the server, and likewise for files in any directory named client/
. This also precludes trying to import
a file to be used on the server from any directory named client/
even if it is nested in an imports/
directory and vice versa for importing client files from server/
.
These main.js
files won't do anything themselves, but they should import some startup modules which will run immediately, on client and server respectively, when the app loads. These modules should do any configuration necessary for the packages you are using in your app, and import the rest of your app's code.
目录布局示例
作为开始,让我们看下示例应用 Todos,当我们构建APP的结构时可用来参考的一款不错的例子。这是目录结构的概览:
imports/
startup/
client/
routes.js # 设置APP中所有的路由
server/
fixtures.js # 当应用启动时,初始化数据
api/
lists/ # 一个业务逻辑单元
server/
publications.js # 所有list相关的发布
publications.tests.js # 针对list发布的测试
lists.js # 定义Lists集合
lists.tests.js # 针对Lists集合的行为测试
methods.js # list相关的方法
methods.tests.js # 针对方法的测试
ui/
components/ # 在应用中所有可复用的组件
# 如果多的话可以以业务来分列
layouts/ # 行为及视觉元素的组建包装
pages/ # 路由器渲染的入口
client/
main.js # 客户端入口,用来导入所有客户端代码
server/
main.js # 服务端入口,用来导入所有的服务端代码
Structuring imports
Now that we have placed all files inside the imports/
directory, let's think about how best to organize our code using modules. It makes sense to put all code that runs when your app starts in an imports/startup
directory. Another good idea is splitting data and business logic from UI rendering code. We suggest using directories called imports/api
and imports/ui
for this logical split.
Within the imports/api
directory, it's sensible to split the code into directories based on the domain that the code is providing an API for --- typically this corresponds to the collections you've defined in your app. For instance in the Todos example app, we have the imports/api/lists
and imports/api/todos
domains. Inside each directory we define the collections, publications and methods used to manipulate the relevant domain data.
Note: in a larger application, given that the todos themselves are a part of a list, it might make sense to group both of these domains into a single larger "list" module. The Todos example is small enough that we need to separate these only to demonstrate modularity.
Within the imports/ui
directory it typically makes sense to group files into directories based on the type of UI side code they define, i.e. top level pages
, wrapping layouts
, or reusable components
.
For each module defined above, it makes sense to co-locate the various auxiliary files with the base JavaScript file. For instance, a Blaze UI component should have its template HTML, JavaScript logic, and CSS rules in the same directory. A JavaScript module with some business logic should be co-located with the unit tests for that module.
Importing Meteor "pseudo-globals"
For backwards compatibility Meteor 1.3 still provides Meteor's global namespacing for the Meteor core package as well as for other Meteor packages you include in your application. You can also still directly call functions such as Meteor.publish
, as in previous versions of Meteor, without first importing them. However, it is recommended best practice that you first load all the Meteor "pseudo-globals" using the import { Name } from 'meteor/package'
syntax before using them. For instance:
import { Meteor } from 'meteor/meteor';
import { EJSON } from 'meteor/ejson';
Startup files
Some of your code isn't going to be a unit of business logic or UI, it's just some setup or configuration code that needs to run in the context of the app when it starts up. In the Todos example app, the imports/startup/client/useraccounts-configuration.js
file configures the useraccounts
login templates and the routes (see the Accounts article for more information about useraccounts
). The imports/startup/client/routes.js
configures all of the routes and then imports all other code that is required on the client, forming the main entry point for the rest of the client application:
import { FlowRouter } from 'meteor/kadira:flow-router';
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
On the server, we import various modules into our server/main.js
to set up the server environment:
// 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';
You can see that here we don't actually import any variables from these files - we just import them so that they execute in this order.
Default file load order
Even though it is recommended that you write your application to use ES2015 modules and the imports/
directory, Meteor 1.3 continues to support eager loading of files, using these default load order rules, to provide backwards compatibility with applications written for Meteor 1.2 and earlier.
There are several load order rules. They are applied sequentially to all applicable files in the application, in the priority given below:
- HTML template files are always loaded before everything else
- Files beginning with
main.
are loaded last - Files inside any
lib/
directory are loaded next - Files with deeper paths are loaded next
- Files are then loaded in alphabetical order of the entire path
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
For example, the files above are arranged in the correct load order. main.html
is loaded second because HTML templates are always loaded first, even if it begins with main.
, since rule 1 has priority over rule 2. However, it will be loaded after nav.html
because rule 2 has priority over rule 5.
client/lib/styles.js
and lib/feature/styles.js
have identical load order up to rule 4; however, since client
comes before lib
alphabetically, it will be loaded first.
You can also use Meteor.startup to control when run code is run on both the server and the client.
Special directories
By default, any JavaScript files in your Meteor application folder are bundled and loaded on both the client and the server. However, the names of the files and directories inside your project can affect their load order, where they are loaded, and some other characteristics. Here is a list of file and directory names that are treated specially by Meteor:
imports
Any directory named
imports/
is not loaded anywhere and files must be imported usingimport
.node_modules
Any directory named
node_modules/
is not loaded anywhere. node.js packages installed intonode_modules
directories must be imported usingimport
or by usingNpm.depends
inpackage.js
.client
Any directory named
client/
is not loaded on the server. Similar to wrapping your code inif (Meteor.isClient) { ... }
. All files loaded on the client are automatically concatenated and minified when in production mode. In development mode, JavaScript and CSS files are not minified, to make debugging easier. CSS files are still combined into a single file for consistency between production and development, because changing the CSS file's URL affects how URLs in it are processed.HTML files in a Meteor application are treated quite a bit differently from a server-side framework. Meteor scans all the HTML files in your directory for three top-level elements:
<head>
,<body>
, and<template>
. The head and body sections are separately concatenated into a single head and body, which are transmitted to the client on initial page load.server
Any directory named
server/
is not loaded on the client. Similar to wrapping your code inif (Meteor.isServer) { ... }
, except the client never even receives the code. Any sensitive code that you don't want served to the client, such as code containing passwords or authentication mechanisms, should be kept in theserver/
directory.Meteor gathers all your JavaScript files, excluding anything under the
client
,public
, andprivate
subdirectories, and loads them into a Node.js server instance. In Meteor, your server code runs in a single thread per request, not in the asynchronous callback style typical of Node.public
All files inside a top-level directory called
public/
are served as-is to the client. When referencing these assets, do not includepublic/
in the URL, write the URL as if they were all in the top level. For example, referencepublic/bg.png
as<img src='/bg.png' />
. This is the best place forfavicon.ico
,robots.txt
, and similar files.private
All files inside a top-level directory called
private/
are only accessible from server code and can be loaded via theAssets
API. This can be used for private data files and any files that are in your project directory that you don't want to be accessible from the outside.client/compatibility
This folder is for compatibility with JavaScript libraries that rely on variables declared with var at the top level being exported as globals. Files in this directory are executed without being wrapped in a new variable scope. These files are executed before other client-side JavaScript files.
It is recommended to use npm for 3rd party JavaScript libraries and use
import
to control when files are loaded.tests
Any directory named
tests/
is not loaded anywhere. Use this for any test code you want to run using a test runner outside of Meteor's built-in test tools.
The following directories are also not loaded as part of your app code:
- Files/directories whose names start with a dot, like
.meteor
and.git
packages/
: Used for local packagescordova-build-override/
: Used for advanced mobile build customizationsprograms
: For legacy reasons
Files outside special directories
All JavaScript files outside special directories are loaded on both the client and the server. Meteor provides the variables Meteor.isClient
and Meteor.isServer
so that your code can alter its behavior depending on whether it's running on the client or the server.
CSS and HTML files outside special directories are loaded on the client only and cannot be used from server code.
Splitting into multiple apps
If you are writing a sufficiently complex system, there can come a time where it makes sense to split your code up into multiple applications. For example you may want to create a separate application for the administration UI (rather than checking permissions all through the admin part of your site, you can check once), or separate the code for the mobile and desktop versions of your app.
Another very common use case is splitting a worker process away from your main application so that expensive jobs do not impact the user experience of your visitors by locking up a single web server.
There are some advantages of splitting your application in this way:
Your client JavaScript bundle can be significantly smaller if you separate out code that a specific type of user will never use.
You can deploy the different applications with different scaling setups and secure them differently (for instance you might restrict access to your admin application to users behind a firewall).
You can allow different teams at your organization to work on the different applications independently.
However there are some challenges to splitting your code in this way that should be considered before jumping in.
代码的共享
The primary challenge is properly sharing code between the different applications you are building. The simplest approach to deal with this issue is to simply deploy the same application on different web servers, controlling the behavior via different settings. This approach allows you to easily deploy different versions with different scaling behavior but doesn't enjoy most of the other advantages stated above.
If you want to create Meteor applications with separate code, you'll have some modules that you'd like to share between them. If those modules are something the wider world could use, you should consider publishing them to a package system, either npm or Atmosphere, depending on whether the code is Meteor-specific or otherwise.
If the code is private, or of no interest to others, it typically makes sense to simply include the same module in both applications (you can do this with private npm modules). There are several ways to do this:
a straightforward approach is simply to include the common code as a git submodule of both applications.
alternatively, if you include both applications in a single repository, you can use symbolic links to include the common module inside both apps.
数据的分享
Another important consideration is how you'll share the data between your different applications.
The simplest approach is to point both applications at the same MONGO_URL
and allow both applications to read and write from the database directly. This works well thanks to Meteor's support for reactivity through the database. When one app changes some data in MongoDB, users of any other app connected to the database will see the changes immediately thanks to Meteor's livequery.
However, in some cases it's better to allow one application to be the master and control access to the data for other applications via an API. This can help if you want to deploy the different applications on different schedules and need to be conservative about how the data changes.
The simplest way to provide a server-server API is to use Meteor's built-in DDP protocol directly. This is the same way your Meteor client gets data from your server, but you can also use it to communicate between different applications. You can use DDP.connect()
to connect from a "client" server to the master server, and then use the connection object returned to make method calls and read from publications.
账户系统的分享
当有两个服务连接同一个数据库时,如果你想在它们之前通过DDP的请求来认证用户,你可以使用 resume token 来设置一个连接来登录。
如果你的用户已经连接到服务A,那么你可以使用DDP.connect()
来打开一个服务B的连接,然后传输服务A的resume token在服务B上进行身份认证。当两个服务都使用同一个数据库时,相同的服务端token将会工作在两个实例中。身份认证的代码看起来是这样的:
// 服务A的token作为默认的`账户`指向我们的服务
const token = Accounts._storedLoginToken();
// 我们创建*第二个*的客户端账户当前指向服务B
const app2 = DDP.connect('url://of.server.b');
const accounts2 = new AccountsClient({ connection: app2 });
// 现在我们可以用token来登录了。而且`accounts2`的请求将通过身份认证。
accounts2.loginWithToken(token);
你可以在这些示例库中查看到这一体系概念的论证。