【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