Vue服务端渲染实践之Web应用首屏耗时最优化方案(2)

如示意图所示,一般的Vue服务端渲染项目,有两个项目入口文件,分别为entry-client.js和entry-server.js,一个仅运行在客户端,一个仅运行在服务端,经过Webpack打包后,会生成两个Bundle,服务端的Bundle会用于在服务端使用虚拟DOM生成应用程序的“快照”,客户端的Bundle会在浏览器执行。

因此,我们需要两个Webpack配置,分别命名为webpack.client.config.js和webpack.server.config.js,分别用于生成客户端Bundle与服务端Bundle,分别命名为vue-ssr-client-manifest.json与vue-ssr-server-bundle.json,关于如何配置,Vue官方有相关示例vue-hackernews-2.0

开发环境搭建

我所在的项目使用Koa作为Web Server Frame,项目使用koa-webpack进行开发环境的构建。如果是在产品环境下,会生成vue-ssr-client-manifest.json与vue-ssr-server-bundle.json,包含对应的Bundle,提供客户端和服务端引用,而在开发环境下,一般情况下放在内存中。使用memory-fs模块进行读取。

const fs = require('fs') const path = require( 'path' ); const webpack = require( 'webpack' ); const koaWpDevMiddleware = require( 'koa-webpack' ); const MFS = require('memory-fs'); const appSSR = require('./../../app.ssr.js'); let wpConfig; let clientConfig, serverConfig; let wpCompiler; let clientCompiler, serverCompiler; let clientManifest; let bundle; // 生成服务端bundle的webpack配置 if ((fs.existsSync(path.resolve(cwd,'webpack.server.config.js')))) { serverConfig = require(path.resolve(cwd, 'webpack.server.config.js')); serverCompiler = webpack( serverConfig ); } // 生成客户端clientManifest的webpack配置 if ((fs.existsSync(path.resolve(cwd,'webpack.client.config.js')))) { clientConfig = require(path.resolve(cwd, 'webpack.client.config.js')); clientCompiler = webpack(clientConfig); } if (serverCompiler && clientCompiler) { let publicPath = clientCompiler.output && clientCompiler.output.publicPath; const koaDevMiddleware = await koaWpDevMiddleware({ compiler: clientCompiler, devMiddleware: { publicPath, serverSideRender: true }, }); app.use(koaDevMiddleware); // 服务端渲染生成clientManifest app.use(async (ctx, next) => { const stats = ctx.state.webpackStats.toJson(); const assetsByChunkName = stats.assetsByChunkName; stats.errors.forEach(err => console.error(err)); stats.warnings.forEach(err => console.warn(err)); if (stats.errors.length) { console.error(stats.errors); return; } // 生成的clientManifest放到appSSR模块,应用程序可以直接读取 let fileSystem = koaDevMiddleware.devMiddleware.fileSystem; clientManifest = JSON.parse(fileSystem.readFileSync(path.resolve(cwd,'./dist/vue-ssr-client-manifest.json'), 'utf-8')); appSSR.clientManifest = clientManifest; await next(); }); // 服务端渲染的server bundle 存储到内存里 const mfs = new MFS(); serverCompiler.outputFileSystem = mfs; serverCompiler.watch({}, (err, stats) => { if (err) { throw err; } stats = stats.toJson(); if (stats.errors.length) { console.error(stats.errors); return; } // 生成的bundle放到appSSR模块,应用程序可以直接读取 bundle = JSON.parse(mfs.readFileSync(path.resolve(cwd,'./dist/vue-ssr-server-bundle.json'), 'utf-8')); appSSR.bundle = bundle; }); }

渲染中间件配置

产品环境下,打包后的客户端和服务端的Bundle会存储为vue-ssr-client-manifest.json与vue-ssr-server-bundle.json,通过文件流模块fs读取即可,但在开发环境下,我创建了一个appSSR模块,在发生代码更改时,会触发Webpack热更新,appSSR对应的bundle也会更新,appSSR模块代码如下所示:

let clientManifest; let bundle; const appSSR = { get bundle() { return bundle; }, set bundle(val) { bundle = val; }, get clientManifest() { return clientManifest; }, set clientManifest(val) { clientManifest = val; } }; module.exports = appSSR;

通过引入appSSR模块,在开发环境下,就可以拿到clientManifest和ssrBundle,项目的渲染中间件如下:

const fs = require('fs'); const path = require('path'); const ejs = require('ejs'); const vue = require('vue'); const vssr = require('vue-server-renderer'); const createBundleRenderer = vssr.createBundleRenderer; const dirname = process.cwd(); const env = process.env.RUN_ENVIRONMENT; let bundle; let clientManifest; if (env === 'development') { // 开发环境下,通过appSSR模块,拿到clientManifest和ssrBundle let appSSR = require('./../../core/app.ssr.js'); bundle = appSSR.bundle; clientManifest = appSSR.clientManifest; } else { bundle = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-server-bundle.json'), 'utf-8')); clientManifest = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-client-manifest.json'), 'utf-8')); } module.exports = async function(ctx) { ctx.status = 200; let html; let context = await ctx.getTplContext(); ctx.logger('进入SSR,context为: ', JSON.stringify(context)); const tpl = fs.readFileSync(path.resolve(__dirname, './newTemplate.html'), 'utf-8'); const renderer = createBundleRenderer(bundle, { runInNewContext: false, template: tpl, // (可选)页面模板 clientManifest: clientManifest // (可选)客户端构建 manifest }); ctx.logger('createBundleRenderer renderer:', JSON.stringify(renderer)); try { html = await renderer.renderToString({ ...context, url: context.CTX.url, }); } catch(err) { ctx.logger('SSR renderToString 失败: ', JSON.stringify(err)); console.error(err); } ctx.body = html; };

如何对现有项目进行改造

基本目录改造

使用Webpack来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用Webpack支持的所有功能。

一个基本项目可能像是这样:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/fac8369b9c07d7099cb30f71b965c6d7.html