likes
comments
collection
share

nuxt3拆包剖析——开发一个属于你的脚手架

作者站长头像
站长
· 阅读数 13

前言

用过nuxt的朋友都知道,在scripts中的dev指令,用的是nuxt dev

nuxt3拆包剖析——开发一个属于你的脚手架

我们探究一下nuxt是如何构建他们的脚手架的

小贴士,nuxt工程模板中的nuxt指令,还有个别名:nuxi,这两个指令指向相同的代码,所以我们也可以执行nuxi dev,是同样的效果

nuxt源码:

nuxt3拆包剖析——开发一个属于你的脚手架

可以看到,其实指向的是同一个文件

阅读本文,你能学到:

  • 脚手架的基本知识和本地调试
  • 使用cac搭建脚手架
  • 参考nuxt搭建脚手架

脚手架的基本知识和本地调试

脚手架的概念我就不赘述了,简单来说就是封装一些脚本,安装了之后你运行命令,就会执行某些任务

常见的比如说我们常用vue create创建vue工程,create-react-app创建react工程,vite启动开发服务器,webpack进行工程打包

我们首先创建工程,我一般是用我fork的模板工程创建一个starter-ts链接

nuxt3拆包剖析——开发一个属于你的脚手架

克隆下来install之后,我们创建src/nuxi/nuxi.tssrc/cac/cac.ts两个文件

nuxt3拆包剖析——开发一个属于你的脚手架

然后输入以下内容

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,可以看到这两个文件都进行了打包

nuxt3拆包剖析——开发一个属于你的脚手架

接下来配置package.json,新增bin属性,我这里是为了区分nuxi而命名成wnuxi

{
    // ...
    "bin": {
        "wnuxi": "./dist/nuxi.mjs",
        "cac": "./dist/cac.mjs"
    },
    // ...
}

在控制台中执行npm link,就能在本地执行bin中设定的两个指令了

nuxt3拆包剖析——开发一个属于你的脚手架

使用cac搭建脚手架

在讲解参考nuxt搭建的脚手架前,我认为有必要先讲解下目前市面上常用的脚手架搭建工具的使用过程,用作对比,而我本人用过commandercac,认为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()

然后按以下执行

nuxt3拆包剖析——开发一个属于你的脚手架

看出来还是很方便使用的,难怪可以有这么高的下载量

参考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)

执行结果如下

nuxt3拆包剖析——开发一个属于你的脚手架

展示版本

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))}`))
}

这里用到了一个工具包clearclear文档,功能就是尽可能的清空控制台Clear the terminal screen if possible

同时,为了输出效果好看,使用了工具包colorettecolorette文档

我们也来引用这两个工具包,改写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即可清空控制台并展示版本

nuxt3拆包剖析——开发一个属于你的脚手架

检查node版本

nuxi执行前都会检查当前node版本是否符合要求,如果不符合,应该提前提示用户升级node以顺利进行后续的操作

这里用到了一个工具包semversemver文档,改写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属性,这样会比较规范

运行下看看效果

nuxt3拆包剖析——开发一个属于你的脚手架

执行命令

我们新建目录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()

接下来我们测试一下

nuxt3拆包剖析——开发一个属于你的脚手架

总结

本文首先搭建了一个简单的脚手架工程,并且分别构建了两种脚手架模式,一种是使用cac构建,一种是参考nuxi的方式构建,其中详细讲解了参考nuxi的方式构建,穿插介绍了会使用到的工具包

本文完整工程

转载自:https://juejin.cn/post/7248145094599376957
评论
请登录