Условная сборка на основе среды с использованием Webpack

95

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

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

Для webpack, похоже, нет способа сделать это. Во-первых, чтобы создать конфигурацию времени выполнения для среды, я использовал resolve.alias, чтобы повторно указать требование в зависимости от среды, например:

// All settings.
var all = {
    fish: 'salmon'
};

// `envsettings` is an alias resolved at build time.
module.exports = Object.assign(all, require('envsettings'));

Затем при создании конфигурации веб-пакета я могу динамически назначать, на какой файл envsettingsуказывает (т.е. webpackConfig.resolve.alias.envsettings = './' + env).

Однако я бы хотел сделать что-то вроде:

if (settings.mock) {
    // Short-circuit ajax calls.
    // Require in all the mock modules.
}

Но, очевидно, я не хочу встраивать эти фиктивные файлы, если среда не является фиктивной.

Я мог бы вручную перенаправить все эти требования в файл-заглушку, снова используя resolve.alias, но есть ли способ, который кажется менее хакерским?

Есть идеи, как я могу это сделать? Спасибо.

Доминик
источник
Обратите внимание, что на данный момент я использовал псевдоним, чтобы указать на пустой (заглушенный) файл в средах, которые мне не нужны (например, require ('mocks') будет указывать на пустой файл в немодовых envs. Кажется, немного взломано, но это работает
Доминик

Ответы:

60

Вы можете использовать плагин определения .

Я использую его, делая что-то столь же простое, как это, в вашем файле сборки веб-пакета, где envэто путь к файлу, который экспортирует объект настроек:

// Webpack build config
plugins: [
    new webpack.DefinePlugin({
        ENV: require(path.join(__dirname, './path-to-env-files/', env))
    })
]

// Settings file located at `path-to-env-files/dev.js`
module.exports = { debug: true };

а затем это в вашем коде

if (ENV.debug) {
    console.log('Yo!');
}

Он удалит этот код из вашего файла сборки, если условие ложно. Здесь вы можете увидеть рабочий пример сборки Webpack .

Мэтт Деррик
источник
Меня это решение немного смущает. Здесь не упоминается, как я должен установить env. Глядя на этот пример, кажется, что они обрабатывают этот флаг с помощью gulp и yargs, которые не все используют.
Андре
1
Как это работает с линтером? Вам нужно вручную определять новые глобальные переменные, которые добавляются в плагин Define?
отметка
2
@ отметьте да. Добавьте что-нибудь вроде "globals": { "ENV": true }в свой .eslintrc
Мэтт Деррик
как мне получить доступ к переменной ENV в компоненте? Я попробовал решение выше, но все равно получаю сообщение об ошибке, что ENV не определен
jasan
20
Он НЕ удаляет код из файлов сборки! Я протестировал его, и код здесь.
Лайонел
42

Не уверен, почему ответ "webpack.DefinePlugin" везде является лучшим для определения импорта / требований на основе среды.

Проблема с этим подходом является то , что вы по - прежнему поставлять все эти модули для клиента -> проверить с WebPack-расслоение-analyezer , например. И вообще не уменьшая размер вашего bundle.js :)

Итак, что действительно хорошо работает и намного логичнее: NormalModuleReplacementPlugin

Поэтому вместо того, чтобы делать условное требование on_client -> просто не включайте ненужные файлы в пакет в первую очередь

надеюсь, это поможет

Роман Жылёв
источник
Nice не знал об этом плагине!
Dominic
Разве в этом сценарии у вас не было бы нескольких сборок для каждой среды? Например, если у меня есть адрес веб-службы для сред dev / QA / UAT / production, мне понадобятся 4 отдельных контейнера, по 1 для каждой среды. В идеале у вас должен быть один контейнер и запускать его с переменной среды, чтобы указать, какую конфигурацию загружать.
Brett Mathe
Нет, не совсем. Это именно то, что вы делаете с плагином -> вы указываете свою среду через env vars, и он создает только один контейнер, но для конкретной среды без лишних включений. Конечно, это также зависит от того, как вы настраиваете конфигурацию веб-пакета, и, очевидно, вы можете создавать все сборки, но это не то, о чем этот плагин и чем он занимается.
Роман Жылев
34

Используйте ifdef-loader. В исходных файлах вы можете делать такие вещи, как

/// #if ENV === 'production'
console.log('production!');
/// #endif

Соответствующая webpackконфигурация

const preprocessor = {
  ENV: process.env.NODE_ENV || 'development',
};

const ifdef_query = require('querystring').encode({ json: JSON.stringify(preprocessor) });

const config = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: `ifdef-loader?${ifdef_query}`,
        },
      },
    ],
  },
  // ...
};
Мэй Оукс
источник
3
Я поддержал этот ответ, поскольку принятый ответ не удаляет код, как ожидалось, и синтаксис, подобный препроцессору, с большей вероятностью будет идентифицирован как условный элемент.
Christian Ivicevic
1
Спасибо! Работает как часы. Несколько часов экспериментов с ContextReplacementPlugin, NormalModuleReplacementPlugin и прочим - все провалилось. И вот ifdef-loader, спасающий мне день.
jeron-diovis
28

В итоге я использовал что-то похожее на ответ Мэтта Деррика , но беспокоился о двух моментах:

  1. Полная конфигурация вводится каждый раз, когда я использую ENV(что плохо для больших конфигураций).
  2. Мне нужно определить несколько точек входа, потому что они require(env)указывают на разные файлы.

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

config/
 └── main.js
 └── dev.js
 └── production.js
src/
 └── app.js
 └── config.js
 └── ...
webpack.config.js

main.jsСодержит все конфигурации по умолчанию вещи:

// main.js
const mainConfig = {
  apiEndPoint: 'https://api.example.com',
  ...
}

module.exports = mainConfig;

dev.jsИ production.jsтолько держать конфигурационный материал , который перекрывает основную конфигурацию:

// dev.js
const devConfig = {
  apiEndPoint: 'http://localhost:4000'
}

module.exports = devConfig;

Важной частью является то, webpack.config.jsчто составляет конфигурацию и использует DefinePlugin для создания переменной среды, __APP_CONFIG__которая содержит составной объект конфигурации:

const argv = require('yargs').argv;
const _ = require('lodash');
const webpack = require('webpack');

// Import all app configs
const appConfig = require('./config/main');
const appConfigDev = require('./config/dev');
const appConfigProduction = require('./config/production');

const ENV = argv.env || 'dev';

function composeConfig(env) {
  if (env === 'dev') {
    return _.merge({}, appConfig, appConfigDev);
  }

  if (env === 'production') {
    return _.merge({}, appConfig, appConfigProduction);
  }
}

// Webpack config object
module.exports = {
  entry: './src/app.js',
  ...
  plugins: [
    new webpack.DefinePlugin({
      __APP_CONFIG__: JSON.stringify(composeConfig(ENV))
    })
  ]
};

Последний шаг теперь config.jsвыглядит так (здесь используется синтаксис импорта и экспорта es6, потому что он находится в веб-пакете):

const config = __APP_CONFIG__;

export default config;

В вашем app.jsтеперь вы можете использовать import config from './config';для получения объекта конфигурации.

дома
источник
2
Действительно лучший ответ здесь
Габриэль
18

другой способ - использовать файл JS как файл proxy, и позволить этому файлу загрузить интересующий модуль commonjsи экспортировать его как es2015 module, например:

// file: myModule.dev.js
module.exports = "this is in dev"

// file: myModule.prod.js
module.exports = "this is in prod"

// file: myModule.js
let loadedModule
if(WEBPACK_IS_DEVELOPMENT){
    loadedModule = require('./myModule.dev.js')
}else{
    loadedModule = require('./myModule.prod.js')
}

export const myString = loadedModule

Затем вы можете использовать модуль ES2015 в своем приложении в обычном режиме:

// myApp.js
import { myString } from './store/myModule.js'
myString // <- "this is in dev"
Алехандро Силва
источник
19
Единственная проблема с if / else и require заключается в том, что оба требуемых файла будут объединены в сгенерированный файл. Я не нашел обходного пути. По сути, сначала происходит объединение, а затем искажение.
Alex
2
это не обязательно верно, если вы используете в своем файле webpack плагин webpack.optimize.UglifyJsPlugin(), оптимизация webpack не будет загружать модуль, так как код строки внутри условного выражения всегда ложен, поэтому webpack удаляет его из сгенерированного пакета
Алехандро Силва,
@AlejandroSilva у вас есть репозиторий этого примера?
Capuchin
1
@thevangelist ага: github.com/AlejandroSilva/mototracker/blob/master/… это нода + реакция + редукция питомца: P
Алехандро Силва
4

Столкнувшись с той же проблемой, что и OP, и из-за лицензирования требовалось не включать определенный код в определенные сборки, я принял webpack-conditional-loader следующим образом:

В моей команде сборки я устанавливаю переменную среды, соответствующую моей сборке. Например, demo в package.json:

...
  "scripts": {
    ...
    "buildDemo": "./node_modules/.bin/webpack --config webpack.config/demo.js --env.demo --progress --colors",
...

В документации, которую я читал, отсутствует запутанный момент, заключается в том, что я должен сделать это видимым на протяжении всей обработки сборки , гарантируя, что моя переменная env будет введена в глобальный процесс, таким образом, в моем webpack.config / demo.js:

/* The demo includes project/reports action to access placeholder graphs.
This is achieved by using the webpack-conditional-loader process.env.demo === true
 */

const config = require('./production.js');
config.optimization = {...(config.optimization || {}), minimize: false};

module.exports = env => {
  process.env = {...(process.env || {}), ...env};
  return config};

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

...
// #if process.env.demo
import Reports from 'components/model/project/reports';
// #endif
...
const routeMap = [
  ...
  // #if process.env.demo
  {path: "/project/reports/:id", component: Reports},
  // #endif
...

Это работает с webpack 4.29.6.

Пол Уипп
источник
1
Также есть github.com/dearrrfish/preprocess-loader, в котором есть больше функций
user9385381
1

Я боролся с настройкой env в своих конфигурациях веб-пакетов. Обычно я хочу установить env так, чтобы его можно было достичь внутри webpack.config.js, postcss.config.jsа внутри самого приложения точки входа (index.js обычно). Я надеюсь, что мои выводы могут кому-то помочь.

Решение, которое я придумал, состоит в том, чтобы передать --env productionили --env development, а затем установить режим внутри webpack.config.js. Однако это не помогает мне сделать envдоступным там, где я хочу (см. Выше), поэтому мне также нужно process.env.NODE_ENVявно указать , как рекомендуется здесь . Наиболее важная часть, которая у меня есть webpack.config.jsниже.

...
module.exports = mode => {
  process.env.NODE_ENV = mode;

  if (mode === "production") {
    return merge(commonConfig, productionConfig, { mode });
  }
  return merge(commonConfig, developmentConfig, { mode });
};
Максимум
источник
0

Используйте переменные envirnment для создания развертываний dev и prod:

https://webpack.js.org/guides/environment-variables/

Саймон Х
источник
2
Я не об этом спрашивал
Доминик
проблема в том, что webpack проигнорирует условие при создании пакета и в любом случае будет включать код, загруженный для разработки ... так что это не решает проблему
sergioviniciuss
-1

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

if (typeof window !== 'undefined') 
    return
}
//run node only code now
Esqarrouth
источник
1
OP спрашивает о решении времени компиляции, ваш ответ - о времени выполнения.
Майкл