教你如何搭建 Vue SEO 的 SSR

# 前言

VueSEO 大家都不陌生,一般有两种,一种是预渲染插件 prerender-spa-plugin,主要用于只需要部分页面的情形。另一种是后端渲染 Vue SSR,比较彻底的一种方案,比较受欢迎。

# 举个例子

SSR 也并没有大家想的那么深奥,上代码。。。

第一步:安装插件 vue-server-renderer、webpack-node-externals、lodash.merge、cross-env

npm i --save-dev vue-server-renderer webpack-node-externals lodash.merge cross-env

第二步:更改路由导出

const router = new Router({
    mode: 'history',
    routes,
});

// ssr 输出
export default function createRouter() {
    return new Router({
        mode: 'history',
        routes,
    });
}

// 普通输出
// export default router;

第三步:在 src 根目录下创建 app.js

// 创建 vue 实例
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'

export default function createApp() {
    const router = createRouter();
    const app = new Vue({
        router,
        render: h => h(App)
    });

    return { app, router }
}

第四步:在 src 目录下分别创建 entry-client.js、 entry-server.js

// entry-client.js ==> 挂载、激活app
import createApp from './app'

const { app, router } = createApp();

router.onReady(() => {
    app.$mount('#app');
})

// entry-server.js ==> 将来渲染首屏
import createApp from './app'

export default context => {
    return new Promise((resolve, reject) => {
        const { app, router } = createApp();
        // 进入到首屏
        router.push(context.url)
        router.onReady(() => {
            resolve(app);
        }, reject)
    });
}

第五步:配置 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 merge = require("lodash.merge");
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
    css: {
        extract: false,
    },
    outputDir: "./dist/" + target,
    configureWebpack: () => {
        if (process.env.WEBPACK_TARGET !== "node") return
        return ({
            entry: `/src/entry-${target}.js`,
            devtool: "source-map",
            target: TARGET_NODE ? "node" : "web",
            node: TARGET_NODE ? undefined : false,
            output: {
                libraryTarget: TARGET_NODE ? "commonjs2" : undefined
            },
            externals: TARGET_NODE ?
                nodeExternals({
                    // whitelist
                    allowlist: [/\.css$/]
                }) : undefined,
            optimization: {
                splitChunks: undefined
            },
            plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
        })
    },
    chainWebpack: config => {
        config.module.rule("vue").use("vue-loader").tap(options => {
            merge(options, { optimizeSSR: false })
        })
    }
};

第六步:与 src 平级,创建 server 文件夹并创建 index.js 文件

const clientManifest = require("../dist/client/vue-ssr-client-manifest.json");
const renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template: fs.readFileSync("../public/index.temp.html", "utf-8"), // 宿主文件
    clientManifest
});

app.use(express.static('../dist/client', { index: false }));

app.get("*", async(req, res) => {
    try {
        const context = {
            url: req.url,
            title: 'test title'
        }

        const html = await renderer.renderToString(context);

        res.send(html)
    } catch (error) {
        console.log(error)
    }
})

app.listen(3000, () => {
    console.log(123456)
})

第七步:修改 packsge.json 文件

"scripts": {
        "serve": "cross-env WEBPACK_TARGET=dev vue-cli-service serve",
        "build:client": "vue-cli-service build",
        "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
        "build": "npm run build:server && npm run build:client",
        "lint": "vue-cli-service lint"
},

第八步:在 public 文件夹下创建 index.temp.html

<!DOCTYPE html>
<html lang="">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">

    <title>ssr</title>
</head>

<body>
    <!--vue-ssr-outlet-->
</body>

</html>

注意:body 里必须这么写

第九步:运行打包

npm run build 

注意: 这里打包会执行两次:客户端 和 服务端

打包后的结果:


image.png

目录总览:


image.png

到这里 SSR 就创建完成了,最后启动服务 node index.js 看下下结果吧

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容