@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。代码如下:
var isWatch = process.title.split(' ')[2].indexOf('w') != -1var myWatch = function (){var fs = require('fs')var table = {}function toWatch(f1){if (!isWatch){return false}//最多2s触发一次watch改动var isPlay = falsefs.watch(f1, function (){if (isPlay){return false}isPlay = truesetTimeout(function (){isPlay = false}, 2000)var f2List = table[f1]f2List.forEach(function (f2){fs.utimes(f2, new Date, new Date)console.log('touch ' + f2)})})}function watch (f1, f2){if (f1 in table){if (table[f1].indexOf(f2) == -1){table[f1].push(f2)}}else {table[f1] = [f2]toWatch(f1)}}return {watch:watch}}()//fis 插件,模拟browserfiy的requirefis.config.set('modules.parser.js', function (content, file, settings){var fs = require('fs')var path = require('path')var crypto = require('crypto')var modTable = []var modLinkTable = {}var scanReg = /require\(['|"](.*?)['|"]\)/gfunction getMd5(str){var md5 = crypto.createHash('md5')md5.update(str)var md58 = md5.digest('hex').slice(-8)//有一定几率出现md58是纯数字,但是firefox不支持window['123']的情况,所以加前缀if (/^\d+$/.test(md58)){md58 = 'ml-' + md58}return md58}function getFullPath(p){var fullPath = path.join(__dirname, p)return fullPath}function getModFile(p){var fullPath = getFullPath(p)var content = fs.readFileSync(fullPath) + ''var windowFunc = 'window["' + getMd5(p.replace(/\\/g, '/')) + '"]'//如果是tpl文件if (p.slice(-4) == '.tpl'){return '//#----------------mod start----------------\n' +windowFunc + '= \'' + content.replace(/\r?\n\s*/g, '') + '\'\n' +'//#----------------mod end----------------\n\n'}//如果是js文件if (p in modLinkTable){for (var relpath in modLinkTable[p]){var abspath = modLinkTable[p][relpath]content = content.replace(RegExp(relpath, 'g'), getMd5(abspath.replace(/\\/g, '/')))}}return '//#----------------mod start----------------\n' +'void function (module, exports){\n\t' +windowFunc + '={};\n' +content.replace(/(module\.)?exports/g, windowFunc).replace(/(^|\n)/g, '\n\t') +'\n}({exports:{}}, {})\n' +'//#----------------mod end----------------\n\n'}function fillModLinkTable(subpath, requireNameA, requireNameB){if (!(subpath in modLinkTable)){modLinkTable[subpath] = {}}modLinkTable[subpath][requireNameA] = requireNameB}function scanMod(subpath){var modTable2 = []var modContent = fs.readFileSync(getFullPath(subpath)) + '';var execValuewhile ( (execValue = scanReg.exec(modContent)) != null ){var requireName = execValue[1]var modPath//如果rquire的是绝对路径if (requireName[0] == '/'){modPath = path.join(requireName)}else {modPath = path.join(path.dirname(subpath), requireName)}fillModLinkTable(subpath, requireName, modPath)modTable2.unshift(modPath)}modTable2.forEach(function (mod){var idx = modTable.indexOf(mod)if (idx != -1){modTable.splice(idx, 1)}modTable.unshift(mod)scanMod(mod)})}//1、是js文件。2、文件名不能下划线打头(下划线的不被release出去)。3、min.js结尾的文件都直接被<script src>if ( (file.filename[0] != '_') && (file.filename.slice(-4) != '.min') ){//console.log(file)modTable = []modLinkTable = {}scanMod(file.subpath)//把mods声明放到最前var modsContent = ''modTable.forEach(function (mod){modsContent += getModFile(mod)myWatch.watch(getFullPath(mod), file.fullname)})content = modsContent + getModFile(file.subpath)//替换所有requirecontent = content.replace(scanReg, function (match, value){return 'window["' + value + '"]'})}return content})
由于前后端项目分离,静态文件被单发到cdn,并且使用单独的域名。所以在开发或者测试环境,我们总要通过配置host来使静态资源指向正确的环境。
然而手机端并不容易改host,有几个办法
前后端用相同的环境,静态资源不带域名。静态资源发单独的环境,但是带上环境ip。买个可以改host的路由器(我们用的极路由),把静态资源域名在路由器上host到ip,然后手机连此路由器的wifi。
在本机开发环境时候,我们使用方案1。在发布QA环境时候,使用方案3。只需要把前端的QA机器ip在路由器上配置好,那么从开发到测试到上线,全程人员无需考虑静态资源访问问题。
这个相信做手机页面开发的同学大部分都知道,我就不细说了。由于fis2自带server,只需要使用browsersync的代理模式,转发请求到fis2就好了,谁用谁知道。
前端工程化之后,一个新的要考虑的问题就是前端如何上线。刀耕火种的年代,只需要把写好的源码ftp到服务器就好了。但是现在问题的变得复杂。
现在工程师写好的源文件不能直接上线,因为需要一个预处理过程,比如sass需要转换成css、commonjs规范的代码要转成浏览器认识的、文件需要压缩、需要打版本号。针对这个过程一般也有几个办法
中心化处理,即运维维护一套预处理程序,对源码处理后上线。去中心化处理,每个程序员在准备好上线时候,自己进行预处理,然后把处理好的代码直接给运维上线。
目前我们用的是方案2。说下原因
一是最初只有一名运维同学,为了减少运维压力。二是在最初的阶段,前端架构随时会有比较大得改动,比如在fis2上模拟browserfiy这个过程,就持续了差不多两个月,期间反复调研,反复修改。如果用方案1,那么期间的沟通改动成本非常高。
所以用了方案二后,前端流程的所有细节都是高度自由可控的,不需要依赖合作方。这对于一个高速前进的团队来说,我觉得是相当有必要的。
但是用了方案二,也带来一些问题,由于开发、测试、上线所需的操作都由前端同学自行解决,很多细节问题会比较繁琐。比如
发QA环境,需要自己跑一边fis压缩打包,然后手动scp到测试服务器。发线上,需要自己跑一边fis压缩打包,然后把处理好的资源邮件发给运维。
所以,搞一个自动化的脚本是十分必要的,我用python写了个脚本,这个脚本掩盖了所有细节,只需要三个命令即可。
开发环境:python run.py dev这个命令只是简单调用fis的release命令。
发测试环境:python run.py qa这个命令会重新跑一边fis release命令,并把处理好的文件自动scp到测试服务器。
准备上线:python run.py www重点说下这个命令,为了方便和运维之间传递代码,针对每个源文件git,建立一个发布git。比如源文件git叫fe.git,那么建立fe-release.git。 执行此命令,会用fis release得到的处理后的源文件来替换fe-release的老文件,并push到gitlab,运维同学只需要用fe-release的代码上线即可。
所以,团队的任何同学,只要第一次配置好了环境,在以后的开发中,只需要记得这三个命令,然后写业务就好了。
发布脚本如下
#coding:utf-8import os,sys,platform,subprocess,time#判断当前系统isWindows = 'Windows' == platform.system()bakTmp = '../__dist/'#前端项目名project = 'licai-pc'#后端分支模板所在目录beRelease = '../web/src/main/webapp/WEB-INF/views/'#前端上线发布分支所在目录feRelease = '../fe-release-group/'#获取当前git分支def getGitBranch():branches = subprocess.check_output(['git', 'branch']).split('\n')for b in branches[0:-1]:if b[0] == '*':return b.lstrip('* ')return Nonedef exeCmd(cmd):if (not isWindows) and ( ('jello' in cmd) or ('rm' in cmd) or ('scp' in cmd)):cmd = 'sudo ' + cmdprint '------------------------------------------------------'print cmdos.system(cmd)def releaseDev():print 'release to dev'exeCmd('jello release -wc')def releaseQa():print 'release to 192.168.50.107 start...'#删除遗留的__distexeCmd('rm -rf ' + bakTmp)#进行打包编译cmd = 'jello release -cD -d ' + bakTmpexeCmd(cmd)#把vm文件拷贝到后端工程cmd = 'scp -r ' + bakTmp + 'WEB-INF/views/page' + ' ' + beReleaseexeCmd(cmd)#拷贝静态资源到测试服务器cmd = 'scp -r ' + bakTmp + project + ' root@192.168.50.107:/opt/soft/tengine/html/mljr/'exeCmd(cmd)cmd = 'rm -rf ' + bakTmpexeCmd(cmd)print 'release to 192.168.50.107 end'def releaseOnline():print 'release to fe-release start...'#检测是否在master分支if getGitBranch() != 'master':print 'please merge to master!'return#删除遗留的__distexeCmd('rm -rf ' + bakTmp)#进行打包编译cmd = 'jello release -comD -d ' + bakTmpexeCmd(cmd)#切到release目录, 并执行git pullcurrPath = os.getcwd()os.chdir(os.path.join(currPath, feRelease, project))exeCmd('git pull')os.chdir(currPath)#清空fe-release中对应的项目目录cmd = 'rm -rf ' + os.path.join(feRelease, project, "*")exeCmd(cmd)#将打包编译的文件拷贝到fe-releasecmd = 'scp -r ' + os.path.join(bakTmp, project, '*') + ' ' + os.path.join(feRelease, project)exeCmd(cmd)cmd = 'scp -r ' + os.path.join(bakTmp, 'WEB-INF/views/page') + ' ' + os.path.join(feRelease, project)exeCmd(cmd)#切到fe-release git pushos.chdir(os.path.join(currPath, feRelease, project))exeCmd('git add .')exeCmd('git commit -m "auto commit" *')exeCmd('git push')#打tagexeCmd('git tag www/' + project + '/' + time.strftime('%Y%m%d.%H%M'))exeCmd('git push --tags')#切回到当前目录os.chdir(currPath)cmd = 'rm -rf ' + bakTmpexeCmd(cmd)print 'release to fe-release end'def main():argv = sys.argvif len(argv) == 1:exeCmd('jello server start -p 80')returncmdType = sys.argv[1]if cmdType == 'dev':releaseDev()elif cmdType == 'qa':releaseQa()elif cmdType == 'www':releaseOnline()else:print 'please choose one : dev,qa,www'if __name__ == "__main__":main()
以上就是我司技术选择上最值得说的几个东西了,并没有什么特别高大上的东西。在工程上,还是以实用为主。
一家以金融服务帮助年轻人的互联网金融公司,刚刚A轮融资了6500w美元,是一家正在高速发展的公司,需要各种前端、后端、设计人才,小伙伴们可以加我qq:7656201103,或者发送简历到bravfing@126.com。