本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
👆 这是第 167 篇不掺水的原创,想要了解更多,请戳下方卡片关注我们吧~
前端本地化部署
前言
现在成熟的前端团队里面都有自己的内部构建平台,我司云长便是我们 CI/CD 的提效利器。我先来简单介绍下我司的云长,此云长非彼云长,云长主要做的是:获取部署的项目,分支,环境基本信息后开始拉取代码,安装依赖,打包,并且将项目的一些资源静态文件上传 CDN,再将生成的代码再打包成镜像文件,然后将这份镜像上传到镜像仓库后,最后调用 K8S 的镜像部署服务,进行镜像按环境的部署,这就是我们云长做的事情。如果想从零开始搭建一个自己团队的部署平台可以看下我们往期文章 如何搭建适合自己团队的构建部署平台,本期我们只是针对云长中静态资源本地化的功能做细致阐述。
场景分析
为了网络安全,客户会要求我们的应用是要完全部署在内网的,那我们需要做什么呢?第一我们需要考虑前端代码中是不是有些直接访问外网资源?第二是不是后端返回了静态资源地址在某种情况下就访问了?第三 CDN 资源具体有那些类型呢?前端直接访问的 CDN 的资源太普遍了,如下既有 at.alicdn.com,又有我们自己内部的静态资源 luban.zcycdn.com, sitecdn.zcycdn.com 等。如下这些就在我们代码中使用的静态资源地址。
<link rel='stylesheet' href='//at.alicdn.com/t/fontnm.css' />
<img src="https://sitecdn.zcycdn.com/f2e/8.png"alt="收货人"/>
<img src="https://luban.zcycdn.com/f2e/8.png"alt="收货人"/>
//css中字体文件
src:url(https://sitecdn.zcycdn.com/t/font_148178j4i.eot);
src:url(https://sitecdn.zcycdn.com/t/font1_4i.woff);为了保证我们内网中可以访问我们讨论出以下两个方案
方案一
DNS 解析做转发
我们通过 DNS 服务这一层去处理,具体 DNS 如何进行的二域名,三级域名进行解析,如何 DNS 缓存,以及什么是 13 台根服务器,我们这次不做深入探讨,我们只需要 DNS 的可以进行域名解析,解析到指定的 IP 服务上即可。
那我们是不是可以想一下,是不是把代码中访问的静态资源的域名拦截一下,DNS 解析成本地服务的地址是不是就可以了呢?为了更清楚的理解,我做一个例子如下:
我们代码中需要访问某个图片,CDN 地址:https://cdn.zcycdn.com/b/a.js
上传提前把 a.js 这个文件提前放到本地服务器上访问地址:https://demo.com/b/a.js
当代码运行的时候,代码中访问了 https://cdn.zcycdn.com 的时候,DNS 直接地址解析成 https://demo.com 的 IP 地址,达到访问静态资源的目的
看起来这个蛮简单的,不需要各个业务负责人排查修改自己代码中的静态资源,胜利在望了,兴致冲冲的跑去找运维童鞋提议是不是可以这样做,然而运维把我说的服服帖帖。运维童鞋说:静态资源放在对象存储或者服务器上,通过 IP 或者域名的方式都可以请求的到,不过 IP 只支持 HTTP 的方式,域名 + SSL 证书的方式支持 HTTPS,可以做一些加密,让你的资源或者请求内容进行加密,不容易被破解,域名证书之前有 3 到 5 年的,3 年前已经改掉了,目前申请的证书都是一年的,那就预示着不仅仅要用户配置我们提供的 DNS 规则,还要配合我们一年一更新证,想要客户这样配合那是不容易。如下图所示:
DNS 只是帮我们把域名解析成了 IP, HTTPS 还需要证书验证服务器身份,仅仅 DNS 拦截解析还不够。模拟实现了一波大致思路:自己启动一个静态资源服务,以及 DNS 本地解析服务,当访问 juejin.cn 域名的时候 IP 解析成本地的 IP 并且成功访问到静态资源,具体如下。
自己写一个 DNS 服务
step1: 本地起一个服务
暂时存放静态资源,模拟服务器上的资源
启动服务访问静态资源
我们的目的:如果访问 http://juejin.cn:3000/zcy.png (http://juejin.cn:3000/zcy.png) 的时候访问到我们本地服务的静态资源:http://10.201.45.121:3000/zcy.png (http://10.201.45.121:3000/zcy.png)
step2: 启动一个本地 DNS 服务,拦截所有请求转发到自己启动的 IP 点击查看源码 (https://sitecdn.zcycdn.com/f2e-assets/7da606eb-d8fc-4a01-a633-fcfd60edc2c5.js)
step3:配置本地 DNS 解析
step4: 测试访问 HTTP 和 HTTPS
访问:http://juejin.cn:3000/zcy.png(http://juejin:3000/zcy.png)
如果是 https://juejin.cn:3000/zcy.png (https://juejin:3000/zcy.png)
如果访问的是 HTTP 请求那就可以访问,HTTPS 就不能访问,侧面证明了 HTTPS 的证书问题。HTTPS 对称加密的秘钥我们采用非对称加密传输,数据传输还是使用对称加密,这保证了数据加密传输,为了保证防止冒充,CA(Certificate Authority), 颁发的证书就称为数字证书 (Digital Certificate),在非对称加密阶段,服务器会把证书会带着非对称加密的公钥,一起返回,向浏览器证明服务器的身份 HTTPS 相比 HTTP 多了一层 SSL/TLS(安全层)如下图。
方案二
项目在构建的时候扫描出项目中的静态资源地址,从我们公网的 CDN 服务放到客户自己的服务器上,修改源文件中的静态资源地址为客户本地服务的访问地址。
优缺点一目了然,方案一无需修改代码,但是需要充分得到客户的大力信任与支持需要配置 DNS 转发,方案二无需劳烦客户,即使后面有新增域名也不需要和客户沟通,完全自己解决,但是对代码有侵入性,会替换静态资源的地址
我们通过以下 4 个阶段拆解
统一封装 runCommand 执行命令
function runCommand(cmd, args, options, before, end) { return new Promise((resolve, reject) => { log(before, blue) const spawn = childProcess.spawn( cmd, args, Object.assign( { cwd: global.WORKSPACE, stdio: 'inherit', shell: true, }, options ) ) spawn.on('error', (error) => { log(error, chalk.red) reject(error) }); spawn.on('close', (code) => { if (code !== 0) { return reject(`sh: ${cmd} ${args.join(' ')}`) } end && log(end, green) resolve() }); })}1、pre 前置环境校验
切换公司 nrm
runCommand('nrm', ['use', 'zcy-server'], {}, 'switch nrm registry to zcy', 'switch nrm registry to zcy success')下载依赖
runCommand('npm', ['i', '--unsafe-perm'], {}, 'npm install', 'npm install success')2、compile 编译
不同环境需要上传不同的地址因此需要动态修改 webpack 的 publicPath
const cdnConfigStr = `assetsPublicPath: 'http://dev.com',`replaceFileContent(configPath, /assetsPublicPath:.+,/g, cdnConfigStr)exports.replaceFileContent = function(filePath, source, target) { const fileContent = fs.readFileSync(filePath, 'utf-8') let targetFileContent = fileContent if (Array.isArray(source)) { source.forEach(([s, target]) => { if (target) { targetFileContent = targetFileContent.replace(s, target) } }) } else { targetFileContent = fileContent.replace(source, target) } fs.writeFileSync(filePath, targetFileContent, 'utf-8')}编译项目
runCommand('npm', ['run', 'build'], {}, `webpack build`, `webpack build success`)3、静态资源替换
替换 url 源码地址
const replaceWebpackDistContent = async function(options = {},collectionAssets,folder) { const fileContent = fs.readFileSync(filePath, 'utf-8'); let targetFileContent=fileContent; [ [/(https\:)?\/\/g.alicdn.com\/[-a-zA-Z0-9@:%_\+.~#?&//=]+\.[-a-zA-Z0-9@:%_\+.~#?&//=]+/g, cdn], [/(https?\:)?\/\/sitecdn.zcycdn.com\/[-a-zA-Z0-9@:%_\+.~#?&//=]+\.[-a-zA-Z0-9@:%_\+.~#?&//=]+/g, cdn], [/(https\:)?\/\/cdn.zcycdn.com\/[-a-zA-Z0-9@:%_\+.~#?&//=]+\.[-a-zA-Z0-9@:%_\+.~#?&//=]+/g, cdn], ].forEach(([reg,uri])=>{ targetFileContent=targetFileContent.replace(reg,function(match){ let basename = ''; let uriMath = match; basename = path.basename(uriMath); if(uriMath.slice(0,4)!='http'){ uriMath='https:'+uriMath; } const parseUrl = url.parse(uriMath); collectionAssets({src:uriMath,fileName:path.basename(parseUrl.pathname)}); console.log('🚀替换前',match); const myURL= new URL(projectName, uri); const replacedUrl = uri+'/'+projectName+parseUrl.path+(parseUrl.hash||''); console.log('🚀替换后', replacedUrl); return replacedUrl; }) }) fs.writeFileSync(filePath, targetFileContent, 'utf-8') }获取写死在前端代码中的静态资源
const downloadAssetsFiles= async function(img,forder){ const staticAssets='staticAssets'; let assetsUrl=getPwdPath(`${forder||''}${path.sep}${staticAssets}`); if(!fs.existsSync(assetsUrl)){ fs.mkdirSync(assetsUrl); } return Promise.all(img.objUnique('src').map(({src,fileName})=>{ if(fileName){ return new Promise(function(resolve,reject){ const originFileDir = path.join(assetsUrl,path.dirname(url.parse(src).pathname)); fs.mkdirSync(originFileDir,{recursive:true}); const uri = path.join(originFileDir,fileName); download(uri,src,resolve,reject); }).catch(err=>{ console.log(err) throw new Error(err); }) } })) }function download(loadedUrl,src){ const writeStream = fs.createWriteStream(loadedUrl); const readStream = request(src); readStream.pipe(writeStream); readStream.on('end', function() { console.log(fileName,'文件下载成功'); }); writeStream.on("finish", function() { console.log(fileName,"文件写入成功"); writeStream.end(); });} downloadAssetsFiles(assetsArr,'dist'); // 发现替换资源里还有cdn,因此替换下载后的cdn里面的cdn const assetsArr=[]; await replaceWebpackDistContent(options,collectionAssets,'staticAssets'); await downloadAssetsFiles(assetsArr,'dist');4、OSS 推送静态资源到客户资源服务
const ossEndpoint = process.env.OSS_ENDPOINT;const commonOptions = { accessKeyId: process.env.OSS_ACCESSKEYID , accessKeySecret: process.env.OSS_ACCESSKEYSECRET, bucket: process.env.OSS_BUCKET, timeout: '120s',}const extraOptions = ossEndpoint ? { endpoint: ossEndpoint, // 从全局数据获取,没有会依赖 region cname: true, } : { region: process.env.OSS_REGION, }const ossOptions = Object.assign({}, commonOptions, extraOptions);const client = new OSS(ossOptions);//onlinePath 访问的文件地址//curPath 上传的文件地址result = await client.put(onlinePath, curPath);参考文档
SSL/TLS 证书 1 年有效期新规 (https://www.trustasia.com/view-398-day-limit/)
node child_process 文档 (https://link.juejin.cn/?target=http%3A%2F%2Fnodejs.cn%2Fapi%2Fchild_process.html%23child_process_child_process_fork_modulepath_args_options)
深入理解 Node.js 进程与线程 (https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fxgangzai%2Farticle%2Fdetails%2F98919412)
看完两件事
如果你觉得这篇内容对你挺有启发,我想邀请你帮我两件小事
点个「在看」,让更多人也能看到这篇内容(点了「在看」,bug -1 😊)
关注公众号「政采云前端团队」,持续为你推送精选好文
招贤纳士
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 90 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的 “老” 兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是 “5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com