vue-cli,vue/cli-service,vue/cli-ui等
Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,通过 @vue/cli
实现的交互式的项目脚手架。通过 @vue/cli
+ @vue/cli-service-global
实现的零配置原型开发。
@vue/cli-service
),该依赖:Vue CLI 致力于将 Vue 生态中的工具基础标准化。
CLI 服务是构建于 webpack 和 webpack-dev-server 之上的。它包含了:
vue-cli-service
命令,提供 serve
、build
和 inspect
命令CLI 插件是向你的 Vue 项目提供可选功能的 npm 包,当你在项目内部运行 vue-cli-service
命令时,它会自动解析并加载 package.json
中列出的所有 CLI 插件。包含serve,build, inspect命令
vue-cli-service serve
命令会启动一个开发服务器 (基于 webpack-dev-server) 并附带开箱即用的模块热重载 (Hot-Module-Replacement)。
vue-cli-service build
会在 dist/
目录产生一个可用于生产环境的包,带有 JS/CSS/HTML 的压缩,和为更好的缓存而做的自动的 vendor chunk splitting。它的 chunk manifest 会内联在 HTML 里。
vue-cli-service inspect
来审查一个 Vue CLI 项目的 webpack config。
进入主题,使用vue-cli创建一个项目
从lib文件夹下的create.js文件入手
首先获取和校验工程名称,如果名称包含.,则取相对路径如果出错会报错直接退出
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative('../', cwd) : projectName
const targetDir = path.resolve(cwd, projectName || '.')
const result = validateProjectName(name)
接下来就要判断该工程是否已经存在,已存在则判断是否强制移除,强制则直接移除
if (options.force) {
await fs.remove(targetDir)
}
如果工程名称包含. 则询问是否在当前目录创建工程,OK则继续往下
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: `Generate project in current directory?`
}
])
if (!ok) {
return
}
否则就询问覆盖、合并、取消,如果命令等于覆盖,则删除当前工程
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)
}
顺着代码我们走到了creator.js文件
首先是输入输出的提示,我们来看这个函数resolveIntroPrompts,获取输出提示函数resolveOutroPrompts
通过getPresets获取默认的配置,默认在你电脑用户目录下的 .vuerc文件。选择你需要类型确认即可
resolveIntroPrompts () {
const presets = this.getPresets()
const presetChoices = Object.entries(presets).map(([name, preset]) => {
let displayName = name
if (name === 'default') {
displayName = 'Default'
} else if (name === '__default_vue_3__') {
displayName = 'Default (Vue 3)'
}
return {
name: `${displayName} (${formatFeatures(preset)})`,
value: name
}
})
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a preset:`,
choices: [
...presetChoices,
{
name: 'Manually select features',
value: '__manual__'
}
]
}
const featurePrompt = {
name: 'features',
when: isManualMode,
type: 'checkbox',
message: 'Check the features needed for your project:',
choices: [],
pageSize: 10
}
return {
presetPrompt,
featurePrompt
}
}
获取各个模块的提示配置,并注入PromptModuleAPI:
getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
const promptAPI = new PromptModuleAPI(this)
promptModules.forEach(m => m(promptAPI))
开始运行我们的creator的create函数:
首先是默认配置中存在cliOptions.preset则走resolvePreset函数,后者inlinePreset
存在直接返回,否则返回默认的配置内容
resolvePreset:
如果默认配置中存在则直接返回,判断是本地文件或者远程文件,相应的进行加载
const savedPresets = this.getPresets()
if (name in savedPresets) {
preset = savedPresets[name]
} else if (name.endsWith('.json') || /^\./.test(name) || path.isAbsolute(name)) {
preset = await loadLocalPreset(path.resolve(name))
} else if (name.includes('/')) {
log(`Fetching remote preset ${chalk.cyan(name)}...`)
this.emit('creation', { event: 'fetch-remote-preset' })
try {
preset = await loadRemotePreset(name, clone)
} catch (e) {
error(`Failed fetching remote preset ${chalk.cyan(name)}:`)
throw e
}
}
接下来注入cli-service插件
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
判断router和vuex是否配置,如果配置则添加该插件
// 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.json的devDependencies中
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
})
生成package.json文件,判断packageManager的管理方式并生成相应的文件
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
如果是测试模式则直接链接到cli-service,否则执行安装依赖
setupDevProject (targetDir) {
return linkBin(
require.resolve('@vue/cli-service/bin/vue-cli-service'),
path.join(targetDir, 'node_modules', '.bin', 'vue-cli-service')
)
}
if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
// in development, avoid installation process
await require('./util/setupDevProject')(context)
} else {
await pm.install()
}
加载各个插件和插件的提示:
resolvePlugins (rawPlugins, pkg) {
// ensure cli-service is invoked first
rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
const plugins = []
for (const id of Object.keys(rawPlugins)) {
const apply = loadModule(`${id}/generator`, this.context) || (() => {})
let options = rawPlugins[id] || {}
if (options.prompts) {
let pluginPrompts = loadModule(`${id}/prompts`, this.context)
if (pluginPrompts) {
const prompt = inquirer.createPromptModule()
if (typeof pluginPrompts === 'function') {
pluginPrompts = pluginPrompts(pkg, prompt)
}
if (typeof pluginPrompts.getPrompts === 'function') {
pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
}
log()
log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
options = await prompt(pluginPrompts)
}
}
plugins.push({ id, apply, options })
}
return plugins
}
生成generate并执行
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs
})
await generator.generate({
extractConfigFiles: preset.useConfigFiles
})
接下来解析generate函数里的内容:
async generate ({
extractConfigFiles = false,
checkExisting = false
} = {}) {
await this.initPlugins()
// save the file system before applying plugin for comparison
const initialFiles = Object.assign({}, this.files)
// extract configs from package.json into dedicated files.
this.extractConfigFiles(extractConfigFiles, checkExisting)
// wait for file resolve
await this.resolveFiles()
// set package.json
this.sortPkg()
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
// write/update file tree to disk
await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
}
我们看一下initPlugins这个方法做了什么
遍历所有插件allPlugins,plugins, 并将插件回调放入afterAnyInvokeCbs
并执行插件的hooks。
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)
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的render函数的主要作用,
比较有趣的是这个函数extractCallDir,通过错误栈来获取路径,并通过正则匹配获取模板的根目录:
function extractCallDir () {
// extract api.render() callsite file location using error stack
const obj = {}
Error.captureStackTrace(obj)
const callSite = obj.stack.split('\n')[3]
// the regexp for the stack when called inside a named function
const namedStackRegExp = /\s\((.*):\d+:\d+\)$/
// the regexp for the stack when called inside an anonymous
const anonymousStackRegExp = /at (.*):\d+:\d+$/
let matchResult = callSite.match(namedStackRegExp)
if (!matchResult) {
matchResult = callSite.match(anonymousStackRegExp)
}
const fileName = matchResult[1]
return path.dirname(fileName)
}
主要是这个render函数:
首先判断source是字符串,对象或者是函数,获取cli-service/generator/template目录下所有文件,如果想要新增自定义模板,可以在template增删文件。通过globby解析获取文件名,加载文件内容放入this.files中在后面的resolveFiles文件中执行
for (const middleware of this.fileMiddlewares) {
await middleware(files, ejs.render)
}
render (source, additionalData = {}, ejsOptions = {}) {
const baseDir = extractCallDir()
if (isString(source)) {
source = path.resolve(baseDir, source)
this._injectFileMiddleware(async (files) => {
const data = this._resolveData(additionalData)
const globby = require('globby')
const _files = await globby(['**/*'], { cwd: source, dot: true })
for (const rawPath of _files) {
const targetPath = rawPath.split('/').map(filename => {
// dotfiles are ignored when published to npm, therefore in templates
// we need to use underscore instead (e.g. "_gitignore")
if (filename.charAt(0) === '_' && filename.charAt(1) !== '_') {
return `.${filename.slice(1)}`
}
if (filename.charAt(0) === '_' && filename.charAt(1) === '_') {
return `${filename.slice(1)}`
}
return filename
}).join('/')
const sourcePath = path.resolve(source, rawPath)
const content = renderFile(sourcePath, data, ejsOptions)
// only set file if it's not all whitespace, or is a Buffer (binary files)
if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
files[targetPath] = content
}
}
})
} else if (isObject(source)) {
this._injectFileMiddleware(files => {
const data = this._resolveData(additionalData)
for (const targetPath in source) {
const sourcePath = path.resolve(baseDir, source[targetPath])
const content = renderFile(sourcePath, data, ejsOptions)
if (Buffer.isBuffer(content) || content.trim()) {
files[targetPath] = content
}
}
})
} else if (isFunction(source)) {
this._injectFileMiddleware(source)
}
}
resolveFiles的具体内容,./util/codemods/injectImports, './util/codemods/injectOptions'加载默认配置文件
async resolveFiles () {
const files = this.files
// get project files invoke render函数
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 把模板写入根目录下,一个模板就已经下载好了
writeFileTree (dir, files, previousFiles, include) {
if (process.env.VUE_CLI_SKIP_WRITE) {
return
}
if (previousFiles) {
await deleteRemovedFiles(dir, files, previousFiles)
}
Object.keys(files).forEach((name) => {
if (include && !include.has(name)) return
const filePath = path.join(dir, name)
fs.ensureDirSync(path.dirname(filePath))
fs.writeFileSync(filePath, files[name])
})
}
写入readme.md文件
if (!generator.files['README.md']) {
// generate README.md
log()
log(' Generating README.md...')
await writeFileTree(context, {
'README.md': generateReadme(generator.pkg, packageManager)
})
}
是否需要初始化git仓库,shouldInitGit。
vue-cli下还有add, invoke, inspect文件, add和invoke是在项目中添加所需插件。