今天简单说下我厂前端方面一些技术选择。
构建工具
构建工具上我们选用了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]。