vue-easy-renderer
Version:
Vue.js 2.0 server-side renderer for *.vue file with Node.js.
221 lines (202 loc) • 6.03 kB
JavaScript
//
const EventEmitter = require('events');
const Vue = require('vue');
const Vuex = require('vuex');
const Router = require('vue-router');
const serialize = require('serialize-javascript');
const vueServerRenderer = require('vue-server-renderer');
const SSRPlugin = require('../plugins/server');
const StreamTransform = require('./transform');
const VueHead = require('./head');
const ErrorTypes = require('../error');
Vue.use(SSRPlugin);
Vue.use(Vuex);
Vue.use(Router);
const defaultRendererOptions = {
head: Object.create(null),
plugins: [],
preCompile: [],
global: Object.create(null),
};
class Renderer extends EventEmitter {
/**
* Creates an instance of Renderer.
* @param {ICompiler} compiler
* @param {RendererOptionParams} options
* @memberof Renderer
*/
constructor(compiler, options) {
super();
this.compiler = compiler;
this.vueRenderer = vueServerRenderer.createRenderer();
this.options = Object.assign({}, defaultRendererOptions, options);
this.init();
}
/**
*
*
* @memberof Renderer
*/
init() {
const needCompiledPlugin = [];
this.options.plugins.forEach((plugin) => {
if (typeof plugin === 'string') {
needCompiledPlugin.push(plugin);
}
});
this.options.preCompile.push(...needCompiledPlugin);
this.compiler.load(this.options.preCompile).then(() => {
this.emit('ready');
}).catch((e) => {
const error = new ErrorTypes.BaseError(e);
this.emit('error', error);
});
}
/**
*
*
* @returns {Promise<Class<Vue>>}
* @memberof Renderer
*/
getVueClass() {
if (this.Vue) return Promise.resolve(this.Vue);
const needCompiledPlugins = [];
this.options.plugins.forEach((plugin) => {
if (typeof plugin === 'string') {
needCompiledPlugins.push(plugin);
} else if (plugin.default && plugin.default.install) {
Vue.use(plugin.default);
} else {
Vue.use(plugin);
}
});
if (needCompiledPlugins.length === 0) {
this.Vue = Vue;
return Promise.resolve(this.Vue);
}
return Promise.all(needCompiledPlugins.map(pluginPath => this.compiler.import(pluginPath)))
.then((plugins) => {
plugins.forEach((plugin) => {
if (plugin.default && plugin.default.install) {
Vue.use(plugin.default);
} else {
Vue.use(plugin);
}
});
this.Vue = Vue;
return this.Vue;
});
}
/**
* get the component
*
* @param {string} path
* @param {RendererContext} context
* @returns {Promise<Vue>}
* @memberof Renderer
*/
getComponent(path, context) {
return Promise.all([
this.getVueClass(),
this.compiler.import(path).then(object => object.default || object),
]).then(([VueClass, VueOptions]) => {
const SSRVueOptions = Object.assign({}, VueOptions, { $context: context });
const component = new VueClass(SSRVueOptions);
if (component.$options.router) {
return new Promise((resolve) => {
component.$options.router.onReady(() => resolve(component));
});
}
return component;
});
}
/**
*
*
* @param {string} path
* @param {Object} state
* @param {RenderOptions} options
* @returns {Promise<stream$Readable>}
* @memberof Renderer
*/
renderToStream(path, state, options) {
const context = {
state: state || {},
url: options ? options.url : '/',
};
const isPure = options && options.pure;
return this.getComponent(path, context).then((component) => {
const bodyStream = this.vueRenderer.renderToStream(component);
bodyStream.on('error', (e) => {
let error;
if (e instanceof ErrorTypes.CompilerError) {
error = e;
} else {
error = new ErrorTypes.RenderError(e);
error.component = path;
error.state = state;
}
this.emit('error', error);
});
if (isPure) return bodyStream;
const head = component.$options.$getHead();
const mergedHead = VueHead.headMerge(head, this.options.head);
const template = Renderer.getTemplateHtml(mergedHead, context.state, this.options.global);
const transform = new StreamTransform(template.head, template.tail);
return bodyStream.pipe(transform);
});
}
renderToString(path, state, options) {
const context = {
state: state || {},
url: options ? options.url : '/',
};
const isPure = options && options.pure;
return this.getComponent(path, context).then(component => new Promise((resolve, reject) => {
this.vueRenderer.renderToString(component, (e, result) => {
if (e) {
e.component = path;
reject(e);
return;
}
if (isPure) {
resolve(result);
return;
}
const head = component.$options.$getHead();
const mergedHead = VueHead.headMerge(head, this.options.head);
const indexHtml = Renderer.getTemplateHtml(mergedHead, context.state, this.options.global);
const html = `${indexHtml.head}${result}${indexHtml.tail}`;
resolve(html);
});
}));
}
/**
*
*
* @static
* @param {Object} headOptions
* @param {Object} state
* @param {Object} globalVars
* @returns {{ head: string, tail: string }}
* @memberof Renderer
*/
static getTemplateHtml(headOptions, state, globalVars) {
const vueHead = new VueHead(headOptions);
const globalString = Object.keys(globalVars).map(key => `window.${key} = ${serialize(globalVars[key], { isJSON: true })}; `).join('\n');
const head = `<!DOCTYPE html>
<html>
<head>
<script>window.__VUE_INITIAL_STATE__ = ${serialize(state, { isJSON: true })};</script>
<script>${globalString}</script>
${vueHead.toHtmlString()}
</head>
<body>
`;
const tail = `
</body>
</html>`;
return { head, tail };
}
}
module.exports = Renderer;