记录我第一次开发和发布npm包

写了不少node脚本,虽说不咋复杂,但用的node_module也不少,想想还没自己做过一个npm包发布上去呢,刚好有了个想法:启动apache或者resin来启动本地web服务,虽然可以,但总是离不开各种配置文件,后来学习了koa-static,发现原来写个脚本也就能启动本地web服务,于是开始了koa-static的脚本启动方式,但每换一个目录,要么就是准备多一份脚本要么就是脚本调整一下项目路径,懒癌发作起来,这也是不能忍的,于是想着不如做一个命令行工具,直接通过命令的方式启动服务,那就方便了。(后来加了配置文件来做更多功能)

场景

这样一个脚本就能启动一个静态web服务了

var app = require('koa')();

var serve = require('koa-static');
//静态资源根目录
var dir = 'D:\\project\\requirejs';

app.use(serve(dir))
//端口号
app.listen(10087);

以这个为出发点,我需要的命令行,应该是可以通过进入到项目目录(cd指令),然后通过工具指令运行,即时启动类似上述的脚本代码完成服务启动。

准备工作

  1. 从上面脚本可以看出,服务主要需要的输入元素是:目录、端口号。这两个元素在命令行中应当以参数的形式存在,而且带有默认值。
  2. 需要用到的基本依赖: commanderkoakoa-static

关于koa,有份不错的资料介绍,介绍其原理等等,相当不错,值得一读

关于commander, 在带有子命令的情况,一般是要建立 命令名+'-'+子命令名.js文件来写子命令的实现。

关于github,项目托管在github,方便开源和开发管理。

npm包的一般开发流程:定义好项目目录-> 生成package.json-> 下载依赖包->编写代码->跑测试->发布

命令行工具的开发细节

因为是命令行工具,我们需要让脚本能被系统从环境变量中检索出来,关于环境变量的定义,这个时候需要项目根目录建立bin目录放置命令行工具脚本,在package.json定义bin字段,"bin":{"qls":"bin/qls.js"}

然后通过 npm link之后

$ npm link

