自定义脚手架
前言
继上一次搞了一个基于webpack4+react的多页面移动端应用程序,我发现下一次需要使用这个模板文件的时候比较麻烦,首先需要去github下载项目模板下来,然后改项目名称,描述,作者等等这些信息,而且这个模板的css预处理器是node-sass,万一我想用less,就要删掉node-sass,sass-loader,然后添加less,less-loader。这些操作是在有点麻烦,所以我就想着模仿create-react-app和vue-cli这些脚手架,通过命令行快速生成一个符合我要求的项目模板和环境。
需求分析
首先分析一下自定义脚手架需要做些什么事请吧。我先把自己做的这个自定义脚手架命名为lin-cli吧,然后命令就叫做 lin
-
这个脚手架由于是自定义的,不像react和vue那些脚手架那样有这么多功能,满足需求即可,所以指令那些并不多,只需要3个;分别如下:
- lin create projectName 类似于vue create,该指令是在当前打开黑窗口的目录下创建一个名为[projectName]的文件夹,并把项目模板那些生成放在里面。
- lin -v | --version 查看当前脚手架的版本号
- lin --help 查看脚手架有什么指令
-
脚手架可以跟用户进行交互,询问项目名称,描述,作者等等信息,并将这些信息填写进package.json文件中
-
脚手架可以让用户选择是否使用css预处理器。如果选择是,则提供less和sass这2个选项给用户选择
-
脚手架在生成项目模板的时候,需要给出适应的提示,比如下载模板的时候就提示正在下载模板,下载完成就提示下载完成
模板文件–webpack配置修改
之前webpack配置是基于node-sass配置的,但是现在需要把它变成通用的,所以这里需要把less也配置上去,修改如下
- 在build/webpack.config.dev.js文件rules下添加以下代码
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'postcss-loader',
'less-loader'
],
include: srcRoot
}
- 在build/webpack.config.build.js文件rules下添加以下代码
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'postcss-loader',
'less-loader'
],
include: srcRoot
}
模板文件–package.json修改
这里需要说明的是模板文件的package.json有些字段是需要根据用户的输入去进行填充的,所以要填充的地方要用占位符,到时候利用脚手架替换掉这些占位符,生成一份新的package.json并回写回去。这里使用handlebars这个工具来处理模板的,所以占位符使用的是handlebars的格式规范
{
"name": "{{name}}",
"version": "1.0.0",
"description": "{{description}}",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.config.dev.js",
"build": "webpack --config ./build/webpack.config.build.js"
},
"keywords": [],
"author": "{{author}}",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.8.7",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.7",
"@babel/preset-react": "^7.8.3",
"autoprefixer": "^9.7.4",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
"file-loader": "^5.1.0",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"postcss-loader": "^3.0.0",
"react-hot-loader": "^4.12.19",
{{#if sass}}
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2",
{{/if}}
{{#if less}}
"less": "^3.0.4",
"less-loader": "^4.1.0",
{{/if}}
"style-loader": "^1.1.3",
"url-loader": "^3.0.0",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-merge": "^4.2.2"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.8.7",
"axios": "^0.19.2",
"core-js": "^3.6.4",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-loadable": "^5.5.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"regenerator-runtime": "^0.13.3"
}
}
这里可以看见,需要根据用户的输入填充的字段有name、description、author,以及依赖包node-sass,sass-loader,less,less-loader。这里根据用户的输入选择性把less和sass的依赖填写上去,而不是直接把less和node-sass的依赖全部填写上去,因为这样会产生无用的依赖包,下载的时候会花费更多的时间。
脚手架–package.json配置
package.json文件有几个字段需要注意。
- name:该字段是用来发布npm包使用的,说明了npm包的名称,也就是publish后可以在npmjs中通过该名称搜索到。
- version:该字段说明版本号
- main:该字段是指程序的主入口。
- bin:该字段是指定你的脚手架在命令行运行的指令,我这里的bin字段配置成"lin": “index.js”,意思是我在命令行中可以使用lin这个指令来调用我得自定义脚手架,自定义脚手架执行的脚本文件就是index.js。比如:lin create testdemo,就是执行index.js文件,并把create testdemo这2个参数传递给index.js。
{
"name": "lin-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"lin": "index.js"
},
"dependencies": {
"chalk": "^2.4.2",
"commander": "^2.20.0",
"download-git-repo": "^2.0.0",
"handlebars": "^4.1.2",
"inquirer": "^6.5.0",
"ora": "^3.4.0"
}
}
脚手架–index.js脚本文件
生成模板文件总的思路就是
- 预先设定好一些问题
- 当用户输入lin create testdemo 并敲下回车键的时候显示预先设定好的问题
- 当用户输入回答并敲下回车键的时候保存用户的输入
- 根据用户的输入显示下一个需要显示的问题
- 当用户回答完预设的问题后,下载模板文件
- 根据用户输入的信息去修改模板文件
- 以上步骤完成后提示用户模板文件生成完成
这里我就直接上代码,然后在代码里面解释,这样更加直观方便
#!/usr/bin/env node
// 使用node开发命令行工具所执行的js脚本必须在顶部加入 #!/usr/bin/env node 生命
// #!/usr/bin/env node 告诉系统该脚本使用node运行,用户必须在系统变量中配置了node
const program = require('commander');
// 下载github仓库
const download = require('download-git-repo');
// 命令行交互
const inquirer = require('inquirer');
// 处理模板
const handlebars = require('handlebars');
// loading效果
const ora = require('ora');
// 给字体增加颜色
const chalk = require('chalk');
const fs = require('fs');
// 模板下载地址
const downloadUrl = 'direct:https://github.com/c10342/react-multipage-template.git';
// 设置脚手架版本
program
.version('0.1.1', '-v, --version');
// 跟用户进行交互
program
// lin create testdemo
.command('create <project>')
// 添加描述
.description('初始化项目模板')
.action(function (projectName) {
// 与用户交互
inquirer
.prompt([
// 输入项目名称
{
type: 'input',
name: 'name',
message: '请输入项目名称:'
},
// 输入项目描述
{
type: 'input',
name: 'description',
message: '请输入项目描述:'
},
// 输入项目作者
{
type: 'input',
name: 'author',
message: '请输入项目作者:'
},
// 选择是否使用node-sass或者less
{
type: 'confirm',
name: 'cssStyle',
message: '是否使用less/node-sass:'
},
// 选择使用node-sass或者less
{
type: 'list',
message: '请选择以下css预处理器:',
name: 'preprocessor',
choices: [
"less",
"sass"
],
// 只有当用户在选择是否使用node-sass或者less时输入了yes才会显示该问题
when: function (answers) {
return answers.cssStyle
}
}
])
.then(answers => {
// 与用户交互完成后,处理用户的选择
let params = {
name: answers.name, // 项目名称
description: answers.description,// 项目描述
author: answers.author // 项目作者
}
if (answers.cssStyle) {
//用户选择使用css预处理器
if (answers.preprocessor === 'less') {
// 用户选择使用less
params.less = true;
params.sass = false;
} else if (answers.preprocessor === 'sass') {
// 用户选择使用sass
params.less = false;
params.sass = true;
}
} else {
// 用户选择不使用css预处理器
params.sass = false;
params.less = false
}
// 打印空行,使输出与输出之间有空行,增加体验效果
console.log("");
// 提示用户正在下载模板,并显示loading图标
var spinner = ora('正在下载中...').start();
// 下载模板到本地
download(downloadUrl, projectName, { clone: true }, err => {
if (err) {
console.log(err);
spinner.text = '下载失败';
spinner.fail() //下载失败
} else {
// 获取模板的package.json的路径
let packagePath = `${projectName}/package.json`;
// 读取模板的package.json文件的内容
let packageStr = fs.readFileSync(packagePath, 'utf-8');
// 根据params参数替换掉模板的package.json文件内容的占位符
let package = handlebars.compile(packageStr)(params);
// 重新写入文件
fs.writeFileSync(packagePath, package);
if (params.sass) {
// 由于国内网络原因,node-sass可能需要翻墙才能下载,所以如果用户选择了sass预处理器则需要创建.npmrc文件,并写入node-sass的代理下载地址
const npmrcPath = `${projectName}/.npmrc`;
const appendContent = '\r\nsass_binary_site=https://npm.taobao.org/mirrors/node-sass/'
if (!fs.existsSync(npmrcPath)) {
fs.writeFileSync(npmrcPath, appendContent)
} else {
fs.appendFileSync(npmrcPath, appendContent)
}
}
// 提示用户下载成功
spinner.text = '下载成功';
spinner.color = '#13A10E';
spinner.succeed();
console.log("");
// 提示进入下载的目录
console.log(" # cd into Project");
console.log(chalk.gray(' $ ') + chalk.blue(`cd ${projectName}`));
console.log("");
// 提示安装依赖
console.log(" # Project setup");
console.log(chalk.gray(' $ ') + chalk.blue(`npm install`));
console.log("");
// 提示运行开发环境
console.log(" # Compiles and hot-reloads for development");
console.log(chalk.gray(' $ ') + chalk.blue(`npm run dev`));
console.log("");
// 提示打包生产环境代码
console.log(" # Compiles and minifies for production");
console.log(chalk.gray(' $ ') + chalk.blue(`npm run build`));
console.log("")
}
})
});
});
// 解析命令行参数
program.parse(process.argv);
发布npm包
- 注册一个npm账号,如果没有可以到 https://www.npmjs.com 注册。
- 检查你的镜像源,命令行输入 npm config get registry。如果是淘宝镜像源需要切换回npm的镜像源,否则会导致登陆失败报错。
- 切换镜像源,命令行输入 npm config set registry https://registry.npmjs.org/。如果镜像源已经是npm地址,则跳过这一步。
- 登录账号,命令行输入 npm login。然后根据提示填写你的用户名,密码,邮箱
- 发布,在命令行中输入 npm publish。
- 使用,成功发布后可以再npm上面搜索并安装使用。
效果图
最后再附上几张效果图
总结
其实,自定义脚手架还是挺好实现的,总的思路就是预设问题,收集用户的输入,然后下载模板,最后根据用户的输入,修改模板内容。难点并不多,就是需要注意一些小细节,还有就是commander、inquirer、handlebars这些依赖包的用法。
附上地址
模板文件源码地址:https://github.com/c10342/react-multipage-template
脚手架源码地址:https://github.com/c10342/lin-cli
脚手架npm地址:https://www.npmjs.com/package/lin-cli