vue-cli 基本原理

编写一个简单的 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-appconst 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 = nameif (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 stackconst obj = {}Error.captureStackTrace(obj)const callSite = obj.stack.split('\n')[3]// the regexp for the stack when called inside a named functionconst namedStackRegExp = /\s\((.*):\d+:\d+\)$/// the regexp for the stack when called inside an anonymousconst 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 = idthis.generator = generatorthis.options = optionsthis.rootOptions = rootOptionsthis.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 = contextthis.plugins = pluginsthis.pkg = Object.assign({}, pkg)this.files = filesthis.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.filesfor (const middleware of this.fileMiddlewares) {await middleware(files, ejs.render)}}async initPlugins () {const rootOptions = this.rootOptionsfor (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 presetif (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运行
评论
