Skip to content

VUE脚手架create-vue

体验一下快如闪电的全新VUE脚手架工具:

An image

一、初始化 VUE3 项目

js
$ npm create vue@3

二、初始化 VUE2 项目

js
$ npm create vue@2

三、与 Vue CLI 的区别

Vue CLI基于webpack,而create Vue基于Vite。Vite支持Vue CLI项目中的大多数配置约定,并且由于其极快的启动和热模块更换速度,提供了更好的开发体验。点击此处了解更多关于我们推荐Vite over webpack的原因。

与Vue CLI不同,create Vue本身只是一个脚手架工具:它根据您选择的功能创建一个预配置的项目,并将其余的委托给Vite。以这种方式构建的项目可以直接利用与Rollup兼容的Vite插件生态系统。

四、源码分析

从上面的截图可以看出,创建项目主要是下面三个步骤:

html
1. 输入项目名称,默认值是 vue-project
2. 询问一些配置 渲染模板等
3. 完成创建项目,输出运行提示
ts
#!/usr/bin/env node
// ...
function isValidPackageName(projectName) {
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName)
}

function toValidPackageName(projectName) {
  // ...
}

function canSkipEmptying(dir: string) {
  // ...
}
// 工具类:清空目录文件夹(创建VUE项目时,目录中如果有同名目录文件夹时,提示用户是否覆盖已有文件夹)
function emptyDir(dir) {
  // ...
}

// 主入口函数
async function init() {
  // ...
}

// async 函数返回的是Promise 可以用 catch 报错
init().catch((e) => {
  console.error(e)
})

4.1 解析命令行的参数

ts
// 返回运行当前脚本的工作目录的路径
const cwd = process.cwd()
// possible options:
// --default
// --typescript / --ts
// --jsx
// --router / --vue-router
// --pinia
// --with-tests / --tests (equals to `--vitest --cypress`)
// --vitest
// --cypress
// --playwright
// --eslint
// --eslint-with-prettier (only support prettier through eslint for simplicity)
// --force (for force overwriting)
// minimist: 轻量级的命令行参数解析引擎
const argv = minimist(process.argv.slice(2), {
  alias: {
    typescript: ['ts'],
    'with-tests': ['tests'],
    router: ['vue-router']
  },
  // all arguments are treated as booleans
  boolean: true
})

备注:minimist的传参例子

js
// 1. 新建 parseTest.js 文件
var args = require('minimist')(process.argv.slice(2));
console.log(args);

// 2. DEMO-1
$ node parseTest.js -a 123 -b 456                   // { _: [], a: 123, b: 456 }

// 3. DEMO-2
$ node parseTest.js -a 123 -b 456 -c=789 bobo SZ    // { _: [ 'bobo', 'SZ' ], a: 123, b: 456, c: 789 }

4.2 跳过第二步:询问一些配置 渲染模板等

ts
// if any of the feature flags is set, we would skip the feature prompts
const isFeatureFlagsUsed =
  typeof (
    argv.default ??
    argv.ts ??
    argv.jsx ??
    argv.router ??
    argv.pinia ??
    argv.tests ??
    argv.vitest ??
    argv.cypress ??
    argv.playwright ??
    argv.eslint
  ) === 'boolean'

4.3 步骤一:设置项目名称和保存个性化定制结果

ts
// 这里参考「minimist的传参例子 - “DEMO-2”」
let targetDir = argv._[0]
// 如果没有设置项目名称,则默认为 “vue-project”
const defaultProjectName = !targetDir ? 'vue-project' : targetDir
// 重写文件夹
const forceOverwrite = argv.force
// 记录询问结果:用于用户个性化定制项目
let result: {
  projectName?: string
  shouldOverwrite?: boolean
  packageName?: string
  needsTypeScript?: boolean
  needsJsx?: boolean
  needsRouter?: boolean
  needsPinia?: boolean
  needsVitest?: boolean
  needsE2eTesting?: false | 'cypress' | 'playwright'
  needsEslint?: boolean
  needsPrettier?: boolean
} = {}

4.4 步骤二:询问一些配置

ts
try {
  // Prompts:
  // - Project name:
  //   - whether to overwrite the existing directory or not?
  //   - enter a valid package name for package.json
  // - Project language: JavaScript / TypeScript
  // - Add JSX Support?
  // - Install Vue Router for SPA development?
  // - Install Pinia for state management?
  // - Add Cypress for testing?
  // - Add Playwright for end-to-end testing?
  // - Add ESLint for code quality?
  // - Add Prettier for code formatting?
  result = await prompts(
    [
      // 询问输入「项目名称」
      {
        name: 'projectName',
        type: targetDir ? null : 'text',
        message: 'Project name:',
        initial: defaultProjectName,
        onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)
      },
      // 询问当前目录如果有同名文件夹,是否覆盖?
      {
        name: 'shouldOverwrite',
        type: () => (canSkipEmptying(targetDir) || forceOverwrite ? null : 'confirm'),
        message: () => {
          const dirForPrompt =
            targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`

          return `${dirForPrompt} is not empty. Remove existing files and continue?`
        }
      },
      // 如果不覆盖,则抛出错误
      {
        name: 'overwriteChecker',
        type: (prev, values) => {
          if (values.shouldOverwrite === false) {
            throw new Error(red('✖') + ' Operation cancelled')
          }
          return null
        }
      },
      {
        name: 'packageName',
        type: () => (isValidPackageName(targetDir) ? null : 'text'),
        message: 'Package name:',
        initial: () => toValidPackageName(targetDir),
        validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name'
      },
      // 询问是否需要:TypeScript / JSX / VueRouter / Pinia / Eslint 等
      {
        name: 'needsTypeScript',
        type: () => (isFeatureFlagsUsed ? null : 'toggle'),
        message: 'Add TypeScript?',
        initial: false,
        active: 'Yes',
        inactive: 'No'
      },
      //  ... 其他省略
    ],
    {
      onCancel: () => {
        throw new Error(red('✖') + ' Operation cancelled')
      }
    }
  )
} catch (cancelled) {
  console.log(cancelled.message)
  process.exit(1)
}

4.5 组合询问结果以及默认值

js
// `initial` won't take effect if the prompt type is null
// so we still have to assign the default values here
const {
  projectName,
  packageName = projectName ?? defaultProjectName,
  shouldOverwrite = argv.force,
  needsJsx = argv.jsx,
  needsTypeScript = argv.typescript,
  needsRouter = argv.router,
  needsPinia = argv.pinia,
  needsVitest = argv.vitest || argv.tests,
  needsEslint = argv.eslint || argv['eslint-with-prettier'],
  needsPrettier = argv['eslint-with-prettier']
} = result

const { needsE2eTesting } = result
const needsCypress = argv.cypress || argv.tests || needsE2eTesting === 'cypress'
const needsCypressCT = needsCypress && !needsVitest
const needsPlaywright = argv.playwright || needsE2eTesting === 'playwright'

const root = path.join(cwd, targetDir)

// 重写清空文件夹 OR 创建文件夹
if (fs.existsSync(root) && shouldOverwrite) {
  emptyDir(root)
} else if (!fs.existsSync(root)) {
  fs.mkdirSync(root)
}
console.log(`\nScaffolding project in ${root}...`)

// 利用「fs.writeFileSync」生成 package.json 文件
const pkg = { name: packageName, version: '0.0.0' }
fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))

4.6 根据「4.5」中的组合结果,渲染「定制化模版」

ts
// todo:
// work around the esbuild issue that `import.meta.url` cannot be correctly transpiled
// when bundling for node and the format is cjs
// const templateRoot = new URL('./template', import.meta.url).pathname
const templateRoot = path.resolve(__dirname, 'template')
const render = function render(templateName) {
  const templateDir = path.resolve(templateRoot, templateName)
  renderTemplate(templateDir, root)
}

// Render base template
render('base')

// Add configs.
if (needsJsx) {
  render('config/jsx')
}
if (needsRouter) {
  render('config/router')
}
if (needsPinia) {
  render('config/pinia')
}
if (needsVitest) {
  render('config/vitest')
}
if (needsCypress) {
  render('config/cypress')
}
if (needsCypressCT) {
  render('config/cypress-ct')
}
if (needsPlaywright) {
  render('config/playwright')
}
if (needsTypeScript) {
  render('config/typescript')

  // Render tsconfigs
  render('tsconfig/base')
  if (needsCypress) {
    render('tsconfig/cypress')
  }
  if (needsCypressCT) {
    render('tsconfig/cypress-ct')
  }
  if (needsVitest) {
    render('tsconfig/vitest')
  }
}

// Render ESLint config
if (needsEslint) {
  renderEslint(root, { needsTypeScript, needsCypress, needsCypressCT, needsPrettier })
}

// Render code template.
// prettier-ignore
const codeTemplate =
  (needsTypeScript ? 'typescript-' : '') +
  (needsRouter ? 'router' : 'default')
render(`code/${codeTemplate}`)

// Render entry file (main.js/ts).
if (needsPinia && needsRouter) {
  render('entry/router-and-pinia')
} else if (needsPinia) {
  render('entry/pinia')
} else if (needsRouter) {
  render('entry/router')
} else {
  render('entry/default')
}

4.7 如果选择了Typescript,则对 .js 文件和相关配置进行二次修改

重命名所有的 .js 文件改成 .ts。 重命名 jsconfig.json 文件为 tsconfig.json 文件等。

js
// We try to share as many files between TypeScript and JavaScript as possible.
// If that's not possible, we put `.ts` version alongside the `.js` one in the templates.
// So after all the templates are rendered, we need to clean up the redundant files.
// (Currently it's only `cypress/plugin/index.ts`, but we might add more in the future.)
// (Or, we might completely get rid of the plugins folder as Cypress 10 supports `cypress.config.ts`)

if (needsTypeScript) {
  // Convert the JavaScript template to the TypeScript
  // Check all the remaining `.js` files:
  //   - If the corresponding TypeScript version already exists, remove the `.js` version.
  //   - Otherwise, rename the `.js` file to `.ts`
  // Remove `jsconfig.json`, because we already have tsconfig.json
  // `jsconfig.json` is not reused, because we use solution-style `tsconfig`s, which are much more complicated.
  preOrderDirectoryTraverse(
    root,
    () => {},
    (filepath) => {
      if (filepath.endsWith('.js')) {
        const tsFilePath = filepath.replace(/\.js$/, '.ts')
        if (fs.existsSync(tsFilePath)) {
          fs.unlinkSync(filepath)
        } else {
          fs.renameSync(filepath, tsFilePath)
        }
      } else if (path.basename(filepath) === 'jsconfig.json') {
        fs.unlinkSync(filepath)
      }
    }
  )

  // Rename entry in `index.html`
  const indexHtmlPath = path.resolve(root, 'index.html')
  const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf8')
  fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
} else {
  // Remove all the remaining `.ts` files
  preOrderDirectoryTraverse(
    root,
    () => {},
    (filepath) => {
      if (filepath.endsWith('.ts')) {
        fs.unlinkSync(filepath)
      }
    }
  )
}

4.8 创建 README.md 文件

ts
// Instructions:
// Supported package managers: pnpm > yarn > npm
const userAgent = process.env.npm_config_user_agent ?? ''
const packageManager = /pnpm/.test(userAgent) ? 'pnpm' : /yarn/.test(userAgent) ? 'yarn' : 'npm'

// README generation
fs.writeFileSync(
  path.resolve(root, 'README.md'),
  generateReadme({
    projectName: result.projectName ?? result.packageName ?? defaultProjectName,
    packageManager,
    needsTypeScript,
    needsVitest,
    needsCypress,
    needsPlaywright,
    needsCypressCT,
    needsEslint
  })
)

4.9 utils工具类解析

TODO...

源码中的第一行「#!/usr/bin/env node」 是什么?

#! 在Linux或者Unix中叫做:shebang,就是一个标识。

带有 #! 就是代表此文件可以当做脚本运行。

/usr/bin/env node这行的意思就是用node来执行此文件,其中「 /usr/bin/env 」表示node的路径。

html
<!-- #! 的几点要求 -->
#! 必须连接在一起
#! 一句必须在文件的最开始,第一行
# 开头的语句一般情况下会被当成注释而忽略,所以Shebang 对文件的内容是没有影响的
#! 开头的一行会设置解释器运行环境

参考:
create-vue(Github)
create-vue 源码解析