为 Single Page App 提供运行时环境变量

最近攻克了一个之前部署 single-page-app 的一个痛点:支持在运行时环境变量。这里讲述一下问题以及目前的解决方案。

SPA 没有运行时环境变量的痛点

目前我的绝大部分的项目都是一个前后端分离的方式开发的。其中前端基本都是用 create-react-app 创建出来的标准的 react 的 spa 应用。这种 spa 在部署是将所有的 js 和 css 打包成一个或多个文件然后用 serve 或者其他类似的 http server 以静态文件的形式对外提供服务,但是这种前端静态文件话的应用没有 nodejs 的支持,没办法使用 process.env 这样的运行时注入环境变量的功能。

目前 create-react-app 提供了一个编译运行时环境变量的方案,因为在 build 的时候是有 nodejs 支持的,通过 REACT_APP_API_URL=http://xxx.com yarn run build 的方式在编译 spa 的时候注入环境变量。那么编译时的环境变量能不能解决问题呢?看情况了...可以做一个简单的对比。

  1. 要知道我们通常要把什么样子的环境变量注入到 spa 中。额,我这里的需求很有限,为了让前后端一起运作,我所需要的环境变量就是后端 API 的入口。对于部署流程简单到之后生产环境且生产环境固定(尤其是后端生产环境 IP、域名固定)的情况,直接在编译时将后端的入口写死注入就行了。但如果有多个环境(staging)的需求就不适用了,假如没有运行时环境变量的支持为不同的环境提供不同的入口只能重新编译应用并注入不同的变量。

  2. 有没有需求在应用运行时修改我们的环境变量。很明显运行时的环境变量支持通过重启就能修改环境变量的功能,如果有这种灵活修改环境变量的情况,编译时环境变量很明显也不能满足。

  3. 在编译时对代码选择和裁剪。很明显,这个是最应该使用编译时环境变量的地方了。

说白了,其实不同时期的环境变量的作用是不一样的。两者不可能做到相互替代,在 [1] [2] 两个场景都是使用运行时环境变量比较舒服的地方,采用编译时的环境变量实在是不太方便。下面就介绍一下目前让 spa 应用支持运行时环境变量的方法,这里还是以 create-react-app 的模板为示例。

全局配置 + Docker 化部署

前端没有 process.env 这样的东西,我们只能用 javascript 的全局变量模拟。在将这个打包好的 spa 运行起来的时候,我们需要利用 shell 脚本生成这个 config.js 文件,让它把必要的环境变量翻译成全局变量。然后让默认的入口 html 文件引入这个全局变量文件。

首先,我们需要一段 shell 脚本,把环境变量翻译成 config.js 文件:

#!/bin/bash

if [[ $CONFIG_VARS ]]; then

  SPLIT=$(echo $CONFIG_VARS | tr "," "\n")
  ARGS=
  for VAR in ${SPLIT}; do
      ARGS="${ARGS} -v ${VAR} "
  done

  JSON=`json_env --json $ARGS`

  echo " ==> Writing ${CONFIG_FILE_PATH}/config.js with ${JSON}"

  echo "window.__env = ${JSON}" > ${CONFIG_FILE_PATH}/config.js
fi

exec "$@"

如果我们提供这样的环境变量

export REACT_APP_API_PREFIX=http://petstore-backend.example.com
export CONFIG_VARS=REACT_APP_API_PREFIX

那么所生成的 config.js 文件是这个样子的:

window.__env = {
  'REACT_APP_API_PREFIX': 'http://petstore-backend.example.com'
}

然后,我们需要在 原来的 index.html 模板文件中引入这个我们生成的 config.js 文件:

<!doctype html>
<html lang="en">
  <head>
  ...
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <script type="text/javascript" src="config.js"></script>
  </body>
</html>

这样,我们就拥有了一个 window.__env 的全局对象,它包含了所有的运行时环境变量。我们可以以如下的方式使用它:

axios.defaults.adapter = httpAdapter;

let baseUrl;
let env = window.__env || {}; // 1

if (process.env.NODE_ENV === 'test') {
  baseUrl = 'http://example.com';
} else if (process.env.NODE_ENV === 'development') {
  baseUrl = env.REACT_APP_API_PREFIX || 'http://localhost:8080'; // 2
} else {
  baseUrl = env.REACT_APP_API_PREFIX;
}

const fetcher = axios.create({
  baseURL: baseUrl,headers: {
    'Content-Type': 'application/json'
  }
});
  1. 直接在文件中引入 window.__env 全局变量
  2. 在需要的地方引用其中的变量即可

当然,这种依赖 shell 生成 config.js 的方案只有我们将 spa 打包好的之后才会使用,为了更好的使用这个 shell 我们可以采用 docker 化的方式把其启动流程以 entrypoint 的方式固化在应用的启动流程中。SocialEngine/docker-nginx-spa 就实现了这个方案,是一个很好的用 base image。如果我们需要创建一个支持运行时环境变量的 create-react-app spa 的时候,首先按照上面的步骤修改 public/index.html 并且用 window.__env 作为环境变量使用。然后提供一个继承自 SocialEngine/docker-nginx-spaDockerfile 即可。

FROM socialengine/nginx-spa

COPY build/ /app

其中 build/create-react-app 编译生成静态文件的默认目录。然后打包运行这个应用的方式如下:

$ yarn run build
$ docker build -t spa-app .
$ docker run -e CONFIG_VARS=REACT_APP_API_PREFIX -e REACT_APP_API_PREFIX=http://petstore-backend.example.com -p 3000:80 spa-app

当然,我们本地开发环境不用这么麻烦。只需要在 public/ 目录下自己创建一个 config.js 然后把开发需要的环境变量塞进去就可以了。在 docker 化后,entrypoint 触发的命令会自动覆盖这个 config.js 文件。

这里 是一个样例项目。

相关资料

  1. create-react-app
  2. compile-time-vs-runtime
  3. serve
  4. SocialEngine/docker-nginx-spa

更多内容请见 aisensiy.github.io

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom