自主搭建5个精品脚手架,玩转前端提效-重磅首发

#1

download:自主搭建5个精品脚手架,玩转前端提效-重磅首发

create-vue的实现原理
源代码
既然命令vue@x是通过npm init执行的,那么npm init做了什么?它怎么知道有vue@x这个命令呢?
这是一个性的问题。先解决吧。
1.npm init vue@x
第一个问题,npm init做了什么?从npm官网对npm init的描述可以知道,执行npm init的时候会转换这个命令。
NPM init[-force |-f |-yes |-y |-scope]
npm init(与“npx /create”相同)
npm init [/](与npx [/]create-)相同)
复制代码
换句话说

npm init foo -> npx create-foo
NPM init @ usr/foo-> npx @ usr/create-foo
npm init @usr -> npx @usr/create

那么结论就是npm init vue@x实际执行的是npx create-vue@x,@后面的参数就是vue的对应版本。核心是npx create-vue,create-vue是真正执行的命令。
知道了create-vue是真正的命令,那么第二个问题就变成了,npx怎么知道有create-vue?
先说结论:npx会在node_modules/的目录下检查命令。bin/和环境变量$PATH。如果它找不到命令,就会远程下载同名的依赖项,通过Node执行。

这个结论是看了阮一峰的npx教程后得出的。后面的【扩展】里也引用了这篇文章。

这对应于node_modules/。bin/create-vue。
看到这里,我以为package.json中bin字段定义的命令是在全局或者局部node_modules/中注册的。bin/目录。
create-vue命令确实是在这个项目的package.json的bin字段中定义的。
至此,npm init做过的事情都明白了,可以继续往下看了。

这里其实还有一个问题。@之后的版本号是怎么生效的?
执行npx create-vue@2时,npm会认为下载的大版本是2的最新版本,@3表示大版本是3的最新版本。

2.命令的条目文件
我知道create-vue命令的定义。
// package.json
" bin": {
" create-vue": "outfile.cjs "
}
复制代码
但是项目里没有找到outfile.cjs这个文件,然后我就想如果会不会是打包的文件,就全局搜索了一下。果然在scripts/build.mjs文件中有一个定义,输出文件是outfile.cjs,对应的导入文件是index.ts
等待esbuild.build({
捆绑:真的,
入口点:[‘index.ts’],
outfile: 'outfile.cjs ',
格式:“cjs”,
平台:“节点”,
目标:“节点14”,

})
复制代码

问题出现了:cjs是什么文件?
Cj代表commonJs,mjs代表ModuleJs。
参考文件:有什么区别。js和。mjs文件?

知道了命令的入口文件,我们来看看这个命令做了什么,是如何实现的。
3.命令的执行
index.ts文件中定义了五个函数,其核心是在init()函数中实现的,所以先以init()函数为起点,后面再看其他函数的具体实现。
3.1.初始化变量

首先通过process.cwd()获取当前工作目录,然后通过minimist获取命令行参数,类似于执行npm init vue@3 - ts。
//可能的选项:
// -默认值
// - typescript / - ts
// - jsx
// -路由器/-vue-路由器
// -皮尼亚
// - with-tests / - tests(等于- vitest - cypress)
// - vitest
// -柏树
// - eslint
//-eslint-with-beauty(为简单起见,仅通过eslint支持beauty)
// - force(用于强制覆盖)
const argv = minimixt(process . argv . slice(2),{
//这里包含了所有参数对应的别名
别名:{
typescript: [‘ts’],
with-tests’: [‘tests’],
路由器:[‘vue-router’]
},
//所有参数都被视为布尔值
布尔值:真
})
复制代码
process.argv得到的是一个数组,第一个元素是启动Node.js进程的可执行文件所在的绝对路径;第二个元素是当前执行的js文件的绝对路径;第三和随后的元素是参数。

通过demo看一下minimist的参数和返回值。
$ node example/minimal it . js-a beep-b boop
{ _: [],a:‘哔’,b: ‘boop’ }

$ node example/minimal it . js-x 3-y 4-n5-ABC-beep = boop foo bar baz
{ _: [ ‘foo ‘,’ bar ‘,’ baz’ ],
x: 3,
y: 4,
n: 5,
答:没错,
是的,
列车员:是的,
哔声:“boop”
}

定义了isFeatureFlagsUsed变量,如果已经在命令行中配置了该变量,则该变量用于跳过用户选择的以下步骤(提示的功能)。
const isFeatureFlagsUsed =
类型(
argv.default??
argv.ts??
argv.jsx??
argv.router??
阿根廷山羊??
argv.tests??
argv.vitest??
argv.cypress??
argv.eslint
)=== ‘布尔型’
复制代码

??函数:当左操作数为空或未定义时,返回右操作数,否则返回左操作数。

尝试从命令行获取项目名称。如果不存在,将采用默认的项目名称。
设targetDir = argv。_[0]
const defaultProjectName =!targetDir?vue-project ':目标目录
复制代码

这里需要注意的是argv的前两个元素。_和process.argv默认是一样的,但是在这里定义argv的时候是通过process.argv.slice(2)得到的,得到的结果是不一样的。

最后,定义forceOverwrite变量来决定是否强制覆盖和清空目录。
const force overwrite = argv . force//NPM init vue @ 3-force
复制代码

3.2.获取用户配置

首先,定义变量result并声明其类型。其次,它通过提示与用户进行交互。
结果=等待提示(
[
{
名称:“项目名称”,
类型:targetDir?空:“文本”,
消息:“项目名称:”,
initial: defaultProjectName,
onState:(state)= >(targetDir = String(state . value)。trim() || defaultProjectName)
},
{
名称:“应覆盖”,
type:()= >(canskip清空(targetDir) || forceOverwrite?空:“确认”),
消息:()=> {
const dirForPrompt =
targetDir === ‘,’?当前目录“:目标目录' $ { targetDir }” 1

返回“${ dirForPrompt }不为空。删除现有文件并继续吗?`
}
}

]
)
复制代码
下面是执行npm init vue@x命令后对用户的一些查询,包括项目名称,是否重写目录,是否引入vue-router/jsx等。具体内容在文章开头的图片中有描述。

这里有一个问题,就是为什么prompts的配置里有两个projectName的配置,因为这个是非阻塞的,后面会解决。

最后,将上述命令行中获得的参数与用户选择的参数相结合,并存储在相应的变量中。
常数{
项目名称,
packageName = projectName??defaultProjectName,
shouldOverwrite = argv.force,
needsJsx = argv.jsx,
needsTypeScript = argv . typescript,
needsRouter = argv.router,
needsPinia = argv.pinia,
needsCypress = argv . cypress | | argv . tests,
needs vitest = argv . vitest | | argv . tests,
needsEslint = argv . eslint | | argv[’ eslint-with-beautiful ‘],
needs prettier = argv[’ eslint-with-prettle ']
} =结果
const needsCypress CT = needsCypress & &!需求测试
复制代码

3.2.创建项目目录或清空现有目录以创建项目。
首先,将当前工作目录和项目名称合并到项目的根目录中。
后来如果目录存在,需要重写,就重写;否则,如果该目录不存在,将会创建它。
让我们来看看如何清空目录。该条目是emptyDir函数。
函数emptyDir(dir) {
//如果不存在则返回
如果(!fs.existsSync(dir)) {
返回
}

postOrderDirectoryTraverse(
dir,
// fs.rmdirSync()只能删除空文件夹。
(dir) => fs.rmdirSync(dir),
// fs.unlinkSync()只能删除文件或符号链接。
(file) => fs.unlinkSync(file)
)
}
复制代码
这里再次调用postOrderDirectoryTraverse函数,删除文件夹和文件的回调函数被定义为参数。该功能的具体实现如下
导出函数postOrderDirectoryTraverse(dir,dirCallback,fileCallback) {
// fs.readdirSync():读取该目录下的所有文件和目录。
for(fs . readdirsync(dir)的常量文件名){
// .git文件跳过
if(文件名=== ‘。git’) {
继续
}

const fullpath = path.resolve(目录,文件名)
// fs.lstatSync():返回文件或目录的信息。
// fs.lstatSync()。isDirectory():判断是否是目录,如果是,判断真假。
if (fs.lstatSync(fullpath))。isDirectory()) {
//对于目录的操作:递归调用
postOrderDirectoryTraverse(完整路径,目录回调,文件回调)

//删除这个目录,fs.rmdirSync()只能删除一个空文件夹,所以最上面递归先删除最下面的文件夹。
目录回调(完整路径)
继续
}
//如果是文件,则删除该文件。
fileCallback(完整路径)
}
}
复制代码

目录中的所有文件都通过fs.readdirSync()遍历,所有操作都在一个循环中进行。
然后判断如果是git文件,就跳过这个处理。
然后获取遍历项的绝对目录,判断该项是文件还是目录。如果是文件,则删除文件,如果是目录,则采用深度优先,逐步删除。

3.3.创建package.json
const pkg = { name: packageName,version: ‘0.0.0’ }
fs . write file sync(path . resolve(root,’ package.json '),JSON.stringify(pkg,null,2))
复制代码
生成基本的package.json文件。
3.4.定义模板文件渲染函数,生成项目基本文件。
const render =函数render(templateName) {
const templateDir = path . resolve(template root,templateName)
renderTemplate(templateDir,root)
}
复制代码

获取模板文件目录
为核心操作调用renderTemplate。这里你需要知道的就是渲染模板,具体实现在后面。
根据用户配置生成基本档案。