lemooljiang avatar

Nuxt 开发指南

lemooljiang

Published: 01 Sept 2025 › Updated: 01 Sept 2025Nuxt 开发指南

Nuxt 开发指南

nuxt42.jpg

下载与资源

官网 |
中文文档 |
中文文档2 |
nuxt-modules |
naive ui |
Unocss |
windicss |
windicss github |
Nuxt 3.0 全栈开发视频 |

安装

"nuxt": ^4.0.3 nodejs: v22.17.1

1. github https://github.com/nuxt/starter/tree/v4

 - npm
 npm create nuxt <project-name>
 eg:npm create nuxt nuxtstart

 -  package.json ->  name

2. npm install --global yarn   //yarn --version  1.22.22
  yarn config set registry https://registry.npmmirror.com  切换到淘宝源
  yarn install

3. yarn dev
http://localhost:3000/ 

//vs code插件
Vetur   Vue 3 Snippets
Naive Ui Snippets   //代码提示
UnoCSS              //代码提示

错误排除

  • 更改配置(nuxt.config.ts)一般要重启。
  • 编译时出错:Named export 'VResizeObserver' not found, nuxt.config.ts 文件中添加如下代码:
    build: { transpile: ['naive-ui', "vueuc"] },
  • MongoServerError:E11000 duplicate key error.... 这可能是数据表中的数据格式冲突导致的(有脏数据),删除整个表即可!
  • Naive UI中表格要有 block 属性,否则样式将变得怪异!<n-form block outline-none>
  • Error: [nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.
  • useFetch的错误, let {data, error} = await useFetch(url) ????
    // data.value == null 都会报错,如下:
    // Uncaught (in promise) TypeError: Cannot read properties of null (reading 'scope')
  • Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client ???
  • devtools: { enabled: false }
  • server-api 在Nginx的https报务中无法开启,总有个错误 500 Server Error

全局错误处理

Nuxt是一个全栈框架,这意味着在不同的上下文中,有几种不可避免的用户运行时错误来源:

  1. Vue渲染生命周期中的错误(SSR + SPA)
  2. API或Nitro服务器生命周期中的错误
  3. 服务器和客户端启动错误(SSR + SPA)
//全局错误页面  error.vue
<template>
  <div class="pt-[50px]">
    <n-result
        status="500"
        title="错误提示"
        :description="error.message"
    >
        <template #icon>
            <img src="~/assets/img/sad.png" alt="喵星">
        </template>
        <template #footer>
            <n-button @click="$router.back()" mr-4></n-button>
            <n-button @click="handleError"></n-button>
        </template>
    </n-result>
  </div>
</template>

<script setup>
import { NButton, NResult } from "naive-ui"

defineProps({
  error: Object
})
const handleError = () => clearError({ redirect: '/' })
</script>

渲染模式

浏览器和服务器都可以解释JavaScript代码,将Vue.js组件渲染成HTML元素。这个步骤被称为渲染。Nuxt支持客户端和通用呈现。
客户端渲染 - CSR, 例如 Vue.js、React
通用渲染 - SSR, 例如 Nuxt、Next.js
静态站点生成 - SSG;
混合渲染 - 混合呈现允许每个路由使用路由规则不同的缓存规则,并决定服务器应该如何响应给定URL上的新请求。例如 Nuxt、Next.js
边缘渲染 - edge-side rendering。

naive UI

官网 |
安装指南 |
安装指南2 |
naive-ui-nuxt |

yarn add naive-ui   //^2.42.0  

1. nuxt.config.ts
build: { transpile: ['naive-ui', "vueuc"] },

2. app -> plugins -> naiveui.ts
import { setup } from '@css-render/vue3-ssr';
 
export default defineNuxtPlugin((nuxtApp) => {
    if (process.server && nuxtApp.ssrContext) {
        const { collect } = setup(nuxtApp.vueApp || {});
        
        // @ts-ignore
        const originalRender = nuxtApp.ssrContext.renderMeta?.bind(nuxtApp.ssrContext) || (() => ({}));
        
        nuxtApp.ssrContext.renderMeta = () => {
            // @ts-ignore
            const result = originalRender();
            // @ts-ignore
            const headTags = result?.headTags || "";
            
            return {
                headTags: headTags + collect()
            };
        };
    }
});

3. 
<template>
  <n-button>naive-ui</n-button>
</template>

<script setup>
  import { NButton } from 'naive-ui'
</script>

消息显示

脱离上下文的API
使用 createDiscreteApi 来创建一系列消息提示,它比较自由,推荐。

import { createDiscreteApi } from "naive-ui"
// import { useMessage } from "naive-ui" useMessage要有专门设置,暂时不用

let { message } = createDiscreteApi(["message"])
message.error("注册失败!\n"+error.value.data, { duration: 5e3 })
// message.error("注册失败!\n"+error.value.data)
message.success("注册成功", { duration: 5e3 })
// message.success("注册成功")
message.warning("How many roads must a man walk down")
message.loading("If I were you, I will realize that I love you more than any other guy")
message.info(
  "I don't know why nobody told you how to unfold your love",
  {
    keepAliveOnHover: true
  }
)
 
// 注意: 如果已经导入 message 而没有使用可能会导致错误!尤其是下拉菜单!

Unocss安装

这是一个按需原子化 CSS 引擎,比起直接使用 TailwindCSS 更轻更快!
它的预设样式可以直接使用 TailwindCSS 和 Windicss 的样式。

// yarn add @unocss/nuxt   //^0.57.4 "^0.58.8"
yarn add unocss @unocss/nuxt //^66.3.3

//配置模块,nuxt.config.ts:
  modules: [
    '@unocss/nuxt',
  ],

// 根目录创建 uno.config.ts
import {
  defineConfig, presetAttributify, presetIcons,
  presetTypography, presetUno, presetWebFonts,
  transformerDirectives, transformerVariantGroup
} from 'unocss'

export default defineConfig({
  rules: [
      //...
  ],
  shortcuts: [
    // ...
  ],
  theme: {
    colors: {
      // ...
    }
  },
  presets: [
    presetUno(),  //工具类预设
    presetAttributify(),  //属性化模式支持
    presetIcons(),    //icon支持
    presetTypography(),
    presetWebFonts({
      fonts: {
        // ...
      },
    }),
  ],
  transformers: [
    transformerDirectives(),
    transformerVariantGroup(),
  ],
})

Unocss基本语法

原子化css |
互动样式 |
样式参考 |
Tailwind CSS |
颜色样式 |

可以认为Unocss兼具了TailwindCSS 和 Windicss的优点,同时还具有自身的定制性和灵活性。按需引入,体积小,速度快。

1. 使classUnocssclass
<div class="bg-blue-200"> Index Page</div>
<div bg-pink-200 text-cyan></div>

2. style
<div class="text"> Index Page</div>
<style>
.text {
  @apply text-xs text-red-300 hover:bg-zinc-900;
}
.btn {
  @apply text-red-700  border border-red-500 !rounded-full;  // rounded-full !important 
}
</style>

3. 
.test {
  color: white;
  font-size: 2rem;
  @apply bg-blue;
}

3. (Attributify Mode)
<button 
  bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"
  text="sm white"
  font="mono light"
  p="y-2 x-4"
  border="2 rounded blue-200"
>
  Button
</button>

4. `uno.config.ts` rules  shortcuts
rules: [
  ['custom-rule', { color: 'red' }],
  ['main2', { margin: '0.25rem', padding: '2rem', color: 'red', 
  'background-color': 'blue'}],
],
shortcuts: {
  // shortcuts to multiple utilities
  'custom-shortcut': 'text-lg text-orange hover:text-teal',
  'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',
  'btn-green': 'text-white bg-green-500 hover:bg-green-700',
  // single utility alias
  'red': 'text-red-100'
}

/* //常用语句 */
bg-purple-100  
text-purple-100 
text-center    
align-middle  
!w-[80%]    //width: 80% !important;
shadow-sm  
cursor-pointer 
outline-none  
font-italic  

hover:bg-purple-500 
focus:bg-purple-900  
flex justify-center items-center  
flex justify-between  
min-h-sceen / min-h-100vh 
min-h-80vh 
h-80vh / calc(100vh - 10px)
w-full / w-100vw 

w-[95%] lg:w-[85%] ~= w-95vw  lg:w-85vw

线
<div flex justify-center items-center><p m-l-4 text-4></p> 
</
div> <div flex ><div ml-auto></div></div> <div flex ><div ml-auto px-2 hover:bg-red>X</div></div> //删除按键 m-auto / mx-auto ml-auto mr-auto px-4 / py-4 /padding: 1rem; px-[20px] pt-4 1rem(padding-top: 1rem;) mx-4 / my-4 / animate-fade-in <div border-t-dashed border-1></div> 线线 /* flex布局,两端对齐 */ <div flex mb-2 justify-between> <p mr-2></p> <div w-65vw lg:w-42vw> xxx </div> </div> /* 响应式 */ 5 smmdlgxl2xl CSS sm 640px @media (min-width: 640px) { ... } md 768px @media (min-width: 768px) { ... } lg 1024px @media (min-width: 1024px) { ... } xl 1280px @media (min-width: 1280px) { ... } 2xl 1536px @media (min-width: 1536px) { ... } /* 定位手机屏幕 */ 使 sm: sm: sm xl 使 <div class="text-center sm:text-left"></div> // flex-1 flex justify-end lg:hidden //手机端时显示,屏幕变大时隐藏 relative hidden lg:flex items-center ml-auto //在电脑端的样式(在手机端时隐藏) hidden lg:flex items-center px-4 //在电脑端的样式(在手机端时隐藏) hidden lg:block ml-auto //在电脑端的样式(在手机端时隐藏) ml-auto lg:hidden//在手机端时居中,电脑端隐藏

icones图标

官网 |
说明 |

yarn add @iconify-json/mdi   //注意按需安装!  //1.2.3
//yarn add @iconify-json/vscode-icons   

<div class="i-mdi-github text-3xl" />
<div class="i-mdi-twitter" />
<div class="i-mdi-arrow-collapse-right" />
<div class="i-mdi-alpha-i-circle" />  //
<div class="i-mdi-chevron-down" /> //



<div class="ml-auto mr-2 bold lg:hidden" @click="activate('top')">
    <div class="i-mdi-dots-vertical" text-2xl/>
</div> 

<div class="i-mdi-chevron-double-right text-2xl"
  mt-20 block lg:hidden  
  text-gray-600 
  @click="activate('left')"
/>

全局变量配置

//app.config.ts
export default defineAppConfig({
    title: 'Hello Nuxt888',
    theme: {
      dark: true,
      colors: {
        primary: '#ff0000'
      }
    }
  })
 //app.vue
 {{ appConfig.title }}
 const appConfig = useAppConfig()

 //另外一个简洁的用法是写在composables中,export即可引用
export const title = "hello nuxt"

页面关键词配置

全局配置 |
配置 |

//nuxt.config.ts
app: {
  head: {
      titleTemplate: "%s - 固定标题",
      title: "这是首页",
      charset: "utf-8",
      htmlAttrs: {
          lang: "zh-cn"
      },
      meta: [
          { name: "description", content: "首页描述" },
          { name: "keywords", content: "首页关键词" },
      ]
  }
},

//index.vue 对单独页面的设置,会覆盖全局配置
useHead({
  title:"首页index",
  meta:[
      { name:"description",content:"首页描述2" },
      { name:"keywords",content:"首页关键词2" },
  ],
})

全局CSS

// 方法一
1. assets -> main.css
2. nuxt.config.ts
  css: [
    "~/assets/main.css",
  ], 

// 方法二
//在 app.vue 中引入样式。注意不是在[removed]中!
<script setup>
import "~/assets/main.css";
</script>  

// 方法三
//或者直接在 `uno.config.ts`中定义 shortcuts,它也是相当于全局的
shortcuts: {
  // shortcuts to multiple utilities
  'containerX': 'w-[95%] mx-auto text-4.4 lg:w-[85%]'
  'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',
  'btn-green': 'text-white bg-green-500 hover:bg-green-700',
  // single utility alias
  'red': 'text-red-100'
}

全局js

引用本地js
Nuxt入门

Nuxtjs/assets或/public

    /assetswebpack
    /public
/public
    /public/videojs/video-js.css
    /public/videojs/video.js
    /public/videojs/videojs-contrib-hls.js

  head: {
    ...
    link: [
      ...
      { rel: 'stylesheet', href: '/videojs/video-js.css' }
    ],
    script: [
      { src: '/videojs/video.js' },
      { src: '/videojs/videojs-contrib-hls.js' }
    ]
  },

外部js和css的引入

: useHead useHeadsetup使
<script setup lang="ts">
  useHead({
  script: [ 
    { src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js' }
  ]
  })
</script>

//这里如果需要将js放置body区域末尾,直接添加参数
script: [ 
  {
    src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js',
    body:true
  }
]  
  
//引入外部css
<script setup lang="ts">
useHead({
  link: [
    {
      rel: 'preconnect',
      href: 'https://fonts.googleapis.com'
    },
    {
      rel: 'stylesheet',
      href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap',
      crossorigin: ''
    }
  ]
})
</script>

全局函数utils

Nuxt使用 utils/ 目录在整个应用程序中使用auto-imports自动导入辅助函数和其他实用程序!

export function test2() {
    console.log(12793, "test.js2")
}

//也可以自定义导出目录
//nuxt.config.ts
imports: {
  dirs: ["apis"]
}

composables

在composables/目录中编写自己的自动导入可重用函数。但它只导入顶层函数,如有二级目录,则必须在config中配置!

export const newFun = (i) => {
    return i+5
  }

//composables/gets/foo.js
imports: {
    dirs: ["composables/**"] 
},  

中间件

middleware 目录,Nuxt提供了一个可定制的路由中间件框架,可以在整个应用程序中使用,非常适合在导航到特定路由之前提取想要运行的代码。

//middleware -> search.js
export default defineNuxtRouteMiddleware((to,from)=>{
    const { type,page } = to.params
    const { keyword } = to.query

    if(!keyword){
        return abortNavigation("搜索关键词不能为空")
    }

    if(!(["course","column"].includes(type)) || isNaN(+page)){
        return abortNavigation("页面不存在")
    }
})
//index.vue
definePageMeta({
    middleware:["search"]
})

//eg2
//中间件可以获取目标路由 to 和来源路由 from,还有两个很常用的工具方法:
abortNavigation(error) from
navigateTo(route)
export default defineNuxtRouteMiddleware((to, from) => {
  if (to.params.id === '1') {
    return abortNavigation()
  }
  return navigateTo('/')
})

//全局中间件
//命名时带上 global
middleware -> search.global.js

使使 `-` 

插件

1. plugins/ -> myPlugin.ts
2. myPlugin.ts
export default defineNuxtPlugin(nuxtApp => {
  // Doing something with nuxtApp
})
export default defineNuxtPlugin(() => {
  return {
    provide: {
      hello: (msg: string) => `Hello ${msg}!`
    }
  }
})
<template>
  <div>
    {{ $hello('world') }}
  </div>
</template>
<script setup lang="ts">
// alternatively, you can also use it here
const { $hello } = useNuxtApp()
</script>

//Nuxt 上下文:NuxtApp
 nuxtApp  NuxtApp  Nuxt 访 Vue

 nuxtApp 便使
    provide (name, value)
    hook(name, cb) nuxt 
    vueApp vue 
    ssrContext
    payload
    isHydrating

layouts布局

layouts -> default.vue, <slot/>
eg:
<template>
    <div class="body">
        <NavBar/>      
        <main class="containerX"> 
            <slot/>
        </main>
    </div>
</template>

//指定页面布局
layouts -> login.vue
//pages -> login.vue
definePageMeta({
    layout:"login",
    middleware:["only-visitor"]
})
使使 `-` 

Grid栅格系统

NaiveUI的Grid栅格系统,可以布局响应式的样式。

cols number | ResponsiveDescription 24 一般是分成24列,每列用 span 表示占比

<n-grid x-gap="12" :cols="4">
  <n-gi>
    <div class="light-green" />
  </n-gi>
  <n-gi>
    <div class="green" />
  </n-gi>
  <n-gi>
    <div class="light-green" />
  </n-gi>
  <n-gi>
    <div class="green" />
  </n-gi>
</n-grid>

//响应式布局 分成4列,占比是手机端时 0:4, 电脑端时 1:3
<n-divider>Screen </n-divider>
<n-grid x-gap="20" cols="4" item-responsive responsive="screen">
  <n-grid-item span="0 l:1">
    <div class="light-green">
      m <br>
      m  l 1<br>
      l  2
    </div>
  </n-grid-item>
  <n-grid-item span="4 l:3">
    <div class="green">
      2
    </div>
  </n-grid-item>
</n-grid>

页面和路由

Nuxt会自动使用Vue Router在底层创建路由,页面的名字就是路由地址。
新建 pages 文件夹,在此文件夹内创建页面。

pages/about.vue ->  localhost:3000/about

`pages/index.vue` /

//二级路由: 页面文件在一个目录下
pages/user/info.vue ->  localhost:3000/user/info

//二级路由中保留父组件内容
1.  vue eg: pages/user + pages/user.vue
2. `user.vue```page

//组件导入则用slot
<slot/>

动态路由

如果您在文件名中使用方括号 [ ],它将被转换为 动态路由 参数。您可以在文件名或目录中混合使用多个参数。

如果您希望参数是 可选的,必须使用双括号 [[]] 括起来,例如 ~/pages/[[slug]]/index.vue 或 ~/pages/[[slug]].vue 将匹配 / 和 /test。

目录结构
-| pages/
---| index.vue
---| users-[group]/
-----| [id].vue
在上述示例中,您可以通过 $route 对象在组件中访问 group/id:
pages/users-[group]/[id].vue

pages/users-[group]/[id].vue
<template>
  <p>{{ $route.params.group }} - {{ $route.params.id }}</p>
</template>

访使 useRoute  Options API  this.$route 
const route = useRoute()
const group = route.params.group
const id = route.params.id

获取当前路径

useRoute 返回当前路由, 必须在setup函数、插件或路由中间件中调用。
在Vue组件的模板中,可以使用$route访问路由。

const route = useRoute()  // == $route
console.log(route.path) // '/about'

//获取路径id
$route.params.id

//除了动态参数和查询参数, useRoute() 还提供了以下与当前路由相关的计算引用:
fullPath: URLpathqueryhash
hash: #开头的URL的解码hash部分
matched: 
meta: 
name: 
path: URL
redirectedFrom: 访

useRouter

useRouter 返回路由器实例,必须在设置函数、插件或路由中间件中调用。
在Vue组件的模板中,你可以使用$router 来访问路由器。

const router = useRouter()

//几个重要参数:  
url: http://localhost:3000/user/userinfo
router.name: 'user-userinfo' 
router.fullPath: '/user/userinfo'
router.path: '/user/userinfo'

//几个重要的方法
router.back()
//$router.back() 返回上一页
router.forward()
router.go()
router.push({ path: "/home" })
router.replace({ hash: "#bio" })
back: router.go(-1)
forward:  router.go(1)
go:  router.back()  router.forward()


//刷新页面
let router = useRouter()
router.go(0)

navigateTo

// 将 'to' 作为字符串传递
await navigateTo('/search')

// ... 或者作为路由对象
await navigateTo({ path: '/search' })

// 动态路由
await navigateTo({ path: '/post/'+permlink})
//post动态页面接受方法
const route = useRoute()
cosnt permlink = route.params.permlink

// ... 或者作为带有查询参数的路由对象
await navigateTo({
  path: '/search',
  query: {
    page: 1,
    sort: 'asc'
  }
})
//serarch页面接受方法
const route = useRoute()
const query = route.query //用此方法接受参数

生命周期函数

手册

由于 Nuxt 整合了 Vue、Nitro 前后端两个运行时,再加上它自身的创建过程,因此框架生命周期钩子分为三类:
Nuxt 钩子;
Vue App 钩子;
Nitro App 钩子。

const nuxtApp = useNuxtApp()
eg:
nuxtApp.hook("page:start",(e)=>{
        bar.value?.start()
        // console.log("page:start");
    })
nuxtApp.hook("page:finish", () => {
  window.scrollTo(0, 0)
})

Vue的钩子函数

参考

//有时无法自动运行
onMounted(() => {
    console.log(699, "moundted")
    function x (){
      console.log(722, "moundted")
    }
    x()
})

//beforeMount mounted

翻页时默认回到顶部

//app -> router.options.js
export default {
    scrollBehavior (to, from, savedPosition) {
        // 在按下 后退/前进 按钮时。就会像浏览器的原生表现那样
        if(savedPosition){
            return savedPosition
        }

        return {
            top:0
        }
    }
}

组件

components -> NaveBar.vue, 
<NaveBar /> 

嵌套组件

components -> Ui -> Menu.vue
<UiMenu>
  test
</UiMenu>
// 或者这样写:
<ui-menu>
  test
</ui-menu>

子组件接受父组件参数

defineProps来接受父组件传来的值

//index.vue
<Menu :active = "active"></Menu>
<NuxtPage :collection="defaultCollection" />

//Menu.vue 接受参数
defineProps({
  active:{
      type:Boolean,
      default:false
  }
})

const props = defineProps({
        title: String,
        info: String,
        author: String,
    })
// 等价于以 字符串数组声明 props
//const props = defineProps(['title', 'info', 'author']);
// 如果在setup中使用则直接 props.title

子组件向父组件传递参数或方法

defineExpose用法

defineExpose来导出自身的值或函数

//Roles.vue
<script setup>
let sonmsg = '这是子数据'
const open = () => { console.log("hello")}
//把数据导出
defineExpose({
  sonmsg,
  open
})
</script>

//index.vue
<template>
<div>
  <Roles ref="RoleRef"></Roles> 
</div>
</template>
<script setup>
let RoleRef = ref(null)  //数据挂载到RoleRef上
//在函数中使用
const onSubmit = () => {
  RoleRef.value.open()
  console.log(566, "imgmodel", RoleRef.value.sonmsg)
}
</script>

获取数据

参考

获取数据推荐useFetchfetch这两个函数就可以。

useFetch
//express
res.status(200).send({
  bot: "hello world"
})
//前端    
const url = "http://localhost:6200/test"
const fetchConfig = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: {
    prompt: "hello",
    temperature: 0.3
  }
}
const { data, pending, error, refresh } = await useFetch(url, fetchConfig)
// console.log(866, data.value.bot)
//参数:
baseURL?: string  //可以配置公共部分的url,再和url拼接
lazy?: boolean  //和pending 一起使用。为true时,先加载页面的pending,得到数据后再展示
transform?: (input: DataT) => DataT // 处理返回的数据

//如果有两个以上useFetch,得到的data会重名,可以另起名字
let { data:dataX,  error:errorX } = await getHttp('/pay/order', token.value)

// 错误处理 error
//express
return res.status(422).send("没有授权!")

let { data, error } = await useFetch(url, optionX)
if(error.value) {
  console.log(444, error.value)
  message.error("失败!\n"+error.value.data, { duration: 5e3 })
  return
}

更改查询结果的名称

transform 函数来对结果拦截,并做出一些处理逻辑。

const { data: mountains } = await useFetch('/api/mountains', {
  transform: (mountains) => {
    return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
  }
})

// 只选择模板中使用的字段
const { data: mountain } = await useFetch('/api/mountains/everest', {
  pick: ['title', 'description']
})
<template>
  <h1>{{ mountain.title }}</h1>
  <p>{{ mountain.description }}</p>
</template>


const { data, pending, error, refresh } = await useFetch("/api/getonepost", config, {
  transform: (res) => {
    return res.result
  }
})

$fetch

$fetch是与useFetch类似的封装函数,不过$fetch使用却是大大不同。
useFetch会和表格关联,造成一些不必要的http访问,$fetch则不会。
$fetch不能直接捕获到错误,只能通过 try catch

const baseURL = "http://localhost:6200"  
const option = {
  method: "POST",
  headers: {
    'Content-Type': 'application/json'
  },
  baseURL
}
const loginApi2 = async (url, body) => {
  let obj = {
      ...option,
      body
  }
  let res
  try {
    res = await $fetch(url, obj)
    console.log(566, "login api2", res)
  } catch (error) {
    res = error
    console.log(444, "login api2 error", error)
  }
  return res
}

fetch

fetch是浏览器原生的方法,非常简洁实用。像streaming(推流)也只能用 fetch 来实现,其它方法实现不了!

//注意:一定要加headers, 否则无法传值!
const fetchConfig = {
  method: 'POST',
  headers: {
      'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      prompt: prompt,
      temperature: 0.3
  })
  }
