美文网首页it编程
现有vue-cli3搭建的vue项目改ssr服务器渲染

现有vue-cli3搭建的vue项目改ssr服务器渲染

作者: ShanksZeng | 来源:发表于2021-01-04 18:28 被阅读0次

    项目简介

    vue+node+koa2

    安装ssr依赖

    npm install vue-server-renderer webpack-node-externals cross-env --save-dev
    npm install koa koa-static vuex-router-sync --save
    

    目录结构

    其中[entry-client.js] [entry-server.js] [index.template.html] [server.js]为新增文件
    目录结构
    文件内容

    【entry-client.js】

    import createApp from "./main";
    
    const { app, router, store } = createApp(window);
    
    if (window.__INITIAL_STATE__) {
      store.replaceState(window.__INITIAL_STATE__);
    }
    
    router.onReady(() => {
      // 添加路由钩子函数,用于处理 asyncData.
      // 在初始路由 resolve 后执行,
      // 以便我们不会二次预取(double-fetch)已有的数据。
      // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
      router.beforeResolve((to, from, next) => {
        const matched = router.getMatchedComponents(to);
        const prevMatched = router.getMatchedComponents(from);
    
        // 我们只关心非预渲染的组件
        // 所以我们对比它们,找出两个匹配列表的差异组件
        let diffed = false;
        const activated = matched.filter((c, i) => {
          return diffed || (diffed = prevMatched[i] !== c);
        });
    
        if (!activated.length) {
          return next();
        }
    
        // 这里如果有加载指示器 (loading indicator),就触发
    
        Promise.all(
          activated.map((c) => {
            if (c.asyncData) {
              return c.asyncData({ store, route: to });
            }
          })
        )
          .then(() => {
            // 停止加载指示器(loading indicator)
    
            next();
          })
          .catch(next);
      });
    
      app.$mount("#app");
    });
    

    【entry-server.js】

    import createApp from "./main";
    
    export default context => {
      return new Promise((resolve, reject) => {
        const { app, router, store } = createApp();
        // 注入用户信息
        if(context.userInfo) {
            store.state.userInfo = JSON.parse(context.userInfo)
            store.state.token = store.state.userInfo.token
        }
        router.push(context.url);
        router.onReady(() => {
          const matchedComponents = router.getMatchedComponents();
          if (!matchedComponents.length) {
              
            return reject({ code: 404 });
          }
    
          // 对所有匹配的路由组件调用 `asyncData()`
          Promise.all(
            matchedComponents.map((Component) => {
                if (Component.asyncData) {
                    return Component.asyncData({
                        store,
                        route: router.currentRoute
                    });
                }
            })
          ).then(() => {
              // 在所有预取钩子(preFetch hook) resolve 后,
              // 我们的 store 现在已经填充入渲染应用程序所需的状态。
              // 当我们将状态附加到上下文,
              // 并且 `template` 选项用于 renderer 时,
              // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
              // 动态TDK
              context.title = store.state.title + ' - ' + store.state.globalConfig.public.seotitle;
              context.keywords = store.state.globalConfig.public.keyword
              context.description = store.state.globalConfig.public.description
              context.state = store.state
              resolve(app);
            })
            .catch(reject);
        }, reject);
      });
    };
    

    【index.template.html】

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="keywords" content="{{keywords}}">
        <meta name="description" content="{{description}}">
        <title>{{title}}</title>
      </head>
      <body>
        <!--vue-ssr-outlet-->
      </body>
    </html>
    

    【server.js】

    const fs = require("fs");
    const Koa = require("koa");
    const path = require("path");
    const koaStatic = require("koa-static");
    const app = new Koa();
    
    const resolve = (file) => path.resolve(__dirname, file);
    // 开放dist目录
    app.use(koaStatic(resolve("./dist/client")));
    
    // 第 2 步:获得一个createBundleRenderer
    const { createBundleRenderer } = require("vue-server-renderer");
    const serverBundle = require("./dist/server/vue-ssr-server-bundle.json");
    const clientManifest = require("./dist/client/vue-ssr-client-manifest.json");
    
    const renderer = createBundleRenderer(serverBundle, {
      runInNewContext: false,
      template: fs.readFileSync(
        path.resolve(__dirname, "./src/index.template.html"),
        "utf-8"
      ),
      clientManifest,
    });
    
    function renderToString(context) {
      return new Promise((resolve, reject) => {
        renderer.renderToString(context, (err, html) => {
          err ? reject(err) : resolve(html);
        });
      });
    }
    
    function getCookie(cookie) {
        let cookieObj = {}
        let cookies = cookie ? cookie.split(';') : []
        if (cookies.length > 0) {
            cookies.forEach(item => {
                if (item) {
                    let cookieArray = item.split('=')
                    if (cookieArray && cookieArray.length > 0) {
                        let key = cookieArray[0].trim()
                        let value = cookieArray[1] ? cookieArray[1].trim() : undefined
                        cookieObj[key] = value
                    }
                }
            })
        }
        return cookieObj
    }
    
    // 第 3 步:添加一个中间件来处理所有请求
    app.use(async (ctx, next) => {
      const context = {
        title: "默认title",
        url: ctx.url,
      };
      // cgi请求,前端资源请求不能转到这里来。这里可以通过nginx做
      if (/\.\w+$/.test(context.url)) {
        return next
      }
      const cookieObj = getCookie(ctx.header.cookie)
      if(cookieObj.userInfo) {
        context.userInfo = decodeURIComponent(cookieObj.userInfo)
      }
      // 将 context 数据渲染为 HTML
      const html = await renderToString(context);
      ctx.body = html;
    });
    
    
    /*服务启动*/
    const port = 3000;
    app.listen(port, function() {
      console.log(`server started at localhost:${port}`);
    })
    
    
    其中[store.js] [router.js] [main.js] [action.js] [vue.config.js] [package.json]需要进行修改
    文件修改内容

    【store.js】

    import Vue from 'vue'
    import Vuex from 'vuex'
    import mutations from "./vuex/mutations"
    import actions from './vuex/actions'
    Vue.use(Vuex)
    export default function createStore() {
        return new Vuex.Store({
            state: {
                token: null,
                userInfo: {},
                homeData: {},
                title: ''
            },
            mutations,
            actions
        })
    }
    

    【router.js】

    import Vue from 'vue'
    import Router from 'vue-router'
    
    Vue.use(Router)
    
    const routes = [
        {
            path: '/',
            component: () => import('./views/index'),
            children: [
                {
                    path: '/',
                    name: 'index',
                    component: () => import('./views/home')
                }
                {
                    path: 'login',
                    name: 'login',
                    component: () => import('./views/login')
                },
                {
                    path: 'user',
                    name: 'user',
                    component: () => import('./views/user')
                }
            ]
        }
    ]
    export default function createRouter() {
        return new Router({
            mode: 'history',
            base: process.env.BASE_URL,
            routes,
            scrollBehavior (to, from, savedPosition) {
                return { x: 0, y: 0 }
            }
        })
    }
    

    【main.js】

    import Vue from 'vue'
    import App from './App.vue'
    import './assets/less/base.less'
    import createRouter from "./router";
    import createStore from "./store";
    import { sync } from "vuex-router-sync"
    
    Vue.config.productionTip = false
    
    Vue.prototype.$routerOpen = (page) => {
        const router = createRouter()
        let routeUrl = router.resolve(page)
        //    window.open(routeUrl.href, '_blank')
        window.location.href = routeUrl.href
    }
    export default function createApp(window) {
        // 创建 router 和 store 实例
        const router = createRouter();
        const store = createStore(window);
      
        // 同步路由状态(route state)到 store
        sync(store, router);
      
        const app = new Vue({
            router,
            store,
            render: h => h(App)
        }).$mount('#app')
        return { app, router, store };
      }
    

    【action.js】

    import { homeCase, homeHotProjects, homeHotState, newslist } from '@/api/request'
    const actions = {
        getHomeData({ commit }) {
            return Promise.all([homeCase(), homeHotProjects(), homeHotState(), newslist()]).then(res => {
                commit('getHomeData', res)
            })
        }
    }
    export default actions
    

    【vue.config.js】

    /*
     * @Author: your name
     * @Date: 2020-07-01 17:17:36
     * @LastEditTime: 2020-12-24 11:09:24
     * @LastEditors: Please set LastEditors
     * @Description: In User Settings Edit
     * @FilePath: \immigrant\vue.config.js
     */
    const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
    const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
    const nodeExternals = require("webpack-node-externals");
    const env = process.env;
    const isServer = env.RUN_ENV === "server";
    
    module.exports = {
        publicPath: './',
        lintOnSave: false, //是否开启eslint
        devServer: {
            disableHostCheck: true,
            proxy: {
                '/localhost': {
                    target: 'http://xxx.com', //API服务器的地址
                    changeOrigin: true, // 虚拟的站点需要更管origin
                    pathRewrite:{
                        '^/localhost':''
                    }
                }
            },
        },
        outputDir: `dist/${env.RUN_ENV}`,
        configureWebpack: {
            // 将 entry 指向应用程序的 server / client 文件
            entry: `./src/entry-${env.RUN_ENV}.js`,
            devtool: "eval",
            // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
            // 并且还会在编译 Vue 组件时,
            // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
            target: isServer ? "node" : "web",
            // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
            output: {
              libraryTarget: isServer ? "commonjs2" : undefined,
            },
            // https://webpack.js.org/configuration/externals/#function
            // https://github.com/liady/webpack-node-externals
            // 外置化应用程序依赖模块。可以使服务器构建速度更快,
            // 并生成较小的 bundle 文件。
            externals: isServer
              ? nodeExternals({
                // 不要外置化 webpack 需要处理的依赖模块。
                // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
                // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
                allowlist: /\.css$/,
              })
              : undefined,
            optimization: { splitChunks: isServer ? false : undefined },
            // 这是将服务器的整个输出
            // 构建为单个 JSON 文件的插件。
            // 服务端默认文件名为 `vue-ssr-server-bundle.json`
            // 客户端默认文件名为 `vue-ssr-client-manifest.json`
            plugins: [isServer ? new VueSSRServerPlugin() : new VueSSRClientPlugin()],
        }
    }
    

    【package.json】

    "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "lint": "vue-cli-service lint",
        "start": "npm run build:server && npm run build:client && npm run service",
        "build:client": "cross-env RUN_ENV=client vue-cli-service build",
        "build:server": "cross-env RUN_ENV=server vue-cli-service build --mode server",
        "service": "node server.js"
      }
    
    组件中获取数据方式 (homeData可直接用于页面数据渲染)
    export default {
        data() { 
            return {}
        },
        asyncData({store, route}) {
            return Promise.all([
                store.dispatch("getHomeData")
            ])
        },
        computed: {
            homeData() {
                return this.$store.state.homeData
            }
        }
    }
    

    运行

    npm run start(编译加运行起服务)
    npm run service(单独运行起服务)

    注意

    本文章本用于作者笔记,所以非常之简陋,如遇问题或有疑问可一起讨论研究。

    运行问题

    (1)编译失败提示依赖库版本不一致,根据报错提示重新安装依赖保证版本一致即可

    相关文章

      网友评论

        本文标题:现有vue-cli3搭建的vue项目改ssr服务器渲染

        本文链接:https://www.haomeiwen.com/subject/grpvoktx.html