背景
每次创建node项目时,都要重新配置例如ts、eslint等内容,开发一个脚手架可以省略掉每次的配置,大大方便开发。
我希望脚手架创建的项目有内置以下功能
-
使用eslint进行检查和代码格式化
-
使用typescript
-
可以支持打包
-
....
上手开发
node项目初始化
首先初始化我们的文件夹
Shell
$ mkdir create-app-node
$ cd create-app-node
$ npm init
启用typescript
Shell
$ yarn add typescript --dev
$ tsc --init
这时项目中会多出一个tsconfig.json,根据需要配置
TypeScript
{
"compilerOptions": {
"target": "ESNext",
"module": "Node16",
"moduleResolution": "node",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": [".eslintrc.js", "src/**/*"],
"exclude": ["node_modules", "dist/**/*"]
}
注意这里的module指定为了node16,是为了强制文件引用带上.js后缀,不然编译后的代码会因没有后缀名而出错,具体可以参考
stackoverflow.com/questions/6...
官方文档
www.typescriptlang.org/tsconfig#mo...
启用eslint
Shell
npm install eslint -save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
然后在项目中新建一个.eslintrc.js配置文件,根据需要修改
PlainText
module.exports = {
ignorePatterns: ['**/config/*.js','**/dist/*'],
rules: {
quotes: [1, 'single'],
'require-jsdoc': 0,
'jsdoc/require-returns-description': 0,
'jsdoc/require-param-description': 0,
'tsdoc/syntax': 0,
'no-console': 0,
'eslint-comments/no-unused-disable': 0,
'eslint-comments/disable-enable-pair': 0,
'no-empty-function': 0,
'no-unused-vars': 0,
},
};
在package.json中新添加lint命令
Shell
"scripts": {
"lint": "eslint --ext .js .",
"lint:fix": "eslint --fix --ext .js ."
},
测试一下,添加成功
添加打包工具
打包工具我们选择esbuild,速度很快,并且使用起来非常方便。关于esbuild可以阅读 ByteTech-Esbuild 架构和应用
首先安装esbuild,因为我们使用了typescript所以也需要安装esbuild-plugin-tsc插件
Shell
yarn add esbuild esbuild-plugin-tsc
新建config文件夹,用于存放打包配置
构建
在config中新建一个build.js,用来存储构建脚本。具体的配置可以参考esbuild官方文档
JavaScript
const esbuild = require('esbuild');
const esbuildPluginTsc = require('esbuild-plugin-tsc');
const process = require('child_process');
const settings = {
entryPoints: ['src/index.ts'],
outfile: 'dist/index.js',
bundle: true,
// 注意这里
external: ['shelljs'],
platform: 'node',
plugins: [
esbuildPluginTsc({
force: true,
}),
],
minify: true,
};
process.exec('rm -rf dist');
esbuild.build(settings).then((res) => {
console.log('打包成功', res);
});
在package.json中添加命令
JSON
"scripts": {
"build": "node config/build.js",
},
开发
新建一个dev.js,用来存储开发脚本。dev脚本和build脚本的区别是dev脚本需要监听文件变化并重新编译
esbuild内置live-reload,调用ctx.watch即可。
JavaScript
const ctx = await esbuild.context(settings);
ctx.watch(() => {
console.log("Listening file change");
});
同时我们写一个onBuildTip插件,用来监听编译的开始和结束事件,给用户提示。dev的脚本如下
JavaScript
const onBuildTip = {
name: "onBuildTip",
setup(build) {
build.onStart((args) => {
console.log("Triggered, start building...");
});
build.onEnd((args) => {
console.log("Building Done\n");
});
},
};
注意在dev时,我们还需要执行一下npm link命令,好让本地能访问到命令进行调试
JavaScript
process.exec('npm link',(err,stdout)=>{
if(err){
console.log('npm link error')
console.log(err)
}else{
console.log('npm link success')
console.log(stdout)
}
})
最后的dev脚本如下
JavaScript
const esbuild = require('esbuild');
const esbuildPluginTsc = require('esbuild-plugin-tsc');
const process = require('child_process');
const onBuildTip = {
name: 'onBuildTip',
setup(build) {
build.onStart(() => {
console.log('Triggered, start building...');
});
build.onEnd(() => {
console.log('Building Done\n');
});
},
};
const settings = {
entryPoints: ['src/index.ts'],
outfile: 'dist/index.js',
bundle: true,
external: ['shelljs'],
platform: 'node',
plugins: [
esbuildPluginTsc({
force: true,
}),
onBuildTip,
],
sourcemap: true,
};
const dev = async () => {
process.exec('rm -rf dist');
const ctx = await esbuild.context(settings);
ctx.watch();
process.exec('npm link', (err, stdout) => {
if (err) {
console.log('npm link error');
console.log(err);
} else {
console.log('npm link success');
console.log(stdout);
}
});
};
dev();
最后在package.json中添加命令,和build一样
JSON
"scripts": {
"dev": "node config/dev.js",
},
测试一下,成功
不同module与不兼容库的处理
要为node项目打包的一个很重要的原因就是为了处理commonjs与es6 module不能混用的问题。我们通常在package.json中来指定type为module/commonjs来区分是commonjs模块还是es6模块。
但是在使用中,两种不同类型的包不能混用,require不能加载es6模块,es6中也必须使用import来加载模块,所以有时候就会出现想要使用一个三方库结果发现module类型不兼容而无法使用
TypeScript
// 试图在es6项目中引用commonjs库
ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/Users/bytedance/Desktop/create-app-node/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
而经过esbuild打包,将他们都转为commonjs打包到一起,就可以解决这个问题。两种module的包就都可以用了
但同时,也有一些库用到了不能够被打包进来,例如shelljs
我们就需要将他放在externals字段中,让esbuild不要将其打包进来,可以解决这个问题,正如上文标黄处所示。在使用中碰到一些类似的库也需要这样处理
脚手架开发
命令行交互
在node脚手架中,经常用到两个库用来处理与用户的交互
-
command 用来解析用户的命令行指令中的参数、options等等,同时内置--help和--version等常用功能
-
inquirer 用来与用户在终端中交互,例如输入文本、多选、单选等内容
这里出于使用简便考虑,只需要获取新的项目名、作者、git仓库等基本信息,使用@inquirer/prompts获取输入即可
TypeScript
// 项目名
const projectName = await input({ message: 'Enter your project name', default: 'myNodeProject' });
// git 仓库
const author = await input({ message: 'Enter author (Optional)' });
// git 仓库
const gitRepo = await input({ message: 'Enter gitRepo (Optional)' });
效果如下
克隆模版并修改配置
下一步就要把项目模版拉到本地,并根据用户事前输入的信息修改项目配置。项目模版使用之前初始化好的内容,单独切一个tmplate分支作为模版即可。克隆直接使用shelljs执行命令行,搭配内置的fs模块修改即可,简单粗暴。需要改以下信息
-
重命名文件夹名为项目名
-
清除.git文件夹并重新init git信息绑定到用户输入的git仓库上
-
重写package.json中的信息
按步骤修改即可,修改好之后安装依赖,项目就搭建好了
TypeScript
/* 克隆模版 */
shelljs.exec('git clone --branch template https://github.com/ichimaru-ght/create-app-node.git', {
async: false,
});
/* 修改配置 */
shelljs.cd('create-app-node');
// 清除 .git
shelljs.exec('rm -rf .git');
shelljs.exec('git init');
if (gitRepo) {
console.log('绑定远程仓库');
shelljs.exec(`git remote add origin ${gitRepo}`);
}
shelljs.cd('..');
// 给文件夹重命名
fs.renameSync('create-app-node', name);
.....
shelljs.exec('yarn');
美化一下
输出的文本都使用console.log,打印出来的文本都是默认颜色的,比较单调,使用colors给可以 console的打印加上颜色,更美观好认
TypeScript
import colors from '@colors/colors/safe';
console.log(colors.green('开始安装依赖'));
然后可以使用ora加一个loading效果
TypeScript
import ora from 'ora';
const spinner = ora('Initializing Project..');
spinner.start();
.....
spinner.stop();
总结
本文尽可能简单的实现了一个node脚手架,能够创建一个本地node工具。其中许多操作都比较粗暴简单,有很多提升空间,之后慢慢迭代。