简介
如果我们要创建一个vue
项目,肯定会用到vue的脚手架,比如vue-cli create myDemo
,但是通过这种方式创建出来的项目肯定是一个非常简单、非常基本的项目,主要有以下原因:
没有划分目录结构
没有进行一些基本配置:比如vue.config.js
里面没有我们常用的一些库,比如说axios
;这样就导致你要手动安装依赖,安装完之后还要进行封装和配置
还有一些常见的组件库,比如说element-ui
、ant-design
,这也是需要单独安装和配置的
没有路由相关的配置
vuex
和redux
相关的配置也没有
如果我们只是自己随便开发一个项目,那倒没关系,但如果我们出自架构师的角度考虑,这肯定是不合适的,所以我们必须去寻找一些高效的方法来帮助我们完成这些事情,比如:
先写一个项目模板,将上面所需要的东西统统配置好(很多脚手架都是那么做的)
然后去开发一个工具,其可以跟大部分的工具比如webpack
、npm
一样可以在命令行执行命令,比如我自定义一个指令dcc
,在终端输入了dcc create myApp
之后,就会将我之前写好的模板从代码仓库中下载到当前目录下的myApp
文件夹中
我们还可以多做一些事情,比如自动去安装项目中的依赖,甚至可以在安装完依赖之后自动打开浏览器。总而言之,我们要实现的就是化繁为简——通过一个命令完成大量的操作
下面我们来看一下,怎么去自定义一个类似于npm
、webpack
一样的命令吧!
自定义命令执行代码
我们需要提前在packag.json
(可以通过npm init -y
命令生成)中配置一个bin
字段,这里指定了我们自定义的命令名称以及对应执行的文件
在index.js
文件的顶部添加上#!/usr/bin/env node
这行代码,表示用node
执行这个文件
在命令行输入npm link
命令将我们自定义的指令绑定到环境变量中
然后就可以使用我们自定义的命令去执行代码了,比如在命令行中输入dcc
之后,就会去到环境变量中查找应该执行哪个文件,比如在这里原先绑定的index.js
文件就会被执行
// index.js#!/usr/bin/env nodeconsole.log('index.js文件代码被执行!');
查看版本信息
我们首选需要下载一个名为commander
的依赖,很多脚手架包括vue-cli
都在使用它,有了它之后可以很方便的帮助我们添加一些指令
为了确保我们的版本号是正确的,所以我们可以到package.json
文件中实时获取当前项目的版本号
引入commander
之后,我们可以在对应的对象上面配置相关的信息,指定输入什么命令之后显示什么内容,比如说输入dcc --version
就可以显示当前版本信息
program.parse(process.argv)
这行代码很重要,因为program.version
只相当于去定义版本信息,program.parse
才是去解析我们输入的命令的函数,然后根据命令去显示内容
const program = require('commander')// 提前先定义好版本号,我这里采用动态的查询package.json文件中的version字段,默认对应的命令是 -V 和 --versionprogram.version(require('./package.json').version)// 这是为了让我们输入dcc -v的时候也能被识别到program.version(require('./package.json').version, '-v')// 上面的代码只是定义了查看版本时显示什么内容,program.parse才是根据输入的命令去定义好的命令中寻找显示内容的program.parse(process.argv)
自定义--help的内容
program.option
方法可以在--help
中加入我们自定义的说明,该方法接收两个参数,一个是用户输入的命令,另一个是对应的内容
我们还可以在第一个参数后面加上一个<用户额外输入的指令>
,这样我们就可以在program._optionValues
中查看到用户额外输入的指令。比如说你的脚手架既可以搭建react
项目,又可以搭建vue
项目,那肯定要根据用户传递进来的参数来选择往哪一个仓库中下拉代码,这个时候就可以通过上面说的那种方式获取到用户传递进来的参数
program.on
可以监听某一个命令,输入被监听的命令之后,会执行传进去的回调函数,我们可以用它来制做--help
的扩展内容
// lib/core/help.jsconst program = require('commander')// 增加自己的Optionsprogram.option('-w --why', 'a dcc CLI')program.option('-d --dest <dest>', 'a destination folder')program.on('--help', () => { console.log(''); console.log('Other'); console.log(' other options');})console.log(program._optionValues.dest);
克隆指定仓库中的代码
在实现自动克隆仓库之前,我们首先需要将创建项目的指令添加上去,要不然我们怎么知道什么时候去克隆模板下来呢?添加指令我们之前有操作过,用的还是commander
这个库
// lib/core/create.jsconst program = require('commander')// 为了后续代码维护更加方便,我们把每个指令对应的函数放到一个actions文件夹中统一管理const { createProjectAction} = require('./actions')// 创建dcc create project指令并进行相应操作const createCommands = () => { program .command('create <project> [others...]') // 定义指令的格式,前面的dcc是默认的,所以不用加,<project>是项目名称,[others...]是后面的参数,很少会用到 .description('clone repository into a folder') // 对这个指令做一个描述 .action(createProjectAction) // 当用户执行了该指令之后,去执行哪一个函数}module.exports = { createCommands}
对应的指令配置好之后,我们就要使用download-git-repo
这个库来帮助我们下载项目到指定的目录下,下面是官方给出的用法
从该库中引入的是一个函数,其接受三个参数,第一个是克隆仓库的目标地址(前面要加上direct:
),第二个参数是克隆到哪个目录下(以执行命令所在的目录为基本目录),第三个参数是克隆成功或失败后回调的一个函数
const download = require('download-git-repo')download('direct:https://gitlab.com/flippidippi/download-git-repo-fixture/repository/archive.zip', 'test/tmp', function (err) { console.log(err ? 'Error' : 'Success')})
由于我们在克隆完仓库模板后还需要执行其它的命令,比如说安装项目依赖,这样一来,我们就需要在回调函数里面写回调,很容易产生回调地狱,所以我们很希望可以把它改成promise
的形式
在node
的核心模块util
中,有一个方法叫promisify
,其就是用来将一些有回调函数的方法转化为promise
格式进行调用的
其原理也很简单,实际上就是在我们指定函数的外层包裹一层promise
,然后当回调函数被执行的时候,判断其有无err
。如果有,则reject
出去;如果没有,则resolve
出去
为了让书写更加优雅,我们采用了async
、await
同步书写代码
// lib/core/actions.jsconst { promisify } = require('util')const download = promisify(require('download-git-repo'))// 这个导入的是我们模板项目的地址,因为后面想要对其做一些扩展,所以单独放到了一个文件中const { vueRepo } = require('../config/repo-config')const createProjectAction = async (project) => { try { // 在进行克隆之前提示一下用户,我们正在为其克隆项目 console.log('dcc is help you create a vue project, please wait~'); // 我们使用promise更加优雅的写法也就是async、await来实现同步书写代码,避免回调地狱 await download(vueRepo, project, { clone: true }) // clone属性表示将仓库中的所有东西都拷贝下来 } catch (err) { console.log(err); }}module.exports = { createProjectAction}
这样一来,当我们在命令行中敲下dcc create myProject
之后,就会自动在当前目录下新建一个myProject
文件夹,然后自动下载对应仓库中的代码,也就实现了脚手架最基本的功能
克隆完项目之后安装依赖
首先安装依赖肯定是要执行npm
、yarn
等命令的,执行这些命令的时候需要node
帮助我们开启一个额外的进程,这就需要node中另一个核心模块:child_process
,该模块中有一个spawn
方法,可以帮助我们执行命令并且开启一个进程
该方法接收3
个参数,第一个是我们的命令的第一个英文,比如说npm或者yarn。第二个参数要求要传一个数组,如果你要执行npm install
命令,那么该数组就是['install']
。如果你要执行npm install axios
,那么这个数组就应该为['install', 'axios']
。第三个参数是个对象,比较常用的就是里面的cwd
属性,其表示我们这个命令要在哪个目录下执行。毫无疑问,安装依赖肯定是要在仓库代码所在的目录也就是用户执行dcc create project
中的project
目录下执行的
因为我们后续还要对脚手架进行扩展,所以肯定不只执行一个命令,那么我们可以将执行命令的代码封装一个函数放在utils
文件夹中,我们可以使用promise
做一层封装,spawn
方法在执行了我们指定的命令并开启了进程之后,会将进程对应的对象返回给我们,因为执行命令是在另一个进程中,所以用户是无法看到打印信息的,此时就需要做一个转移,利用进程对象childProcess
上面的stdout
或stderr
属性中的pipe
方法,将该进程中要打印的东西传递给当前process
对象下的stdout或stderr属性,也就是说在新开进程中打印的数据和错误现在都可以在当前进程中看到了
因为里面一些方法可能需要传递回调函数,比如childProcess.on
方法,他可以监听我们开启的进程有没有关闭,当进程关闭之后,我们可以执行resolve
函数,这样外面就可以使用async
、await
语法糖啦
// lib/utils/terminal.jsconst { spawn } = require('child_process')const commandSpawn = (...args) => { return new Promise((resolve, reject) => { const childProcess = spawn(...args) childProcess.stdout.pipe(process.stdout) childProcess.stderr.pipe(process.stderr) childProcess.on('close', () => { resolve() }) })}module.exports = { commandSpawn}
还有一个很重要的问题,当我们在命令行执行npm
或yarn
命令时,在windows
系统下面其实它帮我们转成了npm.cmd
或者yarn.cmd
命令,Mac OS
和Linux
操作系统中就没有转化,所以我们还需要利用全局对象process
下的platform
属性的判断一下用户所处在什么操作系统中,如果是windwos操作系统,那么要传递的第一个参数就是npm.cmd或yarn.cmd,如果不是windows操作系统,要传递的参数就应该直接是npm或yarn
下面的代码结合了之前克隆代码的操作,也就是说当前代码已经可以自动下载模板和安装依赖了
// lib/core/actions.jsconst { promisify } = require('util')const download = promisify(require('download-git-repo'))const { vueRepo } = require('../config/repo-config')// 引入工具函数中引入执行命令行的方法const { commandSpawn } = require('../utils/terminal')const createProjectAction = async (project) => { try { console.log('dcc is help you create a vue project, please wait~'); // 1. clone项目 await download(vueRepo, project, { clone: true }) // 2. 执行npm install命令安装依赖 // const command = process.platform === 'win32' ? 'npm.cmd' : 'npm' const command = process.platform === 'win32' ? 'yarn.cmd' : 'yarn' await commandSpawn(command, ['install'], { cwd: `./${project}` }) } catch (err) { console.log(err); }}module.exports = { createProjectAction}
安装完项目之后自动运行并打开浏览器对应端口
自动运行项目肯定又要用到命令行的,比如运行react
项目可能是npm start
,运行vue
项目可能是npm run serve
,针对不同项目有不同的运行指令,但既然是要执行命令的,那么就需要去使用我们封装在工具函数中的commandSpawn
方法了
这里就以vue项目为例,使用npm run serve
来运行项目并打开浏览器中的8080
端口(8080是vue项目默认打开的端口号)
打开浏览器可以用一个名为open
的库,只需要传递url
进去即可
注意,使用npm run serve
指令运行代码时,如果我们没有主动关掉它,那么这个进程是会一直存在的,自然也就不会触发childProcess.on
方法,也就等待不到resolve
函数执行了,这样的话如果我们使用了await
等待结果,那么该await语句后续的代码都不会被执行,浏览器也就不能打开了,所以我们这里不需要await进行等待
这里我们做的比较简单,直接打开了8080
端口,但其实这个端口是可能会被占用的,也就是说很有可能打开的页面并不是我们当前这个项目的,如果我们想要精准的打开这个项目所在的页面,那么可能需要对webpack
中做一些配置才行
const { promisify } = require('util')const download = promisify(require('download-git-repo'))const open = require('open')const { vueRepo } = require('../config/repo-config')const { commandSpawn } = require('../utils/terminal')const createProjectAction = async (project) => { try { console.log('dcc is help you create a vue project, please wait~'); // 1. clone项目 await download(vueRepo, project, { clone: true }) // 2. 执行npm install命令安装依赖 // const command = process.platform === 'win32' ? 'npm.cmd' : 'npm' const command = process.platform === 'win32' ? 'yarn.cmd' : 'yarn' await commandSpawn(command, ['install'], { cwd: `./${project}` }) // 3. 运行npm run serve commandSpawn(command, ['run', 'serve'], { cwd: `./${project}` }) open('http://localhost:8080/') } catch (err) { console.log(err); }}module.exports = { createProjectAction}
目前为止,我们自己的脚手架已经实现了 自动拷贝模板项目仓库中的代码 - 拷贝完之后自动安装依赖 - 自动运行项目并打开浏览器的操作
使用命令自动创建组件
首先我们要定义创建组件的命令,跟以前的方法一样,利用program.command
方法进行定义
// lic/core/create.jsprogram .command('addCpn <components>') // 定义好了创建组件的命令为dcc addCpn componentName .description('add a components into a folder') // 为该命令增加描述 .action(name => { // createComponentAction函数是用户输入对应命令之后执行的函数 createComponentAction( // 将对应的组件名称和其小写名称传递进去,因为组件所对应的文件会用得到 { name, lowerName: name.toLowerCase() }, // 创建组件时用户可能会在-d命令后面写上自己想将组件放到哪个路径下,所以我们还要从从program获取到当前路径,如果用户没有传递,则默认将组件放到src/components目录下 program._optionValues.dest || 'src/components' ) })
我们这里以小程序Taro
组件为例,一般里面都会有个index.js
文件和index.less
文件。其实创建组件的核心就是将我们提前写好的代码文件注入到用户的项目当中去,这就需要我们提前写好对应的ejs
模板
ejs模板是可以用其独特的语法<%= 变量 %>
来动态接收变量值的
这样我们就可以实现根据用户要创建的组件名称来动态的更改文件中的值了
const program = require('commander')// 提前先定义好版本号,我这里采用动态的查询package.json文件中的version字段,默认对应的命令是 -V 和 --versionprogram.version(require('./package.json').version)// 这是为了让我们输入dcc -v的时候也能被识别到program.version(require('./package.json').version, '-v')// 上面的代码只是定义了查看版本时显示什么内容,program.parse才是根据输入的命令去定义好的命令中寻找显示内容的program.parse(process.argv)0
因为我们的模板用到了ejs
语法,但其要求传入对应的变量而且要进行编译之后才能变成我们正常使用的代码。提到编译ejs文件,那肯定是离不开ejs这个库的,所以我们还需要手动安装一下ejs依赖
从ejs库中导入的对象上有一个renderFile
方法,其专门就是用来解析ejs文件的,解析完成后它会返回解析过后的代码
const program = require('commander')// 提前先定义好版本号,我这里采用动态的查询package.json文件中的version字段,默认对应的命令是 -V 和 --versionprogram.version(require('./package.json').version)// 这是为了让我们输入dcc -v的时候也能被识别到program.version(require('./package.json').version, '-v')// 上面的代码只是定义了查看版本时显示什么内容,program.parse才是根据输入的命令去定义好的命令中寻找显示内容的program.parse(process.argv)1
因为我们希望用户不仅可以将组件插入到src/components
文件夹中,其还可以自己指定将组件创建在哪一个文件夹里面去,比如在控制台输入dcc addCpn MyPrj -d source/myCpn
指令,就会在source/myCpn
这个文件夹下新建一个名为MyPrj
的文件夹,并且把index.js
和index.less
文件放进去
但是该目录可能并没有提前被用户手动所创建,我们也就无法将对应的文件放进去
所以我们需要自己封装一个函数,可以根据传入进去的路径自动帮助用户创建对应的文件夹
因为用户输入的路径可能对应多个文件夹,所以我们创建文件夹的函数必须要递归调用才行
const program = require('commander')// 提前先定义好版本号,我这里采用动态的查询package.json文件中的version字段,默认对应的命令是 -V 和 --versionprogram.version(require('./package.json').version)// 这是为了让我们输入dcc -v的时候也能被识别到program.version(require('./package.json').version, '-v')// 上面的代码只是定义了查看版本时显示什么内容,program.parse才是根据输入的命令去定义好的命令中寻找显示内容的program.parse(process.argv)2
原文:https://juejin.cn/post/7097186410880335886
向对应的文件写入解析后的模板代码
代码写入文件的操作比较简单,因为在此之前我们已经能够获得模板文件编译过后的代码了,所以只需要利用node
的核心模块fs
中的writeFile
方法即可将代码写入进去
但是writeFile
方法只能帮助我们创建一个文件,所以目标文件所在的目录必须要提前创建好才行,所以需要先执行createDirSync(dest)
函数将该文件所在的目录创建出来
const program = require('commander')// 提前先定义好版本号,我这里采用动态的查询package.json文件中的version字段,默认对应的命令是 -V 和 --versionprogram.version(require('./package.json').version)// 这是为了让我们输入dcc -v的时候也能被识别到program.version(require('./package.json').version, '-v')// 上面的代码只是定义了查看版本时显示什么内容,program.parse才是根据输入的命令去定义好的命令中寻找显示内容的program.parse(process.argv)3
这样一来,我们在命令行下敲上 dcc addCpn CpnName
的时候就会创建一个名为 CpnName
的组件到src/components
目录下,敲上 dcc addCpn CpnName -d source/myDir
命令的时候,对应的组件就会被创建到source/myDir
目录下
通过自动创建组件的这个示例,我们还可以扩展出很多指令出来,比如说自动创建页面、自动创建store文件,自动创建路由文件等等
原文:https://juejin.cn/post/7097186410880335886