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.

Dominic
fonte
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 .

Matt Derrick
fonte
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

Roman Zhyliov
fonte
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}`,
        },
      },
    ],
  },
  // ...
};
May Oakes
fonte
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.

ofhouse
fonte
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"
Alejandro Silva
fonte
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.

Paul Whipp
fonte
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 });
};
Max
fonte
0

Use variáveis ​​de ambiente para criar implantações de desenvolvimento e produção:

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

Simon H
fonte
2
Não era isso que eu estava perguntando
Dominic
o problema é que o webpack irá ignorar a condição ao construir o pacote e incluir o código carregado para desenvolvimento de qualquer maneira ... então isso não resolve o problema
sergioviniciuss
-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
Esqarrouth
fonte
1
OP está perguntando sobre uma decisão de tempo de compilação, sua resposta é sobre o tempo de execução.
Michael