Введение в разработку приложений с помощью React и TypeScript.
Мы собираемся разработать знаменитое TODO приложение из проекта TodoMVC с помощью React и TypeScript:
В этом посте будет проделано следующее:
- Настройка среды.
- Создание проекта.
- Изучение основ React-компонентов.
- Разработка React-компонентов с помощью TypeScript.
- Компиляция и запуск приложения.
Настройка среды
Мы начнем с настройки среды: скачайте и установите Node.js с официального сайта, затем установите TypeScript и tsd с помощью npm (испрользуйте sudo
, если вы работаете в OSX):
$ npm install -g typescript tsd
$ apm install atom-typescript
Этот плагин обладает некоторыми интересными функциями, например, конвертирование HTML в JSX:
Или отображение вида зависимостей:
Зайдите на страницу проекта на GitHub, чтобы узнать больше о возможностях atom-typescript.
Установите инструменты разработчика для работы с React для Хрома. Это расширение пригодится нам для отладки React-приложений путем отображения значений свойств и состояния выбранного компонента.
Создание проекта
К концу этого учебника структура проекта будет такой:
├── index.html
├── js
│ ├── app.js
│ ├── app.tsx
│ ├── constants.js
│ ├── constants.ts
│ ├── footer.js
│ ├── footer.tsx
│ ├── interfaces.d.ts
│ ├── todoItem.js
│ ├── todoItem.tsx
│ ├── todoModel.js
│ ├── todoModel.ts
│ ├── tsconfig.json
│ ├── utils.js
│ └── utils.ts
├── node_modules
│ ├── director
│ ├── react
│ └── todomvc-app-css
├── package.json
├── tsd.json
└── typings
├── react
│ ├── react-global.d.ts
│ └── react.d.ts
└── tsd.d.ts
Давайте начнем с создания корневой папки приложения.
$ mkdir typescript-react
$ cd typescript-react
Затем создайте новый файл package.json в корневой папке приложения:
{
private: true,
dependencies: {
director: "^1.2.0",
react: "^0.13.3",
todomvc-app-css: "^2.0.0"
}
}
После этого вы можете установить зависимости проекта с помощью npm:
# из корневой папки приложения
$ npm install
Эта команда создаст папку с именем node_modules внутри корневой папки приложения. node_modules должен содержать 3 папки: directorem, react и todomvc-app-css.
├── node_modules
│ ├── director
│ ├── react
│ └── todomvc-app-css
Теперь мы установим некоторые файлы-определители типов TypeScript. Такие файлы исользуются для объявления интерфейсов публичных API сторонних таких библиотек, как React. Такие интерфейсы могут использоваться в IDE для развития приложений TypeScript с такими функциями как IntelliSense.
Файлы-определители типа также используются компилятором TypeScript для гарантии, что мы используем сторонние библиотеки правильно.
Мы будем нуждаться в файлах-определителях типов. Мы можем установить их с помощью следующей команды:
# из корневой папки приложения
$ tsd init
$ tsd install react --save
Команда выше создаст файл с именем tsd.json и папку с именем typings в корневой папке приложения. Папка typings должна содержать папку по имени react.
Нам также необходимо вручную скачать и сохранить файл с именем react-global.d.ts под папкой typings/react.
└── typings
├── react
│ ├── react-global.d.ts
│ └── react.d.ts
└── tsd.d.ts
Теперь, давайте создадим файл index.html внутри корневой папки приложения:
<!doctype html>
<html lang="en" data-framework="typescript">
<head>
<meta charset="utf-8">
<title>React • TodoMVC</title>
<link rel="stylesheet"
href="node_modules/todomvc-common/base.css">
<link rel="stylesheet"
href="node_modules/todomvc-app-css/index.css">
</head>
<body>
<section class="todoapp"></section>
<footer class="info">
<p>Дважды щелкните мышкой для редактирования TODO</p>
</footer>
<script type="text/javascript"
src="node_modules/react/dist/react-with-addons.js">
</script>
<script type="text/javascript"
src="node_modules/director/build/director.js">
</script>
<script type="text/javascript" src="js/constants.js"></script>
<script type="text/javascript" src="js/utils.js"></script>
<script type="text/javascript" src="js/todoModel.js"></script>
<script type="text/javascript" src="js/todoItem.js"></script>
<script type="text/javascript" src="js/footer.js"></script>
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>
На этом этапе вы должны иметь следующие файлы и папки:
├── index.html
├── node_modules
│ ├── director
│ ├── react
│ └── todomvc-app-css
├── package.json
├── tsd.json
└── typings
├── react
│ ├── react-global.d.ts
│ └── react.d.ts
└── tsd.d.ts
Вы можете заметить, что некоторые из JavaScript файлов, на которые ссылается наш файл index.html отсутствуют. Теперь мы перейдем к решению этой проблемы.
Базовый экскурс в React-компоненты
Компоненты - главный строительный блок React-приложения. Компонент представляет собой автономный кусок пользовательского интерфейса. Компонент, как правило, отображает некоторые данные и взаимодействует с пользователем.
Компонент может содержать дочерние компоненты. Приложение, которое мы создаем, - небольшое, поэтому мы будем только развивать один компонент верхнего уровня с именем TodoApp.
Компонент TodoApp будет состоять из нескольких компонентов, в том числе одного компонента TodoFooter и списка компонентов TodoItem.

Компоненты отличать два различных набора данных: свойства и состояние.
Свойства
Props (сокращенное от properties (свойства)) - это конфигурации Компонента, можно сказать, его опции. Они получены выше и неизменны до тех пор, пока не затрагивается компонент, получающий их.
Компонент не может изменить свои свойства, но отвечает за соединение свойств дочерних Компонентов.
Состояние
Состояние имеет значение по умолчанию при установке компонента, а затем зависит от изменений (в основном генерируется из пользовательских событий). Это сериализуемое представление одного момента времени – снэпшот.
Компонент сам управляет своим состоянием, но, помимо установки начального состояния, не изменяет состояние дочерних компонентов. Можно сказать, что его состояние приватно.
Когда мы объявляем новый React компонент с помощью машинописи мы должны объявить интерфейс свойств и состояния так:
class SomeComponent extends React.Component<ISomeComponentProps, ISomeComponentState> {
// ...
}
Теперь, когда мы сформировали структуру проекта и изучили основы о компонентах, пришло время начать разработку компонентов.
Резработка React-компонентов с помощью TypeScript
Давайте создадим новую папку с именем js в корневой папке приложения. Мы собираемся создать в ней следующие файлы:
├── js
│ ├──interfaces.d.ts
│ ├── constants.ts
│ ├── utils.ts
│ ├── todoModel.js
│ ├── footer.tsx
│ ├── todoItem.tsx
│ └── app.tsx
Вы можете создать их сейчас или по мере необходимости и применения каждого из них.
interfaces.d.ts
Мы будем использовать этот файл, чтобы определить все интерфейсы в вашем приложении. Мы используем расширение .d.ts (который также используется файлами-определителями типа) вместо .ts, поскольку эти файлы не будут транспилированы в файл JavaScript. Файл не транспилируется, поскольку интерфейсы TypeScript не переделываются в JavaScript-код в процессе компиляции.
// Определяет интерфейс структуры задачи
interface ITodo {
id: string,
title: string,
completed: boolean
}
// Определяет интерфейс свойств компонента TodoItem
interface ITodoItemProps {
key : string,
todo : ITodo;
editing? : boolean;
onSave: (val: any) => void;
onDestroy: () => void;
onEdit: () => void;
onCancel: (event : any) => void;
onToggle: () => void;
}
// Определяет интерфейс состояния компонента TodoItem
interface ITodoItemState {
editText : string
}
// Определяет интерфейс свойств компонента Footer
interface ITodoFooterProps {
completedCount : number;
onClearCompleted : any;
nowShowing : string;
count : number;
}
// Определяет интерфейс TodoModel
interface ITodoModel {
key : any;
todos : Array<ITodo>;
onChanges : Array<any>;
subscribe(onChange);
inform();
addTodo(title : string);
toggleAll(checked);
toggle(todoToToggle);
destroy(todo);
save(todoToSave, text);
clearCompleted();
}
// Определяет интерфейс свойств компонента App
interface IAppProps {
model : ITodoModel;
}
// Определяет интерфейс состояния компонента App
interface IAppState {
editing? : string;
nowShowing? : string
}
constants.ts
Этот файл используется, чтобы определить некоторые константы. Константы используются для хранения числового значения клавиш клавиатуры (ENTER_KEY и ESCAPE_KEY), которые мы будем использовать позже, чтобы установить некоторые слушатели событий. Мы также будем использовать некоторые значения, чтобы определить текущий отображаемый список задач по его статусу:
- COMPLETED_TODOS используется при отображении выполненных задач
- ACTIVE_TODOS используется при отображении неполных задач
- ALL_TODOS используется при отображении всех задач
namespace app.constants {
export var ALL_TODOS = 'all';
export var ACTIVE_TODOS = 'active';
export var COMPLETED_TODOS = 'completed';
export var ENTER_KEY = 13;
export var ESCAPE_KEY = 27;
}
utils.ts
Этот файл содержит класс с именем Utils. Класс Utils - это не более чем набор полезных статических функций.
namespace app.miscelanious {
export class Utils {
// создает новый Универсальный Уникальный Идентификатор (УУД)
// УУД используется для идентификации каждой из задач
public static uuid() : string {
/*jshint bitwise:false */
var i, random;
var uuid = '';
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
.toString(16);
}
return uuid;
}
// добавляет 's' к концу world когда count > 1
public static pluralize(count, word) {
return count === 1 ? word : word + 's';
}
// сохраним данные, используя localStorage API
public static store(namespace, data?) {
if (data) {
return localStorage.setItem(namespace, JSON.stringify(data));
}
var store = localStorage.getItem(namespace);
return (store && JSON.parse(store)) || [];
}
// просто помощник для наследования
public static extend(...objs : any[]) : any {
var newObj = {};
for (var i = 0; i < objs.length; i++) {
var obj = objs[i];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
}
return newObj;
}
}
}
todoModel.ts
TodoModel – это общий объект моделей. Приложение имеет весьма маленький объем, поэтому не имеет смысла даже выносить логику отдельно,
/// <reference path="../typings/react/react-global.d.ts" />
/// <reference path="./interfaces.d.ts"/>
namespace app.models {
export class TodoModel implements ITodoModel {
public key: string; // ключ, используемый для локального хранения
public todos: Array<ITodo>; // список задач
public onChanges: Array<any>; // список событий
constructor(key) {
this.key = key;
this.todos = app.miscelanious.Utils.store(key);
this.onChanges = [];
}
// следующие методы используется
// для управления списком задач
public subscribe(onChange) {
this.onChanges.push(onChange);
}
public inform() {
app.miscelanious.Utils.store(this.key, this.todos);
this.onChanges.forEach(function (cb) { cb(); });
}
public addTodo(title : string) {
this.todos = this.todos.concat({
id: app.miscelanious.Utils.uuid(),
title: title,
completed: false
});
this.inform();
}
public toggleAll(checked) {
// Обратите внимание: лучше использовать структуры данных
// без изменений, т.к. их проще аргументировать, а React очень
// хорошо с ними работает. Вот почему мы везде используем map()
// и filter() вместо изменения единиц данных из массива todo.
this.todos = this.todos.map<ITodo>((todo : ITodo) => {
return app.miscelanious.Utils.extend(
{}, todo, {completed: checked}
);
});
this.inform();
}
public toggle(todoToToggle) {
this.todos = this.todos.map<ITodo>((todo : ITodo) => {
return todo !== todoToToggle ?
todo :
app.miscelanious.Utils.extend(
{}, todo, {completed: !todo.completed}
);
});
this.inform();
}
public destroy(todo) {
this.todos = this.todos.filter(function (candidate) {
return candidate !== todo;
});
this.inform();
}
public save(todoToSave, text) {
this.todos = this.todos.map(function (todo) {
return todo !== todoToSave ? todo : app.miscelanious.Utils.extend({}, todo, {title: text});
});
this.inform();
}
public clearCompleted() {
this.todos = this.todos.filter(function (todo) {
return !todo.completed;
});
this.inform();
}
}
}
Этот файл использует расширение .tsx вместо расширения .ts, потому что он содержит TSX код.
TSX – это набор JSX. Мы будем его использовать вместо HTML клиентских шаблонов как Handlebars, поскольку TSX и JSX используются для создания внутреннего представления DOM. Когда состояние компонентов или свойства меняются, React рассчитывает наиболее эффективный способ обновления внутреннего представления DOM, а затем применяет эти изменения к настоящему DOM’у. Этот процесс делает React весьма эффективным, когда речь идет об управлении DOM’ом.
Нам необходимо использовать некоторые дополнительные опции компилятора для компиляции .tsx. Мы узнаем больше об этой теме в конце статьи.
Компонент footer (нижний колонтитул) позволяет фильтровать списки задач по их статусу и отображает количество задач. У этого компонента нет состояния (обратите внимание на то, как {}
передались к React.Component
в качестве интерфейса состояния), но есть некоторые свойства (ITodoFooterProps), которые заданы родительским компонентом (TodoApp).
/// <reference path="../typings/react/react-global.d.ts" />
/// <reference path="./interfaces.d.ts"/>
namespace app.components {
export class TodoFooter extends React.Component<ITodoFooterProps, {}> {
public render() {
var activeTodoWord = app.miscelanious.Utils.pluralize(this.props.count, 'item');
var clearButton = null;
if (this.props.completedCount > 0) {
clearButton = (
<button
className="clear-completed"
onClick={this.props.onClearCompleted}>
Очистка завершена
</button>
);
}
// Аналог React'а ярлыка для `classSet`, который будет часто использоваться
var cx = React.addons.classSet;
var nowShowing = this.props.nowShowing;
return (
<footer className="footer">
<span className="todo-count">
<strong>{this.props.count}</strong> {activeTodoWord} left
</span>
<ul className="filters">
<li>
<a
href="#/"
className={cx({selected: nowShowing === app.constants.ALL_TODOS})}>
Все
</a>
</li>
{' '}
<li>
<a
href="#/active"
className={cx({selected: nowShowing === app.constants.ACTIVE_TODOS})}>
Активные
</a>
</li>
{' '}
<li>
<a
href="#/completed"
className={cx({selected: nowShowing === app.constants.COMPLETED_TODOS})}>
Завершенные
</a>
</li>
</ul>
{clearButton}
</footer>
);
}
}
}
todoItem.tsx
Компонент TodoItem представляет собой одну из задач в списке задач. Он имеет свойство ITodoItemProps и состояние ITodoItemState.
Начальное состояние компонента установлено в конструкторе компонента само собой в то время как свойства передаются в качестве аргументов и устанавливаются родительским компонентом (TodoApp).
/// <reference path="../typings/react/react-global.d.ts" />
/// <reference path="./interfaces.d.ts"/>
namespace app.components {
export class TodoItem extends React.Component<ITodoItemProps, ITodoItemState> {
constructor(props : ITodoItemProps){
super(props);
// устанавливаем начальное состояние
this.state = { editText: this.props.todo.title };
}
public handleSubmit(event) {
var val = this.state.editText.trim();
if (val) {
this.props.onSave(val);
this.setState({editText: val});
} else {
this.props.onDestroy();
}
}
public handleEdit() {
this.props.onEdit();
this.setState({editText: this.props.todo.title});
}
public handleKeyDown(event) {
if (event.which === app.constants.ESCAPE_KEY) {
this.setState({editText: this.props.todo.title});
this.props.onCancel(event);
} else if (event.which === app.constants.ENTER_KEY) {
this.handleSubmit(event);
}
}
public handleChange(event) {
this.setState({editText: event.target.value});
}
// Это полностью опциональное повышение производительности,
// которое можно применить к любому компоненту React. Если вы
// удалите этот метод, приложение все равно будет корректно
// работать (и будет очень производительным!), мы лишь используем
// его в качестве примера того, как маленький код может
// привнести значительные улучшения производительности.
public shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.todo !== this.props.todo ||
nextProps.editing !== this.props.editing ||
nextState.editText !== this.state.editText
);
}
// Безопасно управлять DOM’ом можно после обновления состояния,
// ссылаясь на this.props.onEdit() в методе handleEdit, описанном выше.
public componentDidUpdate(prevProps) {
if (!prevProps.editing && this.props.editing) {
var node = React.findDOMNode<HTMLInputElement>(this.refs["editField"]);
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}
}
public render() {
return (
<li className={React.addons.classSet({
completed: this.props.todo.completed,
editing: this.props.editing
})}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={this.props.todo.completed}
onChange={this.props.onToggle}
/>
<label onDoubleClick={ e => this.handleEdit() }>
{this.props.todo.title}
</label>
<button className="destroy" onClick={this.props.onDestroy} />
</div>
<input
ref="editField"
className="edit"
value={this.state.editText}
onBlur={ e => this.handleSubmit(e) }
onChange={ e => this.handleChange(e) }
onKeyDown={ e => this.handleKeyDown(e) }
/>
</li>
);
}
}
}
app.tsx
Этот файл содержит точку входа приложения и объявление компонента TodoApp, который является единственным компонентом верхнего уровня в этом приложении.
/// <reference path="../typings/react/react-global.d.ts" />
/// <reference path="./interfaces.d.ts"/>
// Нам нужно было установить объявление типа, но для набора
// director npm, но оно недоступно, так что мы будем использовать
// это объявление, чтобы избежать ошибок в компиляции.
declare var Router : any;
var TodoModel = app.models.TodoModel;
var TodoFooter = app.components.TodoFooter;
var TodoItem = app.components.TodoItem;
namespace app.components {
export class TodoApp extends React.Component<IAppProps, IAppState> {
constructor(props : IAppProps) {
super(props);
this.state = {
nowShowing: app.constants.ALL_TODOS,
editing: null
};
}
public componentDidMount() {
var setState = this.setState;
// мы будем настраивать маршрутизатор (роутер) здесь
// наш роутер предоставлен модулем director npm
// роутер отслеживает изменения в URL
// и запускает события в компоненте в соответствии с
var router = Router({
'/': setState.bind(this, {nowShowing: app.constants.ALL_TODOS}),
'/active': setState.bind(this, {nowShowing: app.constants.ACTIVE_TODOS}),
'/completed': setState.bind(this, {nowShowing: app.constants.COMPLETED_TODOS})
});
router.init('/');
}
public handleNewTodoKeyDown(event) {
if (event.keyCode !== app.constants.ENTER_KEY) {
return;
}
event.preventDefault();
var val = React.findDOMNode<HTMLInputElement>(this.refs["newField"]).value.trim();
if (val) {
this.props.model.addTodo(val);
React.findDOMNode<HTMLInputElement>(this.refs["newField"]).value = '';
}
}
public toggleAll(event) {
var checked = event.target.checked;
this.props.model.toggleAll(checked);
}
public toggle(todoToToggle) {
this.props.model.toggle(todoToToggle);
}
public destroy(todo) {
this.props.model.destroy(todo);
}
public edit(todo) {
this.setState({editing: todo.id});
}
public save(todoToSave, text) {
this.props.model.save(todoToSave, text);
this.setState({editing: null});
}
public cancel() {
this.setState({editing: null});
}
public clearCompleted() {
this.props.model.clearCompleted();
}
// синтаксис JSX интуитивно понятен. Обратитесь к
// https://facebook.github.io/react/docs/jsx-in-depth.html
// если испытываете затруднения
public render() {
var footer;
var main;
var todos = this.props.model.todos;
var shownTodos = todos.filter(function (todo) {
switch (this.state.nowShowing) {
case app.constants.ACTIVE_TODOS:
return !todo.completed;
case app.constants.COMPLETED_TODOS:
return todo.completed;
default:
return true;
}
}, this);
var todoItems = shownTodos.map(function (todo) {
return (
<TodoItem
key={todo.id}
todo={todo}
onToggle={this.toggle.bind(this, todo)}
onDestroy={this.destroy.bind(this, todo)}
onEdit={this.edit.bind(this, todo)}
editing={this.state.editing === todo.id}
onSave={this.save.bind(this, todo)}
onCancel={ e => this.cancel() }
/>
);
}, this);
var activeTodoCount = todos.reduce(function (accum, todo) {
return todo.completed ? accum : accum + 1;
}, 0);
var completedCount = todos.length - activeTodoCount;
if (activeTodoCount || completedCount) {
footer =
<TodoFooter
count={activeTodoCount}
completedCount={completedCount}
nowShowing={this.state.nowShowing}
onClearCompleted={ e=> this.clearCompleted() }
/>;
}
if (todos.length) {
main = (
<section className="main">
<input
className="toggle-all"
type="checkbox"
onChange={ e => this.toggleAll(e) }
checked={activeTodoCount === 0}
/>
<ul className="todo-list">
{todoItems}
</ul>
</section>
);
}
return (
<div>
<header className="header">
<h1>todos</h1>
<input
ref="newField"
className="new-todo"
placeholder="Что должно быть сделано?"
onKeyDown={ e => this.handleNewTodoKeyDown(e) }
autoFocus={true}
/>
</header>
{main}
{footer}
</div>
);
}
}
}
var model = new TodoModel('react-todos');
var TodoApp = app.components.TodoApp;
function render() {
React.render(
<TodoApp model={model}/>,
document.getElementsByClassName('todoapp')[0]
);
}
model.subscribe(render);
render();
Убедитесь, что оператор this
все время указывает на правильный элемент. Например, вы должны использовать функции со стрелками:
onKeyDown={ e => this.handleNewTodoKeyDown(e) }
вместо
onKeyDown={ this.handleNewTodoKeyDown }
чтобы убедиться, что оператор this
указывает на компонент внутри функции handleNewTodoKeyDown.
Компиляция Приложения
Для компиляции нашего приложения необходимо добавить в папку js файл tsconfig.json:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"isolatedModules": false,
"jsx": "react",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"preserveConstEnums": true,
"suppressImplicitAnyIndexErrors": true
},
"filesGlob": [
"**/*.ts",
"**/*.tsx",
"!node_modules/**"
],
"files": [
"constants.ts",
"interfaces.d.ts",
"todoModel.ts",
"utils.ts",
"app.tsx",
"footer.tsx",
"todoItem.tsx"
],
"exclude": []
}
Если мы проверим Опции компилирования на TypeScript то сможем узнать, как работать с файлом tsconfig.json file:
--project
или -p
может использоваться для компиляции проекта в заданной директории. Директория при этом должна содержать файл tsconfig.json.
Приложение можно скомпилировать с помощью следующей команды:
# из корневой папки приложения
$ tsc -p js
Это создаст следующие JavaScript-файлы в папке js:
├── js
│ ├── app.js
│ ├── constants.js
│ ├── footer.js
│ ├── todoItem.js
│ ├── todoModel.js
│ └── utils.ts
Вот файлы, к которым мы ссылались в файле index.html:
<script type="text/javascript" src="js/constants.js"></script>
<script type="text/javascript" src="js/utils.js"></script>
<script type="text/javascript" src="js/todoModel.js"></script>
<script type="text/javascript" src="js/todoItem.js"></script>
<script type="text/javascript" src="js/footer.js"></script>
<script type="text/javascript" src="js/app.js"></script>
Теперь мы готовы запустить приложение.
Запуск приложения
Для запуска приложения, нам нужен веб-сервер. Мы будем использовать npm-модуль http-server.
Мы можем установить этот пакет с помощью следующей команды:
$ npm install -g http-server
Используйте sudo, если работает с OSX
Используйте следующую команду для запуска приложения:
# из корневой папки приложения
$ http-server
Если вы откроете браузер и перейдете на http://127.0.0.1:8080/, вы сможете увидеть работу приложения:
Не забывайте, что в Хроме можно посмотреть расширение с инструментами для разработчиков на React, а также то, как изменяются значения свойств и состояния компонентов при работе с приложением.
Выводы
В этой статье мы узнали как подготовить среду разработки и создать новый проект для работы с TypeScript и React. Теперь попробуйте сделать что-то свое)