码迷,mamicode.com
首页 > 其他好文 > 详细

美利金融前端技术架构杂谈

时间:2015-12-06 15:53:58      阅读:162      评论:0      收藏:0      [点我收藏+]

标签:

今天简单说下我厂前端方面一些技术选择。

 

构建工具

构建工具上我们选用了fis2,可以自动化文件压缩、打版本号,而且自带数据mock功能,可以充分实现前后端并行开发。

在选择js模块化方案时候,我们选择了commonjs规范的模块加载,为了降低团队的使用难度,我非常希望使用browserfiy的模式,即把所有require进来的模块打包成一个文件,这样既很舒服的使用了commonjs规范,又无需主动配置文件合并策略。

但研究后发现想要很舒服的配合fis2+browserfiy使用并不容易,还好fis2可以自定义插件,允许在某个时机对js文件做一些自定义操作。

那么当fis在处理js文件的过程中,通过写一个钩子程序递归处理js文件的require,module.exports,用被require的文件的内容来替代模块路径,从而实现一个简单的browserfiy。代码如下:

技术分享
  1 //开启sass
  2 fis.config.set(‘modules.parser‘, {
  3      sass : ‘sass‘,
  4      scss: ‘sass‘
  5 });
  6 
  7 // fis.config.merge({
  8 //     project : { include : [‘page/**‘, ‘static/**‘] }
  9 // });
 10 
 11 fis.config.set(‘roadmap.ext‘, {
 12     sass: ‘css‘,
 13     scss: ‘css‘
 14 });
 15 
 16 var project = ‘/licai-pc‘
 17 fis.config.merge({
 18     statics : project + ‘/static‘,
 19     roadmap : {
 20         domain : ‘//s1.mljr.com‘
 21     }
 22 });
 23 
 24 
 25 var isWatch = process.title.split(‘ ‘)[2].indexOf(‘w‘) != -1
 26 
 27 var myWatch = function (){
 28     var fs = require(‘fs‘)
 29     var table = {}
 30 
 31     function toWatch(f1){
 32         if (!isWatch){
 33             return false
 34         }
 35 
 36         //最多2s触发一次watch改动
 37         var isPlay = false
 38 
 39         fs.watch(f1, function (){
 40             if (isPlay){
 41                 return false
 42             }
 43             isPlay = true
 44             setTimeout(function (){
 45                 isPlay = false
 46             }, 2000)
 47 
 48             var f2List = table[f1]
 49             f2List.forEach(function (f2){
 50                 fs.utimes(f2, new Date, new Date)
 51                 console.log(‘touch ‘ + f2)
 52             })
 53         })
 54     }
 55 
 56     function watch (f1, f2){
 57         if (f1 in table){
 58             if (table[f1].indexOf(f2) == -1){
 59                 table[f1].push(f2)
 60             }
 61         }
 62         else {
 63             table[f1] = [f2]
 64             toWatch(f1)
 65         }
 66     }
 67 
 68     return {watch:watch}
 69 }()
 70 
 71 //fis 插件,模拟browserfiy的require
 72 fis.config.set(‘modules.parser.js‘, function (content, file, settings){
 73 
 74     var fs = require(‘fs‘)
 75     var path = require(‘path‘)
 76     var crypto = require(‘crypto‘)
 77 
 78     var modTable = []
 79     var modLinkTable = {}
 80     var scanReg = /require\([‘|"](.*?)[‘|"]\)/g
 81 
 82     function getMd5(str){
 83         var md5 = crypto.createHash(‘md5‘)
 84         md5.update(str)
 85 
 86         var md58 = md5.digest(‘hex‘).slice(-8)
 87 
 88         //有一定几率出现md58是纯数字,但是firefox不支持window[‘123‘]的情况,所以加前缀
 89         if (/^\d+$/.test(md58)){
 90             md58 = ‘ml-‘ + md58
 91         }
 92 
 93         return md58
 94     }
 95 
 96     function getFullPath(p){
 97         var fullPath = path.join(__dirname, p)
 98         return fullPath
 99     }
100 
101     function getModFile(p){
102         var fullPath = getFullPath(p)
103         var content = fs.readFileSync(fullPath) + ‘‘
104 
105         var windowFunc = ‘window["‘ + getMd5(p.replace(/\\/g, ‘/‘)) + ‘"]‘
106 
107         //如果是tpl文件
108         if (p.slice(-4) == ‘.tpl‘){
109             return ‘//#----------------mod start----------------\n‘ +
110                 windowFunc + ‘= \‘‘ + content.replace(/\r?\n\s*/g, ‘‘) + ‘\‘\n‘ +
111                 ‘//#----------------mod end----------------\n\n‘
112         }
113 
114         //如果是js文件
115         if (p in modLinkTable){
116             for (var relpath in modLinkTable[p]){
117                 var abspath = modLinkTable[p][relpath]
118                 content = content.replace(RegExp(relpath, ‘g‘), getMd5(abspath.replace(/\\/g, ‘/‘)))
119             }
120         }
121 
122         return ‘//#----------------mod start----------------\n‘ +
123             ‘void function (module, exports){\n\t‘ +
124             windowFunc + ‘={};\n‘ +
125             content.replace(/(module\.)?exports/g, windowFunc).replace(/(^|\n)/g, ‘\n\t‘) +
126             ‘\n}({exports:{}}, {})\n‘ +
127             ‘//#----------------mod end----------------\n\n‘
128     }
129 
130     function fillModLinkTable(subpath, requireNameA, requireNameB){
131         if (!(subpath in modLinkTable)){
132             modLinkTable[subpath] = {}
133         }
134 
135         modLinkTable[subpath][requireNameA] = requireNameB
136     }
137 
138     function scanMod(subpath){
139         var modTable2 = []
140         var modContent = fs.readFileSync(getFullPath(subpath)) + ‘‘;
141 
142         var execValue
143         while ( (execValue = scanReg.exec(modContent)) != null ){
144             var requireName = execValue[1]
145             var modPath
146 
147             //如果rquire的是绝对路径
148             if (requireName[0] == ‘/‘){
149                 modPath = path.join(requireName)
150             }
151             else {
152                 modPath = path.join(path.dirname(subpath), requireName)
153             }
154             fillModLinkTable(subpath, requireName, modPath)
155             modTable2.unshift(modPath)
156         }
157 
158         modTable2.forEach(function (mod){
159             var idx = modTable.indexOf(mod)
160             if (idx != -1){
161                 modTable.splice(idx, 1)
162             }
163             modTable.unshift(mod)
164             scanMod(mod)
165         })
166     }
167 
168     //1、是js文件。2、文件名不能下划线打头(下划线的不被release出去)。3、min.js结尾的文件都直接被<script src>
169     if ( (file.filename[0] != ‘_‘) && (file.filename.slice(-4) != ‘.min‘) ){
170         //console.log(file)
171 
172         modTable = []
173         modLinkTable = {}
174         scanMod(file.subpath)
175 
176         //把mods声明放到最前
177         var modsContent = ‘‘
178 
179         modTable.forEach(function (mod){
180             modsContent += getModFile(mod)
181             myWatch.watch(getFullPath(mod), file.fullname)
182         })
183 
184         content = modsContent + getModFile(file.subpath)
185 
186         //替换所有require
187         content = content.replace(scanReg, function (match, value){
188             return ‘window["‘ + value + ‘"]‘
189         })
190 
191     }
192 
193     return content
194 })
View Code

 

手机端测试

 

使用路由器

由于前后端项目分离,静态文件被单发到cdn,并且使用单独的域名。所以在开发或者测试环境,我们总要通过配置host来使静态资源指向正确的环境。

然而手机端并不容易改host,有几个办法

 
1 前后端用相同的环境,静态资源不带域名。
2 静态资源发单独的环境,但是带上环境ip。
3 买个可以改host的路由器(我们用的极路由),把静态资源域名在路由器上host到ip,然后手机连此路由器的wifi。

 

在本机开发环境时候,我们使用方案1。在发布QA环境时候,使用方案3。只需要把前端的QA机器ip在路由器上配置好,那么从开发到测试到上线,全程人员无需考虑静态资源访问问题。

 

使用browsersync

这个相信做手机页面开发的同学大部分都知道,我就不细说了。由于fis2自带server,只需要使用browsersync的代理模式,转发请求到fis2就好了,谁用谁知道。


 

如何上线

前端工程化之后,一个新的要考虑的问题就是前端如何上线。刀耕火种的年代,只需要把写好的源码ftp到服务器就好了。但是现在问题的变得复杂。

现在工程师写好的源文件不能直接上线,因为需要一个预处理过程,比如sass需要转换成css、commonjs规范的代码要转成浏览器认识的、文件需要压缩、需要打版本号。针对这个过程一般也有几个办法

 
1 中心化处理,即运维维护一套预处理程序,对源码处理后上线。
2 去中心化处理,每个程序员在准备好上线时候,自己进行预处理,然后把处理好的代码直接给运维上线。

 

目前我们用的是方案2。说下原因

 
1 一是最初只有一名运维同学,为了减少运维压力。
2 二是在最初的阶段,前端架构随时会有比较大得改动,比如在fis2上模拟browserfiy这个过程,就持续了差不多两个月,期间反复调研,反复修改。如果用方案1,那么期间的沟通改动成本非常高。

 

所以用了方案二后,前端流程的所有细节都是高度自由可控的,不需要依赖合作方。这对于一个高速前进的团队来说,我觉得是相当有必要的。

但是用了方案二,也带来一些问题,由于开发、测试、上线所需的操作都由前端同学自行解决,很多细节问题会比较繁琐。比如

 
1 发QA环境,需要自己跑一边fis压缩打包,然后手动scp到测试服务器。
2 发线上,需要自己跑一边fis压缩打包,然后把处理好的资源邮件发给运维。

 

所以,搞一个自动化的脚本是十分必要的,我用python写了个脚本,这个脚本掩盖了所有细节,只需要三个命令即可。

1 开发环境:python run.py dev
2 这个命令只是简单调用fis的release命令。
1 发测试环境:python run.py qa
2 这个命令会重新跑一边fis release命令,并把处理好的文件自动scp到测试服务器。
1 准备上线:python run.py www
2 重点说下这个命令,为了方便和运维之间传递代码,针对每个源文件git,建立一个发布git。比如源文件git叫fe.git,那么建立fe-release.git。 执行此命令,会用fis release得到的处理后的源文件来替换fe-release的老文件,并push到gitlab,运维同学只需要用fe-release的代码上线即可。

所以,团队的任何同学,只要第一次配置好了环境,在以后的开发中,只需要记得这三个命令,然后写业务就好了。

发布脚本如下

