前端vue.js后端node.js连接Mysql上线阿里云的全栈开发(三、主页面)

394 阅读9分钟

前言

第二章丰富登录页面的功能,使得用户的信息得以被安全保护、也避免了用户的非正规操作即设置了路由守卫并给数据生成token令牌、创建了注册的功能让新用户的数据注入数据库。

正文

完成了登录和注册的所有内容,接下来就开始编写我们的主页面,丰富页面的内容,实现一些功能

  1. 设置路由白名单(三个页面不在路由守卫范围内)
  2. 主页面的编写(原始页面NoteClass.vue、子组件个人信息页面Menu.vue)
  3. 主页面中的笔记列表Note.vue(前后端的完成)
  4. 列表页面中的具体笔记NoteDeail.vue(前后端的完成)

设置路由白名单

在日常使用的很多app中会发现这么一个现象,即使没有登陆也依然可以访问首页,当点进进入商品或者文章时才弹出登录框,这是因为app为一些页面设置了白名单,即没有用户信息也能直接访问,当发送其他未进入白名单的页面时才会受到路由守卫的拦截

在路由守卫设置白名单

const whilePath = ['/login','/register','/noteClass']

主页面

原始页面NoteClass.vue

<template>
    <div class="note-class-wrapper">
        <div class="note-class" :class="{ hide: isShowMenu }">
            <header>
                <div @click="isShowMenu = true">
                    <van-icon name="wap-nav" />
                </div>
                <div>
                    <van-icon name="edit" @click="router.push('/notePublish')"/>
                    <van-icon name="like-o" />
                    <van-icon name="search" />
                </div>
            </header>

            <section>
                <div class="note-item" v-for="(item, index) in noteClassList" 
                :key="index" :style="{ backgroundColor: item.bgColor }" @click="goNoteList(item.title)">
                    <span class="title">{{ item.title }}</span>
                </div>
            </section>
        </div>

        <!-- menu -->
        <Menu class="menu" @hidden="handle" :class="{ show: isShowMenu }"></Menu>
         
    </div>
</template>
50dbbb92461426778f63e607b761cb9.png

分为两部分头部和身体部分

  • 头部由几个图标组成,分别为个人信息标签、写笔记标签、收藏标签、搜索标签(这些后续东江设置为点击事件,分别跳转至相应的页面)
  • 身体部分为所有的笔记的分类
头部

头部的图标可以在vant库中找到

Icon 图标 - Vant 4 (vant-ui.github.io)

main.js中引入Icon

import { createApp } from 'vue';
import { Icon } from 'vant';
const app = createApp();
app.use(Icon);

点击想要的图标点击复制,再粘贴使用即可

3b8a79e3e8e16c37a7a9cd538342f66.png

身体
<div class="note-item" v-for="(item, index) in noteClassList" 
   :key="index" :style="{ backgroundColor: item.bgColor }" @click="goNoteList(item.title)">
   <span class="title">{{ item.title }}</span>
</div>

列表中的每个项都是noteClassList数组中的一个对象。使用v-for指令来遍历数组,并为每个对象生成一个带有类名note-itemdiv元素。

div元素的key属性设置为数组项的索引,style属性设置为对象的bgColor属性值作为背景色。当用户点击div元素时,会触发goNoteList方法,并将对象的title属性作为参数传递给该方法。

循环noteClassList数组中的每一个对象

const noteClassList = [
  { bgColor: '#f0aa84', title: '美食' },
  { bgColor: '#dcf189', title: '旅行' },
  { bgColor: '#e0c2f1', title: '恋爱' },
  { bgColor: '#c2ebf1', title: '学习' },
  { bgColor: '#949c9d', title: '吵架' }
]

个人信息标签

头部由几个图标组成,其中个人信息标签是设置在原始页面NoteClass.vue中属于它的子组件,而其他三个标签是新的页面,是一种有趣的交互体验。

当点击个人信息标签时,将父组件隐藏且子组件显露,反之在子组件点返回时将子组件隐藏父组件显露

将子组件Menu.vue引入父组件

