一、vue-cli使用和源码下载
npm安装vue-cli:
npm install -g vue-cli // 现在版本已经到了5了,如果需要下载2版本的需要指定版本了
使用vue init命令创建工程(使用webpack模板)
vue init webpack <project-name>
通过以上两个命令,可以得到一个基于webpack模板生成的项目脚手架
源码下载:
vue-cli2:git clone -b v2 https://github.com/vuejs/vue-cli
vue-cli源码项目结构:
整个项目的目录结构如上图所示,下面我大概介绍每个文件夹的东西大致都是干嘛的。
- bin //这里放的vue的一些命令文件,比如vue init这样的命令都是从由这里控制的
- docs //一些注意事项啥的,不重要的目录,可以直接忽略
- lib //这里存放着一些vue-cli需要的一些自定义方法
- node_modules //这里就不用我多说了
- test // 单元测试 开会vue-cli工具时会用到,我们读源码的时候可以直接忽略掉
- 一些杂七杂八的东西 //比如eslint配置、.gitignore、LICENSE等等诸如此类这些东西。不影响阅读源码,直接忽略掉。
- package.json/README.md //这个也不用我多说了,大家都知道的
二、vue-cli命令分析
本地相同时运行vuecli2和vuecli3命令,可以参考如下进行配置:https://blog.csdn.net/weixin_41346436/article/details/121966844
查看package.json文件:
bin指定命令对应的可执行文件位置
在vue-cli 2.X版本中,vue-build,vue-create不支持
1. vue命令:
#!/usr/bin/env node
/**
* 指明用node执行该脚本文件,系统去env环境找node的安装路径
* #! -----在Linux或者Unix中,是一个符号名称,用于指明这个脚本文件的解释程序,即用什么来执行这个脚本文件。
* /usr/bin/env:告诉系统可以在env设置里面查找该解释器的路径。如果出现No such file or directory的错误,则添加node环境变量配置就可以了
* 注意:windows不支持Shebang,它是通过文件的扩展名来确定使用什么解释器来执行脚本
*/
// commander是Node.js命令行的解决方案(即Node.js下实现自定义命令)
const program = require('commander')
program
.version(require('../package').version) // version:定义版本----会添加到Options
.usage('<command> [options]') // usage:帮助信息输出,不写.name('xx')则Usage的名字当前文件的名字,否则为.name('xxxx')的xxxx
.command('init', 'generate a new project from a template')
.command('list', 'list available official templates') // 生成一个官方的模板
.command('build', 'prototype a new project')
.command('create', '(for v3 warning only)')
// .parse的第一个参数是要解析的字符串数组,也可以省略参数而使用process.argv。
program.parse(process.argv) // parse:解析参数用于匹配对应选项或命令
注意:帮助信息Options是 Commander 基于你的程序自动生成的,默认的帮助选项是-h,--help。
通过命令进行调试:./bin/vue
执行vue命令,查看输出:
2. nodejs commander命令行
commander命令想详细了解可以参考一下这边文章:
想阅读vue-cli源码,commander你都不会吗? – 掘金 (juejin.cn)
这里用到的是 commander 这个库。它的文档地址是:https://www.npmjs.com/package/commander
const program = require("commander");
// 分为2种操作, 2种操作互相冲突
// Options 操作
program
.version("0.0.1")
.option("-t, --types [type]", "test options")
// option这句话必须加
.parse(process.argv);
// Commands 操作
program
// 命令与参数: <> 必填; [] 选填
.command("exec <cmd> [env]")
// 别名
.alias("ex")
// 帮助信息
.description("execute the given remote cmd")
// 没用,option和command是冲突的
.option("-e, --exec_mode <mode>", "Which exec mode to use")
// 执行的操作
.action((cmd, env, options) => {
// 参数可以拿到
console.log(`env is ${env}`);
console.log('exec "%s" using %s mode', cmd, options.exec_mode);
})
// 自定义help信息
.on("--help", function() {
console.log("自定义help信息");
});
// 参数长度不够, 打印帮助信息
if (!process.argv.slice(2).length) {
program.outputHelp();
}
if (program.types) {
console.log(program.types);
}
// 解析命令行参数
program.parse(process.argv);
文档上基本都写明白了,但是有几个需要注意的点:
- 它主要提供
options
和commands
两种操作,option
就是形如“-t,–types”这样的传参,commands
就是形如“exec”这样的传参。 不要混用两者 。 - 读取
commands
中传入的参数,写在 .action 中;读取options
传入的参数,是通过访问program
上的变量。除此之外,options
操作需要执行.parse(process.argv)
解析命令行参数 - -V 和 -h 默认也是提供的,但是也可以通过自定义覆盖
- 一般都把
options
写在前面, 顺便标识版本号 ;把commands
写在后面;最后会判断一下参数长度,不够会自动输出打印信息
3. vue build
const chalk = require('chalk') // 高亮
console.log(chalk.yellow(
'\n' +
' We are slimming down vue-cli to optimize the initial installation by ' +
'removing the `vue build` command.\n' +
' Check out Poi (https://github.com/egoist/poi) which offers the same functionality!' +
'\n'
))
4. vue create
#!/usr/bin/env node
// chalk 用于高亮终端打印出来的信息
const chalk = require('chalk')
console.log()
console.log(
` ` +
chalk.yellow(`vue create`) +
' is a Vue CLI 3 only command and you are using Vue CLI ' +
require('../package.json').version + '.'
)
console.log(` You may want to run the following to upgrade to Vue CLI 3:`)
console.log()
console.log(chalk.cyan(` npm uninstall -g vue-cli`))
console.log(chalk.cyan(` npm install -g @vue/cli`))
console.log()
5. vue list
拉取vuejs-templates的模板信息并输出。
#!/usr/bin/env node
// 记录
const logger = require('../lib/logger') //自定义工具-用于日志打印。
const request = require('request') //发送http请求的工具。
const chalk = require('chalk') //用于高亮console.log打印出来的信息。
/**
* Padding.
*/
console.log()
process.on('exit', () => {
console.log('11111')
})
/**
* List repos.
*/
request({
url: 'https://api.github.com/users/vuejs-templates/repos',
headers: {
'User-Agent': 'vue-cli'
}
}, (err, res, body) => {
if (err) logger.fatal(err)
const requestBody = JSON.parse(body)
if (Array.isArray(requestBody)) {
console.log(' Available official templates:')
console.log()
requestBody.forEach(repo => {
console.log(
' ' + chalk.yellow('★') +
' ' + chalk.blue(repo.name) +
' - ' + repo.description)
})
} else {
console.error(requestBody.message)
}
})
// 当输入"vue list"时(我们测试时,可以直接在当前源码文件目录下的终端上输入“bin/vue-list”),vue-cli会请求接口,获取官方模板的信息,然后做了一定处理,在终端上显示出来模板名称和对应的说明。
6. vue init(重点)
- 引进一些模块和本地常用工具类方法
#!/usr/bin/env node
// 加载依赖配置
// 下载远程仓库
const download = require("download-git-repo");
// 命令行处理工具
// commander可以让node命令更加简单,提供了命令行输入、参数解析等强大功能
// 可以将文字输出到终端当中的模块
const program = require("commander");
// 文件操作,existsSync - 同步的方法检测文件路径的,从而判断路径是否存在,存在返回true,否则返回false
const exists = require("fs").existsSync;
// node自带的path模块,包含了一些工具函数,用于处理文件与目录的路径,通常用来拼接路径
const path = require("path");
// 在node执行脚本时候,在控制台显示loading效果、显示各种状态的图标等
const ora = require("ora");
// 获取用户根目录
const home = require("user-home");
// 绝对路径用波浪号替换 比如/Users/Documents/dev → ~/dev
const tildify = require("tildify");
// 高亮
const chalk = require("chalk");
// 用户与脚本的命令行交互
// 是一个命令行回答的模块,可以自己设定终端的问题,然后对这些回答给出相应的处理
const inquirer = require("inquirer");
// 是一个可以使用 UNIX 命令的模块,rm -rf
const rm = require("rimraf").sync;
// ------------------下面是脚手架内部提供的功能库----------------------
// 生产日志 - 用于日志打印
const logger = require("../lib/logger");
// 生成模板 - 组织静态文件
const generate = require("../lib/generate");
// 版本校验 - 模块兼容版本
const checkVersion = require("../lib/check-version");
// 告警
const warnings = require("../lib/warnings");
// 内部路径
const localPath = require("../lib/local-path");
// 检测本地路径
const isLocalPath = localPath.isLocalPath;
// 检测本地模板路径
const getTemplatePath = localPath.getTemplatePath;
- 下面代码声明了vue init的用法
/**
* Usage.
*/
// 面试: 如何使用本地离线下载好的或者自己预设的模板 --offline
// -------------------------------------------- 1 --------------------------------------------
/**
* 下面的代码声明了vue init的用法,如果在终端当中 输入 vue init --help或者跟在vue init 后面的参数长
* 度小于1,会输出下面的描述
*/
/**
* Usage.
* usage: 显示help的时候,自定义命令行第一行的用法描述
* option:添加命令行
*/
program
// 模板名称-项目名称 <必填> [选填] 例:vue init webpack myvue
.usage("<template-name> [project-name]")
.option("-c, --clone", "use git clone")
.option("--offline", "use cached template");
//如果我们已经下载过了模板就不需要再下载模板了 例:vue init webplck myvue --offline
/**
* Help.
* on:自定义监听事件
*/
// 可以使用vue init --help输出下面的打印信息
program.on("--help", () => {
console.log(" Examples:");
console.log();
console.log(
chalk.gray(" # create a new project with an official template")
);
console.log(" $ vue init webpack my-project");
console.log();
console.log(
chalk.gray(" # create a new project straight from a github template")
);
console.log(" $ vue init username/repo my-project");
console.log();
});
/**
* Help.
*/
function help() {
program.parse(process.argv); // 解析命令行参数,参数放在属性args上
if (program.args.length < 1) return program.help(); // 显示帮助信息
}
help();
在控制台输入vue init或者vue init –help,输出信息如下所示:
说明:process是一个全局对象,它提供当前 Node.js 进程的有关信息,以及控制当前 Node.js 进程。process.argv属性返回一个数组,具有的元素如下所述:
// process.argv 返回一个数组
process.argv[0]:返回启动Node.js进程的可执行文件所在的绝对路径
process.argv[1]:返回当前执行的JavaScript文件绝对路径
剩余的元素为其他命令行参数
示例如下图所示:
- 下面代码主要是获取变量值:
/**
* Settings.
*/
/***
* 获取program.args
*
* process.argv返回一个数组
* process.argv[0]:返回启动Node.js进程的可执行文件所在的绝对路径
* process.argv[1]:返回当前执行的JavaScript文件绝对路径
* 剩余的元素为其他命令行参数
*
* 打印的process.argv数据
* [
'C:\Program Files\nodejs\node.exe',
'C:\Program Files\nodejs\node_modules\vue-cli\bin\vue-init'
]
*/
// 模板名称 template
let template = program.args[0];
// 例如:vue init webpack myvue ----- 则template = webpack
// 是否包含斜杠 => 是一个路径(如果含有'/'代表是非官方的模板) vue init username/repo my-project
const hasSlash = template.indexOf("/") > -1;
// 项目名称 刚刚输入的为 myvue
const rawName = program.args[1];
// 如果不存在项目名称或项目名称输入是'.' ,则name取的是 当前文件夹的名称
const inPlace = !rawName || rawName === "."; //rawName存在或者为“.”的时候,视为在当前目录下构建
// 当前目录名为项目构建的目录名称 or 当前目录子目录
// process.cwd():获取Node.js进程的当前工作目录,如 C:\Users\joys\myvue
// path.relative('../', process.cwd()) 如:myvue
const name = inPlace ? path.relative("../", process.cwd()) : rawName;
// 输出路径,如/Users/lily/Documents/project/testcommander
const to = path.resolve(rawName || ".");
console.log("program===", program);
// 是否用到git clone 监听options是否使用了 -c 命令
const clone = program.clone || false;
// 本地目录的拼接
// tmp为本地模板路径,如果是离线状态,那么模板路径取本地的
// home = require('user-home'),得到用户的主目录路径,如:/Users/lily
// tmp,如:/Users/lily/.vue-templates/-Users-lily-.nvm-versions-node-v18.7.0-bin-node
const tmp = path.join(home, ".vue-templates", template.replace(/[/:]/g, "-"));
console.log("program.offline===", program.offline);
// 是否使用了vue init --offline xxx 命令,使用了就为true,否则为false
if (program.offline) {
// tildify(tmp)将绝对路径转换为波形路径,如:~/.vue-templates/-Users-tangxiujiang-.nvm-versions-node-v18.7.0-bin-node
// 即~相当于/Users/lily
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`);
template = tmp; // 如果读取的是本地模板,则把本地模板赋值给template
}
// ls cat vim/nano/…… tail 《鸟哥的linux私房菜》
/**
* Padding.
*/
console.log();
// 监听exit事件
process.on("exit", () => {
console.log();
});
// inPlace:如果不存在项目名称或项目名称输入是'.'
// 或者输出路径存在,则在终端显示设定的问题,并根据回答进行处理
// inquirer是一个命令行回答的模块,可以自己设定终端的问题,然后对这些回答给出相应的处理
// 是否在当前目录下构建 || 指定路径
if (inPlace || exists(to)) {
// 弹出问题,进行交互
inquirer
.prompt([
{
type: "confirm",
message: inPlace
? "Generate project in current directory?" //是否在当前目录下构建项目
: "Target directory exists. Continue?", //构建目录已存在,是否继续
name: "ok",
},
])
.then((answers) => {
if (answers.ok) {
run();
}
})
.catch(logger.fatal);
} else {
run();
}
inquirer
是一个命令行回答的模块,可以自己设定终端的问题,然后对这些回答给出相应的处理,如下图所示,输入vue init ff
,则终端显示等待用户输入答案,并根据答案进行相应的处理:
- 根据模版名称,下载、生成模版
/**
* Check, download and generate the project.
*/
// 主函数
function run() {
// check if template is local
// 是否本地路径
if (isLocalPath(template)) {
// 模板路径 ls ~/.vue-template/ ……
const templatePath = getTemplatePath(template);
// 如果本地模版路径存在 则开始生成模版
if (exists(templatePath)) {
// 通过模板生成最终文件项目 - mark
generate(name, templatePath, to, (err) => {
if (err) logger.fatal(err);
console.log();
logger.success('Generated "%s".', name);
});
} else {
// 模板查找失败
logger.fatal('Local template "%s" not found.', template);
}
} else {
// 非本地
// 检查版本号
checkVersion(() => {
// 官方 or 第三方
// 路径是否包含‘/’ , 路径不包含'/',则进入该分支,使用官方模版
if (!hasSlash) {
// use official templates
// 从这句话以及download-git-repo的用法,我们得知了vue的官方的模板库的地址:https://github.com/vuejs-templates
const officialTemplate = "vuejs-templates/" + template;
// 路径有'#'则直接下载 // 例:vuejs-template/pwa#rc-2.0
if (template.indexOf("#") !== -1) {
downloadAndGenerate(officialTemplate);
} else {
// 路径不包含'-2.0',则输出模版废弃的相关提示
if (template.indexOf("-2.0") !== -1) {
warnings.v2SuffixTemplatesDeprecated(template, inPlace ? "" : name);
return;
}
// 下载并生成模版
downloadAndGenerate(officialTemplate);
}
} else {
downloadAndGenerate(template);
}
});
}
}
/**
* Download a generate from a template repo.
*
* @param {String} template
*/
function downloadAndGenerate(template) {
// 显示loading icon + 'downloading template'
const spinner = ora("downloading template");
spinner.start();
// Remove if local template exists
// 删除本地存在的模版
if (exists(tmp)) rm(tmp);
// 下载模版
// template目标地址,tmp为下载地址,clone代表是否需要clone
download(template, tmp, { clone }, (err) => {
spinner.stop(); // 停止动画
// 下载出错,则输出日志并终止进程
if (err)
logger.fatal(
"Failed to download repo " + template + ": " + err.message.trim()
);
// 模版下载成功之后,调生成模版的方法
generate(name, tmp, to, (err) => {
if (err) logger.fatal(err);
console.log();
logger.success('Generated "%s".', name);
});
});
}
****下载模版方法:download
download
是download-git-repo
模块,该方法的使用可参考文档:download-git-repo – npm,作用是从代码仓库中下载代码,如下是API的介绍:
download(repository, destination, options, callback)
// repository ---- 代码仓库地址
1. 可采用简写方式:
// 如:'github:liuli/uni-app'或者'liubbc/uni-app'
1.1 GitHub - github:owner/name or simply owner/name
// 如:'gitlab:liuli/uni-app'
1.2 GitLab - gitlab:owner/name
// 如:'bitbucket:liuli/uni-app'
1.3 Bitbucket - bitbucket:owner/name
// 注意:仓库下载的默认分支是master分支,但是可以修改repository的下载分支名,如下所示:
// 即在仓库名称后加上'#分支名称',如liuli/uni-app#dev,表示下载的是dev分支代码
// 另外,可以指定自定义来源,如 gitlab:custom.com:owner/name.,自定义来源默认为 https 或 git@ , 你也可以自己定义协议
2. Direct - direct:url方式
这种方式会跳过上面简写的方式,直接传递 url。有以下注意事项:
2.1 如果使用 direct,并且没有 clone配置项, 必须传入完整的zip文件地址, 包括分支(如果需要的话);
2.2 如果使用 direct 并带有 clone配置项, 必须传入完整的 git repo url ,可以通过 direct:url#dev指定分支
// destination----下载的仓库放置的路径
// options--------选项参数
// callback-------回调函数
生成模版的方法:generate
生成模版的方法generate在文件lib/generate.js文件
// 可以修改终端输出字符样式
const chalk = require('chalk')
// 一个非常简单、可插拔的静态站点生成器。用于遍历文件夹,判断是否需要进行模板渲染
const Metalsmith = require('metalsmith')
// 是一个模版编译器,通过template和json,输出一个html
const Handlebars = require('handlebars')
// 异步处理模块,类似于让方法变成一个线程
const async = require('async')
// 模版引擎整合库
const render = require('consolidate').handlebars.render
const path = require('path')
// 字符串数组匹配的库
const multimatch = require('multimatch')
// options.js自定义的配置项文件
const getOptions = require('./options')
// 本地定义的工具类
// ask设置提问的问题,并且对输入的答案进行处理
const ask = require('./ask')
// 过滤不符合条件的数据
const filter = require('./filter')
// 打印日志:区分失败,成功,普通日志
const logger = require('./logger')
// ------------------------------ 1 -----------------------------
// 注册两两个渲染器
// register handlebars helper
Handlebars.registerHelper('if_eq', function (a, b, opts) {
return a === b
? opts.fn(this)
: opts.inverse(this)
})
Handlebars.registerHelper('unless_eq', function (a, b, opts) {
return a === b
? opts.inverse(this)
: opts.fn(this)
})
// ------------------------------ 1 -----------------------------
/**
* Generate a template given a `src` and `dest`.
*
* @param {String} name
* @param {String} src
* @param {String} dest
* @param {Function} done
*/
module.exports = function generate (name, src, dest, done) {
// 读取src目录下的配置文件meta.json或meta.js
// 同时设置name ,author(当前git用户)到配置opts中
const opts = getOptions(name, src)
// 在该目录下生成静态文件
const metalsmith = Metalsmith(path.join(src, 'template'))
// data赋值
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true
})
// 遍历opts中的helpers对象,注册渲染模版数据
opts.helpers && Object.keys(opts.helpers).map(key => {
Handlebars.registerHelper(key, opts.helpers[key])
})
const helpers = { chalk, logger }
// 数据合并
if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
opts.metalsmith.before(metalsmith, opts, helpers)
}
// askQuestions在终端里面询问一些问题
metalsmith.use(askQuestions(opts.prompts))
.use(filterFiles(opts.filters))
.use(renderTemplateFiles(opts.skipInterpolation))
if (typeof opts.metalsmith === 'function') {
opts.metalsmith(metalsmith, opts, helpers)
} else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
opts.metalsmith.after(metalsmith, opts, helpers)
}
// clean:设置在写入之前是否删除原先目标目录 默认为true
// source:设置原路径
// destination:设置输出的目录
// build:执行构建
metalsmith.clean(false)
.source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
.destination(dest)
.build((err, files) => {
done(err)
if (typeof opts.complete === 'function') {
// 当生成完毕之后执行 meta.js当中的 opts.complete方法
const helpers = { chalk, logger, files }
opts.complete(data, helpers)
} else {
logMessage(opts.completeMessage, data)
}
})
return data
}
/**
* Create a middleware for asking questions.
*
* @param {Object} prompts
* @return {Function}
*/
function askQuestions (prompts) {
return (files, metalsmith, done) => {
ask(prompts, metalsmith.metadata(), done)
}
}
/**
* Create a middleware for filtering files.
*
* @param {Object} filters
* @return {Function}
*/
function filterFiles (filters) {
return (files, metalsmith, done) => {
filter(files, filters, metalsmith.metadata(), done)
}
}
/**
* Template in place plugin.
*
* @param {Object} files
* @param {Metalsmith} metalsmith
* @param {Function} done
*/
function renderTemplateFiles (skipInterpolation) {
skipInterpolation = typeof skipInterpolation === 'string'
? [skipInterpolation]
: skipInterpolation
return (files, metalsmith, done) => {
const keys = Object.keys(files)
const metalsmithMetadata = metalsmith.metadata()
async.each(keys, (file, next) => {
// skipping files with skipInterpolation option
if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
return next()
}
const str = files[file].contents.toString()
// do not attempt to render files that do not have mustaches
if (!/{{([^{}]+)}}/g.test(str)) {
return next()
}
render(str, metalsmithMetadata, (err, res) => {
if (err) {
err.message = `[${file}] ${err.message}`
return next(err)
}
files[file].contents = new Buffer(res)
next()
})
}, done)
}
}
/**
* Display template complete message.
*
* @param {String} message
* @param {Object} data
*/
function logMessage (message, data) {
if (!message) return
render(message, data, (err, res) => {
if (err) {
console.error('\n Error when rendering template complete message: ' + err.message.trim())
} else {
console.log('\n' + res.split(/\r?\n/g).map(line => ' ' + line).join('\n'))
}
})
}
暂无评论内容