cli 脚手架
vue-router 事实上是一个栈,不同的vue实例做一个集中化处理
若干个实例中有状态机,支持若干个实例中的状态流转
脚手架在最底层,用来初始化模版
vue cli 作用
- 基础,用于生成项目
- metadata 配置 问询用户交互
- 渲染 生成
初始化
vue init 参数一 模版
可以通过vue list 查看有哪些模版: webpack webpack-mini pwa
新版本为 vue create
主逻辑
- 本地已经有默认模版,直接拉取local-template > 默认/自定义
- vue-cli 拉取本地没有的remote-template > 官方模版/第三方模版
源码解析
bin 主要命令主入口
- vue-build 范式化模版
- create v-3版本
- list 提供列举模版 4.vue-init
从大的结构开始看,先上后下 function run () 入口
rawName: my-project inPlace: 如果是true,指向当前路径构架
exists: 是否存在当前路径
// 是否为当前目录下构建 or 存在当前路径
if (inPlace || exists(to)) {
inquirer.prompt([{
type: 'confirm',
message: inPlace
? 'Generate project in current directory?'
: 'Target directory exists. Continue?',
init定义主函数:
run ():
isLocalPath(template): 是否模版是本地的
getTemplatePath(template): 获取文件路径
**本地文件都在ls -a ~/**中的.vue-templates
开始生成框架: generate()
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)
}
logger 打印状态
检查版本号:是否有slash
先下载,后生成: downloadAndGenerate()
官方直接下载;非官方拼接一段
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)
}
init 的主流程如下:
其他一些工具: 下载远程仓库 const download = require('download-git-repo')
命令行处理工具 const program = require('commander')
路径检测 const exists = require('fs').existsSync
用户根目录 const home = require('user-home')
波浪符路径转换 const tildify = require('tildify')
高亮 const chalk = require('chalk')
命令行与开发者交流的工具 const inquirer = require('inquirer')
日志打印 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
配置commander使用方法
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()
})
工具类函数使用: github.com/tj/commande…
settings:
模板名称 let template = program.args[0]
是否包含斜杠,主要判断是否是官方 const hasSlash = template.indexOf('/') > -1
项目构建目录名 const rawName = program.args[1]
前往绝对路径 const to = path.resolve(rawName || '.')
模版加载地址
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
}
lib库中的generator
读取配置项入口 const opts = getOptions(name, src)
静态网页生成,利用配置生成网页 const Metalsmith = require('metalsmith')
metalsmith初始化 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)
console.log('metalsmith before')
}
// 询问
metalsmith.use(askQuestions(opts.prompts))
// 配置过滤
.use(filterFiles(opts.filters))
.use(renderTemplateFiles(opts.skipInterpolation))
// 直接执行(ms为函数,直接执行)
// 有after配置的 执行after钩子
if (typeof opts.metalsmith === 'function') {
opts.metalsmith(metalsmith, opts, helpers)
console.log('metalsmith exec')
} else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
opts.metalsmith.after(metalsmith, opts, helpers)
console.log('metalsmith after')
}
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)
console.log('metalsmith complete')
} else {
// 有completeMessage 完成调用logMessage
logMessage(opts.completeMessage, data)
}
})
generator 函数:构建项目
- opts 获取vue默认参数
- generator 流程
模版引擎:handleBars const Handlebars = require('handlebars')
生成单页的HTML文件,通过路由生成若干个html文件 -> 项目中的多页形态
run dev 实时生成多个HTML 文件,不同的页面绑定不同HTML 文件,不同的文件加载的不同的依赖(jquery、vue、跨平台)
模板引擎解析渲染器 const render = require('consolidate').handlebars.render
多个条件匹配
const multimatch = require('multimatch')
const getOptions = require('./options')
const ask = require('./ask')
const filter = require('./filter')
const logger = require('./logger')
用户交互入口
function askQuestions (prompts) {
return (files, metalsmith, done) => {
// 自定义函数
ask(prompts, metalsmith.metadata(), done)
}
}
ask 函数
// meta.json meta.js prompts
// data - Metalsmith.metadata()
// done - next流程
module.exports = function ask (prompts, data, done) {
async.eachSeries(Object.keys(prompts), (key, next) => {
prompt(data, key, prompts[key], next)
}, done)
}
function prompt (data, key, prompt, done) {
// skip prompts whose when condition is not met
if (prompt.when && !evaluate(prompt.when, data)) {
return done()
}
// 赋予默认值
let promptDefault = prompt.default
if (typeof prompt.default === 'function') {
promptDefault = function () {
return prompt.default.bind(this)(data)
}
}
inquirer.prompt([{
type: promptMapping[prompt.type] || prompt.type,
name: key,
message: prompt.message || prompt.label || key,
default: promptDefault,
choices: prompt.choices || [],
validate: prompt.validate || (() => true)
}]).then(answers => {
// 根据答案类型拼接数据
if (Array.isArray(answers[key])) {
data[key] = {}
answers[key].forEach(multiChoiceAnswer => {
data[key][multiChoiceAnswer] = true
})
} else if (typeof answers[key] === 'string') {
data[key] = answers[key].replace(/"/g, '\\"')
} else {
data[key] = answers[key]
}
done()
}).catch(done)
}
过滤配置
function filterFiles (filters) {
return (files, metalsmith, done) => {
// 过滤自定义函数
filter(files, filters, metalsmith.metadata(), done)
}
}
// 经过ask获取用户需求之后 => metadata => 过滤掉不需要的模板文件
// files 模板内所有文件
// filters - 配置中的filters字段
// metadata
// done => next
module.exports = (files, filters, data, done) => {
if (!filters) {
return done()
}
const fileNames = Object.keys(files)
Object.keys(filters).forEach(glob => {
fileNames.forEach(file => {
// 匹配文件名
if (match(file, glob, { dot: true })) {
const condition = filters[glob]
// condition表达式不成立就删除文件
if (!evaluate(condition, data)) {
delete files[file]
}
}
})
})
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) => {
// 进入异步处理每个文件
// skipInterpolation - 要跳过的文件
// 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)
}
}
options.js 获取原配置文件 => name设置校验 => 和本地git作者信息做合并
const path = require('path')
// 读取元数据参数
const metadata = require('read-metadata')
const exists = require('fs').existsSync
// 获取本地git配置
const getGitUser = require('./git-user')
// npm包校验器
const validateName = require('validate-npm-package-name')
通过git 读到用户名字
const author = getGitUser()
if (author) {
setDefault(opts, 'author', author)
}
git-user.js
const exec = require('child_process').execSync
// 对于用户git信息的收集
module.exports = () => {
let name
let email
try {
name = exec('git config --get user.name')
email = exec('git config --get user.email')
} catch (e) {}
name = name && JSON.stringify(name.toString().trim()).slice(1, -1)
email = email && (' <' + email.toString().trim() + '>')
return (name || '') + (email || '')
}
check-version
检查node版本号(直接block) => 检查vue cli 的版本(提示)
module.exports = done => {
// Ensure minimum supported node version is used
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()
})
}
如何将字符串转函数 eval() - 但是语法不支持的话查不到错误: 因此将eval 用new Function实现
module.exports = function evaluate (exp, data) {
/* eslint-disable no-new-func */
const fn = new Function('data', 'with (data) { return ' + exp + '}')
try {
return fn(data)
} catch (e) {
console.error(chalk.red('Error when evaluating filter condition: ' + exp))
}
}
local-path.js 检查当前路径
logger.js 统一打印管理器
warnings.js 提示