Представляем

Пример Webpack-dev-server с библиотекой socket.io при разработке веб-сайта.

В этом примере при сборке проекта мы получим все пути к файлам из общедоступной папки и отобразим на сайте. В режиме разработки мы подключим библиотеку socket.io, запустим сервер на порту 3000 и в режиме реального времени будем получать/обновлять наш список файлов.

Для тех, кто хочет сразу увидеть первоисточник:

https://github.com/alexeykhr/webpack-dev-server-socket-io

Установка и подготовка

Создайте проект и инициализируйте npm:

$ mkdir webpack-dev-server-socket-io
$ cd ./webpack-dev-server-socket-io
$ npm init

Установите библиотеки npm:

Webpack:
$ npm i --save-dev webpack webpack-cli webpack-dev-server
Babel:
$ npm i --save-dev @babel/core
Webpack Plugins:
$ npm i --save-dev clean-webpack-plugin mini-css-extract-plugin html-webpack-plugin
Webpack Loaders:
$ npm i --save-dev css-loader babel-loader
Socket.io:
$ npm i --save-dev socket.io socket.io-client
Watcher:
$ npm i --save-dev chokidar

Добавьте 2 скрипта в package.json, чтобы запустить сервер в режиме разработки с помощью webpack-dev-server и для работы с использованием webpack:

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

Создайте webpack.config.js:

$ touch webpack.config.js

Добавьте базовый контент, чтобы начать проект:

'use strict'
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
  entry: [
    './src/index.js',
    './src/styles/index.css'
  ],
  output: {
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.js$/,
        use: [
          'babel-loader'
        ]
      }
    ]
  },
  plugins: [
    /**
     * Remove build folder before building.
     * @see https://github.com/johnagan/clean-webpack-plugin
     */
    new CleanWebpackPlugin(),
    /**
     * Simplifies creation of HTML files to serve your webpack bundles.
     * @see https://github.com/jantimon/html-webpack-plugin
     */
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './src/index.ejs'
    }),
    /**
     * This plugin extracts CSS into separate files.
     * It creates a CSS file per JS file which contains CSS.
     * @see https://github.com/webpack-contrib/mini-css-extract-plugin
     */
    new MiniCssExtractPlugin({
      chunkFilename: 'static/css/[name].[hash].css',
      filename: 'static/[name].[hash].css'
    })
  ]
}

В этом файле установлена ​​многоосновная запись (index.js, index.css), которую мы создадим позже. Также файл index.ejs прописан в HtmlWebpackPlugin, по умолчанию, если вы не указываете загрузчик, используйте загрузчик lodash:

https://github.com/jantimon/html-webpack-plugin/blob/master/docs/template-option.md#1-dont-set-any-loader

Создайте файл index.ejs для HtmlWebpackPlugin:

$ mkdir ./src
$ touch ./src/index.ejs

Добавьте базовый HTML-шаблон:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Webpack Dev Server with Socket.io</title>
</head>
<body>
  <h1>Files</h1>
</body>
</html>

Создайте файл index.css:

$ mkdir ./src/styles
$ touch ./src/styles/index.css

Добавьте несколько стилей:

h1 {
  margin: 20px;
  font-size: 2em;
  margin-bottom: 20px;
}
ul {
  margin: 0;
  padding: 0 20px;
  list-style-type: none;
}
li {
  margin-bottom: 20px;
}
.path {
  display: inline-block;
  background: #e8e8e8;
  padding: 5px 20px;
  border-radius: 5px;
}

Создайте файл index.js:

$ touch ./src/index.js

И просто выведите сообщение в консоль:

'use strict'
console.log('Execute index.js')

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

$ npm run dev

И откройте сайт: https://localhost:8080/

Список выходных файлов

Создайте папку core, в которой будут храниться все файлы, которые будут выполняться перед выполнением webpack:

$ mkdir ./core

Создайте файл fn.js, в котором будут храниться небольшие вспомогательные функции и переменные:

$ touch ./core/fn.js

И добавляем содержимое:

'use strict'
const path = require('path')
const fs = require('fs')
/**
 * List all files in a directory recursively in
 * a synchronous fashion
 * @param {string} dirPath
 * @return {IterableIterator<String>}
 */
const walkSync = function* (dirPath) {
  const files = fs.readdirSync(dirPath)
  for (const file of files) {
    const pathFile = path.join(dirPath, file)
    const isDirectory = fs.statSync(pathFile).isDirectory()
    if (isDirectory) {
      yield *walkSync(pathFile)
    } else {
      yield pathFile
    }
  }
}
module.exports = {
  /**
   * @type {string}
   */
  publicFolderPath: path.resolve(__dirname, '../public'),
  walkSync
}

Создайте файл files.js, который будет возвращать функцию, возвращающую список файлов в общедоступной папке:

$ touch ./core/files.js

Содержание:

'use strict'
const fn = require('./fn')
/**
 * List of files in a folder
 * @return {array} of file path
 */
module.exports = () => {
  const result = []
  for (const filePath of fn.walkSync(fn.publicFolderPath)) {
    result.push(filePath)
  }
  return result
}

Теперь нам нужно использовать эти данные в нашем шаблоне (.ejs). Подключаемый модуль HtmlWebpackPlugin поддерживает параметр templateParameters, который позволяет передавать параметры в шаблон. Давайте немного отредактируем файл webpack.config.js:

'use strict'
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const files = require('./core/files')
const path = require('path')
...
new HtmlWebpackPlugin({
  filename: 'index.html',
  template: './src/index.ejs',
  templateParameters: {
    files: files()
  }
})

Теперь мы можем получить доступ к переменной files в нашем шаблоне (index.ejs):

...
<body>
  <h1>Files</h1>
  <ul>
    <% files.forEach((filePath) => { %>
      <li>
        <div class="path"><%= filePath %></div>
      </li>
    <% }) %>
  </ul>
</body>
...

Теперь создайте папку public и поместите туда пару файлов, а затем запустите проект:

$ mkdir ./public
# Put some file to this folder
$ npm run dev

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

Подключение сервера Socket.io

Для начала создадим 2 файла, первый для socket.io, второй для прослушивания изменений в папке public.

$ touch ./core/io.js

Содержание:

'use strict'
const server = require('http').createServer()
const io = require('socket.io')(server)
const files = require('./files')
/**
 * List all connections
 * @type {string: SocketIO.Socket}
 */
const sockets = {}
io.on('connection', (socket) => {
  // Save the list of all connections to a variable
  sockets[socket.io] = socket
  
  // After connection - send a list of all files
  socket.emit('files', files())
  // When disconnect, delete the socket with the variable
  socket.on('disconnect', () => {
    delete sockets[socket.id]
  })
})
server.listen(3000)
module.exports = {
  sockets,
  server,
  io
}

Здесь мы создаем наш сервер, запускаем его на 3000порте. Теперь создайте файл watch.js для работы с событиями из файлов.

$ touch ./core/watch.js

Содержание:

'use strict'
const chokidar = require('chokidar')
const { sockets } = require('./io')
const fn = require('./fn')
/*
 * Listen to all the events that occur in the folder (recursively)
 */
chokidar.watch(fn.publicFolderPath, {
  ignored: /(^|[\/\\])\../
})
  .on('all', (evt, path, stats) => {
    // Send events to all active connections
    Object.values(sockets).forEach((socket) => {
      socket.emit('chokidar', {
        evt,
        path,
        stats
      })
    })
  })

После каждого изменения в этой папке мы отправляем события на веб-сокет всем подключенным клиентам. Теперь подключим наш сервер при работе в режиме разработки и изменим логику вывода файлов с помощью websocket.

webpack.config.js:

...
const path = require('path')
/**
 * Is build for Production
 * @type {boolean}
 */
const isProd = process.argv.includes('production')
/*
 * In development mode, we connect our own websocket server
 */
if (!isProd) {
  require('./core/watch')
}
...
new HtmlWebpackPlugin({
  filename: 'index.html',
  template: './src/index.ejs',
  templateParameters: {
    files: files(),
    isProd
  }
})

index.ejs:

<ul>
  <% if (isProd) { %>
    ...
  <% } %>
</ul>

Теперь мы заблокировали нормальный вывод данных при построении на продакшене.

index.js:

'use strict'
console.log('Execute index.js')
if (process.env.NODE_ENV === 'development') {
  /*
   * This code is available only in development mode
   */
  console.log('Only Development')
  require('./dev')
}

Теперь добавьте файл dev.js, который будет доступен только в режиме разработки, и этот код не попадет в производство:

$ touch ./src/dev.js

Содержание:

'use strict'
const io = require('socket.io-client')
const socket = io('localhost:3000')
// Initialization
let ulElement
window.addEventListener('load', () => {
  // Get list of files
  ulElement = document.querySelector('ul')
})
socket.on('connect', () => {
  // Events for a specific file/folder
  socket.on('chokidar', (payload) => {
    console.log('socket.chokidar', payload)
    action(payload.evt, payload.path, false)
  })
  // Get a list of all files, previously delete existing
  // elements to avoid duplicates
  socket.on('files', (files) => {
    console.log('socket.files', files)
    clearAllElements()
    files.forEach(filePath => action('add', filePath))
  })
})
/**
 * @param {string} evt - add, addDir, change, unlink, unlinkDir
 * @param {string} path
 * @param {boolean} appendLast
 */
function action(evt, path, appendLast = true) {
  if (!ulElement) {
    return
  }
  let liElement
  switch (evt) {
  case 'add':
    liElement = createListItemElement(path)
    if (appendLast) {
      ulElement.appendChild(liElement)
    } else {
      ulElement.insertBefore(liElement, ulElement.firstChild)
    }
    break
  case 'unlink':
    liElement = findElementByTextContent(path)
    if (liElement) {
      liElement.remove()
    }
    break
  }
}
/**
 * The list structure is identical to index.ejs
 * @param {string} path
 */
function createListItemElement(path) {
  return createElementFromHTML(`<li>
    <div class="path">${path}</div>
  </li>`)
}
/**
 * Create an element based on text
 * @param {string} htmlString
 * @return {Element}
 */
function createElementFromHTML(htmlString) {
  const div = document.createElement('div')
  div.innerHTML = htmlString.trim()
  return div.firstChild
}
/**
 * Find the element by its contents
 * @param {string} textContent
 * @return {Element|null}
 */
function findElementByTextContent(textContent) {
  const list = document.querySelectorAll('li')
  for (const li of list) {
    if (li.textContent.trim() === textContent) {
      return li
    }
  }
  return null
}
/**
 * Clear the <ul> list
 * @return {void}
 */
function clearAllElements() {
  if (!ulElement) {
    return
  }
  ulElement.innerHTML = ''
}

Вывод

У нас есть дополнительный сервер на 3000 порту, который начинается с webpack-dev-server и служит вспомогательным инструментом для работы с узлом. В этом случае помогает получение данных о папке, когда как без этого мы не получаем сдачу в режиме реального времени.

Причём с билдом размер билда не увеличивается, при лишнем коде с помощью этой строчки:

if (process.env.NODE_ENV === ‘development’) {
  /* Your Code */
}

Бонус — вывод данных на ПК

Отобразим потребление памяти в ОС на сайте. Создайте новый файл statistics.js, который будет каждую секунду отправлять данные всем активным пользователям веб-сокета:

$ touch ./core/statistics.js

Содержание:

'use strict'
const { sockets } = require('./io')
const os = require('os')
/**
 * @type {number} milliseconds
 */
const SECOND = 1000
/*
 * We send every second an update about memory consumption
 */
setInterval(() => {
  Object.values(sockets).forEach((socket) => {
    socket.emit('statistics', {
      totalmem: os.totalmem(),
      freemem: os.freemem()
    })
  })
}, SECOND)

Подключите этот файл к webpack.config.js:

...
/*
 * In development mode, we connect our own websocket server
 */
if (!isProd) {
  require('./core/statistics')
  require('./core/watch')
}
...

А теперь мы отобразим эти данные на нашем сайте. Отредактируйте файл dev.js:

...
// Initialization
let ulElement
let statisticsElement
window.addEventListener('load', () => {
  // Get list of files
  ulElement = document.querySelector('ul')
  // Add Statistics
  statisticsElement = createElementFromHTML('<div id="dev"></div>')
  // Add styles
  const styleElement = document.createElement('style')
  styleElement.innerHTML = `
    #dev {
      position: absolute;
      right: 10px;
      top: 10px;
      padding: 10px;
      border-radius: 10px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
      z-index: 99999;
    }
    #dev > div {
      font-weight: bold;
      text-align: center;
      margin-bottom: 5px;
    }
    #dev > span {
      font-size: .9rem;
    }
  `
  document.body.appendChild(statisticsElement)
  document.body.appendChild(styleElement)
})
...
socket.on('connect', () => {
  ...
  socket.on('statistics', (payload) => {
    statisticsElement.innerHTML = `
      <div>Statistics</div>
      <span>${formatBytes(payload.freemem)}</span> / <span>${formatBytes(payload.totalmem)}</span>
    `
  })
})
...
/**
 * Convert size in bytes to KB, MB, GB
 * @param {number} bytes
 * @param {number} decimals
 * @return {number}
 * @see https://stackoverflow.com/a/18650828
 */
function formatBytes(bytes, decimals = 2) {
  if (bytes === 0) {
    return '0 Bytes'
  }
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}

Исходный код: https://github.com/alexeykhr/webpack-dev-server-socket-io