记录我第一次开发和发布npm包
15 May 2017写了不少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
指令),然后通过工具指令运行,即时启动类似上述的脚本代码完成服务启动。
准备工作
- 从上面脚本可以看出,服务主要需要的输入元素是:目录、端口号。这两个元素在命令行中应当以参数的形式存在,而且带有默认值。
- 需要用到的基本依赖: commander、koa、koa-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.js
、qls-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
我的配置:
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
的进阶……