1. 前言
1.1 环境
- 操作系统: macOS 11.5.2
- 浏览器: Chrome 94.0.4606.81
-
1.2 阅读该文章可以get以下知识点
学会全新的官方脚手架工具 create-vue 的使用和原理
- 学会使用 VSCode 直接打开 github 项目
- 学会使用测试用例调试源码
2. 开始
2.1 如何使用
```javascript // 安装vue3工程 npm init vue@3 // 安装vue2工程 npm init vue@2
// 通过输入下面的命令,可以看到npm 的用法
npm init -h
npm init [—force|-f|—yes|-y|—scope]
npm init <@scope> (same as npx <@scope>/create)
npm init [<@scope>/]npx [<@scope>/]create-<name>)
npm init => npx create-vue
npx create-vue会去下载包,并执行bin里面的脚本<br />[npm init文档](https://docs.npmjs.com/cli/v6/commands/npm-init)<a name="t3FLa"></a>## 2.2 index.js 引入的npm包```javascriptimport fs from 'fs'import path from 'path'// 命令行args获取import minimist from 'minimist'// 交互式命令行import prompts from 'prompts'// 标准输入的颜色import { red, green, bold } from 'kolorist'
kolorist
minimist
prompts
这些常用库,基本用法都差不多,之前有讲过
2.3 index.js源码
// 校验包名称function isValidPackageName(projectName) {return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName)}// 将一些情况转义成中划线或删除function toValidPackageName(projectName) {return projectName.trim().toLowerCase().replace(/\s+/g, '-').replace(/^[._]/, '').replace(/[^a-z0-9-~]+/g, '-')}// 判断是否存在该目录function canSafelyOverwrite(dir) {return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0}// 清空文件和文件夹function emptyDir(dir) {postOrderDirectoryTraverse(dir,(dir) => fs.rmdirSync(dir),(file) => fs.unlinkSync(file))}async function init() {// 脚本运行地址const cwd = process.cwd()// possible options:// --default// --typescript / --ts// --jsx// --router / --vue-router// --pinia// --with-tests / --tests / --cypress// --force (for force overwriting)// 参数const argv = minimist(process.argv.slice(2), {// 设置别名alias: {typescript: ['ts'],'with-tests': ['tests', 'cypress'],router: ['vue-router']},// all arguments are treated as booleans// 参数处理成booleanboolean: true})// if any of the feature flags is set, we would skip the feature prompts// use `??` instead of `||` once we drop Node.js 12 support// 这段代码的目的是如果传入了下面的这些参数,不需要进入prompt去设置了const isFeatureFlagsUsed =typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.pinia || argv.tests) ==='boolean'// 目标目录let targetDir = argv._[0]// 默认工程名,存在目标文件夹使用文件夹名称,不存在使用vue-projectconst defaultProjectName = !targetDir ? 'vue-project' : targetDir// 强制写入const forceOverwrite = argv.forcelet result = {}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?// 交互命令,生成配置result = await prompts([// 项目名称{name: 'projectName',// 如果type=null是不会显示的type: targetDir ? null : 'text',message: 'Project name:',initial: defaultProjectName,onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)},// 是否覆盖{name: 'shouldOverwrite',// 校验目标目录是否有效type: () => (canSafelyOverwrite(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',// 校验包名是否有效,自己实现的,没有用validate-npm-package-name这个库,减少项目依赖type: () => (isValidPackageName(targetDir) ? null : 'text'),message: 'Package name:',initial: () => toValidPackageName(targetDir),// 非法校验,如果校验不通过,不会执行到下一步validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name'},// ts{name: 'needsTypeScript',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add TypeScript?',initial: false,active: 'Yes',inactive: 'No'},//jsx{name: 'needsJsx',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add JSX Support?',initial: false,active: 'Yes',inactive: 'No'},// router{name: 'needsRouter',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add Vue Router for Single Page Application development?',initial: false,active: 'Yes',inactive: 'No'},// pinia 状态管理库{name: 'needsPinia',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add Pinia for state management?',initial: false,active: 'Yes',inactive: 'No'},// 测试{name: 'needsTests',type: () => (isFeatureFlagsUsed ? null : 'toggle'),message: 'Add Cypress for testing?',initial: false,active: 'Yes',inactive: 'No'}],// 第二个参数,如果取消操作,可以返回一些信息{onCancel: () => {throw new Error(red('✖') + ' Operation cancelled')}})} catch (cancelled) {console.log(cancelled.message)//结束进程process.exit(1)}// `initial` won't take effect if the prompt type is null// so we still have to assign the default values here// 通过参数设置默认值,有可能不存在const {packageName = toValidPackageName(defaultProjectName),shouldOverwrite,needsJsx = argv.jsx,needsTypeScript = argv.typescript,needsRouter = argv.router,needsPinia = argv.pinia,needsTests = argv.tests} = result// 获取目标目录const root = path.join(cwd, targetDir)// 强制写入if (shouldOverwrite) {emptyDir(root)// 不存在创建} else if (!fs.existsSync(root)) {fs.mkdirSync(root)}console.log(`\nScaffolding project in ${root}...`)// 包名称和版本const pkg = { name: packageName, version: '0.0.0' }fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))// 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 templaterender('base')// Add configs.if (needsJsx) {render('config/jsx')}if (needsRouter) {render('config/router')}if (needsPinia) {render('config/pinia')}if (needsTests) {render('config/cypress')}if (needsTypeScript) {render('config/typescript')}// Render code template.// prettier-ignoreconst 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')}// Cleanup.// 如果使用tsif (needsTypeScript) {// rename all `.js` files to `.ts`// rename jsconfig.json to tsconfig.json// 替换文件名preOrderDirectoryTraverse(root,() => {},(filepath) => {if (filepath.endsWith('.js')) {fs.renameSync(filepath, filepath.replace(/\.js$/, '.ts'))} else if (path.basename(filepath) === 'jsconfig.json') {fs.renameSync(filepath, filepath.replace(/jsconfig\.json$/, 'tsconfig.json'))}})// 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'))}// 不需要测试,直接删除if (!needsTests) {// All templates assumes the need of tests.// If the user doesn't need it:// rm -rf cypress **/__tests__/preOrderDirectoryTraverse(root,(dirpath) => {const dirname = path.basename(dirpath)if (dirname === 'cypress' || dirname === '__tests__') {emptyDir(dirpath)fs.rmdirSync(dirpath)}},() => {})}// Instructions:// Supported package managers: pnpm > yarn > npm// Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved,// it is not possible to tell if the command is called by `pnpm init`.// 包管理工具 pnpm > yarn > npmconst packageManager = /pnpm/.test(process.env.npm_execpath)? 'pnpm': /yarn/.test(process.env.npm_execpath)? 'yarn': 'npm'// README generationfs.writeFileSync(path.resolve(root, 'README.md'),// 生成readmegenerateReadme({projectName: result.projectName || defaultProjectName,packageManager,needsTypeScript,needsTests}))console.log(`\nDone. Now run:\n`)if (root !== cwd) {console.log(` ${bold(green(`cd ${path.relative(cwd, root)}`))}`)}console.log(` ${bold(green(getCommand(packageManager, 'install')))}`)console.log(` ${bold(green(getCommand(packageManager, 'dev')))}`)console.log()}// 初始化运行init,捕获异常,打印errorinit().catch((e) => {console.error(e)})
3. 总结
- 整个index.js代码非常简练,熟悉了prompt的一些用法,ru type=null,不显示,validate校验输入
- 为了使npx下载速度快,使用的包非常少
render渲染模板,模板都是写在香港文件夹下的,需要渲染生成的模块直接拷贝,非常方便
4. 参考文档
- https://github.com/vuejs/create-vue
