Строгая типизация для приложений Vue.js на TypeScript

  • Tutorial
Вопрос: Каковы самые слабые места Vue?

Oтвет: На данный момент, наверное, недружественность к типизации. Наш API разрабатывался без планирования поддержки типизированных языков (типа TypeScript), но мы сделали большие улучшения в 2.5.

Вопрос: Тони Хор (Tony Hoare) назвал null ошибкой на миллиард долларов. Какое было самое неудачное техническое решение в твоей карьере?

Oтвет: Было бы неплохо использовать TypeScript изначально, еще когда я начал переписывать код для Vue 2.x.

из интервью "Создатель Vue.js отвечает Хабру"

Недружественность Vue.js к типизации вынуждает применять "костыли", чтобы использовать преимущества TypeScript. Один из предлагаемых в официальной документации Vue.js вариантов — это применение декораторов вместе с библиотекой "vue-class-component".


Я применяю другой вариант "костылей" для решения проблемы строгой типизации в приложениях Vue.js (без декораторов и vue-class-component). Через явное определение интерфейсов для опций "data" и "props", используемых в конструкторе экземпляров Vue-компоненты. В ряде случаев это проще и удобнее.


В данном tutorial, для иллюстрации обоих подходов к типизации (с декораторами и без) используется решение Visual Studio 2017 с приложениями Vue.js + Asp.Net Core MVC + TypeScript. Хотя приведенные здесь примеры можно поместить и в другое окружение (Node.js + Webpack).


Попутно демонстрируется, как компоненту на JavaScript быстро переделать под «полноценный» TypeScript с включенной строгой типизацией.


Содержание


Введение
Используемые механизмы
— Включение опций строгой типизации
— Типизация через декораторы
— Типизация через интерфейсы входных и выходных данных
Проект TryVueMvcDecorator
— Тестовое приложение
— Корректировка конфигурации
— Корректировка Index.cshtml
— Переход на декораторы
— Сборка и запуск проекта
Проект TryVueMvcGrid
— Тестовое приложение
— Создание заготовки AppGrid
— Сборка и запуск проекта
— Адаптация под строгую типизацию
Заключение


Введение


Данная статья является продолжением серии статей:



В примерах, которые приводились в этих статьях, TypeScript использовался только наполовину — строгая типизация была сознательно отключена. Теперь попробуем перейти к полноценному использованию TypeScript.


На широких просторах Интернета можно найти массу качественных примеров и готовых приложений, использующих Vue.js. Но подавляющее большинство этих примеров написано на JavaScript. Поэтому заталкивание этих примеров в "прокрустово ложе" TypeScript требует некоторых усилий.


API, который предлагается в официальной документации Vue.js, позволяет определить Vue-компоненту на основе классов при помощи официально поддерживаемого декоратора vue-class-component. Использование декораторов требует установки опции компилятора {"experimentalDecorators": true}, что несколько напрягает (есть вероятность существенных изменений в будущих версиях TypeScript). Кроме того, требуется использовать дополнительную библиотеку.


Параноидальное стремление избавляться от "лишних" библиотек привело меня к использованию явного определения интерфейсов для свойств и данных Vue-компонент при решении проблемы строгой типизации в приложениях Vue.js + TypeScript.


котик


В данном tutorial сначала опишем механизмы использования обоих вариантов "костылей", затем создадим 2 проекта: TryVueMvcDecorator, TryVueMvcGrid.


Используемые механизмы


Если исходный код Vue-компонеты, который загоняем в модуль TypeScript, написан на JavaScript, то сначала можно попытаться его откомпилировать, просто отключив все опции компилятора, отвечающие за контроль (по умолчанию они отключены). Затем в работающем коде приложения "закручиваем гайки", путем включения нужных опций с устранением причин ругани компилятора TypeScript.


После включения ряда опций компилятора код Vue-компонент может перестать компилироваться. Т.к. отсутствует явное определение переменных, перечисленных в "data" и "props". Ниже опишем способ решения этой проблемы при помощи декораторов и без них.


Включение опций строгой типизации


Опция {"strict": true} сразу включает множество проверок (noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, strictPropertyInitialization), поэтому бывает полезно включать эти проверки последовательно. Затем можно дополнительно ужесточить контроль, например, включив проверку на наличие неиспользуемых переменных и параметров.


скрытый текст фрагмента tsconfig.json
{
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    //"noImplicitAny": true,
    //"noImplicitThis": true,
    //"alwaysStrict": true,
    //"strictNullChecks": true,
    //"strictFunctionTypes": true,
    //"strictPropertyInitialization": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  },
  "include": [
    "./ClientApp/**/*.ts"
  ]
}

Постепенное ужесточение контроля компилятора TypeScript ("закручивание гаек") позволяет достаточно быстро включить строгую типизацию, не особенно вникая в логику работы Vue-компоненты.


Типизация через декораторы


Определение Vue-компоненты выглядит похожим на определение класса, но на самом деле — это вызов функции Vue.extend(), которая создает и регистрирует экземпляр объекта Vue с определенными свойствами и методами. Так как определение свойств и методов задаются в параметре вызова функции Vue.extend(), то компилятор TypeScript не всё о них знает.


В приведенном примере подразумевается, что у экземпляра Vue есть свойства: name, initialEnthusiasm, enthusiasm, а также методы: increment(), decrement(), exclamationMarks(). Естественно, компилятор TypeScript может начать ругаться благим матом при попытке включить соответствующие опции контроля типов.


Декоратор vue-class-component позволяет использовать определение Vue-компоненты в виде полноценного класса. Соответственно, появляется возможность определения всех свойств и методов Vue-компоненты в явном виде. А такое компилятор TypeScript вполне нормально переваривает.


скрытый текст фрагмента Hello.ts
// Исходный текст определения Vue-компоненты
export default Vue.extend({
    template:'#hello-template',
    props: ['name', 'initialEnthusiasm'],
    data() {
        return {
            enthusiasm: this.initialEnthusiasm
        }
    },
    methods: {
        increment() { this.enthusiasm++; },
        decrement() {
            if (this.enthusiasm > 1) {
                this.enthusiasm--;
            }
        }
    },
    computed: {
        exclamationMarks(): string {
            return Array(this.enthusiasm + 1).join('!');
        }
    }
});

// Текст определения Vue-компоненты с использованием декоратора
@Component({
    template: '#hello-template',
    props: ['name', 'initialEnthusiasm']
})
export default class HelloComponent extends Vue {
    enthusiasm!: number;
    initialEnthusiasm!: number;

    data() {
        return {
            enthusiasm: this.initialEnthusiasm
        }
    };

    // methods:
    increment() { this.enthusiasm++; };
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    };

    // computed:
    get exclamationMarks() {
        return Array(this.enthusiasm + 1).join('!');
    }
};

Типизация через интерфейсы входных и выходных данных


Применение строгой типизации через определение интерфейсов для свойств и данных основано на следующем моменте: у экземпляров Vue есть соответствующие прокси (this.$props, this.$data).


vm.$data

Объект с данными, над которым экземпляр Vue осуществляет наблюдение. Экземпляр проксирует сюда вызовы своих полей. (Например, vm.a будет указывать на vm.$data.a)

vm.$props

Объект, предоставляющий доступ к текущим входным данным компонента. Экземпляр Vue проксирует доступ к свойствам своего объекта входных данных.

Подробнее смотрите в официальной документации.

Благодаря этому, в приведенном примере для Vue-компоненты получаем: this.initialEnthusiasm эквивалентно this.$props.initialEnthusiasm, а также this.enthusiasm эквивалентно this.$data.enthusiasm. Остается в явном виде определить интерфейсы для свойств и данных, а также обеспечить явное приведение типов при использовании this.$props, this.$data.


// Пример явного определения интерфейсов
interface HelloProps {
    name: string;
    initialEnthusiasm: number;
}
interface HelloData {
    enthusiasm: number;
}

// Примеры приведения типов при использовании свойств экземпляра Vue
...
    enthusiasm = (this.$props as HelloProps).initialEnthusiasm;
...
    var thisData = this.$data as HelloData;
    if (thisData.enthusiasm > 1) {
        thisData.enthusiasm--;
    }
...

Для лучшего понимания применяемого здесь подхода приводим более сложный пример использования интерфейсов для строгой типизации:


скрытый текст фрагмента DemoGrid.ts
// Фрагмент ClientApp/components/DemoGrid.ts
interface DemoGridProps {
    rows: Array<any>;
    columns: Array<string>;
    filterKey: string;
}
interface DemoGridData {
    sortKey: string;
    sortOrders: { [index: string]: number };
}

export default Vue.extend({
...
    computed: {
        filteredData: function () {
            var thisData = (this.$data as DemoGridData);
            var thisProps = (this.$props as DemoGridProps);

            var sortKey = thisData.sortKey;
            var filterKey = thisProps.filterKey && thisProps.filterKey.toLowerCase();
            var order = thisData.sortOrders[sortKey] || 1;
            var rows = thisProps.rows;
            if (filterKey) {
                rows = rows.filter(function (row) {
                    return Object.keys(row).some(function (key) {
                        return String(row[key]).toLowerCase().indexOf(filterKey) > -1
                    })
                })
            }
            if (sortKey) {
                rows = rows.slice().sort(function (a, b) {
                    a = a[sortKey]
                    b = b[sortKey]
                    return (a === b ? 0 : a > b ? 1 : -1) * order
                })
            }
            return rows;
        }
    },
...
    methods: {
        sortBy: function (key: string) {
            var thisData = (this.$data as DemoGridData);
            thisData.sortKey = key
            thisData.sortOrders[key] = thisData.sortOrders[key] * -1
        }
    }
});

В результате получаем простой способ перехода к строгой типизации — после явного определения интерфейсов свойств и данных, тупо ищем this.someProperty и применяем в этих местах явное приведение типов. Например, this.columns превратится в (this.$props as DemoGridProps).columns.


