验证码登录
目标:完成验证码登录,后端设置验证码默认为246810
原因:因为接口短信不是免费的,防止攻击者恶意盗刷
步骤:
- 在utils/request.js配置axios请求基地址,作用:提取公共前缀地址,配置后axios请求时都会baseURL+url
- 收集手机号和验证码数据
- 基于axios调用验证码登陆接口
- 使用Bootstrap的Alert警告框反馈结果给用户
验证码登录流程:
token的介绍
- 概念:访问权限的令牌,本质上是一串字符串
- 创建:正确登录后,由后端签发并返回
- 作用:判断是否有登录状态等,控制访问权限
- 目标:只有登陆状态,才可以访问内容页面
步骤:
- 在utils/auth.js中判断无token令牌字符串,则强制跳转到登录页(手动修改地址栏测试)
- 在登录成功后,保存token令牌字符串到本地,再跳转到首页(手动修改地址栏测试)
注意:前端只能判断token的有无,后端可以通过解密提取token字符串的原始信息,判断有效性
应用:
// 判断无token令牌字符串,则强制跳转内容列表页面
const token=localStorage.getItem('token')
if(!token){
location.href('../login/index.html')
}
登陆成功后先本地存储token,判断时,先从本地存储中提取token,若不存在,就直接跳转到登陆页面,若提取成功,跳转页面到首页,注意为了让alert警告框停留一会,设置定时器调用跳转函数
myAlert(true,'登陆成功')
// 登陆成功后,保存token令牌字符串到本地,并跳转到内容列表页面
localStorage.setItem('token',result.data.data.token)
setTimeout(()=>{
// 延迟跳转 让alert警告框停留一会
location.href='../content/index.html'
},1500)
个人信息设置和axios请求拦截器
需求:设置用户昵称
语法:axios可以在headers选项传递请求头参数
问题:很多接口,都需要携带token令牌字符串,代码会显得冗余
解决:在请求拦截器统一设置headers选项
axios请求拦截器:发起请求之前,触发的配置函数,对请求参数进行额外配置
使用:有公共配置和设置时,统一设置在请求拦截器中
// 添加请求拦截器
axios.interceptors.request.use(function(config){
// 在发送请求之前做些什么
// 统一携带token令牌字符串在请求头上
const token=localStorage.getItem('token')
token&&(config.headers.Authorization=`Bearer ${token}`)
return config
},function(error){
// 对请求错误做些什么
return Promise.reject(error)
})
axios响应拦截器和身份验证失败
axios响应拦截器:响应回到then/catch之前,触发的拦截函数,对响应结果统一处理
例如:身份验证失败,统一判断并做处理
优化 - axios响应结果
// 添加响应拦截器
axios.interceptors.response.use(function(response){
// 2XX范围内的状态码都会触发该函数
// 对响应数据做点什么,。例如:直接返回服务器的响应结果对象
const result=response.data
return result
},function(error){
// 超出2xx范围内的状态码都会触发该函数
// 对相应错误做点什么,例如:统一对401身份验证失败情况做出处理
console.dir(error)
if(error?.response?.status===401){
alert('身份验证失败,请重新登录')
localStorage.clear()
locationo.href='../login/index.html'
}
return Promise.reject(error)
})
发布文章 - 富文本编辑器
- 富文本:带样式,多格式的文本,在前端一般使用标签配合内联样式实现
- 富文本编辑器:用于编写富文本内容的容器
- 目标:发布文章页,富文本编辑器的集成
- 使用:wangEditor插件
- 步骤:
- 引入CSS定义样式
- 定义HTML结构
- 引入JS创建编辑器
- 监听内容改变,保存在隐藏文本域(便于后期收集)
在官方网站中获取插件使用方式
// 富文本编辑器
// 创建编辑器函数,创建工具栏函数
const { createEditor, createToolbar } = window.wangEditor
// 编辑器配置对象
const editorConfig = {
// 占位提示文字
placeholder: '发布文章内容',
// 编辑器内容变化时回调函数
onChange(editor) {
// 获取富文本内容
const html = editor.getHtml()//获取编辑器对应的内容标签
console.log('editor content', html)
// 也可以同步到 <textarea>
// 为了后续快速收集整个表单内容做铺垫
document.querySelector('.public-content').value=html
}
}
const editor = createEditor({
// 创建位置
selector: '#editor-container',
// 默认内容
html: '<p><br></p>',
// 配置项
config: editorConfig,
mode: 'default', // or 'simple'
// mode: 'default' 默认模式 - 集成了 wangEditor 所有功能
// mode: 'simple' 简洁模式 - 仅有部分常见功能,但更加简洁易用
})
// 工具栏配置对象
const toolbarConfig = {}
// 创建工具栏
const toolbar = createToolbar({
// 为指定编辑器创建工具栏
editor,
// 工具栏创建的位置
selector: '#toolbar-container',
// 工具栏配置的对象
config: toolbarConfig,
// 配置集成模式
mode: 'default', // or 'simple'
})
目标:展示频道列表,供用户选择
步骤:
- 获取频道列表数据
- 展示到下拉菜单中
/**
* 目标1:设置频道下拉菜单
* 1.1 获取频道列表数据
* 1.2 展示到下拉菜单中
*/
// 1.1获取频道列表数据
async function setChannleList(){
const res=await axios({
url:'/v1_0/channers'
})
console.log(res)
const htmlStr=res.data.channels.map(item=>` <option value="" selected>请选择文章频道</option>`+`
<option value="${item.id}">${item.name}</option>
`).join('')
document.querySelector('.form-select').innerHTML=htmlStr
}
// 网页运行后,默认调用一次
setChannlelist()
发布文章 - 封面设置
目标:文章封面的设置
步骤:
- 准备标签结构和样式
- 选择文件并保存在FormData
- 单独上传图片并得到图片url地址
- 回显并切换img标签展示(隐藏+号上传标签)
注意:图片地址临时存储在img标签上,并未和文章关联保存
tip:
FormData
是一个用于构造键值对的数据结构,它主要用于发送数据到服务器,特别适合用于XMLHttpRequest
或fetch
请求中上传文件。以下是使用FormData
的基本步骤和示例:创建 FormData 实例
首先,你需要创建一个
FormData
的实例。你可以选择性地传入一个表单元素的引用作为参数,这样表单中的所有字段(包括文件输入)都会自动被添加到FormData
对象中。// 不带参数,手动添加数据 let formData = new FormData(); // 带表单元素作为参数,自动收集表单数据 let formElement = document.querySelector('form'); let formDataFromForm = new FormData(formElement);
添加数据
你可以使用
append
、set
方法向FormData
对象中添加数据。
append(key, value[, filename])
: 添加一对键值。如果键已经存在,则追加到已有值的后面。对于文件,可选的第三个参数可以指定文件名。set(key, value[, filename])
: 添加一对键值。如果键已经存在,则会替换原有的值。对于文件同样可以指定可选的第三个参数。formData.append('key1', 'value1'); formData.append('key2', 'value2'); formData.set('key3', 'this will replace any existing value for key3'); // 添加文件 let fileInput = document.querySelector('input[type="file"]'); formData.append('file', fileInput.files[0], 'myFile.jpg');
发送请求
使用
XMLHttpRequest
或fetch
发送带有FormData
的请求。使用 XMLHttpRequest
let xhr = new XMLHttpRequest(); xhr.open('POST', '/your-endpoint-url', true); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { console.log(xhr.responseText); } }; xhr.send(formData);
- 当使用
FormData
发送请求时,浏览器会自动设置合适的Content-Type
,通常是multipart/form-data
或application/x-www-form-urlencoded
,因此通常不需要手动设置。- 如果你手动设置了
Content-Type
,确保它与FormData
的预期类型匹配,否则可能导致服务器无法正确解析数据。- 文件上传时,
FormData
是非常实用的,因为它能够处理二进制数据和大文件上传。
/**
* 目标2:文章封面设置
* 2.1 准备标签结构和样式
* 2.2 选择文件并保存在 FormData
* 2.3 单独上传图片并得到图片 URL 网址
* 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
*/
// 选择文件并保存在FormData
document.querySelector('.img-file').addEventListener('change',async e =>{
const file=e.target.files[0]
const fd=new FormData()
fd.append('image',file)
// 单独上传图片并得到图片URL网址
const res=await axios({
url:'/v1_0/upload',
method:'POST',
data:fd
})
console.log(res)
// 回显并切换 img 标签展示 (隐藏+号上传标签)
const imgUrl=res.data.url
document.querySelector('.rounded').src=imgUrl
document.querySelector('.rounded').classList.add('show')
document.querySelector('.place').classList.add('hide')
})
// 优化:点击img可以重新切换封面
// 思路:img点击=>用JS方式触发文件选择元素click事件方法
document.querySelector('.rounded').addEventListener('click',()=>{
document.querySelector('.img-file').click()
})
发布文章 - 收集并保存
目标:收集文章内容,并提交保存
步骤:
- 基于form-serialize插件收集表单数据对象
- 基于axios提交到服务器保存
- 调用Alert警告框反馈结果给用户
- 重置表单并跳转到列表页
/**
* 目标3:发布文章保存
* 3.1 基于 form-serialize 插件收集表单数据对象
* 3.2 基于 axios 提交到服务器保存
* 3.3 调用 Alert 警告框反馈结果给用户
* 3.4 重置表单并跳转到列表页
*/
// 基于form-serialize插件收集表单数据对象
document.querySelector('.send').addEventListener('click',async e=>{
const form=document.querySelector('.art-form')
const data=serialize(form,{hash:true,empty:true})
// 发布文章的时候,不需要id属性,所以可以删除掉(id为了后续做编辑使用)
delete data.id
// console.log(data)
data.cover={
type:1,
images:[document.querySelector('.rounded').src]//封面图片URL网址
}
// 基于axios提交到服务器保存
try{
const res= await axios({
url:'v1_0/mp/articles',
method:'POST',
data:data
})
// 调用Alert警告框反馈结果给用户
myAlert(true,'发布成功')
// 重置表单并跳转到列表页
form.reset()
// 封面需要手动重置
document.querySelector('.rounded').src=''
document.querySelector('.rounded').classList.remove('show')
document.querySelector('.place').classList.remove('hide')
// 富文本编辑器重置
editor.setHtml('')
setTimeout(()=>{
location.href='../content/index.html'
},1500)
}catch(error){
// console.dir(error)
myAlert(false,error.response.data.message)
}
// console.log(res)
})
内容管理 - 文章列表展示
目标:获取文章列表展示
步骤:
- 准备查询参数对象
- 获取文章列表数据
- 展示到指定的标签结构中
/**
* 目标1:获取文章列表并展示
* 1.1 准备查询参数对象
* 1.2 获取文章列表数据
* 1.3 展示到指定的标签结构中
*/
// 准备查询参数对象
const queryObj={
status:'',//文章状态(1 - 待审核,2-审核通过 )空字符串 - 全部
channel_id:'',//文章频道 id,空字符串- 全部
page:1,//当前页码
per_page:2//当前页面条数
}
async function setArtileList(){
// 获取文章列表数据
const res=await axios({
url:'/v1_0/mp/articles',
params:queryObj
})
// console.log(res)
// 展示到对应的标签结构中
const htmlStr=res.data.results.map(item=>`
<tr>
<td>
<img src="${item.cover.type===0?`https://img2.baidu.com/it/u=2640406343,1419332367&fm=253&fmt=auto&app=138&f=JPEG?w=708&h=500`:item.cover.images[0]}" alt="">
</td>
<td>${item.title}</td>
<td>${item.status===1?`<span class="badge text-bg-primary">待审核</span>`:`<span class="badge text-bg-success">审核通过</span>`}
</td>
<td>
<span>${item.pubdate}</span>
</td>
<td>
<span>${item.read_count}</span>
</td>
<td>
<span>${item.comment_count}</span>
</td>
<td>
<span>${item.like_count}</span>
</td>
<td data-id="${item.id}">
<i class="bi bi-pencil-square edit"></i>
<i class="bi bi-trash3 del"></i>
</td>
</tr>
`).join('')
document.querySelector('.art-list').innerHTML=htmlStr
}
setArtileList()
内容管理 - 筛选功能
目标:根据筛选条件,获取匹配数据展示
步骤:
- 设置频道列表数据
- 监听筛选条件改变,保存查询信息到查询参数对象
- 点击筛选时,传递查询参数对象到服务器
- 获取匹配数据,覆盖到页面展示
/**
* 目标2:筛选文章列表
* 2.1 设置频道列表数据
* 2.2 监听筛选条件改变,保存查询信息到查询参数对象
* 2.3 点击筛选时,传递查询参数对象到服务器
* 2.4 获取匹配数据,覆盖到页面展示
*/
// 设置频道列表数据
async function setChannleList(){
const res=await axios({
url:'/v1_0/channels'
})
const htmlStr=` <option value="" selected>请选择文章频道</option>`+res.data.channels.map(item=>`
<option value="${item.id}">${item.name}</option>
`).join('')
document.querySelector('.form-select').innerHTML=htmlStr
}
setArtileList()
// 监听筛选条件改变,保存查询信息到查询参数对象
// 筛选状态标记数字->change事件->绑定到查询参数对象上
document.querySelectorAll('.form-check-input').forEach(radio=>{
radio.RadioNodeList('change',e=>{
queryObj.status=e.target.value
})
})
// 筛选频道 id->change事件-> 绑定到查询参数上
document.querySelector('.form-select').addEventListener('change',e=>{
// console.log(e.target.value)
queryObj.channel_id=e.target.value
})
// 点击筛选时,传递查询参数对象到服务器
document.querySelector('.sel-btn').addEventListener('click',()=>{
// 获取匹配数据 覆盖到页面展示
setArtileList()
})
内容管理 - 分页功能
目标:完成文章列表,分页管理功能
步骤:
- 保存并设置文章总条数
- 点击下一页,做临界值判断,并切换页码参数请求最新数据
- 点击上一页,做临界值判断,并切换页码参数请求最新数据
/**
* 目标3:分页功能
* 3.1 保存并设置文章总条数
* 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
* 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
*/
// 点击下一页 做临界值判断 并切换页码参数并请求最新数据
document.querySelector('.next').addEventListener('click',e=>{
// 当前页码小于最大页码数
if(queryObj.page<Math.ceil(totalCount/queryObj.per_page)){
queryObj.page++
document.querySelector('.page-now').innerHTML=`第${queryObj.page}页`
setArtileList()
}
})
// 点击上一页 做临界值判断 并切换页码参数并请求最新数据
document.querySelector('.last').addEventListener('click',e=>{
// 大于1的时候 才能翻到上一页
if(queryObj.page>1){
queryObj.page--
document.querySelector('.page-now').innerHTML=`第${queryObj.page}页`
setArtileList()
}
})
内容管理 - 删除功能
目标:完成删除文章功能
步骤:
- 关联文章id到删除图标
- 点击删除时,获取文章id
- 调用删除接口,传递文章id到服务器
- 重新获取文章列表,并覆盖展示
/**
* 目标4:删除功能
* 4.1 关联文章 id 到删除图标
* 4.2 点击删除时,获取文章 id
* 4.3 调用删除接口,传递文章 id 到服务器
* 4.4 重新获取文章列表,并覆盖展示
* 4.5 删除最后一页的最后一条,需要自动向前翻页
*/
// 点击删除时,获取文章id
document.querySelector('.art-list').addEventListener('click',async e=>{
// 判断点击的是删除元素
if(e.target.classList.contains('del')){
const delId=e.target.parentNode.dataset.id
// 调用删除接口 传递文章id到服务器
const res=await axios({
url:`/v1_0/mp/articles/${delId}`,
method:'DELETE'
})
// 删除最后一页的最后一条,需要自动向前翻页
const children=document.querySelector('.art-list').children
if(children.length===1&&queryObj.page!==1){
queryObj.page--
}
// 重新获取文章列表 并覆盖展示
setArtileList()
}
})
内容管理 - 编辑文章 - 回显
目标:编辑文章时,回显数据到表单
步骤:
- 页面跳转传参(URL查询参数方式)
- 发布文章页面接收参数判断(共用同一套表单)
/**
* 目标4:编辑-回显文章
* 4.1 页面跳转传参(URL 查询参数方式)
* 4.2 发布文章页面接收参数判断(共用同一套表单)
* 4.3 修改标题和按钮文字
* 4.4 获取文章详情数据并回显表单
*/
;(function(){
// 发布文章页面接收参数判断(共用同一套表单)
const paramsStr=location.search
const params=new URLSearchParams(paramsStr)
params.forEach(async (value,key)=>{
// console.log(value,key)
// 当前有要编辑的文章 id被传过来,查询参数有id说明是修改,无则说明是发布
if(key==='id'){
// 修改标题和按钮文字
document.querySelector('.title span').innerHTML='修改文章'
document.querySelector('.send').innerHTML='修改'
// 获取文章详情数据并回显表单
const res= await axios({//res为后台返回的数据
url:`/v1_0/mp/articles/${value}`
})
// console.log(res)
// 组织我仅仅需要的数据对象 为后续遍历回显到页面上做铺垫
const dataObj={
channel_id:res.data.channel_id,
title:res.data.title,
rounded:res.data.cover.images[0],//封面图片地址
content:res.data.content,
id:res.data.id
}
// 遍历数据对象属性,映射到页面元素上 快速赋值
Object.keys(dataObj).forEach(key =>{
if(key==='rounded'){
// 封面设置
if(dataObj[key]){
// 有封面
document.querySelector('.rounded').src=dataObj[key]
document.querySelector('.rounded').classList.add('show')
document.querySelector('.place').classList.add('hide')
}
}else if(key==='content'){
editor.setHtml=(dataObj[key])
}else {
// 用数据对象属性名 作为标签 name属性选择器值来找到匹配的标签
document.querySelector(`[name=${key}]`).value=dataObj[key]
}
})
}
})
})()
内容管理 - 编辑文章 - 保存
目标:确认修改,保存文章到服务器
步骤:
- 判断按钮文字,区分业务(因为公用一套表单)
- 调用编辑文章接口,保存信息到服务器
- 基于Alert反馈结果给用户
/**
* 目标5:编辑-保存文章
* 5.1 判断按钮文字,区分业务(因为共用一套表单)
* 5.2 调用编辑文章接口,保存信息到服务器
* 5.3 基于 Alert 反馈结果消息给用户
*/
document.querySelector('.send').addEventListener('click',async e=>{
// 判断按钮文字,区分业务
if(e.target.innerHTML!=='修改') return
const form=document.querySelector('.art-form')
const data=serialize(form,{hash:true,empty:true})
// 调用编辑文章接口,保存信息到服务器
try{
const res=await axios({
url:`/v1_0/mp/articles/${data.id}`,
method:'PUT',
data:{
...data,//解构赋值
cover:{
type:document.querySelector('.rounded').src?1:0,
images:[document.querySelector('.rounded').src]
}
}
})
console.log(res)
myAlert(true,'修改成功')
}catch(error){
myAlert(false,error.response.data.message)
}
})
退出登录
目标:完成退出登陆效果
步骤:
- 绑定点击事件
- 清空本地缓存,跳转至登录页面
/**
* 目标3:退出登录
* 3.1 绑定点击事件
* 3.2 清空本地缓存,跳转到登录页面
*/
document.querySelector('.quit').addEventListener('click',e=>{
// 清空本地缓存 跳转到登录页面
localStorage.clear()
location.href='../login/index.html'
})
以上知识点笔记以及后台服务器均出自于哔站黑马程序员!!!