讀完這篇文章你可能會學到哪些知識?html
①node實現終端命令行
②終端命令行交互
③深copy整個文件夾
④nodejs執行終端命令 如 npm install
⑤創建子進程通訊
⑥webpack底層操做,啓動webpack
,合併配置項
⑦編寫一個plugin,理解各階段
⑧require.context實現前端自動化前端
mycli creat
建立項目mycli start
運行項目mycli build
打包項目咱們在這邊文章裏面用的是mycli
,可是我並無上傳項目到npm
,可是這篇文章的技術是筆者以前的一個腳手架原型,感興趣的同窗本地下載能夠體驗效果。vue
全局下載腳手架rux-cli
node
windowsreact
npm install rux-cli -g
複製代碼
macwebpack
sodu npm install rux-cli -g
複製代碼
一條命令建立項目,安裝依賴,編譯項目,運行項目。git
rux create
複製代碼
咱們但願用一條命令行,實現項目建立,依賴下載,項目運行,依賴收集等衆多流程。若是一口氣設計整個功能,可能會感到腦殼一片空白,因此咱們要學會分解目標。實際縱覽整個流程,主要分爲 建立文件階段 , 構建,集成webpack階段 , 運行項目階段 。梳理每一個階段咱們須要作的事情。github
咱們指望像vue-cli
那樣 ,經過自定義的命令行vue create
,開始建立一個項目,首先可以讓程序終端識別咱們的自定義指令,咱們首先須要修改bin
。web
例子:正則表達式
mycli create
複製代碼
咱們但願的終端可以識別mycli
,而後經過 mycli create
建立一個項目。實際上流程大體是這樣的經過mycli
能夠指向性執行指定的node
文件。接下來咱們一塊兒分析一下具體步驟。
執行終端命令號,指望結果是執行當前的node
文件。
創建工程
如上圖所示咱們在終端執行命令行的時候,統一走bin
文件夾下面的 mycli.js
文件。
mycli.js文件
#!/usr/bin/env node
'use strict';
console.log('hello,world')
複製代碼
而後在package.json
中聲明一下bin
。
{
"name": "my-cli",
"version": "0.0.1",
"description": "",
"main": "index.js",
"bin": {
"mycli": "./bin/mycli.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "👽",
"license": "ISC",
"dependencies": {
"chalk": "^4.0.0",
"commander": "^5.1.0",
"inquirer": "^7.1.0",
"which": "^2.0.2"
}
}
複製代碼
萬事俱備,爲了在本地調試,my-cli
文件夾下用npm link
,若是在mac
上須要執行 sudo npm link
而後咱們隨便新建一個文件夾,執行一下 mycli
。看到成功打印hello,world
,第一步算是成功了。接下來咱們作的是讓node
文件(demo
項目中的mycli.js
)可以讀懂咱們的終端命令。好比說 mycli create
建立項目; mycli start
運行項目; mycli build
打包項目; 爲了可以在終端流利的操縱命令行 ,咱們引入 commander
模塊。
爲了能在終端打印出花裏胡哨的顏色,咱們引入chalk
庫。
const chalk = require('chalk')
const colors = [ 'green' , 'blue' , 'yellow' ,'red' ]
const consoleColors = {}
/* console color */
colors.forEach(color=>{
consoleColors[color] = function(text,isConsole=true){
return isConsole ? console.log( chalk[color](text) ) : chalk[color](text)
}
})
module.exports = consoleColors
複製代碼
接下來須要咱們用 commander
來聲明的咱們終端命令。
Commander.js node.js
命令行界面的完整解決方案,受 Ruby Commander
啓發。 前端開發node cli
必備技能。
version
版本var program = require('commander');
program
.version('0.0.1')
.parse(process.argv);
#執行結果:
node index.js -V
0.0.1
複製代碼
option
選項使用.option()
方法定義commander
的選項options
,示例:.option('-n, --name [items2]', 'name description', 'default value')。
program
.option('-d, --debug', 'output extra debugging')
.option('-s, --small', 'small pizza size')
program.parse(process.argv)
if( program.debug ){
blue('option is debug')
}else if(program.small){
blue('option is small')
}
複製代碼
終端輸入
mycli -d
複製代碼
終端輸出
commander
自定義指令(重點)做用:添加命令名稱, 示例:.command('add <num>
1 命令名稱<必須>:命令後面可跟用 <> 或 [] 包含的參數;命令的最後一個參數能夠是可變的,像實例中那樣在數組後面加入 ... 標誌;在命令後面傳入的參數會被傳入到 action
的回調函數以及 program.args
數組中。
2 命令描述<可省略>:若是存在,且沒有顯示調用 action(fn)
,就會啓動子命令程序,不然會報錯 配置選項<可省略>:可配置noHelp、isDefault
等。
由於咱們作的是腳手架,最基本的功能,建立項目,運行項目(開發環境),打包項目(生產環境),因此咱們添加三個命令,代碼以下:
/* mycli create 建立項目 */
program
.command('create')
.description('create a project ')
.action(function(){
green('👽 👽 👽 '+'歡迎使用mycli,輕鬆構建react ts項目~🎉🎉🎉')
})
/* mycli start 運行項目 */
program
.command('start')
.description('start a project')
.action(function(){
green('--------運行項目-------')
})
/* mycli build 打包項目 */
program
.command('build')
.description('build a project')
.action(function(){
green('--------構建項目-------')
})
program.parse(process.argv)
複製代碼
效果
mycli create
複製代碼
第一步算是完成了。
咱們指望像vue-cli
或者dva-cli
再或者是taro-cli
同樣,實現和終端的交互功能。這就須要另一個 nodejs
模塊 inquirer
。Inquirer.js
提供用戶界面和查詢會話。
上手:
var inquirer = require('inquirer');
inquirer
.prompt([
/* 把你的問題傳過來 */
])
.then(answers => {
/* 反饋用戶內容 */
})
.catch(error => {
/* 出現錯誤 */
});
複製代碼
因爲咱們作的是react
腳手架,因此咱們和用戶交互問題設定爲,是否建立新的項目?(是/否) -> 請輸入項目名稱?(文本輸入) -> 請輸入做者?(文本輸入) -> 請選擇公共管理狀態?(單選) mobx
或 redux
。上述prompt
第一個參數須要對這些問題作基礎配置。咱們的 question
配置大體是這樣
const question = [
{
name:'conf', /* key */
type:'confirm', /* 確認 */
message:'是否建立新的項目?' /* 提示 */
},{
name:'name',
message:'請輸入項目名稱?',
when: res => Boolean(res.conf) /* 是否進行 */
},{
name:'author',
message:'請輸入做者?',
when: res => Boolean(res.conf)
},{
type: 'list', /* 選擇框 */
message: '請選擇公共管理狀態?',
name: 'state',
choices: ['mobx','redux'], /* 選項*/
filter: function(val) { /* 過濾 */
return val.toLowerCase()
},
when: res => Boolean(res.conf)
}
]
複製代碼
而後咱們在 command('create')
回調 action()
裏面繼續加上以下代碼。
program
.command('create')
.description('create a project ')
.action(function(){
green('👽 👽 👽 '+'歡迎使用mycli,輕鬆構建react ts項目~🎉🎉🎉')
inquirer.prompt(question).then(answer=>{
console.log('answer=', answer )
})
})
複製代碼
運行
mycli create
複製代碼
效果以下
接下來咱們要作的是,根據用戶提供的信息copy
項目文件,copy
文件有兩種方案,第一種項目模版存在腳手架中,第二種就是向github
這種遠程拉取項目模版,咱們在這裏用的是第一種方案。咱們在腳手架項目中新建template
文件夾。放入react-typescript
模版。接下來要作的是就是複製整個template
項目模版了。
因爲咱們的template
項目模版,有多是深層次的 文件夾 -> 文件 結構,咱們須要深複製項目文件和文件夾。因此須要node
中原生模塊fs
模塊來助陣。fs
大部分api
是異步I/O操做,因此須要一些小技巧來處理這些異步操做,咱們稍後會講到。
筆者看過一些樸靈《深刻淺出nodejs》,裏面有一段關於異步I/O描述。
const fs = require('fs')
fs.readFile('/path',()=>{
console.log('讀取文件完成')
})
console.log('發起讀取文件')
複製代碼
'發起讀取文件'是在'讀取文件完成'以前輸出的,說明用readFile
讀取文件過程是異步的,這樣的意義在於,在node
中,咱們能夠在語言層面很天然地進行並行的I/O操做。每一個調用之間無須等待以前的I/O調用結束,在編程模型上能夠極大提高效率。回到咱們的腳手架項目上來,咱們須要一次性大規模讀取模板文件,複製模版文件,也就是會操做不少上述所說的異步I/O操做。
咱們須要nodejs
中 fs
模塊,實現拷貝整個項目功能。相信對於使用過nodejs
開發者來講,fs
模塊並不陌生,基本上涉及到文件操做的功能都有用到,因爲篇幅的緣由,這裏就不一一講了,感興趣的同窗能夠看看 nodejs中文文檔-fs模塊基礎教程
思路:
① 選擇項目模版 :首先解析在第一步inquirer
交互模塊下用戶選擇的項目配置,咱們項目有可能有多套模版。由於好比上述選擇狀態管理mobx
或者是redux
,再好比說是選擇js
項目,或者是ts
項目,項目的架構和配置都是不一樣的,一套模版知足不了全部狀況。咱們在demo
中,就用了一種模版,就是最多見的react ts
項目模版,這裏指的就是在template
文件下的項目模版。
② 修改配置:對於咱們在inquirer
階段,提供的配置項,好比項目名稱,做者等等,須要咱們對項目模版單獨處理,修改配置項。這些信息通常都存在package.json
中。
③ 複製模版生成項目: 選擇好了項目模版,首先咱們遍歷整個template
文件夾下面全部文件,判斷子文件文件類型,若是是文件就直接複製文件,若是是文件夾,建立文件夾,而後遞歸遍歷文件夾下子文件,重複以上的操做。直到全部的文件所有複製完成。
④ 通知主程序執行下一步操做。
咱們在mycli
項目src
文件夾下面建立create.js
專門用於建立項目。廢話很少說,直接上代碼。
const create = require('../src/create')
program
.command('create')
.description('create a project ')
.action(function(){
green('👽 👽 👽 '+'歡迎使用mycli,輕鬆構建react ts項目~🎉🎉🎉')
/* 和開發者交互,獲取開發項目信息 */
inquirer.prompt(question).then(answer=>{
if(answer.conf){
/* 建立文件 */
create(answer)
}
})
})
複製代碼
接下來就是第一階段核心:
create
方法
module.exports = function(res){
/* 建立文件 */
utils.green('------開始構建-------')
/* 找到template文件夾下的模版項目 */
const sourcePath = __dirname.slice(0,-3)+'template'
utils.blue('當前路徑:'+ process.cwd())
/* 修改package.json*/
revisePackageJson( res ,sourcePath ).then(()=>{
copy( sourcePath , process.cwd() ,npm() )
})
}
複製代碼
在這裏咱們要弄明白兩個路徑的意義:
__dirname
:Node.js
中,__dirname
老是指向被執行 js
文件的絕對路徑,因此當你在 /d1/d2/mycli.js
文件中寫了__dirname
, 它的值就是/d1/d2
。
process.cwd()
: process.cwd()
方法會返回 Node.js
進程的當前工做目錄。
第一步實際很簡單,選擇好咱們要複製文件夾的路徑,而後根據用戶信息進行修改package.json
模版項目中的package.json
,咱們這裏簡單的作一個替換,將 demoName
和 demoAuthor
替換成用戶輸入的項目名稱和項目做者。
{
"name": "demoName",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "mycli start",
"build": "mycli build"
},
"author": "demoAuthor",
"license": "ISC",
"dependencies": {
"@types/react": "^16.9.25",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
//...更多內容
},
}
複製代碼
revisePackageJson修改package.json
function revisePackageJson(res,sourcePath){
return new Promise((resolve)=>{
/* 讀取文件 */
fs.readFile(sourcePath+'/package.json',(err,data)=>{
if(err) throw err
const { author , name } = res
let json = data.toString()
/* 替換模版 */
json = json.replace(/demoName/g,name.trim())
json = json.replace(/demoAuthor/g,author.trim())
const path = process.cwd()+ '/package.json'
/* 寫入文件 */
fs.writeFile(path, new Buffer(json) ,()=>{
utils.green( '建立文件:'+ path )
resolve()
})
})
})
}
複製代碼
效果如上所示,這一步實際流程很簡單,就是讀取template
中的package.json
文件,而後根據模版替換,接下來從新在目標目錄中生成package.json
。接下來revisePackageJson
返回的promise
中進行真正的複製文件流程。
let fileCount = 0 /* 文件數量 */
let dirCount = 0 /* 文件夾數量 */
let flat = 0 /* readir數量 */
/** * * @param {*} sourcePath //template資源路徑 * @param {*} currentPath //當前項目路徑 * @param {*} cb //項目複製完成回調函數 */
function copy (sourcePath,currentPath,cb){
flat++
/* 讀取文件夾下面的文件 */
fs.readdir(sourcePath,(err,paths)=>{
flat--
if(err){
throw err
}
paths.forEach(path=>{
if(path !== '.git' && path !=='package.json' ) fileCount++
const newSoucePath = sourcePath + '/' + path
const newCurrentPath = currentPath + '/' + path
/* 判斷文件信息 */
fs.stat(newSoucePath,(err,stat)=>{
if(err){
throw err
}
/* 判斷是文件,且不是 package.json */
if(stat.isFile() && path !=='package.json' ){
/* 建立讀寫流 */
const readSteam = fs.createReadStream(newSoucePath)
const writeSteam = fs.createWriteStream(newCurrentPath)
readSteam.pipe(writeSteam)
color.green( '建立文件:'+ newCurrentPath )
fileCount--
completeControl(cb)
/* 判斷是文件夾,對文件夾單獨進行 dirExist 操做 */
}else if(stat.isDirectory()){
if(path!=='.git' && path !=='package.json' ){
dirCount++
dirExist( newSoucePath , newCurrentPath ,copy,cb)
}
}
})
})
})
}
/** * * @param {*} sourcePath //template資源路徑 * @param {*} currentPath //當前項目路徑 * @param {*} copyCallback // 上面的 copy 函數 * @param {*} cb //項目複製完成回調函數 */
function dirExist(sourcePath,currentPath,copyCallback,cb){
fs.exists(currentPath,(ext=>{
if(ext){
/* 遞歸調用copy函數 */
copyCallback( sourcePath , currentPath,cb)
}else {
fs.mkdir(currentPath,()=>{
fileCount--
dirCount--
copyCallback( sourcePath , currentPath,cb)
color.yellow('建立文件夾:'+ currentPath )
completeControl(cb)
})
}
}))
}
複製代碼
這一步的流程大體是這樣的,首先用 fs.readdir
讀取template
文件夾下面的文件,而後經過 fs.stat
讀取文件信息,判斷文件的類型,若是當前文件類型是文件類型,那麼經過讀寫流fs.createReadStream
和fs.createWriteStream
建立文件;若是當前文件類型是文件夾類型,判斷文件夾是否存在,若是當前文件夾存在,遞歸調用copy
複製文件夾下面的文件,若是不存在,那麼從新新建文件夾,而後執行遞歸調用。這裏有一點注意的是,因爲咱們對package.json
單獨處理,因此這裏的一切文件操做應該排除package.json
。由於咱們要在整個項目文件所有複製後,進行自動下載依賴等後續操做。
小技巧:三變量計數法控制異步I/O操做
上面的內容講到了fs
模塊基本都是異步I/O操做,並且咱們的複製文件是深層次遞歸調用,這就有一個問題,如何纔可以判斷全部的文件都已經複製完成呢 ,對於這種層次和數量都是未知的文件結構,很難經過promise
等異步解決方案來處理。這裏咱們沒有引入第三方異步流程庫,而是巧妙的運用變量計數法來判斷是否全部文件均以複製完畢。
變量一flat
: 每一次copy函數調用,會執行異步fs.readdir
讀取文件夾下面的全部文件,咱們用 flat++
記錄 readdir
數量, 每次readdir
完成執行flat--
。
變量二fileCount
: 每一次文件(可能文件或者文件夾)的遍歷,咱們用fileCount++
來記錄,當文件建立完成或者文件夾建立完成,執行 fileCount--
。
變量三dirCount
: 每一次判斷文件夾的操做,咱們用 dirCount++
來記錄,當新的文件夾被建立完成,執行 dirCount--
。
function completeControl(cb){
/* 三變量均爲0,異步I/O執行完畢。 */
if(fileCount === 0 && dirCount ===0 && flat===0){
color.green('------構建完成-------')
if(cb && !isInstall ){
isInstall = true
color.blue('-----開始install-----')
cb(()=>{
color.blue('-----完成install-----')
/* 判斷是否存在webpack */
runProject()
})
}
}
}
複製代碼
咱們在每次建立文件或文件夾事件執行以後,都會調用completeControl
方法,經過判斷flat
,fileCount
,dirCount
三個變量均爲0,就能判斷出整個複製流程,執行完畢,並做出下一步操做。
效果
建立項目階段完畢
第二階段咱們主要完成的功能有如下兩個方面:
第一部分: 上述咱們複製了整個項目,接下來須要下載依賴和運行項目;
第二部分: 咱們只是完成了 mycli create
建立項目流程,對於 mycli start
運行項目 ,和 mycli build
打包編譯項目,尚未弄。接下來咱們慢慢道來。
以前咱們介紹了,經過修改bin
,藉助commander
模塊來經過輸入終端命令行,來執行node
文件,來對應啓動咱們的程序。接下來咱們要作的是經過nodejs
代碼,來執行對應的終端命令。這個功能的背景是,咱們須要在複製整個項目目錄以後,來自動下載依賴npm, install
,啓動項目npm start
。
首先咱們在mycli
腳手架項目的src
文件夾下,新建npm.js
,用來處理下載依賴,啓動項目操做。
which
模塊助力找到npm
像unixwhich實用程序同樣。在PATH環境變量中查找指定可執行文件的第一個實例。不緩存結果,所以hash -rPATH更改時不須要。也就是說咱們能夠找到npm
實例,經過代碼層面控制npm
作某些事。
例子🌰🌰🌰:
var which = require('which')
//異步用法
which('node', function (er, resolvedPath) {
// 若是在PATH上找不到「節點」,則返回er
// 若是找到,則返回exec的絕對路徑
})
//同步用法
const resolved = which.sync('node')
複製代碼
在npm.js下
const which = require('which')
/* 找到npm */
function findNpm() {
var npms = process.platform === 'win32' ? ['npm.cmd'] : ['npm']
for (var i = 0; i < npms.length; i++) {
try {
which.sync(npms[i])
console.log('use npm: ' + npms[i])
return npms[i]
} catch (e) {
}
}
throw new Error('please install npm')
}
複製代碼
在上面咱們成功找到npm
以後,須要用 child_process.spawn
運行當前命令。
child_process.spawn(command[, args][, options])
command <string>
要運行的命令。 args <string[]>
字符串參數列表。 options <Object>
配置參數。
/** * * @param {*} cmd * @param {*} args * @param {*} fn */
/* 運行終端命令 */
function runCmd(cmd, args, fn) {
args = args || []
var runner = require('child_process').spawn(cmd, args, {
stdio: 'inherit'
})
runner.on('close', function (code) {
if (fn) {
fn(code)
}
})
}
複製代碼
接下來咱們①②步驟的內容整合在一塊兒,把整個npm.js
npm
方法暴露出去.
/** * * @param {*} installArg 執行命令 命令行組成的數組,默認爲 install */
module.exports = function (installArg = [ 'install' ]) {
/* 經過第一步,閉包保存npm */
const npm = findNpm()
return function (done){
/* 執行命令 */
runCmd(which.sync(npm),installArg, function () {
/* 執行成功回調 */
done && done()
})
}
}
複製代碼
使用例子🌰🌰
const npm = require('./npm')
/* 執行 npm install */
const install = npm()
install()
/* 執行 npm start */
const start = npm(['start]) start() 複製代碼
咱們在上一步複製項目中,回調函數cb
究竟是什麼? 相信細心的同窗已經發現了。
const npm = require('./npm')
copy( sourcePath , process.cwd() ,npm() )
複製代碼
cb
函數就是執行npm install
的方法。
咱們接着上述的複製成功後,啓動項目來說。在三變量判斷項目建立成功以後,咱們開始執行安裝項目.
function completeControl(cb){
if(fileCount === 0 && dirCount ===0 && flat===0){
color.green('------構建完成-------')
if(cb && !isInstall ){
isInstall = true
color.blue('-----開始install-----')
/* 下載項目 */
cb(()=>{
color.blue('-----完成install-----')
runProject()
})
}
}
}
複製代碼
咱們在安裝依賴成功的回調函數中,繼續調用runProject
啓動項目。
function runProject(){
try{
/* 繼續調用 npm 執行,npm start 命令 */
const start = npm([ 'start' ])
start()
}catch(e){
color.red('自動啓動失敗,請手動npm start 啓動項目')
}
}
複製代碼
效果:因爲安裝依賴時間過長,運行項目階段沒有在視頻裏展現
runProject
代碼很簡單,繼續調用 npm
, 執行 npm start
命令。
到此爲止,咱們實現了經過 mycli create
建立項目,安裝依賴,運行項目全流程,裏面還有集成webpack
, 進程通訊等細節,咱們立刻慢慢道來。
咱們既然搞定了mycli create
細節和實現。接下來咱們須要實現mycli start
和 mycli build
兩個功能。
咱們打算用webpack
做爲腳手架的構建工具。那麼咱們須要mycli
主進程,建立一個子進程來管理webpack
,合併webpack
配置項,運行webpack-dev-serve
等,這裏注意的是,咱們的主進程是在mycli
全局腳手架項目中,而咱們的子進程要創建在咱們本地經過mycli create
建立的react
新項目node_modules
中,因此咱們寫了一個腳手架的plugin
用來一方面創建和mycli
進程通訊,另外一方面管理咱們的react
項目的配置,操控webpack
。
爲了方便你們瞭解,我畫了一個流程圖。
mycli-react-webpack-plugin
在建立項目中package.json
中,咱們在安裝依賴的過程當中,已經安裝在了新建項目的node_modules
中。
mycli start
和 mycli build
接下來咱們在mycli
腳手架項目src
文件夾下面建立start.js
爲了和上述的plugin
創建起進程通訊。由於不管是執行mycli start
或者是 mycli build
都是須要操縱webpack
因此咱們寫在了一塊兒了。
咱們繼續在mycli.js
中完善 mycli start
和 mycli build
兩個指令。
const start = require('../src/start')
/* mycli start 運行項目 */
program
.command('start')
.description('start a project')
.action(function(){
green('--------運行項目-------')
/* 運行項目 */
start('start').then(()=>{
green('-------✅ ✅運行完成-------')
})
})
/* mycli build 打包項目 */
program
.command('build')
.description('build a project')
.action(function(){
green('--------構建項目-------')
/* 打包項目 */
start('build').then(()=>{
green('-------✅ ✅構建完成-------')
})
})
複製代碼
modulePath
:子進程運行的模塊。
參數說明:(重複的參數說明就不在這裏列舉)
execPath
: 用來建立子進程的可執行文件,默認是/usr/local/bin/node
。也就是說,你可經過execPath
來指定具體的node
可執行文件路徑。(好比多個node
版本) execArgv::
傳給可執行文件的字符串參數列表。默認是 process.execArgv
,跟父進程保持一致。 silent:
默認是false
,即子進程的stdio從父進程繼承。若是是true
,則直接pipe
向子進程的child.stdin、child.stdout
等。 stdio:
若是聲明瞭stdio
,則會覆蓋silent
選項的設置。
咱們在start.js
中啓動子進程和上述的mycli-react-webpack-plugin
創建起通訊。接下來就是介紹start.js
。
start.js
'use strict';
/* 啓動項目 */
const child_process = require('child_process')
const chalk = require('chalk')
const fs = require('fs')
/* 找到mycli-react-webpack-plugin的路徑*/
const currentPath = process.cwd()+'/node_modules/mycli-react-webpack-plugin'
/** * * @param {*} type type = start 本地啓動項目 type = build 線上打包項目 */
module.exports = (type) => {
return new Promise((resolve,reject)=>{
/* 判斷 mycli-react-webpack-plugin 是否存在 */
fs.exists(currentPath,(ext)=>{
if(ext){ /* 存在 啓動子進程 */
const children = child_process.fork(currentPath + '/index.js' )
/* 監聽子進程信息 */
children.on('message',(message)=>{
const msg = JSON.parse( message )
if(msg.type ==='end'){
/* 關閉子進程 */
children.kill()
resolve()
}else if(msg.type === 'error'){
/* 關閉子進程 */
children.kill()
reject()
}
})
/* 發送cwd路徑 和 操做類型 start 仍是 build */
children.send(JSON.stringify({
cwdPath:process.cwd(),
type: type || 'build'
}))
}else{ /* 不存在,拋出警告,下載 */
console.log( chalk.red('mycli-react-webpack-plugin does not exist , please install mycli-react-webpack-plugin') )
}
})
})
}
複製代碼
這一步實際很簡單,大體分爲二步:
1 判斷 mycli-react-webpack-plugin
是否存在,若是存在啓動 mycli-react-webpack-plugin
下的index.js
爲子進程。若是不存在,拋出警告下載plugin
。
2 綁定子進程事件message
,向子進程發送指令,是啓動項目仍是構建項目。
接下來作的事就是讓mycli-react-webpack-plugin
完成項目配置,項目構建流程。
mycli-react-webpack-plugin
插件項目文件結構
項目目錄大體是如上的樣子,config
文件下,是不一樣構建環境的基礎配置文件,在項目構建過程當中,會讀取建立新項目的mycli.config.js
在生產環境和開發環境的配置項,而後合併配置項。
咱們的新建立項目的mycli.config.js
const RunningWebpack = require('./lib/run')
/** * 建立一個運行程序,在webpack的不一樣環境下運行配置文件 */
/* 啓動 RunningWebpack 實例 */
const runner = new RunningWebpack()
process.on('message',message=>{
const msg = JSON.parse( message )
if(msg.type && msg.cwdPath ){
runner.listen(msg).then(
()=>{
/* 構建完成 ,通知主進程 ,結束子進程 */
process.send(JSON.stringify({ type:'end' }))
},(error)=>{
/* 出現錯誤 ,通知主進程 ,結束子進程 */
process.send(JSON.stringify({ type:'error' , error }))
}
)
}
})
複製代碼
咱們這裏用RunningWebpack
來執行一系列的webpack
啓動,打包操做。
EventEmitter
的 RunningWebpack
咱們的 RunningWebpack
基於 nodejs
的 EventEmitter
模塊,EventEmitter
能夠解決異步I/O,能夠在合適的場景觸發不一樣的webpack
命令,好比 start
或者是 build
等。
nodejs
全部的異步 I/O 操做在完成時都會發送一個事件到事件隊列。
Node.js 裏面的許多對象都會分發事件:一個 net.Server
對象會在每次有新鏈接時觸發一個事件, 一個 fs.readStream
對象會在文件被打開的時候觸發一個事件。 全部這些產生事件的對象都是 events.EventEmitter
的實例。
//event.js 文件
var EventEmitter = require('events').EventEmitter;
var event = new EventEmitter();
event.on('some_event', function() {
console.log('some_event 事件觸發');
});
setTimeout(function() {
event.emit('some_event');
}, 1000);
複製代碼
webpack
配置項上述介紹完用 EventEmitter
做爲運行webpack
的事件模型,接下咱們來分析如下,當運行入口文件的時候。
runner.listen(msg).then
複製代碼
const merge = require('./merge')
const webpack = require('webpack')
const runMergeGetConfig = require('../config/webpack.base')
/** * 接受不一樣的webpack狀態,合併 */
listen({ type,cwdPath }){
this.path = cwdPath
this.type = type
/* 合併配置項,獲得新的webpack配置項 */
this.config = merge.call(this,runMergeGetConfig( cwdPath )(type))
return new Promise((resolve,reject)=>{
this.emit('running',type)
this.once('error',reject)
this.once('end',resolve)
})
}
複製代碼
listen
入參參數有兩個,type
是主線程的傳遞過來的webpack
命令,分爲start
和build
,cwdPath
是咱們輸入終端命令行的絕對路徑,接下來咱們要作的是讀取新建立項目的mycli.config.js
。而後和咱們的默認配置進行合併操做。
runMergeGetConfig 能夠根據咱們傳遞的環境(start
or build
)獲得對應的webpack
基礎配置。咱們來一塊兒看看runMergeGetConfig
作了什麼。
const merge = require('webpack-merge')
module.exports = function(path){
return type => {
if (type==='start') {
return merge(Appconfig(path), devConfig(path))
} else {
return merge(Appconfig(path), proConfig)
}
}
}
複製代碼
runMergeGetConfig
很簡單就是將 base
基礎配置,和 dev
或者pro
環境進行合併獲得腳手架的基本配置,而後再和mycli.config.js
文件下的自定義配置項合併,咱們接着看。
咱們接着看 mycli-react-webpack-plugin
插件下,lib
文件夾下的merge.js
。
const fs = require('fs')
const merge = require('webpack-merge')
/* 合併配置 */
function configMegre(Pconf,config){
const {
dev = Object.create(null),
pro = Object.create(null),
base= Object.create(null)
} = Pconf
if(this.type === 'start'){
return merge(config,base,dev)
}else{
return merge(config,base,pro)
}
}
/** * @param {*} config 通過 runMergeGetConfig 獲得的腳手架基礎配置 */
function megreConfig(config){
const targetPath = this.path + '/mycli.config.js'
const isExi = fs.existsSync(targetPath)
if(isExi){
/* 獲取開發者自定義配置 */
const perconfig = require(targetPath)
/**/
const mergeConfigResult = configMegre.call(this,perconfig,config)
return mergeConfigResult
}
/* 返回最終打包的webpack配置項 */
return config
}
module.exports = megreConfig
複製代碼
這一步實際很簡單,獲取開發者的自定義配置,而後和腳手架的默認配置合併,獲得最終的配置。並會返回給咱們的running
實例。
webpack
接下來咱們作的是啓動webpack
。生產環境比較簡單,直接 webpack(config)
就能夠了。在開發環境中,因爲須要webpack-dev-server
搭建起服務器,而後掛起項目,因此須要咱們單獨處理。首先將開發環境下的config
傳入webpack
中獲得compiler
,而後啓動dev-server
服務,compiler
做爲參數傳入webpack
並監聽咱們設置的端口,完成整個流程。
const Server = require('webpack-dev-server/lib/Server')
const webpack = require('webpack')
const processOptions = require('webpack-dev-server/lib/utils/processOptions')
const yargs = require('yargs')
/* 運行生產環境webpack */
build(){
try{
webpack(this.config,(err)=>{
if(err){
/* 若是發生錯誤 */
this.emit('error')
}else{
/* 結束 */
this.emit('end')
}
})
}catch(e){
this.emit('error')
}
}
/* 運行開發環境webpack */
start(){
const _this = this
processOptions(this.config,yargs.argv,(config,options)=>{
/* 獲得webpack compiler*/
const compiler = webpack(config)
/* 建立dev-server服務 */
const server = new Server(compiler , options )
/* port 是在webpack.dev.js下的開發環境配置項中 設置的監聽端口 */
server.listen(options.port, options.host, (err) => {
if (err) {
_this.emit('error')
throw err;
}
})
})
}
複製代碼
完整代碼
const EventEmitter = require('events').EventEmitter
const Server = require('webpack-dev-server/lib/Server')
const processOptions = require('webpack-dev-server/lib/utils/processOptions')
const yargs = require('yargs')
const merge = require('./merge')
const webpack = require('webpack')
const runMergeGetConfig = require('../config/webpack.base')
/** * 運行不一樣環境下的webpack */
class RunningWebpack extends EventEmitter{
/* 綁定 running 方法 */
constructor(options){
super()
this._options = options
this.path = null
this.config = null
this.on('running',(type,...arg)=>{
this[type] && this[ type ](...arg)
})
}
/* 接受不一樣狀態下的webpack命令 */
listen({ type,cwdPath }){
this.path = cwdPath
this.type = type
this.config = merge.call(this,runMergeGetConfig( cwdPath )(type))
return new Promise((resolve,reject)=>{
this.emit('running',type)
this.once('error',reject)
this.once('end',resolve)
})
}
/* 運行生產環境webpack */
build(){
try{
webpack(this.config,(err)=>{
if(err){
this.emit('error')
}else{
this.emit('end')
}
})
}catch(e){
this.emit('error')
}
}
/* 運行開發環境webpack */
start(){
const _this = this
processOptions(this.config,yargs.argv,(config,options)=>{
const compiler = webpack(config)
const server = new Server(compiler , options )
server.listen(options.port, options.host, (err) => {
if (err) {
_this.emit('error')
throw err;
}
})
})
}
}
module.exports = RunningWebpack
複製代碼
接下來咱們要講的項目運行階段,一些附加的配置項,和一塊兒其餘的操做。
plugin
咱們寫一個webpack
的plugin
作爲mycli
腳手架的工具,爲了方便向開發者展現修改的文件,和一次webpack
構建時間,整個插件是在webpack
編譯階段完成的。咱們須要簡單瞭解webpack
一些知識。
在開發 Plugin
時最經常使用的兩個對象就是 Compiler
和 Compilation
,它們是 Plugin
和 Webpack
之間的橋樑。 Compiler
和 Compilation
的含義以下:
Compiler
對象包含了 Webpack
環境全部的的配置信息,包含 options,loaders,plugins
這些信息,這個對象在 Webpack
啓動時候被實例化,它是全局惟一的,能夠簡單地把它理解爲 Webpack
實例; Compilation
對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack
以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation
將被建立。 Compilation
對象也提供了不少事件回調供插件作擴展。經過 Compilation
也能讀取到 Compiler
對象。 Compiler
和 Compilation
的區別在於: Compiler
表明了整個 Webpack
從啓動到關閉的生命週期,而 Compilation
只是表明了一次新的編譯。
Compiler
編譯階段咱們要理解一次Compiler
各個階段要作的事,才能在特定的階段用指定的鉤子來完成咱們的自定義plugin
。
啓動一次新的編譯
和 run
相似,區別在於它是在監聽模式下啓動的編譯,在這個事件中能夠獲取到是哪些文件發生了變化致使從新啓動一次新的編譯。
該事件是爲了告訴插件一次新的編譯將要啓動,同時會給插件帶上 compiler
對象。
當 Webpack
以開發模式運行時,每當檢測到文件變化,一次新的 Compilation
將被建立。一個 Compilation
對象包含了當前的模塊資源、編譯生成資源、變化的文件等。Compilation
對象也提供了不少事件回調供插件作擴展。
一個新的 Compilation
建立完畢,即將從 Entry
開始讀取文件,根據文件類型和配置的 Loader
對文件進行編譯,編譯完後再找出該文件依賴的文件,遞歸的編譯和解析。
一次 Compilation
執行完成。
當遇到文件不存在、文件編譯錯誤等異常時會觸發該事件,該事件不會致使 Webpack
退出。
咱們編寫的webpack
插件,須要在改動時候,打印出當前改動的文件 ,並用進度條展現一次編譯的時間。
const chalk = require('chalk')
var slog = require('single-line-log');
class MycliConsolePlugin {
constructor(options){
this.options = options
}
apply(compiler){
/* 監聽文件改動 */
compiler.hooks.watchRun.tap('MycliConsolePlugin', (watching) => {
const changeFiles = watching.watchFileSystem.watcher.mtimes
for(let file in changeFiles){
console.log(chalk.green('當前改動文件:'+ file))
}
})
/* 在一次編譯建立以前 */
compiler.hooks.compile.tap('MycliConsolePlugin',()=>{
this.beginCompile()
})
/* 一次 compile 完成 */
compiler.hooks.done.tap('MycliConsolePlugin',()=>{
this.timer && clearInterval( this.timer )
console.log( chalk.yellow(' 編譯完成') )
})
}
/* 開始記錄編譯 */
beginCompile(){
const lineSlog = slog.stdout
let text = '開始編譯:'
this.timer = setInterval(()=>{
text += '█'
lineSlog( chalk.green(text))
},50)
}
}
module.exports = RuxConsolePlugin
複製代碼
插件的使用,由於咱們這個插件是在開發環境下,因此只須要在webpack.dev.js
加入上述的MycliConsolePlugin
插件。
const webpack = require('webpack')
const MycliConsolePlugin = require('../plugins/mycli-console-pulgin')
const devConfig =(path)=>{
return {
devtool: 'cheap-module-eval-source-map',
mode: 'development',
devServer: {
contentBase: path + '/dist',
open: true, /* 自動打開瀏覽器 */
hot: true,
historyApiFallback: true,
publicPath: '/',
port: 8888, /* 服務器端口 */
inline: true,
proxy: { /* 代理服務器 */
} },
plugins: [
new webpack.HotModuleReplacementPlugin(),
new MycliConsolePlugin({
dec:1
})
]
}
}
module.exports = devConfig
複製代碼
前端自動化已經脫離 mycli
範疇了,可是爲了讓你們明白前端自動化流程,這裏用webpack
提供的API
中的require.context
爲案例。
require.context(directory, useSubdirectories = true, regExp = /^\.\/.*$/, mode = 'sync');
複製代碼
能夠給這個函數傳入三個參數: ① directory
要搜索的目錄, ② useSubdirectories
標記表示是否還搜索其子目錄, ③ regExp
匹配文件的正則表達式。
webpack
會在構建中解析代碼中的 require.context()
。
官網示例:
/* (建立出)一個 context,其中文件來自 test 目錄,request 以 `.test.js` 結尾。 */
require.context('./test', false, /\.test\.js$/);
/* (建立出)一個 context,其中全部文件都來自父文件夾及其全部子級文件夾,request 以 `.stories.js` 結尾。 */
require.context('../', true, /\.stories\.js$/);
複製代碼
咱們接着用mycli
建立的項目做爲demo
,咱們在項目src
文件夾下面新建model
文件夾,用來自動收集裏面的文件。model
文件下,有三個文件 demo.ts
, demo1.ts
,demo2.ts
,咱們接下來作的是自動收集文件下的數據。
項目目錄
demo.ts
const a = 'demo'
export default a
複製代碼
· demo1.ts
const b = 'demo1'
export default b
複製代碼
demo2.ts
const b = 'demo2'
export default b
複製代碼
探索 require.context
const file = require.context('./model',false,/\.tsx?|jsx?$/)
console.log(file)
複製代碼
打印file
,咱們發現webpack
的方法。接下來咱們獲取文件名組成的數組。
const file = require.context('./model',false,/\.tsx?|jsx?$/)
console.log(file.keys())
複製代碼
解析來咱們自動收集文件下的a , b ,c 變量。
/* 用來收集文件 */
const model ={}
const file = require.context('./model',false,/\.tsx?|jsx?$/)
/* 遍歷文件 */
file.keys().map(item=>{
/* 收集數據 */
model[item] = file(item).default
})
console.log(model)
複製代碼
到這裏咱們實現了自動收集流程。若是深層次遞歸收集,咱們能夠將 require.context
第二個參數設置爲true
require.context('./model',true,/\.tsx?|jsx?$/)
複製代碼
項目目錄
demo3.ts
const d = 'demo3'
export default d
複製代碼
打印完美遞歸收集了子文件下的model
整個自定義腳手架包含的技術有;
感興趣的同窗能夠本身去嘗試寫一個屬於本身的腳手架,過程當中會學會不少知識。
送人玫瑰,手留餘香,閱讀的朋友能夠給筆者點贊,關注一波 。 陸續更新前端文章。
感受有用的朋友能夠關注筆者公衆號 前端Sharing 持續更新好文章。
[2.深刻淺出webpack]