本文参加了由 公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第9期,链接:Vue 团队公开的全新脚手架工具。
Vue 团队公开的全新脚手架工具,未来将替代 Vue-CLI
npm create vue 创建 Vue 工程
摘自GitHub:The recommended way to start a Vite-powered Vue project
bash
npm create vue@latest
### Or, if you need to support IE11, you can create a Vue 2 project with
npm create vue@legacy
下图是一个使用npm create vue
创建工程的完善示例图:

npm create
官网说明使用npm命令npm create vue
创建工程
npm create
是npm init
的alias,详情可查阅npm文档
流程分析
解析命令行
使用命令npm create vue
创建工程时,脚手架允许我们在命令后跟上项目所需依赖的参数。
通过 minimist 库解析命令行参数,获取创建工程所需的依赖。可选参数,详见下方注释:
typescript
const cwd = process.cwd()
// possible options:
// --default
// --typescript / --ts
// --jsx
// --router / --vue-router
// --pinia
// --with-tests / --tests (equals to `--vitest --cypress`)
// --vitest
// --cypress
// --nightwatch
// --playwright
// --eslint
// --eslint-with-prettier (only support prettier through eslint for simplicity)
// --force (for force overwriting)
const argv = minimist(process.argv.slice(2), {
alias: {
typescript: ['ts'],
'with-tests': ['tests'],
router: ['vue-router']
},
string: ['_'],
// all arguments are treated as booleans
boolean: true
})
tips:近期发现,在24年2.20日项目更新了一个提交,内容为使用 node:utils 包下的 parseArgs 工具函数替换了 minimist 库
typescript
const args = process.argv.slice(2)
const options = {
typescript: { type: 'boolean' },
ts: { type: 'boolean' },
'with-tests': { type: 'boolean' },
tests: { type: 'boolean' },
'vue-router': { type: 'boolean' },
router: { type: 'boolean' }
} as const
const { values: argv } = parseArgs({
args,
options,
strict: false
})
对参数进行判断,如果设置了 feature flags (即已经在命令行输入了依赖参数) 跳过 prompts 依赖选择的询问
typescript
// 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.nightwatch ??
argv.playwright ??
argv.eslint
) === 'boolean'
let targetDir = argv._[0]
const defaultProjectName = targetDir || 'vue-project'
命令行交互获取项目依赖
若在敲入npm create
命令时未设置 feature flags,则进入命令行交互阶段。
通过 prompts 库实现命令行交互,并获取结果。
typescript
const language = getLanguage()
let result = {}
try {
result = await prompts(
[
{
name: 'projectName',
type: targetDir ? null : 'text',
message: language.projectName.message,
initial: defaultProjectName,
onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)
},
// 省略若干配置
{
name: 'needsTypeScript',
type: () => (isFeatureFlagsUsed ? null : 'toggle'),
message: language.needsTypeScript.message,
initial: false,
active: language.defaultToggleOptions.active,
inactive: language.defaultToggleOptions.inactive
}
],
{
onCancel: () => {
throw new Error(red('✖') + ` ${language.errors.operationCancelled}`)
}
}
)
} catch (cancelled) {
console.log(cancelled.message)
process.exit(1)
}
const {
projectName,
needsTypeScript = argv.typescript
} = result
国际化
在上述代码中,我们发现第一行通过调用getLanguage()
方法获取了一个东西。跟进查看utils/getLanguage.ts
,发现这是一个处理国际化语言的工具函数。
此工具逻辑为:
- 通过当前环境获取设备区域的标识字符串,如:zh-Hans
- 通过区域标识获取 locales 目录下预存放的语言文件,其中包含:
- locales/en-US.json
- locales/fr-FR.json
- locales/tr-TR.json
- locales/zh-Hans.json
- locales/zh-Hant.json
typescript
// 统一区域标识符
function linkLocale(locale: string) {
let linkedLocale: string
switch (locale) {
case 'zh-TW':
case 'zh-HK':
case 'zh-MO':
linkedLocale = 'zh-Hant'
break
case 'zh-CN':
case 'zh-SG':
linkedLocale = 'zh-Hans'
break
default:
linkedLocale = locale
}
return linkedLocale
}
// 获取区域标识符
function getLocale() {
const shellLocale =
process.env.LC_ALL || // POSIX locale environment variables
process.env.LC_MESSAGES ||
process.env.LANG ||
Intl.DateTimeFormat().resolvedOptions().locale || // Built-in ECMA-402 support
'en-US' // Default fallback
return linkLocale(shellLocale.split('.')[0].replace('_', '-'))
}
// 获取语言json文件
function getLanguage() {
const locale = getLocale()
const localesRoot = path.resolve(__dirname, 'locales')
const languageFilePath = path.resolve(localesRoot, `${locale}.json`)
const doesLanguageExist = fs.existsSync(languageFilePath)
const lang: Language = doesLanguageExist
? require(languageFilePath)
: require(path.resolve(localesRoot, 'en-US.json'))
return lang
}
后续在交互中进行询问的信息便是 language 对象内的信息
初始化工程
创建项目工程目录 ,初始化 package.json
如果目录文件存在,则根据询问是否覆盖shouldOverwrite
处理
typescript
const root = path.join(cwd, targetDir)
if (fs.existsSync(root) && shouldOverwrite) {
emptyDir(root)
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root)
}
console.log(`\n${language.infos.scaffolding} ${root}...`)
const pkg = { name: packageName, version: '0.0.0' }
fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))
添加工程依赖
根据交互结果,渲染需要的文件以及配置。在 template 目录下存放了一些预定的项目所需文件。
引入工具函数renderTemplate
,该函数将预定的配置等文件拷贝至目标工程里,特别对package.json
文件中的依赖做了合并、排序操作。
typescript
import renderTemplate from './utils/renderTemplate'
const templateRoot = path.resolve(__dirname, 'template')
const callbacks = []
function render(templateName) {
const templateDir = path.resolve(templateRoot, templateName)
renderTemplate(templateDir, root, callbacks)
}
// Render base template
render('base')
// Add configs.
if (needsRouter) {
render('config/router')
}
if (needsPinia) {
render('config/pinia')
}
// 省略若干配置
TS配置
如果项目需要引入TS,需要做两件事。
一、配置tsconfig.node.json
typescript
if (needsTypeScript) {
render('config/typescript')
// Render tsconfigs
render('tsconfig/base')
const rootTsConfig = {
// It doesn't target any specific files because they are all configured in the referenced ones.
files: [],
// All templates contain at least a `.node` and a `.app` tsconfig.
references: [
{
path: './tsconfig.node.json'
},
{
path: './tsconfig.app.json'
}
]
}
// 省略若干配置
if (needsVitest) {
render('tsconfig/vitest')
// Vitest needs a standalone tsconfig.
rootTsConfig.references.push({
path: './tsconfig.vitest.json'
})
}
// 写入tsconfig.json
fs.writeFileSync(
path.resolve(root, 'tsconfig.json'),
JSON.stringify(rootTsConfig, null, 2) + '\n',
'utf-8'
)
}
二、js文件转换为ts文件,删除jsconfig.json
,同时修改文件内容中的带有.js
的后缀
TypeScript
if (needsTypeScript) {
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'))
}
生成 README 文件,给出运行项目的提示
获取用户的包管理器,生成README文件对项目进行一些说明
打印工程创建成功日志以及执行提示
typescript
// 生成Readme文件工具函数
import generateReadme from './utils/generateReadme'
// 命令行颜色优化库
import { red, green, bold } from 'kolorist'
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,
needsNightwatch,
needsPlaywright,
needsNightwatchCT,
needsCypressCT,
needsEslint
})
)
// 打印日志
console.log(`\n${language.infos.done}\n`)
if (root !== cwd) {
const cdProjectName = path.relative(cwd, root)
console.log(
` ${bold(green(`cd ${cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName}`))}`
)
}
console.log(` ${bold(green(getCommand(packageManager, 'install')))}`)
if (needsPrettier) {
console.log(` ${bold(green(getCommand(packageManager, 'format')))}`)
}
console.log(` ${bold(green(getCommand(packageManager, 'dev')))}`)
总结
create-vue 专注于 vue.js 框架的工程脚手架,对生态的预设及配置更加完善。从体验来说,相对于 create-vite, create-vue 能够更完整的搭建vue工程。
该项目对专注前端工程化的同学一定会有帮助,其中不少工具函数的源码也值得学习。
最后,捞一下😁:【脚手架之旅】探究 create-vite 实现原理