汇总:【第2167期】埋点自动收集方案-路由依赖分析
优采云 发布时间: 2022-11-20 10:20汇总:【第2167期】埋点自动收集方案-路由依赖分析
前言
前段时间看到相关需求,但是已经在某个团队开发完成了。今天的前端早读课文章由转转@肖文豪分享,公众号:大转转FE授权分享。
@柳文豪,目前就职于转转平台运营部,负责C2C业务线前端工作。参与过两次创业,担任过项目经理,喜欢发现业务痛点,同时热爱技术研究,致力于通过技术手段提升产品和用户体验,推动业务增长。
正文从这里开始~~
这两个问题我们先想一想。
1、一个项目总共多少钱?每个页面有多少个组件?
2、哪些组件是公共组件,哪些页面引用了它们?
关注本文,一起探讨,希望能帮助您找到答案。
背景
由于组件化的思想深入人心,所以当我们在开发中遇到具体的功能模块或者UI模块时,就会想到将其抽取成组件。更高级的方法是将多个页面的相似部分提取到公共组件中。
组件化的“魔咒”
但往往对一件事的依赖越强,就越容易陷入它的“魔咒”。当项目中的组件较多时,开发人员更难建立它们之间的关系,尤其是当组件的一行代码发生变化时,甚至无法准确判断这行代码的变化影响了哪些页面。代码。我暂时称它为“组件化的诅咒”。如果我们有完整的组件依赖,就可以很好的解决这个问题。
下面以下面的场景为例,看看依赖分析的重要性和必要性。
在整个方案中,埋点的数据源非常重要,数据源与页面的对应关系是保证数据源完整性的关键。例如:首页的产品流和个人主页都使用同一个产品卡片,开发者自然会将产品卡片分离成一个通用的组件。如下:
<p>//Index.vue 首页
import Card from './common/Card.vue' //依赖商品卡片组件
<br />
//Home.vue 个人主页
import Card from './common/Card.vue' //依赖商品卡片组件
<br />
//Card.vue 商品卡片组件
goDetail(item) {
/**
* @mylog 商品卡片点击
*/
this.$log('card-click') // 埋点发送
}</p>
这就带来了一个问题:产品卡片(埋点的数据来源)的点击信息可能来自首页,也可能来自个人主页,而jsdoc在采集
埋点评论时,无力判断这种归属。所以我们必须想办法获取组件和页面的映射关系。
预期效果
项目中的实际依赖:
对应的依赖分析关系:(每个组件,与引用它的页面路由的映射)
解决思路
那么,如何做依赖分析呢?在思考这个问题之前,我们先看一下创建依赖的常用语法。
<p>//a.ts
import B from './b.ts'
import getCookie from '@/libs/cookie.ts'
<br />
//c.ts
const C = require('./b.ts')
<br />
//b.ts
div {
background: url('./assets/icon.png') no-repeat;
}
import './style.css'
// c.vue
import Vue from Vue
import Card from '@/component/Card.vue'</p>
下面是依赖分析的三个思路:
递归
从项目的路由配置开始,对每个路由页面进行依赖的递归解析。这个想法简单明了,但是实现起来可能比较麻烦,需要解析页面中各种形式的依赖关系。
借助工具的统计分析数据进行二次加工
在实际项目中,我们都会用到webpack打包工具,它的一大特点就是会自动帮助开发者进行依赖分析(独立的enhanced-resolve库)。相比第一种重写解析的方法,何乐而不为站在webpack的肩膀上来解决问题。
先看下webpack的整体编译流程:
" />
可以看到,每个文件都会经过resolve阶段,最后编译完成后,会得到本次编译的统计分析信息。
<p>//done是compiler的钩子,在完成一次编译结束后的会执行
compiler.hooks.done.tapAsync("demoPlugin",(stats,cb)=>{
fs.writeFile(appRoot+'/stats.json', JSON.stringify(stats.toJson(),'','\t'), (err) => {
if (err) {
throw err;
}
})
cb()
})</p>
详细的编译数据是done事件中的回调参数stats。处理后大致如下:
通过对这个统计分析信息的二次加工分析,也可以得到预期的依赖关系(插件webpack-bundle-analyzer也是根据这个数据生成的分析图表)。这个数据看起来更像是基本块和模块的依赖分析。对于组件或者公共组件的依赖问题,需要综合分析chunk和modules来解决。同时我们也发现这个数据的数据量还是挺大的,有很多开发者不关心的数据(截图是只有两个路由页面时的数据量)。接下来讨论的方案是笔者实际采用的方案,也是基于webpack的。区别在于分析和采集
依赖关系的时间。
在webpack的解析阶段,分析采集
依赖
我们看到,虽然 webpack 的分析数据非常臃肿,但它确实帮助开发者完成了这项繁重的工作。只是我们希望自定义数据的范围,主动采集
预期的数据,所以我们推测是否可以在每个文件解析阶段进行一定的“干预”,即通过条件判断或过滤。那么问题来了,究竟应该在哪个阶段“介入”,如何“介入”?
好了,我们先来大致了解一下webpack的事件流流程:
很显然,afterResolve是每个文件解析阶段的结束,所以我们应该从这里开始。
执行
先上流程图
初始化
首先,这是一个webpack插件。在初始化阶段,指定解析路由文件地址(如src/route),排除解析文件地址(如src/lib、src/util),因为这些被排除的文件不会存在。点数据。
采集
依赖项
在afterResolve钩子函数中,获取当前解析文件的路径及其父文件路径。
<p>apply(compiler) {
compiler.hooks.normalModuleFactory.tap(
"demoPlugin",
nmf => {
nmf.hooks.afterResolve.tapAsync(
"demoPlugin",
(result, callback) => {
const { resourceResolveData } = result;
// 当前文件的路径
let path = resourceResolveData.path;
// 父级文件路径
let fatherPath = resourceResolveData.context.issuer;
callback(null,result)
}
);
}
)
}</p>
构建依赖树
根据上一步得到的引用关系生成依赖树。
<p>// 不是nodemodule中的文件,不是exclude中的文件,且为.js/.jsx/.ts/.tsx/.vue
if(!skip(this.ignoreDependenciesArr,this.excludeRegArr,path, fatherPath) && matchFileType(path)){
if(fatherPath && fatherPath != path){ // 父子路径相同的排除
if(!(fatherPath.endsWith('js') || fatherPath.endsWith('ts')) || !(path.endsWith('js') || path.endsWith('ts'))){
// 父子同为js文件,认为是路由文件的父子关系,而非组件,故排除
let sonObj = {};
sonObj.type = 'module';
sonObj.path = path;
sonObj.deps = []
// 如果本次parser中的path,解析过,那么把过去的解析结果copy过来。
sonObj = copyAheadDep(this.dependenciesArray,sonObj);
let obj = checkExist(this.dependenciesArray,fatherPath,sonObj);
this.dependenciesArray = obj.arr;
if(!obj.fileExist){
let entryObj = {type:'module',path:fatherPath,deps:[sonObj]};
this.dependenciesArray.push(entryObj);
}
}
} else if(!this.dependenciesArray.some(it => it.path == path)) {
// 父子路径相同,且在this.dependenciesArray不存在,认为此文件为依赖树的根文件
let entryObj = {type:'entry',path:path,deps:[]};
this.dependenciesArray.push(entryObj);
}
}</p>
那么此时生成的依赖树如下:
解析路由信息
组件的依赖树基本上是通过上一步得到的,但是我们发现对于公共组件Card,它只存在于主页的依赖中,而没有存在于个人主页的依赖中,这显然不符合预期(在步骤 6 中解释)。那么下一步就是找到这个依赖树和路由信息之间的关系。
<p>compiler.hooks.done.tapAsync("RoutePathWebpackPlugin",(stats,cb)=>{
this.handleCompilerDone()
cb()
})</p>
<p>// ast解析路由文件
handleCompilerDone(){
if(this.dependenciesArray.length){
let tempRouteDeps = {};
// routePaths是项目的路由文件数组
for(let i = 0; i it && Object.prototype.toString.call(it) == "[object Object]" && it.components);
// 获取真实插件传入的router配置文件的依赖,除去main.js、filter.js、store.js等文件的依赖
this.dependenciesArray =
getRealRoutePathDependenciesArr(this.dependenciesArray,this.routePaths);
}
}</p>
通过这一步的AST解析,可以得到如下路由信息:
<p>[
{
"name": "index",
"route": "/index",
"title": "首页",
"components": ["../view/newCycle/index.vue"]
},
{
"name": "home",
"route": "/home",
"title": "个人主页",
"components": ["../view/newCycle/home.vue"]
}
]</p>
依赖树和路由信息综合分析
<p>// 将路由页面的所有依赖组件deps,都存放在路由信息的components数组中
const getEndPathComponentsArr = function(routeDeps,dependenciesArray) {
for(let i = 0; i {
routeDeps = routeDeps.map(routeObj=>{
if(routeObj && routeObj.components){
let relativePath =
routeObj.components[0].slice(routeObj.components[0].indexOf('/')+1);
if(page.path.includes(relativePath.split('/').join(path.sep))){
// 铺平依赖树的层级
routeObj = flapAllComponents(routeObj,page);
// 去重操作
routeObj.components = dedupe(routeObj.components);
}
}
return routeObj;
})
})
}
return routeDeps;
}
//建立一个map数据结构,以每个组件为key,以对应的路由信息为value
// {
// 'path1' => Set { '/index' },
// 'path2' => Set { '/index', '/home' },
// 'path3' => Set { '/home' }
// }
const convertDeps = function(deps) {
let map = new Map();
......
return map;
}</p>
集成分析后的依赖关系如下:
" />
<p>{
A: ["index&_&首页&_&index"],// A代表组件A的路径
B: ["index&_&首页&_&index"],// B代表组件B的路径
Card: ["index&_&首页&_&index"],
// 映射中只有和首页的映射
D: ["index&_&首页&_&index"],// D代表组件D的路径
E: ["home&_&个人主页&_&home"],// E代表组件E的路径
}</p>
由于上一步的依赖采集
部分,Card组件没有成功采集
到个人主页的依赖中,所以这一步集成分析无法建立准确的映射关系。并查看下面的解决方案。
修改 unsafeCache 配置
为什么采集
依赖时公共组件Card只采集
一次?如果不解决这个问题,意味着只采集
首页的产品点击,引用该组件的其他页面的产品点击会丢失。哪里有问题,哪里就有机会,机会意味着解决问题的可能性。
webpack4 提供了 resolve 的配置入口。开发者可以通过几个设置来决定如何解析文件,比如扩展名、别名等。其中一个属性——unsafeCache 成功地引起了作者的注意,这就是问题的根源所在。
unsafeCache 是 webpack 提高编译性能的优化措施。
unsafeCache 默认为 true,表示 webpack 会缓存已经解析的文件依赖。当文件需要再次解析时,直接从缓存中返回结果,避免重复解析。
我们看一下源码:
<p>//webpack/lib/WebpackOptionsDefaulter.js
this.set("resolveLoader.unsafeCache", true);
//这是webpack初始化配置参数时对unsafeCache的默认设置
<br />
//enhanced-resolve/lib/Resolverfatory.js
if (unsafeCache) {
plugins.push(
new UnsafeCachePlugin(
"resolve",
cachePredicate,
unsafeCache,
cacheWithContext,
"new-resolve"
)
);
plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));
} else {
plugins.push(new ParsePlugin("resolve", "parsed-resolve"));
}
//前面已经提到,webpack将文件的解析独立为一个单独的库去做,那就是enhanced-resolve。
//缓存的工作是由UnsafeCachePlugin完成,代码如下:
//enhanced-resolve/lib/UnsafeCachePlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {
if (!this.filterPredicate(request)) return callback();
const cacheId = getCacheId(request, this.withContext);
// !!划重点,当缓存中存在解析过的文件结果,直接callback
const cacheEntry = this.cache[cacheId];
if (cacheEntry) {
return callback(null, cacheEntry);
}
resolver.doResolve(
target,
request,
null,
resolveContext,
(err, result) => {
if (err) return callback(err);
if (result) return callback(null, (this.cache[cacheId] = result));
callback();
}
);
});
}</p>
在UnsafeCachePlugin的apply方法中,判断有缓存文件结果时,直接回调,不继续后续的解析动作。
这对我们的集合依赖有何影响?
缓存解析后的文件,意味着再次遇到这个文件时,事件流会提前终止,afterResolve hook自然不会执行,这样我们的依赖关系就无从谈起了。
其实webpack的resolve过程可以看作是一系列的事件。当所有连接在一起的事件都被执行时,解析就结束了。我们来看一下原理:
用于解析文件的库是 enhanced-resolve。Resolverfatory在生成resolver解析对象时,会注册大量的插件,正是这些插件构成了一系列的解析事件。
<p>//enhanced-resolve/lib/Resolverfatory.js
exports.createResolver = function(options) {
......
let unsafeCache = options.unsafeCache || false;
if (unsafeCache) {
plugins.push(
new UnsafeCachePlugin(
"resolve",
cachePredicate,
unsafeCache,
cacheWithContext,
"new-resolve"
)
);
plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));
// 这里的事件流大致是:UnsafeCachePlugin的事件源(source)是resolve,
//执行结束后的目标事件(target)是new-resolve。
//而ParsePlugin的事件源为new-resolve,所以事件流机制刚好把这两个插件串联起来。
} else {
plugins.push(new ParsePlugin("resolve", "parsed-resolve"));
}
...... // 各种plugin
plugins.push(new ResultPlugin(resolver.hooks.resolved));
<br />
plugins.forEach(plugin => {
plugin.apply(resolver);
});
<br />
return resolver;
}</p>
每个插件执行完自己的逻辑后,会调用resolver.doResolve(target, ...),其中target是触发下一个插件的事件名,以此类推,直到事件源为result ,递归终止,解析完成。
resolve事件系列流程图大致如下:
UnsafeCachePlugin插件第一次解析文件时,由于没有缓存,会触发目标为new-resolve的事件,即ParsePlugin,并将解析结果记录到缓存中。当判断文件有缓存结果时,UnsafeCachePlugin的apply方法会直接回调,不再继续执行resolver.doResolve(),也就是说整个resolve事件流程在UnsafeCachePlugin中终止。这就解释了为什么只建立了主页和Card组件的映射,而无法获取个人主页和Card组件的映射。
解决方案
分析了原因,就好办了,设置unsafeCache为false(嗯,就这么简单)。这时候你可能会担心它会降低项目编译的速度,但是如果你进一步思考,依赖分析完全可以独立于开发阶段之外,只要我们在需要的时候执行这个能力,这样由开发人员通过命令行参数控制。
<p>//package.json
"analyse": "cross-env LEGO_ENV=analyse vue-cli-service build"
<br />
//vue.config.js
chainWebpack(config) {
// 这一步解决webpack对组件缓存,影响最终映射关系的处理
config.resolve.unsafeCache = process.env.LEGO_ENV != 'analyse'
}</p>
最终依赖
<p>{
A: ["index&_&首页&_&index"],// A代表组件A的路径
B: ["index&_&首页&_&index"],// B代表组件B的路径
Card: ["index&_&首页&_&index",
"home&_&个人主页&_&home"],
// Card组件与多个页面有映射关系
D: ["index&_&首页&_&index"],// D代表组件D的路径
E: ["home&_&个人主页&_&home"],// E代表组件E的路径
}</p>
可以看出,个人主页的路由信息被添加到与公共组件Card关联的映射页面中,这是准确的依赖数据。在埋点自动采集项目中,将这些依赖数据交给jsdoc来完成所有埋点信息与页面的映射关系。
还有一件事
webpack 5,它来了,它带有持久缓存策略。前面提到的unsafeCache虽然可以提升应用的构建性能,但是牺牲了一定的解析精度。同时意味着持续构建过程需要反复重启解析策略,这就需要采集
文件搜索策略(resolutions)的变化,来识别判断文件解析是否发生了变化,这一系列的过程也有一个成本,这就是为什么它被称为 unsafeCache 而不是 safeCache(安全)。
webpack5规定配置信息中缓存对象的类型可以设置为memory和fileSystem。memory指的是之前的unsafeCache缓存,fileSystem指的是相对安全的磁盘持久缓存。
<p>module.exports = {
cache: {
// 1. Set cache type to filesystem
type: 'filesystem',
<br />
buildDependencies: {
// 2. Add your config as buildDependency to get cache invalidation on config change
config: [__filename]
<br />
// 3. If you have other things the build depends on you can add them here
// Note that webpack, loaders and all modules referenced from your config are automatically added
}
}
};</p>
所以对于webpack5,如果需要做完整的依赖分析,只需要动态设置cache.type为内存,resolve.unsafeCache为false即可。(有兴趣的童鞋可以试试)
总结
上面我们讲解了组件化可能带来的隐患,提到了路由依赖分析的重要性,给出了依赖分析的三种思路,重点介绍了其中一种方案基于埋点自动采集项目的具体实现。在此与大家分享,期待共同成长~
解决方案:百度seo排名软件|做seo优化会用到哪些软件?
百度seo排名软件是帮助seo优化人员优化网站排名的重要工具之一。您使用什么软件进行 SEO 优化?让我给你详细介绍一下。
1:搜索引擎优化的基本原则
搜索引擎优化是增加网站或网页在搜索引擎“自然”或无偿(“有机”)搜索结果中的可见度的过程。一般来说,一个网站出现在搜索结果列表中的时间越早、出现频率越高(或页面排名靠前),它就会从搜索引擎用户那里获得越多的访问者,这些访问者可以转化为客户。
" />
SEO 可能针对不同类型的搜索,包括图像搜索、本地搜索、视频搜索、学术搜索、[1] 新闻搜索和行业特定的垂直搜索引擎。作为一种互联网营销策略,SEO 会考虑搜索引擎的工作方式、人们搜索的内容、输入搜索引擎的实际搜索词或关键字,以及目标受众喜欢的搜索引擎。优化网站可能需要编辑其内容、HTML 和相关编码,以增加其与特定关键字的相关性并消除搜索引擎索引活动的障碍。提升您的网站以增加反向链接或入站链接的数量是另一种 SEO 策略。
2:SEO中的常用工具
百度seo排名软件:百度seo排名软件是一款针对百度搜索引擎的优化软件,可以帮助用户快速提高网站在百度搜索中的排名。SEOmoz Pro:SEOmoz Pro 是一款专业的 SEO 工具,提供 关键词 工具、竞争分析工具和网站监控工具等功能。Web CEO:Web CEO是一款简单易用的SEO软件,提供关键词工具、网站分析工具、网站监控工具等功能。SEMrush:SEMrush 是一款专业的 SEO 工具,提供 关键词 工具、竞争分析工具和网站监控工具等功能。
" />
3:如何使用这些工具来实现您的 SEO 目标
实现 SEO 目标的最终方法是使用正确的工具。那么,有哪些工具可以帮助我们实现我们的SEO目标呢?1、首先要了解搜索引擎的排名机制。其次,根据搜索引擎的排名机制,我们可以选择合适的关键词。2. 然后我们将使用关键词 选择工具来确定我们的关键词。3. 最后,我们需要使用关键词优化工具来实现我们的SEO目标。
希望大家可以根据以上介绍选择适合自己的百度seo排名软件,优化自己的网站。有什么问题可以在评论区留言,我们会尽快回复您。