自Web项目诞生以来,程序员们渐渐把整个项目的开发拆分为了前端与后端两个部分。随着项目复杂度的不断增加,同时为了快速迭代出更多Web项目,IT界就把两个部分的工作分成了两个工种来完成--前端开发工程师以及后端XX语言开发工程师(我们在此简称为FE与RD)。所以,在一个Web项目开发过程中,就出现了前后端定义数据接口、参数等等工作,同时产生了一个巨大的耦合问题--前端工程师完全需要依赖后端工程师的数据接口以及后端联调环境。当一个FE快速完成了页面的搭建,需要后端数据来完成页面交互等工作时,他唯一能做的就是等待RD完成他的工作,有时甚至还需要RD来搭建一个联调环境等等。
也许更多时候情况比我上述所说的更复杂,需要项目管理者协调好项目进度、前后端协同开发等等问题。当然更多时候FE希望自己能解决这些问题,靠别人不如靠自己,对吧?这时候为了不依赖后端工程师,可以自己搞定后端环境及数据,都需要准备什么呢?下面我来举例:
如果可以达到以上要求,你便能拥有一套完整的本地开发环境,拥有独立开发前端项目的环境以及解决与后端开发的耦合问题。使用过FIS2.0的用户也许会发现,FIS2.0可以完整解决以上的问题,在任何地方、任何环境都可以独立开发,接下来我为大家介绍下FIS本地开发环境是如何做到这些要求的。
我们需要一个什么样的Server?
以上这些就是我们总结出一个合理可靠轻巧的服务器需要达到的要求,它是本地开发环境的基石。面对需求,我们需要的就是解决这些需求,当然从最开始用很挫的方式一步一步尝试,到最后发现需要怎么实现这个server,经历的痛苦也是不言而喻的。最后我们发现需要实现以下内容以及找到了一些匹配的“轮子”来做这些事:
我们需要一个强大的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中,有几个比较重要的模块:
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,你依然可以解决这个问题。
当然你会想到既然我们使用了Jetty,就可以使用其rewrite配置,但这并不能达到我们的要求而且过于复杂。比如我们要运行PHP项目时,我们可能需要在FastCGIServlet处理URL转发的问题,相当麻烦,而且使Server不够独立、过于复杂。所以我们是这样做的:
所以我们的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可预览页面,具体的大家可以动手尝试下[Fis官网](http://fis.baidu.com)提供的Demo。
纵观以上流程,你会发现fis-plus提供的整个本地调试预览都是依赖本文最开始提出的三个要求来进行打造的,你可以根据项目以及环境的实际情况打造属于你们团队自己的调试平台。一个好的易用的本地调试辅助开发工具,可以有效的提高开发联调效率同时减少一些繁琐重复甚至是烦人的环节。程序员们当然希望拥有更多的时间去面对写码,而不是浪费太多时间以及精力在搭环境、催单子、找接口人等环节。FIS本地辅助开发工具就是秉着此原则和要求不断挖掘以及通用化FE的本地开发需求,希望可以给大家带来更多的帮助和想法。
袁方,百度Web前端研发部前端集成解决方案(FIS)小组核心成员,负责FIS本地开发环境的设计与开发、参与实现PC端解决方案等项目,全面负责FIS与百度各产品线前端团队合作落地。