你的位置:首页 > ASP.net教程

[ASP.net教程]美利金融前端技术架构杂谈


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

 

构建工具

构建工具上我们选用了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] = requireNameB136   }137 138   function scanMod(subpath){139     var modTable2 = []140     var modContent = fs.readFileSync(getFullPath(subpath)) + '';141 142     var execValue143     while ( (execValue = scanReg.exec(modContent)) != null ){144       var requireName = execValue[1]145       var modPath146 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     //替换所有require187     content = content.replace(scanReg, function (match, value){188       return 'window["' + value + '"]'189     })190 191   }192 193   return content194 })

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 dev2 这个命令只是简单调用fis的release命令。

1 发测试环境:python run.py qa2 这个命令会重新跑一边fis release命令,并把处理好的文件自动scp到测试服务器。

1 准备上线:python run.py www2 重点说下这个命令,为了方便和运维之间传递代码,针对每个源文件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   #打tag102   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 ' + bakTmp108   exeCmd(cmd)109 110   print 'release to fe-release end'111 112 def main():113   argv = sys.argv114   if len(argv) == 1:115     exeCmd('jello server start -p 80')116     return117 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。