[前端]手写Nuxt和使用

来源

单页面程序,客户端渲染 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核数来决定开启几个实例

其实除了使用方式有些不同外,其他页面开发基本相同

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值