Meteor - Best Practices

meteor

https://guide.meteor.com/structure.html - done reading
https://blog.tableflip.io/large-meteor-projects-best-practices/

What is the default directory structure?

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:

  1. imports: Any directory named imports/ is not loaded anywhere and files must be imported using import
  2. node_modules: Any directory named node_modules/ is not loaded anywhere. node.js packages installed into node_modules directories must be imported using import or by using Npm.depends in package.js.
  3. client: Any directory named client/ is not loaded on the server. Similar to wrapping your code in if (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.
  4. server: Any directory named server/ is not loaded on the client. Similar to wrapping your code in if (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 the server/ directory. Meteor gathers all your JavaScript files, excluding anything under the client, public, and private 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.
  5. public: All files inside a top-level directory called public/ are served as-is to the client. When referencing these assets, do not include public/ in the URL, write the URL as if they were all in the top level. For example, reference public/bg.png as <img src='/bg.png' />. This is the best place for favicon.ico, robots.txt, and similar files.
  6. private: All files inside a top-level directory called private/ are only accessible from server code and can be loaded via the Assets 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.
  7. 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.
  8. 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:

  1. Files/directories whose names start with a dot, like .meteor and .git
  2. packages/: Used for local packages
  3. cordova-build-override/: Used for advanced mobile build customizations
  4. programs: For legacy reasons

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.

How does Meteor organize files?

The client side stuffs go into the client folder, and the server side stuffs go into the server folder. CSS and HTML stuffs should be on the client side, and that is why you see the main.html and main.css in the client folder but not in the server folder by default.

What is this imports folder?

Files inside imports/ only load if they are imported, so we'll need to import imports/ui/body.js from our frontend JS entrypoint (client/main.js):

import '../imports/ui/body.js';

You can read more about how imports work and how to structure your code in the Application Structure article of the Meteor Guide.

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.

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.

What is this import / export stuff?

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….

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.

Meteor allows you to import not only JavaScript in your application, but also CSS and HTML to control load order:

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 also supports the standard ES2015 modules export syntax:

export const listRenderHold = LaunchScreen.hold();  // named export
export { Todos };                                   // named export
export default Lists;                               // default export
export default new Collection('lists');             // default export

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 Meteor Atmosphere packages, but the import path must be prefixed with meteor/ to avoid conflict with the npm package namespace. For example, to import moment from npm and HTTP from Atmosphere:

import moment from 'moment';          // default import from npm
import { HTTP } from 'meteor/http';   // named import from Atmosphere

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 imports 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'

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'

How can we import 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';

What is the purpose of the ecmascript package?

ECMAScript, the language standard on which every browser’s JavaScript implementation is based, has moved to yearly standards releases. The newest complete standard is ES2015, which includes some long-awaited and very significant improvements to the JavaScript language. Meteor’s ecmascript package compiles this standard down to regular JavaScript that all browsers can understand using the popular Babel compiler. It’s fully backwards compatible to “regular” JavaScript, so you don’t have to use any new features if you don’t want to. We’ve put a lot of effort into making advanced browser features like source maps work great with this package, so that you can debug your code using your favorite developer tools without having to see any of the compiled output.

The ecmascript package is included in all new apps and packages by default, and compiles all files with the .js file extension automatically. See the list of all ES2015 features supported by the ecmascript package. To get the full experience, you should also use the es5-shim package which is included in all new apps by default. This means you can rely on runtime features like Array#forEach without worrying about which browsers support them.

What is the 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. You may combine both eager loading and lazy loading using import in a single app. Any import statements are evaluated in the order they are listed in a file when that file is loaded and evaluated using these rules.

There are several load order rules. They are applied sequentially to all applicable files in the application, in the priority given below:

  1. HTML template files are always loaded before everything else
  2. Files beginning with main. are loaded last
  3. Files inside any lib/ directory are loaded next
  4. Files with deeper paths are loaded next
  5. 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.

Why should we use let or const instead of var?

If you adopt a convention that you must always use let or const instead of var, you can now use a tool to ensure all of your variables are scoped the way you expect. That means you can avoid bugs where variables act in unexpected ways. Also, by enforcing that all variables are declared before use, you can easily catch typos before even running any code!

How can we check our code using ESLint?

We recommend choosing and sticking to a JavaScript style guide and enforcing it with tools. A popular option that we recommend is the Airbnb style guide with the ES6 extensions (and optionally React extensions).

“Code linting” is the process of automatically checking your code for common errors or style problems. For example, ESLint can determine if you have made a typo in a variable name, or some part of your code is unreachable because of a poorly written if condition.

We recommend using the Airbnb eslint configuration which verifies the Airbnb styleguide.

To setup ESLint in your application, you can install the following npm packages:

meteor npm install --save-dev babel-eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-meteor eslint-plugin-react eslint-plugin-jsx-a11y eslint-import-resolver-meteor eslint

You can also add a eslintConfig section to your package.json to specify that you’d like to use the Airbnb config, and to enable ESLint-plugin-Meteor. You can also setup any extra rules you want to change, as well as adding a lint npm command:

{
  ...
  "scripts": {
    "lint": "eslint .",
    "pretest": "npm run lint --silent"
  },
  "eslintConfig": {
    "parser": "babel-eslint",
    "parserOptions": {
      "allowImportExportEverywhere": true
    },
    "plugins": [
      "meteor"
    ],
    "extends": [
      "airbnb",
      "plugin:meteor/recommended"
    ],
    "settings": {
      "import/resolver": "meteor"
    },
    "rules": {}
  }
}

To run the linter, you can now simply type:

meteor npm run lint

How can we use CoffeeScript?

While we recommend using ES2015 with the ecmascript package as the best development experience for Meteor, everything in the platform is 100% compatible with CoffeeScript and many people in the Meteor community prefer it. All you need to do to use CoffeeScript is add the right Meteor package:

meteor add coffeescript

All code written in CoffeeScript compiles to JavaScript under the hood, and is completely compatible with any code in other packages that is written in JS or ES2015.

Why should we avoid using Meteor.user().profile?

Do you have any code that looks like this:

if (Meteor.user().profile.isAdmin)
  // do important admin things

Any user can open up a console and run:

Meteor.users.update(Meteor.userId(), {$set: {'profile.isAdmin': true}});

Surprise! User profiles are editable by default even if insecure has been removed. To prevent this, just add the following deny rule:

Meteor.users.deny({
  update: function() {
    return true;
  }
});

Should we sort the data as part of publishing it?

No. When you publish documents to the client, they are merged with other documents from the same collection and rearranged into an in-memory data store called minimongo. The key word being rearranged. Many new meteor developers have a mental model of published data as existing in an ordered list. This leads to questions like: "I published my data in sorted order, so why doesn't it appear that way on the client?" That's expected. There's one simple rule to follow:

If you need your documents to be ordered on the client, sort them on the client.

Sorting in a publish function isn't usually necessary unless the result of the sort changes which documents are sent (e.g. you are using a limit). You may, however, want to retain the server-side sort in cases where the data transmission time is significant. Imagine publishing several hundred blog posts but initially showing only the most recent ten. In this case, having the most recent documents arrive on the client first would help minimize the number of template renderings.

How can we avoid having to query the DOM whenever we need data?

Remember that hack where you used to stuff all of your application state into the DOM using data attributes? I constantly see code from new users that looks like this:

<template name="nametag">
  <div data-name="{{name}}">{{name}}</div>
</template>

Template.nametag.events({
  click: function(e) {
    console.log($(e.currentTarget).data().name);
  }
});

Meteor is here to stop the madness. Because helpers, event handlers, and templates share the same context, the same code should be written as:

<template name="nametag">
  <div>{{name}}</div>
</template>
Template.nametag.events({
  click: function(e) {
    console.log(this.name);
  }
});

Caveat: Data attributes may be necessary if you are using a jQuery plugin which requires them.

How does Meteor merge subscriptions on the browser side?

MergeBox is a problem you'll encounter once you start using publications with fields projections. Let's assume a simplified users collection with the following document:

_id: 'abc123',
username: 'bobama',
profile: {firstName: 'Barack', lastName: 'Obama'},
isPresident: true

Imagine there are two publications for users:

  1. return Meteor.users.find({_id: userId}, {fields: {username: 1, profile: 1}});
  2. return Meteor.users.find({_id: userId}, {fields: {'profile.lastName': 1}});

The client can only have one copy of any given document. The server will evaluate which fields should be transmitted to the client, using something called the MergeBox. In an ideal world, if both of these publishers were active, the client would always have the complete user profile (one sends the full version and the other sends a partial version).

Unfortunately, the MergeBox only operates on top-level fields, as hinted at by this text in the subscribe documentation:

If more than one subscription sends conflicting values for a field (same collection name, document ID, and field name), then the value on the client will be one of the published values, chosen arbitrarily.

In other words, documents are not deeply merged, and sometimes the complete profile from publication (1) will be chosen, and sometimes the partial version from (2) will be chosen. The client could see a document like this:

_id: 'abc123',
username: 'bobama',
profile: {lastName: 'Obama'}

There are some complicated workarounds to this problem, but the easiest solution is just to use sub-field projections with caution, and to avoid them entirely when you have multiple publishers for the same field.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License