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-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运行
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值