const s = await fetch(url, fetchConfig)
console.log(559, s) 
// 它得到的结果是直接传送回来的对象

eg: 
let option = {
  method: "POST",
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + captchaToken.value
  },
  body: JSON.stringify(form)
}
let response = await fetch(baseURL+'/userapi/register', option)
if(response.ok){
  let resX = await response.json()
  message.success("注册成功", { duration: 5e3 })
} else {
  let err = await response.text()
  message.error("错误!\n"+err, { duration: 5e3 }) 
}

useLazyFetch

useLazyFetch 为useFetch提供了一个包装器,通过将lazy选项设置为true,在处理程序解析之前触发导航。
默认情况下,useFetch 会阻塞导航,直到它的async处理程序被解决。useLazyFetch则不会阻塞,实时渲染数据!它将有更好的用户体验。
useLazyFetch 与 useFetch 具有相同的签名。

<template>
  <div v-if="pending">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      
    </div>
  </div>
</template>

<script setup>
/* Navigation will occur before fetching is complete.
  Handle pending and error states directly within your component's template
*/
const { pending, data: posts } = useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
  // Because posts starts out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

只客户端渲染

//可以用以下测试是否在客户端:
console.log(12, document.cookie)
console.log(33, window)
 setup

//组件中客户端渲染
<ClientOnly> component slot
<ClientOnly fallback-tag="span" fallback="Loading comments...">
  
