vue2前端灰度

93 阅读3分钟

本文仅提供一种前端的灰度解决方案,必然不是最优解,但是仅提供一种可行的实现方式

项目使用的技术栈为vue2+webpack4

注意事项

  • 项目已经开发很久且经手开发人员很多,既有后端也有前端(不能影响原来的开发流程)
  • 没有统一的规范,导致项目有ajax,也有用axios等进行请求
  • 静态资源也需要灰度
  • 前端不知道当前项目是处于g还是b

构建灰度文件

项目总体是需要蓝绿两种部署方案,前端需要将当前项目打包出来的文件分成g和b (b的文件,统一在打包之后的拓展名前面增加.blue,g的文件的文件名不需要变动)

image.png

项目已经开发很久且经手开发人员很多,既有后端也有前端(不能影响原来的开发流程)

所以前端灰度需要在原有的项目打包完成之后,再进行相应的处理。因为项目是在webpack4下开发的,所以webpack4本身在build文件夹里有个build.js的文件,里面有个rm的函数,当项目打包完成之后,会执行该函数。故,在该函数中对已经打包出来的项目文件,进行变更。

image.png

      // 给所有静态文件增加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,这样前端就可以不需要考虑运维部署的环境