背景简介和挑战
Domino 是 IBM Lotus 下面的一个旗舰产品,由于其提供了多层级的安全解决方案,内置集成的协同服务应用和目录服务并提供灵活的数据库复制机制,因成为很多企业应用的重要平台。
随着企业对用户体验的日益重视以及 Web 2.0 理念的日趋普及(参见 参考资料),越来越多的企业也开始考虑构建基于 Web 2.0 的企业应用。对于 Domino 用户而言,能否平滑快速的实现这一目标呢?
从商业角度讲,Domino 现有客户以及基于 Domino 提供解决方案的合作伙伴最关心的一个问题就是能否在不增加已有投资基础上(包括人员投资)快速提供 Web 2.0 的 Domino 应用。从技术角度来讲,我们知道 Domino 的编程模型和我们常见的明确 Web 编程模式略有差别,更偏向于一种扁平和混合的特点。例如 Domino 的设计元素表单同时兼具显示和存储的功能,也就是说 Domino 的开发人员在构建系统时必须同时考虑前端显示和后端数据结构两方面的问题。在 Domino 这种传统编程模式下,其他有经验的前端 Web 开发人员往往难以直接参与到对应的 Domino 开发团队中,实现有效分工合作开发。此外 Web 2.0 开发比以往也更加强调前端 Web 展示以及其带来的用户体验,而现有的 Domino Designer 在开发和调试前端代码时还是略显不方便。虽然单纯从技术上讲,Domino 完全支持在现有编程模型下开发 Web 2.0 应用。但是在实践中,这些因素 ( 扁平混合的编程模式,Domino Designer 对 Web UI 端开发调试的不便,难以直接有效引入独立前端 Web 开发人员参与合作开发 ) 导致要快速构建高质量、大规模和灵活的 Domino Web 2.0 应用往往存在很多困难。
如何消除现有 Domino 开发人员开发高质量 Domino Web 2.0 企业应用的瓶颈?如何促进其他前端 Web 开发人员也能够加入到 Domino Web 2.0 应用的开发团队,有效进行协同开发?如何简化两类开发人员的合作流程,并最终方便快捷的实现系统集成和部署?
本文在总结以往项目的基础上介绍了开发 Domino Web 2.0 应用的最佳实践。这些最佳实践覆盖了系统开发的整个生命周期。第二节 将介绍在系统设计阶段对编程模型的转换以及开发人员的分工。第三节 将涉及在系统实现阶段的一些常见问题,在结合 Domino 的自身特性及 Web 2.0 应用特点等诸多因素中如何选择最适的实现方法。第四节 将介绍如何实现代码的高效组装和系统灵活部署。
系统设计阶段 - 采用分层架构设计
我们知道,传统 Domino 的编程模型与常见 Web 编程模型略有不同,它是一种基于文档的扁平混合模式。 .
图 1. Domino 开发人员与独立前端 Web 开放人员对于 Web 应用编程模型不一样的理解
在这种编程模型下,要开发一个高质量的 Domino Web 2.0 应用会面临什么问题?一方面对 Domino 开发人员而言,首当其冲的就是要求他们必须在对象存储,应用逻辑以及前端 Web 页面显示等方面同时都具备清晰正确和深入的理解,这无疑提高了开发的技术门槛,传统的 Domino 开发人员无疑承担了更大的压力;但是另一方面,我们也看到,随着 Web 2.0 的日益普及,在 Web 开发的技术社区里也出现了一批相对分工更明确的前端 Web 开发人员,他们往往专注于与 Web 前端相关的技术((X)HTML、JavaScript、CSS、Web 2.0 UI 设计模式等), 并能与其他后台开发人员有效合作,共同开发高质量的 Web 2.0 应用。但是这些常常独立存在的前端 Web 开发人员却往往很难理解传统的 Domino Web 开发模式,难以加入 Domino Web 开发团队并有效释放他们的技术特长。此外使用现有的 Domino Designer 对于前端 Web 开发和调试往往也有诸多不便。这些因素表明,在现有 Domino 编程模式下实现灵活高效的 Domino Web 2.0 企业应用还是存在很多困难。
要消除这种开发瓶颈,首要的方式就是突破传统的 Domino 开发模式的束缚,采取分层的架构设计。下图给出了一个开发 Domino Web 2.0 应用的一般架构。
图 2. Domino Web 应用的分层架构设计
从上面图 2 中可以看到,传统 Domino 扁平混合的模式被转变成分层的架构设计。采用这种方式,整个 Domino Web 2.0 系统被显式地分解为三部分:前端显示,逻辑服务以及数据存储。前端显示与服务层之间通过各类以 URL 形式表现的服务来调用连接,以 Web 2.0 通常采用的 XML 或 JSON 作为交换的数据格式。同时将 Domino 服务器看成是 Web 服务器和数据存储服务器。在这种架构下,前端显示层则可以完全交给独立的前端 Web 开发人员完成,而服务层和数据存储层的相关实现则交由传统的 Domino 开发人员负责。
上面的这种分层结构对于常见的 Web 应用开发人员而言并不新,但是对于 Domino 开发团队在一开始而言确实是一个变化。这种架构要求传统的 Domino 开发人员需要放弃他们所熟悉的那种看待整体系统的混合观点,而更多的将关注点放在系统的后台逻辑及存储。这种架构并不限制 Domino 开发人员使用表单或 page,但是应用这些 Domino 特有的元素是为逻辑服务层和数据存储层服务,并不用于前端显示。这种改变对于传统 Domino 开发人员来说一开始甚至是一种痛苦,不过这种改变可以在后来为提高整个开发团队的效率起到积极的作用。
首先,采用这种模式可以有效地将其他前端 Web 开发人员有效的黏合进来,利用各自的技术专长共同开发高质量的 Domino Web 2.0 应用。对于独立的前端 Web 开发人员而言,他们几乎没有任何进入门槛。他们可以继续利用他们已有的前端 Web 开发技术,利用他们所熟悉的 IDE 对前端代码进行开发和调试。对于他们而言,开发一个 Domino Web 2.0 的应用和之前开发的其他 Web 应用没有什么特别的差别,在某种程度上讲,前端开发人员甚至可以没有任何关于 Domino 特性的理解。(在我们的实践中,我们也确实引入了对 Domino 毫无理解的 Web 开发人员按照这样的分工参与 Domino Web 开发,取得了很好的效果)另一方面,这种做法也没有提高对传统 Domino 开发人员的技术要求,他们过去所掌握的 Domino 开发的技能完全可以处理这种开发,唯一改变的是对编程模型,对整个系统架构的看法。
其次,这种模式可以使得两类开发人员更专注于他们的强项。前端 Web 开发人员可以更多的考虑如何提高 Web 2.0 所强调的用户体验(User experience),后端 Domino 开发人员可以更加专注于如何更好的利用 Domino 的特性解决与后台相关的问题。专注范围的减少最终有助于系统质量的提高。
最后,这种架构设计可以使得前后端的耦合程度大大降低,并加速开发的流程。在我们的实践中,前端 Web 开发人员使用 Eclipse 和 WTP 插件来开发相应的前端代码,并在本地建立一个 Tomcat 服务器,使用预先定义的 XML 片段来模拟 URL 调用后的响应进行测试。当相应的服务逻辑在后台真正实现后,只需在前端的相应代码中将相关的 URL 调整为来自 Domino 的 URL 即可,这种并行开发极大的降低了开发的时间成本。
系统实现阶段 - 常见重要问题的处理
本节将介绍在系统实现阶段,开发人员常遇到的一些问题的处理方法。一般而言,每个问题都有多种不同的处理方法,结合 Web 2.0 的特性,Domino 自身优势以及上面所介绍的分层架构设计等因素,我们会给出这些常见问题的技术实现的最佳实践,帮助开发人员做出合适的技术实现决策。
逻辑服务层的实现方法
在前面的分层架构中,前端显示与逻辑服务层之间是以 URL 的方式进行相应的服务调用,两者之间数据传递的格式采用 XML 或 JSON 。对于这里所涉及到的逻辑服务实现方式,Domino 开发人员共有三个选择:代理(agent),servlet 以及 Domino URL 命令。每一种方法都有一定的优势和不足。
对于 Domino 开发人员而言,首先需要考虑的是能否直接利用 Domino URL 命令来实现这些服务,比如要完成创建或保存一个对象。如能使用这种方式,则可以极大的提高工作效率。使用这种方式时,Domino 开发人员需要在 Domino designer 里面定义相应的表单来表示这个对象,前端 Web 开发人员也需要根据需要定义一个相应的 Web 表单,唯一需要注意的是,这个 Web 表单中的字段名字应该与相应的 Domino 表单中的相应的字段名字一致,这样在前端的 Web 页面中就可以直接使用 Domino URL 命令 (createDocument) 来完成相应表单的提交,直接实现相应数据在 Domino 存储中的创建或修改。
如果你决定自己实现某些逻辑服务,则 agent 和 servlet 是两种考虑。在 参考资料 中有对这两种方式的具体比较。一般而言,从部署的角度讲,agent 优于 servlet 。如果只使用 agent,在后续的部署阶段则无需做其他配置工作。但是从效率的角度而言,由于 Domino runtime 对于 agent 和 servlet 采用不同的生命周期管理,agent 是在每次调用后实行初始化,执行和销毁的全过程,而 servlet 则可以在首次初始化后就驻留在 runtime 容器中为多次请求服务,因此从这个角度讲,servlet 要由于 agent 。对于 Web 2.0 应用而言,通常用户和系统之间小规模的数据交互往往会比较频繁,效率提升往往比部署简化显得更重要,因此对于这种情况,通常适合采用 servlet 作为服务实现的方式。
表 1 对 agent 和 servlet 优劣的比较
从部署的难易程度而言 | Agent 优于 Servlet | 从执行效率的高低而言 | Servlet 优于 Agent | 如果采用 servlet 来实行服务,除了要配置 Domino 服务器文档里面 servlet jar 文件的路径以及修改 Domino 服务器下的 servlets.properities 文件外,还需要注意在 servlet 开发环境中采用 Domino JVM 而非缺省的 Sun JVM, 以防在后面生成 jar 文件部署在 Domino 服务器上出现由于 JVM 版本不一致造成 servlet 不兼容的错误。 .
数据对象的访问权限控制 .
几乎绝大多数 Web 2.0 应用都会涉及到访问权限控制的问题,对于企业应用而言更是如此。开发人员总是需要思考这样一个基本问题,在什么情况下谁可以以什么权限访问什么数据 .
常见 Web 应用开发中,这类问题需要开发人员亲自处理,但是传统 Domino 开发人员通常会利用 Domino 所内置的安全机制来处理这些访问权限控制的问题。那么对于一个 Domino Web 2.0 的应用而言,应该采用什么方式那?
这个问题的回答主要取决于在整个系统解决方案中究竟使用的什么目录服务,如果系统方案采用 Domino 目录服务去进行用户的身份认证,则 Domino 开发人员只需要完成以下几步:
用 Domino 表单定义相应的数据模型,并额外添加两个字段,分别设置其类型为 Readers 和 Authors 。
将合适的用户名或用户组名设置在这两个字段里,或根据需要动态的修改这两个字段里面的用户名或用户组名。
则后续的流程中涉及到对当前用户请求对该数据对象操作权限的验证过程都将由 Domino 自己的安全机制去完成。
图 3. 利用 Domino 的访问控制机制设计相应字段类型
如果使用第三方的目录服务来进行用户验证,则上面的方式就不再适用,需要开发人员自己实现相应的服务来解决访问控制的问题。除此之外还需要注意的是,要对相应的 Domino 数据库的访问权限控制列表进行检查,对缺省用户和匿名用户给予 designer 以上的权限,如下图 4 所示,以确保 Domino Web 2.0 的应用能够被正常访问。
图 4. 自行实现访问权限管理时,对访问权限控制列表中的缺省用户及匿名用户权限设置
附件处理
上传附件(例如文档,照片甚至视频)也是 Web 应用中常见的需求。对于常见 Web 开发人员而言,是直接利用 HTML 元素中类型为 file 的 input 元素或其它一些封装后的控件来实现将文件上传到数据库或文件服务器中。
对于传统 Domino Web 开发人员而言,最简便和通用的做法如下图 5 所示,在一个表单中添加一个 Domino 所提供的 file upload 控件,即将某个字段的类型设置为 File upload 。在设计表单时进行这样的设计后,当该表单在前台展示时,所涉及到的后续用户操作及上传保存附件则都由 Domino 完成。这种方法虽然简便,但是这样使用表单,将会出现前端显示与后端数据表达产生混合的情况,不符合我们之前提出的分层解耦合的思想。
图 5. 传统 Domino Web 应用中使用 Domino 的 File upload 控件来处理附件上传的问题
为了严格遵循前面所提出来的工作模式和分层的架构设计,同时利用 Domino 对附件处理的优势,可以采用如下的方法:
前端 Web 开发人员创建相应的 HTML 页面和 Web 表单
前端 Web 开发人员像以往一样创建相应的 HTML 页面。为了处理附件上传,需要创建一个 HTML 的 Web form,其中包含一个为 file 类型的 input 元素,一个类型为 submit 的 button 以及其他一些必要的 HTML 元素,包括标题,分类等。
唯一需要注意的特殊点是设置 file 类型的 input 元素的名字,必须满足如下的形式:
de><input type="file" name="%%File.X">de>
X 可以是任意一个 string 。当一个 Web 页面的 Form 中包含了多个 file 类型的 input 时,则以此做区分。
Domino 开发人员创建相应的 Domino 表单
Domino 开发人员需要创建相应的 Domino 表单,比如该表单名字为 FmXYZ,作为相应 Web 表单数据的存储对象。类似前面介绍实现逻辑服务时所采用的 Domino URL 命令一样。这个 Domino 表单应该包含除了 file 类型的 input 和 submit 类型的 button 以外的其他所有 HTML 的 Web 表单中的元素。需要注意的是,这个 Domino 表单中的字段的名字需要与 HTML 对应的 Web 表单中各字段的元素名字相同,此外在 Domino designer 中设置这个表单的 enctype 属性为 multipart/form-data。
设置 Web 表单中的 action
由于我们在第二步中建立了一个 Domino 的表单来作为这个 HTML Web 表单的对应物,在 action 中我们就可以直接使用相应的 Domino URL 命令来完成 HTML Web 表单的提交或更新。相应的格式如下
创建文档:/DatabaseName/FmXYZ? CreateDocument
更新文档:/DatabaseName/FmXYZ? EditDocument
下图给出了相应的示意:
图 6. 使用标准 html 元素 -file 类型的 input 来处理文件上传
配置 notes.ini 文件
在 notes.ini 文件中,还需要添加如下的信息:DominoDisableFileUploadChecks=1 。这个设置表明可以让 Domino 服务其来支持标准的 HTML file 类型的 input 元素。
这个做法的关键是利用了 Domino 所缺省保留的一个特殊字段 $File,来简化附件的操作。 这个字段用于在 Domino 的一个表单中存储附件。当 File upload 控件指明了本地上传文件的路径,在保存这个表单的时候,相关的附件也就被引入到这个缺省的保留字段里保存。
XML 的操作
XML 操作会广泛应用于前后端进行数据传递和交换。 Domino 自身对 XML 有很好的支持,我们可以使用 DXL (Domino XML Language) 来处理所有 XML 相关的操作(参见 参考资料),包括 XML 文档的组装和解析等。 DXL 是一种 XML 语言,用于描述一个 Domino 数据库中的数据结构。 DXL 以 tag 的方式来描述相应的数据结构中每个元素的值、属性等。借助于 DXL 处理类(NotesDXLExporter 和 NotesDXLImporte),XML 的处理类 (NotesDOMParser, NotesSAXParser 和 NotesXSLTransformer) 以及相应的 helper 类 (NotesStream and NotesNodeCollection),我们可以完整的实现所有 XML 相关的操作。对于更多的细节,可以参考 Domino designer help guide(参见 参考资料)。
另一方面,如果希望采用更灵活轻便的方式来生成 XML 数据片段,也可以考虑使用如下的某种方法。
1)设计 Domino 元素并利用 Domino URL 命令来生成 XML
采用这种方法的步骤
以 Domino 表单为例,在设计时,Domino 开发人员可以直接将表达中需要对外生成 XML 的部分字段直接设计为 XML well-format 的形式
对于整个表单的属性,设置其 content-type 为 others,并明示为“ text/XML ”
对于不需要输出的表单字段,则在其属性中设置其在 Web 浏览器访问时隐藏
使用相应的 Domino URL command-openDocument 即可以直接得到关于相应数据的 XML 输出流
下图给出了上述过程的一个示例,使用这种方法可以方便直接地产生前端所需要的 XML 数据。
图 7. 利用 Domino 表单和 Domino URL 命令来生成 XML 数据流传
采用相同的想法和思路,Domino 开发人员也可以利用 Domino page,并内嵌某一视图,通过设计 page 和内嵌视图的格式以及配置其输出的 content type 为 others-text/XML, 前端代码也可以通过直接调用 Domino URL 命令中的 openPage 命令,直接得到某一列表信息的 XML 数据表达。需要强调的是,这种方法虽然利用了 Domino 的设计元素中 page 和 form 可以用于前端展示的特点来进行 XML 数据流的输出,不过这个 XML 流被前端代码获取后是如何进行显示和布局,则依旧完全由前端 Web 开发人员控制,因此这种做法与我们之前所强调的分层架构的思想完全一致,并不矛盾。
2)使用 servlet
设置 servlet 的 response 的 content type 为’ text/XML ” , 然后利用任何 XML helper 的 lib 或通过直接串接的方式得到 XML 输出。
清单 1. 使用 servlet 来生成 XML response
de>response.setContentType(“text/XML; charset=GBK”);
PrintWriter pw = response.getWriter();
…
pw.println(stringXMLText)de>
3)使用 Agent
类似的,也可以采用 agent 来实现 XML 输出,在一开始也需要设置 agent 的 response 的 content type 为“ text/XML ”
清单 2. 使用 agent 来生成 XML response
de>Print "Content-type: text/XML"
Print "<? XML version=‘1.0’?>"
…
Print stringXMLTextde>
上面介绍的四种处理 XML 的方法,Domino 开发人员可以根据系统要求以及自身对相关技术熟悉程度,灵活选择。
系统装配阶段 - 快速组装和灵活部署
上面的章节分别强调了在设计阶段将扁平混合的传统 Domino Web 开发模型转换为分层的结构,以有效引入独立前端 Web 开发人员参与 Domino Web 2.0 应用开发,有效降低传统 Domino 开发人员的开发门槛。在系统实现阶段,对于一些常见问题的处理方式和解决思路,使得开发人员可以在分层架构下依然有效利用 Domino 的优势,并在各自的熟悉的开发环境下并行开发。接下来的问题是,当各自并行独立开发完之后,应该如何将前端开发人员的代码组合进来,并以一个完整的解决方案的形式部署在 Domino 服务器上?
由于我们所讨论的是 Domino Web 2.0 应用,所以装配工作一般由 Domino 开发人员完成。通常 Domino 开发人员采用的装配流程如下所示:
Domino 开发人员从 Web 前端的开发人员那里 check out 与 Web 前端相关的代码,包括 HTML,CSS,Javascript 等
Domino 开发人员打开 Domino designer,重新组织这些不同的代码,将其拷贝至 Domino 数据库的不同设计元素下,比如将 HTML 代码放在 Pages 下面,将 CSS 放在 shared resources/Style sheets 下面,将 Javascript 放在 shared resources/Files 下面
然后保存新修改的 Domino nsf 数据库文件
在部署阶段,Domino developer 或 Domino administrator 会采用如下的流程完成系统的部署更新:
将新版本的 nsf 文件重新拷贝,部署在 Domino 服务器上
如果有新更新的 servlet jar 文件,拷贝到相应的 Domino 服务器上
如果需要,修改 Domino 服务器文档中有关 servlet jar 的路径,以及修改 servlets.properties 文件的配置
重启 Domino 服务器使修改更新生效
图 8. 装配代码及更新部署的常见流程解
一般而言,上面所描述的装配和部署流程没有任何问题。但是我们需要考虑 Web 2.0 的一个重要特征 - “永远处于测试版” ( “ always in beta ”或“ perpetual beta ”) 的情况(参考资料)。这一特征本质上与 Web 2.0 理念中所强调的用户,用户体验以及用户参与等特点紧密相关,这一特征要求相应解决方案要能不断根据用户的反馈和需求的变化进行持续更新,反过来也就要求相应的系统需要足够灵活以应对不断变化。
如果考虑“永远处于测试版”这一特点,则我们会注意到上面所提到的关于代码组装与部署更新的流程在这种环境中将会变得非常低效,以至于最后无法接受。试想采用上述的流程,每一次更新时,Domino 开发人员在 check out 相关的前端 Web 代码后,都必须要清楚的知道哪些前端的代码被修改过,然后逐一拷贝那些修改过的代码文件的内容。在 Domino designer 中打开相应的 Domino nsf 数据库,粘贴到的特定位置的设计元素里,或者当前端的某个代码文件被删除时,Domino 开发人员需要记住这些被删除的代码,并用 Domino designer 打开 Domino nsf 数据库,在特点的位置下将对应的设计元素删除。随着系统开发的不断变化,这种装配方式所引起的时间开销将随着系统代码变化的数量而迅速增加,以至到达无法接受的地步。
为了提高装配阶段的工作效率,我们采用了新的装配形式。不同于以往 Domino 开发人员所熟悉和惯用的将相关前端 Web 代码放在 Domino 数据库文件中的特定设计元素下,我们将整个与前端 Web 相关的代码整体直接放在 Domino 服务器的 html 目录下,(例如:~ ~DominoInstallPathdataDominohtml)。下表对比了使用这两种装配方法的不同。
表 3. 两种装配方式的比较
| 常用的装配方法 | 新的装配方法 | 整个 Domino Web 系统的组成部分 | | | 特殊组件的部署路径 | Servlets 需要部署在以下目录中 ~ DominoInstallPath dataDominojava 或 ~ DominoInstallPath dataDominoservlets folder
| Servlets 需要部署在以下目录中 ~ DominoInstallPath dataDominojava 或
~ DominoInstallPath dataDominoservlets folder
前端 Web 代码需要部署在以下目录里 ~ DominoInstallPath dataDominohtml folder
| 装配的人员 | 只有 Domino 开发人员可以完成这种装配工作 | 开发团队中的 Domino 开发人员以及前端 Web 开发人员都可以参与完成相应部分代码的装配和更新 | 对外的 URL 格式 | http://hostname/xyz.nsf/abc.html | http://hostname/xyz/abc.html | 使用我们所提出来的方法可以极大的提高整个开发团队组装代码的效率。
首先,不同于常用的装配方法,装配工作必须由 Domino 开发人员完成,需打开 Designer,将前端发生变化的代码一一覆盖在特定的设计元素类别下,时间开销与前端修改的代码文件数量成正比;新的装配方法直接将前端的代码拷贝或覆盖到 Domino 服务器的特定文件目录下即可。因此整个装配更新的时间开销几乎不变,同时这种更新由于无需在 Domino designer 里完成,因此装配更新的工作可以由整个开发团队中的任意一个人完成。从而提高了代码组装的效率。
另一方面,使用这种方法来装配一个 Domino Web 2.0 应用,也可以同时在部署方面带来很强的灵活性。结合 Domino 自身的一些特性,下面给出了采用这种装配方式后可以支持的一些不同的部署场景。
图 9. 部署场景一
图 10. 部署场景二
图 11. 部署场景三
图 12. 部署场景四
图 13. 部署场景五
结论
在以上的章节中,我们介绍了开发 Domino Web 2.0 应用的一些最佳实践。这些最佳实践可以帮助现有 Domino 客户在不增加投资的基础上提供 Web 2.0 的 Domino 应用。同时从技术方面,这些最佳实践有助于消除采用传统 Domino Web 模式下开发高质量企业应用所遇到的瓶颈,有效引入独立前端 Web 开发人员加入到 Domino Web 应用的开发团队中发挥其技术特长,同时简化两类开发人员合作开发的流程和代码组装。这些最佳实践所覆盖的工作模式可以由下图来描述。
图 14. 共同开发 Domino Web 2.0 应用的工作模式示意
在 第二节 中,我们强调了在系统设计阶段将整个系统架构由 Domino 开发人员所熟悉的扁平混合的编程模型调整为分层的结构。这样可以有助于其他独立的前端 Web 开发人员真正容易地加入到整个 Domino Web 开发团队中,并发挥自己的技术特长。两类开发人员可以在各自熟悉的 IDE 中并行开发和测试,最终降低开发高质量 Domino Web 2.0 应用的瓶颈。第三节 覆盖了在系统实现阶段的一些常见问题,结合 Web 2.0 的基本特征,Domino 编程的一些独特优势以及分层架构设计等考虑因素,给出了关于这些问题的不同实现方法或最佳实践。第四节 主要介绍了如何在‘永远处于测试版”的开发环境下,选择有效的系统装配方法来组装来自前端 Web 开发人员的代码,提供一个统一的解决方案,并最终支持灵活多样的系统部署场景。
作者:张 强 责编:豆豆技术应用
文章来源:http://yiyihuazi.blog.163.com/blog/static/151874052010012104543446/ |