In our last post discussing the benefits of moving your methods, publications, and templates into modules, we mentioned that all of this Meteor-specific functionality relied on modifying global state.
This means that our modules didn’t need to export anything. However, they do need to be imported at least once by your main Meteor application.
Importing these modules executes your calls to Meteor.methods(...)
, Meteor.publish(...)
, etc… and makes them accessible to the rest of your application.
Depending on how you structure your imports, this kind of upfront importing can quickly get out of hand.
The Problem With Direct Imports
Imagine we have an /imports/lib
folder in our project. Within that folder we break up our application’s functionality into distinct components, like foo
and bar
. Each component has it’s own set of Meteor methods defined in a methods.js
file:
.
└── imports
└── lib
├── bar
│ └── methods.js
└── foo
└── methods.js
To make sure these methods are registered when our application starts, we’ll need to update both our /client/mains.js
and our /server/main.js
to import these method files:
import "/imports/lib/foo/methods";
import "/imports/lib/bar/methods";
This import structure seems to make sense so far.
It might get more difficult to deal with if we start to aggressively break up our methods, but we’ll put that out of our minds for now.
When we begin adding template modules, our /imports
folder structure will quickly begin to balloon in size:
.
└── imports
├── client
│ ├── bar
│ │ ├── template-1
│ │ │ ├── template-1.js
│ │ │ └── template-2.html
│ │ └── template-2
│ │ └── ...
│ └── foo
│ ├── template-3
│ │ ├── template-3.js
│ │ └── template-3.html
│ └── template-4
│ └── ...
└── lib
├── bar
│ └── methods.js
└── foo
└── methods.js
Now we’ll have to update our /client/main.js
to pull in each of these templates:
import "/imports/lib/foo/methods";
import "/imports/lib/bar/methods";
import "/imports/client/bar/template-1/template-1";
import "/imports/client/bar/template-2/template-2";
import "/imports/client/foo/template-3/template-3";
import "/imports/client/foo/template-4/template-4";
Our /client/main.js
file has to keep up with every defined method and template in the system. Similarly, our /server/main.js
will have to keep up with ever method definition and publication definition (and potentially every template definition, if we’re using SSR).
This breaks the clean modularity of our system. Our main.js
files need to be intimately aware of the structure and implementation of all of our component pieces.
Index Files to the Rescue
Thankfully, index files can lead us out of this increasingly hairy situation.
When an index.js
file is present in a directory, attempting to import that directory will cause the index.js
file to be imported on its behalf. For example, consider this folder structure:
.
└── baz
└── index.js
If we import "baz"
(import "./baz"
), index.js
will be imported instead.
We can leverage this to organize our /imports
structure and clean up our main.js
files. Let’s start by adding an index.js
file to each method and template “component”:
.
└── imports
├── client
│ ├── bar
│ │ ├── template-1
│ │ │ ├── index.js
│ │ │ ├── template-1.js
│ │ │ └── template-2.html
│ │ └── template-2
│ │ └── ...
│ └── foo
│ └── ...
└── lib
├── bar
│ │── index.js
│ └── methods.js
└── foo
└── ...
Our /imports/client/bar/template-1/index.js
file only needs to be concerned about importing the files related to the template-1
component:
import "./template-1";
Similarly, our /imports/lib/bar/index.js
file only needs to be concerned about importing the method and other server-side functionality related to the bar
component:
import "./methods.js";
Fantastic. Now, let’s move up in our folder tree, adding index.js
files at each step along the way until we hit our client
, lib
, or server
folders:
.
└── imports
├── client
│ ├── index.js
│ ├── bar
│ │ ├── index.js
│ │ ├── template-1
│ │ │ ├── index.js
│ │ │ ├── template-1.js
│ │ │ └── template-2.html
│ │ └── template-2
│ │ └── ...
│ └── foo
│ └── ...
└── lib
├── index.js
├── bar
│ │── index.js
│ └── methods.js
└── foo
└── ...
Our newly created /imports/client/bar/index.js
file is concerned about importing all of the templates and functionality related to the bar
component:
import "./template-1";
import "./template-2";
We can finish up our import chain on the client by updating our new /imports/client/index.js
file to import the foo
and bar
client-side components:
import "./bar";
import "./foo";
We can do the same thing in our /imports/lib
folder by updating our new /imports/server/index.js
file:
import "./bar";
import "./foo";
Finally, we can drastically simplify our /client/main.js
and /server/main.js
files to only pull in what we need at a very high level.
On the client (/client/main.js
), we’ll just want to import client-only and shared components:
import "/imports/lib";
import "/imports/client";
And on the server (/server/main.js
), we (currently) only want to import the shared components:
import "/imports/lib";
If we had a set of server-only components we could easily include it there as well.
Reaping the Benefits
I’m a big fan of this structure.
Each level of our dependency tree only has to concern itself with the next level. Our client
folder only has to know that it wants to pull in the foo
and bar
components. It doesn’t need to know which templates those components use. The foo
and bar
components manage the importing of their templates themselves!
If you wanted to add a new template to the bar
component, you’d simply add the template folder into /imports/client/bar/
, with an index file that pulls in the required files. Lastly, you’d update /imports/client/bar/index.js
to import that new template.
Removing a template is as simple as deleting its folder and removing the import
reference from its parent’s index.js
file.