vue3+vite+ts+koa2构建vue3 SSR项目

前言

我司之前部分H5为了追求极致的首屏渲染速度,使用Next.js去开发SSR项目。

去年对我司技术栈升级到Vue3之后,萌生了对SSR项目升级到Nuxt3的想法,奈何当时Nuxt3还不稳定,不建议在生产环境使用。于是打算自己搭个Vue3 SSR服务出来。

长话短说,直接开干

一、初始化项目

npm create vite vue3-ssr -- --template vue-ts

二、修改客户端入口

1.修改~/src/main.ts

import { createSSRApp } from 'vue'
import App from './App.vue'

// 因为每次请求都会到达服务器,为了数据不会被相互污染,
// 每次请求我们都需要使用工厂函数创建新的实例,每次都返回全新的vue, router, store等
export const createApp = () => {
    const app = createSSRApp(App)
    return { app }
}

2.新建~/src/entry-client.ts

import { createApp } from './main'

const { app } = createApp()

app.mount("#app")

3.修改~/index.html的入口




    ...

    

    ...

上述文件修改完,你会发现仅仅是把main.ts文件拆分出了一个客户端入口文件。其他什么都没改变。所以可直接运行npm run dev工程就可以运行起来了。~/src/entry-client.ts文件只是替换了main.ts作为项目的入口。

三、创建开发服务器

使用Koa2

1.安装Koa

npm i koa && npm i @types/koa -D

2.新建~/server.js

const Koa = require('koa')

;(async () => {
    const app = new Koa()
    
    app.use(async (ctx) => {
        ctx.body = `
      
        vue3 SSR
        
          

vue3 SSR

`; }); const port = 3000 app.listen(port, () => { console.log(`server is listening in ${port}`) }); })();

3.运行node server.js

4.结果:

vue3+vite+ts+koa2构建vue3 SSR项目_第1张图片

 四、渲染替换成项目根目录下的index.html

1.修改server.js服务返回的是Index.html

 const fs = require('fs')
 const path = require('path')
 ​
 const Koa = require('koa')
 ​
 ;(async () => {
     const app = new Koa()
 ​
     // 获取 index.html
     const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8')
 ​
     app.use(async (ctx) => {
         ctx.body = template
     })
 ​
     const port = 3000
     app.listen(port, () => {
        console.log(`server is listening in ${port}`)
     });
 })();

2.运行node server.js后, 我们就会看到对应的index.html内容 了,但是我们需要返回的是 vue模板 ,那么我们只需要做个正则的替换。

3.给index.html添加标记,作为之后替换的占位符。

 
 
   
     
     
     
     vue3 ssr
   
   
     
           

4.修改server.js中的ctx.body

