Von NPM über React zum Next.js-Server

Dieser Abschnitt ist keine Einführung in npm, React oder Next.js. Er soll vielmehr das grundsätzlichen Zusammenspiel der Softwarepakete aufzeigen

Daten zur Registrierungsdatenbank npm sindhierzu finden.
Informationen zurn JavaScript-Softwarebibliothek React gibt eshier
Das Framework Next.js isthier ist hier dokumnentiert.
Leider mangelt es an deutschen Versionen.

Wie im vorigen Abschnitt vorgeschlagen, entwickeln wir den Server auf dem PC. Dazu erstellen wir ein neues Verzeichnis (in dem Beispiel testWeb), starten den VS Code Editor und öffnen in diesem das Verzeichnis.

Mit node.js haben wir auch den Node Package Manager (NPM) installiert. Das lässt sich verifizieren, indem man z.B. in VS Code ein Terminal öffnet und die Version abfragt.

npm -v
6.14.9

Mit dem nächsten Kommando erzeugen wir eine package.json-Datei. NPM frägt normalerweise bei der Erstellung mehrere Parameter ab, das wird durch den Parameter -y unterdrückt.

npm init -y

package.json-Datei
package.json-Datei

Wie aus obigem Bild ersichtlich, wird die Eigenschaft "name" angemeckert. Dort wurde der Verzeichnisname, der einen Großbuchstaben hat, eingetragen. Zulässig sind aber nur kleine Buchstaben. Wir korrigieren das, die Datei package.json werden wir noch oft mit VS Code ändern.

Next.jssetzt aufReactauf. Wir werden zunächst einen "Hello World" React-Server konfigurieren, diesen dann zu einem Next.js Server umbauen und später diesen umBootstrapundSassergänzen.

Für React müssen wir nur zwei Pakete installieren. Wir geben also in die Konsole von VS Code folgende Installationskommandos ein:

npm install react react-dom

In der package.json-Datei erscheint nach der Installation die Eigenschaft dependencies mit den zwei installierten Paketen. Die Software selbst wird in dem neu angelegten Verzeichnis node_modules gespeichert. Zudem gibt es die neue Datei package-lock.json, in der die Installationsabhängigkeiten mit einem "Dependency-Tree" gespeichert sind, um die Paket-Installationen, unabhängig von inzwischen aktuelleren Versionen, reproduzieren zu können.

{
"dependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}

Würde Next.js als weiteres Paket installiert, hätten wir bereits einen funktionsfähigen Next.js-Server. Um das Hintergrundgeschehen etwas transparenter zu machen, soll zunächst ein React-Server mit Babel und webpack aufgesetzt werden. Wir installieren also Babel und webpack, diesmal als devDependencies. Das sind Pakete, die nur in der Entwicklungsumgebung gebraucht werden, beim späteren Einsatz (nach dem Transpilieren und Packen der Anwendung) entbehrlich sind. Aber warum Babel und webpack?

React und Next.js Programme werden üblicherweise inJSX(JavaScript XML) undES6geschrieben. JSX ist eine Erweiterung von JavaScript. Man könnte natürlich auch reines JavaScript verwenden. Damit ein JSX-Programm auf einem Browser oder in Node.js ausgeführt werden kann, muss es in reines JavaScript übersetzt (man sagt auch transpiliert) werden. Zudem verstehen ältere Browser kein ES6. Man übersetzt deshalb JSX und ES6 Programme inES5. Das erledigtbabel. Sie können eshierausprobieren. Das folgendes JSX-Programm

import React from 'react';
import ReactDOM from 'react-dom';
const name = 'Josh Perez';
const element = <h1 className="headline">Hello, {name}</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);

transpiliert babel z.B. zu:

var _react = _interopRequireDefault(require("react"));
var _reactDom = _interopRequireDefault(require("react-dom"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const name = "Josh Perez";
const element = /*#__PURE__*/ _react.default.createElement(
"h1",
{
className: "headline"
},
"Hello, ",
name
);
_reactDom.default.render(element, document.getElementById("root"));

Programmiersprachen haben im Allgemeinen Mechanismen zur Aufteilung von Programmen in unabhängige Module, die bei Bedarf konfliktlos (z. B. bei gleichen Variablennamen in unterschiedlichen Modulen) importiert werden können. Node.js besitzt diese Fähigkeit seit langem. Für JavaScript gibt es eine Reihe von Bibliotheken und Frameworks (wie webpack), die die Verwendung von Modulen ermöglichen. Auch Browser haben begonnen, diese Funktionalität nativ zu unterstützen. React und Next.js setzen unter der Haube webpackein. Webpack bündelt Module mit Abhängigkeiten zu statischen JavasScipt-Dateien, die alle Browser verarbeiten können.

Wir zäunen das Pferd von hinten auf und beginnen mit webpack. Im Terminal geben wir

npm install webpack webpack-cli --save-dev

ein und ändern das Objekt scripts in der Datei package.json zu

"scripts": {
"build": "webpack"
}

Die Datei package.json sieht danach wie folgt aus:

{
"name": "testweb",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"devDependencies": {
"webpack": "^5.11.0"
"webpack-cli": "^4.2.0"
}
}

Mit dem Paket webpack-cli wurde ein Script gleichen Namens installiert. Dieses Script können wir durch die Ergänzung des scripts-Objekts in der package.json-Datei mit dem Kommando npm run build starten. Wir geben im Terminal den Befehl

npm run build

ein und erhalten die Fehlermeldung

ERROR in main
Module not found: Error: Can't resolve './src' in '/Users/peter/Desktop/testWeb'

webpack sucht den Eintrittspunkt im Verzeichnis ./src, bei dem es mit der Bündelung beginnen will. Wir kreieren, wunschgemäß, ein neues Verzeichnis src, erstellen in diesem die Datei index.js mit dem Inhalt console.log("Hello World"); und starten erneut das Kommando npm run build. Jetzt legt webpack im neuen Verzeichnis dist die Datei main.js an, die den transpilierten Inhalt der Quelldatei index.js hat.

Webpack "Hello World"
Webpack "Hello World"

webpack unterscheidet zwischen Entwicklungs- und Produktionsmode. Im Entwicklungsmode sind die Quelldateien verfügbar, ein Debugging ist möglich, und, wenn konfiguriert, das sofortige Laden von Änderungen. Im Produktionsmode dagegen werden die Quelldateien minimiert und mit anderen Assets, wie CSS und Bildern geladen. Das probieren wir aus, indem wir das scripts-Objekt in der package.json-Datei wie folgt ändern:

"scripts": {
"dev": "webpack --mode=development",
"build": "webpack --mode=production"
},

Um den Entwicklungsmode zu starten, geben wir in einem Terminal das Kommando

npm run dev

ein, für den Produktionsmode dagegen

npm run build

Die Unterschiede in der ./dist/main.js sind offensichtlich.

Die webpack-cli isthier dokumentiert. Wir können die Defaultwerte überschreiben. So könnte das script -Objekt z. B. so aussehen:

"scripts": {
"dev": "webpack --mode=development --entry=./index.js --output-path=./foo/main.js",
"build": "webpack --mode=production --entry=./index.js --output-path=./foo/main.js"
},

Um die webpack-Konfiguration problemlos erweitern zu können legen wir im Basisverzeichnis (im Verzeichnis, in dem die package.json liegt) die Dateiwebpack.config.js mit folgendem Inhalt an:

const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};

path ist ein Modul von node.js, die Startdatei ist wieder ./src/index.js und die Ausgabe soll in die Datei ./dist/main.js erfolgen.. Damit diese webpack-Konfiguration verwendet wird, ändern wir dass scripts-Objekt in der Datei package.json wie folgt:

"scripts": {
"dev": "webpack --mode=development --config=webpack.config.js ",
"build": "webpack --mode=production --config=webpack.config.js"
},

Eine Webseite ganz ohne HTML ist nahezu nutzlos. Um mit HTML in webpack zu arbeiten installieren wir das html-webpack-plugin,

npm install html-webpack-plugin --save-dev

ergänzen die webpack.config.js-Datei um das gerade installierte Plug-In,

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'index.html'),
}),
],
};

erstellen im Verzeichnis ./src eine index.html Datei mit folgendem Inhalt

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Webpack tutorial</title>
</head>
<body>
<h1>Hello Webpack</h1>
</body>
</html>

und starten im Terminal das Kommando

npm run build

webpack generiert im ./dist-Verzeichnis die Dateien index.html und main.js. Die index.html-Datei aus dem Verzeichnis ./src wurde von webpack um die Zeile

<script src="main.js"></script>

ergänzt. Diese Datei können wir in einem Browser öffnen.

webpack HTML-Plugin
webpack HTML-Plugin

Das Transpilieren wird ebenfalls mit Hilfe von webpack durchgeführt. Hierfür werden sogenannte Loader eingesetzt. Sie übersetzen den Quellcode, wenn er geladen wird (daher "Loader"). In unserem Fall brauchen wir 4 Softwarepakete:

Die Pakete werden mit folgendem Kommando installiert:

npm install --save-dev @babel/core babel-loader @babel/preset-env @babel/preset-react

Danach hat die Datei package.json folgenden Inhalt:

{
"name": "testweb",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack --mode=development --config=webpack.config.js ",
"build": "webpack --mode=production --config=webpack.config.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"babel-loader": "^8.2.2",
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.11.0",
"webpack-cli": "^4.2.0"
}
}

Die Konfiguration von Babel ist einfach: Wir legen im Basisverzeichnis eine neue Datei babel.config.json mit folgendem Inhalt an:

{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

und ergänzen die Datei webpack.config.js um das Objekt module:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
entry: './src/index.js',
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
],
},
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'index.html'),
}),
],
};

Um den Effekt zu sehen, ändern wir den Inhalt der Dateien./src/index.html und./src/index.js:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Webpack tutorial</title>
</head>
<body>
<h1>Hello Webpack</h1>
<div id="root"></div>
</body>
</html>

let greeting = 'Hello World';
console.log(`${greeting}`);
import React, { useState } from 'react';
import { render } from 'react-dom';
function App() {
const [state, setState] = useState('CLICK ME');
return <button onClick={() => setState('CLICKED')}>{state}</button>;
}
render(<App />, document.getElementById('root'));

und geben im Terminal das Kommando

npm run dev

ein. Die Änderungen spiegelt die Datei./dist/main.js deutlich. Wiederum können wir die./dist/index.html in einem Browser aufrufen. Dabei wird der Button, der in der Datei ./src/index.js mit React definiert wurde, sichtbar.

Soweit der Blick hinter die Kulissen. Wir wollen jetzt den Next.js-Server aufsetzen. Dazu löschen wir alle Dateien, die wir von Hand angelegt haben und auch die installierten Pakete außer react und react-dom. Im Gegenzug installieren wir Next.js und ändern das scripts-Objekt der Datei package.json:

npm uninstall --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader html-webpack-plugin webpack webpack-cli
npm install next

"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},

next.js - Server
next.js - Server

Zum Abschluss soll dieser Server noch mit einer Willkommensmeldung gestartet werden. Hierfür erstellen wir ein neues Verzeichnis ./pages und befüllen es mit der neuen Datei index.js, die folgenden Inhalt hat:

function HomePage() {
return <div>Welcome to Next.js!</div>
}
export default HomePage

Wir brauchen react nicht zu importieren, das macht Next.js selbst.

Starten können wir den Entwicklungs-Server mit dem Kommando:

npm run dev

mit dem Port 3000. Die Willkommensseite zeigt er im Browser, wenn wir die URL http://localhost:3000aufrufen

Die Besonderheit von Next.js ist, dass der Server im Produktions-Mode die Seiten statisch rendert und beim Aufruf einer Seite, dies statisch gerenderte Seite an den Browser liefert. Das können Sie daran sehen, dass im Browser Quelltext dieser Seite angezeigt wird. Hierdurch wird der erste Seitenaufbau gegenüber einer Seite, die durch JavaScript im Browser aufbaut wird , deutlich schneller. Wir können das nicht richtig ausprobieren, da wir nur die Mini-Seite haben.

Die Anwendung für den Produktions-Mode erstellt man durch das Kommando:

npm run build

Den Anwendungsserver können wir mit folgendem Kommando mit dem Port 3000 starten:

npm run start

Dieser Port 3000 mag für Entwicklungszwecke geeignet sein, für Anwendungszwecke ist der Default-Port 80 und/oder 443 obligatorisch. Next.js bietet die Möglichkeit einen Custom Server einzurichten. Dort lässt sich der Port explizit angeben. Dazu fügen wir im Basisverzeichnis die Datei server.js mit folgendem Inhalt hinzu:

const http = require('http');
const { parse } = require('url');
const next = require('next');
const portHttp = 80;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev: dev, dir: '.' });
const handle = app.getRequestHandler();
app.prepare().then(() => {
http
.createServer((req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
})
.listen(portHttp, (err) => {
if (err) throw err;
console.log(`> Ready on http://localhost:${portHttp}`);
});
});

Next.js muss noch mitgeteilt werden, dass es diesen Custom Server verwenden soll. Die geht über das scripts-Objekt:

"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},

Wenn wir den Server neu starten (`STRG-c` zum Beenden des laufenden Servers, `sudo npm run dev` zum Starten), können wir die Willkommensseite mit http://localhostaufrufen.

Leider funktioniert das nicht immer problemlos und auf den verschieden Betriebssystemen auch noch unterschiedlich. Gemeinsam ist, dass die Scripts bei Verwendung von Port 80 unter Administratorrechten gestartet werden müssen, auf UNIX artigen Systemen also mit sudo npm ..... Manchmal muss zusätzlich im scripts-Objekt die Zeile "dev": "node server.js," in "dev": "sudo node server.js",geändert werden und vor dem Aufruf eines anderen Scripts das Verzeichnis ./next gelöscht werden, da die Zugriffsrechte auf Dateien nicht ausreichen. Ich möchte das nicht weiter vertiefen, wir werden in der Google-Cloud nginx als Reverse-Proxy einsetzen und dem Node.js-Server einen Port oberhalb 1024 zuteilen. Dann sind keine Administratorrechte erforderlich.

Der nächste Schritt ist, diesen rudimentären Next.js - Server in der VM-Maschine der Google-Cloud zu installieren und mit dem Port 3000 zu starten, so dass er unter seiner externen IP-Adresse aufgerufen werden kann.