大厂技术高级前端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]
集成eslint
和prettier
统一代码质量风格的:查看教程[11]
集成commitizen
和husky
规范 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
结果:
渲染替换成项目根目录下的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 模板中的内容了,如下图
并且我们 `右键查看显示网页源代码`,也会看到渲染的正常 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.js
为server-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 中渲染时都是动态去生成的对应的js
和css
等;
那么我们要是在用户获取服务端模板
(也就是执行vite build
后生成的dist/client
目录) 的时候,直接在 html 中把对应的js
和css
文件预渲染了,这就是静态站点生成(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],我主要会从如下图几个方面讲解,请大家拭目以待吧!!!
靓仔靓女们
,都看到这里了,要不点个赞再走呗 🌹🌹🌹
关于本文
作者:易师傅
/post/7086467466703929358
最后
Node 社群我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客www.inode.club让我们一起成长点赞和在看就是最大的支持❤️