Вопрос: Каковы самые слабые места 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
— Сборка и запуск проекта
— Адаптация под строгую типизацию
Заключение
Введение
Данная статья является продолжением серии статей:
- Приложение Vue.js + Asp.NETCore + TypeScript без Webpack
- RequireJS для приложений Vue.js + Asp.NETCore + TypeScript
- Vue.js + Asp.Net Core MVC + TypeScript и ещё Bootstrap4
В примерах, которые приводились в этих статьях, 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), поэтому бывает полезно включать эти проверки последовательно. Затем можно дополнительно ужесточить контроль, например, включив проверку на наличие неиспользуемых переменных и параметров.
{
"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 вполне нормально переваривает.
// Исходный текст определения 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--;
}
...
Для лучшего понимания применяемого здесь подхода приводим более сложный пример использования интерфейсов для строгой типизации:
// Фрагмент 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".
{
"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.
[
{
"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 *@
@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
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
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
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
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. Также сборку и запуск можно произвести через командную строку в каталоге проекта. Должны получить что-то подобное изображенному на скриншоте.
Свой результат выполнения описанных действий можете сравнить с проектом 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
import Vue from "vue";
import AppGrid from "./components/AppGrid";
new Vue({
el: "#app-root",
render: h => h(AppGrid),
components: {
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/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>
После пересборки и запуска приложения в браузере должно получиться что-то подобное изображенному на скриншоте.
Встраивание примера DemoGrid
Переносим код AppGrid.ts и содержимое шаблона. Производим замену возвращаемого свойства 'gridData' -> 'gridRows'
, чтобы не путать с data()
. Компиляция ts-кода должна пройти нормально даже после включения опций контроля типов, т.к. здесь строгая типизация не требуется.
// 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/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 */
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, описанного ранее. После пересборки и запуска приложения в браузере должно получиться что-то подобное изображенному на скриншоте.
Адаптация под строгую типизацию
Теперь начинаем закручивать гайки. Если попробовать сразу поставить опцию компилятора {"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/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-компонент. Ведь тип входных данных одной компоненты часто совпадает с выходными данными другой.
Благодарности
- Заглавные цитаты взяты из статьи на Хабре: "Создатель Vue.js отвечает Хабру"
- Фото кота с костылем взято здесь.
- При создании примеров частично использовался: "TypeScript Vue Starter".
- При создании примеров использовался: "Grid Component Example".
Комментарии (14)
edward_nsk Автор
20.03.2018 01:50Всё зависит от области применения. Ваши варианты в определенных условиях будут предпочтительнее.
Вариант с интерфейсами удобен, когда не сам проектируешь компоненты, а сдираешь чужой js-код.
Кроме того, бывает полезно видеть результат компиляции из TypeScript в JavaScript без посредников. Декоратор несколько искажает картину.
XaveScor
20.03.2018 11:43Хорошо. Вы типизировали js код. А что делать с шаблонами? Ваша типизация никак не влияет на связи между компонентами. И это огромная проблема сейчас. Потому что это затрудняет рефакторинг, а так же просто может дать сломаться приложению в рантайме по недосмотру кого-то из разработчиков.
Да, лучше это, чем ничего. Но говорить, что это строгая типизация — это очень большая натяжка.edward_nsk Автор
20.03.2018 12:42Надежная связь между ts-кодом компоненты и его представленим (шаблоном) — это частный случай общей проблемы поддержания взаимосвязей между моделью и представлением.
Например, те же проблемы есть в приложениях WPF или XamarinForms. XAML-файлы представления тоже можно ненароком сломать. И здесь тоже определенные ошибки проявятся только в рантайм.
На 100% эту проблему никто ещё не решил нормально. Поэтому рефакторинг объектов, которые участвуют в биндинге — везде вызывает затруднения.
На уровне привязки ts-кода Vue-компоненты к его шаблону особых проблем нет.
Обычно это решается соглашением по именам (для AppGrid templateId=«app-grid-template», для DemoGrid templateId=«demo-grid-template»).
На уровне привязки (биндинга) атрибутов компоненты к определенным элементам представления, естественно можно нарваться на ошибки в рантайм.
Тут только грамотное тестирование сможет помочь. По-моему, Vue.js ругается вполне осмысленно в случае отсутствия нужных тэгов или сломанного биндинга.
Но ведь строгая типизация, в любом случае, помогает улучшать код. Поэтому её надо применять в той части, в которой она работает.DarthVictor
20.03.2018 16:47Вообще-то в TSX решили еще в первой версии TypeScript. И в шаблонах ангуляра вроде тоже.
edward_nsk Автор
20.03.2018 17:16Про TSX и ангруляр ничего не скажу.
К этому ещё можно добавить Razor-рендеринг в приложениях Asp.Net Core MVC.
Razor-синтаксис позволяет жестко связать модель и представление.
Но здесь одно лечат, другое — калечат.
JSmitty
20.03.2018 13:01Очень хорошо, что кто-то пишет на русском про 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.edward_nsk Автор
20.03.2018 14:28При написании tutotial я не ставил себе задачу написать идеальный пример.
Я выбрал официальный пример известного автора: Evan You.
Затем постарался минимизировать изменения в исходном коде.
Чтобы не потерять узнаваемость.
Так что: "так вообще писать нельзя" — это к автору :).
Что касается кривости кода — посмотрите на цитаты от создателя Vue.js в начале статьи. В них содержится ответ.
Vue.js изначально был плохо заточен под TypeScript.
Например, дублирование определений props неизбежно даже при использовании vue-class-component.
Поэтому и приходится использовать "костыли".
Есть варианты "костылей", которые выглядят получше, чем использованный мной.
Описанный мной вариант с интерфейсами, мне лично, экономит массу времени и нервов при изучении потрохов самого Vue.js, а также "чужих" компонент, которые я подбираю для использования в своих приложениях.
Обычно сам пример мне не нужен, поэтому тратиться на него нет смысла.
Главное — быстро заставить работать подходящий пример использования "нужной" компоненты.
А потом ставишь точки остановки и шаришься по коду "нужной" компоненты и Vue.js.
В подобных применениях будет удобнее, как мне кажется, подход с явным определением интерфейсов для входных и выходных данных Vue-компоненты.
Не требуется "включать мозги" и разбираться во внутренней логике работы "чужого" примера.
Всё зависит от поставленных целей.
edward_nsk Автор
20.03.2018 14:54Кстати, первый пример, который я использую в этой статье, тоже практически официальный.
Переход с официального сайта typescriptlang.org, выбрать Vue.js.
Автор примера: DanielRosenwasser — Program Manager of TypeScript.
Развве может он писать на TypeScript криво?
JSmitty
20.03.2018 16:30+1А что, имя автора кода как-то гарантирует, что конкретный его код — хороший? Если он не очень знаком с фреймворком, ожидаемо что он может писать криво. Сразу из стартера — общепринято бить по рукам, если data является объектом, а не функцией, возвращающей объект. Впрочем остальное в стартере выглядит опрятно.
Образец на vuejs.org, куда вы ссылаетесь — лохматого года, и явно не Typescript. Вы его портируете, и остальные лохматости оставляете как есть. Причём там есть извиняющая причина (не факт, что браузер свежий), а у вас её нет (т.к. компилятор TS обязателен).
К слову, Эван, при всём к нему уважении, зачастую пишет/выкладывает невменяемый код. С документированием кода во всей Vue.js экосистеме (включая сам фреймворк) очень плохо всё. Как пример, можно смотреть на vuex. И сравнить его с redux. Оцените например как реализован vuex хелпер mapGetters (то, что вчера пришлось смотреть).edward_nsk Автор
20.03.2018 16:54+1Полностью с вами согласен. Имя автора ничего не гарантирует. Но зато, при сильных наездах, легче отмазаться :).
Насчет "явно не TypeScript":
Образец на vuejs.org, куда вы ссылаетесь — лохматого года, и явно не Typescript. Вы его портируете, и остальные лохматости оставляете как есть.
И я про тоже самое говорю:
На широких просторах Интернета можно найти массу качественных примеров и готовых приложений, использующих Vue.js. Но подавляющее большинство этих примеров написано на JavaScript.
Зачастую, при попытке причесать "лохматости" что-нибудь отваливалось. Поэтому я и стараюсь оставлять "как есть".
edward_nsk Автор
21.03.2018 10:52Официальный стайл-гайд требует, чтобы для фреймворка явно описывались типы свойств компонента. У вас — массив. Исправляем, делаем явно — и получаем дубликат описаний, то есть еще хуже.
Массив использовать опасно ещё по одной причине — у компилятора 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
.
Вот такой баг-фича.
RouR
20.03.2018 19:18Была похожая проблема связать 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>
edward_nsk Автор
20.03.2018 20:51Меня тоже не устраивает описанный мной вариант для использования в продакшн. Только для тестирования и прототипирования.
В продакшн используем декоратор vue-class-component.
serf
Сразу скажу с Vue не работаю, но сказать что имею. Разве явный каст
во множестве мест не нивелирует преимущества строгой типизации? Вот подумайте, чтобы сделать каст, вам нужно знать во что кастить в том или ином случае, и так в каждом участке кода, ошибиться с таким кастом очень легко, создается видимость что типизация есть, когда в полноценном виде ее нет. То есть по хорошему типы для props и data нужно бы при их объявлении указать один раз и дальше никогда каст/as не делать. Вот vue-class-component пропаганирует подобный подход, но не ваш пример. И например, абстрактно, с дженериками это могло бы выглядеть как-то такРазве нельзя использовать интерфейсы совместно с vue-class-component (прямо в декораторе, вместо инлайн описания типа)?
PS если уж хочется делать явный каст, то почему не один раз, например так (и можно развить идею, вынести метод cast в базовый класс и сделать его generic, тогда DemoGridData/DemoGridProps фигурировали бы только одни раз в шапке класса как generic параметры): И далее использовать как-то так: