Jak używać TypeScript w NetCore i go debugować?

Jak używać TypeScript w NetCore i go debugować?

Jeśli próbujesz użyć TypeScript w NetCore i coś Ci nie idzie – to jest artykuł dla Ciebie.

Wstęp

Hej, jakiś czas temu tworzyłem pewien kod na frontendzie, w którym musiałem użyć JavaScriptu. Dużo JavaScriptu i Ajaxa. Jak już pewnie zdążyłeś się przekonać – nie lubię JavaScriptu 🙂 Przypomniałem sobie, że przecież istnieje coś takiego jak Typescript! Super! Mogę wreszcie pisać w normalnym języku. Więc zacząłem ogarniać, jak używać TypeScript w NetCore. Okazało się, że mimo że dokładnie robię wszystko to, co opisuje Microsoft i milion innych artykułów, to nic u mnie nie chce działać tak jak powinno. Po kilku dniach szamotaniny, przeczytaniu i przeanalizowaniu całego Internetu, w końcu wygrałem! Okazało się, że są pewne kruczki, o których nikt wtedy nie mówił. Zatem w tym artykule przedstawię Ci te kruczki.

(uwaga, dotyczy to raczej sytuacji, w której nie używasz CAŁEGO node.js, ale generalnie nawet w tym przypadku artykuł może Ci się przydać)

Jak działa TypeScript

Generalnie przeglądarki jeszcze (2021 rok) nie są w stanie obsłużyć TypeScriptu jako takiego. Możesz o TS pomyśleć jak o języku wysokiego poziomu. Jeśli piszesz program np. w C++, komputer nie wie, co ma z nim zrobić. Taki program musi być skompilowany. Ostatecznie uruchamiany jest kod assemblera, z którym komputer już wie co zrobić. Analogiczna sytuacja jest tutaj. Przeglądarka nie wie, co ma zrobić z TypeScriptem. Więc jest on najpierw przerabiany do postaci JavaScriptu i dopiero ten JS jest uruchamiany przez przeglądarkę. Tą „kompilacją” w pewnym sensie steruje plik konfiguracyjny TypeScriptu – tscongif.json

Zaczynamy

To chcemy osiągnąć:

  • debugować kod Typescript w Visual Studio
  • używać innych modułów
  • używać skryptów w moim kodzie html, np:
<button onclick="myTypeScriptFunction()">OK</button>

Krok po kroku

Przygotowanie

  1. Upewnij się, że masz zainstalowany node.js w VisualStudio. Po prostu uruchom VisualStudioInstaller i sprawdź, czy masz ten moduł. Jeśli nie – zainstaluj go.
  2. Pobierz pakiet NuGet: Microsoft.TypeScript.MSBuild – dzięki temu będziesz mógł w ogóle pisać w TypeScript w NetCore.
  3. Kliknij prawym klawiszem myszy na swój projekt i wybierz Add -> New File -> TypeScript JSON configuration file.
    W Twoim projekcie pojawi się nowy plik: tsconfig.json. Jest to plik konfiguracyjny dla TypeScriptu, który steruje kompilacją do JS. tsconfig.json na dzień dobry powinien wyglądać tak:
{
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": true,
    "target": "ES6",
    "outDir": "wwwroot/js",
    "esModuleInterop": true,
    "module": "AMD",
    "moduleResolution": "Node"
  },
  "compileOnSave": true,
  "include": [
    "scripts/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}

Krótkie wyjaśnienie tsconfig.json

  • noImplicitAny – jesli true, wtedy wszędzie tam, gdzie używasz „typu” any, będzie rzucany wyjątek
  • noEmitOnError – jeśli true, wtedy nie będą wyświetlane żadne outputy, gdy pojawi się błąd. Możesz sobie tutaj ustawić false w środowisku testowym/developerskim, ale pamiętaj żeby na produkcji lepiej nie pokazywać takich błędów. Dla bezpieczeństwa
  • removeComments – jeśli true, wszystkie użyte przez Ciebie komentarze zostaną usunięte w wynikowym JavaScripcie. Ja to zostawiłem na false, chcę widzieć komentarze. Ale możesz to zmienić.
  • sourceMap – podczas kompilacji generowana jest tzw. mapa. Chodzi o zmapowanie kodu TS z JS. Dzięki temu możliwe jest debugowanie TypeScriptu. Więc zostaw to na TRUE
  • target – określa wersję docelową ECMAScript. To jest standard skryptowego języka programowania. Jak np. .NET Framework 4.5. To znaczy, że w starszych wersjach nie będziesz miał dostępu do CAŁEGO aktualnego standardu języka. Niestety, niektóre moduły nie chcą za bardzo współpracować z pewnymi standardami. W moim przypadku ES6 było ok. Ale możesz spróbować też ES5, jeśli coś nie będzie działać
  • outDir – katalog, do którego ma trafiać wynikowy kod JavaScript. Pamiętaj, że ten katalog musi być widoczny z poziomu HTML
  • esModuleInterop – ustawione na true pomaga importować typy i moduły. Nie będę wchodził w szczegóły, po prostu tak zostaw – to jest ważne
  • module – określa w jaki sposób będą ładowane moduły. AMD wskazuje na asynchroniczne ładowanie. Tutaj lepiej wypowiedzą się JavaScriptowcy (zapraszam do komentowania). Ten artykuł jest pisany pod „AMD” i tego się trzymajmy.
  • moduleResolution – określa w jaki sposób jest uzyskiwany dostęp do modułów. Tutaj posługujemy się node.js, więc zostawiamy na Node
  • compileOnSave – czy kompilować przy każdym zapisie pliku. Oznacza, że za każdym razem, gdy zapiszesz zmiany w pliku TS, będzie on kompilowany do JS. W przeciwnym razie kompilacja będzie tylko przy budowaniu projektu.
  1. Utwórz w projekcie katalog Scripts. W nim będziesz tworzył pliki .ts

Zewnętrzne biblioteki (thirdparties)

  1. Teraz zajmiemy się zewnętrznymi bibliotekami, których na pewno używasz w .NetCore. Sprawdź, czy masz w projekcie plik package.json. Jeśli nie, dodaj go w taki sposób:
    • prawym klawiszem na projekt
    • Add -> New item
    • wyszukaj i dodaj npm configuration file. Jeśli tego nie widzisz, to najpewniej nie zainstalowałeś node.js, o czym mówiliśmy w pierwszym kroku. Czym jest npm? NPM to taki manager pakietów. Coś jak NuGet. Tutaj masz pakiety do weba. I właściwie tyle. Są pewne alternatywny, np. yarn albo LibraryManager. Ale tutaj ogarniamy za pomocą npm.
  2. Skoro masz już plik package.json – czyli konfigurację zewnętrznych modułów, upewnij się, że wygląda podobnie do tego:
{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
    "@types/bootstrap": "4.5.0",
    "@types/jquery": "3.5.0",
    "@types/jquery-validation-unobtrusive": "3.2.11",
    "autonumeric": "4.6.0"
  },
  "scripts": {
    "build": "tsc --build tsconfig.json"
  }
}

Krótkie objaśnienie package.json

Tutaj ważna jest zawartość devDependencies. Koniecznie musisz mieć bootstrap, jquery i jquery-validation-unobtrusive. Autonumeric zostawiam dla przykładu. Jest to jedna z bibliotek, której używam. Ogarnia rzeczy związane z numerami, walutami etc. Chcę tylko pokazać, że w devDependencies będziesz też trzymał inne moduły, których używasz. Niektóre moduły mogą wymagać, żebyś miał je w dependencies. Ale to tak naprawdę zależy od konkretnej biblioteki. Niestety niektóre biblioteki nie mają (jeszcze) odpowiedników w TypeScript.

Pamiętaj, że musisz mieć takie wersje jQuery i boostrap, których używasz w projekcie. Te 3 linijki (@types/…) umożliwiają Ci używanie typów z bootstrap i jquery w Twoich skryptach TypeScript. To cholernie pomaga. CHOLERNIE.

Eksporty

  1. Podczas pisania kodu TypeScript, musisz eksportować klasy i funkcje, których używasz (dotyczy to tej właśnie konfiguracji, którą zrobiliśmy), tj.:
