本文仅提供一种前端的灰度解决方案,必然不是最优解,但是仅提供一种可行的实现方式
项目使用的技术栈为vue2+webpack4
注意事项
- 项目已经开发很久且经手开发人员很多,既有后端也有前端(不能影响原来的开发流程)
- 没有统一的规范,导致项目有ajax,也有用axios等进行请求
- 静态资源也需要灰度
- 前端不知道当前项目是处于g还是b
构建灰度文件
项目总体是需要蓝绿两种部署方案,前端需要将当前项目打包出来的文件分成g和b (b的文件,统一在打包之后的拓展名前面增加.blue,g的文件的文件名不需要变动)
项目已经开发很久且经手开发人员很多,既有后端也有前端(不能影响原来的开发流程)
所以前端灰度需要在原有的项目打包完成之后,再进行相应的处理。因为项目是在webpack4下开发的,所以webpack4本身在build文件夹里有个build.js的文件,里面有个rm的函数,当项目打包完成之后,会执行该函数。故,在该函数中对已经打包出来的项目文件,进行变更。
// 给所有静态文件增加env判断语句
fs.writeFileSync(
path.resolve(__dirname, `../dist/static/js/${val}`),
fs.readFileSync(path.resolve(__dirname, `../dist/static/js/${val}`),'utf-8')
// 变更懒加载css
.replace(/(\"\.css\")/g,'(window._ENV == "b"?".blue.css?env=b":".css?env=g")')
// 变更懒加载js
.replace(/(\"\.js\")/g,'$1+(window._ENV?"?env="+window._ENV:"")')
// 变更static/img图片加载
.replace(/(\/static\/img.*?)\"/g,'$1"+(window._ENV?"?env="+window._ENV:"")')
// 变更static/media多媒体加载
.replace(/(\/static\/media.*?)\"/g,'$1"+(window._ENV?"?env="+window._ENV:"")')
,'utf-8'
);
})
fs.readdirSync(path.resolve(__dirname, '../dist/static/css')).forEach(val => {
// 复制一份蓝色样式文件
fs.writeFileSync(
path.resolve(__dirname, `../dist/static/css/${val.replace(".css",".blue.css")}`),
fs.readFileSync(path.resolve(__dirname, `../dist/static/css/${val}`),'utf-8')
.replace(/(\/static\/.*?)\)/g,'$1?env=b)')
,'utf-8'
);
// 原样式文件变更为绿色
fs.writeFileSync(
path.resolve(__dirname, `../dist/static/css/${val}`),
fs.readFileSync(path.resolve(__dirname, `../dist/static/css/${val}`),'utf-8')
.replace(/(\/static\/.*?)\)/g,'$1?env=g)')
,'utf-8'
);
});
构建中间html文件
创建一个project.html文件,用于将编译出来的index.html进行重构之后,再将index.html的内容替换成project.html的内容。
使用project.html中间文件而不直接变更编译出来的index.html文件,是因为project.html内容较多,直接写到js中并不便捷,需要修改的时候也更方便
<html style="overflow:hidden">
<head>
<projectHead/>
</head>
<body style="font-size: 12px;">
<!-- bodyHtml放置处 -->
<projectBody/>
<script id="greyScript">
// 灰度逻辑代码
!~(function() {
var href = window.location.href;
var search = href.substring(href.lastIndexOf("?")+1,href.indexOf("#")>href.lastIndexOf("?")?href.indexOf("#"):undefined);
var env = (~href.indexOf("?")?Object.assign({},...search.split("&").map(val => ({[val.split("=")[0]]:val.split("=")[1]}))):{}).env;
var jsFileList = <jsFileList/>; // js文件列表
var bCssFileList = <bCssFileList/>; // 蓝色css列表
var gCssFileList = <gCssFileList/>; // 绿色css列表
// 变更url地址(如果截取到env但是.html后面没有附带query参数,则变更地址)
if(env && !href.match(/\.html.*?env=.*?\#/) && href.includes("#")) {
if(href.includes(".html?")) {
window.location.href = href.replace("#","&env=" + env + "#");
} else {
window.location.href = href.replace(".html",".html?env=" + env);
}
return;
}
// 如果env不存在,或者env不为g和b,则设置env为g
if(!env || !/^[gb]$/.test(env)) {
env = 'g';
}
addIcon(env);
window._ENV = env;
// 设置cookie
setCookie('env',env);
// 加载相关的蓝绿css文件
(env == 'b'?bCssFileList:gCssFileList).forEach(val => {
let css = document.createElement("link");
css.rel = 'stylesheet';
css.href = val + (val.includes('?')?'&':'?') + 'env=' + env;
document.head.appendChild(css);
});
// 拦截所有请求并且给所有请求增加env
var _REQUESTOPEN = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.env = env;
window.XMLHttpRequest.prototype.open = function() {
let url = arguments[1];
if(!url.includes("127.0.0.1")) {
if(~url.indexOf('?')) {
url = url + '&env=' + this.env;
} else {
url = url + '?env=' + this.env;
}
// 请求的时候,如果有env,则延续env
getCookie('env') && setCookie('env',getCookie('env'));
}
arguments[1] = url;
var self = this,
argus = arguments;
return (function() {
_REQUESTOPEN.apply(self, [].slice.call(argus));
if(!url.includes("127.0.0.1")) {
self.setRequestHeader('env',self.env);
}
})()
}
// 加载js文件
jsFileList = jsFileList.map(val => {
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = env?(val + (val.includes('?')?'&':'?') + 'env=' + env):val;
return script;
});
jsFileList.forEach((val,index) => {
var script = jsFileList[index + 1];
val.onload = function(){
if(script) {
document.body.appendChild(script);
} else {
// js全部加载完毕去除加载代码
document.getElementById("greyScript").remove();
}
}
});
document.body.appendChild(jsFileList[0]);
// 写cookie
function setCookie(name,value) {
var exp = new Date();
exp.setTime(exp.getTime() + 30*60*1000);
document.cookie = name + "="+ escape(value) + ";expires=" + exp.toGMTString();
}
//读取cookies
function getCookie(name) {
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
if(arr=document.cookie.match(reg)) {
return unescape(arr[2]);
} else {
return null;
}
}
// 加载icon图标
function addIcon(env) {
let css = document.createElement("link");
css.rel = 'shortcut icon';
if(env) {
css.href = './favicon.png?env=' + env;
} else {
css.href = './favicon.png';
}
document.head.appendChild(css);
}
})();
</script>
</body>
</html>
替换index.html内容
path.resolve(__dirname, `../dist/index.html`),
// 变更head内容
projectHtml
.replace("<projectHead/>",indexHtml.substring(indexHtml.indexOf("<head>")+6,indexHtml.indexOf("</head>")))
// 去除css标签
.replace(/\<link.*?\>/g,str => {
if(str.includes('stylesheet')) {
cssFileList.push(/href=\"?(.*?\.css)/.exec(str)[1]);
return '';
} else {
return str;
}
})
// 变更蓝色css文件列表
.replace("<bCssFileList/>",JSON.stringify(cssFileList.map(val => val.replace(".css",".blue.css"))))
// 变更绿色css文件列表
.replace("<gCssFileList/>",JSON.stringify(cssFileList.map(val => val)))
// 获取转换成js的css文件地址
.replace("<converCssFileList/>",JSON.stringify(cssFileList.map((val,index) => val.replace(/(.*?\/static).*/g,`$1/js/convertCss${index}.js`))))
// 变更body内容
.replace("<projectBody/>",indexHtml.substring(indexHtml.indexOf("<body>")+6,indexHtml.indexOf("</body>")))
// 去除js标签
.replace(/\<script.*?script\>/g,str => {
if(str.includes('/static/')) {
jsFileList.push(/src=\"?(.*?\.js)/.exec(str)[1]);
return '';
} else {
return str;
}
})
// 变更js列表
.replace("<jsFileList/>",JSON.stringify(jsFileList)),
'utf-8'
);
注意事项:
- js请求应该从XMLHttpRequest进行拦截,因为不管是用的ajax还是axios,都是二次封装的XMLHttpRequest对象
- js的加载应该通过document.createElement的方式进行,否则会导致script不会被加载
- 静态资源不是通过XMLHttpRequest的方式加载的,所以需要对静态资源进行query传参操作
- env的场景是用户登录之后,后台判断当前用户是否为灰度然后重定向到前端并将env放到url上。前端通过获取url的query来判断当前的环境,直接将env放到window上,可能存在被用户篡改的风险(可以考虑闭包的方式)
- 每次编译出来的文件都会有g和b,这样前端就可以不需要考虑运维部署的环境