汇总:【第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排名软件,优化自己的网站。有什么问题可以在评论区留言,我们会尽快回复您。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线