前言
最近在研究前端工程化的一些东西, 那就得有自己的一套脚手架, 于是瞄上了vue-cli
, 也看了下create-react-app
, 感觉vue-cli
更符合我的预期, 于是撸了遍源码, 写了个小demo脚手架。
献上源码地址: 源码
体验方法:
$ npm i masoneast-cli -g
$ masoneast init my-project
现在, 我们一起来了解下vue-cli
到底帮我们做了什么,让我们可以一行命令就可以生成一个工程吧!
整体流程
我们先了解下如何使用vue-cli
, 再详细讲解每一步的实现。
vue-cli
提供了多种模板, 我们这里以webpack
模板为例。
-
安装:
npm install vue-cli -g
-
使用:
- 直接下载使用:
vue init webpack my-project
- 离线使用:
vue init webpack my-projiect --offline
- clone使用:
vue init webpack my-projiect --clone
- 直接下载使用:
这样, 我们就能在当前目录下得到一个vue的初始工程了。
当我们使用vue-cli
时, 其实依赖了两个东西: 一个是vue-cli
命令行, 一个是vue-template
模板, 用于生成工程。
流程:
- 当我们全局安装了
vue-cli
后, 会注册环境变量,生成软连接, 这样我们在命令行中任意路径就可以使用该命令了。 - 当我们敲下
vue init webpack my-project
时,vue-cli
会提示你正在下载模板。
此时, vue-cli
就是从github托管的代码中download
对应的webpack
模板。
对应的webpack模板的git地址在这里: webpack模板
拼接url代码是这段:
function getUrl (repo, clone) {
var url
// Get origin with protocol and add trailing slash or colon (for ssh)
var origin = addProtocol(repo.origin, clone)
if (/^git\@/i.test(origin))
origin = origin + ':'
else
origin = origin + '/'
// Build url
if (clone) {
url = origin + repo.owner + '/' + repo.name + '.git'
} else {
if (repo.type === 'github')
url = origin + repo.owner + '/' + repo.name + '/archive/' + repo.checkout + '.zip'
else if (repo.type === 'gitlab')
url = origin + repo.owner + '/' + repo.name + '/repository/archive.zip?ref=' + repo.checkout
else if (repo.type === 'bitbucket')
url = origin + repo.owner + '/' + repo.name + '/get/' + repo.checkout + '.zip'
}
return url
}
- 当模板下载完毕后,
vue-cli
会将它放在你的本地,方便你以后离线使用它生成项目, 路径是/Users/xxx/.vue-templates
, 如果你之前有使用vue-cli
生成过项目, 应该在你的管理员路径下能找到对应的.vue-templates
文件夹。里面的webpack文件就和上面git地址里的代码一模一样。
注意: .开头的文件夹默认是隐藏的, 你需要让它展示出来才能看到。
- 询问交互
接下, vue-cli
会问你一堆问题, 你回答的这些问题它会将它们的答案存起来, 在接下来的生成中, 会根据你的答案来渲染生成对应的文件。
- 文件筛选
在你回答完问题后, vue-cli
就会根据你的需求从webpack模板中筛选出无用的文件, 并删除, 它不是从你本地删除, 只是在给你生成的项目中删除这些文件。
- 模板渲染
在模板中, 你的src/App.vue
长这样:
<template>
<div id="app">
<img src="./assets/logo.png">
{{#router}}
<router-view/>
{{else}}
<HelloWorld/>
{{/router}}
</div>
</template>
<script>
{{#unless router}}
import HelloWorld from './components/HelloWorld'
{{/unless}}
export default {
name: 'App'{{#router}}{{else}},
components: {
HelloWorld
}{{/router}}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
如果在选择是否需要路由, 你选是,那最后生成在你的项目的App.vue
长这样:
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
它会根据你的要求,渲染出不同的文件给你。
- 文件生成
在完成渲染后, 接下来就会在你当前目录下生成对应的文件了, 至此, vue-cli
的工作就完成了。
动手实现
搞明白了vue-cli
的工作原理, 我们完全可以自己做一个简单点的cli出来了。
命令注册
通过npm init
生成你的package.json
文件, 在里面加入bin
"bin": {
"xxx": "bin/index.js"
},
这样, 当你全局装包的时候才会把你xxx
命令注册到环境变量中。
接下来就是bin/index.js
的事了。
使用commander
完成命令行中的命令
program
.command('init [project-name]')
.description('create a project')
.option("-c, --clone", `it will clone from ${tmpUrl}`)
.option('--offline', 'use cached template')
.action(function (name, options) {
console.log('we are try to create "%s"....', name);
downloadAndGenerate(name, options)
}).on('--help', function () {
console.log('');
console.log('Examples:');
console.log('');
console.log(' $ masoneast init my-project');
console.log(` $ path: ${home}`);
});
program.parse(process.argv)
通过上面代码, 你就有了init
命令, 和clone
, offline
参数了, 此时你就有了:
$ masoneast init my-project
$ masoneast init my-project --clone
$ masoneast init my-project --offline
关于commander
包的具体使用, 可以看这里: commander
实现下载和clone模板
这里你需要有有个模板的地址供你下载和clone, 如果你只是玩玩的话也可以直接使用vue
提供的模板地址, 或者我的模板地址:
模板
下载实现代码:
这里依赖了两个库: git-clone
和download
。
function download (name, clone, fn) {
if (clone) {
gitclone(tmpUrl, tmpPath, err => {
if (err) fn(err)
rm(tmpPath + '/.git')
fn()
})
} else {
const url = tmpUrl.replace(/\.git*/, '') + '/archive/master.zip'
console.log(url)
downloadUrl(url, tmpPath, { extract: true, strip: 1, mode: '666', headers: { accept: 'application/zip' } })
.then(function (data) {
fn()
})
.catch(function (err) {
fn(err)
})
}
}
实现询问交互
交互的实现, 主要依赖了inquirer
库。
function askQuestion (prompts) { //询问交互
return (files, metalsmith, done) => {
async.eachSeries(Object.keys(prompts), (key, next) => {
prompt(metalsmith.metadata(), key, prompts[key], next)
}, done)
}
}
将询问得到的答案存贮起来, 留给后面渲染使用
function prompt (data, key, prompt, done) { //将用户操作存储到metaData中
inquirer.prompt([{
type: prompt.type,
name: key,
message: prompt.message || prompt.label || key,
default: prompt.default,
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)
}
实现模板渲染
模板渲染, 依赖了前端模板引擎handlebar
和解析模板引擎的consolidate
库。 上面看到的vue-template
模板里的{{#router}}
其实就是handlebar
的语法。
function renderTemplateFiles () {
return (files, metalsmith, done) => {
const keys = Object.keys(files)
const metalsmithMetadata = metalsmith.metadata() //之前用户操作后的数据存在这里面
async.each(keys, (file, next) => { //对模板进行遍历, 找到需要渲染内容的文件
const str = files[file].contents.toString()
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)
}
}
实现将文件从本地写到你的项目目录中
这里用到了一个核心库: metalsmith
。它主要功能就是读取你的文件, 并通过一系列的中间件对你的文件进行处理, 然后写到你想要的路径中去。就是通过这个库, 将我们的各个流程串联起来, 实现对模板的改造, 写出你想要的项目。
metalsmith.use(askQuestion(options.prompts)) //这一段是generator的精华, 通过各种中间件对用户选择的模板进行处理
.use(filterFiles(options.filters)) //文件筛选过滤
.use(renderTemplateFiles()) //模板内部变量渲染
.source('.')
.destination(projectPath) //项目创建的路径
.build((err, files) => {
if (err) console.log(err)
})
后话
我这里实现的demo就是vue-cli
的精简版, 主要功能有:
-
- 从git上download和clone项目模板
-
- 保存模板到本地,方便离线使用
-
- 询问问题, 按用户需求定制模板
vue-cli
还有有很多的容错判断, 以及其他模板, 下载源等的切换我这里都没有做处理了。
这个masoneast-cli
就是我阅读vue-cli
源码的学习成果, 这里做一个总结。 如果 对大家有帮助, 随手给个star
✨呗。