tech share
  • tech-share
  • Engineering
    • 登录鉴权
    • SSR 页面路由
    • npm 版本号
    • 缓存
    • 数据库容灾
    • 动态效果导出 gif
    • Chrome-devtools
    • C 端 H5 性能优化
    • Docker
    • Monorepo 最佳实践
    • 技术架构演化
    • 项目规范最佳实践
    • snowpack
    • 静态资源重试
    • 前端页面渲染分析
    • Git
    • 前端重构
    • 微前端
    • 项目依赖分析
    • 前端监控原理
    • webpack
    • BS 架构与 CS 架构
    • HTTPS
    • package-lock.json 生成逻辑
    • SVN(Subversion)
    • 数据库分类
    • gulp
    • 前端架构
    • Bundle & Bundless
    • 控制反转 IoC
  • JavaScript
    • Javascript 性能
    • JavaScript 原型(2) - 原型与原型链
    • JavaScript 原型(1) - 构造函数
    • JavaScript - Promise
    • ES6 解构赋值
    • 前端离线化
    • Proxy
    • Object.defineProperty()简介
    • TypeScript
  • MachineLearning
    • GAN生成对抗网络
    • 虚拟对抗训练
    • 深度度量学习
    • 原型网络
    • PyTorch优化器
    • 隐马尔可夫模型2
    • Shapley Value 算法
    • Embarassingly Autoencoder算法
    • AutoRec算法及其后续发展
    • 深度学习常用激活函数
    • 序列预测ConvTran算法
    • 联邦学习
    • 深度学习推荐系统算法整理
    • 隐马尔可夫模型
    • 黎曼优化方法
    • FM算法
    • 机器学习常见评价指标
    • VAE算法
    • Adam优化器详解
    • Transformer算法
    • Self-attention 推荐算法
    • CNN 卷积神经网络
    • 图嵌入
    • 集成学习算法
    • RecBole开源框架
    • NCE-PLRec
    • 深度学习初始化方法
    • RNN循环神经网络
    • PyTorch数据处理
    • PyTorch安装和基本操作
    • XGBoost算法
    • NCF算法与简单MF的对比
    • 计算最佳传输
  • CSS
    • 什么是BFC
    • 纯CSS实现可拖动布局
    • 滚动穿透解决方案
  • React
    • React 生命周期
    • React Ref
    • React Hooks
    • SWR
    • React 数据流
    • React 函数式组件和类组件的区别
  • 可视化
    • OffscreenCanvas
    • Echarts 平滑曲线端点为什么不平滑
    • 颜色空间
    • 词云布局解析
    • 3D 数学基础
    • Canvas 图片处理
    • GLGL ES
    • WebGL 中绘制直线
    • Graphics API
    • 现代计算机图形学基础
    • Canvas 灰度
  • Vue
    • Vue2.x全局挂载整理
    • Vue2.6.x源码阅读
      • Vue2.6.x源码阅读 - 2.目录结构分析
      • Vue2.6.x源码阅读 - 4.源码阅读-platform
      • Vue2.6.x源码阅读 - 1.准备工作
      • Vue2.6.x源码阅读 - 5.源码阅读-core-Vue构造函数
      • Vue2.6.x源码阅读 - 7.源码阅读-core-响应式原理
      • Vue2.6.x源码阅读 - 3.源码阅读-shared
      • Vue2.6.x源码阅读 - 6.源码阅读-core-组件挂载
    • Vue + TypeScript Web应用实践
    • Vue2.x指令
    • nextTick()的使用
    • vue-cli2.x 的使用与项目结构分析
    • Vue响应式原理及总结
    • VueX的使用
    • Electron-Vue + Python 桌面应用实践
    • Vite
    • Vue组件通信整理
    • 记录一个问题的探索过程
  • Linux
    • memcg
  • GameDev
    • 游戏中的几种投影视图
    • 从零开始写软渲染器06
    • 从零开始写软渲染器05
    • 从零开始写软渲染器04
    • 从零开始写软渲染器03
    • 从零开始写软渲染器02
    • 从零开始写软渲染器01
    • 从零开始写软渲染器00
    • 现代游戏常用的几种寻路方案(一)
  • Node
    • NPM Dependency
    • Node 优势
    • Node Stream
    • Node 模块系统
  • HTML
    • html5语义与结构元素
  • 跨端
    • Flutter 介绍
  • Golang
    • Golang 基础
  • AR
    • SceneKit
