自 Web 项目诞生以来,程序员们渐渐把整个项目的开发拆分为了前端与后端两个部分。随着项目复杂度的不断增加,同时为了快速迭代出更多 Web 项目,IT 界就把两个部分的工作分成了两个工种来完成–前端开发工程师以及后端 XX 语言开发工程师 (我们在此简称为 FE 与 RD)。所以,在一个 Web 项目开发过程中,就出现了前后端定义数据接口、参数等等工作,同时产生了一个巨大的耦合问题–前端工程师完全需要依赖后端工程师的数据接口以及后端联调环境。当一个 FE 快速完成了页面的搭建,需要后端数据来完成页面交互等工作时,他唯一能做的就是等待 RD 完成他的工作,有时甚至还需要 RD 来搭建一个联调环境等等。
也许更多时候情况比我上述所说的更复杂,需要项目管理者协调好项目进度、前后端协同开发等等问题。当然更多时候 FE 希望自己能解决这些问题,靠别人不如靠自己,对吧?这时候为了不依赖后端工程师,可以自己搞定后端环境及数据,都需要准备什么呢?下面我来举例:
- 完整的 server 支持,可以真实运行模板、后端程序等,同时最好满足可以跨平台运行、支持常用后端语言 (java、php) 等
- 模拟 url 请求,能够有控制 url 请求的能力,不是单纯的直接访问返回内容的 server,通过控制 url 来解决 ajax 请求模拟返回的功能
- 数据模拟,有本地数据 mock 的能力,分离模板与对后端数据的依赖,使得 fe 能独立于 rd 进行项目开发
如果可以达到以上要求,你便能拥有一套完整的本地开发环境,拥有独立开发前端项目的环境以及解决与后端开发的耦合问题。使用过 FIS2.0 的用户也许会发现,FIS2.0 可以完整解决以上的问题,在任何地方、任何环境都可以独立开发,接下来我为大家介绍下 FIS 本地开发环境是如何做到这些要求的。
一个轻巧独立的服务器
我们需要一个什么样的 Server?
- 可以监听请求,负责对页面请求进行响应
- 后端语言的解析能力,比如可运行 java、php 等,不是简单的静态 Web 服务器
- 易搭建,没有繁琐的安装过程,尽可能的不依赖其他复杂环境
- 性能与可伸缩性,可快速的响应请求,同时稳定响应一定级别的并发数
- 平台需求,可跨平台运行,解决使用不同平台开发的用户需求
- 可靠性,可稳定长期运行,有服务器异常处理机制等
以上这些就是我们总结出一个合理可靠轻巧的服务器需要达到的要求,它是本地开发环境的基石。面对需求,我们需要的就是解决这些需求,当然从最开始用很挫的方式一步一步尝试,到最后发现需要怎么实现这个 server,经历的痛苦也是不言而喻的。最后我们发现需要实现以下内容以及找到了一些匹配的“轮子”来做这些事:
- 为了可跨平台部署及易搭建,我们选择了用 JAVA 开发。
- 为了满足性能需求,我们需要多个 CGI 进程来处理并发请求,同时需要一个队列来保证接受请求不会丢失
- 为了可以运行 PHP 程序,我们选择使用 PHP-CGI 进行解析以及通过 Fastcgi 协议与其通信
- 为了可扩展不同的 Web 服务,我们需要一个可灵活扩展的 Servlet 容器,可处理不同的 Web 应用
- 为了保证服务器的稳定性,我们需要一个服务器守护进程,保证服务器是正常运行的
我们需要一个强大的 HTTP SERVER 来监听请求、分析 HTTP 协议、创建 socket 通讯,同时还得需要一个 Servlet 容器来扩展不同的 Servlet 服务,进行不同 Web 应用的解析,因此我们选择了 JETTY 来作为 SERVER 的 Web 服务器。JETTY 可做作为 Web 服务器嵌入到 JAVA 程序中,而且是轻量级、性能极高的,同时提供灵活可扩展的 Servlet 容器,满足开发针对各类 Web 应用的业务 Servlet。
选择了 JETTY,就意味着 sever 天生就可以运行 JSP/Servlet,剩下我们需要解决的就是如何运行 PHP 了。为了跨平台且满足高效的性能,我们选择了使用 FASTCGI 开发扩展与 PHP-CGI 进行通信,server 将 CGI 环境变量和标准输入发送到 FastCGI 子进程 php-cgi,子进程完成处理后将标准输出和错误信息从同一连接返回 Server。我们为了可以解决并发请求必须启动多个 PHP-CGI 进程,保持可以多线程处理请求。同时需要一个进程管理器对 PHP-CGI 进行管理,比如当 PHP-CGI 处理了几千个请求有内存溢出现象时,需要 KILL 掉重新启动新的 PHP-CGI 进程,以及当 PHP-CGI 进程出现异常情况挂掉后,会及时发现且启动新的进程保持可用的进程数。
当然你会发现做这些事是十分困难的,需要将 HTTP SERVER 与 PHP-CGI 建立链接、通过 FASTCGI 协议封装请求与 PHP-CGI 进行通信等等。我们最开始也尝试过且成功的运行了服务器,只是发现并不是那么完美,特别是在进程管理方面。最后我们发现了一个牛逼且轻巧的东西——php-java-bridge,利用其对 PHP-CGI 封装的 FastCGIServlet, 与 JETTY 进行完美对接,可在多平台且高性能的运行 PHP。其实 php-java-bridge 的作用不止于此,它最厉害的地方是可以在 PHP 中运行 JAVA 程序,这就是另外一个扩展点了。
有了以上这些开源的东西,我们要做的就是把这个服务器如何搭建起来,建立对应的 Web 应用。JETTY 作为内嵌 Web 服务器,需要一个基础的 Web.xml 初始化参数,同时建立 WebAppContext 来负责处理 Web 应用请求。在 Jetty 中,有几个比较重要的模块:
- Connector 负责解析服务器请求并产生应答,不同的 Connector 用于处理不同协议的请求。
- Handler 用于处理经过 Connector 解析的请求并产生应答内容,同样可以通过配置不同的 Handler 来负责处理不同的请求。
- TheadPool:管理和调度多个线程,用于服务于多个连接请求。
- Server 代表一个 Jetty 服务器对象,主要作用是协同 Connector、Handler 和 TheadPool 的工作。
JETTY 启动需要做的事,启动 ThreadPool 线程池,启动设置到 Server 的 Handler,通常这个 Handler 会有很多子 Handler,这些 Handler 将组成一个 Handler 链。最后会启动 Connector,打开端口,接受客户端请求。在启动 handler 时,会启动 Handler 链上的子 Handler,比如我们针对运行一个 Web 应用程序创建一个 WebAppContext,同时在 WebAppContext 初始化时设置处理请求时对应的 Servlet,这样配置的请求都会传送到这个 WebAppContext 进行处理。
下面一段伪代码说明如何启动 Jetty 及配置可处理 PHP 程序的 Web 应用程序:
//context HandlerCollection hc = new HandlerCollection(); WebAppContext context = new WebAppContext(root, "/"); //set default descriptor String descriptor = Thread.currentThread().getClass().getResource("/jetty/Web.xml").toString(); context.setDefaultsDescriptor(descriptor); //Servlet Iterator<Entry<String, String>> iter = map.entrySet().iterator(); while(iter.hasNext()){ Entry<String, String> entry = iter.next(); String key = entry.getKey().toLowerCase(); String value = entry.getValue(); System.setProperty("php.java.bridge." + key, value); } context.addServlet(FastCGIServlet.class, "*.php"); context.addEventListener(new ContextLoaderListener()); hc.addHandler(context); Server server = new Server(port); server.setHandler(hc); try { server.start(); } catch(Exception e){ System.out.print("fail"); }
启动 Server 时,添加一个 WebAppContext 处理请求 php 的文件,同时使用 php-java-bridge 中的 FastCGIServlet 来解析 php 程序,这样一个 PHP 服务器就打造出来了。同理,可以利用这样的原理处理其他支持 CGI 的后端语言程序。
从启动 Server 的代码中,大家可以发现我们会设置一个路由页面路径或者一个工程目录,这相当于 Tomcat、Apache 设置 Web 工程目录,是服务器访问文件的根目录。还有一个需要注意的问题是从 Server 访问的文件都是可以正常上线的代码,而非还需要被“处理”的源码。
源码!= 上线代码,这个关系开发者应该都是了解。随着很多自动化工具的诞生,我们开发的代码都是会经过工具处理后才会发布到线上机器,这个过程就是所谓的编译过程。在本地开发时,我们依然需要将源码编译发布到一个本地临时预览环境中,通过 Server 去访问 Web 工程。这样的目的就是让 Sever 是个完全独立的东西,只是接受 HTTP 请求、分析 URL、解析代码就够了,至于代码编译发布的工作就不需要交给 Server 去做了。而且,也没必要同时再 Server 中实现一遍编译工作,Server 和编译上线是没有关系的,它的责任就是负责本地服务器的作用。
一个可模拟的环境
有了本地运行服务器作为基石,接下来我们就可以打造属于 FE 自己的开发环境。也许你会发现通过 Server 你可以正常访问到你工程中的页面,但是与后端对接的数据接口怎么办?异步请求怎么处理?突然发现还有很多请求问题没有得到完美的解决。在联调环境中,也许 RD 会为项目配置好服务转发规则,不论是使用的是 Aapache、Tomcat, 还是 lighttpd。当然作为本地 Server,你依然可以解决这个问题。
- 制定一套转发配置的规则
- 打造一个 Rewrite 模块,可以与 Server 配合使用
当然你会想到既然我们使用了 Jetty,就可以使用其 rewrite 配置,但这并不能达到我们的要求而且过于复杂。比如我们要运行 PHP 项目时,我们可能需要在 FastCGIServlet 处理 URL 转发的问题,相当麻烦,而且使 Server 不够独立、过于复杂。所以我们是这样做的:
- 一个 conf 文件配置 URL 转发规则
- 一个 PHP 文件充当一个 Route 模块
所以我们的 Server 就需要有两种状态,一种是不转发状态,一种是全部 URL 转发到 Router 文件中。因此我们的 Server 需要这样改造:
//context HandlerCollection hc = new HandlerCollection(); WebAppContext context; if(rewrite){ context = new FISWebAppContext(root, "/" + script); } else { context = new WebAppContext(root, "/"); } //set default descriptor String descriptor = Thread.currentThread().getClass().getResource ("/jetty/Web.xml").toString(); context.setDefaultsDescriptor(descriptor); //Servlet if(hasCGI){ Iterator<Entry<String, String>> iter = map.entrySet().iterator(); while(iter.hasNext()){ Entry<String, String> entry = iter.next(); String key = entry.getKey().toLowerCase(); String value = entry.getValue(); System.setProperty("php.java.bridge." + key, value); } context.addServlet(FastCGIServlet.class, "*.php"); context.addEventListener(new ContextLoaderListener()); } hc.addHandler(context); Server server = new Server(port); server.setHandler(hc); try { server.start(); } catch(Exception e){ System.out.print("fail"); }
同时我们需要构造 FISWebAppContext 继承 WebAppContext 类重写 doScope 方法
class FISWebAppContext extends WebAppContext { private String filename = "/index.php"; public FISWebAppContext(String root, String input) { super(root, "/"); filename = input; } @Override public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { super.doScope(filename, baseRequest, request, response); } }
当我们启动 Server 时,如果添加了 rewrite 参数,便将所有的请求转发到路由页面中,默认是 index.php,路由页面将根据转发配置规则进行 URL 处理。我们可将各类 URL 都由路由页面转发不同的文件中进行处理,比如一个异步请求需要返回 JSON 数据,一个模拟线上 URL 显示模板页面等等。
在 rewrite 模式下,我们会将所有的请求都转发到 index.php 中,比如下面的情况:
当我们把路由页面写成一个固定内容时,任何请求都是返回 index.php 里的内容,所以我们需要根据不同的 URI 来做不同的处理,比如:
这时我们对 URI 为"/a.js"的请求做了特殊处理,返回一个 JS 内容。其实路由页面的作用就是要针对 URI 做不同的处理,就像 lighttpd 一样需要一个配置文件,将线上 URI 转发对应上服务器的各个文件,因此我们也需要类似一个 Rewrite 库以及配置文件:
我们根据 server.conf 的配置来控制 URI 找到对应的文件,当然这是最基本的要求。同时我们也可以模拟一些 Ajax 请求, 来满足异步数据的获取:
你会发现现在解决了很多问题,静态资源可以访问了、异步数据也可以获取了,当然根据不同类型的项目,我们还需要模板。为了达到需求,我们为 Rewrite 模块添加一类转发类型
Rewrite 模块提供灵活的添加转发规则的机制,可让我们在不重启 Server 的情况下动态对转发配置进行修改。根据项目情况可以具体打造路由页面、Rewrite 模块以及 Server.conf, 来合理的模拟开发环境。
大家可能会发现一个问题,我们的路由页面和转发配置放在哪?其实我们的源码会经过编译后发布到本地预览环境中,在没有 rewrite 状态时,Server 会根据 URL 请求直接读取到预览环境中的文件。当启动 rewrite 模式时,Server 会将请求都转发到路由页面,同时路由页面还需要找到转发配置文件进行分析,找到被转发的文件进行渲染。当你发现配置文件、路由页面和你的项目文件都有路径关系时,那就代表其实这三样都应该发布在预览环境中的,同时转发配置文件应该跟随源码进行维护。
我们需要的数据
通过以上努力,我们已经可以正常在本地运行页面了,当然我们还缺数据。通常页面所需要的数据基本分为两种,一种是页面渲染时由后端传入的数据,一种是通过 Ajax 请求获取的数据。通过请求获取的数据,我们可以通过控制 URL 的方式来获取,那由后端传入的数据我们该怎么做呢?
通过以上的路由页面的处理流程可以发现,处理模板有个独立的过程:
在开发的过程中,我们可以建立模板与测试数据的对应关系,将测试数据与源码工程一起维护。当源码工程进行编译发布到预览环境中, 浏览预览环境中的模板时,可以获取测试数据进行渲染, 同时也可对测试数据进行在线编辑。
对于测试数据的管理,一方面我们可以根据与后端的数据接口创建测试数据,一方面我们可以通过某种方式获取线上或沙盒环境的真实数据进行调试。
FIS 的本地开发环境
从上文的介绍中,我们了解到建立一个本地开发环境都需要哪些必须的条件。下面我简单的为大家介绍下 FIS 的本地开发环境是怎么样的。
- 独立、跨平台、高性能的 Server
- 路由页面
- Rewrite 模块
- 测试数据模块
以上四样法宝构成了一个本地开发环境,开发者将开发代码经过 FIS 编译后发布到本地的临时预览目录,通过 Server 可预览页面,具体的大家可以动手尝试下 http://fis.baidu.com ">Fis 官网提供的 Demo。
纵观以上流程,你会发现 fis-plus 提供的整个本地调试预览都是依赖本文最开始提出的三个要求来进行打造的,你可以根据项目以及环境的实际情况打造属于你们团队自己的调试平台。一个好的易用的本地调试辅助开发工具,可以有效的提高开发联调效率同时减少一些繁琐重复甚至是烦人的环节。程序员们当然希望拥有更多的时间去面对写码,而不是浪费太多时间以及精力在搭环境、催单子、找接口人等环节。FIS 本地辅助开发工具就是秉着此原则和要求不断挖掘以及通用化 FE 的本地开发需求,希望可以给大家带来更多的帮助和想法。
作者简介
袁方,百度 Web 前端研发部前端集成解决方案(FIS)小组核心成员,负责 FIS 本地开发环境的设计与开发、参与实现 PC 端解决方案等项目,全面负责 FIS 与百度各产品线前端团队合作落地。
感谢崔康对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论