React z wykorzystaniem ECMAScript 6 w ASP.NET Core – Część I
Witam wszystkich serdecznie w moim kolejnym wpisie. Tym razem chciałbym przedstawić, krótki tutorial poświęcony ReactJS z wykorzystaniem ASP.NET Core.
Ostatnio zaczynałem jeden projekt oparty o .NET Core i potrzebny był prosty UI dla użytkowników. W takich sytuacjach mój wybór przeważnie był prosty – AngularJS. Jednak tym razem postanowiliśmy zamieszać trochę w naszym technologicznym stacku i wybór padł na Reacta.
W internecie jest bardzo dużo poradników dotyczących tej biblioteki – niestety bardzo często pisane są one w ES5, a ja z chęcią sięgam do ES6. Więc to jest pierwszy powód, dla którego powstał ten poradnik. Druga rzecz to mała ilość ciekawych materiałów jak wpasować reacta w .NET core, dlatego może mój przykład pomoże komuś kto chciałby iść tą drogą. Co prawda jest template przygotowany przez Microsoft dla aplikacji ReactJS z ASP.NET Core, jednak jest on napisany w TypeScript, a moim zdaniem Reactowi z nim nie po drodze :).
O czym będę mówić w części pierwszej?
- Inicjalizacja aplikacji ASP.NET Core Web API
Jeżeli dopiero zaczynasz swoją przygodę z .NET Core zapraszam do mojego pierwszego wpisu właśnie na ten temat – https://developerlife.pl/net-core-na-niewindowsie-czesc-i-net-core-cli/ - Konfiguracja Node.js, gulpa oraz pierwsza prosta aplikacja w React
- Podstawy Reacta – props, state, componentDidMount, componentWillMount, componentWillUnmount
Każda z części ma swój branch na githubie. Dzięki temu nie musisz zaczynać od początku. Zaczynamy!
Inicjalizacja ASP.NET Core Web API
- Utworzenie aplikacji
Komenda: dotnet new webapi –n ReactWithDotnet (-n – nazwa aplikacji)
- Dodanie referencji do Static Files
(https://www.nuget.org/packages/Microsoft.AspNetCore.StaticFiles/).W katalogu w plikiem .csproj wykonaj poniższą komendęKomenda: dotnet add package Microsoft.AspNetCore.StaticFiles
- Restore pakietów
Komenda: dotnet restore
- Ustawienie statycznych oraz domyślnych plikówOtwórz plik Startup.cs i na końcu metody Configure dopisz poniższe dwie linijki kodu
app.UseDefaultFiles(); app.UseStaticFiles();
- Dodanie pliku index.html w katalogu wwwroot
Dotnetowo.pl <div id="app-root"></div> <script src="index.js"></script>
Utworzenie prostego API
- W katalogu Controllers znajduje się klasa ValuesController. Wykorzystamy ją w naszej aplikacji. Tak jak mówiłem chciałbym się bardziej skupić tutaj na ReactJS, więc nie będę implementować zapisu do bazy danych, a dane będę trzymać w statycznym obiekcie. Moja klasa wygląda jak na poniższym listingu.
[Route("api/people")] [Route("api/people")] public class ValuesController : Controller { protected static List<Person> people = new List<Person> { new Person { Id = 1, FirstName = "Artur", LastName = "Ziemianski", PhoneNumber = "+48 712311231" } }; // GET api/values [HttpGet] public IEnumerable<Person> Get() { return people; } // GET api/values/5 [HttpGet("{id}")] public Person Get(int id) { return people.FirstOrDefault(w=>w.Id == id); } // POST api/values [HttpPost] public void Post([FromBody]Person value) { people.Add(value); } // PUT api/values/5 [HttpPut("{id}")] public void Put(int id, [FromBody]Person value) { var person = people.First(w=>w.Id == id); person.FirstName = value.FirstName; person.LastName = value.LastName; person.PhoneNumber = value.PhoneNumber; } // DELETE api/values/5 [HttpDelete("{id}")] public void Delete(int id) { var person = people.First(w=>w.Id==id); people.Remove(person); } } public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string PhoneNumber { get; set; } }
- Oczekiwany efekt przy wywołaniu localhost:5000/api/people
Jeżeli chcesz pominąć krok tworzenia API możesz je pobrać tutaj:
https://github.com/artzie92/dotnetowo_react_dotnetcore/tree/clear_api
Przygotowanie konfiguracji
- Inicjalizacja pliku package.json (upewnij się, że posiadasz zainstalowany Node.js)Plik ten możesz przekopiować z poniższego listingu lub wykorzystać do tego polecenie npm-init w katalogu aplikacji i wypełnić samemu podstawowe dane, tak jak na screenie
Jak widzisz wykorzystałem w tym przykładzie terminal dostępny w Visual Studio Code. Polecam go, ponieważ sporo przyspiesza pracę i nie wymaga przechodzenia pomiędzy oknami.
Przejdźmy teraz do package.json, który został wygenerowany i dodajmy niezbędne paczki.
{ "name": "react-with-dotnet", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/artzie92/dotnetowo_react_dotnetcore.git" }, "author": "artur ziemianski", "license": "ISC", "bugs": { "url": "https://github.com/artzie92/dotnetowo_react_dotnetcore/issues" }, "homepage": "https://github.com/artzie92/dotnetowo_react_dotnetcore#readme", "dependencies": { "classnames": "^2.2.3", "immutable": "^3.8.0", "react": "^15.0.2", "react-dom": "^15.0.1" }, "devDependencies": { "babel-core": "^6.7.6", "babel-loader": "^6.2.4", "babel-plugin-syntax-async-functions": "^6.5.0", "babel-plugin-syntax-flow": "^6.5.0", "babel-plugin-syntax-jsx": "^6.5.0", "babel-plugin-syntax-object-rest-spread": "^6.5.0", "babel-plugin-syntax-trailing-function-commas": "^6.5.0", "babel-plugin-transform-flow-strip-types": "^6.5.0", "babel-plugin-transform-object-rest-spread": "^6.6.5", "babel-plugin-transform-react-jsx": "^6.7.5", "babel-plugin-transform-regenerator": "^6.5.2", "babel-plugin-transform-runtime": "^6.5.2", "babel-preset-es2015": "^6.5.0", "babel-preset-react": "^6.22.0", "babelify": "^7.3.0", "browserify": "^14.0.0", "events": "^1.1.1", "flux": "^3.1.2", "flux-utils": "^1.0.2-stop", "gulp": "^3.9.1", "vinyl-source-stream": "^1.1.0", "webpack":"2.5.1" } }
- Konfiguracja gulpaTak jak mówiłem wcześniej w naszej aplikacji będziemy korzystać z ES6, która nie jest wspierana przez przeglądarki. Dlatego potrzebujemy narzędzia, który przekonwertuje nasz kod do ES5, a następnie przerzuci do jednego pliku, który zostanie dołączony do naszej strony.Na początek zainstalujmy gulpa
– npm install gulp -g
Dzięki temu polecenie gulp powinno być dostępne w obrębie całego systemu.
Następnie utwórz plik gulpfile.js i wypełnij go tak jak na poniższym listingu.
//gulpfile.js var gulp = require('gulp'); var browserify = require('browserify'); var babelify = require('babelify'); var source = require('vinyl-source-stream'); gulp.task('build', function () { return browserify({entries: './clientapp/root', extensions: ['.jsx', '.js'], debug: true}) .transform('babelify', { presets: ['es2015', 'react'] }) .bundle() .pipe(source('index.js')) .pipe(gulp.dest('./wwwroot/')); }); gulp.task('watch', function () { gulp.watch('./clientapp/**/*{.js,.jsx}', ['build']); }); gulp.task('default', ['watch']);
Jak zapewne widzisz w konfiguracji znajdują się 3 taski:
- ‘build’ – służy do buildowania naszej aplikacji i utworzenia pliku o nazwie index.js, a następnie umieszczenia go w katalogu wwwroot. To ten plik zostanie podczepiony wewnątrz index.html.
- ‘watch’ – służy do śledzenia plików o rozszerzeniach .js oraz .jsx. Kiedy, któryś z nich zostanie zmodyfikowany wywoła task ‘build’.
- ‘default’ – tutaj określamy, które taski mają być wywoływane domyślnie.
Utworzenie prostej aplikacji
Posiadamy już wszystko co jest potrzebne do rozpoczęcia pracy z ReactJS. Zaczynamy
- W katalogu aplikacji utwórz poniższą strukturę zaczynając od clientapp. To tutaj znajdzie się cała część front-endu
W tym wpisie zajmiemy się tylko podstawami Reacta więc nie wykorzystamy całej struktury, ale będzie ona potrzebna w dalszych częściach.
- Modyfikacja pliku app.container.jsW tym pliku utworzymy nasz pierwszy komponent, który będzie punktem wejściowym całej aplikacji.
//app.container.js import React, { Component } from 'react' class AppContainer extends Component{ constructor(props){ super(props); } render(){ return( <div>Welcome on dotnetowo.pl</div> ); } } export default AppContainer;
Pierwsza linijka kodu jest importem z modułu ‘react’. Następnie został utworzony komponent o nazwie AppContainer. W konstruktorze na razie nic ciekawego się nie dzieje. Wrócimy tutaj później :). Ostatni element to funkcja render. To ona odpowiada za renderowanie kodu HTML.
- Modyfikacja pliku root.jsTutaj zostanie zainicjalizowany nasz komponent i wyświetlony w odpowiednim miejscu w pliku index.html.
//root.js 'use strict'; import AppContainer from './containers/app.container'; import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render( <AppContainer /> , document.getElementById('app-root') );
Metoda render obiektu ReactDOM, jako pierwszy argument przyjmuje element, który ma zostać dodany do DOM. Drugim argumentem jest kontener, w którym element ma zostać wyrenderowany.
- Instalacja pakietów i build aplikacjiCzas na utworzenie naszego pliku index.js, który zostanie zapisany w katalogu wwwroot i wczytany w index.html.
- Instalacja pakietów nodejsW katalogu z plikiem package.json wykonaj poniższe polecenie
npm install
- Build aplikacjiW katalogu z plikiem gulpfile.js wykonaj poniższe polecenie
gulp build
- Uruchomienie aplikacjiJeżeli wszystko zostało zakończone pozytywnie w katalogu wwwroot został wygenerowany plik index.js. Teraz w katalogu z plikiem ReactWithDotnet.csproj wykonaj poniższe polecenie
dotnet run
Aplikacja będzie dostępna na localhost:5000 i powinna wyglądać jak na poniższym screenie
To co do tej pory zrobiliśmy znajdziesz tutaj:
https://github.com/artzie92/dotnetowo_react_dotnetcore/tree/base_react_structure - Instalacja pakietów nodejsW katalogu z plikiem package.json wykonaj poniższe polecenie
Komponenty
Gratulacje. Udało Ci się utworzyć Twoją pierwszą aplikację napisaną w ReactJS. Aplikacja oparta o Reacta składa się z jednego lub wielu komponentów, na które powinieneś dzielić aplikację już podczas samego projektowania. Dzięki takiemu podziałowi w łatwy sposób jest możliwe ponowne wykorzystywanie kodu.
Jak zapewne zauważyłeś we wcześniej utworzonym API nasza aplikacja będzie prostą książką kontaktów. W takiej aplikacji na pewno będzie potrzebna lista osób, dla której utworzymy komponent o nazwie ‘people-list.component.js’. Dodaj poniższy plik w katalogu components.
//people-list.component.js import React, { Component } from 'react'; class PeopleListComponent extends Component { constructor(props) { super(props); } render() { return ( <div> <h2>People list component</h2> {this.props.name} </div> ); } } export default PeopleListComponent;
Następnie zmodyfikuj plik app.container.js
//app.container.js import React, { Component } from 'react' import PeopleListComponent from '../components/people-list.component'; class AppContainer extends Component{ constructor(props){ super(props); } render(){ return( <div>Welcome on dotnetowo.pl</div> ); } } export default AppContainer;
W pliku people-list.component.js wykorzystaliśmy obiekt o nazwie props, który został przekazany w konstruktorze. Przypisanie wartości nastąpiło natomiast w pliku app.container.js. Dzięki temu na Waszych ekranach powinien być poniższy efekt.
W przykładzie tym pojawił się obiekt props, o którym przeczytasz w kolejnym punkcie.
Podstawy, czyli: props, state, componentDidMount, componentWillMount, componentWillUnmount
Do kontrolowania komponentu wykorzystuje się dwa typy danych props i state. Pierwsze z nich są stałe przez cały czas życia komponentu i są ustawiane przez rodzica. W powyższym punkcie w komponencie AppContainer przypisaliśmy wartość dla parametru „name”, a następnie w komponencie PeopleListComponent odwołaliśmy się do niego.
W momencie, kiedy chcemy, aby dane były na bieżąco aktualizowane w naszym komponencie powinniśmy wykorzystać obiekt state. Wartość do obiektu state należy przypisać jedynie w konstruktorze komponentu. W każdym innym miejscu należy używać metody setState(), która zapewni odświeżenie naszego komponentu. Na ten moment nasza aplikacja jest w fazie początkowej, ale zanim przejdziemy dalej na, krótką chwilę zmodyfikuj PeopleListComponent jak poniżej.
//people-list.component.js import React, { Component } from 'react'; class PeopleListComponent extends Component { constructor(props) { super(props); this.incrementer = this.incrementer.bind(this); this.wrongDecrementer = this.wrongDecrementer.bind(this); this.counter = 0; this.wrong = 0; this.state = { test: this.counter, test2: this.wrong } } componentDidMount(){ this.incrementer(); this.wrongDecrementer(); } wrongDecrementer(){ this.wrong--; this.state.test2 = this.wrong; setInterval(this.wrongDecrementer, 2000); } incrementer() { this.counter++; this.setState({ test: this.counter }) setInterval(this.incrementer, 5000); } render() { return ( <div> <h2>People list component</h2> {this.props.name} Test variable in state object: {this.state.test} Test set state in wrong way: {this.state.test2} </div> ); } } export default PeopleListComponent;
W powyższym kodzie dodałem dwie zmienne do obiektu state: test i test2. Pierwsza z nich powinna być zwiększana o jeden co 5 sekund a druga zmniejszana o jeden co 2 sekundy. Dla pierwszej zmiennej wykorzystałem prawidłową metodę do zmiany wartości obiektu state – setState(). W drugim przypadku przypisywałem wartość bezpośrednio do obiektu state. Jak zapewne zauważyliście wartość zmiennej test2 nie aktualizuje się na ekranie co 2 sekundy, a dopiero co 5 sekund, ponieważ wtedy dopiero następuje wywołanie funkcji render.
Dlatego tak ważne jest ustawianie sesji za pomocą metody setState().
Na pewno zauważyłeś dwie poniższe linijki:
this.incrementer = this.incrementer.bind(this); this.wrongDecrementer = this.wrongDecrementer.bind(this);
Służą one do bindowania kontekstu komponentu do metod. Dzięki temu odwołując się po słowie kluczowym this mamy pewność, że nadal jesteśmy w kontekście komponentu i mamy dostęp do jego metod.
Następną nowością jest metoda componentDidMount()
componentDidMount(){ this.incrementer(); this.wrongDecrementer(); }
Jak się domyślasz zostaje ona wywołana w momencie, kiedy komponent zostanie już utworzony. Spróbuj wywołać zawartość tej metody w konstruktorze i zobacz konsolę w przeglądarce wtedy na pewno zrozumiesz, dlaczego w tym przypadku została wykorzystana. Kolejne dwie ważne metody to componentWillMount() i componentWillUnmount(). Po samych nazwach powienieneś się domyśleć, kiedy zostają wywoływane. Wykorzystamy je później, ale zachęcam samodzielne sprawdzenie ich działania :).
Podsumowanie
W pierwszej części przedstawiłem, jak połączyć w prosty sposób Web API napisane w .NET core razem z ReactJS. Zachęcam do samodzielnej zabawy z przyswojoną wiedzą. Utwórz kilka komponentów, połącz je ze sobą, sprawdź na własnej skórze jak działa state i props oraz trzy poznane funkcje. Jest to fundament, bez którego nie pójdziesz dalej, dlatego przyłóż się, aby jak najwięcej zrozumieć.
W kolejnym wpisie przejdziemy do koncepcji Flux, która zapewni nam prawidłowe zarządzanie cyklem życia aplikacji.
Dziękuję za odwiedziny i zapraszam ponownie :).
Repozytorium:
https://github.com/artzie92/dotnetowo_react_dotnetcore/tree/base_react_knowledge
Bardzo fajny wpis, na pewno przyda się komuś, kto zaczyna z react’em i .net’em.
Sam jestem mocno zainteresowany .NET Core’em na Unixowych platformach, więc mam nadzieję, że będzie więcej wpisów na tym blogu 🙂
Warto wspomnieć, że dzięki stworzonemu w gulpie taskowi ‘watch’ możemy, automatycznie ‘obserwować’ naszą aplikację. Najwygodniej w VSCode otworzyć drugi terminal i będąc w katalogu z plikiem gulpfile.js wpisać gulp watch lub po prostu gulp (watch jest w naszym przypadku defaultowym taskiem). Tym sposobem każdy save na js’ach będzie skutkować wywołaniem polecenia gulp build, co podmieni nam aplikacje i pozwoli podejrzeć zmiany bez potrzeby ‘recznego’ buildowania.
Ciekawy tutorial, szczególnie część opisująca działanie setState()
Wpis bardzo merytoryczny i dobry, żeby rozpocząć przygodę z Reactem. Mam nadzieję, że seria będzie kontynuowana 🙂