[关闭]
@bravf 2015-12-06T07:22:44.000000Z 字数 7107 阅读 1012

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

架构


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


构建工具

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

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

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

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

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

手机端测试

使用路由器

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

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

  1. 前后端用相同的环境,静态资源不带域名。
  2. 静态资源发单独的环境,但是带上环境ip
  3. 买个可以改host的路由器(我们用的极路由),把静态资源域名在路由器上hostip,然后手机连此路由器的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. 这个命令只是简单调用fisrelease命令。
  1. 发测试环境:python run.py qa
  2. 这个命令会重新跑一边fis release命令,并把处理好的文件自动scp到测试服务器。
  1. 准备上线:python run.py www
  2. 重点说下这个命令,为了方便和运维之间传递代码,针对每个源文件git,建立一个发布git。比如源文件gitfe.git,那么建立fe-release.git 执行此命令,会用fis release得到的处理后的源文件来替换fe-release的老文件,并pushgitlab,运维同学只需要用fe-release的代码上线即可。

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

发布脚本如下

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

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


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

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