create-vite 源码解析它来喽

news2024/12/27 13:26:49

create-vite的源码很简单,只有一个文件,代码总行数400左右,但是实际需要阅读的代码大约只有200行左右,废话不多说,直接开始吧。

代码结构

create-vite的代码结构非常简单,直接将index.ts拉到最底下,发现只执行了一个函数init()

init().catch((e) => {console.error(e)
}) 

我们的故事将从这里开始。

init()

init()函数的代码有点长,但是实际上也不复杂,我们先来看看它最开头的两行代码:

async function init() {const argTargetDir = formatTargetDir(argv._[0])const argTemplate = argv.template || argv.t
} 

首先可以看到init函数是一个异步函数,最开始的两行代码分别获取了argv._[0]argv.template或者argv.t

这个argv是怎么来的,当然是通过一个解析包来解析的,在顶部有这样的一段代码:

const argv = minimist(process.argv.slice(2), { string: ['_'] }) 

就是这个minimist包,它的作用就是解析命令行参数,感兴趣的可以自行了解,据说这个包也是百来行代码。

继续往下,这两个参数就是我们在执行create-vite命令时传入的参数,比如:

create-vite my-vite-app 

那么argv._[0]就是my-vite-app

如果我们执行的是:

create-vite my-vite-app --template vue 

那么argv.template就是vue

argv.t就是argv.template的简写,相当于:

create-vite my-vite-app --t vue

# 等价于

create-vite my-vite-app --template vue 

通过打断点的方式,可以看到结果和我们预想的一样。

formatTargetDir(argv._[0])就是格式化我们传入的目录,它会去掉目录前后的空格和最后的/,比如:

formatTargetDir(' my-vite-app ') // my-vite-app
formatTargetDir(' my-vite-app/') // my-vite-app 

这个代码很简单,就不贴出来了,继续往下:

let targetDir = argTargetDir || defaultTargetDir 

targetDir是我们最终要创建的目录,defaultTargetDir的值是vite-project,如果我们没有传将会用这个值来兜底。

紧接着后面跟着一个getProjectName的函数,通常来讲这种代码可以跳过先不看,但是这里的getProjectName函数有点特殊;

const getProjectName = () =>targetDir === '.' ? path.basename(path.resolve()) : targetDir 

它会根据targetDir的值来判断我们的项目是不是在当前目录下创建的,如果是的话,就会返回当前目录的名字,比如:

create-vite . 

可以看到如果项目名称传的是.,那么getProjectName函数就会返回当前目录的名字,也就是create-vite(根据自己的情况而定);

不看源码还真不知道这里还可以这么用,继续往下,就是定义了一个问题数组:

result = await prompts([]) 

这个prompts函数是一个交互式命令行工具,它会根据我们传入的问题数组来进行交互,就比如源码中,一共列出了6个问题:

  • projectName:项目名称
  • overwrite:是否覆盖已存在的目录
  • overwriteChecker:检测覆盖的目录是否为空
  • packageName:包名
  • framework:框架
  • variant:语言

当执行create-vite命令时,后面不跟着任何参数,而且我们一切操作都是合规的,那么只会经历三个问题:

  • projectName:项目名称
  • framework:框架
  • variant:语言

projectName:项目名称

配置项如下:

var projectName = {type: argTargetDir ? null : 'text',name: 'projectName',message: reset('Project name:'),initial: defaultTargetDir,onState: (state) => {targetDir = formatTargetDir(state.value) || defaultTargetDir}
} 

先来简单介绍一个每一个配置项的含义:

  • type:问题的类型,这里的null表示不需要用户输入,直接跳过这个问题,这个配置项的值可以是textselectconfirm等,具体可以看这里;
  • name:问题的名称,这里的projectName是用来在prompts函数的返回值中获取这个问题的答案的;
  • message:问题的描述,这里的Project name:是用来在命令行中显示的;
  • initial:问题的默认值,这里的defaultTargetDir是用来在命令行中显示的;
  • onState:问题的回调函数,每次用户输入的时候都会触发这个函数,这里的state就是用户输入的值;

可以看到这里的type配置是根据argTargetDir的值来决定的,如果argTargetDir有值,那么就会跳过这个问题,直接使用argTargetDir的值作为项目名称;

