Jak połączyć: Parcel, React i TypeScript
Parcel, React i TypeScript to doskonałe i często wykorzystywane narzędzia. Ich rozwój jest tak szybki, że dokumentacja nie zawsze za nim nadąża. Wiele osób tworzy konfigurację metodą kopiuj i wklej. Nie ma czasu na zastanawianie się, do czego służą poszczególne opcje. Dziś spróbuję połączyć Parcel, React i TypeScript, przy użyciu najprostszej konfiguracji.
Instalacja niezbędnych pakietów
Zakładam, że masz zainstalowany NodeJS. Ja używam wersji 12.17
% node --version
v12.17.0
oraz menadżera pakietów npm 6.14.4.
% npm --version
v6.14.4
Jeżeli chcesz używać różnych wersji NodeJS, polecam świetny menadżer wersji dla NodeJS n. Niestety nie działa on w Windows. Dla Windows możesz użyć nvm-windows.
Zacznę od utworzenia katalogu dla projektu:
mkdir parcel-react-typescript
Następnie wchodzę do tego katalogu:
cd parcel-react-typescript
Poleceniem npm init -y tworzę plik package.json dla aplikacji. Opcja -y powoduje, że nie pojawią się pytania o takie rzeczy, jak nazwa i wersja aplikacji, nazwa pliku wejściowego itp. Zostaną wybrane wartości domyślne.
npm init -y
Utworzony plik package.json wygląda tak:
{
"name": "parcel-react-typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Napisałem wcześniej, że używam NodeJS w wersji 12. Do pliku package.json można dodać opcje:
"engines" : { "node" : "12" },
"engineStrict": true
Uwaga: Wpis "engines" powinien dawać ostrzeżenie, w przypadku użycia nieodpowiedniej wersji Node. Wpis "engineStrict" powinien zablokować całkowicie możliwość użycia innej wersji Node. Problem polega jednak na tym, że pierwsza z opcji obecnie nie działa, a druga została wycofana w wersji 3.0 npm-a. Link do dokumentacji engines i engineStrict. Zainteresowane osoby odsyłam do pakietu check-node-version, który potrafi sprawdzić, czy używamy odpowiedniej wersji NodeJS, npm itp.
Wracam jednak do tematu.
Parcel
Instaluję pakiet parcel-bundler - program tworzący pakiety (więcej na temat Parcela można przeczytać w jego dokumentacji). Dokumentacja Parcel-a zaleca użycie opcji -g, aby zainstalować go globalnie. Moim zdaniem lepiej jest jednak dodać pakiet bezpośrednio do naszego projektu:
npm install parcel-bundler
Dlaczego parcel a nie webpack ? Uważam, że parcel jest szybszy i prostszy w konfiguracji. W chwili, gdy powstaje ten tekst, dostępna jest już wersja 2.0.0-alpha. Jednak jest jeszcze za wcześnie, aby ją opisać. Dlatego użyję wersji 1.x. Gdy wyjdzie finalna wersja 2.0, wtedy poświęcę jej oddzielny wpis.
Zainstaluję pakiet rimraf, który użyję w skrypcie, do usuwania zawartości katalogów:
npm install rimraf
W pliku package.json dodam trzy skrypty:
"start": "parcel src/index.html --out-dir build/debug",
"prebuild": "rimraf build/release",
"build": "parcel build src/index.html --no-source-maps --out-dir build/release --public-url ./",
- start - uruchamia wersję deweloperską aplikacji
- prebuild - uruchamia się automatycznie przed uruchomieniem build i czyści katalog /build/release
- build - tworzy wersję produkcyjną aplikacji w katalogu build/release
React
Nadszedł czas na instalację biblioteki React. Instaluję dwa pakiety: react i react-dom. React - zawiera funkcjonalność związaną z definiowaniem komponentów. React-dom umieszcza te komponenty w drzewie DOM.
npm install react react-dom
Potrzebny będzie też plik z definicjami typów. Dlaczego ? Ponieważ, gdy chcę w TypeScript skorzystać z istniejących bibliotek, które nie są napisane w TypeScript, to kompilatorowi brakuje informacji o typach. Plik ten uzupełnia te informacje.
npm install @types/react @types/react-dom
TypeScript
Instaluję Typescript, który jest darmowym i open source-owym językiem programowania stworzonym i rozwijanym przez Microsoft. TypeScript jest nadzbiorem JavaScript. Mówi się, że każdy program napisany w języku JavaScript jest poprawnym programem TypeScript. Nie do końca się z tym zgadzam.
npm install typescript
Konfiguracja TypeScript-a
Tworzę plik tsconfig.json o takiej zawartości:
{
"compilerOptions": {
"moduleResolution": "node",
"jsx": "react",
"strict": true,
"baseUrl": "./src",
"paths": {
"~/*": ["./*"]
},
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
}
- moduleResolution - ustawienie na node powoduje, że moduły są szukane w katalogu node_modules,
- jsx - określa sposób przetwarzania elementów HTML w plikach JSX/TSX, ustawiam react ponieważ go używam,
- strict - włącza ściślejsze sprawdzanie kodu TypeScriptu,
- baseUrl - określa główny katalog używany podczas rozwiązywania zależności modułów,
- paths - Parcel używa tyldy do określania relatywnej ścieżki dla modułu, aby TypeScript potrafił to zrozumieć trzeba umieścić powyższy wpis,
- allowSyntheticDefaultImports i esModuleInterop - pomagają importować moduły bez domyślnego eksportu. Ogólnie chodzi o zgodność kodu źródłowego.
W wielu miejscach w internecie, w konfiguracji można znaleźć , dwa wpisy:
- target - Docelowa wersja języka JavaScript, w której kompilator zapisze pliki wynikowe. Np. ES6 (odpowiednik ES2015) wspierany przez większość nowych przeglądarek.
- module - Wybór formatu modułu. Kod w TypeScript domyślnie ma zasięg globalny (global scope). Wartości lub funkcje napisane w jednym pliku są dostępne w innych plikach projektu. Nie jest to dobre. Może powodować błędy i konflikty. Dlatego stworzono moduły. Kod w module ma zasięg lokalny. Nic co jest zdefiniowane w danym module nie jest widoczne na zewnątrz bez użycia opcji export. Ponieważ powstało wiele formatów modułów, opcja module określa jakiego chcemy użyć. Dużo zależy od tego, gdzie uruchamiany będzie projekt. Np. projekt dla node powinien zostać użyty commonjs.
W tym przypadku nie ma znaczenia, czy ich użyję. Wynikowy plik będzie taki sam. Na końcu to i tak Parcel przy pomocy Babel-a transpiluje kod do odpowiedniej wersji. Nie udało mi się znaleźć opcji wyłączającej Babel, tak aby użyć samego TypeScript-a.
Pliki i katalogi
W katalogu parcel-react-typescript, tworzę katalog src, w którym znajdą się pliki źródłowe. Polecam edytor Visual Studio Code. Można oczywiście użyć każdego innego edytora. W katalogu src tworzę dwa pliki index.html i index.tsx.
index.html:
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./index.tsx"></script>
</body>
</html>
index.tsx:
import App from '~/components/App';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('app'));
Następnie w katalogu src tworzę katalog components a w nim plik App.tsx:
import React from 'react';
const App = () => <div>lorem ipsum</div>;
export default App;
Całość wygląda tak:
Plik package.json:
{
"name": "parcel-react-typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "parcel src/index.html --out-dir build/debug",
"prebuild": "rimraf build/release",
"build": "parcel build src/index.html --no-source-maps --out-dir build/release --public-url ./",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/react": "^16.9.41",
"@types/react-dom": "^16.9.8",
"parcel-bundler": "^1.12.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"rimraf": "^3.0.2",
"typescript": "^3.9.6"
}
}
Start
Uruchamiam całość:
npm start
Server running at http://localhost:1234
Efekt widać pod adresem: http://localhost:1234
Bonus - testy
Na koniec szybko dodam możliwość uruchomienia testów dla projektu. Instaluję niezbędne pakiety:
npm i --dev jest ts-jest
npm i --dev @types/jest
npm i --dev @testing-library/react
Dodaję skrypt do package.json
"test": "jest"
W głównym katalogu projektu tworzę plik konfiguracyjny dla jest-a - jest.config.js:
module.exports = {
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/src/$1',
},
};
W src/components tworzę test app.test.tsx:
import App from './App';
import React from 'react';
import { render } from '@testing-library/react';
describe('App.tsx', () => {
test('render properly', () => {
const { getByText } = render(<App />);
expect(getByText(/ipsum/i)).toBeTruthy();
});
});
Uruchamiam test:
npm test
Repozytorium
I to tyle. Jeżeli macie jakieś pytania lub uwagi, to zapraszam do pozostawienia komentarza.
Link do repozytorium z przykładem: https://github.com/blogwebpl/parcel-react-typescript