转载

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

构建工具

构建工具上我们选用了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。 
正文到此结束
Loading...