【AngularJS】自定义文件分割实践

使用ES6/TypeScript和Browserify的AngularJS。

参考了『AngularJSモダンプラクティス – Qiita』,我将AngularJS开发中的目录结构和其他相关内容进行了简要总结。

目标

    • TypeScript 1.5 or ECMAScript 6

 

    • AngularJS >= 1.3.0

 

    Browserify

方案

    • 基本的にはTypeScriptで記述

 

    • 型定義ファイルはdtsmで管理

 

    • エントリーポイントを定め,そこに全部importする

 

    • app nameやdirectiveのprefix等は定数にしてexport

各ファイル内でangular.module(appName)してangularのmoduleを取り出す

directiveやfactoryの登録は各ファイル内で行う

tscでコンパイルしたファイルを一時ディレクトリにおいて,それをbrowserifyに投げる

元ファイル: ./ui/assets/{javascript,template,typing}s

tscのoutDir: ./tmp/assets/javascripts

browserifyの出力先: ./public/javascripts/bundle.js(本番環境は./public/assets/bundle.js)
(ディレクトリ名が変なのはRailsアプリ内で利用してるやつだからです)

directiveのtemplateはgulp-angular-templatecacheでひとまとめにする
テストはES6で書いたものをspec/javascriptsに配置し,browserify+babelifyに喰わせる

テストをTypeScriptで書くとmockまわりとかで超面倒になる
テスト対象はtscとbrowserify通した後の最終出力(ここ微妙かも…)

directiveを中心とした,Component志向なAngularJSを目指す

目录结构

将app.ts作为入口点传给tsc和browserify进行处理。
如果要将angular.module分割成多个部分,可以在javascripts目录下创建命名空间目录,并为每个部分创建相应的入口点。

ui/assets/javascripts
├── app.ts
├── constants.ts
├── directives
│   ├── task_list.ts
│   └── index.ts
├── factories
├── resources
│   ├── index.ts
│   └── tasks.ts
└── routes.ts

tsconfig.json的内容如下。
将*.js文件生成到任意目录中,以便供browserify使用。

{
  "version": "1.5.0-beta",
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "outDir": "tmp/assets/javascripts"
  },
  "files": [
    "./ui/assets/javascripts/app.ts"
  ]
}

browserify可以将作为入口点的文件(例如app.js)和未在代码中使用require引入的库文件(例如angular-route.js)打包在一起。这些文件可以在任意位置进行配置(例如package.json)。
如果使用gulp-angular-templatecache等工具将模板文件转换为.js文件,则可以将这些文件作为browserify的输入。

{
  "browserify": {
    "entries": [
      "./tmp/assets/javascripts/app.js",
      "./tmp/assets/javascripts/templates.js",
      "./node_modules/angular-route/angular-route.js",
      "./node_modules/angular-resource/angular-resource.js"
    ]
  }
}

常数:constants.ts

赋予一些适当的常量(模块名称、指令名称的前缀等)。

export const appName = "myApp";
export const prefix = "my";
export const externalModules = ["ngRoute", "ngResource"];
export const apiBaseUrl = "/api/v1/";

入口点:app.ts

在这里进行angular.module的初始化,然后导入每个模块。
由于import “./hoge”会被编译为require(“./hoge”),因此不需要node.d.ts或browserify.d.ts(从TypeScript 1.5开始?)。

import angular = require("angular");
import {appName, externalModules} from "./constants";

angular.module(appName, externalModules);

import "./routes";
import "./resources/index";
import "./directives/index";

如果从以下文件中引用了app.ts,那么虽然可以编译,但在运行时会出现模块 “../app” 未找到的奇怪问题。

资源

在resources/index.ts中,将resources目录下的所有内容一次性引入。

import "./task";
import "./user";

以下是有关资源的具体示例和注意事项。

    • 属性値とかはng.resource.IResource実装クラスに定義

$resource()はng.resource.IResourceClassになる

$resource()返り値をそのまま or それをラップしたやつをfactoryとして登録

