前言

本文分享了笔者在过往落地SSR时的一个不同落地思路,心愿通过此文给尝试落地SSR的团队多一个能够尝试的方向。本文以VUE框架的实现为例阐明整体落地过程,其它框架可在渲染层进行适配解决。

背景

自己之前所在团队业务次要为做C端为主,SEO相干的我的项目也在自己所在团队,在做SEO我的项目的过程中一直演化出SSR的服务化计划,通过本文简要介绍服务化形式的次要思路。

整体的技术计划

先上整体技术计划的设计图

在用户拜访层通过Nginx解决当SSR出问题或以后页面不须要SSR时进行降级到CSR。
在落地页我的项目中须要做的改变相对来说较低,首相须要在落地页我的项目中形象出store一层,通过store一层解决csr/ssr中的页面初始数据获取/注入的问题。其次在落地页我的项目中入口文件将主页面组件裸露进来,以便在SSR服务器中获取到以后页面的组件进行渲染。
在SSR服务的实现中以外围 + 插件的模式提供服务。底层外围性能负责解决以后申请的渲染逻辑,返回提供给用户的html内容。插件实现与业务相对来说关系没那么严密,然而对业务有加强作用的一些能力。绿色局部重的组件为所在团队的一些相干业务插件。

Nginx局部

nginx局部次要实现SSR/CSR的降级解决。如果以后申请须要SSR则进行SSR解决,如果以后申请不须要SSR解决则返回给nginx解决返回给用户CSR相干的实现。
在实现的过程中所在域名的所有前端申请皆会由Nginx反向代理到SSR服务上,SSR服务不须要解决或者解决出问题时皆抛出异样返回状态码给Nginx,在nginx上通过error_page配置将申请降级为CSR解决。配置大略如下:

proxy_intercept_errors  on;error_page      400 404 500 502 = @local;

其中@local为解决返回CSR动态资源的配置。

落地页我的项目相干解决

在SSR服务化的计划中因为SSR服务须要抓取落地页我的项目的html及js内容进行渲染解决,为了不便SSR服务抓取解决js,在落地页我的项目中对打包的js资源进行了3方面的解决:

  1. 将js资源打包为library,个别格局为umd,不便抓取后进行解决。
  2. 入口js进行CSR/SSR兼容解决。在SSR页面中会注入$$SSR的环境变量,如果页面为CSR则进行mount解决,因为SSR没有mount操作,所以无需进行mount解决。将入口的js对象裸露进来,不便SSR服务抓取js对象进行渲染解决。
  3. 形象出store一层,进行页面初始化数据的获取,在store一层进行CSR/SSR的兼容解决,让开发人员在开发的时候只须要依照标准解决页面初始数据申请即可。

其中1与其余js library打包没有区别,此处不做阐明。
2的解决较为简单,大抵如下:

function render() { // 渲染函数,不便服务端抓取进行渲染解决。  return new Vue({    router,    render: h => h(App)  });}if (!window.$$ssr) { // SSR服务会在页面注入$$SSR环境变量,不便做解决  render().$mount('#app')}export default {  render, // 服务端抓取该函数进行渲染解决  el: '#app', // 服务端抓取该配置将渲染后的dom string增加到该节点下  router, // 裸露路由不便服务端mock用户端的申请。  store: { // 此处为3形象的store层,在本例中不便解决没有将store独自抽出一个文件解决。    '/home': store,  }}

其中3形象的store一层本例中基于vue进行形象,次要性能为解决CSR/SSR的兼容。store的外围代码如下:

import Vue from 'vue';export default function createStore(store) {    const _vm = new Vue({ data: { state: store.state } });    store.state = _vm.$data.state;    return {        data() {            return {                store            }        },        computed: {            $store() {                return this.store.state            }        },       async beforeCreate () { // 因为vue的SSR只执行beforeCreate和created2个生命周期,本例中在beforeCreated这个节点进行页面初始化数据的抓取和解决。最初返回state次要为不便服务端抓取数据进行解决。            const state = window.__INITIAL_STATE__ || await store.init();            this.$set && this.$set(store, 'state', state);            return state;        }    }}

下面代码最为createStore的办法,实现较为简单,能够依据团队的具体情况进行封装,在具体的业务页面中store层通过调用该办法创立store,页面中应用到的初始数据也从store层进行抓取解决,页面中的store层代码大抵如下:

import createStore from '../store';import axios from 'axios';export default createStore({    state: { name: 'csr' },    async init() {        const { data } = await axios.get('/api/test.json');        return data;    }})

页面应用store的代码大抵如下:

<template>  <div @click="onClickHandler">    {{ store.state.name }}  </div></template><script>import store from './store';export default {  mixins: [store],  methods: {    onClickHandler() {      alert('click')    }  }}</script>

至此C端我的项目的相干革新曾经实现,在启动时拜访页面对应的路由即可看见页面初始状态相干的信息。

SSR服务端计划

在服务端插件零碎次要是基于koa的洋葱模型实现,此处不做过多阐明,可参照相干的koa或者egg js等文章查看详情。服务端次要实现为下半局部的沙箱环境解决,因为js是从落地页进行抓取的,无奈确保js代码的安全性,并且须要在服务端模仿用户拜访的环境及相干的操作,所以相干渲染局部的代码都运行在js沙箱中。执行渲染的过程次要分为2局部:

  1. 模仿用户拜访的环境
  2. 资源抓取和渲染

其中第一局部模仿用户拜访的环境次要基于jsdom实现,大抵代码如下:

import { JSDOM, CookieJar, ResourceLoader } from 'jsdom';// 基于Egg JS的示例代码export default function createMockObject(html) {    const { ctx } = this;    const cookieJar = new CookieJar();    (ctx.request.header.cookie as string || '').split(';').forEach(cookie => {        if (cookie) cookieJar.setCookie(cookie, `${ctx.request.protocol}://${ctx.request.host}/`, {}, noop)    })    const dom = new JSDOM(html, {        url: ctx.request.href,        referrer: ctx.referrer,        contentType: "text/html",        cookieJar,        resources: new ResourceLoader({        userAgent: ctx.get('user-agent')        })    });    const window = dom.window;    const exports = {};    const module = { exports };    window.$$ssr = true;    return {        window,        ...window,        XMLHttpRequest: window.XMLHttpRequest,        exports,        module    };}

在模仿用户行为的过程中以后次要应用到的环境会是用户的cookie信息,ua信息以及接口申请的xhr对象等。

第二局部的资源抓取和渲染次要分为抓取和渲染2局部,资源抓取应用http申请进行抓取。html资源的话与以后解决的申请统一,在此处抓取资源时需注意解决,如果域名也统一的话在抓取时可在连贯上增加参数解决,免得SSR服务陷入申请的死循环中。js资源的话能够以配置文件的模式配置申请的url地址对应的抓取资源地址,之后基于http申请进行js资源的抓取。数据抓取局部次要由服务端在抓取到js资源后依据以后申请的path获取对应的store层,通过store层的办法调用 + mock的xhr对象进行数据抓取。资源抓取之后即能够应用vue的ssr renderer组件进行渲染并拼接dom string返回给用户。
在此过程中抓取html和js资源的过程较为简单,此处暂不做阐明,次要问题为抓取到js后的解决和数据抓取,其中抓取到js后的处理过程次要如下:

const script: vm.Script = new vm.Script(esModuleJS);const ctx = vm.createContext(obj);script.runInNewContext(ctx);return (ctx as any).module.exports.default;
const script = await ctx.service.sandbox.run(js, dom); // js 为抓取到的js文件代码,dom为下面createMockObject函数返回的mock对象。此处实现代码在下面代码块ctx.body = await ctx.service.render.render(script, dom);// 上面代码为render service中的办法,本文中以egg js为例实现。public getDocType(docType: DocumentType) {    return '<!DOCTYPE '            + docType.name            + (docType.publicId ? ' PUBLIC "' + docType.publicId + '"' : '')            + (!docType.publicId && docType.systemId ? ' SYSTEM' : '')             + (docType.systemId ? ' "' + docType.systemId + '"' : '')            + '>';}public async render(js, mock) {    const { document, window } = mock;    const data = await this.fetchData(js); // 模仿用户申请,进行页面初始数据的抓取    window.__INITIAL_STATE__ = data;    const vnode = await this.getVnode(js);    const appDom = document.querySelector(js.el);    const fragment = document.createElement('div');    fragment.innerHTML = await renderer.renderToString(vnode);    appDom.parentNode.insertBefore(fragment.childNodes[0], appDom);    appDom.remove();    const script = document.createElement('script'); // SSR数据注入,在页面激活时绕过申请间接应用SSR返回的数据    script.type = 'text/javascript';    script.text = `window.__INITIAL_STATE__ = ${JSON.stringify(data)}`;    document.head.appendChild(script);    return `${this.getDocType(document.doctype)}${document.documentElement.outerHTML}`;}private async fetchData(js) { // 数据抓取    const store = js.store[this.ctx.request.path];    if (!store) return undefined;    return await store.beforeCreate();}private async getVnode(js) {    return new Promise((resolve, reject) => {        const { render, router } = js;        const app = render();        router.onReady(() => {            // 匹配不到的路由,执行 reject 函数,并返回 404            if (!router.getMatchedComponents().length) {                return reject({ code: 404 });            }                resolve(app);        }, reject)    });}

在抓取到js之后次要将js代码在以后用户的mock环境沙箱中执行,获取到对应的js对象,并获取对象中的相干办法及变量进行解决,获取到以后页面的相干数据及vnode对象,由vue-server-renderer组件将vnode对象转为dom string并append到以后页面中,至此SSR的服务化大抵思路曾经实现,在该计划中可实现SSR服务的高低线对用户无感知,并且开发人员只减少对store层的简略了解即可反对开发的页面进行SSR解决,根本能够做到对开发人员无感知。其中还有很多问题须要持续解决,比方SSR服务的治理,缓存解决等问题。

最初

本文写的相对来说比拟简陋,有不明确的中央可随时发表评论探讨。