</ClientOnly>

//js中的使用
if (process.server){
  //服务器端
}
process.client  //客户端

//在路由中设为单页面渲染,也就是关闭ssr
//nuxt.config.ts 如下设置,则gpt页面则只客户端渲染
routeRules: {
    '/gpt': { ssr: false },    
},

状态管理

useState用于创建响应式的且服务端友好的跨组件状态,类似于Vue中的state的功能。
简单的需求只用useState就可以,复杂的话可以结合pinia

const counter = useState('counter', () => Math.round(Math.random() * 1000))
counter++
counter.value = 20 //重新赋值

const user = useState('user', () => {
  return {
    token: token.value,
    user: userObj.value
 }
})

//composables/useState.ts
export const useCounter = () => useState<number>('counter', () => 0)
export const useColor = () => useState<string>('color', () => 'pink')

<script setup>
const color = useColor() // Same as useState('color')
</script>

// 初始化状态
使使 app.vue  callOnce 
<script setup lang="ts">
const websiteConfig = useState('config')

await callOnce(async () => {
  websiteConfig.value = await $fetch('https://my-cms.com/api/website-config')
})
</script>

Pinia

文档

管理全局状态的模块,可以和useState一起使用。

yarn add pinia @pinia/nuxt

modules: [
    // ...
    '@pinia/nuxt',
  ],

