腳手架類的命令行工具用到了哪些技術?
自己做的 NodeJS 項目,想實現像 Express 那樣 $express myapp 就自動生成了項目結構和相關文件,請問都需要用到哪些技術?
或者像 VUE的命令行工具 vue init webpack my-project 那樣的效果也行。
如果能夠簡單科普一下細節就更感謝了。
--
你提到的這兩個,術語叫:腳手架 / 骨架 / boilerplate。
主要的有:
- yeoman.io -- 目前的主流
- amwmedia/plop
- 其他自己實現的
它們的底層,主要是用到了
- SBoudrias/Inquirer.js 處理互動式回答,譬如讓你輸入某個數據,或選擇某個數據。
- 命令處理 + 參數分析,這個的實現就多了:
- tj/commander.js -- 老牌,不過功能有點薄弱,要配合 substack/minimist 等做參數分析。
- yargs -- 這個功能挺強大的,雖然內部代碼寫的很爛。
- 還有我們團隊對 yargs 做的一個上層封裝 node-modules/common-bin ,支持 async 等高級功能。
- chalk/chalk 命令行輸出不同顏色
- 順便提下,還有一些更高級的命令行 dashboard ,如 Yomguithereal/react-blessed
其實初始化只是其中一小步,真正做完善的話,還有很多點,如:
- 實現 sub generator,譬如 egg-init add controller Test
- 修改原有的文件,如添加一個 controller 後,自動往 router.js 裡面加一個註冊啥的。(不是簡單的 append,而是要分析語法)
- 如何對你的工具進行完備的測試?可以用我們寫的 popomore/coffee 和 eggjs/egg-bin 。
- 腳手架的升級機制:目前很多腳手架的實現是一次性的,初始化後就沒有關係了,若是腳手架升級新版本了,之前的項目都需要手動去升級,很麻煩。但其實這塊是可以考慮用 facebook/codemod 等方式去自動化升級。
- 等等
以上。
--
歪個樓。
腳手架最近我們也在實踐,有一些體會。這個東西,說白了就是個可執行文件,簡單點shell就能寫,前端順手的node用fs自己擼一個都行,再扯遠點你看vs,eclipse創建項目還自動生成目錄結構呢。所以呢,
項目啟動器=腳手架,
功能做多了就變成 makefile了
再加個GUI就是IDE了。。。剛好最近在做團隊的CLI工程化相關工作,差不多做了2個月了。這裡將腳手架部分的內容分享出來,核心的技術工具:yeoman、git或者gitlab三方api、CLI和shell交互。整體上需要對node.js裡面的文件操作、子進程及npm 的registry等機制有所了解。
首先,一個團隊裡面有多種業務類型,比如活動相關、Hybrid業務相關、組件相關等,因此需要多個腳手架。腳手架的整體架構如下:
技術點1,初始化目錄結構生成
使用Yeoman提供的API this.fs.copy來進行文件拷貝。如下圖,templates為需要拷貝的目錄結構。對於src目錄裡面的內容,直接採用深度優先演算法進行拷貝。對於templates目錄src同級的其它文件逐個的採用this.fs.copy API進行,因為這些文件很可能需要CLI和用戶進行交互,獲取用戶的輸入參數,比如項目基本信息、描述、離線包id等等。
深度優先拷貝代碼如下:
const fs = require("fs");
const path = require("path");
function read(root, filter, files, prefix) {
prefix = prefix || "";
files = files || [];
filter = filter || noDotFiles;
const dir = path.join(root, prefix);
if (!fs.existsSync(dir)) return files;
if (fs.statSync(dir).isDirectory())
fs.readdirSync(dir)
.filter(filter)
.forEach(function (name) {
read(root, filter, files, path.join(prefix, name));
});
else
files.push(prefix);
return files
}
function noDotFiles(x) {
return x[0] !== ".";
}
module.exports = {
read
};
技術點2,和Github或者Gitlab等平台打通
首先需要申請公共帳號,比如ivwebgit並且拿到它的token。然後將這個帳號加入到某個group下面,並且設置為master許可權。這樣,這個帳號就可以創建倉庫、克隆、自動提交等等。 然後,你需要寫一個Git三方api的一個proxy模塊,方便你去調用相關的API。類似下面的:
_Proxy(url, method, params) {
const self = this;
// 調用request(options, callback)這個API
return new Promise(function (resolve, reject) {
const options = {
url: API_PREFIX + url,
method: method,
headers: {
"PRIVATE-TOKEN": self.privateToken
},
form: params
};
rp(options)
.then((response) =&> {
// 創建成功
resolve({
success: true,
data: response,
msg: "API invoke success!"
})
})
.catch((err) =&> {
// 創建失敗
resolve({
success: false,
msg: err err.error
});
});
});
}
這個proxy模塊對外暴露一個個的原子操作,比如創建倉庫,獲取某個用戶信息等等。
技術點3,CLI和shell之間的交互和調用
- 命令交互相關:可選的有Node.js自帶的child_process模塊,裡面有spawn和exec方法,或者使用shelljs ,用起來很方便。
- 命令參數解析:採用minimist
- 問詢用戶輸入:採用inquirer
- 其它美化操作:採用chalk
inquirer.prompt([
{
type: "input",
name: "username",
message: "請輸入Git code用戶名 (目前Git不支持綁定私鑰):"
}, {
type: "password",
name: "password",
message: "請輸入Git code密碼:"
}, {
type: "password",
name: "confirmPassword",
message: "請再次輸入確認Git code密碼:"
}
]).then(function (answers) {
const {username, password, confirmPassword} = answers;
};
技術點4,generator放在npm上更新機制
如果你對npm update了解的話,應該知道npm有個registry機制。CLI如何判斷一個generator是否有版本更新,就是通過請求這個regsitry ul來獲取latest版本,看看和用戶當前安裝的本地版本是否一致。這裡的generator不要做為依賴寫到上層CLI裡面。CLI需要做一個插件機制,提供類似 install命令和update 命令來更新 本地的generator。這裡面核心的需要做一個模塊註冊與發現機制。
$ cli install generator-xxx # CLI安裝generator
運行這個命令,cli會安裝generator到用戶目錄下的某個文件夾下面,並且將安裝的generator的名稱、描述等信息寫入到該文件夾下的某個文件裡面進行註冊,eg: generator.json。
$ cli init
運行cli init命令會從generator.json文件裡面讀取本地可用的generator,這個是模塊發現功能。之後會檢查是否需要更新,最後幫助開發者初始化。
最後總結下: 運行這個 init命令後,會首先從本地來發現可用的generator模塊,並且檢查和提示開發者是否需要更新。之後會提示開發者輸入項目的基本結構化信息,然後在遠程Git平台的某個group幫助用戶創建倉庫,克隆到本地,最後初始化目錄結構,並且自動提交初始化的內容到遠程。
------------ 順便裝個B ----------
https://github.com/egoist/sao
https://github.com/egoist/cac
https://github.com/egoist/majo
https://github.com/egoist/kopy
在做業務邏輯的時候發現有很多重複的代碼,就寫了個自動生成工具。過程中參考了各位大神的做法,前來還願。
從百度跳槽到了大疆,又開始用起了vue。下面是一個從0開始開發CLI的過程,小白來的,大神輕拍。實現的基本功能是根據模板文件,生成或者刪除模塊。
代碼結構
- tools (root)
- bin
- index.js 入口文件
- lib 用來被複制的模板
- helper 復用比較多的API文件
- store 復用比較多的store文件
- test 復用比較多的mock文件
- node_modules 依賴
- util
- index.js 用來讀取文件、操作文件
- package.json 基本配置
- readme.md 說明
編碼過程
安裝依賴
初始化程序並安裝依賴
npm init
npm install --save commander colors
// commander封裝了命令行參數的介面,colors可以修改命令行的顏色
配置package.json,添加全局命令
"bin": {
"tools": "./bin/index.js"
// tools會直接執行./bin/index.js
}
這樣運行npm install -g之後就可以直接在控制台使用tools命令了。最後我的package.json文件是這樣的。
{
"name": "tools",
"version": "1.0.0",
"description": "a tools to develop aps",
"main": "index.js",
"bin": {
"tools": "./bin/index.js"
},
"scripts": {
"test": "echo "Error: no test specified" exit 1"
},
"keywords": [
"tools",
"aps"
],
"author": "geovanni.zhang",
"license": "ISC",
"dependencies": {
"colors": "^1.1.2",
"commander": "^2.11.0"
}
}
編寫命令行交互邏輯
引入commander和colors庫
#!/usr/bin/env node
let program = require("commander")
let colors = require("colors")
讓commander能夠根據用戶輸入參數選擇執行不同代碼
program.version("0.1.0") // 配置版本
program
.command("store [name]")
// 響應tools store命令
.alias("s")
// 響應tools s,這個命令和tools store等價
.description("generate or remove store")
// 在tools --help會顯示介紹
.option("-r, --remove [remove]", "which store to remove")
// 響應tools store -r
// 響應tools store -r xxx
// 響應tools store --remove
// 響應tools store --remove xxx
.option("-g, --generate [generate]", "which store to generate")
// 響應tools store -g
// 響應tools store -g xxx
// 響應tools store --generate
// 響應tools store --generate xxx
.action(function (name, options) {
if (options.remove) {
// 響應tools store -r [options.remove==true]
// 響應tools store -r xxx [options.remove==xxx]
console.log(options.remove)
}
if (options.generate) {
console.log(options.generate)
}
})
program.parse(process.argv) //初始化執行
根據上面的代碼,複製粘貼。你就可以根據用戶輸入執行不同邏輯了,是不是很方便。
文件操作
引入fs用於文件操作,引入path用於生成路徑
let fs = require("fs")
let path = require("path")
我們先實現一個拷貝文件的功能,如果路徑不存在需要先創建路徑(文件夾)
let createFolder = function (to) {
let sep = path.sep
let folders = path.dirname(to).split(sep)
let p = ""
while (folders.length) {
p += folders.shift() + sep
if (!fs.existsSync(p)) {
fs.mkdirSync(p)
}
}
}
接著查找源文件夾的所有文件,讀取並輸出到目標文件夾
module.exports.copyDirClear = (from, to, name) =&> {
let fromPath = path.join(__dirname, from)
let toPath = path.join(__dirname, to)
let files = fs.readdirSync(fromPath)
files.forEach((v, i, arr) =&> {
let filePath = path.join(fromPath, v)
let data = fs.readFileSync(filePath, "utf-8")
createFolder(path.join(toPath, v))
fs.writeFileSync(path.join(toPath, v), data)
})
}
有些文件在複製時可能要重命名
module.exports.copyRenameFile = (from, to, name) =&> {
let fromPath = path.join(__dirname, from)
let toPath = path.join(__dirname, to)
let data = fs.readFileSync(fromPath, "utf-8")
createFolder(path.join(toPath, name))
fs.writeFileSync(path.join(toPath, name), data)
}
還有些文件可能在複製的時候需要自定義內容,這裡我在源文件中寫入了{{name}},然後在node程序中用正則找出並替換
module.exports.copyTemplate = (from, to, name) =&> {
let fromPath = path.join(__dirname, from)
let toPath = path.join(__dirname, to)
let files = fs.readdirSync(fromPath)
files.forEach((v, i, arr) =&> {
let filePath = path.join(fromPath, v)
let data = fs.readFileSync(filePath, "utf-8")
let res = data.toString().replace(/{{name}}/g, name)
createFolder(path.join(toPath, v))
fs.writeFileSync(path.join(toPath, v), res)
})
}
然後又寫了一個既可以改名字,又可以根據模板生成內容的函數
module.exports.copyRenameTemplate = (from, to, rename, name) =&> {
let fromPath = path.join(__dirname, from)
let toPath = path.join(__dirname, to)
let filepath = path.join(toPath, rename)
let data = fs.readFileSync(fromPath, "utf-8")
let res = data.toString().replace(/{{name}}/g, name)
createFolder(filepath)
fs.writeFileSync(filepath, res)
}
刪除文件的函數
module.exports.deleteFile = (url) =&> {
let filepath = path.join(__dirname, url)
let check = fs.existsSync(filepath)
if (check) {
fs.unlinkSync(filepath)
}
}
刪除文件夾的函數,這裡用到了遞歸
let deleteDir = (dir) =&> {
let url = path.join(__dirname, dir)
let files = fs.readdirSync(url)
files.forEach((v, i, arr) =&> {
let filepath = path.join(url, v)
let stats = fs.statSync(filepath)
if (stats.isDirectory()) {
deleteDir(filepath)
} else {
fs.unlinkSync(filepath)
}
})
fs.rmdirSync(url)
}
module.exports.deleteDir = deleteDir
當然還有在文件中新增行的函數,這裡我用正則進行替換的,稍微有點難受
module.exports.changeMockIndex = (url, name) =&> {
let filepath = path.join(__dirname, url)
let data = fs.readFileSync(filepath, "utf-8")
// camel name by user
let res = data.toString().replace(/import/, `import ${name} from "./api/${name}"
import`)
res = res.toString().replace(/export {/, `export {
${name},`)
fs.writeFileSync(filepath, res)
}
在文件中移除行的函數,同樣是正則
module.exports.removeMockIndex = (url, name) =&> {
let filepath = path.join(__dirname, url)
let data = fs.readFileSync(filepath, "utf-8")
// camel name by user
let importReg = new RegExp(`import ${name} from "./api/${name}"
`)
let res = data.toString().replace(importReg, "")
let exportRex = new RegExp(`${name},
`)
res = res.toString().replace(exportRex, "")
fs.writeFileSync(filepath, res)
}
最後把這些函數全部註冊在主文件里,對應用戶的操作使用就可以了。
結束
其實前端自動化的工具已經很多了,不過在某種業務場景還是需要自己寫一些自動化工具的。也稍微明白了面試的時候面試官問文件操作的良苦用心(要不再面一次?)。最後還聽了Tutor的設想,可以把這些全部放在electron里,做成一個類似scrat或者vue-cli的超全腳手架。
類似vue-cli 使用的 vue init webpack my-project 這種命令, 使用的是一個叫 commander 的包實現的。並且使用的是 commander 提供的一個 API: command()。
但是僅僅使用 commander 還無法做到在全局都能使用你所指定的命令,需要配置項目的 package.json 中的 bin 欄位. 然後發布包,再全局安裝這個包。
正常情況下你會在 /usr/local/bin 目錄裡面看見你在 bin 中定義的命令。
關於生成腳手架: 一般不是通過fs 模塊創建, 而是你把你的腳手架放到你的 github 倉庫裡面, 然後通過 download-git-repo 包下載到本地。
關於另外一種互動式工具, 類似 npm init 時候出現的那個玩意, 使用的是一個叫 inquirer 的包. 官方的 github 上面有例子, 看一遍大概就知道是幹嘛的。
如果題主問的只是生成目錄結構,那麼用node的fs就能搞定。
如果是想包括生成目錄以及基於這個目錄提供編譯輸出自動化功能,那麼我下面介紹的可能對題主有點用。
下面主要講的是京東前端腳手架工具重構版jdfx開發用到的node包。
https://github.com/jdf2e/jdf/blob/master/package.json
從項目創建到編譯到輸出一整套流程都有,用到的包還是挺多的。
我挑選了一些比較關鍵的包來說明一下。
// js解析器,引入目的是為了更精確的對js內容做控制,以及修改。比如加cdn前綴,編譯路徑替換等
"acorn": "^4.0.4",
// 編譯es6
"babel-core": "^6.18.0",
"babel-preset-es2015": "^6.24.0",
// 靜態伺服器,提供瀏覽器無F5自動刷新
"browser-sync": "^2.17.6",
"bs-html-injector": "^3.0.3",
// node端jQuery,也是為了更精確對html內容進行控制修改。
"cheerio": "^0.22.0",
// 命令行工具,比較方便的定製一些命令,比如jdf init projectname創建一個項目目錄
"commander": "^2.9.0",
// 將字元串轉成正則,這個比較實用,不用自己寫new RegExp
"escape-string-regexp": "^1.0.5",
// js解析對象重新生成js文件
"escodegen": "^1.8.1",
// node fs的封裝版,功能強點
"fs-extra": "^1.0.0",
// 利用glob統配符來匹配文件,這個glob包在windows下存在bug,同樣的匹配模式,在非同步和同步兩種情況下,它的參數似乎不是那麼符合預期。
"glob": "^7.1.1",
// 雪碧圖、壓縮、日誌、工具、對比、上傳等功能包。放這裡是因為存在抽離任務獨立出來這個過程。
"jdf-css-sprite": "^1.1.2",
"jdf-img-minify": "0.1.0",
"jdf-log": "^0.0.4",
"jdf-upload": "^0.1.4",
"jdf-utils": "^1.0.8",
"jdiff": "0.0.2",
// js工具庫,函數操作神器,用的好的話既能節省大量時間又能保證演算法的安全。
"lodash": "^4.17.2",
// glob通配符匹配工具, node-glob就是依賴的它。
"minimatch": "^3.0.3",
// 代碼更新監聽,
"node-watch": "^0.3.4",
// postcss生態必裝,css解析器
"postcss": "^5.2.5",
// 在js代碼中調用同功能的shell命令
"shelljs": "^0.7.5",
// json文件允許寫注釋,用這個庫處理文件後再交由JSON庫處理
"strip-json-comments": "^2.0.1",
// 混淆壓縮
"uglify-js": "2.4.12",
"uuid": "^3.0.0",
// vm模板解析引擎
"velocityjs": "0.4.3"
個人感覺做腳手架以及自動化構建,它的目的是將許多任務組織起來,大部分任務處理本身業界有需要良好的方案可供選擇,我們只要稍加封裝就好。
難點在於任務流的把控以及個別任務處理,比如文件讀取,讀取的過濾,編譯任務順序,必須順序執行任務順序執行,可並行的任務並行執行,修改項目內容保存後的快速編譯,輸出文件的路徑替換和自動CDN前綴。要把這些任務都做的清晰,可靠,還是需要花費不少功夫的。
最重要的是,必須提供足夠的測試用例,這類基礎工具沒有測試用例去覆蓋,很難持續開發,也很難保證可靠性。
最近擼過一個,其實還蠻簡單的,新手的話可以用commander.js來解析輸入,對fs熟悉一點就可以了,剩下的具體看需求,一點點豐富功能~然後yeoman也有構建CLI的CLI
看懂這些腳手架的源代碼,接著自己仿寫個簡單版,然後一點一點豐富~
自己寫了一個簡單的功能腳手架,tool-kit-cli,做一些項目初始化的模板,半天做的,後面還沒想好做啥
最後可以放在安卓機上跑,利用node搭個web中間價,路由,放在手機上運行謝謝樓上各位的回答,很感動。這方面我是小白一枚,不知道從哪裡入門,結果一下子收穫這麼多。
曾經由於項目太多導致不記得package.json里的眾多npm指令,寫了一個簡單的命令行工具npm-scripts-catalog,地址:stop2stare/catalog。
界面大概如下:
如你所見,就是一個簡單的命令行交互。將package.json里的所有指令列出來,上下移游標,選擇你要執行的命令,然後執行。quite simple but useful。
不過當然,這個小工具沒辦法跟express-generator這樣的大型命令行工具相比,提供命令行交互的插件就是Inquirer.js,地址:https://github.com/SBoudrias/Inquirer.js 。
專門去看了一下expressjs/generator 的源碼,主要的是./bin/express-cli.js這個文件,而拋開其他的參數配置等等代碼,主要的就是下面的createApplication函數。
function createApplication (name, path) {
var wait = 5
console.log()
function complete () {
if (--wait) return
var prompt = launchedFromCmd() ? "&>" : "$"
// 列印一些日誌
}
// JavaScript
var app = loadTemplate("js/app.js")
var www = loadTemplate("js/www")
// App name
www.locals.name = name
// App modules
app.locals.modules = Object.create(null)
app.locals.uses = []
mkdir(path, function () {
mkdir(path + "/public", function () {
mkdir(path + "/public/javascripts")
mkdir(path + "/public/images")
mkdir(path + "/public/stylesheets", function () {
switch (program.css) {
case "less":
copyTemplate("css/style.less", path + "/public/stylesheets/style.less")
break
case "stylus":
copyTemplate("css/style.styl", path + "/public/stylesheets/style.styl")
break
case "compass":
copyTemplate("css/style.scss", path + "/public/stylesheets/style.scss")
break
case "sass":
copyTemplate("css/style.sass", path + "/public/stylesheets/style.sass")
break
default:
copyTemplate("css/style.css", path + "/public/stylesheets/style.css")
break
}
complete()
})
})
mkdir(path + "/routes", function () {
copyTemplate("js/routes/index.js", path + "/routes/index.js")
copyTemplate("js/routes/users.js", path + "/routes/users.js")
complete()
})
mkdir(path + "/views", function () {
switch (program.view) {
case "dust":
copyTemplate("dust/index.dust", path + "/views/index.dust")
copyTemplate("dust/error.dust", path + "/views/error.dust")
break
case "ejs":
copyTemplate("ejs/index.ejs", path + "/views/index.ejs")
copyTemplate("ejs/error.ejs", path + "/views/error.ejs")
break
case "jade":
copyTemplate("jade/index.jade", path + "/views/index.jade")
copyTemplate("jade/layout.jade", path + "/views/layout.jade")
copyTemplate("jade/error.jade", path + "/views/error.jade")
break
case "hjs":
copyTemplate("hogan/index.hjs", path + "/views/index.hjs")
copyTemplate("hogan/error.hjs", path + "/views/error.hjs")
break
case "hbs":
copyTemplate("hbs/index.hbs", path + "/views/index.hbs")
copyTemplate("hbs/layout.hbs", path + "/views/layout.hbs")
copyTemplate("hbs/error.hbs", path + "/views/error.hbs")
break
case "pug":
copyTemplate("pug/index.pug", path + "/views/index.pug")
copyTemplate("pug/layout.pug", path + "/views/layout.pug")
copyTemplate("pug/error.pug", path + "/views/error.pug")
break
case "twig":
copyTemplate("twig/index.twig", path + "/views/index.twig")
copyTemplate("twig/layout.twig", path + "/views/layout.twig")
copyTemplate("twig/error.twig", path + "/views/error.twig")
break
case "vash":
copyTemplate("vash/index.vash", path + "/views/index.vash")
copyTemplate("vash/layout.vash", path + "/views/layout.vash")
copyTemplate("vash/error.vash", path + "/views/error.vash")
break
}
complete()
})
// CSS Engine support
switch (program.css) {
case "less":
app.locals.modules.lessMiddleware = "less-middleware"
app.locals.uses.push("lessMiddleware(path.join(__dirname, "public"))")
break
case "stylus":
app.locals.modules.stylus = "stylus"
app.locals.uses.push("stylus.middleware(path.join(__dirname, "public"))")
break
case "compass":
app.locals.modules.compass = "node-compass"
app.locals.uses.push("compass({ mode: "expanded" })")
break
case "sass":
app.locals.modules.sassMiddleware = "node-sass-middleware"
app.locals.uses.push("sassMiddleware({
src: path.join(__dirname, "public"),
dest: path.join(__dirname, "public"),
indentedSyntax: true, // true = .sass and false = .scss
sourceMap: true
})")
break
}
// Template support
switch (program.view) {
case "dust":
app.locals.modules.adaro = "adaro"
app.locals.view = {
engine: "dust",
render: "adaro.dust()"
}
break
default:
app.locals.view = {
engine: program.view
}
break
}
// package.json
var pkg = {
name: name,
version: "0.0.0",
private: true,
scripts: {
start: "node ./bin/www"
},
dependencies: {
"body-parser": "~1.17.1",
"cookie-parser": "~1.4.3",
"debug": "~2.6.3",
"express": "~4.15.2",
"morgan": "~1.8.1",
"serve-favicon": "~2.4.2"
}
}
switch (program.view) {
case "dust":
pkg.dependencies.adaro = "~1.0.4"
break
case "jade":
pkg.dependencies["jade"] = "~1.11.0"
break
case "ejs":
pkg.dependencies["ejs"] = "~2.5.6"
break
case "hjs":
pkg.dependencies["hjs"] = "~0.0.6"
break
case "hbs":
pkg.dependencies["hbs"] = "~4.0.1"
break
case "pug":
pkg.dependencies["pug"] = "~2.0.0-beta11"
break
case "twig":
pkg.dependencies["twig"] = "~0.10.3"
break
case "vash":
pkg.dependencies["vash"] = "~0.12.2"
break
}
// CSS Engine support
switch (program.css) {
case "less":
pkg.dependencies["less-middleware"] = "~2.2.0"
break
case "compass":
pkg.dependencies["node-compass"] = "0.2.3"
break
case "stylus":
pkg.dependencies["stylus"] = "0.54.5"
break
case "sass":
pkg.dependencies["node-sass-middleware"] = "0.9.8"
break
}
// sort dependencies like npm(1)
pkg.dependencies = sortedObject(pkg.dependencies)
// write files
write(path + "/package.json", JSON.stringify(pkg, null, 2) + "
")
write(path + "/app.js", app.render())
mkdir(path + "/bin", function () {
write(path + "/bin/www", www.render(), MODE_0755)
complete()
})
if (program.git) {
copyTemplate("js/gitignore", path + "/.gitignore")
}
complete()
})
}
如上所見,這這基本上就是根據你的輸入配置來一項一項地轉移文件了。而其中所有的文件都放在./templates/目錄下面。
所以目前來看,基本上就是將模板文件寫好,然後根據配置項一個一個轉移到約定位置。
推薦閱讀:
※vue.js 能否設置某個組件不被keep-alive?
※vue.js 有哪些知名公司或項目用於實際生產環境了呢?
※Knockout, Vue 和 AvalonJS 等 MVVM 框架實現中是否用到 eval 或 Function?
※Vue.js如何優雅的進行form validation?
※Vue 中如何使用 MutationObserver 做批量處理?