首页>>前端>>Vue->硬刚VueCli3源码系列五

硬刚VueCli3源码系列五

时间:2023-11-30 本站 点击:0

写在开头

上一篇 文章中,我们通过3000文字的介绍,终于介绍完 Preset 参数的"前世今生",其中我们也了解到它能通过好几个渠道来手动注入。 完全理解 Preset 参数是我们学习 vue-cli 源码的第一步,开头这一步请一定要走好,避免后面掉坑里了。✿(。◕ᴗ◕。)✿

预备知识

ora模块

相信网站的 Loading 效果大家估计都知道吧,那么 cmd 中的 Loading 效果应该如何来做呢?ora 模块就是来干这么一件事的,它能用于 cmd 控制台进度美化,它的使用也是非常的简单。

安装:

npm install ora@3.4.0

示例:

const ora = require('ora');const spinner = ora('Loading...');spinner.start(); setTimeout(() => {  spinner.stop();  console.log('loading stop...')}, 3000)

效果如下:

org(string/options)

参数类型描述默认值textString转轮后方的文字colorString转轮的颜色,提供值:'black'、'red'、'green'、'yellow'、'blue'、'magenta'、'cyan'、'white'、'gray''cyan'spinnerString/Object转轮的动画,可自定义:{interval: 80, frames: ['-', '+', '-']}'dots'hideCursorBoolean隐藏鼠标指针trueintervalNumber转轮动画每帧之间的时间间隔,单位ms100

methods

方法描述返回值start(text)运行转轮,text为指针后的文案返回当前实例stop()停止转轮并清除返回当前实例succeed(text)成功状态的转轮返回当前实例fail(text)失败状态的转轮返回当前实例warn(text)警告状态的转轮返回当前实例info(text)提示状态的转轮返回当前实例isSpinning()判断转轮当前是否在转BooleanstopAndPersist(options)暂停转轮,替换设置返回当前实例clear()清空转轮-跟stop很像返回当前实例render()渲染帧返回当前实例frame()获取下一帧返回当前实例

fs-extra模块

fs-extra 模块是对原生的 fs 模块的封装,它继承原生 fs 模块,并对此进行扩展,提供了更多遍历的API,让用户让好的操作文件系统。

安装:

npm install fs-extra@7.0.1

它有以下这些方法,这些方法有对应的链接,这里小编就不多讲了,偷个懒。

异步方法:

copy:复制文件或文件夹。

emptyDir:清空文件夹(文件夹目录不删,内容清空)。

ensureFile:确保文件存在(文件目录结构没有会新建)。

ensureDir:确保文件夹存在(文件夹目录结构没有会新建)。

ensureLink:确保符号链接存在(目录结构没有会新建)。

ensureSymlink:同ensureDir。

mkdirp:同ensureDir。

mkdirs:同ensureDir。

move:移动文件或文件夹。

outputFile:同fs.writeFile(),写文件(目录结构没有会新建)。

outputJson:写json文件(目录结构没有会新建)。

pathExists:判断文件是否存在。

readJson:读取JSON文件,将其解析为对象。

remove:删除文件或文件夹,类似rm -rf。

writeJson:将对象写入JSON文件。

同步方法:

copySync

emptyDirSync

ensureFileSync

ensureDirSync

ensureLinkSync

ensureSymlinkSync

mkdirpSync

mkdirsSync

moveSync

outputFileSync

outputJsonSync

pathExistsSync

readJsonSync

removeSync

writeJsonSync

execa模块

execa 模块是一个能调用 shell 和本地外部程序的 JS 封装,它改进了 child_process 包的方法,它会启动子进程执行命令,支持多种操作系统,如果父进程退出,则生成的全部子进程都将被杀死。熟悉 Node 的小伙伴应该对它不陌生,不熟悉的小伙伴也没关系,你只要记得它能帮我们执行各种命令即可。

安装:

npm install execa@1.0.0

示例:

const execa = require('execa');// 执行 npm -v 命令const result = execa('npm', ['-v'], {}); // 监听命令执行结束result.stdout.on('close', r => {  console.log(r)})async function fn() {  // echo命令可用于cmd窗口中打印信息, 如 echo 'hello world' 命令可执行在cmd中执行  const {stdout} = await execa('echo', ['你好']);   console.log(stdout); // 你好}fn()async function fn1() {  // 相当于执行了 npm config get registry 命令  const {stdout} = await execa('npm', ['config', 'get', 'registry']);   console.log(stdout); // https://packages.aliyun.com/5eb501ef3fd198000181afca/npm/npm-registry/}fn1()

更多使用方式可以自行查阅文档。传送门

生成package.json文件

前面我们讲完 Preset 参数的获取,接下来我们需要根据这个 Preset 参数的信息,来生成对应的一些模板的文件,首先我们来看看如何生成 package.json 文件:

// Creator.jsconst {hasYarn, hasPnpm3OrLater, logWithSpinner} = require('@vue/cli-shared-utils');...const chalk = require('chalk');const semver = require('semver');const getVersions = require('./util/getVersions');const writeFileTree = require('./util/writeFileTree');module.exports = class Creator {   constructor (name, context, promptModules) { ... }  async create(cliOptions = {}, preset = null) {     ...    preset = cloneDeep(preset);    // name为项目名 context为项目路径 createCompleteCbs为模板文件创建完成的回调(可以先不管)    const { name, context, createCompleteCbs } = this;     // preset.plugin格式调整    preset.plugins['@vue/cli-service'] = Object.assign({      projectName: name    }, preset);    // 下载源    const packageManager = (cliOptions.packageManager ||      loadOptions().packageManager || (hasYarn() ? 'yarn' : null) ||      (hasPnpm3OrLater() ? 'pnpm' : 'npm'));    // 清屏    await clearConsole();    // loading效果    logWithSpinner(`✨`, `Creating project in ${chalk.yellow(context)}.`);    // 确定脚手架的版本号    const { current } = await getVersions();    const currentMinor = `${semver.major(current)}.${semver.minor(current)}.0`;    // 构建 package.json 文件内容    const pkg = {      name,      version: '0.1.0',      private: true,      devDependencies: {}    }    const deps = Object.keys(preset.plugins);      deps.forEach(dep => {      if (preset.plugins[dep]._isPreset) {        return      }      pkg.devDependencies[dep] = (        preset.plugins[dep].version ||        ((/^@vue/.test(dep)) ? `^${currentMinor}` : `latest`)      )    });    // 生成 package.json 文件    await writeFileTree(context, { 'package.json': JSON.stringify(pkg, null, 2) });  }  async promptAndResolvePreset (answers = null) { ... }  async resolvePreset (name, clone) { ... }  resolveFinalPrompts () { ... }  resolveIntroPrompts() { ... }  getPresets () { ... }  resolveIntroPrompts () { ... }  resolveOutroPrompts () { ... }}

上面主要在 create() 方法中增加了十几行代码,接下来我们依次来做一些解释。

preset.plugin格式调整:

vue-cli 源码中,大部分插件(如vue-router/vuex等)的模板都是放置在 @vue/cli-service 这个包里面,但也不全是,像 labeleslint 就有独立的包存放模板,当然这指 vue-cli 的3版本,后续的版本 vue-cli 为所有插件都抽离了单独的包。

所以 preset.plugins 的格式,我们需要调整到和 options.js 文件中的标准 preset 格式一样

下载源 packageManager

cliOptions.packageManager 来自于可选参数 -m,其他参数自行查看 文档。

从上面代码中可以看到,我们的 packageManager 变量是直接从 loadOptions() 中取的,实际也就是从 .vuerc 文件中读取,这是为啥?为什么不从 Preset 参数中取?上上一篇 文章中不是有下载源相关的问答题目吗?

这里需要注意哦,下载源的问答题目是有条件限定的,只有第一次会出现这个问答,后续都是直接就从 .vuerc 文件中读取了。

Loading效果:

logWithSpinner() 是我们直接从 vue-cli 的工具包 @vue/cli-shared-utils 中导出的,它其实就是对在预备知识中提到的 ora 模块的封装而已。

