Представляем
Пример 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:
Создайте файл 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