从0开发一个node脚手架

背景

每次创建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

github.com/evanw/esbui...

我们就需要将他放在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工具。其中许多操作都比较粗暴简单,有很多提升空间,之后慢慢迭代。

相关推荐
昨天今天明天好多天2 小时前
【Node.js]
前端·node.js
熊的猫2 小时前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
爱编程的鱼5 小时前
Node.js事件循环:解锁异步编程的奥秘
node.js
南暮思鸢5 小时前
Node.js is Web Scale
经验分享·web安全·网络安全·node.js·ctf题目·hackergame 2024
程序员小杰@5 小时前
Playwright 快速入门:Playwright 是一个用于浏览器自动化测试的 Node.js 库
node.js
Martin -Tang6 小时前
vite和webpack的区别
前端·webpack·node.js·vite
王解7 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
ldq_sd17 小时前
node.js安装和配置教程
node.js
我真的很困18 小时前
坤坤带你学浏览器缓存
前端·http·node.js
whyfail21 小时前
ESM 与 CommonJS:JavaScript 模块化的两大主流方式
javascript·node.js