Node.js技术原理分析系列——Node.js的perf_hooks模块作用和用法
Node.js 是一个开源的、跨平台的 JavaScript 运行时环境,它允许开发者在服务器端运行 JavaScript 代码。Node.js 是基于 Chrome V8 引擎构建的,专为高性能、高并发的网络应用而设计,广泛应用于构建服务器端应用程序、网络应用、命令行工具等。
本系列将分为9篇文章为大家介绍 Node.js 技术原理:从调试能力分析到内置模块新增,从性能分析工具 perf_hooks 的用法到 Chrome DevTools 的性能问题剖析,再到 ABI 稳定的理解、基于 V8 封装 JavaScript 运行时、模块加载方式探究、内置模块外置以及 Node.js addon 的全面解读等主题,每一篇都干货满满。
本文内容为本系列第3篇,由体验技术团队屈金雄原创。以下为正文内容。
前言
在 Node.js 中,如何测量一段代码的耗时呢?
比如 for 循环遍历 100 次的耗时。让我们带着这个问题,开始本文的讲解。
1.用 Date.now()
很容易就能想到,在循环前后分别取一个时间,再相减不就行了
const start = Date.now();
for (let i = 0; i < 100; i++) {// ...
}
const end = Date.now();
console.log(`Execution time: ${end - start}ms`);
运行多次后发现,结果永远是 0。原因是 Date.now() 只能精确到毫秒级别,无法用于计算零点几毫秒这种的耗时。
2.用 console.time() 和 console.timeEnd()
console.time('example');
for (let i = 0; i < 100; i++) {// ...
}
console.timeEnd('example');
运行上述代码,在控制台打印:example: 0.179ms。 目的是达到了,但是这种方法仅适用于在控制台打印结果。如果要放到日志中,就不行了。
3.使用一些三方库
这样的三方库当然是有的,比如 benchmark.js 库,也能提供高精度时间戳。但问题就在于这些库是第三方的,如果 Node.js 本身的代码需要测试或记录性能数据,三方库就不再可用了。
在本文主角 perf_hooks 模块出现之前,还有更多的解决方案被采用,这里就不再列举。总之,我们需要 Node.js 自己的模块。 在 2017 年,也就是 node 8.5 版本发布时,就有了 perf_hooks 模块,用来提供高精度时间戳,测试代码耗时。
接下来我们探索一下,这个模块的用法。 由于 node 官方文档总是很简洁,很少写为什么,像字典一样。我们习惯性的全网搜索好心人提供的新手教程,但是这次失败了,关于 perf_hooks 介绍的文档很少。无奈之下,回到官方文档。
官方文档一开头就给出了一个示例:
const { PerformanceObserver, performance } = require('node:perf_hooks');const obs = newPerformanceObserver(items => {
console.log(items.getEntries()[0].duration);performance.clearMarks();
});
obs.observe({ type: 'measure' });
performance.measure('Start to Now');performance.mark('A');
doSomeLongRunningProcess(() => {performance.measure('A to Now', 'A');performance.mark('B');performance.measure('A to B', 'A', 'B');
});
相信初学的你和我一样,有十万个为什么想问
- 为什么用起来和我想象的不一样?
- 为什么要 new PerformanceObserver?
- PerformanceObserver 是干什么的?
- …
这时我们只有最后一招了,啃源码。其中过程与艰辛不表,只讲源码中啃出的结论,开头问题的解法会附在解说示例中。
本文使用的 Node.js 源码是18.20.2。
运行原理
讲运行原理之前,我们得先明白三个基本概念,就像我们学习编程语言之前,会不会写代码不要紧,总得先把变量、函数、类和对象,这些基本概念先熟悉一下,才能学习语句、api 等。
三个基本概念
-
高精度时间戳
本章第一个示例就告诉我们,想要计算代码耗时,先得有高精度时间戳。
perf_hooks 模块使用的时间戳都是高精度时间戳,依赖的底层 api 是
process.hrtime(),单位是毫秒(ms),保留 14 位小数点,例如 35.87660002708435。与通常意义的时间戳不同,高精度时间戳表示从 Node.js 进程启动到记录时间,中间间隔的时长。例如
performance.now()返回的是 Node.js 进程启动到 now 方法执行时的时长。这种设计可以避免操作系统时钟对精度的影响,适合 Node.js 性能度量。
-
PerformanceEntry
其中的 entry 翻译为条目最合适。 PerformanceEntry 是 perf_hooks 模块用来表示单条性能数据的数据结构,它有四个子类分别是 PerformanceNodeTiming、PerformanceResourceTiming、PerformanceMark 和 PerformanceMeasure。
一般来说,开发者不需要直接操作这几个类。
下图是简化后的示意图,实际层级关系比图中更多。

-
Performance Timeline
Performance Timeline 这个关键词,在文档中出现了 17 次,但是并没有介绍它是什么。Web Performance API 关于 Performance Timeline 的介绍,看起来和 perf_hooks 的源码又不相符。所以,我们根据 perf_hooks 源码和官方文档,可以做个未论证的推论,方便我们建立基本认知。
推论:Performance Timeline 是一个全局的变量,类型是数组,数组元素是 PerformanceEntry 类型的对象。
运行原理讲解
理解了前面两个基本概念,运行原理就好讲了。可以自行脑补一下,内存中有个数组,称呼为 Performance Timeline,用于按时间顺序存放一条条的性能数据。
那么接下来,分别实现向 timeline 数组中写入、读取等能力,就可以基本满足这个性能模块的需求了,具体实现在后面的章节会讲。
接下来用一个示例进一步说明运行原理:
const { performance, PerformanceObserver } = require('perf_hooks');// 创建一个 PerformanceObserver 实例
const obs = newPerformanceObserver(list => {
// 获取性能条目列表中的所有条目
const entries = list.getEntries();entries.forEach(entry => {console.log(`${entry.name}: ${entry.startTime}`);});
});// 监听 'mark' 类型的性能条目
obs.observe({ entryTypes: ['mark'] });// 创建性能标记和测量
performance.mark('A'); // 创建标记 'A'setTimeout(() => {performance.mark('B'); // 创建标记 'B'
}, 100);performance.mark('C'); // 创建标记 'C'
如上代码打印的时间数据,如下图:

可以看到打印了 A、B、C 三个条目的 startTime,对应的时间轴示意图如下图:

向 timeline 写数据
perf_hooks 直译是性能钩子。其实 hook 翻译为挂钩更为贴切。挂钩一端固定在软件系统上,至于挂钩上挂什么,由用户决定,总之挂的东西(代码)会被执行,执行时机由系统决定。那 perf_hooks 体系中的 hooks 具体是指什么呢?
性能数据(PerformanceEntry)分为 node、mark、measure、gc、function、http、http2 等几种类型。续接前文提到的挂钩,向 timeline 写数据,实际是在 node 系统上放置挂钩。除了 mark 和 measure,其他几种类型挂钩已经提前放好了(内置)。
mark 和 measure 对应的函数就是封装后的挂钩(包括了钩子函数和定义好的函数内容),它们可以搭配使用,测量指定两个挂钩执行的时间差,这个时间差可以称为自定义指标。准确来说 mark 挂钩(performance.mark)用来在指定代码位置收集执行时间等性能数据,measure 挂钩(performance.measure)用来计算两个挂钩的时间差。
简单来说,performance.mark('A')执行时,会创建一条 PerformanceMark(也是 PerformanceEntry 的子类)类型的条目,并写入 timeline;performance.measure('A')执行时,会创建一条 PerformanceMeasure(也是 PerformanceEntry 的子类)类型的条目,并写入 timeline。
从 timeline 查询数据
从 timeline 查询数据的方式有两种,一种同步方式,一种是异步方式。为避免查询 timeline 数据的代码本身,对性能造成影响,通常推荐的方式是异步方式。
以下三个方法用于同步地从 timeline 查询性能数据,返回 timeline 完整数据或其子集。
performance.getEntries()performance.getEntriesByName(name[, type])performance.getEntriesByType(type)
如下两个示例中,查询代码是同步执行的,如果 timeline 上的性能数据过多,查询动作会明显阻塞主线程。
示例代码 1:
// test_perf_hooks.jsperformance.mark('A');
console.log(performance.getEntries()[0]);
这个示例中, performance.mark('A')创建了一条 PerformanceMark 类型的数据,并添加到 timeline 中;performance.getEntries()取到当前 timeline 中的所有数据,它是一个数组,此时数组中只有一条数据;performance.getEntries()[0]取到第一条数据。
用 node test_perf_hooks.js 命令,运行这个文件,打印结果如下:
PerformanceMark {name: 'A',entryType: 'mark',startTime: 28.137699961662292,duration: 0,detail: null
}
结果中 startTime 的值是从 node 进程启动到这条数据创建时的毫秒数;duration 表示时长,PerformanceMark 类型的数据不存在时长,值都是 0。
示例代码 2:
这个示例就解决了文章开头提出的疑问。缺点是示例中的查询是同步的。
// test_perf_hooks.jsperformance.mark('A');
for (let i = 0; i < 100; i++) {// ...
}
performance.mark('B');
performance.measure('A to B', 'A', 'B');
console.log(performance.getEntriesByName('A to B')[0]);
performance.getEntriesByName('A to B')从 timeline 中查询出所有名称为’A to B’的数据,这里只有一条。 用 node test_perf_hooks.js 命令,运行这个文件,打印结果如下:
PerformanceMeasure {name: 'A to B',entryType: 'measure',startTime: 28.32800006866455,duration: 0.14899992942810059,detail: null
}
这条 PerformanceMeasure 类型的数据,表示标记 A 到标记 B 所用时长,这里 for 循环 100 次耗时是 duration 的值 0.14899992942810059。
以下三个方法用于异步地从 timeline 查询性能数据,同样返回 timeline 完整数据或其子集。
performanceObserverEntryList.getEntries()performanceObserverEntryList.getEntriesByName(name[, type])performanceObserverEntryList.getEntriesByType(type)
如下两个示例中,new PerformanceObserver 是在创建观察者,其构造函数入参是一个等待触发执行的函数,当监听的指定类型的条目数据被创建时,构造函数入参会被触发执行。本节所谓的异步,就是指定义在构造函数入参中的查询动作,时异步执行的,无论查询耗时多长,都不阻塞主线程。构造函数入参的入参是性能条目列表。
示例代码 1:
const { performance, PerformanceObserver } = require('perf_hooks');// 创建一个 PerformanceObserver 实例
const obs = newPerformanceObserver(list => {
// 获取性能条目列表中的所有条目
const entries = list.getEntries();entries.forEach(entry => {console.log(`${entry.name}: ${entry.startTime}`);});
});// 监听 'measure' 类型的性能条目
obs.observe({ entryTypes: ['mark'] });// 创建性能标记和测量
performance.mark('A'); // 创建标记 'A'
最后一行的执行,触发new PerformanceObserver时入参函数的执行。
示例代码 2:
这个示例是本文开头问题的最优解。
const { performance, PerformanceObserver } = require('perf_hooks');// 创建一个 PerformanceObserver 实例
const obs = newPerformanceObserver(list => {
// 获取性能条目列表中的所有条目
const entries = list.getEntries();entries.forEach(entry => {console.log(`${entry.name}: ${entry.duration}`);});
});// 监听 'measure' 类型的性能条目
obs.observe({ entryTypes: ['measure'] });// 创建性能标记和测量
performance.mark('A'); // 创建标记 'A'
for (let i = 0; i < 100; i++) {
// ...
}
performance.mark('B'); // 创建标记 'B'
performance.measure('A to B', 'A', 'B'); // 创建 'A to B' 的测量条目
运行后,打印结果:A to B: 0.15059995651245117,与其他几种的结果相符。
API串讲
理解 Performance Timeline 是本模块的设计思想的关键所在。除了定义性能数据结构的 API,就是向 timeline 读写数据的 API。理解到了这一层,整个模块就算理解的差不多了。
一些使用场景及其示例
测量异步操作耗时
'use strict';
const async_hooks = require('node:async_hooks');
const { performance, PerformanceObserver } = require('node:perf_hooks');const set = newSet();
const hook = async_hooks.createHook({
init(id, type) {if (type === 'Timeout') {performance.mark(`Timeout-${id}-Init`);set.add(id);}},
destroy(id) {if (set.has(id)) {set.delete(id);performance.mark(`Timeout-${id}-Destroy`);performance.measure(`Timeout-${id}`, `Timeout-${id}-Init`, `Timeout-${id}-Destroy`);}},
});
hook.enable();const obs = newPerformanceObserver((list, observer) => {
console.log(list.getEntries()[0]);performance.clearMarks();performance.clearMeasures();observer.disconnect();
});
obs.observe({ entryTypes: ['measure'], buffered: true });setTimeout(() => {}, 1000);
测量模块加载耗时
'use strict';
const { performance, PerformanceObserver } = require('node:perf_hooks');
const mod = require('node:module');// Monkey patch the require function
mod.Module.prototype.require = performance.timerify(mod.Module.prototype.require);
require = performance.timerify(require);// Activate the observer
const obs = newPerformanceObserver(list => {
const entries = list.getEntries();entries.forEach(entry => {console.log(`require('${entry[0]}')`, entry.duration);});performance.clearMarks();performance.clearMeasures();obs.disconnect();
});
obs.observe({ entryTypes: ['function'], buffered: true });require('some-module');
测量 HTTP 请求耗时
'use strict';
const { PerformanceObserver } = require('node:perf_hooks');
const http = require('node:http');const obs = newPerformanceObserver(items => {items.getEntries().forEach(item => {console.log(item);});
});obs.observe({ entryTypes: ['http'] });constPORT = 8080;http.createServer((req, res) => {res.end('ok');}).listen(PORT, () => {http.get(`http://127.0.0.1:${PORT}`);});
总结与回顾
文中推论,timeline 是一个全局变量,其实不准确,timeline 的准确构成和自动清除机制都存疑。但是这么理解能帮助我们快速建立对 perf_hooks 模块的基本认知。
本系列前两节内容请查看:
- Node.js技术原理分析系列——Node.js调试能力分析
- Node.js技术原理分析系列——如何在Node.js中新增一个内置模块
下一节,将分享 使用Chrome DevTools分析Node.js性能问题,请大家持续关注本系列内容~学习完本系列,你将获得:
- 提升调试与性能优化能力
- 深入理解模块化与扩展机制
- 探索底层技术与定制化能力
关于OpenTiny
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:https://opentiny.design
OpenTiny 代码仓库:https://github.com/opentiny
TinyVue 源码:https://github.com/opentiny/tiny-vue
TinyEngine 源码: https://github.com/opentiny/tiny-engine
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~
相关文章:
Node.js技术原理分析系列——Node.js的perf_hooks模块作用和用法
Node.js 是一个开源的、跨平台的 JavaScript 运行时环境,它允许开发者在服务器端运行 JavaScript 代码。Node.js 是基于 Chrome V8 引擎构建的,专为高性能、高并发的网络应用而设计,广泛应用于构建服务器端应用程序、网络应用、命令行工具等。…...
大模型在术后认知功能障碍预测及临床方案制定中的应用研究
目录 一、引言 1.1 研究背景与意义 1.2 研究目的与方法 1.3 研究创新点 二、术后认知功能障碍概述 2.1 定义与表现 2.2 危害与影响 2.3 发病机制与相关因素 三、大模型技术原理与应用现状 3.1 大模型技术原理 3.2 大模型在医疗领域的应用 四、基于大模型的术后认知…...
用DeepSeek来帮助学习three.js加载3D太极模形
画一个平面的太极图是很容易,要实现3D的应该会很难 一、参考3D模形效果 看某网页看到一个效果,像一个3D太极球,觉得挺有趣,挺解压的,想进一步去了解下这是如何实现 效果: 链接地址: http://www.…...
【JavaEE进阶】Spring Boot配置文件
欢迎关注个人主页:逸狼 创造不易,可以点点赞吗 如有错误,欢迎指出~ 目录 SpringBoot配置⽂件 举例: 通过配置文件修改端口号 配置⽂件的格式 properties基本语法 读取配置⽂件 properties配置文件的缺点 yml配置⽂件 yml基本语法 yml和proper…...
学习通用多层次市场非理性因素以提升股票收益预测
“Learning Universal Multi-level Market Irrationality Factors to Improve Stock Return Forecasting” 论文地址:https://arxiv.org/pdf/2502.04737 Github地址:https://github.com/lIcIIl/UMI 摘要 深度学习技术与量化交易相结合,在股…...
【Godot4.3】基于绘图函数的矢量蒙版效果与UV换算
概述 在设计圆角容器时突发奇想: 将圆角矩形的每个顶点坐标除以对应圆角矩形所在Rect2的size,就得到了顶点对应的UV坐标。然后使用draw_colored_polygon,便可以做到用图片填充圆角矩形的效果。而且这种计算的效果就是图片随着其填充的图像缩…...
记一些工具(持续更新)
wireshark——网络抓包工具 mitmproxy ——利用中间人攻击进行https抓包的工具(需配合安装由此代理自己签发的根证书到客户机系统) Cloudflare—— 一个网站中间层,通过基于反向代理的内容分发网络(CDN),Cloudflare提供包括CDN、优化工具、安全、分析以…...
DeepSeek开源周Day1:FlashMLA引爆AI推理性能革命!
项目地址:GitHub - deepseek-ai/FlashMLA 开源日历:2025-02-24起 每日9AM(北京时间)更新,持续五天! 一、开源周震撼启幕 继上周预告后,DeepSeek于北京时间今晨9点准时开源「FlashMLA」,打响开源周五连…...
PCL 点云添加高斯噪声
文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 在点云模型中将所有的点沿其法向量方向随机偏移一定距离,以此来得到点云实体的噪声点,偏移的幅度与点云尺度有关,偏移距离服从高斯分布,以此来获得高斯分布的噪声数据。 二、实现代码 // 标准文件 #include &l…...
通过恒定带宽服务器调度改进时间敏感网络(TSN)流量整形
论文标题 英文标题:Improving TSN Traffic Shaping with Constant Bandwidth Server Scheduling 中文标题:通过恒定带宽服务器调度改进时间敏感网络(TSN)流量整形 作者信息 作者:Benjamin van Seggelen 指导教师&am…...
软件测试高频面试题
以下是一些软件测试高频面试题: 基础概念类 HTTP和HTTPS的区别:HTTPS使用SSL/TLS协议对传输数据加密,HTTP没有加密;HTTPS可确保数据完整性,防止传输中被篡改,HTTP不保证;HTTP默认用80端口&…...
英语学习DAY5
内心旁白 关于我为什么从2月5号开的这个篇章现在才第五天这件事? 咳咳咳,容许我狡辩一下,我是有事去忙了,我真的很想每日学习英语(信我兄弟们)! 虽然英语学习对我来说真的很难,你…...
如何查看图片的原始格式
问题描述:请求接口的时候,图片base64接口报错,使用图片url请求正常 排查发现是图片格式的问题: 扩展名可能被篡改:如果文件损坏或扩展名被手动修改,实际格式可能与显示的不同,需用专业工具验证…...
Kronecker分解(K-FAC):让自然梯度在深度学习中飞起来
Kronecker分解(K-FAC):让自然梯度在深度学习中飞起来 在深度学习的优化中,自然梯度下降(Natural Gradient Descent)是一个强大的工具,它利用Fisher信息矩阵(FIM)调整梯度…...
赛前启航 | 三场重磅直播集结,予力微软 AI 开发者挑战赛!
随着微软 AI 开发者挑战赛的火热进行,赛前指导直播已成为众多参赛者获取技术干货、灵感碰撞和实战技巧的绝佳平台。继前两期的精彩呈现,第三、四、五期直播即将接连登场,为开发者们带来更加深入的 AI 技术剖析和项目实战指引。无论你是想进一…...
VMware安装Centos 9虚拟机+设置共享文件夹+远程登录
一、安装背景 工作需要安装一台CentOS-Stream-9的机器环境,所以一开始的安装准备工作有: vmware版本:VMware Workstation 16 镜像版本:CentOS-Stream-9-latest-x86_64-dvd1.iso (kernel-5.14.0) …...
【HarmonyOS Next】地图使用详解(一)
背景 这系列文章主要讲解鸿蒙地图的使用,当前可以免费使用,并提供了丰富的SDK给开发者去自定义控件开发。目前可以实现个性化显示地图、位置搜索和路径规划等功能,轻松完成地图构建工作。需要注意的是,现在测试只能使用实体手机去…...
【NLP 37、激活函数 ③ relu激活函数】
—— 25.2.23 ReLU广泛应用于卷积神经网络(CNN)和全连接网络,尤其在图像分类(如ImageNet)、语音识别等领域表现优异。其高效性和非线性特性使其成为深度学习默认激活函数的首选 一、定义与数学表达式 ReLU࿰…...
顶刊配图复现:Origin+DeepSeek完美协同
学习目标: (1)软件掌握熟练安装并配置Origin,掌握基础操作与核心功能。学会利用Origin进行多类型图表绘制及美化。掌握DeepSeek的数据清洗、统计分析与可视化方法。(2)设计能力理解顶刊图表的设计原则&…...
Fisher信息矩阵(Fisher Information Matrix, FIM)与自然梯度下降:机器学习中的优化利器
Fisher信息矩阵与自然梯度下降:机器学习中的优化利器 在机器学习尤其是深度学习中,优化模型参数是一个核心任务。我们通常依赖梯度下降(Gradient Descent)来调整参数,但普通的梯度下降有时会显得“笨拙”,…...
Scratch032(百发百中)
提示:知识回顾 1、排列克隆体的方法 2、复习“发送广播并等待”积木 3、“获取第几个字符”积木的使用 4、使用角色显示得分 前言 提示:中国射箭拥有悠久的历史,是最早进入教育体系的运动项目之一,君子六艺中“礼,乐,射,御,书,数”的射 ,就是指的射箭。这节课我带你…...
DeepSeek技术全景解析:架构创新与行业差异化竞争力
一、DeepSeek技术体系的核心突破 架构设计:效率与性能的双重革新 Multi-head Latent Attention (MLA):通过将注意力头维度与隐藏层解耦,实现显存占用降低30%的同时支持4096超长上下文窗口。深度优化的MoE架构:结合256个路由专家…...
开课倒计时 | 3月1-2日,DeepSeek时代下的可观测性(Observability)认证培训
前言: 随着DeepSeek等前沿AI技术的广泛应用,企业对可观测性的需求日益增长。DeepSeek作为一款强大的AI模型,已经在多个领域展现出其卓越的性能。然而,随着技术复杂性的增加,如何有效监控和优化这些系统成为关键挑战。…...
相似性搜索(2)
在本篇中,我们通过播客相似性搜索为例,进一步研究基于chroma 的相似性搜索: 参考: https://www.kaggle.com/code/switkowski/building-a-podcast-recommendation-engine/notebook 数据集来源: https://www.kaggle.…...
Python天梯赛L1-018-大笨钟详解
018-大笨钟 微博上有个自称“大笨钟V”的家伙,每天敲钟催促码农们爱惜身体早点睡觉。不过由于笨钟自己作息也不是很规律,所以敲钟并不定时。一般敲钟的点数是根据敲钟时间而定的,如果正好在某个整点敲,那么“当”数就等于那个整点…...
HTTP代理与HTTPS代理的区别及HTTPS的工作原理
在互联网世界中,数据的传输与访问安全性是用户和企业共同关注的焦点。HTTP和HTTPS代理作为两种常用的网络协议代理,它们在工作原理和应用场景上存在显著区别。本文将深入浅出地解析HTTP代理与HTTPS代理的区别,并简明扼要地介绍HTTPS的工作原理…...
【Godot4.3】静态模板字符串函数库
概述 Godot的静态函数从3.4版本一直用到现在的4.3,也曾经编写过不少的静态函数库。 但是一直没怎么用过静态变量。这几天有心重新开发一下静态网页生成器。需要编写一些类,还有保存HTML页面或局部的模板字符串以及生成函数。静态变量就刚好用上了。 这…...
Minio分布式多节点多驱动器集群部署
Minio分布式多节点多驱动器集群部署 Minio分布式多节点多驱动器集群部署节点规划先决条件开放防火墙端口设置主机名更新域名映射文件时间同步存储要求内存要求 增加虚拟机磁盘(所有机器都要执行)部署分布式 MinIO测试上传与预览测试高可用MinIO 配置限制模拟单节点磁盘故障模拟…...
忽略Git文件的修改,让它不被提交
使用Git托管的工程中,经常有这样的需求,希望文件只是本地修改,不提交到服务端。 如果仅仅是本地存在的文件,我们可以通过.gitignore配置避免文件被提交。 有的时候文件是由git托管的,但是我们希望只在本地修改&#…...
EntityFrameCore DbFirst 迁移
ORM框架:不用关心sql语句,只需要以类为单位,去操作数据库,以面向对象的思想来完成对数据库的操作。 EntityFrameCore-DbFirst Nuget引入程序集 Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.SqlServer Microsoft.EntityFrameworkCore.SqlServer.Design…...
