需求背景
我所在的项目组主要负责公司的A产品A1模块的界面开发。经过上半年紧锣密鼓、加班加点地开发之后,终于在7月份在国内的L局点成功上线。当时那个激动啊,苦逼的生活终于过去了,大家都跟我high起来!可是到了下半年,由于公司市场人员的给力表现,又在海外开拓了D局点和T局点,真是喜(yu)大(ku)普(wu)奔(lei)啊!
由于L局点的需求还没有明确,所以L局点的事情先按住不表,先说说D局点的需求。
其实,客户的实际要求也不多,对于界面来说,无非是整体风格要与客户现有的产品保持一致。所以最终预计的工作量就是换换主题色而已,一切都是如此的easy,大家接着high!
简单修改之后就给一线发了一个联调版本。
在一线将版本安装完成之后,与客户联调过程中(我们的产品是要嵌入到客户系统中,作为客户系统的一个模块工作),客户对界面提了很多意见(该局点的客户都比较严(jiao)谨(zhen))。
目前来看,页面的国际化文件要准备两份(国内的L局点中文+英文,海外的D局点英文+德文),样式要准备两套(L局点一套,D局点一套)。如果客户再对页面结构有一定的要求,比如要变更页面的布局、元素等,那么html也要准备两份了。
最终分析来看,最好是能再做一套页面出来。
按照公司现在的策略,代码分支只能有一个,做出来的版本不能有任何对于局点的定制,也就是说这个版本要在所有局点都能安装使用。
现在问题来了,如何在一套代码中支持两套页面呢?
方案制定
我们知道,一旦在页面中引用一个静态资源,那么这个资源的路径就定死了,不能改动。比如,我们的页面上要使用到公司的logo,这个logo图片的路径一旦确定,就不能再修改了,但是D局点要求将logo换成客户的logo。
既然公司不允许在做版本的时候进行任何的定制,那么怎么解决这个问题,想了半天,对现有代码目录做一下调整,最终确定了如下方案:
webapp |
| public |
| base |
| js
| theme |
| default |
| css
| image
| font
| skin2 |
| uiwidget
| custom | js
| theme |
| default |
| css
| image
| font
| skin2 |
| pageL | js
| theme |
| default |
| css
| image
| font
| skin2 |
| pageD | js
| theme |
| default |
| css
| image
| font
| skin2 |
- public目录 存放所有对外开放的静态资源文件,public以外的其他目录外界不可访问。
- base目录 主要放置与后台的数据接口模块,也就是公共Model层。另外还放置公共的、框架性的样式文件以及一些工具类。
- uiwidget目录 放置组件库
- custom目录 页面最终的工作目录
- pageL目录 L局点要求的页面
- pageD目录 D局点要求的页面
剩余要做的:
一是在打包的时候将pageL的内容拷贝到custom目录中,打一个L局点的ui包,再将pageD的内容拷贝到custom目录中,打一个D局点的UI包。最终将这两个ui包打入一个war包中。
二是新增一个脚本,在安装完成之后,根据局点决定使用哪个ui包,删除无用的ui包。
先说明一下为何要这样划分目录。
除了页面变动以外,D局点的另一个重要的需求就是要对公司产品的功能进行一些裁剪,把不需要的功能去掉。因此,不需要的接口也要统统屏蔽掉。所以,此处将所有页面目录都规划到public下,方便后面接口管理。
其他页面的划分,主要还是基于如下图方案的考虑:
对于页面来说,后台的接口是不会变得,所以将与后台的交互模块独立出来,放进base/js目录中,作为公共的Model层并对页面暴露公共的数据API。其他还有一些公共的css、图片等都放到base/theme目录下,作为基础样式。另外,一些公共的utils工具也可以放到base/js目录下。
PageL/PageD 就是根据具体页面具体开发了。我们组使用AngularJS进行开发,使用RequireJS进行JS模块化管理,具体的开发内容不在本篇范围内,按住不表。
下面就要说说具体的构建过程了。
工程构建
前端的朋友都知道,出于对页面性能以及用户体验的考虑,通常要对进行静态资源进行压缩、合并、指定缓存策略等,具体措施网上有很多的讨论,google一下可以翻很多页,这里也按住不表。
我们组采取的措施主要是将css、js文件压缩合并,并对每个文件进行签名并配置永久缓存。图片的话,主要是将图标类的进行合并,然后通过css sprite进行分割。
在构建工程时,之前一直使用的是公司内别人基于gulp开发的的maven插件进行构建,最后由maven负责打包。每次需要修改什么东西的时候都要求助于工具开发者,实在是有一种寄人篱下的感觉。而且功能实在有限,集成到maven中又缺乏足够的灵活性,已经适应不了蓬勃的需求变化啦。
现在前端构建工具这么多,何不自己搞呢,后面也能自由修改和管理。于是就google了一翻。
现在构建工具有很多,主流的主要有gulp、grunt、webpack等,于是我选择了grunt。没有什么别的原因,就是因为grunt的logo比较酷炫。看下图:
前面说了,我们对grunt的需求主要就是js、css压缩合并,静态资源文件签名。另外还有一点需要注意下,就是文件签名后,文件名前面会多一串hash值,所以所有的原来引用这些资源的地方都要修改为签名后的文件名。RequireJS的路径配置也要修改。
看了一下grunt的插件列表,基本满足要求,唯独没有一个靠谱的替换文件名的插件(也有可能是我没有找到)。怎么办,自己动手丰衣足食。
综合考虑了一下,决定采用如下grunt工作流。
clean:build copy cssmin filerev replaceRefrence mkRequirePath clean:package uglify
其中clean、copy、cssmin、filerev和uglify都有现成的grunt插件,而且除了filerev以外都是grunt团队出品的插件,应该比较可靠。其他的两个,replaceRefrence和mkRequirePath,就是我自己开发的了。
replaceRefrence的想法很简单,就是通过filerev插件生成的签名前后的文件映射grunt.filerev.summary
和正则表达式来替换目标目录中的每个文件中的引用。正则表达式也很简单,就是匹配某一路径开头,以具体的文件后缀(.js.html.css.png等)结尾的文件路径字符串。正则表达式的简要形式如下:
var regExp = /(/start/)(.js|.html|.css|.png|.jpg|.gif)/g
其中start就是路径开头。
有了正则表达式,然后通过String.replace(regExp, function(){})
将匹配到的字符串替换掉。具体样例如下:
var fileData = fs.readFileSync(filePath, {encoding:"utf8"});
var newFileData = fileData.replce(regExp, function (str) {
return fileMap[str] ? fileMap[str] : str;
});
注意: 这里的fileMap是
grunt.filerev.summary
经过一定的处理之后的结果。
通过以上方法就可以将html、css、js文件中引用到的其他文件替换成签名后的文件了。
完成上面的工作之后,就只剩一个任务了,就是如何让RequireJS认识这些签名后的文件?我的想法是,通过
requirejs.config({
paths: {
module1: "module1/js",
module2: "module2/js"
}
});
的方式,将签名后的文件路径配置进来。
思路与replaceRefrence一样,还是通过grunt.filerev.summary
来做。具体的思路如下:
如上面的代码所示,我们一般都会在paths中设置某个路径的快捷方式,即
require("module1/a.js") 等同于 module1/js/a.js
所以,我们只需要将这些原始的路径配置拿到之后,根据这些原始配置与grunt.filerev.summary
中的具体路径进行一次匹配处理,就可以将grunt.filerev.summary
输出作为RequireJS的路径配置对象了。
具体的方法如下:
原来grunt.filerev.summary
中一条记录可能是
"module1/js/a.js": "module1/js/1234567.a.js"
现在我们知道,代码中通过
require("module1/a")
加载的js模块,最终要别识别成
module1/js/1234567.a.js
那我们只需要生成这样一条记录:
"module1/a": "module1/js/1234567.a.js"
具体做法就是用原始路径配置中的kv对中的v匹配grunt.filerev.summary
每条记录中的key,如果key以v开头,则将key做如下处理:
key = k + key.substring(v.length);
这样就生成了正确的路径配置了。
最后一步要做的就是将生成的路径配置保存为一个manifest.js文件,插入到RequireJS的下方,如:
<script type="text/javascript" src="path1/require.js"></script>
<script type="text/javascript" src="path2/manifest-2015-11-15.js"></script>
到这里,整个构建过程就基本OK了。