nuxt3拆包剖析——开发一个属于你的脚手架
前言
用过nuxt
的朋友都知道,在scripts
中的dev
指令,用的是nuxt dev
我们探究一下nuxt
是如何构建他们的脚手架的
小贴士,nuxt工程模板中的nuxt指令,还有个别名:nuxi,这两个指令指向相同的代码,所以我们也可以执行nuxi dev,是同样的效果
nuxt源码:
可以看到,其实指向的是同一个文件
阅读本文,你能学到:
- 脚手架的基本知识和本地调试
- 使用cac搭建脚手架
- 参考nuxt搭建脚手架
脚手架的基本知识和本地调试
脚手架的概念我就不赘述了,简单来说就是封装一些脚本,安装了之后你运行命令,就会执行某些任务
常见的比如说我们常用vue create
创建vue工程,create-react-app
创建react工程,vite
启动开发服务器,webpack
进行工程打包
我们首先创建工程,我一般是用我fork的模板工程创建一个starter-ts
,链接
克隆下来install之后,我们创建src/nuxi/nuxi.ts
和src/cac/cac.ts
两个文件
然后输入以下内容
nuxi.ts
#!/usr/bin/env node
console.log('nuxt')
cac.ts
#!/usr/bin/env node
console.log('cac')
配置打包配置build.config.ts
,添加这两个入口
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
+ 'src/cac/cac',
+ 'src/nuxi/nuxi',
],
declaration: true,
clean: true,
rollup: {
emitCJS: true,
},
})
执行pnpm build
,可以看到这两个文件都进行了打包
接下来配置package.json
,新增bin
属性,我这里是为了区分nuxi
而命名成wnuxi
{
// ...
"bin": {
"wnuxi": "./dist/nuxi.mjs",
"cac": "./dist/cac.mjs"
},
// ...
}
在控制台中执行npm link
,就能在本地执行bin
中设定的两个指令了
使用cac搭建脚手架
在讲解参考nuxt搭建的脚手架前,我认为有必要先讲解下目前市面上常用的脚手架搭建工具的使用过程,用作对比,而我本人用过commander
和cac
,认为cac
会更顺手一些,所以这里就讲解下cac
的使用过程
首先当然是安装pnpm add cac
然后修改cac.ts
#!/usr/bin/env node
import cac from 'cac'
import { version } from '../../package.json'
const cli = cac('cac')
cli.command('dev', 'dev mode')
.option('-p,--port', 'port')
.action((value) => {
console.log('im cac, dev mode, value: ', value)
})
cli.command('build', 'build mode')
.action(() => {
console.log('im cac, build mode')
})
cli.version(version)
cli.help()
cli.parse()
然后按以下执行
看出来还是很方便使用的,难怪可以有这么高的下载量
参考nuxt搭建脚手架
我们先看下完整的nuxi
的主要执行逻辑,我加上了注释
async function _main () {
const _argv = (process.env.__CLI_ARGV__ ? JSON.parse(process.env.__CLI_ARGV__) : process.argv).slice(2) // 读取参数
const args = mri(_argv, {
boolean: [
'no-clear'
]
})
const command = args._.shift() || 'usage' // 确定执行命令
showBanner(command === 'dev' && args.clear !== false && !args.help) // 展示版本
if (!(command in commands)) { // 没有命令错误处理
console.log('\n' + red('Invalid command ' + command))
await commands.usage().then(r => r.invoke())
process.exit(1)
}
// Check Node.js version in background
setTimeout(() => { checkEngines().catch(() => {}) }, 1000) // 检查node版本
const cmd = await commands[command as Command]() as NuxtCommand // 获取命令对应的执行逻辑
if (args.h || args.help) {
showHelp(cmd.meta) // 如果是--help,则展示使用方法
} else {
const result = await cmd.invoke(args) // 开始执行命令
return result
}
}
nuxt
选择自己去控制脚手架执行逻辑,而不是使用市面上封装的工具,应该是为了控制打包后体积的尽可能小
我们一步步地复刻上面的功能
读取参数
nuxt
使用了一个工具mri
来解析参数,mri文档
我们pnpm add mri
安装后初步使用下,改写nuxi.ts
#!/usr/bin/env node
import mri from 'mri'
// 读取参数
const args = mri(process.argv.slice(2))
console.log(args)
执行结果如下
展示版本
nuxt
封装了showBanner
方法用于展示当前使用nuxi
版本,并且会根据参数clear
来控制是否清空控制台
import clear from 'clear'
import { bold, gray, green } from 'colorette'
export function showBanner (_clear?: boolean) {
if (_clear) { clear() }
console.log(gray(`Nuxi ${(bold(version))}`))
}
这里用到了一个工具包clear
,clear文档,功能就是尽可能的清空控制台Clear the terminal screen if possible
同时,为了输出效果好看,使用了工具包colorette
,colorette文档
我们也来引用这两个工具包,改写nuxi.ts
#!/usr/bin/env node
import mri from 'mri'
import clear from 'clear'
import { bold, gray } from 'colorette'
// 读取参数
const args = mri(process.argv.slice(2))
console.log(args)
// 获取命令,如果没有传入,则默认展示使用方法
const command = args._.shift() || 'usage'
function showBanner(_clear?: boolean) {
if (_clear)
clear()
console.log(gray(`WNuxi ${bold('1.0.0')}`))
}
// 展示版本
showBanner(command === 'dev' && args.clear)
我们执行命令wnuxi dev --clear
即可清空控制台并展示版本
检查node版本
nuxi
执行前都会检查当前node版本是否符合要求,如果不符合,应该提前提示用户升级node以顺利进行后续的操作
这里用到了一个工具包semver
,semver文档,改写nuxi.ts
#!/usr/bin/env node
import mri from 'mri'
import clear from 'clear'
import { bold, gray, redBright } from 'colorette'
import { satisfies } from 'semver'
// 读取参数
const args = mri(process.argv.slice(2))
console.log(args)
// 获取命令,如果没有传入,则默认展示使用方法
const command = args._.shift() || 'usage'
function showBanner(_clear?: boolean) {
if (_clear)
clear()
console.log(gray(`WNuxi ${bold('1.0.0')}`))
}
// 展示版本
showBanner(command === 'dev' && args.clear)
function checkEngines() {
const currentNode = process.versions.node
const nodeRange = '^14.18.0 || >=20.10.0' // 这里是控制node范围
if (!satisfies(currentNode, nodeRange))
console.warn(redBright(`Current version of Node.js (\`${currentNode}\`) is unsupported and might cause issues.\n Please upgrade to a compatible version (${nodeRange}).`))
}
// 控制node版本
checkEngines()
这里有两个注意点:
- 我当前是node18,这里我为了展示效果,把版本控制在大于node20,正常其实大于node16即可
- node的范围可以写在
package.json
中的engines
属性,这样会比较规范
运行下看看效果
执行命令
我们新建目录commands
,新建文件index.ts
dev.ts
build.ts
usage.ts
index.ts
import type { Argv } from 'mri'
import { cyan, magenta } from 'colorette'
const _rDefault = (r: any) => r.default || r
const lazyImport = (path: string) => () => import(path).then(_rDefault)
// 命令的集合
export const commands = {
dev: lazyImport('./dev'),
build: lazyImport('./build'),
usage: lazyImport('./usage'),
}
export type Command = keyof typeof commands
export interface NuxtCommandMeta {
name: string
usage: string
description: string
[key: string]: any
}
export interface NuxtCommand {
meta: NuxtCommandMeta
invoke(args: Argv, options?: Record<string, any>): Promise<void> | void
}
// ts处理的构建命令方法
export function defineNuxtCommand(command: NuxtCommand): NuxtCommand {
return command
}
// 展示帮助信息
export function showHelp(meta?: Partial<NuxtCommandMeta>) {
const sections: string[] = []
if (meta) {
if (meta.usage) {
sections.push(`${magenta('> ')} Usage: ${cyan(meta.usage)}`)
}
if (meta.description) {
sections.push(magenta('⋮ ') + meta.description)
}
}
sections.push(`Use ${cyan('npx wnuxi [command] --help')} to see help for each command`)
console.log(`${sections.join('\n\n')}\n`)
}
index.ts
作为commands
目录的入口,提供了构建命令的ts方法,能在构建命令过程有友好的ts提示,同时收集并到处所有命令方法
dev.ts
import { defineNuxtCommand } from '.'
export default defineNuxtCommand({
meta: {
name: 'dev',
usage: 'npx wnuxi dev [--clear] [--port, -p]',
description: 'Run development server',
},
invoke() {
console.log('运行dev逻辑')
},
})
这是命令wnuxi dev
后执行的逻辑
build.ts
import { defineNuxtCommand } from '.'
export default defineNuxtCommand({
meta: {
name: 'build',
usage: 'npx wnuxi build [--minify]',
description: 'Build production deployment',
},
invoke() {
console.log('运行build逻辑')
},
})
这是命令wnuxi dev
后执行的逻辑
usage.ts
import { cyan } from 'colorette'
import { commands, defineNuxtCommand, showHelp } from '.'
export default defineNuxtCommand({
meta: {
name: 'help',
usage: 'nuxt help',
description: 'Show help',
},
invoke(_args) {
const sections: string[] = []
sections.push(`Usage: ${cyan(`npx nuxi ${Object.keys(commands).join('|')} [args]`)}`)
console.log(`${sections.join('\n\n')}\n`)
// Reuse the same wording as in `-h` commands
showHelp({})
},
})
这是命令wnuxi [--help, -h]
后执行的逻辑
以下是完整的nuxi.ts
内容
#!/usr/bin/env node
import mri from 'mri'
import clear from 'clear'
import { bold, gray, redBright } from 'colorette'
import { satisfies } from 'semver'
import { type Command, type NuxtCommand, commands, showHelp } from './commands'
async function main() {
// 读取参数
const args = mri(process.argv.slice(2))
console.log(args)
// 获取命令,如果没有传入,则默认展示使用方法
const command = args._.shift() || 'usage'
function showBanner(_clear?: boolean) {
if (_clear)
clear()
console.log(gray(`WNuxi ${bold('1.0.0')}`))
}
// 展示版本
showBanner(command === 'dev' && args.clear)
function checkEngines() {
const currentNode = process.versions.node
const nodeRange = '^14.18.0 || >=16.10.0' // 这里是控制node范围
if (!satisfies(currentNode, nodeRange))
console.warn(redBright(`Current version of Node.js (\`${currentNode}\`) is unsupported and might cause issues.\n Please upgrade to a compatible version (${nodeRange}).`))
}
// 控制node版本
checkEngines()
// 获取命令实例
const cmd = await commands[command as Command]() as NuxtCommand
if (args.h || args.help) {
showHelp(cmd.meta)
} else {
await cmd.invoke(args) // 执行命令方法
}
}
main()
接下来我们测试一下
总结
本文首先搭建了一个简单的脚手架工程,并且分别构建了两种脚手架模式,一种是使用cac
构建,一种是参考nuxi
的方式构建,其中详细讲解了参考nuxi
的方式构建,穿插介绍了会使用到的工具包
转载自:https://juejin.cn/post/7248145094599376957