Проект TryVueMvcDecorator


В данном разделе tutorial создаем приложение Vue.js на TypeScript с вариантом решения проблемы строгой типизации при помощи декторатора "vue-class-component".


Тестовое приложение


В качестве отправной точки для тестового приложения берём на github проект TryVueMvc для Visual Studio 2017. Либо создаем этот проект "с нуля" по предыдущему tutorial Vue.js + Asp.Net Core MVC + TypeScript и ещё Bootstrap4. Сборку и запуск проекта можно произвести в среде VS2017 либо через командную строку в каталоге проекта:


npm install
dotnet build
dotnet bundle
dotnet run

В браузере открываем страницу, адрес которой dotnet сообщает в консоли, например, http://localhost:52643.


Для предпочитающих однофайловые Vue-компонеты и сборку при помощи Webpack, в качестве отправной точки для тестового приложения можно использовать проект TryVueWebpack. Для сборки и запуска приложения, через командную строку в каталоге проекта выполняем следующее:


npm install
npm run build

Далее можно также воспользоваться dotnet run, а можно просто открыть файл wwwroot\index.html.


Корректировка конфигурации


В файле tsconfig.json добавить опцию компилятора {"experimentalDecorators": true}.


Добавляем в файл package.json установку NPM-пакета "vue-class-component".


скрытый текст package.json
{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "dependencies": {
    "jquery": "^3.3.1",
    "popper.js": "^1.12.9",
    "bootstrap": "^4.0.0",
    "vue": "^2.5.13",
    "systemjs": "^0.21.0",
    "vue-class-component": "^6.2.0"
  }
}

Корректируем bundleconfig.json для обеспечения возможности копирования vue.js и vue-class-component.js из каталога node_modules в wwwroot/vendor.


