bigape
Version:
an bigpipe inpired node structure based on express
580 lines (491 loc) • 14.1 kB
JavaScript
/**
* @desc: Pagelets
* @authors: Yex
* @date: 2016-08-03 20:32:19
*
*
*/
'use strict';
var EventEmitter = require('events').EventEmitter;
var _ = require('lodash');
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');
// TODO: when use bluebird, when write after end error occured, cpu will increase to 100%, use default promise will be ok
// var Promise = require('bluebird');
var util = require('./util');
function Pagelet(name, options) {
// pagelet uid
this.name = name;
// request
this.req = options.req;
// response
this.res = options.res;
// bigpipe 实例
this.bigpipe = options.bigpipe;
// 脚手架模块实例
this._bootstrap = options.layout || options.bootstrap;
// 适配
this.adapt();
// 生命周期 created
this.onCreated();
// emitter
this.setMaxListeners(100);
}
Pagelet.prototype = {
constructor: Pagelet,
monitor: '',
name: '',
domID: '',
// template
template: '',
// 渲染模式 append html prepend layout remove
mode: 'html',
// 脚本x`x``
scripts: '',
/**
* 样式
* @type {String}
*/
styles: '',
// 需要依赖的模块
wait: [],
// 是否是关键性的模块, 如果出错了是否立即终止请求,并返回错误
isErrorFatal: false,
// 发生错误时候渲染错误页面的模板
errorTemplate: 'partials/error.njk',
// 不输出该模块的logs,主要是数据logs
noLog: false,
// pagelet data 的 key, 默认为name
dataKey: '',
/**
* 生命周期函数 pagelet 实例初始化之后
* @return {[type]} [description]
*/
onCreated: function() {},
/**
* beforeRender lifyCycle function
* @param {Object} data the parsed data after onServiceDone
* @return {any}
*/
onBeforeRender: function(data) {
return data;
},
/**
* 钩子函数(hook function),获取 pagelet 的原始数据
* 获取渲染的原始数据 可以被覆盖,默认是通过service取接口数据,返回promise
* 支持返回同步数据或者Promise异步
* @return {[type]} [description]
*/
getService: function() {
return null;
},
/**
* 适配器,为了兼容老的(1.0.x)api
*/
adapt: function() {
this.dataKey = this.pageletDataKey || this.dataKey;
this.getService = this.getRenderData || this.getService;
this.onServiceDone = this.beforeRender || this.onServiceDone;
this.name = this.name || this.id;
this.template = this.template || this.templateID;
},
bootstrap: function(value) {
if (!value) {
return !this._bootstrap && this.name === 'bootstrap'
? this
: this._bootstrap || {};
}
if (value && value.name === 'bootstrap') {
return (this._bootstrap = value);
}
},
/**
* 通用的获取本模块的pagelet数据的方法,返回Promise
* @return {Object} Promise
*/
get: function() {
var pagelet = this;
// 因为是异步,防止被多次调用,造成资源浪费
if (this._isGetting) {
return this._isGetting;
}
this._isGetting = this.ready()
.then(function() {
return pagelet.getServiceData();
})
.then(function(data) {
data = pagelet.onServiceDone(data);
pagelet.setCache(data);
logger(
'数据处理成功,触发事件[' + pagelet.name + ':done]',
pagelet.noLog ? '{noLog}' : JSON.stringify(data)
);
pagelet.bigpipe.emit(pagelet.name + ':done', data);
return data;
})
.catch(function(error) {
errorLog('数据处理异常', error);
pagelet.catch(error);
});
return this._isGetting;
},
/**
* 依赖数据已经ready,本模块可以正常render
* @param {string} done 是否已经ready
* @return {Object} Promise
*/
ready: function(done) {
if (!this._ready) {
this._ready = new Promise(
function(resolve) {
this.once('ready', function() {
resolve(null);
});
}.bind(this)
);
}
if (done) {
this.emit('ready');
}
return this._ready;
},
/**
* 生命周期,获取原始数据成功之后,一般用来处理原始数据,并返回
* 处理通过getService获取的原始数据
* @param {Object} json 原始数据
* @return {Object} 处理之后的数据
*/
onServiceDone: function(json) {
return json;
},
/**
* 生命周期函数,渲染 html 片段完成之后
* @param {string} html html string
* @return {string} html string
*/
afterRender: function(html) {
return html;
},
/**
* 执行pagelet的渲染,
* @param {Object} renderData 可选,如果传入则直接使用该数据渲染,否则通过service调用获取数据
*/
render: function(renderData) {
var pagelet = this;
logger('开始渲染Pagelet模块[' + pagelet.name + ']@', new Date());
if (renderData) {
this.setCache(renderData);
}
return this.getRenderHtml()
.then(function(source) {
logger('xxx');
var chunk = pagelet.createChunk(source);
pagelet.emit('active', chunk);
return chunk;
})
.catch(function(err) {
var msg = '系统繁忙,请稍后重试' + err.message;
errorLog('Pagelet render error::', err);
pagelet.catch(err);
pagelet.emit('active', msg);
return msg;
});
},
/**
* 渲染html-fragment 片段
* @param {String} html render result
* @return {String} 处理之后的数据
*/
renderSnippet: function(renderData) {
var pagelet = this;
if (renderData) {
this.setCache(renderData);
}
return (
this.getRenderHtml()
.then(function(html) {
return html;
})
// handle error
.catch(function(err) {
errorLog('Pagelet render snippet error::', err);
return pagelet.catch(err);
})
);
},
renderSyncWithData: function(data) {
if (typeof this.onBeforeRender(data) === 'function') {
this.onBeforeRender(data);
}
var viewEngine = config.config('viewEngine'); //require('jnpm-template');
var html = viewEngine.render(this.getTemplatePath(), data);
return this.createChunk(html);
},
/**
* 暴露出的获取本pagelet数据的函数 readonly
* @return {Object} parsed pagelet data {name: data} function(data){}
*/
getServiceData: function() {
var pagelet = this;
// logger('开始获取数据['+ pagelet.name +']');
// 优先使用缓存数据
// 避免重复获取数据
var _cache = this.getCache();
if (_cache) {
logger('使用数据缓存[' + pagelet.name + ']' /*, _cache*/);
return Promise.resolve(_cache);
}
var getOriginData = this.getService();
// 如果数据可以同步, 直接返回同步数据
if (!util.isPromise(getOriginData)) {
// logger('使用同步方式获取数据['+ pagelet.name +']');
logger('获取模块数据成功[' + pagelet.name + ']');
getOriginData = Promise.resolve(getOriginData);
}
return getOriginData
.then(
function(json) {
logger('获取模块数据成功[' + pagelet.name + ']');
return json;
},
function(error) {
errorLog('获取pagelet数据失败', pagelet.name, error);
return pagelet.catch(error);
}
)
.catch(function(error) {
// monitor.addCount('module_handler_error');
errorLog('获取pagelet数据异常', pagelet.name, error);
return pagelet.catch(error);
});
},
/**
* 获取 html 片段渲染结果
* @return {Object} Promise function(html){};
*/
getRenderHtml: function() {
var pagelet = this;
var viewEngine = config.config('viewEngine'); //require('jnpm-template');
return this.get()
.then(
function(parsed) {
if (typeof pagelet.onBeforeRender(parsed) === 'function') {
pagelet.onBeforeRender(parsed);
}
// 统一为渲染数据增加locals参数
return viewEngine.render(
pagelet.getTemplatePath(),
pagelet.getActRenderData(parsed)
);
// 模板渲染reject时候,渲染错误信息
},
function(error) {
errorLog('渲染pagelet失败', pagelet.name, error);
var errorObj = pagelet.getErrObj(error);
return viewEngine.render(pagelet.errorTemplate, errorObj);
}
)
.then(function(html) {
return pagelet.afterRender(html);
})
.catch(function(error) {
// monitor.addCount('module_render_error');
errorLog('渲染pagelet异常', pagelet.name, error);
return viewEngine.render(
pagelet.errorTemplate,
pagelet.getErrObj(error)
);
});
},
getRenderChunk: function() {
var pagelet = this;
return this.getRenderHtml().then(function(html) {
return pagelet.composeChunkObj(html);
});
},
getTemplatePath: function() {
if (!this.template) {
return 'index.njk';
}
if (this.isBootstrap()) {
return this.template;
// return 'pages/' + this.template;
} else {
return 'partials/' + this.template;
}
},
getActRenderData: function(parsed) {
return {
[this.renderDataKey || this.name]: parsed || null,
locals: this.res.locals,
query: this.req.query,
// 页面统一增加 process env
NODE_ENV: process.env.NODE_ENV
};
},
/**
* 生成数据块
* @param {String} html
*/
createChunk: function(html) {
// 如果是主框架则直接返回
if (this.isBootstrap()) {
return html;
}
var chunkObj = this.composeChunkObj(html);
return '<script>BigPipe.onArrive(' +
JSON.stringify(chunkObj) +
')</script>';
},
/**
* 拼接pagelet chunk Object
* @param {string} html rendered html string
* @return {Object} chunk
*/
composeChunkObj: function(html) {
// 如果是主框架则直接返回
if (this.isBootstrap()) {
return html;
}
return {
id: this.name,
html: html,
scripts: this.scripts,
data: this.getPipeData(this.getCache()),
styles: this.styles,
domID: this.domID,
modID: this.name,
mode: this.mode,
dataKey: this.dataKey || this.name,
dataEventName: this.dataEventName || this.dataKey || this.name,
pageletEventName: this.pageletEventName || this.domID || this.name
};
},
/**
* 钩子函数(hook function),获取传递给客户端的数据,默认返回空
* @param {Object} modData 本模块的数据
* @return {*} 返回给客户端的数据
*/
getPipeData: function() {
return null;
},
/**
* 是否是基础模块
* @return {Boolean}
*/
isBootstrap: function() {
return this.name == 'layout' || this.name == 'bootstrap';
},
/**
* flush
* @return {[type]} [description]
*/
flush: function() {
this.bigpipe.flush();
},
/**
* flush
* @param {[type]} name [description]
* @param {[type]} chunk [description]
* @return {[type]} [description]
*/
write: function(name, chunk) {
if (!chunk) {
chunk = name;
name = this.name;
}
return this.bigpipe.queue(name, chunk);
},
/**
* end flush
* @param {[type]} chunk [description]
* @param {Boolean} force 是否强制结束
* @return {[type]} [description]
*/
end: function(chunk, force) {
var pagelet = this;
if (chunk) this.write(chunk);
//
// Do not close the connection before all pagelets are send unless force end.
//
if (this.bigpipe.length > 0 && !force) {
return false;
}
//
// Everything is processed, close the connection and clean up references.
//
this.bigpipe.flush(function close(error) {
if (error) return pagelet.catch(error, true);
logger('Bigpipe end @', new Date());
pagelet.res.end('</html>');
});
return true;
},
/**
* don't do anything, just end the response whit given chund
* @param {Object} chunk chunk data
*/
endOnce: function(chunk) {
this.res.end(chunk);
},
/**
* 根据error Object 获取error json
* @param {Object} error error stack 或者Object
* @return {Object} error json
*/
getErrObj: function(error) {
return {
status: error.status || error.code || 502,
message: error.status || error.code
? 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);
},
getStore: function() {
var store = this.bigpipe.store;
return store.get.apply(store, arguments);
},
setStore: function() {
var store = this.bigpipe.store;
return store.set.apply(store, arguments);
},
getCache: function() {
return this.getStore(this.name);
},
setCache: function(data) {
return this.setStore(this.name, data);
}
};
/*###########################*
* 类继承
*##########################*/
// extend eventEmitter
_.extend(Pagelet.prototype, EventEmitter.prototype);
Pagelet.extend = util.extend;
Pagelet.create = (function() {
var __instance = {};
return function(name, options) {
if (!options) {
options = name || {};
name = 'defaults';
}
__instance[name] = new this(name, options);
return __instance[name];
};
})();
module.exports = Pagelet;