vue-cli及create-react-app源码分析

柳高卓
2023-12-01

vue-cli及create-react-app 工具调研

vue-cli v3项目创建过程分析

vue-cli的运行命令为vue create [projectName]

在vue-cli项目v3分支中,找到package/@vue/cli/bin/vue.js,这个文件里定义了vue-cli的相关命令:

program
  .command('create <app-name>')
  .description('create a new project powered by vue-cli-service')
	// ...
  .action((name, cmd) => {
    const options = cleanArgs(cmd)

    if (minimist(process.argv.slice(3))._.length > 1) {
      console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))
    }
    // --git makes commander to default git to true
    if (process.argv.includes('-g') || process.argv.includes('--git')) {
      options.forceGit = true
    }
    require('../lib/create')(name, options)
  })

可以看到这个命令最后加载了package/@vue/cli/lib/create.js这个模块:

module.exports = (...args) => {
  return create(...args).catch(err => {
    stopSpinner(false) // do not persist
    error(err)
    if (!process.env.VUE_CLI_TEST) {
      process.exit(1)
    }
  })
}

执行了create方法,

async function create (projectName, options) {
  // 项目存放地址校验,项目名校验
	// getPromptModules() 获取需要确认的模块
  // 'babel','typescript'等模块
  const creator = new Creator(name, targetDir, getPromptModules())
  await creator.create(options)
}

Creator的构造方法是:

constructor (name, context, promptModules) {
  super()

  this.name = name
  this.context = process.env.VUE_CLI_CONTEXT = context
  const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
  this.presetPrompt = presetPrompt
  this.featurePrompt = featurePrompt
  this.outroPrompts = this.resolveOutroPrompts()
  this.injectedPrompts = []
  this.promptCompleteCbs = []
  this.createCompleteCbs = []

  this.run = this.run.bind(this)

  const promptAPI = new PromptModuleAPI(this)
  promptModules.forEach(m => m(promptAPI))
}

这一段初始化代码主要做的事情是将一些需要用户确认的模块push到对应的成员变量里,PromptModuleAPI的构造方法是:

injectFeature (feature) {
  this.creator.featurePrompt.choices.push(feature)
}

injectPrompt (prompt) {
  this.creator.injectedPrompts.push(prompt)
}

injectOptionForPrompt (name, option) {
  this.creator.injectedPrompts.find(f => {
    return f.name === name
  }).choices.push(option)
}

onPromptComplete (cb) {
  this.creator.promptCompleteCbs.push(cb)
}

需要确认的模块在package/@vue/cli/lib/util/createTools.js中定义:

exports.getPromptModules = () => {
  return [
    'babel',
    'typescript',
    'pwa',
    'router',
    'vuex',
    'cssPreprocessors',
    'linter',
    'unit',
    'e2e'
  ].map(file => require(`../promptModules/${file}`))
}

接下来看create方法,这个方法很长,用文字代替

async create (cliOptions = {}, preset = null) {
	// 1. 对项目预设配置进行初始化
  // 2. 给preset添加plugin @vue/cli-service
  // 3. 确定依赖管理工具 yarn pnpm npm
  // 4. 初始化package.json内容,检查预制依赖的版本
  // 5. 是否使用git init
  // 6. 安装依赖
  // 7. generator
    const generator = new Generator(context, {
      pkg, // package.json的内容
      plugins, // preset.plugin
      completeCbs: createCompleteCbs
    })
    await generator.generate({
      extractConfigFiles: preset.useConfigFiles
    })
  // 8. 创建README.md文件
  // 9. 根据命令行参数决定是否执行git commit -m"init"
}

在第七步的源码中,我们可以看到创建了一个Generator对象,并执行了它的generate方法。构造参数中的plugins为:

async resolvePlugins (rawPlugins) {
  // ensure cli-service is invoked first
  rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
  const plugins = []
  for (const id of Object.keys(rawPlugins)) {
    // 注意这里的apply,加载了@vue/cli-service/generator模块
    const apply = loadModule(`${id}/generator`, this.context) || (() => {})
    let options = rawPlugins[id] || {}
    if (options.prompts) {
      const prompts = loadModule(`${id}/prompts`, this.context)
      if (prompts) {
        log()
        log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
        options = await inquirer.prompt(prompts)
      }
    }
    plugins.push({ id, apply, options })
  }
  return plugins
}

loadModule方法定义在packages/@vue/cli-shared-utils/lib/module.js

exports.loadModule = function (request, context, force = false) {
  const resolvedPath = exports.resolveModule(request, context)
  if (resolvedPath) {
    if (force) {
      clearRequireCache(resolvedPath)
    }
    return require(resolvedPath)
  }
}

Generator构造方法中对传进来的plugins进行了处理,调用了apply方法。

plugins.forEach(({ id, apply, options }) => {
  const api = new GeneratorAPI(id, this, options, rootOptions)
  apply(api, options, rootOptions, invoking)
})

因此我们回过头去看@vue/cli-service/generator这个模块做了什么,这个模块的文件列表如下:

├── index.js
├── router
│   ├── index.js
│   └── template
│       └── src
├── template // 代码模板
│   ├── _gitignore
│   ├── public
│   │   ├── favicon.ico
│   │   └── index.html
│   └── src
│       ├── App.vue
│       ├── assets
│       ├── components
│       └── main.js
└── vuex
    ├── index.js
    └── template
        └── src

index.js

module.exports = (api, options) => {
  api.render('./template', {
    doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript')
  })
	// 下面的代码是,根据命令行参数扩展package.json的内容
  // 如增加router依赖 vuex依赖 less sass 等等依赖
}

可以看到执行apply(api, options, rootOptions, invoking)这句代码实际上就是执行了api.render方法,因为apiGeneratorAPI的实例,找到它的render方法:

  /**
   * Render template files into the virtual files tree object.
   *
   * @param {string | object | FileMiddleware} source -
   *   Can be one of:
   *   - relative path to a directory;
   *   - Object hash of { sourceTemplate: targetFile } mappings;
   *   - a custom file middleware function.
   * @param {object} [additionalData] - additional data available to templates.
   * @param {object} [ejsOptions] - options for ejs.
   */
  render (source, additionalData = {}, ejsOptions = {}) {
    const baseDir = extractCallDir()
    if (isString(source)) {
      source = path.resolve(baseDir, source)
      // _injectFileMiddleware 方法定义为
      //    this.generator.fileMiddlewares.push(middleware)
      this._injectFileMiddleware(async (files) => {
        const data = this._resolveData(additionalData)
        const globby = require('globby')
        const _files = await globby(['**/*'], { cwd: source })
        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
          }
        }
      })
    }
    // 其他对于source类型的判断

实际上是将一个箭头函数push到了generatorfileMiddlewares数组里,generator实际上就是new GeneratorAPI时传进去的this

generate方法定义为:

async generate ({
  extractConfigFiles = false,
  checkExisting = false
} = {}) {
  // 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() // 此处执行了fileMiddlewares内部的方法
  // 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)
}

resolveFiles中执行了数组中存放的箭头函数:

async resolveFiles () {
  const files = this.files
  for (const middleware of this.fileMiddlewares) {
    await middleware(files, ejs.render) // 此处执行之前push进去的箭头函数
  }

  // 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] = runCodemod(
        require('./util/codemods/injectImports'),
        { path: file, source: files[file] },
        { imports }
      )
    }

    let injections = this.rootOptions[file]
    injections = injections instanceof Set ? Array.from(injections) : injections
    if (injections && injections.length > 0) {
      files[file] = runCodemod(
        require('./util/codemods/injectOptions'),
        { path: file, source: files[file] },
        { injections }
      )
    }
  })

  for (const postProcess of this.postProcessFilesCbs) {
    await postProcess(files)
  }
  debug('vue:cli-files')(this.files)
}

回到之前的箭头函数,方法体中执行了renderFile方法,

