Nodejs除了賦予前端后端的能力外,還能有各種各樣的腳本,極大的簡單各種操作。在早期,腳本做的工作大都是生成固定的模版,所以你需要了解的,僅僅生成的項(xiàng)目就夠了。然而,隨著框架的完善,架構(gòu)師往往希望通過腳本處理默認(rèn)的配置或者環(huán)境,這樣能減少環(huán)境差異導(dǎo)致的問題,還能簡化升級(jí)核心框架的升級(jí),例如Vue CLI做的事。
這時(shí)候,我們不得不探一探它的神秘面紗。
這次帶來的是Create的原理解析。
Package
怎么看的呢?第一步是尋找package.json文件,它定義了項(xiàng)目所需的依賴以及項(xiàng)目的名稱等信息,所以都存這樣的文件。CLI的腳本放在bin屬性下,它會(huì)隨著安裝放入系統(tǒng)的環(huán)境變量中,所以我們可以在任意的位置使用它。
在Vue CLI的項(xiàng)目中,目錄結(jié)構(gòu)是奇怪的。一般情況下,最外面的是項(xiàng)目本身,也就是CLI工程,然而它是文檔。他的CLI項(xiàng)目放在packages/@vue目錄下。
這里做個(gè)備注,此時(shí)的hash值是7375b12c8e75bd4ddc5f04a475512971e1f2bd04,你們可以看這個(gè)位置的源碼。
里面的CLI項(xiàng)目很多,不過我這次找的算簡單的,在cli的這個(gè)項(xiàng)目里,他的package.json如下。
{
"name": "@vue/cli",
"version": "3.6.3",
"description": "Command line interface for rapid Vue.js development",
"bin": {
"vue": "bin/vue.js"
},
//...
}
bin/vue.js這是所執(zhí)行的腳本。
vue.js
接下來看一下第一個(gè)腳本。
#!/usr/bin/env node
// 定義使用node環(huán)境
const minimist = require('minimist')
const program = require('commander')
const loadCommand = require('../lib/util/loadCommand')
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')
//...more option
.option('--skipGetStarted', 'Skip displaying "Get started" instructions')
.action((name, cmd) => {
const options = cleanArgs(cmd)
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)
})
// commander passes the Command object itself as options,
// extract only actual options into a fresh object.
function cleanArgs (cmd) {
const args = {}
cmd.options.forEach(o => {
const key = camelize(o.long.replace(/^--/, ''))
// if an option is not present and Command has a method with the same name
// it should not be copied
if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') {
args[key] = cmd[key]
}
})
return args
}
上面,我只拷貝了相關(guān)代碼。
我們可以看到,create命令是通過commander創(chuàng)建的,這是個(gè)轉(zhuǎn)換命令行的工具,簡單說就是將vue create xxx --a aaa --b vvv這些東西格式化。cleanArgs函數(shù),將program屬性想換為配置對(duì)象,便于后面的操作
其中process是node的線程也是就是這次命令的線程,它可以用來獲取當(dāng)前目錄以及變量等,當(dāng)然也可以通過它異常結(jié)束。比如process.argv.slice(3)這因?yàn)樵诿钚兄?,第一個(gè)變量是自己本身,而vue create分別占據(jù)第二第三變量,所需從第四個(gè)起。
minimist也是同樣,起到格式化的作用,詳見它的文檔。
接下來,它調(diào)用../lib/create腳本。
create
第二個(gè)腳本
const fs = require('fs-extra')
const path = require('path')
const chalk = require('chalk')
const inquirer = require('inquirer')
const Creator = require('./Creator')
const { clearConsole } = require('./util/clearConsole')
const { getPromptModules } = require('./util/createTools')
const { error, stopSpinner, exit } = require('@vue/cli-shared-utils')
const validateProjectName = require('validate-npm-package-name')
async function create (projectName, options) {
if (options.proxy) {
process.env.HTTP_PROXY = options.proxy
}
const cwd = options.cwd || process.cwd()
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative('../', cwd) : projectName
const targetDir = path.resolve(cwd, projectName || '.')
const result = validateProjectName(name)
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: "${name}"`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim('Error: ' + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim('Warning: ' + warn))
})
exit(1)
}
if (fs.existsSync(targetDir)) {
if (options.force) {
await fs.remove(targetDir)
} else {
await clearConsole()
if (inCurrent) {
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: `Generate project in current directory?`
}
])
if (!ok) {
return
}
} else {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Merge', value: 'merge' },
{ name: 'Cancel', value: false }
]
}
])
if (!action) {
return
} else if (action === 'overwrite') {
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
await fs.remove(targetDir)
}
}
}
}
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options)
}
module.exports = (...args) => {
return create(...args).catch(err => {
stopSpinner(false) // do not persist
error(err)
if (!process.env.VUE_CLI_TEST) {
process.exit(1)
}
})
}
module.exports = (...args)接受不定參,然后,不管是傳入也好調(diào)用也好,都是固定的兩個(gè)參數(shù),不明白這里的目的,偷懶?
這里的代碼大部分都是對(duì)命令行參數(shù)的校驗(yàn)與解析,不存在難點(diǎn)。
最后,通過new Creator(name, targetDir, getPromptModules())創(chuàng)建創(chuàng)建者對(duì)象,以及執(zhí)行create。所以重點(diǎn)在Creator對(duì)象中。
Creator
太長了!??所以這部分不細(xì)說了,大體上與之前一致,只不過復(fù)雜點(diǎn)。值得一提的是Creator繼承了EventEmitter,所以它在創(chuàng)建過程的不同階段發(fā)布事件,根據(jù)這個(gè),我們可以拆分著看。
this.emit('creation', { event: 'fetch-remote-preset' })
this.emit('creation', { event: 'creating' })
this.emit('creation', { event: 'git-init' })
this.emit('creation', { event: 'plugins-install' })
this.emit('creation', { event: 'invoking-generators' })
this.emit('creation', { event: 'deps-install' })
this.emit('creation', { event: 'completion-hooks' })
this.emit('creation', { event: 'done' })
一共有以上幾個(gè)事件,當(dāng)然,也有可能由于配置原因,某些步驟會(huì)跳過。比如設(shè)置shouldInitGit為false。
大體上就是這樣。唉,這可能是唯一的一篇講解Vue CLI源碼的文章,太忙了(我是后端,后端,后端~~)
本文作者:Mr.J
本文鏈接:https://www.dnocm.com/articles/beechnut/vue-cli-create/
版權(quán)聲明:本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協(xié)議。轉(zhuǎn)載請(qǐng)注明出處!