Статьи

Создание магистральных приложений с помощью TypeScript

Горячий от прессы, TypeScript был объявлен ранее на этой неделе и привел к взрыву комментариев через Twitter и различные блоги. Язык претендует на то, чтобы быть адаптированным для разработки приложений с использованием JavaScript, предоставляя типизированный надмножество. Он компилируется в JavaScript, поэтому он будет работать в любом браузере. Обещание состоит в том, чтобы упростить организацию и структурирование кода, а также избавиться от тех досадных ошибок, которые заставляют пользователей искать сотни строк кода в поисках ошибочной запятой или проблемы с чувствительностью к регистру.

По иронии судьбы я говорил с посетителем нашей конференции Devscovery о будущем JavaScript прямо перед объявлением TypeScript. Очевидно, что JavaScript здесь, чтобы остаться, и он не станет лучше. Он может получить лучшие детали, но обратная совместимость заставит плохие части оставаться в течение долгого времени. Что еще хуже, нет ничего, что убедило бы меня, что производители браузеров будут хорошо играть со спецификациями, когда у нас есть стандарт на протяжении почти 20 лет, и он все еще по-разному реализован в разных браузерах. Я пришел к выводу о том, что до того, как я несколько лет был испорчен простотой написания надежных бизнес-приложений Silverlight, я пришел к выводу, что, если нам приходится иметь дело с этим, нам нужно решать его с трех сторон:

  1. Образование: JavaScript — это динамический язык, где функции, а не классы, правила. Разработчики должны прекратить пытаться играть в объектно-ориентированного разработчика на клиентском пространстве и научиться играть по правилам клиента.
  2. Умеренность : сказав, что есть функции, которые просто делают слишком легким выстрелить себе в ногу. Несмотря на то, что есть подходящие решения для решения этой проблемы, такие как запуск JSHint / JSLint, я лично приветствую подход, позволяющий выявлять проблемы во время компиляции (или, что еще лучше, разработки) и сохранять нас всех честными. Ошибки легче исправить, чем ближе к источнику и чем раньше вы находитесь в процессе. Одна из вещей, которые я упомянул, заключалась в том, что должно быть реализовано что-то вроде CoffeeScript , которое помогает защитить нас от плохих частей при разработке, но все же генерирует в этот дружественный обратно-совместимый JavaScript-код, который любят все браузеры.
  3. Лучше клей . Да, я сказал это. Разработчики Silverlight знают, о чем я говорю * кашляю * RIA Services * кашляю …… теперь, когда у нас есть очень стандартные способы представления API, должны быть более простые способы приклеить клиента к серверу, чтобы разработчики работали над «чем отличается И не тратить 75% своего времени на создание методов контроллера и написание AJAX-вызовов для клиента.

Я считаю, что TypeScript — это инструмент, который сохраняет первый элемент (он поддерживает JavaScript и не пытается скрыть динамическую и гибкую природу) и обращается ко второму элементу (он позволяет нам гораздо яснее заявить о своем намерении и сразу получить обратную связь) и, вероятно, ничего не делает для последнего элемента.