function renderFile (name, data, ejsOptions) {
  if (isBinaryFileSync(name)) {
    return fs.readFileSync(name) // return buffer
  }
  const template = fs.readFileSync(name, 'utf-8')

  // custom template inheritance via yaml front matter.
  // ---
  // extend: 'source-file'
  // replace: !!js/regexp /some-regex/
  // OR
  // replace:
  //   - !!js/regexp /foo/
  //   - !!js/regexp /bar/
  // ---
  const yaml = require('yaml-front-matter')
  const parsed = yaml.loadFront(template)
  const content = parsed.__content
  let finalTemplate = content.trim() + `\n`

  if (parsed.when) {
    finalTemplate = (
      `<%_ if (${parsed.when}) { _%>` +
        finalTemplate +
      `<%_ } _%>`
    )

    // use ejs.render to test the conditional expression
    // if evaluated to falsy value, return early to avoid extra cost for extend expression
    const result = ejs.render(finalTemplate, data, ejsOptions)
    if (!result) {
      return ''
    }
  }

  if (parsed.extend) {
    const extendPath = path.isAbsolute(parsed.extend)
      ? parsed.extend
      : resolve.sync(parsed.extend, { basedir: path.dirname(name) })
    finalTemplate = fs.readFileSync(extendPath, 'utf-8')
    if (parsed.replace) {
      if (Array.isArray(parsed.replace)) {
        const replaceMatch = content.match(replaceBlockRE)
        if (replaceMatch) {
          const replaces = replaceMatch.map(m => {
            return m.replace(replaceBlockRE, '$1').trim()
          })
          parsed.replace.forEach((r, i) => {
            finalTemplate = finalTemplate.replace(r, replaces[i])
          })
        }
      } else {
        finalTemplate = finalTemplate.replace(parsed.replace, content.trim())
      }
    }
  }

  return ejs.render(finalTemplate, data, ejsOptions)
}

最后是调用了ejs.render方法来将代码模版输出成正常的代码文件.

vue-cli的代码组织方式

preset(项目预设配置)可以使用默认的vue配置,也可以通过命令行参数preset来指定远程github或者其他类gitpreset存放地址,相关代码如下

if (!preset) {
  if (cliOptions.preset) {
    // vue create foo --preset bar
    preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
  } else if (cliOptions.default) {
    // vue create foo --default
    preset = defaults.presets.default
  } else if (cliOptions.inlinePreset) {
    // vue create foo --inlinePreset {...}
    try {
      preset = JSON.parse(cliOptions.inlinePreset)
    } catch (e) {
      error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
      exit(1)
    }
  } else {
    preset = await this.promptAndResolvePreset()
  }
}

resolvePreset部分源码:

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('/')) {
  logWithSpinner(`Fetching remote preset ${chalk.cyan(name)}...`)
  this.emit('creation', { event: 'fetch-remote-preset' })
  try {
    // 拉取远程预设
    preset = await loadRemotePreset(name, clone)
    stopSpinner()
  } catch (e) {
    stopSpinner()
    error(`Failed fetching remote preset ${chalk.cyan(name)}:`)
    throw e
  }
}

loadRemotePreset:

const fs = require('fs-extra')
const loadPresetFromDir = require('./loadPresetFromDir')

module.exports = async function fetchRemotePreset (name, clone) {
  const os = require('os')
  const path = require('path')
  const download = require('download-git-repo')
  const tmpdir = path.join(os.tmpdir(), 'vue-cli')

  // clone will fail if tmpdir already exists
  // https://github.com/flipxfx/download-git-repo/issues/41
  if (clone) {
    await fs.remove(tmpdir)
  }

  await new Promise((resolve, reject) => {
    download(name, tmpdir, { clone }, err => {
      if (err) return reject(err)
      resolve()
    })
  })

  return await loadPresetFromDir(tmpdir)
}

create-react-app项目创建过程分析

create-react-app命令入口:/packages/create-react-app/index.js

对当前版本信息进行了判断,然后引入createReactApp文件

'use strict';

var currentNodeVersion = process.versions.node;
var semver = currentNodeVersion.split('.');
var major = semver[0];

if (major < 8) {
  console.error(
    'You are running Node ' +
      currentNodeVersion +
      '.\n' +
      'Create React App requires Node 8 or higher. \n' +
      'Please update your version of Node.'
  );
  process.exit(1);
}

require('./createReactApp');

文件开头定义了create-react-app启动命令: create-react-app

let projectName;

const program = new commander.Command(packageJson.name)
  .version(packageJson.version)
  .arguments('<project-directory>')
  .usage(`${chalk.green('<project-directory>')} [options]`)
  .action(name => {
    projectName = name;
  })
// ... 其他log

projectName不为空的时候,执行createApp方法

createApp(
  projectName,
  program.verbose, // --verbose
  program.scriptsVersion, // --scripts-version 用于指定react-script版本
  program.template, // 代码模板 ts or js
  program.useNpm, // --use-npm
  program.usePnp, // --use-pnp
  program.typescript // --typescript 是否启用typescript模板
);

看一下createApp方法定义:

function createApp(
  name,
  verbose,
  version,
  template,
  useNpm,
  usePnp,
  useTypeScript
) {
  //  ...
  // 检查node版本并设置react-scripts初始版本为@0.9.x
  // ...

  const root = path.resolve(name); // 初始化的项目目录路径
  const appName = path.basename(root); // 看起来似乎appName === name...

  checkAppName(appName); // 检查app名称是否符合规范
  fs.ensureDirSync(name); // 确保当前目录存在,不存在则创建此目录
  // 判断目录下是否存在一些隐藏文件 比如:.DS_Store .git等等 存在即返回false
  if (!isSafeToCreateProjectIn(root, name)) {
    process.exit(1);
  }

  console.log(`Creating a new React app in ${chalk.green(root)}.`);
  console.log();

  const packageJson = {
    name: appName,
    version: '0.1.0',
    private: true,
  };
  // 在当前目录下创建package.json文件
  fs.writeFileSync(
    path.join(root, 'package.json'),
    JSON.stringify(packageJson, null, 2) + os.EOL // EOL \n => POSIX \r\n -> windows
  );

  const useYarn = useNpm ? false : shouldUseYarn();
  // 当前工作目录,即项目目录的父级  
  const originalDirectory = process.cwd();
  // 改变cwd -> 项目目录  
  process.chdir(root);
  // 判断当前工作目录是否可以执行npm  
  if (!useYarn && !checkThatNpmCanReadCwd()) {
    process.exit(1);
  }

  // ...  
  // 检查yarn pnp的版本支持 
  // ...
    
  // 兼容之前的版本命令 --typescript
  if (useTypeScript) {
    console.log(
      chalk.yellow(
        'The --typescript option has been deprecated and will be removed in a future release.'
      )
    );
    console.log(
      chalk.yellow(
        `In future, please use ${chalk.cyan('--template typescript')}.`
      )
    );
    console.log();
    if (!template) {
      template = 'typescript';
    }
  }

 // 设置yarn的依赖仓库

  run(
    root, // 项目目录
    appName, // 项目目录名 -> 项目名
    version, // scripts-version 
    verbose, // --verbose
    originalDirectory, // 项目目录的父级目录
    template, // 代码模板 如果使用了--typescript参数 则template为typescript
    useYarn, // boolean 是否使用yarn作为包管理工具
    usePnp // boolean 是否使用pnpm作为包管理工具
  );
}

run方法是一系列的promise链式调用,方法比较多也比较复杂:

function run(
  root,
  appName,
  version,
  verbose,
  originalDirectory,
  template,
  useYarn,
  usePnp
) {
  Promise.all([
    // 返回正确的react-script版本依赖字符串,如react-scripts@3.2.0
    getInstallPackage(version, originalDirectory),
    // 返回代码模板地址 template为空时返回cra-template template为typescript时返回cra-template-typescript
    // 其他根据正则匹配返回本地文件或网络文件地址
    getTemplateInstallPackage(template, originalDirectory),
  ]).then(([packageToInstall, templateToInstall]) => {
    const allDependencies = ['react', 'react-dom', packageToInstall];

    console.log('Installing packages. This might take a couple of minutes.');

    Promise.all([
      // 对于网络地址的解析(.tar等压缩文件,git+等代码库文件)
      // 不是以上地址的直接返回原地址,如cra-template 
      // 对于react-scripts返回对象{name: 'react-scripts', version: '3.2.0'}
      getPackageInfo(packageToInstall),
      getPackageInfo(templateToInstall),
    ])
      .then(([packageInfo, templateInfo]) =>
        // 对yarn离线模式的处理
        checkIfOnline(useYarn).then(isOnline => ({
          isOnline,
          packageInfo,
          templateInfo,
        }))
      )
      .then(({ isOnline, packageInfo, templateInfo }) => {
        // coerce 将字符串转换为semver格式的对象,比如'v2' -> 2.0.0
        let packageVersion = semver.coerce(packageInfo.version);

        // This environment variable can be removed post-release.
        const templatesVersionMinimum = process.env.CRA_INTERNAL_TEST
          ? '3.2.0'
          : '3.3.0';

        // Assume compatibility if we can't test the version.
        // 兼容semver解析错误的情况
        if (!semver.valid(packageVersion)) {
          packageVersion = templatesVersionMinimum;
        }

        // Only support templates when used alongside new react-scripts versions.
        // packageVersion是否大于等于templatesVersionMinimum
        const supportsTemplates = semver.gte(
          packageVersion,
          templatesVersionMinimum
        );
        // 检查template模板和react-script版本是否兼容
        if (supportsTemplates) {
          allDependencies.push(templateToInstall);
        } else if (template) {
          console.log('');
          console.log(
            `The ${chalk.cyan(packageInfo.name)} version you're using ${
              packageInfo.name === 'react-scripts' ? 'is not' : 'may not be'
            } compatible with the ${chalk.cyan('--template')} option.`
          );
          console.log('');
        }

        // TODO: Remove with next major release.
        // 如果template包含typescript 则添加相应的类型声明及依赖
        if (!supportsTemplates && (template || '').includes('typescript')) {
          allDependencies.push(
            '@types/node',
            '@types/react',
            '@types/react-dom',
            '@types/jest',
            'typescript'
          );
        }
				// log...

      // 根据allDependencies的内容执行相关的包管理器依赖下载命令
        return install(
          root,
          useYarn,
          usePnp,
          allDependencies,
          verbose,
          isOnline
        ).then(() => ({
          packageInfo, // {name: 'react-scripts', version: '3.2.0'}
          supportsTemplates, //  packageVersion是否大于等于templatesVersionMinimum 3.2.0或3.3.0
          templateInfo, // cra-template 或 cra-template-typescript 或 远程压缩包 或 .git
         }));
      })
      .then(async ({ packageInfo, supportsTemplates, templateInfo }) => {
        const packageName = packageInfo.name;
        const templateName = supportsTemplates ? templateInfo.name : undefined;
        checkNodeVersion(packageName);
        setCaretRangeForRuntimeDeps(packageName);

        const pnpPath = path.resolve(process.cwd(), '.pnp.js');

        const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];

       // 执行了react-scripts/scripts/init.js
        await executeNodeScript(
          {
            cwd: process.cwd(),
            args: nodeArgs,
          },
          [root, appName, verbose, originalDirectory, templateName],
          `
        var init = require('${packageName}/scripts/init.js');
        init.apply(null, JSON.parse(process.argv[1]));
      `
        );

        // ... 低版本react-scripts的兼容log
      })
      .catch(reason => {
        // 安装失败的log处理说明
      	// ...

        // On 'exit' we will delete these files from target directory.
        const knownGeneratedFiles = [
          'package.json',
          'yarn.lock',
          'node_modules',
        ];
        // 删除以上文件..
      
      	// 对剩余文件的清理,如果还有遗留文件默认不处理,如果没有则删除当前创建的文件目录
        const remainingFiles = fs.readdirSync(path.join(root));
        if (!remainingFiles.length) {
          // Delete target folder if empty
          console.log(
            `Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan(
              path.resolve(root, '..')
            )}`
          );
          process.chdir(path.resolve(root, '..'));
          fs.removeSync(path.join(root));
        }
        console.log('Done.');
        process.exit(1);
      });
  });
}

