手摸手之前端覆蓋率實踐
點擊上方 前端瓶子君,關(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í)。
插件解釋以及選擇
按照一些前輩的講述,以下這塊我也不能跳過。如果你是第一次聽到前端覆蓋率,那么你肯定會好奇,有哪些工具能拿來用,并且哪個工具會更加契合。我不能一上來就告訴你要用什么?這樣會叫你囫圇吞棗,云里霧里。
如果這樣,我也就沒有做好寫這篇文章的真正的目的,就是讓看的人能夠帶著思考看完且看懂。

上面這張圖,就是對能拿來做覆蓋率的工具的一個匯總,對比,詳情。
看完這個,相信大家就能知道,自己所涉及到的業(yè)務(wù),需要用什么樣的工具去實現(xiàn)覆蓋率收集的工作了。
當(dāng)然,光看這個,你肯定還是不大能知道,就算選擇 Istanbul 之后,怎么去用?里面有哪些東西?我該怎么插樁?服務(wù)端渲染和客戶端渲染這樣不同處理的情況下,如何插樁?等等。
顯然,已經(jīng)給你準(zhǔn)備好了:

這里解釋一下,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 依賴吧:

要注意的是,盡量在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)成功了第一步

并且這個時候,你就能欣喜的看到 babel-plugin-istanbul 做的事情了,給你項目只要是涉及到的文件,都做了插樁處理,回傳給你這些覆蓋率信息。
要是想看具體點,就隨便點一個詳情,他會清楚的告訴你,所覆蓋的行信息,語句,方法等。

底層簡介
說了這么多,都忘記了覆蓋率所應(yīng)當(dāng)講的基本了。大意了,沒有閃。
覆蓋率維度
Statements: 語句覆蓋率,所有語句的執(zhí)行率; Branches: 分支覆蓋率,所有代碼分支如 if、三目運算的執(zhí)行率; Functions: 函數(shù)覆蓋率,所有函數(shù)的被調(diào)用率; Lines: 行覆蓋率,所有有效代碼行的執(zhí)行率,和語句類似,但是計算方式略有差別
插裝詳解

插裝原理

其實看到這個原理,我覺得大家就能理解,上面我說過 babel-plugin-istanbul 會生成對應(yīng)的插樁后文件了。其實這些文件,存放在你的項目中,并不會影響你的項目,最多是占用了項目容量。
這個圖也清晰的給出了,讀取覆蓋率數(shù)據(jù)的原理,就是會根據(jù)你當(dāng)前訪問的頁面,拿到一對一的映射關(guān)系,找到插樁后的文件。我看到這個原理的時候,就大寫的一個字,服!
插裝前后文件對比
插裝前:

插裝后:

應(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)功能。

這是im項目里給到的四個接口,看到這,大家應(yīng)該就恍然大悟了。也就是說,這個中間件 im 就是提供了四個接口,來對 window.coverage 收集到的數(shù)據(jù)進行處理啊。
那我們一個個來看,這四個接口分別是什么?
請求全量 / ; 重置 reset ; 下載 download ; 提交 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ù)啟動起來,走一個試試唄。

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

哇。看到了什么,簡直太棒了。
當(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了。

也就是說,如果你有好幾個業(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í)。
