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包
```javascript
import 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
// 参数处理成boolean
boolean: 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-project
const defaultProjectName = !targetDir ? 'vue-project' : targetDir
// 强制写入
const forceOverwrite = argv.force
let 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 template
render('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-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')
}
// Cleanup.
// 如果使用ts
if (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 > npm
const packageManager = /pnpm/.test(process.env.npm_execpath)
? 'pnpm'
: /yarn/.test(process.env.npm_execpath)
? 'yarn'
: 'npm'
// README generation
fs.writeFileSync(
path.resolve(root, 'README.md'),
// 生成readme
generateReadme({
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,捕获异常,打印error
init().catch((e) => {
console.error(e)
})
3. 总结
- 整个index.js代码非常简练,熟悉了prompt的一些用法,ru type=null,不显示,validate校验输入
- 为了使npx下载速度快,使用的包非常少
render渲染模板,模板都是写在香港文件夹下的,需要渲染生成的模块直接拷贝,非常方便
4. 参考文档
- https://github.com/vuejs/create-vue