JSON.stringify(pkg, null, 2)

JSON.stringify() 相信大家都用得很熟了,但是它的第二、第三个参数你可知道是啥意思?小编这里就不展开聊了,给你准备 传送门 。

剩下的新增代码就是对 package.json 内容的组成了,也没啥好解释的,自己悟吧。(✪ω✪)

我们来新建 util/writeFileTree.js 文件:

const fs = require('fs-extra');const path = require('path');function deleteRemovedFiles (directory, newFiles, previousFiles) {  // 从 previousFiles 中获取不存在 newFiles 中的文件  const filesToDelete = Object.keys(previousFiles)    .filter(filename => !newFiles[filename])  // 删除每个文件  return Promise.all(filesToDelete.map(filename => {    return fs.unlink(path.join(directory, filename))  }))}/** * 生成真实的文件 * @param { String } dir: 目录路径 * @param { Object } files: 文件集合, key为文件名, value为文件内容 * @param {*} previousFiles: 以前的文件集合 */module.exports = async function writeFileTree (dir, files, previousFiles) {  if (previousFiles) {    await deleteRemovedFiles(dir, files, previousFiles);  }  Object.keys(files).forEach((name) => {    const filePath = path.join(dir, name); // 拼接文件全路径    fs.ensureDirSync(path.dirname(filePath)); // 创建目录    fs.writeFileSync(filePath, files[name]); // 创建文件  })}

这个文件就比较简单,也写了注释,这里就不做过多解析了。

需要注意安装一下 fs-extra 模块:

npm install fs-extra@7.0.1

安装后,你可以尝试执行 juejin-vue-cli 命令,看看是否有相应的 package.json 文件生成。

安装依赖

既然 package.json 文件已经生成完毕,那么接下来需要根据这个文件来安装项目所需的依赖模块了,其实本质就是执行一下 npm install 命令就行了,我们来看看 vue-cli 内部是如何来做的。

// Creator.jsconst {hasYarn, hasPnpm3OrLater, logWithSpinner, log} = require('@vue/cli-shared-utils');...const {installDeps} = require('./util/installDeps');module.exports = class Creator {   constructor (name, context, promptModules) { ... }  async create(cliOptions = {}, preset = null) {     ...    log(`⚙  开始下载依赖`);    // 下载 package.json 文件依赖    await installDeps(context, packageManager, cliOptions.registry);    stopSpinner();    log(`依赖下载完成`);  }  ...}

新建 ./util/installDeps.js 文件:

const execa = require('execa');const registries = require('./registries');const shouldUseTaobao = require('./shouldUseTaobao');// 验证只能是这几个下载源const supportPackageManagerList = ['npm', 'yarn', 'pnpm'];function checkPackageManagerIsSupported (command) {  if (supportPackageManagerList.indexOf(command) === -1) {    throw new Error(`Unknown package manager: ${command}`)  }}// 记录每个源需要执行的命令const packageManagerConfig = {  npm: {    installDeps: ['install', '--loglevel', 'error'],    installPackage: ['install', '--loglevel', 'error'],    uninstallPackage: ['uninstall', '--loglevel', 'error'],    updatePackage: ['update', '--loglevel', 'error']  },  pnpm: {    installDeps: ['install', '--loglevel', 'error', '--shamefully-flatten'],    installPackage: ['install', '--loglevel', 'error'],    uninstallPackage: ['uninstall', '--loglevel', 'error'],    updatePackage: ['update', '--loglevel', 'error']  },  yarn: {    installDeps: [],    installPackage: ['add'],    uninstallPackage: ['remove'],    updatePackage: ['upgrade']  }}const taobaoDistURL = 'https://npm.taobao.org/dist';/** * 给下载源添加registry地址 * @param {String} command: npm/yarn/pnpm * @param {Array<String>} args: 被执行的命令列表, [install, ....] * @param {*} cliRegistry: registry地址, -r <url> */async function addRegistryToArgs (command, args, cliRegistry) {  // cliRegistry来自于可选参数-r: vue create ProjectName -r --registry <url>  const altRegistry = (cliRegistry ||              ((await shouldUseTaobao(command)) ? registries.taobao: null));  // 如果确定使用其他下载源的registry地址或者使用淘宝镜像, 则需要在被执行的命令列表中放入--registry与--disturl命令  // --registry: 设置下载源的registry地址  // --disturl: 设置node的国内镜像地址, 主要是解决依赖C++模块所带来的问题, 具体可以看看这篇文章的介绍.https://zhuanlan.zhihu.com/p/147005226  if (altRegistry) {    args.push(`--registry=${altRegistry}`)    if (altRegistry === registries.taobao) {      args.push(`--disturl=${taobaoDistURL}`)    }  }}/** * 执行命令 * @param {String} command: npm/yarn/pnpm * @param {Array<String>} args: 被执行的命令列表 * @param {String} targetDir: 项目目录地址 * @returns */function executeCommand (command, args, targetDir) {  return new Promise((resolve, reject) => {    // 开始下载 - 通过 execa 模块去执行命令    const child = execa(command, args, {      cwd: targetDir, // 子进程的当前工作目录      stdio: ['inherit'] // 子 stdio 配置, 默认为 pipe    })    // 下载完成    child.on('close', code => {      if (code !== 0) {        reject(`command failed: ${command} ${args.join(' ')}`)        return      }      resolve()    })  })}// 下载依赖exports.installDeps = async function installDeps (targetDir, command, cliRegistry) {  // 验证下载源  checkPackageManagerIsSupported(command);  // 获取需要执行的命令  const args = packageManagerConfig[command].installDeps;  // 添加registry地址  await addRegistryToArgs(command, args, cliRegistry);  // 执行命令  await executeCommand(command, args, targetDir);}// 下载具体的包exports.installPackage = async function (targetDir, command, cliRegistry, packageName, dev = true) {}// 卸载具体的包exports.uninstallPackage = async function (targetDir, command, cliRegistry, packageName, dev = true) {}// 更新具体的包exports.updatePackage = async function (targetDir, command, cliRegistry, packageName) {}

这个文件内容有点多,但不要慌,内容实际也不复杂,主要看看 installDeps() 这个方法的过程就行,小编也都详细标明了注释,只要你看了就能懂。(@^▽^@)

上面代码中还有几个方法是省略的,它们的作用是针对单个依赖的操作,这里没涉及到就先省略掉了,感兴趣的小伙伴可以直接阅读 vue-cli 源码。传送门

下载 execa 模块:

npm install execa@1.0.0

继续新建 ./util/shouldUseTaobao.js 文件:

const ora = require('ora');const spinner = ora('Loading...');spinner.start(); setTimeout(() => {  spinner.stop();  console.log('loading stop...')}, 3000)1

最后新建 ./util/registries.js 文件:

const ora = require('ora');const spinner = ora('Loading...');spinner.start(); setTimeout(() => {  spinner.stop();  console.log('loading stop...')}, 3000)2

这个文件存放一些下载源 registry 地址,相信有点前端经验的小伙伴可能见过这个命令:

const ora = require('ora');const spinner = ora('Loading...');spinner.start(); setTimeout(() => {  spinner.stop();  console.log('loading stop...')}, 3000)3

它的作用是将我们本地的下载源设置成 cnpm 下载源,也就是淘宝镜像。你也可以全局安装一下 nrm 模块来管理所有下载源,nrm 模块能很方便的切换下载源,这里就不作过多的解释了,感兴趣的小伙伴可以私下去了解了解。

接下来,我们来尝试执行 juejin-vue-cli create gg 命令试试:

如果执行完命令后,你也能正常下载完依赖,那么就说明你成功了(^m^)。 上面我们创建好了 package.json 文件,也安装完项目的依赖,接下来就是创建项目的目录结构了,但由于涉及的内容比较多,就放到下一篇文章再讲吧。

至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。 老样子,点赞+评论=你会了,收藏=你精通了。

原文:https://juejin.cn/post/7099697365220589582


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Vue/3786.html