1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        手摸手之前端覆蓋率實(shí)踐

        共 31410字,需瀏覽 63分鐘

         ·

        2021-06-01 13:26

        點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)

        回復(fù)算法,加入前端編程面試算法每日一題群

        來源:i.m.t

        https://juejin.cn/post/6959147556295180324

        前述

        今天開一個(gè)新坑,講講前端覆蓋率:Istanbul

        談到前端覆蓋率,早在18年就有接觸,但由于種種原因,并沒有去具體的落地。僅有的也只是了解了這是個(gè)什么,用來干什么!

        最終這個(gè)應(yīng)該怎么做?如何落地?并沒有深究探索。想起來略有遺憾。

        但這次一定!這篇文章,將講述如何使用 Istanbul 去收集前端覆蓋率、對(duì)源碼的解析、如何貼合業(yè)務(wù)理解對(duì)源碼做相應(yīng)的修改、覆蓋率一鍵上報(bào)等。

        寫在前面

        在寫之前,先貼一下我參考過的四篇文章,個(gè)人認(rèn)為,是關(guān)于前端覆蓋率寫的很好的文章:

        基于 Istanbul 優(yōu)雅地搭建前端 JS 覆蓋率平臺(tái)

        聊聊前端代碼覆蓋率 (長文慎入)

        React Native 代碼覆蓋率獲取探索

        覆蓋率實(shí)時(shí)統(tǒng)計(jì)工具

        以及相關(guān)的GitHub開源:

        babel-plugin-istanbul

        istanbul-middleware

        nyc

        code-coverage

        好了,搬好小板凳,我們開始吧~

        正文

        前端 web 覆蓋率統(tǒng)計(jì)

        首先前端覆蓋率,在當(dāng)下的業(yè)務(wù)場(chǎng)景中,包含了 web 和 mobile ,那么很多情況下,如果mobile中不是用native原生寫的,大都都是內(nèi)嵌H5頁面的形式存在。如果你所涉及到的業(yè)務(wù),大部分都是用 webview。

        那就直接整web端的就可以了。當(dāng)然如果情況不同,那在上面的鏈接中也給到了,一個(gè)RN的設(shè)計(jì)鏈接,也可以進(jìn)行參考~

        當(dāng)然了,相信現(xiàn)在前端都是用 vue 或者 React 居多了,如果還是用jQuery來做,肯定太落后了。如果你們的前端項(xiàng)目是使用 vue 或者 React 來構(gòu)建的,那么接下來的文章,對(duì)你們來說,或多或少能得到一些幫助。

        反之,我個(gè)人建議不用花時(shí)間看下去,因?yàn)榭赡軟]辦法很好的落地。但如果你感興趣,能夠看完,那么相信你也能看到一些有價(jià)值的東西。嚴(yán)格來講是互相學(xué)習(xí)。

        插件解釋以及選擇

        按照一些前輩的講述,以下這塊我也不能跳過。如果你是第一次聽到前端覆蓋率,那么你肯定會(huì)好奇,有哪些工具能拿來用,并且哪個(gè)工具會(huì)更加契合。我不能一上來就告訴你要用什么?這樣會(huì)叫你囫圇吞棗,云里霧里。

        如果這樣,我也就沒有做好寫這篇文章的真正的目的,就是讓看的人能夠帶著思考看完且看懂。

        image.png

        上面這張圖,就是對(duì)能拿來做覆蓋率的工具的一個(gè)匯總,對(duì)比,詳情。

        看完這個(gè),相信大家就能知道,自己所涉及到的業(yè)務(wù),需要用什么樣的工具去實(shí)現(xiàn)覆蓋率收集的工作了。

        當(dāng)然,光看這個(gè),你肯定還是不大能知道,就算選擇 Istanbul 之后,怎么去用?里面有哪些東西?我該怎么插樁?服務(wù)端渲染和客戶端渲染這樣不同處理的情況下,如何插樁?等等。

        顯然,已經(jīng)給你準(zhǔn)備好了:

        image.png

        這里解釋一下,nyc 是 Istanbul 的進(jìn)階版,解決了很多問題,并且當(dāng)下一直有很多優(yōu)秀的人在維護(hù)這個(gè)開源。當(dāng)然,這篇文章不講這個(gè)。

        看完這兩張圖,相信大家已經(jīng)有點(diǎn)知道,做前端覆蓋率,為什么選擇 Istanbul ,以及 如何選擇性的對(duì)前端項(xiàng)目進(jìn)行插樁。

        為了方便大家理解,我還是需要拷貝一下這位老師的東西??截惖南嚓P(guān)原文,在上面列舉的文章中。(原諒我如此不要臉...嚶嚶嚶)

        運(yùn)行前插樁

        nyc instrument

        針對(duì)編譯之后的JS文件 , 進(jìn)行手動(dòng)插樁 , 形成插樁后的新JS文件

        babel-plugin-istanbul

        istanbul提供的babel插件 , 能夠在代碼編譯打包階段直接植入插樁代碼 適用于使用babel的前端工程,基于react和vue的工程都可以

        運(yùn)行時(shí)插樁

        **im.hookLoader **

        適用于服務(wù)端的文件掛載 比如node應(yīng)用 當(dāng)應(yīng)用啟動(dòng)時(shí) , 會(huì)在require入口處添加hook方法 , 使得應(yīng)用啟動(dòng)時(shí)加載到的都是插樁后的代碼

        im.createClientHandler

        適用于客戶端的JS掛載 ,比如react和vue的js 通過指定root路徑,會(huì)把所有該路徑的js文件請(qǐng)求攔截,返回插樁后的代碼,即瀏覽器請(qǐng)求靜態(tài)資源的動(dòng)作 效果與babel-plugin-istanbul類似,區(qū)別在于該方法是在瀏覽器請(qǐng)求js時(shí)才會(huì)返回插樁代碼,是一個(gè)動(dòng)態(tài)過程

        babel-plugin-istanbul

        最上面,我講到如果項(xiàng)目是用 vue 或者 React 的,可以直接使用插件 babel-plugin-istanbul,我簡單說下這個(gè)東西是干嘛的,他其實(shí)就是一個(gè)做好了的npm包,這個(gè)包做的事情,就是給你所在的項(xiàng)目進(jìn)行插樁。那它是在什么時(shí)候插樁呢?

        ?就是在你 run 項(xiàng)目的時(shí)候

        npm install babel-plugin-istanbul 之后,你再去run你的前端項(xiàng)目的時(shí)候,會(huì)發(fā)現(xiàn),編譯的時(shí)間會(huì)比原來run的時(shí)間 稍微的拉長了那么一點(diǎn)。那這一點(diǎn)時(shí)間,其實(shí)就是它在工作,再給你的項(xiàng)目處理,對(duì)你項(xiàng)目里面所設(shè)計(jì)的文件,進(jìn)行插樁編譯處理。

        這里肯定會(huì)有疑問,會(huì)不會(huì)給項(xiàng)目帶來什么影響?這個(gè)會(huì)影響前端項(xiàng)目嗎,emmm,并不會(huì)。

        它只會(huì)在你的項(xiàng)目里生成相對(duì)應(yīng)的覆蓋率文件(在后面調(diào)用的過程中有一個(gè)映射關(guān)系,后面會(huì)說到)。

        看下具體的 npm 依賴吧:

        image.png

        要注意的是,盡量在dev環(huán)境下進(jìn)行安裝此依賴。

        文件配置

        如果已經(jīng) install 完成了,還需要做一件事,就是配置文件做下配置:

        babel.config.js

        module.exports = {
          presets: [
            '@vue/app'
          ],
          plugins: [
            ['babel-plugin-istanbul', {
              extension: ['.js''.vue']
            }],
            '@babel/plugin-transform-modules-commonjs'
          ],

          env: {
            test: {
              plugins: [
                ["istanbul", {
                  useInlineSourceMaps: false
                }]
              ]
            }
          }
        }
        復(fù)制代碼

        .babelrc

           {
          "presets":["@babel/preset-env"],
          "plugins": [
            "@babel/plugin-transform-modules-commonjs",
            ["babel-plugin-istanbul", {
              "extension": [".js"".vue"]
            }]
          ],
          "env": {
            "test": {
              "plugins": [
                ["istanbul", {
                  "exclude": [
                    "**/*.spec.js"
                  ],
                  "useInlineSourceMaps"false
                }]
              ]
            }
          }
        }
        復(fù)制代碼

        這是針對(duì)兩種不同的文件的不同配置,為了方便大家我就都貼出來了。具體這些配置有什么用?干嗎用?

        在源碼的 source-maps 這一塊有講,看大家需要做自己的處理,但是按照上面的方法去配置,是完全OK的。

        然后,這里面細(xì)心的同學(xué)已經(jīng)注意到了一個(gè)點(diǎn),就是:'@babel/plugin-transform-modules-commonjs' ,為什么要用這個(gè)呢?其他都是些正常的配置。

        這里呢,其實(shí)在我最上面貼出來的一篇文章中的評(píng)論區(qū) reply-169807 有詳細(xì)的解說,我在這里簡單說下就是:使用 babel-plugin-Istanbul 插樁,和 babel-plugin-import 插件不兼容,導(dǎo)致編譯失敗 。

        npm run

        如果以上工作你都做好了,那就試試 把你的服務(wù)run起來吧。

        ok,假設(shè)你已經(jīng)run好了,這個(gè)時(shí)候,打開你的項(xiàng)目,再打開控制臺(tái)。執(zhí)行 window.coverage。

        如果你看到的同下面一致,那么恭喜你,你已經(jīng)成功了第一步

        image.png

        并且這個(gè)時(shí)候,你就能欣喜的看到 babel-plugin-istanbul 做的事情了,給你項(xiàng)目只要是涉及到的文件,都做了插樁處理,回傳給你這些覆蓋率信息。

        要是想看具體點(diǎn),就隨便點(diǎn)一個(gè)詳情,他會(huì)清楚的告訴你,所覆蓋的行信息,語句,方法等。

        image.png

        底層簡介

        說了這么多,都忘記了覆蓋率所應(yīng)當(dāng)講的基本了。大意了,沒有閃。

        覆蓋率維度

        • Statements: 語句覆蓋率,所有語句的執(zhí)行率;
        • Branches: 分支覆蓋率,所有代碼分支如 if、三目運(yùn)算的執(zhí)行率;
        • Functions: 函數(shù)覆蓋率,所有函數(shù)的被調(diào)用率;
        • Lines: 行覆蓋率,所有有效代碼行的執(zhí)行率,和語句類似,但是計(jì)算方式略有差別

        插裝詳解

        image.png

        插裝原理

        image.png

        其實(shí)看到這個(gè)原理,我覺得大家就能理解,上面我說過 babel-plugin-istanbul 會(huì)生成對(duì)應(yīng)的插樁后文件了。其實(shí)這些文件,存放在你的項(xiàng)目中,并不會(huì)影響你的項(xiàng)目,最多是占用了項(xiàng)目容量。

        這個(gè)圖也清晰的給出了,讀取覆蓋率數(shù)據(jù)的原理,就是會(huì)根據(jù)你當(dāng)前訪問的頁面,拿到一對(duì)一的映射關(guān)系,找到插樁后的文件。我看到這個(gè)原理的時(shí)候,就大寫的一個(gè)字,服!

        插裝前后文件對(duì)比

        插裝前:

        image.png

        插裝后:

        image.png

        應(yīng)該能很明顯看到一些計(jì)數(shù)器。這些就是數(shù)據(jù)統(tǒng)計(jì)用的計(jì)數(shù)了。

        源碼解析-istanbul-middleware

        大家還記得上面說到了 window.coverage 已經(jīng)給到了你 覆蓋率統(tǒng)計(jì)后的相關(guān)數(shù)據(jù)了,但是還沒講怎么收集對(duì)不對(duì),接下來就講收集。

        對(duì)于 window.coverage 所收集到的這些信息,進(jìn)行收集處理就要用到另一個(gè)偉大的開源,就是最上面給到的:istanbul-middleware ,我這邊也給這簡稱:im。這個(gè)主要是干嘛的呢?

        具體詳情就點(diǎn)這個(gè)鏈接去看了,不贅述咯。主要說下,這個(gè)中間件,提供了以下的相關(guān)功能。

        image.png

        這是im項(xiàng)目里給到的四個(gè)接口,看到這,大家應(yīng)該就恍然大悟了。也就是說,這個(gè)中間件 im 就是提供了四個(gè)接口,來對(duì) window.coverage 收集到的數(shù)據(jù)進(jìn)行處理啊。

        那我們一個(gè)個(gè)來看,這四個(gè)接口分別是什么?

        1. 請(qǐng)求全量 / ;
        2. 重置 reset ;
        3. 下載 download ;
        4. 提交 client 。

        其實(shí)還有一個(gè)show接口 展示用的。

        ok,知道了這些,就相對(duì)完美了,我們就要用這個(gè) client,來提交收集到的數(shù)據(jù)。關(guān)于這四個(gè)接口具體怎么用,其實(shí)也很簡單,就是在本地起一個(gè)node服務(wù),這幾個(gè)接口都寫在這個(gè)服務(wù)下面,直接根據(jù)ip來調(diào)用就可以了。

        具體的可以看這個(gè)開源項(xiàng)目中,有一個(gè)演示就是在test目錄下有個(gè)app。這個(gè)就是作者給到的一個(gè)演示的demo。聰明的你,一看就會(huì)。

        源碼解析-test/app/demo

        為了方便大家能迅速上手,我這里給大家整一下這個(gè)demo。當(dāng)然,這里我已經(jīng)在源碼上做了改動(dòng),不過影響也不大。

        首先進(jìn)入app這個(gè)路徑之后,最外層的 index.js 就是這個(gè)node服務(wù)的入口文件,在readme 中能看到就是說啟動(dòng)的script。node index.js --coverage # start the app with coverage 。他其實(shí)給到了四個(gè)腳本語句,但是我們只用這一個(gè)。

        執(zhí)行完這個(gè)之后,服務(wù)就啟動(dòng)起來了。這個(gè)時(shí)候,輸入 https://localhost:8988/xxx 。就能看到你的覆蓋率收集的數(shù)據(jù)了。

        好,我們?cè)僬f一處,就是在server下的index.js。為什么要說這個(gè)呢,其實(shí)在最外層的 index.js 文件中,你能看到一個(gè)引用。就是:require('./server').start(port, coverageRequired);。所以其實(shí)大概就知道,這個(gè)入口中的一些配置項(xiàng),都是在server下讀取的。

        const { json } = require('body-parser');

        /*jslint nomen: true */
        var
            // nopt = require('nopt'),
            // config = nopt({ coverage: Boolean }),
            istanbulMiddleware = require('istanbul-middleware'),
            coverageRequired = true,
            port = 8988;

        if (coverageRequired) {
            console.log('server start with coverage !');
            istanbulMiddleware.hookLoader(__dirname, { verbose: true });
        }
        // console.log('Starting server at: http://localhost:' + port);
        // if (!coverageRequired) {
        //     console.log('Coverage NOT turned on, run with --coverage to turn it on');
        // }
        require('./server').start(port, coverageRequired);
        復(fù)制代碼

        這個(gè)時(shí)候,我們看到server中的index.js 文件的時(shí)候,能看到一些東西,端口號(hào)啊,以及我們當(dāng)初啟動(dòng)服務(wù)的 --coverage 傳參啊,以及express 相關(guān)啟服務(wù)操作等等。

        所以,看到這,你就可以根據(jù)你自己的喜好,改一些東西。

        比方我不喜歡每次啟node服務(wù)的時(shí)候,都是 node index.js --coverage 我想簡單點(diǎn),就直接 node index.js 那其實(shí)就能在外面的index文件里面改,直接把coverageRequired 這個(gè)參數(shù)設(shè)置成true就好了。

        再比如,新開始的情況下,服務(wù)啟動(dòng)起來,如果需要被其他端調(diào),就涉及到跨域,那么就要做跨域的處理。再比如,后面要講到的覆蓋率上報(bào)插件,只能識(shí)別 https,你本地起的服務(wù),訪問都是用 http 訪問,那么也需要在這邊進(jìn)行改動(dòng)等等一系列。

        這邊我給一下,我在server 下的 index 文件做的處理,大家可以參考:

        /*jslint nomen: true */
        var path = require('path'),
            express = require('express'),
            url = require('url'),
            publicDir = path.resolve(__dirname, '..''public'),
            coverage = require('istanbul-middleware'),
            bodyParser = require('body-parser');

        function matcher(req) {
            var parsed = url.parse(req.url);
            return parsed.pathname && parsed.pathname.match(/\.js$/) && !parsed.pathname.match(/jquery/);
        }

        module.exports = {
            start: function (port, needCover) {
                var app = express();
                var http = require('http');
                var https = require('https');
                var fs = require('fs');

                //設(shè)置跨域訪問
                app.all('*'function (req, res, next) {
                    // console.log('req: ', req)
                    res.header("Access-Control-Allow-Credentials""true");  //服務(wù)端允許攜帶cookie
                    res.header("Access-Control-Allow-Origin", req.headers.origin);  //允許的訪問域
                    res.header("Access-Control-Allow-Headers""Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE");  //訪問頭
                    res.header("Access-Control-Allow-Methods""PUT,POST,GET,DELETE,OPTIONS");  //訪問方法
                    res.header("Content-Security-Policy"" upgrade-insecure-requests");
                    res.header("X-Powered-By"' 3.2.1');
                    if (req.method == 'OPTIONS') {
                        res.header("Access-Control-Max-Age", 86400);
                        res.sendStatus(204); //讓options請(qǐng)求快速返回.
                    }
                    else {
                        next();
                    }
                });
                if (needCover) {
                    console.log('Turn on coverage reporting at' + '/bilibili/webcoverage');
                    app.use('/bilibili/webcoverage', coverage.createHandler({ verbose: true, resetOnGet: true }));
                    app.use(coverage.createClientHandler(publicDir, { matcher: matcher }));
                }
                app.use('/', express.static(__dirname + ''));
                app.use(bodyParser.urlencoded({ extended: true }));
                app.use(bodyParser.json());

                app.set('view engine''hbs');
                app.engine('hbs', require('hbs').__express);
                app.use(express['static'](publicDir));

                // app.listen(port);

                var httpServer = http.createServer(app);
                var httpsServer = https.createServer({
                    key: fs.readFileSync('E:/coveragewithweb/app/cert/privatekey.pem''utf8'),  //app\cert\privatekey.pem app\server\index.js
                    cert: fs.readFileSync('E:/coveragewithweb/app/cert/certificate.crt''utf8')
                }, app);

                // httpServer.listen(port, function () {
                //     console.log('HTTP Server is running on: http://localhost:%s', port);
                // });
                httpsServer.listen(port, function () {
                    console.log('HTTPS Server is running on: https://localhost:%s', port);
                });
            }
        };
        復(fù)制代碼

        源碼解析-提交 client

        好了,看完服務(wù)了,我們來看這個(gè)client,既然是提交接口,并且是本地啟的node服務(wù),并且,嚴(yán)格聲明了請(qǐng)求格式:"Content-Type", "application/json"。

        那么簡單了,我們來直接把服務(wù)啟動(dòng)起來,走一個(gè)試試唄。

        image.png

        很完美,已經(jīng)提交成功了。那既然提交了,就再看看提交的結(jié)果唄。

        image.png

        哇??吹搅耸裁矗喼碧袅?。

        當(dāng)然了,這邊看到的url效果是我處理后的,我等下會(huì)一個(gè)個(gè)講哦,大家先別急。

        既然知道了client 是提交接口,是不是要去看下源碼?

        定位到源碼。是一個(gè)叫 handlers.js 的文件,里面很明顯能看到我們剛剛看到的四個(gè)接口,并且這四個(gè)接口,都在一個(gè)叫 creatHandler 的對(duì)象下面。其實(shí)看到這,我才能理解當(dāng)初看其他老師說,根據(jù)自己的業(yè)務(wù)去改 creatHandler 源碼的意思,其實(shí)就是說的這邊。

        這邊我們講client 那就只看這一個(gè),具體的大家可以去源碼看

            //merge client coverage posted from browser
                app.post('/client'function (req, res) {
                // console.log('req: ', req)
                var body = req.body;
                if (!(body && typeof body === 'object')) { //probably needs to be more robust
                 return res.send(400, 'Please post an object with content-type: application/json');
                }
                core.mergeClientCoverage(body);
                res.send({ success: '上報(bào)成功' });
            });
        復(fù)制代碼

        可以看到client,底層是調(diào)用的 core.mergeClientCoverage() 這個(gè)方法,并且將我們通過client 傳進(jìn)來的 覆蓋率信息 作為參數(shù)傳進(jìn)去。

        那么好了,我們直接去看 mergeClientCoverage() 這個(gè)就可以了,這個(gè)就在 handlers.js 的同一層級(jí)下的 core.js 中。我們把源碼貼出來

        function mergeClientCoverage(obj) { // resolve coverage datas  這邊傳進(jìn)來的就是 coverage的 window.coverage 信息 
                var coverage = getCoverageObject();  // 初始狀態(tài)下的coverage 覆蓋信息
                Object.keys(obj).forEach(function (filePath) {
                var original = coverage[filePath], // filepath = key 即文件路徑 || original 是已經(jīng)存在的數(shù)據(jù)
                    added = obj[filePath],  // added 最新的覆蓋率信息數(shù)據(jù)
                    result;  // 定義一個(gè)預(yù)備內(nèi)存
                if (original) {
                    result = utils.mergeFileCoverage(original, added);
                } else {
                    result = added;
                }
                coverage[filePath] = result;  // 最終的覆蓋率信息 存放到臨時(shí)內(nèi)存!getCoverageObject()
            }
        });
            // coverage 是最終的覆蓋率信息,每次進(jìn)行存儲(chǔ),用key值來關(guān)聯(lián),key指向業(yè)務(wù)版本
        }
        復(fù)制代碼

        這段源碼中,其實(shí)可以看到他是怎么處理的,首先是調(diào)用 getCoverageObject() 去拉取最初始的覆蓋率信息,給一個(gè)初始定義 coverage。

        Object.keys(obj).forEach(function (filePath) {} 這個(gè)就是遍歷處理傳進(jìn)來的新的覆蓋率對(duì)象了。

        original 定義的是,覆蓋率服務(wù)原本就已經(jīng)存在的數(shù)據(jù)。

        added 則是去處理新進(jìn)來的覆蓋率數(shù)據(jù)。

        最后定義一個(gè)臨時(shí)內(nèi)存存儲(chǔ) result 。

        在后面的便利過程中,會(huì)有一個(gè)merge的過程,就是去判斷 original 是否存在,如果存在就是存在原始數(shù)據(jù),就需要將新數(shù)據(jù)和原始數(shù)據(jù) 進(jìn)行merge合并,即:result = utils.mergeFileCoverage(original, added);,如果沒有,那就直接新增就好了 result = added;

        最后全部的覆蓋率數(shù)據(jù)都給到了result 這個(gè)對(duì)象,最后,直接將最后的結(jié)果,再放到先開始定義的 coverage 中,就完成了整個(gè)覆蓋率合并的操作。

        其實(shí)大家可以在往深了看,就是這個(gè) utils.mergeFileCoverage(original, added) 做了什么事。這邊我就不一一講述了。

        那么為什么要把這些拿出來看呢,其實(shí)這塊需要做的處理有幾個(gè),一個(gè)就是,我在文章最初給到的那些老師的文章中有說到,就是根據(jù)自己的業(yè)務(wù)邏輯去設(shè)計(jì)自己的覆蓋率數(shù)據(jù)采集。

        這里我又要不要臉了,我要去copy了。

        image.png

        也就是說,如果你有好幾個(gè)業(yè)務(wù),你不能一下子不分版本,分支,項(xiàng)目名的都傳進(jìn)來進(jìn)行處理。你需要跟你的項(xiàng)目的分支,版本,業(yè)務(wù)名等等進(jìn)行自己的處理,再傳進(jìn)來進(jìn)行收集。

        另一個(gè)就是,這里啟的node服務(wù),都是占用的臨時(shí)內(nèi)存。需要我們?nèi)⑦@些收集到的覆蓋率數(shù)據(jù)采集完成之后,再去落庫。存儲(chǔ)起來,不然服務(wù)掛了,什么都沒了。這是多可怕的一件事。

        還有一個(gè)最終的要就是,你需要在這里去處理不同機(jī)器上的文件統(tǒng)計(jì)。

        這里是什么意思呢,大家可能很好奇。其實(shí)這個(gè)就是我踩過的坑。就是比方說,一個(gè)項(xiàng)目發(fā)布到了一個(gè)機(jī)器上去,這里簡稱這臺(tái)服務(wù)為A,而你的覆蓋率統(tǒng)計(jì)的這個(gè)node服務(wù),部署再你的另一臺(tái)機(jī)器上,我們簡稱B。

        那么這個(gè)時(shí)候,你拿到A的覆蓋率數(shù)據(jù)信息,就不能被B所解析,這里的解析的意思就是,回傳過來的哪些覆蓋率數(shù)據(jù),B服務(wù)器上,沒有文件能對(duì)的上,因?yàn)锽服務(wù)上沒有發(fā)布過你的那個(gè)項(xiàng)目啊。你A服務(wù)上的覆蓋率信息,可以通過,window.coverage 上報(bào)回來。但是我本地沒有文件能跟你匹配。

        所以就會(huì)報(bào)錯(cuò),如果你的B服務(wù)上的node服務(wù),后期自己不去寫的健壯一些,可能就會(huì)因?yàn)檎也坏?,服?wù)掛了。

        所以,你需要在這里,去將A服務(wù)上的項(xiàng)目,同步一份到B服務(wù)上,并且,你需要去更改收集到的覆蓋率信息中的 key 關(guān)鍵字,以及 每個(gè)子對(duì)象下的path 路徑為B服務(wù)的key 以及 路徑。

        emmm 不知道大家能不能看懂,要是沒明白,再聯(lián)系我細(xì)講吧。

        源碼解析-提交 show

        好了,花了大的篇幅,講client。差不多也講完了。

        那就再來看看 show 吧。一樣的,先看源碼:

            //merge client coverage posted from browser
            //show page for specific file/ dir for /show?file=/path/to/file
            app.get('/show'function (req, res) {
            var origUrl = url.parse(req.originalUrl).pathname
            u = url.parse(req.url).pathname,
                pos = origUrl.indexOf(u),  // show 起使位置
                file = req.query.p;  //  p的參數(shù)值

            if (pos >= 0) {
                origUrl = origUrl.substring(0, pos);
            }
            if (!file) {
                res.setHeader('Content-type''text/plain');
                return res.end('[p] parameter must be specified');
            }
            // console.log('res: ', res)
            core.render(file, res, origUrl);
        });
        復(fù)制代碼

        其實(shí)這邊也可以清楚的看到,這個(gè)show,其實(shí)也是調(diào)用的底層的 core.render(file, res, origUrl); 只不過在調(diào)用之前,給他傳了 show 后面跟的參數(shù),返回,origUrl。

        話不多說,直接去看 render()。

        function render(filePath, res, prefix) {
            var collector = new istanbul.Collector(),
                treeSummary,
                pathMap,
                linkMapper,
                outputNode,
                report,
                coverage,
                fileCoverage;
            // coverage = getCoverageObject();  // 查詢已經(jīng)處理后的覆蓋率信息
            //  這邊的覆蓋率信息可以從庫中讀取,指定訪問地址,查詢校驗(yàn)  webvideopakageistanbul-1.0
            try {
                    coverage = getCoverageObject()
                    if (!(coverage && Object.keys(coverage).length > 0)) {
                        res.setHeader('Content-type''text/plain');
                        return res.end('No coverage information has been collected'); //TODO: make this a fancy HTML report
                    }
                    prefix = prefix || '';
                    if (prefix.charAt(prefix.length - 1) !== '/') {  // 處理路徑,路徑后面跟目錄路徑指示
                        prefix += '/';
                    }
                    utils.removeDerivedInfo(coverage);
                    collector.add(coverage); // 覆蓋率收集容器
                    treeSummary = getTreeSummary(collector); //  處理覆蓋率的樹信息,filepath,filename,fileinfo ...
                    pathMap = getPathMap(treeSummary);  // 處理每個(gè)不同路下的覆蓋信息 將全部的分類,收集存入數(shù)組
                    filePath = filePath || treeSummary.root.fullPath();  // 如果沒有指定路徑查找相關(guān)的覆蓋信息,就展示全量的數(shù)據(jù)
                    outputNode = pathMap[filePath]; // 如果有具體的搜索參數(shù),則在數(shù)組中找對(duì)應(yīng)的對(duì)象 返回node節(jié)點(diǎn)信息
                    if (!outputNode) {  // 查詢路徑下不存在相關(guān)信息 處理
                        res.statusCode = 404;
                        return res.end('No coverage for file path [' + filePath + ']');
                    }
                    linkMapper = {
                        hrefFor: function (node) {
                            return 'https://10.23.176.55:8988' + prefix + 'show?p=' + node.fullPath();
                        },
                        fromParent: function (node) {
                            return this.hrefFor(node);
                        },
                        ancestor: function (node, num) {
                            var i;
                            for (i = 0; i < num; i += 1) {
                                node = node.parent;
                            }
                            return this.hrefFor(node);
                        },
                        asset: function (node, name) { // 資源文件處理 resource files 
                            return 'https://10.23.176.55:8988' + prefix + 'asset/' + name;
                        }
                    };
                    report = Report.create('html', { linkMapper: linkMapper });  // 處理最終的報(bào)告
                    res.setHeader('Content-type''text/html');
                    if (outputNode.kind === 'dir') {
                        report.writeIndexPage(res, outputNode);
                    } else {
                        fileCoverage = coverage[outputNode.fullPath()];
                        utils.addDerivedInfoForFile(fileCoverage);
                        report.writeDetailPage(res, outputNode, fileCoverage);
                    }
                    return res.end();
                
            } catch (e) {
                res.send({ '查找失敗,錯(cuò)誤詳情: ': e });
            }
        }
        復(fù)制代碼

        var collector = new istanbul.Collector() 首先看這個(gè),這個(gè)具體是干嘛用的呢,我理解下來,他其實(shí)就是一個(gè)覆蓋率收集容器,主要做的工作,就是根據(jù)你show 后面帶進(jìn)來的參數(shù),去解析相關(guān)的數(shù)據(jù)給你展示。

        coverage = getCoverageObject() 這個(gè)眼熟不,他還是去拿覆蓋率數(shù)據(jù)用的,但是這里的coverage,其實(shí)要根據(jù)你自己的業(yè)務(wù),進(jìn)行修改,這個(gè) getCoverageObject() 永遠(yuǎn)都是初始的數(shù)據(jù),或者就是你服務(wù)目前收集到的數(shù)據(jù)。絕對(duì)不能一直這樣用。

        后面你需要跟你你自己落庫的覆蓋率數(shù)據(jù),進(jìn)行修改。將coverage = xxxx 替換成你自己的東西。

        emmm。不知道你們能不能看懂,看不懂沒關(guān)系,后面問我。

        treeSummary = getTreeSummary(collector); 這個(gè)其實(shí)是處理覆蓋率的樹信息,能看到我們收集道德數(shù)據(jù),都是一個(gè)json的樹結(jié)構(gòu),這個(gè)就是將那些數(shù)據(jù)做一個(gè)數(shù)據(jù)化處理。

        pathMap = getPathMap(treeSummary); // 處理每個(gè)不同路下的覆蓋信息 將全部的分類,收集存入數(shù)組

        filePath = filePath || treeSummary.root.fullPath(); // 如果沒有指定路徑查找相關(guān)的覆蓋信息,就展示全量的數(shù)據(jù)

        linkMapper 這個(gè)具體是干嘛用的呢,細(xì)心看的化,就會(huì)發(fā)現(xiàn),這個(gè)其實(shí)就是在展示覆蓋率數(shù)據(jù),所需要的資源文件,其中包含了 css 以及 js。自帶屬性。

        utils.addDerivedInfoForFile(fileCoverage); report.writeDetailPage(res, outputNode, fileCoverage);

        這兩個(gè)就是判斷到訪問的數(shù)據(jù)存在之后,進(jìn)行文件整合,就是在上面 treeSummary pathMap filePath 處理完之后,進(jìn)行全量數(shù)據(jù)吞吐。report 就是講數(shù)據(jù)進(jìn)行文本處理,最終展示html即可。

        同樣的來分析這快源碼的意義是什么呢?

        這塊的處理相對(duì)而言就較為簡單,上面client 說到,需要根據(jù)不同的業(yè)務(wù),對(duì)不同的項(xiàng)目,不同的分支等等進(jìn)行落庫處理,那么這里其實(shí)就是,你在show 后面 的p參數(shù)值,拿到之前存儲(chǔ)的數(shù)據(jù)進(jìn)行展示的時(shí)候,進(jìn)行一對(duì)一處理用。

        其實(shí)這塊還可以往下再看,就比方說,getTreeSummary() 這個(gè)方法做了哪些事,等等,如果想徹底搞清楚,可以再接著往下讀。我這邊就不詳細(xì)敘述了。

        插件上報(bào)

        上面將istanbul 相關(guān)的源碼都解讀了,相信大家都能看懂,甚至能看的比我都深。那再來說下關(guān)于數(shù)據(jù)的上報(bào)吧。

        數(shù)據(jù)上報(bào)這塊,在其他老師的文章中可以看到,有兩種方法:chrome插件 和 fiddler。其實(shí)還有一種方法 就是 sidebar,容器邊車模式。這個(gè)也是我請(qǐng)教了以為大佬。給到的方案。但是我沒有搞透徹。要是你知道,可以私我。我想請(qǐng)教一二。

        那這邊我只講一下,插件上報(bào)。即 chrome插件。首先需要說一下,就是我們的覆蓋率數(shù)據(jù),是存在與當(dāng)前頁的,如果當(dāng)前頁的 window 對(duì)象下面是有coverage集合的,就可以通過 window.coverage 進(jìn)行獲取,再調(diào)用client 進(jìn)行上報(bào)。

        那么如果使用chrome 插件 就需要 chrome插件 能讀到 你當(dāng)前頁的window對(duì)象。不然就獲取不到下面的coverage集合。

        如果有寫過chrome 插件的,肯定是知道,content_script.js 是可以與當(dāng)前頁面進(jìn)行dom交互,但是會(huì)有一個(gè)問題,就是拿不到當(dāng)前頁的window對(duì)象。

        所以我這邊做了一個(gè)處理:就是再覆蓋率的node服務(wù)上,單獨(dú)寫一個(gè)文件,通過 content_script.js 寫到 當(dāng)前頁的dom中。代碼如下:

        test.js
        setTimeout(() => {
            if (window.__coverage__) {
                localStorage.setItem('coveragecollect', JSON.stringify(window.__coverage__))
            }
        }, 3000)


        content_script.js
        setTimeout(function () {
            var ss = document.createElement('script')

            ss.src = "https://10.23.176.55:8988/test.js"

            document.body.appendChild(ss)
        }, 3000)
        復(fù)制代碼

        為了方便,先寫到local中,然后,再使用插件的當(dāng)前頁的js 執(zhí)行一段executeScript,植入另一個(gè)腳本,與 content_script.js 進(jìn)行交互。這樣就解決了無法獲取的問題:

        popup.js
        let changeColor = document.getElementById('changeColor');

        changeColor.onclick = function (element) {
            let color = element.target.value;
            chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
                chrome.tabs.executeScript(
                    tabs[0].id,
                    { file: 'js/test.js' });
            });
        };

        interact.js
        setTimeout(function () {
            var aa = localStorage.getItem('coveragecollect')
            if (aa) {
                console.log('存在')
                var data = aa;
                var xhr = new XMLHttpRequest();
                xhr.withCredentials = true;

                xhr.addEventListener("readystatechange"function () {
                    if (this.readyState === 4) {
                        alert(this.responseText);
                    }
                });

                xhr.open("POST""https://10.23.176.55:8988/bilibili/webcoverage/client");
                xhr.setRequestHeader("Content-Type""application/json");

                xhr.send(data);
            } else {
                console.log('不存在')
            }

        }, 3000);
        復(fù)制代碼

        詳細(xì)代碼請(qǐng)見coverage-chrome。

        插件的具體使用可以參考我之前寫的一篇chrome插件

        結(jié)尾

        后面我將持續(xù)開坑關(guān)于前端覆蓋率之自動(dòng)化集成,引用 code-coverage,另一個(gè)優(yōu)秀的開源。

        好了,以上就是本文要講的全部內(nèi)容了,要是你有更高的見地,很歡迎與我交流,我們互相學(xué)習(xí)。

        最后

        歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
        回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
        回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
        回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
        如果這篇文章對(duì)你有幫助,在看」是最大的支持
         》》面試官也在看的算法資料《《
        “在看和轉(zhuǎn)發(fā)”就是最大的支持



        瀏覽 47
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            亚洲黄色免费观看 | 上床视频网站 | 欧美做爰BBB性BBBBB丨D | 色七七久久影院 | 99re6在线精品视频免费播放 | 色色日韩| 91亚洲精品久久久久久久久久久久 | 啊轻点灬太粗嗯太深了用力了视频 | 寡妇高潮一级毛片免费视频 | 美女扒开胸罩免费视频网 |