process在前端工程化中的应用

5/16/2022, 6:49:52 PM

process在前端工程化中的应用

node之process模块

process是什么

process 对象提供有关当前 Node.js 进程的信息并对其进行控制。

process 对象是 EventEmitter 的实例。

node在启动过程中,process 对象 node 环境中的全局变量。

在nodejs启动的过程中,首先在c++层面新建一个process 对象,并保存在env里。随后调用node时,将process添加到 global对象上,在node中访问process,实际上就是访问global.process

// node/src/node_process_object.cc

MaybeLocal<Object> CreateProcessObject(Environment* env) {
  // ...
  // process.version
  READONLY_PROPERTY(process,
                    "version",
                    FIXED_ONE_BYTE_STRING(env->isolate(), NODE_VERSION));

  // process.versions
  Local<Object> versions = Object::New(env->isolate());
  READONLY_PROPERTY(process, "versions", versions);

  // ...
  return scope.Escape(process);
}

 

// node/src/node.cc

MaybeLocal<Value> Environment::BootstrapNode() {
  EscapableHandleScope scope(isolate_);

  Local<Object> global = context()->Global();
  // TODO(joyeecheung): this can be done in JS land now.
  global->Set(context(), FIXED_ONE_BYTE_STRING(isolate_, "global"), global)
      .Check();

  // process, require, internalBinding, primordials
  std::vector<Local<String>> node_params = {
      process_string(),
      require_string(),
      internal_binding_string(),
      primordials_string()};
  std::vector<Local<Value>> node_args = {
      process_object(), // process实例
      native_module_require(),
      internal_binding_loader(),
      primordials()};

  MaybeLocal<Value> result = ExecuteBootstrapper(
      this, "internal/bootstrap/node", &node_params, &node_args);
  // ...
  auto process_state_switch_id =
      owns_process_state()
          ? "internal/bootstrap/switches/does_own_process_state"
          : "internal/bootstrap/switches/does_not_own_process_state";
  result = ExecuteBootstrapper(
      this, process_state_switch_id, &node_params, &node_args);

  if (result.IsEmpty()) {
    return MaybeLocal<Value>();
  }
	
  // 设置 process.env (proxy?存疑,node中查看process.env和普通属性没明显区别,可能在c++层面做了proxy行为)
  Local<String> env_string = FIXED_ONE_BYTE_STRING(isolate_, "env");
  Local<Object> env_var_proxy;
  if (!CreateEnvVarProxy(context(), isolate_).ToLocal(&env_var_proxy) ||
      process_object()->Set(context(), env_string, env_var_proxy).IsNothing()) {
    return MaybeLocal<Value>();
  }
}

 

// node/lib/internal/bootstrap/node.js 

function setupProcessObject() {
  const EventEmitter = require('events');
  const origProcProto = ObjectGetPrototypeOf(process);
  ObjectSetPrototypeOf(origProcProto, EventEmitter.prototype);
  FunctionPrototypeCall(EventEmitter, process);
  ObjectDefineProperty(process, SymbolToStringTag, {
    enumerable: false,
    writable: true,
    configurable: false,
    value: 'process'
  });
  // Make process globally available to users by putting it on the global proxy
  ObjectDefineProperty(globalThis, 'process', {
    value: process,
    enumerable: false,
    writable: true,
    configurable: true
  });
}

js文件通过request函数加载模块,首先会从C++ builtin模块里面找,然后也是从C++里面constants模块找。最后到JS模块找

process.env

process属性在C++模块、js中都有赋值,如node.cc, nodeprocessobject.cc, nodeprocessmethods,node.js 等文件中都有相关操作,存储当前环境相关的所有信息

process.env属性返回一个包含用户环境信息的对象。包括环境变量,设备信息,操作系统信息等

process.env.NODE_ENV

我们通常所说的process.env.NODE_ENV,指的是启动node时shell中自定义的临时变量,变量名为NODE_ENV,一般来说windows的cmd中可通过

SET NODE_ENV=production

linuxs的bash中可通过

NODE_ENV=production

设置临时的变量名为NODE_ENV,值为 production的变量

可在当前shell中通过set查看当前环境的所有变量和函数,而在执行node脚本时,node会将该环境的所有变量添加到到process.env属性上。所以当我们在bash中通过以下命令启动test脚本时

SET NODE_ENV=production node ./test.js

可以在通过process.env.NODEENV访问到NODE_ENV的值

至于为什么项目中均采用NODE_ENV这个变量名来作为我们项目的环境,node源码中没找到相关的内容,官方文档中也没有说明,npm源码中有涉及,通过选项 only 等仅安装dependencies依赖时

define('only', {
  default: null,
  type: [null, 'prod', 'production'],
  deprecated: `
    Use \`--omit=dev\` to omit dev dependencies from the install.
  `,
  description: `
    When set to \`prod\` or \`production\`, this is an alias for
    \`--omit=dev\`.
  `,
  flatten (key, obj, flatOptions) {
    definitions.omit.flatten('omit', obj, flatOptions)
  },
})

npm install --only=prod

此时process.env.NODE_ENV会被设置为 production

const buildOmitList = obj => {

  const include = obj.include || []
  const omit = obj.omit || []

  const only = obj.only
  if (/^prod(uction)?$/.test(only) || obj.production) {
    omit.push('dev')
  } else if (obj.production === false) {
    include.push('dev')
  }

  if (/^dev/.test(obj.also)) {
    include.push('dev')
  }

  if (obj.dev) {
    include.push('dev')
  }

  if (obj.optional === false) {
    omit.push('optional')
  } else if (obj.optional === true) {
    include.push('optional')
  }

  obj.omit = [...new Set(omit)].filter(type => !include.includes(type))
  obj.include = [...new Set(include)]

  if (obj.omit.includes('dev')) {
    process.env.NODE_ENV = 'production'
  }

  return obj.omit
}

npm源码中主动设置process.env.NODE_ENV的逻辑仅此一处,执行npm命令运行脚本,npm不会主动去设置process.env.NODE_ENV,因此通过命令启动的子进程的process.env.NODE_ENV为undefined,如果项目或者脚本需要通过NODE_ENV区分环境,需要手动设置。

NODE_ENV一说是由 express框架采用该变量名区分项目环境而后因为express流行NODE_ENV的用法逐渐约定俗成。

process在前端工程化中最多的用途还是process.env

如webpack中经常使用的 NODE_ENV, vue-cli中的 VUE_APP_TITLEBASE_URL

如何设置process.env

设置系统变量(环境变量和临时变量)

这两种方式的过程均在node的c++层完成

环境变量

不同系统有不同配置文件,可通过在配置文件中添加NODE_ENV变量,如我们在系统中配置node命令的路径就是一个环境变量

临时变量

shell中执行node脚本中,linux环境下

NODE_ENV=production node ./test.js

会在当前bash下添加一个NODE_ENV变量,然后通过node执行test文件

node环境中直接修改当前进程process.env属性

js中给process.env添加属性

process.env.NODE_ENV = 'production'

采用何种方式

以NODEENV为例,一般需要在项目启动或者cli启动时就需确定NODE_ENV值,因此一般在shell中去通过命令手动设置临时变量NODE_ENV

但是不同操作系统shell中设置临时变量的命令不同,需要考虑系统兼容性,如在window的cmd中执行以下命令就会报错

NODE_ENV=production node ./test.js

因此以前npm的script中经常会有这种命令

 // package.json
 "scripts": {
    "dev": "WEBPACK_ENV=online node ./node_modules/.bin/webpack-dev-server",
    "dev_win": ""set WEBPACK_ENV=onlin && node ./node_modules/.bin/webpack-dev-server",
  },

现在常用的 cross-env 模块即用来解决兼容性问题

 // package.json
 "scripts": {
    "dev": "cross-env node WEBPACK_ENV=online node ./node_modules/.bin/webpack-dev-server",
  },

实际上cross-env并不是通过判断系统环境来设置临时变量或者环境变量来实现跨平台的,而是通过node的js层,给process.env添加属性完成,因为shell中启动时传入的参数的处理在不同系统上是一致的(c++)。

cross-env基本原理并不复杂,执行node执行cross-env脚本,cross-env获取命令行传入的参数process.argv,如WEBPACKENV=online,可分割为变量名为WEBPACKENV,值为online的变量,获取到参数后通过node child_proces模块提供的进程api

开启一个子进程,将env和分割得到的参数一并传递,由node合并至子进程process.env,部分源码如下

const cp = require('child_process')

function crossEnv(args, options = {}) {
  const [envSetters, command, commandArgs] = parseCommand(args)
  const env = getEnvVars(envSetters)
  if (command) {
    const proc = spawn(
      // run `path.normalize` for command(on windows)
      commandConvert(command, env, true),
      // by default normalize is `false`, so not run for cmd args
      commandArgs.map(arg => commandConvert(arg, env)),
      {
        stdio: 'inherit',
        shell: options.shell,
        env,
      },
    )
 }

function spawn(command, args, options) {
    // Parse the arguments
    const parsed = parse(command, args, options);

    // Spawn the child process
    const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);

    return spawned;
}

常用的一些cli工具也采用此种方式给process.env添加属性,如常用的vue-cli,启动项目会执行

vue-cli-service build --mode development

// vue-cli-service
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
// ....
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})
class Service {
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
      process.VUE_CLI_SERVICE = this
      // ...
  }
  init (mode = process.env.VUE_CLI_MODE) {
   // ...
    this.mode = mode

    // load mode .env
    if (mode) {
      this.loadEnv(mode)
    }
   // ...
  }
  loadEnv (mode) {
    // ...
    if (mode) {
      const shouldForceDefaultEnv = (
        process.env.VUE_CLI_TEST &&
        !process.env.VUE_CLI_TEST_TESTING_ENV
      )
      const defaultNodeEnv = (mode === 'production' || mode === 'test')
        ? mode
        : 'development'
      if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
        process.env.NODE_ENV = defaultNodeEnv
      }
      if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
        process.env.BABEL_ENV = defaultNodeEnv
      }
    }
  }
  async run (name, args = {}, rawArgv = []) {
    // ...
   	const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
    await this.init(mode)
	// ...
    const { fn } = command
    return fn(args, rawArgv) // 执行 webpack()
    
  }
}

除了上述两种通过参数的形式传入NODE_ENV,还有一种方式是在项目中配置 .env 文件。如 vue-cli 的项目中,经常会看到以下文件

// .env.development
VUE_APP_MODE=development
VUE_APP_BASE_API = 'xxx.xxx.com'

此种配置依赖 dotenv 库,会将文件中配置的key -value 写入 process.env 中,依然是js层面通过proces.env[key] = value 写入环境变量,核心代码如下

// dotenv/main.js
function config (options) {
  let dotenvPath = path.resolve(process.cwd(), '.env')
  let encoding = 'utf8'
  const debug = Boolean(options && options.debug)
  const override = Boolean(options && options.override)

  if (options) {
    if (options.path != null) {
      dotenvPath = _resolveHome(options.path)
    }
    if (options.encoding != null) {
      encoding = options.encoding
    }
  }

    // Specifying an encoding returns a string instead of a buffer
    const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }))

    Object.keys(parsed).forEach(function (key) {
      if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
        process.env[key] = parsed[key]
      } else {
        if (override === true) {
          process.env[key] = parsed[key]
        }
      }
    })
    return { parsed }
}