Представляем
Пример 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