分享前端覆盖率最佳实践

发表于 3年以前  | 总阅读数:237 次

前述

今天开一个新坑,讲讲前端覆盖率:Istanbul

谈到前端覆盖率,早在18年就有接触,但由于种种原因,并没有去具体的落地。仅有的也只是了解了这是个什么,用来干什么!

最终这个应该怎么做?如何落地?并没有深究探索。想起来略有遗憾。

但这次一定!这篇文章,将讲述如何使用 Istanbul 去收集前端覆盖率、对源码的解析、如何贴合业务理解对源码做相应的修改、覆盖率一键上报等。

写在前面

在写之前,先贴一下我参考过的四篇文章,个人认为,是关于前端覆盖率写的很好的文章:

基于 Istanbul 优雅地搭建前端 JS 覆盖率平台

聊聊前端代码覆盖率 (长文慎入)

React Native 代码覆盖率获取探索

覆盖率实时统计工具

以及相关的GitHub开源:

babel-plugin-istanbul

istanbul-middleware

nyc

code-coverage

好了,搬好小板凳,我们开始吧~

正文

前端 web 覆盖率统计

首先前端覆盖率,在当下的业务场景中,包含了 web 和 mobile ,那么很多情况下,如果mobile中不是用native原生写的,大都都是内嵌H5页面的形式存在。如果你所涉及到的业务,大部分都是用 webview。

那就直接整web端的就可以了。当然如果情况不同,那在上面的链接中也给到了,一个RN的设计链接,也可以进行参考~

当然了,相信现在前端都是用 vue 或者 React 居多了,如果还是用jQuery来做,肯定太落后了。如果你们的前端项目是使用 vue 或者 React 来构建的,那么接下来的文章,对你们来说,或多或少能得到一些帮助。

反之,我个人建议不用花时间看下去,因为可能没办法很好的落地。但如果你感兴趣,能够看完,那么相信你也能看到一些有价值的东西。严格来讲是互相学习。

插件解释以及选择

按照一些前辈的讲述,以下这块我也不能跳过。如果你是第一次听到前端覆盖率,那么你肯定会好奇,有哪些工具能拿来用,并且哪个工具会更加契合。我不能一上来就告诉你要用什么?这样会叫你囫囵吞枣,云里雾里。

如果这样,我也就没有做好写这篇文章的真正的目的,就是让看的人能够带着思考看完且看懂。

image.png

上面这张图,就是对能拿来做覆盖率的工具的一个汇总,对比,详情。

看完这个,相信大家就能知道,自己所涉及到的业务,需要用什么样的工具去实现覆盖率收集的工作了。

当然,光看这个,你肯定还是不大能知道,就算选择 Istanbul 之后,怎么去用?里面有哪些东西?我该怎么插桩?服务端渲染和客户端渲染这样不同处理的情况下,如何插桩?等等。

显然,已经给你准备好了:

image.png

这里解释一下,nyc 是 Istanbul 的进阶版,解决了很多问题,并且当下一直有很多优秀的人在维护这个开源。当然,这篇文章不讲这个。

看完这两张图,相信大家已经有点知道,做前端覆盖率,为什么选择 Istanbul ,以及 如何选择性的对前端项目进行插桩。

为了方便大家理解,我还是需要拷贝一下这位老师的东西。拷贝的相关原文,在上面列举的文章中。(原谅我如此不要脸...嘤嘤嘤)

运行前插桩

nyc instrument

针对编译之后的JS文件 , 进行手动插桩 , 形成插桩后的新JS文件

babel-plugin-istanbul

istanbul提供的babel插件 , 能够在代码编译打包阶段直接植入插桩代码 适用于使用babel的前端工程,基于react和vue的工程都可以

运行时插桩

**im.hookLoader **

适用于服务端的文件挂载 比如node应用 当应用启动时 , 会在require入口处添加hook方法 , 使得应用启动时加载到的都是插桩后的代码

im.createClientHandler