我们就可以直接在命令行窗口里敲击qls命令 ,并运行。其实际效果就是去执行对应的bin/qls.js脚本了。(取消的话,可以用npm unlink

进入开发

在准备工作做好之后,即可以开始入门敲代码了,确认好项目结构,执行启动服务的脚本 ,应该承担起封装对象并运行启动服务的职责,该部分不做额外的接受参数操作,而是通过其被调用时传参来获取输入信息,如上面提到的目录路径和端口号,暂定为index.js。

命令行脚本

设定主要指令格式:

$ qls run # 启动服务,默认当前目录和端口号
$ qls run -p 10089 # 使用10089端口号
$ qls run -d 项目路径 # 指定项目路径,主要是减少要进入比较长路径的项目的cd命令操作
$ qls init # 配置文件并初始化

要实现这几个指令,其实很简单,主要入口脚本 qls.js, 子命令脚本qls-run.jsqls-init.js

qls.js

#! /usr/bin/env node

const program = require('commander');

const pkg = require('../package.json');

program
    .version(pkg.version)
    .usage('<command> [option]')
    .command('init', 'init qls.config.js')
    .command('run [option]', 'start service [option]')
    .parse(process.argv);

qls-init.js

const path = require('path');
const fs = require('fs');

const cwd = process.cwd();

const defConf = path.resolve(cwd, 'qls.config.js');

function pathString(str) {
    return str.replace(/(\\)/g, '$1$1');
}

fs.writeFile(defConf, `module.exports = {
    port:10086,
    dir:"${pathString(cwd)}",
    proxy:{}
}`, function (err) {
    if (err) {
        console.error(err);
        return;
    }
    console.log(defConf, ' generated');
});

qls-run.js

const path = require('path');
const program = require('commander');

const QLS = require('../index.js')();

var options = {}; // eslint-disable-line

var error = false; // eslint-disable-line

const cwd = process.cwd();

const defConf = path.resolve(cwd, 'qls.config.js');

program
    .option('-p, --port <n>', 'use port', parseInt)
    .option('-d, --dir <value>', 'set root directory')
    .option('-c, --config <value>', 'use config file')
    .parse(process.argv);

function moduleAvailable(name) {
    try {
        require.resolve(name);
        return true;
    } catch (e) {} // eslint-disable-line
    return false;
}


if (program.config) {
    options = require(path.resolve(cwd, program.config)); // eslint-disable-line
    if (!options.port || !options.dir) {
        console.error('Your custom config file ', program.config, ' need setting port and dir');
        error = true;
    }
} else if (moduleAvailable(defConf)) {
    options = require(defConf); // eslint-disable-line
    if (!options.port || !options.dir) {
        console.error('qls.config.js need setting port and dir');
        error = true;
    }
} else {
    options.port = program.port || 10086;
    options.dir = program.dir || cwd;
}
if (!error) {
    QLS.run(options);
}

在使用commander过程中,遇到的几个奇怪点,一个是用其自带的帮助提示和显示usage时,会把文件名(不带后缀)显示,于是我才把主文件名改为qls.js。 而子命令文件需要以 command +'-'+subcommand+'.js'的方式命令,否则执行时会报找不到对应文件。

子命令才是命令行的主体逻辑,实际就是通过运行业务逻辑脚本模块内容来执行的。

主体逻辑实现

const app = require('koa')();
const serve = require('koa-static');
 
const prPortOccupied = require('./util/portOccupyPromise');
 
  var QLS = function () { // eslint-disable-line
    return new QLS.fn.init(); // eslint-disable-line
  };
 
 QLS.fn = QLS.prototype;
 
 QLS.fn.init = function(){}; // eslint-disable-line
 
 QLS.fn.init.prototype = QLS.prototype;
 
 QLS.prototype.run = function (option) {
        if (!option) {
            console.error('Option required!');
            return;
        }

        prPortOccupied(option.port).then(function(){
           app.use(serve(option.dir));
           app.listen(option.port, function () {
               console.log(`service started\n port: ${option.port} , dir:${option.dir}`);
           });
        },function(res){
           res.status && console.error(res.desc);
        });
  };

  module.exports = QLS;

主体逻辑说白了就是一开头说的koa脚本那几行实现,只不过这里加上了参数判断以及判断端口占用问题。

写到这里,基本的qls命令行就完成了。后续我又加载上了proxy(参考vue-cli的proxyTable跨域接口代理)。

代码检查-ESLint

入门ESLint

我的配置:

module.exports = {
  "extends": "eslint:recommended",
  "env": {
    node: true,
    es6: true
  },
  "rules": {
    "no-console": 0,
    "func-names": 0,
    "no-tabs":0,
    "indent":0,
    "prefer-arrow-callback":0,
    "linebreak-style":0
  } 
}

单元测试

参考资料

由于项目主要是启动服务,所以测试需要用到supertest来测试http请求相关内容。

发布npm

项目基本完善完可以发布时,进入项目目录,登录npm账户(需要去npm注册账号)

$ npm adduser

根据提示输入用户名和密码。注意:如果你有用nrm对npm源做切换的操作,先把源切回npm原来的地址,不然貌似发布不成功。

发布命令:

$ npm publish

后续更新,每次修改代码之后,在项目目录下直接输入指令npm version patch,将会把package.json的version更新,(如果项目还是用了git管理的话,package.json的更新还是自动commit)最后继续npm publish即可把更新内容发布到npm上去了。

参考

后续

后面开发了proxy代理接口后,本想接着做mock数据,不过在做proxy过程中,遇到post请求时,自己一开始挖坑先做post数据解析导致proxy后实际接收方得不到正确的post数据,虽然修正了,但如果做mock数据,避免不了在实现响应数据结合请求数据渲染时,再次遇到要先解析post数据的问题,暂时就搁置了,先顺手学习一下单元测试的编写以及koa的进阶……