资讯专栏INFORMATION COLUMN

AngularJS 风格指南 (ES2015)

JaysonWang / 1139人阅读

摘要:风格指南的最佳实践涵盖体系结构文件结构组件单向数据流和生命周期。模块概述模块的设计直接反应了我们的文件结构从而保持了可维护性和可预测性。指令不应再声明模板和控制器,或者通过绑定接收数据。指令应该只是用来和互交。

# AngularJS 风格指南 (ES2015)

AngularJs1.6.x的最佳实践.涵盖体系结构,文件结构,组件,单向数据流和生命周期。 目录

模块化体系结构

概述

Root module

Component module

Common module

Low-level modules

文件命名约定

可伸缩文件结构

组件

概述

支持的属性

控制器

单项数据流和事件

有状态组件/容器型组件

无状态组件/展示型组件

路由组件

指令

概述

推荐的属性

Constants or Classes

服务

概述

Classes for Service

样式

ES2015 and Tooling

状态管理

模块化体系结构

Angular应用程序中的每个模块都应该是组件模块。组件模块用来封装逻辑、模板、路由和子组件。

模块概述

模块的设计直接反应了我们的文件结构, 从而保持了可维护性和可预测性。我们最好有三个高级模块: root、component和common。root模块是用来启动我们应用和相应模板的基础模块,root模块中导入component和common模块作为依赖模块。component和common模块则负责引入Low-level modules,Low-level modules通常包含可重用的组件,控制器,服务,指令,过滤器和测试.

Back to top

Root 模块

root模块中定义整个应用的根组件,根组件中通常包含路由组件,比如ui-router 中的 ui-view

// app.component.js
export const AppComponent = {
  template: `
    
Hello world
Copyright MyApp 2016.
` };
// app.module.js
import angular from "angular";
import uiRouter from "angular-ui-router";
import { AppComponent } from "./app.component";
import { ComponentsModule } from "./components/components.module";
import { CommonModule } from "./common/common.module";
import "./app.scss";

export const AppModule = angular
  .module("app", [
    ComponentsModule,
    CommonModule,
    uiRouter
  ])
  .component("app", AppComponent)
  .name;

使用 .component("app", AppComponent) 方法在root模块中注册根组件,你可能注意到样式也被引入到了根组件,我们将在后面的章节介绍这一点.

Back to top

Components 模块

所有的可重用组件应该注册在component模块上。上面例子中展示了我们是如何导入 ComponentsModule 并将它们注册在root模块中。这样做可以轻松的将component模块移动到任何其他应用程序中,因为root模块与component模块是分离的。

import angular from "angular";
import { CalendarModule } from "./calendar/calendar.module";
import { EventsModule } from "./events/events.module";

export const ComponentsModule = angular
  .module("app.components", [
    CalendarModule,
    EventsModule
  ])
  .name;

Back to top

Common module

所有我们不希望用在其他应用中的组件应该注册在common模块上。比如布局、导航和页脚之类的内容。

import angular from "angular";
import { NavModule } from "./nav/nav.module";
import { FooterModule } from "./footer/footer.module";

export const CommonModule = angular
  .module("app.common", [
    NavModule,
    FooterModule
  ])
  .name;

Back to top

Low-level modules

Low-level module是包含独立功能的组件模块,将会被导入到像component module或者是 common module这样的higher-level module中,下面是一个例子。一定要记住在每个export的模块最后添加.name。你可能注意到路由定义也在这个模块中,我们将在本指南后面的章节中介绍这一点。

import angular from "angular";
import uiRouter from "angular-ui-router";
import { CalendarComponent } from "./calendar.component";
import "./calendar.scss";

export const CalendarModule = angular
  .module("calendar", [
    uiRouter
  ])
  .component("calendar", CalendarComponent)
  .config(($stateProvider, $urlRouterProvider) => {
    "ngInject";
    $stateProvider
      .state("calendar", {
        url: "/calendar",
        component: "calendar"
      });
    $urlRouterProvider.otherwise("/");
  })
  .name;

Back to top

文件命名约定

保持文件名简单,并且使用小写字母,文件名使用 " - "分割,比如 calendar-grid.*.js 。使用 *.component.js 标示组件,使用 *.module.js 标示模块