技术分享
  1 #coding:utf-8
  2 import os,sys,platform,subprocess,time
  3 
  4 #判断当前系统
  5 isWindows = Windows == platform.system()
  6 
  7 bakTmp = ../__dist/
  8 
  9 #前端项目名
 10 project = licai-pc
 11 
 12 #后端分支模板所在目录
 13 beRelease = ../web/src/main/webapp/WEB-INF/views/
 14 
 15 #前端上线发布分支所在目录
 16 feRelease = ../fe-release-group/
 17 
 18 #获取当前git分支
 19 def getGitBranch():
 20     branches = subprocess.check_output([git, branch]).split(\n)
 21     for b in branches[0:-1]:
 22         if b[0] == *:
 23             return b.lstrip(* )
 24 
 25     return None
 26 
 27 
 28 def exeCmd(cmd):
 29     if (not isWindows) and ( (jello in cmd) or (rm in cmd) or (scp in cmd)):
 30         cmd = sudo  + cmd
 31 
 32     print ------------------------------------------------------
 33     print cmd
 34     os.system(cmd)
 35 
 36 def releaseDev():
 37     print release to dev
 38     exeCmd(jello release -wc)
 39 
 40 def releaseQa():
 41     print release to 192.168.50.107 start...
 42 
 43     #删除遗留的__dist
 44     exeCmd(rm -rf  + bakTmp)
 45 
 46     #进行打包编译
 47     cmd = jello release -cD -d  + bakTmp
 48     exeCmd(cmd)
 49 
 50     #把vm文件拷贝到后端工程
 51     cmd = scp -r  + bakTmp + WEB-INF/views/page +   + beRelease
 52     exeCmd(cmd)
 53 
 54     #拷贝静态资源到测试服务器
 55     cmd = scp -r  + bakTmp + project +  root@192.168.50.107:/opt/soft/tengine/html/mljr/
 56     exeCmd(cmd)
 57 
 58     cmd = rm -rf  + bakTmp
 59     exeCmd(cmd)
 60 
 61     print release to 192.168.50.107 end
 62 
 63 def releaseOnline():
 64     print release to fe-release start...
 65 
 66     #检测是否在master分支
 67     if getGitBranch() != master:
 68         print please merge to master!
 69         return
 70 
 71     #删除遗留的__dist
 72     exeCmd(rm -rf  + bakTmp)
 73 
 74     #进行打包编译
 75     cmd = jello release -comD -d  + bakTmp
 76     exeCmd(cmd)
 77 
 78     #切到release目录, 并执行git pull
 79     currPath = os.getcwd()
 80     os.chdir(os.path.join(currPath, feRelease, project))
 81     exeCmd(git pull)
 82     os.chdir(currPath)
 83 
 84     #清空fe-release中对应的项目目录
 85     cmd = rm -rf  + os.path.join(feRelease, project, "*")
 86     exeCmd(cmd)
 87 
 88     #将打包编译的文件拷贝到fe-release
 89     cmd = scp -r  + os.path.join(bakTmp, project, *) +   + os.path.join(feRelease, project)
 90     exeCmd(cmd)
 91 
 92     cmd = scp -r  + os.path.join(bakTmp, WEB-INF/views/page) +   + os.path.join(feRelease, project)
 93     exeCmd(cmd)
 94 
 95     #切到fe-release git push
 96     os.chdir(os.path.join(currPath, feRelease, project))
 97     exeCmd(git add .)
 98     exeCmd(git commit -m "auto commit" *)
 99     exeCmd(git push)
100 
101     #打tag
102     exeCmd(git tag www/ + project + / + time.strftime(%Y%m%d.%H%M))
103     exeCmd(git push --tags)
104 
105     #切回到当前目录
106     os.chdir(currPath)
107     cmd = rm -rf  + bakTmp
108     exeCmd(cmd)
109 
110     print release to fe-release end
111 
112 def main():
113     argv = sys.argv
114     if len(argv) == 1:
115         exeCmd(jello server start -p 80)
116         return
117 
118     cmdType = sys.argv[1]
119 
120     if cmdType == dev:
121         releaseDev()
122 
123     elif cmdType == qa:
124         releaseQa()
125 
126     elif cmdType == www:
127         releaseOnline()
128 
129     else:
130         print please choose one : dev,qa,www
131 
132 if __name__ == "__main__":
133     main()
View Code

以上就是我司技术选择上最值得说的几个东西了,并没有什么特别高大上的东西。在工程上,还是以实用为主。


最后简单介绍下我们公司:美利金融 (注册领大礼包

一家以金融服务帮助年轻人的互联网金融公司,刚刚A轮融资了6500w美元,是一家正在高速发展的公司,需要各种前端、后端、设计人才,小伙伴们可以加我qq:7656201103,或者发送简历到bravfing@126.com。

 

 
 
 

美利金融前端技术架构杂谈

标签:

原文地址:http://www.cnblogs.com/bravfing/p/meili-fe-framework.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!