export class Person //definicja klasy
{
   //reszta kodu
}

export function foo() //definicja funkcji
{
  //reszta kodu
}

W plikach ts, w których chcesz używać tych klas i funkcji, musisz je zaimportować. Na przykład:

import { Person } from "./person";

export class ClassThatUsesPerson
{
    _person: Person;
   //i reszta kodu
}

Pamiętaj, że dyrektywy import powinny być na początku pliku. Tutaj są istotne dwa szczegóły:

  • importujesz plik BEZ rozszerzenia, czyli „./person” zamiast „./person.ts”
  • wskazujesz na bieżący katalog za pomocą „./”. Jeśli zaimportujesz w taki sposób: import { Person } from "person", to nie zadziała. Musi być "./person"
  1. Inne biblioteki, których używasz importujesz w analogiczny sposób. Oczywiście musisz mieć wpisany pakiet w npm (package.json) i wtedy np:
    import AutoNumeric from 'autonumeric'
  2. Teraz kolejna istotna rzecz – pobierz bibliotekę requireJS. To jest biblioteka JavaScript, która posiada pewne funkcje, występujące w pełnym node.js. Np. require. Pliki TypeScript są kompilowane w taki sposób, że kod JavaScript dołącza inne pliki za pomocą funkcji require. To nie jest standardowa funkcja JS. Jak już pisałem, występuje w pełnym node.js. Jeśli nie używasz pełnego frameworka node.js, to musisz pobrać tę bibliotekę.

Przygotowanie strony na TypeScript – entrypoint

  1. Teraz kilka zmian w pliku _Layout.cshtml, żeby TypeScript w NetCore ruszył z miejsca
    Pozbądź się nagłówków związanych z jQuery i bootstrap. Dodaj jednak te:
<script src="~/lib/requirejs.js"></script>
<script src="~/js/entrypoint.js"></script>

(zakładam, że pobrałeś require.js i znajduje się ona w wwwroot/lib)
(zakładam, że masz katalog: wwwroot/js i tam będziesz trzymał swoje skrypty js)

  1. Teraz w katalogu wwwroot/js utwórz plik entrypoint.js (to ma być JavaScript, a nie TypeScript). To będzie punkt wejścia Twojej aplikacji. Niech on wygląda podobnie do tego:
requirejs.config({
    baseUrl: "/js",
    shim: {
        bootstrap: {
            "deps": ["jquery"]
        }
    },
    paths: {
        jquery: "https://ajax.googleapis.com/ajax/libs/jquery/3.5.0/jquery.min",
        bootstrap: "https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min",
        "jquery-validation": "https://cdn.jsdelivr.net/npm/jquery-validation@1.19.2/dist/jquery.validate.min",
        "jquery.validate.unobtrusive": "https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min"
    }
});

require(["jquery", "bootstrap", "app"], function (jq, bs, app) {
    app.App.init();
});

Pamiętaj, żeby zgadzały się wersje bibliotek z tymi, wpisanymi w package.json. Zakładam, że w katalogu Scripts masz plik app.ts, który wygląda co najmniej tak:

export class App {

	static init() {
	
	}
}

Stworzyłem sobie taką główną klasę aplikacji. W metodzie init() inicjuję wszystko, czego używam na każdej lub większości stron. Np. bootstrap carousel:

export class App {

	static init() {
	    $(".carousel").carousel(); //inicjalizacja bootstrapowej karuzeli
	}
}

Normalnie zakodowałbyś to w document.ready albo body.onload. A tak mamy metodę init() w klasie App, w której już tworzysz czysty TypeScript.

Wyjaśnienie entrypoint.js

Jeśli chodzi o resztę pliku entrypoint.js:

Na początku konfigurujemy bibliotekę require.js (możesz sprawdzić jej pełną konfigurację na stronie requireJS); w skrócie:

  • baseUrl – domyślna lokalizacja, w której requireJS będzie szukał plików JS (tych „czystych” jak i wykompilowanych z TypeScript)
  • shim – to jest wymagane, żeby bootstrap działał poprawnie, szczerze powiem że nie wiem co oznacza ponad to.
  • paths – tutaj możesz określić ścieżki do swoich modułów. Po prostu dodaj tu biblioteki, których nie masz w folderze js (libs) i chciałbyś pobierać je za pomocą CDN. Te 3 to wymagane minimum, jeśli używasz jQuery, jQuery-validation i bootstrap. A zapewne używasz, skoro używasz TypeScript w NetCore.
  • UWAGA! Upewnij się, że używasz tutaj bootstrap.bundle.min zamiast boostrap.min. Wersja „bundle” ma dodatkowe referencje (np. do popper.js), co czyni rzeczy duuuużo prostszymi.
  • UWAGA! Nazwy modułów są istotne. To MUSI być „jquery-validation” i „jquery.validate.unobtrusive” (zwróć uwagę na literówki, kropki i myślniki)

Na końcu pliku entrypoint jest instrukcja require. Ona mówi tyle:

Załaduj moduły: jquery, bootstrap i app pod takimi zmiennymi: js, bs, app. Ładowanie w tym miejscu jquery i bootstrap jest zasadniczo wymagane, żeby reszta strony mogła tego używać.

Używanie TypeScript w HTML

  1. Upewnij się teraz, że w CAŁYM kodzie (poza _Layout.cshtml) nie masz żadnego <script src="..."></script> . Teraz skrypty będziesz dołączał inaczej – używając funkcji require. Przykładowo, jeśli masz gdzieś:
<script src="~/js/person.js"></script>

powinieneś zmienić na:

<script>
require(["person"]);
</script>

A jeśli chciałbyś utworzyć obiekt i używać go później na stronie (w pliku html), zrób tak:

<script>
var personObj;
require(["person"], function(personFile) {
  personObj = new personFile.Person();
});

</script>

Pamiętaj, że na dzień dzisiejszy (maj 2021) nie możesz używać bezpośrednio TypeScript w kodzie HTML. Ale bardzo łatwo możesz to obejść – po prostu napisz sobie kod analogiczny do tego powyżej i będziesz mógł zrobić już wszystko -> ale bez Intellisense, np:

<button onclick="personObj.IncreaseAge();">Postarz</button>
<button onclick="personObj.DecreaseAge();">Odmłódź</button>

To właściwie tyle. Brawo ja.

Walidacja

Jeszcze kilka słów zakończenia. Podpowiadam, żebyś zmienił plik _ValidationScriptsPartial.cshtml w taki sposób:

Zamiast:

<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

daj:

<script>
    require(["jquery", "jquery-validation", "jquery.validate.unobtrusive"]);
</script>

To wynika z tego, co pisałem wyżej. A jeśli używasz dodatkowych walidacji, jak np. opisanych w tym artykule, to powinno to wyglądać tak:

<script>
    require(["jquery", "jquery-validation", "jquery.validate.unobtrusive"], function ($) {
        $.validator.addMethod("chboxreq", function (value, element, param) {
            if (element.type != "checkbox")
                return false;

            return element.checked;
        });

        $.validator.unobtrusive.adapters.addBool("chboxreq");
    });
</script>

TypeScript w OnClick albo innych zdarzeniach

Teraz druga uwaga. Czasem bywa tak, że chcesz wykonać jakąś metodę w onclick buttona albo gdziekolwiek indziej. Od razu podpowiadam (choć to wynika już samo z siebie), przykład:

Załóżmy, że masz taki plik MyClass.ts

export class MyClass {
	constructor() {
	}
	
	public onOkBtnClick(sender: object) {
	   alert("Siema");
	}
}

I teraz chcesz metodę onOkBtnClick wywołać po kliknięciu przycisku. Więc Twój plik .cshtml powinien wyglądać tak:

<script>
	var myObj;
	requirejs(["MyClass"], function(myClassFile) {
		myObj = new myClassFile.MyClass();
	});
</script>

<button onclick="myObj.onOkBtnClick(this);">OK</button>

Teraz już możesz szaleć z TypeScript w NetCore i nawet go debugować! Pamiętaj tylko, że:

debugować TypeScript można tylko w przeglądarce Google Chrome

stan na maj 2021

Jeśli masz problem albo znalazłeś błąd w tekście, podziel się w komentarzu.

Podziel się artykułem na: