egg-view-react-ssr
Version:
react server side render for egg
144 lines (127 loc) • 4.72 kB
JavaScript
'use strict';
const fs = require('fs');
const React = require('react');
const serialize = require('serialize-javascript');
const ReactDOMServer = require('react-dom/server');
const Resource = require('server-side-render-resource');
const REACT_RESOURCE = Symbol('Application#resource');
class Engine {
constructor(app) {
this.app = app;
this.config = app.config.reactssr;
this.fileCache = [];
}
get resource() {
if (!this[REACT_RESOURCE]) {
if (fs.existsSync(this.config.manifest)) {
this[REACT_RESOURCE] = new Resource(this.app, this.config);
}
}
if (process.env && process.env.NODE_ENV === 'production' && !this[REACT_RESOURCE]) {
this.app.logger.warn('[egg-view-react-ssr] react server rendering missing manifest!');
}
return this[REACT_RESOURCE];
}
getAsset(name, state) {
const manifest = this.resource && this.resource.manifest || {};
const deps = manifest.deps || {};
const res = deps[name] || {};
return {
js: res.js || [],
css: res.css || [],
state: serialize(state || {}, { isJSON: true }),
};
}
normalizeLocals(locals = {}) {
[ 'ctx', 'request', 'helper' ].forEach(key => {
Object.defineProperty(locals, key, { enumerable: false });
});
return locals;
}
mergeLocals(ctx, locals, options, engine = true) {
options = options || {};
locals = engine ? options.locals : locals;
locals = this.setCSRFLocals(ctx, locals);
if (this.config.mergeLocals) {
// if egg-view engine mode, the locals had merged
return Object.assign({}, { ctx, request: ctx.request, helper: ctx.helper }, ctx.locals, locals);
}
return Object.assign({}, { ctx, request: ctx.request, helper: ctx.helper }, locals);
}
setCSRFLocals(ctx, locals) {
// when csrf enable, set ctx csrf
const security = this.app.config.security;
if (security.csrf && security.csrf.enable) {
return Object.assign({}, { csrf: ctx.csrf }, locals);
}
return locals;
}
normalizeReactElement(reactElement) {
return reactElement && reactElement.default ? reactElement.default : reactElement;
}
async render(name, locals, options) {
const reactElement = require(name);
return this.renderElement(reactElement, locals, options);
}
async readFile(filepath) {
if (this.fileCache[filepath]) {
return this.fileCache[filepath];
}
return new Promise((resolve, reject) => {
fs.readFile(filepath, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
this.fileCache[filepath] = data;
resolve(data);
}
});
});
}
async renderPage(name, locals, options) {
// 支持自定义 layout html 模板
const html = /\.(html|htm|tpl)$/.test(name) ? await this.readFile(name) : await this.render(name, locals, options);
if (this.app.react.resource) {
locals = this.normalizeLocals(locals);
return this.app.react.resource.inject(html, options.name, locals, options);
}
return html;
}
// eslint-disable-next-line no-unused-vars
async renderElement(reactElement, locals, options) {
reactElement = this.normalizeReactElement(reactElement);
// support asyncData
if (reactElement.asyncData) {
const data = await reactElement.asyncData(locals);
locals = Object.assign(locals, data);
return this.renderToString(reactElement, locals);
}
return this.renderToString(reactElement, locals);
}
async renderAsset(ctx, name, locals, options = {}) {
const layout = options.layout || this.config.layout;
const viewEngine = options.viewEngine || this.config.viewEngine || 'nunjucks';
// 输出到页面的 state 数据
const state = Object.assign({}, ctx.locals, locals);
const asset = this.getAsset(name, state);
// egg-view 自动合并 ctx, request, response, helper
const template = await this.readFile(layout);
const context = Object.assign({}, locals, { asset });
return ctx.renderString(template, context, { viewEngine });
}
renderMarkup(name, locals) {
const reactElement = require(name);
return Promise.resolve(this.renderToStaticMarkup(reactElement, locals));
}
renderToString(reactElement, locals) {
reactElement = this.normalizeReactElement(reactElement);
const element = React.createElement(reactElement, locals);
return ReactDOMServer.renderToString(element);
}
renderToStaticMarkup(reactElement, locals) {
reactElement = this.normalizeReactElement(reactElement);
const element = React.createElement(reactElement, locals);
return ReactDOMServer.renderToStaticMarkup(element);
}
}
module.exports = Engine;