vue-cli的简单实现

这个demo我是模仿Vue-CLI 2.0写的一个简单的构建工具,3.0的源码还没去看,所以会有不同的地方。

已经上传到github上了

先安装开发依赖的工具

npm i commander handlebars inquirer metalsmith -D

commander:用来处理命令行参数

handlerbars:一个简单高效的语义化模板构建引擎,比如我们用vue-cli构建项目后命令行会有一些交互行为,让你选择要安装的包什么的等等,而Handlerbars.js会根据你的这些选择回答去渲染模版。

inquirer:会根据模版里面的meta.js或者meta.json文件中的设置,与用户进行一些简单的交互以确定项目的一些细节。

metalsmith:一个非常简单的可插拔的静态网站生成器,通过添加一些插件对要构建的模版文件进行处理。

安装完后就能在package.json中看到如下的依赖

依赖

项目目录结构

image.png

其中template-demo里面包含了本次要构建的项目模版templae,和meta.js文件

代码编写

1.bin/dg.js之后在命令行下面运行

node bin/dg.js xxx xxx

就可以构建项目了。
两个 xxx的地方 第一个是项目的模版,第二个是要输入到哪个目录下也就是要构建的项目名称

// dg.js
const program = require('commander')
const path = require('path')
const chalk = require('chalk')  // 终端字体颜色
const inquirer = require('inquirer')
const exists = require('fs').existsSync // 判断 路径是否存在
const generate = require('./lib/generate')

/**
 * 注册一个help的命令
 * 当在终端输入 dg --help 或者没有跟参数的话
 * 会输出提示
 */
program.on('--help', () => {{
  console.log('  Examples:')
  console.log()
  console.log(chalk.gray('    # create a new project with an template')) // 会以灰色字体显示
  console.log('    $ dg dgtemplate my-project')
}})

/**
 * 判断参数是否为空
 * 如果为空调用上面注册的 help命令
 * 输出提示
 */
function help () {
  program.parse(process.argv)  //commander 用来处理 命令行里面的参数, 这边的process是node的一个全局变量不明白的可以查一下资料
  if (program.args.length < 1) return program.help()
}
help()

/**
 * 获取命令行参数
 */
let template = program.args[0] // 命令行第一个参数 模版的名字
const rawName = program.args[1] // 第二个参数 项目目录

/**
 * 获取项目和模版的完整路径
 */
const to = path.resolve(rawName) // 构建的项目的 绝对路径
const tem = path.join(process.cwd(), template) //模版的路径  cwd是当前运行的脚本是在哪个路径下运行

/**
 * 判断这个项目路径是否存在也就是是否存在相同的项目名
 * 如果存在提示 是否继续然后运行 run
 * 如果不存在 则直接运行 run 最后会创建一个项目目录
 */
if (exists(to)) {
  inquirer.prompt([  // 这边就用到了与终端交互的inquirer了
    {
      type: 'confirm',
      message: 'Continue?',
      name: 'ok'
    }
  ]).then(answers => {
    if (answers.ok) {
      run ()
    }
  })
} else {
  run ()
}

/**
 * run函数则是用来调用generate来构建项目
 */
function run () {
  if (exists(tem)) {
    generate(rawName, tem, to, (err) => {
      if (err) console.log(err)  // 如果构建失败就调用的回调函数
    })
  }
}

注释说明 都在代码里面了。

2.接下来就是很重要的lib/generate.js文件了

// generate.js
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const path = require('path')
const chalk = require('chalk')
const getOptions = require('./options')
const ask = require('./ask')


/**
 * 把generate 导出去给dg.js使用
 * opts是通过getOptions()函数用来获取 meta.js中的配置
 * metalsmith是通过metalsmith.js获取模版的元数据
 * metalmith可以让我们编写一些插件来对项目下面的文件进行配置
 * 其中第一个use的第一个插件就是用来在终端中输入一些问题一些选项让我们设置一些模版中的细节
 * 而这些问题就是 放在meta.js中
 * 第二个use的插件这是渲染模版,这里就是用了handebars.js来渲染模版
 * 
 */
module.exports = function generate (name, tem, dest, done) {
  const opts = getOptions(name, tem)
  const metalsmith = Metalsmith(path.join(tem, 'template'))
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd()
  })
  metalsmith.use(askQuestions(opts.prompts)).use(renderTemplateFiles())  // 这两个插件在下面的代码中
  // 在构建前执行一些函数
  metalsmith.clean(false)
    .source('.') // 默认的source路径是 ./src 所以这边要改成整个 template 这个根据自己要输出的需求配置
    .destination(dest)  // 要输出到哪个路径下 这里就是 我们的项目地址
    .build((err, files) => {  // 最后进行构建项目
      done(err) // 执行 回掉函数
      if (typeof opts.complete === 'function') {
        const helpers = { chalk }
        opts.complete(data, helpers)  // 判断meta.js中是否定义了构建完成后要执行的函数 这里是判断是否执行自动安装依赖
      } else {
        console.log('complete is not a function')
      }
    })
}


/**
 * 这里通过这个函数返回一个metalsmith的符合metalsmith插件格式的函数
 * 第一个参数fils就是 这个模版下面的全部文件
 * 第二个参数ms就是元数据这里我们的问题以及回答会已键值对的形式存放在里面用于第二个插件渲染模版
 * 第三个参数就是类似 next的用法了 调用done后才能移交给下一个插件运行
 * ask函数则在另外一个js文件中
 */
function askQuestions (prompts) {
  return (fils, ms, done) => {
    ask(prompts, ms.metadata(), done)
  }
}

/**
 * render函数则是通过我们第一个插件收集这些问题以及回答后
 * 然后渲染我们的模版
 */
function renderTemplateFiles () {
  return (files, ms, done) => {
    const keys = Object.keys(files)  // 获取模版下的所有文件名
    keys.forEach(key => {  // 遍历对每个文件使用handlerbars渲染
      const str = files[key].contents.toString()
      let t = Handlebars.compile(str)
      let html = t(ms.metadata())
      files[key].contents = new Buffer.from(html)  // 渲染后重新写入到文件中
    })
    done() // 移交给下个插件
  }
}

其实generate.js功能就是用来收集我们在命令行下交互的问题的答案用来渲染模版,只不过我这边只是简单的实现,在vue-cli 2.0中还有对文件的过滤,跳过不符合使用handlebars渲染文件,添加一些handlebars的helpers来制定文件渲染的规则等等

  1. lib/options.js
// options.js
const path = require('path')

/**
 * 这里的options内容比较简单
 * 就是用于用来获取 meta.js 里面的配置
 */
module.exports = function options (name, dir) {
  const metaPath = path.join(dir, 'meta.js')
  const req = require(metaPath)
  let opts = {}
  opts = req
  return opts
}

options我也是简单的实现,有兴趣的话可以查看vue-cli的源码

  1. lib/ask.js
// ask.js
const async = require('async')  // 这是node下一个异步处理的工具
const inquirer = require('inquirer')

const promptMapping = {
  string: 'input'
}

/**
 * 这个函数就是 根据meta.js里面定义的prompts来与用户进行交互
 * 然后收集用户的交互信息存放在metadate 也就是metalsmith元数据中
 * 用于渲染模版使用
 */
module.exports = function ask (prompts, metadate, done) {
  async.eachSeries(Object.keys(prompts), (key, next) => {  // 这里不能简单的使用数组的 foreach方法 否则只直接跳到最后一个问题
    inquirer.prompt([{
      type: promptMapping[prompts[key].type] || prompts[key].type,
      name: key,
      message: prompts[key].message,
      choices: prompts[key].choices || [],
    }]).then(answers => {
      if (typeof answers[key] === 'string') {
        metadate[key] = answers[key].replace(/"/g, '\\"')
      } else {
        metadate[key] = answers[key]
      }
      next()
    }).catch(done)
  }, done) // 全部回答完 调用 done移交给下一个插件
}

收集问题的答案用于渲染模版

下面是用于渲染模版的配置中的代码

为了方便 我把要渲染的模版,直接跟 构建工具 项目放到了同个文件夹下面,就是上面我截图的项目结构的 template-demo 里面包含了要渲染的模版 放在 template-demo/template下面了,还包含了渲染模版的配置文件meta.js

// meta.js
const { installDependencies } = require('./utils')
const path = require('path')


/***
 * 要交互的问题都放在 prompts中 
 * when是当什么情况下 用来判断是否 显示这个问题
 * type是提问的类型
 * message就是要显示的问题
 */
module.exports = {
  prompts: {
    name: {
      when: 'ismeta',
      type: 'string',
      message: '项目名称:'
    },
    description: {
      when: 'ismeta',
      type: 'string',
      message: '项目介绍:'
    },
    author: {
      when: 'ismeta',
      type: 'string',
      message: '项目作者:'
    },
    email: {
      when: 'ismeta',
      type: 'string',
      message: '邮箱:'
    },
    dgtable: {
      when: 'ismeta',
      type: 'confirm',
      message: '是否安装dg-table(笔者编写的基于elementui二次开发的强大的表格)',
    },
    genius: {
      when: 'ismeta',
      type: 'list',
      message: '想看想看?',
      choices: [
        {
          name: '想',
          value: '想',
          short: '想'
        },
        {
          name: '很想',
          value: '很想',
          short: '很想'
        }
      ]
    },
    autoInstall: {
      when: 'ismeta',
      type: 'confirm',
      message: '是否自动执行npm install 安装依赖?',
    },
  },
  complete: function(data, { chalk }) {
    /**
     * 用于判断是否执行自动安装依赖
     */
    const green = chalk.green // 取绿色
    const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)
    if (data.autoInstall) {
      installDependencies(cwd, 'npm', green) // 这里使用npm安装
        .then(() => {
          console.log('依赖安装完成')
        })
        .catch(e => {
          console.log(chalk.red('Error:'), e)
        })
    } else {
      // printMessage(data, chalk)
    }
  }
}

主要是用于配置交互的问题,和再项目构建完成后执行的 complete 函数,这里就是 判断用户是否 选择了 自动安装依赖,如果autoInstall为true就自动安装依赖

const spawn = require('child_process').spawn  // 一个node的子线程

/**
 * 安装依赖
 */
exports.installDependencies = function installDependencies(
  cwd,
  executable = 'npm',
  color
) {
  console.log(`\n\n# ${color('正在安装项目依赖 ...')}`)
  console.log('# ========================\n')
  return runCommand(executable, ['install'], {
    cwd,
  })
}


function runCommand(cmd, args, options) {
  return new Promise((resolve, reject) => {
    /**
     * 如果不清楚spaw的话可以上网查一下
     * 这里就是 在项目目录下执行 npm install
     */
    const spwan = spawn(
      cmd,
      args,
      Object.assign(
        {
          cwd: process.cwd(),
          stdio: 'inherit',
          shell: true, // 在shell下执行
        },
        options
      )
    )
    spwan.on('exit', () => {
      resolve()
    })
  })
}

执行安装的具体实现函数。

最后你就可以在构建工具的根目录下 执行

node bin/dg.js template-demo demo

来构建项目啦。
如果把dg.js添加到$PATH中 就可以 直接使用dg template-demo demo来构建项目。

参数为空或者--helpe

Boolean类型

多选类型

自动安装依赖

依赖安装完成

demo就是我们构建的项目

demo/package.json

最后我们可以看到我们在命令行回答的问题被渲染到了这里面来了,根据是否安装 dg-table让这个插件出现在了依赖列表里面,当然包括模版中的index.html也被渲染了。这里图片就不贴出来了。这个模版只不过是为了演示没有其他意义了。

主要是我比较懒,挺多功能没实现,还有vue-cli可以自动从github上面拉取模版,const download = require('download-git-repo') //用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket

如果想更清楚的了解内部实现最好还是看下Vue-cli2.0的源码

已经上传到github上了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 目录 UI组件 开发框架 实用库 服务端 辅助工具 应用实例 Demo示例 UI组件 element★13489 ...
    余生社会阅读 19,658评论 7 233
  • 地产的根本,要看背后的数据。比如当初深圳房价开始怪兽式不可理喻的暴涨,那么很简单,你有兴趣又有数据和途径的话,就去...
    思想家如怀阅读 156评论 0 0
  • 记得以前,也就一两年之前吧,那时候不管怎么晒太阳,皮肤晒黑了,过一段时间就会自己恢复,会白回来,现如今不经晒了,一...
    Kasonn阅读 361评论 0 0
  • 最近每天早上必喝一杯咖啡,才开始一天的生活。以前只是偶尔喝一杯,我是属于敏感体质,有时...
    喜风SanPedroSula阅读 352评论 0 0
  • 重新认知“裂变” ——《流量池》第4章读书笔记 一、内容概要 第4章主要讲解了“裂变营销”相关的基本理论、方法模型...
    WendaoSolemn阅读 301评论 0 0