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

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


构建工具

构建工具上我们选用了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 })


手机端测试

使用路由器

由于前后端项目分离,静态文件被单发到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 + ‘ [email protected]:/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()



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



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

一家以金融服务帮助年轻人的互联网金融公司,刚刚A轮融资了6500w美元,是一家正在高速发展的公司,需要各种前端、后端、设计人才,小伙伴们可以加我qq:7656201103,或者发送简历到[email protected]。
时间: 2024-10-10 05:46:35

美利金融前端技术架构杂谈的相关文章

从2014年D2前端技术论坛看前端发展趋势

上周六有幸参加了在杭州阿里巴巴西溪园区举办的2014年D2前端技术论坛和晚上的酒会,实地感受了一下阿里巴巴前端开发的技术氛围和影响力,总体上看这次D2规模还是挺大的,国内前端的知名大牛基本上都到了. D2今年的主题是绽放,确实挺符合现在前端发展的阶段,随着对用户体验的不断追求,移动Web的迅猛发展,HTML5的普及,NodeJS的投入商用,AngularJS等新框架的出现,前端的重要性和工程化程度不断提高,上午会上有个妹子说2015年D2的主题是逆袭,我个人觉得挺合适的.从参会人数看,目测有接近

D2 前端技术论坛总结(上)

得幸获得D2前端技术论坛门票一张,今天就去了,公司还给批假了(有可能不会算做请假,哈哈). 早上8点50出门,骑个小毛驴,大概9点30分左右,到了阿里巴巴西溪园区,很多人,进去的门口有专人接待,看D2门票,然后给贴一张阿里巴巴的logo贴纸在身上,然后我进去找了地方停小毛驴,突然发现贴纸不见了...估计给秋风给顺走了,于是立马赶回门口又给拿了一张 O(∩_∩)O哈哈~ 今天我一直呆在主会场,主会场的几场分享我给列列 10:30 张可竞<指尖上的数据> 13:30 林楠<nodejs一小步

余额宝技术架构及演进

html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.6}body{-webkit-touch-callout:none;font-family:-apple-system-font,"Helvetica Neue","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-se

JavaWeb网站技术架构

JavaWeb网站技术架构总结 题记 工作也有几多年了,无论是身边遇到的还是耳间闻到的,多多少少也积攒了自己的一些经验和思考,当然,博主并没有太多接触高大上的分布式架构实践,相对比较零碎,随时补充(附带架构装逼词汇). 俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也不是一天就建成的,当然对于我们开发人员来说,一个好的架构也不是一蹴而就的. 初始搭建 开始的开始,就是各种框架一搭,然后扔到Tomcat容器中跑就是了,这时候我们的文件,数据库,应用都在一个服务器上. 服务分离 随着系统的

微博首席架构师杨卫华:新浪微博技术架构分析

作为国内微博市场的绝对领军者,新浪微博公布一系列针对开发者的扶持政策,以期与第三方开发者联手推动微博行业的整体发展. 以下为演讲实录: 大家下午好,在座的大部分都是技术开发者,技术开发者往往对微博这个产品非常关心.最晚的一次,是12点多收到一个邮件说想了解一下微博底层是怎么构架的.很多技术人员对微博的构架非常感兴趣,就是一个明星他有300万粉丝,这个技术怎么来实现?今天在这里跟大家分享一下微博的底层机构,让大家对微博的底层技术有更好的了解.另外不管是做客户端.Web 1.0.Web 2.0.论坛

前端技术的发展和趋势

Web 发展了几十个春秋,风起云涌,千变万化.我很庆幸自己没有完整地经历过这些年头,而是站在前人的肩膀上行走.Web 技术发展的速度让人感觉那几乎不是继承式的迭代,而是一次又一次的变革,一次又一次的创造.这几年的前端,更为之甚! 我从 12 年底开始接触前端,12 年之前的前端发展情况只能从上一辈的笔触中领会.本文会盘点从 09 年开始到 15 年间前端技术的革新,同时也会从多个角度,解读近几年前端技术发展的潜在因素,其中穿插了若干对前端演进的拙见,难免会有错误和疏漏,望读者可以补充和斧正. 那

大中型网站技术架构浅析 - 实时通信

本文所讲述的『实时通信』主要围绕浏览s器端和服务器端之间的实时通信.大中型网站技术架构浅析 系列之一. 实时通信主要分3大类: 1. Pull技术,轮询(Polling) 客户端定时轮询请求,服务器端立刻返回. 优点:短链接,服务器处理方便,支持跨域. 缺点:有一定延迟 微博未读微博数和未读消息(评论,@)就是用polling实现的. 应用场景:对实时性要求不高的应用,如新微博提示,评论提示,回复提示等. 2. Push,反向Ajax(Reverse Ajax)或者叫Comet. 实现方式主要有

浅析微信小程序技术架构(原创)

周末万里虎抽空体验了下微信小程序的DEMO,对小程序的开发有了一个基础的了解与认识,今天就来和大家分享一下我对小程序的看法. 从官方DEMO来看,小程序在技术架构上非常清晰易懂.JS负责业务逻辑的实现,而表现层则WXML和WXSS来共同实现,前者其实就是一种微信定义的模板语言,而后者类似CSS.所以对于擅长前端开发,或者WEB开发的广大开发者而已,小程序的开发可谓降低了不少门槛. 从上面的微信小程序架构图上可以清晰的看出,小程序借助的是JSBridge实现了对底层API接口的调用,所以在小程序里

大型网站技术架构-核心原理与案例分析-阅读笔记4

在第四章案例章节中的淘宝网的架构演化案例分析小节中作者主要分析了淘宝架构的演化,以淘宝网的实例给我们分析介绍了淘宝网的业务发展历程及淘宝网的技术架构演化两个方面,在业务发展中作者写到淘宝的技术是随着淘宝业务一起发展起来的,业务是推动这技术发展的动力,淘宝如今的规模和当初有很明显的变化,在技术架构演化中介绍了架构技术的更新升级,该章节中主要介绍淘宝网的发展的历程,在随着时间的发展不断中网站的架构不断的引用着新的技术,由最初简单的c2c更改过来的网站,放弃了lamp架构转而使用java作为开发平台并