<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手摸手之前端覆蓋率實踐

          共 31410字,需瀏覽 63分鐘

           ·

          2021-06-01 13:26

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

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

          來源:i.m.t

          https://juejin.cn/post/6959147556295180324

          前述

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

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

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

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

          寫在前面

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

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

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

          React Native 代碼覆蓋率獲取探索

          覆蓋率實時統(tǒng)計工具

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

          babel-plugin-istanbul

          istanbul-middleware

          nyc

          code-coverage

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

          正文

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

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

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

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

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

          插件解釋以及選擇

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

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

          image.png

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

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

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

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

          image.png

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

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

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

          運行前插樁

          nyc instrument

          針對編譯之后的JS文件 , 進行手動插樁 , 形成插樁后的新JS文件

          babel-plugin-istanbul

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

          運行時插樁

          **im.hookLoader **

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

          im.createClientHandler

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

          babel-plugin-istanbul

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

          ?就是在你 run 項目的時候

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

          這里肯定會有疑問,會不會給項目帶來什么影響?這個會影響前端項目嗎,emmm,并不會。

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

          看下具體的 npm 依賴吧:

          image.png

          要注意的是,盡量在dev環(huá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ù)制代碼

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

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

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

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

          npm run

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

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

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

          image.png

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

          要是想看具體點,就隨便點一個詳情,他會清楚的告訴你,所覆蓋的行信息,語句,方法等。

          image.png

          底層簡介

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

          覆蓋率維度

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

          插裝詳解

          image.png

          插裝原理

          image.png

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

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

          插裝前后文件對比

          插裝前:

          image.png

          插裝后:

          image.png

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

          源碼解析-istanbul-middleware

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

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

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

          image.png

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

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

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

          其實還有一個show接口 展示用的。

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

          具體的可以看這個開源項目中,有一個演示就是在test目錄下有個app。這個就是作者給到的一個演示的demo。聰明的你,一看就會。

          源碼解析-test/app/demo

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

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

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

          好,我們再說一處,就是在server下的index.js。為什么要說這個呢,其實在最外層的 index.js 文件中,你能看到一個引用。就是:require('./server').start(port, coverageRequired);。所以其實大概就知道,這個入口中的一些配置項,都是在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ù)制代碼

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

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

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

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

          這邊我給一下,我在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請求快速返回.
                      }
                      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ù)了,我們來看這個client,既然是提交接口,并且是本地啟的node服務(wù),并且,嚴(yán)格聲明了請求格式:"Content-Type", "application/json"

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

          image.png

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

          image.png

          哇。看到了什么,簡直太棒了。

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

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

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

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

              //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: '上報成功' });
              });
          復(fù)制代碼

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

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

          function mergeClientCoverage(obj) { // resolve coverage datas  這邊傳進來的就是 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;  // 定義一個預(yù)備內(nèi)存
                  if (original) {
                      result = utils.mergeFileCoverage(original, added);
                  } else {
                      result = added;
                  }
                  coverage[filePath] = result;  // 最終的覆蓋率信息 存放到臨時內(nèi)存!getCoverageObject()
              }
          });
              // coverage 是最終的覆蓋率信息,每次進行存儲,用key值來關(guān)聯(lián),key指向業(yè)務(wù)版本
          }
          復(fù)制代碼

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

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

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

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

          最后定義一個臨時內(nèi)存存儲 result 。

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

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

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

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

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

          image.png

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

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

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

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

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

          所以就會報錯,如果你的B服務(wù)上的node服務(wù),后期自己不去寫的健壯一些,可能就會因為找不到,服務(wù)掛了。

          所以,你需要在這里,去將A服務(wù)上的項目,同步一份到B服務(wù)上,并且,你需要去更改收集到的覆蓋率信息中的 key 關(guān)鍵字,以及 每個子對象下的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ù)制代碼

          其實這邊也可以清楚的看到,這個show,其實也是調(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)處理后的覆蓋率信息
              //  這邊的覆蓋率信息可以從庫中讀取,指定訪問地址,查詢校驗  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);  // 處理每個不同路下的覆蓋信息 將全部的分類,收集存入數(shù)組
                      filePath = filePath || treeSummary.root.fullPath();  // 如果沒有指定路徑查找相關(guān)的覆蓋信息,就展示全量的數(shù)據(jù)
                      outputNode = pathMap[filePath]; // 如果有具體的搜索參數(shù),則在數(shù)組中找對應(yīng)的對象 返回node節(jié)點信息
                      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 });  // 處理最終的報告
                      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({ '查找失敗,錯誤詳情: ': e });
              }
          }
          復(fù)制代碼

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

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

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

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

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

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

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

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

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

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

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

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

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

          插件上報

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

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

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

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

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

          所以我這邊做了一個處理:就是再覆蓋率的node服務(wù)上,單獨寫一個文件,通過 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,植入另一個腳本,與 content_script.js 進行交互。這樣就解決了無法獲取的問題:

          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ì)代碼請見coverage-chrome。

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

          結(jié)尾

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

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

          最后

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



          瀏覽 47
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲口爆| 色婷婷国产精品一区在线观看 | 性少妇暴力猛交69HD | 欧美全黄一级裸片 | 一级黄色电影免费在线 |