Сборка под разные target: web, node, electron

Параметр target в конфигурации Webpack определяет среду выполнения итогового бандла. От выбранного значения зависит:

  • способ генерации обёртки модулей;
  • работа встроенных переменных;
  • поддержка Node.js API;
  • формат динамических импортов;
  • поведение chunk loading;
  • доступность window, document, global;
  • формат output;
  • оптимизации runtime.

Webpack умеет собирать код не только для браузера, но и для серверных приложений, Electron, worker-потоков и гибридных платформ.

Базовый пример:

module.exports = {
  target: 'web'
}

Поддерживаются разные значения:

target: 'web'
target: 'node'
target: 'electron-main'
target: 'electron-renderer'
target: 'webworker'

Сборка под браузер: target: 'web'

web — стандартная цель Webpack. Используется по умолчанию.

module.exports = {
  target: 'web'
}

В этом режиме Webpack предполагает:

  • наличие DOM;
  • существование window;
  • загрузку чанков через <script>;
  • выполнение кода в браузере;
  • работу через HTTP/HTTPS.

Особенности browser runtime

Webpack генерирует runtime, адаптированный под браузер.

Например:

import('./module.js')

превращается в загрузку отдельного JS-файла через script tag.

Runtime примерно выполняет:

const script = document.createElement('script')
script.src = chunkUrl
document.head.appendChild(script)

Для Node.js такой механизм неприменим.


Доступ к DOM API

При target: 'web' доступны:

document.querySelector()
window.location
localStorage
navigator.userAgent

Пример:

const button = document.querySelector('.btn')

button.addEventListener('click', () => {
  console.log(window.location.href)
})

Проблемы browser target

Код Node.js API работать не будет:

const fs = require('fs')

Ошибка:

Module not found: Error: Can't resolve 'fs'

Webpack 5 перестал автоматически полифилить Node.js-модули.

Ранее автоматически подключались:

  • path-browserify
  • crypto-browserify
  • stream-browserify

Теперь это необходимо делать вручную.


Полифилы Node.js для браузера

Пример:

npm install path-browserify

Конфигурация:

const path = require('path')

module.exports = {
  target: 'web',

  resolve: {
    fallback: {
      path: require.resolve('path-browserify')
    }
  }
}

Отключение встроенных модулей

Иногда Node API не нужен вовсе.

Тогда можно явно отключить модуль:

module.exports = {
  resolve: {
    fallback: {
      fs: false
    }
  }
}

Сборка под Node.js: target: 'node'

Webpack может собирать серверные приложения.

module.exports = {
  target: 'node'
}

В этом режиме:

  • runtime адаптируется под Node.js;
  • чанки грузятся через require;
  • доступны Node API;
  • отсутствует DOM;
  • window не существует.

Runtime для Node.js

При динамическом импорте:

import('./service.js')

Webpack использует:

require('./service.js')

а не <script>.


Работа с Node API

Доступны:

const fs = require('fs')
const path = require('path')
const os = require('os')

Пример:

const fs = require('fs')

const content = fs.readFileSync('./data.txt', 'utf-8')

console.log(content)

Отсутствие DOM

Следующий код вызовет ошибку:

document.querySelector('.app')

Ошибка:

ReferenceError: document is not defined

Переменная global

В Node.js существует:

global

В браузере:

window

Webpack учитывает это различие автоматически.


Настройка output для Node.js

Часто используется:

module.exports = {
  target: 'node',

  output: {
    filename: 'server.js'
  }
}

External dependencies в Node.js

Серверные сборки обычно не включают зависимости из node_modules.

Используются externals.

Пример:

npm install webpack-node-externals
const nodeExternals = require('webpack-node-externals')

module.exports = {
  target: 'node',

  externals: [nodeExternals()]
}

Что делает externals

Без externals:

bundle.js

содержит:

  • express;
  • lodash;
  • mongoose;
  • axios;
  • все зависимости проекта.

Размер может достигать десятков мегабайт.

С externals:

require('express')

остаётся внешним вызовом.


Пример server-side bundle

const path = require('path')

module.exports = {
  mode: 'production',

  target: 'node',

  entry: './src/server.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'server.js'
  }
}

Node.js и __dirname

При browser target значение может изменяться.

Для Node.js часто включают:

module.exports = {
  target: 'node',

  node: {
    __dirname: false,
    __filename: false
  }
}

Сборка под Electron

Electron сочетает:

  • Chromium;
  • Node.js;
  • desktop runtime.

У Electron несколько процессов:

  • Main Process;
  • Renderer Process;
  • Preload Script.

Для каждого требуется собственный target.


Electron Main Process

Main Process — это backend Electron-приложения.

Используется:

target: 'electron-main'

Пример:

module.exports = {
  target: 'electron-main'
}

Особенности electron-main

Доступны:

  • Node.js API;
  • Electron API;
  • filesystem;
  • native modules.

Недоступны:

  • DOM;
  • document;
  • browser window context.

Пример main.js

const { app, BrowserWindow } = require('electron')

function createWindow() {
  const window = new BrowserWindow({
    width: 1200,
    height: 800
  })

  window.loadURL('http://localhost:3000')
}

app.whenReady().then(createWindow)

Конфигурация Webpack для Electron Main

const path = require('path')

module.exports = {
  target: 'electron-main',

  entry: './src/main.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  }
}

Electron Renderer

Renderer — это браузерное окно Electron.

Используется:

target: 'electron-renderer'

Особенности electron-renderer

Доступны одновременно:

  • DOM API;
  • Electron API;
  • часть Node.js API.

Пример:

document.querySelector('.btn')

и одновременно:

const os = require('os')

Пример renderer.js

const os = require('os')

document.querySelector('#info').innerHTML = `
  Platform: ${os.platform()}
`

Конфигурация renderer

const path = require('path')

module.exports = {
  target: 'electron-renderer',

  entry: './src/renderer.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'renderer.js'
  }
}

Electron Preload

Preload Script выполняется между:

  • renderer;
  • main process.

Обычно используется:

target: 'electron-preload'

Назначение preload

Preload безопасно предоставляет API:

contextBridge.exposeInMainWorld()

Пример:

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('api', {
  version: '1.0'
})

Конфигурация preload

module.exports = {
  target: 'electron-preload'
}

Несколько target одновременно

Крупные проекты часто имеют:

  • frontend;
  • backend;
  • preload;
  • worker.

Webpack позволяет экспортировать массив конфигураций.


Multi-compiler configuration

const path = require('path')

module.exports = [
  {
    target: 'web',

    entry: './src/client.js',

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'client.js'
    }
  },

  {
    target: 'node',

    entry: './src/server.js',

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'server.js'
    }
  }
]

Разделение frontend/backend

Типичная архитектура:

src/
  client/
  server/

Конфигурации:

webpack.client.js
webpack.server.js

Общая конфигурация

Часто используется merge.

npm install webpack-merge

Пример merge

const { merge } = require('webpack-merge')

const common = {
  mode: 'production'
}

const client = merge(common, {
  target: 'web'
})

const server = merge(common, {
  target: 'node'
})

module.exports = [client, server]

Target и Babel

target Webpack не заменяет targets Babel.

Webpack определяет:

  • runtime;
  • environment;
  • chunk loading.

Babel определяет:

  • транспиляцию синтаксиса;
  • поддержку браузеров;
  • polyfills.

Babel targets

Пример:

presets: [
  [
    '@babel/preset-env',
    {
      targets: {
        node: '18'
      }
    }
  ]
]

Комбинация Webpack и Babel

module.exports = {
  target: 'node',

  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader'
      }
    ]
  }
}

Web Workers

Для worker-потоков используется:

target: 'webworker'

Особенности webworker

В worker отсутствуют:

  • document;
  • window;
  • DOM.

Доступны:

self
postMessage()
onmessage

Пример worker

self.onmess age = event => {
  const result = event.data * 2

  self.postMessage(result)
}

Конфигурация worker

module.exports = {
  target: 'webworker'
}

Universal / Isomorphic приложения

Один и тот же код может выполняться:

  • в браузере;
  • на сервере.

Тогда target разделяется.


Пример SSR архитектуры

client bundle -> browser
server bundle -> node.js

Проверка среды выполнения

const isBrowser = typeof window !== 'undefined'

Разделение платформенного кода

if (typeof window !== 'undefined') {
  localStorage.setItem('theme', 'dark')
}

Различия chunk loading

Browser

<script src="chunk.js">

Node.js

require('./chunk.js')

Worker

importScripts()

Webpack автоматически выбирает стратегию.


Target и output.libraryTarget

Это разные параметры.

target:

target: 'node'

определяет среду выполнения.

libraryTarget:

output: {
  libraryTarget: 'commonjs2'
}

определяет формат экспорта библиотеки.


Сборка библиотек под разные платформы

Иногда библиотека публикуется сразу в нескольких вариантах:

  • browser;
  • node;
  • esm;
  • commonjs.

Пример multi-target build

module.exports = [
  {
    target: 'web',

    output: {
      filename: 'library.browser.js'
    }
  },

  {
    target: 'node',

    output: {
      filename: 'library.node.js'
    }
  }
]

Ошибки несовместимости target

Использование DOM в Node.js

window.location

Ошибка:

window is not defined

Использование fs в браузере

require('fs')

Ошибка:

Can't resolve 'fs'

Electron renderer и security risks

Опасная конфигурация:

nodeIntegration: true

Renderer получает полный доступ к Node.js.

Современный Electron рекомендует:

contextIsolation: true
sandbox: true

Проверка target во время сборки

Webpack предоставляет:

compiler.options.target

Это полезно для кастомных плагинов.

Пример:

class TargetPlugin {
  apply(compiler) {
    console.log(compiler.options.target)
  }
}

Условная конфигурация target

module.exports = (env) => {
  const isServer = env.server

  return {
    target: isServer ? 'node' : 'web'
  }
}

Практическая структура крупного проекта

project/
  src/
    client/
    server/
    electron/
      main/
      renderer/
      preload/

  webpack/
    webpack.client.js
    webpack.server.js
    webpack.electron.js

Production-конфигурация для нескольких target

const path = require('path')

module.exports = [
  {
    mode: 'production',

    target: 'web',

    entry: './src/client/index.js',

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'client.bundle.js'
    }
  },

  {
    mode: 'production',

    target: 'node',

    entry: './src/server/index.js',

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'server.bundle.js'
    }
  },

  {
    mode: 'production',

    target: 'electron-main',

    entry: './src/electron/main/index.js',

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'electron-main.js'
    }
  }
]