/// <reference path="../../typings/vendor/angularjs/angular-resource.d.ts" />

import angular = require("angular");
import {appName, apiBaseUrl} from "../constants";

export interface TaskResource extends ng.resource.IResource<TaskResource> {
  title: string;
  body: string;
  doneAt: string;
}

export interface TaskResourceClass extends ng.resource.IResourceClass<TaskResource> {
}

export function taskFactory($resource: ng.resource.IResourceService) : TaskResourceClass {
  const url = `${apiBaseUrl}/tasks/:taskId.json`;
  const params = { taskId: "@taskId" };
  let queryAction: ng.resource.IActionDescriptor = {
    method: "GET",
    isArray: true
  };
  return <TaskResourceClass> $resource(url, params, { query: queryAction });
};

let app = angular.module(appName);
app.factory("Task", ["$resource", taskFactory]);

也许最好是将URL的前缀(或者URL本身)作为常量来写。

只要以普通的工厂或服务以同样的方式处理即可。

指导方针

在directives/index.ts中以与resources相似的方式一次性加载指令。

import "./task_list";
import "./profile";

下面是具体的例子和注意事项。

    • ディレクティブのクラスはあくまでDirective Definition Object

イベント処理とかそんなんは全部Controllerにやらせる
controllerAsちゃんとつかう(名前が競合したら死ぬのでprefix除いたディレクティブ名そのままがいいかもしれない)
AngularJS 1.4以降のbindToControllerは神なので積極的に使いましょう(参考: AngularJS1.4とbindToController – Qiita)

/// <reference path="../../../typings/vendor/angularjs/angular-route.d.ts" />
/// <reference path="../../../typings/vendor/angularjs/angular-resource.d.ts" />

import angular = require("angular");
import {appName, prefix} from "../../constants";
import {TaskResource, TaskResourceClass} from "../../resources/task";

class TaskListController {
  tasks: Array<TaskResource>;

  constructor(private Task: TaskResourceClass) {
    getTasks();
  }

  getTasks() {
    this.tasks = this.Task.query();
  }
}

class TaskListDirective {
  restrict = "E";
  controller = ['TaskList', TaskListController];
  controllerAs = 'tasklist';
  scope = {};
  bindToController = true;
  templateUrl = "task_list.html";
}

let app = angular.module(appName);
app.directive(`${prefix}TaskList`, () => {
  return new TaskListDirective();
});

控制器

其实控制器这个概念确实是一种负面遗产,组件才是正确的选择。

行程规划

路由的定义。
在不使用控制器的情况下,我们创建了一个外部的指令来承担所有的“脏活”,并将其作为模板进行配置。

/// <reference path="../typings/vendor/angularjs/angular-route.d.ts" />

import angular = require("angular");
import {appName} from "./constants";

let app = angular.module(appName);

app.config(($routeProvider: ng.route.IRouteProvider, $locationProvider: ng.ILocationProvider) => {
  $locationProvider.html5Mode(true);
  $routeProvider
    .when("/tasks", { template: "<task-list></task-list>" });
});

总结

本文介绍了在AngularJS项目中使用自己的文件分割方法。特别是关于使用Common JS(如Browserify的require和ES6的import)进行文件分割,陷阱多且方法因人而异。我希望通过这个来引发一些“这样做可能更好”或“这个应该避免”的讨论。

让我们一起共同创建最佳实践。

感谢词

我非常感谢前文作者以及Twitter上提供了大量关于AngularJS和TypeScript的建议的@armorik83先生。本篇文章的内容受到了《AngularJS现代实践》以及其中介绍的AngularJS应用程序《likr/interactive-sem》的极大影响。

参考文献

    • AngularJSモダンプラクティス – Qiita

 

    • likr/interactive-sem

 

    • What’s new in TypeScript · Microsoft/TypeScript Wiki

 

    • vvakame/dtsm

 

    • gulp-angular-templatecache

 

    AngularJS1.4とbindToController – Qiita
广告
将在 10 秒后关闭
bannerAds