<Menu class="menu" @hidden="handle" :class="{ show: isShowMenu }"></Menu>
import Menu from '../components/menu.vue'
个人信息页面Menu.vue
<template>
    <div class="menu-wrap">
      <div class="back" @click="hideMenu">
        <van-icon name="arrow-left" size="26px"/>
      </div>
      <section class="header">
        <div class="avatar">
          <img src="https://ts1.cn.mm.bing.net/th/id/R-C.d0cdb390350600169835c8343480b0af?rik=gLkyDf9xwbXdQg&riu=http%3a%2f%2finews.gtimg.com%2fnewsapp_match%2f0%2f15103659087%2f0&ehk=zK0MTj34tChhjHcpXaZER22pNZjchBBOnrkMTllNa0w%3d&risl=&pid=ImgRaw&r=0" alt="">
        </div>
        <p class="user">坤坤</p>
      </section>
  
      <div class="setting">
        <div class="set-item">
          <van-icon name="contact" size="0.4rem"/>
          <span>个人主页</span>
        </div>
        <div class="set-item">
          <van-icon name="bullhorn-o" size="0.4rem"/>
          <span>通知</span>
        </div>
        <div class="set-item">
          <van-icon name="revoke" size="0.4rem"/>
          <span>退出登录</span>
        </div>
      </div>
    </div>
  </template>

父组件和子组件的关系如同这个样子,争抢一块屏幕 a265913373b6de48fe20a4d2501d3d4.png

属于是个典型的父子路由通信,可以使用多种方法来传递数值,比如创建事件监听,父子组件互相订阅一个变量,监听并实时改变

在父组件中设置

动态绑定CSS类:divMenu元素的class属性通过:class动态绑定,其中{ hide: isShowMenu }表示如果isShowMenu的值为true,则div会添加hide这个CSS类。

组件事件监听:Menu组件被包含在这个div中,其@hidden事件被绑定到handle函数。这意味着当Menu组件的hidden事件触发时,handle函数会被调用。

状态管理: isShowMenu是一个使用ref创建的响应式引用,初始值为false。这通常用于控制某些UI元素的显示或隐藏。

事件处理函数:handle函数接收一个事件参数e(虽然在这个函数体中没有使用),当调用时,它会将isShowMenu的值设置为false,从而可能触发div元素上的hide类的添加,导致菜单隐藏。

在子组件中设置

事件发射器定义:使用defineEmits定义了一个可以发射的自定义事件hidden。这意味着组件可以通过emits对象发射hidden事件,通常用于父组件监听子组件的状态变化。

事件处理函数:hideMenu函数在被调用时,会通过emits对象发射hidden事件,并传入参数false。这通常意味着组件想要通知外部(比如父组件)它的某个状态(如菜单的显示状态)发生了变化。

事件触发:在HTML模板中,<div class="back">元素绑定了一个@click事件处理器,即hideMenu函数。这意味着当用户点击这个div元素时,hideMenu函数会被调用,进而发射hidden事件。

使用css4中的transform: translateX(100%)动态实现下次细讲

数组中的对象Note.vue

前端

在主页面的脚本部分添加跳转的路由

// 跳转笔记
const goNoteList = (title) => {
    router.push({ path: '/note', query: {title} });
}

点击事件@click="goNoteList(item.title)传入当前点击的title

goNoteList函数被调用时,它会根据传入的title值,跳转到'/note'页面,并设置路由传参在URL中附加title作为查询参数。这样,目标页面就可以根据这个title参数来决定展示哪些笔记,只展示属于特定分类的笔记,这样就不需要为每一个title设置一个新页面。

路由传参query: {title}

e7cf4f0199879f773568de74c177904.png

模板部分
<template>
    <div class="note-list">
        <ul>
            <li v-for="item in noteList" :key="item.id" @click="goNoteDetail(item.id)">
                <div class="img">
                    <img :src="item.head_img" alt="">
                </div>
                <div class="time">{{ item.m_time }}</div>
                <div class="title">{{ item.title }}</div>
            </li>
        </ul>
    </div>
</template>
  • 列表渲染: 使用v-for指令遍历noteList数组中的每一项,为每一条笔记生成一个<li>元素。每个元素都有一个唯一的key属性,这里设置为item.id,以提高列表更新的性能。
  • 图片展示: 每个列表项包含一个<img>标签,其src属性绑定到item.head_img,展示笔记的头像或缩略图。
  • 时间显示: 显示笔记的时间戳item.m_time
  • 标题显示: 显示笔记的标题item.title
  • 点击事件: 当列表项被点击时,触发goNoteDetail方法,并将item.id作为参数传递,以便导航到笔记详情页。
脚本部分
<script setup>
import { useRoute } from 'vue-router';
import { useRouter } from 'vue-router';
import { ref } from 'vue';
import axios from '../api/index';

