背景
在平時工作中會有遇到許多以相同模板定制的小程序,因此想自己建立一個生成模板的腳手架工具,以模板為基礎構建對應的小程序,而平時的小程序都是用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
const?download?=?require('download-git-repo')const?program?=?require('commander')const?exists?=?require('fs').existsSyncconst?path?=?require('path')const?ora?=?require('ora')const?home?=?require('user-home')const?tildify?=?require('tildify')const?chalk?=?require('chalk')const?inquirer?=?require('inquirer')const?rm?=?require('rimraf').syncconst?logger?=?require('../lib/logger')const?generate?=?require('../lib/generate')const?checkVersion?=?require('../lib/check-version')const?warnings?=?require('../lib/warnings')const?localPath?=?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//?獲取模板路徑的方法?如果路徑參數是絕對路徑?則直接返回?如果是相對的?則根據當前路徑拼接const?getTemplatePath?=?localPath.getTemplatePath 復制代碼/** ?*?Usage. ?*/program ?.usage('<template-name>?[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. ?*/function?help?()?{?program.parse(process.argv)?if?(program.args.length?<?1)?return?program.help() }help() 復制代碼
這部分代碼聲明了vue init用法,如果在終端當中 輸入 vue init --help或者跟在vue init 后面的參數長度小于1,也會輸出下面的描述
?Usage:?vue-init?<template-name>?[project-name] ?Options: ?-c,?--clone?use?git?clone ?--offline?use?cached?template ?-h,?--help?output?usage?information ?Examples:?#?create?a?new?project?with?an?official?template ?$?vue?init?webpack?my-project?#?create?a?new?project?straight?from?a?github?template ?$?vue?init?username/repo?my-project 復制代碼
接下來是一些變量的獲取
/** ?*?Settings. ?*///?模板路徑let?template?=?program.args[0]const?hasSlash?=?template.indexOf('/')?>?-1//?項目名稱const?rawName?=?program.args[1]const?inPlace?=?!rawName?||?rawName?===?'.'//?如果不存在項目名稱或項目名稱輸入的'.'?則name取的是?當前文件夾的名稱const?name?=?inPlace???path.relative('../',?process.cwd())?:?rawName//?輸出路徑const?to?=?path.resolve(rawName?||?'.')//?是否需要用到?git?cloneconst?clone?=?program.clone?||?false//?tmp為本地模板路徑?如果?是離線狀態?那么模板路徑取本地的const?tmp?=?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. ?*/function?run?()?{?//?判斷是否是本地模板路徑 ?if?(isLocalPath(template))?{?//?獲取模板地址 ?const?templatePath?=?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'下的都是官方的模板包 ?const?officialTemplate?=?'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 ?*/function?downloadAndGenerate?(template)?{?//?執行加載動畫 ?const?spinner?=?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
const?chalk?=?require('chalk')const?Metalsmith?=?require('metalsmith')const?Handlebars?=?require('handlebars')const?async?=?require('async')const?render?=?require('consolidate').handlebars.renderconst?path?=?require('path')const?multimatch?=?require('multimatch')const?getOptions?=?require('./options')const?ask?=?require('./ask')const?filter?=?require('./filter')const?logger?=?require('./logger') 復制代碼
chalk 是一個可以讓終端輸出內容變色的模塊 Metalsmith是一個靜態網站(博客,項目)的生成庫 handlerbars 是一個模板編譯器,通過template和json,輸出一個html async 異步處理模塊,有點類似讓方法變成一個線程 consolidate 模板引擎整合庫 multimatch 一個字符串數組匹配的庫 options 是一個自己定義的配置項文件
隨后注冊了2個渲染器,類似于vue中的 vif velse的條件渲染
//?register?handlebars?helperHandlebars.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方法
module.exports?=?function?generate?(name,?src,?dest,?done)?{?//?讀取了src目錄下的?配置文件信息,?同時將?name?auther(當前git用戶)?賦值到了?opts?當中 ?const?opts?=?getOptions(name,?src)?//?拼接了目錄?src/{template}?要在這個目錄下生產靜態文件 ?const?metalsmith?=?Metalsmith(path.join(src,?'template'))?//?將metalsmitch中的meta?與?三個屬性合并起來?形成?data ?const?data?=?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]) ?})?const?helpers?=?{?chalk,?logger?}?//?將metalsmith?metadata?數據?和?{?isNotTest,?isTest?合并?} ?if?(opts.metalsmith?&&?typeof?opts.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?(typeof?opts.metalsmith?===?'function')?{ ?opts.metalsmith(metalsmith,?opts,?helpers) ?}?else?if?(opts.metalsmith?&&?typeof?opts.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?(typeof?opts.complete?===?'function')?{?//?當生成完畢之后執行?meta.js當中的?opts.complete方法 ?const?helpers?=?{?chalk,?logger,?files?} ?opts.complete(data,?helpers) ?}?else?{ ?logMessage(opts.completeMessage,?data) ?} ?})?return?data } 復制代碼
meta.js
接下來看以下complete方法
complete:?function(data,?{?chalk?})?{?const?green?=?chalk.green?//?會將已有的packagejoson?依賴聲明重新排序 ?sortDependencies(data,?green)?const?cwd?=?path.join(process.cwd(),?data.inPlace???''?:?data.destDirName)?//?是否需要自動安裝?這個在之前構建前的詢問當中?是我們自己選擇的 ?if?(data.autoInstall)?{?//?在終端中執行?install?命令 ?installDependencies(cwd,?data.autoInstall,?green) ?.then(()?=>?{?return?runLintFix(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 2.0的,但實際上 3.0也已經整裝待發,如果后續有機會,深入了解之后,再和大家分享,謝謝大家。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。