背景
大家周五好,正好填一下之前的坑我准备了很久的学习如何开发一个 Vue CLI 工具的学习的内容输出成文章,我觉得学习的最终过程是需要输出成果的,这也是我的一个学习过程的记录。文章内容比较长,需要慢慢学习,全文超三万字,希望帮到大家学习和总结
本质上就是一个 Node
项目,Vue CLI 是一个基于 Node.js 的命令行工具,用于快速搭建和管理 Vue.js 项目的脚手架。其核心原理是通过命令行接口获取用户的配置选项,然后根据这些选项生成项目文件和目录结构。以下是一个实现简易版 Vue CLI 的基本步骤和示例代码。
分析过程
Vue-CLI 工具中有很多的功能,今天我们这里最主要学习使用的是 create
, 最后输出的文件是长这样的
create command 命令
create 命令我们在使用 Vue-CLI
工具的时候一般是构建项目,快速搭建脚手架,自定义配置需求点,生成对应的文件工程模版,这对于我们快速开发作用很大。我们不妨看一看源码,其实与 init
指令也是雷同的这里就不一一描述了
js
const program = require('commander')
program
.command('create <app-name>')
.description('create a new project powered by vue-cli-service')
.option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
.option('-d, --default', 'Skip prompts and use default preset')
.option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
.option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
.option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
.option('-g, --git [message]', 'Force git initialization with initial commit message')
.option('-n, --no-git', 'Skip git initialization')
.option('-f, --force', 'Overwrite target directory if it exists')
.option('--merge', 'Merge target directory if it exists')
.option('-c, --clone', 'Use git clone when fetching remote preset')
.option('-x, --proxy <proxyUrl>', 'Use specified proxy when creating project')
.option('-b, --bare', 'Scaffold project without beginner instructions')
.option('--skipGetStarted', 'Skip displaying "Get started" instructions')
.action((name, options) => {
if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))
}
// --git makes commander to default git to true
if (process.argv.includes('-g') || process.argv.includes('--git')) {
options.forceGit = true
}
require('../lib/create')(name, options)
})
这里对应官方网站上的文档介绍内容参数填写相关的部分
cmd
用法:create [options] <app-name>
创建一个由 `vue-cli-service` 提供支持的新项目
选项:
-p, --preset <presetName> 忽略提示符并使用已保存的或远程的预设选项
-d, --default 忽略提示符并使用默认预设选项
-i, --inlinePreset <json> 忽略提示符并使用内联的 JSON 字符串预设选项
-m, --packageManager <command> 在安装依赖时使用指定的 npm 客户端
-r, --registry <url> 在安装依赖时使用指定的 npm registry
-g, --git [message] 强制 / 跳过 git 初始化,并可选的指定初始化提交信息
-n, --no-git 跳过 git 初始化
-f, --force 覆写目标目录可能存在的配置
-c, --clone 使用 git clone 获取远程预设选项
-x, --proxy 使用指定的代理创建项目
-b, --bare 创建项目时省略默认组件中的新手指导信息
-h, --help 输出使用帮助信息
这里可以看到这个 Create
命令中附带了一个参数 然后传递到里面的 一个 create
方法,附带上传入的 projectName
参数数据
create & Creator 实例对象
我们先看看 create
方法,里面的方法很简单,详细看下图,里面的内容点互相嵌套,实际上是分开几步来实现完整的功能的
实际上是将部分的参数,做格式化的处理和边界的逻辑判断,然后将它们传入到创建对象的实例方法中 Creator
,最终调用 creator.create
方法。
接下来我们看看 Creator
实例对象,它是属于最核心的功能点,可以看到优质的代码一般都是面向对象开发的。
这部分代码比较的长,我们将代码收起来一个个拆分来看
1. 初始化 生成 Prompt & Preset
这里做了很多的验证和边界处理,这里就不一一阐述了,本文的初衷是让大家去阅读源码,然后实现一个功能
js
constructor (name, context, promptModules) {
super(); // 继承 EventEmitter
this.name = name; // ProjectName
this.context = process.env.VUE_CLI_CONTEXT = context; // 默认的上下文,一般是传入目标CWD
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts(); // 就是我们选择的 Prompts
this.presetPrompt = presetPrompt; // 选择 preset Vue2 或者 Vue3
this.featurePrompt = featurePrompt; // 选择 Feature 的分支
this.outroPrompts = this.resolveOutroPrompts(); // 使用 manual 模式的时候用到 Prompts
this.injectedPrompts = []; // 注入的 Prompts
this.promptCompleteCbs = [];
this.afterInvokeCbs = [];
this.afterAnyInvokeCbs = [];
this.run = this.run.bind(this)
const promptAPI = new PromptModuleAPI(this); // 对应管理整个 Feature 的选项或者 Preset V2或V3
promptModules.forEach(m => m(promptAPI)); // 传入
}
这里有三个分支,一个是选择版本,一个手动选择 Feature. PromptModuleAPI
对应的就是这个功能点,这里贴的代码是只做参考,详细的还是需要到 Github 上研究的
js
exports.getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
PromptModuleAPI
这个实例 实际上是负责管理整个 PromptModule
选择和注入的
js
module.exports = class PromptModuleAPI {
constructor (creator) {
this.creator = creator
}
// 注入设置的 Feature
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}
// // 注入设置的 prompt
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}
injectOptionForPrompt (name, option) {
this.creator.injectedPrompts.find(f => {
return f.name === name
}).choices.push(option)
}
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}
单独的文件负责管理,每一个对应的配置,用到的时候在 实例对象中管理中遍历出来使用 这里是举例其中的一个文件,进行说明,总共有很多个,详细可以看这个文件夹
js
module.exports = cli => {
cli.injectFeature({
name: 'Babel',
value: 'babel',
short: 'Babel',
description: 'Transpile modern JavaScript to older versions (for compatibility)',
link: 'https://babeljs.io/',
checked: true
})
cli.onPromptComplete((answers, options) => {
if (answers.features.includes('ts')) {
if (!answers.useTsWithBabel) {
return
}
} else if (!answers.features.includes('babel')) {
return
}
options.plugins['@vue/cli-plugin-babel'] = {}
})
}
这里用到 配置设置,实际看起来有点绕,其实是值得我们学习如何进行管理这些配置文件的,更加系统和优雅的处理文件,把需要经常改动的静态资源和配置功能点分开,从而实现在命令行中,输入选择,获取对应功能点的功能。
2. 生成 Package.json 文件 & install
设置 Preset
对象,这里就是根据我们前面输入的,动态设置到 Preset
Plugin 中
js
preset = cloneDeep(preset) // clone before mutating
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset) // inject core service
if (cliOptions.bare) {
preset.plugins['@vue/cli-service'].bare = true
}
// legacy support for router
if (preset.router) {
preset.plugins['@vue/cli-plugin-router'] = {}
if (preset.routerHistoryMode) {
preset.plugins['@vue/cli-plugin-router'].historyMode = true
}
}
// legacy support for vuex
if (preset.vuex) {
preset.plugins['@vue/cli-plugin-vuex'] = {}
}
初始化 Package 对象
按照选择 Preset 需求, 在 plugins
里面添加,例如 vuex
,vue-router
,是否为 history-mode
等等
然后开始生成 Package.json
并且下载依赖
js
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm')
); // 定义包管理器,按照优先级来确定
await clearConsole();
// pm 就是整个负责下载依赖的 包管理器
const pm = new PackageManager({ context, forcePackageManager: packageManager })
log(`✨ Creating project in ${chalk.yellow(context)}.`); // 上下文 context 一般是 cwd
this.emit('creation', { event: 'creating' }); // 用到了 EventEmitter 类的方法 向所有订阅发通知
// 目的是通过这个方法同步当前的任务状态
// get latest CLI plugin version
const { latestMinor } = await getVersions(); // 获取插件的最新版本
// 生成 依赖插件 这里就是就最最最基础的 Package 对象
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {},
...resolvePkg(context)
}
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
if (preset.plugins[dep]._isPreset) {
return
}
let { version } = preset.plugins[dep]
if (!version) {
if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
version = isTestOrDebug ? `latest` : `~${latestMinor}`
} else {
version = 'latest'
}
}
pkg.devDependencies[dep] = version
})
// write package.json 写出来 package.json
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
// generate a .npmrc file for pnpm, to persist the `shamefully-flatten` flag
// 这里省略一部分 关于 npmrc 是 pnpm的情况
// intilaize git repository before installing deps
// so that vue-cli-service can setup git hooks.
// 省略一部分 关于 GIT 配置相关的
// install plugins
log(`⚙\u{fe0f} Installing CLI plugins. This might take a while...`)
log()
this.emit('creation', { event: 'plugins-install' })
if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
// in development, avoid installation process
await require('./util/setupDevProject')(context)
} else {
await pm.install(); // 下载 node_module 依赖内容
}
生成 Package.json 文件举例
json
{
"name": "vue-project-demo",
"version": "0.1.0",
"private": true,
"dependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.4",
"@vue/cli-plugin-eslint": "~4.5.4",
"@vue/cli-service": "~4.5.4",
"@vue/compiler-sfc": "^3.0.0",
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
}
3. Generator 生成模版 拓展 Package
js
// run generator
log(`🚀 Invoking generators...`)
this.emit('creation', { event: 'invoking-generators' })
const plugins = await this.resolvePlugins(preset.plugins, pkg)
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs
})
// 这里调用 Generate 方法
await generator.generate({
extractConfigFiles: preset.useConfigFiles
})
// install additional deps (injected by generators) Genrerator 中拓展依赖 deps install
log(`📦 Installing additional dependencies...`)
this.emit('creation', { event: 'deps-install' })
log()
if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
await pm.install()
}
// run complete cbs if any (injected by generators)
log(`⚓ Running completion hooks...`)
this.emit('creation', { event: 'completion-hooks' }); // 创建 Hooks
for (const cb of afterInvokeCbs) {
await cb()
}
for (const cb of afterAnyInvokeCbs) {
await cb()
}
我们看看 Generator
实例方法
js
module.exports = class Generator {
constructor (context, {
pkg = {},
plugins = [],
afterInvokeCbs = [],
afterAnyInvokeCbs = [],
files = {},
invoking = false
} = {}) {
this.context = context
this.plugins = sortPlugins(plugins)
this.originalPkg = pkg
this.pkg = Object.assign({}, pkg)
this.pm = new PackageManager({ context })
this.imports = {}
this.rootOptions = {}
this.afterInvokeCbs = afterInvokeCbs
this.afterAnyInvokeCbs = afterAnyInvokeCbs
this.configTransforms = {} // 插件通过 GeneratorAPI 暴露的 addConfigTransform 方法添加如何提取配置文件
this.defaultConfigTransforms = defaultConfigTransforms // 默认的配置文件
this.reservedConfigTransforms = reservedConfigTransforms // 保留配置的 vue config 文件 配置
this.invoking = invoking
// for conflict resolution
this.depSources = {}
// virtual file tree
this.files = Object.keys(files).length
// when execute `vue add/invoke`, only created/modified files are written to disk
? watchFiles(files, this.filesModifyRecord = new Set())
// all files need to be written to disk
: files
this.fileMiddlewares = []
this.postProcessFilesCbs = []
// exit messages
this.exitLogs = []
// load all the other plugins
this.allPlugins = this.resolveAllPlugins()
const cliService = plugins.find(p => p.id === '@vue/cli-service')
const rootOptions = cliService
? cliService.options
: inferRootOptions(pkg)
this.rootOptions = rootOptions
}
async initPlugins () {
const { rootOptions, invoking } = this
const pluginIds = this.plugins.map(p => p.id)
// avoid modifying the passed afterInvokes, because we want to ignore them from other plugins
const passedAfterInvokeCbs = this.afterInvokeCbs
this.afterInvokeCbs = []
// apply hooks from all plugins to collect 'afterAnyHooks'
for (const plugin of this.allPlugins) {
const { id, apply } = plugin
const api = new GeneratorAPI(id, this, {}, rootOptions) // 每个插件对应生成一个 GeneratorAPI 实例,并将实例 api 传入插件暴露出来的 generator 函数
if (apply.hooks) {
await apply.hooks(api, {}, rootOptions, pluginIds)
}
}
// We are doing save/load to make the hook order deterministic
// save "any" hooks
const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs
// reset hooks
this.afterInvokeCbs = passedAfterInvokeCbs
this.afterAnyInvokeCbs = []
this.postProcessFilesCbs = []
// apply generators from plugins
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
const api = new GeneratorAPI(id, this, options, rootOptions)
await apply(api, options, rootOptions, invoking)
if (apply.hooks) {
// while we execute the entire `hooks` function,
// only the `afterInvoke` hook is respected
// because `afterAnyHooks` is already determined by the `allPlugins` loop above
await apply.hooks(api, options, rootOptions, pluginIds)
}
}
// restore "any" hooks
this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
}
}
GeneratorAPI
- hasPlugin:判断项目中是否有某个插件
- extendPackage:拓展 package.json 配置
- render:利用 ejs 渲染模板文件
- onCreateComplete:内存中保存的文件字符串全部被写入文件后的回调函数
- exitLog:当 generator 退出的时候输出的信息
- genJSConfig:将 json 文件生成为 js 配置文件
- injectImports:向文件当中注入import语法的方法
- injectRootOptions:向 Vue 根实例中添加选项
js
module.exports = (api, options) => {
// 渲染 ejs 模版
api.render('./template', {
doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript'),
useBabel: api.hasPlugin('babel')
})
if (options.vueVersion === '3') {
// 拓展 package.json
api.extendPackage({
dependencies: {
'vue': '^3.2.13'
}
})
} else {
// 拓展 package.json
api.extendPackage({
dependencies: {
'vue': '^2.6.14'
},
devDependencies: {
'vue-template-compiler': '^2.6.14'
}
})
}
// 拓展 package.json
api.extendPackage({
scripts: {
'serve': 'vue-cli-service serve',
'build': 'vue-cli-service build'
},
browserslist: [
'> 1%',
'last 2 versions',
'not dead',
...(options.vueVersion === '3' ? ['not ie 11'] : [])
]
})
// 是否使用 css 预编译
if (options.cssPreprocessor) {
const deps = {
sass: {
sass: '^1.32.7',
'sass-loader': '^12.0.0'
},
'dart-sass': {
sass: '^1.32.7',
'sass-loader': '^12.0.0'
},
less: {
'less': '^4.0.0',
'less-loader': '^8.0.0'
},
stylus: {
'stylus': '^0.55.0',
'stylus-loader': '^6.1.0'
}
}
api.extendPackage({
devDependencies: deps[options.cssPreprocessor]
})
}
// 引入路由
if (options.router && !api.hasPlugin('router')) {
require('./router')(api, options, options)
}
// 引入 vuex
if (options.vuex && !api.hasPlugin('vuex')) {
require('./vuex')(api, options, options)
}
// 拓展 Package
if (options.configs) {
api.extendPackage(options.configs)
}
// 如果使用 ts 就删除 jsconfig.json
if (api.hasPlugin('typescript')) {
api.render((files) => delete files['jsconfig.json'])
}
}
resolveFiles
fileMiddlewares
里面包含了 ejs render
函数,所有插件调用 api.render
时候只是把对应的渲染函数 push 到了 fileMiddlewares
中,等所有的 插件执行完以后才会遍历执行 fileMiddlewares
里面的所有函数,即在内存中生成模板文件字符串。
injectImportsAndOptions
就是将 generator 注入的 import 和 rootOption 解析到对应的文件中,比如选择了 vuex, 会在 src/main.js
中添加 import store from './store'
,以及在 vue 根实例中添加 router 选项。
postProcessFilesCbs
是在所有普通文件在内存中渲染成字符串完成之后要执行的遍历回调。
js
async resolveFiles () {
const files = this.files // 存放 files 文件的对象,用来保存文件的,最后用来生成文件
for (const middleware of this.fileMiddlewares) {
await middleware(files, ejs.render)
}
// normalize file paths on windows
// all paths are converted to use / instead of \
normalizeFilePaths(files)
// handle imports and root option injections
Object.keys(files).forEach(file => {
let imports = this.imports[file]
imports = imports instanceof Set ? Array.from(imports) : imports
if (imports && imports.length > 0) {
files[file] = runTransformation(
{ path: file, source: files[file] },
require('./util/codemods/injectImports'),
{ imports }
)
}
let injections = this.rootOptions[file]
injections = injections instanceof Set ? Array.from(injections) : injections
if (injections && injections.length > 0) {
files[file] = runTransformation(
{ path: file, source: files[file] },
require('./util/codemods/injectOptions'),
{ injections }
)
}
})
for (const postProcess of this.postProcessFilesCbs) {
await postProcess(files)
}
debug('vue:cli-files')(this.files)
}
writeFileTree
在提取了配置文件和模板渲染之后调用了 sortPkg
对 package.json
的字段进行了排序并将 package.json
转化为 json
字符串添加到项目的 files
中。 此时整个项目的文件已经在内存中生成好了(在源码中就是对应的 this.files
),接下来就调用 writeFileTree
方法将内存中的字符串模板文件生成在磁盘中。
4. 安装额外依赖 & 生成 REAME.md & 输出日志
js
// 下载依赖
log(`📦 Installing additional dependencies...`)
this.emit('creation', { event: 'deps-install' })
log()
if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
await pm.install()
}
// 执行完成以后的 Callback
log(`⚓ Running completion hooks...`)
this.emit('creation', { event: 'completion-hooks' })
for (const cb of afterInvokeCbs) {
await cb()
}
for (const cb of afterAnyInvokeCbs) {
await cb()
}
if (!generator.files['README.md']) {
// 生成 README.md 文件
log()
log('📄 Generating README.md...')
await writeFileTree(context, {
'README.md': generateReadme(generator.pkg, packageManager)
})
}
// GIT 相关
let gitCommitFailed = false
if (shouldInitGit) {
await run('git add -A')
if (isTestOrDebug) {
await run('git', ['config', 'user.name', 'test'])
await run('git', ['config', 'user.email', 'test@test.com'])
await run('git', ['config', 'commit.gpgSign', 'false'])
}
const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
try {
await run('git', ['commit', '-m', msg, '--no-verify'])
} catch (e) {
gitCommitFailed = true
}
}
log()
log(`🎉 Successfully created project ${chalk.yellow(name)}.`)
if (!cliOptions.skipGetStarted) {
log(
`👉 Get started with the following commands:\n\n` +
(this.context === process.cwd() ? `` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : packageManager === 'pnpm' ? 'pnpm run serve' : 'npm run serve'}`)
)
}
log()
this.emit('creation', { event: 'done' })
if (gitCommitFailed) {
warn(
`Skipped git commit due to missing username and email in git config, or failed to sign commit.\n` +
`You will need to perform the initial commit yourself.\n`
)
}
// // 最后打印日志输出日志
generator.printExitLogs()
5. 总结
可以看到这个过程是十分的复杂的,其中我们重点看 Creator
& Generator
这两个构造函数。 调用 create
方法的时候会调用创建一个 Creator
实例,然后在 Creator
的 create
方法中又调用创建 Generator
实例,再在这个实例里面再调用 Generator
方法。这里是比较复杂和绕的。
在这个过程中,总的来说是收集了用户选择的信息,在对象中保存需要加载的插件等等,然后将数据保存在内存对象中,到最后再生成对应的文件,这样实现我们的最后的目的
实现过程
说了一大堆我们终于开始初始化整一个 Node
项目用来生成 template
了。
1. 初始化一个 Node.js 项目
在你的工作目录下,运行以下命令来初始化一个新的 Node.js 项目:
bash
mkdir my-vue-cli
cd my-vue-cli
npm init -y
这将创建一个 package.json
文件。
2. 安装必要的包
你需要安装一些包来帮助你创建 CLI 工具:
bash
npm install inquirer fs-extra -D
3. 创建 CLI 脚本
在项目根目录下,创建一个名为 cli.js
的文件,并写入以下代码:
javascript
#!/usr/bin/env node
const inquirer = require('@inquirer/prompts');
const fs = require('fs-extra');
const path = require('path');
async function init() {
const answers = await inquirer.input(
{
type: 'input',
name: 'projectName',
message: 'What is the name of your project?',
default: 'my-vue-project'
}
);
console.log(answers)
const projectPath = path.join(process.cwd(), answers);
await fs.ensureDir(projectPath);
await fs.writeFile(path.join(projectPath, 'index.html'), '<h1>Hello Vue!</h1>');
console.log('Project created successfully!');
}
init();
这段代码会询问用户项目的名称,并在当前目录下创建一个新的文件夹,里面包含一个简单的 index.html
文件。
html
<h1>Hello Vue!</h1>
4. 设置命令行接口
在 package.json
文件中,添加一个 bin
字段来指定 CLI 的入口文件:
json
"bin": {
"my-vue-cli": "./cli.js"
}
5. 全局安装你的 CLI 工具
在项目目录下运行:
bash
npm link
这会创建一个全局的符号链接,指向你的 CLI 脚本。
6. 使用你的 CLI 工具
现在你可以在命令行中运行你的 CLI 工具了:
bash
my-vue-cli
跟随提示输入项目名称,CLI 工具将会在当前目录下创建一个包含 index.html
文件的新文件夹。相信这里的功能实质上是不能满足我们的需求的,我们需要很多个性化自定义的内容,项目模板的管理、代码风格和格式化、错误处理和日志记录。这里离我们的目标功能还差比较远呢,所以我们继续升级
升级功能
目的我们是优化提示内容,
你需要安装一些包来帮助你升级 CLI 工具:
chalk
用于在命令行输出中添加颜色和样式,让输出的信息更易于阅读和区分。chalkora
: 用于在命令行中显示旋转的加载指示器,提升用户等待时的体验。oracommander
用于更容易地解析命令行参数和选项。download-git-repo
用于下载并提取 git 仓库,适用于从 GitHub、GitLab、Bitbucket 等服务上 下载项目模板。handlebars
是一个强大的模板引擎,可以用来根据用户的输入渲染文件和目录。validate-npm-package-name
用于检查字符串是否是一个有效的 npm 包名。
javascript
#!/usr/bin/env node
const inquirer = require('inquirer');
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const ora = require('ora');
const validateNpmPackageName = require('validate-npm-package-name');
async function init() {
console.log(chalk.green('Welcome to My Vue CLI'));
const answers = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: 'What is the name of your project?',
validate: input => {
const validationResult = validateNpmPackageName(input);
if (validationResult.validForNewPackages) {
return true;
} else {
return 'Invalid project name. Please choose another one.';
}
}
}
]);
const spinner = ora('Creating project...').start();
try {
const projectPath = path.join(process.cwd(), answers.projectName);
await fs.ensureDir(projectPath);
await fs.writeFile(path.join(projectPath, 'index.html'), '<h1>Hello Vue!</h1>');
spinner.succeed('Project created successfully!');
console.log(`\nNavigate to your project by running: ${chalk.cyan(`cd ${answers.projectName}`)}`);
console.log(`Run your project with: ${chalk.cyan('npm run serve')}\n`);
} catch (error) {
spinner.fail('Failed to create project');
console.error(chalk.red(error));
}
}
init();
在这个脚本中,我们做了以下几点改进:
- 引入了
chalk
来为命令行输出添加颜色,提升了用户体验。 - 引入了
ora
来显示加载指示器,给用户一个正在处理的反馈。 - 对用户输入的项目名称进行了验证,确保其为有效的 npm 包名。
- 对项目创建过程中可能发生的错误进行了捕获和处理,确保了更好的错误反馈。
在运行这个脚本创建项目时,用户将会看到一个彩色的欢迎信息,输入项目名称后将看到一个加载指示器,直到项目创建完成。如果项目名称无效或者在创建过程中发生错误,用户将会收到相应的错误信息。这些改进提供了更友好和更专业的用户体验。
升级功能 ✖ 2
我们可以在 Vue CLI 官网上看到这个 选择需要用到的模版 ,依葫芦画瓢我们可以按照它的功能点,简单实现一下功能点
这个默认的设置非常适合快速创建一个新项目的原型,而手动设置则提供了更多的选项,它们是面向生产的项目更加需要的。
当你需要添加更多选项和根据这些选项生成不同的 Vue 开发模板时,你可以使用 inquirer
收集用户的选择,然后根据这些选择生成不同的项目结构和配置文件。
代码逻辑
下载对应的模版,参考这里的 template模版
js
#!/usr/bin/env node
const inquirer = require('inquirer');
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const ora = require('ora');
const validateNpmPackageName = require('validate-npm-package-name');
async function init() {
console.log(chalk.green('Welcome to My Vue CLI'));
const answers = await inquirer.prompt([
// ... 保持现有的提示问题不变 ...
{
type: 'list',
name: 'packageManager',
message: 'Which package manager would you like to use?',
choices: ['npm', 'yarn'],
},
]);
const spinner = ora('Creating project...').start();
try {
// 创建 package 对象
const package = {
name: answers.projectName,
version: '1.0.0',
description: `${answers.projectName} - a Vue.js project`,
main: 'index.js',
scripts: {
serve: 'vue-cli-service serve',
build: 'vue-cli-service build',
},
dependencies: {},
devDependencies: {},
keywords: [],
author: '',
license: 'ISC',
};
// 根据用户的选择动态添加依赖项
package.dependencies.vue = answers.vueVersion === '3.x' ? '^3.0.0' : '^2.0.0';
if (answers.useBabel) {
package.devDependencies['@babel/core'] = '^7.0.0'; // 示例版本号
package.devDependencies['@babel/preset-env'] = '^7.0.0'; // 示例版本号
}
if (answers.useEslint) {
package.devDependencies.eslint = '^7.0.0'; // 示例版本号
}
if (answers.vueRouterVersion !== 'No Vue Router') {
package.dependencies['vue-router'] = answers.vueRouterVersion === '3.x' ? '^3.0.0' : '^2.0.0';
}
if (answers.useVuex) {
package.dependencies.vuex = answers.vueVersion === '3.x' ? '^4.0.0' : '^3.0.0';
}
// 定义项目路径并确保目录存在
const projectPath = path.join(process.cwd(), answers.projectName);
await fs.ensureDir(projectPath);
// 写入 index.html 文件
await fs.writeFile(path.join(projectPath, 'index.html'), '<h1>Hello Vue!</h1>');
// 根据 package 对象生成 package.json 文件
await fs.writeJson(path.join(projectPath, 'package.json'), package, { spaces: 2 });
// 这里预先下载好
// 调用函数来从本地模板生成项目
const templatePath = 'path/to/your/local/template'; // Replace with your local template path
const projectPath = path.join(process.cwd(), answers.projectName);
await generateFromTemplate(templatePath, projectPath, { projectName: answers.projectName });
// 判断选择的包管理器并执行相应命令安装依赖
const installCmd = answers.packageManager === 'npm' ? 'npm install' : 'yarn install';
const installProcess = spawn(installCmd, { stdio: 'inherit', shell: true, cwd: projectPath });
installProcess.on('close', (code) => {
if (code === 0) {
spinner.succeed('Dependencies installed successfully!');
console.log(`\nNavigate to your project by running: ${chalk.cyan(`cd ${answers.projectName}`)}`);
console.log(`Run your project with: ${chalk.cyan(`${answers.packageManager} run serve`)}\n`);
} else {
spinner.fail('Failed to install dependencies');
console.error(chalk.red(`Installation process exited with code ${code}`));
}
});
spinner.succeed('Project created successfully!');
console.log(`\nNavigate to your project by running: ${chalk.cyan(`cd ${answers.projectName}`)}`);
console.log(`Run your project with: ${chalk.cyan('npm run serve')}\n`);
} catch (error) {
spinner.fail('Failed to create project');
console.error(chalk.red(error));
}
}
init();
实现结果
声明 & 总结
本文旨意是学习交流,不是完完全全复刻实现 Vue-CLI
,如果需要阅读源码和细节的可以到 Github 上进行阅读,这里我更加推荐这个兄弟的网站,上面分析 Vue-CLI
源码的学习内容也是十分的有帮助,向他学习,地址
后面我也再丰富我的内容让内容更加可读,方便大家学习。如果这篇文章帮到你的话,可以给我一个点赞收藏吗?这对我继续创作真的有很大的帮助,谢谢大家Thanks♪(・ω・)ノ!!!
文章参考
cli.vuejs.org/zh/guide/cr...
github.com/vuejs/vue-c...
github.com/vuejs/vue-c...
kuangpf.com/vue-cli-ana...