VUE_SSR_Blog搭建

Ymc 2019-04-21 00:00:00 60 Page View 19.5'm Read Time VueWebpack
历时数月,翻了两版的Blog系统终于上线了,上线了!虽然还有很多地方需要优化的,但是强行推上来了,欢迎大家批评指正~ 简单说下吧,博客分l两个端(两个端放置在同一个项目主体下,通过url不同进行访问...),前端展示就是如你所见的,还有一个后天管理端目前只开发了图片管理和文章管理两个小模块,后续如有新模块可以随时加入;大体的烹制方法如下:准备上等的vue ssr,以及新鲜出炉的webpack,最好有个秀气的盘子Mongoose,少许bable、eslint、JWT、还有vue全家桶呀,最后撒上serverWork,然后我们直接上锅吧~

通往美食的列车发车咯~

基础架构

主要用到的技术栈如题 外加一些涉及到业务的模块,值得一提的是这次翻版,将前台整合到一起,大体上就是一套server代码两个入口页通过路由分别对应两个前端服务~,虽然这样一定程度降低了耦合,但是同样也存在一些痛点后续再,纯手工搭建webpack4以及前端界面,后台基于element-ui组件,服务端使用koa2使用jwt进行鉴权

目录结构

大体的目录结构基本如下:

+---build
+---configs
+---framwork
+---server
+---src
|   +---module
    |   +---front
    |   +---admin
+---dist
\---theme

Server 搭建

由于是纯手工搭建代码可能有些累赘,见谅

开始之前我们先简述下ssr的原理:

客户端请求 -> 服务端接收,并处理页面数据整合,吐出处理过后的`html`文件 -> 客户端直接渲染html文件,并接管服务 -> 回归客户端控制

如图:
vue-ssr原理

vue-ssr api

利用koa2启动服务

入口文件server.js

import Koa from 'koa';

const app = new Koa();

const router = require('koa-router')();

router.get('*', async(ctx, next) => {
  if (!renderer) {
    return (ctx.body = 'waiting for compilation... refresh in a moment.');
  }
  ctx.body = await render(ctx, next);
});

app.use(router.routes()).use(router.allowedMethods());

// create server
app.listen(GConfig.port, () => {
  console.log(`> Build await... `);
  console.log(`You application is running here ${GConfig.host}:${GConfig.port}`);
});

如上就能启动一个简单的koa服务

接着我们需要将打包/热更新的代码塞给服务这里分了两个环境

if (isProd) {
  // 生产环境下直接读取构造渲染器
  const bundle = require('./../dist/vue-ssr-server-bundle.json');
  const template = fs.readFileSync(resolve('./../dist/front.html'), 'utf-8');
  renderer = createRenderer(bundle, template);
  app.use(serve('./dist'));
} else {
  // 开发环境下使用hot/dev middleware拿到bundle与template
  require('./../build/setup-dev-server')(
    app,
    (bundle, template) => {
      renderer = createRenderer(bundle, template);
    }
  );
}

由于项目是前后端使用一套服务,所有这里需要特殊处理下请求url

app.use(
  convert(
    historyApiFallback({
      verbose: true,
      index: '/admin.html',
      rewrites: [
        {
          from: /^\/admin$/,
          to: '/admin.html'
        }
      ],
      path: /^(\/admin)|(\/demo)/
    })
  )
);

将/admin 、/demo路径下的请求直接走/admin.html入口页面,无需ssr处理

下面我们进入webpack配置中去

webpack配置

通过上面的代码我们知道入口文件是setup-dev-server.js
我们去看下这里干了什么事吧~

其实做的事很简单

  1. 服务端打包一个json文件以备服务端渲染的时候用
  2. 正常打包一次
  3. 进行热更新处理(koa中热更新特殊处理下,引入koa-webpack-middleware
    这里直接附上代码(仅保留核心代码),webpack4需要注意点后面会说
const path = require("path");
const MFS = require("memory-fs");
const webpack = require("webpack");
const clientConfig = require("./webpack.client.config");
const serverConfig = require("./webpack.server.config");

const {
    devMiddleware,
    hotMiddleware
} = require("koa-webpack-middleware");

module.exports = function setupDevServer(app, cb) {
    let bundle;
    let template;

    const clientCompiler = webpack(clientConfig);
    const devMiddle = devMiddleware(clientCompiler, {
        publicPath: clientConfig.output.publicPath
    });
    app.use(devMiddle);

    clientCompiler.plugin("done", () => {
        const fs = devMiddle.fileSystem;
    const filePath = path.join(clientConfig.output.path, 'front.html');
        if (fs.existsSync(filePath)) {
            template = fs.readFileSync(filePath, "utf-8");
            console.log('client-over')
            if (bundle) {
                cb(bundle, template);
            }
        }
    });

    // hot middleware
    app.use(hotMiddleware(clientCompiler));

    // 服务端渲染打包
    // watch and update server renderer
    const serverCompiler = webpack(serverConfig);
    const mfs = new MFS();
    serverCompiler.outputFileSystem = mfs;
    serverCompiler.watch({}, (err, stats) => {
        if (err) throw err;
        stats = stats.toJson();
        stats.errors.forEach(err => console.error(err));
        stats.warnings.forEach(err => console.warn(err));
        const bundlePath = path.join(
            serverConfig.output.path,
            "vue-ssr-server-bundle.json"
        );
        bundle = JSON.parse(mfs.readFileSync(bundlePath, "utf-8"));
        console.log('server-over')
        if (template) {
            cb(bundle, template);
        }
  });
};

上面我们注意到只有当vue-ssr-server-bundle.json文件和template全部完成,再返回让vue-server-renderer中的createBundleRenderer方法处理

代码浏览到这里,忽略webpack详细配置,可以得知,我们通过koa2齐了个服务并,使用webpack打包出了两种东西,一个是供服务端使用的vue-ssr-server-bundle.json文件以及正常打包下的资源文件~

下面我们接着说webpack;
其实很明了了,webpack将配置两份,一份是打包正常资源的,一份是负责打包,给上面的出口文件使用,没错吧!
这里有几点由于前后端使用同一套,所以==入口页面两个哦==
并且各种资源代码,我们既然使用了webpack4更要体验下分块
webpack4将new HtmlWebpackPlugin() 去除添加了optimization配置项

  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 4,
      name: false,
      cacheGroups: {//这里提供自定义分块方法
        libs: {
          name: 'chunk-libs',
          test: /[\\/]node_modules[\\/]/,
          priority: 10,
          chunks: 'initial' // 只打包初始时依赖的第三方
          // enforce: true,//排除默认配置如minChunks、maxInitialRequests
        },
         mavonEditor: {
          name: 'chunk-mavonEditor', // 单独将 mavonEditor 拆包
          priority: 18, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
          test: /[\\/]node_modules[\\/]mavon-editor[\\/]/
        },
        swiper: {
          name: 'chunk-swiper', // 单独将 swiper 拆包
          priority: 19, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
          test: /[\\/]node_modules[\\/]swiper[\\/]/
        },
        elementUI: {
          name: 'chunk-elementUI', // 单独将 elementUI 拆包
          priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
          test: /[\\/]node_modules[\\/]element-ui[\\/]/
        }
      }
    },

其他配置资源分类配置不在啰嗦

Vue ssr实现方式

注意到同学应该发现了几个问题,前后各一种打包,他们怎么相互接管的呢,还有服务端请求数据,vue提供的很多钩子不会执行呀。。。

好的我们一个一个来,我们之前说的都是大概,至于打包之前的资源文件配置这块并没有细说,现在来说吧。

index.js==客户端打包使用的入口==

import {
  createApp
} from './app';

const {
  app,
  router,
  store
} = createApp();

// store替换使client rendering和server rendering匹配
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

/**
 * 异步组件
 * 挂载# app
 */
router.onReady(() => {
  app.$mount('#app');
});

发现有什么不同?
对过了个window.__INITIAL_STATE__这个就是服务端渲染的资源和客户端接管的那枚钥匙

服务端将数据存储在window.__INITIAL_STATE__中并传递到客户端供使用并接管

index.js==服务端打包需要的入口==

