UNPKG

bigape

Version:

an bigpipe inpired node structure based on express

890 lines (749 loc) 21.7 kB
/** * @desc: Bigpipe * @authors: Yex * @date: 2016-08-03 20:32:03 * */ 'use strict'; var _ = require('lodash'); var EventEmitter = require('events').EventEmitter; // var config = require('./config'); // var monitor = config.plugins('monitor'); //require('@qnpm/q-monitor'); var debug = require('debug'); //require('@qnpm/q-logger'); var logger = debug('bigape'); var errorLog = debug('bigape:error'); var Promise = require('bluebird'); var Store = require('./Store'); var ErrorPagelet = require('./errorPagelet'); var htmlParser = require('./htmlParser'); // 国内的一些搜索引擎,因为谷歌和必应对异步的网址做了优化,所以忽略 var spiderReg = /Baiduspider|HaoSouSpider|360spider|(Sogou\s*(web|inst)?\s*Spider)/i; var CONFIG = {}; function BigPipe(name, options) { this.bigpipe = this; // 标识符id,需要唯一 this.name = name; this.options = options; // pagelet cache data this.store = new Store(options.storeID || 'store'); // first-class pagelet module list this.pagelets = options.pagelets || []; // just cache this._originalPagelets = options.pagelets || []; // layout bootstrap this.layout = options.layout || options.bootstrap || {}; // 错误 module this.errorPagelet = options.errorPagelet || CONFIG.errorPagelet || ErrorPagelet; // monitor key this.monitor = options.qmonitor || options.monitor || name; // 实例化的一级 first-class pagelets this._pagelets = []; // 实例化的layout页面 this._layout = null; // 实例化的页面片段集合 this._pageletList = []; // pagelet 缓存 this._pageletMap = {}; // errorpagelet 实例 this._errorPagelet = null; // 需要flush到客户端的片段集合 this._queue = []; // http something this._req = null; this._res = null; this._next = null; // 所有的一级 pagelet 数量 this.length = 1; //Object.keys(options.pagelets).length || 1; this.initialize.call(this, options); // emitter this.setMaxListeners(100); } BigPipe.prototype = { constructor: BigPipe, // buffer encoding charset charset: 'utf-8', initialize: function(options) { if (!options) return this; var actions = options.actions || {}; var bigpipe = this; _.forIn(actions, function(value, key) { if (!bigpipe.hasOwnProperty(key)) { bigpipe['$' + key] = value; // console.log('add props', key); } else { console.info('新增实例方法[' + key + ']与Bigpipe原实例方法冲突'); } }); return this; }, /** * 覆盖bigpipe的pagelet模块 * @param {Array} pageletsArray 模块map * @return {this} */ usePagelets: function(pageletsArray) { if (_.isPlainObject(pageletsArray)) { var temp = []; _.forEach(pageletsArray, function(value) { temp.push(value); }); pageletsArray = temp; } this.pagelets = pageletsArray; return this; }, // same with usePagelets pipe: function(pageletsArray) { return this.usePagelets.call(this, pageletsArray); }, /** * route 请求,每次处理新请求,需要更新bigpipe和对于模块的req,res,next * @return {this} */ router: function(req, res, next) { logger( '开始Bigpip, start router使用模块为[' + getPageletsName(this.pagelets) + ']' ); // monitor.addCount(this.monitor + '_page_visit'); this.clear(); this.bootstrap(req, res, next); this.createPagelets(); this.start(); return this; }, clear: function() { this.store.clear(); this._pageletMap = {}; this._pageletList = []; this._pagelets = []; this._queue = []; }, start: function() { var bigpipe = this; bigpipe._pageletList.forEach(function(pagelet) { bigpipe.analyze(pagelet, function() { pagelet.ready('ready'); }); }); bigpipe._layout.ready('done'); bigpipe._errorPagelet.ready('done'); this.once('page:error', function(err) { logger('出现错误, 需要终止页面渲染', err, '\n'); bigpipe.renderError(err); }); }, /** * [function description] * @param {Object} 需要处理的の pagelet 实例 * @param {Function} done 处理好依赖之后的回调 * @return {Object} Promise */ analyze: function(pagelet, done) { var bigpipe = this; var waitMods = pagelet.wait || []; var waitModNames = waitMods.map(function(mod) { if (typeof mod === 'string') { return mod; } return mod.prototype.name; }); logger( 'start analyze module', pagelet.name, '依赖模块[' + waitModNames.join('|') + ']' ); Promise.map(waitModNames, function(modName) { return bigpipe.waitFor(modName); }).then(function() { logger('analyze module done', pagelet.name); done.call(pagelet, pagelet); }); }, /** * 等待依赖模块ready * @param {string} modName 模块名字 * @return {Object} Promise */ waitFor: function(modName) { var bigpipe = this; // 首先需要触发pagelet的start why? // 加上这个是因为有些依赖的模块,只存在依赖中,如果不手动调用,是没有调用的入口的 // 但是必须保证每个模块调用且只能调用一次,不可逆的 bigpipe._pageletMap[modName].get(); return new Promise(function(resolve, reject) { // pagelet load and parse data ready bigpipe.once(modName + ':done', function(data) { bigpipe.store.set(modName, data); resolve({ name: modName, data: data }); }); // pagelet处理数据失败 bigpipe.once(modName + ':fail', function(data) { bigpipe.store.set(modName, data); reject({ name: modName, data: data }); }); }); }, /** * 将 render 之后的 pagelet push 到队列中 * @param {string} name pagelet name * @param {Object} chunk pagelet chunk * @return {this} this */ queue: function(name, chunk) { this.length--; this._queue.push({ name: name, view: chunk }); return this; }, /** * 清空队列 */ clearQueue: function() { this._queue = []; }, /** * flush chunk * @param {Function} done flush 完成之后的callback * @return {[type]} [description] */ flush: function(done) { if (typeof done !== 'function') { done = NOOP; } this.once('done', done); // end 的时候也触发了次flush,避免再次response到客户端 if (!this._queue.length) { this.emit('done'); return; } // 确保不会在 end 之后再 write chunk if (this._res.finished) { errorLog('Response was closed, unable to flush content'); this.emit( 'end', new Error('Response was closed, unable to flush content') ); return; } var data = new Buffer(this.join(), this.charset); var pageletName = this._queue .map(function(q) { return q.name; }) .join('&'); if (data.length) { logger( 'info: flush pagelet [' + pageletName + '] data {{', data.toString() // '暂不记录}}' ); this._res.write(data, true, done); } // response.write(chunk[, encoding][, callback]) // 如果write时候没有传回调,可以手动调用 if (this._res.write.length !== 3 || !data.length) { this.emit('done'); } // 所有pagelet都已经从队列中输出 if (!this.length) { this.emit('end'); } this.clearQueue(); }, /** * 合并chunk * @return {String} 合并后的chunk */ join: function() { var result = this._queue.map(function(item) { // return item.data; return item.view; }); return result.join(''); }, /** * 实例化 pagelets * @param {} pagelets [description] * @return {[type]} [description] */ createPagelets: function() { var bigpipe = this; var _pageletList = this._pageletList; var _pageletMap = this._pageletMap; var _pagelets = this._pagelets; var allPagelets = this._getAllPagelets(); var options = { req: bigpipe._req, res: bigpipe._res, next: bigpipe._next, query: bigpipe._query, layout: bigpipe._layout, bigpipe: bigpipe }; // create error pagelet this._errorPagelet = this.errorPagelet.create('error', options); _.forIn(allPagelets, function(pagelet, name) { var newPagelet = pagelet.create(name, options); _pageletMap[name] = newPagelet; _pageletList.push(newPagelet); }); // first-class pagelets this.pagelets.forEach(function(pg) { _pagelets.push(_pageletMap[pg.prototype.name]); }); //refresh length + layout bigpipe.length = bigpipe.pagelets.length; return _pageletList; }, /** * 获取pagelet and name * pagelet的名字如果没有指定的话则使用声明时候的name,如果在使用的时候有指定则覆盖 * @return {Object} {pageletName: pageletClass} */ _getAllPagelets: function() { var pagelets = this.pagelets; var allPageletObj = {}; // TODO 为了兼容之前(1.0.x)的API,需要后期统一成数组 if (_.isPlainObject(pagelets)) { var temp = []; _.forIn(pagelets, function(pagelet) { temp.push(pagelet); }); this.pagelets = temp; } pagelets.forEach(function(pagelet) { getDepends(pagelet); }); return allPageletObj; // 递归获取所有用到的pagelets function getDepends(pagelet) { if (!pagelet) { return; } var pgClass = pagelet.prototype; // 已经解析过 if (allPageletObj[pgClass.name]) { // logger('捉到重名的pagelet一只,请注意::', pgClass.name); return; } allPageletObj[pgClass.name] = pagelet; if (pgClass.wait.length) { pgClass.wait.forEach(function(pageletItem) { getDepends(pageletItem); }); } } }, /** * render layout * @param {[type]} req [description] * @param {[type]} res [description] * @param {Function} next [description] * @return {[type]} [description] */ bootstrap: function(req, res, next) { this._req = req; this._res = res; this._next = next; this._layout = this.layout.create('layout', { req: this._req, res: this._res, next: this._next, bigpipe: this }); // 客户端是否是蜘蛛抓取 this.isSpider = spiderReg.test(req.headers['user-agent']); return this; }, /** * 渲染layout页面 * @return {Promise} */ renderLayout: function() { var bigpipe = this; bigpipe._layout.ready('ready'); bigpipe.length++; logger('开始渲染layout脚手架模块'); return this._layout.render().then(function(chunk) { logger('渲染layout脚手架模块完成'); return bigpipe._layout.write(chunk).flush(); }); }, /** * 异步渲染pagelets, flush chunk to client as soon as possible without order * @return {Object} Promise */ renderAsync: function() { if (this.isSpider && BigPipe.optimizeForSeo) { return this.renderSync.apply(this, arguments); } var bigpipe = this; var layout = this._layout; this.renderLayout() .then(function() { return Promise.map(bigpipe._pagelets, function(pagelet) { // render Promise return pagelet .render() .then( function(chunk) { pagelet.write(chunk).flush(); return chunk; }, function(errData) { errorLog('render Async failed', errData); throw errData; } ) .catch(function(error) { errorLog('render Async error', error); throw error; }); }) .then(function() { layout.end(); }) .catch(function(err) { return bigpipe.catch(err); }); }) .catch(function(err) { // monitor.addCount(bigpipe.monitor + '_rendlayout_error'); bigpipe.catch(err); }); }, render: function() { if (this.isSpider && BigPipe.optimizeForSeo) { return this.renderSync.apply(this, arguments); } return this.renderAsync.apply(this, arguments); }, /** * 同步渲染 pagelet 模块 * @return {[type]} [description] */ renderSync: function() { var bigpipe = this; var staticHtml = (this.staticHtml = htmlParser(this.length)); bigpipe._layout .getRenderHtml() .then(function(html) { staticHtml.setLayout(html); logger('start render sync - html parser') return Promise.map(bigpipe._pagelets, function(pagelet) { // render Promise return pagelet .getRenderChunk() .then( function(chunk) { staticHtml.setPagelet(chunk.domID, chunk); return chunk.html; }, function(errData) { errorLog('render sync failed', errData); throw errData; } ) .catch(function(error) { errorLog('render sync error', error); throw error; }); }) .then(function() { logger('render sync flush to client') var html = staticHtml.getHtml() || ''; html = html.replace(/<\/html>$/, ''); bigpipe._layout.end(html, true); }) .catch(function(err) { return bigpipe.catch(err); }); }) .catch(function(err) { // monitor.addCount(bigpipe.monitor + '_rendlayout_error'); bigpipe.catch(err); }); }, /** * 按照指定的pagelet的顺序输出到客户端,异步渲染,flush chunk to client as soon as possible with order * @return {} [description] */ renderPipeline: function() { var bigpipe = this; var layout = this._layout; var pageletOrder = []; var pageletOrderMap = {}; this.renderLayout().then(function() { bigpipe._pagelets.forEach(function(pagelet) { pageletOrder.push(pagelet.name); pagelet.once('active', function(chunk) { pageletOrderMap[pagelet.name] = { name: pagelet.name, ready: true, flushed: false, chunk: chunk }; flushPagelets(); }); pagelet.render(); }); function flushPagelets() { for (var i = 0, len = pageletOrder.length; i < len; i++) { var pl = pageletOrderMap[pageletOrder[i]]; // haven't render ready if (!pl || !pl.ready) { break; } // flush once if (!pl.flushed) { bigpipe.queue(pl.name, pl.chunk).flush(); pl.flushed = true; } // last one if (i === len - 1) { layout.end(); } } } }); }, /** * 渲染模块的json数据 * @param {Array} modules 需要渲染的模块名称数组 * @return {Promise} 获取json数据并返回到客户端 */ renderJSON: function(modules) { var bigpipe = this; // default all pagelets if(!modules) { modules = this._pagelets.map(p => p.name); } if(!_.isArray(modules)) { return this.renderSingleJSON(modules); } // reset actural bigpipe length bigpipe.length = modules.length; if (!modules || !modules.length) { errorLog('处理失败,没有传入需要处理的模块'); bigpipe._json({ status: 500, message: '未获取到数据' }); } logger('开始处理JSON接口数据, 模块[' + modules.join(' | ') + ']'); Promise.map(modules, function(modName) { if (_.isFunction(modName)) { modName = modName.prototype.name; } var mod = bigpipe._pageletMap[modName]; /** * [{key1: '..'}, {key1: '..'}] */ return mod.get().then(function(data) { return { [mod.name]: data }; }); }) .then( function(data) { logger('获取API接口数据成功'); bigpipe._json(data); }, function() { logger('获取API接口数据失败'); } ) .catch(function(error) { errorLog('处理JSON数据接口错误', error); var errObj = bigpipe.getErrObj(error); bigpipe._json(errObj); }); }, renderSingleJSON: function(modName) { var bigpipe = this; bigpipe.length = 1; if (!modName) { errorLog('处理失败,没有传入需要处理的模块'); bigpipe._json({ status: 500, message: '未获取到数据' }); } if (_.isFunction(modName)) { modName = modName.prototype.name; } logger('开始处理JSON接口数据, 模块[' + modName + ']'); var mod = bigpipe._pageletMap[modName]; return mod .get() .then(function(data) { bigpipe._jsonSuc(data); }) .catch(function(error) { errorLog('处理JSON数据接口错误', error); var errObj = bigpipe.getErrObj(error); bigpipe._json(errObj); }); }, /** * 渲染html片段 * @param {string} moduleName 需要渲染的模块名称 * @return {Promise} [description] */ renderSnippet: function(moduleName) { var bigpipe = this; bigpipe.length = 1; if (!moduleName) { errorLog('处理失败,没有传入需要处理的模块'); this._json({ status: 500, message: '未获取到数据' }); } if (_.isFunction(moduleName)) { moduleName = moduleName.prototype.name; } logger('开始处理html snippet接口数据, 模块[' + moduleName + ']'); var module = this._pageletMap[moduleName]; // bigpipe._res.set('Content-Type', 'text/html; charset=utf-8'); module .renderSnippet() .then(function(snippet) { logger('获取snippet成功,flush到客户端', snippet); module.endOnce(snippet); }) .catch(function(error) { errorLog('处理snippet数据错误', error); var errObj = bigpipe.getErrObj(error); bigpipe._json(errObj); }); }, _jsonSuc: function(json) { return this._json({ status: 0, message: 'success', data: json }); }, /** * response json data to the client * @param {Object|Array} data 需要render的原始数据,数组会被处理成Object */ _json: function(data) { if (!data || _.isPlainObject(data)) { return this._res.json(data); } data = data.reduce( function(pre, cur) { return _.extend(pre, cur); }, {} ); this._res.json({ status: 0, message: 'success', data: data }); }, /** * 根据error Object 获取error json * @param {Object} error error stack 或者Object * @return {Object} error json */ getErrObj: function(error) { return { status: error.status || 502, message: typeof error.status == 'undefined' ? '系统繁忙,请稍后重试' : error.message || '系统繁忙,请稍后重试' }; }, /** * catch error * @param {[type]} error [description] * @return {[type]} [description] */ catch: function(error) { if (this.isErrorFatal) { this.bigpipe.emit('page:error', error); } errorLog('catch error', error, '\n'); return this.getErrObj(error); }, renderError: function(error) { if (this.isSpider && BigPipe.optimizeForSeo) { // TODO 整体异常的时候能够同步渲染完整的页面 errorLog('终止pagelet end @ spider', error); return; } errorLog('终止pagelet end', error); var html = this._errorPagelet.renderSyncWithData(error); this._errorPagelet.end(html, true); } }; // extend eventEmitter _.extend(BigPipe.prototype, EventEmitter.prototype); BigPipe.create = (function() { var __instance = {}; return function(name, options) { if (!options) { options = name || {}; name = 'defaults'; } if (!__instance[name]) { __instance[name] = new BigPipe(name, options); } return __instance[name]; }; })(); /** * 渲染是否真的seo做优化,即把bigpipe的异步渲染,转换成同步渲染,返回给spider */ BigPipe.setOptimizeForSeo = function(flag) { BigPipe.optimizeForSeo = !!flag; }; // 全局开关等配置,目前支持的有 // errorPagelet 全局错误模块 // spiderReg 需要优化的搜索引擎 regexp // optimizeForSeo seo优化开关 // /** * 全局开关 * @param {string} key config key * @param {any} value config resource * @return {any} 如果只传入key,则获取,如果有value则赋值 */ BigPipe.config = function(key, value) { if (!key || typeof key !== 'string') { return CONFIG; } if (!value) { return CONFIG[key]; } CONFIG[key] = value; }; // pageletName function getPageletsName(pagelets) { if (typeof pagelets === 'string') { return pagelets; } if (_.isArray(pagelets)) { return pagelets .map(function(pre) { return pre.prototype.name; }) .join('|'); } } // function noop function NOOP() {} module.exports = BigPipe;