Compilación condicional basada en el entorno usando Webpack


93

Tengo algunas cosas para desarrollar, por ejemplo, simulacros con los que no me gustaría inflar mi archivo de compilación distribuido.

En RequireJS, puede pasar una configuración en un archivo de complemento y, de manera conditonal, requerir cosas en función de eso.

Para el paquete web, no parece haber una forma de hacer esto. En primer lugar, para crear una configuración de tiempo de ejecución para un entorno, he usado resolve.alias para volver a señalar un requerimiento según el entorno, por ejemplo:

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

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

Luego, al crear la configuración del paquete web, puedo asignar dinámicamente a qué archivo envsettingsapunta (es decir webpackConfig.resolve.alias.envsettings = './' + env).

Sin embargo, me gustaría hacer algo como:

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

Pero obviamente no quiero construir esos archivos simulados si el entorno no es simulado.

Posiblemente podría volver a apuntar manualmente todos esos requisitos a un archivo de código auxiliar usando resolve.alias nuevamente, pero ¿hay alguna forma que se sienta menos hacky?

¿Alguna idea de cómo puedo hacer eso? Gracias.


Tenga en cuenta que, por ahora, he usado alias para señalar un archivo vacío (stub) en entornos que no quiero (por ejemplo, require ('mocks') apuntará a un archivo vacío en entornos no simulados. Parece un poco hacky pero funciona.
Dominic

Respuestas:


59

Puede utilizar el complemento de definición .

Lo uso haciendo algo tan simple como esto en el archivo de compilación de su paquete web, donde envestá la ruta a un archivo que exporta un objeto de configuración:

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

y luego esto en tu código

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

Eliminará este código de su archivo de compilación si la condición es falsa. Puedes ver un trabajo ejemplo de construcción de Webpack aquí .


Estoy un poco confundido con esta solución. No menciona cómo se supone que debo configurar env. Mirando ese ejemplo, parece que están manejando esa bandera a través de gulp y yargs que no todos están usando.
Andre

1
¿Cómo funciona esto con linters? ¿Tiene que definir manualmente nuevas variables globales que se agregan en el complemento Definir?
marcar

2
@marque sí. Agregue algo como "globals": { "ENV": true }a su .eslintrc
Matt Derrick

¿Cómo accedería a la variable ENV en un componente?
Probé

18
¡NO elimina el código de los archivos de compilación! Lo probé y el código está aquí.
Lionel

42

No estoy seguro de por qué la respuesta "webpack.DefinePlugin" es la mejor en todas partes para definir importaciones / requisitos basados ​​en el entorno.

El problema con ese enfoque es que todavía está entregando todos esos módulos al cliente -> verifique con webpack-bundle-analyezer, por ejemplo. Y sin reducir el tamaño de su bundle.js en absoluto :)

Entonces, lo que realmente funciona bien y es mucho más lógico es: NormalModuleReplacementPlugin

Entonces, en lugar de hacer un requisito condicional on_client -> simplemente no incluya archivos no necesarios en el paquete en primer lugar

Espero que ayude


¡Nice no sabía nada de ese complemento!
Dominic

Con este escenario, ¿no tendría varias compilaciones por entorno? Por ejemplo, si tengo una dirección de servicio web para entornos dev / QA / UAT / producción, necesitaría 4 contenedores separados, 1 para cada entorno. Idealmente, tendría un contenedor y lo lanzaría con una variable de entorno para especificar qué configuración cargar.
Brett Mathe

No en realidad no. Eso es exactamente lo que hace con el complemento -> especifica su entorno a través de env vars y construye solo un contenedor, pero para un entorno particular sin inclusiones redundantes. Por supuesto, eso también depende de cómo configure la configuración de su paquete web y, obviamente, puede construir todas las compilaciones, pero no es de lo que se trata ni de lo que hace este complemento.
Roman Zhyliov

34

Utilice ifdef-loader. En sus archivos de origen puede hacer cosas como

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

La webpackconfiguración relevante es

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
Elegí esta respuesta ya que la respuesta aceptada no elimina el código como se esperaba y es más probable que la sintaxis similar a la del preprocesador se identifique como un elemento condicional.
Christian Ivicevic

1
¡Muchas gracias! Funciona a las mil maravillas. Varias horas de experimentos con ContextReplacementPlugin, NormalModuleReplacementPlugin y otras cosas, todos fallaron. Y aquí está ifdef-loader, salvándome el día.
jeron-diovis

27

Terminé usando algo similar a la respuesta de Matt Derrick , pero estaba preocupado por dos puntos:

  1. La configuración completa se inyecta cada vez que uso ENV(lo que es malo para configuraciones grandes).
  2. Tengo que definir varios puntos de entrada porque require(env)apunta a diferentes archivos.

Lo que se me ocurrió es un compositor simple que crea un objeto de configuración y lo inyecta en un módulo de configuración.
Aquí está la estructura de archivos que estoy usando para esto:

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

El main.jssostiene todas las cosas de configuración por defecto:

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

module.exports = mainConfig;

El dev.jsy production.jsúnico elemento de configuración que anula la configuración principal:

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

module.exports = devConfig;

La parte importante es la webpack.config.jsque compone la configuración y usa el DefinePlugin para generar una variable de entorno __APP_CONFIG__que contiene el objeto de configuración compuesto:

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

El último paso es ahora config.js, se ve así (usando la sintaxis de importación y exportación de es6 aquí porque está en el paquete web):

const config = __APP_CONFIG__;

export default config;

En su app.js, ahora podría usar import config from './config';para obtener el objeto de configuración.


2
Realmente la mejor respuesta aquí
Gabriel

18

otra forma es usar un archivo JS como proxy, y dejar que ese archivo cargue el módulo de interés commonjsy lo exporte como es2015 module, así:

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

Entonces puede usar el módulo ES2015 en su aplicación normalmente:

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

19
El único problema con if / else y require es que ambos archivos requeridos se incluirán en el archivo generado. No he encontrado una solución. Básicamente, la agrupación ocurre primero y luego la manipulación.
alex

2
eso no es necesariamente cierto, si usa en su archivo de paquete web el complemento webpack.optimize.UglifyJsPlugin(), la optimización del paquete web no cargará el módulo, ya que el código de línea dentro del condicional siempre es falso, por lo que el paquete web lo elimina del paquete generado
Alejandro Silva

@AlejandroSilva ¿tienes un ejemplo de repositorio de esto?
thevangelist

1
@thevangelist yep: github.com/AlejandroSilva/mototracker/blob/master/… es un proyecto de mascota node + react + redux: P
Alejandro Silva

4

Enfrentado con el mismo problema que el OP y requerido, debido a la licencia, no incluir cierto código en ciertas compilaciones, adopté el cargador condicional de paquetes web de la siguiente manera:

En mi comando de compilación, establezco una variable de entorno apropiada para mi compilación. Por ejemplo, 'demo' en package.json:

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

El bit confuso que falta en la documentación que leí es que tengo que hacer esto visible durante todo el procesamiento de compilación asegurándome de que mi variable env se inyecte en el proceso global, por lo tanto, en mi 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};

Con esto en su lugar, puedo excluir condicionalmente cualquier cosa, asegurándome de que cualquier código relacionado se elimine correctamente del JavaScript resultante. Por ejemplo, en mis rutas.js, el contenido de demostración se mantiene fuera de otras compilaciones, por lo que:

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

Esto funciona con webpack 4.29.6.


1
También está github.com/dearrrfish/preprocess-loader que tiene más funciones
user9385381

1

He tenido problemas para configurar env en las configuraciones de mi paquete web. Lo que suelo quiero es env conjunto para que pueda ser alcanzado en el interior webpack.config.js, postcss.config.jsy dentro de la aplicación en sí punto de entrada (index.js por lo general). Espero que mis hallazgos puedan ayudar a alguien.

La solución que se me ocurrió es pasar --env productiono --env development, y luego establecer el modo dentro webpack.config.js. Sin embargo, eso no me ayuda a hacer envaccesible donde quiero (ver arriba), por lo que también necesito establecerlo process.env.NODE_ENVexplícitamente, como se recomienda aquí . La parte más relevante que tengo a webpack.config.jscontinuación.

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

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


-1

Si bien esta no es la mejor solución, puede funcionar para algunas de sus necesidades. Si desea ejecutar un código diferente en el nodo y el navegador usando esto funcionó para mí:

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

1
OP está preguntando sobre una decisión de tiempo de compilación, su respuesta es sobre el tiempo de ejecución.
Michael
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.