一.bin
先从bin文件夹开始:
vue.js
#!/usr/bin/env node
// 主入口
const program = require('commander')
// 对应指令 & 具体文件
program
.version(require('../package').version)
.usage('<command> [options]')
.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)')
program.parse(process.argv)
vue-build
#!/usr/bin/env node
// 高亮工具
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'
))
vue-init
#!/usr/bin/env node
// 加载依赖配置
// 下载远程仓库内容
const download = require('download-git-repo')
// 命令行处理工具
const program = require('commander')
// node下的文件操作了,existsSync - 检测路径是否存在
const exists = require('fs').existsSync
// node自带path模块,拼接路径
const path = require('path')
// 命令行加载效果工具
const ora = require('ora')
// 获取用户的根目录
const home = require('user-home')
// 绝对路径替换成带波浪号的路径
const tildify = require('tildify')
// 高亮
const chalk = require('chalk')
// 用户与脚本的命令行交互工具
const inquirer = require('inquirer')
// rm -rf js版本
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
/**
* Usage.
*/
// 面试:如何使用第三方模版?
program
.usage('<template-name> [project-name]')
.option('-c, --clone', 'use git clone')
.option('--offline', 'use cached template')
/**
* 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)
if (program.args.length < 1) return program.help()
}
help()
/**
* Settings. 主要设置
*/
// 如何取到模版 => 如何获取命令行参数
// 模版名称
let template = program.args[0]
// 是否包含斜杠 => 模版名称是否包含路径层级
const hasSlash = template.indexOf('/') > -1
// 项目名称
const rawName = program.args[1]
// 输入空的项目名称 => 是否在当前目录新建
const inPlace = !rawName || rawName === '.'
// 当前目录名为项目构建目录名 or 当前目录新建子目录
const name = inPlace ? path.relative('../', process.cwd()) : rawName
const to = path.resolve(rawName || '.')
const clone = program.clone || false
// 拼接目录的地址 => 本地缓存路径
const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
if (program.offline) {
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
template = tmp
}
/**
* Padding.
*/
console.log()
process.on('exit', () => {
console.log()
})
// 标准的交互询问(确认类型)
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()
}
/**
* Check, download and generate the project.
*/
// 主功能函数
function run () {
// check if template is local
// 是否为本地路径
if (isLocalPath(template)) {
// ~/.vue-template/...
const templatePath = getTemplatePath(template)
if (exists(templatePath)) {
// 用本地的模板去生成最终文件项目
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 使用官方模板
const officialTemplate = 'vuejs-templates/' + template
// #可用
if (template.indexOf('#') !== -1) {
downloadAndGenerate(officialTemplate)
} else {
if (template.indexOf('-2.0') !== -1) {
warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
return
}
// warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
downloadAndGenerate(officialTemplate)
}
} else {
downloadAndGenerate(template)
}
})
}
}
/**
* Download a generate from a template repo.
*
* @param {String} template
*/
function downloadAndGenerate (template) {
const spinner = ora('downloading template')
spinner.start()
// Remove if local template exists
// 删除本地模版
if (exists(tmp)) rm(tmp)
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)
})
})
}
vue-list
#!/usr/bin/env node
const logger = require('../lib/logger')
const request = require('request')
const chalk = require('chalk')
/**
* Padding.
*/
console.log()
process.on('exit', () => {
console.log()
})
/**
* List repos.
*/
// 面试:vue list展示的是所有模版吗?只有官方的吗?会展示本地缓存模版吗?->只要官方,不会展示本地缓存模板
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)
}
})
二.lib
check-version.js
const request = require('request')
const semver = require('semver')
const chalk = require('chalk')
const packageConfig = require('../package.json')
// 版本检测
module.exports = done => {
// Ensure minimum supported node version is used
// 面试:如何检测功能模块与node版本是否搭配
if (!semver.satisfies(process.version, packageConfig.engines.node)) {
return console.log(chalk.red(
' You must upgrade node to >=' + packageConfig.engines.node + '.x to use vue-cli'
))
}
// 面试:如何检测当前模块是否已经最新并提示用户升级
request({
url: 'https://registry.npmjs.org/vue-cli',
timeout: 1000
}, (err, res, body) => {
if (!err && res.statusCode === 200) {
const latestVersion = JSON.parse(body)['dist-tags'].latest
const localVersion = packageConfig.version
if (semver.lt(localVersion, latestVersion)) {
console.log(chalk.yellow(' A newer version of vue-cli is available.'))
console.log()
console.log(' latest: ' + chalk.green(latestVersion))
console.log(' installed: ' + chalk.red(localVersion))
console.log()
}
}
done()
})
}
generate.js
// 依赖加载
const chalk = require('chalk')
// 静态内容生成
const Metalsmith = require('metalsmith')
// 模版引擎
const Handlebars = require('handlebars')
const async = require('async')
const render = require('consolidate').handlebars.render
const path = require('path')
// 多个条件匹配
const multimatch = require('multimatch')
const getOptions = require('./options')
const ask = require('./ask')
const filter = require('./filter')
const logger = require('./logger')
// 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)
})
/**
* Generate a template given a `src` and `dest`.
*
* @param {String} name
* @param {String} src
* @param {String} dest
* @param {Function} done
*/
// 1. 获取一个完全体的配置
module.exports = function generate (name, src, dest, done) {
// 读取配置项
const opts = getOptions(name, src)
// ms初始化数据
const metalsmith = Metalsmith(path.join(src, 'template'))
// 配置完全体合并
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true
})
// 注册配置对象 - 动态组件处理
opts.helpers && Object.keys(opts.helpers).map(key => {
Handlebars.registerHelper(key, opts.helpers[key])
})
const helpers = { chalk, logger }
// 设置调用before钩子
if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
opts.metalsmith.before(metalsmith, opts, helpers)
}
// 问询 + 处理 + 生成
metalsmith.use(askQuestions(opts.prompts))
.use(filterFiles(opts.filters))
.use(renderTemplateFiles(opts.skipInterpolation))
// 执行态
// after函数执行参数
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)
}
// 结尾
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)
// complete钩子
if (typeof opts.complete === 'function') {
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
*/
// 1. 文件索引处理 2. 跳过要跳过的,去除内容字符串 3. 内容结合元数据进行渲染
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'))
}
})
}
logger.js
const chalk = require('chalk')
const format = require('util').format
/**
* Prefix.
*/
// 统一打印管理
const prefix = ' vue-cli'
const sep = chalk.gray('·')
/**
* Log a `message` to the console.
*
* @param {String} message
*/
exports.log = function (...args) {
const msg = format.apply(format, args)
console.log(chalk.white(prefix), sep, msg)
}
/**
* Log an error `message` to the console and exit.
*
* @param {String} message
*/
exports.fatal = function (...args) {
if (args[0] instanceof Error) args[0] = args[0].message.trim()
const msg = format.apply(format, args)
console.error(chalk.red(prefix), sep, msg)
process.exit(1)
}
/**
* Log a success `message` to the console and exit.
*
* @param {String} message
*/
exports.success = function (...args) {
const msg = format.apply(format, args)
console.log(chalk.white(prefix), sep, msg)
}