New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从vue-cli源码学习如何写模板 #56

Open
dwqs opened this Issue Jul 26, 2017 · 9 comments

Comments

Projects
None yet
5 participants
@dwqs
Owner

dwqs commented Jul 26, 2017

vue-clivuejs 官方提供的基于 vuejs 的项目脚手架工具, 可以很快的帮助 vuejs 开发者搭建一个 startup 项目, 免去环境配置的繁琐, 开箱即用. 今天就来看下 vue-cli 的实现.

vue-cli 的版本是 2.8.2

vue-init

vue init 是基于第三方模板生成项目的命令. 先看下其整体流程:

vue-init

首先, vue cli 获取到输入的参数:

# vue-cli/bin/vue-init
// ...
var template = program.args[0]
var hasSlash = template.indexOf('/') > -1
var rawName = program.args[1]
// ...

之后, 会先判断用户是否输入了 offline 选项. 如果有, 则会使用之前缓存的模板:

# vue-cli/bin/vue-init
// ...
var tmp = path.join(home, '.vue-templates', template.replace(/\//g, '-'))
if (program.offline) {
  console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
  template = tmp
}
// ...

如果没有, 则判断将会生成的项目目录是否存在. 若存在, 则会向用户确认是否在当前目录生成项目(代码在这); 若不存在, 之后就会生成一个新的目录.

然后, 会去判断使用的模板是否是本地的, 是本地且存在则使用本地模板生成项目, 反之使用线上模板生成项目(代码在这).

在判断是使用线上的模板之后, 会根据模板名是否带 / 判断是使用官方提供的模板还是使用第三方模板(代码在这).

最后会调用 downloadAndGenerate 去下载官方模板或第三方模板来生成项目(代码在这). vue cli 对模板的下载依赖于 download-git-repo, 所以使用第三方模板时, 对指定模板的输入要求可以见 download.

模板下载成功之后, vue cli 会调用 generate 来生成模板, 这是 cli 的核心模块, 其源码在 lib/generate.js 中. 接下来就具体分析 generate 模块.

generate 模块导出之前, 会先在 handlebars 中注册两个辅助函数: if_equnless_eq, 用于模板中的表达式判断:

# vue-cli/lib/generate.js

//...

// 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 函数接收四个参数: 项目目录名、下载的模板的临时路径、项目目录路径和一个回调函数. 回调函数用于项目生成之后在终端输出一些提示信息. 在 generate 函数内, 首先会读取模板的 meta 信息, 读取的 meta 信息来自于模板目录下的 meta.{js,json} 文件:

# vue-cli/lib/options.js
// ...
// dir 是模板下载成功之后的临时路径
var json = path.join(dir, 'meta.json')
var js = path.join(dir, 'meta.js')
var opts = {}

// ...

具体实现戳此. 之后会读取用户的 git 昵称和邮箱用于设置 meta 信息的一些默认属性.

得到基本的 meta 信息之后, 会利用 metalsmith 读取 template 内容:

# vue-cli/lib/generate.js
// ...
// src 是模板下载成功之后的临时路径
var opts = getOptions(name, src)  
var metalsmith = Metalsmith(path.join(src, 'template'))

// ...

需要注意的是, 读取的内容是模板的 tempalte 目录. metalsmith 会返回文件路径和文件内容相映射的对象, 这样会方便 metalsmith 的中间件对文件进行处理.

之后, vue cli 使用了三个中间件来处理模板:

//vue-cli/lib/generate.js#L53-L55

metalsmith.use(askQuestions(opts.prompts))
	.use(filterFiles(opts.filters))
	.use(renderTemplateFiles(opts.skipInterpolation))

askQuestions

中间件 askQuestions 用于读取用户输入:

function askQuestions (prompts) {
  return function (files, metalsmith, done) {
    ask(prompts, metalsmith.metadata(), done)
  }
}

ask 的源码在 vue-cli/lib/ask.js 中, 其会遍历 prompts, 在终端交互式的读取用户输入, 并将数据保存在 global metadata 中, 便于后续依赖 global metadata 的中间件对模板进行进一步处理. prompts 是一个对象, 每个 prompt 都是一个 Inquirer.js question object. 示例如下:

// meta.{js,json}
{
    "prompts": {
    	"name": {
        	"type": "string",
        	"required": true,
	       "message" : "Project name"
	    },
	    "version": {
	       "type": "input",
	       "message": "project's version",
	       "default": "1.0.0"
	    }
    }
}

ask 中, 对 meta 信息中的 prompt 会有条件的咨询用户:

// vue-cli/lib/ask.js#prompt

inquirer.prompt([{
	type: prompt.type,
	message: prompt.message,
	default: prompt.default
	//...
}], function(answers) {
	// 保存用户的输入
})

经过 askQuestions 中间件处理之后, global metadata 是一个以 prompt 中的 key 为 key, 用户的输入为 value 的对象:

// global metadata
{
	name: 'test',
	version: '0.1.1'
	// ...
}

filterFiles

中间件 filterFiles 会根据 meta 信息中的 filters 都文件进行过滤:

function filterFiles (filters) {
  return function (files, metalsmith, done) {
    filter(files, filters, metalsmith.metadata(), done)
  }
}

filter 的源码在 vue-cli/lib/filter.js 中:

module.exports = function (files, filters, data, done) {
  // 没有 filters 直接返回
  if (!filters) {
    return done()
  }
  
  // 获取所有的文件名(即路径, eg: test/**)
  var fileNames = Object.keys(files)
 	
  // 遍历 filters
  Object.keys(filters).forEach(function (glob) {
    fileNames.forEach(function (file) {
      if (match(file, glob, { dot: true })) {
        // 获取到匹配的值
        var condition = filters[glob]
        if (!evaluate(condition, data)) {
          // 删除文件
          delete files[file]
        }
      }
    })
  })
  done()
}

evaluate 用于执行 js 表达式, 关键定义如下:

// vue-cli/lib/eval.js

var fn = new Function('data', 'with (data) { return ' + exp + '}')

所以在 filters 中, 可以将某些 keyvalue 定义为一个 js 表达式.

renderTemplateFiles

根据用户的输入过滤掉不需要的文件之后, 就可以利用 renderTemplateFiles 中间件来渲染模板了:

// vue-cli/lib/generate.js#renderTemplateFiles

// ...
var render = require('consolidate').handlebars.render
var async = require('async')
// ...

function renderTemplateFiles(//...){
	return function (files, metalsmith, done) {
		var keys = Object.keys(files)
    	var metalsmithMetadata = metalsmith.metadata()
    	
    	// 遍历 keys
    	async.each(keys, function(file, next){
    		// 读取文件内容
    		var str = files[file].contents.toString()
    		
    		// 不渲染不含mustaches表达式的文件
    		if (!/{{([^{}]+)}}/g.test(str)) {
	        	return next()
	      	}
	      	
	      	// 调用 handlebars 渲染文件
	      	render(/* 渲染文件 */)
    	     })
	}
}

渲染完成之后, metalsmith 会将最终结果 build 的 dest 目录. 若失败, 则将 err 传给回调输出; 反之, 如果 meta 信息有 complete(函数) 或者 completeMessage(字符串), 则会进行调用或输出:


// vue-cli/lib/generate.js

// ...
var opts = getOptions(name, src)

// ...

if (typeof opts.complete === 'function') {
	var helpers = {chalk, logger, files}
	opts.complete(data, helpers)
} else {
	logMessage(opts.completeMessage, data)
}

// ...

vue-list

vue list 命令用于查看官方提供的模板列表, 源码在 vue-cli/bin/vue-list 中, 关键代码如下:

// ...
var request = require('request')

//...

request({
	url: 'https://api.github.com/users/vuejs-templates/repos',
   headers: {
     'User-Agent': 'vue-cli'
   }
}, function(err, res, body) {
	// 在终端输出列表
})

需要注意的是, Github Api 对未认证的请求是有请求数限制的, 超过限制则会报错, 但可以通过 BA 认证的方式来提高请求数限制, 具体可以戳此.

这是个潜在的问题, 已经有 vue-cli 的用户碰到过认证失败的问题: #368. vue-cli 的下一个版本可能会解决这个问题, 已经有社区用户提出 PR.

怎么自己写模板呢

从上述的分析可以知道, 模板是有特定的目录结构的:

  • 模板仓库的根目录下必须有 template 目录, 在该目录下定义你的模板文件
  • 模板仓库的根目录下必须有 meta.{js,json} 文件, 该文件必须导出为一个对象, 用于定义模板的 meta 信息

对于 meta.{js,json} 文件, 目前可定义的字段如下:

  • prompts<Object>: 收集用户自定义数据
  • filters<Object>: 根据条件过滤文件
  • completeMessage<String>: 模板渲染完成后给予的提示信息, 支持 handlebars 的 mustaches 表达式
  • complete<Function>: 模板渲染完成后的回调函数, 优先于 completeMessage
  • helpers<Object>: 自定义的 Handlebars 辅助函数

prompts

prompts 是一个对象, 每个 prompt 都是一个 Inquirer.js question object. 示例如下:

// meta.{js,json}
{
    "prompts": {
    	"name": {
        	"type": "string",
        	"required": true,
	       "message" : "Project name"
	    },
	    "test": {
        	"type": "confirm",
	       "message" : "Unit test?"
	    },
	    "version": {
	       "type": "input",
	       "message": "project's version",
	       "default": "1.0.0"
	    }
    }
}

所有的用户输入完成之后, template 目录下的所有文件将会用 Handlebars 进行渲染. 用户输入的数据会作为模板渲染时的使用数据:

// template/package.json

{{#test}}
"test": "npm run test"
{{/test}}

在上述示例中, 只有用户在 test 中的回答值是 yes 时, test 脚本才会在 package.json 文件中生成.

prompt 可以添加一个 when 字段, 该字段表示此 prompt 会根据 when 的值来判断是否出现在终端提示用户进行输入. 在 vue-cli 中, 其会根据 when 进行 eval 运算:

// ...

if (prompt.when && !evaluate(prompt.when, data)) {
	return done()
}

//...

whenprompt 示例:

{
  "prompts": {
    "lint": {
        "type": "confirm",
        "message": ""Use ESLint to lint your code?"
    },
    "eslint": {
      "when": "lint",
      "type": "list",
      "message": "Pick a lint config",
      "choices": [
        "standard",
        "airbnb",
        "none"
      ]
    }
  }
}

在上述示例中, 只有用户在 lint 中的回答值是 yes 时, eslint 才会被触发, 在终端显示让用户选择 eslint 的配置规范.

filters

filters 字段是一个包含文件过滤规则的对象, 键用于定义符合 minimatch glob pattern 规则的过滤器, 键值是 prompts 中用户的输入值或者表达式. 例如:

{
  "prompts": {
      "unit": {
          "type": "confirm",
          "message": "Setup unit tests with Mocha?"
      }
  },  
  "filters": {
    "test/*": "unit"
  }
}

在上述示例中, template 目录下 test 目录只有用户在 unit 中的回答值是 yes 时才会生成, 反之会被删除.

如果要匹配以 . 开头的文件, 则需要将 minimatch 的 dot 选项设置成 true.

helpers

helpers 字段是一个包含自定义的 Handlebars 辅助函数的对象, 自定义的函数可以在 template 中使用:

{
	"helpers": {
	    "if_or": function (v1, v2, options) {
	      if (v1 || v2) {
	        return options.fn(this);
	      }
	
	      return options.inverse(this);
	    }
  	},
}

template 的文件使用该 if_or:

{{#if_or val1 val2}}
// 当 val1 或者 val2 为 true 时, 这里才会被渲染
{{/if_or}}

complete

在渲染完成后的 complete 回调:

{
	"complete": function(data, helpers) {}
}

datahelpersvue cli 传入:

// vue-cli/lib/generate.js

// ...
var data = Object.assign(metalsmith.metadata(), {
	destDirName: name,
	inPlace: dest === process.cwd(),
	noEscape: true
})

// ...

// files 是 metalsmith build 之后的文件对象
var helpers = {chalk, logger, files}

// ...

如果 complete 有定义, 则调用 complete, 反之会输出 completeMessage.

总结

vue-cli 的源码还是很好分析的, 参考 vue-cli, 写了一个简化的脚手架工具 chare, 其新加了三个功能:

  • token 设置, 用于 Github Api 的 BA 认证
  • init project 时可以关联一个远程仓库
  • 支持 prompt filter

自己针对日常使用的 vuejsreact 框架写了一些 startup, 欢迎指正:

@sinoon

This comment has been minimized.

sinoon commented Mar 1, 2018

学习了,感谢分享

@lihaizhong

This comment has been minimized.

lihaizhong commented Mar 22, 2018

谢谢分享,根据您的分享,我自己也成功搭建了一个属于自己的vue模板。

@fundatou

This comment has been minimized.

fundatou commented Mar 28, 2018

请问大佬一个问题,我写的针对业务的模版中有vue变量({{obj.name}}这种形式)在html中,但是初始化之后的代码中,貌似被当作 handlebars 的 mustaches 表达式,因为找不到对应的变量就直接被忽略了,请问有什么办法能阻止这种问题吗?我目前的想法是给handlebars的变量替换加个范围,然后让某些文件中的mustaches写法的不受影响,但是找不到如何下手,求大佬指点一下

@dwqs

This comment has been minimized.

Owner

dwqs commented Mar 28, 2018

@fundatou{{obj.name}} 改成 \{{obj.name}} 试试

@fundatou

This comment has been minimized.

fundatou commented Mar 28, 2018

@dwqs 这样是可以的,谢谢大佬!

@choukin

This comment has been minimized.

choukin commented Oct 25, 2018

如果模版想部署在 私有 gitlab 库 并且想用 vue cli 应该从哪些地方入手

@dwqs

This comment has been minimized.

Owner

dwqs commented Oct 25, 2018

vue cli 貌似并不支持,你可以改下源码 哈哈 @choukin

@fundatou

This comment has been minimized.

fundatou commented Oct 26, 2018

我觉得好像可以诶,按照vue cli中用的download-git-repo的源码写正确的初始化命令就行了吧 @choukin ,不知道你是不是想问这个问题

@choukin

This comment has been minimized.

choukin commented Oct 30, 2018

@fundatou 多谢我试试

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment