前言
这几天由于项目需求,搭建了好几次开发环境,每次都得重新配置一大堆东西,想着干脆自己搭一个脚手架好了。于是乎这两天自己简单的实现了一个脚手架,主要还是要能够拓展,写个文章来复盘一下。
搭建脚手架
接下来我们一步一步的实现脚手架的功能,感受一下整个过程。搭建用到的模块我都会附上链接,并简单介绍功能。
初始化项目
首先我们新建一个文件夹,并初始化
$ npm init -y
这样项目根目录就有了package.json
文件了
创建命令文件
我们在根目录创建bin
文件夹,并添加my-cli.js文件
,内容如下:
#!/usr/bin/env node
// 把lib下的index.js作为入口
require('../lib/index')
同时,在package.json
中添加以下内容:
"bin": {
"my-cli": "bin/my-cli.js"
},
解释一下这两步,首先package.json
中的bin
内容,能够让我们在命令行调用my-cli xxx
时去执行bin文件夹下面的my-cli.js
文件。
另外#!/usr/bin/env node
这句话就是去查找PATH
中的node
,也就是电脑中安装的nodejs
来执行这个文件。
接下来我们执行下面的语句:
$ npm link
这句命令可以把我们的命令语句my-cli
绑定到全局去,然后我们就可以直接在命令行调用一下了,我们试试直接调用
$ my-cli
会发现报错了,提示找不到..lib/index
下的模块,因为我们还没创建。我们在根目录创建lib
文件夹,并添加index.js
文件,在里面输入console.log("Hello my-cli!")
。
然后我们再次执行my-cli
,发现控制台输出了"Hello my-cli!"
。
处理解析指令
刚刚我们实现了my-cli
指令,但是我们需要的是通过my-cli create xxx
这种形式的指令来快速创建项目,那么我们就需要用到commander模块来帮助我们处理指令了。
先安装模块:
$ npm i commander
然后修改lib/index.js
的内容如下:
// lib/index.js
const cmd = require('commander')
cmd
.version(`${require('../package.json').version}`, '-v --version')
.usage('<command> [options]');
cmd
.command('create <name>')
.description('Create new project')
.action(async(name) => {
//这里可以拿到传入的参数
console.log('projectName:',name);
});
cmd.parse(process.argv);
关于commander
的使用方法可以去主页详细看看,这里我们声明了脚手架的版本,以及定义了create
命令。<name>
可以解析出传入的字符,并作为参数传入action
回调中,因此我们现在调用my-cli create demo
试试,结果就会输出projectName: demo
。
除了create
指令,还可以定义许多别的自定义指令,需要自己去拓展了。
初始化项目
接下来我们需要在用户输入my-cli create xxx
的时候,在当前目录下生成项目xxx
,我们来实现一下这个功能。
为了不让index.js
变得太臃肿,我们在lib
下新建order
文件夹来处理对应的命令,同时在order
下创建create.js
:
// lib/order/create.js
const { initProjectDir } = require("../utils/create");
module.exports = async function create(projectName) {
//初始化项目目录
initProjectDir(projectName);
};
同时把lib/index.js
内容修改一下:
//lib/index.js
const cmd = require('commander');
const create = require('./order/create');
cmd
.version(`${require('../package.json').version}`, '-v --version')
.usage('<command> [options]');
cmd
.command('create <name>')
.description('Create new project')
.action(async(name) => {
//这里直接执行create命令
create(name);
});
cmd.parse(process.argv);
然后我们在lib/utils/create.js
中实现initProjectDir
:
const { getProjectPath } = require("./common");
const { exec, cd } = require("shelljs");
const { existsSync } = require('fs');
function initProjectDir(projectName) {
// 判断文件是否已经存在
const file = getProjectPath(projectName);
// // 验证文件是否已经存在,存在则退出
if (existsSync(file)) {
console.log(`${file} 已经存在`);
process.exit(1);
}
exec(`mkdir ${projectName}`);
cd(projectName);
}
module.exports = {
initProjectDir,
};
这里用到了shelljs模块,用于在命令行执行语句,安装一下:
$ npm i shelljs
还有一些共用方法,我们在lib/utils/common.js
中添加:
const { resolve } = require('path')
function getProjectPath(projectName) {
return resolve(process.cwd(), projectName);
}
module.exports = {
getProjectPath,
}
然后当我们执行my-cli create demo
时,就会在当前目录创建出demo
文件夹
处理用户交互
初始化出了项目的文件夹,接下来就是安装用户需要的功能,我们使用Inquirer来处理用户的交互,先安装一下:
$ npm i inquirer
然后我们在lib/utils/create.js
中添加需要的功能(这里不要直接复制粘贴,是相对于之前添加的内容):
const { prompt } = require('inquirer')
async function selectFeature() {
const { feature } = await prompt([{
name: 'feature',
type: 'checkbox',
message: 'Check the features needed for your project',
choices: [
{ name: 'vite', value: 'vite', checked: true },
{ name: 'typescript', value: 'typescript' },
{ name: 'babel', value: 'babel' },
],
}, ]);
return feature;
}
module.exports = {
selectFeature,
}
type
为checkbox
表示让用户多选,choices
就是用户可选的模块:
选择的结果会存入feature
中,可以用于下一步按照模块作为基准。
安装选中的模块
用户选好了需要的功能,那么接下来我们就需要把选中的功能给他安装到项目里去,这一块我们分两步走
添加包依赖
这一步需要把功能用到的npm
模块依赖添加到package.json
里面去。
我们在lib
下新建一个文件夹feature
,里面的文件负责处理对应的模块。以babel
为例,我们在feature
下新建babel.js
,添加下面的内容:
// lib/feature/babel.js
const { extendPackage} = require('../utils/common');
module.exports = function(packageJson) {
mergePackage(packageJson);
}
function mergePackage(packageJson) {
const babelConfig = {
babel: {
presets: ['@babel/preset-env'],
},
dependencies: {
'core-js': '^3.8.3',
},
devDependencies: {
'@babel/core': '^7.12.13',
'@babel/preset-env': '^7.12.13',
'babel-loader': '^8.2.2',
},
}
extendPackage(babelConfig, packageJson);
}
这里预先把需要的babel
依赖版本等写好,然后通过extendPackage
函数合并到总的package.json
里面去。
我们在lib/utils/common.js
中添加extendPackage
函数:
function extendPackage(minor, main) {
for (let key in minor) {
if (main[key] === undefined) {
main[key] = minor[key];
} else {
if (Object.prototype.toString.call(minor[key]) === '[object Object]') {
extendPackage(minor[key], main[key]);
} else {
main[key] = minor[key];
}
}
}
};
module.exports = {
extendPackage,
}
这样package.json
文件就被改写掉了,至于什么时候传入packageJson
我们等下再介绍。
添加配置文件
类似babel
的功能,用户可能会用babel.config.js
来配置文件,那么我们怎么在用户选择了babel
功能后,同时给他创建出一个基本配置好了的配置文件呢?
答案就是用模板文件,我们可以预先创建好默认的babel.config.js
文件,在用户选择了babel
功能之后,把默认的配置文件复制一份到新创建的目录下面,方便快捷。
我们在lib
下创建template
文件夹,下面对应的每一个文件夹都是一个功能的模块。我们创建babel
文件夹,然后添加babel.config.js
文件:
module.exports = {
presets: [
[
'@babel/preset-env',
{
loose: true,
targets: { node: 'current' }
}
],
],
};
接下来我们要做的就是,把模板文件按照目录结构复制到新的项目里去,我们在lib/utils/common.js
添加赋值模板文件的函数:
const { resolve } = require('path')
const { writeFile, readdir, stat, readFileSync, mkdirSync } = require('fs');
async function copyTemplate(from, to) {
stat(from, (err, stat) => {
//如果是目录,则遍历复制
if (stat.isDirectory()) {
readdir(from, (err, paths) => {
paths.forEach(path => {
//如果是文件夹,则创建
if (!/\./.test(path)) {
mkdirSync(to + "\\" + path)
}
copyTemplate(from + "\\" + path, to + "\\" + path)
})
})
} else {
//否则直接复制文件
writeFile(to, readFileSync(from), () => {});
}
})
}
function generateFiles(tempName) {
const from = resolve(__dirname, `../template/${tempName}`);
const to = process.cwd();
copyTemplate(from, to);
}
module.exports = {
generateFiles
}
到这里安装模块的准备工作就完成了,我们回到lib/utils/create.js
,添加installFeature
函数:
function installFeature(feature, projectName) {
//根据需要的feature,到文件名对应的路径下加载对应的功能模块
const featureArr = feature.map(name => require(`../feature/${name}`));
//设置默认的package.json内容
const packageJson = {
name: projectName,
version: '1.0.0',
dependencies: {},
devDependencies: {},
}
//调用对应功能的创建方法
featureArr.forEach(item => {
item(packageJson)
})
return packageJson;
}
module.exports = {
installFeature,
};
这里做了一个遍历用户选中的功能,并执行对应功能模块的创建方法的操作,而合并配置文件和复制模板文件的操作在每个功能模块自己的创建方法里:
// lib/feature/babel.js
const { extendPackage, generateFiles } = require('../utils/common');
module.exports = function(packageJson) {
mergePackage(packageJson);
generateFiles('babel');
}
function mergePackage(packageJson) {
const babelConfig = {
babel: {
presets: ['@babel/preset-env'],
},
dependencies: {
'core-js': '^3.8.3',
},
devDependencies: {
'@babel/core': '^7.12.13',
'@babel/preset-env': '^7.12.13',
'babel-loader': '^8.2.2',
},
}
extendPackage(babelConfig, packageJson);
}
然后我们回到lib/order/create.js
,执行一下installFeature
:
const { initProjectDir, selectFeature, installFeature, } = require("../utils/create");
// create 命令
module.exports = async function create(projectName) {
// 初始化项目目录
initProjectDir(projectName);
// 选择需要的功能
const feature = await selectFeature();
//安装对应的功能
const package = installFeature(feature, projectName);
}
生成package.json
刚刚通过installFeature
,我们拿到了合并完所有功能的最终的package.json
配置内容,我们写个函数写入到生成的项目中:
// lib\utils\create.js
function initPackage(package) {
writeFileSync(process.cwd() + "/package.json", JSON.stringify(package, null, 4));
}
module.exports = {
initPackage,
}
// lib\order\create.js
const { initProjectDir, selectFeature, installFeature, initPackage } = require("../utils/create");
module.exports = async function create(projectName) {
// 初始化项目目录
initProjectDir(projectName);
// 选择需要的功能
const feature = await selectFeature();
//安装对应的功能
const package = installFeature(feature, projectName);
// 写入package
initPackage(package);
}
这样就生成了package.json
文件
安装依赖
到这一步,基本上的工作都做完了,我们只需要在新项目中执行npm i
把包全都装上就好了:
// lib\utils\create.js
const { existsSync, writeFileSync } = require('fs');
function installModule() {
exec('npm i')
}
module.exports = {
installModule,
}
// lib\order\create.js
const { initProjectDir, selectFeature, installFeature, initPackage, installModule } = require("../utils/create");
module.exports = async function create(projectName) {
// 初始化项目目录
initProjectDir(projectName);
// 选择需要的功能
const feature = await selectFeature();
//安装对应的功能
const package = installFeature(feature, projectName);
// 写入package
initPackage(package);
//进入目录并安装modules
installModule();
}
拓展
如果需要添加功能的话,只需要在lib/feature
下面添加对应的功能文件,并在lib/template
下添加模板文件即可。
如果需要添加命令,可以在lib/order
下添加命令文件即可。
大功告成!
总结
本篇主要讲述了如何搭建一个可拓展的脚手架,并通过模板文件的方式来生成需要的功能。由于本人也是第一次搭建脚手架,存在很多不足的地方,比如模板文件其实还可以设置成动态模板的形式,根据用户的输入改变模板的内容。但是总的来说这些都可以在这个脚手架上进行拓展和改进,如果有什么更好的建议也欢迎大家提出,一定虚心改正!
写在最后
- 以上操作源码已放在github.com/AaronY666/m… ,并按照步骤有
commit
记录 - 很感谢你能看到这里,不妨点个赞支持一下,万分感激~!
- 以后会更新更多文章和知识点,感兴趣的话可以关注一波~
参考文章:
- 搭建自己的 typescript 项目 + 开发自己的脚手架工具 ts-cli
- 手把手教你写一个脚手架
本文正在参与「掘金 2021 春招闯关活动」, 点击查看活动详情
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!