写在前面
本文比较基操,主要是有一个流程概念。第二弹已出:传送门
效果栗子
脚手架概念
所谓的脚手架,在我看来,就是一个集成项目初始化、调试、构建、测试、部署等等流程,能够让使用者专注于code
的工具。用白话说就是,一个建筑已经搭好架子,我们只需要不断加入砖头就行。
一个完整的脚手架一般包含三个方面的内容:
- 脚手架命令脚本:我们所需要安装到全局的脚手架,通过它可以方便的开始一个项目的开发。(也是本文讲解目标)
scripts
包:一般我们会将打包、编译、测试以及读取自定义配置文件等等操作(例如webpack
相关配置操作,本地服务器相关内容等等),单独做成npm
包。让使用者不必关心这些操作,专心code
。- 模板文件:显而易见,就是我们初始化项目的时候,所拉取的项目内容。
常见的脚手架栗子:
create-react-app
vue-cli
- ...
新建项目
$ mkdir project && cd project
$ npm init -y
在package.json
文件中,加入bin
字段
{
//...
"bin": {
"hello": "./index.js"
},
//...
}
bin
的作用就是官网是这样解释的:
许多
npm
包都具有一个或多个要安装到PATH
中的可执行文件。package.json
中提供一个字段bin
,该字段是命令名到本地文件名的映射。在安装时,npm
会将文件符号链接到prefix/bin
以进行全局安装或./node_modules/.bin/
本地安装。
说白了,就是在安装的时候,会创建一个快捷方式
,通过快捷方式
能够很方便的使用对应的node
脚本命令。这也是为什么我们可以直接随便打开命令行,通过cra
方式创建项目。
而对应的index.js
文件,在开头必须以#!/usr/bin/env node
。usr/bin/env
表示可以去PATH
目录中查找脚本解释器,同时指定使用node
去执行该文件。
#!/usr/bin/env node
console.log('hello world!');
然后再通过npm link
,在全局中创建符号链接,将package.json
里的bin
字段内容进行映射链接。
$ npm link
那么在任何地方都可以直接使用hello
命令。
命令行操作
这边,我是使用commander.js
去读取命令。
命令
const program = require('commander');
program
.command('create <name>')
.description('请输入项目名称')
.action(name => {
console.log(`你要创建的项目名称:${name}`);
});
program.parse(process.argv);
.command()
的第一个参数可以配置命令名称及参数,参数支持必选(尖括号表示)、可选(方括号表示)及变长参数(点号表示,如果使用,只能是最后一个参数)。
版本提示
常见的像是create-react-app -V
,一般就是读取自身的package.json
中的version
字段,然后使用program.version
。
const program = require('commander');
const packageJson = require('./package.json');
program.version(packageJson.version);
help信息
help
是commander.js
基于代码自动生成的,默认的帮助选项是-h
,--help
。
那么就简单的介绍完一些常规操作了,更多操作可以去官方文档上看,这里就不过多阐述了。
交互问题
很多脚手架在使用的时候,会和用户进行一些交互操作。这个可以通过inquirer.js
去使用。
//...
const answer = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: '请输入项目名称',
},
]);
提示反馈
在命令行中执行脚本命令后,一般会有一些比较友好的提示,例如:loading
,success
,error
等等。这个可以使用ora
const ora = require('ora');
const loading = ora('Loading unicorns');
loading.text = '疯狂加载中';
loading.color = 'green';
loading.start();
还有例如colors
,chalk
等等,就不一一阐述了。
拉取模板文件
脚手架最重要的一部就是根据用户的输入,去拉取不同的模板文件。比如我输入语言选择typescript
,那么拉取的模板文件自然是支持typescript
的,这块是使用download-git-repo
。
download-git-repo
支持三大平台下载:
GitHub
-github:owner/name
orowner/name
GitLab
-gitlab:owner/name
Bitbucket
-bitbucket:owner/name
同时他还支持通过#
去拉取不同分支上的代码,当然默认是master
。那么我们这里其实就可以直接通过用户的输入,去拉取不同branch
上的代码,去实现拉取不同的模板文件。(或者就建立多个仓库,拉不同仓库代码)。
const download = require('download-git-repo');
const downloadAdress = lang => `owner/name#${LANG_LIST[lang]}`;
program
.command('create')
.description('初始化项目')
.action(async () => {
const answer = await inputer.prompt([
//...
{
type: 'list',
message: '使用哪种语言进行开发',
name: 'lang',
choices: ['typescript', 'javascript'],
},
]);
download(
downloadAdress(answer.lang),
`./${answer.name}`, // path
downloadCallback.bind(null, answer), // callback
);
});
修改package.json
因为拉取的模板文件,package.json
都是固定写好的。而大多数脚手架都是在初始化的时候,根据用户输入,来替换package.json
中的相应字段。
这个就是一个文件读写操作,通过readFileSync
去读文件内容,再替换相应字段,再重新写入。
const filename = `${answer.name}/package.json`;
if (fs.existsSync(filename)) {
let newPagJson = fs.readFileSync(filename).toString();
newPagJson = JSON.parse(newPagJson);
newPagJson = {...newPagJson, ...answer};
newPagJson = JSON.stringify(newPagJson, null, '\t');
fs.writeFileSync(filename, newPagJson);
//...
}
利用stringify
的其他参数,将newPagJson
格式化,使其重新写入也是格式正常的。
发布
在开发完成后,我们一般都会选择发布到npm
平台上。
发布流程也很简单:
npm login
npm publish
,记得更新version
删除发布
因为每次更新发布,都会在npm
的历史里留存,如果想删除某次或者整个npm
包,可以使用npm unpublish
。
使用也很简单
$ npm unpublish [<@scope>/]<pkg>@<version>
$ npm unpublish [<@scope>/]<pkg> --force
前者表示删除某个版本npm
包,只是删除后,该version
再也不能使用了,也不能重新发布该version
。
后者表示删除整个npm
包,删除后需要24
小时后,才能重新发布。
检查更新
脚手架的发布后,可能有些用户没有手动更新,如果没有什么提示功能,可能一直不会选择更新。那么我们可以在脚手架使用的时候,去判断最新版本与当前版本是否一致,并决定是否提示更新。
因为拉取版本是需要花费时间的,所以一般是间隔性的判断。
这里我是保存上次拉取时间,通过当前时间戳和上次拉取时间戳之差,判断本次是否需要判断。
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const resolve = _path => path.join(__dirname, _path);
const timePath = resolve('index.txt');
const MAX_TIME = 86400000;
const checkTime = () => {
const lastTime = +fs.readFileSync(timePath).toString();
const nowTime = new Date().getTime();
if (lastTime && nowTime - lastTime <= MAX_TIME) {
return;
}
fs.writeFileSync(timePath, nowTime);
const lastV = execSync('npm view yourNpmPackage version', { encoding: 'utf8' });
return lastV;
};
这块涉及的其实就是node
基本api
操作。如果对node
不熟悉可以多看看node
文档,因为脚手架的开发,和node
息息相关的。
以上的内容,其实还是属于基操的。
我也是在这周空闲的时候有想法,决定自己做一个脚手架,一边复习一边学习。之后也会尽量完善它,之后如果有新的内容,也会分享出来。