
-
[naive UI](#naive UI "#naive-ui")
-
[fetch](#fetch "#fetch")
下载与资源
安装
"nuxt": ^4.0.3 nodejs: v22.17.1
js
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是一个全栈框架,这意味着在不同的上下文中,有几种不可避免的用户运行时错误来源:
-
Vue渲染生命周期中的错误(SSR + SPA)
-
API或Nitro服务器生命周期中的错误
-
服务器和客户端启动错误(SSR + SPA)
js
//全局错误页面 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
js
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>
消息显示
使用 createDiscreteApi 来创建一系列消息提示,它比较自由,推荐。
js
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 的样式。
js
// 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基本语法
可以认为Unocss兼具了TailwindCSS 和 Windicss的优点,同时还具有自身的定制性和灵活性。按需引入,体积小,速度快。
css
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图标
html
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')"
/>
全局变量配置
js
//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"
页面关键词配置
js
//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
```js
// 方法一
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中引用第三方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的引入
js
可组合函数: 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自动导入辅助函数和其他实用程序!
js
export function test2() {
console.log(12793, "test.js2")
}
//也可以自定义导出目录
//nuxt.config.ts
imports: {
dirs: ["apis"]
}
composables
在composables/目录中编写自己的自动导入可重用函数。但它只导入顶层函数,如有二级目录,则必须在config中配置!
js
export const newFun = (i) => {
return i+5
}
//composables/gets/foo.js
imports: {
dirs: ["composables/**"]
},
```
## 中间件
middleware 目录,Nuxt提供了一个可定制的路由中间件框架,可以在整个应用程序中使用,非常适合在导航到特定路由之前提取想要运行的代码。
```js
//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
另外,中间件全名不能使用小驼峰的方法,要使用 `-` !
插件
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布局
js
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 表示占比
html
<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 文件夹,在此文件夹内创建页面。
js
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
js
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访问路由。
js
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 来访问路由器。
js
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
js
// 将 '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 钩子。
js
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的钩子函数
js
//有时无法自动运行
onMounted(() => {
console.log(699, "moundted")
function x (){
console.log(722, "moundted")
}
x()
})
//beforeMount mounted
翻页时默认回到顶部
js
//app -> router.options.js
export default {
scrollBehavior (to, from, savedPosition) {
// 在按下 后退/前进 按钮时。就会像浏览器的原生表现那样
if(savedPosition){
return savedPosition
}
return {
top:0
}
}
}
组件
js
components -> NaveBar.vue,
<NaveBar /> 可直接导入无需引入
嵌套组件
js
components -> Ui -> Menu.vue
<UiMenu>
test
</UiMenu>
// 或者这样写:
<ui-menu>
test
</ui-menu>
子组件接受父组件参数
defineProps
来接受父组件传来的值
js
//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
来导出自身的值或函数
js
//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>
获取数据
获取数据推荐useFetch
、fetch
这两个函数就可以。
js
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
函数来对结果拦截,并做出一些处理逻辑。
js
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
。
js
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 来实现,其它方法实现不了!
js
//注意:一定要加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 具有相同的签名。
js
<template>
<div v-if="pending">
Loading ...
</div>
<div v-else>
<div v-for="post in posts">
<!-- 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>
只客户端渲染
js
//可以用以下测试是否在客户端:
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
js
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一起使用。
js
yarn add pinia @pinia/nuxt
modules: [
// ...
'@pinia/nuxt',
],
useCookie
在你的页面中,组件和插件你可以使用useCookie,一个SSR友好的组合来读写cookies。
js
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
Nuxt具备的类似于express的功能,具有接受和响应http的能力。
Nuxt自动扫描~/server/api, ~/server/routes, 和 ~/server/middleware目录中的文件,以注册具有HMR支持的API和服务器处理程序。
每个文件都应该导出一个用defineEventHandler()定义的默认函数。
处理程序可以直接返回JSON数据,一个Promise或使用event.node.res.end()发送响应。
js
//创建一个新文件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 ,则自动导入的函数名为函数定义的名称。
js
// 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
启动预览服务。
js
//默认情况下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
js
export default defineNuxtConfig({
ssr: false,
})
- SSG
nuxt generate。产物只有 .output/public 中的静态文件,发布 .output/public 即可。这种方式会在创建时生成页面内容,因此只需要提供静态服务即可预览。
- 其他服务:presets,可用于其他非 node 运行时打包,例如 deno,serverless,edge worker 等。产物根据预设不同会有不同,部署需要按照对应的平台进行操作。
启动端口
js
//开发环境
//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都不行!
js
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())
}
加密
js
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是用户认证的一种协议,可以理解为服务端颁发给客户端的一张身份证,拿到身份证后此后客户端进行接口访问时携带身份证,服务端通过验证以允许客户端访问服务器资源,每一项的含义:
-
Json表示令牌的原始值是一个Json格式的数据
-
web表示是在互联网传播
-
token表示令牌
js
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地址
js
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()
图形验证码
js
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
js
// 用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
<div id="img1">
图片1
<input type="button" onclick="del('img1')" value="删除元素"/>
</div>
<script>
function del(divId){
document.getElementById(divId).remove()
}
</script>
模板字符串语法
模板字面量是用反引号(`)分隔的字面量,允许多行字符串、带嵌入表达式的字符串插值和一种叫带标签的模板的特殊结构。
js
`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
html
<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 (
`
<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
js
//<!-- 上传图片 -->
<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
}
}
显示图片
js
<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>
图片自适应宽度或高度
js
.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
js
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
js
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应用。
js
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文本
js
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)
实时监听和渲染
js
//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实现实时数据更新
js
//<!-- 标签选项 -->
<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>