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

в 21:09, , рубрики: .net, ASP, asp.net core, dotnet core, mvc, system.js, TypeScript, Visual Studio, vue, vue.js, vuejs, webpack

Вопрос: Каковы самые слабые места 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, а можно просто открыть файл wwwrootindex.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&MinifierUpdate 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-компонент. Ведь тип входных данных одной компоненты часто совпадает с выходными данными другой.

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

Автор: Эдуард Щавелев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js