手把手教你實現腳手架工具Koa-generator

私信我或關注微信號:猿來如此呀,回覆:學習,獲取免費學習資源包。

我們日常中經常使用各種cli來加速我們的工作,你們也一定和我一樣想知道這些cli內部都幹了什麼?接下來我們就以實現一個koa-generator來打開腳手架工具的大門,來跟著我一步一步做吧

為了加快我們的學習進度,更快的理解cli,我們這裡會省略一些內容,旨在幫助大家更快建立基本的概念和入門方法

需求分析

首先我們先對我們要實現的工具做一個簡單的需求分析:

  1. 自動化生成koa初始項目結構
  2. 可以自定義一些內容
  3. 發佈

是不是很簡單?沒錯,真的很簡單!

逐步實現

1

想要自動化生成koa初始項目結構的前提,就是要知道我們構建出來的結構是什麼樣的:

手把手教你實現腳手架工具Koa-generator


上圖就是我們想要生成的項目結構

明確了我們的目的接下來就開始著手吧!

2

2.1

創建文件夾

mkdir koa
-
simple
-
generator


2.2

進入項目目錄

cd koa
-
simple
-
generator


2.3

初始化npm(等不及實踐就一路enter,後面也可以再做修改)

npm init


2.4

打開我們的package.json,如下

手把手教你實現腳手架工具Koa-generator


將下面的代碼複製到package.json裡

{

"name"
:

"koa-simple-generator"
,

"version"
:

"1.0.0"
,

"description"
:

""
,

"scripts"
:

{

"test"
:

"echo "Error: no test specified" && exit 1"

},

"author"
:

""
,

"license"
:

"ISC"

,

"main"
:

"bin/wowKoa"
,

"bin"
:

{

"koa2"
:

"./bin/wowKoa"

},

"dependencies"
:

{

"commander"
:

"2.7.1"
,

"mkdirp"
:

"0.5.1"
,

"sorted-object"
:

"1.0.0"

},

"devDependencies"
:

{

"mocha"

:

"2.2.5"
,

"rimraf"
:

"~2.2.8"
,

"supertest"
:

"1.0.1"

},

"engines"
:

{

"node"
:

">= 7.0"

}
}
1.
dependencies

devDependencies
簡單來說就是應用的依賴包,
devDependencies
只會在開發環境安裝
2.

這句話的意思是我們的這個工具需要
node7
.
0
及以上的版本才能支持
"engines"
:


{

"node"
:

">= 7.0"

}
重點是這兩句
"main"
:

"bin/wowKoa"
,

"bin"
:

{

"wowKoa"
:

"./bin/wowKoa"

},

意思是默認執行的是
bin
目錄下的
wowKoa


執行
wowKoa
的命令,執行的也是
bin
目錄下的
wowKoa

2.5

接下來安裝我們的依賴吧

npm i


2.6

安裝完,我們新建一個目錄template

mkdir 
template

然後我們可以把我們想要生成的目錄結構拷貝進去,這裡我就只是把koa2的目錄拷貝進去,現在我們的目錄長這樣:

手把手教你實現腳手架工具Koa-generator


2.7

新建bin目錄,在bin下新建文件wowKoa

手把手教你實現腳手架工具Koa-generator


2.8

接下來就是關鍵了,我們的所有工作都是在bin下的wowKoa文件裡完成的 直接複製粘貼下面的,然後進入項目目錄運行 node bin/wowKoa就能看到結果了

代碼我已經大部分都註釋啦

#!/usr/bin/env node

// 告訴Unix和Linux系統這個文件中的代碼用node可執行程序去運行
var
program
=

require
(
'commander'
);
var
mkdirp
=

require
(
'mkdirp'
);
var
os
=

require
(
'os'
);
var
fs
=

require
(
'fs'
);
var
fsm
=

require
(
'fs-extra'
)
var

path
=

require
(
'path'
);
var
readline
=

require
(
'readline'
);
var
pkg
=

require
(
'../package.json'
);
// 退出node進程
var
_exit
=
process
.
exit
;
// s.EOL屬性是一個常量,返回當前操作系統的換行符(Windows系統是\r\n,其他系統是\n)
var
eol
=
os
.
EOL
;
var
version
=
pkg
.
version
;
// Re-assign process.exit because of commander

// TODO: Switch to a different command framework
process
.
exit

=

exit
program

/**
* .version('0.0.1', '-v, --version')
* 1版本號,
* 2自定義標誌:默認為 -V 和 --version
*
* .option('-n, --name<path>', 'name description', 'default name')
* 1 自定義標誌:分為長短標識,中間用逗號、豎線或者空格分割;標誌後面可跟必須參數或可選參數,前者用 <> 包含,後者用 [] 包含
* 2 選項描述:在使用 --help 命令時顯示標誌描述
* 3 默認值
*
* .usage('[options] [dir]')
* 作用:只是打印用法說明
*
* .parse(process.argv)
* 作用:用於解析process.argv,設置options以及觸發commands
* process.argv獲取命令行參數
*
*
* Commander提供了api來取消未定義的option自動報錯機制, .allowUnknownOption()
*/

.
version
(
version
,

'-v, --version'
)

.
allowUnknownOption
()

.
usage
(
'[options] [dir]'
)

.
option
(
'-f, --force'
,

'force on non-empty directory'
)

.
parse
(
process
.
argv
);
// 沒有退出時執行主函數
if

(!
exit
.
exited
)

{
main
();
}
/**
* 主函數
*/
function
main
()