如果在使用create-vite命令时,后面跟着了项目名称,那么argTargetDir就有值了,也就是会跳过这个问题,后面的属性就没什么好分析了,接着往下。

overwrite:是否覆盖已存在的目录

配置项如下:

var overwrite = {type: () =>!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm',name: 'overwrite',message: () =>(targetDir === '.'? 'Current directory': `Target directory "${targetDir}"`) +` is not empty. Remove existing files and continue?`
} 

这里的type配置项是一个函数,这个函数的返回值是null或者confirm

如果targetDir目录不存在,或者targetDir目录下面没有东西,那么就会跳过这个问题,直接使用null作为type的值;

message配置项也是一个函数,这个函数的返回值是一个字符串,这个字符串就是在命令行中显示的内容;

同样因为人性化的考虑,会显示不同的提示语来帮助用户做出选择;

overwriteChecker:检测覆盖的目录是否为空

配置项如下:

var overwriteChecker = {type: (_, {overwrite}: { overwrite?: boolean }) => {if (overwrite === false) {throw new Error(red('✖') + ' Operation cancelled')}return null},name: 'overwriteChecker'
} 

overwriteChecker会在overwrite问题之后执行,这里的type配置项是一个函数,里面接收了两个参数;

第一个参数名为_,通常这种行为是占位的,表示这个参数没有用到,但是又不能省略;

第二个参数是一个对象,这个对象里面有一个overwrite属性,这个属性就是overwrite问题的答案;

他通过overwrite的值来判断用户是否选择了覆盖,如果选择了覆盖,就会跳过这个问题;

否则的话就证明这个目录下面存在文件,那么就会抛出一个错误,这里抛出错误是会终止整个命令的执行的;

这一部分,在定义问题数组的时候有做处理,使用try...catch来捕获错误,如果有错误,就会使用return来终止整个命令的执行;

try {result = await prompts([])
} catch (cancelled: any) {console.log(cancelled.message)return
} 

packageName:包名

配置项如下:

var packageName = {type: () => (isValidPackageName(getProjectName()) ? null : 'text'),name: 'packageName',message: reset('Package name:'),initial: () => toValidPackageName(getProjectName()),validate: (dir) =>isValidPackageName(dir) || 'Invalid package.json name'
} 

这里的type配置项是一个函数,里面通过isValidPackageName来判断项目名称是否是一个合法的包名;

getProjectName在上面已经介绍过了,这里就不再赘述;

isValidPackageName是用来判断包名是否合法的,这个函数的实现如下:

function isValidPackageName(projectName: string) {return /^(?:@[a-z\d-*~][a-z\d-*._~]*/)?[a-z\d-~][a-z\d-._~]*$/.test(projectName)
} 

validate用来验证用户输入的内容是否合法,如果不合法,就会显示Invalid package.json name

framework:框架

配置项如下:

var framework ={type:argTemplate && TEMPLATES.includes(argTemplate) ? null : 'select',name: 'framework',message:typeof argTemplate === 'string' && !TEMPLATES.includes(argTemplate)? reset(`"${argTemplate}" isn't a valid template. Please choose from below: `): reset('Select a framework:'),initial: 0,choices: FRAMEWORKS.map((framework) => {const frameworkColor = framework.colorreturn {title: frameworkColor(framework.display || framework.name),value: framework}})
} 

这里的就相对来说复杂了点,首先判断了argTemplate是否存在,如果存在,就会判断argTemplate是否是一个合法的模板;

TEMPLATES的定义是通过FRAMEWORKS来生成的:

const TEMPLATES = FRAMEWORKS.map((f) => {const variants = f.variants || [];const names = variants.map((v) => v.name);return names.length ? names : [f.name];}).reduce((a, b) => a.concat(b), []) 

这里我将代码拆分了一下,这样看着会更清晰一点,最后的reduce的作用应该是对值进行一个拷贝处理;

源码里面的map返回的都是引用值,所以需要进行拷贝(这是我猜测的),源码如下:

const TEMPLATES = FRAMEWORKS.map((f) => (f.variants && f.variants.map((v) => v.name)) || [f.name]
).reduce((a, b) => a.concat(b), []) 

FRAMEWORKS是写死的一个数组,代码很长,就不贴出来了,这里就贴一下type的定义:

type Framework = {name: stringdisplay: stringcolor: ColorFuncvariants: FrameworkVariant[]
}

type FrameworkVariant = {name: stringdisplay: stringcolor: ColorFunccustomCommand?: string
} 
  • name是框架的名称;
  • display是显示的名称;
  • color是颜色;
  • variants是框架的语言,比如reacttypescriptjavascript两种语言;
  • customCommand是自定义的命令,比如vuevue-cli就是自定义的命令;

分析到这里,再回头看看framework的配置项,就很好理解了,这里的choices就是通过FRAMEWORKS来生成的:

var framework = {choices: FRAMEWORKS.map((framework) => {const frameworkColor = framework.colorreturn {title: frameworkColor(framework.display || framework.name),value: framework}})
} 

choices是一个数组,用于表示typeselect时的选项,数组的每一项都是一个对象,对象的title是显示的名称,value是选中的值;

上面的代码就是用来生成choices的,frameworkColor是一个颜色函数,用来给framework.display或者framework.name上色;

variant:语言

配置项如下:

var variant = {type: (framework: Framework) =>framework && framework.variants ? 'select' : null,name: 'variant',message: reset('Select a variant:'),choices: (framework: Framework) =>framework.variants.map((variant) => {const variantColor = variant.colorreturn {title: variantColor(variant.display || variant.name),value: variant.name}})
} 

这里的type是一个函数,函数的第一个参数就是framework,这里的type是根据framework来判断的,如果framework存在并且framework.variants存在,就让用户继续这一个问题。

通过之前的分析,这一块应该都能看明白,就继续往下走;

获取用户输入

接着往下走就是获取用户输入了,用户回答完所有问题后,结果会返回到result中,可以用过解构的方式来获取:

const { framework, overwrite, packageName, variant } = result 

清空目录

接着就是对生成项目的位置进行处理,根据上面分析的逻辑,会有目录下有文件的情况,所以需要先清空目录:

// 确定项目生成的目录
const root = path.join(cwd, targetDir)

// 清空目录
if (overwrite) {emptyDir(root)
} else if (!fs.existsSync(root)) {fs.mkdirSync(root, {recursive: true})
} 

emptyDir是一个清空目录的方法,fs.existsSync是用来判断目录是否存在的,如果不存在就创建一个;

function emptyDir(dir: string) {// 如果目录不存在,啥也不管if (!fs.existsSync(dir)) {return}// 读取目录下的所有文件for (const file of fs.readdirSync(dir)) {// 忽略 .git 的目录if (file === '.git') {continue}// 删除文件,如果是目录就递归删除fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })}
} 

existsSync第二个参数是一个对象,recursive表示是否递归创建目录,如果目录不存在,就会创建目录,如果目录存在,就会报错;

生成项目

继续往下走,就是生成项目相关的,最开始肯定是确定项目的内容。

确定项目模板

// 确定项目模板
const template: string = variant || framework?.name || argTemplate 

这里的template就是项目的模板,如果用户选择了variant,那么就用variant,如果没有选择,就用framework,如果framework不存在,就用argTemplate

这些变量代表什么,从哪来的上面都有分析。

确定包管理器

const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent) 

这里的process.env.npm_config_user_agent并不是我们自己定义的,是npm自己定义的;

这个变量值是指的当前运行环境的包管理器,比如npmyarn等等,当然这个值肯定没我写的这么简单;

通过debug可以看到我的值是pnpm/7.17.0 npm/? node/v14.19.2 win32 x64,每个人的值根据环境的不同而不同;

pkgFromUserAgent是一个解析userAgent的方法,大白话就是解析包管理器的名称和版本号;

例如{name: 'npm', version: '7.17.0'},代码如下:

function pkgFromUserAgent(userAgent: string | undefined) {if (!userAgent) return undefinedconst pkgSpec = userAgent.split(' ')[0]const pkgSpecArr = pkgSpec.split('/')return {name: pkgSpecArr[0],version: pkgSpecArr[1]}
} 

这个代码也没那么高深,就是解析字符串,然后返回一个对象,给你写也一定可以写出来的;

后面两段代码就是正式确定包管理器的名称和版本号了,代码如下:

const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.') 

yarn的版本如果是1.x后面会有一些特殊处理,所以会有isYarn1这个变量;

接着就是确定包管理器的命令了,代码如下:

 const { customCommand } =FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {} 

这一段是用来确定部分模板的包管理器命令的,比如vue-clivue-cli的包管理器命令是vue,会有不一样的命令;

if (customCommand) {const fullCustomCommand = customCommand.replace('TARGET_DIR', targetDir).replace(/^npm create/, `${pkgManager} create`)// Only Yarn 1.x doesn't support `@version` in the `create` command.replace('@latest', () => (isYarn1 ? '' : '@latest')).replace(/^npm exec/, () => {// Prefer `pnpm dlx` or `yarn dlx`if (pkgManager === 'pnpm') {return 'pnpm dlx'}if (pkgManager === 'yarn' && !isYarn1) {return 'yarn dlx'}// Use `npm exec` in all other cases,// including Yarn 1.x and other custom npm clients.return 'npm exec'})const [command, ...args] = fullCustomCommand.split(' ')const {status} = spawn.sync(command, args, {stdio: 'inherit'})process.exit(status ?? 0)
} 

这里的处理代码比较多,但是也没什么好看的,就是各种替换字符串,然后生成最终的命令;

正式生成项目

接下来就是重点了,首先确定模板的位置,代码如下:

const templateDir = path.resolve(fileURLToPath(import.meta.url),'../..',`template-${template}`
) 

这里的import.meta.url是当前ES模块的绝对路径,这里是一个知识点。

import大家都知道是用来导入模块的,但是import.meta是什么呢?

import.meta是一个对象,它的属性和方法提供了有关模块的信息,比如url就是当前模块的绝对路径;

同时他还允许在模块中添加自定义的属性,比如import.meta.foo = 'bar',这样就可以在模块中使用import.meta.foo了;

所以我们在vite项目中可以使用import.meta.env来获取环境变量,比如import.meta.env.MODE就是当前的模式;

点到为止,我们继续看代码,这一段就是确定模板的位置,应该都看的懂;

后面就是读取模板文件,然后生成项目了,代码如下:

const files = fs.readdirSync(templateDir)

// package.json 不需要写进去
for (const file of files.filter((f) => f !== 'package.json')) {write(file)
} 

这里的write函数就是用来生成项目的,代码如下:

const write = (file: string, content?: string) => {const targetPath = path.join(root, renameFiles[file] ?? file)if (content) {fs.writeFileSync(targetPath, content)} else {copy(path.join(templateDir, file), targetPath)}
} 

根据上面的逻辑这个分析直接简化为:

const write = (file: string) => {const targetPath = path.join(root, file)copy(path.join(templateDir, file), targetPath)
} 

这个没啥好说的,然后就到了copy函数的分析了,代码如下:

function copy(src: string, dest: string) {const stat = fs.statSync(src)if (stat.isDirectory()) {copyDir(src, dest)} else {fs.copyFileSync(src, dest)}
} 

这里的copy函数就是用来复制文件的,如果是文件夹就调用copyDir函数,代码如下:

function copyDir(srcDir: string, destDir: string) {fs.mkdirSync(destDir, { recursive: true })for (const file of fs.readdirSync(srcDir)) {const srcFile = path.resolve(srcDir, file)const destFile = path.resolve(destDir, file)copy(srcFile, destFile)}
} 

这里的fs.mkdirSync函数就是用来创建文件夹的,recursive参数表示如果父级文件夹不存在就创建父级文件夹;

这里的fs.readdirSync函数就是用来读取文件夹的,返回一个数组,数组中的每一项就是文件夹中的文件名;

最后通过递归调用copy函数来复制文件夹中的文件;

创建package.json

接下来是对package.json文件的单独处理,代码如下:

// 获取模板中的 package.json
const pkg = JSON.parse(fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8')
)

// 修改 package.json 中的 name 值
pkg.name = packageName || getProjectName()

// 写入 package.json
write('package.json', JSON.stringify(pkg, null, 2)) 

这里的pkg就是模板中的package.json文件,然后修改name字段,最后写入到项目中;

之前不复制package.json是因为这里会修改name字段,如果复制了你的项目的name属性就不正确。

完成

console.log(`\nDone. Now run:\n`)
if (root !== cwd) {console.log(`cd ${path.relative(cwd, root)}`)
}
switch (pkgManager) {case 'yarn':console.log('yarn')console.log('yarn dev')breakdefault:console.log(`${pkgManager} install`)console.log(`${pkgManager} run dev`)break
}
console.log() 

最后就是一些提示信息,如果你的项目不在当前目录下,就会提示你cd到项目目录下,然后根据你的包管理器来提示你安装依赖和启动项目。

总结

整体下来这个脚手架的实现还是比较简单的,整体非常清晰:

1.通过minimist来解析命令行参数;
2.通过prompts来交互式的获取用户输入;
3.确认用户输入的信息,整合项目信息;
4.通过nodefs模块来创建项目;
5.最后提示用户如何启动项目。

代码不多,但是整体走下来还是有很多细节的,例如:

1.以后写node项目的时候知道怎么获取命令行参数;
2.用户命令行的交互式输入,里面用户体验是非常好的,这个可以在很多地方是做为参考;
3.fs模块的使用,这个模块是node中非常重要的模块;
4.node中的path模块,这个模块也是非常重要的,很多地方都会用到;
5.import的知识点,真的学到了。

就到这里了,谢谢大家的阅读。

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/132498.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目学习--谷粒商城

1. 微服务搭建 本地安装虚拟机,静态IP划分和docker的使用微服务功能模块介绍以及业务模块划分与抽取 2. 前端搭建 3. 后端业务代码完善 云储存OSS的整合与使用JSR303校验以及自定义分组校验 4. Elasticsearch学习 Elasticsearch的基础使用Elasticsearch字段ma…

volatile 详解

1. 前言 在并发编程的过程中,volatile属性非常重要。首先我们要了解并发编程的三大特性:可见性, 有序性, 原子性而我们今天的了解的volatile 就牵扯到可见性, 有序性。同时我也会从个人了解的角度给大家分析下,如果有什么不对的地方也希望大家…

物联网与射频识别技术,课程实验(一)

目录 (1)communication.py (2)Reader.py (3)Tag.py 实验1—— EPC C1G2标准下的标签状态转换仿真 实验说明: 利用Python或Matlab模拟C1G2标签的状态转换模型; 程序应能显示标签…

深度学习:01 神经网络与激活函数

目前,最广泛使用的定义是Kohonen于1988年的描述: 神经网络是由具有适应性的简单单元组成的广泛并行互连的网络,它的组织能够模拟生物神经系统对真实世界物体所做出的交互反应。 目录 对神经网络的概述 神经网络的表示 激活函数 为什么激…

2022年总结:从初二学生到算法作者的蜕变之路

目录 一年的创作历程 我和 CSDN 在编程竞赛的合作 About CBC 技术社区的发展 夜跑奇遇 About 博客之星 新年致谢 元旦祝福 一年的创作历程 2022年,这一年对于我来说是十分重要的一年。在这一年里,我作为一名初二在校学生,在CSDN上发布…

【Linux 环境变量】环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数

1.问题:为什么自己写的程序需要加上./才能执行,但是指令可以直接使用,例:ls -al? 因为:不加"./"执行程序的时候会按环境变量PATH里面的各个路径找到就正常执行,找不到就报错&#xff…

SQL笔试题总结

文章目录前言一、列转行题目:将表Student转化为下面的形式展示先放答案逐步剖析二、row_number() over() 的使用题目:统计订单交易表(orders)每个商品交易金额最高的那一条数据先放答案逐步剖析三、逐行累加题目:还是订…

✿✿✿JavaScript --- jQuery框架二

目 录 1.高级事件 (1)浏览器一打开自动触发我们绑定的事件 (2) Trigger 传递数据 (3)自定义事件 (4)trigger 简写方案 (5)triggerHandler() (6)trigger和triggerHandler的区别 (7)on off one 2.动画 (1)显示 隐藏 动画 (2)队列动画 (3)下滑 上卷 (4)淡入淡出…

Nacos启动出现Error creating bean with name ‘memoryMonitor‘ 、‘externalDumpService‘

目录 🧡问题 🧡解决方法 💟这里是CS大白话专场,让枯燥的学习变得有趣! 💟没有对象不要怕,我们new一个出来,每天对ta说不尽情话! 💟好记性不如烂键盘&#x…

WSL2支持systemctl命令

文章目录背景相关知识systemdinit安装方法一:微软官方支持方法(推荐)方法二:安装daemonize实现参考背景 微软官方推出Windows Terminal第一时间,我就安装了这个终端软件。现在GitHub已经有86.8k星,且发布了…

一名普通Java程序员的2022的总结和2023的展望

前言今天是元旦节,也是2023年的第一天,首先祝各位亲朋好友们元旦快乐,在新的一年全家身体康健,诸事顺遂,阖家幸福,最重要的是身体健康,工作顺利,永无BUG永不加班!&#x…

计算机组成原理【1】初识硬件

目录 考点1:硬件发展———————————————————————————— 一.计算机硬件的基本组成 1.早期冯诺依曼机 (1)冯.诺依曼计算机的特点: 2.现代计算机的结构 3.总结图 二.各个硬件的工作原理 1.寄存器MAR,MDR 2.主存…

EMNLP22 外部知识注入:Prompt-Learning for Short Text Classification

Prompt-Learning for Short Text Classification 任务形式:短文本分类问题,但是短文本的短长度、特征稀疏性和高模糊性给分类任务带来了巨大挑战。 1以往的工作,在注入外部信息上 大多数提示学习方法手动扩展标签词或仅考虑类别名称以纳入…

得分_UVa1585分子量_UVa1586数数字_UVa1225周期串_UVa455子序列_UVa10340

目录 P57_习题3-1_得分_UVa1585 P57_习题3-2_分子量_UVa1586 原子数范围0~99 书上给的代码 P57_习题3-3_数数字_UVa1225 P57_习题3-4_周期串_UVa455 P59_习题3-9_子序列_UVa10340 P57_习题3-1_得分_UVa1585 给出一个由O和X组成的串(长度为1~80&a…

网络原理2 TCP协议

TCP协议 文章目录TCP协议TCP的特点TCP的基本特性确认应答机制超时重传机制丢包连接管理机制TCP建立连接---三次握手TCP断开连接---四次挥手滑动窗口机制丢包问题流量控制机制拥塞控制机制延迟应答机制捎带应答机制面向字节流问题TCP中的异常处理程序崩溃了正常关机突然断电关机…

在前端解决跨域

1、环境依赖 C:\Users\cyberzhaohyvm>node -v v14.17.3 C:\Users\cyberzhaohyvm>vue -V vue/cli 5.0.4 2、在项目所在目录,安装axios 进入项目所在目录: D:\01sourcecode\10Tutorial\08Vue\17-2022-12-28-v2\elementui-demo npm install axios …

Redis单线程为什么这么快?

Redis单线程为什么这么快? 第一章 Redis单线程为什么这么快 Redis深度剖析【第一章】Redis单线程为什么这么快?前言一、Redis为什么要使用单线程,而不是多线程?单线程的优势如果Redis使用多线程:既然多线程切换存在消…

【博学谷学习记录】大数据课程-学习第一周总结

Linux服务器 对于Linux操作系统来说,其本身是一个整体,包括Linux内核、系统库和系统程序,Linux内核是其最基础的部分,它实现了对硬件资源的管理,并且提供了使用这些硬件资源的通用接口。 自1991年发布Linux内核来&…

项目实战之旅游网(十四)项目部署-Docker

为了节约资源,在生产环境中我们更多的是使用Docker容器部署SpringBoot应用, 我们要用maven里的docker插件来生成镜像并且远程连接Docker, 开启远程docker服务: # 修改docker配置文件 vim /lib/systemd/system/docker.service 把…

简阅人体姿态估计深度学习方法-simpread-Human Pose Estimation Deep Learning Approach

What is Human Pose Estimation? Human Pose Estimation (HPE) is a way of identifying and classifying the joints in the human body Human Pose Estimation(HPR 人体姿态估计)是一个对人体关节进行识别和分类的方法。 Essentially it is a way to capture a set of co…