由 GitBook 提供支持
在本页
  • 背景
  • Webpack 依赖解析
  • Webpack 对于依赖对处理的过程
  • Why Plugin
  • 编写 Plugin
  • 构建依赖树
  • 获取 Git diff
  • 完整插件代码

这有帮助吗?

  1. Engineering

项目依赖分析

分析项目的依赖为树图,对比两次上线的 diff,得出受影响的页面,方便进行回归测试

背景

平台组件应用情况复杂,模块众多,在修改代码后,如何尽可能准确地回归受影响的模块依赖开发去检查。通过项目依赖的分析,能够:

  1. 在上线打包时自动对模块进行分析,解析出受影响的页面,提供用于测试的回归列表

  2. 分析出未使用的模块,缩减无用代码

  3. 对项目的依赖关系进行分析,了解代码组织结构,为代码重构与拆分提供参考

  4. 多页面 repo 而言,通过依赖分析拿到此次需要构建的资源,可以做到单页面发布

Webpack 依赖解析

Webpack 对于依赖对处理的过程

webpack 拿到入口文件 entry 后,会通过先获取资源的正确路径,再经过 loader 解析文件,最后通过遍历 ast 来拿到模块中引用的依赖 dependences ,再对 dependences 做递归处理,最终拿到依赖树。 这跟我们最初设想的思路基本一致,同时借助 loader 可以将不同的资源无法解析的问题也一并解决了。

Why Plugin

webpack 官方已经给出了 webpack-bundle-analyzer 这类的工具了, 每次构建后 stats 中都能拿到文件依赖,不直接使用它的原因是:

  1. 构建过程非常缓慢,特别是有几十个页面存在的情况下

  2. 我们只是想拿到资源依赖,不想对整个前端 repo 进行一次构建,也不想生成任何 bundle

    所以能不能既可以使用 loader 找到文件依赖,又不需要生成和压缩 bundle 呢,这个时候我们就需要使用 Webpack Plugin

编写 Plugin

我们需要的 Plugin 整体结构比较简单,在 apply 函数中,我们将 webpack 的钩子与相应函数进行绑定。我们需要的主要有三个钩子:beforeResolve/afterResolve/finishModules。 在 beforeResolve 中,我们对不需要分析的模块,例如 node_modules 的模块进行拦截终止。 在 afterResolve 中,将文件及其依赖传入,进行依赖树的构建。 在 finishModules 中,可以使用最终生成的依赖树做一些操作。

class DependencesAnalysisPlugin {
  beforeResolve(resolveData, callback) {
    if (analysisBreak(request, issuer)) {
      callback(null, null);
    } else {
      callback(null, resolveData);
    }
  }
  afterResolve(result, callback) {
    const { resourceResolveData } = result;
    const {
      context: { issuer },
      path,
    } = resourceResolveData;
    tree.addDependency(issuer, path);
    callback(null, result);
  }
  handleFinishModules(modules, callback) {
    // handle tree, etc: upload...
    callback(null, modules);
  }
  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => {
      nmf.hooks.beforeResolve.tapAsync(pluginName, this.beforeResolve);
      nmf.hooks.afterResolve.tapAsync(pluginName, this.afterResolve);
    });
    compiler.hooks.compilation.tap(pluginName, (compilation) => {
      compilation.hooks.finishModules.tapAsync(
        pluginName,
        this.handleFinishModules
      );
    });
  }
}
module.exports = DependencesAnalysisPlugin;

构建依赖树

构建依赖树有几点需要注意:

  1. 同一个文件可能被不同的模块进行引用,在模块插入子节点时,应该为所有路径相同的模块插入子模块

  2. 可能会出现重复依赖的情况,例如组件 A 引用了组件 B,但是组件 B 同时使用了组件 A 中定义的一些常量

    为了解决第一个问题,在构建树时,我们同时维护一个节点列表,使用节点的引用进行构建树,当插入子节点时,只需要从列表中找到引用,操作引用的子节点。

    当构建完依赖树,还需要进行一步对树的剪枝,在这一步我们解决循环依赖的问题。以树的来看,我们需要的最终结果是在标记节点修改时,要将父节点也同时标记为已修改。在从根结点到叶节点上,不同的节点只需要出现一次,所以做一遍遍历,同一条路径中只保留第一次出现的不重复节点,重复节点进行删除。

获取 Git diff

在产出最终的 json 之前,需要获取到更改的文件。通过 打包平台 中的环境变量,我们可以拿到这一次构建的 Commit ID 和分支。通过 打包平台 API,可以拿到分支下构建的历史版本信息。取到两次的 Commit ID,通过 gitlab API,可以获取到两次 commit 之间的 diff,也就能获取到更改的文件列表。

完整插件代码

const fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
const axios = require("axios");
const { default: routerConfig } = require("../config/router.config");
const pluginName = "DependencesAnalysisPlugin";
const ignoreDependenciesArr = Object.keys(
  require("../package.json").dependencies
);

const analysisBreak = (request, issuer) => {
  return (
    (issuer && issuer.includes("node_modules")) ||
    (request && request.includes("node_modules"))
  );
};

const getRouterInfo = (config) => {
  if (!config.component && (!config.routes || !config.routes.length)) {
    return null;
  }
  const pagePath = path.resolve(__dirname, "../src/pages/");
  const proRoot = path.resolve(__dirname, "../");
  let children = undefined;
  if (config.routes && config.routes.length) {
    children = config.routes.map(getRouterInfo).filter(Boolean);
  }
  return {
    route: config.path,
    path: config.component
      ? path.relative(
          proRoot,
          require.resolve(path.resolve(pagePath, config.component))
        )
      : undefined,
    children,
  };
};

class DependencyTree {
  constructor() {
    this.uniqNodes = [];
  }

  /**
   * 遍历树找到对应的结点列表
   * @param {*} issuer
   */
  traverseTree(issuer) {
    const nodes = this.uniqNodes.filter((n) => n.name === issuer);
    return nodes;
  }

  /**
   * 为结点列表添加子节点
   * @param {*} nodes
   * @param {*} request
   */
  addChildNode(nodes, request) {
    let uniqNode = this.uniqNodes.find((n) => n.name === request);
    if (!uniqNode) {
      uniqNode = {
        name: request,
      };
      this.uniqNodes.push(uniqNode);
    }
    nodes.forEach((node) => {
      if (!node.children) {
        node.children = [uniqNode];
      } else {
        if (!node.children.find((n) => n.name === request)) {
          node.children.push(uniqNode);
        }
      }
    });
  }

  /**
   * 增加一个依赖信息
   * @param {*} issuer
   * @param {*} request
   */
  addDependency(issuer, request) {
    if (!this.root) {
      const firstChildNode = {
        name: request,
      };
      const rootNode = {
        name: issuer,
        children: [firstChildNode],
      };
      this.root = rootNode;
      this.uniqNodes.push(rootNode, firstChildNode);
      return;
    }
    const nodes = this.traverseTree(issuer);
    this.addChildNode(nodes, request);
  }

  /**
   * 遍历树,将同一条链路上的重复出现的结点删除,第一次出现的保留
   */
  clearTree(root, nodes) {
    if (root.changed) {
      nodes.forEach((n) => {
        n.changed = true;
      });
    }
    if (!root.children) {
      return;
    }
    const indexes = root.children
      .map((child) => nodes.find((n) => n.name === child.name))
      .reduce((iter, cur, index) => iter.concat(cur ? index : []), []);
    root.children = root.children.reduce((iter, cur, index) => {
      if (indexes.includes(index)) {
        return iter;
      }
      return iter.concat(cur);
    }, []);
    root.children.forEach((child) => {
      this.clearTree(child, nodes.concat(root));
    });
  }

  getFiles(filePath) {
    const sum = [];
    const files = fs.readdirSync(filePath);
    for (const file of files) {
      const stat = fs.statSync(path.resolve(filePath, file));
      if (stat.isFile()) {
        sum.push(path.resolve(filePath, file));
      } else if (stat.isDirectory()) {
        sum.push(...this.getFiles(path.resolve(filePath, file)));
      }
    }
    return sum;
  }

  unusedFiles() {
    const root = path.resolve(__dirname, "../src/");
    const proRoot = path.resolve(__dirname, "../");
    const proFiles = this.getFiles(root).map((filePath) =>
      path.relative(proRoot, filePath)
    );
    const unused = proFiles.filter(
      (filePath) => !this.uniqNodes.find((node) => node.name === filePath)
    );
    const filePath = `unused.json`;
    fs.writeFileSync(filePath, JSON.stringify(unused, null, 2));
  }

  gitDiffFiles(commitHash, branch) {
    return axios
      .get(
        `xxx` // 获取此次打包的信息
      )
      .then((res) => res && res.data)
      .then((res) => {
        if (!res || !res.results || !res.results.length) {
          return Promise.reject("当前分支无上线版本,无法进行依赖分析");
        }
        const index = res.results.findIndex(
          (r) => r.base_commit_hash === commitHash
        );
        return res.results[
          index === -1 || index >= res.results.length - 1 ? 0 : index + 1
        ].base_commit_hash;
      })
      .then((lastHash) => {
        console.log("Diff hash from: ", lastHash, " to: ", commitHash);
        return axios.get(
          `xxx` // get git diff files from gitlab
        );
      })
      .then((res) => res.data)
      .then((res) => res.diffs.map((diff) => diff.new_path));
  }

  generate(commitHash, branch) {
    const router = getRouterInfo(routerConfig[0]);
    return this.gitDiffFiles(commitHash, branch)
      .then((filesTemp) => {
        const files = filesTemp.filter((p) => p.match("^src/"));
        console.log("Diff files: ", files);
        return files;
      })
      .then((files) => {
        this.uniqNodes.forEach((node) => {
          if (files.includes(node.name)) {
            node.changed = true;
          }
        });
        this.clearTree(this.root, []);
        return this.root;
      })
      .then((data) =>
        axios({
          url: "xxx", // upload result
          method: "POST",
          data: { body: data, commitHash, router, branch },
          headers: { "Content-Type": "application/json" },
        })
      )
      .then((res) => res.data)
      .then((res) => {
        if (res.code === 0) {
          console.log("依赖分析构建成功!");
        } else {
          console.error("依赖分析构建失败:", res.message);
        }
      })
      .catch((err) => {
        console.error("依赖分析构建失败:", err);
      });
  }
}

const tree = new DependencyTree();

class DependencesAnalysisPlugin {
  constructor(options = {}) {
    this.options = options;
  }

  afterResolve(result, callback) {
    const { resourceResolveData } = result;
    const {
      context: { issuer },
      path,
      descriptionFileRoot,
    } = resourceResolveData;
    if (!analysisBreak(path, issuer)) {
      tree.addDependency(
        issuer && issuer.replace(new RegExp(`^${descriptionFileRoot}/`), ""),
        path.replace(new RegExp(`^${descriptionFileRoot}/`), "")
      );
    }
    callback(null, result);
  }

  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap(pluginName, (nmf) => {
      nmf.hooks.afterResolve.tapAsync(pluginName, this.afterResolve);
    });
    compiler.hooks.done.tap(pluginName, (
      stats /* stats is passed as an argument when done hook is tapped.  */
    ) => {
      tree.generate(this.options.commitId, this.options.branch);
    });
  }
}

module.exports = DependencesAnalysisPlugin;
上一页微前端下一页前端监控原理

最后更新于4年前

这有帮助吗?