vue-cli 原理分析
Posted 前端精髓
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue-cli 原理分析相关的知识,希望对你有一定的参考价值。
1、实现交互式命令行
const inquirer = require('inquirer')
const path = require('path')
const fs = require('fs')
const execa = require('execa')
const Module = require('module')
const ejs = require('ejs')
const isManualMode = answers => answers.preset === '__manual__'
const context = path.resolve(__dirname, 'my-app') // 假设要输出到 my-app 文件
const name = 'my-app' // vue create my-app
const promptCompleteCbs = [
(answers, options) => {
if (answers.features.includes('vuex')) {
options.plugins['@vue/cli-plugin-vuex'] = {}
}
}
]
const defaultPreset = {
useConfigFiles: false,
cssPreprocessor: undefined,
plugins: {
'@vue/cli-plugin-babel': {},
'@vue/cli-plugin-eslint': {
config: 'base',
lintOn: ['save']
}
}
}
const presets = {
'default': Object.assign({ vueVersion: '2' }, defaultPreset),
'__default_vue_3__': Object.assign({ vueVersion: '3' }, defaultPreset)
}
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}`,
value: name
}
})
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a preset:`,
choices: [
...presetChoices,
{
name: 'Manually select features',
value: '__manual__'
}
]
}
let features = [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
]
const featurePrompt = {
name: 'features',
when: isManualMode,
type: 'checkbox',
message: 'Check the features needed for your project:',
choices: features,
pageSize: 10
}
const prompts = [
presetPrompt,
featurePrompt
]
async function create () {
let answers = await inquirer.prompt(prompts);
console.log(answers)
}
create()
处理 preset 以及里面的 plugins
async function create () {
let answers = await inquirer.prompt(prompts);
console.log(answers)
let preset; // { plugins: { @vue/cli-service: {} } }
// answers.preset = __manual__ | default | __default_vue_3__
if (answers.preset !== '__manual__') {
preset = presets[answers.preset]
} else {
preset = {
useConfigFiles: false,
plugins: {}
}
}
promptCompleteCbs.forEach(cb => cb(answers, preset))
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
}
构造一个package并且写入文件系统
async function writeFileTree (dir, files) {
Object.keys(files).forEach((name) => {
const filePath = path.join(dir, name)
fs.ensureDirSync(path.dirname(filePath))
fs.writeFileSync(filePath, files[name])
})
}
async function create () {
let answers = await inquirer.prompt(prompts);
console.log(answers)
let preset
if (answers.preset !== '__manual__') {
preset = presets[answers.preset]
} else {
preset = {
useConfigFiles: false,
plugins: {}
}
}
promptCompleteCbs.forEach(cb => cb(answers, preset))
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
// 构造一个pkg
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {}
}
// 往devDependencies加依赖
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
pkg.devDependencies[dep] = 'latest'
})
// 把package.json写入文件系统
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
}
安装依赖解析插件
function run (command, args) {
return execa(command, args, { cwd: context })
}
function loadModule (request, context) {
Module.createRequire(path.resolve(context, 'package.json'))(request)
// require('@vue/cli-service/generator')
}
async function resolvePlugins (rawPlugins, pkg) {
const plugins = []
for (const id of Object.keys(rawPlugins)) {
// loadModule(`@vue/cli-service/generator`, context)
const apply = loadModule(`${id}/generator`, context) || (() => {})
let options = rawPlugins[id] || {}
plugins.push({ id, apply, options })
}
// 返回了插件的数组,注意每个插件都有属性 id, apply, options
return plugins
}
async function create () {
let answers = await inquirer.prompt(prompts);
console.log(answers)
let preset
if (answers.preset !== '__manual__') {
preset = presets[answers.preset]
} else {
preset = {
useConfigFiles: false,
plugins: {}
}
}
promptCompleteCbs.forEach(cb => cb(answers, preset))
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {}
}
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
pkg.devDependencies[dep] = 'latest'
})
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
// 安装依赖
await run('npm', ['install'])
// 解析插件
const plugins = await resolvePlugins(preset.plugins, pkg)
}
调用生成器函数
async function create () {
let answers = await inquirer.prompt(prompts);
console.log(answers)
let preset
if (answers.preset !== '__manual__') {
preset = presets[answers.preset]
} else {
preset = {
useConfigFiles: false,
plugins: {}
}
}
promptCompleteCbs.forEach(cb => cb(answers, preset))
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {}
}
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
pkg.devDependencies[dep] = 'latest'
})
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
await run('npm', ['install'])
const plugins = await resolvePlugins(preset.plugins, pkg)
// 调用生成器函数
const generator = new Generator(context, {
pkg,
plugins,
})
await generator.generate()
}
Generator 生成器
class Generator {
constructor (context, {
pkg = {},
plugins = [],
files = {}
}) {
this.context = context
this.plugins = plugins
this.pkg = Object.assign({}, pkg)
this.files = files
this.fileMiddlewares = []
const cliService = plugins.find(p => p.id === '@vue/cli-service')
this.rootOptions = cliService.options
}
async generate () {
// 初始化插件
await this.initPlugins()
}
async initPlugins () {
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
// 为每个插件创建一个生成器API对象
const api = new GeneratorAPI(id, this, options, this.rootOptions)
// 调用apply方法
await apply(api, options, rootOptions)
}
}
}
为每个插件创建一个GeneratorAPI对象
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)
}
class GeneratorAPI {
constructor (id, generator, options, rootOptions) {
this.id = id
this.generator = generator
this.options = options
this.rootOptions = rootOptions
this.pluginsData = generator.plugins
}
// 往fileMiddlewares里面添加了函数
_injectFileMiddleware (middleware) {
this.generator.fileMiddlewares.push(middleware)
}
_resolveData (additionalData) {
return Object.assign({
options: this.options,
rootOptions: this.rootOptions,
plugins: this.pluginsData
}, additionalData)
}
// render用来渲染模板的方法
render (source, additionalData = {}, ejsOptions = {}) {
const baseDir = extractCallDir()
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
}
}
})
}
}
}
真正执行中间件
class Generator {
constructor (context, {
pkg = {},
plugins = [],
files = {}
}) {
this.context = context
this.plugins = plugins
this.pkg = Object.assign({}, pkg)
this.files = files
this.fileMiddlewares = []
const cliService = plugins.find(p => p.id === '@vue/cli-service')
this.rootOptions = cliService.options
}
async generate () {
await this.initPlugins()
// 初始化之后得到了 fileMiddlewares
await this.resolveFiles()
}
async resolveFiles () { // 真正执行中间件
const files = this.files
for (const middleware of this.fileMiddlewares) {
await middleware(files, ejs.render)
}
}
async initPlugins () {
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
const api = new GeneratorAPI(id, this, options, this.rootOptions)
await apply(api, options, rootOptions)
}
}
}
最后写入文件系统
class Generator {
以上是关于vue-cli 原理分析的主要内容,如果未能解决你的问题,请参考以下文章