这是我10月份做的项目其中的一个部件,主要用于url检验的。
我们知道Javascript做url检验,通常是使用正则表达式来判定,其格式是否正确,例如:
/^https?:///.test(url);
当然还有更好的检测方法比如基于RFC 3986, RFC 3966, RFC 4694, RFC 4759, RFC 4904等标准的进行验证的valid-url库。
不过个根据格式进行验证当然不能确定该url是否存在啦,所以就有了url-valid,我们基于HTTP请求进行验证。
接口设计
- 实际上我们只需要一个函数传入一个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 GET 还是 HTTP HEAD
本来我们想利用HTTP HEAD请求来实现的,因为HEAD请求只会返回头信息,这可以减少请求时间,但是HEAD请求,不一定所有链接都会支持。
所以最后我们使用HTTP GET方式,在得到正确的statusCode后立刻abort掉请求。
处理301-303
因为301到303都是重定向状态所以,我们需要继续检查对应Location是否依然存在。
利用process.nextTick异步执行
为了在注册监听后,再执行代码,我们使用process.nextTick来一步操作。
实现
OK,大致就这样,下面就是实现:
/*! * valid * Copyright (c) 2013 Daniel Yang <miniflycn@justany.net> * 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; })();