目錄
項目介紹
功能
項目準備
技術
驗證碼
驗證碼登錄
驗證碼登錄-流程
關于token
token的介紹
token的使用
個人信息設置
個人信息設置和axios請求攔截器
axios響應攔截器和身份驗證失敗
優化-axios響應結果
發布文章
發布文章-富文本編輯器
發布文章-頻道列表
發布文章-封面設置
發布文章-收集并保存
內容管理
內容管理-文章列表展示
內容管理-篩選功能
內容管理-分頁功能
內容管理-刪除功能
內容管理-刪除最后一條
內容管理-編輯文章-回顯
內容管理-編輯文章-保存
退出登錄
項目介紹
黑馬頭條-數據管理平臺:對IT資訊移動網站的數據,進行數據管理
數據管理平臺-演示:配套代碼在本地運行
移動網站-演示:http://geek.itheima.net/
?
功能
- 登錄和權限判斷
- 查看文章內容列表(篩選,分頁)
- 編輯文章(數據回顯)
- 刪除文章
- 發布文章(圖片上傳,富文本編輯器)
?
總結:
1. 黑馬頭條-數據管理平臺,是什么樣網站,要完成哪些功能?
- 數據管理網站, 登錄后對數據進行增刪改查
2. 數據管理平臺,未登錄能否管理數據?
- 不能,數據是公司內部的,需賬號登錄后管理
項目準備
技術
- 基于Bootstrap搭建網站標簽和樣式
- 集成wangEditor插件實現富文本編輯器
- 使用原生JS完成增刪改查等業務
- 基于axios與黑馬頭條線上接口交互
- 使用axios攔截器進行權限判斷
?
項目準備:準備配套的素材代碼
包含:html,CSS,js,靜態圖片,第三方插件等等
目錄管理:建議這樣管理,方便查找
- assets:資源文件夾(圖片,字體等)
- lib:資料文件夾( 第三方插件,例如:form-serialize )
- page:頁面文件夾
- utils:實用程序文件夾(工具插件)
?
總結:
1. 為什么要按照一定的結構,管理代碼文件?
- 方便以后的查找和擴展
驗證碼
驗證碼登錄
目標:完成驗證碼登錄,后端設置驗證碼默認為246810
原因:因為短信接口不是免費的,防止攻擊者惡意盜刷
步驟:
1. 在utils/request.js配置axios請求基地址
- 作用:提取公共前綴地址,配置后axios請求時都會baseURL + url
2. 收集手機號和驗證碼數據
3. 基于axios調用驗證碼登錄接口
4. 使用Bootstrap的Alert警告框反饋結果給用戶
驗證碼登錄-流程
手機號+驗證碼,登錄流程:
?
關于token
token的介紹
概念:訪問權限的令牌,本質上是一串字符串
創建:正確登錄后,由后端簽發并返回
?
作用:判斷是否有登錄狀態等,控制訪問權限
注意:前端只能判斷token有無,而后端才能判斷token的有效性
?
token的使用
目標:只有登錄狀態,才可以訪問內容頁面
步驟:
- 在utils/auth.js 中判斷無token令牌字符串,則強制跳轉到登錄頁(手動修改地址欄測試)
- 在登錄成功后,保存token令牌字符串到本地,再跳轉到首頁(手動修改地址欄測試)
?
總結:
1. token的作用?
- 判斷用戶是否有登錄狀態等
2. token的注意:
- 前端只能判斷token的有無
- 后端通過解密可以提取token字符串的原始信息,判斷有效性
?
/*** 目標1:驗證碼登錄* 1.1 在 utils/request.js 配置 axios 請求基地址* 1.2 收集手機號和驗證碼數據* 1.3 基于 axios 調用驗證碼登錄接口* 1.4 使用 Bootstrap 的 Alert 警告框反饋結果給用戶*/// 1.2 收集手機號和驗證碼數據
document.querySelector('.btn').addEventListener('click', () => {const form = document.querySelector('.login-form')const data = serialize(form, { hash: true, empty: true })console.log(data)// 1.3 基于 axios 調用驗證碼登錄接口axios({url: '/v1_0/authorizations',method: 'POST',data}).then(result => {// 1.4 使用 Bootstrap 的 Alert 警告框反饋結果給用戶myAlert(true, '登錄成功')console.log(result)// 登錄成功后,保存 token 令牌字符串到本地,并跳轉到內容列表頁面localStorage.setItem('token', result.data.token)setTimeout(() => {// 延遲跳轉,讓 alert 警告框停留一會兒location.href = '../content/index.html'}, 1500)}).catch(error => {myAlert(false, error.response.data.message)console.dir(error.response.data.message)})
})
個人信息設置
個人信息設置和axios請求攔截器
需求:設置用戶昵稱
語法:axios 可以在headers選項傳遞請求頭參數
?
問題:很多接口,都需要攜帶token令牌字符串
解決:在請求攔截器統一設置公共headers選項
?
axios請求攔截器:發起請求之前,觸發的配置函數,對請求參數進行額外配置
總結:
1. 什么是axios請求攔截器?
- 發起請求之前,調用的一個函數,對請求參數進行設置
2. axios 請求攔截器,什么時候使用?
- 有公共配置和設置時,統一-設置在請求攔截器中
axios響應攔截器和身份驗證失敗
axios響應攔截器:響應回到then/catch之前,觸發的攔截函數,對響應結果統一處理
例如:身份驗證失敗,統一判斷并做處理
?
// axios 公共配置
// 基地址
axios.defaults.baseURL = 'http://geek.itheima.net'// 添加請求攔截器
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.interceptors.response.use(function (response) {// 2xx 范圍內的狀態碼都會觸發該函數。// 對響應數據做點什么return response;
}, function (error) {// 超出 2xx 范圍的狀態碼都會觸發該函數。// 對響應錯誤做點什么,例如:統一對 401 身份驗證失敗情況做出處理console.dir(error)if (error?.response?.status === 401) {alert('身份驗證失敗,請重新登錄')localStorage.clear()location.href = '../login/index.html'}return Promise.reject(error);
});
總結:
1. 什么是axios響應攔截器?
- 響應回到then/catch之前,觸發的攔截函數,對響應結果統一處理
2. axios 響應攔截器,什么時候觸發成功/失敗的回調函數?
- 狀態為2xx觸發成功回調,其他則觸發失敗的回調函數
優化-axios響應結果
目標:axios直接接收服務器返回的響應結果
?
// axios 公共配置
// 基地址
axios.defaults.baseURL = 'http://geek.itheima.net'// 添加請求攔截器
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.interceptors.response.use(function (response) {// 2xx 范圍內的狀態碼都會觸發該函數。// 對響應數據做點什么,例如:直接返回服務器的響應結果對象const result = response.datareturn result;
}, function (error) {// 超出 2xx 范圍的狀態碼都會觸發該函數。// 對響應錯誤做點什么,例如:統一對 401 身份驗證失敗情況做出處理console.dir(error)if (error?.response?.status === 401) {alert('身份驗證失敗,請重新登錄')localStorage.clear()location.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()// 也可以同步到 <textarea>// 為了后續快速收集整個表單內容做鋪墊document.querySelector('.publish-content').value = html}
}// 創建編輯器
const editor = createEditor({// 創建位置selector: '#editor-container',// 默認內容html: '<p><br></p>',// 配置項config: editorConfig,// 配置集成模式(default 全部)(simple 簡潔)mode: 'default', // or '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/channels'})// 1.2 展示到下拉菜單中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
}
// 網頁運行后,默認調用一次
setChannleList()
發布文章-封面設置
目標:文章封面的設置
步驟:
- 準備標簽結構和樣式
- 選擇文件并保存在FormData
- 單獨上傳圖片并得到圖片URL地址
- 回顯并切換img標簽展示(隱藏+號上傳標簽)
注意:圖片地址臨時存儲在img標簽上,并未和文章關聯保存
/*** 目標2:文章封面設置* 2.1 準備標簽結構和樣式* 2.2 選擇文件并保存在 FormData* 2.3 單獨上傳圖片并得到圖片 URL 網址* 2.4 回顯并切換 img 標簽展示(隱藏 + 號上傳標簽)*/
// 2.2 選擇文件并保存在 FormData
document.querySelector('.img-file').addEventListener('change', async e => {const file = e.target.files[0]const fd = new FormData()fd.append('image', file)// 2.3 單獨上傳圖片并得到圖片 URL 網址const res = await axios({url: '/v1_0/upload',method: 'POST',data: fd})// 2.4 回顯并切換 img 標簽展示(隱藏 + 號上傳標簽)const imgUrl = res.data.urldocument.querySelector('.rounded').src = imgUrldocument.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 重置表單并跳轉到列表頁*/
// 3.1 基于 form-serialize 插件收集表單數據對象
document.querySelector('.send').addEventListener('click', async e => {if (e.target.innerHTML !== '發布') returnconst form = document.querySelector('.art-form')const data = serialize(form, { hash: true, empty: true })// 發布文章的時候,不需要 id 屬性,所以可以刪除掉(id 為了后續做編輯使用)delete data.idconsole.log(data)// 自己收集封面圖片地址并保存到 data 對象中data.cover = {type: 1, // 封面類型images: [document.querySelector('.rounded').src] // 封面圖片 URL 網址}// 3.2 基于 axios 提交到服務器保存try {const res = await axios({url: '/v1_0/mp/articles',method: 'POST',data: data})// 3.3 調用 Alert 警告框反饋結果給用戶myAlert(true, '發布成功')// 3.4 重置表單并跳轉到列表頁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) {myAlert(false, error.response.data.message)}
})
內容管理
內容管理-文章列表展示
目標:獲取文章列表并展示
步驟:
- 準備查詢參數對象
- 獲取文章列表數據
- 展示到指定的標簽結構中
?
/*** 目標1:獲取文章列表并展示* 1.1 準備查詢參數對象* 1.2 獲取文章列表數據* 1.3 展示到指定的標簽結構中*/
// 1.1 準備查詢參數對象
const queryObj = {status: '', // 文章狀態(1-待審核,2-審核通過)空字符串-全部channel_id: '', // 文章頻道 id,空字符串-全部page: 1, // 當前頁碼per_page: 2 // 當前頁面條數
}
let totalCount = 0 // 保存文章總條數// 獲取并設置文章列表
async function setArtileList() {// 1.2 獲取文章列表數據const res = await axios({url: '/v1_0/mp/articles',params: queryObj})// 1.3 展示到指定的標簽結構中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// 3.1 保存并設置文章總條數totalCount = res.data.total_countdocument.querySelector('.total-count').innerHTML = `共 ${totalCount} 條`
}
setArtileList()
內容管理-篩選功能
目標:根據篩選條件,獲取匹配數據展示
步驟:
- 設置頻道列表數據
- 監聽篩選條件改變,保存查詢信息到查詢參數對象
- 點擊篩選時,傳遞查詢參數對象到服務器
- 獲取匹配數據,覆蓋到頁面展示
?
/*** 目標2:篩選文章列表* 2.1 設置頻道列表數據* 2.2 監聽篩選條件改變,保存查詢信息到查詢參數對象* 2.3 點擊篩選時,傳遞查詢參數對象到服務器* 2.4 獲取匹配數據,覆蓋到頁面展示*/
// 2.1 設置頻道列表數據
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
}
setChannleList()
// 2.2 監聽篩選條件改變,保存查詢信息到查詢參數對象
// 篩選狀態標記數字->change事件->綁定到查詢參數對象上
document.querySelectorAll('.form-check-input').forEach(radio => {radio.addEventListener('change', e => {queryObj.status = e.target.value})
})
// 篩選頻道 id -> change事件 -> 綁定到查詢參數對象上
document.querySelector('.form-select').addEventListener('change', e => {queryObj.channel_id = e.target.value
})
// 2.3 點擊篩選時,傳遞查詢參數對象到服務器
document.querySelector('.sel-btn').addEventListener('click', () => {// 2.4 獲取匹配數據,覆蓋到頁面展示setArtileList()
})
內容管理-分頁功能
目標:完成文章列表,分頁管理功能
步驟:
- 保存并設置文章總條數
- 點擊下一頁,做臨界值判斷,并切換頁碼參數請求最新數據
- 點擊上一頁,做臨界值判斷,并切換頁碼參數請求最新數據
?
/*** 目標3:分頁功能* 3.1 保存并設置文章總條數* 3.2 點擊下一頁,做臨界值判斷,并切換頁碼參數并請求最新數據* 3.3 點擊上一頁,做臨界值判斷,并切換頁碼參數并請求最新數據*/
// 3.2 點擊下一頁,做臨界值判斷,并切換頁碼參數并請求最新數據
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()}
})
// 3.3 點擊上一頁,做臨界值判斷,并切換頁碼參數并請求最新數據
document.querySelector('.last').addEventListener('click', e => {// 大于 1 的時候,才能翻到上一頁if (queryObj.page > 1) {queryObj.page--document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 頁`setArtileList()}
})
內容管理-刪除功能
目標:完成刪除文章功能.
步驟:
- 關聯文章id到刪除圖標
- 點擊刪除時,獲取文章id
- 調用刪除接口,傳遞文章id到服務器
- 重新獲取文章列表,并覆蓋展示
?
內容管理-刪除最后一條
目標:在刪除最后一頁,最后一條時有Bug
解決:
- 刪除成功時,判斷DOM元素只剩一條,讓當前頁碼page--
- 注意,當前頁碼為1時不能繼續向前翻頁
- 重新設置頁碼數,獲取最新列表展示
?
內容管理-編輯文章-回顯
目標:編輯文章時,回顯數據到表單
步驟:
- 頁面跳轉傳參(URL 查詢參數方式)
- 發布文章頁面接收參數判斷(共用同一套表單)
- 修改標題和按鈕文字
- 獲取文章詳情數據并回顯表單
?
/*** 目標4:編輯-回顯文章* 4.1 頁面跳轉傳參(URL 查詢參數方式)* 4.2 發布文章頁面接收參數判斷(共用同一套表單)* 4.3 修改標題和按鈕文字* 4.4 獲取文章詳情數據并回顯表單*/; (function () {// 4.2 發布文章頁面接收參數判斷(共用同一套表單)const paramsStr = location.searchconst params = new URLSearchParams(paramsStr)params.forEach(async (value, key) => {// 當前有要編輯的文章 id 被傳入過來if (key === 'id') {// 4.3 修改標題和按鈕文字document.querySelector('.title span').innerHTML = '修改文章'document.querySelector('.send').innerHTML = '修改'// 4.4 獲取文章詳情數據并回顯表單const res = await axios({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 => {// 5.1 判斷按鈕文字,區分業務(因為共用一套表單)if (e.target.innerHTML !== '修改') return// 修改文章邏輯const form = document.querySelector('.art-form')const data = serialize(form, { hash: true, empty: true })// 5.2 調用編輯文章接口,保存信息到服務器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)}
})
退出登錄
目標:完成退出登錄效果
步驟:
- 綁定點擊事件
- 清空本地緩存,跳轉到登錄頁面
?