此文項目代碼:https://github.com/bei-yang/I-want-to-be-an-architect
碼字不易踩验,辛苦點個star,感謝转唉!
引言
此篇文章主要涉及以下內(nèi)容:
-
UI
庫選型思路 - 全家桶融會貫通
vue-router
+vuex
- 前端登錄和權(quán)限控制
- 前后端交互
- 解決跨域問題
學習資源
-
UI
庫:cube-ui - 后端接口編寫:koa
- 請求后端接口:axios
著重關(guān)注請求暑劝、響應(yīng)攔截 - 令牌機制:Bearer Token
- 代理配置、mock數(shù)據(jù):vue-cli配置指南厢塘、webpack配置指南
開發(fā)環(huán)境
選擇一個合適的UI庫
vue add cube-ui
擴展性
任何ui
庫都不能滿足全部的業(yè)務(wù)開發(fā)需求,都需要自己進行定制和擴展,組件化設(shè)計思路至關(guān)重要
登錄頁面
- 安裝
router
:vue add router
- 安裝
vuex
:vue add vuex
- 配置路由
router.js
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Login from "./views/Login.vue";
Vue.use(Router);
const router = new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/login",
name: "login",
component: Login
},
{
path: "/about",
name: "about",
meta: {
auth: true
},
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "./views/About.vue")
}
]
});
// 路由守衛(wèi)
router.beforeEach((to, from, next) => {
if (to.meta.auth) {
// 需要登錄
const token = localStorage.getItem("token");
if (token) {
next();
} else {
next({
path: "/login",
query: { redirect: to.path }
});
}
} else { // 不需要登錄驗證
next()
}
});
export default router;
- 登錄狀態(tài)闺属,store.js
export defalut new Vuex.Store({
state: {
isLogin: false
},
mutations: {
setLoginState(state, b) {
state.isLogin = b;
}
},
actions: {}
})
- 登錄表單:cube-ui的表單文檔
<div>
<div class="logo">
<img src="https://img.kaikeba.com/logo-new.png"
alt>
</div>
<!-- <cube-button>登錄</cube-button> -->
<cube-form :model="model"
:schema="schema"
@submit="handleLogin"
@validate="haneldValidate"></cube-form>
</div>
分別設(shè)置model
和schema
model: {
username: "",
passwd: ""
},
schema: {
// 表單結(jié)構(gòu)定義
fields: [
// 字段數(shù)組
{
type: "input",
modelKey: "username",
label: "用戶名",
props: {
placeholder: "請輸入用戶名"
},
rules: {
// 校驗規(guī)則
required: true
},
trigger: "blur"
},
{
type: "input",
modelKey: "passwd",
label: "密碼",
props: {
type: "password",
placeholder: "請輸入密碼",
eye: {
open: true
}
},
rules: {
required: true
},
trigger: "blur"
},
{
type: "submit",
label: "登錄"
}
]
}
model
就是輸入框綁定的數(shù)據(jù),schema
是具體的表單的描述周霉,會動態(tài)渲染一個表單
每次校驗都會觸發(fā) handleValidate
方法掂器,打印校驗的結(jié)果
handleValidate(ret){
console.log(ret)
}
點擊登錄觸發(fā)handleLogin
發(fā)起登錄請求
Login.vue
handleLogin (e) {
// 組織表單默認提交行為
e.preventDefault();
// 登錄請求
// this.login(this.model) // 使用mapActions
this.$store
.dispatch("login", this.model)
.then(code => {
if (code) {
// 登錄成功重定向
const path = this.$route.query.redirect || "/";
this.$router.push(path);
}
})
.catch(error => {
// 有錯誤發(fā)生或者登錄失敗
const toast = this.$createToast({
time: 2000,
txt: error.message || error.response.data.message || "登錄失敗",
type: "error"
});
toast.show();
});
},
登錄動作編寫:提交登錄請求,成功后緩存token
并且提交至store
里
store.js
import Vue from "vue";
import Vuex from "vuex";
import us from "./service/user";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
isLogin: localStorage.getItem('token') ? true : false
},
mutations: {
setLoginState(state, b) {
state.isLogin = b;
}
},
actions: {
login({ commit }, user) {
// 登錄請求
return us.login(user).then(res => {
const { code, token } = res.data;
if (code) {
// 登錄成功
commit("setLoginState", true);
localStorage.setItem("token", token);
}
return code;
});
}
}
});
編寫接口服務(wù)
service/user.js
import axios from 'axios'
export default {
login(user){
return axios.get('/api/login', {params:user})
}
}
webpack devServer
對post
支持不好俱箱,這里暫時使用get
請求
configureWebpack: {
devServer: {
before(app) {
app.get('/api/login', function (req, res) {
const {
username,
passwd
} = req.query;
console.log(username, passwd);
if (username === 'xxx' && password === 'xxx') {
res.json({
code: 1,
token: 'abcdtoken'
});
} else {
res.status(401).json({
code: 0,
message: '用戶名或密碼錯誤'
})
}
})
}
}
}
檢查點
- 如何路由守衛(wèi)
- 如何進行異步操作
- 如何保存登錄狀態(tài)
- 如何模擬接口
http攔截器
有了token
之后国瓮,每次http
請求發(fā)出,都要加在header
上
// interceptor.js
const axios=require('axios')
export default function(){
axios.interceptors.request.use(config=>{
const token=localStorage.getItem('token')
if(token){
config.headers.token=token;
}
return config;
})
}
// 啟用 main.js
import interceptor from './interceptor'
interceptor()
可以通過登錄接口測試攔截器效果狞谱,但是不合理乃摹,我們編一個用戶信息接口,需要攜帶token才能訪問
// mock接口 vue.config.js
function auth(req, res, next) {
if (req.headers.token) {
// 已認證
next()
} else {
res.sendStatus(401)
}
}
app.get('/api/userinfo', auth, function (req, res) {
res.json({
code: 1,
data: {
name: 'xxx',
age: '18'
}
})
})
// 清localStorage測試
注銷
- 需要清除
token
緩存的兩種情況:- 用戶主動注銷
- token過期
- 需要做的事情:
- 清空緩存
- 重置登錄狀態(tài)
- 用戶主動注銷
// app.vue
<button v-if="$store.state.isLogin" @click="logout">注銷</button>
export default {
methods: {
logout() {
this.$store.dispatch('logout')
}
},
}
// store.js
logout({ commit }){
// 清緩存
localStorage.removeItem('token')
// 重置狀態(tài)
commit("setLoginState", false);
}
- token過期導致請求失敗的情況可能出現(xiàn)在項目的任何地方跟衅,可以通過響應(yīng)攔截統(tǒng)一處理
http攔截響應(yīng)
統(tǒng)一處理401狀態(tài)碼孵睬,清理token
跳轉(zhuǎn)login
// interceptor.js
export default function(vm){ // 傳入vue實例
// ...
// 響應(yīng)攔截
// 這里只關(guān)心失敗響應(yīng)
axios.interceptors.response.use(null,err=>{
if(err.respinse.status===401){
// 清空vuex和localstorage
vm.$store.dispatch('logout')
// 跳轉(zhuǎn)login
vm.$router.push('/login')
}
return Promise.reject(err)
})
}
// app.vue
const app=new Vue({...}).$mount('#app')
interceptor(app) // 傳入vue實例
深入理解令牌機制
-
Bearer Token規(guī)范
- 概念:描述在
HTTP
訪問OAuth2
保護資源時如何使用令牌的規(guī)范 - 特點:令牌就是身份證明,無需證明令牌的所有權(quán)
- 具體規(guī)定:在請求頭中定義
Authorization
Authorization:Bearer <token>
- 概念:描述在
-
Json Web Token規(guī)范
- 概念:令牌的具體定義方式
- 規(guī)定:令牌由三部分構(gòu)成‘頭伶跷,載荷掰读,簽名’
- 頭:包含加密算法、令牌類型等信息
- 載荷:包含用戶信息叭莫、簽發(fā)時間和過期時間等信息
- 簽名:根據(jù)頭蹈集、載荷及秘鑰加密得到的哈希串 HMac Sha1 256
- 實踐:
- 服務(wù)端:-server/server.js
const Koa = require("koa"); const Router = require("koa-router"); // 生成令牌、驗證令牌 const jwt = require("jsonwebtoken"); const jwtAuth = require("koa-jwt"); // 生成數(shù)字簽名的秘鑰 const secret = "it's a secret"; const app = new Koa(); const router = new Router(); router.get("/api/login", async ctx => { const { username, passwd } = ctx.query; console.log(username, passwd); if (username == "kaikeba" && passwd == "123") { // 生成令牌 const token = jwt.sign( { data: { name: "kaikeba" }, // 用戶信息數(shù)據(jù) exp: Math.floor(Date.now() / 1000) + 60 * 60 // 過期時間 }, secret ); ctx.body = { code: 1, token }; } else { ctx.status = 401; ctx.body = { code: 0, message: "用戶名或者密碼錯誤" }; } }); router.get( "/api/userinfo", jwtAuth({ secret }), async ctx => { ctx.body = { code: 1, data: { name: "jerry", age: 20 } }; } ); app.use(router.routes()); app.listen(3000);
- 修改配置文件:啟用開發(fā)服務(wù)器代理雇初,vue.config.js
devServer: { proxy: { "/api": { target: "http://127.0.0.1:3000/", changOrigin: true } },
- 攔截器的修改拢肆,interceptor.js
config.headers.Authorization='Bearer'+token
你的贊是我前進的動力
求贊,求評論抵皱,求分享...