Вместо того, чтобы обсуждать, нужен ли нам другой язык, или он хороший или плохой (и, эй, я люблю C #, и я любил TurboPascal, так что тот факт, что он был разработан Андерсом Хейлсбергом , не помешает), я бы лучше занялся используя его, я могу начать понимать, как это помогает или причиняет боль. Завтра я буду в течение трех часов выступать на эту конкретную тему (как использовать библиотеки в разработке JavaScript для помощи в масштабировании для больших проектов и для больших групп), поэтому я решил взять пример, который я создал с Backbone.JS, чтобы увидеть, что это будет принять в порт. Вот так!

Первым шагом была просто реализация подключаемого модуля VS 2012. Я уверен, что с треском провалился. Я получаю подсветку синтаксиса, но нет шаблона для добавления нового файла TypeScript, нет автоматической компиляции и нет эффективного IntelliSense. Я уверен, что это ошибка оператора, так что следите за обновлениями. Вы можете получить все, что вам нужно (или я должен сказать «все, что доступно») по ссылке загрузки TypeScript . Затем я добавил файл JavaScript и переименовал его, чтобы он имел расширение .ts, и добавил сценарий после сборки для его компиляции:

tsc $(ProjectDir)/Scripts/app.ts

Затем я взял спецификацию языка и пошел на работу. Во-первых, я обнаружил, что мне нужно объявить некоторые переменные для учета внешних модулей:

declare var $: any;
declare var _: any;

Это было просто! Однако использование Backbone было немного сложнее. Я обманул, взглянув на существующий образец TodoMVC с сайта TypeScript. Чтобы Backbone хорошо играл с TypeScript, я должен определить модуль и экспортировать классы, которые описывают API. Это не должен быть полный API, но он может сосредоточиться на частях, которые я буду использовать. Вот пример экспорта объекта Backbone Model в виде класса:

declare module Backbone {   
   
    export class Model {
        constructor (attr? , opts? );
        get(name: string): any;
        set(name: string, val: any): void;
        set(obj: any): void;
        save(attr? , opts? ): void;
        destroy(): void;
        bind(ev: string, f: Function, ctx?: any): void;
        toJSON(): any;
    }
}

Обратите внимание, что у меня есть дополнительные параметры в конструкторе. Я могу объявить намерение, указав тип для именованных параметров и тип возвращаемого значения функции. Довольно круто. Мне также нравится использовать Backbone.Events, а TypeScript позволяет мне определять интерфейсы. Вот интерфейс для событий, определяющих подпись для «вкл» и «выкл» (зарегистрируйтесь и отмените регистрацию):

interface HasEvents {
    on: (event: string, callback: (parms?: any, moreParms?: any) => any) => any;
    off: (event: string) => any;
}

Самый простой способ прочитать это, на мой взгляд, справа налево. Функция, которая возвращает любой тип, принимает параметры, ну, в случае первого, функции, которая имеет два необязательных параметра и возвращает любой тип, а первый параметр является строкой. Я также сделал интерфейс для простой версии контакта, чтобы было понятно, когда я ожидаю такой пример:

interface ContactInterface {
    id?: number;
    firstName: string;
    lastName: string;
    email: string;
    address: string;
    city: string;
    state: string;           
};

Вы также можете объявить статические свойства для классов, поэтому я создал шаблонный класс просто для хранения скомпилированных шаблонов, которые я использую:

class Templates {
    static contact: (json: any) => string;
    static editContact: (json: any) => string;
    static contacts: (json: any) => string;
};

Обратите внимание, что скомпилированный шаблон принимает любой тип объекта и возвращает строку. Наконец, я создал глобальный объект для размещения сквозных событий вместо использования библиотеки pub / sub, такой как Postal . Обратите внимание на реализацию интерфейса HasEvents .

class AppEvents implements HasEvents
{
    on: (event: string, callback: (model: Contact, error: any) => any) => any;
    off: (event: string) => any; 
    trigger: (event: string, data: any) => any; 
};

Наконец, вы видите простой JavaScript — я создаю объект и расширяю его, чтобы иметь события:

var appEvents = new AppEvents();
_.extend(appEvents, Backbone.Events); 

Итак, теперь давайте рассмотрим, как мы это делали «по-старому» против «по-новому» Вот старый способ, которым я определил свою модель контакта:

App.Models.Contact = Backbone.Model.extend({
    initialize: function () {
        console.log("Contact init.");
    },   
    defaults: function () {
        return {
            id: null,
            firstName: '',
            lastName: '',
            email: '',
            address: '',
            city: '',
            state: ''
        };
    },   
    validate: function (attrs) {
        if (_.isEmpty(attrs.firstName)) {
            return "First name is required.";
        }
        if (_.isEmpty(attrs.lastName)) {
            return "Last name is required.";
        }
        if (_.isEmpty(attrs.email)) {
            return "Email is required.";
        }
        if (attrs.email.indexOf("@") < 1 ||
        attrs.email.indexOf(".") < 1) {
            return "The email address appears to be invalid.";
        }
        return "";
    }
});

По сути, я что-то записываю на консоль, предоставляю шаблон для экземпляра контакта и объявляю мои проверки. Обратите внимание, что нет способа гарантировать то, что я возвращаю из функции (я мог бы так же легко отправить обратно {foo: “bar”} ), и что атрибуты для проверки могут быть чем угодно. С TypeScript я могу быть более конкретным и даже генерировать ошибки времени разработки (и времени компиляции), если я не делаю то, что ожидаю:

class Contact extends Backbone.Model implements HasEvents {
    on: (event: string, callback: (model: Contact, error: any) => any) => any;
    off: (event: string) => any;   
    isValid: () => bool;   
    initialize() {
        console.log("Contact init.");
    };
    defaults() : ContactInterface {
       return {
            id: null,
            firstName: '',
            lastName: '',
            email: '',
            address: '',
            city: '',
            state: ''
        };
    };
    validate(attrs: ContactInterface) {      
       if (_.isEmpty(attrs.firstName)) {
            return "First name is required.";
       }      
       if (_.isEmpty(attrs.lastName)) {
            return "Last name is required.";
       }      
       if (_.isEmpty(attrs.email)) {
            return "Email is required.";
       }      
       if (attrs.email.indexOf("@") < 1 ||
        attrs.email.indexOf(".") < 1) {
            return "The email address appears to be invalid.";
       }      
       return "";
    }
};

Обратите внимание, что я реализую события более конкретным способом, предоставляя подробную подпись для обратного вызова. Функции просто объявляются аналогично тому, как они определены в классах C #, и мне не нужно беспокоиться о представлении имени свойства. Наконец, я могу ввести возвращаемое значение и параметры для различных функций. Довольно круто, нет? Я все еще использую JavaScript, и объект все еще может быть динамическим, но я могу по крайней мере ограничить его конкретным интерфейсом, который я собираюсь использовать в своем методе.

Вот старое определение для коллекции, передавая параметры для ее настройки, чтобы они указывали на REST API на моем сервере:

App.Collections.Contacts = Backbone.Collection.extend({
    model: App.Models.Contact,
    url: "/api/Contacts",
    initialize: function () {
        console.log("Contacts init.");
    }
});

Вот новое определение, которое происходит от базового класса и использует вместо него конструктор. Я также ясно даю понять, что использую события в коллекции, чего не видно в оригинальном JavaScript.

class Contacts extends Backbone.Collection implements HasEvents {   
    on: (event: string, callback: (parms?: any, moreParms?: any) => any) => any;   
    off: (event: string) => any;
    url: string;  
    model: Contact;       
    initialize() {
        console.log("Contacts init.");
    }
    constructor(options?: any) {
        this.url = "/api/Contacts";   
        super(options);       
    };
};

Еще одна интересная особенность — я могу объявлять типы, а затем приводить вызовы, которые не видны типу. Например, я не сообщил TypeScript, что jQuery может вернуть элемент DOM, но в моем объявлении для представлений я использую тип HTMLElement. Чтобы согласовать это, я могу просто привести результат так:

this.el = <HTMLElement>$("#contact");    

Я знаю, что это похоже на HTML-тег, но это потому, что это не истинное приведение, а всего лишь подсказка, что это ожидаемый тип возврата. После еще нескольких моделей и представлений я определяю общий объект приложения, который раскручивает начальную коллекцию в конструкторе. Обратите внимание, как я также устанавливаю свойства статического шаблона — очень простой синтаксис. Создание объекта запускает приложение:

class Application {
       
    public static editContactView: EditContactView;

    constructor() {
        Templates.contact = _.template($("#contactTemplate").html());
        Templates.editContact = _.template($("#editContactTemplate").html());
        Templates.contacts = _.template($("#contactsTemplate").html());
        var contacts = new Contacts();
        contacts.fetch();
        var contactsView = new ContactList({
            collection: contacts
        });       
        contactsView.render();
    };
};

// host container
var App = new Application();

Приложение демонстрирует CRUD со списком, который заполняет форму с проверкой, где пользователь может редактировать, удалять и добавлять новые контакты.

backbonetypescriptexample

Процесс сборки компилирует TypeScript в файл JavaScript, на который фактически ссылаются из представления MVC. Вы не увидите интерфейсы или определения, потому что они помогают навязывать вещи во время разработки и компиляции. Как выглядит результат? Вот скомпилированный объект события всего приложения, который я создал:

var AppEvents = (function () {
    function AppEvents() { }
    return AppEvents;
})();
; ;
var appEvents = new AppEvents();
_.extend(appEvents, Backbone.Events);

Вот определение модели контакта — обратите внимание, как передается базовая модель для расширения:

var Contact = (function (_super) {
    __extends(Contact, _super);
    function Contact() {
        _super.apply(this, arguments);

    }
    Contact.prototype.initialize = function () {
        console.log("Contact init.");
    };
    Contact.prototype.defaults = function () {
        return {
            id: null,
            firstName: '',
            lastName: '',
            email: '',
            address: '',
            city: '',
            state: ''
        };
    };
    Contact.prototype.validate = function (attrs) {
        if(_.isEmpty(attrs.firstName)) {
            return "First name is required.";
        }
        if(_.isEmpty(attrs.lastName)) {
            return "Last name is required.";
        }
        if(_.isEmpty(attrs.email)) {
            return "Email is required.";
        }
        if(attrs.email.indexOf("@") < 1 || attrs.email.indexOf(".") < 1) {
            return "The email address appears to be invalid.";
        }
        return "";
    };
    return Contact;
})(Backbone.Model);

Джереми, где полная загрузка? Извините, сейчас проект зарезервирован для посетителей Devscovery — у меня может быть полный проект, который будет доступен после этой недели, но сейчас вы можете посмотреть полный исходный код TypeScript (это все клиентское приложение), нажав здесь ,

Благодаря! Что я думаю? Пока мои мысли очень позитивны. Я считаю, что это полезный инструмент, который позволяет командам писать код, к которому они привыкли, с возможностью добавлять меры безопасности и типы, где это необходимо, чтобы избежать ошибок и облегчить организацию, чтение и понимание кода. Мне придется использовать его еще немного, но этот короткий проект преобразования приложения заставил меня поднять оба больших пальца вверх.