【源码共读】dotenv:从 .env 文件中读取环境变量

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【源码共读】dotenv:从 .env 文件中读取环境变量


dotenv.env文件中读取环境变量,然后将其添加到process.env中。这是一个非常简单的库,但是它在开发中非常有用,因为它允许你在.env文件中存储敏感信息,而不是将其存储在代码中。


现在很多库都支持.env文件,例如create-react-appvue-clinext.js等。


使用


根据READMEdotenv只有两个方法:


  • config:读取.env文件并将其添加到process.env中。
  • parse:解析一段包含环境变量的字符串或Buffer,并返回一个对象。
const dotenv = require('dotenv')
// 读取.env文件并将其添加到process.env中
dotenv.config()
// 解析一段包含环境变量的字符串或Buffer,返回一个对象
const config1 = dotenv.parse('FOO=bar\nBAR=foo')
console.log(config1) // { FOO: 'bar', BAR: 'foo' }
const buffer = Buffer.from('FOO=bar\nBAR=foo')
const config2 = dotenv.parse(buffer)
console.log(config2) // { FOO: 'bar', BAR: 'foo' }

可以看到,dotenv的使用非常简单,通常我们只需要调用config方法即可。


还有一种方法是预加载,直接通过node -r dotenv/config来运行脚本,这样就不需要在脚本中引入dotenv了。


源码


源码在lib/main.js中,先来看一下全部的代码:

const fs = require('fs')
const path = require('path')
const os = require('os')
const packageJson = require('../package.json')
const version = packageJson.version
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\'|[^'])*'|\s*"(?:\"|[^"])*"|\s*`(?:\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
// Parser src into an Object
function parse (src) {
  const obj = {}
  // Convert buffer to string
  let lines = src.toString()
  // Convert line breaks to same format
  lines = lines.replace(/\r\n?/mg, '\n')
  let match
  while ((match = LINE.exec(lines)) != null) {
    const key = match[1]
    // Default undefined or null to empty string
    let value = (match[2] || '')
    // Remove whitespace
    value = value.trim()
    // Check if double quoted
    const maybeQuote = value[0]
    // Remove surrounding quotes
    value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
    // Expand newlines if double quoted
    if (maybeQuote === '"') {
      value = value.replace(/\n/g, '\n')
      value = value.replace(/\r/g, '\r')
    }
    // Add to object
    obj[key] = value
  }
  return obj
}
function _log (message) {
  console.log(`[dotenv@${version}][DEBUG] ${message}`)
}
function _resolveHome (envPath) {
  return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
}
// Populates process.env from .env file
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
    }
  }
  try {
    // 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]
        }
        if (debug) {
          if (override === true) {
            _log(`"${key}" is already defined in `process.env` and WAS overwritten`)
          } else {
            _log(`"${key}" is already defined in `process.env` and was NOT overwritten`)
          }
        }
      }
    })
    return { parsed }
  } catch (e) {
    if (debug) {
      _log(`Failed to load ${dotenvPath} ${e.message}`)
    }
    return { error: e }
  }
}
const DotenvModule = {
  config,
  parse
}
module.exports.config = DotenvModule.config
module.exports.parse = DotenvModule.parse
module.exports = DotenvModule

可以看到最后导出的是一个对象,包含了configparse两个方法。


config


config方法的作用是读取.env文件,并将其添加到process.env中。

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

首先定义了一些变量:


  • dotenvPath.env文件的路径
  • encoding是文件的编码
  • debugoverride分别表示是否开启调试模式和是否覆盖已有的环境变量。
if (options) {
  if (options.path != null) {
    dotenvPath = _resolveHome(options.path)
  }
  if (options.encoding != null) {
    encoding = options.encoding
  }
}

然后判断了一下options是否存在,如果存在的话,就会根据options的值来修改dotenvPathencoding的值。

const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }))

然后是调用parse方法来解析.env文件,parse方法的实现在下面会讲到。


这里是只用fs.readFileSync来读取.env文件,然后将其传入parse方法中,接着往下:

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]
        }
        if (debug) {
            if (override === true) {
                _log(`"${key}" is already defined in `process.env` and WAS overwritten`)
            } else {
                _log(`"${key}" is already defined in `process.env` and was NOT overwritten`)
            }
        }
    }
})

这里是遍历parsed对象,然后将其添加到process.env中,如果process.env中已经存在了该环境变量,那么就会根据override的值来决定是否覆盖。


debug的值表示是否开启调试模式,如果开启了调试模式,那么就会打印一些日志。


最后就是直接返回parsed对象。


parse


parse方法的作用是解析.env文件,将其转换为一个对象。

const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\'|[^'])*'|\s*"(?:\"|[^"])*"|\s*`(?:\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
function parse (src) {
  const obj = {}
  // Convert buffer to string
  let lines = src.toString()
  // Convert line breaks to same format
  lines = lines.replace(/\r\n?/mg, '\n')
  let match
  while ((match = LINE.exec(lines)) != null) {
    const key = match[1]
    // Default undefined or null to empty string
    let value = (match[2] || '')
    // Remove whitespace
    value = value.trim()
    // Check if double quoted
    const maybeQuote = value[0]
    // Remove surrounding quotes
    value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
    // Expand newlines if double quoted
    if (maybeQuote === '"') {
      value = value.replace(/\n/g, '\n')
      value = value.replace(/\r/g, '\r')
    }
    // Add to object
    obj[key] = value
  }
  return obj
}

首先定义了一个正则表达式LINE,用来匹配.env文件中的每一行。


然后是将src转换为字符串,然后将换行符统一为\n


接着就是核心,通过正则表达式的特性通过while循环来匹配每一行。


这个正则着实有点复杂,我是正则渣渣,可以在regex101查看一下。

image.png

这个正则上面标出了三种颜色,和下面的匹配的值的颜色相互对应,然后右边会展示匹配的值。


这里我不过多解读,可以自己去看一下,然后输入不同的值对比一下结果。


通过上面的截图可以看到匹配会捕获两个值,第一个是环境变量的名称,第二个是环境变量的值。


然后对值进行处理,首先去掉首尾的空格,然后通过正则去掉首尾的引号,最后再将转义的换行符转换还原。


经过上面的处理,就可以将每一行的环境变量添加到obj对象中了,最后返回obj对象。


总结


dotenv真的是非常惊艳的一个库,没有任何依赖,只有一个文件,而且功能也非常强大。


如果你将README中的内容全部看完,你还会发现dotenv还有很多其他的功能,都是一些很实用的功能,并且还有很多引导你如何使用的例子。


目录
相关文章
|
JavaScript
源码学习:Vite中加载环境变量(loadEnv)的实现
源码学习:Vite中加载环境变量(loadEnv)的实现
7233 0
|
5月前
|
存储 运维 Serverless
函数计算产品使用问题之在YAML文件中配置了环境变量,但在PHP代码中无法读取到这些环境变量,是什么原因
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。
|
3月前
|
Python
Python 代码从 `.env` 文件中读取环境变量
这篇文章介绍了如何在Python项目中使用`python-dotenv`库从`.env`文件读取环境变量的详细步骤,包括安装库、创建`.env`文件、在代码中加载和读取环境变量。
|
4月前
|
Java Serverless 应用服务中间件
函数计算操作报错合集之JVM启动时找不到指定的日志目录,该如何解决
Serverless 应用引擎(SAE)是阿里云提供的Serverless PaaS平台,支持Spring Cloud、Dubbo、HSF等主流微服务框架,简化应用的部署、运维和弹性伸缩。在使用SAE过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
4月前
|
开发工具
环境变量,环境变量就是在操作系统中记录的一些关键性信息,以辅助系统运行,env,echo $PATH可以取出环境变量,全局变量的使用方法是定义,什么时候用,什么时候取,export MYNAME=it
环境变量,环境变量就是在操作系统中记录的一些关键性信息,以辅助系统运行,env,echo $PATH可以取出环境变量,全局变量的使用方法是定义,什么时候用,什么时候取,export MYNAME=it
|
6月前
|
存储 Linux Shell
【Linux系统编程】环境变量--1
【Linux系统编程】环境变量--1
|
6月前
|
Linux Shell
【Linux系统编程】环境变量--2
【Linux系统编程】环境变量--2
|
测试技术 数据库
如何用nest中对环境变量等文件进行配置
如何用nest中对环境变量等文件进行配置
|
数据采集 安全 Unix
[oeasy]python0029_放入系统路径_PATH_chmod_程序路径_执行原理
[oeasy]python0029_放入系统路径_PATH_chmod_程序路径_执行原理
138 0
[oeasy]python0029_放入系统路径_PATH_chmod_程序路径_执行原理
|
Java 编译器
jdk的环境变量配置,解决javac不是内部命令的问题(配图)
jdk的环境变量配置,解决javac不是内部命令的问题(配图)
jdk的环境变量配置,解决javac不是内部命令的问题(配图)