本文转载自微信公众号「程序员鱼皮」,作者鱼皮 。转载本文请联系程序员鱼皮公众号。
大家好,我是鱼皮,今天给大家分享企业项目开发的重要知识 —— 多环境。
本文大纲:
鱼皮 - 多环境技术大纲
什么是多环境?
先思考一个问题。
假如我们有一个数百万用户正在用的网站,网页文件部署在几台服务器上。那现在我们要开发上线一个新功能,应该怎么做呢?
老弟小阿巴问:写好代码后,直接更新服务器上的网页文件么?
我一巴掌抽过去:那万一你的代码有 Bug,不就影响到线上用户的使用了么?
老弟思考了下:那写好代码后,在本地测试运行没有问题后,再发布上线?
我:思路不错,但问题在于,如果本地和线上运行项目时,连接的是同一个数据库,那么当你在本地测试向数据库中插入乱七八糟的假数据、或者修改数据库表结构时,不就会影响到线上的数据了么?
老弟一拍手:对哦,那如何让本地的测试不影响到线上项目呢?
这就需要 多环境 。根据实际需要,将同一个项目(或同一套代码)按照一定方法进行区分,并将所需资源和项目本身部署到不同的机器上。不同环境的项目可以有 不同的行为 ,且能够 同时存在、互不影响 。
举个例子,可以给线上项目搭建一套开发环境,开发环境的数据存储在独立的开发数据库,并且为了调试方便,不需要登录也能够访问所有的用户数据:
这样一来,本地和线上的项目就完全隔离开了,开发者在本地想怎么折腾就怎么折腾!这便是多环境的好处。
常用环境
多环境听起来虽然挺爽的,但事实上,环境不是区分的越多越好!
一方面是搭建多环境需要额外的工作量;另一方面是项目依赖的资源越多,成本就越高,而且维护起来也更麻烦。
因此,企业中常用的环境也就那么几种,都快成为一种约定俗成的规范了,下面给大家介绍一下。
不同团队区分环境的方式可能不同,仅供参考。
本地环境
一般用 local 标识,是指前端或后端独立开发、自主测试的环境。通常就是让项目和依赖在我们自己的电脑上运行,比如数据库、缓存、队列等各种服务,可能需要自己在本地搭建。
本地环境
开发环境
一般用 dev 标识,是指前端和后端(或者多个程序员)一起协作开发、联调的环境。通常将项目和依赖放在员工电脑可以直接访问的开发机上,不用自己搭建,直接跑起来项目,提高开发和协作效率。
对规模不大的团队来说,开发和本地环境其实有一套就够了,毕竟本地也可以连接公用的数据库等服务。
开发环境
测试环境
一般用 test 标识,是指前端和后端开发和联调完成,做出完整的新功能后,交给测试同学去找 Bug 的环境。
通常在测试环境需要有独立的测试数据库和其他服务,让测试同学大显身手。每次修改完 Bug 后,也都要再次发布项目到测试环境,让测试同学重新验证。
测试环境
预发布环境
一般用 pre 标识,这是和线上项目最接近的环境,一般是测试验证通过、产品经理体验过后,才能将项目发布到这个环境。
实际上,预发布环境的项目调用的后端接口、连接的数据库、服务等都 和线上项目一致 ,和线上唯一的区别就是前端访问的域名不同。
正因如此,预发布环境看到的都是真实的用户数据,可以发现更多测试环境因为数据不足而没查出来的 Bug。
预发布环境
生产环境
一般用 prod 标识,又叫线上环境,是给所有真实用户使用的环境。
因此不能随意修改,且发布项目到该环境时必须格外小心。线上的数据库、机器等资源一般也是由专业的运维来负责,想要登录机器、修改配置,都需要经过严格审批。
生产环境
如何实现?
最后再介绍下多环境的实现方式,其实大同小异,遵循 3 个步骤:抽象配置类 + 配置文件化 + 注入环境参数,就能轻松实现。
抽象配置类
将项目代码中需要根据环境的变化而更改的变量整理到一个或多个配置类中,集中管理。
举个例子,连接数据库时,我们需要数据库 IP、端口、配置等信息,代码如下:
- // 数据库基本信息
- DB db = new DB();
- db.setIp("10.0.0.1");
- db.setPort(3306);
- // 数据库连接配置
- DBConnection c = new DBConncetion();
- c.setTimeout(1000);
我们可以将这些代码中写死的值全部替换成变量,将同类变量放到一个类中:
- // 数据库配置类
- class DBConfig {
- String ip = "10.0.0.1";
- int port = 3306;
- long timeout = 1000L;
- }
然后从这个类中读取变量的值:
- DB db = new DB();
- DBConfig cf= new DBConfig()
- // 从类中获取
- db.setIp(cf.getIp());
- db.setPort(cf.getPort());
- DBConnection c = new DBConncetion();
- c.setTimeout(cf.getTimeout());
这样的好处是,如果代码中还有其他地方用到了这些变量,也都可以从同一个类去获取,而不是把 死值 重复写多次,难以维护。
配置文件化
我们可以用专门的配置文件来维护配置,从而让用户修改配置更方便,不用再去找代码、改代码。
常见的配置文件格式有 properties、yaml、yml、json 等,比如新建一个数据库配置文件 db.properties :
- db.ip=10.0.0.1
- db.port=3306
- db.timeout=1000
接下来在初始化数据库时,就可以将配置文件中的值加载到上一步写好的配置类中,然后读取啦:
- // 从文件读取配置的值
- DBConfig cf = new DBConfig("db.properties");
- db.setIp(cf.getIp());
- db.setPort(cf.getPort());
- ...
其实只不过是把配置的值从代码中移到了文件中而已。
但这样一来,我们想加载哪个配置文件就能加载哪个!
比如要搞一套测试环境的配置,只需再新建一个 db-test.properties 文件(文件名中加个环境名称),就能在这个文件中编写独立的配置了,然后在代码中加载该文件即可:
- new DBConfig("db-test.properties");
无论是前端还是后端,大部分的多环境实现都是这个原理 —— 搞多套配置,所以总能在项目中看到类似的配置文件:
多环境配置文件
注入环境参数
到目前为止,其实我们还是在代码中写了 死值 ,来告诉程序应该加载哪个名称的配置文件。
比如在本地开发时,加载 db-dev.properties ,开发完成后、正式上线前,再改代码为加载 db-prod.properties。
但这样不仅麻烦,而且可能忘了修改,把开发环境的项目发布到了线上。
最理想的效果应该是:无论项目要切换到哪个环境,整个项目都完全不用修改。
因此,我们可以将 指定环境 这件事放到最后,在通过命令去打包或者启动项目时,将环境参数写进去。
举个例子,我们在启动 java 项目时,给 env 系统变量传递不同参数:
- # 测试环境
- java -jar -Denv=test dist.jar
- # 生产环境
- java -jar -Dend=prod dist.jar
然后在程序中读取该参数,加载对应的配置即可:
- // 读取 env 参数
- String env = System.getProperty("env");
- new DBConfig("db-" + env + ".properties");
同理,对于前端项目,可以在打包构建时传入环境变量,然后自己在代码中读取,或者交给 Webpack 之类的打包工具处理:
- {
- "scripts": {
- "serve": "env=dev serve",
- "build:test": "env=test build"
- "build": "env=prod build"
- }
- }