Nuxt 开发指南
- 下载与资源
- 安装
- 错误排除
- 全局错误处理
- 渲染模式
- naive UI
- 消息显示
- Unocss安装
- Unocss基本语法
- icones图标
- 全局变量配置
- 页面关键词配置
- 全局CSS
- 全局js
- 外部js和css的引入
- 全局函数utils
- composables
- 中间件
- 插件
- layouts布局
- Grid栅格系统
- 页面和路由
- 动态路由
- 获取当前路径
- useRouter
- navigateTo
- 生命周期函数
- Vue的钩子函数
- 翻页时默认回到顶部
- 组件
- 嵌套组件
- 子组件接受父组件参数
- 子组件向父组件传递参数或方法
- 获取数据
- 更改查询结果的名称
- $fetch
- fetch
- useLazyFetch
- 只客户端渲染
- 状态管理
- Pinia
- useCookie
- Layers
- Server
- 服务端工具函数
- 打包
- 启动端口
- 数据推流
- 加密
- jwt验证
- 获取用户IP地址
- 图形验证码
- 本地存值localStorage
- 删除div
- 模板字符串语法
- 实时渲染div
- 上传图片至IPFS
- 显示图片
- 图片自适应宽度或高度
- mavon-editor
- nuxt-tiptap-editor
- turndown
- Marked
- 实时监听和渲染
- 实时多选数据进输入框
下载与资源
官网 |
中文文档 |
中文文档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是一个全栈框架,这意味着在不同的上下文中,有几种不可避免的用户运行时错误来源:
- Vue渲染生命周期中的错误(SSR + SPA)
- API或Nitro服务器生命周期中的错误
- 服务器和客户端启动错误(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>
</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" /> //向下的
<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
在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):跳过,留在 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: 与当前路由关联的编码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来导出自身的值或函数
//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这两个函数就可以。
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
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是用户认证的一种协议,可以理解为服务端颁发给客户端的一张身份证,拿到身份证后此后客户端进行接口访问时携带身份证,服务端通过验证以允许客户端访问服务器资源,每一项的含义:
- Json表示令牌的原始值是一个Json格式的数据
- web表示是在互联网传播
- token表示令牌
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()
图形验证码
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
<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
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:
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.