2017-07-10
zelder
2017-07-20
10/07
2017

Настройка приложения на NodeJs + TypeScript + React

Описание


Задача
Создать приложение - одностраничный сайт с большим количеством динамики.
Приложение будет иметь много различных визуальных компонентов, которые необходимо будет обновлять.
Логика приложения будет обновляться и дополняться.
На выходе должен быть сайт со всем необходимым, что можно "просто" положить на любой хостинг и это должно работать без каких либо настроек сервера.

Технологии
- NodeJS. Используем на серверной стороне для компиляции кода (TypeScript) и прочих работ. 
- TypeScript, для типизации JavaScript кода. Приложение подразумевается большим на скриптах и с частыми обновлениями (поддержкой).
- React. Применим для отображения визуальных компонент и их связь с кодом.
- Webpack. Пакет nodejs для компиляции кода TypeScript с учетом модульности (CommonJs). Позволяет оставить некоторые модули вне общего бандла (отделим React, jQuery и подобные).
- Gulp. Сборка всего и вся. Бандл стилей (less), скриптов и прочие операции для продакшн.

Инструменты
- Windows 10 (используется при написании этой статьи)
- командная строка Windows
- Visual Studio Code (подойдет любой текстовый редактор)
Подготовка проекта


Установить NodeJS (https://nodejs.org)

Дальнейшие команды будут выполняться через командную строку (либо PowerShell) с правами администратора,
где необходимо перейти в новую папку (создайте ее) для будущего приложения.


Убеждаемся о наличии NodeJS
node -v
В этой статье использовалась версия 8.1.3, с ним уже идет npm - установщик пакетов для nodejs.

Инициализация приложения
npm init
Далее необходимо будет ответить на вопросы, такие как, название пакета, версия, описание и прочие.
После чего будет создан файл package.json.
Установка пакетов

Устанавливать nodejs-пакеты можно двумя способами:
1. прописав в файле package.json в ветках dependencies/devDependencies необходимые пакеты и выполнив команду npm install.
2. из командной строки, указывая какие именно пакеты установить (после установки они пропишутся в package.json сами).

Webpack
Будем использовать только для компиляции TypeScript кода и бандл всего этого с учетом модульности (CommonJs).
npm install -g webpack
Теперь вы можете выполнить команду webpack в консоли, но пока это бесполезно. Необходимо создать файл конфигурации для него webpack.config.js (создадим позже).

TypeScript
Мы будем собирать TypeScript код через Webpack, поэтому установим сам компилятор, и awesome-typescript-loader - который поможет Webpack компилировать скрипты используя стандартный файл конфигурации tsconfig.json (создадим позже).
npm install --save-dev typescript awesome-typescript-loader source-map-loader
После этой операции будет создана папка node_modules, в которой лежат наши установленные и связанные пакеты.
Кстати, в папке node_modules\.bin вы можете найти файл tsc.cmd. Теперь вы можете вручную запускать компилятор typescript (tsc), зайдя в эту папку и выполнив команду tsc.

React
Далее установим скрипты React и ReactDOM. Это минимум нам необходимый.
npm install --save react react-dom @types/react @types/react-dom
Обратите внимание на установку @types/react и @types/react-dom. Этим самым мы научим nodejs понимать о существовании этих модулей.
Тем самым, в коде, мы сможем обращаться к этим модулям, прописав import * as React from "react";

Gulp
Этот модуль позволяет запускать задачи, которые мы и укажем ему. В частности, запуск webpack (который в свою очередь, компилирует typescript), бандл стилей в один файл, перенос внешних плагинов (React, jQuery) в публичную папку и многое другое.
Эти задачи можно было бы выполнить и в Webpack, но он не покроет всех задач в будущем, поэтому будем применять и Gulp.
npm install --save-dev gulp
Задачи для Gulp мы будем прописывать в файле gulpfile.js (создадим позже).
Чтобы запускать сам gulp из командной строки, установим для него командер:
npm install --global gulp-cli

Настройка

TypeScript
Создадим файл конфигурации tsconfig.json для компилятора tsc. В нем пропишем какие файлы компилировать, где их искать, какой паттерн модульности использовать и прочее.
{
"compilerOptions": {
"outDir": "./public/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react"

},
"include": [
"./src/js/**/*"
]
}
Теперь, если положить какой либо файл с кодом на typescript (.ts) в папку ./src/js/ (или какую укажите в конфиге выше) и выполнить команду в консоли node_modules\.bin\tsc, то будут скомпилированы javascript файлы.

Webpack
Простые файлы с typescript хорошо компилируются и работают. Но наш проект будет использовать модули (React, jQuery). При использовании модулей, сами становимся модулем.
Модули компилируются по разным паттеранм (AMD, SystemJs, CommonJs и прочие). На серверной стороне NodeJs знает как с этим работать (для него понятна запись вроде, var gulp = require('gulp'); ), а вот на стороне клиента необходимо обучить браузер этому.
Есть несколько решений этой проблемы. Можно вручную скачать пакет RequireJs и его настроить. Но он будет асинхронно загружать необходимые модули. Есть альтернатива webpack`у - browserify. Его легко настроить в gulp, но он бандлит все скрипты нашего проекта и все внешние модули (React) в один большой файл. На данный момент иначе он не умеет, а нам желательно внешние модули оставить отдельно (незачем им все время участвовать в компиляции, да и пускай браузер их кэширует отдельно и их проще будет заменить/обновить отдельно от всего проекта). А вот webpack может решить нашу задачу. Весь наш проект будут сложен в один javascript файл, а внешние модули будут отдельно.

Создадим файл конфигурации webpack.config.js для webpack, в котором пропишем что и куда надо собрать, а что не включать в конечный бандл (конечный файл с javascript кодом).
module.exports = {
    entry: "./src/js/index", // точка входа
    output: { // это конечный бандл нашего проекта
        filename: "bundle.js",
        path: __dirname + "/public"
    },
    devtool: "source-map",
    resolve: {
        extensions: [".ts", ".tsx", ".js", ".json"]
    },
    module: {
        rules: [
            // 'awesome-typescript-loader' - сборщик из TypeScript в JavaScript, он умеет читать настройки из tsconfig.json
            { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
            // 'source-map-loader'
            { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
        ]
    },
    // исключаем модули из общего бандла (незачем это все таскать хвостом, и пускай браузер кэширует это отдельно)
    externals: {
        "react": "React",
        "react-dom": "ReactDOM"
    },
};
Попробуйте теперь выполнить в командной сроке команду webpack, скорее всего вы получите сообщение об ошибке 'Entry module not found'. Он пытается найти точку входа в наше приложение.
Это первый файл для запуска нашего приложения, в нем, как правило создается основной объект для работы с приложением и прописаны связи с модулем (на typescript выглядит так, import * as React from "react";).
Этот файл необходим для webpack, т.к. он через точку входа определяет все связи модулей, и собирает их при необходимости в конечный бандл.

В этом файле конфигурации нам важны три пункта:
1. точка входа приложения (entry)
2. конечный бандл файл (output)
3. внешние модули, не включаемые в конечный бандл (externals)

Если при компиляции вы увидите подобную ошибку ERROR in [at-loader] ... TS2307: Cannot find module 'react-dom'.
Значит вы забыли выполнить npm install --save @types/react-dom

Если все настроено верно, то после команды webpack будет создан файл ./public/bundle.js (исходя из конфигурации как выше). Это и есть наше приложение, его необходимо подцеплять на страницу в html разметке, после всех внешних модулей.

Gulp
В принципе, все настроено и работает. Но наш проект (и скорее всего, любой другой), не ограничивается только скриптами. Хорошо еще бы собирать стили (например, из less файлов), перекладывать в публичную папку необходимый контент (те же внешние модули) и прочее.
Для начала, научим наш gulp запускать webpack, собирать стили и перекладывать внешние модули в публичную папку.
Создадим файл gulpfile.js с нашими настройками:
var gulp = require('gulp');
var uglify = require('gulp-uglify');
var webpack = require("webpack");
var gutil = require("gulp-util");
var less = require('gulp-less');
var cleanCSS = require('gulp-clean-css');
var concatCss = require('gulp-concat-css');
var runSequence = require('run-sequence');
var path = {
    buildPath: 'public/',
    lessFiles: [
        //'src/styles/**/*.less',
        'src/styles/main.less',
        'src/styles/ext.less'
    ],
    destCssFile: 'main.css',
sourceAppFile: 'src/scripts/_prebuild/bundle.js', // собранный файл из webpack
externalJsFiles: [ // внешние модули, которые необходимо переложить в публичную папку
'node_modules/react/dist/react.min.js',
'node_modules/react-dom/dist/react-dom.min.js'
]
}
//
// Запускаем Webpack (в нем компиляция TypeScript и реализация модульности - CommonJs)
//  какие модули и файлы войдут в бандл, а какие исключение (например, React) - смотреть в файле конфигурации ./webpack.config.js
//  больше в Webpack мы ничего не будем делать, все остальное тут, в Gulp
//  альтернатива Browserify, но он кладет все модули в один бандл
//
gulp.task("webpack", function(callback) {
    webpack(
        require('./webpack.config.js')
        , function(err, stats) {
            if(err) throw new gutil.PluginError("webpack", err);
            gutil.log("[webpack]", stats.toString({}));
            callback();
        }
    );
});
//
// Стили
//
gulp.task('styles', function(){
  return gulp.src(path.lessFiles)
    .pipe(less())
    .pipe(concatCss(path.destCssFile))
    .pipe(cleanCSS())
    .pipe(gulp.dest(path.buildPath))
});
//
// Скрипты
//
gulp.task('min:js', function(done){
    return gulp.src(path.sourceAppFile)
            .pipe(uglify())
            .pipe(gulp.dest(path.buildPath));
});
gulp.task('copy:js', function(done){
    return gulp.src(path.sourceAppFile)
            .pipe(gulp.dest(path.buildPath));
});
gulp.task('scripts:dev', function(done){
    runSequence('webpack', function() {
runSequence('copy:js', function() {
            done();
        });
    });
});
gulp.task('scripts:release', function(done){
runSequence('webpack', function() {
        runSequence('min:js', function() {
            done();
        });
    });
});
//
// Внешние модули
//
gulp.task('external:js', function(done){
    return gulp.src(path.externalJsFiles)
            .pipe(gulp.dest(path.buildPath));
});
//
// Сборки
//
gulp.task('build:dev', ['scripts:dev', 'styles'], function(){
});
gulp.task('build:release', ['scripts:release', 'styles', 'external:js'], function(){
});
// default
gulp.task('default', ['build:dev'], function(){
});

Теперь выполните команду gulp. Если консоль выдаст ошибку, что не знает такой команды - установите gulp-cli.
Теперь вы можете получить ошибку, вроде 'Error: Cannot find module 'gulp-uglify'', это значит, вам необходимо установить соответствующий модуль, который мы использовали в задачах в gulp.
Просмотрите все импортируемые модули в gulp (объявлены вверху файла конфигурации, в виде var uglify = require('gulp-uglify');) и установите необходимые.

Обратите внимание, чтобы внешние модули (React, ReactDOM) скопировались в публичную папку,
необходимо выполнить задачу gulp build:release

Сборка

Чтобы скомпилировать изменения, нужно выполнить
gulp
Будет запущена задача default, которая запустит webpack и прочее (что указано в конфиге).

Для полной сборки приложения 
gulp build:release



Чтобы вручную не выполнять команду gulp, можно настроить задачу с watch,
на просмотр изменений в файлах, и при необходимости, запуск компилятора tsc.








.