林秀栋的技术博客

前端搭建脚手架

文章来源

实现 node 和 shell 的交互

首先创建一个项目目录, 叫 learn-cli

mkdir learn-cli

然后, 创建一个 node 项目

cd learn-cli

npm init -y

用你喜欢的编辑器, 打开 learn-cli 目录, 在 package.json 中添加代码

"bin": {
    "learn": "./bin/learn.js"
},

创建 bin 目录和 learn.js 文件, 在 learn.js 中添加以下代码. 文件头部的 #!/usr/bin/env node 这一句是告诉 shell 要以 node 来解析接下来的 learn 文件.

#!/usr/bin/env node
console.log('hello world')

通过终端进入到项目的根目录执行 npm link 如果出现一下提示说明 link 成功

这个命令的作用其实就是添加了两个软链(win 用户可以理解为快捷方式)到系统的环境变量下. 此时, 在命令行中输入 learn 即可输出 ‘hello world’

接收命令行传来的参数

使用原生的方式获取命令好传入的参数

调整 learn.js 文件为如下:

#!/usr/bin/env node
console.log(process.argv[2] + 'hello world')

再次回到终端执行 ‘learn 圈圈’, 即可得到输出 ‘圈圈hello world’

关于为什么要用 process.argv[2] 请参阅node文档

使用 commander 接收命令行传入的参数

使用原生的方式更适合简单的演示项目, 在这里推荐 commander 来处理参数问题

安装

npm i commander -S

官方示例

#!/usr/bin/env node
const program = require('commander')
program
.version('0.0.1')
.option('-C, --chdir <path>', 'change the working directory')
.option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
.option('-T, --no-tests', 'ignore test hook')

program
.command('setup')
.description('run remote setup commands')
.action(function() {
  console.log('setup');
});

program
.command('exec <cmd>')
.description('run the given remote command')
.action(function(cmd) {
  console.log('exec "%s"', cmd);
});

program
.command('teardown <dir> [otherDirs...]')
.description('run teardown commands')
.action(function(dir, otherDirs) {
  console.log('dir "%s"', dir);
  if (otherDirs) {
    otherDirs.forEach(function (oDir) {
      console.log('dir "%s"', oDir);
    });
  }
});

program
.command('*')
.description('deploy the given env')
.action(function(env) {
  console.log('deploying "%s"', env);
});

program.parse(process.argv);

将官方示例粘贴到 learn.js 中, 执行命令 learn 圈圈 即可

这里输出 deploying “圈圈”

配置 eslint 开启 vscode 自动修复

npm i eslint -D

在项目的根目录下执行 ./node_modules/.bin/eslint –init

根据需求在显示的配置选项中选择

vscode插件中搜索 eslint ,安装

完成后重启编辑器

开启 vscode 自动修复

  1. 打开 vscode 配置
  2. 在功能搜索框中输入 autofix
  3. 选上 auto fix on save

兼容 es6

原文作者的ps:以下配置用于普通的 node 项目没有问题, 但是在脚手架项目中会出现时而好用时而不好用的问题。目前的解决方案是, 把 es6 的模块导入规则手动改成了 commonjs 规范(实在不喜欢 babel 转码然后还给更改目录)。

引入 babel

npm i @babel/core babel-core babel-plugin-transform-es2015-modules-commonjs babel-polyfill babel-preset-env babel-preset-latest-node babel-register -S

在项目的根目录中添加 .babelrc

{
    "presets": ["env"],
    "plugins": ["transform-es2015-modules-commonjs"]
}

创建入口文件 index.js

require('babel-register');
const babel = require('@babel/core');
const babelPresetLatestNode = require('babel-preset-latest-node');

babel.transform('code();', {
  presets: [[babelPresetLatestNode, {
    target: 'current',
  }]],
});

require('babel-polyfill');
require('./src');

创建 src 目录, 并添加 index.js a.js 文件

// index.js
import a from './a';
a.a();

// a.js
export default {
  a() {
    console.log('12345');
  },
};

此时执行 node index.js 顺利打印出 12345

最后改造 bin/learn.js 内容如下:

#!/usr/bin/env node
require('../'); // 执行入口文件

此时在命令行中执行 learn, 打印出 12345

兼容 es6 成功

创建一个专门用于维护项目模板的项目组

为了不和自己平时写的各种辣鸡代码混杂在一起, 这里专门创建了一个 organization, 具体创建方法可百度

目录切换到刚刚创建的 organization 上

01

在组内创建一个项目,并添加一个文件, 上传到 github 并打好 tag

打tag

本地新建轻量级标签

git tag xx

提交到服务器

git push origin –tags

ps.下面没用 organization,直接创建了一个项目

通过 github 开放 api 获取项目信息

api.github.com 提供了一些 github 的开发 api

比如

curl https://api.github.com/users/用户名/repos 会得到个人所有repo的JSON格式列表

curl https://api.github.com/repos/用户名/仓库地址/tags 可以得到该仓库每个tag的消息列表

由于 github 开放 api 有请求次数限制(未授权每小时 60 次), 所以可能报告api被限制而无法使用, 解决方法是在请求中加入认证信息(authToken), 其实就是申请一个 github 授权 token 写到代码里. 申请 token 的步骤在这里;也可以使用你的账户+密码

使用账户+密码

curl -u “你的github账号:你的github密码” https://api.github.com/users/用户名/repos

使用token

申请token步骤

02

token可在请求时作为heade传输

headers: {
    Authorization: 'token 0364152cf3b0d2a580508634ab0dfab9949bd3a1'
}

要注意的时,token内容需要先做处理,不能直接使用。否则上传脚手架代码到github时,检测到token明文就会将其删除

比如token为123456,可以写

const token = '6 5 4 3 2 1'

//...
headers: {
    Authorization: `token ${token.split(' ').reverse().join('')}`,
},
//...

最终项目

本节用到的工具:

03

// bin/learn.js
#!/usr/bin/env node
require('../');


// index.js
require('babel-register');
const babel = require('@babel/core');
const babelPresetLatestNode = require('babel-preset-latest-node');

babel.transform('code();', {
  presets: [[babelPresetLatestNode, {
    target: 'current',
  }]],
});

require('babel-polyfill');
require('./src');


// src/index.js
// colors console.log 文本添加字体颜色, 美观
// import 'colors';

// 接收命令行参数, 提供基础信息提示功能
import commander from 'commander';

// 内部模块
import { existsSync } from 'fs';
import { resolve } from 'path';
import { version } from '../package.json';

commander.version(version)
  .parse(process.argv);

// 获取命令行中传入的第一个参数
const [todo = ''] = commander.args;

// 判断如果 command 目录下是否存在用户输入的命令对应的文件
if (existsSync(resolve(__dirname, `command/${todo}.js`))) {
  require(`./command/${todo}.js`);
} else {
  console.log(
    `
      你输入了未知指令, 小哥哥我已经受不了挂了...
    `.red,
  );
  process.exit(-1);
}


// command/download.js
// command 目录下存放的是我们整个项目中所有的命令文件, 不同的命令对应不同的文件, 体现了单一职责的设计. download 命令用到了我们上一节中提到的两个接口(即获取项目列表和获取版本号列表)
// 命令管理
import commander from 'commander';
// 命令行交互工具
import inquirer from 'inquirer';
// 命令行中显示加载中
import ora from 'ora';
import Git from '../tools/git';

class Download {
  constructor() {
    this.git = new Git();
    this.commander = commander;
    this.inquirer = inquirer;
    this.getProList = ora('获取项目列表...');
    this.getTagList = ora('获取项目版本...');
    this.downLoad = ora('正在加速为您下载代码...');
  }

  run() {
    this.commander
      .command('download')
      .description('从远程下载代码到本地...')
      .action(() => { this.download(); });

    this.commander.parse(process.argv);
  }

  async download() {
    let getProListLoad;
    let getTagListLoad;
    let downLoadLoad;
    let repos;
    let version;

    // 获取所在项目组的所有可开发项目列表
    try {
      getProListLoad = this.getProList.start();
      repos = await this.git.getProjectList();
      getProListLoad.succeed('获取项目列表成功');
    } catch (error) {
      console.log(error);
      getProListLoad.fail('获取项目列表失败...');
      process.exit(-1);
    }

    // 向用户咨询他想要开发的项目
    if (repos.length === 0) {
      console.log('\n可以开发的项目数为 0, 肯定是配置错啦~~\n'.red);
      process.exit(-1);
    }
    const choices = repos.map(({ name }) => name);
    const questions = [
      {
        type: 'list',
        name: 'repo',
        message: '请选择你想要开发的项目类型',
        choices,
      },
    ];
    const { repo } = await this.inquirer.prompt(questions);

    // 获取项目的版本, 这里默认选择确定项目的最近一个版本
    try {
      getTagListLoad = this.getTagList.start();
      [{ name: version }] = await this.git.getProjectVersions(repo);
      getTagListLoad.succeed('获取项目版本成功');
    } catch (error) {
      console.log(error);
      getTagListLoad.fail('获取项目版本失败...');
      process.exit(-1);
    }

    // 向用户咨询欲创建项目的目录
    const repoName = [
      {
        type: 'input',
        name: 'repoPath',
        message: '请输入项目名称~',
        validate(v) {
          const done = this.async();
          if (!v.trim()) {
            done('项目名称不能为空~');
          }
          done(null, true);
        },
      },
    ];
    const { repoPath } = await this.inquirer.prompt(repoName);

    // 下载代码到指定的目录下
    try {
      downLoadLoad = this.downLoad.start();
      await this.git.downloadProject({ repo, version, repoPath });
      downLoadLoad.succeed('下载代码成功');
    } catch (error) {
      console.log(error);
      downLoadLoad.fail('下载代码失败...');
    }
  }
}
const D = new Download();
D.run();


// tools/git.js
// 此文件是 git 相关的操作的文件, 由于脚手架的核心功能就是获取项目的 github 地址, 并下载, 所以我的 Git 类规划了以上几个功能, 获取项目列表 获取项目版本号列表 获取项目地址 下载项目
import download from 'download-git-repo';
import request from './request';
import { orgName } from '../../config';

class Git {
  constructor() {
    this.orgName = orgName;
  }

  getProjectList() {
    return request(`/users/LinXiudong/repos`);
    // 好像是用organization时的接口写法
    // return request(`/orgs/${this.orgName}/repos`);
  }

  getProjectVersions(repo) {
    return request(`/repos/LinXiudong/${this.orgName}/tags`);
    // 好像是用organization时的接口写法
    // return request(`/repos/${this.orgName}/${repo}/tags`);
  }

  downloadProject({ repo, version, repoPath }) {
    return new Promise((resolve, reject) => {
      // 下载到运行命令的目录的 test/tmp 下
      // 这里先直接把路径写死了,前面不论选什么都会下该路径的内容
      download(`LinXiudong/learn-cli`,'test/tmp', (err) => {
      //download(`${this.orgName}/${repo}#${version}`, repoPath, (err) => {
      if (err) reject(err);
      resolve(true);
      });
    });
  }
}

export default Git;


// tools/request.js
// axios封装
import axios from 'axios';
import { baseURL } from '../../config';

const instance = axios.create({
  baseURL,
  timeout: 1e4,
});

// Add a request interceptor
instance.interceptors.request.use(config => config,
  error => Promise.reject(error));

// Add a response interceptor
instance.interceptors.response.use(response => response.data,
  error => Promise.reject(error));

export default instance;


// config/index.js
// github 接口基础地址
export const baseURL = 'https://api.github.com';
// organization 名称,这里没建组织,就用仓库名称
export const orgName = 'test';
// github token
exports.token = 'b 2 b 4 c 7 8 c 5 9 d 2 1 6 6 e a d 0 2 1 a 5 4 1 a 2 7 b 0 e 4 0 d 8 6 9 4 0 c';

之所以要放在organization下是为了防止污染其他代码,不过也可以在写代码路径时稍微改下来处理

将代码发布到 npm 上

npm config set registry http://registry.npmjs.org

发包完成后可执行下面的语句, 把镜像替换为淘宝镜像速度快的飞起.

npm config set registry https://registry.npm.taobao.org

如果报错,根据报错信息运行 npm adduser

成功后执行 npm i learn-cli -g 全局安装 learn-cli