{

// 獲取當前命令執行路徑

var
destinationPath
=
program
.
args
.
shift
()

||

'.'
;

// 根據文件夾名稱定義appname

// 用於package.json裡的name

var
appName
=
path
.
basename
(
path
.
resolve
(
destinationPath
));

// 判斷當前文件目錄是否為空
emptyDirectory
(
destinationPath
,

function

(
empty

)

{

// 如果為空或者強制執行時,就直接生成項目

if

(
empty
||
program
.
force
)

{
createApplication
(
appName
,
destinationPath
);

}

else

{

// 否則詢問
confirm
(
'當前文件夾不為空,是否繼續?[y/N] '
,

function

(
ok
)

{

if

(

ok
)

{

// 控制檯不再輸入時銷燬
process
.
stdin
.
destroy
();
createApplication
(
appName
,
destinationPath
);

}

else

{
console
.
error
(
'aborting'
);

exit
(
1
);

}

});

}

})
}
/**
* Check if the given directory `path` is empty.
* 判斷文件夾是否為空
* @param {String} path
* @param {Function} fn

*/
function
emptyDirectory
(
path
,
fn
)

{
fs
.
readdir
(
path
,

function

(
err
,
files
)

{

if

(
err
&&

'ENOENT'

!=
err
.
code
)

throw
err
;
fn
(!
files
||

!

files
.
length
);

});
}
/**
* 在給定路徑中創建應用
* @param {String} path
*/
function
createApplication
(
app_name
,
path
)

{

// wait的值等於complete函數執行的次數

// 用於選擇在哪一次complete函數執行後執行控制檯打印引導使用的文案

var
wait
=

1
;
console
.
log
();

function
complete
()

{

if

(--
wait

)

return
;

var
prompt
=
launchedFromCmd
()

?

'>'

:

'$'
;
console
.
log
();
console
.
log
(
' install dependencies:'
);
console
.
log
(
' %s cd %s && npm install'
,
prompt
,
path
);
console
.
log
();
console
.
log
(
' run the app:'
);

// 根據控制檯的環境不同打印不同文案(linux或者win)

if

(
launchedFromCmd
())

{
console
.
log
(
' %s SET DEBUG=koa* & npm start'
,
prompt
,
app_name
);

}

else

{
console
.
log
(
' %s DEBUG=%s:* npm start'
,
prompt
,
app_name
);

}

}
copytmp
(
complete
,
path
,
app_name
)
}

// 拷貝模擬裡的文件到本地
function
copytmp
(
fn
,
destinationPath
,
app_name
)

{

// 獲取模板文件的文件目錄
tmpPath
=
path
.
join
(
__dirname
,

'..'
,

'template'
)

// 創建目錄
fsm
.
ensureDir
(
destinationPath
+

'/'
+
app_name
)

.
then
(()

=>

{

// 拷貝模板
fsm
.
copy
(
tmpPath
,
destinationPath
+

'/'
+
app_name
,
err
=>

{

if

(
err
)

return
console
.
log
(
err
)
fn
()

})

})
}
/**
* Determine if launched from cmd.exe
* 判斷控制檯環境(liux或者win獲取其他)
*/
function
launchedFromCmd
()

{

return
process
.
platform
===

'win32'

&&
process
.
env
.
_
===

undefined
;
}
/**
* node是使用process.stdin和process.stdout來實現標準輸入和輸出的
* readline 模塊提供了一個接口,用於一次一行地讀取可讀流(例如 process.stdin)中的數據。 它可以使用以下方式訪問:
*/
var
rl
=
readline
.
createInterface
({
input
:
process
.
stdin
,
output
:
process
.
stdout
});
// 控制檯問答

function
confirm
(
msg
,
callback
)

{
rl
.
question
(
msg
,

function

(
input
)

{
callback
(
/^y|yes|ok|true$/
i
.
test
(
input
));

});
}
// 控制檯問答
function
wrieQuestion
(
msg
,
callback
)

{
rl
.
question
(
msg

,

function

(
input
)

{

// rl.close()後就不再監聽控制檯輸入了
rl
.
close
();
callback
(
input
)

});
}
/**
* 通過fs讀取模板文件內容
*/
function
loadTemplate
(
name
)

{

return
fs
.
readFileSync
(
path
.
join
(
__dirname
,

'..'
,

'template'
,
name
),

'utf-8'
);
}
/**
* echo str > path.
* 寫入文件
* @param {String} path
* @param {String} str
*/
function
write
(
path
,
str
,
mode
)

{
fs
.
writeFileSync
(
path
,
str
,

{
mode
:
mode
||

0666

});
console
.
log
(
' \\x1b[36mcreate\\x1b[0m : '

+

path
);

}
/**
* 這裡是主要解決在winodws上的一些bug,不用卡在這裡,核心目的就是為了能讓進程優雅退出
* Graceful exit for async STDIO
*/
function

exit
(
code
)

{

// flush output for Node.js Windows pipe bug

// https://github.com/joyent/node/issues/6247 is just one bug example

// https://github.com/visionmedia/mocha/issues/333 has a good discussion

function

done
()

{

if

(!(
draining
--))
_exit
(
code
);

}

var
draining
=

0

;

var
streams
=

[
process
.
stdout
,
process
.
stderr
];

exit
.
exited
=

true
;
streams
.
forEach
(
function

(
stream
)

{

// submit empty write request and wait for completion
draining
+=

1
;
stream
.
write
(
''
,

done
);


});

done
();
}
/<path>


分享到:


相關文章: