Vue3 + Nuxt3 项目笔记

Vue3+Nuxt3在线教育项目笔记

1. 项目初始化

1.1 引入Nuxt3

初始化项目

1
2
npx nuxi init nuxt-edu
npm install

运行项目

1
yarn dev

1.2 引入Naive-UI

参考官方文档服务端渲染SSR引入流程

  1. 安装 naive-ui@css-render/vue3-ssr

  2. nuxt.config.ts 增添下列配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // https://v3.nuxtjs.org/api/configuration/nuxt.config
    export default defineNuxtConfig({
    build: {
    transpile:
    process.env.NODE_ENV === 'production'
    ? [
    'naive-ui',
    'vueuc',
    '@css-render/vue3-ssr',
    '@juggle/resize-observer'
    ]
    : ['@juggle/resize-observer']
    },
    vite: {
    optimizeDeps: {
    include:
    process.env.NODE_ENV === 'development'
    ? ['naive-ui', 'vueuc', 'date-fns-tz/esm/formatInTimeZone']
    : []
    }
    }
    })
  3. 在 Nuxt 项目的 plugins 文件夹增加这个插件

1.3 引入Windi CSS

  1. 安装

    1
    yarn add nuxt-windicss -D
  2. nuxt.config.ts 增添下列配置

    1
    2
    3
    4
    5
    export default {
    buildModules: [
    'nuxt-windicss',
    ],
    }

1.4 自定义全局错误页面

从naive-ui中复制错误模板页,拷贝到根目录的error.vue页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="pt-[80px]">
<n-result status="500" title="错误提示" :description="error.message">
<template #footer>
<n-button @click="handleError">回到首页</n-button>
</template>
</n-result>
</div>
</template>

<script setup>
import {
NResult,
NButton
} from 'naive-ui'
const props = defineProps({
error: Object
})

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

1.5 自定义全局loading

在plugins中创建globalloading.js文件,在生命周期hook函数中加载loading条

loadingBar设置参考naive-ui的独立API设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import {
createDiscreteApi
} from 'naive-ui'

export default defineNuxtPlugin((nuxtApp) => {
const bar = ref(null)
nuxtApp.hook("app:mounted", (e) => {
if (!bar.value) {
const { loadingBar } = createDiscreteApi(["loadingBar"])
bar.value = loadingBar
}
console.log("app:mounted")
})
nuxtApp.hook("page:start", (e) => {
bar.value?.start()
console.log("page:start")
})
nuxtApp.hook("page:finish", (e) => {
setTimeout(() => {
bar.value?.finish()
}, 150)
console.log("page:finish")
})
nuxtApp.hook("app:error", (e) => {
if (process.client) {
setTimeout(() => {
bar.value?.finish()
}, 150)
}
})
})

2. 全局Layout布局

2.1 主布局实现

  1. 修改app.vue页面,用NuxtLayout包裹NuxtPage,并禁用inline主题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <template>
    <NConfigProvider inline-theme-disabled>
    <NuxtLayout>
    <NuxtPage/>
    </NuxtLayout>
    </NConfigProvider>
    </template>

    <script setup>
    import {
    NConfigProvider
    } from "naive-ui"
    </script>
  2. 根目录创建layouts文件夹,创建default.vue文件

    1
    2
    3
    4
    5
    6
    7
    <template>
    <div class="body">
    <NavBar/>
    <slot/>
    <PageFooter/>
    </div>
    </template>
  3. 根目录创建components文件夹,创建导航栏与页脚两个组件

2.2 引入全局css

保证每个页面的左右边距一致

  1. 在根目录下创建assets文件夹,创建main.css文件

    1
    2
    3
    4
    5
    6
    7
    8
    .container {
    width: 100%;
    max-width: 1140px;
    padding-left: 15px;
    padding-right: 15px;
    margin-left: auto;
    margin-right: auto;
    }
  2. 在nuxt.config.ts文件中引入css

    1
    2
    3
    css: [
    "@/assets/main.css"
    ],
  3. 更改default.vue页面,加入container样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <template>
    <div class="body">
    <NavBar/>
    <main class="container">
    <slot/>
    </main>
    <PageFooter/>
    </div>
    </template>

2.3 引入图标库

  1. 安装图标

    1
    npm i -D @vicons/ionicons5
  2. 在vue中引入import

    1
    2
    3
    4
    5
    6
    7
    8
    <script setup>
    import {
    NIcon
    } from "naive-ui"
    import {
    Search
    } from "@vicons/ionicons5"
    </script>

2.4 公共头部开发

修改components/NavBar组件,设置不同的css样式

  1. 左侧logo采用NButton制作

  2. 中间偏左为导航菜单栏

  3. 右侧为搜索与登陆按钮

    1. 搜索采用带图标的button组件
    2. 用户头像登陆按钮采用NDropdown组件

2.5 动态路由

动态路由跳转页面

1
2
3
<div class="menu">
<div class="menu-item" v-for="(item, index) in menus" :key="index" :class="{'menu-item-active': (route.path == item.path)}" @click="handleOpen(item.path)">{{ item.name }}</div>
</div>

在pages下创建页面时,需要遵循一定规则

  • 带有中括号表示是动态路由
  • menus中的path路径属性与pages中的文件目录及文件一一对应

image-20230126214254731

2.6 组件封装

  1. 通过插槽slot代表需要存放的内容

  2. 通过defineProps定义组件中传递进来的参数

    1
    2
    3
    4
    5
    6
    defineProps({
    active: {
    type: Boolean,
    default: false
    }
    })

2.7 引入Element-Plus以及图标库

  1. 安装相应库

    1
    npm install sass element-plus @element-plus/icons-vue unplugin-vue-components unplugin-auto-import --save
  2. 在assets/index.scss中加入如下内容,放置到首行

    1
    @use "element-plus/dist/index.css";
  3. 在plugins下创建 element-plus.client.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    import * as ElementPlus from 'element-plus/dist/index.full'
    import zhCn from 'element-plus/es/locale/lang/zh-cn'

    export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.vueApp.use(ElementPlus, {
    locale: zhCn,
    })
    })
  4. 修改 nuxt.config.ts 如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // https://v3.nuxtjs.org/api/configuration/nuxt.config
    export default defineNuxtConfig({
    css: [
    'element-plus/dist/index.css',
    "~/assets/main.scss"
    ],
    modules: [
    'nuxt-windicss',
    ],
    build: {
    transpile:
    [
    'naive-ui',
    'vueuc',
    '@css-render/vue3-ssr',
    '@juggle/resize-observer',
    "element-plus"
    ]
    },
    vite: {
    optimizeDeps: {
    include: ['date-fns-tz/esm/formatInTimeZone']
    }
    }
    })
  5. 在对应的vue中按需引入

    1
    2
    import { ElButton, ElIcon } from "element-plus";
    import { Star } from "@element-plus/icons-vue";

3. 前后端数据交互

3.1 未封装前

采用useFetch方法调用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const {
pending,
data,
refresh,
error
} = await useFetch("/index", {
key: "IndexData",
baseURL: "http://demonuxtapi.dishait.cn/pc",
headers: {
appid: "bd9d01ecc75dbbaaefce"
},
// 响应之前的数据处理
transform: (res) => {
return res.data
},
// 是否开启缓存
initialCache: false,
// 懒加载
lazy: true
})

3.2 请求和响应拦截器封装

创建composables文件夹,在其中创建useHttp.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const fetchConfig = {
baseURL: "http://demonuxtapi.dishait.cn/pc",
headers: {
appid: "bd9d01ecc75dbbaaefce"
},
}

function useGetFetchOptions(options = {}) {
options.baseURL = options.baseURL ?? fetchConfig.baseURL
options.headers = options.headers ?? {
appid: fetchConfig.appid
}
options.initalCache = options.initialCache ?? false
options.lazy = options.lazy ?? false

// 用户登陆,默认传token
return options
}

export async function useHttp(key, url, options = {}) {
options = useGetFetchOptions(options)
options.key = key
let res = await useFetch(url, {
...options,
// 相当于响应拦截器
transform: (res) => {
return res.data
}
})
// 错误处理
return res
}

// Get请求
export function useHttpGet(key, url, options = {}) {
options.method = 'GET'
return useHttp(key, url, options)
}

// POST请求
export function useHtppPost(key, url, options = {}) {
options.method = 'POST'
return useHttp(key, url, options)
}

4. 自定义全局方法

  1. 在plugins目录下新建tools.js文件

  2. 定义方法:commonOpen

    1
    2
    3
    4
    5
    6
    7
    export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.provide("commonOpen", (item) => {
    if(item.type == 'webview') {
    window.open(item.url)
    }
    })
    })
  3. 调用:$开头

    1
    @click="$commonOpen(item)"

定义插件之后需要重启页面

5. 页面标题设置

  1. 全局标题:在nuxt.config.js中配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    app: {
    head: {
    titleTemplate: '%s - 帝莎编程', // 所有页面标题模板
    title: '帝莎编程',
    charset: 'utf-8',
    htmlAttrs: {
    lang: "zh-cn"
    },
    meta: [
    { name: 'description', content: '帝莎编程描述' },
    { name: 'keywords', content: '帝莎编程关键词' }
    ],
    script: [
    // { src: "http://xxx.js" }
    ],
    link: [
    // { rel: "stylesheet", href: "http://xxx.css" }
    ]
    }
    }
  2. 页面标题:在对应的vue-script中配置

    1
    2
    3
    useHead({
    title: '帝莎编程首页'
    })

6. 父子组件通信

6.1 父传子

  1. 父组件在子组件上标记ref

    1
    <SearchBar ref="SearchBarRef"></SearchBar>
  2. 父组件定义方法,调用子组件内的方法

    1
    2
    const SearchBarRef = ref(null)
    const openSearch = () => SearchBarRef.value.open() // 按钮点击调用openSearch
  3. 子组件定义被ref调用的方法

    1
    2
    3
    4
    const open = () => {
    keyword.value = ""
    drawer.value = true
    }

注意:vue3内通过ref定义的const常量,需要通过.value获取值和修改值

7. 指定页面布局

某些页面不使用default布局,而是自定义另外的布局

在这些页面的script中加入:

1
2
3
definePageMeta({
layout: 'login'
})

8. CSS不可复制

1
2
3
4
5
6
7
body {
-webkit-user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
user-select: none;
}

Vue3 + Nuxt3 项目笔记
https://ltyzzzxxx.github.io/2023/01/30/Vue3-Nuxt3-项目笔记/
作者
ltyzzz
发布于
2023年1月30日
许可协议