当你使用vue-cli时, 你有没有想过什么?一起来实现一个精简版吧

3,301 阅读5分钟

前言

最近在研究前端工程化的一些东西, 那就得有自己的一套脚手架, 于是瞄上了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

  • 使用:

    1. 直接下载使用: vue init webpack my-project
    2. 离线使用: vue init webpack my-projiect --offline
    3. clone使用: vue init webpack my-projiect --clone

这样, 我们就能在当前目录下得到一个vue的初始工程了。

当我们使用vue-cli时, 其实依赖了两个东西: 一个是vue-cli命令行, 一个是vue-template模板, 用于生成工程。

流程:

  1. 当我们全局安装了vue-cli, 会注册环境变量,生成软连接, 这样我们在命令行中任意路径就可以使用该命令了。
  2. 当我们敲下vue init webpack my-projectvue-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
}
  1. 当模板下载完毕后vue-cli会将它放在你的本地,方便你以后离线使用它生成项目, 路径是/Users/xxx/.vue-templates, 如果你之前有使用vue-cli生成过项目, 应该在你的管理员路径下能找到对应的.vue-templates文件夹。里面的webpack文件就和上面git地址里的代码一模一样。

注意: .开头的文件夹默认是隐藏的, 你需要让它展示出来才能看到。

  1. 询问交互

接下, vue-cli会问你一堆问题, 你回答的这些问题它会将它们的答案存起来, 在接下来的生成中, 会根据你的答案来渲染生成对应的文件。

  1. 文件筛选

在你回答完问题后, vue-cli就会根据你的需求从webpack模板中筛选出无用的文件, 并删除, 它不是从你本地删除, 只是在给你生成的项目中删除这些文件。

  1. 模板渲染

在模板中, 你的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>

它会根据你的要求,渲染出不同的文件给你。

  1. 文件生成

在完成渲染后, 接下来就会在你当前目录下生成对应的文件了, 至此, 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-clonedownload

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的精简版, 主要功能有:

    1. 从git上download和clone项目模板
    1. 保存模板到本地,方便离线使用
    1. 询问问题, 按用户需求定制模板

vue-cli还有有很多的容错判断, 以及其他模板, 下载源等的切换我这里都没有做处理了。

这个masoneast-cli就是我阅读vue-cli源码的学习成果, 这里做一个总结。 如果 对大家有帮助, 随手给个star✨呗。