// const res = await axios.get(`/findNoteListByType?note_type=${route.query.title}`)
const route = useRoute();
const router = useRouter()
const noteList = ref([])

axios.get('/findNoteListByType',{
    params: {
        note_type:route.query.title
    }
}).then(res=>{
    noteList.value = res.data
    console.log(noteList.value);
})

const goNoteDetail = (id) => {
    router.push({ path: '/noteDetail', query: {id} });
}


</script>
  • 依赖导入:

    • useRoute 和 useRouter 从 vue-router 导入,用于获取当前路由信息和进行路由跳转。
    • ref 从 vue 导入,用于创建响应式引用。
    • axios 从项目中的API模块导入,用于发送HTTP请求。

(其中的useRoute()函数用于获取当前活跃路由的信息。它返回一个响应式的路由对象,包含了当前路由的所有信息,route对象被用来获取URL中的查询参数title,然后将其用于API请求中,以获取与该title相关的笔记列表。)

  • 数据获取:

    • 使用axios发送GET请求到/findNoteListByType端点,参数note_type从当前路由的查询参数title获取。
    • 请求成功后,响应数据存储在noteList响应式引用中,这将触发视图更新,显示获取到的笔记列表。
  • 路由跳转:

    • goNoteDetail 方法接收一个id参数,使用router.push方法导航至/noteDetail路径,并将id作为查询参数附加,以便目标页面能够获取到具体笔记的ID。

后端

请求/findNoteListByType端点

使用axios发送GET请求到/findNoteListByType端点 和在第二章设置的的用户登录注册路由管理类似 @koa/router中间件的使用

新建note.js,在 rotes/note.js文件中添加得到/findNoteListByType路由请求的数据,因此使用的是get

在HTTP协议中:

  • GET请求:通常用于请求资源,数据(如果有的话)会作为URL的一部分(查询字符串)附加在URL后面。因此,如果你使用GET方法发送请求,并且想要传递额外的数据,这些数据就会被编码成查询字符串的形式。在服务器端,这些数据可以通过ctx.request.query来访问。
  • POST请求:通常用于提交数据给服务器,数据不是放在URL中,而是放在请求体(request body)中。在服务器端,这些数据可以通过ctx.request.body来访问。
router.get('/findNoteListByType',jwt.verify(),async (ctx,next) => {
    // 先检验Token合理再去数据库查找数据
    const { note_type } = ctx.request.query
    // console.log(res);
    try{
        const res = await findNoteListByType(note_type,ctx.userId)
        if(res.length){
            ctx.body = {
                code:800,
                data:res,
                msg:'查询成功'
            }
        }else{
            ctx.body = {
                code:813,
                data:null,
                msg:'查询失败'
            }
        }
    }catch(error){
        ctx.body = {
            code:814,
            data:error,
            msg:'服务器异常'
        }
    } 
})

中间件验证

  • router.get(...)定义了一个处理GET请求的路由规则。
  • jwt.verify()是一个中间件,用于验证请求中的JWT(JSON Web Token)。这意味着在访问此路由之前,必须先通过JWT验证。如果验证失败,请求不会到达后续的处理逻辑。

请求处理

  • const { note_type } = ctx.request.query:从请求的查询参数中解析出note_type,这是要查询的笔记类型。

  • 使用try-catch块,用于捕获并处理可能出现的错误:

1、调用findNoteListByType函数,传入note_type和从上下文中获取的userId。这是一个异步操作,使用await关键字等待结果。

2、如果findNoteListByType函数成功返回数据并且数据长度不为0,则设置响应体ctx.body,返回一个成功的响应,包含状态码800、数据res以及消息'查询成功'。反之失败

别忘记在index.js中启动

 const userRouter = require('./routes/note')

app.use(noteRouter.routes())
app.use(noteRouter.allowedMethods())
jwt.verify()中间件

验证请求中的JWT(JSON Web Token)。这意味着在访问此路由之前,必须先通过JWT验证。如果验证失败,请求不会到达后续的处理逻辑。

哈哈,反正文章没人看,等下次有时间多再补充说明,还有token令牌得再细细分析一下,以及这篇文章的更多细节,路由传参啦,route和router的区别啦等等

设置findNoteListByType函数取出数据库数据

controllers/index模块添加findNoteListByType函数来查询并得到相应的笔记类型数据

const findNoteListByType = (note_type,id) => {
    const _sql = `select * from note where note_type="${note_type}" and userId="${id}";`
    return allService.query(_sql)
}

note表中选择所有列,但仅返回那些note_type等于传入的note_type参数,且userId等于传入的id参数的记录。

