.env
文件在由 creact-react-app 生成的项目里是一个关于当环境的配置文件(先不去管这个文件的起源),我们可以在这个文件里写一些配置,然后通过某种手段在代码里读取、应用这些配置,比如我们会有开发、staging、生产等环境,每个环境的配置都不尽相同,最基本地,每个环境请求的后端服务器的 url 就不相同,自己 mock 的时候的本机地址、与后端联调时候的局域网的地址,staging 的域名和正式上线环境的域名,之前的做法是一般会在 api 请求相关的目录下有 config.js
这样一个文件,那些配置就当成一个常量写到这个文件里,然后导出供其他需要的模块引用,那么问题就来了,每次切换环境时都需要去更改这些配置变量为对应环境的值,这是一件很繁琐的事情,一般我是
const HOST = 'xxx.xxx.xxx'
// const HOST = 'yyy.yyy.yyy'
在用哪个环境的时候就把另一个环境的赋值语句给注释掉,然后后面如果在需要 build 的时候把多余的注释掉的删掉,当然,删除注释的工作可以由工具来做,但是开发时一堆无关代码的注释躺在那里看起来很恶心,然后我就想能不能不管怎么切换环境,需要放到浏览器上去跑的那段代码永远不用变,而只通过启动时输入不同的命令就可以应用对应的配置,比如 npm run start:mock
表示使用本地的 mock 环境,同时开启本地的 mock 服务;npm run start
表示使用默认的开发环境,使用后端提供的环境,然后 npm run build
表示正式上线环境的编译;npm run build:staging
表示 staging 环境的编译。然后我就找到了 .env 文件。
要实现上面说的那些,我们需要用到 dotenv
和 dotenv-webpack
这两个 npm 包(前提是在通过 create-react-app 创建的项目里,同时还要 run eject
,当然其他脚手架生成的项目肯定也可以做到这样,因为凭自己目前的功力还没有办法自己捣鼓出一个完成的项目骨架,所以就先把 create-react-app 研究透)。关于 .env
文件的用法,就不多说了 https://github.com/motdotla/dotenv 里有,然后我看到里面:
Should I have multiple .env files?
No. We strongly recommend against having a "main" .env file and an "environment" .env file like .env.test. Your config should vary between deploys, and you should not be sharing values between environments.
里面是不提倡用多个 .env.*.*
这样的文件的,可是 create-react-app 里则提倡用多个这样的文件,比如 .env.development.local
、.env.development
、.env.local
……这样的,然后读取这些配置的时候有个优先级,这些代码都写在项目根目录下的 config/env.js
里的,然后在切换环境的时候都会执行一遍这个文件,读取对应的配置,我暂时还没弄懂它读取所有 .env.*.*
文件的用意。但是我目前也没想到如何单凭一个 .env
文件而能实现我想要的东西,所以我也用的多个 .env 文件,但是我稍微改写了env.js
、start.js
、webpcakDevServer.js
、webpack.config.dev.js
和 webpack.config.prod.js
,使得一个环境对应特定的 .env 文件,每个环境里的通用配置的字段都一样,然后正式代码里只用读取这些字段就可以了,这些字段会根据不同的环境提供不同的值。其起作用的原理 https://github.com/mrsteele/dotenv-webpack 里有写:
The plugin can be installed with little-to-no configuration needed. Once installed, you can access the variables within your code using process.env as you would with dotenv.
就是说,在编译时,将正式代码里引用了 .env
文件配置的变量直接替换成对应的值,比如 .env
里有 HOST = xxx.xxx.xxx
,然后正式的 js 代码里有 const host = process.env.HOST
,然后在编译过程中 HOST
这个变量会被替换成 xxx.xxx.xxx
,所以编译后的代码就会是 const host = 'xxx.xxx.xxx'
,相当于对正式 js 代码提供了一个全局变量 process.env.HOST
,但是这个变量只在编译时有效。dotenv-webpack
这个插件的用法:
const Dotenv = require('dotenv-webpack');
module.exports = {
...
plugins: [
new Dotenv()
]
...
}
用了这个插件后 new webpack.definePlugin({...})
这条语句实现的功能可一并由 dotenv-webpack
来实现了,只需要在对应 .env
文件里写上 NODE_ENV = development
或 NODE_ENV = production
就可以了。此外,new DotEnv()
接受一个配置对象,里面有个 path
字段,值是加载的 .env
文件的路径,这就允许我们根据不同的条件加载不同的 .env.*.*
文件,这是实现我想要的功能的关键(不过这也应该是一个插件必然会提供的)。
下面我以默认开发环境和 mock 环境为例,简单说下大致细节。这二者都算开发环境,但还是可能会在某些时候需要区分。现在我为 mock 环境配一个 .env.development.local
文件,然后当我执行 npm run start:mock
时让 new DotEnv(option)
加载这个文件,而在 npm run start
时加载 .env.develpment
文件。 npm run start
真正执行的是 node ./scripts/start.js
,然后这个 start.js
文件里有对 webpcak.config.dev.js
这个文件的引用,需要用到它导出的 webpack 的配置,这是一个静态的对象,所以如果我们在 webpcak.config.dev.js
里将 new DotEnv(option)
里的 option 写死的话,那么我们对于多个 option
就需要导出多个 webpack 配置,所以可以将 webpcak.config.dev.js
的导出做成一个接受一个 option
的函数,函数的返回值是应用了这个 option
而生成的配置对象。然后我们需要一个变量来决定 option
的取值,这个变量需要在 start.js
里出现,然后这个变量显然不能是通过在 start.js
文件里通过 const arg = 'xxx'
来生成而只能通过外部传入,怎么传入呢?很自然想到在命令行里传入,然后再对应的脚本文件里读取 process.argv
来获取。于是:
// package.json
{
...
"start:mock": "node ./scrips/start.js mock"
...
}
将 mock 这个字符串传到 start.js
里,然后 start.js
通过判断 process.argv[2] === ‘mock’
来决定加载哪个 .env
文件。加载其他 .env
的原理一致。
此外,当我们切换到 mock 环境时候,还有一件事情要做,就是开启本地的 mock 服务,当然我们可以在输入 npm run start:mock
之后再又去手动开启 mock 服务,但步也可以放到 npm run start:mock
命令去完成,通过 npm-run-all
这个 npm 模块。
然后,通常 mock 服务是不是也会有个类似的 config.js
文件,但是这里我们既然已经有了 env.development.lcoal
那便不必再费事去多维护一些多余的数据了。通过
const config = require('dotenv').config({
path: dotEnvFile
}).parsed
就可以读取一个由 .env
里的配置转换而成的配置对象,然后这就保持正式 js 代码里的配置值和实际 mock 服务的配置永远一致了。