1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > 教你使用 koa2 + vite + ts + vue3 + pinia 构建前端 SSR 企业级项目

教你使用 koa2 + vite + ts + vue3 + pinia 构建前端 SSR 企业级项目

时间:2022-11-16 23:11:19

相关推荐

教你使用 koa2 + vite + ts + vue3 + pinia 构建前端 SSR 企业级项目

大厂技术高级前端Node进阶

点击上方程序员成长指北,关注公众号

回复1,加入高级Node交流群

前言

大家好,我是 易[1],在上一篇文章中,我们有讲到《如何使用 vite+vue3+ts+pinia+vueuse 打造前端企业级项目》[2],能看的出来很多同学喜欢,今天给大家带来爆肝许久的如何使用vite 打造前端 SSR 企业级项目,希望大家能喜欢!

如果大家对 Vite 感兴趣可以去看看专栏:《Vite 从入门到精通》[3]

了解 SSR

什么是 SSR

服务器端渲染(Server-Side Rendering)是指由服务端完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。

简单理解就是html是由服务端写出,可以动态改变页面内容,即所谓的动态页面。早年的 php[4]、asp[5] 、jsp[6] 这些 Server page 都是 SSR 的。

为什么使用 SSR

网页内容在服务器端渲染完成,一次性传输到浏览器,所以首屏加载速度非常快

有利于SEO,因为服务器返回的是一个完整的 html,在浏览器可以看到完整的 dom,对于爬虫、百度搜索等引擎就比较友好;

快速查看

github 仓库地址[7]

长话短说,直接开干 ~

建议包管理器使用优先级:pnpm > yarn > npm > cnpm

一、初始化项目

pnpmcreatevitekoa2-ssr-vue3-ts-pinia----templatevue-ts复制代码

集成基本配置

由于本文的重点在于SSR 配置,为了优化读者的观感体验,所以项目的基本配置就不做详细介绍,在我上一篇文章《手把手教你用 vite+vue3+ts+pinia+vueuse 打造企业级前端项目》[8]中已详细介绍,大家可以自行查阅

修改tsconfig.json:查看代码[9]

修改vite.config.ts:查看代码[10]

集成eslintprettier统一代码质量风格的:查看教程[11]

集成commitizenhusky规范 git 提交:查看教程[12]

到这里我们项目的基本框架都搭建完成啦~

二、修改客户端入口

修改 `~/src/main.ts`

import{createSSRApp}from"vue";importAppfrom"./App.vue";//为了保证数据的互不干扰,每次请求需要导出一个新的实例exportconstcreateApp=()=>{constapp=createSSRApp(App);return{app};}复制代码

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

import{createApp}from"./main"const{app}=createApp();app.mount("#app");复制代码

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

<!DOCTYPEhtml><htmllang="en">...<scripttype="module"src="/src/entry-client.ts"></script>...</html>复制代码

到这里你运行pnpm run dev,发现页面中还是可以正常显示,因为到目前只是做了一个文件的拆分,以及更换了createSSRApp方法;

三、创建开发服务器

使用 Koa2

安装 `koa2`

pnpmikoa--save&&pnpmi@types/koa--save-dev复制代码

安装中间件 `koa-connect`

pnpmikoa-connect--save复制代码

使用:新建~/server.js

备注:因为该文件为 node 运行入口,所以用 js 即可,如果用 ts 文件,需单独使用 ts-node 等去运行,导致程序变复杂

constKoa=require('koa');(async()=>{constapp=newKoa();app.use(async(ctx)=>{ctx.body=`<!DOCTYPEhtml><htmllang="en"><head><title>koa2+vite+ts+vue3+vue-router</title></head><body><h1style="text-align:center;">使用koa2+vite+ts+vue3+vue-router集成前端SSR企业级项目</h1></body></html>`;});app.listen(9000,()=>{console.log('serverislisteningin9000');});})();复制代码

运行node server.js

结果:

Untitled.png

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

修改 `server.js` 中的 `ctx.body` 返回的是 `index.html`

constfs=require('fs');constpath=require('path');constKoa=require('koa');(async()=>{constapp=newKoa();//获取index.htmlconsttemplate=fs.readFileSync(path.resolve(__dirname,'index.html'),'utf-8');app.use(async(ctx)=>{ctx.body=template;});app.listen(9000,()=>{console.log('serverislisteningin9000');});})();复制代码

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

给 `index.html` 添加 `<!--app-html-->` 标记

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"/><linkrel="icon"href="/favicon.ico"/><metaname="viewport"content="width=device-width,initial-scale=1.0"/><title>koa2+vite+ts+vue3</title></head><body><divid="app"><!--app-html--></div><scripttype="module"src="/src/entry-client.ts"></script></body></html>复制代码

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

//othercode...(async()=>{constapp=newKoa();//获取index.htmlconsttemplate=fs.readFileSync(path.resolve(__dirname,'index.html'),'utf-8');app.use(async(ctx)=>{letvueTemplate='<h1style="text-align:center;">现在假装这是一个vue模板</h1>';//替换index.html中的<!--app-html-->标记lethtml=template.replace('<!--app-html-->',vueTemplate);ctx.body=html;});app.listen(9000,()=>{console.log('serverislisteningin9000');});})();复制代码

运行node server.js后,我们就会看到返回的变量 vueTemplate内容

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

四、新增服务端入口

因为 vue 返回的是vue 实例模板而不是可渲染的 html,所以我们需要使用@vue/server-renderer进行转换

安装 `@vue/server-renderer`

pnpmi@vue/server-renderer--save复制代码

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

import{createApp}from'./main';import{renderToString}from'@vue/server-renderer';exportconstrender=async()=>{const{app}=createApp();//注入vuessr中的上下文对象constrenderCtx:{modules?:string[]}={}letrenderedHtml=awaitrenderToString(app,renderCtx)return{renderedHtml};}复制代码

那么如何去使用entry-server.ts呢,到这里就需要vite

五、注入vite

修改 `~/server.js`

constfs=require('fs')constpath=require('path')constKoa=require('koa')constkoaConnect=require('koa-connect')constvite=require('vite');(async()=>{constapp=newKoa();//创建vite服务constviteServer=awaitvite.createServer({root:process.cwd(),logLevel:'error',server:{middlewareMode:true,},})//注册 vite 的 Connect 实例作为中间件(注意:vite.middlewares 是一个 Connect 实例)app.use(koaConnect(viteServer.middlewares))app.use(asyncctx=>{try{//1.获取index.htmllettemplate=fs.readFileSync(path.resolve(__dirname,'index.html'),'utf-8');// 2. 应用 Vite HTML 转换。这将会注入 Vite HMR 客户端,template=awaitviteServer.transformIndexHtml(ctx.path,template)//3.加载服务器入口,vite.ssrLoadModule将自动转换const{render}=awaitviteServer.ssrLoadModule('/src/entry-server.ts')//4.渲染应用的HTMLconst{renderedHtml}=awaitrender(ctx,{})consthtml=template.replace('<!--app-html-->',renderedHtml)ctx.type='text/html'ctx.body=html}catch(e){viteServer&&viteServer.ssrFixStacktrace(e)console.log(e.stack)ctx.throw(500,e.stack)}})app.listen(9000,()=>{console.log('serverislisteningin9000');});})()复制代码

运行node server.js就可以看到返回的 App.vue 模板中的内容了,如下图

Untitled 1.png

并且我们 `右键查看显示网页源代码`,也会看到渲染的正常 html

<!DOCTYPEhtml><htmllang="en"><head><scripttype="module"src="/@vite/client"></script><metacharset="UTF-8"/><linkrel="icon"href="/favicon.ico"/><metaname="viewport"content="width=device-width,initial-scale=1.0"/><title>koa2+vite+ts+vue3</title></head><body><divid="app"><!--[--><imgalt="Vuelogo"src="/src/assets/logo.png"><!--[--><h1data-v-469af010>HelloVue3+TypeScript+Vite</h1><pdata-v-469af010>RecommendedIDEsetup:<ahref="</>"target="_blank"data-v-469af010>VSCode</a>+<ahref="</johnsoncodehk/volar>"target="_blank"data-v-469af010>Volar</a></p><pdata-v-469af010>See<codedata-v-469af010>README.md</code>formoreinformation.</p><pdata-v-469af010><ahref="<https://vitejs.dev/guide/features.html>"target="_blank"data-v-469af010>ViteDocs</a>|<ahref="</>"target="_blank"data-v-469af010>Vue3Docs</a></p><buttontype="button"data-v-469af010>countis:0</button><pdata-v-469af010>Edit<codedata-v-469af010>components/HelloWorld.vue</code>totesthotmodulereplacement.</p><!--]--><!--]--></div><scripttype="module"src="/src/entry-client.ts"></script></body></html>复制代码

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

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

六、添加开发环境

为了将 SSR 项目可以在生产环境运行,我们需要:

正常构建生成一个 `客户端构建包`;

再生成一个 SSR 构建,使其通过`require()`直接加载,这样便无需再使用 Vite 的`ssrLoadModule`;

修改 `package.json`

...{"scripts":{//开发环境"dev":"nodeserver-dev.js",//生产环境"server":"nodeserver-prod.js",//构建"build":"pnpmbuild:client&&pnpmbuild:server","build:client":"vitebuild--outDirdist/client","build:server":"vitebuild--ssrsrc/entry-server.js--outDirdist/server",},}...复制代码

修改server.jsserver-dev.js

运行pnpm run build构建包

新增server-prod.js

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

constKoa=require('koa');constsendFile=require('koa-send');constpath=require('path');constfs=require('fs');constresolve=(p)=>path.resolve(__dirname,p);constclientRoot=resolve('dist/client');consttemplate=fs.readFileSync(resolve('dist/client/index.html'),'utf-8');constrender=require('./dist/server/entry-server.js').render;constmanifest=require('./dist/client/ssr-manifest.json');(async()=>{constapp=newKoa();app.use(async(ctx)=>{//请求的是静态资源if(ctx.path.startsWith('/assets')){awaitsendFile(ctx,ctx.path,{root:clientRoot});return;}const[appHtml]=awaitrender(ctx,manifest);consthtml=template.replace('<!--app-html-->',appHtml);ctx.type='text/html';ctx.body=html;});app.listen(8080,()=>console.log('startedserveronhttp://localhost:8080'));})();复制代码

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

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

七、预加载

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

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

闲话少说,明白道理了之后,直接开干 ~

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

...{"scripts":{..."build:client":"vitebuild--ssrManifest--outDirdist/client",...},}...复制代码

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

exportconstrender=async(ctx:ParameterizedContext,manifest:Record<string,string[]>):Promise<[string,string]>=>{const{app}=createApp();console.log(ctx,manifest,'');constrenderCtx:{modules?:string[]}={};constrenderedHtml=awaitrenderToString(app,renderCtx);constpreloadLinks=renderPreloadLinks(renderCtx.modules,manifest);return[renderedHtml,preloadLinks];};/***解析需要预加载的链接*@parammodules*@parammanifest*@returnsstring*/functionrenderPreloadLinks(modules:undefined|string[],manifest:Record<string,string[]>):string{letlinks='';constseen=newSet();if(modules===undefined)thrownewError();modules.forEach((id)=>{constfiles=manifest[id];if(files){files.forEach((file)=>{if(!seen.has(file)){seen.add(file);links+=renderPreloadLink(file);}});}});returnlinks;}/***预加载的对应的地址*下面的方法只针对了js和css,如果需要处理其它文件,自行添加即可*@paramfile*@returnsstring*/functionrenderPreloadLink(file:string):string{if(file.endsWith('.js')){return`<linkrel="modulepreload"crossoriginhref="${file}">`;}elseif(file.endsWith('.css')){return`<linkrel="stylesheet"href="${file}">`;}else{return'';}}复制代码

给 `index.html` 添加 `<!--preload-links-->` 标记

改造 `server-prod.js`

...(async()=>{constapp=newKoa();app.use(async(ctx)=>{...const[appHtml,preloadLinks]=awaitrender(ctx,manifest);consthtml=template.replace('<!--preload-links-->',preloadLinks).replace('<!--app-html-->',appHtml);//dosomething});app.listen(8080,()=>console.log('startedserveronhttp://localhost:8080'));})();复制代码

运行pnpm run build && pnpm run serve就可正常显示了

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

八、集成 vue-router

安装 vue-router

pnpmivue-router--save复制代码

新增对应的路由页面 `index.vue` 、 `login.vue` 、 `user.vue`

新增 `src/router/index.ts`

import{createRouterascreateVueRouter,createMemoryHistory,createWebHistory,Router}from'vue-router';exportconstcreateRouter=(type:'client'|'server'):Router=>createVueRouter({history:type==='client'?createWebHistory():createMemoryHistory(),routes:[{path:'/',name:'index',meta:{title:'首页',keepAlive:true,requireAuth:true},component:()=>import('@/pages/index.vue')},{path:'/login',name:'login',meta:{title:'登录',keepAlive:true,requireAuth:false},component:()=>import('@/pages/login.vue')},{path:'/user',name:'user',meta:{title:'用户中心',keepAlive:true,requireAuth:true},component:()=>import('@/pages/user.vue')}]});复制代码

修改入口文件 `src/enter-client.ts`

import{createApp}from'./main';import{createRouter}from'./router';constrouter=createRouter('client');const{app}=createApp();app.use(router);router.isReady().then(()=>{app.mount('#app',true);});复制代码

修改入口文件 `src/enter-server.ts`

...import{createRouter}from'./router'constrouter=createRouter('client');exportconstrender=async(ctx:ParameterizedContext,manifest:Record<string,string[]>):Promise<[string,string]>=>{const{app}=createApp();//路由注册constrouter=createRouter('server');app.use(router);awaitrouter.push(ctx.path);awaitrouter.isReady();...};...复制代码

运行pnpm run build && pnpm run serve就可正常显示了

九、集成 pinia

安装

pnpmipinia--save复制代码

新建 `src/store/user.ts`

import{defineStore}from'pinia';exportdefaultdefineStore('user',{state:()=>{return{name:'张三',age:20};},actions:{updateName(name:string){this.name=name;},updateAge(age:number){this.age=age;}}});复制代码

新建 `src/store/index.ts`

import{createPinia}from'pinia';importuseUserStorefrom'./user';exportdefault()=>{constpinia=createPinia();useUserStore(pinia);returnpinia;};复制代码

新建 `UsePinia.vue` 使用,并且在 `pages/index.vue` 中引入

<template><h2>欢迎使用vite+vue3+ts+pinia+vue-router4</h2><div>{{ userStore.name }}的年龄:{{ userStore.age }}</div><br/><button@click="addAge">点击给{{userStore.name}}的年龄增加一岁</button><br/></template><scriptlang="ts">import{defineComponent}from'vue';importuseUserStorefrom'@/store/user';exportdefaultdefineComponent({name:'UsePinia',setup(){constuserStore=useUserStore();constaddAge=()=>{userStore.updateAge(++userStore.age);};return{userStore,addAge};}});</script>复制代码

注入 `pinia` :修改 `src/entry-client.ts`

...importcreateStorefrom'@/store';constpinia=createStore();const{app}=createApp();app.use(router);app.use(pinia);//初始化pini//注意:__INITIAL_STATE__需要在 src/types/shims-global.d.ts中定义if(window.__INITIAL_STATE__){pinia.state.value=JSON.parse(window.__INITIAL_STATE__);}...复制代码

修改 `src/entry-server.ts`

...importcreateStorefrom'@/store';exportconstrender=()=>{...//piniaconstpinia=createStore();app.use(pinia);conststate=JSON.stringify(pinia.state.value);...return[renderedHtml,state,preloadLinks];}...复制代码

修改 `server-dev.js` 和 `server-prod.js`

...const[renderedHtml,state,preloadLinks]=awaitrender(ctx,{});consthtml=template.replace('<!--app-html-->',renderedHtml).replace('<!--pinia-state-->',state);//server-prod.js.replace('<!--preload-links-->',preloadLinks)...复制代码

给 `index.html` 添加 `<!--pinia-state-->` 标记

<script>window.__INITIAL_STATE__='<!--pinia-state-->';</script>复制代码

运行pnpm run dev就可正常显示了

备注:集成 pinia这块由于注入较为复杂且方法不一,暂时不做详细讲解,如果大家有需要,后面会出详细解析!

十、其它

vueuse的集成:可参考 《手把手教你用 vite+vue3+ts+pinia+vueuse 打造大厂企业级前端项目》[13]

CSS 集成:参考如上[14]

可使用:原生 css variable 新特性scss或者less

CSS 的 UI 库:参考同上[15]

需要注意的是按需引入

当然还有很多需要考量的,比如压测,并发,负载均衡等,但是这些不在文章主题范围内,这里就不做详细介绍,感兴趣的可以留言,后面有时间会新开对应的专栏

其中负载均衡这块前端同学可使用pm2, 或者直接丢给运维去搞个docker

项目模板地址

传送门[16]

最后

友情提示:目前 Vite 的 SSR 支持还处于试验阶段,可能会遇到一些未知 bug ,所以在公司的生产环境请谨慎使用,个人项目中可以滥用哟 ~

该系列会是一个持续更新系列,关于整个《Vite 从入门到精通》专栏[17],我主要会从如下图几个方面讲解,请大家拭目以待吧!!!

Untitled.png

靓仔靓女们,都看到这里了,要不点个赞再走呗 🌹🌹🌹

关于本文

作者:易师傅

/post/7086467466703929358

最后

Node 社群我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客www.inode.club让我们一起成长点赞和在看就是最大的支持❤️

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。