本文介绍如何使用 Node.js 的 url-valid 模块验证 URL 有效性。该模块通过发送 HTTP GET 请求并立即中止来检查链接状态,支持处理 301-303 重定向。同时,它利用 EventEmitter 提供链式 API,允许开发者在验证链接的同时获取网页数据,适用于需要批量检查链接可用性并提取信息的场景。
URL 验证方法对比
在 JavaScript 中,通常使用正则表达式来检验 URL 的格式是否正确,例如:
代码如下:
/^https?:\/\/\/.test(url);此外,还有基于 RFC 3986、RFC 3966、RFC 4694、RFC 4759 和 RFC 4904 等标准进行验证的 valid-url 库。
然而,仅凭格式验证无法确定 URL 是否真实存在,因此出现了 url-valid,它基于 HTTP 请求进行验证。
接口设计
url-valid 接口设计与使用
实际上,我们只需要一个函数,传入一个 URL 地址,并通过回调返回该链接是否可用。
但由于请求可能产生未知错误,我们在回调函数中传入一个 error 参数,如果不为空,则表示有错误发生。
我们可能还希望能够得到网页的相关数据,未来用于页面的信息提取。
尽可能使用链式操作。
所以最后使用上大概是这样的:
代码如下:
valid(url)
.on('check', function (err, status) {
if (err) throw err;
status ?
console.log('url是可用的') :
console.log('url是不可用的');
})
.on('data', function (err, data) {
console.log(data);
})
.on('end', function (err, data) {
console.log('请求结束');
})HTTP 请求策略与重定向处理
HTTP GET 还是 HTTP HEAD
本来我们想利用 HTTP HEAD 请求来实现,因为 HEAD 请求只会返回头信息,这可以减少请求时间,但是 HEAD 请求不一定所有链接都会支持。
所以最后我们使用 HTTP GET 方式,在得到正确的 statusCode 后立刻 abort 掉请求。
处理301-303
因为 301 到 303 都是重定向状态,所以需要继续检查对应 Location 是否依然存在。
利用process.nextTick异步执行
为了在注册监听后,再执行代码,我们使用 process.nextTick 来一步操作。
核心代码实现
实现
代码如下:
/*!
* valid
* MIT Licensed
*/
module.exports = (function () {
'use strict';
var http = require('http')
, https = require('https')
, EventEmitter = require('events').EventEmitter
, URL = require('url')
, urlReg = /^(https?):\/\/\/;
/**
* Valid
* @class
*/
function Valid(url, callback) {
var that = this;
this.url = url;
this.emitter = new EventEmitter();
process.nextTick(function () {
that.get(url);
});
this.fetch = false;
callback && this.emitter.on('check', callback);
}
Valid.prototype = {
constructor: Valid,
/**
* get
* @param {String} url
*/
get: function (url) {
var match = url.match(urlReg)
, that = this;
if (match) {
var httpLib = (match[1].toLowerCase() === 'http') ? http : https
, opts = URL.parse(url)
, req;
opts.agent = false;
opts.method = 'GET';
req = httpLib.request(opts, function (res) {
var statusCode = res.statusCode;
if (statusCode === 200) {
that.emitter.emit('check', null, true);
that.fetch ?
(res.on('data', function (data) {
that.emitter.emit('data', null, data);
}) && res.on('end', function () {
that.emitter.emit('end');
})) :
(req.abort() || that.emitter.emit('end'));
} else if (300 < statusCode && statusCode < 304) {
req.abort();
var emitter = that.emitter
, valid = one(URL.resolve(url, res.headers.location), function (err, valid) {
emitter.emit('check', err, valid);
});
that.fetch && valid.on('data', function (err, data) {
emitter.emit('data', err, data);
});
valid.on('error', function (err) {
that.emitter.emit('error', err);
});
valid.on('end', function () {
that.emitter.emit('end');
});
} else {
that.emitter.emit('check', null, false);
}
res.on('error', function (err) {
req.abort();
that.emitter.emit('data', err);
});
});
req.on('error', function (err) {
req.abort();
return that.emitter.emit('check', null, false);
});
req.end();
} else {
return that.emitter.emit('check', null, false);
}
},
/**
* on
* @param {Stirng} event
* @param {Function} callback
*/
on: function (event, callback) {
(event === 'data') && (this.fetch = true);
this.emitter.on(event, callback);
return this;
},
/**
* destroy
*/
destroy: function () {
this.emitter.removeAllListeners();
this.url = undefined;
this.emitter = null;
this.fetch = undefined;
},
/**
* removeAllListeners
* @param
*/
removeAllListeners: function (event) {
event ?
this.emitter.removeAllListeners(event) :
this.emitter.removeAllListeners();
return this;
},
/**
* listeners
* @param
*/
listeners: function (event) {
if (event) {
return this.emitter.listeners(event);
} else {
var res = []
, that = this
, _push = Array.prototype.push;
Object.keys(this.emitter._events).forEach(function (key) {
_push.apply(res, that.emitter.listeners(key));
});
return res;
}
}
}
/**
* one
* @param {String} url
* @param {Function} callback
* @return {Valid}
*/
function one(url, callback) {
return (new Valid(url, callback));
}
one.one = one;
return one;
})();