背景
創(chuàng)新互聯(lián)是一家集網(wǎng)站建設(shè),南票企業(yè)網(wǎng)站建設(shè),南票品牌網(wǎng)站建設(shè),網(wǎng)站定制,南票網(wǎng)站建設(shè)報(bào)價(jià),網(wǎng)絡(luò)營(yíng)銷,網(wǎng)絡(luò)優(yōu)化,南票網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競(jìng)爭(zhēng)力??沙浞譂M足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時(shí)我們時(shí)刻保持專業(yè)、時(shí)尚、前沿,時(shí)刻以成就客戶成長(zhǎng)自我,堅(jiān)持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。
在平時(shí)工作中會(huì)有遇到許多以相同模板定制的小程序,因此想自己建立一個(gè)生成模板的腳手架工具,以模板為基礎(chǔ)構(gòu)建對(duì)應(yīng)的小程序,而平時(shí)的小程序都是用mpvue框架來寫的,因此首先先參考一下Vue-cli的原理。知道原理之后,再定制自己的模板腳手架肯定是事半功倍的。
在說代碼之前我們首先回顧一下Vue-cli的使用,我們通常使用的是webpack模板包,輸入的是以下代碼。
vue?init?webpack?[project-name]復(fù)制代碼
在執(zhí)行這段代碼之后,系統(tǒng)會(huì)自動(dòng)下載模板包,隨后會(huì)詢問我們一些問題,比如模板名稱,作者,是否需要使用eslint,使用npm或者yarn進(jìn)行構(gòu)建等等,當(dāng)所有問題我們回答之后,就開始生成腳手架項(xiàng)目。
我們將源碼下來,源碼倉(cāng)庫(kù)點(diǎn)擊這里,平時(shí)用的腳手架還是2.0版本,要注意,默認(rèn)的分支是在dev上,dev上是3.0版本。
我們首先看一下package.json,在文件當(dāng)中有這么一段話
{?"bin":?{?"vue":?"bin/vue",?"vue-init":?"bin/vue-init",?"vue-list":?"bin/vue-list" ?} } 復(fù)制代碼
由此可見,我們使用的命令 vue init,應(yīng)該是來自bin/vue-init這個(gè)文件,我們接下來看一下這個(gè)文件中的內(nèi)容
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') 復(fù)制代碼
download-git-repo 一個(gè)用于下載git倉(cāng)庫(kù)的項(xiàng)目的模塊 commander 可以將文字輸出到終端當(dāng)中 fs 是node的文件讀寫的模塊 path 模塊提供了一些工具函數(shù),用于處理文件與目錄的路徑 ora 這個(gè)模塊用于在終端里有顯示載入動(dòng)畫 user-home 獲取用戶主目錄的路徑 tildify 將絕對(duì)路徑轉(zhuǎn)換為波形路徑 比如/Users/sindresorhus/dev → ~/dev inquirer 是一個(gè)命令行的回答的模塊,你可以自己設(shè)定終端的問題,然后對(duì)這些回答給出相應(yīng)的處理 rimraf 是一個(gè)可以使用 UNIX 命令 rm -rf的模塊 剩下的本地路徑的模塊其實(shí)都是一些工具類,等用到的時(shí)候我們?cè)賮碇v
//?是否為本地路徑的方法?主要是判斷模板路徑當(dāng)中是否存在?`./`const?isLocalPath?=?localPath.isLocalPath//?獲取模板路徑的方法?如果路徑參數(shù)是絕對(duì)路徑?則直接返回?如果是相對(duì)的?則根據(jù)當(dāng)前路徑拼接const?getTemplatePath?=?localPath.getTemplatePath 復(fù)制代碼/** ?*?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. ?*/function?help?()?{?program.parse(process.argv)?if?(program.args.length?1)?return?program.help() }help() 復(fù)制代碼
這部分代碼聲明了vue init用法,如果在終端當(dāng)中 輸入 vue init --help或者跟在vue init 后面的參數(shù)長(zhǎng)度小于1,也會(huì)輸出下面的描述
?Usage:?vue-init??[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 復(fù)制代碼
接下來是一些變量的獲取
/** ?*?Settings. ?*///?模板路徑let?template?=?program.args[0]const?hasSlash?=?template.indexOf('/')?>?-1//?項(xiàng)目名稱const?rawName?=?program.args[1]const?inPlace?=?!rawName?||?rawName?===?'.'//?如果不存在項(xiàng)目名稱或項(xiàng)目名稱輸入的'.'?則name取的是?當(dāng)前文件夾的名稱const?name?=?inPlace???path.relative('../',?process.cwd())?:?rawName//?輸出路徑const?to?=?path.resolve(rawName?||?'.')//?是否需要用到?git?cloneconst?clone?=?program.clone?||?false//?tmp為本地模板路徑?如果?是離線狀態(tài)?那么模板路徑取本地的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 } 復(fù)制代碼
接下來主要是根據(jù)模板名稱,來下載并生產(chǎn)模板,如果是本地的模板路徑,就直接生成。
/** ?*?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(()?=>?{?//?路徑中是否?包含'/' ?//?如果沒有?則進(jìn)入這個(gè)邏輯 ?if?(!hasSlash)?{?//?拼接路徑?'vuejs-tempalte'下的都是官方的模板包 ?const?officialTemplate?=?'vuejs-templates/'?+?template ?//?如果路徑當(dāng)中存在?'#'則直接下載 ?if?(template.indexOf('#')?!==?-1)?{ ?downloadAndGenerate(officialTemplate) ?}?else?{?//?如果不存在?-2.0的字符串?則會(huì)輸出?模板廢棄的相關(guān)提示 ?if?(template.indexOf('-2.0')?!==?-1)?{ ?warnings.v2SuffixTemplatesDeprecated(template,?inPlace???''?:?name)?return ?}?//?下載并生產(chǎn)模板 ?downloadAndGenerate(officialTemplate) ?} ?}?else?{?//?下載并生生成模板 ?downloadAndGenerate(template) ?} ?}) ?} } 復(fù)制代碼
我們來看下 downloadAndGenerate這個(gè)方法
/** ?*?Download?a?generate?from?a?template?repo. ?* ?*?@param?{String}?template ?*/function?downloadAndGenerate?(template)?{?//?執(zhí)行加載動(dòng)畫 ?const?spinner?=?ora('downloading?template') ?spinner.start()?//?Remove?if?local?template?exists ?//?刪除本地存在的模板 ?if?(exists(tmp))?rm(tmp)?//?template參數(shù)為目標(biāo)地址?tmp為下載地址?clone參數(shù)代表是否需要clone ?download(template,?tmp,?{?clone?},?err?=>?{?//?結(jié)束加載動(dòng)畫 ?spinner.stop()?//?如果下載出錯(cuò)?輸出日志 ?if?(err)?logger.fatal('Failed?to?download?repo?'?+?template?+?':?'?+?err.message.trim())?//?模板下載成功之后進(jìn)入生產(chǎn)模板的方法中?這里我們?cè)龠M(jìn)一步講 ?generate(name,?tmp,?to,?err?=>?{?if?(err)?logger.fatal(err) ?console.log() ?logger.success('Generated?"%s".',?name) ?}) ?}) } 復(fù)制代碼
到這里為止,bin/vue-init就講完了,該文件做的最主要的一件事情,就是根據(jù)模板名稱,來下載生成模板,但是具體下載和生成的模板的方法并不在里面。
下載模板
下載模板用的download方法是屬于download-git-repo模塊的。
最基礎(chǔ)的用法為如下用法,這里的參數(shù)很好理解,第一個(gè)參數(shù)為倉(cāng)庫(kù)地址,第二個(gè)為輸出地址,第三個(gè)是否需要 git clone,帶四個(gè)為回調(diào)參數(shù)
download('flipxfx/download-git-repo-fixture',?'test/tmp',{?clone:?true?},?function?(err)?{?console.log(err???'Error'?:?'Success') }) 復(fù)制代碼
在上面的run方法中有提到一個(gè)#的字符串實(shí)際就是這個(gè)模塊下載分支模塊的用法
download('bitbucket:flipxfx/download-git-repo-fixture#my-branch',?'test/tmp',?{?clone:?true?},?function?(err)?{?console.log(err???'Error'?:?'Success') }) 復(fù)制代碼
生成模板
模板生成generate方法在generate.js當(dāng)中,我們繼續(xù)來看一下
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') 復(fù)制代碼
chalk 是一個(gè)可以讓終端輸出內(nèi)容變色的模塊 Metalsmith是一個(gè)靜態(tài)網(wǎng)站(博客,項(xiàng)目)的生成庫(kù) handlerbars 是一個(gè)模板編譯器,通過template和json,輸出一個(gè)html async 異步處理模塊,有點(diǎn)類似讓方法變成一個(gè)線程 consolidate 模板引擎整合庫(kù) multimatch 一個(gè)字符串?dāng)?shù)組匹配的庫(kù) options 是一個(gè)自己定義的配置項(xiàng)文件
隨后注冊(cè)了2個(gè)渲染器,類似于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) }) 復(fù)制代碼
接下來看關(guān)鍵的generate方法
module.exports?=?function?generate?(name,?src,?dest,?done)?{?//?讀取了src目錄下的?配置文件信息,?同時(shí)將?name?auther(當(dāng)前git用戶)?賦值到了?opts?當(dāng)中 ?const?opts?=?getOptions(name,?src)?//?拼接了目錄?src/{template}?要在這個(gè)目錄下生產(chǎn)靜態(tài)文件 ?const?metalsmith?=?Metalsmith(path.join(src,?'template'))?//?將metalsmitch中的meta?與?三個(gè)屬性合并起來?形成?data ?const?data?=?Object.assign(metalsmith.metadata(),?{?destDirName:?name,?inPlace:?dest?===?process.cwd(),?noEscape:?true ?})?//?遍歷?meta.js元數(shù)據(jù)中的helpers對(duì)象,注冊(cè)渲染模板數(shù)據(jù) ?//?分別指定了?if_or?和?template_version內(nèi)容 ?opts.helpers?&&?Object.keys(opts.helpers).map(key?=>?{ ?Handlebars.registerHelper(key,?opts.helpers[key]) ?})?const?helpers?=?{?chalk,?logger?}?//?將metalsmith?metadata?數(shù)據(jù)?和?{?isNotTest,?isTest?合并?} ?if?(opts.metalsmith?&&?typeof?opts.metalsmith.before?===?'function')?{ ?opts.metalsmith.before(metalsmith,?opts,?helpers) ?}?//?askQuestions是會(huì)在終端里詢問一些問題 ?//?名稱?描述?作者?是要什么構(gòu)建?在meta.js?的opts.prompts當(dāng)中 ?//?filterFiles?是用來過濾文件 ?//?renderTemplateFiles?是一個(gè)渲染插件 ?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方法是設(shè)置在寫入之前是否刪除原先目標(biāo)目錄?默認(rèn)為true ?//?source方法是設(shè)置原路徑 ?//?destination方法就是設(shè)置輸出的目錄 ?//?build方法執(zhí)行構(gòu)建 ?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')?{?//?當(dāng)生成完畢之后執(zhí)行?meta.js當(dāng)中的?opts.complete方法 ?const?helpers?=?{?chalk,?logger,?files?} ?opts.complete(data,?helpers) ?}?else?{ ?logMessage(opts.completeMessage,?data) ?} ?})?return?data } 復(fù)制代碼
meta.js
接下來看以下complete方法
complete:?function(data,?{?chalk?})?{?const?green?=?chalk.green?//?會(huì)將已有的packagejoson?依賴聲明重新排序 ?sortDependencies(data,?green)?const?cwd?=?path.join(process.cwd(),?data.inPlace???''?:?data.destDirName)?//?是否需要自動(dòng)安裝?這個(gè)在之前構(gòu)建前的詢問當(dāng)中?是我們自己選擇的 ?if?(data.autoInstall)?{?//?在終端中執(zhí)行?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) ?} ?} 復(fù)制代碼
構(gòu)建自定義模板
在看完vue-init命令的原理之后,其實(shí)定制自定義的模板是很簡(jiǎn)單的事情,我們只要做2件事
首先我們需要有一個(gè)自己模板項(xiàng)目
如果需要自定義一些變量,就需要在模板的meta.js當(dāng)中定制
由于下載模塊使用的是download-git-repo模塊,它本身是支持在github,gitlab,bitucket上下載的,到時(shí)候我們只需要將定制好的模板項(xiàng)目放到git遠(yuǎn)程倉(cāng)庫(kù)上即可。
由于我需要定義的是小程序的開發(fā)模板,mpvue本身也有一個(gè)quickstart的模板,那么我們就在它的基礎(chǔ)上進(jìn)行定制,首先我們將它fork下來,新建一個(gè)custom分支,在這個(gè)分支上進(jìn)行定制。
我們需要定制的地方有用到的依賴庫(kù),需要額外用到less以及wxparse 因此我們?cè)?template/package.json當(dāng)中進(jìn)行添加
{?//?...?部分省略?"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" ?} } 復(fù)制代碼
除此之外,我們還需要定制一下eslint規(guī)則,由于只用到standard,因此我們?cè)趍eta.js當(dāng)中 可以將 airbnb風(fēng)格的提問刪除
"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" ?} ?] } 復(fù)制代碼
.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 ?} 復(fù)制代碼
最后我們?cè)跇?gòu)建時(shí)的提問當(dāng)中,再設(shè)置一個(gè)小程序名稱的提問,而這個(gè)名稱會(huì)設(shè)置到導(dǎo)航的標(biāo)題當(dāng)中。 提問是在meta.js當(dāng)中添加
"prompts":?{?"name":?{?"type":?"string",?"required":?true,?"message":?"Project?name" ?},?//?新增提問 ?"appName":?{?"type":?"string",?"required":?true,?"message":?"App?name" ?} } 復(fù)制代碼
main.json
{?"pages":?[?"pages/index/main",?"pages/counter/main",?"pages/logs/main" ?],?"window":?{?"backgroundTextStyle":?"light",?"navigationBarBackgroundColor":?"#fff", ?//?根據(jù)提問設(shè)置標(biāo)題?"navigationBarTitleText":?"{{appName}}",?"navigationBarTextStyle":?"black" ?} } 復(fù)制代碼
最后我們來嘗試一下我們自己的模板
vue?init?Baifann/mpvue-quickstart#custom?min-app-project復(fù)制代碼
總結(jié)
以上模板的定制是十分簡(jiǎn)單的,在實(shí)際項(xiàng)目上肯定更為復(fù)雜,但是按照這個(gè)思路應(yīng)該都是可行的。比如說將一些自行封裝的組件也放置到項(xiàng)目當(dāng)中等等,這里就不再細(xì)說。原理解析都是基于vue-cli 2.0的,但實(shí)際上 3.0也已經(jīng)整裝待發(fā),如果后續(xù)有機(jī)會(huì),深入了解之后,再和大家分享,謝謝大家。