calendar.module.js
calendar.component.js
calendar.service.js
calendar.directive.js
calendar.filter.js
calendar.spec.js
calendar.html
calendar.scss

Back to top

可伸缩文件结构

文件结构是非常重要的,这描述了一个可伸缩和可预测的结构,下面是一个例子。

├── app/
│   ├── components/
│   │  ├── calendar/
│   │  │  ├── calendar.module.js
│   │  │  ├── calendar.component.js
│   │  │  ├── calendar.service.js
│   │  │  ├── calendar.spec.js
│   │  │  ├── calendar.html
│   │  │  ├── calendar.scss
│   │  │  └── calendar-grid/
│   │  │     ├── calendar-grid.module.js
│   │  │     ├── calendar-grid.component.js
│   │  │     ├── calendar-grid.directive.js
│   │  │     ├── calendar-grid.filter.js
│   │  │     ├── calendar-grid.spec.js
│   │  │     ├── calendar-grid.html
│   │  │     └── calendar-grid.scss
│   │  ├── events/
│   │  │  ├── events.module.js
│   │  │  ├── events.component.js
│   │  │  ├── events.directive.js
│   │  │  ├── events.service.js
│   │  │  ├── events.spec.js
│   │  │  ├── events.html
│   │  │  ├── events.scss
│   │  │  └── events-signup/
│   │  │     ├── events-signup.module.js
│   │  │     ├── events-signup.component.js
│   │  │     ├── events-signup.service.js
│   │  │     ├── events-signup.spec.js
│   │  │     ├── events-signup.html
│   │  │     └── events-signup.scss
│   │  └── components.module.js
│   ├── common/
│   │  ├── nav/
│   │  │     ├── nav.module.js
│   │  │     ├── nav.component.js
│   │  │     ├── nav.service.js
│   │  │     ├── nav.spec.js
│   │  │     ├── nav.html
│   │  │     └── nav.scss
│   │  ├── footer/
│   │  │     ├── footer.module.js
│   │  │     ├── footer.component.js
│   │  │     ├── footer.service.js
│   │  │     ├── footer.spec.js
│   │  │     ├── footer.html
│   │  │     └── footer.scss
│   │  └── common.module.js
│   ├── app.module.js
│   ├── app.component.js
│   └── app.scss
└── index.html

顶级文件夹仅包含index.html and app/,其余的模块在app/

Back to top

组件 组件概述

组件是带有控制器的模板,组件可以通过 bindings 定义数据或是事件的输入和输出,你可以将组件视为完整的代码段,而不仅仅是 .component() 定义的对象,让我们探讨一些关于组件的最佳实践和建议, 然后深入了解如何通过有状态的、无状态的和路由组件的概念来构造它们。

Back to top

支持的属性

这些是 .component() 支持的属性

Property Support
bindings Yes, use "@", "<", "&" only
controller Yes
controllerAs Yes, default is $ctrl
require Yes (new Object syntax)
template Yes
templateUrl Yes
transclude Yes

Back to top

控制器

控制器应该只和组件一起使用,如果你感觉需要一个控制器,可能你需要的是一个管理这个特定行为的组件。

这是一些使用 Class 定义controllers的建议:

使用 controller: class TodoComponent {...} 这种写法以应对未来向Angular迁移

始终使用 constructor 来进行依赖注入

使用 babel-plugin-angularjs-annotate 的 "ngInject";语法

如果需要访问词法作用域,请使用箭头函数

除了箭头函数 let ctrl = this; 也是可以接受的

将所有的公共函数绑定到 class{}

适当的使用 $onInit, $onChanges, $postLink and $onDestroy 等生命周期函数

Note: $onChanges is called before $onInit, see resources section for articles detailing this in more depth

Use require alongside $onInit to reference any inherited logic

Do not override the default $ctrl alias for the controllerAs syntax, therefore do not use controllerAs anywhere

Back to top

单向数据流和事件

AngularJS 1.5中引入了单向数据流,这重新定义了组件之间的通信.

这里有一些使用单向数据流的建议:

在组件中接受数据时,总是使用单向数据绑定语法 "<"

任何时候都不要在使用双向绑定语法 "="

使用 bindings 绑定传入数据时,在 $onChanges 生命周期函数中深复制传入的对象以解除父组件的引用

在父组件的方法中使用 $event 作为函数的参数(查看下面有状态组件的例子$ctrl.addTodo($event))

