从零开始搭建Vue服务端渲染项目(三):开发环境的搭建

写在前面

之前已经将部署环境配置完毕了,但是如果我们在本地开发时,一旦有修改,就要重新构建的话。大大拖慢了我们的开发进度,这是万万不行的。
还记得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-middlewareKoa版本。安装好后我们就可以开始使用它了,当然,虽然是打包到内存中,但是其访问路径还是和硬盘的访问路径相同:

//构建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-middlewareKoa版本。
安装完成之后就需要照着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,以及一些不要踩进去的坑,在下一章节,我会一一告诉大家。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的前端小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值