Construção condicional baseada no ambiente usando Webpack


93

Tenho algumas coisas para desenvolvimento - por exemplo, simulações com as quais gostaria de não sobrecarregar meu arquivo de construção distribuído.

No RequireJS, você pode passar uma configuração em um arquivo de plug-in e exigir coisas condicionalmente com base nisso.

Para o webpack, não parece haver uma maneira de fazer isso. Em primeiro lugar, para criar uma configuração de tempo de execução para um ambiente, usei resolve.alias para reposicionar uma exigência dependendo do ambiente, por exemplo:

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

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

Então, ao criar a configuração do webpack, posso atribuir dinamicamente para quais envsettingspontos de arquivo (ou seja webpackConfig.resolve.alias.envsettings = './' + env).

No entanto, gostaria de fazer algo como:

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

Mas obviamente não quero construir nesses arquivos fictícios se o ambiente não for fictício.

Eu poderia reposicionar manualmente todos esses requisitos em um arquivo stub usando resolve.alias novamente - mas existe uma maneira que pareça menos hacky?

Alguma ideia de como posso fazer isso? Obrigado.


Observe que, por enquanto, usei alias para apontar para um arquivo vazio (stub) em ambientes que não quero (por exemplo, require ('mocks') apontará para um arquivo vazio em envs não simulados. Parece um pouco hackeado, mas trabalhos.
Dominic

Respostas:


59

Você pode usar o plugin define .

Eu o uso fazendo algo tão simples quanto isso em seu arquivo de compilação do webpack, onde envé o caminho para um arquivo que exporta um objeto de configurações:

// 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 };

e então isso em seu código

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

Ele removerá esse código de seu arquivo de construção se a condição for falsa. Você pode ver um exemplo de construção de Webpack funcional aqui .


Estou um pouco confuso com esta solução. Não menciona como devo definir env. Olhando através desse exemplo, parece que eles estão lidando com aquele sinalizador via gulp e yargs que nem todo mundo está usando.
Andre

1
Como isso funciona com linters? Você tem que definir manualmente novas variáveis ​​globais que são adicionadas no plugin Definir?
marque

2
@marque sim. Adicione algo como "globals": { "ENV": true }ao seu .eslintrc
Matt Derrick

como eu acessaria a variável ENV em um componente? Tentei a solução acima, mas ainda recebo o erro de que ENV não está definido
jasan 09/09/16

17
Ele NÃO remove o código dos arquivos de construção! Eu testei e o código está aqui.
Lionel,

42

Não sei por que a resposta "webpack.DefinePlugin" é a principal em todos os lugares para definir as importações / necessidades baseadas no ambiente.

O problema com essa abordagem é que você ainda está entregando todos esses módulos ao cliente -> verifique com webpack-bundle-analyezer, por exemplo. E não reduzindo o tamanho do seu bundle.js :)

Então, o que realmente funciona bem e muito mais lógico é: NormalModuleReplacementPlugin

Então, ao invés de fazer um requisito condicional on_client -> apenas não incluir arquivos não necessários ao pacote em primeiro lugar

espero que ajude


Nice não sabia sobre esse plugin!
Dominic

Com este cenário, você não teria vários builds por ambiente? Por exemplo, se eu tiver um endereço de serviço da web para ambientes dev / QA / UAT / produção, precisaria de 4 contêineres separados, 1 para cada ambiente. Idealmente, você teria um contêiner e iniciá-lo com uma variável de ambiente para especificar qual configuração carregar.
Brett Mathe

Não, na verdade não. Isso é exatamente o que você faz com o plugin -> você especifica seu ambiente por meio de env vars e ele cria apenas um contêiner, mas para um ambiente específico sem inclusões redundantes. É claro que isso também depende de como você configura seu webpack e, obviamente, você pode construir todas as compilações, mas esse plugin não é e faz isso.
Roman Zhyliov de

33

Use ifdef-loader. Em seus arquivos de origem, você pode fazer coisas como

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

A webpackconfiguração relevante é

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}`,
        },
      },
    ],
  },
  // ...
};

2
Votei positivamente nesta resposta, pois a resposta aceita não remove o código como esperado e a sintaxe semelhante ao pré-processador é mais provável de ser identificada como um elemento condicional.
Christian Ivicevic de

1
Muito obrigado! Ele funciona como um encanto. Várias horas de experimentos com ContextReplacementPlugin, NormalModuleReplacementPlugin e outras coisas - todos falharam. E aqui está o ifdef-loader, salvando o meu dia.
jeron-diovis

27

Acabei usando algo semelhante à resposta de Matt Derrick , mas estava preocupado com dois pontos:

  1. A configuração completa é injetada toda vez que eu uso ENV(o que é ruim para configurações grandes).
  2. Tenho que definir vários pontos de entrada porque require(env)aponta para arquivos diferentes.

O que eu criei é um compositor simples que constrói um objeto de configuração e o injeta em um módulo de configuração.
Aqui está a estrutura do arquivo, estou usando para isso:

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

O main.jscontém todas as coisas de configuração padrão:

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

module.exports = mainConfig;

O dev.jse production.jsmaterial de configuração única espera que substitui a configuração principal:

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

module.exports = devConfig;

A parte importante é o webpack.config.jsque compõe a configuração e usa o DefinePlugin para gerar uma variável de ambiente __APP_CONFIG__que contém o objeto de configuração composto:

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))
    })
  ]
};

A última etapa é agora config.js, tem a seguinte aparência (usando a sintaxe es6 import export aqui porque está sob webpack):

const config = __APP_CONFIG__;

export default config;

Em seu, app.jsagora você pode usar import config from './config';para obter o objeto de configuração.


2
Realmente a melhor resposta aqui
Gabriel,

18

outra maneira é usar um arquivo JS como um proxye deixar esse arquivo carregar o módulo de interesse commonjse exportá-lo como es2015 module, assim:

// 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

Então você pode usar o módulo ES2015 em seu aplicativo normalmente:

// myApp.js
import { myString } from './store/myModule.js'
myString // <- "this is in dev"

19
O único problema com if / else e require é que ambos os arquivos necessários serão agrupados no arquivo gerado. Eu não encontrei uma solução alternativa. Essencialmente, o empacotamento acontece primeiro, depois a mutilação.
alex

2
isso não é necessariamente verdade, se você usar em seu arquivo webpack o plugin webpack.optimize.UglifyJsPlugin(), a otimização do webpack não carregará o módulo, pois o código de linha dentro da condicional é sempre falso, então o webpack remove-o do pacote gerado
Alejandro Silva

@AlejandroSilva você tem um exemplo de repo disso?
thevangelist de

1
@thevangelist sim: github.com/AlejandroSilva/mototracker/blob/master/… é um nó + react + redux projeto de estimação: P
Alejandro Silva

4

Diante do mesmo problema do OP e obrigado, por causa do licenciamento, a não incluir determinado código em certas compilações, adotei o webpack-conditional-loader da seguinte maneira:

No meu comando de construção, eu defino uma variável de ambiente apropriada para minha construção. Por exemplo 'demo' em package.json:

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

A parte confusa que está faltando na documentação que li é que tenho que tornar isso visível em todo o processamento da compilação , garantindo que minha variável env seja injetada no processo global, portanto, em meu 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};

Com isso implementado, posso excluir qualquer coisa condicionalmente, garantindo que qualquer código relacionado seja devidamente retirado do JavaScript resultante. Por exemplo, em meu routes.js, o conteúdo de demonstração é mantido fora de outras compilações, assim:

...
// #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
...

Isso funciona com o webpack 4.29.6.


1
Também há github.com/dearrrfish/preprocess-loader que tem mais recursos
user9385381

1

Tenho lutado para definir env nas configurações do meu webpack. O que eu costumo quero é set env para que ele possa ser alcançado dentro webpack.config.js, postcss.config.jse dentro do aplicativo ponto de entrada em si ( index.jsgeralmente). Espero que minhas descobertas possam ajudar alguém.

A solução que encontrei é passar --env productionou --env developmente definir o modo interno webpack.config.js. No entanto, isso não me ajuda a tornar envacessível onde quero (veja acima), então também preciso definir process.env.NODE_ENVexplicitamente, conforme recomendado aqui . A parte mais relevante que tenho a webpack.config.jsseguir abaixo.

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

  if (mode === "production") {
    return merge(commonConfig, productionConfig, { mode });
  }
  return merge(commonConfig, developmentConfig, { mode });
};


-1

Embora essa não seja a melhor solução, pode funcionar para algumas de suas necessidades. Se você deseja executar um código diferente no nó e no navegador usando isso funcionou para mim:

if (typeof window !== 'undefined') 
    return
}
//run node only code now

1
OP está perguntando sobre uma decisão de tempo de compilação, sua resposta é sobre o tempo de execução.
Michael
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.