从无状态组件的方法中传递回 $event: {} 对象(查看下面的无状态组件例子 this.onAddTodo)

Back to top

有状态组件

让我们定义一个有状态组件

获取状态,实质上是通过服务与后台API通信

不要直接变化状态

一个包括low-level module定义的有状态组件的例子(为了简洁省略了一些代码)

/* ----- todo/todo.component.js ----- */
import templateUrl from "./todo.html";

export const TodoComponent = {
  templateUrl,
  controller: class TodoComponent {
    constructor(TodoService) {
      "ngInject";
      this.todoService = TodoService;
    }
    $onInit() {
      this.newTodo = {
        title: "",
        selected: false
      };
      this.todos = [];
      this.todoService.getTodos().then(response => this.todos = response);
    }
    addTodo({ todo }) {
      if (!todo) return;
      this.todos.unshift(todo);
      this.newTodo = {
        title: "",
        selected: false
      };
    }
  }
};

/* ----- todo/todo.html ----- */
/* ----- todo/todo.module.js ----- */ import angular from "angular"; import { TodoComponent } from "./todo.component"; import "./todo.scss"; export const TodoModule = angular .module("todo", []) .component("todo", TodoComponent) .name;

这个例子展示了一个有状态组件,在控制器中通过服务获取数据,然后传递给无状态子组件。注意我们在模板中并没有使用ng-repeat 之类的指令,而是委托给 组件

Back to top

无状态组件

让我们第一我们所谓的无状态组件:

使用 bindings: {} 明确的定义输入和输出

通过属性绑定传递数据

数据通过事件离开组件

不要关心数据来自哪里 - 它是无状态的

是高度可重用的组件

也被称为展示型组件

一个无状态组件的例子( 为了简洁省略了一些代码)

/* ----- todo/todo-form/todo-form.component.js ----- */
import templateUrl from "./todo-form.html";

export const TodoFormComponent = {
  bindings: {
    todo: "<",
    onAddTodo: "&"
  },
  templateUrl,
  controller: class TodoFormComponent {
    constructor(EventEmitter) {
        "ngInject";
        this.EventEmitter = EventEmitter;
    }
    $onChanges(changes) {
      if (changes.todo) {
        this.todo = Object.assign({}, this.todo);
      }
    }
    onSubmit() {
      if (!this.todo.title) return;
      // with EventEmitter wrapper
      this.onAddTodo(
        this.EventEmitter({
          todo: this.todo
        })
      );
      // without EventEmitter wrapper
      this.onAddTodo({
        $event: {
          todo: this.todo
        }
      });
    }
  }
};

/* ----- todo/todo-form/todo-form.html ----- */
/* ----- todo/todo-form/todo-form.module.js ----- */ import angular from "angular"; import { TodoFormComponent } from "./todo-form.component"; import "./todo-form.scss"; export const TodoFormModule = angular .module("todo.form", []) .component("todoForm", TodoFormComponent) .value("EventEmitter", payload => ({ $event: payload })) .name;

注意, 组件没有状态,仅仅是接收数据,通过属性绑定的事件传递数据回到父组件。上例中,在 $onChanges 函数内深复制了 this.todo ,这意味着在提交回父组件前,父组件的数据是不受影响的,

Back to top

路由组件

让我们定义一个路由组件。

一个带有路由定义的有状态组件

没有 router.js 文件

使用路由组件来定义他们自己的路由逻辑

组件数据的输入是通过路由的 resolve

在这个例子中,我们将使用路由定义和 bindings重构 组件.我们将其视为路由组件,因为它本质上是一个"视图"

/* ----- todo/todo.component.js ----- */
import templateUrl from "./todo.html";

export const TodoComponent = {
  bindings: {
    todoData: "<"
  },
  templateUrl,
  controller: class TodoComponent {
    constructor() {
      "ngInject"; // Not actually needed but best practice to keep here incase dependencies needed in the future
    }
    $onInit() {
      this.newTodo = {
        title: "",
        selected: false
      };
    }
    $onChanges(changes) {
      if (changes.todoData) {
        this.todos = Object.assign({}, this.todoData);
      }
    }
    addTodo({ todo }) {
      if (!todo) return;
      this.todos.unshift(todo);
      this.newTodo = {
        title: "",
        selected: false
      };
    }
  }
};

/* ----- todo/todo.html ----- */
/* ----- todo/todo.service.js ----- */ export class TodoService { constructor($http) { "ngInject"; this.$http = $http; } getTodos() { return this.$http.get("/api/todos").then(response => response.data); } } /* ----- todo/todo.module.js ----- */ import angular from "angular"; import uiRouter from "angular-ui-router"; import { TodoComponent } from "./todo.component"; import { TodoService } from "./todo.service"; import "./todo.scss"; export const TodoModule = angular .module("todo", [ uiRouter ]) .component("todo", TodoComponent) .service("TodoService", TodoService) .config(($stateProvider, $urlRouterProvider) => { "ngInject"; $stateProvider .state("todos", { url: "/todos", component: "todo", resolve: { todoData: TodoService => TodoService.getTodos() } }); $urlRouterProvider.otherwise("/"); }) .name;

Back to top

指令 指令概述

指令有 template, scope 绑定, bindToController, link 和很多其他特性。在基于组件的应用中应该注意他们的使用。指令不应再声明模板和控制器,或者通过绑定接收数据。指令应该只是用来和DOM互交。简单来说,如果你需要操作DOM,那就写一个指令,然后在组件的模板中使用它。如果您需要大量的DOM操作,还可以考虑$ postLink生命周期钩子,但是,这不是让你将所有DOM操作迁移到这个函数中。

这有一些使用指令的建议:

不要在使用 templates, scope, bindToController or controllers

指令总是使用 restrict: "A"

必要时使用编译和链接函数

记住在 $scope.$on("$destroy", fn); 中销毁和解除绑定事件处理程序

Back to top

推荐的属性

由于指令支持.component()所做的大多数事情(指令是原始组件),我建议将指令对象定义限制为仅限于这些属性,以避免错误地使用指令:

Property Use it? Why
bindToController No Use bindings in components
compile Yes For pre-compile DOM manipulation/events
controller No Use a component
controllerAs No Use a component
link functions Yes For pre/post DOM manipulation/events
multiElement Yes See docs
priority Yes See docs
require No Use a component
restrict Yes Defines directive usage, always use "A"
scope No Use a component
template No Use a component
templateNamespace Yes (if you must) See docs
templateUrl No Use a component
transclude No Use a component

Back to top

Constants or Classes

在ES2015中有几种方式使用指令,要么通过箭头函数,要么使用 Class,选择你认为合适的。

下面是一个使用箭头函数的例子:

/* ----- todo/todo-autofocus.directive.js ----- */
import angular from "angular";

export const TodoAutoFocus = ($timeout) => {
  "ngInject";
  return {
    restrict: "A",
    link($scope, $element, $attrs) {
      $scope.$watch($attrs.todoAutofocus, (newValue, oldValue) => {
        if (!newValue) {
          return;
        }
        $timeout(() => $element[0].focus());
      });
    }
  }
};

/* ----- todo/todo.module.js ----- */
import angular from "angular";
import { TodoComponent } from "./todo.component";
import { TodoAutofocus } from "./todo-autofocus.directive";
import "./todo.scss";

export const TodoModule = angular
  .module("todo", [])
  .component("todo", TodoComponent)
  .directive("todoAutofocus", TodoAutoFocus)
  .name;

或者使用 ES2015 Class 创建一个对象(注意在注册指令时手动调用 "new TodoAutoFocus")

/* ----- todo/todo-autofocus.directive.js ----- */
import angular from "angular";

export class TodoAutoFocus {
  constructor($timeout) {
    "ngInject";
    this.restrict = "A";
    this.$timeout = $timeout;
  }
  link($scope, $element, $attrs) {
    $scope.$watch($attrs.todoAutofocus, (newValue, oldValue) => {
      if (!newValue) {
        return;
      }
      this.$timeout(() => $element[0].focus());
    });
  }
}

/* ----- todo/todo.module.js ----- */
import angular from "angular";
import { TodoComponent } from "./todo.component";
import { TodoAutofocus } from "./todo-autofocus.directive";
import "./todo.scss";

export const TodoModule = angular
  .module("todo", [])
  .component("todo", TodoComponent)
  .directive("todoAutofocus", ($timeout) => new TodoAutoFocus($timeout))
  .name;

Back to top

服务 服务概述

服务本质上是业务逻辑的容器。服务包含其他内置或外部服务例如$http,我们可以在应用的其他位置注入到控制器中。我们有两种使用服务的方式,.service().factory()。如果要使用ES2015的 Class,我们应该使用.service(),并且使用$inject自动完成依赖注入。

Back to top

Classes for Service

这是一个使用 ES2015 Class的例子:

/* ----- todo/todo.service.js ----- */
export class TodoService {
  constructor($http) {
    "ngInject";
    this.$http = $http;
  }
  getTodos() {
    return this.$http.get("/api/todos").then(response => response.data);
  }
}

/* ----- todo/todo.module.js ----- */
import angular from "angular";
import { TodoComponent } from "./todo.component";
import { TodoService } from "./todo.service";
import "./todo.scss";

export const TodoModule = angular
  .module("todo", [])
  .component("todo", TodoComponent)
  .service("TodoService", TodoService)
  .name;

Back to top

样式

使用 Webpack 我们可以在*.module.js 中用 import 导入我们的 .scss 文件,这样做可以让我们的组件在功能和样式上都是隔离的。

如果你有一些变量或全局使用的样式,比如表单输入元素,那么这些文件仍然应该放在根目录scss文件夹中。 例如 SCSS / _forms.scss。这些全局样式可以像通常那样被 @importe.

Back to top

ES2015 and Tooling
ES2015

使用Babel编译你写的ES2015+代码

考虑使用TypeScript

Tooling

如果想支持组件路由,那么使用 ui-router latest alpha

使用 Webpack 编译你的ES2015+代码和样式

使用webpack的ngtemplate-loader

使用babel的babel-plugin-angularjs-annotate

Back to top

状态管理

考虑使用redux管理你应用的状态.

Angular Redux

Back to top

资源

Stateful and stateless components, the missing manual

Understanding the .component() method

Using "require" with $onInit

Understanding all the lifecycle hooks, $onInit, $onChanges, $postLink, $onDestroy

Using "resolve" in routes

Redux and Angular state management

Sample Application from Community

Back to top

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93368.html

相关文章

  • Angular1.x + TypeScript 编码风格

    摘要:组件还包含数据事件的输入与输出,生命周期钩子和使用单向数据流以及从父组件上获取数据的事件对象备份。 说明:参照了Angular1.x+es2015的中文翻译,并将个人觉得不合适、不正确的地方进行了修改,欢迎批评指正。 架构,文件结构,组件,单向数据流以及最佳实践 来自@toddmotto团队的实用编码指南 Angular 的编码风格以及架构已经使用ES2015进行重写,这些在Angul...

    ytwman 评论0 收藏0
  • 前端练级攻略(第二部分)

    摘要:是文档的一种表示结构。这些任务大部分都是基于它。这个实践的重点是把你在前端练级攻略第部分中学到的一些东西和结合起来。一旦你进入框架部分,你将更好地理解并使用它们。到目前为止,你一直在使用进行操作。它是在前端系统像今天这样复杂之前编写的。 本文是 前端练级攻略 第二部分,第一部分请看下面: 前端练级攻略(第一部分) 在第二部分,我们将重点学习 JavaScript 作为一种独立的语言,如...

    BWrong 评论0 收藏0
  • 【译】前端练级攻略

    摘要:由于系统变得越来越复杂,人们提出了称为预处理器和后处理器的工具来管理复杂性。后处理器在由预处理器手写或编译后对应用更改。我之前建议的文章,,也涵盖了预处理器相关的知识。 译者:前端小智 原文:medium.freecodecamp.org/from-zero-t… medium.freecodecamp.org/from-zero-t… 我记得我刚开始学习前端开发的时候。我看到了很多文章及...

    wuyumin 评论0 收藏0
  • 前端文档收集

    摘要:系列种优化页面加载速度的方法随笔分类中个最重要的技术点常用整理网页性能管理详解离线缓存简介系列编写高性能有趣的原生数组函数数据访问性能优化方案实现的大排序算法一怪对象常用方法函数收集数组的操作面向对象和原型继承中关键词的优雅解释浅谈系列 H5系列 10种优化页面加载速度的方法 随笔分类 - HTML5 HTML5中40个最重要的技术点 常用meta整理 网页性能管理详解 HTML5 ...

    muddyway 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<