适用于客户端的JS挂载 ,比如react和vue的js 通过指定root路径,会把所有该路径的js文件请求拦截,返回插桩后的代码,即浏览器请求静态资源的动作 效果与babel-plugin-istanbul类似,区别在于该方法是在浏览器请求js时才会返回插桩代码,是一个动态过程

babel-plugin-istanbul

最上面,我讲到如果项目是用 vue 或者 React 的,可以直接使用插件 babel-plugin-istanbul,我简单说下这个东西是干嘛的,他其实就是一个做好了的npm包,这个包做的事情,就是给你所在的项目进行插桩。那它是在什么时候插桩呢?

⭐就是在你 run 项目的时候

npm install babel-plugin-istanbul 之后,你再去run你的前端项目的时候,会发现,编译的时间会比原来run的时间 稍微的拉长了那么一点。那这一点时间,其实就是它在工作,再给你的项目处理,对你项目里面所设计的文件,进行插桩编译处理。

这里肯定会有疑问,会不会给项目带来什么影响?这个会影响前端项目吗,emmm,并不会。

它只会在你的项目里生成相对应的覆盖率文件(在后面调用的过程中有一个映射关系,后面会说到)。

看下具体的 npm 依赖吧:

image.png

要注意的是,尽量在dev环境下进行安装此依赖。

文件配置

如果已经 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
        }]
      ]
    }
  }
}
复制代码

.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
        }]
      ]
    }
  }
}
复制代码

这是针对两种不同的文件的不同配置,为了方便大家我就都贴出来了。具体这些配置有什么用?干吗用?

在源码的 source-maps 这一块有讲,看大家需要做自己的处理,但是按照上面的方法去配置,是完全OK的。

然后,这里面细心的同学已经注意到了一个点,就是:'@babel/plugin-transform-modules-commonjs' ,为什么要用这个呢?其他都是些正常的配置。

这里呢,其实在我最上面贴出来的一篇文章中的评论区 reply-169807 有详细的解说,我在这里简单说下就是:使用 babel-plugin-Istanbul 插桩,和 babel-plugin-import 插件不兼容,导致编译失败

npm run

如果以上工作你都做好了,那就试试 把你的服务run起来吧。

ok,假设你已经run好了,这个时候,打开你的项目,再打开控制台。执行 window.coverage****。

如果你看到的同下面一致,那么恭喜你,你已经成功了第一步

image.png

并且这个时候,你就能欣喜的看到 babel-plugin-istanbul 做的事情了,给你项目只要是涉及到的文件,都做了插桩处理,回传给你这些覆盖率信息。

要是想看具体点,就随便点一个详情,他会清楚的告诉你,所覆盖的行信息,语句,方法等。

image.png

底层简介

说了这么多,都忘记了覆盖率所应当讲的基本了。大意了,没有闪。

覆盖率维度

  • Statements: 语句覆盖率,所有语句的执行率;
  • Branches: 分支覆盖率,所有代码分支如 if、三目运算的执行率;
  • Functions: 函数覆盖率,所有函数的被调用率;
  • Lines: 行覆盖率,所有有效代码行的执行率,和语句类似,但是计算方式略有差别

插装详解

image.png

插装原理

image.png

其实看到这个原理,我觉得大家就能理解,上面我说过 babel-plugin-istanbul 会生成对应的插桩后文件了。其实这些文件,存放在你的项目中,并不会影响你的项目,最多是占用了项目容量。

这个图也清晰的给出了,读取覆盖率数据的原理,就是会根据你当前访问的页面,拿到一对一的映射关系,找到插桩后的文件。我看到这个原理的时候,就大写的一个字,服!

插装前后文件对比

插装前:

image.png

插装后:

image.png

应该能很明显看到一些计数器。这些就是数据统计用的计数了。

源码解析-istanbul-middleware

大家还记得上面说到了 window.coverage**** 已经给到了你 覆盖率统计后的相关数据了,但是还没讲怎么收集对不对,接下来就讲收集。

对于 window.coverage**** 所收集到的这些信息,进行收集处理就要用到另一个伟大的开源,就是最上面给到的:istanbul-middleware ,我这边也给这简称:im。这个主要是干嘛的呢?

具体详情就点这个链接去看了,不赘述咯。主要说下,这个中间件,提供了以下的相关功能。

image.png

这是im项目里给到的四个接口,看到这,大家应该就恍然大悟了。也就是说,这个中间件 im 就是提供了四个接口,来对 window.coverage**** 收集到的数据进行处理啊。

那我们一个个来看,这四个接口分别是什么?

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

其实还有一个show接口 展示用的。

ok,知道了这些,就相对完美了,我们就要用这个 client,来提交收集到的数据。关于这四个接口具体怎么用,其实也很简单,就是在本地起一个node服务,这几个接口都写在这个服务下面,直接根据ip来调用就可以了。

具体的可以看这个开源项目中,有一个演示就是在test目录下有个app。这个就是作者给到的一个演示的demo。聪明的你,一看就会。

源码解析-test/app/demo

为了方便大家能迅速上手,我这里给大家整一下这个demo。当然,这里我已经在源码上做了改动,不过影响也不大。

首先进入app这个路径之后,最外层的 index.js 就是这个node服务的入口文件,在readme 中能看到就是说启动的script。node index.js --coverage # start the app with coverage 。他其实给到了四个脚本语句,但是我们只用这一个。

执行完这个之后,服务就启动起来了。这个时候,输入 https://localhost:8988/xxx 。就能看到你的覆盖率收集的数据了。

好,我们再说一处,就是在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);
复制代码

这个时候,我们看到server中的index.js 文件的时候,能看到一些东西,端口号啊,以及我们当初启动服务的 --coverage 传参啊,以及express 相关启服务操作等等。

所以,看到这,你就可以根据你自己的喜好,改一些东西。

比方我不喜欢每次启node服务的时候,都是 node index.js --coverage 我想简单点,就直接 node index.js 那其实就能在外面的index文件里面改,直接把coverageRequired 这个参数设置成true就好了。

再比如,新开始的情况下,服务启动起来,如果需要被其他端调,就涉及到跨域,那么就要做跨域的处理。再比如,后面要讲到的覆盖率上报插件,只能识别 https,你本地起的服务,访问都是用 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');

        //设置跨域访问
        app.all('*', function (req, res, next) {
            // console.log('req: ', req)
            res.header("Access-Control-Allow-Credentials", "true");  //服务端允许携带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);
        });
    }
};
复制代码

源码解析-提交 client

好了,看完服务了,我们来看这个client,既然是提交接口,并且是本地启的node服务,并且,严格声明了请求格式:"Content-Type", "application/json"

那么简单了,我们来直接把服务启动起来,走一个试试呗。

image.png

很完美,已经提交成功了。那既然提交了,就再看看提交的结果呗。

image.png

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

当然了,这边看到的url效果是我处理后的,我等下会一个个讲哦,大家先别急。

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

定位到源码。是一个叫 handlers.js 的文件,里面很明显能看到我们刚刚看到的四个接口,并且这四个接口,都在一个叫 creatHandler 的对象下面。其实看到这,我才能理解当初看其他老师说,根据自己的业务去改 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: '上报成功' });
    });
复制代码

可以看到client,底层是调用的 core.mergeClientCoverage() 这个方法,并且将我们通过client 传进来的 覆盖率信息 作为参数传进去。

那么好了,我们直接去看 mergeClientCoverage() 这个就可以了,这个就在 handlers.js 的同一层级下的 core.js 中。我们把源码贴出来

function mergeClientCoverage(obj) { // resolve coverage datas  这边传进来的就是 coverage的 window.coverage 信息 
        var coverage = getCoverageObject();  // 初始状态下的coverage 覆盖信息
        Object.keys(obj).forEach(function (filePath) {
        var original = coverage[filePath], // filepath = key 即文件路径 || original 是已经存在的数据
            added = obj[filePath],  // added 最新的覆盖率信息数据
            result;  // 定义一个预备内存
        if (original) {
            result = utils.mergeFileCoverage(original, added);
        } else {
            result = added;
        }
        coverage[filePath] = result;  // 最终的覆盖率信息 存放到临时内存!getCoverageObject()
    }
});
    // coverage 是最终的覆盖率信息,每次进行存储,用key值来关联,key指向业务版本
}
复制代码

