从零开始搭建脚手架
- 代码仓库: github.com/panmin-code…
- 代码在 dev 分支上
简介
-
脚手架是一个集成项目初始化、调试、构建、测试、部署等等流程,能够让使用者专注于 code 的工具。用白话说就是,一个建筑已经搭好架子,我们只需要不断加入砖头就行。
-
提高效率,在统一的基础上,提供更多提高效率的工具,这个效率不只是开发效率,是从开发到上线的全流程的效率,比如一些业务组件的封装,提高上线效率的发布系统,各种 utils 等。
-
一个完整的脚手架一般包含三个方面的内容:
- 脚手架命令脚本:我们所需要安装到全局的脚手架,通过它可以方便的开始一个项目的开发
- scripts 包:一般我们会将打包、编译、测试以及读取自定义配置文件等等操作(例如 webpack 相关配置操作,本地服务器相关内容等等),单独做成 npm 包。让使用者不必关心这些操作,专心 code。
- 模板文件:显而易见,就是我们初始化项目的时候,所拉取的项目内容。
项目初始化
-
使用
npm init初始化项目 -
使用
git init将项目添加到 git 管理 -
创建远程 git 仓库并与之关联
-
使用 eslint 做代码检查:
-
安装:
npm install eslint @babel/eslint-parser -D -
配置
.eslintrc.json{ "env": { "node": true, "commonjs": true }, "parser": "@babel/eslint-parser", "parserOptions": { "ecmaVersion": 2015, "requireConfigFile": false }, "extends": ["eslint:recommended", "prettier"], "rules": { "semi": ["error", "always"], "quotes": "off", "no-console": "off", "no-redeclare": "warn" } } -
配置
.eslintignore#井号是注释 根据自己的项目需要进行忽略 # 如果 .eslintrc 开启了 env nodejs 那么 默认 node_modules是自动忽略的 node_modules /node_modules/**
-
-
在
package.json文件中,加入 bin 字段// package.json "bin": { "pri": "./bin/pri.js" }sspri是 cli 的名称,类似 npm 的 npm 或 npx 命令./bin/pri.js是指运行 pri 命令是执行的是 bin 目录下的pri.js文件
-
使用 prettier 进行代码格式化
-
安装:
npm install prettier eslint-config-prettier -D -
配置
.prettierrc{ "singleQuote": true, "semi": true, "tabWidth": 2 }
-
-
在根目录下 创建
bin文件夹,添加pri.js#!/usr/bin/env node const package = require("../package.json"); const { version } = package; console.log(version);-
打开终端,执行
npm link, 然后运行 pri 打印版本号 1.0.0 即表示项目初始化成功
-
初认识 commander
-
commander:TJ 大神开发的工具,能够更好地组织和处理命令行的输入。
- 中文文档
- 是完整的 node.js 命令行解决方案。
- 安装:
npm install commander
-
通过
pri命令输出pri-cli的 version:-
修改
pri.js#!/usr/bin/env node const { Command } = require("commander"); const package = require("../package.json"); // 获取package.version const { version } = package; const program = new Command(); // 定义当前版本 program.version( `pri: ${version}`, "-v, --version", "output the current pri version" ); // 解析运行参数(必须且要放在最后一行) program.parse(process.argv); -
运行
pri -v或pri --version, 将在控制台打印pri: 1.0.0
-
-
自动化帮助信息:帮助信息是 Commander 基于你的程序自动生成的,默认的帮助选项是
-h,--help。运行pri -h或pri --help, 将在控制台看到下面的帮助信息。
创建项目
注册 create 命令
-
我们需要实现一个能通过命令行创建项目的
create命令 -
在
pri.js中添加下面的代码:// 定义create命令 program .command("create <app-name>") .description("Create a new pri project.") .alias("c") .action((name) => { console.log(`project -> `, name); }); -
在控制台执行
pri create my-app可以看到下面的结果 -
控制台出现上面的结果表示 create 命令注册成功了,但实际中 create 命令后可能不止有一个参数,同时我们还希望控制台的输出能更加美观,这需要用到下面两个库。
-
安装
chalk和minimist:npm install chalk@4.1.0 minimist。注意 chalk v5 是 es 模块的。 -
在 pri.js 的 create action 命令中添加下面的代码:
.alias('c') .action((name) => { if (minimist(process.argv.slice(3))._.length > 1) { const info = `Info: You provided more than one argument. The first one will be used as the app's name, the rest are ignored. `; log(chalk.yellow(info)); } }); -
在控制台输入
pri create my-app test,可以看到下面的输出结果
创建项目的根目录
-
创建项目的根目录有以下几种情况
- 项目名是否为
.,为.则获取当前目录名作为项目名,并将当前目录作为项目根目录。 - 项目名是否符合 npm 包的命名规则,不符则提示错误,并且退出项目的创建
- 是否通过
pri create app-name -f或者pri create app-name --force指定了强制创建项目,如果指定了-f或者--force则先删除目标目录下的内容然后再创造项目。 - 目标目录是否为当前目录,如果是则先询问是否在当前目录创建项目,是则创建,否则退出创建
- 目标目录是否是已存在的非当前目录,如果是则询问是否覆盖、合并或取消,然后根据选择创建目录。
- 如果目标目录不存在,则创建目标目录并生成文件。
- 项目名是否为
-
根据上面的描述可以知道需要三个 库来帮助我们创建目录
- 和检查项目名是否符合 npm 规范的库:validate-npm-package-name
- terminal 和用户交互的库:inquirer
- 删除目录的库: fs-extra
-
修改
bin/pri.js中 create 命令部分的代码为:program .command("create <app-name>") .description("Create a new pri project.") .option("-f, --force", "Overwrite target directory if it exists") .alias("c") .action((name, options) => { if (minimist(process.argv.slice(3))._.length > 1) { const info = `Info: You provided more than one argument. The first one will be used as the app's name, the rest are ignored. `; log(chalk.yellow(info)); } create(name, options); }); -
增加
lib/error.jsconst { logErrors } = require("./logger"); function catchErrorHof(fn) { return (...args) => { try { return fn(...args); } catch (error) { logErrors([error.message]); } }; } function asyncCatchErrorHof(fn) { return async (...args) => { try { return await fn(...args); } catch (error) { logErrors([error.message]); } }; } module.exports = { asyncCatchErrorHof, catchErrorHof }; -
增加
lib/create.js"use strict"; const chalk = require("chalk"); const readline = require("readline"); const dim = { error: "❌", warn: "⚠️", }; /** * @param {string[]|undefined} infos * @param {string} dim */ function logInfos(infos, dim) { if (!infos) return; infos.forEach((msg) => { const str = dim ? chalk.cyan.dim(dim, msg) : chalk.cyan(msg); console.log(str); }); } /** * @param {string[]|undefined} warnings * @param {string} dim */ function logWarnings(warnings, dim) { if (!warnings) return; warnings.forEach((msg) => { const str = dim ? chalk.yellow.dim(dim, msg) : chalk.yellow(msg); console.warn(str); }); } /** * @param {string[]|undefined} errors * @param {string} dim */ function logErrors(errors, dim) { if (!errors) return; errors.forEach((msg) => { const str = dim ? chalk.red.dim(dim, msg) : chalk.red(msg); console.error(str); }); } /** * @param {string|undefined} msg */ function clearConsole(msg) { const blank = "\n".repeat(process.stdout.rows); console.log(blank); readline.cursorTo(process.stdout, 0, 0); readline.clearScreenDown(process.stdout); if (msg) { logInfos([msg]); } } module.exports = { clearConsole, dim, logInfos, logErrors, logWarnings, }; -
增加
lib/logger.js"use strict"; const fs = require("fs-extra"); const path = require("path"); const chalk = require("chalk"); const { prompt } = require("inquirer"); const validateNpmPackageName = require("validate-npm-package-name"); const { asyncCatchErrorHof } = require("./error"); const { clearConsole, dim, logInfos, logWarnings, logErrors, } = require("./logger"); /** * @description 创建pri 项目 * @param {string} projectName * @param {{[p:string]:string}} options */ async function create(projectName, options) { const cwd = options.cwd || process.cwd(); // process.cwd(): 返回是当前执行node命令时候的文件夹地址 const inCurrent = projectName === "."; // 如果项目名称为 '.' 表示要在当前目录下直接创建项目 const name = inCurrent ? path.relative("../", projectName) : projectName; const targetDir = path.resolve(cwd, projectName); // 获取创建项目的地址 const result = validateNpmPackageName(name); // 检查项目名称是否符合npm 包的命名规范 // 不符合npm 包的命名规范 if (!result.validForNewPackages) { return handleInvalidName(result, name); } // 当要存在和要创建的项目相同的文件夹时 if (fs.existsSync(targetDir)) { const isCreate = await createInExistTargetDir(targetDir, { ...options, inCurrent, }); if (!isCreate) return; } console.log(`Creating project: ${name}`); } function handleInvalidName(result, name) { logErrors([`Invalid project name ${name}`]); logErrors(result.errors, dim.error); logWarnings(result.warnings, dim.warn); process.exit(1); } /** * @description 处理文件夹存在的情况 * @param {string} targetDir * @param {{[p:string]:string}} options * @returns {Promise<boolean>} 如果是false表示不在已存在的目录中创建项目 */ async function createInExistTargetDir(targetDir, options) { const { force, inCurrent } = options; // 强制创建 if (force && !inCurrent) { await fs.remove(targetDir); //清除当前文件和文件夹 return true; } // 不是强制创建 // 在当前文件夹下创建 clearConsole(); if (inCurrent) { return createInCurrentDir(); } // 不是在当前文件夹下创建 return createInSubDir(targetDir); } /** * @returns {Promise<boolean>} */ async function createInCurrentDir() { const { ok } = await prompt([ { name: "ok", type: "confirm", message: chalk.cyan("Create project in current directory?"), }, ]); return ok; } const actionEnum = { Overwrite: 2, Merge: 1, Cancel: 0, }; async function createInSubDir(targetDir) { const { action } = await prompt([ { name: "action", type: "list", message: chalk.cyan( `Target directory ${targetDir} exists. Choose an action` ), choices: [ { name: "Overwrite", value: 2 }, { name: "Merge", value: 1 }, { name: "Cancel", value: 0 }, ], }, ]); if (!action || action === actionEnum.Cancel) return false; if (action === actionEnum.Overwrite) { logInfos([`Removing ${targetDir}`]); await fs.remove(targetDir); } return true; } module.exports = { create: asyncCatchErrorHof(create), }; -
简单的进行测试,在终端执行
pri create <app-name>可以看到下面的效果