import {
  createApp
} from './app';
export default context => {
  // 注意下面这句话要写在export函数里供服务端渲染调用,重新初始化那store、router
  function errorCallback(params) {
    console.log('router onReady  error!');
  }
  const {
    app,
    router,
    store
  } = createApp();
  return new Promise((resolve, reject) => {
    router.push(context.url);
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();// getMatchedComponents 方法返回对应要渲染的组件
      if (!matchedComponents.length) {
        reject({
          code: 404
        });
      }
      Promise.all(
        matchedComponents.map(component => {
          if (component.preFetch) {
            // 调用组件上的preFetch(这部分只能拿到router第一级别组件,子组件的preFetch拿不到)
            return component.preFetch(store);
          }
        })
      )
        .then(() => {
          // 暴露数据到HTMl,使得客户端渲染拿到数据,跟服务端渲染匹配
          context.state = store.state;
                    resolve(app);
        })
        .catch(reject);
    }, [errorCallback]);
  });
};

这串代码最重要

这里调用了自定义的钩子函数preFetch拿到数据后暴露出去(其实即使暴露到之前提到的window.__INITIAL_STATE__中)

并且每次请求都重新实例化router、store

==注:== 使用ssr必须拥有第三方类似Flux的状态管理工具进行客户端和服务短的数据传递!!!

业务功能

没啥特别的业务,毕竟前台都是用来看的。。。

首页

列表图片添加了懒加载并添加了加载动画,

home.png

blog汇总页面

blog.png

blog详情

这里的目录使用了 css新属性 content: counter(variate) 、counter-increment: variate;并使用highlight.js进行定制化代码样式

detail.png

归档

archive.png

介绍页

isme.png

更新记录

update.png

要点回顾

升级webpack4 遇到的问题

基本变化

1.使用webpack4必须node版本8.0+
2.删除CommonsChunkPlugin插件
3.使用内置APIoptimization.splitChunks和optimization.runtimeChunk;webpack会默认的帮你生成共享的代码块

遇到的问题

1.TypeError: Cannot read property 'vue' of undefined (所有vue组件引入的时候都报如上错误)

解决方案:升级 vue-loader 到14.X版本

2.TypeError: Cannot read property 'babel' of undefined when going from v7 to v8 / Cannot find module '@babel/core' (babel升级版本问题)

解决方案: babel-loader 和 babel的版本需要对应,如webpack 3.X babel-loader 7.X | babel 6.X 或者 webpack 3.X babel-loader 8.X | babel 7.X

3.You may need an appropriate loader to handle this file type.

github解决方案 更新npm包不奏效 使用降级 npm install webpack@4.28.4

最终解决方案

分析:webpack@4.29 和 vue-loader@15 的锅,将webpack降级到4.28.4并vue-loader@14.2.2即可

目前只记得这些,当时几个问题卡了我一天的好不...

开启Gzip 并开启nginx缓存

Gzip

new CompressionWebpackPlugin({
        asset: '[path].gz[query]',
        algorithm: 'gzip',
        test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')$'),
        threshold: 10240,
        // deleteOriginalAssets: false, //删除源文件,不建议
        minRatio: 0.8
      })

这个需要nginx配置配合哦

nginx缓存自己想设置个策略,还没有系统规划好,后期会加上

使用serverWorker进行离线缓存

刚开始自己封装的使用的时候发现一个很严重的问题,每次build之后生成的资源文件的hash不一致需要手动替换,尝试后没有发现很好的解决方案,最后无意中发现两个插件,妈妈发再也不用担心我sw玩的不溜了~遛~

推荐使用两个插件

"sw-precache-webpack-plugin": "^0.11.5",
"sw-register-webpack-plugin": "^1.0.21",

BundleAnalyzerPlugin插件分析buid文件

使用这个插件,可以很明了的分析那块可以进行优化(js优化贼溜)

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
      .BundleAnalyzerPlugin
    webpackConfig.plugins.push(
      new BundleAnalyzerPlugin({
        analyzerMode: 'server',
        analyzerHost: '127.0.0.1',
        analyzerPort: 9999,
        reportFilename: 'report.html',
        defaultSizes: 'parsed',
        openAnalyzer: true,
        generateStatsFile: false,
        statsFilename: 'stats.json',
        statsOptions: null,
        logLevel: 'info'
      })
    )

小结

虽然上上去了,但是还存在一些问题,有空在优化和添加新的功能吧,通过这次对blog的同构,引入serverWorker以及将webpack整理和升级,还有就是nginx配置,收获还是很多的,今后有空也要开始写点东西了,大家有什么意见或建议,欢迎批评指正!(轻轻拍砖,人家还是个孩子)

Bye-bye~

comments