useCookie

在你的页面中,组件和插件你可以使用useCookie,一个SSR友好的组合来读写cookies。

const cookie = useCookie(name, options)
eg:
const counter = useCookie('counter', { maxAge: 60 }) //60秒有效期
counter.value = {
  name: "hello",
  age: 37
}
const token = useCookie('token', {maxAge: 60 * 5})
token.value = "hello world"

let user = useCookie('users', {maxAge: 60 * 60 * 24 * 30})  //30天 本地测试会有些问题
user.value = "hello ddskljdsfklj"
console.log(257, "user", user, 886, user.value)

// 获取
const s = useCookie('counter')
s.value

const balance2 = useCookie('balance', {maxAge: 60 * 60, SameSite: "none"})

const hasConsent = useCookie('dialog', {
  sameSite: 'strict',
  default: () => 'no',
  maxAge: 2592000, // 30 days in seconds
});

//删除则设值为null即可
s.value = null
const token = useCookie("token")
token.value = null

const s = useCookie('counter')
const removeCookie = () => {
    s.value = null
}

// 将用户登录成功返回的token存储在cookie当中,用户登录成功的标识
const token = useCookie("token")
token.value = data.value.token
const user = useUser()
user.value = data.value

//判断token是否有值
const token = useCookie("token")
token.value == undefined  //无值
 token.value == null
 !token.value  //无值则为true