这段源码中,其实可以看到他是怎么处理的,首先是调用 getCoverageObject() 去拉取最初始的覆盖率信息,给一个初始定义 coverage。

Object.keys(obj).forEach(function (filePath) {} 这个就是遍历处理传进来的新的覆盖率对象了。

original 定义的是,覆盖率服务原本就已经存在的数据。

added 则是去处理新进来的覆盖率数据。

最后定义一个临时内存存储 result 。

在后面的便利过程中,会有一个merge的过程,就是去判断 original 是否存在,如果存在就是存在原始数据,就需要将新数据和原始数据 进行merge合并,即:result = utils.mergeFileCoverage(original, added);,如果没有,那就直接新增就好了 result = added;

最后全部的覆盖率数据都给到了result 这个对象,最后,直接将最后的结果,再放到先开始定义的 coverage 中,就完成了整个覆盖率合并的操作。

其实大家可以在往深了看,就是这个 utils.mergeFileCoverage(original, added) 做了什么事。这边我就不一一讲述了。

那么为什么要把这些拿出来看呢,其实这块需要做的处理有几个,一个就是,我在文章最初给到的那些老师的文章中有说到,就是根据自己的业务逻辑去设计自己的覆盖率数据采集。

这里我又要不要脸了,我要去copy了。

image.png

也就是说,如果你有好几个业务,你不能一下子不分版本,分支,项目名的都传进来进行处理。你需要跟你的项目的分支,版本,业务名等等进行自己的处理,再传进来进行收集。

另一个就是,这里启的node服务,都是占用的临时内存。需要我们去将这些收集到的覆盖率数据采集完成之后,再去落库。存储起来,不然服务挂了,什么都没了。这是多可怕的一件事。

还有一个最终的要就是,你需要在这里去处理不同机器上的文件统计。

这里是什么意思呢,大家可能很好奇。其实这个就是我踩过的坑。就是比方说,一个项目发布到了一个机器上去,这里简称这台服务为A,而你的覆盖率统计的这个node服务,部署再你的另一台机器上,我们简称B。

那么这个时候,你拿到A的覆盖率数据信息,就不能被B所解析,这里的解析的意思就是,回传过来的哪些覆盖率数据,B服务器上,没有文件能对的上,因为B服务上没有发布过你的那个项目啊。你A服务上的覆盖率信息,可以通过,window.coverage 上报回来。但是我本地没有文件能跟你匹配。

所以就会报错,如果你的B服务上的node服务,后期自己不去写的健壮一些,可能就会因为找不到,服务挂了。

所以,你需要在这里,去将A服务上的项目,同步一份到B服务上,并且,你需要去更改收集到的覆盖率信息中的 key 关键字,以及 每个子对象下的path 路径为B服务的key 以及 路径。

emmm 不知道大家能不能看懂,要是没明白,再联系我细讲吧。

源码解析-提交 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的参数值

    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);
});
复制代码

其实这边也可以清楚的看到,这个show,其实也是调用的底层的 core.render(file, res, origUrl); 只不过在调用之前,给他传了 show 后面跟的参数,返回,origUrl。

话不多说,直接去看 render()。

function render(filePath, res, prefix) {
    var collector = new istanbul.Collector(),
        treeSummary,
        pathMap,
        linkMapper,
        outputNode,
        report,
        coverage,
        fileCoverage;
    // coverage = getCoverageObject();  // 查询已经处理后的覆盖率信息
    //  这边的覆盖率信息可以从库中读取,指定访问地址,查询校验  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);  // 处理每个不同路下的覆盖信息 将全部的分类,收集存入数组
            filePath = filePath || treeSummary.root.fullPath();  // 如果没有指定路径查找相关的覆盖信息,就展示全量的数据
            outputNode = pathMap[filePath]; // 如果有具体的搜索参数,则在数组中找对应的对象 返回node节点信息
            if (!outputNode) {  // 查询路径下不存在相关信息 处理
                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 });
    }
}

var collector = new istanbul.Collector() 首先看这个,这个具体是干嘛用的呢,我理解下来,他其实就是一个覆盖率收集容器,主要做的工作,就是根据你show 后面带进来的参数,去解析相关的数据给你展示。

coverage = getCoverageObject() 这个眼熟不,他还是去拿覆盖率数据用的,但是这里的coverage,其实要根据你自己的业务,进行修改,这个 getCoverageObject() 永远都是初始的数据,或者就是你服务目前收集到的数据。绝对不能一直这样用。

后面你需要跟你你自己落库的覆盖率数据,进行修改。将coverage = xxxx 替换成你自己的东西。

emmm。不知道你们能不能看懂,看不懂没关系,后面问我。

treeSummary = getTreeSummary(collector); 这个其实是处理覆盖率的树信息,能看到我们收集道德数据,都是一个json的树结构,这个就是将那些数据做一个数据化处理。

pathMap = getPathMap(treeSummary); // 处理每个不同路下的覆盖信息 将全部的分类,收集存入数组

filePath = filePath || treeSummary.root.fullPath(); // 如果没有指定路径查找相关的覆盖信息,就展示全量的数据

linkMapper 这个具体是干嘛用的呢,细心看的化,就会发现,这个其实就是在展示覆盖率数据,所需要的资源文件,其中包含了 css 以及 js。自带属性。

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

这两个就是判断到访问的数据存在之后,进行文件整合,就是在上面 treeSummary pathMap filePath 处理完之后,进行全量数据吞吐。report 就是讲数据进行文本处理,最终展示html即可。

同样的来分析这快源码的意义是什么呢?

这块的处理相对而言就较为简单,上面client 说到,需要根据不同的业务,对不同的项目,不同的分支等等进行落库处理,那么这里其实就是,你在show 后面 的p参数值,拿到之前存储的数据进行展示的时候,进行一对一处理用。

其实这块还可以往下再看,就比方说,getTreeSummary() 这个方法做了哪些事,等等,如果想彻底搞清楚,可以再接着往下读。我这边就不详细叙述了。

插件上报

上面将istanbul 相关的源码都解读了,相信大家都能看懂,甚至能看的比我都深。那再来说下关于数据的上报吧。

数据上报这块,在其他老师的文章中可以看到,有两种方法:chrome插件 和 fiddler。其实还有一种方法 就是 sidebar,容器边车模式。这个也是我请教了以为大佬。给到的方案。但是我没有搞透彻。要是你知道,可以私我。我想请教一二。

那这边我只讲一下,插件上报。即 chrome插件。首先需要说一下,就是我们的覆盖率数据,是存在与当前页的,如果当前页的 window 对象下面是有coverage集合的,就可以通过 window.coverage 进行获取,再调用client 进行上报。

那么如果使用chrome 插件 就需要 chrome插件 能读到 你当前页的window对象。不然就获取不到下面的coverage集合。

如果有写过chrome 插件的,肯定是知道,content_script.js 是可以与当前页面进行dom交互,但是会有一个问题,就是拿不到当前页的window对象。

所以我这边做了一个处理:就是再覆盖率的node服务上,单独写一个文件,通过 content_script.js 写到 当前页的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)

为了方便,先写到local中,然后,再使用插件的当前页的js 执行一段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);

详细代码请见coverage-chrome。

插件的具体使用可以参考我之前写的一篇chrome插件

结尾

后面我将持续开坑关于前端覆盖率之自动化集成,引用 code-coverage,另一个优秀的开源。

好了,以上就是本文要讲的全部内容了,要是你有更高的见地,很欢迎与我交流,我们互相学习。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/-X0_7u1jmeyzy4RmwJY8gg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录