写在开头
上一篇 文章中,我们通过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)
:
methods
:
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
这个包里面,但也不全是,像 label
和eslint
就有独立的包存放模板,当然这指 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