angular 可重用结构建议

838 阅读3分钟
原文链接: github.com

I apologize in advance for this issue's massive length.

I would like to start a discussion on some ways I think the generators could be changed to make the resulting components more easily reusable. I think when developing, we should have the potential for as much of our code as possible to be "drag and drop reusable". AngularJS already sets us well on our way by promoting separation of concerns in its internal architecture, so why shouldn't our development tools do the same?

The reference structure I am using is my own ngBoilerplate.

Each section builds on the previous.

Update [07 Mar, 0840 PST]: I use the term "component" here a little more loosely than Bower. I just mean it to refer to functionality that is somewhat self-contained. Unless otherwise noted, it refers to both bundled code and internal app features.

Organize by Feature

Instead of bundling code by the layer to which it belongs (controllers, services, filters, directives, etc.), I would like to see code bundled by the feature to which it belongs. There can be many manifestations of this, but here are two examples:

(a) Routes

Instead of something like this for a /home route:

|-- src/
|   |-- scripts/
|   |   |-- controllers/
|   |   |   |-- home.js
|   |-- test/
|   |   |-- spec/
|   |   |   |-- controllers/
|   |   |   |   |-- home.js
|   |-- views/
|   |   |-- home.html

I'd prefer to see it like this:

|-- src/
|   |-- app/
|   |   |-- home/
|   |   |   |-- home.js
|   |   |   |-- home.spec.js
|   |   |   |-- home.tpl.html

This is a much simpler directory structure, but makes the home module very portable. It is also self-contained: it can be totally refactored without impacting the rest of the application.

(b) Multi-Component Features:

If we have complex component, it is likely composed of multiple smaller components. For example, an editing component might have a persistence service, a representation service for translating markup to HTML, and a directive or two for display. With the existing structure, each component would be created independently and be mixed throughout the various layers; without a comprehensive, holistic understanding of the application, it is difficult to see how these components are (or should be) inter-related. It demands manually surfing through the code and/or doing searches for component names.

A cleaner directory structure would look something like this:

|-- src/
|   |-- components/
|   |   |-- editor/
|   |   |   |-- editing.js
|   |   |   |-- editor.js
|   |   |   |-- editor.spec.js
|   |   |   |-- editingStorage.js
|   |   |   |-- editingStorage.spec.js
|   |   |   |-- editingRender.js
|   |   |   |-- editingRender.spec.js

Now it's pretty clear we're talking about one component, albeit a complex one that spans multiple layers. But each sub-component of the editor is still standalone, if need be - that's just good programming.

Isolate the Modules

A logical extension of this reorganization is to so-define our modules. In this pattern, each directory roughly corresponds to a module. Instead of this:

angular.module( 'myApp' )
  .controller( 'HomeCtrl', function ($scope) {
    // ...
  });

We would have this:

angular.module( 'home', [] )
  .controller('HomeCtrl', function ($scope) {
    // ...
  });

Similarly for the editing component:

angular.module( 'editor', [
  'editing.editor',
  'editing.editingStorage',
  'editing.editingRender'
])

Where those modules are defined in their respective files. And our main app.js file simply requires the top-level modules:

angular.module( 'myApp', [
  'home',
  'editing'
]);

Adjacent Tests

Everyone coming from server-side development (including myself) is familiar with the separate test directory, but I've always found it vexing. Placing test files directly adjacent to the code they test makes them easier to locate. More important, however, is reusability; if our tests are in a separate directory, we now have two things we have to copy between projects in order to reuse a component.

This is where it's important to separate two kinds of projects: libraries and apps. When developing libraries, we design them to be self-contained and self-sufficient, so we have no need to take tests with us to reuse in another project - that should have all been part of a build. But when developing apps, where some components may depend more subtly on other components (or at least on assumptions about our app architecture), it makes sense to take the tests with us and ensure they still pass in the new environment. Also see the 'Internal "Components"' section below.

It's okay to keep the tests side-by-side because our build tools are sophisticated enough to be able to tell the difference. Grunt 0.4, for example, now includes negative file selectors, so our build can exclude files with patterns like src/**/*.spec.js or src/**/*Spec.js from compilation and minification.

Adjacent Templates

The same concept also applies to views, partials, and templates. I also prefer they be suffixed with .tpl.html or something similar to indicate they're fragments.

Modularized Routing

Using the above example, yo angular:route home would generate this:

|-- src/
|   |-- app/
|   |   |-- home/
|   |   |   |-- home.js
|   |   |   |-- home.spec.js
|   |   |   |-- home.tpl.html

But instead of defining the route in app.js, we would let the home module set up its own routing:

angular.module( 'home', [] )

.config( function ( $routeProvider ) {
  $routeProvider
    .when( '/home', {
      templateUrl: 'home/home.tpl.html',
      controller: 'HomeCtrl'
    });
})

.controller( 'HomeCtrl', function ( $scope ) {
    // ...
})

Nothing is required in the app.js in terms of routing, unless the user should choose to define a default redirect, such as to /home.

Feature Nesting

The existing directory structure is very flat. For small projects, this is perfectly fine, but for non-trivial projects it can become a file management nightmare. If we organize our code by the feature or component they implement and use adjacent templates and tests, it also makes sense to be able to nest them.

Considering the route example:

|-- src/
|   |-- app/
|   |   |-- products/
|   |   |   |-- products.js
|   |   |   |-- products.spec.js
|   |   |   |-- products.tpl.html
|   |   |   |-- create/
|   |   |   |   |-- create.js
|   |   |   |   |-- create.tpl.html
|   |   |   |-- ...

In this case, each directory should roughly correspond to a single "submodule". The products directory is a module called products, making create something like products.create. Using this pattern, the products module can require all requisite submodules:

angular.module( 'products', [
  'products.list',
  'products.view',
  'products.create',
  'products.search'
]);

Again, because the target is reusability, each app module is responsible for declaring its own dependencies, which will "bubble up" from products.create to products to myApp. Routing would work similarly; each submodule can define its own routing, in theory "namespacing" to its parent module. For example, products.create could define a route of /products/create.

This same "nested" pattern would also apply to complex components, though they would obviously not include routing. E.g.:

|-- src/
|   |-- components/
|   |   |-- editor/
|   |   |   |-- editor.js
|   |   |   |-- plugins/
|   |   |   |   |-- syntax.js
|   |   |   |   |-- align.js

Internal "Components"

Lastly, I make a distinction between app code, the stuff that is somewhat unique to our problem domain, and components, the stuff that may come from a third party but that is more immediately reusable in unrelated projects.

With this concept in mind, we should be able to mix in the components directory the third-party libraries that come from Bower, the third-party libraries we download manually, and the reusable components that we are coding for this application specifically. e.g.:

|-- src/
|   |-- components/
|   |   |-- angular-placeholders/     
|   |   |-- angular-ui-bootstrap/     
|   |   |-- editor/                   

All combined, I think this would improve code reusability and readability.