UNPKG

vue-easy-renderer

Version:

Vue.js 2.0 server-side renderer for *.vue file with Node.js.

221 lines (202 loc) 6.03 kB
// 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;