這是我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; })();