vue-cli 基本原理

共 7769字,需浏览 16分钟

 ·

2021-08-31 09:04


编写一个简单的 vue-cli 就可以轻松明白原理是怎么运行的了。理解 cli 的插件形式开发,并且可以选择配置,下载不同的模板内容。


const inquirer = require('inquirer')const path = require('path')const fs = require('fs-extra')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 isString = val => typeof val === 'string'const isFunction = val => typeof val === 'function'const isObject = val => val && typeof val === 'object'
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]
function run (command, args) { return execa(command, args, { cwd: context })}
function loadModule (request, context) { return Module.createRequire(path.resolve(context, 'package.json'))(request)}
async function resolvePlugins (rawPlugins, pkg) { const plugins = [] for (const id of Object.keys(rawPlugins)) { const apply = loadModule(`${id}/generator`, context) || (() => {}) let options = rawPlugins[id] || {} plugins.push({ id, apply, options }) } return plugins}
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)}
function renderFile (name, data, ejsOptions) { const template = fs.readFileSync(name, 'utf-8')
let finalTemplate = template.trim() + `\n`
return ejs.render(finalTemplate, data, ejsOptions)}
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]) })}
class GeneratorAPI { constructor (id, generator, options, rootOptions) { this.id = id this.generator = generator this.options = options this.rootOptions = rootOptions this.pluginsData = generator.plugins } _injectFileMiddleware (middleware) { this.generator.fileMiddlewares.push(middleware) } _resolveData (additionalData) { return Object.assign({ options: this.options, rootOptions: this.rootOptions, plugins: this.pluginsData }, additionalData) } extendPackage (fields, options = {}) { // 合并两个package } render (source, additionalData = {}, ejsOptions = {}) { const baseDir = extractCallDir() console.log(source, 'source') if (isString(source)) { source = path.resolve(baseDir, source) // 找到了插件的tempalte目录 // 放到 fileMiddlewares 数组里面去,并没有执行中间件 this._injectFileMiddleware(async (files) => { const data = this._resolveData(additionalData) const globby = require('globby') const _files = await globby(['**/*'], { cwd: source, dot: true }) // 模板里面是 _gitignore 要变 .gitignore 文件,防止被忽略 for (const rawPath of _files) { const targetPath = rawPath.split('/').map(filename => { 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) if (Buffer.isBuffer(content) || /[^\s]/.test(content)) { 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() await this.resolveFiles() this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n' // 写入文件系统 await writeFileTree(this.context, this.files) } async resolveFiles () { // GeneratorAPI 里面的render方法修改了 fileMiddlewares,最后在这里执行了 const files = this.files for (const middleware of this.fileMiddlewares) { await middleware(files, ejs.render) } } async initPlugins () { const rootOptions = this.rootOptions for (const plugin of this.plugins) { const { id, apply, options } = plugin // 插件generator文件导出的函数在这里执行 const api = new GeneratorAPI(id, this, options, rootOptions) await apply(api, options, rootOptions) } }}
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) // 暂时用一个我自己写的cli插件 preset.plugins['cli-plugin-demo'] = {}
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) })
console.log(`⚙\u{fe0f} Installing CLI plugins. This might take a while...`) await run('npm', ['install'])
console.log(`🚀 Invoking generators...`) // [{ id, apply, options }] id 插件的名字, apply 插件generator文件导出的函数,options 是参数 const plugins = await resolvePlugins(preset.plugins, pkg) const generator = new Generator(context, { pkg, plugins, }) await generator.generate()
console.log(`📦 Installing additional dependencies...`) await run('npm', ['install'])
console.log(`🎉 Successfully created project ${name}.`)}
create()// 最后使用node运行



浏览 40
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报