读书是易事,思索是难事,但两者缺一,便全无用处。
前言
依赖分析工具的目标是解析项目中的依赖关系,并以可视化的方式呈现这些关系。通过这样的工具,我们可以更直观地理解项目结构,发现潜在的问题,优化依赖管理。
项目流程图
效果图
源码github: 传送阵
要点分享
1. web
渲染组件
1.定义 createGraph
函数,用于创建和绘制图表。
const createGraph = (data: GraphData) => {})
2.使用 D3.js 创建缩放
功能,并为图表添加缩放行为。
const zoom = d3
.zoom()
.scaleExtent([1, 10])
.on('zoom', (d3: any) => zoomed(d3))
const svg = d3
.select(graph.value)
.append('svg')
.attr('width', width)
.attr('height', height)
.call(zoom as d3.ZoomBehavior<any, any>)
function zoomed(event: any) {
const transform = event.transform
link.attr('transform', transform)
node.attr('transform', transform)
}
设置缩放比例范围,并为图表添加缩放行为。在 zoomed
函数中,调整节点和链接的变换属性,实现缩放效果。
3.创建 D3.js 力导向模拟
,定义节点和链接的力作用。
simulation = d3
.forceSimulation(data.nodes as any)
.force(
'link',
d3.forceLink(data.links).id((d: any) => d.id),
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
定义节点和链接的力作用。forceSimulation
创建一个力导向图的模拟,forceLink
用于定义链接的力,forceManyBody
用于定义节点之间的引力或斥力,forceCenter
将图的中心设定在 SVG 的中心位置。
4.创建 SVG 元素,并为链接和节点添加样式和交互行为。
const link = svg
.append('g')
.attr('class', 'links')
.selectAll('line')
.data(data.links)
.enter()
.append('line')
.attr('stroke-width', 2)
.attr('stroke', '#999')
const node = svg
.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(data.nodes)
.enter()
.append('circle')
.attr('r', 5)
.attr('fill', '#69b3a2')
.call(drag)
node.append('title').text((d: any) => d.id)
link
变量用于创建链接,并设置其宽度和颜色。node
变量用于创建节点,设置其半径和填充颜色,并添加拖动行为。同时,为每个节点添加一个 title
元素,显示节点的 id
。
打开前端页面逻辑
import express from 'express'
import path from 'path'
import open from 'open'
let app: express.Application
export const startWebProject = (data: any): void => {
app = express()
const port = 3000
// 设置静态文件目录为Vue构建后的dist目录
const publicDir = path.join(__dirname, '../../dist')
console.log(publicDir, 'publicDir')
app.use(express.static(publicDir))
app.use(express.json())
// 提供一个API接口获取graphData
app.get('/api/graph-data', (req, res) => {
res.json(data)
})
// 默认路由
app.get('*', (req, res) => {
res.sendFile(path.join(publicDir, 'index.html'))
})
app.listen(port, () => {
console.log(`Web project is running at http://localhost:${port}`)
})
open(`http://localhost:${port}`)
}
export const renderGraph = (graphData: any): void => {
console.log('Rendering graph with data:', graphData)
// 这里可以调用实际的渲染逻辑,例如将数据发送到前端页面
}
2. cli
定义命令行
利用commander这个插件进行命令行的编写
import { Command } from 'commander'
import { doAnalysis } from './analysis'
export const main = async () => {
const program = new Command()
program.name('dep-cli').usage('<command> [options]').version('1.0.0')
program
.command('ana <pnpm-lock-filename>')
.description('分析模块依赖关系,目前仅支持 pnpm lock file')
.option('--depth <number>', '分析深度', '5') // 添加 depth 选项,默认值为 5
.action(async (filename: string, options: { depth: string }) => {
const depth = parseInt(options.depth, 10) //十进制转换
await doAnalysis(filename, depth)
})
program.parse(process.argv)
}
调用其他项目中的方法
两种方式:
1. 利用如rush类似的monorepo,首先在rush.json注册项目,然后执行rush install/rush build
"projects": [
{
"packageName": "xxx_name",
"projectFolder": "xxx_path"
},
{
"packageName": "xxx_name2",
"projectFolder": "xxx_path2"
},
...
]
-在xxx_name项目中定义项目依赖
{
"name": "xxx_name",
"version": "1.0.0",
"dependencies": {
"xxx_name2": "workspace:*",
...
}
}
-调用xxx_name2的方法
import { helloFrom } from 'xxx_name2';
helloFrom();
2. 一种简单的方法:在package.json中定义项目的路径,并执行pnpm install 会在自己的node_module中生产一个依赖包
"dependencies": {
"commander": "^12.1.0",
"core": "file:../core",
"web": "file:../web"
}
3. core
解析依赖文件
1.解析规范字符串
const parseFromSpecify = (specifier: string) => {
const REGEXP = /(@?[\w\-\d\.]+(\/[\w\-\d\.]+)?)@?([\d\w\.\-]+)?/
if (!REGEXP.test(specifier)) {
throw new Error(`Can not parse this key: ${specifier}`)
}
const [, name, , version] = REGEXP.exec(specifier)!
return {
name,
specifier,
localVersion: version,
version,
}
}
用于解析依赖项的规范字符串。正则表达式用于提取名称和版本信息
,如果不匹配规范字符串格式,则抛出错误。
2.获取依赖关系节点
const getDepGraphNode = (
name: string,
version: string,
depType: DepTypes,
packages: Record<string, PackageSnapshot>,
currentDepth: number,
maxDepth: number,
packageKey: string
): DepGraphNode => {
const packageInfo = packages[packageKey] // 获取包信息
if (!packageInfo) {
return {
name,
version,
external: true,
dependencies: [],
}
}
// 获取包的依赖列表,并确保递归深度限制
const deps: DepGraphNode[] =
currentDepth < maxDepth
? Object.entries(packageInfo.dependencies || {}).map(
([depName, depVersion]) => {
const { name } = parseFromSpecify(depName)
return getDepGraphNode(
name,
depVersion as string,
depType,
packages,
currentDepth + 1,
maxDepth,
depName + '@' + depVersion
)
}
)
: []
return {
name,
version,
external: !!packageInfo.resolution && 'type' in packageInfo.resolution, // 判断是否为外部依赖
dependencies: deps,
}
}
递归获取依赖关系节点。函数检查包信息是否存在,并获取包的依赖列表,确保递归深度不超过最大深度
。返回包含名称、版本、是否为外部依赖和依赖列表
的节点。
3.定义继承自 BaseDepGraph 的类
export class PnpmDepGraph extends BaseDepGraph {
private filePath: string
private maxDepth: number
constructor(filePath: string, maxDepth: number) {
super()
this.filePath = filePath
this.maxDepth = maxDepth
}
}
构造函数接收文件路径和最大递归深度,初始化类属性。后续可扩展
npm、yarn等其他依赖文件lock.file
4.解析依赖图的方法
async parse(): Promise<DepGraph> {
const lockfile: Lockfile | null = await readWantedLockfile(
path.dirname(this.filePath),
{
ignoreIncompatible: false,
}
)
if (!lockfile) throw new Error('Failed to read lockfile')
const packages = lockfile.packages as Record<string, PackageSnapshot>
if (!packages) {
throw new Error('No packages found in lockfile')
}
const depGraph: DepGraph = Object.entries(packages).map((keys) => {
const [packageKey, packageInfo] = keys
const { name, version } = parseFromSpecify(packageKey)
const depType: DepTypes =
'peerDependencies' in packageInfo
? 'peer'
: 'devDependencies' in packageInfo
? 'dev'
: 'prod'
return getDepGraphNode(
name,
version,
depType,
packages,
0,
this.maxDepth,
packageKey
) // 获取依赖关系节点并控制递归深度
})
return depGraph
}
首先读取并解析锁文件,如果锁文件不存在则抛出错误。然后获取包信息,遍历包并调用 getDepGraphNode
递归获取依赖关系节点,最终返回构建的依赖图数据结构。
可能遇到的问题及解决方案:
1.如何打开前端项目: 利用open依赖安装8.4.2,注意高版本可能会报错导致无法正常打开网址
2.d3渲染逻辑看不懂: 建议查看官网或者使用chatgpt
3.为什么要拆分成3个项目: 解耦,同时支持后续的扩展和方便维护。