Как написать свой сайт на javascript

Время на прочтение
15 мин

Количество просмотров 134K

После прочтения ряда статей (например, этой) решил перейти на современный подход с использованием Node.js при написании простых сайтов с подхода «динозавров». Ниже представлен разбор примера сборки простого статического сайта с помощью Webpack 4. Статья написана, так как инструкции с решением моей задачи не нашел: пришлось собирать всё по кусочкам.

Постановка задачи

Сайт представляет собой простой набор HTML-страниц со своим CSS стилями и файлом JavaScript. Необходимо написать проект, который бы собирал наш сайт из исходников:

  • из SASS (точнее SCSS) файлов формируется один CSS файл;
  • из различных JavaScript библиотек и пользовательского кода формируется один JavaScript файл;
  • HTML страницы собираются с помощью шаблонизатора, где содержимое шапки и футера можно разнести по отдельным файлам.

В собранном сайте не должны использоваться React, Vue.js.

При выборе технологий выбираются по возможности наиболее популярные на данный момент. По этой причине отказался и от Grunt и Gulp в пользу Webpack, хотя, если честно, синтаксис Gulp мне понравился больше своим однообразием.

Для примера будет сверстано несколько страничек на базе Bootstrap 4. Но это только для примера.

Предполагается, что Node.js установлен (в Windows просто скачивается установщик и устанавливается в стиле «далее, далее»), и вы умеете работать с командной строкой.

Update. Нужно получить набор готовых HTML страниц, которые можно залить на хостинг без дополнительных настроек (например, на GitHub Pages) или открыть локально на компьютере.

Структура проекта

Общая структура проекта представлена ниже:

.
├── dist                 - папка, куда будет собираться сайт
├─┬ src                  - папка с исходниками сайта
│ ├── favicon            - папка с файлами иконок для сайта
│ ├── fonts              - папка со шрифтами
│ ├─┬ html               - папка заготовок HTML страниц
│ │ ├── includes         - папка с встраиваемыми шаблонами (header, footer)
│ │ └── views            - папка с самими HTML страницами
│ ├── img                - папка с общими изображениями (логотип, иконки и др.)
│ ├── js                 - папка с JavaScript файлами
│ ├── scss               - папка с SСSS файлами
│ └── uploads            - папка с файлами статей (картинки, архивы и др.)
├── package.json         - файл настроек Node.js
└── webpack.config.js    - файл настроек Webpack

Та же структура, но с показом файлов, которые присутствуют в примере:

.
├── dist
├─┬ src
│ ├─┬ favicon
│ │ └── favicon.ico
│ ├─┬ fonts
│ │ └── Roboto-Regular.ttf
│ ├─┬ html
│ │ ├─┬ includes
│ │ │ ├── footer.html
│ │ │ └── header.html
│ │ └─┬ views
│ │   ├── index.html
│ │   └── second.html
│ ├─┬ img
│ │ └── logo.svg
│ ├─┬ js
│ │ └── index.js
│ ├─┬ scss
│ │ └── style.scss
│ └─┬ uploads
│   └── test.jpg
├── package.json
└── webpack.config.js

Под favicon выделена целая папка, так как в современном web обычным одним ico файлом не обойтись. Но для примера используется только этот один файл.

Спорным решением может показаться разделение картинок на две папки: img и uploads. Но здесь использовал идеологию расположения файлов из WordPress. На мой взгляд, кидать все изображения в одну папку — не очень хорошая идея.

Для работы с проектом использую Visual Studio Code, которым очень доволен. Особенно мне нравится, что командная строка встроена в программу и вызывается через Ctrl + `.

Сделаем болванку Node.js проекта. Для этого создадим папку нашего проекта с вышеописанной структурой и перейдем в неё в командной строке, где вызовем команду для создания файла package.json.

npm init

На все вопросы можно просто отвечать, нажимая Enter, если заполнять подробную информацию не хочется.

Установим три общих пакета, которые нам потребуются в любом случае: webpack, webpack-cli (работу с командной строкой в webpack вынесли в отдельный пакет) и webpack-dev-server (для запуска локального сервера, чтобы в браузере сразу отображались сохраненные изменения проекта).

npm install webpack webpack-cli webpack-dev-server --save-dev

Файл package.json сейчас выглядит примерно так:

{
  "name": "static-site-webpack-habr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.1.1",
    "webpack-cli": "^2.0.11",
    "webpack-dev-server": "^3.1.1"
  }
}

Также создастся файл package-lock.json, который вообще не трогаем. Но в git репозиторий добавлять этот файл нужно, в отличии от папки node_modules, которую нужно прописать в файле .gitignore, если пользуетесь git.

Собираем JavaScript

Так как Webpack создан в первую очередь для сборки js файлов, то эта часть будем самой простой. Чтобы можно было писать javascript в современном виде ES2015, который не поддерживается браузерами, поставим пакеты babel-core, babel-loader, babel-preset-env.

npm install babel-core babel-loader babel-preset-env --save-dev

После создаем файл настроек webpack.config.js с таким содержимым:

const path = require('path');

module.exports = {
  entry: [
    './src/js/index.js',
  ],
  output: {
    filename: './js/bundle.js'
  },
  devtool: "source-map",
  module: {
    rules: [{
        test: /.js$/,
        include: path.resolve(__dirname, 'src/js'),
        use: {
          loader: 'babel-loader',
          options: {
            presets: 'env'
          }
        }
      },
    ]
  },
  plugins: [
  ]
};

В разделе entry (точки входа) указываем, какой js файл будем собирать, в разделе output указываем путь в папке dist, куда будем помещаться собранный файл. Обратите внимание, что в webpack 4 в пути output саму папку dist указывать не нужно! И да, как же мне не нравится, что в одном файле webpack в одних случаях нужно писать относительный путь, в других случаях относительный путь в специальной папке, в третьих случаях нужен уже абсолютный путь (например, его получаем этой командой path.resolve(__dirname, 'src/js')).

Также указано значение параметра devtool, равное: source-map, что позволит создавать карты исходников для js и css файлов.

Для обработки конкретных файлов (по расширению, по месторасположению) в webpack создаются правила в разделе rules. Сейчас у нас там стоит правило, что все js файлы пропускаем через транслятор Babel, который преобразует наш новомодный ES2015 в стандартный javascript вариант, понятный браузерам.

В нашем тестовом примере мы верстаем наши странице на Boostrap 4. Поэтому нам нужно будет установить три пакета: bootstrap, jquery, popper.js. Второй и третий пакет мы устанавливаем по требованию Bootstrap.

npm install bootstrap jquery popper.js --save

Обратите внимание на то, что эти три пакета нам нужны именно для самого сайта, а не для его сборки. Поэтому эти пакеты мы устанавливаем с флагом --save, а не --save-dev.

Теперь можно приступить к написанию нашего index.js файла:

import jQuery from 'jquery';
import popper from 'popper.js';
import bootstrap from 'bootstrap';

jQuery(function() {
    jQuery('body').css('color', 'blue');
});

В качестве примера пользовательского кода js просто перекрасили цвет текста на синий.

Теперь можно перейти к сборке js файла. Для этого в файле package.json в разделе scripts пропишем следующие npm скрипты:

  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production",
    "watch": "webpack --mode development --watch",
    "start": "webpack-dev-server --mode development --open"
  },

Теперь при запуске в командной строке строчки npm run dev произойдет сборка проекта (css и html файлы потом также будут собираться этой командой), и в папке /dist/js появятся файлы bundle.js и bundle.js.map.

При запуске команды npm run build также произойдет сборка проекта, но уже итоговая (с оптимизацией, максимальной минимизацией файла), которую можно выкладывать на хостинг.

При запуске npm run watch запускается режим автоматического просмотра изменений файлов проекта с автоматическим допостроением измененных файлов. Да, чтобы в командной строке отключить этот режим (например, чтобы можно было написать другие команды) можно нажать Ctrl + C (как минимум в PowerShell).

При запуске npm run start запустится локальный сервер, который запустит html страницу и также будет отслеживать изменения в файлах. Но пока этой командой не пользуемся, так как сборку html страниц не добавили.

Режим построения проекта создает или переписывает файлы в папке dist. Но во время разработки проекта при разных сборках файлы могут переименовываться, удаляться. И Webpack не будет следить, чтобы уже ненужные файлы, оставшиеся после предыдущих сборок, удалялись из папки dist. Поэтому добавим еще один пакет clean-webpack-plugin, который будет очищать папку dist перед каждой сборкой проекта.

Update 2018.04.11. Пришлось отказаться от clean-webpack-plugin. Почему? Когда запускаешь сервер через команду npm run start (webpack-dev-server --mode development --open), то webpack компилирует файлы автоматом, не сохраняя их в папку dist. И это нормально. Но при этом папка dist очищается из-за наличия clean-webpack-plugin. В результате в режиме работы локального сервера папка dist пустует, что негативно сказывается на работе с git (только в случае, если вы в git репозиторий сохраняется сборку проекта, как и я): после каждого запуска сервера появляется куча изменений из-за удаленных файлов. Было бы хорошо, чтобы очистка папки dist происходила только при полноценной сборке, например, npm run build-and-beautify (об этой команде ниже). Плагин clean-webpack-pluginнастроить нужным способом не смог. Поэтому использую другой плагин del-cli, который не связан с webpack и работает отдельно.

npm install del-cli --save-dev

Внесем изменения в файл package.json.

{
...
  "scripts": {
...
    "clear": "del-cli dist"
  },
...
}

Сборка CSS файла

CSS файл будем собирать из SCSS файлов, под которые у нас зарезервирована папка src/scss. В ней создадим файл style.scss, например, со следующим содержимым:

$font-stack: -apple-system, BlinkMacSystemFont,Roboto,'Open Sans','Helvetica Neue',sans-serif;

@import "~bootstrap/scss/bootstrap";

@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(../fonts/Roboto-Regular.ttf);
}

body {
  font-family: $font-stack;
  #logo {
    width: 10rem;
  }
  .container {
    img {
      width: 20rem;
    }
  }
}

Обратите внимание на то, что стили Bootstrap подключаем не через его CSS файл, а через SСSS (@import "node_modules/bootstrap/scss/bootstrap" @import "~bootstrap/scss/bootstrap";), который позволит в случае надобности переписать те или иные свойства библиотеки, использовать его миксины и др. Но что печалит. Если при сборке js файла при подключении js файла Bootstrap библиотеки Webpack знает, где находятся нужные файлы, то при подключении стилей нужно указывать путь к папке в node_modules.

Для обработки css файлов нам будут нужны следующие модули: node-sass, sass-loader, css-loader и extract-text-webpack-plugin (говорят, что в следующей версии Webpack в последнем плагине надобность отпадет).

Важно! На момент написания статьи плагин extract-text-webpack-plugin в стабильной версии не умеет работать с Webpack 4. Поэтому нужно устанавливать его beta версию через @next:

npm install node-sass sass-loader css-loader extract-text-webpack-plugin@next --save-dev

Надеюсь, что вскоре можно будет устанавливать все плагины по нормальному:

npm install node-sass sass-loader css-loader extract-text-webpack-plugin --save-dev

В webpack.config.js добавим следующие изменения:

...
const ExtractTextPlugin = require("extract-text-webpack-plugin");
...

module.exports = {
  entry: [
    ...
    './src/scss/style.scss'
  ],
  ...
  module: {
    rules: [{
      ...
      {
        test: /.(sass|scss)$/,
        include: path.resolve(__dirname, 'src/scss'),
        use: ExtractTextPlugin.extract({
          use: [{
              loader: "css-loader",
              options: {
                sourceMap: true,
                minimize: true,
                url: false
              }
            },
            {
              loader: "sass-loader",
              options: {
                sourceMap: true
              }
            }
          ]
        })
      },
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      filename: './css/style.bundle.css',
      allChunks: true,
    }),
    ...
  ]
};

Обратите внимание на то, что в точках входа entryмы добавили новый входной файл style.scss, но выходной файл указали не в output, а в вызове плагина ExtractTextPlugin в разделе plugins. Включаем поддержку карт источников sourceMap для пакетов sass-loader и css-loader.

Также можно заметить, что тут нет пакета style-loader, который чаще всего упоминается при работе с css в Webpack. Данный пакет встраивает css код в файл HTML, что может быть удобно для одностраничных приложений, но никак не для многостраничного.

И самый спорный момент. Для пакета css-loader мы добавили параметр url, равный false. Зачем? По умолчанию url=true, и если Webpack при сборке css находит ссылки на внешние файлы: фоновые изображения, шрифты (например, в нашем случае есть ссылка на файл шрифта url(../fonts/Roboto-Regular.ttf)), то он эти файлы попросит как-то обработать. Для этого используют чаще всего пакеты file-loader (копирует файлы в папку сборки) или url-loader (маленькие файлы пытается встроить в HTML код). При этом прописанные относительные пути к файлам в собранном css могут быть изменены.

Но с какой проблемой столкнулся на практике. Есть у меня папка src/scss с SСSS кодом. Есть папка src/img с картинками, на которые ссылаются в SСSS коде. Всё хорошо. Но, например, мне потребовалось подключить на сайт стороннюю библиотеку (например, lightgallery). SCSS код у неё располагается в папке node_modules/lightgallery/src/sass, который ссылается на картинки из папки node_modules/lightgallery/src/img через относительные пути. И если добавить стили библиотеки в наш style.scss, то file-loader будет искать картинки библиотеки lightgallery в моей папке src/img, а не там, где они находятся. И побороть я это не смог.

Update. С последней проблемой можно справиться, как подсказал Odrin, с помощью пакета resolve-url-loader и file-loader.

Пример решения

...

module.exports = {
  ...
  module: {
    rules: [
    ...
     {
        test: /.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {name: 'img/[name].[ext]'}  
          }
        ]
      },
      {
        test: /.(sass|scss)$/,
        include: path.resolve(__dirname, 'src/scss'),
        use: ExtractTextPlugin.extract({
          use: [{
              loader: "css-loader",
              options: {
                sourceMap: true,
                minimize: true//,
                //url: false
              }
            },
            {
              loader: "resolve-url-loader"
            },
            {
              loader: "sass-loader",
              options: {
                sourceMap: true
              }
            }
          ]
        })
      }
      ...
    ]
  },
...
};

То есть пакет resolve-url-loader вместо относительных путей ставит пути, которые webpack поймет. А уже file-loader будет копировать нужные файлы. Проблема в свойстве name в file-loader. Если его указать как name: '[path]/[name].[ext]', то в моей примере в папке dist появится папка distnode_moduleslightgallerysrcimg, в которой уже находятся изображения. Нет, в css будут прописаные верные пути до этой папки, но это будет не красиво. Поэтому лучше название файла указывать без пути (например name: 'img/[name].[ext]'). Правда, тогда все картинки пойдут в одну папку — не всегда это будет полезно.

Поэтому установкой url=false говорим, что все ссылки на файлы в SCSS коде не трогаем, пути не меняем, никакие файлы не копируем и не встраиваем: с ними разберемся потом отдельно. Возможно, это решение плохое, и вы предложите более правильный подход.

Сборка HTML страниц

Перейдем к самому веселому: к сборке HTML страниц, где у меня возникли самые большие трудности.

Для сборки HTML страниц будем использовать плагин html-webpack-plugin, который поддерживает различные виды шаблонизаторов. Также нам потребуются пакет raw-loader.

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

В качестве шаблонизатора HTML будем использовать шаблонизатор по умолчанию lodash. Вот так будет выглядеть типичная HTML страница до сборки:

<% var data = {
  title: "Заголовок | Проект",
  author: "Harrix"
}; %>
<%= _.template(require('./../includes/header.html'))(data) %>

<p>text</p>

<%= _.template(require('./../includes/footer.html'))(data) %>

Вначале в переменной data прописываем все наши переменные страницы, которые хотим использовать на этой странице. Потом встраиваем шаблоны шапки и футера через _.template(require()).

Важное уточнение. В статьях про сборку HTML страниц через html-webpack-plugin обычно подключают встраиваемые шаблоны просто через команду:

require('html-loader!./../includes/header.html')

Но при этом в этих встраиваемых шаблонах синтаксис lodash работать не будет (я так и не понял, почему так происходит). И данные из переменной data туда не передадутся. Поэтому принудительно говорим webpack, что мы встраиваем именно шаблон, который надо обработать как lodash шаблон.

Теперь мы можем использовать полноценные lodash синтаксис в встраиваемых шаблонах. В коде файла header.html ниже через <%=title%> печатаем заголовок статьи.

<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel="shortcut icon" href="favicon/favicon.ico">
    <link rel="stylesheet" href="css/style.bundle.css">

    <title><%=title%></title>
  </head>
  <body>
    <header><img src="img/logo.svg" id="logo"></header>

В пакете html-webpack-plugin есть возможность генерировать несколько HTML страниц:

 plugins: [
    new HtmlWebpackPlugin(), // Generates default index.html
    new HtmlWebpackPlugin({  // Also generate a test.html
      filename: 'test.html',
      template: 'src/assets/test.html'
    })
  ]

Но прописывать для каждой страницы создание своего экземпляра плагина точно не есть хорошо. Поэтому автоматизируем этот процесс, найдя все HTML файлы в папке src/html/views и создадим для них свои версии new HtmlWebpackPlugin().

Для этого в файле webpack.config.js внесем следующие изменения:

...
const HtmlWebpackPlugin = require('html-webpack-plugin');
const fs = require('fs')

function generateHtmlPlugins(templateDir) {
  const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir));
  return templateFiles.map(item => {
    const parts = item.split('.');
    const name = parts[0];
    const extension = parts[1];
    return new HtmlWebpackPlugin({
      filename: `${name}.html`,
      template: path.resolve(__dirname, `${templateDir}/${name}.${extension}`),
      inject: false,
    })
  })
}

const htmlPlugins = generateHtmlPlugins('./src/html/views')

module.exports = {
  module: {
      ...
      {
        test: /.html$/,
        include: path.resolve(__dirname, 'src/html/includes'),
        use: ['raw-loader']
      },
    ]
  },
  plugins: [
    ...
  ].concat(htmlPlugins)
};

Функция generateHtmlPlugins будет осуществлять поиск всех HTML страниц. Обратите внимание, что в коде функции есть настройка inject: false, которая говорит Webpack, что не нужно встраивать ссылки на js и css файл в HTML код самостоятельно: мы сделаем всё сами вручную в шаблонах header.html и footer.html.

Также нужно отметить, что встраиваемые шаблоны обрабатываются плагином raw-loader (содержимое файла просто загрузить как текст), а не html-loader, как чаще всего предлагают. И также, как в случае с CSS, не использую пакеты file-loader или url-loader.

И остается последний необязательный момент для работы с HTML. JavaScript файл и CSS файл у нас будут минимифицроваться. А вот HTML файлы хочу, наоборот, сделать красивыми и не минифицировать. Поэтому после сборки всех HTML файлов хочется пройтись по ним каким-то beautify плагином. И тут меня ждала подстава: не нашел способа как это сделать в Webpack. Проблема в том, что обработать файлы нужно после того, как будут вставлены встраиваемые шаблоны.

Нашел пакет html-cli, который может это сделать независимо от Webpack. Но у него 38 установок в месяц. То есть это означает два варианта: либо никому не нужно приводить к красивому внешнему виду HTML файлы, либо есть другое популярное решение, о котором я не знаю. А ради только одной этой функции Gulp прикручивать не хочется.

Устанавливаем этот плагин:

npm install html-cli --save-dev

И в файле package.json прописываем еще два скрипта, которые после работы Webpack будут приводить к красивому внешнему виду HTML файлы с установкой табуляции в два пробела.

  "scripts": {
    "build-and-beautify": "del-cli dist && webpack --mode production && html dist/*.html --indent-size 2",
    "beautify": "html dist/*.html --indent-size 2"
  },

Update 2018.04.11 Обратите внимание на то, что в команду build-and-beautify я добавил еще del-cli dist, который очищает папку dist перед сборкой.

Поэтому для итоговой сборки рекомендую использовать не команду *npm run build, а команду npm run build-and-beautify.

Копирование оставшихся файлов

Мы сгенерировали js, css файлы, HTML страницы. Остались файлы изображений, шрифтов и др., которые мы не трогали и сознательно не копировали через file-loader или url-loader. Поэтому скопируем все оставшиеся папки через плагин copy-webpack-plugin:

npm install copy-webpack-plugin --save-dev

В файле webpack.config.js внесем изменения:

...
const CopyWebpackPlugin= require('copy-webpack-plugin');
...

module.exports = {
  ...
  plugins: [
  ...
    new CopyWebpackPlugin([{
        from: './src/fonts',
        to: './fonts'
      },
      {
        from: './src/favicon',
        to: './favicon'
      },
      {
        from: './src/img',
        to: './img'
      },
      {
        from: './src/uploads',
        to: './uploads'
      }
    ]),
  ]...
};

Всё. Теперь командой npm run build-and-beautify собираем проект и в папке dist появится собранный статический сайт.

Итоговые файлы

Файл package.json:

{
  "name": "static-site-webpack-habr",
  "version": "1.0.0",
  "description": "HTML template",
  "main": "src/index.js",
  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production",
    "build-and-beautify": "del-cli dist && webpack --mode production && html dist/*.html --indent-size 2",
    "watch": "webpack --mode development --watch",
    "start": "webpack-dev-server --mode development --open",
    "beautify": "html dist/*.html --indent-size 2",
    "clear": "del-cli dist"
  },
  "dependencies": {
    "bootstrap": "^4.1.0",
    "jquery": "^3.3.1",
    "popper.js": "^1.14.3"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.3",
    "babel-preset-env": "^1.6.1",
    "copy-webpack-plugin": "^4.5.0",
    "css-loader": "^0.28.11",
    "del-cli": "^1.1.0",
    "extract-text-webpack-plugin": "^4.0.0-beta.0",
    "html-cli": "^1.0.0",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.8.3",
    "raw-loader": "^0.5.1",
    "sass-loader": "^6.0.6",
    "webpack": "^4.5.0",
    "webpack-cli": "^2.0.14",
    "webpack-dev-server": "^3.1.3"
  }
}

Файл webpack.config.js:

const path = require('path');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const fs = require('fs')

function generateHtmlPlugins(templateDir) {
  const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir));
  return templateFiles.map(item => {
    const parts = item.split('.');
    const name = parts[0];
    const extension = parts[1];
    return new HtmlWebpackPlugin({
      filename: `${name}.html`,
      template: path.resolve(__dirname, `${templateDir}/${name}.${extension}`),
      inject: false,
    })
  })
}

const htmlPlugins = generateHtmlPlugins('./src/html/views');

module.exports = {
  entry: [
    './src/js/index.js',
    './src/scss/style.scss'
  ],
  output: {
    filename: './js/bundle.js'
  },
  devtool: "source-map",
  module: {
    rules: [{
        test: /.js$/,
        include: path.resolve(__dirname, 'src/js'),
        use: {
          loader: 'babel-loader',
          options: {
            presets: 'env'
          }
        }
      },
      {
        test: /.(sass|scss)$/,
        include: path.resolve(__dirname, 'src/scss'),
        use: ExtractTextPlugin.extract({
          use: [{
              loader: "css-loader",
              options: {
                sourceMap: true,
                minimize: true,
                url: false
              }
            },
            {
              loader: "sass-loader",
              options: {
                sourceMap: true
              }
            }
          ]
        })
      },
      {
        test: /.html$/,
        include: path.resolve(__dirname, 'src/html/includes'),
        use: ['raw-loader']
      },
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      filename: './css/style.bundle.css',
      allChunks: true,
    }),
    new CopyWebpackPlugin([{
        from: './src/fonts',
        to: './fonts'
      },
      {
        from: './src/favicon',
        to: './favicon'
      },
      {
        from: './src/img',
        to: './img'
      },
      {
        from: './src/uploads',
        to: './uploads'
      }
    ]),
  ].concat(htmlPlugins)
};

Файл шаблона index.html:

<% var data = {
  title: "Заголовок | Проект",
  author: "Harrix"
}; %>
<%= _.template(require('./../includes/header.html'))(data) %>

<div class="container">
  <p>Первая страница.</p>
  <p><img src="uploads/test.jpg"></p>
</div>

<%= _.template(require('./../includes/footer.html'))(data) %>

Файл шаблона шапки header.html:

<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel="shortcut icon" href="favicon/favicon.ico">
    <link rel="stylesheet" href="css/style.bundle.css">

    <title><%=title%></title>
  </head>
  <body>
    <header><img src="img/logo.svg" id="logo"></header>

Файл шаблона footer.html:

<footer><%=author%></footer>

<script src="js/bundle.js"></script>
</body>
</html>

Сгенерированный index.html:

<!doctype html>
<html lang="ru">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <link rel="shortcut icon" href="favicon/favicon.ico">
  <link rel="stylesheet" href="css/style.bundle.css">

  <title>Заголовок | Проект</title>
</head>

<body>
  <header><img src="img/logo.svg" id="logo"></header>

  <div class="container">
    <p>Первая страница.</p>
    <p><img src="uploads/test.jpg"></p>
  </div>

  <footer>Harrix</footer>

  <script src="js/bundle.js"></script>
</body>

</html>

Исходники

Ссылка на репозиторий с рассмотренным проектом.

  • Назад
  • Обзор: Express Nodejs
  • Далее

Эта вторая статья в нашем учебнике Express показывает, как создать каркас проекта веб-сайта, который позже можно будет заполнить с помощью путей сайта, шаблонов представлений и обращений к базе данных.

Необходимые знания: Установить среду разработки Node. Просмотреть учебник Express.
Задача: Научиться запускать свои проекты используя Express Application Generator.

Обзор

В этой статье показано, как создать каркас сайта с помощью средства Express Application Generator. Каркас затем можно будет заполнить с помощью путей сайта, шаблонов/представлений и обращений к базе данных. Мы используем это средство для создания основы нашего сайта Local Library. К основе будет добавлен код, необходимый сайту. Создание каркаса чрезвычайно просто — требуется только вызвать генератор в командной строке, указав имя нового проекта, дополнительно можно указать также движок шаблона сайта и генератор CSS.

Далее показано, как вызвать генератор приложений, и даётся небольшое пояснение различных вариантов представлений и CSS. Мы поясним структуру каркаса веб-сайта. В конце мы покажем, как запустить веб-сайт, чтобы убедиться, что он работает.

Примечание: Express Application Generator — не единственный генератор Express-приложений, и созданный проект —не единственный жизнеспособный способ организации ваших файлов и каталогов. Однако созданный сайт имеет модульную структуру, которую легко понять и расширить. О минимальном Express приложении смотрите Hello world example в документации Express.

Применение генератора приложений

Вы уже должны были установить express-generator, читая статью установка среды разработки Node. Напомним, что генератор установлен с помощью менеджера пакетов NPM, при выполнении команды:

npm install express-generator -g

express-generator имеет ряд параметров, которые можно увидеть, выполнив команду express —help (или express -h):

> express --help

  Usage: express [options] [dir]

  Options:

    -h, --help           output usage information (информация по применению)
        --version        output the version number (номер версии express)
    -e, --ejs            add ejs engine support (добавить поддержку движка ejs)
        --pug            add pug engine support (добавить поддержку движка pug)
        --hbs            add handlebars engine support (добавить поддержку движка handlebar)
    -H, --hogan          add hogan.js engine support (добавить поддержку движка hogan.js)
    -v, --view <engine>  add view <engine> support (ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)
                         (добавить поддержку движков представлений. По умолчанию - jade)
    -c, --css <engine>   add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)
                         (добавить поддержку движков стилей, по умолчанию - простой CSS)
        --git            add .gitignore (добавить поддержку .gitignore)
    -f, --force          force on non-empty directory (работать в каталоге с файлами)

Команда express создаст проект в текущем каталоге с использованием (устаревшего) движка представления Jade и обычного CSS. Если указать express name, проект будет создан в подкаталоге name текущего каталога.

Можно выбрать движок представления (шаблон), используя --view параметр --css позволяет выбрать движок для создания CSS.

Примечание: Другие опции (--hogan, --ejs, --hbs и пр.) для выбора шаблонизатора устарели. Используйте --view (или -v)!

Какой движок представлений следует использовать?

Express-generator даёт возможность сконфигурировать несколько популярных движков, включая EJS, Hbs, Pug (Jade), Twig, и Vash, но по умолчанию выбран Jade. Экспресс сразу после установки может поддерживать большое количество и других шаблонизаторов.

Примечание: При желании использовать шаблонизатор, который не поддерживается генератором, просмотрите документацию Using template engines with Express и документацию для нужного шаблонизатора.

Как правило, следует выбрать шаблонизатор, который имеет всю необходимую вам функциональность и обеспечивает вам высокую производительность — так же, как вы выбираете любой другой компонент! Некоторые критерии для сравнения шаблонизаторов:

  • Время до получения результата — если ваша команда уже имела дело с шаблонизатором, то, скорее всего, продуктивнее будет использовать этот шаблонизатор. Если нет, тогда следует учесть все относительные сложности изучения кандидатов в шаблонизаторы.
  • Популярность и активность — проверьте популярность движка, возможно, у него есть активное сообщество. Очень важно иметь поддержку для движка, если у вас возникнут проблемы в течении жизни веб-сайта.
  • Стиль — некоторые шаблонизаторы используют особую разметку для отображения вставленного контента внутри «обычного» HTML, а другие строят HTML, используя специальный синтаксис (например, используя отступы или блочные имена).
  • Производительность и время интерпретации.
  • Особенности — следует выбирать движок с учётом таких особенностей:
    • Наследование макета: позволяет определить базовый шаблон и затем наследовать только те части, которые отличаются для конкретной страницы. Это, как правило, лучший подход, чем создание шаблонов путём включения нескольких необходимых компонентов или создания шаблона с нуля каждый раз.
    • Поддержка «Include»: позволяет создавать шаблоны, включая другие шаблоны.
    • Краткий синтаксис управления переменными и циклами.
    • Возможность фильтровать значения переменных на уровне шаблона (например, делать переменные в верхнем регистре или форматировать значение даты).
    • Возможность создавать выходные форматы, отличные от HTML (например, JSON или XML).
    • Поддержка асинхронных операций и потоковой передачи.
    • Возможность использования как на клиенте, так и на сервере. Возможность применения движка шаблона на клиенте позволяет обслуживать данные и выполнять все действия или их большую часть на стороне клиента.

Примечание: В интернете множество ресурсов, которые помогут сравнить различные варианты!

Для этого проекта мы используем шаблонизатор Pug (в прошлом назывался Jade) — один из популярнейших Express/JavaScript шаблонизаторов, который поддерживается в Express-generator «из коробки».

Какие шаблонизаторы CSS следует использовать?

Express Application Generator позволяет создавать проекты, настроенные для применения шаблонизаторов CSS: LESS, SASS, Compass, Stylus.

Примечание: простой CSS имеет некоторые ограничения, затрудняющие выполнение задач. Шаблонизаторы CSS позволяют использовать более эффективный подход для создании таблиц стилей CSS, но требуют компиляции файлов таблиц стилей в стандартный CSS для применения в браузере.

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

Какую базу данных следует использовать?

Сгенерированный код не использует и не содержит в себе какой-либо базы данных. Express может использовать любой движок базы данных, который поддерживается Node (Express не предъявляет каких-либо особых требований к базе данных).

Мы обсудим взаимодействие с базой данных в следующей статье.

Создание проекта

Разрабатывая пример — приложение Local Library, мы построим проект с именем express-locallibrary-tutorial. Используем библиотеку шаблонов Pug, а движок CSS применять не будем.

Выберем место для нового проекта — каталог express-locallibrary-tutorial — и выполним команду:

express express-locallibrary-tutorial --view=pug

Будет создан каталог express-locallibrary-tutorial и выведен список созданных внутри каталога проектных файлов.

   create : express-locallibrary-tutorial
   create : express-locallibrary-tutorial/package.json
   create : express-locallibrary-tutorial/app.js
   create : express-locallibrary-tutorial/public/images
   create : express-locallibrary-tutorial/public
   create : express-locallibrary-tutorial/public/stylesheets
   create : express-locallibrary-tutorial/public/stylesheets/style.css
   create : express-locallibrary-tutorial/public/javascripts
   create : express-locallibrary-tutorial/routes
   create : express-locallibrary-tutorial/routes/index.js
   create : express-locallibrary-tutorial/routes/users.js
   create : express-locallibrary-tutorial/views
   create : express-locallibrary-tutorial/views/index.pug
   create : express-locallibrary-tutorial/views/layout.pug
   create : express-locallibrary-tutorial/views/error.pug
   create : express-locallibrary-tutorial/bin
   create : express-locallibrary-tutorial/bin/www

   install dependencies:
     > cd express-locallibrary-tutorial && npm install

   run the app:
     > SET DEBUG=express-locallibrary-tutorial:* & npm start

После списка файлов генератор выведет инструкции для установки зависимостей (указанных в файле package.json) и запуска приложения (инструкции предназначены для Windows; для Linux/Mac OS X они могут слегка отличаться).

Запускаем каркас сайта

Сейчас у нас есть готовый каркас проекта. Сайт пока ничего не делает, но его стоит запустить, чтобы убедиться в его работоспособности.

  1. Прежде всего установим зависимости (команда install запросит все пакеты зависимостей, указанные в файле package.json).
    cd express-locallibrary-tutorial
    npm install
    
  2. Затем запустим приложение.
    • В Windows используйте команду:
      SET DEBUG=express-locallibrary-tutorial:* & npm start
      
    • В Mac OS X или Linux используйте команду:
      DEBUG=express-locallibrary-tutorial:* npm start
      
  3. Откроем http://localhost:3000/ в браузере. Мы должны увидеть такую страницу:

Browser for default Express app generator website

У нас получилось веб-приложение на базе Express, работающее по адресу localhost:3000.

Примечание: Можно также запустить приложение командой npm start. Переменная DEBUG, указанная в примере, включает логирование в консоль для дальнейшей отладки. Так, при посещении страницы веб-приложения, вы увидите похожий вывод в консоль:

>SET DEBUG=express-locallibrary-tutorial:* & npm start

> express-locallibrary-tutorial@0.0.0 start D:express-locallibrary-tutorial
> node ./bin/www

  express-locallibrary-tutorial:server Listening on port 3000 +0ms
GET / 200 288.474 ms - 170
GET /stylesheets/style.css 200 5.799 ms - 111
GET /favicon.ico 404 34.134 ms - 1335

Обеспечиваем
перезапуск сервера при изменении файлов

Любые изменения, внесённые на веб-сайт Express, не будут отображаться до перезапуска сервера. Остановка (Ctrl-C) и перезапуск сервера каждый раз после внесения изменений быстро становится раздражающей, поэтому стоит автоматизировать перезапуск.

Одно из самых простых средств для этого —
nodemon. Его обычно устанавливают глобально (так как это «инструмент»), но сейчас мы установим его и будем применять локально как зависимость разработки, так что любые разработчики проекта получат его автоматически при установке приложения. Выполним следующую команду (предполагаем, что мы находимся в корневом каталоге):

npm install --save-dev nodemon

Если вы предпочитаете установить nodemon глобально, не только для этого проекта, надо выполнить команду

npm install -g nodemon

В файле package.json проекта появится новый раздел с этой зависимостью (на вашей машине номер версии nodemon может быть другим) :

  "devDependencies": {
    "nodemon": "^1.11.0"
  }

Поскольку nodemon не установлен глобально, его нельзя запустить из командной строки (пока мы не добавим его в путь), но его можно вызвать из сценария NPM, так как NPM знает все об установленных пакетах. Раздел scripts в файле package.json исходно будет содержать одну строку, которая начинается с "start". Обновите его, поставив запятую в конце строки, и добавьте строку "devstart", показанную ниже:

  "scripts": {
    "start": "node ./bin/www",
    "devstart": "nodemon ./bin/www"
  },

Теперь можно запустить сервер почти так же, как и ранее, но командой npm run devstart:

  • В Windows:
SET DEBUG=express-locallibrary-tutorial:* & npm run devstart
  • Для macOS или Linux:
DEBUG=express-locallibrary-tutorial:* npm run devstart

Примечание: Сейчас после изменения любого файла проекта сервер будет перезапускаться (или можно самостоятельно перезапустить его, введя rs в командной строке). Вам всё равно придётся обновить страницу в браузере .

Теперь мы должны выполнять команду «npm run <scriptname>» а не просто npm start, поскольку «start», это, по сути, команда NPM, сопоставленная сценарию в файле package.json. Можно заменить команду в сценарии «start», но, так как мы хотим использовать nodemon только во время разработки, разумно создать новую команду сценария.

Созданный проект

Давайте посмотрим на созданный проект.

Структура каталогов

После установки зависимостей проект имеет такую структуру файлов (файлы — это элементы без префикса»/»). Файл package.json определяет имя файла с приложением, сценарии запуска, зависимости и др. Сценарий запуска задаёт точку входа приложения, у нас — файл JavaScript /bin/www. Этот файл настраивает некоторые обработчики ошибок приложения, а затем загружает app.js для выполнения остальной работы. Пути приложения хранятся в отдельных модулях каталога routes/. Шаблоны хранятся в каталоге /views.

/express-locallibrary-tutorial
    app.js
    /bin
        www
    package.json
    /node_modules
        [about 4,500 subdirectories and files]
    /public
        /images
        /javascripts
        /stylesheets
            style.css
    /routes
        index.js
        users.js
    /views
        error.pug
        index.pug
        layout.pug

Далее файлы описаны более подробно.

package.json

Файл package.json указывает зависимости приложения и содержит другие данные:

{
  "name": "express-locallibrary-tutorial",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www",
    "devstart": "nodemon ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.15.2",
    "cookie-parser": "~1.4.3",
    "debug": "~2.2.0",
    "express": "~4.14.0",
    "morgan": "~1.7.0",
    "pug": "~2.0.0-beta6",
    "serve-favicon": "~2.3.0"
  },
  "devDependencies": {
    "nodemon": "^1.11.0"
  }
}

Зависимости включают пакет express и пакет для выбранного движка представления (pug). Кроме того, указаны пакеты, полезные во многих веб-приложениях:

  • body-parser: — анализирует часть тела входящего запроса HTTP и облегчает извлечение из него различных частей. Например, мы можно читать POST-параметры.
  • cookie-parser: разбирает заголовок и заполняет req.cookies (по сути, даёт удобный метод для доступа к информации cookie).
  • debug: небольшой отладчик, работающий по образцу методики отладки ядра node.
  • morgan: средство логирования запросов HTTP для node.
  • serve-favicon: средство обработки favicon (значка, используемого для представления сайта на вкладках браузера, закладках и т. д).

Раздел «scripts» определяет скрипт» start», выполняемый при запуске сервера командой npm start. Можно видеть, что самом деле выполняется команда node ./bin/www. Кроме того, определяется script «devstart«, который вызывается командой npm run devstart. Запускается тот же файл ./bin/www ,но командой nodemon вместо node.

"scripts": {
    "start": "node ./bin/www",
    "devstart": "nodemon ./bin/www"
  },

Файл www

Файл /bin/www – это входная точка приложения. Сначала в файле создаётся объект основного приложения, расположенного в app.js — выполняется app= require(./app).

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');

Примечание: require() — это глобальная функция node для импорта модулей в текущий файл. Для модуля app.js указан относительный путь, а расширение файла по умолчанию (.js) опущено.

Оставшаяся часть кода настраивает порт сервера node для HTTP (определён в переменной среды или 3000, если не определён), и начинает обработку и протоколирование соединений и ошибок сервера. Сейчас вам не требуется дополнительных сведений о коде (все в этом файле шаблонно), но, при желании, его можно посмотреть.

Файл app.js

Этот файл создаёт объект приложения express (с именем app, по соглашению), настраивает приложение и промежуточное ПО, а затем экспортирует приложение из модуля. В приведённом ниже коде показаны только те части файла, которые создают и экспортируют объект приложения:

var express = require('express');
var app = express();
...
module.exports = app;

Именно этот экспортированный объект использован в рассмотренном ранее файле www.

Рассмотрим детали файла app.js. Сначала при помощи require(…) выполняется импорт некоторых полезных библиотек node: express, serve-favicon, morgan, cookie-parse, body-parser (они ранее были загружены для нашего приложения командой npm install), а также path из основной библиотеки node (применяется для разбора путей каталогов и файлов).

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

Затем require запрашивает модули из каталога путей route. Эти модули и файлы содержат код для обработки конкретного набора соответствующих путей (URL маршрутов). Если мы расширим каркас приложения, например, чтобы получить список книг библиотеки, нам следует добавить новый файл для обработки пути, связанного с книгами.

var index = require('./routes/index');
var users = require('./routes/users');

Примечание: Здесь мы только импортируем модули. В действительности эти пути ещё не используются — это произойдёт в файле несколько позже.

Далее, импортированные модули express применяются для создания объекта app, который потом устанавливает движки-шаблоны представления. Установка движков состоит их двух частей. В первой мы задаём значение ‘view’, указывая папку, в которой будут размещаться шаблоны (у нас это /views). Во второй мы задаём значение движка ‘view engine’, указывая на библиотеку шаблона (у нас — «pug»).

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

Следующие строки вызывают app.use(…), чтобы добавить промежуточные (middleware) библиотеки в цепочку обработки запросов. Кроме сторонних библиотек, импортированных ранее, используем библиотеку Express.static, что позволит обрабатывать статические файлы из папки /public корня проекта.

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

Теперь, когда промежуточные библиотеки настроены, мы добавляем (импортированный ранее) код обработки путей в цепочку обработки запросов. Импортированный код будет задавать отдельные пути для разных частей сайта:

app.use('/', index);
app.use('/users', users);

Примечание: . пути, указанные выше (‘/’ и ‘/users') рассматриваются как префиксы путей, определённых в импортированных файлах. Так, например, если импортированный модуль users определяет путь для /profile, для доступа следует указать /users/profile. Мы поговорим подробнее о путях в последующей статье.

Последняя в файле промежуточная библиотека добавляет методы обработки ошибок и ответов 404 от HTTP.

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

Объект app приложения Express теперь полностью настроен. Остался последний шаг — добавить его к экспортируемым элементам модуля (это позволит импортировать его в файле /bin/www).

Пути (Routes)

Файл путей /routes/users.js приведён ниже (файлы путей имеют сходную структуру, поэтому нет необходимости приводить также index.js). Сначала загружается модуль Express, затем он используется для получения объекта express.Router. После этого для этого объекта задаётся путь, и, наконец, объект-роутер экспортируется из модуля (именно это позволяет импортировать файл в app.js):.

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});

module.exports = router;

Путь определяет колбэк-функцию, которая будет вызвана, когда обнаружится HTTP GET-запрос корректного вида. Образец для сопоставления пути задаётся при импорте модуля — (‘/users‘) плюс что-то, определяемое в этом файле (‘/‘). Иными словами, этот путь будет использован, когда получен URL-запрос /users/.

Примечание: запустите сервер и задайте в браузере URL http://localhost:3000/users/. Вы должны увидеть сообщение: ‘respond with a resource’.

Стоит отметить, что колбэк-функция имеет третий аргумент — ‘next‘, т. е. является не простой колбэк-функцией, а колбэк-функцией промежуточного модуля. Пока третий аргумент не используется, но будет полезен в дальнейшем, если мы захотим создать несколько обработчиков пути '/'.

Представления (шаблоны)

Файлы преставлений (шаблонов) хранятся в каталоге /views (это указано в app.js) и имеют расширение** .pug. Метод Response.render() выполняет указанный шаблон, передавая объекту значение именованной переменной, и затем посылает результат как ответ. В коде из /routes/index.js (приводится ниже) можно увидеть, что роут отвечает, используя шаблон «index» с переданным значением переменной «title» из шаблона.

/* GET home page. */
router.get('/', function(req, res) {
  res.render('index', { title: 'Express' });
});

Шаблон для пути ‘/’ приведён ниже (файл index.pug). О синтаксисе мы поговорим позже. Сейчас важно знать, что переменная title со значением ‘Express’ помещена в определённое место шаблона.

extends layout

block content
  h1= title
  p Welcome to #{title}

Мини-тест

Создайте новый путь в /routes/users.js, чтобы выводить текст «You’re so cool» или «Ну, вы крутой!» по URL /users/cool/. Проверьте его, запустив сервер и посетив в браузере http://localhost:3000/users/cool/.

Итоги

Сейчас создан каркас проекта Local Library. Мы проверили, что он запускается с использованием Node. Но главное, что вы поняли структуру проекта, и знаете, где и как добавить пути и представления для нашей локальной библиотеки.

Далее мы изменим каркас, чтобы он работал как библиотечный веб-сайт

Смотрите также

Создать полноценный VEB-сайт полностью на языке JavaScript невозможно,да и ничего хорошего в этом нет.Надо создать HTML страницу а уже потом включать в него коды JavaScript.

Спецификация HTML 4 поддерживает атрибут type: <script type=»text/javascript»>
В HTML 4 если вы собираетесь использовать в файле только JavaScript можно прописать в мете-тегах указание на его использование:
<meta http-equiv=»Content-Script-Type» content=»text/javascript»>
При этом атрибуты language или type указывать не надо.

Javascript подключается к HTML-странице и работает без компиляции. Чтобы подключить Javascript надо в странице указать:

<script language=»JavaScript»>
Здесь пишется содержимое JS
</script>

Можно подключить и так:
<script language=»JavaScript»> </script>

Запишем пример включения Javascript в страницу:

<html>
<head>
<title>JavaScript</title>
<script type=»text/javascript» language=»javascript»>
</script>
</head>
<body>
<script type=»text/javascript» language=»javascript»>
Здесь пишется содержимое JS </script>
</body>
<html>

Размещайте JavaScript в самом конце страницы перед <body>

В качестве примера возьмем создание простейшего сайта на главной странице.

Это код данного сайта.

<html>
<head>
<title>
Создание сайта </title>
<meta http-equiv=»Content-Type» content=»text/html; charset=windows-1251″> </head>
<body>
Создание собственного сайта.
<img src=»images/com (8).gif» width=213 height=170 border=0>
</body>
</html>

Теперь включим JavaScript в страницу
<html>
<head>
<title>Создание сайта </title>
<meta http-equiv=»Content-Type» content=»text/html; charset=windows-1251″> </head>
<body>
Создание собственного сайта.
<img src=»images/com (8).gif» width=213 height=170 border=0>

<script language=»JavaScript»> document.write(«Это включение JavaScript») </script>
</body>
</html>

В малом окне вы можете посмотреть результат включения JavaScript в HTML.

Создание собственного сайта.

В JavaScript 1.1 тег <script> поддерживает атрибут src. Если вы разместили скрипт в другом файле то надо указать путь к URL файлу, в котором содержится код:

<script src=»../javascript/name.js»></script>

How to Develop and Deploy Your First Full-Stack Web App Using A Static Site and Node.js

This tutorial will show you how to convert a static website that uses HTML, CSS and JavaScript (JS) to a dynamic one using MongoDB, Express, Static HTML, CSS, JS, and Node.js.

Our tech stack will be similar to the popular MEAN/MERN stack (MongoDB, Express, Angular or React, and NodeJS). But instead of using Angular or React, we will use a templating engine called EJS (Embedded JavaScript.)

Other popular templating engines include Handlebars, Pug, and Nunjucks.

Afterwards, we will deploy our Node.js web app to DigitalOcean and cover domain names, SSL, reverse proxies, and process managers.

Learning a templating language can be easier than a JS framework. You can just write HTML, and it lets you insert the same piece of code in multiple locations (called partials) or pass server-side variables to be displayed on the front-end (such as a username).

Table of Contents

  • Developing Your First Node.js Web App
    • Installing Node.js
    • Testing The Install
    • Creating Your First Server
    • Next Steps
    • Templating Basics
    • Passing Server-Side Data to the Front-End
  • Deploying Your First Node.js Web App
    • Setting Up DigitalOcean
    • Connecting To Your Droplet
    • Deploying Your Node.js Web App
    • Configuring Your Domain Name
    • Removing the Port Number From Your URL
    • Running the App on Boot (Setting Up A Process Manager)

Developing Your First Node.js Web App

Installing Node.js

First, make sure you’ve installed Node.js on your local machine or VPS hosting provider. If you haven’t installed it, go to the Node.js website to do so.

With Node.js, you can write server-side code using a special form of JavaScript so you can use an already familiar language.

The Node.js installer comes bundled with the package manager NPM. NPM is a repository for Node Modules, reusable pieces of code that can extend the functionality of your server. It’s similar to a plugin repository, and Node Modules can be thought of as code snippets or libraries (depending on how large they are).

Windows Users: Need to add Node and NPM to their PATH so they can call them easily on the command line. For more in-depth instructions, see my guide here.

Testing the Install

To test that the installation has worked correctly, open a terminal window, and type node -v and npm -v. If the resulting message starts with a v and is followed by some numbers (indicating a version), then the installation has been successful. Now you’re ready to create your first server.

Creating Your First Server

Once you have created a static website, the first step in creating a Node.js app is to create an Express web server.

First, move all your website’s static files (HTML, CSS, JS, images, etc.) into a folder called public and create a file called server.js in the root directory of your website folder. In the server.js file type:

// Load Node modules
var express = require('express');
// Initialise Express
var app = express();
// Render static files
app.use(express.static('public'));
// Port website will run on
app.listen(8080);

Then in the terminal, type: npm init. Press enter to accept the default parameters for all the following options, but make sure the entry point is server.js.

Finally, type: npm start and then go to the IP Address of your VPS host, or localhost:8080/index.html (or the name of one of your webpages) in the browser. The Express server you just created should now be serving your website’s static files.

Next Steps

Moving forward, we will discuss how to convert your static files to dynamic ones using the EJS templating engine. Then we’ll look at how to copy repeated code using partials and inject server-side variables to the front-end.

Templating Basics

Installing EJS

The first step to use EJS is to install it. A simple npm install ejs --save will do the trick. The --save parameter saves the module to the package.json file.

This makes it so anyone who clones the git repo (or otherwise downloads the site’s files) can install all the required Node modules for the project (called dependencies) using the npm install command instead. Then they don’t have to type npm install (module name) for however many modules they need.

Converting Static Pages to EJS Files

Next, you need to convert your static HTML files into dynamic EJS ones and set up your folder structure in the way EJS expects.

In the root directory of your website, create a folder called views. Inside that folder create two sub-folders called pages and partials. Move all your HTML files into the pages sub-folder and rename the .html file extensions to .ejs.

Your folder structure should look similar to the picture below.

nodejs-file-structure

Reusing Code — Creating Your First EJS Partial

When creating static sites, there’s often code that you repeat on every page such as the head (where the meta tags are located), header, and footer sections.

It’s inconvenient to change them on every page (especially on larger sites) if alterations are needed. But if you use EJS partials then you won’t have to. Editing one template (partial) file will update the code on every page that the file is included in.

We’ll take a typical part of a website to be templated, the header, as an example. Create a new file called header.ejs in the partials folder. Copy and paste all the code between the <header></header> tags on one of your EJS pages into it.

Finally, on all pages with a header delete the code between the <header></header> tags (the same code you copied to the header.ejs partial file) and replace it with <% include('../partials/header') %>. Now, you’ve created your first EJS partial. Repeat the process for any other repetitive pieces of code such as the head and footer sections.

Small Tip: If you find it hard to differentiate between your pages and partials since they have the same .ejs file extension, it can be helpful to put an underscore _ in front of the names of partials (so _ header.ejs). This is a naming convention that some developers use that can be helpful.

Rendering EJS Pages

Now we get to the exciting part: making the server render the EJS pages and partials so you can see them on the front-end.

server.js Example

// Load Node modules
var express = require('express');
const ejs = require('ejs');
// Initialise Express
var app = express();
// Render static files
app.use(express.static('public'));
// Set the view engine to ejs
app.set('view engine', 'ejs');
// Port website will run on
app.listen(8080);

// *** GET Routes - display pages ***
// Root Route
app.get('/', function (req, res) {
    res.render('pages/index');
});

First, we need to add the EJS Node module to our server. So, in the server.js file (see example above), add const ejs = require('ejs');.

Second, we need to tell our Express server to use EJS so add app.set('view engine', 'ejs');.

Now, we need to configure routes. Routes tell the server what to do when a user goes to a certain URL in your website such as http://testapp.com/login.

There are two types of routes, GET and POST. GET routes display pages and POST routes upload data from the front-end to the server (usually via a form) typically before a page is rendered and the uploaded data is somehow used.

Since we only want to display our EJS pages, we will just use GET routes. Add them after the app.listen(8080) line in server.js. For the index page, the route will be:

// *** GET Routes - display pages ***
// Root Route
app.get('/', function (req, res) {
    res.render('pages/index');
});

The ‘/’ specifies the URL of the website the code will activate on, the req stands for request and res for response. So, the response returned when going to http://testapp.com is rendering (displaying to the browser) the pages/index.ejs page. Add similar routes for your other EJS pages.

Passing Server-Side Data to the Frontend

The main attraction of templating, apart from reusing code, is that you can pass server-side variables to the front-end. Either a single variable like the current user’s username, or an array, like the details of every registered user.

However, the real strength of passing server-side variables becomes apparent when using APIs or databases.

For a basic example, the below code will display «Louise» in the h2 tag of the index page:

server.js

// Route Route
app.get('/', function (req, res) {
    var name = "Louise";
    // Render index page
    res.render('pages/index', {
        // EJS variable and server-side variable
        name: name
    });
});

The first name is the name of the EJS variable (the name for displaying it on the front-end), and the second is the variable that contains the data you want to send. (They don’t have to be identical.)

index.ejs

<h2>My name is <%= name %></h2>

For a simple array, you can use this example instead, which will create a p tag for every name in the listnames variable:

server.js

// Route Route
app.get('/', function (req, res) {
    var listnames = ["Louise", "Sadie", "Erik", "Raph", "Gina"];
    // Render index page
    res.render('pages/index', {
        // EJS variable and server-side variable
        listnames: listnames
    });
});

index.ejs

<% listnames.forEach(function(name) { %>
        <p><%= name %></p>
        <% }); %>

Congratulations. You’ve finished developing your first Node.js web app. In the next part, we will see how we can make it live (deploy it) on the web so you can show it off.

Deploying Your First Node.js Web App

There are many hosting platforms you can use to deploy your Node.js web apps such as Section, Heroku, Vultr, Linode, Google Cloud Platform and Amazon Web Services.

In this walk-through, we will be using DigitalOcean to deploy our Node.js app.

Setting up DigitalOcean

First, create an account on the DigitalOcean platform. There are discount codes available to add free credit to your account such as the code available in the Github Student Developer Pack. Be aware that you can only redeem one code per account.

Second, you need to create a droplet. A droplet is a VPS (Virtual Private Server.) It’s similar to a Linux VM which is hosted on a server farm somewhere.

Once you’ve logged into your account, go to droplets under the Manage heading and click create and then droplets.

You can leave most of the settings as the default but change the plan to the basic $5 a month which contains enough resources for your app. You can scale this up later if needed.

Also, choose the datacenter closest to the target audience of your app and change the authentication to password. While password authentication is less secure (SSH Keys is recommended), it’s much easier to set up. So, for demonstration purposes, we’ll use this method.

All that’s left now is to pick a name (hostname) and click Create Droplet.

Connecting to your Droplet

Shortly afterward, you’ll receive an email containing the username and password of your droplet which you’ll use to login.

Back on the DigitalOcean website, under droplets, click the name of your newly created droplet, and then click on Console. This will open a new tab that will let you control your droplet.

Alternatively, you can use any SSH client with the IP address and user credentials contained in the email.

On your first login, since you used password authentication, it will prompt you to set a new password. A great way to generate secure passwords and store them is a password manager like LastPass.

Deploying Your Node.js Web App

First, you’ll need to copy the code for your web app to your droplet. If you’re using source control such as Git, then it’s as simple as installing git using apt-get install git -y and then using the git clone command git clone (link to your repository), adding the link to your repository at the end.

Second, you’ll need to install Node. Type:

curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs

Third, you’ll need to navigate to the folder containing your web app. Type ls and then enter to view all the folders in your current working directory (location). This will look like the image below:

website-folders

Type cd and then the name of the folder that appears. Type ls again and you should see the files in your web app’s root directory.

Next, you’ll need to install the node modules (dependencies) for your web app. If you installed all your modules with -save at the end, which saves them to the package.json file, then just type npm install and press enter.

If not, when you run npm start an error will appear with module not found. Type npm install (module name) and press enter and then try running npm start again. Repeat the process until the error disappears.

If you need to install MongoDB (if you’ve created a MongoDB database), then follow these instructions.

Finally, type npm start to start your web app. Now that your web app is running, in a new browser tab, type the IP Address of your droplet (found in the email that DigitalOcean sent when you created the droplet) followed by a colon and the port your app runs on. For example, 167.172.54.51:8080.

If you’re using an Express web server (which if you followed my getting started with Node.js guide, you did), you’ll find the port number located in the app.listen() line inside the server.js file. For example, app.listen(8080) which is a common port used.

Congratulations, your first Node.js web app should be displayed in your web browser which is running on your DigitalOcean droplet.

Configuring Your Domain Name

You typed in an IP Address and port number to view your web app but, wouldn’t you prefer a custom domain name like yourapp.com?

Assuming you’ve already bought a domain, the first step is to add a DNS record so your domain name will resolve to the IP address of your DigitalOcean droplet. A DNS record tells your browser what to do when they load your domain. In this case, it should go to the IP address of your droplet.

If you’ve not bought a domain, domain registrars like Namecheap sell domain names and often other services such as email and static/CMS hosting, though there are benefits to going with a dedicated hosting and email provider.

Netlify offers hosting for static sites and SiteGround for CMS websites. Office365 and GSuite are the kings of custom email providers. See my guide for Setting Up a Professional Email to read a comparison of Office365 and GSuite.

advanced-dns

Login to your domain registrar and go to the advanced DNS settings of your domain. For example, on Namecheap, it’s the Advanced DNS tab on the Manage Domain screen.

dns-records

You want to add a new record as follows: the type should be set to A, the host should be either @ or blank (depending on your provider), and the value should be the IP Address of your droplet. Repeat the process for the host www which will do the same for the www version of your domain.

dns-check

It can take up to 24-48hrs for the changes to process, but it’s usually between 15 minutes to an hour. A quick way to check when it’s done is to go to DNSChecker. Type in your domain name and make sure the type is set to A. When the result comes back as the IP Address of your droplet, then you’ve connected your domain successfully.

The final test is to type your domain name followed by a colon and then the port number (e.g. yourdomain.com:8080). You should now see your web app loading.

Removing the Port Number from your URL

Now that you’ve got a cool domain name hooked up to your web app, you’ll probably want to remove that pesky port number.

We can do this by setting up what’s called a reverse proxy. A reverse proxy will tell your droplet when a user goes to yourdomain.com, it should serve the site at yourdomain.com:8080. We will use the popular reverse proxy Nginx to do so.

The first step is to install Nginx. Type the following to update your package list (so you can get the latest version) and install Nginx:

sudo apt-get update
sudo apt-get install nginx

Since DigitalOcean droplets are created with a firewall enabled, you’ll have to allow Nginx through it so it can work properly. sudo ufw allow 'Nginx Full' will do this.

To check the installation has gone smoothly, go to the http version of your domain name e.g. http://yourdomain.com. If you see a Welcome to Nginx landing page, then it’s been successful.

The second step is to secure your reverse proxy. Currently going to https://yourdomain.com won’t work. That’s because we haven’t configured SSL yet, and we need to install a package called Certbot to do so.

To install Certbot, type the following to ensure you get the latest version:

sudo add-apt-repository ppa:certbot/certbot
sudo apt-get install python-certbot-nginx

Next, you need to add your domain to Nginx so Certbot can generate a certificate to the correct domain. Open the configuration file using sudo nano /etc/nginx/sites-available/default and replace the underscores in the server_name line to your domain. For example, server_name yourdomain.com www.yourdomain.com;. Save the file and exit by typing CTRL+x, y and then enter.

To test that there are no errors in the file, type sudo nginx -t and if there’s none, type sudo systemctl reload nginx to reload Nginx so it will use the updated configuration.

Now we just need to generate the SSL certificate. sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com will start the process. You should choose option 2 for the redirect process because it will forward anyone trying to access the insecure version of your site (http) to the secure (https) version instead.

To test this, go to https://yourdomain.com and you should see the Nginx Welcome screen again.

Finally, we’re onto the last step, adding the Nginx configuration for your web app. For demonstration purposes, we’ll just modify the default one instead of creating a new one specifically for your web app. If you need to host several web apps on one droplet, you’d need to add a new configuration for each site.

Type: sudo nano /etc/nginx/sites-available/default to edit the default configuration file.

You need to change the server_name parameter to the name of your domain. For example: yourdomain.com. Under location /, proxy_pass should be changed to http://localhost:(port name). The ssl_certificate_key should be modified: /etc/letsencrypt/live/(domain name)/privkey.pem. Finally, add the code block below to the end of the file and then type CTRL+X, and then y to exit.

server {
    if ($host = auroraspotter.space) {
        return 301 https://$host$request_uri;
    } # managed by Certbot
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name auroraspotter.space;
    return 404; # managed by Certbot

Here’s a complete example of what it should look like. Note: the server_name should be the name of your domain.

server {
        root /var/www/html;      
        index index.html index.htm index.nginx-debian.html;
        server_name auroraspotter.space;
         
location / {
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-NginX-Proxy true;
       proxy_pass http://localhost:8080;
       proxy_set_header Host $http_host;
       proxy_cache_bypass $http_upgrade;
       proxy_redirect off;
 }
    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/auroraspotter.space/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/auroraspotter.space/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
    if ($host = auroraspotter.space) {
        return 301 https://$host$request_uri;
    } # managed by Certbot
    
        listen 80 default_server;
        listen [::]:80 default_server;
        
        server_name auroraspotter.space;
    return 404; # managed by Certbot

To test that there are no errors in the file, type sudo nginx -t. If there’s none, type sudo systemctl reload nginx to reload Nginx so it will use the updated configuration.

Finally, you should be able to go to yourdomain.com and your web app will be running.

Running the App on Boot (Setting up a Process Manager)

You’ve hooked your domain name up to your droplet and configured Nginx to serve your web app, but how do you keep it running all the time especially after restarting your droplet?

That’s where a process manager comes in. It will manage your Node.js web app, log any errors, and start/stop it as needed. We will be using the process manager called PM2.

The first step is to install PM2 using sudo npm install pm2@latest -g. Next, to run it on boot, run pm2 startup systemd. It should say to setup the startup script, copy and paste the following command which will be sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u (username) --hp /home/(username).

If you’re using the default login that DigitalOcean provided, this will be root. Type this into the terminal and press enter. If it says command successfully executed (like below) then it has worked.

[ 'systemctl enable pm2-root' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-root.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-root...
[PM2] [v] Command successfully executed.

Using the cd command, navigate to the folder of your web app. Then type pm2 start server.js. This will start the web app using pm2. Afterward, type pm2 save which will save it to be started on boot. If it says successfully saved, then it’s been saved correctly.

[PM2] Saving current process list...
[PM2] Successfully saved in /root/.pm2/dump.pm2

Finally, type sudo systemctl start pm2-(username).

Try restarting your droplet by typing reboot and after a few minutes, go to yourdomain.com. Your web app should be up and running like normal.

If you’re looking to build on the skills you’ve learned in this tutorial, I suggest using EJS templating to work with APIs and databases.



Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

How to Develop and Deploy Your First Full-Stack Web App Using A Static Site and Node.js

This tutorial will show you how to convert a static website that uses HTML, CSS and JavaScript (JS) to a dynamic one using MongoDB, Express, Static HTML, CSS, JS, and Node.js.

Our tech stack will be similar to the popular MEAN/MERN stack (MongoDB, Express, Angular or React, and NodeJS). But instead of using Angular or React, we will use a templating engine called EJS (Embedded JavaScript.)

Other popular templating engines include Handlebars, Pug, and Nunjucks.

Afterwards, we will deploy our Node.js web app to DigitalOcean and cover domain names, SSL, reverse proxies, and process managers.

Learning a templating language can be easier than a JS framework. You can just write HTML, and it lets you insert the same piece of code in multiple locations (called partials) or pass server-side variables to be displayed on the front-end (such as a username).

Table of Contents

  • Developing Your First Node.js Web App
    • Installing Node.js
    • Testing The Install
    • Creating Your First Server
    • Next Steps
    • Templating Basics
    • Passing Server-Side Data to the Front-End
  • Deploying Your First Node.js Web App
    • Setting Up DigitalOcean
    • Connecting To Your Droplet
    • Deploying Your Node.js Web App
    • Configuring Your Domain Name
    • Removing the Port Number From Your URL
    • Running the App on Boot (Setting Up A Process Manager)

Developing Your First Node.js Web App

Installing Node.js

First, make sure you’ve installed Node.js on your local machine or VPS hosting provider. If you haven’t installed it, go to the Node.js website to do so.

With Node.js, you can write server-side code using a special form of JavaScript so you can use an already familiar language.

The Node.js installer comes bundled with the package manager NPM. NPM is a repository for Node Modules, reusable pieces of code that can extend the functionality of your server. It’s similar to a plugin repository, and Node Modules can be thought of as code snippets or libraries (depending on how large they are).

Windows Users: Need to add Node and NPM to their PATH so they can call them easily on the command line. For more in-depth instructions, see my guide here.

Testing the Install

To test that the installation has worked correctly, open a terminal window, and type node -v and npm -v. If the resulting message starts with a v and is followed by some numbers (indicating a version), then the installation has been successful. Now you’re ready to create your first server.

Creating Your First Server

Once you have created a static website, the first step in creating a Node.js app is to create an Express web server.

First, move all your website’s static files (HTML, CSS, JS, images, etc.) into a folder called public and create a file called server.js in the root directory of your website folder. In the server.js file type:

// Load Node modules
var express = require('express');
// Initialise Express
var app = express();
// Render static files
app.use(express.static('public'));
// Port website will run on
app.listen(8080);

Then in the terminal, type: npm init. Press enter to accept the default parameters for all the following options, but make sure the entry point is server.js.

Finally, type: npm start and then go to the IP Address of your VPS host, or localhost:8080/index.html (or the name of one of your webpages) in the browser. The Express server you just created should now be serving your website’s static files.

Next Steps

Moving forward, we will discuss how to convert your static files to dynamic ones using the EJS templating engine. Then we’ll look at how to copy repeated code using partials and inject server-side variables to the front-end.

Templating Basics

Installing EJS

The first step to use EJS is to install it. A simple npm install ejs --save will do the trick. The --save parameter saves the module to the package.json file.

This makes it so anyone who clones the git repo (or otherwise downloads the site’s files) can install all the required Node modules for the project (called dependencies) using the npm install command instead. Then they don’t have to type npm install (module name) for however many modules they need.

Converting Static Pages to EJS Files

Next, you need to convert your static HTML files into dynamic EJS ones and set up your folder structure in the way EJS expects.

In the root directory of your website, create a folder called views. Inside that folder create two sub-folders called pages and partials. Move all your HTML files into the pages sub-folder and rename the .html file extensions to .ejs.

Your folder structure should look similar to the picture below.

nodejs-file-structure

Reusing Code — Creating Your First EJS Partial

When creating static sites, there’s often code that you repeat on every page such as the head (where the meta tags are located), header, and footer sections.

It’s inconvenient to change them on every page (especially on larger sites) if alterations are needed. But if you use EJS partials then you won’t have to. Editing one template (partial) file will update the code on every page that the file is included in.

We’ll take a typical part of a website to be templated, the header, as an example. Create a new file called header.ejs in the partials folder. Copy and paste all the code between the <header></header> tags on one of your EJS pages into it.

Finally, on all pages with a header delete the code between the <header></header> tags (the same code you copied to the header.ejs partial file) and replace it with <% include('../partials/header') %>. Now, you’ve created your first EJS partial. Repeat the process for any other repetitive pieces of code such as the head and footer sections.

Small Tip: If you find it hard to differentiate between your pages and partials since they have the same .ejs file extension, it can be helpful to put an underscore _ in front of the names of partials (so _ header.ejs). This is a naming convention that some developers use that can be helpful.

Rendering EJS Pages

Now we get to the exciting part: making the server render the EJS pages and partials so you can see them on the front-end.

server.js Example

// Load Node modules
var express = require('express');
const ejs = require('ejs');
// Initialise Express
var app = express();
// Render static files
app.use(express.static('public'));
// Set the view engine to ejs
app.set('view engine', 'ejs');
// Port website will run on
app.listen(8080);

// *** GET Routes - display pages ***
// Root Route
app.get('/', function (req, res) {
    res.render('pages/index');
});

First, we need to add the EJS Node module to our server. So, in the server.js file (see example above), add const ejs = require('ejs');.

Second, we need to tell our Express server to use EJS so add app.set('view engine', 'ejs');.

Now, we need to configure routes. Routes tell the server what to do when a user goes to a certain URL in your website such as http://testapp.com/login.

There are two types of routes, GET and POST. GET routes display pages and POST routes upload data from the front-end to the server (usually via a form) typically before a page is rendered and the uploaded data is somehow used.

Since we only want to display our EJS pages, we will just use GET routes. Add them after the app.listen(8080) line in server.js. For the index page, the route will be:

// *** GET Routes - display pages ***
// Root Route
app.get('/', function (req, res) {
    res.render('pages/index');
});

The ‘/’ specifies the URL of the website the code will activate on, the req stands for request and res for response. So, the response returned when going to http://testapp.com is rendering (displaying to the browser) the pages/index.ejs page. Add similar routes for your other EJS pages.

Passing Server-Side Data to the Frontend

The main attraction of templating, apart from reusing code, is that you can pass server-side variables to the front-end. Either a single variable like the current user’s username, or an array, like the details of every registered user.

However, the real strength of passing server-side variables becomes apparent when using APIs or databases.

For a basic example, the below code will display «Louise» in the h2 tag of the index page:

server.js

// Route Route
app.get('/', function (req, res) {
    var name = "Louise";
    // Render index page
    res.render('pages/index', {
        // EJS variable and server-side variable
        name: name
    });
});

The first name is the name of the EJS variable (the name for displaying it on the front-end), and the second is the variable that contains the data you want to send. (They don’t have to be identical.)

index.ejs

<h2>My name is <%= name %></h2>

For a simple array, you can use this example instead, which will create a p tag for every name in the listnames variable:

server.js

// Route Route
app.get('/', function (req, res) {
    var listnames = ["Louise", "Sadie", "Erik", "Raph", "Gina"];
    // Render index page
    res.render('pages/index', {
        // EJS variable and server-side variable
        listnames: listnames
    });
});

index.ejs

<% listnames.forEach(function(name) { %>
        <p><%= name %></p>
        <% }); %>

Congratulations. You’ve finished developing your first Node.js web app. In the next part, we will see how we can make it live (deploy it) on the web so you can show it off.

Deploying Your First Node.js Web App

There are many hosting platforms you can use to deploy your Node.js web apps such as Section, Heroku, Vultr, Linode, Google Cloud Platform and Amazon Web Services.

In this walk-through, we will be using DigitalOcean to deploy our Node.js app.

Setting up DigitalOcean

First, create an account on the DigitalOcean platform. There are discount codes available to add free credit to your account such as the code available in the Github Student Developer Pack. Be aware that you can only redeem one code per account.

Second, you need to create a droplet. A droplet is a VPS (Virtual Private Server.) It’s similar to a Linux VM which is hosted on a server farm somewhere.

Once you’ve logged into your account, go to droplets under the Manage heading and click create and then droplets.

You can leave most of the settings as the default but change the plan to the basic $5 a month which contains enough resources for your app. You can scale this up later if needed.

Also, choose the datacenter closest to the target audience of your app and change the authentication to password. While password authentication is less secure (SSH Keys is recommended), it’s much easier to set up. So, for demonstration purposes, we’ll use this method.

All that’s left now is to pick a name (hostname) and click Create Droplet.

Connecting to your Droplet

Shortly afterward, you’ll receive an email containing the username and password of your droplet which you’ll use to login.

Back on the DigitalOcean website, under droplets, click the name of your newly created droplet, and then click on Console. This will open a new tab that will let you control your droplet.

Alternatively, you can use any SSH client with the IP address and user credentials contained in the email.

On your first login, since you used password authentication, it will prompt you to set a new password. A great way to generate secure passwords and store them is a password manager like LastPass.

Deploying Your Node.js Web App

First, you’ll need to copy the code for your web app to your droplet. If you’re using source control such as Git, then it’s as simple as installing git using apt-get install git -y and then using the git clone command git clone (link to your repository), adding the link to your repository at the end.

Second, you’ll need to install Node. Type:

curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs

Third, you’ll need to navigate to the folder containing your web app. Type ls and then enter to view all the folders in your current working directory (location). This will look like the image below:

website-folders

Type cd and then the name of the folder that appears. Type ls again and you should see the files in your web app’s root directory.

Next, you’ll need to install the node modules (dependencies) for your web app. If you installed all your modules with -save at the end, which saves them to the package.json file, then just type npm install and press enter.

If not, when you run npm start an error will appear with module not found. Type npm install (module name) and press enter and then try running npm start again. Repeat the process until the error disappears.

If you need to install MongoDB (if you’ve created a MongoDB database), then follow these instructions.

Finally, type npm start to start your web app. Now that your web app is running, in a new browser tab, type the IP Address of your droplet (found in the email that DigitalOcean sent when you created the droplet) followed by a colon and the port your app runs on. For example, 167.172.54.51:8080.

If you’re using an Express web server (which if you followed my getting started with Node.js guide, you did), you’ll find the port number located in the app.listen() line inside the server.js file. For example, app.listen(8080) which is a common port used.

Congratulations, your first Node.js web app should be displayed in your web browser which is running on your DigitalOcean droplet.

Configuring Your Domain Name

You typed in an IP Address and port number to view your web app but, wouldn’t you prefer a custom domain name like yourapp.com?

Assuming you’ve already bought a domain, the first step is to add a DNS record so your domain name will resolve to the IP address of your DigitalOcean droplet. A DNS record tells your browser what to do when they load your domain. In this case, it should go to the IP address of your droplet.

If you’ve not bought a domain, domain registrars like Namecheap sell domain names and often other services such as email and static/CMS hosting, though there are benefits to going with a dedicated hosting and email provider.

Netlify offers hosting for static sites and SiteGround for CMS websites. Office365 and GSuite are the kings of custom email providers. See my guide for Setting Up a Professional Email to read a comparison of Office365 and GSuite.

advanced-dns

Login to your domain registrar and go to the advanced DNS settings of your domain. For example, on Namecheap, it’s the Advanced DNS tab on the Manage Domain screen.

dns-records

You want to add a new record as follows: the type should be set to A, the host should be either @ or blank (depending on your provider), and the value should be the IP Address of your droplet. Repeat the process for the host www which will do the same for the www version of your domain.

dns-check

It can take up to 24-48hrs for the changes to process, but it’s usually between 15 minutes to an hour. A quick way to check when it’s done is to go to DNSChecker. Type in your domain name and make sure the type is set to A. When the result comes back as the IP Address of your droplet, then you’ve connected your domain successfully.

The final test is to type your domain name followed by a colon and then the port number (e.g. yourdomain.com:8080). You should now see your web app loading.

Removing the Port Number from your URL

Now that you’ve got a cool domain name hooked up to your web app, you’ll probably want to remove that pesky port number.

We can do this by setting up what’s called a reverse proxy. A reverse proxy will tell your droplet when a user goes to yourdomain.com, it should serve the site at yourdomain.com:8080. We will use the popular reverse proxy Nginx to do so.

The first step is to install Nginx. Type the following to update your package list (so you can get the latest version) and install Nginx:

sudo apt-get update
sudo apt-get install nginx

Since DigitalOcean droplets are created with a firewall enabled, you’ll have to allow Nginx through it so it can work properly. sudo ufw allow 'Nginx Full' will do this.

To check the installation has gone smoothly, go to the http version of your domain name e.g. http://yourdomain.com. If you see a Welcome to Nginx landing page, then it’s been successful.

The second step is to secure your reverse proxy. Currently going to https://yourdomain.com won’t work. That’s because we haven’t configured SSL yet, and we need to install a package called Certbot to do so.

To install Certbot, type the following to ensure you get the latest version:

sudo add-apt-repository ppa:certbot/certbot
sudo apt-get install python-certbot-nginx

Next, you need to add your domain to Nginx so Certbot can generate a certificate to the correct domain. Open the configuration file using sudo nano /etc/nginx/sites-available/default and replace the underscores in the server_name line to your domain. For example, server_name yourdomain.com www.yourdomain.com;. Save the file and exit by typing CTRL+x, y and then enter.

To test that there are no errors in the file, type sudo nginx -t and if there’s none, type sudo systemctl reload nginx to reload Nginx so it will use the updated configuration.

Now we just need to generate the SSL certificate. sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com will start the process. You should choose option 2 for the redirect process because it will forward anyone trying to access the insecure version of your site (http) to the secure (https) version instead.

To test this, go to https://yourdomain.com and you should see the Nginx Welcome screen again.

Finally, we’re onto the last step, adding the Nginx configuration for your web app. For demonstration purposes, we’ll just modify the default one instead of creating a new one specifically for your web app. If you need to host several web apps on one droplet, you’d need to add a new configuration for each site.

Type: sudo nano /etc/nginx/sites-available/default to edit the default configuration file.

You need to change the server_name parameter to the name of your domain. For example: yourdomain.com. Under location /, proxy_pass should be changed to http://localhost:(port name). The ssl_certificate_key should be modified: /etc/letsencrypt/live/(domain name)/privkey.pem. Finally, add the code block below to the end of the file and then type CTRL+X, and then y to exit.

server {
    if ($host = auroraspotter.space) {
        return 301 https://$host$request_uri;
    } # managed by Certbot
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name auroraspotter.space;
    return 404; # managed by Certbot

Here’s a complete example of what it should look like. Note: the server_name should be the name of your domain.

server {
        root /var/www/html;      
        index index.html index.htm index.nginx-debian.html;
        server_name auroraspotter.space;
         
location / {
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-NginX-Proxy true;
       proxy_pass http://localhost:8080;
       proxy_set_header Host $http_host;
       proxy_cache_bypass $http_upgrade;
       proxy_redirect off;
 }
    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/auroraspotter.space/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/auroraspotter.space/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
    if ($host = auroraspotter.space) {
        return 301 https://$host$request_uri;
    } # managed by Certbot
    
        listen 80 default_server;
        listen [::]:80 default_server;
        
        server_name auroraspotter.space;
    return 404; # managed by Certbot

To test that there are no errors in the file, type sudo nginx -t. If there’s none, type sudo systemctl reload nginx to reload Nginx so it will use the updated configuration.

Finally, you should be able to go to yourdomain.com and your web app will be running.

Running the App on Boot (Setting up a Process Manager)

You’ve hooked your domain name up to your droplet and configured Nginx to serve your web app, but how do you keep it running all the time especially after restarting your droplet?

That’s where a process manager comes in. It will manage your Node.js web app, log any errors, and start/stop it as needed. We will be using the process manager called PM2.

The first step is to install PM2 using sudo npm install pm2@latest -g. Next, to run it on boot, run pm2 startup systemd. It should say to setup the startup script, copy and paste the following command which will be sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u (username) --hp /home/(username).

If you’re using the default login that DigitalOcean provided, this will be root. Type this into the terminal and press enter. If it says command successfully executed (like below) then it has worked.

[ 'systemctl enable pm2-root' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-root.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-root...
[PM2] [v] Command successfully executed.

Using the cd command, navigate to the folder of your web app. Then type pm2 start server.js. This will start the web app using pm2. Afterward, type pm2 save which will save it to be started on boot. If it says successfully saved, then it’s been saved correctly.

[PM2] Saving current process list...
[PM2] Successfully saved in /root/.pm2/dump.pm2

Finally, type sudo systemctl start pm2-(username).

Try restarting your droplet by typing reboot and after a few minutes, go to yourdomain.com. Your web app should be up and running like normal.

If you’re looking to build on the skills you’ve learned in this tutorial, I suggest using EJS templating to work with APIs and databases.



Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

#Руководства

  • 22 янв 2020

  • 16

Одностраничные приложения уже давно не в новинку, а скоро вообще станут стандартом веб-разработки. Узнайте, как их создавать, — пока не поздно.

 vlada_maestro / shutterstock

Евгений Кучерявый

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

SPA (англ. Single Page Application — одностраничное приложение) — это сайт, для работы которого не требуется обновление страницы, потому что все данные загружаются с помощью скриптов.

Принцип работы SPA прост: когда вы совершаете какое-то действие, например нажимаете на ссылку, скрипт перехватывает это событие. Он отменяет действие по умолчанию и вместо этого сам обменивается данными с сервером, а потом выводит их на странице.

Обычно такой сайт быстрее загружается, потому что ему нужно скачать только определённые данные, а не всю страницу. Также часть контента может быть закеширована, то есть загружена заранее. В этом случае обновление может произойти мгновенно.

Сайты-приложения, или SPA, совместимы с большим количеством устройств. Они быстро открываются и работают на стационарных ПК, ноутбуках, планшетах, смартфонах. Мы получаем универсальный инструмент веб-разработки.

SPA отлично подходит для интернет-магазинов: пользователь может нажать на кнопку «Добавить в корзину» и тут же продолжить смотреть другие товары. Но и для обычных блогов такая технология вполне уместна.

Создать что-то подобное можно и за час. Вы можете посмотреть репозиторий с полным кодом приложения.

Чтобы SPA было удобно пользоваться, нужно правильно выстроить обмен данными с сервером. Вот несколько рекомендаций.

  • При обновлении контента должен меняться адрес страницы в браузере.
  • Важно, чтобы любую внутреннюю ссылку на сайте можно было скопировать, а при переходе должна загружаться релевантная страница.
  • Если страница не загружается слишком долго, нужно сообщить пользователю, что идёт загрузка или что произошла ошибка.

Это небольшой список, но он очень важен, потому что иначе ваше SPA станет менее удобным, чем обычный сайт.

Для начала нужно создать макет сайта:

<!DOCTYPE html>
<html>
    <head>
   	 <title>My sick SPA</title>
   	 <meta charset="utf-8">
   	 <link rel="stylesheet" type="text/css" href="/spa/styles/app.css">
    </head>
    <body>
   	 <div class="wrapper">
   		 <header class="header">
   			 <div class="header__content">
   				 <h1 class="header__title" id="title">Main</div>
   			 </div>
   		 </header>
   		 <main class="main">
   			 <div class="main__content" id="body">
   				 <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. </p>

   				 <a href="/Articles/1" class="link link_internal">Article 1</a><br>
   				 <a href="/Articles/2" class="link link_internal">Article 2</a><br>
   				 <a href="/Articles/3" class="link link_internal">Article 3</a><br>
   				 <a href="/Articles/4" class="link link_internal">Article 4</a><br>
   			 </div>
   		 </main>
   	 </div>
   	 <script type="text/javascript" src="/spa/scripts/app.js"></script>
    </body>
</html>

Обратите внимание на ссылки с классом link_internal — они загружаются без обновления. Работу других ссылок менять не нужно.

В объектах с идентификаторами title и body контент будет меняться. Вы можете добавить панель навигации, корзину или любые другие функции, но простых ссылок достаточно, чтобы объяснить принцип работы сайта.

Подключаем стили:

body, html
{
    width: 100%;
    height: 100%;
    padding: 0;
    margin: 0;
    font-size: 16px;
    font-family: arial;
    background: #f7f7f7;
    color: #373737;
}

.wrapper
{
    width: 100%;
}

.header
{
    background: #373737;
}

.header__content
{
    padding: 5px;
}

.header__title
{
    display: block;
    padding: 5px;
    margin: 5px;
    color: #f7f7f7;
}

.main
{
    width: 90%;
    margin: 10px auto;
}

.link, .link:visited
{
    color: #111;
}

.link:hover
{
    text-decoration: none;
}

Получается так:

Серверная часть будет представлять собой PHP-скрипт, который получает запрос от клиента, обрабатывает его и возвращает какие-то данные в формате JSON. В нашем случае в клиентском запросе содержится раздел (Main или Articles) и идентификатор страницы.

Создадим файл core.php:

<?php

//Указываем в заголовках, что страница возвращает данные в формате JSON
header("Content-type: application/json; charset=utf-8");

//Создаём класс, который будем возвращать. В нём всего два свойства - заголовок и тело страницы
class Page
{
    public $title;
    public $body;

    public function __construct($title, $body)
    {
   	 $this->title = $title;
   	 $this->body = $body;
    }
}

$articles = //Создаём статьи
[
    new Page("Article 1", "<p>asdas 1</p> <a href='/Main' class='link link_internal'>Return to main page</a>"),
    new Page("Article 2", "<p>asdas 2</p> <a href='/Main' class='link link_internal'>Return to main page</a>"),
    new Page("Article 3", "<p>asdas 3</p> <a href='/Main' class='link link_internal'>Return to main page</a>"),
    new Page("Article 4", "<p>asdas 4</p> <a href='/Main' class='link link_internal'>Return to main page</a>")
];

//Получаем запрос от клиента
if(isset($_GET["page"]))
{
    $page = $_GET["page"];
}
else
{
    $page = "404";
}

if(isset($_GET["id"]))
{
    $id = $_GET["id"];
}
else
{
    $id = 0;
}

//Если никакая страница не подойдёт под запрос, то пользователь увидит сообщение, что страница не найдена
$response = new Page("404", "<p>Page not found</p> <a href='/Main' class='link link_internal'>Return to main page</a>");

switch($page) //Выбираем страницы
{
    case "main": //Главная
   	 $response = new Page("Main", "<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. </p> <a href='/Articles/1' class='link link_internal'>Article 1</a><br><a href='/Articles/2' class='link link_internal'>Article 2</a><br><a href='/Articles/3' class='link link_internal'>Article 3</a><br><a href='/Articles/4' class='link link_internal'>Article 4</a><br>");
   	 break;

    case "articles": //Статьи
   	 if($id > 0)
   	 {
   		 if(isset($articles[$id - 1]))
   		 {
   			 $response= $articles[$id - 1];
   		 }
   	 }
   	 break;
}

die(json_encode($response)); //Возвращаем страницу
?>

Получился небольшой скрипт, которого достаточно, чтобы проверить работу приложения. Позже этот скрипт можно будет усовершенствовать — например, добавить работу с базой данных.

Можно открыть скрипт и проверить его работу:

Серверная часть может быть написана на любом языке — главное, чтобы она возвращала данные в формате JSON.

Клиентский скрипт будет перехватывать нажатия по внутренним ссылкам, а потом получать из них параметры и отправлять их на сервер.

Ссылки вида

должны преобразовываться в следующий запрос:

Но чтобы они корректно открывались, нужно создать .htaccess-файл и добавить переадресацию для ошибки 404:

ErrorDocument 404 /spa/index.html

Теперь можно заняться самим скриптом:

var links = null; //Создаём переменную, в которой будут храниться ссылки

var loaded = true; //Переменная, которая обозначает, загрузилась ли страница

var data = //Данные о странице
{
    title: "",
    body: "",
    link: ""
};

var page = //Элементы, текст в которых будет меняться
{
    title: document.getElementById("title"),
    body: document.getElementById("body")
};

//По умолчанию в макете содержится контент для главной страницы.
//Но если пользователь перейдёт по ссылке, которая ведёт на какую-нибудь статью, он увидит не то, что ожидает.
//Поэтому нужно проверить, на какой странице находится пользователь, и загрузить релевантные данные.
OnLoad();

function OnLoad()
{
    var link = window.location.pathname; //Ссылка страницы без домена

    //У меня сайт находится по ссылке http://localhost/spa, поэтому мне нужно обрезать часть с spa/
    var href = link.replace("spa/", "");

    LinkClick(href);
}

function InitLinks()
{
    links = document.getElementsByClassName("link_internal"); //Находим все ссылки на странице

    for (var i = 0; i < links.length; i++)
    {
   	 //Отключаем событие по умолчанию и вызываем функцию LinkClick
   	 links[i].addEventListener("click", function (e)
   	 {
   		 e.preventDefault();
   		 LinkClick(e.target.getAttribute("href"));  
   		 return false;
   	 });
    }
}

function LinkClick(href)
{
    var props = href.split("/"); //Получаем параметры из ссылки. 1 - раздел, 2 - идентификатор

    switch(props[1])
    {
   	 case "Main":
   		 SendRequest("?page=main", href); //Отправляем запрос на сервер
   		 break;

   	 case "Articles":
   		 if(props.length == 3 && !isNaN(props[2]) && Number(props[2]) > 0) //Проверяем валидность идентификатора и тоже отправляем запрос
   		 {
   			 SendRequest("?page=articles&id=" + props[2], href);
   		 }
   		 break;
    }
}

function SendRequest(query, link)
{
    var xhr = new XMLHttpRequest(); //Создаём объект для отправки запроса

    xhr.open("GET", "/spa/core.php" + query, true); //Открываем соединение

    xhr.onreadystatechange = function() //Указываем, что делать, когда будет получен ответ от сервера
    {
   	 if (xhr.readyState != 4) return; //Если это не тот ответ, который нам нужен, ничего не делаем

   	 //Иначе говорим, что сайт загрузился
   	 loaded = true;

   	 if (xhr.status == 200) //Если ошибок нет, то получаем данные
   	 {
   		 GetData(JSON.parse(xhr.responseText), link);
   	 }
   	 else //Иначе выводим сообщение об ошибке
   	 {
   		 alert("Loading error! Try again later.");
   		 console.log(xhr.status + ": " + xhr.statusText);
   	 }
    }

    loaded = false; //Говорим, что идёт загрузка

    //Устанавливаем таймер, который покажет сообщение о загрузке, если она не завершится через 2 секунды
    setTimeout(ShowLoading, 2000);
    xhr.send(); //Отправляем запрос
}

function GetData(response, link) //Получаем данные
{
    data =
    {
   	 title: response.title,
   	 body: response.body,
   	 link: link
    };

    UpdatePage(); //Обновляем контент на странице
}

function ShowLoading()
{
    if(!loaded) //Если страница ещё не загрузилась, то выводим сообщение о загрузке
    {
   	 page.body.innerHTML = "Loading...";
    }
}

function UpdatePage() //Обновление контента
{
    page.title.innerText = data.title;
    page.body.innerHTML = data.body;

    document.title = data.title;
    window.history.pushState(data.body, data.title, "/spa" + data.link); //Меняем ссылку

    InitLinks(); //Инициализируем новые ссылки
}

Теперь можно смотреть, как работает сайт:

Видно, как меняется URL в поле с адресом сайта и сам контент на странице. Это происходит практически мгновенно, потому что пока сайт находится на локальном сервере. Но, учитывая его простоту, скорость почти не уменьшится, даже если загрузить сайт в интернет.

SPA может казаться чем-то сложным, но нам хватило небольшого скрипта, чтобы заставить всё работать. Даже не пришлось никакие фреймворки использовать.

Учись бесплатно:
вебинары по программированию, маркетингу и дизайну.

Участвовать

Научитесь: Профессия Frontend-разработчик
Узнать больше

СКАЧАТЬ ИСХОДНЫЕ КОДЫ

В данной статье мы создадим полноценный сайт с контрольной панелью для управления контентом. Как вы можете догадаться, в итоговой версии проекта будет очень много различных файлов. Я писал данный мануал шаг за шагом, параллельно занимаясь разработкой. Весь исходный код доступен на GitHub, и я рекомендую вам пользоваться им в процессе работы.

  • Введение
    • Настройка
    • FastDelivery
    • Конфигурация
    • Тестирование
    • База данных
    • MVC
    • Модель (Model)
    • Вид (View)
    • Контроллер (Controller)
    • Сайт FastDelivery
    • Контрольная панель
    • Защита административной панели
    • Управление контентом
    • Лицевая часть (Front-End)
    • Развертывание
  • Заключение
    • Исходный код

Express это один из лучших фреймворков для Node. У него отличная поддержка со стороны разработчиков и множество возможностей. По Express имеется много информации в интернете, раскрывающей азы работы с ним. В этой статье я хочу копнуть немного глубже основ и поделиться своим опытом создания полноценного веб-сайта.

В целом, речь пойдет не только об Express – в работе используются и другие хорошие инструменты, находящиеся в арсенале Node-разработчика.

Предполагается, что вы знакомы с Node.js, имеете установленную копию на своем компьютере и определенный опыт разработки.

Сердцем Express является Connect. Это связующий фреймворк, содержащий много полезных вещей. Если вы не понимаете что из себя представляет связующий фреймворк (middleware framework), то взгляните на небольшой пример:

var connect = require('connect'),
    http = require('http');

var app = connect()
    .use(function(req, res, next) {
        console.log("Это мой первый Middleware Framework");
        next();
    })
    .use(function(req, res, next) {
        console.log("Это мой второй Middleware Framework");
        next();
    })
    .use(function(req, res, next) {
        console.log("Конец");
        res.end("Hello World!");
    });
 
http.createServer(app).listen(3000);

Middleware (промежуточное ПО) это функция, которая принимает запросы от объектов и отвечает на них. Каждое middleware может ответить, используя соответствующий объект или передать управление следующему middleware, используя функцию next().

В примере выше, при удалении вызова метода next() во втором middleware, строка «Hello World!» никогда не будет передана браузеру. Так, в общих чертах, работает Express.

В составе фреймворка имеется несколько предопределенных middleware, что, несомненно, экономит время. Например, парсер Body, поддерживающий типы application/json, application/x-www-form-urlencoded и multipart/form-data, который обрабатывает тело запроса. Или парсер Cookie, обрабатывающий заголовки cookie и populatesreq.cookies с помощью объекта, ассоциированного с именем cookie.

Express дополняет Connect и добавляет в него новую функциональность, делающую разработку более удобной, например, функцию логики маршрутизации. Ниже дан пример управления запросом GET:

app.get('/hello.txt', function(req, res){
    var body = 'Hello World!';
    res.setHeader('Content-Type', 'text/plain');
    res.setHeader('Content-Length', body.length);
    res.end(body);
});

Есть два способа настройки Express. Первый – размещение файла package.json и запуск установки через пакетный менеджер npm.

{
    "name": "MyWebSite",
    "description": "My website",
    "version": "0.0.1",
    "dependencies": {
        "express": "3.x"
    }
}

Код фреймворка будет размещен папке node_modules, и вы сможете создать копию. Однако я предпочитаю альтернативный вариант – использование командной строки. Для этого нужно запустить команду npm install -g express. После чего, Express будет готов к работе. Для проверки запустите:

express --sessions --css less --hogan app

После чего Express создаст скелет предварительно сконфигурированного приложения. Вот список управляющих команд для команды express:

Пример использования: express [список параметров]

Параметры:

-h, —help вывод справки по параметрам;
-V, —version вывод номера версии;
-s, —sessions активация поддержки сессий;
-e, —ejs активация поддержки движка ejs (по умолчанию для Jade);
-J, —jshtml активация поддержки движка jshtml (по умолчанию для Jade);
-H, —hogan активация поддержки движка hogan.js;
-c, —css активация поддержки стилей (Less|Stylus) (по умолчанию для Plain CSS);
-f, —force принудительные непустые директории.

Как видите, команд не так уж и много, но этого хватает. Обычно я использую CSS-препроцессоры less и hogan для шаблонизации.

В нашем примере, нам также понадобится поддержка сессий, в чем поможет аргумент —sessions. Когда команда, приведенная в листинге выше, выполнится, структура папок нашего проекта будет такой:

/public
    /images
    /javascripts
    /stylesheets
/routes
    /index.js
    /user.js
/views
    /index.hjs
/app.js
/package.json

Если вы откроете файл package.json, то увидите, что все необходимые зависимости были добавлены, хотя они еще не были установлены. Чтобы сделать это, просто запустите установку через npm, и появится папка node_modules.

Я осознал, что метод, описанный выше, не всегда уместен. Возможно, вы захотите поместить свои обработчики маршрутов в другую папку или нужно будет сделать что-то еще. Но, как вы поймете из следующих нескольких глав, я внесу изменения в сгенерированные структуры, что достаточно легко сделать. Так что просто думайте о команде express как о шаблонизаторе.

Для этой статьи, я разработал простой сайт вымышленной компании FastDelivery. Вот скриншот, демонстрирующий законченный дизайн:

FastDelivery

В конце статьи, у нас получится полноценное веб-приложение, с контрольной панелью. Основная идея, заключается в том, чтобы каждой частью нашего сайта можно было управлять по отдельности.

Шаблон был создан в Photoshop и оформлен в файлах CSS (less) и HTML (hogan). Я не буду показывать процесс создания шаблона, так как это не относится к теме нашей статьи. После создания шаблона, структура файлов нашего проекта должна быть следующей:

/public
    /images (несколько изображений, экспортированных из Photoshop)
    /javascripts
    /stylesheets
        /home.less
        /inner.less
        /style.css
        /style.less (импорт home.less и inner.less)
/routes
    /index.js
/views
    /index.hjs (домашняя страница)
    /inner.hjs (шаблон всех страниц сайта)
/app.js
/package.json

Мы собираемся администрировать следующие элементы сайта:

  • Главная (баннер в центре – заголовок и текст);
  • Блог (добавление, удаление и редактирование статей);
  • Услуги;
  • Карьера;
  • Контакты.

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

Как известно, каждый скрипт node.js запускается в виде консольной программы. Поэтому мы легко можем указать аргументы, которые будут определять текущую среду. Я оформил этот код в виде отдельного модуля, чтобы позже протестировать. Вот содержание файла /config/index.js:

var config = {
    local: {
        mode: 'local',
        port: 3000
    },
    staging: {
        mode: 'staging',
        port: 4000
    },
    production: {
        mode: 'production',
        port: 5000
    }
}
module.exports = function(mode) {
    return config[mode || process.argv[2] || 'local'] || config.local;
}

Пока что тут у нас всего две настройки – mode и port. Как можно догадаться, наше приложение использует разные порты для разных серверов. По этой причине, нам нужно изменить точку входа на сайт в файле app.js.

...
var config = require('./config')();
...
http.createServer(app).listen(config.port, function(){
    console.log('Express server listening on port ' + config.port);
});

Для переключения между конфигурациями, просто добавьте тип сервера в конец команды. Например:

Вот что произойдет:

Express server listening on port 4000

Теперь, все наши настройки находятся в одном месте и ими легко управлять.

Я большой приверженец подхода test-driven development (разработка через тестирование). Я попытаюсь рассказать обо всех основных классах, используемых в данной статье. Разумеется, тестирование абсолютно всего сделает статью неимоверно большой, и по этой причине я этого делать не буду.

Но в целом, у вас должно сложиться понимание, как это делается при создании собственных приложений. Одним из моих самых любимых фреймворков для тестирования является jasmine. Он также доступен для установки через npm:

npm install -g jasmine-node

Давайте создадим папку, в которой будут располагаться наши тесты. Первое, что мы собираемся проверить, это наш скрипт с конфигурацией. Spec-файлы должны оканчиваться на .spec.js, поэтому наш мы назовем config.spec.js.

describe("Конфигурация для", function() {
    it("локального сервера", function(next) {
        var config = require('../config')();
        expect(config.mode).toBe('local');
        next();
    });
    it("тестового сервера", function(next) {
        var config = require('../config')('staging');
        expect(config.mode).toBe('staging');
        next();
    });
    it("производственного сервера", function(next) {
        var config = require('../config')('production');
        expect(config.mode).toBe('production');
        next();
    });
});

Запускаем jasmine-node ./tests и видим следующую картину:

Finished in 0.008 seconds
3 tests, 6 assertions, 0 failures, 0 skipped

Итак, я сначала создал конфигурацию, а потом провел её тест. Это не слишком близко к подходу «разработка через тестирование», но далее, я буду более строго его придерживаться.

Очень рекомендуется уделить достаточное время тестированию. Нет ничего лучше, чем хорошо протестированное приложение.

Пару лет назад я понял нечто важное, что может помочь вам создавать более совершенные программы. Каждый раз, когда вы начинаете писать новый класс, модуль или любой другой логически законченный кусок кода, спросите себя: Как я могу его протестировать?

Ответ на этот вопрос поможет вам писать код более эффективно, создавать более качественные API и грамотно располагать части программы по отдельным блокам. Вы не сможете написать тест для кода, запутанного как спагетти. Например, в конфигурационном файле выше (/config/index.js), я добавил возможность передавать режим в конструктор модуля.

Вы, возможно, будете удивлены, почему я сделал так, когда изначально предполагалось получать режим через аргументы командной строки. Все просто — мне надо провести тестирование.

Представим, что месяцем позже мне нужно будет проверить что-нибудь в конфигурации для производственного сервера, но скрипт node запускается с параметрами командной строки. Я вряд ли смогу вспомнить все настройки. Поэтому, отход от первоначальной идеи поможет в будущем избежать проблем.

После того, как мы создали динамический сайт, необходимо сохранить данные в базе. Для примера в данной статье, я буду использовать базу данных mongodb. Mongo это документо-ориентированная СУБД без поддержки SQL. Инструкции по её установке можно найти здесь.

Так как я использую Windows, то мне понадобятся инструкции по установке для Windows. После окончания установки, запустите демон MongoDB, который по-умолчанию слушает порт 27017. Теоретически, мы можем подключиться к этому порту и взаимодействовать с сервером mongodb.

Чтобы сделать это из node-скрипта, нам понадобится модуль/драйвер mongodb. Если вы скачаете исходные файлы к этой статье, то этот модуль в них уже включен в файле package.json. В противном случае, просто добавьте «mongodb»: «1.3.10» в список зависимостей и запустите установку через npm.

Далее, мы напишем тест, проверяющий запущен ли сервер mongodb, который будет располагаться в файле ./tests/mongodb.spec.js:

describe("MongoDB", function() {
    it("сервер запущен", function(next) {
        var MongoClient = require('mongodb').MongoClient;
        MongoClient.connect('mongodb://127.0.0.1:27017/fastdelivery', function(err, db) {
            expect(err).toBe(null);
            next();
        });
    });
});

Callback-вызов в методе .connect клиента mongodb, посылает объект db. Мы будем использовать его позже для управления нашими данными. Это означает, что мы должны получать доступ к этим данным внутри нашей модели.

Создавать новый объект MongoClient каждый раз, когда нам нужно сделать запрос к базе данных это не самая лучшая идея. Вот почему я переместил запуск сервера Express в callback-вызов внутрь функции connect:

MongoClient.connect('mongodb://127.0.0.1:27017/fastdelivery', function(err, db) {
    if(err) {
        console.log('Извините, но сервер mongo db не запущен.');
    } else {
        var attachDB = function(req, res, next) {
            req.db = db;
            next();
        };
        http.createServer(app).listen(config.port, function(){
            console.log('Сервер Express слушает порт' + config.port);
        });
    }
});

Даже лучше, что вместо параметров командной строки мы использовали конфигурационный файл. Мы поместили туда имя хоста и номер порта mongodb, а затем изменили URL-адрес в функции connect на:

'mongodb://' + config.mongo.host + ':' + config.mongo.port + '/fastdelivery'

Обратите особое внимание на middleware под названием attachDB, которое я добавил сразу после вызова функции http.createServer.

Спасибо выбору в пользу конфигурационного файла, благодаря которому мы можем заполнить свойство .db для запрашиваемого объекта. Хорошая новость в том, что мы можем присоединить несколько функций при определении маршрута.

Например:

app.get('/', attachDB, function(req, res, next) {
    ...
})

Этим кодом, Express заранее вызывает функцию attachDB, чтобы запустить наш обработчик маршрута. После того, как это случилось, объект request получит значение свойства .db, которое мы сможем использовать для доступа к базе данных.

Скорее всего, вы знакомы с MVC. Задача состоит в применении этой схемы к Express. Более или менее, это вопрос интерпретации. В следующих нескольких главах, я создам модули, которые будут взаимодействовать по схеме: модель-представление-контроллер.

Модель управляет данными в нашем приложении. Она должна иметь доступ к объекту db, который возвращается MongoClient. Наша модель также должна иметь метод для расширения этого объекта, потому что, возможно, мы захотим создать различные типы моделей.

Например, мы можем создать модель BlogModel или ContactsModel. Поэтому нужно создать новый spec-файл: /tests/base.model.spec.js, для тестирования двух этих будущих моделей. Помните, что определяя этот функционал ДО начала реализации в виде кода, мы гарантируем, что наш модуль будет делать только то, что от него ожидается.

var Model = require("../models/Base"),
    dbMockup = {};
describe("Модели", function() {
    it("должны создавать новые модели", function(next) {
        var model = new Model(dbMockup);
        expect(model.db).toBeDefined();
        expect(model.extend).toBeDefined();
        next();
    });
    it("быть расширяемыми", function(next) {
        var model = new Model(dbMockup);
        var OtherTypeOfModel = model.extend({
            myCustomModelMethod: function() { }
        });
        var model2 = new OtherTypeOfModel(dbMockup);
        expect(model2.db).toBeDefined();
        expect(model2.myCustomModelMethod).toBeDefined();
        next();
    })
});

Вместо реального объекта db, я решил передать объект mockup. Это сделано потому, что позже, я, возможно, захочу сделать специфический тест, в зависимости от информации, поступающей из базы данных. Будет проще определить эти данные вручную.

Реализация расширенного метода немного более хитрая, потому что нам нужно изменить прототип module.exports, но сохранить оригинальный конструктор. Благодаря тому, что мы ранее написали хороший тест, который подтверждает работоспособность нашего кода. Код, представленный выше, будет выглядеть так:

module.exports = function(db) {
    this.db = db;
};
module.exports.prototype = {
    extend: function(properties) {
        var Child = module.exports;
        Child.prototype = module.exports.prototype;
        for(var key in properties) {
            Child.prototype[key] = properties[key];
        }
        return Child;
    },
    setDB: function(db) {
        this.db = db;
    },
    collection: function() {
        if(this._collection) return this._collection;
        return this._collection = this.db.collection('fastdelivery-content');
    }
}

Вот два наших helper-метода: первый инициализирует объект db, а второй получает collection из базы данных.

Вид отображает информацию на экране. По сути, вид это класс, который посылает ответ браузеру. Express предоставляет удобный способ сделать это:

res.render('index', { title: 'Express' });

Объект response является оберткой (wrapper), которая имеет хороший API, делающий нашу жизнь проще. Однако я предпочитаю создать модуль, который будет инкапсулировать данный функционал. Сменим стандартную папку для видов на templates и создадим новый вид, который будет базовым классом (base view class).

Это маленькое изменение влечет за собой еще одно — нам нужно уведомить Express о том, что файлы нашего шаблона теперь размещены в другой папке:

app.set('views', __dirname + '/templates/');

Итак, для начала я определю то, что мне нужно, напишу тест, после чего, создам реализацию. Нам нужен модуль, который должен удовлетворять следующим требованиям:

  • Его конструктор должен получать объект response и имя шаблона;
  • Он должен иметь метод render, который выводит объект data;
  • Он должен быть расширяемым.

Вы возможно удивлены тем, что я расширил класс View. Не проще ли просто вызвать метод response.render? На практике возникают случаи, когда вам нужно послать другой заголовок или определенным образом манипулировать объектом response. Например, имеются такие данные JSON:

var data = {"developer": "Krasimir Tsonev"};
response.contentType('application/json');
response.send(JSON.stringify(data));

Вместо того чтобы делать это каждый раз, намного проще иметь классы HTMLView и JSONView. Или даже класс XMLView для отсылки XML-данных браузеру. Особенно это полезно, когда вы создаете большой сайт – у вас есть шаблоны и вам не нужно много раз копировать-вставлять один и тот же код.

Вот spec для /views/Base.js:

var View = require("../views/Base");
describe("Base view", function() {
    it("создает и отображает новый вид", function(next) {
        var responseMockup = {
            render: function(template, data) {
                expect(data.myProperty).toBe('value');
                expect(template).toBe('template-file');
                next();
            }
        }
        var v = new View(responseMockup, 'template-file');
        v.render({myProperty: 'value'});
    });
    it("должна быть расширяемой", function(next) {
        var v = new View();
        var OtherView = v.extend({
            render: function(data) {
                expect(data.prop).toBe('yes');
                next();
            }
        });
        var otherViewInstance = new OtherView();
        expect(otherViewInstance.render).toBeDefined();
        otherViewInstance.render({prop: 'yes'});
    });
});

Чтобы протестировать вывод, мне пришлось создать объект mockup. В данном случае, я создал объект, который имитирует Express’овский объект response. Во второй части теста, я создал другой класс View, который наследует модель Base и применяет кастомный метод render. Вот это класс — /views/Base.js.

module.exports = function(response, template) {
    this.response = response;
    this.template = template;
};
module.exports.prototype = {
    extend: function(properties) {
        var Child = module.exports;
        Child.prototype = module.exports.prototype;
        for(var key in properties) {
            Child.prototype[key] = properties[key];
        }
        return Child;
    },
    render: function(data) {
        if(this.response && this.template) {
            this.response.render(this.template, data);
        }
    }
}

Теперь у нас есть три spec’а в папке tests и, если мы запустим команду jasmine-node ./tests, результат будет следующим:

Finished in 0.009 seconds
7 tests, 18 assertions, 0 failures, 0 skipped

Помните маршруты (routes) и как мы их определили?

app.get('/', routes.index);

Символ ‘/’, в примере выше, это и есть контроллер. Это middleware-функция, которая принимает request, response и next.

exports.index = function(req, res, next) {
    res.render('index', { title: 'Express' });
};

В примере выше показано, как должен выглядеть ваш контроллер, в контексте Express. Команда express создает папку с именем routes, но в нашем случае, лучше назвать её controllers. Поэтому я переименовал её таким образом, чтобы отразить используемую нами схему MVC.

Так как создаваемое нами приложение является достаточно серьезным, будет мудрым решением создать класс base, который затем может быть расширен. Если нам когда-нибудь понадобиться передать некоторую функциональность другим контроллерам, то этот класс сослужит нам большую пользу. И снова, я сначала напишу тест, так что давайте определимся, что нам нужно от класса:

  • Он должен иметь метод nextend, который принимает объект и возвращает новый дочерний экземпляр;
  • Дочерний экземпляр должен иметь метод run, являющийся старой middleware-функцией;
  • Класс должен содержать в себе свойство name, которое идентифицирует контроллер;
  • Мы должны иметь возможность создавать независимые объекты, основанные на этом классе.

Не такие уж большие возможности, но ведь мы сможем добавить новую функциональность позже. Тест будет выглядеть следующим образом:

var BaseController = require("../controllers/Base");
describe("Base controller", function() {
    it("должен иметь метод extend, который возвращает дочерний экземпляр", function(next) {
        expect(BaseController.extend).toBeDefined();
        var child = BaseController.extend({ name: "my child controller" });
        expect(child.run).toBeDefined();
        expect(child.name).toBe("my child controller");
        next();
    });
    it("должен уметь создавать различные дочерние экземпляры", function(next) {
        var childA = BaseController.extend({ name: "child A", customProperty: 'value' });
        var childB = BaseController.extend({ name: "child B" });
        expect(childA.name).not.toBe(childB.name);
        expect(childB.customProperty).not.toBeDefined();
        next();
    });
});

А вот реализация /controllers/Base.js:

var _ = require("underscore");
module.exports = {
    name: "base",
    extend: function(child) {
        return _.extend({}, this, child);
    },
    run: function(req, res, next) {
 
    }
}

Разумеется, каждый дочерний класс должен определять собственный метод run, который имеет собственную логику.

Отлично, теперь у нас есть достаточный набор классов для реализации архитектуры MVC. Также мы создали тест к каждому модулю. Мы готовы продолжить создание сайта вымышленной компании FastDelivery. Представим, что сайт разделен на две части – лицевая (front-end) и административная (back-end).

Лицевая будет использоваться для отображения пользователям информации, имеющейся в базе данных. Административная панель же будет использоваться для управления этими данными. Начнем с административной (контрольной) панели.

Для начала, давайте создадим простой контроллер, который будет обслуживать административную страницу и находиться в файле ./controllers/Admin.js:

var BaseController = require("./Base"),
    View = require("../views/Base");
module.exports = BaseController.extend({ 
    name: "Admin",
    run: function(req, res, next) {
        var v = new View(res, 'admin');
        v.render({
            title: 'Административная панель',
            content: 'Добро пожаловать в административную панель'
        });
    }
});

Используя заранее написанные базовые классы для контроллеров и видов, мы легко можем создать точку входа в административную панель. Класс View принимает имя файла шаблона. Согласно коду, приведенному выше, файл должен быть назван admin.hjs и расположен в папке /templates/. Его содержимое должно быть следующим:

<!DOCTYPE html>
<html>
    <head>
        <title>{{ title }}</title>
        <link rel='stylesheet' href='/stylesheets/style.css' />
    </head>
    <body>
        <div class="container">
            <h1>{{ content }}</h1>
        </div>
    </body>
</html>

Чтобы статья не увеличивалась в объеме, я не буду выкладывать каждый отдельный шаблон вида. Вы можете просмотреть их исходный код на GitHub.

Чтобы сделать контроллер видимым, нам нужно добавить в него маршрут в файле app.js:

var Admin = require('./controllers/Admin');
...
var attachDB = function(req, res, next) {
    req.db = db;
    next();
};
...
app.all('/admin*', attachDB, function(req, res, next) {
    Admin.run(req, res, next);
});

Заметьте, что мы не посылаем метод Admin.run напрямую в middleware, чтобы не нарушить контекст. Если мы сделаем так:

app.all('/admin*', Admin.run);

То слово this в административном модуле будет вести в другое место.

Каждая страница, начинающаяся с /admin должна быть защищена. Для этого, нам нужно использовать middleware, встроенное в Express, под названием Sessions. Этот инструмент просто прикрепляет объект к запросу названному session. Теперь нам нужно изменить контроллер нашей административной панели таким образом, чтобы он делал две вещи:

  • Проверял, доступна ли сессия. Если нет, то отобразить форму логина;
  • Принимал данные, посланные через форму логина и авторизовывал пользователя при совпадении логина и пароля.

Вот небольшая helper-функция, которую мы можем использовать, чтобы реализовать это:

authorize: function(req) {
    return (
        req.session && 
        req.session.fastdelivery && 
        req.session.fastdelivery === true
    ) || (
        req.body &&
        req.body.username === this.username &&
        req.body.password === this.password
    );
}

В этом листинге сначала идет выражение, которое пробует распознать пользователя через объект session. Далее, мы проверяем, была ли отправлена форма. Если да, то данные из формы становятся доступны через объект request.body,который заполняется при помощи middleware bodyParser. Наконец, мы проверяем имя пользователя и пароль.

А теперь, реализуем метод контроллера run, который использует наш новый хелпер. Если пользователь авторизован, то отображаем административную панель, иначе – панель логина:

run: function(req, res, next) {
    if(this.authorize(req)) {
        req.session.fastdelivery = true;
        req.session.save(function(err) {
            var v = new View(res, 'admin');
            v.render({
                title: 'Административная панель',
                content: 'Добро пожаловать в административную панель'
            });
        });         
    } else {
        var v = new View(res, 'admin-login');
        v.render({
            title: 'Пожалуйста, представьтесь'
        });
    }       
}

Как я пояснил выше, у нас есть много объектов, которые нужно администрировать. Чтобы упростить этот процесс, давайте оставим все данные в одной коллекции. Каждая запись будет иметь название, произвольный текст, картинку и свойство type.

Свойство type будет определять владельца данной записи. Например, для страницы «Контакты» будет нужна только одна запись type: ‘contacts’, в то время как страница «Блог» потребует большее количество записей. Поэтому, нам нужно три новых страницы для добавления, редактирования и вывода записей.

Но перед тем как мы приступим непосредственно к реализации новых шаблонов, стилей и функционала контроллера, нам нужно написать класс нашей модели, которая располагается между сервером MongoDB и приложением, и предоставляет API.

// /models/ContentModel.js
 
var Model = require("./Base"),
    crypto = require("crypto"),
    model = new Model();
var ContentModel = model.extend({
    insert: function(data, callback) {
        data.ID = crypto.randomBytes(20).toString('hex'); 
        this.collection().insert(data, {}, callback || function(){ });
    },
    update: function(data, callback) {
        this.collection().update({ID: data.ID}, data, {}, callback || function(){ });   
    },
    getlist: function(callback, query) {
        this.collection().find(query || {}).toArray(callback);
    },
    remove: function(ID, callback) {
        this.collection().findAndModify({ID: ID}, [], {}, {remove: true}, callback);
    }
});
module.exports = ContentModel;

Модель берет на себя ответственность за генерацию уникального ID для каждой записи. Это потом понадобится нам для обновления информации.

Если мы хотим добавить новую запись на страницу «Контакты», то просто делаем следующее:

var model = new (require("../models/ContentModel"));
model.insert({
    title: "Контакты",
    text: "...",
    type: "contacts"
});

Итак, у нас есть отличный API для управления данными в нашей коллекции mongodb. Теперь мы готовы к созданию пользовательского интерфейса для имеющейся функциональности. Для этого нужно будет немного изменить контроллер административной панели.

Для упрощения задачи я решил совместить список добавленных записей и форму для их добавления/редактирования. Как вы можете увидеть на скриншоте ниже, левая часть страницы зарезервирована под список, а правая – под форму.

Управление контентом

Расположив все на одной странице, мы сможем сфокусироваться на реализации вывода страницы и других специфических вещах, например, данных пересылаемых в шаблон. По этой причине, я создал несколько helper-функций, объединенных между собой вот так:

var self = this;
...
var v = new View(res, 'admin');
self.del(req, function() {
    self.form(req, res, function(formMarkup) {
        self.list(function(listMarkup) {
            v.render({
                title: 'Административная панель',
                content: 'Добро пожаловать в административную панель',
                list: listMarkup,
                form: formMarkup
            });
        });
    });
});

Наша административная панель выглядит очень угловато, но работает именно так, как задумывалось. Первая helper-функция это метод del, который проверяет текущие параметры GET и, если находит строку action=delete&id=[id записи], то удаляет данные из коллекции.

Вторая функция называется form и отвечает в основном за отображение формы в правой части страницы. Она проверяет, отправлена ли форма, и обновляет/создает записи в базе данных. В конце, метод list выбирает информацию и подготавливает HTML-таблицу, которая будет послана шаблону. Реализацию трех этих хелперов можно найти здесь.

В статье же, я решил показать функцию, которая управляет загрузкой файла:

handleFileUpload: function(req) {
    if(!req.files || !req.files.picture || !req.files.picture.name) {
        return req.body.currentPicture || '';
    }
    var data = fs.readFileSync(req.files.picture.path);
    var fileName = req.files.picture.name;
    var uid = crypto.randomBytes(10).toString('hex');
    var dir = __dirname + "/../public/wp-content/uploads/" + uid;
    fs.mkdirSync(dir, '0777');
    fs.writeFileSync(dir + "/" + fileName, data);
    return '/wp-content/uploads/' + uid + "/" + fileName;
}

Если файл отправлен, то свойство .files объекта request заполняется данными. В нашем случае, у нас есть следующий HTML-элемент:

<input type="file" name="picture" />

Это значит, что мы можем получить доступ к отправленным данным через req.files.picture. В коде, приведенном выше, req.files.picture.path используется, чтобы получить необработанное содержимое файла.

Позже, в те же данные записывается новый каталог и в конце возвращается URL-адрес. Все эти операции синхронны, но очень полезно использовать асинхронные версии readFileSync, mkdirSync и writeFileSync.

Самая сложная часть работы выполнена. Административная панель работает и у нас есть класс ContentModel, который дает доступ к информации, сохраненной в базе данных. Теперь нам нужно реализовать контроллеры фронт-энда и привязать их к сохраненному содержимому.

Ниже представлен контроллер для домашней страницы — /controllers/Home.js:

module.exports = BaseController.extend({ 
    name: "Домашняя страница",
    content: null,
    run: function(req, res, next) {
        model.setDB(req.db);
        var self = this;
        this.getContent(function() {
            var v = new View(res, 'home');
            v.render(self.content);
        })
    },
    getContent: function(callback) {
        var self = this;
        this.content = {};
        model.getlist(function(err, records) {
            ... здесь идет сохранение данных в объект content
            model.getlist(function(err, records) {
                ... здесь идет сохранение данных в объект content 
                callback();
            }, { type: 'blog' });
        }, { type: 'home' });
    }
});

Домашняя страница требует одной записи типа home и четырех типа blog. После создания контроллера, нам нужно добавить маршрут в файл app.js:

app.all('/', attachDB, function(req, res, next) {
    Home.run(req, res, next);
});

И вновь, мы добавляем объект db к request. Это практически то же самое, что мы делали для административной панели.

Остальные страницы для нашего фронт-энда (клиентской части) идентичны: все они имеют контроллер, который извлекает данные с помощью класса модели и определяет маршрут. Есть пару моментов, которые я бы хотел пояснить. Первый касается страницы «Блог».

Она должна уметь отображать не только все статьи, но и каждую из них по отдельности. Поэтому, мы должны зарегистрировать два маршрута:

app.all('/blog/:id', attachDB, function(req, res, next) {
    Blog.runArticle(req, res, next);
}); 
app.all('/blog', attachDB, function(req, res, next) {
    Blog.run(req, res, next);
});

Обе функции используют один и тот же контроллер Blog, но вызывают метод run по-разному. Обратите внимание на строку /blog/:id. Этот маршрут будет совпадать с URL-адресами вида /blog/4e3455635b4a6f6dccfaa1e50ee71f1cde75222b, а длинная хеш-функция будет доступна через req.params.id. Другими словами, мы можем определить динамические параметры.

В данном случае, это ID записи. После получения этой информации, мы можем создать уникальную страницу для каждой статьи.

Вторым интересным моментом является то, каким образом я создал страницы «Услуги», «Карьера» и «Контакты». Ясно, что они используют только одну запись из базы данных. Если нам нужно создать разные контроллеры для каждой страницы, то необходимо скопировать/вставить тот же код и изменить поле type.

Это оптимальный способ, когда имеется только один контроллер, который принимает значение type в методе run. Итак, вот маршруты:

app.all('/services', attachDB, function(req, res, next) {
    Page.run('services', req, res, next);
}); 
app.all('/careers', attachDB, function(req, res, next) {
    Page.run('careers', req, res, next);
}); 
app.all('/contacts', attachDB, function(req, res, next) {
    Page.run('contacts', req, res, next);
});

А вот как будет выглядеть контроллер:

module.exports = BaseController.extend({ 
    name: "Page",
    content: null,
    run: function(type, req, res, next) {
        model.setDB(req.db);
        var self = this;
        this.getContent(type, function() {
            var v = new View(res, 'inner');
            v.render(self.content);
        });
    },
    getContent: function(type, callback) {
        var self = this;
        this.content = {}
        model.getlist(function(err, records) {
            if(records.length > 0) {
                self.content = records[0];
            }
            callback();
        }, { type: type });
    }
});

Процедура развертывания сайта на базе Express аналогична, развертыванию любого другого Node.js-приложения:

  • Перемещение файлов на сервер;
  • Остановка процесса node (если он запущен);
  • Запуск команды npm install для установки новых зависимостей;
  • Запуск node.

Надо понимать, что Node это достаточно молодая платформа, и не все может работать, как ожидается, но улучшения делаются постоянно. Например, CLI-инструмент forever гарантирует, что ваше Node.js-приложение будет запущено вечно. Это делается командой:

Я использую это на всех своих серверах. Это отличный инструмент, решающий множество проблем. Если вы запускаете свою программу с помощью node yourapp.js, то после неожиданного завершения её работы, сервер упадет. Forever, просто перезапускает приложение в этом случае.

Я не системный администратор, но у меня есть желание поделиться своим опытом интеграции node-приложений с Apache и Nginx, потому что я считаю, что это часть рабочего процесса и помогает развитию программного обеспечения в принципе.

Как вы знаете, Apache нормально работает на 80 порту, а это означает, что если вы перейдете по адресу http://localhost или http://localhost:80, то увидите страницу Apache-сервера. Чаще всего, ваш node-скрипт слушает другой порт.

Поэтому, вам нужно добавить виртуальный хост, который будет принимать запросы и направлять их на нужный порт. Представим, что я хочу расположить созданный нами сайт, на своем локальном Apache-сервере по адресу expresscompletewebsite.dev. Для этого, первым делом нужно добавить наш домен в файл hosts:

127.0.0.1   expresscompletewebsite.dev

После чего, надо отредактировать файл httpd-vhosts.conf, расположенный в папке с конфигурационными файлами Apache, добавив в него:

# expresscompletewebsite.dev
<VirtualHost *:80>
    ServerName expresscompletewebsite.dev
    ServerAlias www.expresscompletewebsite.dev
    ProxyRequests off
    <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>
    <Location />
        ProxyPass http://localhost:3000/
        ProxyPassReverse http://localhost:3000/
    </Location>
</VirtualHost>

Сервер все еще посылает запросы на порт 80, но перенаправляет их на порт 3000, где их слушает node.

Настройка Nginx проще и, честно говоря, он лучше подходит для Nodejs-приложений. Первым шагом все также нужно добавить наш домен в файл hosts. После чего, просто создайте новый файл в папке /sites-enabled в директории с установленным Nginx. Содержимое файла должно выглядеть следующим образом:

server {
    listen 80;
    server_name expresscompletewebsite.dev
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $http_host;
    }
}

Вы не сможете запустить Apache и Nginx с настройками hosts-файлов, приведенными выше, потому что они требуют порт 80. Также, если вы системный администратор, то, скорее всего, захотите поэкспериментировать с настройками для улучшения производительности. Но повторюсь, я не эксперт в этой области.

Express это замечательный фреймворк, который хорошо подходит для того, чтобы начать создавать свои веб-приложения. И, как вы могли заметить, результат будет зависеть от того, каким образом вы построите его, и что будете использовать при создании.

Express упрощает решение рутинных задач, он предоставляет отличное middleware и, в то же время, оставляет реализацию ответственных частей кода разработчику.

Исходные коды для данной статьи доступны на GitHub. Используйте их и экспериментируйте.

Вот краткая инструкция по запуску сайта:

  • Скачайте исходные коды;
  • Перейдите в папку app;
  • Запустите npm install;
  • Запустите демон mongodb;
  • Запустите команду node app.js.

Понравилась статья? Поделить с друзьями:
  • Как написать свой хук react
  • Как написать свой риф
  • Как написать свой хостинг
  • Как написать свой рендер
  • Как написать свой хоррор