《若川视野x源码共读》第22期 源码学习系列之dotenv

472 阅读3分钟

前言

本文参加了由公众号@若川视野 发起的每周源码共读活动。从简单到进阶学习源码中的巧妙之处,旨在于将学习的东西应用到实际的开发中,同时借鉴源码中的思想,规范自我开发,以及锻炼自己的开发思维。也欢迎大家加入大佬的源码共读活动。一起卷起来。

疑问

在工作中我们常常会遇到.env的文件,在这个文件中我们配置的一些环境变量,可以在程序中通过process.env进行获取使用,好多人都不知道.env 中的变量是如何添加到process.env上去的,接下来我们一起去探索。

.env

我们项目尝尝用到的.env文件格式如下:

APP_NAME='测试'
NAME='源码共读'
AGE=7
OTHER='welcome yours'

从上述的疑问中我们可以知道,.env 文件中的内容最后将以对象的形式,扩展到process.env 上 所以我们大胆的猜测dotenv会做如下几件事情:

  • 读取.env文件
  • 将.env文件转成对象
  • 将对象扩展到process.env对象上

初步的实现

根据我们猜测我们对于dotenv 进行简单的实现

const fs =  require('fs')
const path = require('path')
const src= path.join(__dirname,'.env')
//1读取env 文件
function readFile(src){
//异步读取文件
   return  fs.readFileSync(src,'utf-8')
}
//2 将其转成对象
function parse(){
    const obj={}
    //通过回车符号进行分隔
    const strArr = readFile(src).split(/\n/)
    strArr.forEach((item,index)=>{
    ///通过=进行分隔 将获取到对象的key 和value值
        obj[item.split('=')[0]]=item.split('=')[1].replace(/\r/,'')

    })
    return obj
}

//3 追加到process.env 上
function config(){
 const obj = parse()
 //将.env 中的文件对象扩展到process.env 对象上
 for(let key  in obj){
     process.env[key]=obj[key]
 }
}

如上我们就实现了简单的dotenv 是不是很开心,但是呢如果用户想自定义.env的路径 和读取文件的编码方式呢由该怎么办呢,别着急我们一步一步来

可控制的dotenv

const fs =  require('fs')
const path = require('path')
function parse(option){
    let src = path.join(__dirname,'.env')
    let encode='utf-8'
    if(option){
      if(option.url){
          //相对路径 其中os.homedir()获取的是系统的默认路径地址 当是相对路径的时候则拼接系统默认路径否则获取的是用户定义的绝对路径
          src=/\.\//.test(option.url) ? path.join(os.homedir(),option.url):option.url
      
      }
      //编码方式
      if(option.encode){
          encode=option.encode
      }
      
    }
    const obj={}
    const strArr = fs.readFileSync(src,{ encoding:encode }).split(/\n/)
    strArr.forEach((item,index)=>{
        obj[item.split('=')[0]]=item.split('=')[1].replace(/\r/,'')

    })
    return obj
}

//3 追加到process.env 上
function config(){
 const obj = parse()
 for(let key  in obj){
     process.env[key]=obj[key]
 }
}
module.exports.parse=parse
 module.exports.config=config

如上是我们实现的dotenv。那源码是如何实现这部分的呢

dotenv源码

//文件模块
const fs = require('fs')
const path = require('path')
//操作系统
const os = require('os')

const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg

// Parser src into an Object
//将.env 文件转成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
  //匹配env 中的每一行数据
  while ((match = LINE.exec(lines)) != null) {
      //key值
    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][DEBUG] ${message}`)
}

function _resolveHome (envPath) {
    //如果是相对路径的话则配置上主目录 如果是绝对路径则直接使用该路径
  return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
}
//os.homedir()获取当前指定的主目录

// Populates process.env from .env file
function config (options) {
    //加载当前环境下的.env文件
  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
    // 解析执行的路径下的.env 文件 并且将其转成对象
    const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }))

    Object.keys(parsed).forEach(function (key) {
        //如果process.env中没有对应的key数据   node环境中添加对应的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

总结

收获

我们知道了.env 文件添加到process.env的原理,是通过文件读取后形成对象,然后扩展到process.env 对象上的。在这个过程中我们还收获了fs文件模块 fs.readFileSync(path,{encoding:'编码方式'}) 返回的是读取文件的内容. 此外还有node的os模块 os模块则是操作系统模块 os.homndir() 返回的是当前用户的主目录的字符串路径。此外还有正则表达式的学习。