//Server端的使用
// server/api/counter.ts
export default defineEventHandler(event => {
  // Read counter cookie
  let counter = getCookie(event, 'counter') || 0

  // Increase counter cookie by 1
  setCookie(event, 'counter', ++counter)

  // Send JSON response
  return { counter }
})

Layers

能够复用之前项目中的配置、已存在的组件、工具方法等等。Nuxt提供的 layers 特性能使我们非常容易地创建和维护这样的项目基础结构。
以下情况下比较适合使用 layers:

  • 共享可重用配置项;
  • 使用 components 目录共享组件库;
  • 使用 composables 和 utils 目录共享工具函数;
  • 创建 Nuxt 主题;
  • 创建模块预设;
  • 共享标准初始化步骤。

Server

文档
服务器 Server

Nuxt具备的类似于express的功能,具有接受和响应http的能力。
Nuxt自动扫描~/server/api, ~/server/routes, 和 ~/server/middleware目录中的文件,以注册具有HMR支持的API和服务器处理程序。
每个文件都应该导出一个用defineEventHandler()定义的默认函数。
处理程序可以直接返回JSON数据,一个Promise或使用event.node.res.end()发送响应。

//创建一个新文件server/api/hello.js:
//server/api/hello.js
export default defineEventHandler((event) => {
  return {
    api: 'works'
  }
})
//使用await $fetch('/api/hello')通用地调用这个API

eg2:
// server/routes/test.post.js  客户端必须使用POST方法
export default defineEventHandler(async (event) => {
  //获取客户端的body
  const body = await readBody(event)  
  console.log(695,"server body", body)
  return {
    hello: 'world'
  }
})
//客户端调用
const config = {
  method: 'POST',
  headers: {
      'Content-Type': 'application/json'
  },
  body: {
    content: "我是什么",
    temperature: 0.3
  }
}
const { data, pending, error, refresh } = await useFetch("/test", config)

// nuxt.config.ts设置
routeRules: {
  // 为 SEO 目的在构建时生成
  '/': { prerender: true },
  // 缓存 5分钟  好像在访问上出问题! 用 useState
  '/api/*': { cache: { maxAge: 60 * 5 } }
},

// 抛出错误
throw createError({
  statusCode: 400,
  statusMessage: "This user does not exist.",  //此处只能用英文,中文无法传输!
})

// String error where `statusCode` defaults to `500`
throw createError("An error occurred")

服务端工具函数

server/utils 目录中的工具函数,会自动导入
若用默认导出 export default ,则自动导入的函数名为文件名
若用具名导出 export function useSum ,则自动导入的函数名为函数定义的名称。

// server/utils/sum.ts
export function useSum(a: number, b: number) {
  return a + b;
}

//在接口文件中直接使用即可
// routes/index.ts
export default defineEventHandler(() => {
  const sum = useSum(1, 2) // auto-imported
  return { sum }
})

打包

  • SSR
    默认情况下,直接执行(nuxt build) yarn build。代码会被打包到.output目录,打包产物分为 public 和 server 两部分。入口为 index.mjs,可以使用 node 或 pm2 等进程管理工具启动服务, node ./.output/server/index.mjs,也可以配合nuxt preview启动预览服务。
  //默认情况下nuxt是不会将环境变量打包进去的,需要在package.json中配置
  "build": "source .env && nuxt build",

   node.js 
  `node .output/server/index.mjs`
  `node aijoe_v5/server/index.mjs`

  //ecosystem.config.js
  module.exports = {
    apps: [
      {
        name: 'czblog',
        port: '8080',
        exec_mode: 'cluster',
        instances: 'max',
        script: './.output/server/index.mjs'
      }
    ]
  }
  pm2 start ecosystem.config.js
  • SPA
    ssr:false + nuxt generate。产物只有 .output/public 中的静态文件,发布 .output/public(还有 dist) 即可。但是 SPA 需要在运行时访问接口获取数据,因此仍然需要提供接口服务才能正常显示页面。
    配置 ssr: false,然后执行 yarn generate
export default defineNuxtConfig({
  ssr: false,
})
  • SSG
    nuxt generate。产物只有 .output/public 中的静态文件,发布 .output/public 即可。这种方式会在创建时生成页面内容,因此只需要提供静态服务即可预览。

  • 其他服务:presets,可用于其他非 node 运行时打包,例如 deno,serverless,edge worker 等。产物根据预设不同会有不同,部署需要按照对应的平台进行操作。

启动端口

//开发环境
//package.json
"dev": "nuxt dev --port 3200"
// 或是这样:
"config": {  // here are the changes
    "nuxt": {
        "host": "0.0.0.0",
        "port": "3333"
    }
}
//或者在 nuxt.config.ts 中设置 只在开发中有效
devServer: {
  port: 9685
}
//或者,在 .env 中设置 `PORT = 9685` 即可!  
访
 if(process.server){
  console.log(566, process.env.PORT)
 } 

//设置服务器端口 port, 找到源码,直接修改即可!
 .output/server/index.mjs
//const port = destr(process.env.NITRO_PORT || process.env.PORT) || 3e3;
const port = 5896

数据推流

经测试,只有浏览器端的 fetch 才有 streaming 的能力,其它方法不行!
useFetch, $fetch都不行!

const url = "http://localhost:6200/gptstreaming"
const prompt = "天空为什么是蓝色的?"
let query = [{role: "user", content: prompt}]
let dataObj = {
  method: 'POST',
  headers: {
      'Content-Type': 'application/json',
  },
  body: JSON.stringify({
      query: query,
  })
}
const response = await fetch(url, dataObj)
if (response.ok) {  
  let i = 0
  let getStream = function (reader) {
      return reader.read().then(function (result) {
        // 如果数据已经读取完毕,直接返回
        if (result.done) {
          console.log(889, "result done")
          return
        }
        // 取出本段数据(二进制格式)
        let chunk = result.value
        console.log(226, chunk, typeof chunk)  //unit8array object
        let text = utf8ArrayToStr(chunk) //数据解析
        // 将本段数据追加到网页之中
        messageDiv.innerHTML += text
        // 递归处理下一段数据
        return getStream(reader)
      })
  }
  getStream(response.body.getReader())
}

加密

npm install bcrypt --save
// npm i node-gyp -g  如果node-pre-gyp ERR!
// npm install bcrypt --unsafe-perm --save

import bcrypt from 'bcrypt'
const saltRounds = 10

//加密
set(val) {
  let salt = bcrypt.genSaltSync(saltRounds)
  let hash = bcrypt.hashSync(val, salt)
  return hash
}

// 验证密码 为true则正确
const isPasswordValid = bcrypt.compareSync(
  req.body.password,
  user.password
)

jwt验证

Json Web Token (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。此信息可以进行验证和信任,因为它是经过数字签名的。可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 对公钥/私钥对对 JWT 进行签名。
可以认为JWT是用户认证的一种协议,可以理解为服务端颁发给客户端的一张身份证,拿到身份证后此后客户端进行接口访问时携带身份证,服务端通过验证以允许客户端访问服务器资源,每一项的含义:

  1. Json表示令牌的原始值是一个Json格式的数据
  2. web表示是在互联网传播
  3. token表示令牌

参考 |
参考2 |
参考3

npm install jsonwebtoken --save
// yarn add jsonwebtoken

import jwt from 'jsonwebtoken'

const SECRET = "fdfh85JYxxxxx"
// 生成token
const token = jwt.sign({
  id: String(user._id),
}, SECRET)

//时限
let token =  jwt.sign(data, SECRET, {
  expiresIn:  60 * 60 * 24 * 3 // 以s作为单位(目前设置的过期时间为3天)
});

//验证
const rawToken = String(req.headers.authorization).split(' ').pop()
const tokenData = jwt.verify(rawToken, SECRET)
// console.log(666, tokenData)  //{ id: 'dddfhxxx', iat: 1684119284, exp: 1684119304 }

// 获取用户id
const id = tokenData.id

// Authorization: Bearer 
const token = useCookie('token')
headers: {
    'Authorization': 'Bearer ' + token.value,
    'Content-Type': 'application/json'
  }

//token token过期的错误会在 catch 捕获 `jwt expired`
try{
  // 获取客户端请求头的token
  const rawToken = String(req.headers.authorization).split(' ').pop()
  if(rawToken == "undefined"){
    return res.status(422).send("您尚未登录!")
  } 
  const tokenData = jwt.verify(rawToken, SECRET)
  // 获取用户id
  const id = tokenData.id
  let user = await User.findById(id)
  if(user){
    req.user_id = id
    req.user = user
    next()
  } else {
    console.error(111)
    res.status(422).send("无效授权!")
  }
} catch (error) {
  console.error(112,"token auth", error)
  res.status(500).send('Something went wrong')
}

获取用户IP地址

async function smsGet(){
 let ip = req.headers['x-forwarded-for'] ||
        req.ip ||
        req.connection.remoteAddress ||
        req.socket.remoteAddress ||
        req.connection.socket.remoteAddress || '';
 if(ip.split(',').length>0){
    ip = ip.split(',')[0]
 }
 ip = ip.substr(ip.lastIndexOf(':')+1,ip.length);

console.log(123, ip) 
}
// smsGet()

图形验证码

文档 |
node生成验证码图片

npm install svg-captcha --save

const svgCaptcha = require('svg-captcha')

const codeConfig = {
  size: 6,// 验证码长度
  ignoreChars: '0o1i', // 验证码字符中排除 0o1i
  noise: 2, // 干扰线条的数量
  fontSize:42,
  color:true,//开启文字颜色
  background:"#cc9966",//背景色
  width:150,
  height: 44
}
const captcha = svgCaptcha.create(codeConfig)
//{text:jjkjk, data: dddd}

//图形验证码与jwt结合
1. `text` jwttoken,  data
2.  text_user token
3. token`text`text_user

//图形验证码与数据库结合
jwt`text` data

本地存值localStorage

// 用localStorage来实现一个预设值
1. localStorage
const defaultRole = ref(null)
if(process.client){
  let latest = localStorage.getItem("defaultRole")
  console.log(566, "latest", latest)
  if(latest != null){
    defaultRole.value = latest
    console.log(599, "latest")
  }
}   
2. //设置或更新 如果是空值 或是与前值不同,都重新赋值
let latest = localStorage.getItem("defaultRole")
if(latest == null || role != latest){
   localStorage.setItem('defaultRole', role)
   console.log(899, "set lastname")
}

删除div

js删除dom节点或div

<div id="img1">
  1
<input  type="button" onclick="del('img1')" value="删除元素"/>
</div>

<script>
function del(divId){
  document.getElementById(divId).remove()
}
</script>

模板字符串语法

字符串的扩展
模板字符串
ES6模板字符串
字符串模板拼接的点击事件

模板字面量是用反引号(`)分隔的字面量,允许多行字符串、带嵌入表达式的字符串插值和一种叫带标签的模板的特殊结构。

`string text`

`string text line 1
 string text line 2`

`string text ${expression} string text`
`string text ${fn()} string text`

//可以单行,多行,嵌入变量和函数都是可以的。还可以生成div结构
// 模板字符串生成dom元素
const loadingStripe = (uniqueId) => {
    return (
        ``
    )
}

//如果要嵌入点击事件,就要相对麻烦一些
// 需要与onclick结合,再将这个函数挂载到window上
const itemStripe = (username, email, created, uniqueId) => {
  return (
    `
    
X
名字: ${username}
email: ${email}
时间: ${TimesToLocal(created)}
`
) } if(process.client){ window.deleteUser = deleteUser } const deleteUser = async (userId) => { let body = { id: userId } let { data, error} = await postHttp('/deluser', body, 'he') if(error.value) { message.error("删除失败!\n"+error.value.data, { duration: 5e3 }) return } // document.getElementById(id).remove() message.success("删除成功!", { duration: 5e3 }) } //TimesToLocal是全局的方法,时间转换

实时渲染div

<div v-for="(item,index) in history" :key="index" 
  :id=item._id border-dashed border-1 text-3.2 mb-2
  >
    <div flex ><div ml-auto px-1 hover:bg-red @click="delX(item._id)">X</div></div>
    <div> {{item.username}}</div>  
    <div>email: {{item.email}}</div>
</div>
<div text-center><n-button @click="more" v-if="moreFlag" strong secondary></n-button></div>
//js
//把取得的值遍历,塞入temp, 再赋给history。history是响应式的,div就会实时渲染出来!
let history = ref(null)
let temp = []
for (let i in data.value.payhistory) {
    temp.push(data.value.payhistory[i])
  }
history.value = temp

//另一种方法
//直接使用document来操作DOM
<div id="item_container"></div> 
//js
const itemStripe = (userId, username, email,created, uniqueId) => {
  return (
    `  
    
X
名字: ${username}
email: ${email}
时间: ${TimesToLocal(created)}
`
) } let itemContainer = document.querySelector('#item_container') for (let i in data.value.payhistory) { console.log(56, data.value.payhistory[i]) let userId = data.value.payhistory[i]._id let username = data.value.payhistory[i].username let email = data.value.payhistory[i].email let created = data.value.payhistory[i].created let uniqueId = generateUniqueId() itemContainer.innerHTML += itemStripe(userId, username, email, created, uniqueId) }

上传图片至IPFS

//
<n-upload mt-2
  :default-upload="false"
  list-type="image-card"
  multiple
  @change="handleChangeX"
>
  
</n-upload>

//js
const handleChangeX = (options) => {
  if(options.fileList.length == 0) {
    return
  }
  handleClick(options.fileList[0].file) 
}
const handleClick = async (file) =>{
  let ref_img = await upImage(file)
  prompt.value += ref_img+' '
}

//composables
yarn add ipfs-http-client

import {create} from 'ipfs-http-client'
import { Buffer } from 'buffer'

export const readFile = (file) => {
  if (process.client){
    return new Promise(resolve => {
      let reader = new FileReader()
      reader.readAsArrayBuffer(file)
      reader.onload = (event) => {
        resolve(Buffer.from(event.target.result))
      }
    })
  }  
}
 
export const upImage = async(pos, file) => {
  if (process.client){
    const ipfs = create({ host: 'ipfs.example.com', port: '9059', protocol: 'https' })
    const ipfs_host = "https://example.com/ipfs/"
    let content = await readFile(file)
    let res = await ipfs.add(content)
    return ipfs_host+res.path
  }
}

显示图片

 <n-image
    width=98%
    mt-3
    src="https://example.com/ipfs/QmQKptGbRRziEjxxxxxx" 
    rounded-2 alt="AIJoe Meme"
 /> 
 //width=98% 可以使得图片自适应大小

<div class="contentimg">
  <img
    width=98%
    mt-3
    :src="takeImage(item.body)"
    rounded-2
  /> 
</div> 
<style>
.contentimg {
  width: 98%;
  height: 28rem;
  overflow: hidden;
  margin: auto;
} 
.contentimg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
</style>

图片自适应宽度或高度

参考

.container {
  width: 50%;
  height: 300px;
  overflow: hidden; /* 防止图片溢出容器 */
}

.container img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

 container  50%  300px overflow  hidden

 img 使 width  height 使 object-fit object-fit  cover

mavon-editor

文档
文档2
github
新版安装指南

1.yarn add mavon-editor@next  //"^3.0.2"
// npm install mavon-editor@next --save

2. ~/plugins/mavon-editor.client.js
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(mavonEditor)
})

3. 使
<template>
  <ClientOnly>
    <mavon-editor v-model="content" :toolbars="toolbarsConfig"/>
  </ClientOnly>
</template>

<script setup>
const model = ref()
const toolbarsConfig = {
  // ... toolbars config
}
</script>

4. 
//增加两个参数: md、@imgAdd
<mavon-editor v-model="content" ref="md" @imgAdd="upImage" /> <br>
<script setup>
  let md = ref()
  const readFile = (file) => {
    return new Promise(resolve => {
      let reader = new FileReader()
      reader.readAsArrayBuffer(file)
      reader.onload = (event) => {
        resolve(Buffer.from(event.target.result))
      }
    })
   }
  const upImage = async(pos, file) => {
    //换成自己的ipfs主机,或是使用自己的上传逻辑
    const ipfs = create({ host: 'ipfs.example.io', port: '2885', protocol: 'https' })
    const ipfs_host = "https://ipfs.example.io/ipfs/" 
    let content = await readFile(file)
    let res = await ipfs.add(content)
    md.value.$img2Url(pos, ipfs_host+res.path)
  }
</script>

5. 稿
@save="saveMavon"   ctrl+s
saveMavon(value,render){
  console.log("this is render"+render)  //html渲染的格式 
  console.log("this is value"+value)   //原始格式, String
}


6. markdown
<ClientOnly>   
  <mavon-editor 
    v-model="post.result.body"
    :subfield="false"
    defaultOpen="preview"
    :toolbarsFlag="false"
    :editable="false"
    :scrollStyle = true
    :ishljs="true"
  ></mavon-editor>
</ClientOnly>

nuxt-tiptap-editor

文档 |
参考 |
参考 |

yarn add nuxt-tiptap-editor
// npx nuxi@latest module add tiptap

//nuxt.config.ts设置:
//会自动添加
modules: ['nuxt-tiptap-editor']
//component -> TiptapEditor.vue
<template>
  <TiptapEditorContent :editor="editor" />
</template>
<script setup>
const editor = useEditor({
  content: "

I'm running Tiptap with Vue.js.

"
, extensions: [TiptapStarterKit], }) const getContent = () => { console.log(111, unref(editor)) //是主要的使用方法集 unref(editor).commands console.log(9988, unref(editor).getHTML()) return unref(editor).getHTML() //getText({ blockSeparator: "\n\n" }) getJSON() } onBeforeUnmount(() => { unref(editor).destroy() }) //如果父组件调用的话就用 defineExpose的方法 defineExpose({ getContent }) </script> // 父组件 edit.vue <TiptapEditor ref="contentRef"/> const contentRef = ref() const content = contentRef.value.getContent() //即可得到编辑器中的数据

turndown

HTML转Markdown 能将复杂的HTML结构转化为简洁的Markdown,方便编辑和发布。
Turndown是轻量级的JavaScript库,适合前端或Node.js项目,易于集成到Web应用。

yarn add turndown  //7.2.0
// npm install marked turndown

import TurndownService from 'turndown'

const turndownService = new TurndownService()

const html = '

Hello, World!

This is a sample HTML document.

'
const markdown = turndownService.turndown(html) console.log(markdown) const { marked } = require('marked') const Turndown = require('turndown') const turndownService = new Turndown() const html = '

Dynamic

Content

'
const markdown = turndownService.turndown(html) console.log(markdown)

Marked

参考

解析Markdown文本

yarn add marked
// npm install marked
// const marked = require('marked');
import { marked } from 'marked'

const markdown = '# Hello, world!'
const options = {
  gfm: true,
  breaks: true,
  smartLists: true,
  highlight: function(code) {
    return require('highlight.js').highlightAuto(code).value
  }
}

const html = marked(markdown, options)
// const html = marked.parse('# Marked in Node.js\n\nRendered by **marked**.')
console.log(html)

<div v-html="mdshow(md)"></div>
const mdshow = (md) => marked(md, options)

实时监听和渲染

//ref() 和 computed() 被自动导入
const count = ref(1)
const double = computed(() => count.value * 2)

//使用computed监听路由变化
const route = useRoute()
const pageKey = computed(()=>route.fullPath)
const activeName = computed(()=>route.name)


//使用watch监听路由变化
watch(route, (to) => {
  console.log('路由',to)
}, {flush: 'pre', immediate: true, deep: true})


// 使用watch监听数据变化,做出相应逻辑
watch(tagOption, () => {
  console.log(6688, 'tagOption2', tagOption.value[[tagOption.value.length - 1]])
  tagsX.value += tagOption.value[[tagOption.value.length - 1]]
}, {flush: 'pre', immediate: true, deep: true})


//使用watch监听路由变化,即刻获取数据,渲染页面
// 每当进入页面都会发生一次 watch
// 每当 keywords 数据发生一次变化,就会发生一次 watch
watch(()=>route.query.keywords,(newVal)=>{
    keywords.value = newVal
    getData()
}, {flush: 'pre', immediate: true, deep: true})

实时多选数据进输入框

使用watch实现实时数据更新

//
<div m-y-3>
<n-checkbox-group v-model:value="tagOption">
  <n-space item-style="display: flex;">
    <n-checkbox value=" blockchain " label="区块链" />
    <n-checkbox value=" cryptocurrency " label="加密货币" /> 
    <n-checkbox value=" bitcoin " label="比特币" />
  </n-space>
</n-checkbox-group>
</div>
<div m-b-3>
<n-input
    v-model:value="tagsX"
    type="text"
    placeholder="输入英文标签,以逗号或空格分隔"
/>
</div>
<script setup>
const tagOption = ref(' ')
const tagsX = ref('cn  cn-reader ')

watch(tagOption, () => {
  tagsX.value += tagOption.value[[tagOption.value.length - 1]] //每次增加一条
}, {flush: 'pre', immediate: true, deep: true})
</script>

Leave Nuxt 开发指南 to:

Written by

Designer , Poet , Technology enthusiasts

Read more #cn posts


Best Posts From lemooljiang

We have not curated any of lemooljiang's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.

More Posts From lemooljiang