findNoteListByType函数将返回一个Promise,这个Promise最终将解析为查询结果,即符合特定类型和用户ID的笔记列表。

于是能得到数据库中的这三条笔记

d0a7a7772655930a6d676b81bb5909e.png

具体笔记NoteDetail.vue

在对象Note.vue中还能查询具体的笔记信息 在每一条笔记中设置了路由跳转:goNoteDetail 方法接收一个id参数,使用router.push方法导航至/noteDetail路径,并设置了路由传参id作为查询参数附加,以便目标页面能够获取到具体笔记的ID。

和上面的Note.vue无比相像,就改改传递的值,然后根据这个值去数据库找数据(Note.vue是title,NoteDetail.vue是id),于是这里我就直接偷懒了。

前端

模板部分

就是简单的拿数据再显示.....

<template>
    <div class="note-detail">
        <div class="note-img">
            <img :src="noteDetail.head_img">
        </div>
        <div class="note-content">
            <div class="tab">
                <span class="note-type">{{noteDetail.note_type}}</span>
                <span class="author">{{noteDetail.nickname}}</span>
            </div>
            <p class="title">{{noteDetail.title}}</p>
            <div class="content" v-html=" noteDetail.note_content "></div>
        </div>
    </div>
</template>
  • 笔记图像src属性绑定到noteDetail.head_img,确保显示正确的图像。
  • 笔记类型和作者:  分别显示笔记的类型和作者的昵称。
  • 标题: 显示笔记的标题。
  • 内容: 内容部分要注意,不能直接使用noteDetail中的数据,会发现存在着<p>的换行符,因此需要使用v-html指令将noteDetail.note_content中的HTML内容直接插入到DOM中,允许富文本或HTML格式的内容显示。
脚本部分
<script setup>
import { useRoute } from 'vue-router';
// import { useRouter } from 'vue-router';
import { ref } from 'vue';
import axios from '../api/index';

const route = useRoute();
// const router = useRouter()
const noteDetail = ref({})

axios.get('/findNoteDetail',{
    params: {
        id:route.query.id
    }
}).then(res=>{
    noteDetail.value = res.data[0]
    console.log(noteDetail.value);
})
</script>
  • 依赖导入
  • 数据获取:
    • 使用axios发送GET请求到/findNoteDetail端点,参数id从当前路由的查询参数id获取。
    • 请求成功后,响应数据的第一项存储在noteDetail响应式引用中,这将触发视图更新,显示获取到的笔记详情。

后端

和上面的请求/findNoteListByType端点简直不要太像吧,于是就简单写一下。

router.get('/findNoteDetail',jwt.verify(),async (ctx,next) => {
    // 先检验Token合理再去数据库查找数据
    const { id } = ctx.request.query
    try{
        const res = await findNoteDetail(id)
        if(res.length){
            ctx.body = {
                code:800,
                data:res,
                msg:'查询成功'
            }
        }else{
            ctx.body = {
                code:813,
                data:null,
                msg:'查询失败'
            }
        }
    }catch(error){
        ctx.body = {
            code:814,
            data:error,
            msg:'服务器异常'
        }
    }
})

请求处理

  • const { id } = ctx.request.query:从请求的查询参数中解析出id,这是要查询的笔记编号。

  • 使用try-catch块,用于捕获并处理可能出现的错误:

1、调用findNoteDetail函数,传入id。这是一个异步操作,使用await关键字等待结果。

2、如果findNoteDetail函数成功返回数据并且数据长度不为0,则设置响应体ctx.body,返回一个成功的响应,包含状态码800、数据res以及消息'查询成功'。反之失败

设置findNoteDetail函数取出数据库数据

controllers/index模块添加findNoteDetail函数来查询并得到相应的笔记类型数据

findNoteDetail函数将返回一个Promise,这个Promise最终将解析为查询结果,即符合特定类型和用户ID的笔记列表。

于是能得到数据库中的这条笔记的详细信息

3ea29dfd30ab7ff10a6dc1807fb5947.png

结语

第三章编写了我们的主页面丰富了页面的内容,并实现一些功能: 先设置路由白名单,让页面不在路由守卫范围内,即不登录也能浏览主页面,接着开始主页面的编写,原始页面及子组件个人信息页面、主页面中的笔记列表,再到列表页面中的具体笔记,完成了笔记数据可视化,接下来的章节将继续完善和丰富主页面的功能,例如写笔记往数据库添加数据等。