скрытый текст bundleconfig.json
[
  {
    "outputFileName": "wwwroot/dist/vendor1.js",
    "inputFiles": [
      "node_modules/jquery/dist/jquery.js",
      "node_modules/popper.js/dist/umd/popper.js",
      "node_modules/bootstrap/dist/js/bootstrap.js",
      "node_modules/systemjs/dist/system.src.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": true
  },
  {
    "outputFileName": "wwwroot/dist/vendor1.css",
    "inputFiles": [
      "node_modules/bootstrap/dist/css/bootstrap.css"
    ],
    "minify": {
      "enabled": false
    }
  },
  {
    "outputFileName": "wwwroot/dist/vendor1.min.css",
    "inputFiles": [
      "node_modules/bootstrap/dist/css/bootstrap.min.css"
    ],
    "minify": {
      "enabled": false
    }
  },
  {
    "outputFileName": "wwwroot/vendor/vue.js",
    "inputFiles": [
      "node_modules/vue/dist/vue.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": true
  },
  {
    "outputFileName": "wwwroot/vendor/vue-class-component.js",
    "inputFiles": [
      "node_modules/vue-class-component/dist/vue-class-component.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": true
  },
  {
    "outputFileName": "wwwroot/dist/main.css",
    "inputFiles": [
      "ClientApp/**/*.css"
    ],
    "minify": {
      "enabled": true
    }
  },
  {
    "outputFileName": "wwwroot/dist/app-bandle.min.js",
    "inputFiles": [
      "wwwroot/dist/app-bandle.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    }
  },
  {
    "outputFileName": "wwwroot/dist/app-templates.html",
    "inputFiles": [
      "ClientApp/**/*.html"
    ],
    "minify": {
      "enabled": false,
      "renameLocals": false
    }
  }
]

Корректировка Index.cshtml


Так как у нас появилось использование vue-class-component, необходимо сообщить SystemJS откуда грузить эту библиотеку. Для этого модифицируем код Razor-рендеринга в Views/Home/Index.cshtml.


скрытый текст Views/Home/Index.cshtml
@* Views/Home/Index.cshtml *@
@using Microsoft.AspNetCore.Hosting
@inject IHostingEnvironment hostingEnv
@{
    var suffix = hostingEnv.IsDevelopment() ? "" : ".min";
    var vueUrl = $"vendor/vue{suffix}.js";
    var vueClassComponentUrl = $"vendor/vue-class-component{suffix}.js";
    var mainUrl = $"dist/app-bandle{suffix}.js";

    ViewData["Title"] = "TryVueMvc Sample";
}
<section id="app-templates"></section>
<div id="app-root">loading..</div>
@section Scripts{
<script>
    System.config({
        map: {
            "vue": "@vueUrl",
            "vue-class-component": "@vueClassComponentUrl"
        }
    });

    $.get("dist/app-templates.html").done(function (data) {
        $('#app-templates').append(data);

        SystemJS.import('@mainUrl').then(function (m) {
            SystemJS.import('index');
        });
    });
</script>
}

Переход на декораторы


Для перехода на декораторы в нашем приложении достаточно поменять код модулей AppHello.ts и Hello.ts.


скрытый текст ClientApp/components/AppHello.ts
// ClientApp/components/AppHello.ts
import Vue from "vue";
import Component from "vue-class-component";
import HelloComponent from "./Hello";

@Component({
    template: '#app-hello-template',
    components: {
        HelloComponent
    }
})
export default class AppHelloComponent extends Vue {
    data() {
        return {
            name: "World"
        }
    }
};
скрытый текст ClientApp/components/Hello.ts
// ClientApp/components/Hello.ts
import Vue from "vue";
import Component from "vue-class-component";

@Component({
    template: '#hello-template',
    props: ['name', 'initialEnthusiasm']
})
export default class HelloComponent extends Vue {
    enthusiasm!: number;
    initialEnthusiasm!: number;

    data() {
        return {
            enthusiasm: this.initialEnthusiasm
        }
    };

    // methods:
    increment() { this.enthusiasm++; };
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    };

    // computed:
    get exclamationMarks() {
        return Array(this.enthusiasm + 1).join('!');
    }
};

Если в качестве отправной точки использовался проект TryVueWebpack, то код модулей AppHello.ts и Hello.ts будет немного отличаться.


скрытый текст ClientApp/components/AppHello.ts
// ClientApp/components/AppHello.ts
import Vue from "vue";
import Component from "vue-class-component";
import HelloComponent from "./Hello.vue";

@Component({
    components: {
        HelloComponent
    }
})
export default class AppHelloComponent extends Vue {
    data() {
        return {
            name: "World"
        }
    }
};
скрытый текст ClientApp/components/Hello.ts
// ClientApp/components/Hello.ts
import Vue from "vue";
import Component from "vue-class-component";

@Component({
    props: ['name', 'initialEnthusiasm']
})
export default class HelloComponent extends Vue {
    enthusiasm!: number;
    initialEnthusiasm!: number;

    data() {
        return {
            enthusiasm: this.initialEnthusiasm
        }
    };

    // methods:
    increment() { this.enthusiasm++; };
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    };

    // computed:
    get exclamationMarks() {
        return Array(this.enthusiasm + 1).join('!');
    }
};

Сборка и запуск проекта


Сборка и запуск приложения — традиционные для среды VS2017. Бандлинг производится через команду "Bundler&Minifier\Update Bundles" контексного меню на файле bundleconfig.json. Также сборку и запуск можно произвести через командную строку в каталоге проекта. Должны получить что-то подобное изображенному на скриншоте.


скрытый скриншот AppHello

image AppHello


Свой результат выполнения описанных действий можете сравнить с проектом TryVueMvcDecorator на github.


Проект TryVueMvcGrid


Теперь создаем приложение Vue.js на TypeScript с вариантом решения проблемы строгой типизации путем явного определения типов для входных (this.$props) и выходных (this.$data) данных Vue-компоненты. На этот раз обходимся без декоратора и дополнительной библиотеки.


Приложение немного усложним, встроив в него пример с официального сайта Vue.js Grid Component Example. Можете посмотреть этот же пример на jsfiddle.


Идем от простого к сложному. Для облегчения понимания разобьём создание AppGrid на четыре этапа:


  • подготовка тестового приложения (клонирование TryVueMvc);
  • создание скелета приложения AppGrid;
  • перенос основного исходного кода примера с официального сайта Vue.js;
  • включение опций строго типизации с адаптацией кода приложения.

Тестовое приложение


В качестве отправной точки для тестового приложения, также, как и в предыдущем случае, берём на github проект TryVueMvc для Visual Studio 2017.


Создание заготовки AppGrid


Заменяем приложение AppHello на заготовку (скелет) приложения AppGrid. Для этого меняем содержимое файла ClientApp/index.ts, а вместо старых файлов в папке ClientApp/components создаем заготовки новых компонент: AppGrid, DemoGrid.


скрытый текст ClientApp/index.ts
// ClientApp/index.ts
import Vue from "vue";
import AppGrid from "./components/AppGrid";

new Vue({
    el: "#app-root",
    render: h => h(AppGrid),
    components: {
        AppGrid
    }
});
скрытый текст ClientApp/AppGrid
// ClientApp/components/AppGrid.ts
import Vue from "vue";
import DemoGrid from "./DemoGrid";

export default Vue.extend({
    template: '#app-grid-template',
    components: {
        DemoGrid
    },
    data: function () {
        return {
            foo: 42
        }
    }
});

<!-- ClientApp/components/AppGrid.html -->
<template id="app-grid-template">
    <div>
        <h2>AppGrid component</h2>
        <demo-grid />
    </div>
</template>
скрытый текст ClientApp/DemoGrid
// ClientApp/components/DemoGrid.ts
import Vue from "vue";

export default Vue.extend({
    template: '#demo-grid-template',
    props: ['foo'],
    data: function () {
        return {
            bar: 42
        }
    }
});

<!-- ClientApp/components/DemoGrid.html -->
<template id="demo-grid-template">
    <h4>DemoGrid component</h4>
</template>

После пересборки и запуска приложения в браузере должно получиться что-то подобное изображенному на скриншоте.


скрытый скриншот AppGrid

image AppGrid


Встраивание примера DemoGrid


Переносим код AppGrid.ts и содержимое шаблона. Производим замену возвращаемого свойства 'gridData' -> 'gridRows', чтобы не путать с data(). Компиляция ts-кода должна пройти нормально даже после включения опций контроля типов, т.к. здесь строгая типизация не требуется.

скрытый текст ClientApp/AppGrid
// ClientApp/components/AppGrid.ts
import Vue from "vue";
import DemoGrid from "./DemoGrid";

export default Vue.extend({
    template: '#app-grid-template',
    components: {
        DemoGrid
    },
    data: function() {
        return {
            searchQuery: '',
            gridColumns: ['name', 'power'],
            gridRows: [
                { name: 'Chuck Norris', power: Infinity },
                { name: 'Bruce Lee', power: 9000 },
                { name: 'Jackie Chan', power: 7000 },
                { name: 'Jet Li', power: 8000 }
            ]
        }
    }
});

<!-- ClientApp/components/AppGrid.html -->
<template id="app-grid-template">
    <div>
        <form id="search">
            Search <input name="query" v-model="searchQuery">
        </form>
        <demo-grid :rows="gridRows"
                   :columns="gridColumns"
                   :filter-key="searchQuery">
        </demo-grid>
    </div>
</template>

Переносим код DemoGrid.ts и содержимое шаблона. Производим замену входного свойства 'data' -> 'rows', чтобы не путать с data(). Определение свойств Vue-компоненты переделываем в массив имен (props: ['rows', 'columns', 'filterKey']).

скрытый текст ClientApp/DemoGrid
// ClientApp/components/DemoGrid.ts
import Vue from "vue";

export default Vue.extend({
    template: '#demo-grid-template',
    props: ['rows', 'columns', 'filterKey'],
    data: function () {
        var sortOrders = {}
        this.columns.forEach(function (key) {
            sortOrders[key] = 1
        })
        return {
            sortKey: '',
            sortOrders: sortOrders
        }
    },
    computed: {
        filteredData: function () {
            var sortKey = this.sortKey
            var filterKey = this.filterKey && this.filterKey.toLowerCase()
            var order = this.sortOrders[sortKey] || 1
            var rows = this.rows
            if (filterKey) {
                rows = rows.filter(function (row) {
                    return Object.keys(row).some(function (key) {
                        return String(row[key]).toLowerCase().indexOf(filterKey) > -1
                    })
                })
            }
            if (sortKey) {
                rows = rows.slice().sort(function (a, b) {
                    a = a[sortKey]
                    b = b[sortKey]
                    return (a === b ? 0 : a > b ? 1 : -1) * order
                })
            }
            return rows
        }
    },
    filters: {
        capitalize: function (str) {
            return str.charAt(0).toUpperCase() + str.slice(1)
        }
    },
    methods: {
        sortBy: function (key) {
            this.sortKey = key
            this.sortOrders[key] = this.sortOrders[key] * -1
        }
    }
});

<!-- ClientApp/components/DemoGrid.html -->
<template id="demo-grid-template">
    <table>
        <thead>
            <tr>
                <th v-for="key in columns"
                    @click="sortBy(key)"
                    :class="{ active: sortKey == key }">
                    {{ key | capitalize }}
                    <span class="arrow" :class="sortOrders[key] > 0 ? 'asc' : 'dsc'">
                    </span>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr v-for="entry in filteredData">
                <td v-for="key in columns">
                    {{entry[key]}}
                </td>
            </tr>
        </tbody>
    </table>
</template>

Создаем файл ClientApp/css/demo-grid.css на основе стилей компоненты DemoGrid.


скрытый ClientApp/css/demo-grid.css
/* ClientApp/css/demo-grid.css */
body {
    font-family: Helvetica Neue, Arial, sans-serif;
    font-size: 14px;
    color: #444;
}

table {
    border: 2px solid #42b983;
    border-radius: 3px;
    background-color: #fff;
    margin-top: .5rem;
}

th {
    background-color: #42b983;
    color: rgba(255,255,255,0.66);
    cursor: pointer;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

td {
    background-color: #f9f9f9;
}

th, td {
    min-width: 120px;
    padding: 10px 20px;
}

    th.active {
        color: #fff;
    }

        th.active .arrow {
            opacity: 1;
        }

.arrow {
    display: inline-block;
    vertical-align: middle;
    width: 0;
    height: 0;
    margin-left: 5px;
    opacity: 0.66;
}

    .arrow.asc {
        border-left: 4px solid transparent;
        border-right: 4px solid transparent;
        border-bottom: 4px solid #fff;
    }

    .arrow.dsc {
        border-left: 4px solid transparent;
        border-right: 4px solid transparent;
        border-top: 4px solid #fff;
    }

Сборка и запуск проекта


Сборка и запуск приложения производится также, как и для проекта TryVueMvcDecorator, описанного ранее. После пересборки и запуска приложения в браузере должно получиться что-то подобное изображенному на скриншоте.


скрытый скриншот AppGrid

image AppGrid


Адаптация под строгую типизацию


Теперь начинаем закручивать гайки. Если попробовать сразу поставить опцию компилятора {"strict": true}, то получим кучу ошибок TypeScript при компиляции.


Как правило, включать контроль лучше поэтапно: включаем одну опцию, устраняем возникшие ошибки, затем делаем тоже самое для следующей опции и т.д.


Для адаптации существующего кода Vue-компоненты под строгую типизацию, в первую очерередь, определяем интерфейсы для входных (props) и выходных данных (data) компоненты.


interface DemoGridProps {
    rows: Array<any>;
    columns: Array<string>;
    filterKey: string;
}
interface DemoGridData {
    sortKey: string;
    sortOrders: { [index: string]: number };
}

Затем ставим опцию компилятора {"noImplicitThis": true} и устраняем ошибки способом, описанным ранее в пункте Типизация через интерфейсы входных и выходных данных.


После установки опции компилятора {"noImplicitAny": true} разбираемся с остальными неопределенными типами. После этого включение {"strict": true} уже ошибок не дает (для нашего примера). Результат адаптации модуля DemoGrid.ts приведен под спойлером.

скрытый текст ClientApp/DemoGrid.ts
// ClientApp/components/DemoGrid.ts
import Vue from "vue";

interface DemoGridProps {
    rows: Array<any>;
    columns: Array<string>;
    filterKey: string;
}

interface DemoGridData {
    sortKey: string;
    sortOrders: { [index: string]: number };
}

export default Vue.extend({
    template: '#demo-grid-template',
    props: ['rows', 'columns', 'filterKey'],
    //props: { rows: Array, columns: Array, filterKey: String },
    data: function () {
        var sortOrders: any = {};
        (this.$props as DemoGridProps).columns.forEach(function (key) {
            sortOrders[key] = 1
        })
        return {
            sortKey: '',
            sortOrders: sortOrders
        } as DemoGridData
    },
    computed: {
        filteredData: function () {
            var thisData = (this.$data as DemoGridData);
            var thisProps = (this.$props as DemoGridProps);

            var sortKey = thisData.sortKey
            var filterKey = thisProps.filterKey && thisProps.filterKey.toLowerCase()
            var order = thisData.sortOrders[sortKey] || 1
            var rows = thisProps.rows
            if (filterKey) {
                rows = rows.filter(function (row) {
                    return Object.keys(row).some(function (key) {
                        return String(row[key]).toLowerCase().indexOf(filterKey) > -1
                    })
                })
            }
            if (sortKey) {
                rows = rows.slice().sort(function (a, b) {
                    a = a[sortKey]
                    b = b[sortKey]
                    return (a === b ? 0 : a > b ? 1 : -1) * order
                })
            }
            return rows
        }
    },
    filters: {
        capitalize: function (str: string) {
            return str.charAt(0).toUpperCase() + str.slice(1)
        }
    },
    methods: {
        sortBy: function (key: string) {
            var thisData = (this.$data as DemoGridData);

            thisData.sortKey = key
            thisData.sortOrders[key] = thisData.sortOrders[key] * -1
        }
    }
});

Свой результат выполнения описанных действий можете сравнить с проектом TryVueMvcGrid на github.


Заключение


У способа определения Vue-компонент через декоратор есть свои преимущества и недостатки. Один из недостатков — необходимость реструктуризации кода, когда работающий пример написан на JavaScript. Что требует большей аккуратности.


Вариант строгой типизации через явное определение интерфейсов для опций "data" и "props", позволяет меньше включать мозги на этапе переноса JavaScript-кода Vue-компонент.


Кроме того, интерфейсы дают возможность повторного использования определений типов для входных и выходных данных Vue-компонент. Ведь тип входных данных одной компоненты часто совпадает с выходными данными другой.


Благодарности


Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 14
  • +3
    Сразу скажу с Vue не работаю, но сказать что имею. Разве явный каст
    as DemoGridData
    во множестве мест не нивелирует преимущества строгой типизации? Вот подумайте, чтобы сделать каст, вам нужно знать во что кастить в том или ином случае, и так в каждом участке кода, ошибиться с таким кастом очень легко, создается видимость что типизация есть, когда в полноценном виде ее нет. То есть по хорошему типы для props и data нужно бы при их объявлении указать один раз и дальше никогда каст/as не делать. Вот vue-class-component пропаганирует подобный подход, но не ваш пример. И например, абстрактно, с дженериками это могло бы выглядеть как-то так
    Vue.extend<DemoGridProps, DemoGridData>({...})

    Кроме того, интерфейсы дают возможность повторного использования определений типов для входных и выходных данных Vue-компонент. Ведь тип входных данных одной компоненты часто совпадает с выходными данными другой.

    Разве нельзя использовать интерфейсы совместно с vue-class-component (прямо в декораторе, вместо инлайн описания типа)?

    PS если уж хочется делать явный каст, то почему не один раз, например так (и можно развить идею, вынести метод cast в базовый класс и сделать его generic, тогда DemoGridData/DemoGridProps фигурировали бы только одни раз в шапке класса как generic параметры):
    cast() {
        return {
            $data: this.$data,
            $props: this.$props,
        } as {$data: DemoGridData, $props: DemoGridProps};
    }
    
    И далее использовать как-то так:
        const {$data, $props} = this.cast();
        ...
    

    • 0
      Всё зависит от области применения. Ваши варианты в определенных условиях будут предпочтительнее.

      Вариант с интерфейсами удобен, когда не сам проектируешь компоненты, а сдираешь чужой js-код.
      Кроме того, бывает полезно видеть результат компиляции из TypeScript в JavaScript без посредников. Декоратор несколько искажает картину.
      • 0

        Хорошо. Вы типизировали js код. А что делать с шаблонами? Ваша типизация никак не влияет на связи между компонентами. И это огромная проблема сейчас. Потому что это затрудняет рефакторинг, а так же просто может дать сломаться приложению в рантайме по недосмотру кого-то из разработчиков.
        Да, лучше это, чем ничего. Но говорить, что это строгая типизация — это очень большая натяжка.

        • –1
          Надежная связь между ts-кодом компоненты и его представленим (шаблоном) — это частный случай общей проблемы поддержания взаимосвязей между моделью и представлением.

          Например, те же проблемы есть в приложениях WPF или XamarinForms. XAML-файлы представления тоже можно ненароком сломать. И здесь тоже определенные ошибки проявятся только в рантайм.

          На 100% эту проблему никто ещё не решил нормально. Поэтому рефакторинг объектов, которые участвуют в биндинге — везде вызывает затруднения.

          На уровне привязки ts-кода Vue-компоненты к его шаблону особых проблем нет.
          Обычно это решается соглашением по именам (для AppGrid templateId=«app-grid-template», для DemoGrid templateId=«demo-grid-template»).

          На уровне привязки (биндинга) атрибутов компоненты к определенным элементам представления, естественно можно нарваться на ошибки в рантайм.

          Тут только грамотное тестирование сможет помочь. По-моему, Vue.js ругается вполне осмысленно в случае отсутствия нужных тэгов или сломанного биндинга.

          Но ведь строгая типизация, в любом случае, помогает улучшать код. Поэтому её надо применять в той части, в которой она работает.
          • 0
            Вообще-то в TSX решили еще в первой версии TypeScript. И в шаблонах ангуляра вроде тоже.
            • 0
              Про TSX и ангруляр ничего не скажу.

              К этому ещё можно добавить Razor-рендеринг в приложениях Asp.Net Core MVC.
              Razor-синтаксис позволяет жестко связать модель и представление.
              Но здесь одно лечат, другое — калечат.
        • +1
          Очень хорошо, что кто-то пишет на русском про TS применительно к Vue.js. Но остальное…

          Про «строгую» типизацию в TS, где есть any и нет soundness — вам уже сказали.

          Построение — мне лично кажется что class MyComponent extends FrameworkName {… } — семантически кривой конструкт. Сравните с:
          class MyComponent extends React.Component {… }

          Извините, но от кода просто кровь из глаз. Как будто ES6 прошел мимо, к чему такая многословность?

          Сам Vue.js очень «волшебный» фреймворк, а вы ему еще магии добавляете. Официальный стайл-гайд требует, чтобы для фреймворка явно описывались типы свойств компонента. У вас — массив. Исправляем, делаем явно — и получаем дубликат описаний, то есть еще хуже.

          Вот так вообще писать нельзя, динамически меняющиеся свойства статикой упихиваем в data секцию:
          data: function () {
                  var sortOrders: any = {};
                  (this.$props as DemoGridProps).columns.forEach(function (key) {
                      sortOrders[key] = 1
                  })
                  return {
                      sortKey: '',
                      sortOrders: sortOrders
                  } as DemoGridData
              }
          


          Для этой цели есть секция computed.
          • 0

            При написании tutotial я не ставил себе задачу написать идеальный пример.
            Я выбрал официальный пример известного автора: Evan You.
            Затем постарался минимизировать изменения в исходном коде.
            Чтобы не потерять узнаваемость.
            Так что: "так вообще писать нельзя" — это к автору :).


            Что касается кривости кода — посмотрите на цитаты от создателя Vue.js в начале статьи. В них содержится ответ.
            Vue.js изначально был плохо заточен под TypeScript.
            Например, дублирование определений props неизбежно даже при использовании vue-class-component.
            Поэтому и приходится использовать "костыли".
            Есть варианты "костылей", которые выглядят получше, чем использованный мной.


            Описанный мной вариант с интерфейсами, мне лично, экономит массу времени и нервов при изучении потрохов самого Vue.js, а также "чужих" компонент, которые я подбираю для использования в своих приложениях.
            Обычно сам пример мне не нужен, поэтому тратиться на него нет смысла.
            Главное — быстро заставить работать подходящий пример использования "нужной" компоненты.
            А потом ставишь точки остановки и шаришься по коду "нужной" компоненты и Vue.js.


            В подобных применениях будет удобнее, как мне кажется, подход с явным определением интерфейсов для входных и выходных данных Vue-компоненты.
            Не требуется "включать мозги" и разбираться во внутренней логике работы "чужого" примера.


            Всё зависит от поставленных целей.

            • 0

              Кстати, первый пример, который я использую в этой статье, тоже практически официальный.
              Переход с официального сайта typescriptlang.org, выбрать Vue.js.
              Автор примера: DanielRosenwasser — Program Manager of TypeScript.


              Развве может он писать на TypeScript криво?

              • +2
                А что, имя автора кода как-то гарантирует, что конкретный его код — хороший? Если он не очень знаком с фреймворком, ожидаемо что он может писать криво. Сразу из стартера — общепринято бить по рукам, если data является объектом, а не функцией, возвращающей объект. Впрочем остальное в стартере выглядит опрятно.

                Образец на vuejs.org, куда вы ссылаетесь — лохматого года, и явно не Typescript. Вы его портируете, и остальные лохматости оставляете как есть. Причём там есть извиняющая причина (не факт, что браузер свежий), а у вас её нет (т.к. компилятор TS обязателен).

                К слову, Эван, при всём к нему уважении, зачастую пишет/выкладывает невменяемый код. С документированием кода во всей Vue.js экосистеме (включая сам фреймворк) очень плохо всё. Как пример, можно смотреть на vuex. И сравнить его с redux. Оцените например как реализован vuex хелпер mapGetters (то, что вчера пришлось смотреть).
                • +1

                  Полностью с вами согласен. Имя автора ничего не гарантирует. Но зато, при сильных наездах, легче отмазаться :).


                  Насчет "явно не TypeScript":


                  Образец на vuejs.org, куда вы ссылаетесь — лохматого года, и явно не Typescript. Вы его портируете, и остальные лохматости оставляете как есть.

                  И я про тоже самое говорю:


                  На широких просторах Интернета можно найти массу качественных примеров и готовых приложений, использующих Vue.js. Но подавляющее большинство этих примеров написано на JavaScript.

                  Зачастую, при попытке причесать "лохматости" что-нибудь отваливалось. Поэтому я и стараюсь оставлять "как есть".

                  • 0
                    Официальный стайл-гайд требует, чтобы для фреймворка явно описывались типы свойств компонента. У вас — массив. Исправляем, делаем явно — и получаем дубликат описаний, то есть еще хуже.

                    Массив использовать опасно ещё по одной причине — у компилятора TypeScript частично отключается контроль типов.


                    Теоретически оба варианта определения props, которые приведены ниже, должны работать одинаково. И это действительно так. Даже получаемый на выходе JavaScript код одинаковый.


                    ...
                    export default Vue.extend({
                        template: '#demo-grid-template',
                        props: ['rows', 'columns', 'filterKey'],
                        //props: { rows: Array, columns: Array, filterKey: String },
                        ...
                    });

                    Но обнаруживается неприятный баг-фича в тайпинге Vue.js.


                    Если props определены как массив, то для компилятора TypeScript перечисленные свойства становятся легальными.
                    Например, компилятор спокойно скушает использование this.filterKey.
                    Перечисленные в props свойства даже в IntelliSense появляются при использовании VS2017.


                    А если определить props явно с указанием типов, то компилятор будет материться на тот же this.filterKey.


                    Вот такой баг-фича.

              • 0

                Была похожая проблема связать TS и Vue. Надо было написать свои однофайловые компоненты, и само приложение целиком на TS.


                Ваш способ мне не нравится.


                Вот что у меня получилось (пример)
                <template>
                    <div>
                        <hr/>
                        MyDebug:
                        <div class="row">
                            <div class="col-lg-6">
                                <pre>{{order }}</pre>
                            </div>            
                        </div>
                
                    </div>
                </template>
                
                <script lang="ts">
                    import Vue from "vue";
                    import {Component, Prop, Emit} from 'vue-property-decorator'
                
                    import {Order} from "models/Order";
                
                    @Component
                    export default class MyDebug extends Vue {
                
                        @Prop()
                        order: Order;
                
                        mounted() {
                        }
                        // https://alligator.io/vuejs/typescript-class-components/
                        // https://github.com/kaorun343/vue-property-decorator
                
                    }
                
                </script>
                
                <style lang="css" scoped>
                </style>
                • 0
                  Меня тоже не устраивает описанный мной вариант для использования в продакшн. Только для тестирования и прототипирования.

                  В продакшн используем декоратор vue-class-component.

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

                Самое читаемое