Vue-cli 原理分析

背景

在平时工作中会有遇到许多以相同模板定制的小程序,因此想自己建立一个生成模板的脚手架工具,以模板为基础构建对应的小程序,而平时的小程序都是用mpvue框架来写的,因此首先先参考一下Vue-cli的原理。知道原理之后,再定制自己的模板脚手架肯定是事半功倍的。

在说代码之前我们首先回顾一下Vue-cli的使用,我们通常使用的是webpack模板包,输入的是以下代码。

vue init webpack [project-name]
在执行这段代码之后,系统会自动下载模板包,随后会询问我们一些问题,比如模板名称,作者,是否需要使用eslint,使用npm或者yarn进行构建等等,当所有问题我们回答之后,就开始生成脚手架项目。

我们将源码下载下来,源码仓库点击这里,平时用的脚手架还是2.0版本,要注意,默认的分支是在dev上,dev上是3.0版本。

我们首先看一下package.json,在文件当中有这么一段话

{

"bin": {

"vue":"bin/vue",

"vue-init":"bin/vue-init",

"vue-list":"bin/vue-list"

}

}

由此可见,我们使用的命令 vue init,应该是来自bin/vue-init这个文件,我们接下来看一下这个文件中的内容

bin/vue-init

constdownload =require('download-git-repo')

constprogram =require('commander')

constexists =require('fs').existsSync

constpath =require('path')

constora =require('ora')

consthome =require('user-home')

consttildify =require('tildify')

constchalk =require('chalk')

constinquirer =require('inquirer')

constrm =require('rimraf').sync

constlogger =require('../lib/logger')

constgenerate =require('../lib/generate')

constcheckVersion =require('../lib/check-version')

constwarnings =require('../lib/warnings')

constlocalPath =require('../lib/local-path')

download-git-repo 一个用于下载git仓库的项目的模块
commander 可以将文字输出到终端当中
fs 是node的文件读写的模块
path 模块提供了一些工具函数,用于处理文件与目录的路径
ora 这个模块用于在终端里有显示载入动画
user-home 获取用户主目录的路径
tildify 将绝对路径转换为波形路径 比如/Users/sindresorhus/dev → ~/dev
inquirer 是一个命令行的回答的模块,你可以自己设定终端的问题,然后对这些回答给出相应的处理
rimraf 是一个可以使用 UNIX 命令 rm -rf的模块
剩下的本地路径的模块其实都是一些工具类,等用到的时候我们再来讲

// 是否为本地路径的方法 主要是判断模板路径当中是否存在 ./
const isLocalPath = localPath.isLocalPath
// 获取模板路径的方法 如果路径参数是绝对路径 则直接返回 如果是相对的 则根据当前路径拼接

constgetTemplatePath = localPath.getTemplatePath

/**

* Usage.

*/

program

.usage(' [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()

})

/**

* Help.

*/

functionhelp(){

program.parse(process.argv)

if(program.args.length <1)returnprogram.help()

}

help()

这部分代码声明了vue init用法,如果在终端当中 输入 vue init --help或者跟在vue init 后面的参数长度小于1,也会输出下面的描述

Usage: vue-init  [project-name]

Options:

-c, --cloneusegit clone

--offlineusecached template

-h, --help   output usage information

Examples:

# create a new project with an official template

$ vue init webpackmy-project

# create a new project straight from a github template

$ vue init username/repomy-project

接下来是一些变量的获取

/**Settings.*/

// 模板路径

lettemplate = program.args[0]

consthasSlash = template.indexOf('/') >-1

// 项目名称

constrawName = program.args[1]

constinPlace = !rawName || rawName ==='.'

// 如果不存在项目名称或项目名称输入的'.' 则name取的是 当前文件夹的名称

constname = inPlace ? path.relative('../', process.cwd()) : rawName

// 输出路径

constto = path.resolve(rawName ||'.')

// 是否需要用到 git clone

constclone = program.clone ||false

// tmp为本地模板路径 如果 是离线状态 那么模板路径取本地的

consttmp = path.join(home,'.vue-templates', template.replace(/[/:]/g,'-'))

if(program.offline) {

console.log(`> Use cached template at${chalk.yellow(tildify(tmp))}`)

template = tmp

}

接下来主要是根据模板名称,来下载并生产模板,如果是本地的模板路径,就直接生成。

/**

* Check, download and generate the project.

*/

functionrun(){

// 判断是否是本地模板路径

if(isLocalPath(template)) {

// 获取模板地址

consttemplatePath = getTemplatePath(template)

// 如果本地模板路径存在 则开始生成模板

if(exists(templatePath)) {

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)

}

}else{

// 非本地模板路径 则先检查版本

checkVersion(()=>{

// 路径中是否 包含'/'

// 如果没有 则进入这个逻辑

if(!hasSlash) {

// 拼接路径 'vuejs-tempalte'下的都是官方的模板包

constofficialTemplate ='vuejs-templates/'+ template

// 如果路径当中存在 '#'则直接下载

if(template.indexOf('#') !==-1) {

downloadAndGenerate(officialTemplate)

}else{

// 如果不存在 -2.0的字符串 则会输出 模板废弃的相关提示

if(template.indexOf('-2.0') !==-1) {

warnings.v2SuffixTemplatesDeprecated(template, inPlace ?'': name)

return

}

// 下载并生产模板

downloadAndGenerate(officialTemplate)

}

}else{

// 下载并生生成模板

downloadAndGenerate(template)

}

})

}

}

我们来看下 downloadAndGenerate这个方法

/**

* Download a generate from a template repo.

*

*@param{String} template

*/

functiondownloadAndGenerate(template){

// 执行加载动画

constspinner = ora('downloading template')

spinner.start()

// Remove if local template exists

// 删除本地存在的模板

if(exists(tmp)) rm(tmp)

// template参数为目标地址 tmp为下载地址 clone参数代表是否需要clone

download(template, tmp, {clone}, err => {

// 结束加载动画

spinner.stop()

// 如果下载出错 输出日志

if(err) logger.fatal('Failed to download repo '+ template +': '+ err.message.trim())

// 模板下载成功之后进入生产模板的方法中 这里我们再进一步讲

generate(name, tmp, to, err => {

if(err) logger.fatal(err)

console.log()

logger.success('Generated "%s".', name)

})

})

}

到这里为止,bin/vue-init就讲完了,该文件做的最主要的一件事情,就是根据模板名称,来下载生成模板,但是具体下载和生成的模板的方法并不在里面。

下载模板

下载模板用的download方法是属于download-git-repo模块的。

最基础的用法为如下用法,这里的参数很好理解,第一个参数为仓库地址,第二个为输出地址,第三个是否需要 git clone,带四个为回调参数

download('flipxfx/download-git-repo-fixture','test/tmp',{clone:true},function(err){

console.log(err ?'Error':'Success')

})

在上面的run方法中有提到一个#的字符串实际就是这个模块下载分支模块的用法

download('bitbucket:flipxfx/download-git-repo-fixture#my-branch','test/tmp', {clone:true},function(err){

console.log(err ?'Error':'Success')

})

生成模板

模板生成generate方法在generate.js当中,我们继续来看一下

generate.js

constchalk =require('chalk')

constMetalsmith =require('metalsmith')

constHandlebars =require('handlebars')

constasync=require('async')

constrender =require('consolidate').handlebars.render

constpath =require('path')

constmultimatch =require('multimatch')

constgetOptions =require('./options')

constask =require('./ask')

constfilter =require('./filter')

constlogger =require('./logger')

chalk 是一个可以让终端输出内容变色的模块
Metalsmith是一个静态网站(博客,项目)的生成库
handlerbars 是一个模板编译器,通过template和json,输出一个html
async 异步处理模块,有点类似让方法变成一个线程
consolidate 模板引擎整合库
multimatch 一个字符串数组匹配的库
options 是一个自己定义的配置项文件

随后注册了2个渲染器,类似于vue中的 vif velse的条件渲染

// register handlebars helper

Handlebars.registerHelper('if_eq',function(a, b, opts){

returna === b

? opts.fn(this)

: opts.inverse(this)

})

Handlebars.registerHelper('unless_eq',function(a, b, opts){

returna === b

? opts.inverse(this)

: opts.fn(this)

})

接下来看关键的generate方法

module.exports =functiongenerate(name, src, dest, done){

// 读取了src目录下的 配置文件信息, 同时将 name auther(当前git用户) 赋值到了 opts 当中

constopts = getOptions(name, src)

// 拼接了目录 src/{template} 要在这个目录下生产静态文件

constmetalsmith = Metalsmith(path.join(src,'template'))

// 将metalsmitch中的meta 与 三个属性合并起来 形成 data

constdata =Object.assign(metalsmith.metadata(), {

destDirName: name,

inPlace: dest === process.cwd(),

noEscape:true

})

// 遍历 meta.js元数据中的helpers对象,注册渲染模板数据

// 分别指定了 if_or 和   template_version内容

opts.helpers &&Object.keys(opts.helpers).map(key=>{

Handlebars.registerHelper(key, opts.helpers[key])

})

consthelpers = { chalk, logger }

// 将metalsmith metadata 数据 和 { isNotTest, isTest 合并 }

if(opts.metalsmith &&typeofopts.metalsmith.before ==='function') {

opts.metalsmith.before(metalsmith, opts, helpers)

}

// askQuestions是会在终端里询问一些问题

// 名称 描述 作者 是要什么构建 在meta.js 的opts.prompts当中

// filterFiles 是用来过滤文件

// renderTemplateFiles 是一个渲染插件

metalsmith.use(askQuestions(opts.prompts))

.use(filterFiles(opts.filters))

.use(renderTemplateFiles(opts.skipInterpolation))

if(typeofopts.metalsmith ==='function') {

opts.metalsmith(metalsmith, opts, helpers)

}elseif(opts.metalsmith &&typeofopts.metalsmith.after ==='function') {

opts.metalsmith.after(metalsmith, opts, helpers)

}

// clean方法是设置在写入之前是否删除原先目标目录 默认为true

// source方法是设置原路径

// destination方法就是设置输出的目录

// build方法执行构建

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)

if(typeofopts.complete ==='function') {

// 当生成完毕之后执行 meta.js当中的 opts.complete方法

consthelpers = { chalk, logger, files }

opts.complete(data, helpers)

}else{

logMessage(opts.completeMessage, data)

}

})

returndata

}

meta.js
接下来看以下complete方法

complete:function(data, { chalk }){

constgreen = chalk.green

// 会将已有的packagejoson 依赖声明重新排序

sortDependencies(data, green)

constcwd = path.join(process.cwd(), data.inPlace ?'': data.destDirName)

// 是否需要自动安装 这个在之前构建前的询问当中 是我们自己选择的

if(data.autoInstall) {

// 在终端中执行 install 命令

installDependencies(cwd, data.autoInstall, green)

.then(()=>{

returnrunLintFix(cwd, data, green)

})

.then(()=>{

printMessage(data, green)

})

.catch(e=>{

console.log(chalk.red('Error:'), e)

})

}else{

printMessage(data, chalk)

}

}

构建自定义模板

在看完vue-init命令的原理之后,其实定制自定义的模板是很简单的事情,我们只要做2件事

首先我们需要有一个自己模板项目
如果需要自定义一些变量,就需要在模板的meta.js当中定制
由于下载模块使用的是download-git-repo模块,它本身是支持在github,gitlab,bitucket上下载的,到时候我们只需要将定制好的模板项目放到git远程仓库上即可。

由于我需要定义的是小程序的开发模板,mpvue本身也有一个quickstart的模板,那么我们就在它的基础上进行定制,首先我们将它fork下来,新建一个custom分支,在这个分支上进行定制。

我们需要定制的地方有用到的依赖库,需要额外用到less以及wxparse
因此我们在 template/package.json当中进行添加

{

// ... 部分省略

"dependencies": {

"mpvue":"^1.0.11"{{#vuex}},

"vuex":"^3.0.1"{{/vuex}}

},

"devDependencies": {

// ... 省略

// 这是添加的包

"less":"^3.0.4",

"less-loader":"^4.1.0",

"mpvue-wxparse":"^0.6.5"

}

}

除此之外,我们还需要定制一下eslint规则,由于只用到standard,因此我们在meta.js当中 可以将 airbnb风格的提问删除

"lintConfig": {

"when":"lint",

"type":"list",

"message":"Pick an ESLint preset",

"choices": [

{

"name":"Standard (https://github.com/feross/standard)",

"value":"standard",

"short":"Standard"

},

{

"name":"none (configure it yourself)",

"value":"none",

"short":"none"

}

]

}

.eslinttrc.js

'rules': {

{{#if_eq lintConfig "standard"}}

"camelcase":0,

//allow paren-less arrow functions

"arrow-parens":0,

"space-before-function-paren":0,

//allow async-await

"generator-star-spacing":0,

{{/if_eq}}

{{#if_eq lintConfig "airbnb"}}

//don't require .vue extension when importing

'

import/extensions': ['error', 'always', {

'

js': 'never',

'

vue': 'never'

}],

// allow optionalDependencies

'

import/no-extraneous-dependencies': ['error', {

'

optionalDependencies': ['test/unit/index.js']

}],

{{/if_eq}}

// allow debugger during development

'

no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0

}

最后我们在构建时的提问当中,再设置一个小程序名称的提问,而这个名称会设置到导航的标题当中。
提问是在meta.js当中添加

"prompts": {

"name": {

"type":"string",

"required":true,

"message":"Project name"

},

// 新增提问

"appName": {

"type":"string",

"required":true,

"message":"App name"

}

}

main.json

{

"pages": [

"pages/index/main",

"pages/counter/main",

"pages/logs/main"

],

"window": {

"backgroundTextStyle":"light",

"navigationBarBackgroundColor":"#fff",

// 根据提问设置标题

"navigationBarTitleText":"{{appName}}",

"navigationBarTextStyle":"black"

}

}

最后我们来尝试一下我们自己的模板

vue init Baifann/mpvue-quickstart#custom min-app-project

Vue-cli 原理分析_第1张图片
Vue-cli 原理分析_第2张图片

总结

以上模板的定制是十分简单的,在实际项目上肯定更为复杂,但是按照这个思路应该都是可行的。比如说将一些自行封装的组件也放置到项目当中等等,这里就不再细说。原理解析都是基于vue-cli 2.0的,但实际上 3.0也已经整装待发,如果后续有机会,深入了解之后,再和大家分享,谢谢大家。

https://github.com/BooheeFE/weekly/issues/9

作者:Baifann 

感兴趣的小伙伴,可以关注公众号【grain先森】,回复关键词 “vue”,获取更多资料,更多关键词玩法期待你的探索~

你可能感兴趣的:(Vue-cli 原理分析)