nuxt3+ts+vue3的ssr项目总结
1.浅谈SSR,SEO,SPA2.NUXT3的基本使用3.NUXT3引入element plus4.NUXT3引入mavon-editor5.NUXT3中v-if与v-show的对比6.Uncaught (in promise) TypeError: Cannot read properties of null (reading 'parentNode')7.NUXT3中含有el-tooltip所
目录
一、什么是SSR、SEO、SPA,它们之间的关系又是怎样的。
一、什么是SSR、SEO、SPA,它们之间的关系又是怎样的。
SSR (Server-Side Rendering):服务器端渲染,是一种网页渲染方法,其中服务器在向客户端发送 HTML 之前将页面渲染为完全呈现的 HTML。在 SSR 中,服务器会处理页面的渲染,然后将呈现的 HTML 发送到客户端浏览器,浏览器接收到的是已经包含内容的页面。这有助于搜索引擎更好地索引页面内容,提高首次加载速度,以及有助于 SEO(搜索引擎优化)。
SEO (Search Engine Optimization):搜索引擎优化,是一系列技术和策略,旨在改进网站在搜索引擎结果页面上的排名,以增加网站的曝光度和访问量。在网站的 SEO 中,内容的可索引性、关键词的使用、页面速度和用户体验等都是重要因素。
SPA (Single Page Application):单页面应用,是一种基于 Web 的应用程序,它在加载初始页面后,通过 AJAX 或其他技术在同一个页面上动态加载内容,而不需要完整的页面刷新。SPA 通常在客户端使用 JavaScript 处理页面路由和渲染。SPA 的优点包括更流畅的用户体验,但它可能对搜索引擎索引和首次加载速度产生挑战。
SSR 和 SEO:SSR 通过在服务器端将完全呈现的 HTML 发送到客户端,有助于提高页面在搜索引擎中的可索引性。搜索引擎可以更容易地读取和理解页面内容,从而提高 SEO。相比之下,SPA 在初次加载时可能只有一个空的 HTML 骨架,内容是通过 JavaScript 动态加载的,这可能对 SEO 造成挑战。
SSR 和 SPA:SSR 和 SPA 是两种不同的页面渲染方法。SSR 在服务器端完成渲染,可以提供更好的 SEO 和首次加载性能,但也可能增加服务器负担。SPA 在客户端动态加载内容,提供更流畅的用户体验,但可能面临搜索引擎索引和初始加载速度的问题。一些项目结合两者,使用 SSR 渲染首次加载的内容,然后在后续页面切换时转为 SPA。
想了解更多关于ssr,seo,spa的知识可以看下面这篇文章:
二、VUE做SSR的几种方法
1、插件prerender-spa-plugin
prerender-spa-plugin 是一个用于预渲染单页面应用(SPA)的插件,它可以帮助你在构建时生成静态 HTML 文件,以优化搜索引擎索引和首次加载性能。它需要搭配webpack进行使用,并且改插件有局限性,只能用于较少页面的渲染,一旦渲染的较多负载会非常大,且不适用动态平衡路由的渲染。如商品详情页,文章详情页等等。且该项目已经停止更新了,请谨慎使用。
这里推荐一篇prerender-spa-plugin详细使用文章:
2、VUE开启SSR渲染模式
Vue.js 的服务器端渲染(SSR)通过创建一个服务器入口文件来处理客户端请求,使用 createApp
函数创建 Vue 应用实例并配置路由,根据请求的 URL 匹配路由并预取数据,使用 renderToString
方法将应用渲染为完全呈现的 HTML 字符串,然后将渲染后的 HTML 和状态数据作为响应返回给客户端,客户端接管渲染过的 HTML 并激活应用。从而实现更好的搜索引擎优化、更快的首次加载速度以及服务器和客户端之间更一致的行为。
Vue.js 的服务器端渲染(SSR)官网:
Vue.js 服务器端渲染指南 | Vue SSR 指南Vue.js 服务端渲染指南https://v2.ssr.vuejs.org/zh/这里附带一篇我认为写的较好的博主的文章,大家可以借鉴一下:
3、使用NUXT框架
接下里来我将对NUXT的一些基本使用与踩坑展开描述。
三、NUXT3+VUE3+TS
首先这里默认你已经安装了NUXT3,如果不会的话,请移步至官网文档,就一个命令的事。(注意:node版本要大于16.11.0)
(一)基本配置
1、文件夹介绍
assets
在根目录文件下创建一个名为assets的文件夹用于存储静态文件。如图所示,可以将css文件,图片文件,svg文件包括iconfont字体等文件都存储在这里。
⚠️:这是官方约定俗成的东西。
components
一般用于存储封装好的通用组件。
pages
存放页面的文件夹
api
存放网络请求
plugins
存放各类插件。
utils
存放各类工具。
2、布局使用
首先新建一个 layouts 文件夹,写入 default.vue 文件。
找到 app.vue 文件添加 NuxtLayout 标签。
运行效果:
3、useFetch请求封装
首先在根目录下新建一个文件夹 service ,并新建一个文件 index.ts 。
添加代码如下:
import { UseFetchOptions } from "nuxt/app";
type Methods = "GET" | "POST" | "DELETE" | "PUT";
const BASE_URL = "https://xxx.com/";
export interface IResultData<T> {
code: number;
data: T;
msg: string;
}
class HttpRequest {
request<T = any>(
url: string,
method: Methods,
data: any,
options?: UseFetchOptions<T>,
) {
return new Promise((resolve, reject) => {
const newOptions: UseFetchOptions<T> = {
baseURL: BASE_URL,
method: method,
...options,
};
if (method === "GET" || method === "DELETE") {
newOptions.params = data;
}
if (method === "POST" || method === "PUT") {
newOptions.body = data;
}
useFetch(url, newOptions)
.then((res) => {
resolve(res.data);
})
.catch((error) => {
reject(error);
});
});
}
// 封装常用方法
get<T = any>(url: string, params?: any, options?: UseFetchOptions<T>) {
return this.request(url, "GET", params, options);
}
post<T = any>(url: string, data: any, options?: UseFetchOptions<T>) {
return this.request(url, "POST", data, options);
}
Put<T = any>(url: string, data: any, options?: UseFetchOptions<T>) {
return this.request(url, "PUT", data, options);
}
Delete<T = any>(url: string, params: any, options?: UseFetchOptions<T>) {
return this.request(url, "DELETE", params, options);
}
}
const httpRequest = new HttpRequest();
export default httpRequest;
再在根目录下新建一个 api 文件夹,新建文件index。在这里引入并定义接口,以便于接口的统一处理。
import httpRequest from "~/service";
const getAppdetail = (params: any) => {
const URL = `/app-server/marketui/${params.marketID}/apps/${params.appID}/detail`
return httpRequest.get(URL, params);
};
export {
getAppdetail,
};
使用如下:
<template>
这是主页面的内容
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { getAppdetail } from '../api/index';
let marketID = ref<string>('859a51f9bb3b48b5bfd222e3bef56425');
let appID = ref<string>('10d2295fc98d4163acc4b2ec9d2917b9');
let getAppdetailData = async () => {
let Appdetail: any = await getAppdetail({
marketID: marketID.value,
appID: appID.value,
});
console.log(JSON.parse(JSON.stringify(Appdetail.value)));
};
getAppdetailData()
</script>
<style scoped>
</style>
请求成功:
(二)引入element-pluse
1、安装
安装element-pluse
npm install element-plus --save
官网文档:
2、引入
在 根目录创建 plugins 文件夹 ,新建一个名为 element-plus.js 的文件,并添加如下文件内容。
import { defineNuxtPlugin } from '#app'
import ElementPlus from 'element-plus'
import { ID_INJECTION_KEY } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(ElementPlus, {
locale: zhCn,
})
nuxtApp.vueApp.provide(ID_INJECTION_KEY, {
prefix: 100,
current: 0,
})
})
大功告成就可以使用了。
3、使用
我们更改一下about页面的代码。
运行效果:
(三)引入md文档
⚠️:此插件使用时需要搭配 <ClientOnly></ClientOnly> 或者 <no-ssr></no-ssr> 使用,否则会报错,因为在服务端获取不到顶级的window对象。
1、下载
命令
npm install mavon-editor --save
GitHub:
2、引入
在 根目录创建 plugins 文件夹 ,新建一个名为 vue-mavon-editor.js 的文件,并添加如下文件内容。
import { defineNuxtPlugin } from '#app'
import mavonEditor from 'mavon-editor'
import "mavon-editor/dist/css/index.css";
export default defineNuxtPlugin( nuxtApp => {
nuxtApp.vueApp.use(mavonEditor)
})
在 nuxt.config.ts 中添加如下代码。
export default defineNuxtConfig({
//其他配置
plugins: [
{ src: '@/plugins/vue-mavon-editor.js', ssr: false, mode: 'client' },
],
})
3、使用
用于输入数据
这次我们更改 home 文件夹下的 index.vue。
<template>
<h1>这是home页面</h1>
<ClientOnly>
<mavon-editor
ref="md"
placeholder="请输入文档内容..."
:boxShadow="false"
style="z-index:1;border: 1px solid #d9d9d9;height:50vh"
v-model="value"
:toolbars="toolbars"
/>
</ClientOnly>
</template>
<script setup lang="ts">
import { ref } from 'vue';
let value = ref<string>('**这是加粗的文字**');
let toolbars = ref<any>({
bold: true, // 粗体
italic: true, // 斜体
header: true, // 标题
underline: true, // 下划线
strikethrough: true, // 中划线
mark: true, // 标记
superscript: true, // 上角标
subscript: true, // 下角标
quote: true, // 引用
ol: true, // 有序列表
ul: true, // 无序列表
link: true, // 链接
imagelink: true, // 图片链接
code: true, // code
table: true, // 表格
fullscreen: true, // 全屏编辑
readmodel: true, // 沉浸式阅读
htmlcode: true, // 展示html源码
help: true, // 帮助
/* 1.3.5 */
undo: true, // 上一步
redo: true, // 下一步
trash: true, // 清空
save: false, // 保存(触发events中的save事件)
/* 1.4.2 */
navigation: true, // 导航目录
/* 2.1.8 */
alignleft: true, // 左对齐
aligncenter: true, // 居中
alignright: true, // 右对齐
/* 2.2.1 */
subfield: true, // 单双栏模式
preview: true // 预览
})
</script>
<style scoped>
</style>
效果如图:
用于回显数据
<template>
<h1>这是home页面</h1>
<ClientOnly>
<mavon-editor
ref="md"
v-highlight class="md"
v-model="value" //引入要转换的内容
:subfield="false"//开启单栏模式
:defaultOpen="'preview'" //开启单栏模式
:toolbarsFlag="false" //开启单栏模式
:editable="false"//开启单栏模式
:scrollStyle="true"//开启滚动条样式
/>
</ClientOnly>
</template>
<script setup lang="ts">
import { ref } from 'vue';
let value = ref<string>('**这是加粗的文字**');
</script>
<style scoped>
</style>
(四)v-show与v-if踩坑
1、区别
-
渲染方式的区别:
v-if
:当条件为真时,元素会被渲染到DOM中;当条件为假时,元素会被完全从DOM中移除。v-show
:无论条件是真还是假,元素都会被渲染到DOM中,但是通过设置元素的display
属性来控制是否显示在页面上。
-
初始渲染性能:
v-if
:在初始渲染时,如果条件为假,元素不会被添加到DOM中,因此可以提高初始加载性能。v-show
:无论条件如何,元素都会被添加到DOM中,只是通过CSS的display
属性来控制显示与隐藏,因此初始加载性能上稍逊于v-if
。
-
切换开销:
v-if
:当条件从假切换到真时,会进行DOM插入操作,从真切换到假时,会进行DOM移除操作,可能涉及更多的DOM操作开销。v-show
:无论切换条件如何,只需操作CSS属性,因此切换时的性能开销较小。
-
适用场景:
v-if
:适合在条件不经常改变的情况下使用,因为它在切换时有更高的开销。v-show
:适合在条件需要经常改变的情况下使用,因为它的切换开销较小。
2、问题
使用v-show可能会导致文件加载错误,或者模块渲染失败。比如在这段代码中:
<div class="tag_box" v-if="parentNode">
<div class="tag_icon">
<img class="helm_icon" src="../../../assets/svg/tag-type.svg" alt="" />
</div>
<div class="tag_name">
{{ parentNode }}
</div>
</div>
我的img文件中指向的是静态文件中的一个icon图标。
当我使用v-if时,正常展示。
当我使用v-show时,展示就出现了问题。
目前不清楚是为何导致的但需要注意这一点。
(五)el- select与el-tooltip踩坑
1、问题
可以详见这个issues。https://github.com/element-plus/element-plus/issues/9414https://github.com/element-plus/element-plus/issues/9414
总的来说就是使用 el- select 与 el-tooltip 的地方会导致渲染出现问题,比如这里。
<!-- 导航栏 -->
<!-- <no-ssr> -->
<nav class="navbar">
<div class="navbar-box">
<div style="font-size: 24px">云原生应用市场</div>
<div>
<ul>
<li>
<a :href="`/docs`" target="_blank"> 文档 </a>
</li>
<li>
<el-popover :width="160" placement="bottom" trigger="hover">
<template #default>
<div>
<img
class="wechart"
:src="
marketInfo &&
marketInfo.banners &&
marketInfo.banners.length > 0 &&
marketInfo.banners[0].imageURL
"
alt=""
/>
<p
style="
font-size: 12px;
margin-bottom: 0px;
margin-top: 6px;
text-align: center;
"
>
扫一扫,邀你进群交流吧
</p>
</div>
</template>
<template #reference>加入群聊</template>
</el-popover>
</li>
<li>
<a :href="`/enterprise/login`"> 登录 </a>
</li>
</ul>
</div>
</div>
</nav>
<!-- </no-ssr> -->
运行效果:
可以看到,我这里点击跳转以后地址栏发生了变化,但是页面并没有跳过去,而且控制台也报错。Uncaught (in promise) TypeError: Cannot read properties of null (reading 'parentNode')
解决方法
使用 <no-ssr></no-ssr> 把对应的 el- select与el-tooltip 包裹住,让这一小段代码不使用ssr渲染,从上面的issues也可以看到这是element plus的一个bug,但是截止到目前仍未修复。
总结:坑还是挺多的,而且很多问题虽然解决了但不知道是为什么(有知道为什么的大佬如果您不嫌弃的话麻烦给我讲解一下),因为这个玩意已经加了两周多的班了,终于捋出来一些头绪了。终于也是快接近尾声了。seo优化这个词也终于从我的面试八股文中走了出来真真切切的体验了一把,虽然过程很艰难,但好在最难的时刻已经过去啦。加油加油各位!
更多推荐
所有评论(0)