目录
来源
单页面程序,客户端渲染 CRS,他默认返回一个空的html页面,如 <body><div id="app"></div></body>,我们通过webpack打包出来的bundle.js和index.html就可以知道,整个页面的内容都是通过JS动态加载的,包括程序的逻辑,UI,服务端数据等,空白的页面引用了打包的JS,这时候浏览器就会处理js,渲染页面
SPA的优缺点,只需要加载一次,页面切换的时候不需要重新加载,比传统的web程序体验,加载更快,不利于SEO,首屏渲染慢,大型复杂的项目变得难以维护
浏览器爬虫的工作流程:分为三个阶段
如何做优化?
SSG静态站点生成:有利于SEO,预先生成好的静态网站,常见的框架有Nuxt,Next
SSG的缺点:不利于展示实时性的内容,SSR则能达到预期,如果站点内容更新了,需要重新构建和部署,偏静态的可以使用SSG
SSR:服务器端渲染,将渲染好的HTML返回给浏览器,也称同构应用
静态页面的过程
全局
手写Nuxt
run:npm install express nodemon webpack webpackcli webpack-node-externals
vue3:npm i vue vue-loader @babel/preset-env babel-loader
开发服务器和打包静态页面
跨请求状态污染的说明:在SPA中,整个生命周期中只有一个App对象实例,因为每个用户在使用浏览器SPA应用,应用模块都会重新初始化,这也是一种单例模式,然而,在SSR环境下,App应用模块只在服务器启动时初始化一次,同一个模块会在多个服务器请求之间被复用,这样我为什么我们用一个方法包裹生成实例
我们开是第一阶段,使用express搭建服务器,这里的导入的Vue文件无需打包编译,我们是把他转化成字符串的,这个服务器是需要打包的,我们运行在浏览器上,这样我们就得到了一个静态页面
const express = require("express");
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
import createRouter from "../router/index"
import { createMemoryHistory, createWebHistory } from "vue-router";
import { createPinia } from "pinia";
const app = express();
app.get("/*", async (req, res) => {
let app = createApp();
let appStringHtml = await renderToString(app);
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
${appStringHtml}
</div>
</body>
</html>`);
});
app.listen(8080);
水合
这个阶段我们只需要把VUE的代码打包一份,也就是我们日常开发VUE的主文件
import {createApp} from "vue"
import App from "../App.vue"
import createRouter from "../router/index"
import { createPinia } from "pinia"
import { createWebHistory } from "vue-router"
let app=createApp(App)
app.mount("#app")
这时候我们的服务端也需要调整,我们通过express的静态资源暴露,来让浏览器找到我们的打包文件
const express = require("express");
import createApp from "../app";
import { renderToString } from "@vue/server-renderer"
const app = express();
app.use(express.static("build"))
app.get("/*", async (req, res) => {
let app = createApp();
let appStringHtml = await renderToString(app);
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
${appStringHtml}
</div>
<script src="/client/client-bundle.js"></script>
</body>
</html>`);
});
app.listen(8080);
优化配置文件
npm install webpack-merge,抽出一个公共的文件,通过merge函数来组合
引入Router
import {createApp} from "vue"
import App from "../App.vue"
import createRouter from "../router/index"
import { createPinia } from "pinia"
import { createWebHistory } from "vue-router"
let app=createApp(App)
//这里也需要等待,再挂载
//这里的报错可能跟vue有关,不明晰
let router=createRouter(createWebHistory)
app.use(router)
try{
router.isReady().then(()=>{
app.mount("#app")
})
}catch(e){
console.log(e,"出错啦");
}
createRouter文件
import { createMemoryHistory, createRouter, createWebHistory } from "vue-router";
const routes=[
{
path:"/",
component:()=>import("../views/about.vue")
},{
path:"/home",
component:()=>import("../views/home.vue")
}
]
export default function(history){
return createRouter({
history,
routes
})
}
服务器
const express = require("express");
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
import createRouter from "../router/index"
import { createMemoryHistory, createWebHistory } from "vue-router";
const app = express();
app.use(express.static("build"))
app.get("/*", async (req, res) => {
let app = createApp();
let router=createRouter(createMemoryHistory())
app.use(router)
await router.push(req.url || "/",) //没有指定路径就是 /
await router.isReady() //有可能是异步组件
let appStringHtml = await renderToString(app);
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
${appStringHtml}
</div>
<script src="/client/client-bundle.js"></script>
</body>
</html>`);
});
app.listen(8080);
引入Pinia
完整的服务器
const express = require("express");
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
import createRouter from "../router/index"
import { createMemoryHistory, createWebHistory } from "vue-router";
import { createPinia } from "pinia";
const app = express();
app.use(express.static("build"))
app.get("/*", async (req, res) => {
let app = createApp();
let router=createRouter(createMemoryHistory())
app.use(router)
await router.push(req.url || "/",) //没有指定路径就是 /
await router.isReady() //有可能是异步组件
let pinia=createPinia()
app.use(pinia)
let appStringHtml = await renderToString(app);
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
${appStringHtml}
</div>
<script src="/client/client-bundle.js"></script>
</body>
</html>`);
});
app.listen(8080);
Pinia
import { defineStore } from "pinia"
export const useHomeStore=defineStore("home",{
state:()=>({
count:88
}),
actions:{
incremnet(){
this.count++
}
}
})
Client
import {createApp} from "vue"
import App from "../App.vue"
import createRouter from "../router/index"
import { createPinia } from "pinia"
import { createWebHistory } from "vue-router"
let app=createApp(App)
//这里也需要等待,再挂载
let pinia=createPinia()
//这里的报错可能跟vue有关,不明晰
let router=createRouter(createWebHistory)
app.use(pinia)
app.use(router)
try{
router.isReady().then(()=>{
app.mount("#app")
})
}catch(e){
console.log(e,"出错啦");
}
初始Nuxt
特点和优势
也可以混合渲染,A页面CSR,B页面SSR
特点:nitro可以用来构建打包文件,支持跨平台部署
环境搭建
推荐第一种或第二种
App.vue
配置环境变量
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
runtimeConfig:{
appKey:"niuniu", //server 这里定义的变量会被.env文件同名的变量,这里的变量不会被打入process
public:{
baseUrl:"ibx" //server/client都可以访问
}
}
})
我们通过一个hook就可以拿到对应的环境变量
<script setup>
//判断代码执行环境
if(process.server){
console.log("运行在服务端");
}
const runtiemConfig=useRuntimeConfig()
console.log(runtiemConfig.appKey)
console.log(runtiemConfig.public.baseUrl)
</script>
head
appconfig
ssr
app配置head,页面配置head,组件配置head来SEO优化
Nuxt3基础
基础组件
ClientOnly可以实现自定义插槽 #fallback
<ClientOnly fallback="loding...">
<!-- 浏览器解析会较后才会显示效果 -->
<!-- node服务器不会解析他,也就是返回的页面没有,浏览器会根据js来渲染他 -->
<h3>dasda</h3>
<template #fallback>
<div>
fsfsfs
</div>
</template>
</ClientOnly>
样式的编写
默认支持scss
全局样式:可以在App.vue直接编写全局变量,不推荐,配置某个样式为全局样式
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
css:[
"@/assets/styles/main.css" //配置文件为全局样式
]
})
Scss的使用
推荐导入方法:上面这种会出现同名覆盖的情况
自动导入scss,会自动找到scss模块,vue什么lang=”Scss“,需要注意的是scss文件也会自动导入,也就是定义全局样式变量
资源的访问
public里的资源可以直接访问,背景图片也一样
访问assets,nuxt内预设别名 @ ~都代表根目录
字体图标
想知道类名,可以去看下载代码的html
新建页面和组件跳转
约定是路由,新建页面可以用命令行、
文件login下的index页面,支持文件夹下页面,和直接在pages下页面
也可以直接访问外链,target:”_blank“ 外链通过新的页面打开,to还有个别名href,可以指定active-class=”active-niu“,可以书写书写replace,替换当前页面,不会压页面栈 ,也可以直接使用a标签,通过href=”/login“
编程导航
不利于seo
function gotocatecory(){
//建议return 返回一个promise
return navigateTo({
path:"/catecory",
query:{
id:200
},{
replace:true
}
})
}
最好在App.vue内监听
动态路由
查询字符串的参数使用,route.query
404
匹配不到的会来到...slug页面,写在子目录就会被子目录捕获,我们也可以pages下捕获,以前支持写一个404.vue现在已经不支持了,这个slug可以自定义,主要三个点,slug也可以使用route获取,他的值就算路径
Nuxt核心
嵌套路由,只需要创建同名的文件夹,Nuxtpage放入parent中
路由中间件
路由切换的时候这些中间件会执行,在浏览器刷新的时候外部的中间件也会执行,我们还可以定义全局中间件,我们在中间件做校验,路由跳转
路由验证
主要是用于做路由跳转携带参数的验证
layout
主要是做一个页面的公共抽取
渲染模式
浏览器和服务器都可以解析成javascript,都可以将vue组件呈现为HTML元素,此过程称为渲染,
SWR和SSG的区别是SSG只会渲染一次,静态页面,SWR我们可以设定时间,发生更改会被重新渲染
我们只需要配置指定路径下的渲染规则即可,我们可以全局配置 ssr指定整个程序是否ssr还是spa
插件的编写
实际上来说就是织入一些方法和属性进入Nuxt实例,也就是上下文,一般在App内注册,这样我们其他页面直接可以用
注意我们这些插件需要书写在plugins文件下,我们可以改为format.client.ts指定整个插件只有客户端可用
我们很可能会注册很多个插件,这些插件可能会互相依赖,可能有执行顺序,例如我们在一个插件里面再调用,NuxtApp.$getdata(),这时候我们可以给文件指定序号
生命周期 (lifecycle)
在插件中注册生命函数回调,会全部被被执行,在页面内监听,因为我们是setup语法,所以只能监听setup生命周期后的生命周期函数
总结来说,,在客户端的生命周期是正常执行的,而在服务端,如果是options API会回调beforeCreate和created,而copsition APi中仅会回调setup,所以我们直接在页面发送网络请求,因为setup本身就是一个生命周期
网络请求
获取数据主要是通过,支持server-client,api为$fetch,但是我们不怎么用,因为他会在服务端发起一次,然后又在客户端发送一次,不难理解,因为setup生命周期在两端都会回调,这时候我们就需要标识我们的网络请求,避免重复发送,官方也为我们提供了一个Hook,再刷新的时候,只有服务器会发送一次请求,在进行页面路由的时候,只有客户端会发送一次网络请求,爬虫是请求服务器,下载到临时目录的,所以不存在SEO问题
$fetch( "www.baidu.com",{
method:"POST"
})
//他只会在服务端发起请求
const res=await useAsyncData("id",()=>{
return $fetch()
})
//自动生成id,id自动生成为文件名+行号
const {data} = useFetch<IResult>("url",{method:"Post"})
注意post使用body,官方有指定options的TS格式,也就是第二个参数
两端都支持拦截
这里的返回值是无法直接像vue一样返回result.data.data,这里的返回值不会到结果中,我们可以自己重写,response.data={ name:"xxxx"}
关于lazy:我们的请求useFetch阻塞我们的路由,也就是跳转的时候,页面不会立即更新,而是等待网络请求完成才会挂载这个页面,可以在options中加载lazy:true,他会先挂载为空,watch确保我们能拿到数据,如果不使用watch,则有可能拿得到,有可能拿不到
简写
刷新页面,请求的状态,注意的是,我们可以传入响应式数据给请求体,当我们更改响应式数据的时候,这个请求也会重新被执行
import type { AsyncData, UseFetchOptions } from "#app";
type methodType="GET" | "POST"
class Niurequest{
request<T=any>( url:string,methods?:methodType,data?:any,options?:UseFetchOptions<T>):Promise<AsyncData<T,Error>>{
let newoptions={
baseURL:"",
method:methods||"GET",
...options
}
if(newoptions.method === "GET"){
newoptions.query=data || {}
}
if(newoptions.method==="POST"){
newoptions.body=data || {}
}
return new Promise((resolve,reject)=>{
useFetch<T>(url,newoptions as any).then(res=>{
resolve(res as AsyncData<T,Error>)
}).catch(err=>reject(err))
})
}
get<T>(url:string,methods:"GET",data?:any,options?:UseFetchOptions<T>){
return this.request(url,methods,data,options)
}
POST<T>(url:string,methods:"POST",data?:any,options?:UseFetchOptions<T>){
return this.request(url,methods,data,options)
}
}
export default new Niurequest()
ServerAPI
我们可以直接编写接口,和操作数据库
homeinfo代码实现
export default defineEventHandler(async (event)=>{
// let query= getQuery(event)
// const body=await readBody(event)
// let {req,res}=event.node
return{
code:200,
data:{
name:"ninuinu",
age:88
}
}
})
页面调用
//支持post/get
const {data}=await commonreq.request("/api/homeinfo")
console.log(data);
let cook=useCookie("token",{
maxAge:20 //s
})
cook.value="aaaa"
全局状态共享Pinia
两种全局状态共享的区别
import { defineStore } from "pinia"
export type counterIType={
couter:number
}
//http://codercba.com:9060/oppo-nuxt/api/home/info
const useCounter=defineStore("counter",{
state:():counterIType=>({couter:88}),
actions:{
incrument(){
this.couter++
}
}
})
export default useCounter
页面使用跟正常的pinia一样,他的action也支持异步网络请求
集成Element-plus
Nuxt项目
除了以下,其他就算正常Vue开发
集群部署
直接把打包的目录拖进来,但是使用node控制台断了,node进程也结束了,我们需要引入PM2
还可以打入一个端口号
编写配置文件,我们只需要运行这个配置文件即可,instance可以给的值有max,即根据cpu核数来决定开启几个实例
其实除了使用方式有些不同外,其他页面开发基本相同