...
(async () => {
    const app = new Koa()

    // 获取index.html
    const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8')

    app.use(async (ctx) => {
        let vueTemplate = '

假装这是一个vue模板

' // 替换 index.html 中的 标记 let html = template.replace('', vueTemplate) ctx.body = html }) const port = 3000 app.listen(port, () => { console.log(`server is listening in ${port}`) }); })();

5.运行node server.js后,就能看到返回的是变量vueTemplate的内容

那么到现在服务已正常启动了,但是我们页面模板使用的是 vue,并且 vue返回的是一个 vue实例模板,所以我就要把这个vue实例模板转换成可渲染的html,那么就要用到@vue/server-renderer了。

五、新增服务端入口

1.安装@vue/server-renderer

npm i @vue/server-renderer --save

2.新建~/src/entry-server.ts

import { createApp } from './main'
import { renderToString } from '@vue/server-renderer'

export const render = async () => {
  const { app } = createApp()
	
  const renderCtx: {modules?: string[]} = {}

  let renderedHtml = await renderToString(app, renderCtx)

  return { renderedHtml }
}

六、注入Vite

1.修改~/server.js

const fs = require('fs')
const path = require('path')

const Koa = require('koa')
const koaConnect = require('koa-connect')

const vite = require('vite')

;(async () => {
    const app = new Koa()

    // 创建 vite 服务
    const viteServer = await vite.createServer({
        root: process.cwd(),
        logLevel: 'error',
        server: {
        middlewareMode: true,
        },
    })

    app.use(async ctx => {
        try {
            // 1. 获取index.html
            let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');

            // 2. 应用 Vite HTML 转换。这将会注入 Vite HMR 客户端,
            template = await viteServer.transformIndexHtml(ctx.path, template)

            // 3. 加载服务器入口, vite.ssrLoadModule 将自动转换
            const { render } = await viteServer.ssrLoadModule('/src/entry-server.ts')

            //  4. 渲染应用的 HTML
            const { renderedHtml } = await render(ctx, {})

            const html = template.replace('', renderedHtml)

            ctx.type = 'text/html'
            ctx.body = html
        } catch (e) {
            viteServer && viteServer.ssrFixStacktrace(e)
            console.log(e.stack)
            ctx.throw(500, e.stack)
        }
    })

    const port = 3000
    app.listen(port, () => {
        console.log(`server is listening in ${port}`);
    });

})()

2.运行node server.js就可以看到返回的是App.vue模板里的内容了

到这里我们就已经在开发环境可以正常的渲染了但在生产环境应该怎么做呢,因为咱们不可能直接在生产环境使用vite吧!

所以咱们接下来处理如何在生产环境运行吧

七、增加生产环境服务

1.修改server.js文件名为server-dev.js

2.修改package.json

...

{
"scripts": {
    // 开发环境
    "dev": "node server-dev.js",
    // 生产环境
    "server": "node server-prod.js",
    // 构建
    "build": "pnpm build:client && pnpm build:server",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --ssr src/entry-server.ts --outDir dist/server",
  },
}

...

3.新增~/server-prod.js

注意:为了处理静态资源,需要新增 koa-send 中间件: pnpm i koa-send --save

const Koa = require('koa')
const sendFile = require('koa-send')

const path = require('path')
const fs = require('fs')

const resolve = (p) => path.resolve(__dirname, p)

const clientRoot = resolve('dist/client')
const template = fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
const render = require('./dist/server/entry-server.js').render
const manifest = require('./dist/client/ssr-manifest.json')

;(async () => {
    const app = new Koa()

    app.use(async (ctx) => {
				
		// 请求的是静态资源
        if (ctx.path.startsWith('/assets')) {
            await sendFile(ctx, ctx.path, { root: clientRoot })
            return
        }

        const [ appHtml ] = await render(ctx, manifest)

        const html = template
            .replace('', appHtml)

        ctx.type = 'text/html'
        ctx.body = html
    });

    const port = 8080
    app.listen(port, () => {
        console.log(`started server on http://localhost:${port}`))
    }
})();

到这里,我们在开发环境生成环境已经都可以正常访问了,那么是不是就万事无忧了呢?

为了用户的更极致的用户体验,那么预加载就必须要安排了。

八、预加载

我们知道vue组件html中渲染时都是动态去生成的对应的jscss等;

那么我们要是在用户获取服务端模板(也就是执行 vite build 后生成的dist/client目录) 的时候,直接在html中把对应的jscss文件预渲染了,这就是静态站点生成(SSG) 的形式。

1.增加指令:在package.json中的build:client添加--ssrManifest标志,运行后生成ssr-manifest.json

...

{
"scripts": {
    ...
    "build:client": "vite build --ssrManifest --outDir dist/client",
    ...
  },
}

...

 2.在entry-server.ts中添加解析生成的ssr-manifest.json方法

export const render = async (
    ctx: ParameterizedContext,
    manifest: Record
): Promise<[string, string]> => {
    const { app } = createApp();
    console.log(ctx, manifest, '');

    const renderCtx: { modules?: string[] } = {};

    const renderedHtml = await renderToString(app, renderCtx);

    const preloadLinks = renderPreloadLinks(renderCtx.modules, manifest);

    return [renderedHtml, preloadLinks];
};

/**
 * 解析需要预加载的链接
 * @param modules
 * @param manifest
 * @returns string
 */
function renderPreloadLinks(
    modules: undefined | string[],
    manifest: Record
): string {
    let links = '';
    const seen = new Set();
    if (modules === undefined) throw new Error();
    modules.forEach((id) => {
        const files = manifest[id];
        if (files) {
            files.forEach((file) => {
                if (!seen.has(file)) {
                    seen.add(file);
                    links += renderPreloadLink(file);
                }
            });
        }
    });
    return links;
}

/**
 * 预加载的对应的地址
 * 下面的方法只针对了 js 和 css,如果需要处理其它文件,自行添加即可
 * @param file
 * @returns string
 */
function renderPreloadLink(file: string): string {
    if (file.endsWith('.js')) {
        return ``;
    } else if (file.endsWith('.css')) {
        return ``;
    } else {
        return '';
    }
}

3.在index.html添加 占位符

4.修改server-prod.js


...

(async () => {
    const app = new Koa();

    app.use(async (ctx) => {
				
	...

        const [appHtml, preloadLinks] = await render(ctx, manifest);

        const html = template
            .replace('', preloadLinks)
            .replace('', appHtml);
    });

    const port = 8080
    app.listen(port, () => {
        console.log(`started server on http://localhost:${port}`));
    }
})();

5.运行npm run build && npm run server就可以正常显示了

到这里基本的渲染就完成了,因为我们是需要在浏览器上渲染的,所以要加上路由vue-router

九、集成vue-router

1.安装vue-router

npm i vue-router

2.新增路由页面page1.vue,page2.vue,page3.vue

3.新增~/src/router/index.ts

import {
    createRouter as createVueRouter,
    createMemoryHistory,
    createWebHistory,
    Router
} from 'vue-router'

export const createRouter = (type: 'client' | 'server'): Router =>
    createVueRouter({
        history: type === 'client' ? createWebHistory() : createMemoryHistory(),
        routes: [
            {
                path: '/page1',
                name: 'page1',
                meta: {
                    title: 'page1',
                    keepAlive: true,
                    requireAuth: true
                },
                component: () => import('@/pages/page1.vue')
            },
            {
                path: '/page2',
                name: 'page2',
                meta: {
                    title: 'page2',
                    keepAlive: true,
                    requireAuth: false
                },
                component: () => import('@/pages/page2.vue')
            },
            {
                path: '/page3',
                name: 'page3',
                meta: {
                    title: 'page3',
                    keepAlive: true,
                    requireAuth: true
                },
                component: () => import('@/pages/page3.vue')
            }
        ]
    });

4.修改入口文件~/src/entry-client.ts

import { createApp } from './main'
import { createRouter } from './router'
const router = createRouter('client')

const { app } = createApp()

app.use(router)

router.isReady().then(() => {
    app.mount('#app', true)
});

5.修改入口文件~/src/entry-server.ts

...
import { createRouter } from './router'
const router = createRouter('client')

export const render = async (
    ctx: ParameterizedContext,
    manifest: Record
): Promise<[string, string]> => {
    const { app } = createApp()

    // 路由注册
    const router = createRouter('server')
    app.use(router)
    await router.push(ctx.path)
    await router.isReady()

    ...
};

...

6.运行npm run build && npm run server就可以正常显示了

最后

可以参考vite对于vue-ssr实现的示例

友情提示:以上都是对于升级vue3 SSR的预演,实际并未采用,因为目前Vite的SSR支持还处于试验阶段,可能会遇到一些未知 bug ,所以在公司的生产环境请谨慎使用,个人项目请随意。

你可能感兴趣的:(javascript,前端,vue.js)