React+TS from scratch. I
В интернете достаточно много разрозненной информации о том, как настроить окружение для стека TypeScript/React, и все они сводятся к npx create-react-app app-name --template typescript
, но это не интересно. Возьмем чистый package.json:
{
"name": "TestTypeScriptTask",
"private": true,
"version": "0",
"description": "Here we touch typescript/react/redux stack"
}
и попробуем настроить сборку и все окружение ручками.
Сперва надо поставить сам typescript: yarn add typescript --dev
После установки добавим несколько пакетов с типами:
yarn add @types/node @types/react @types/react-dom
Теперь надо добавить файл конфигурации для TypeScript:
tsc --init
После выполнения команды появится файл tsconfig.json. В этом файле нас в первую очередь интересует:
- Настройка спецификации языка("target"), которая по-умолчанию установлена в es5. На дворе 2021 и не понятно, зачем в es5 транспилировать наш код, возьмем es2017
- Настройка модульной системы("module"), которая определяет какой способ связывания модулей будем использовать:
- commonjs
// utils.js
function add(r){
return r + r;
}
// export (expose) add to other modules
exports.add = add;
// index.js
var utils = require('./utils.js');
utils.add(4); // = 8
- amd (async module definition)
// add.js
define(function() {
return add = function(r) {
return r + r;
}
});
// index.js
define(function(require) {
require('./add');
add(4); // = 8
}
- es2015
// add.js
export function add(r) {
return r + r;
}
// index.js
import add from "./add";
add(4); // = 8
что интересно! в коде хочу использовать es2015, но тогда сборка вебпаком не работает нихрена, а если указать "commonjs" то все собирается и работает и не возникает проблем что модули оформлены в es2015, а не commonjs. Пока не могу объяснить почему это так
Полностью конфиг выглядит так:
{
"compilerOptions": {
"target": "ES2017", // описан выше
"module": "commonjs", // описан выше
// список библиотек в которых содержится описание различных API,
// которые хотим использовать
"lib": [
"esnext",
"dom",
"dom.iterable"
],
// разрешаем использовать иногда js код
"allowJs": false,
// определяем спецификацию используемых jsx/tsx файлов(react/react-native)
"jsx": "react",
// будем генерировать сорс мапы, чтобы в отладке было легче
"sourceMap": true,
// удалять ли комменты из кода после транспиляции
"removeComments": true,
// включает поддержку итератора из es2015 и
// генерирует совсем другой код для циклов for-of,
// spread и распаковке, когда target сборки es5/es3
"downlevelIteration": true,
// строгий режим, все проверки типов включены
"strict": true,
// выбрал TS - страдай и описывай все типы, никаких any
"noImplicitAny": true,
// стратегия поиска модулей в импортах: node/classic.
// Всюду преимущественно используется node
// Если кратко то так: при импорте "import Foo from './foobar'":
// - сперва ищется файл foobar.js,
// - потом каталог foobar и в нем package.json
// в котором есть секция main,
// - потом каталог foobar и в нем index.js
//
// classic оставлен для обратной совместимости
"moduleResolution": "node",
// директория с которой разрешать относительные имена модулей
"baseUrl": "./",
// мапаем пути импорта в проекте на другие, относительно baseUrl
"paths": {
"components/*": [
"src/components/*"
]
},
// разрешить импортировать default из модулей,
// которые его не экспортируют и не будет генерировать
// ошибок при проверке типов(ошибка импорта из файла без экспорта)
"allowSyntheticDefaultImports": true,
// включает возможность импорта модулей commonjs в стиле ES6 модулей
// у commonjs модулей как бы нет export default
// так же подразумевает использование allowSyntheticDefaultImports
"esModuleInterop": true
},
"include": [
"./src",
"./webpack.config.ts"
]
}
Добавим теперь собственно сам React и накидаем небольшую структуру приложения: yarn add react react-dom
Добавим шаблон в ./public/index.html
немного вертски:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Task Notes</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Так же добавим ./src/index.tsx
, который будет точкой входа для сборки проекта (тут все стандартно):
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
небольшое отступление: в каждом файле нужно импортировать реакт, так как каждый файл это отдельный модуль со своей областью видимости и чтобы транспилер заменял jsx шаблоны на React.createElement в скоупе модуля нужен экземпляр React. Избавиться от этого постоянного импорта можно с помощью плагина ProvidePlugin для webpack, который может засунуть реакт в глобальный скоуп доступный всем модулям
сам src/App.tsx
(прошу заметить, что компонента импортируется не через относительный путь ./components/
это из за paths
и baseUrl
в tsconfig):
import React from 'react';
import HelloWorld from 'components/HelloWorld';
const App = () => <HelloWorld/>;
export default App;
ну и компонента (./src/components/HelloWorld/index.tsx
)
import React from 'react';
const HelloWorld = () => (
<>
<h1>Hello World</h1>
<hr/>
<h3>Test react/ts application</h3>
</>
);
export default HelloWorld;
Так как собираться все это будет вебпаком, надо поставить его и зависимости:
yarn add webpack webpack-dev-server ts-node @types/webpack-dev-server tsconfig-paths-webpack-plugin webpack-cli --dev
по прядку - сам webpack, dev-server к нему, ts-node нужен чтобы нода могла выполнять ts файлы, tsconfig-paths-webpack-plugin - чтобы использовать мапы путей из tsconfig в окружении вебпака(чтобы не дублировать эти мапы в конфиге вебпака) и cli интерфейс к вебпаку
так же ставим:
yarn add ts-loader fork-ts-checker-webpack-plugin html-webpack-plugin --dev
по порядку - загрузчик файлов, fork-ts-checker-webpack-plugin - плагин который позволяет запускать проверку типов в отдельном процессе, чем ускоряет работу, html-webpack-plugin для генерации html файла с правильными ссылками на файлы бандла.
После установки добавляем файл конфигурации вебпака:
import path from 'path';
import webpack, {Configuration} from 'webpack';
import HTMLPlugin from 'html-webpack-plugin';
import TypeCheckPlugin from 'fork-ts-checker-webpack-plugin';
import {TsconfigPathsPlugin} from 'tsconfig-paths-webpack-plugin';
const config = (env: any): Configuration => ({
// откуда начинать сборку
entry: './src/index.ts',
// определяет тип source-map, влияет на скорость сборки,
// для прода не включаем их,
// в разработке - включаем режим eval-source-map - медленная сборка,
// код файлов - исходный
...(env.production || !env.development ? {} : {'devtool': 'eval-source-map'}),
// как и где искать модули,
// в этой же секции можно описать алиасы(маппинг-путей)
resolve: {
// в каком порядке разрешать чтение модулей
// если есть index.js, index.tsx в одном модуле, то
// файл с расширением первым в списке - пойдет первым
extensions: [".ts", ".tsx", ".js"],
// собственно подключаем плагин, который поможет
// вебпаку определиться с путями импорта модулей, которые
// описаны в tsconfig
// @ts-expect-error
plugins: [new TsconfigPathsPlugin()],
},
// куда выкладывать сборку
output: {
path: path.join(__dirname, '/dist'),
filename: "build.js"
},
module: {
// правила загрузки файлов
// пока у нас только tsx - используем ts-loader
// Опция transpileOnly - выключает проверку типов загрузчиком
// у нас для этого будет использоваться другой плагин
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options:{transpileOnly: true},
exclude: /dist/,
}
]
},
plugins: [
// определяем где лежит шаблон,
// в который надо добавить собранный проект
new HTMLPlugin({template: './public/index.html'}),
// определяем переменные окружения доступные фронтенду
new webpack.DefinePlugin({
"process.env.PRODUCTION": env.production || !env.development,
"process.env.NAME": JSON.stringify(require('./package.json').name),
"process.env.VERSION": JSON.stringify(require('./package.json').version),
}),
// плагин проверки типов в отдельном потоке
new TypeCheckPlugin({ eslint: {
files: './src/**/*.{ts,tsx,js,jsx}'
}})
]
});
export default config;
и добавим скрипты запуска в package.json
:
"scripts": {
// запускаем дев-сервер, в режиме разработки,
// с открытием новой табы в браузере и hot-reload
"start:dev": "webpack-cli serve --mode=development --env development --open --hot",
"build": "webpack --mode=production --env production --progress",
"lint": "eslint './src/**/*.{ts,tsx}'",
"lint:fix": "eslint './src/**/*.{ts,tsx}' --fix"
},
Пока все это запускать рано, у нас не установлен и не настроен eslint
Перед тем как ставить его - поставим Prettier(штука для форматирования кода, тут инструкция по интеграции в IDE)
yarn add prettier --dev
добавим конфиг в .prettierrc
:
{
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"endOfLine": "auto",
"jsxSingleQuote": true,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"useTabs": false,
"htmlWhitespaceSensitivity": "css"
}
с опциями преттиера ознакомимся позже, когда начнем писать много кода и определимся с code-style
Наконец ставим линтер и обвязку плагинов и парсеров для TS:
yarn add eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-prettier eslint-config-prettier eslint-plugin-import --dev
по порядку - сам линтер, плагин с правилами для реакта, плагин с правилами для хуков реакта, плагин с правилами TS, парсер TS для линтера, плагин с правилами для преттиера, eslint-config-prettier - выключает правила eslint, которые будут конфликтовать с конфигом преттиера, eslint-plugin-import - добавляет поддержку синтаксиса импорта/экспорта ES2015+ и предотвращает опечатки в путях к файлам и имен импорта.
Поставив эти пакеты, добавляем конфиг для линтера .eslintrc.json
:
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"react",
"react-hooks",
"eslint-plugin-import",
"prettier"
],
"env": {
"browser": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"ecmaVersion": 2017,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".jsx",
".tsx"
]
}
],
},
"settings": {
"react": {
"version": "detect"
}
}
}
В самом конце создаем .eslintignore
, запишем в него конфиг вебпака(имя файла всмысле) и запустим приложение: yarn start:dev
в новой вкладке браузера откроется окно с приложением, а в консоли будет насрано много ошибок линтера)