在最后的promise中通过executeNodeScript方法执行了/package/react-scripts/scripts/init.jsmodule.exports 方法:

function executeNodeScript({ cwd, args }, data, source) {
  return new Promise((resolve, reject) => {
    const child = spawn(
      process.execPath,
      [...args, '-e', source, '--', JSON.stringify(data)],
      { cwd, stdio: 'inherit' }
    );

    child.on('close', code => {
      if (code !== 0) {
        reject({
          command: `node ${args.join(' ')}`,
        });
        return;
      }
      resolve();
    });
  });
}

init文件中的方法比较长,这里删除了一些log及一些不影响程序流的判断分支

module.exports = function(
  appPath,
  appName,
  verbose,
  originalDirectory,
  templateName
) {
  // 创建出来的package.json文件对象
  const appPackage = require(path.join(appPath, 'package.json'));
  const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));

  // ...
	// templateName为空的情况退出程序
  // ...

  const templatePath = path.join(
    require.resolve(templateName, { paths: [appPath] }),
    '..'
  );

  // Copy over some of the devDependencies
  appPackage.dependencies = appPackage.dependencies || {};

  // Setup the script rules
  appPackage.scripts = {
    start: 'react-scripts start',
    build: 'react-scripts build',
    test: 'react-scripts test',
    eject: 'react-scripts eject',
  };

  // Setup the eslint config
  appPackage.eslintConfig = {
    extends: 'react-app',
  };

  // Setup the browsers list
  appPackage.browserslist = defaultBrowsers;

  // 重写package.json文件内容
  fs.writeFileSync(
    path.join(appPath, 'package.json'),
    JSON.stringify(appPackage, null, 2) + os.EOL
  );

  const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
  if (readmeExists) {
    fs.renameSync(
      path.join(appPath, 'README.md'),
      path.join(appPath, 'README.old.md')
    );
  }

  // Copy the files for the user
  const templateDir = path.join(templatePath, 'template');
  if (fs.existsSync(templateDir)) {
    // 注意这里,这里将template代码模板拷贝到生成的项目目录中
    fs.copySync(templateDir, appPath);
  } else {
    console.error(
      `Could not locate supplied template: ${chalk.green(templateDir)}`
    );
    return;
  }
 
  // 对yarn的一些处理

  // Rename gitignore after the fact to prevent npm from renaming it to .npmignore
  // See: https://github.com/npm/npm/issues/1862
  // 处理.gitignore文件

  let command;
  let remove;
  let args;

  // 对yarn和npm两种情况进行判断并进行以上命令变量赋值

  // Install additional template dependencies, if present
 
  // 对旧版本的一些依赖兼容及终端log输出
};

create-react-app的代码组织方式

与vue-cli类似,代码模板支持使用cli本地的代码模板也支持网络资源.

相似之处

两个cli工具基本大同小异,甚至连cli源码组织方式都差不多。他们的基本工作流程是:(顺序可能有不同,但基本过程都差不多)

  1. 检查node版本与待安装依赖的关系
  2. 定义package.json变量,注入相关依赖,写入package.json文件
  3. 确定包管理工具并安装依赖
  4. 对其他依赖再次进行判断,再require进来生成的package.json文件,进行扩展再写入
  5. 复制cli工具内置的代码模板或远程预设到生成的项目目录下
  6. 是否进行git init
  7. 是否创建README文件
  8. 是否commit

都依赖外部依赖来进行项目(创建的新项目)启动,create-react-app使用react-scripts,vue-cli使用cli-service,通过package.json依赖加载。

写在最后

两个项目都是通过lerna来管理package的,这个需要学习一下…

 类似资料: