总结:埋点自动收集方案-路由依赖分析

优采云 发布时间: 2022-10-25 06:18

  总结:埋点自动收集方案-路由依赖分析

  1.一个项目总共有多少个组件?每个页面收录多少个组件?

  2. 哪些组件是公共组件,它们被哪些页面引用?

  让我们考虑一下这两个问题。睡觉...

  跟着这个文章一起来讨论,希望能帮助你找到答案。

  由于组件化的思想深入人心,当我们在开发中遇到具体的功能模块或UI模块时,就会想到提取组件。更高级的方法是将多个页面的相似部分提取到公共组件中。

  组件化的“诅咒”

  但往往对一件事的依赖越强,就越容易落入它的“魔咒”。当项目的组件比较多的时候,开发者更难建立它们之间的关系,尤其是当一个组件的一行代码发生变化时,甚至无法准确判断哪些页面会受到这一行变化的影响的代码。我暂时称它为“组件化的诅咒”。如果我们有完整的组件依赖,就可以很好的解决这个问题。

  让我们以下面的场景为例,来看看依赖分析的重要性和必要性。

  通过前面的文章文章,你一定对埋点自动采集方案有一个宏观全面的了解。以下是简要概述:

  嵌入点的自动采集是基于 jsdoc 采集注解信息的能力。通过对路由页面中的所有嵌入点添加注解,在编译时建立页面与嵌入点信息的对应关系。

  点击查看

  在整个方案中,埋点的数据源非常重要,数据源与页面的对应关系是保证数据源完整性的关键。比如首页和个人首页的产品流使用同一张产品卡片,开发者自然会将产品卡片分离成一个通用组件。如下:

  //Index.vue 首页<br />import Card from './common/Card.vue' //依赖商品卡片组件<br /><br />//Home.vue 个人主页<br />import Card from './common/Card.vue' //依赖商品卡片组件<br /><br />//Card.vue 商品卡片组件<br />goDetail(item) {<br />    /**<br />    * @mylog 商品卡片点击<br />    */<br />    this.$log('card-click') // 埋点发送<br />}<br />

  这就带来了一个问题:产品卡片的点击信息(埋点的数据来源)可能在首页,也可能在个人主页,而jsdoc在采集埋点注解时,无法判断归属。所以我们必须想办法得到组件和页面之间的映射关系。

  项目中的实际依赖:

  对应的依赖分析关系:(每个组件,与引用它的页面路由的映射)

  那么,如何进行依赖分析呢?在考虑这个之前,让我们看一下构建依赖项的一些常用语法。

  //a.ts<br />import B from './b.ts'<br />import getCookie from '@/libs/cookie.ts'<br /><br />//c.ts<br />const C = require('./b.ts')<br /><br />//b.ts<br />div {<br />    background: url('./assets/icon.png') no-repeat;<br />}<br />import './style.css'<br />// c.vue<br />import Vue from Vue<br />import Card from '@/component/Card.vue'<br />

  以下是依赖分析的三个想法:

  1 递归分析

  从项目的路由配置文件开始,递归解析每个路由页面的依赖关系。这个思路简单直接,但是实现起来可能比较麻烦,页面中各种形式的依赖都需要解决。

  2 使用webpack工具的统计分析数据进行二次处理

  在实际项目中,我们都使用 webpack 打包工具,它的一大特色就是会自动帮助开发者做依赖分析(独立的增强解析库)。相比第一种重写解析的方法,何不站在 webpack 的肩膀上解决问题。

  我们先来看看webpack的整体编译过程:

  可以看到,每个文件都会经过resolve阶段,最后在编译完成后,会得到本次编译的统计分析信息。

  //done是compiler的钩子,在完成一次编译结束后的会执行<br />compiler.hooks.done.tapAsync("demoPlugin",(stats,cb)=>{<br />  fs.writeFile(appRoot+'/stats.json', JSON.stringify(stats.toJson(),'','\t'), (err) => {<br />      if (err) {<br />          throw err;<br />      }<br />  })<br />  cb()<br />})<br />

  

  详细的编译数据是 done 事件中的回调参数 stats。处理后大致如下:

  通过对这个统计分析信息的二次处理和分析,也可以得到预期的依赖关系(插件webpack-bundle-analyzer也可以根据这个数据生成分析图表)。这些数据看起来更像是对基本块和模块的依赖分析。对于组件或通用组件的依赖关系,需要对chunk和modules进行综合分析来解决。同时我们也发现这个数据的数据量相当大,还有很多开发者不关心的数据(截图是只有两个路由页面时的数据量)。接下来讨论的方案是作者实际采用的方案,也是基于webpack,区别在于分析和采集依赖的时机。

  3 在webpack的解析阶段,分析采集依赖

  我们看到,虽然 webpack 的分析数据非常臃肿,但它确实帮助开发人员完成了繁重的工作。只是我们希望自定义数据的范围,主动采集想要的数据,所以推测是否可以在每个文件解析阶段进行一定的“干预”,即通过条件判断或者过滤来达到目的. 那么问题来了,在解决的哪个阶段我们应该“干预”,如何“干预”?

  好的,我们先来概述一下 webpack 事件流流程:

  显然,afterResolve 是每个文件解析阶段的结束,应该从这里开始。

  先提供流程图

  1 初始化

  首先,这是一个 webpack 插件。在初始化阶段,指定解析的路由文件的地址(如src/route)和排除文件的地址(如src/lib、src/util),因为这些排除的文件不会被掩埋。点数据。

  2 采集依赖

  在 afterResolve 钩子函数中,获取当前解析文件的路径及其父文件路径。

  apply(compiler) {<br />  compiler.hooks.normalModuleFactory.tap(<br />    "demoPlugin",<br />    nmf => {<br />      nmf.hooks.afterResolve.tapAsync(<br />        "demoPlugin",<br />        (result, callback) => {<br />          const { resourceResolveData } = result;<br />          // 当前文件的路径<br />          let path = resourceResolveData.path; <br />          // 父级文件路径<br />          let fatherPath = resourceResolveData.context.issuer; <br />          callback(null,result)<br />        }<br />      );<br />    }<br />  )<br />}<br />

  3 构建依赖树

  根据上一步得到的引用关系生成依赖树。

  // 不是nodemodule中的文件,不是exclude中的文件,且为.js/.jsx/.ts/.tsx/.vue<br />if(!skip(this.ignoreDependenciesArr,this.excludeRegArr,path, fatherPath) && matchFileType(path)){ <br />  if(fatherPath && fatherPath != path){ // 父子路径相同的排除<br />    if(!(fatherPath.endsWith('js') || fatherPath.endsWith('ts')) || !(path.endsWith('js') || path.endsWith('ts'))){ <br />      // 父子同为js文件,认为是路由文件的父子关系,而非组件,故排除<br />      let sonObj = {};<br />      sonObj.type = 'module';<br />      sonObj.path = path;<br />      sonObj.deps = []<br />      // 如果本次parser中的path,解析过,那么把过去的解析结果copy过来。<br />      sonObj = copyAheadDep(this.dependenciesArray,sonObj);<br />      let obj = checkExist(this.dependenciesArray,fatherPath,sonObj);<br />      this.dependenciesArray = obj.arr;<br />      if(!obj.fileExist){<br />        let entryObj = {type:'module',path:fatherPath,deps:[sonObj]};<br />        this.dependenciesArray.push(entryObj);<br />      }<br />    }<br />} else if(!this.dependenciesArray.some(it => it.path == path)) {<br />// 父子路径相同,且在this.dependenciesArray不存在,认为此文件为依赖树的根文件<br />    let entryObj = {type:'entry',path:path,deps:[]};<br />    this.dependenciesArray.push(entryObj);<br />  }<br />}<br />

  那么生成的依赖树如下:

  4 解析路由信息

  通过上一步,我们基本上得到了组件的依赖树,但是我们发现对于公共组件Card,它只存在于首页的依赖中,而没有存在于个人首页的依赖中,这显然是不符合预期(在步骤 6 中解释)。那么接下来就是找到这个依赖树和路由信息的关系了。

  compiler.hooks.done.tapAsync("RoutePathWebpackPlugin",(stats,cb)=>{<br />  this.handleCompilerDone()<br />  cb()<br />})<br />

  // ast解析路由文件<br />handleCompilerDone(){<br />  if(this.dependenciesArray.length){<br />    let tempRouteDeps = {};<br />    // routePaths是项目的路由文件数组<br />    for(let i = 0; i it && Object.prototype.toString.call(it) == "[object Object]" && it.components);<br />    // 获取真实插件传入的router配置文件的依赖,除去main.js、filter.js、store.js等文件的依赖<br />    this.dependenciesArray = <br />    getRealRoutePathDependenciesArr(this.dependenciesArray,this.routePaths);<br />  }<br />}<br />

  通过ast解析这一步,可以得到如下路由信息:

  [<br />  {<br />    "name": "index",<br />    "route": "/index",<br />    "title": "首页",<br />    "components": ["../view/newCycle/index.vue"]<br />  },<br />  {<br />    "name": "home",<br />    "route": "/home",<br />    "title": "个人主页",<br />    "components": ["../view/newCycle/home.vue"]<br />  }<br />]<br />

  5 整合分析依赖树和路由信息

  // 将路由页面的所有依赖组件deps,都存放在路由信息的components数组中<br />const getEndPathComponentsArr = function(routeDeps,dependenciesArray) {<br />  for(let i = 0; i {<br />      routeDeps = routeDeps.map(routeObj=>{<br />        if(routeObj && routeObj.components){<br />          let relativePath = <br />          routeObj.components[0].slice(routeObj.components[0].indexOf('/')+1);<br />          if(page.path.includes(relativePath.split('/').join(path.sep))){<br />            // 铺平依赖树的层级<br />            routeObj = flapAllComponents(routeObj,page);<br />            // 去重操作<br />            routeObj.components = dedupe(routeObj.components);<br />          }<br />        }<br />        return routeObj;<br />      })<br />    })<br />  }<br />  return routeDeps;<br />}<br />//建立一个map数据结构,以每个组件为key,以对应的路由信息为value<br />//  {<br />//    'path1' => Set { '/index' },<br />//    'path2' => Set { '/index', '/home' },<br />//    'path3' => Set { '/home' }<br />//  }<br />const convertDeps = function(deps) {<br />    let map = new Map();<br />    ......<br />    return map;<br />}<br />

  综合分析后的依赖关系如下:

  {<br />    A: ["index&_&首页&_&index"],// A代表组件A的路径<br />    B: ["index&_&首页&_&index"],// B代表组件B的路径<br />    Card: ["index&_&首页&_&index"],<br />    // 映射中只有和首页的映射<br />    D: ["index&_&首页&_&index"],// D代表组件D的路径<br />    E: ["home&_&个人主页&_&home"],// E代表组件E的路径<br />}<br />

  因为上一步的依赖采集部分,Card组件没有成功采集到个人主页的依赖,所以这一步综合分析无法建立准确的映射关系。请参阅下面的解决方案。

  

  6 修改unsafeCache配置

  为什么公共组件 Card 在采集依赖时只采集一次?如果这个问题不解决,就意味着只采集首页的产品点击,其他引用该组件的页面产品点击会丢失。哪里有问题,哪里就有机会,而机会就是解决问题的可能性。

  webpack4 为解析提供了一个配置入口。开发者可以通过几个设置来决定如何解析文件,比如扩展名、别名等。有一个属性——unsafeCache成功引起了笔者的注意,这就是问题的根源。

  6.1 unsafeCache 是 webpack 提高编译性能的优化措施。

  unsafeCache 的默认值为 true,表示 webpack 会缓存解析后的文件依赖。当需要再次解析文件时,会直接从缓存中返回结果,避免重复解析。

  让我们看一下源代码:

  //webpack/lib/WebpackOptionsDefaulter.js<br />this.set("resolveLoader.unsafeCache", true);<br />//这是webpack初始化配置参数时对unsafeCache的默认设置<br /><br />//enhanced-resolve/lib/Resolverfatory.js<br />if (unsafeCache) {<br /> plugins.push(<br />  new UnsafeCachePlugin(<br />   "resolve",<br />   cachePredicate,<br />   unsafeCache,<br />   cacheWithContext,<br />   "new-resolve"<br />  )<br /> );<br /> plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));<br />} else {<br /> plugins.push(new ParsePlugin("resolve", "parsed-resolve"));<br />}<br />//前面已经提到,webpack将文件的解析独立为一个单独的库去做,那就是enhanced-resolve。<br />//缓存的工作是由UnsafeCachePlugin完成,代码如下:<br />//enhanced-resolve/lib/UnsafeCachePlugin.js<br />apply(resolver) {<br /> const target = resolver.ensureHook(this.target);<br /> resolver<br />  .getHook(this.source)<br />  .tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {<br />   if (!this.filterPredicate(request)) return callback();<br />   const cacheId = getCacheId(request, this.withContext);<br />   // !!划重点,当缓存中存在解析过的文件结果,直接callback<br />   const cacheEntry = this.cache[cacheId];<br />   if (cacheEntry) {<br />    return callback(null, cacheEntry);<br />   }<br />   resolver.doResolve(<br />    target,<br />    request,<br />    null,<br />    resolveContext,<br />    (err, result) => {<br />     if (err) return callback(err);<br />     if (result) return callback(null, (this.cache[cacheId] = result));<br />     callback();<br />    }<br />   );<br />  });<br />}<br />

  UnsafeCachePlugin的apply方法中,当判断有缓存文件结果时,直接回调,不再继续后续的解析动作。

  6.2 这对我们的依赖集合有何影响?

  解析后的文件是缓存的,也就是说当再次遇到该文件时,事件流会提前终止,afterResolve钩子自然不会被执行,所以我们的依赖也就无从谈起了。

  其实webpack的resolve过程可以看成是事件的串联。当所有连接的事件都被执行时,解析就结束了。我们来看看原理:

  用于解析文件的库是增强解析。Resolverfactory在生成resolver解析对象时,注册了大量的插件,正是这些插件构成了一系列的解析事件。

  //enhanced-resolve/lib/Resolverfatory.js<br />exports.createResolver = function(options) {<br />    ......<br /> let unsafeCache = options.unsafeCache || false;<br /> if (unsafeCache) {<br />  plugins.push(<br />   new UnsafeCachePlugin(<br />    "resolve",<br />    cachePredicate,<br />    unsafeCache,<br />    cacheWithContext,<br />    "new-resolve"<br />   )<br />  );<br />  plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));<br />  // 这里的事件流大致是:UnsafeCachePlugin的事件源(source)是resolve,<br />  //执行结束后的目标事件(target)是new-resolve。<br />  //而ParsePlugin的事件源为new-resolve,所以事件流机制刚好把这两个插件串联起来。<br /> } else {<br />  plugins.push(new ParsePlugin("resolve", "parsed-resolve"));<br /> }<br /> ...... // 各种plugin<br /> plugins.push(new ResultPlugin(resolver.hooks.resolved));<br /><br /> plugins.forEach(plugin => {<br />  plugin.apply(resolver);<br /> });<br /><br /> return resolver;<br />}<br />

  每个插件执行完自己的逻辑后,会调用resolver.doResolve(target, ...),其中target为触发下一个插件的事件名称,以此类推,直到事件源为result,递归终止,并解决完成。

  resolve的事件链流程图大致如下:

  UnsafeCachePlugin插件第一次解析文件时,由于没有缓存,会触发target为new-resolve的事件,即ParsePlugin,并将解析结果记录到缓存中。当判断文件有缓存结果时,UnsafeCachePlugin的apply方法会直接回调而不继续执行resolver.doResolve(),也就是说整个resolve事件流在UnsafeCachePlugin中终止。这就解释了为什么只建立了首页与Card组件的映射,却无法获取到个人首页与Card组件的映射。

  6.3 解决方案

  分析了原因,很简单,把unsafeCache设置为false(嗯,就这么简单)。这时候你可能会担心项目编译速度会变慢,但是再仔细想想,依赖分析完全可以独立于开发阶段,只要我们在需要的时候执行这个能力,比如由开发者通过命令行参数来控制。

  //package.json<br />"analyse": "cross-env LEGO_ENV=analyse vue-cli-service build"<br /><br />//vue.config.js<br />chainWebpack(config) {<br />    // 这一步解决webpack对组件缓存,影响最终映射关系的处理<br />    config.resolve.unsafeCache = process.env.LEGO_ENV != 'analyse'<br />}<br />

  7 最终依赖

  {<br />    A: ["index&_&首页&_&index"],// A代表组件A的路径<br />    B: ["index&_&首页&_&index"],// B代表组件B的路径<br />    Card: ["index&_&首页&_&index",<br />    "home&_&个人主页&_&home"],<br />    // Card组件与多个页面有映射关系<br />    D: ["index&_&首页&_&index"],// D代表组件D的路径<br />    E: ["home&_&个人主页&_&home"],// E代表组件E的路径<br />}<br />

  可以看到,个人主页的路由信息​​被添加到与公共组件Card关联的映射页面中,就是准确的依赖数据。在埋点自动采集项目中,通过jsdoc处理这个依赖数据,就可以完成所有埋点信息与页面的映射关系。

  还有一件事

  webpack5,它来了,它带有持久缓存策略。前面提到的unsafeCache虽然可以提高应用构建性能,但是牺牲了一定的分辨率精度。同时,这意味着连续构建过程需要反复重启决策策略,这就需要采集文件搜索策略(resolutions)的变化。, 识别和判断文件分辨率是否发生变化,这一系列的过程也是有代价的,所以才叫unsafeCache,而不是safeCache(安全)。

  webpack5 在配置信息中指定缓存对象的类型,可以设置为内存和文件系统。memory 指的是之前的 unsafeCache 缓存,fileSystem 指的是相对安全的磁盘持久缓存。

  module.exports = {<br />  cache: {<br />    // 1. Set cache type to filesystem<br />    type: 'filesystem',<br /><br />    buildDependencies: {<br />      // 2. Add your config as buildDependency to get cache invalidation on config change<br />      config: [__filename]<br /><br />      // 3. If you have other things the build depends on you can add them here<br />      // Note that webpack, loaders and all modules referenced from your config are automatically added<br />    }<br />  }<br />};<br />

  所以对于webpack5来说,如果需要做完整的依赖分析,只需要动态设置cache.type为memory,resolve.unsafeCache为false即可。(有兴趣的童鞋可以试一试)

  上面我们讲解了组件化可能存在的隐患,提到了路由依赖分析的重要性,给出了依赖分析的三个思路,并重点介绍了其中一种基于埋点自动采集的解决方案的具体实现。在这里与大家分享,期待共同成长~

  完美:国内各大采集器优缺点对比

  国内各大采集器优缺点对比

  大数据时代已经到来。在数据驱动业务发展的时代,数据成为大家关注的焦点。近年来,国内出现了一些新兴数据采集器。本文将对国内几款采集器的优缺点进行对比分析,帮助大家根据需要选择合适的采集器。

  1. 优采云

  优采云采集器是一款互联网数据抓取、处理、分析、挖掘软件,可以抓取网页上分散的数据信息,通过一系列的分析处理,精准挖掘出需要的数据。

  优势:

  它使用分布式采集系统。这样提高了采集的效率,支持PHP和C#插件扩展,方便数据的修改和处理;还支持通过txt导入大量的url,也可以生成。对于不会编程的新手用户,可以直接使用别人制定的规则。专家可以定制开发并分享他们制定的规则。缺点:

  功能复杂,软件占用内存,

  CPU资源,大批量采集速度不好,只有WIN版,很*敏*感*词*需要企业版才能使用。无法访问API,不支持验证码识别,有一定限制

  网站 中有很多 采集。

  2. 优采云

  优采云 是出现在 优采云 之后的 采集器。可以从不同的网站获取归一化的数据,帮助客户实现数据的自动化采集、编辑、归一化,从而降低成本,提高效率。

  

  优势:

  国内首个真正意义上自定义可视化规则的采集器,简单易用,图形操作全可视化;内置可扩展的OCR接口,支持解析图片中的文字;采集任务是自动运行的,可以按照指定的周期自动运行采集。支持验证码识别,自定义不同浏览器logo,可有效防止IP。

  缺点:

  目前APP采集只支持微信和微博,其他APP不支持采集。没有文件托管和数据库管理。

  3.采集客户

  一款简单好用的网页信息抓取软件,可以抓取网页文字、图表、超链接等各种网页元素,并提供好用的网页抓取软件、数据挖掘策略、行业信息和前沿技术。

  优势:

  可以抓取手机网站上的数据;支持抓取指数图表上显示的数据;成员们互相帮助捕捉,提高采集的效率。

  缺点:

  验证码无法识别,需要每天清除浏览器cookie。更换ip很麻烦。需要重启路由器或登录路由器的web管理界面。

  4. 优采云

  一款云端在线智能爬虫/采集器,基于优采云分布式云爬虫框架,帮助用户快速获取大量规范化网页数据。

  

  优势:

  自动登录验证码识别,网站自动完成验证码输入,无需人工监督;可在线生成图标,采集结果以丰富的表格形式展示;

  缺点:

  DOM操作比较复杂,不能简单的实现滚动。通常,抓包分析用于解决加载问题。没有数据可视化操作,无法访问API。

  以上是国内常见的几种采集器的优缺点对比分析。您可以根据自己的需要选择适合自己的采集器。优采云业内开发时间最长,但不适合没有编程基础的新手用户。其他几个采集器,如优采云和优采云,可以提供智能的采集模式和官方规则,降低采集的难度。

  相关 采集 教程:

  天猫商品信息采集

  美团商业资讯采集

  58城市资讯采集

  优采云——70万用户选择的网页数据采集器。

  1.操作简单,任何人都可以使用:不需要技术背景,只要能上网采集即可。完成流程可视化,点击鼠标完成操作,2分钟快速上手。

  2、功能强大,任意网站可选:对于点击、登录、翻页、身份验证码、瀑布流、Ajax脚本异步加载数据,所有页面都可以通过简单设置采集。

  3.云采集,也可以关机。配置采集任务后,可以将其关闭,并可以在云端执行任务。庞大的云采集集群24*7不间断运行,无需担心IP阻塞和网络中断。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线