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. 基本使用,可以直接在class中定义。在Unocss省略class直接写样式也是可以的,而且有代码提示。
<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> <hr style="border: 1px solid gray; width: 75%;"></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 个断点,sm、md、lg、xl、2xl :
断点前缀    最小宽度    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" /> //向下的


(html comment removed:  手机端 )
<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 中引入样式。注意不是在<style>中!
<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入门

在Nuxt中引用第三方js等资源文件,可将文件放在/assets或/public目录下
区别
    /assets目录下的文件会被webpack编译
    /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, useHead只能与组件的setup和生命周期钩子一起使用
<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):跳过,留在 fromnavigateTo(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`中加入`<NuxtPage/>`即可索引到下一级的页面(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: 与当前路由关联的编码URL,包含path、query和hash
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">
      (html comment removed:  do something )
    </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`用 jwt生成token, 和 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 (
        `<span style='font-size: 20px;color: #191970;' id=${uniqueId}></span>`
    )
}

//如果要嵌入点击事件,就要相对麻烦一些
// 需要与onclick结合,再将这个函数挂载到window上
const itemStripe = (username, email, created, uniqueId) => {
  return (
    `
    <div border-dashed border-1 text-3.2 mb-2 id=${uniqueId}>
      <div flex ><div ml-auto px-2 hover:bg-red onclick="deleteUser('${userId}')">X</div></div> 
      <div>名字: ${username}</div>  
      <div>email: ${email}</div>
      <div>时间: ${TimesToLocal(created)}</div>
    </div>
    `
  )
}
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, 再赋给historyhistory是响应式的,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 (
    `  
    <div border-dashed border-1 text-3.2 mb-2 id=${uniqueId}>
      <div flex ><div ml-auto px-1 hover:bg-red onclick="deleteUser('${userId}','${uniqueId}')">X</div></div> 
      <div>名字: ${username}</div>  
      <div>email: ${email}</div>
      <div>时间: ${TimesToLocal(created)}</div>
    </div>
    `
  )
}
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

//(html comment removed:  上传图片 )
<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 标签中,我们使用了 widthheight 属性将图片的大小设置为与容器相同,并且使用了 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: "<p>I'm running Tiptap with Vue.js.</p>",
  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 = '<h1>Hello, World!</h1><p>This is a <em>sample</em> HTML document.</p>'
const markdown = turndownService.turndown(html)

console.log(markdown)


const { marked } = require('marked')
const Turndown = require('turndown')
const turndownService = new Turndown()
 
const html = '<div><h1>Dynamic</h1><p>Content</p></div>'
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实现实时数据更新

//(html comment removed:  标签选项 )
<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>