写在前面
之前已经将部署环境配置完毕了,但是如果我们在本地开发时,一旦有修改,就要重新构建的话。大大拖慢了我们的开发进度,这是万万不行的。
还记得webpack有一个自带的服务器devServer
吗?它就会做一些开发环境的构建,但是由于我们是SSR架构,所以并不能用webpack的devSever
,我们需要手动实现一个devServer
。
实现dev-server
创建文件
build/dev-server.js
,这里主要放的是我们实现devServer
的源码。
touch build/dev-server.js
编写server/ssr/index.js
之前我们这里对于本地开发环境如何获取renderer
是空开的,这里我们先补上。
createBundleRenderer
需要三样东西:serverBundle
,template
,clientManifest
。
于是我们需要在build/dev-server.js
里抛出一个函数,通过这个函数就可以获取上面所需的三个东西。
同时我们需要注意webpack构建的IO操作是异步的,所以build/dev-server.js
抛出的函数需要返回一个Promise
,当webpack构建完毕我们才需要进行renderToString
渲染出html代码传给客户端。
所以build/dev-server.js
抛出的函数应该是这样的一个结构:
(app, (serverBundle, template, clientManifest) => void) => Promise;
知道了函数结构后,我们先不用管函数的实现,我们先来写函数的调用,首先肯定是要定义一个变量来接收函数返回的Promise
:
let onReady;
之后就是调用函数,生成renderer
:
onReady = setupDevServer(app, (serverBundle, template, clientManifest) => {
try {
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
});
} catch (e) {
logger.error(e);
}
});
之后我们就可以在前端访问后台路由的时候用这个生成的renderer
来进行渲染啦~不过要当心哦!这个renderer
是异步生成的,需要等待onReady
的执行完毕:
router.get('/(.*)', async (ctx, next) => {
try {
//本地环境需要等待webpack构建完毕之后再向客户端吐出html
//渲染HTML
isLocal && (await onReady);
const html = await renderer.renderToString({ url: ctx.url });
ctx.body = html;
} catch (e) {
//渲染失败
logger.error(e);
await next();
}
});
最后改完的server/ssr/index.js
文件应该是这样的:
const fs = require('fs');
const { createBundleRenderer } = require('vue-server-renderer');
const setupDevServer = require('../../build/dev-server');
const log4js = require('log4js');
const logger = log4js.getLogger();
const resolve = path => require('path').resolve(__dirname, path);
const template = fs.readFileSync(resolve('../../index.template.html'), 'utf-8');
//创建一个服务端渲染器
let renderer;
let onReady;
let isLocal = process.env.NODE_ENV === 'local';
module.exports = (server, router) => {
//本地开发环境
if (isLocal) {
onReady = setupDevServer(
server,
(serverBundle, template, clientManifest) => {
try {
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
});
} catch (e) {
logger.error(e);
}
}
);
} else {
//部署环境
const serverBundle = require('../public/dist/vue-ssr-server-bundle.json');
const clientManifest = require('../public/dist/vue-ssr-client-manifest.json');
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
});
}
router.get('/(.*)', async (ctx, next) => {
try {
//本地环境需要等待webpack构建完毕之后再向客户端吐出html
isLocal && (await onReady);
//渲染HTML
const html = await renderer.renderToString({ url: ctx.url });
ctx.body = html;
} catch (e) {
//渲染失败
logger.error(e);
await next();
}
});
};
编写setupDevServer
写完了函数的调用,我们现在就要来实现这个函数啦。
我们先定义三个变量用于之后的返回。
module.exports = function(app, cb) {
return new Promise((res, rej) => {
let template, serverBundle, clientManifest;
});
};
还需要定义一个update
函数用于更新这三个变量。
module.exports = function(app, cb) {
return new Promise((resolve, reject) => {
let template, serverBundle, clientManifest;
//用于更新三个bundle
const update = () => {
if (template && serverBundle && clientManifest && cb) {
cb(serverBundle, template, clientManifest);
res();
}
};
});
};
template
首先我们先从最简单的template
开始,当template
更改时,我们就需要从磁盘中获取最新的template
文件,然后调用update
通知更新。
由于这里需要监视template
的更改,我这里推荐一个库chokidar
,用于监视文件的变化。
接下来我们就可以使用chokidar
来监视template
并通知更新啦:
const resolve = path => require('path').resolve(__dirname, path);
...some code here
//更新template
const templatePath = resolve('../index.template.html');
template = fs.readFileSync(templatePath, 'utf-8');
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8');
update();
});
template
的更新还是比较简单的,接下来我们来构建serverBundle
。
serverBundle
首先我们需要使用webpack
的node API,获取一个serverBundle
的编译对象。
const webpack = require('webpack');
const serverConf = require('./webpack.server');
...some code here
//构建serverBundle
const serverCompiler = webpack(serverConf);
之后我们就需要调用这个编译对象,开始进行编译,当编译结束后从硬盘中取出最新的serverBundle
并通知更新。但是硬盘的IO效率是比较低的,在开发过程中,为了追求效率,我们需要将文件打包到内存中,而内存的IO效率是非常高的。为了能使我们的打包能放进内存中,我们需要安装一个插件koa-webpack-dev-middleware
。其实就是webpack-dev-middleware
的Koa
版本。安装好后我们就可以开始使用它了,当然,虽然是打包到内存中,但是其访问路径还是和硬盘的访问路径相同:
//构建serverBundle
const buildingConf = require('./building-config');
const webpackDevMiddleware = require('koa-webpack-dev-middleware');
...some code here
//构建serverBundle
const serverCompiler = webpack(serverConf);
webpackDevMiddleware(serverCompiler);
//webpack 编译结束后执行的hook
serverCompiler.hooks.done.tap('server', () => {
//使用临时的文件系统
const _fs = serverCompiler.outputFileSystem;
serverBundle = JSON.parse(_fs.readFileSync(
resolve(buildingConf.assetsRoot + '/vue-ssr-server-bundle.json'),
'utf-8'
));
update();
});
clientManifest
clientManifest
的构建流程其实大体上和serverBundle
差不多,但是我们还需要添加一个HMR
的模式,也就是模块热替换,不知道的同学可以出门右转B站,有很多webpack
的学习视频。使用HMR
需要安装一个koa-webpack-hot-middleware
,也就是webpack-hot-middleware
的Koa
版本。
安装完成之后就需要照着webpack-hot-middleware
的文档使用就行啦,我这里直接呈现上配置完成的代码:
const hotMiddleware = require('koa-webpack-hot-middleware');
const clientConf = require('./webpack.client');
...some code here
/**
* 构建clientManifest
*/
clientConf.plugins.push(new webpack.HotModuleReplacementPlugin());
clientConf.entry.app = [
'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true',
...clientConf.entry.app
];
clientConf.output.filename = '[name].js';
const clientCompiler = webpack(clientConf);
const clientMiddleware = webpackDevMiddleware(clientCompiler, {
publicPath: clientConf.output.publicPath,
quiet: true,
stats: {
colors: true,
children: true,
modules: false,
chunks: false,
chunkModules: false
},
watchOptions: {
ignored: /node_modules/
}
});
clientCompiler.hooks.done.tap('client', () => {
clientManifest = JSON.parse(
clientCompiler.outputFileSystem.readFileSync(
resolve(buildingConf.assetsRoot + '/vue-ssr-client-manifest.json'),
'utf-8'
)
);
update();
});
app.use(
hotMiddleware(clientCompiler, {
log: false,
reload: true
})
);
看上去代码好像挺多,其实只要照着文档配置,你也会哦~
最后还有最重要的一点,需要让服务器打开内存空间的访问,让客户端可以获取到clientManifest
,其实也很简单:
//将内存中的文件开放访问
app.use(clientMiddleware);
完整的dev-server.js
文件:
const fs = require('fs');
const resolve = path => require('path').resolve(__dirname, path);
const webpack = require('webpack');
const serverConf = require('./webpack.server');
const clientConf = require('./webpack.client');
const buildingConf = require('./building-config');
const webpackDevMiddleware = require('koa-webpack-dev-middleware');
const hotMiddleware = require('koa-webpack-hot-middleware');
const chokidar = require('chokidar');
module.exports = function(app, cb) {
return new Promise((res, rej) => {
let template, serverBundle, clientManifest;
//用于更新三个bundle
const update = () => {
if (template && serverBundle && clientManifest && cb) {
console.log(31231212312131231231);
cb(serverBundle, template, clientManifest);
res();
}
};
//更新template
const templatePath = resolve('../index.template.html');
template = fs.readFileSync(templatePath, 'utf-8');
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8');
update();
});
//构建serverBundle
const serverCompiler = webpack(serverConf);
webpackDevMiddleware(serverCompiler);
//webpack 编译结束后执行的hook
serverCompiler.hooks.done.tap('server', () => {
//使用临时的文件系统
const _fs = serverCompiler.outputFileSystem;
serverBundle = JSON.parse(
_fs.readFileSync(
resolve(buildingConf.assetsRoot + '/vue-ssr-server-bundle.json'),
'utf-8'
)
);
update();
});
/**
* 构建clientManifest
*/
clientConf.plugins.push(new webpack.HotModuleReplacementPlugin());
clientConf.entry.app = [
'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true',
...clientConf.entry.app
];
clientConf.output.filename = '[name].js';
const clientCompiler = webpack(clientConf);
const clientMiddleware = webpackDevMiddleware(clientCompiler, {
publicPath: clientConf.output.publicPath,
quiet: true,
stats: {
colors: true,
children: true,
modules: false,
chunks: false,
chunkModules: false
},
watchOptions: {
ignored: /node_modules/
}
});
clientCompiler.hooks.done.tap('client', () => {
clientManifest = JSON.parse(
clientCompiler.outputFileSystem.readFileSync(
resolve(buildingConf.assetsRoot + '/vue-ssr-client-manifest.json'),
'utf-8'
)
);
update();
});
app.use(
hotMiddleware(clientCompiler, {
log: false,
reload: true
})
);
//将内存中的文件开放访问
app.use(clientMiddleware);
});
};
完成
大功告成!现在你可以启动npm run local
就可以看到开发环境的页面啦~当你修改源代码后devServer
就会自动编译,并且进行模块热替换啦!
one more thing…
当然这只是环境的搭建,之后我们还需要做很多的工作来进行更好的SEO,以及一些不要踩进去的坑,在下一章节,我会一一告诉大家。