读完这篇文章你可能会学到哪些知识?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]