[TOC]
>[success] # 登陆/登出以及JWT认证
在本章中将使用一个 **后端的真实服务** ,来结合这个服务做 **前端应用** 的 **登陆** 以及 **登出** ,并且我们使用 **JWT(全称:Json Web Token)** 来 **进行认证** 。
>[success] ## 后端代码概览
这里使用 **[express搭建](https://www.kancloud.cn/wangjiachong/code/1272384)** 了一个服务,这个 **服务运行在本地的 3000端口** ,大家想看可以直接进行 **代码下载** ,下载 **现成的代码** ,进行运行,下面主要看一下几个主要用到的 **后端接口** 。
**本地服务代码下载地址** :
链接:https://pan.baidu.com/s/1LcKTiDsoZy_RmEjmfJuKIw
提取码:cdaq
**运行服务执行指令** :
~~~
npm start
~~~
1. **serve/routes/index.js**
**login登陆接口**
~~~
var express = require('express');
var router = express.Router();
const jwt = require('jsonwebtoken')
// 模拟从数据库获取用户信息
const getPasswordByName = (name) => {
return { password: '123' }
}
router.post('/getUserInfo', function(req, res, next) {
res.status(200).send({
code: 200,
data: {
name: 'Lison'
}
})
});
// 登陆接口
router.post('/login', function(req, res, next) {
// 获取前端传过来的userName跟password
const { userName, password } = req.body
// 如果有用户名
if (userName) {
// 获取用户信息(如果有密码通过这个方法去数据库查询该用户信息,如果没有密码,用户信息就返回一个空字符串)
const userInfo = password ? getPasswordByName(userName) : ''
// 【用户信息为空】 或者 【密码为空】 或者 【输入密码与用户信息密码不对】3项中1项不对就抛出错误
if (!userInfo || !password || userInfo.password !== password) {
res.status(401).send({ // 抛出错误:用户名或密码不对
code: 401,
mes: 'user name or password is wrong',
data: {}
})
} else {
// 成功
res.send({ // 给前端返回一个token,token是通过一个jwt的一个库来生成的
code: 200,
mes: 'success',
data: {
// JWT生成规则: 可以自己来定义规则
// 参数1:是一个对象传入的是 【用户名称】
// 参数2:我们用来加密的一个自定义的一个字符串,这里可以定义我们的密钥,这里随便写一个abcd
// 参数3:我们可以在里面设置一些信息,这里设置了一个【token过期时间】
token: jwt.sign({ name: userName }, 'abcd', {
expiresIn: '1d' // 1d = 1天 10000 = 10秒
})
}
})
}
} else { // 如果无用户名
res.status(401).send({ // 抛出错误:用户名为空
code: 401,
mes: 'user name is empty',
data: {}
})
}
});
module.exports = router;
~~~
2. **serve/routes/users.js**
**授权接口**
~~~
var express = require('express');
var router = express.Router();
const jwt = require('jsonwebtoken')
/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
router.get('/getUserInfo', function(req, res, next) {
res.send('success')
})
// 授权接口
router.get('/authorization', (req, res, next) => {
// req.userName从app.js中中间件(拦截器)的token都正确才给添加的用户名称
const userName = req.userName
res.send({
code: 200,
mes: 'success',
data: {
token: jwt.sign({ name: userName }, 'abcd', {
expiresIn: '1d' // 1d = 1天 10000 = 10秒
})
}
})
})
module.exports = router;
~~~
3. **serve/app.js(中间件:类似拦截器)**
**应用核心配置文件**
~~~
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const jwt = require('jsonwebtoken')
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var dataRouter = require('./routes/data');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// app.all('*', (req, res, next) => {
// res.header('Access-Control-Allow-Origin', '*')
// res.header('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type')
// res.header('Access-Control-Allow-Methods','PUT,POST,GET,DELETE,OPTIONS')
// next()
// })
// 白名单(不需要做token校验的接口)
const whiteListUrl = {
get: [
],
post: [
'/index/login',
]
}
// 判断请求是否在白名单中的方法
const hasOneOf = (str, arr) => {
return arr.some(item => item.includes(str))
}
// 中间件(类似拦截器,每次请求时候会走这里)
app.all('*', (req, res, next) => {
// 获取当前请求方式并且转换成小写(post 或者 get等等)
let method = req.method.toLowerCase()
// 获取当前请求的路径
let path = req.path
// 有一些接口是不需要token校验的,所以在这里设置一下白名单,
// 如果请求方式在白名单里面的对应上,并且请求方式里有请求的这个地址就返回true,接口正常执行,不需要token
if(whiteListUrl[method] && hasOneOf(path, whiteListUrl[method])) next() // 白名单走这里
else { // 白名单之外的都需要token校验
// 在请求的header中取出authorization
const token = req.headers.authorization
// 判断没有token就抛出没有token请登录
if (!token) res.status(401).send('there is no token, please login')
else {
// 有token就判断使用JWT提供的方法,校验token是否正确
// 第一个参数把获取到的token传入
// 第二个参数是我们生成token时候传入的密钥,当然这个密钥可以抽离出一个文件每次从文件中获取密钥
// 第三个参数是一个回调函数,第一个参数是错误信息,第二个参数是从token中解码出来的信息
jwt.verify(token, 'abcd', (error, decode) => {
if (error) res.send({ // token错误
code: 401,
mes: 'token error',
data: {}
})
else { // token正确返回用户名,继续往下走
req.userName = decode.name
next()
}
})
}
}
})
app.use('/index', indexRouter);
app.use('/users', usersRouter);
app.use('/data', dataRouter);
// catch 404 and forward to error handler
app.use(function(err, req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
~~~
>[success] ## 登陆以及Token处理
具体操作如下:
>[success] ### 安装所需依赖
首先 **安装2个模块** ,**js-cookie(它是对cookie进行一些操作,可以设置、读取cookie)** 、**md5(可以对字符串进行md5加密,用在登陆时候提交密码时候加密密码)** ,然后我们依次 **执行安装指令** ,进行安装
**js-cookie 安装指令**
~~~
npm install js-cookie --save
~~~
**md5 安装指令**
~~~
npm install md5
~~~
>[success] ### 登陆页面逻辑
![](https://img.kancloud.cn/f3/49/f34999ee35fa12ddb6d7e3dc0a9a75f9_688x454.png)
![](https://img.kancloud.cn/da/ce/dacee54a1aac4c31c64f2272951bf296_2169x235.jpg)
1. **登陆页面代码以及逻辑**
在 **login.vue(登陆页面)** 里面写好一个 **简单的表单** ,大概是下图这个样子
![](https://img.kancloud.cn/a7/40/a740bc7ecab76ad10a982e5fdebc8255_436x43.png)
**点击登陆按钮** 后执行 **vuex** 中的 **actions** 的 **login异步方法**
**src/views/login.vue**
~~~
<template>
<div>
<input type="text" placeholder="请输入账号" v-model="userName" />
<input type="password" placeholder="请输入密码" v-model="password" />
<button @click="handleSubmit">登陆</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'login_page',
data(){
return {
userName: 'Lison', // 用户名
password: '123' // 密码
}
},
methods: {
// 引入action中的login方法
...mapActions([
'login'
]),
// 登陆
handleSubmit(){
this.login({
userName: this.userName,
password: this.password
}).then(() => {
console.log('成功')
this.$router.push({
name: 'home'
})
}).catch(error => {
console.log(error)
})
}
}
}
</script>
<style>
</style>
~~~
2. **vuex中的代码展示**
**点击登陆时候** 调用 **vuex** 中 **actions** 里定义的 **login方法**,这个 **login方法** 中 **调用了后端的登陆接口**
**src/store/module/user.js**
~~~
// 接口引入
import { login, authorization } from '@/api/user'
// 引入业务类方法
import { setToken } from '@/lib/util'
const state = {}
const mutations = {}
const actions = {
/**
* 登陆方法
* @param commit 执行一个mutation方法来修改state
* @param params this.$store.dispatch('login', '我是params')时传的参数
*/
login({ commit }, { userName, password }){
return new Promise((resolve, reject) => {
// 登陆接口
login({ userName, password }).then(res => {
if(res.code === 200 && res.data.token){
setToken(res.data.token)
resolve()
} else {
reject(new Error('错误'))
}
}).catch(error => {
reject(error)
})
})
},
/**
* 校验token是否失效
* @param commit 执行一个mutation方法来修改state
* @param token token信息
*/
authorization({ commit }, token){
return new Promise((resolve, reject) => {
authorization().then(res => {
if(parseInt(res.code) === 401){
reject(new Error('token error'))
} else {
resolve()
}
}).catch(error => {
reject(error)
})
})
}
}
export default {
state,
mutations,
actions
}
~~~
3. **接口文件**
**src/api/user.js**
~~~
import axios from './index'
// 登陆接口
export const login = ({ userName, password }) => {
return axios.request({
url: '/index/login',
method: 'post',
data: {
userName,
password
}
})
}
// 检验token是否有效
export const authorization = () => {
return axios.request({
url:'/users/authorization',
method: 'get' // 这行可以不写,默认是get
})
}
~~~
**接口成功** 后, **后端会返回一个 token** ,我们需要把 **token** 储存起来,在 **后续接口调用时,将它添加到我们请求的header中,传给后端,后端拿到 token 进行验证**,此时需要用到刚刚下载的 **js-cookie** 在 **util.js** 中 **封装一个储存 token 的业务类方法** ,代码如下:
**src/lib/util.js**
~~~
import Cookie from 'js-cookie'
// 设置title
export const setTitle = (title) => {
window.document.title = title || 'admin' // 默认title
}
/**
* 设置token
* @param {string} token - 登陆成功后,返回的token
* @param {string} tokenName - 储存到Cookie时的token名字
*/
export const setToken = (token, tokenName = 'token') => {
Cookie.set(tokenName, token)
}
/**
* 获取token
* @param {string} tokenName - 储存到Cookie时的token名字
*/
export const getToken = (tokenName = 'token') => {
return Cookie.get(tokenName)
}
~~~
4. **路由配置**
**login接口成功返回token** 后 **需要跳转到首页** ,此时需要在 **路由拦截器中做判断处理** ,如果有 **token** 会调用一个接口,进行 **token是否可用的校验 ,如果校验成功跳转到首页** ,**如果没有 token 证明未登录,跳转到登录页**
**src/router/index.js**
~~~
import Vue from 'vue'
import Router from 'vue-router'
import routes from './router'
import store from '@/store'
import { setTitle, setToken, getToken } from '@/lib/util'
// 注册路由
Vue.use(Router)
// vue-router实例
const router = new Router({ routes })
// 注册全局前置守卫
router.beforeEach((to, from, next) => {
// 动态设置title
to.meta && setTitle(to.meta.title)
// 获取token
const token = getToken()
if(token){ // 已登录
// 调用接口判断token是否失效
store.dispatch('authorization', token).then(() => { // token验证成功
// 如果跳转的页面为登陆页,就强制跳转到首页
if(to.name === 'login') next({ name: 'home' })
else next()
}).catch(error => { // token验证错误
// 这里需要清空token,再返回登录页,不然会陷入死循环,回到登录页还是有token,token失效还是回到登录页,如此反复
setToken('') // 这里也可以使用js-cookie提供的clear方法
next({ name: 'login' })
})
} else { // 未登录
// 如果去的页面是登陆页,直接跳到登陆页
if(to.name === 'login') next()
// 如果不是登陆页,强行跳转到登陆页
else next({ name: 'login' })
}
})
export default router
~~~
5. **axios请求拦截器添加token**
因为在调用 **authorization 校验接口** 时,需要 **参数 token** , **只需要添加到请求拦截器** 中即可,这样后续接口的请求 **token都会在请求头上添加** 。
**src/lib/axios.js**
~~~
import axios from 'axios'
import { baseURL } from '@/config'
import { getToken } from '@/lib/util'
class HttpRequest {
constructor(baseUrl = baseURL){ // baseUrl = baseURL 是ES6的默认值写法等同于 baseUrl = baseUrl || baseURL
this.baseUrl = baseUrl // this指向创建的实例,当你使用new HttpRequest创建实例时候,它会把this中定义的变量返回给你
this.queue = {} // 创建队列,每次请求都会向里面添加一个key:value,请求成功后就会去掉这个key:value,直到this.queue中没有属性值时,loading关闭
}
/**
* 默认options配置
*/
getInsideConfig(){
const config = {
baseURL: this.baseUrl,
headers: {
//
}
}
return config
}
distroy (url) {
delete this.queue[url]
if (!Object.keys(this.queue).length) {
// Spin.hide()
}
}
/**
* 拦截器
* @param {Object} instance - 通过axios创建的实例
* @param {String} url - 接口地址
*/
interceptors(instance, url){
/**
* 请求拦截器
* @param {Function} config - 请求前的控制
* @param {Function} error - 出现错误的时候会提供一个错误信息
*/
instance.interceptors.request.use(config => {
// 添加全局的Lodaing...
if(!Object.keys(this.queue).length){
// Spin.show()
}
this.queue[url] = true
// 每次请求都会把token加到请求头中
config.headers['Authorization'] = getToken()
return config
}, error => {
return Promise.reject(error)
})
/**
* 响应拦截器
* @param {Function} res - 服务端返回的东西
* @param {Function} error - 出现错误的时候会提供一个错误信息
*/
instance.interceptors.response.use(res => {
this.distroy(url) // 关闭全局的Lodaing...
const { data } = res
return data
}, error => {
this.distroy(url) // 关闭全局的Lodaing...
return Promise.reject(error.response.data)
})
}
request(options){
const instance = axios.create()
options = Object.assign(this.getInsideConfig(), options) // Object.assign会将2个对象合并成1个对象,相同属性值会被后者覆盖
this.interceptors(instance, options.url) // 拦截器
return instance(options)
}
}
export default HttpRequest
~~~
>[success] ### 注释掉mock.js
把 **main.js** 中引入的 **mock.js** 的引入 **注释掉** ,因为我们要 **请求真实的接口数据** ,如果这个地方引入了 **mock.js** ,它会用 **mock** 对你的 **所有请求行为进行拦截** ,所以需要 **注释掉**,代码如下:
**src/main.js**
~~~
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import Bus from './lib/bus'
// 非生产环境时引入 mock
// if(process.env.NODE_ENV !== 'production') require('./mock')
Vue.config.productionTip = false
Vue.prototype.$bus = Bus
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
~~~
>[success] ### 配置代理
因为 **express** 起的 **后端服务URL** 是 **http://localhost:3000/** ,而 **前端服务URL** 是 **http://localhost:8080/** ,两者的 **端口号不同(后端端口:3000,前端端口:8080)** ,所以调用接口时候会 **产生跨域** ,这时候在 **后端没有添加header头** , **前端解决跨域需要配置代理**,首先在 **vue.config.js** 中把 **devServer 中 proxy** 修改成 **要代理的后端服务地址** 如下:
**vue.config.js**
~~~
const path = require('path') // 引入nodejs的path模块
const resolve = dir => path.join(__dirname, dir) // resolve方法用来加载路径
const BASE_URL = process.env.NODE_ENV === 'production' ? '/iview-admin/' : '/' // 判断当前为开发环境还是打包环境, '/'意思是代表指定在域名的根目录下,如果要指定到iview-admin下就这样写'/iview-admin/', production为生产坏境,development为开发环境
module.exports = {
lintOnSave: false, // 取消每次保存时都进行一次' ESLint '检测
publicPath: BASE_URL, // 项目的基本路径,vuecli2.0时打包经常静态文件找不到,就是需要配置这个属性为'./'
chainWebpack: config => { // 配置Webpack
config.resolve.alias
.set('@', resolve('src')) // 引入文件时候“ @ ”符号就代表src
.set('_c', resolve('src/components')) // 引入组件文件夹中的文件就可以用“ _c ”代替src/components
},
productionSourceMap: false, // 打包时不生成.map文件,会减少打包体积,同时加快打包速度
devServer: { // 跨域有2种解决方案: 1. 在后端的header中配置, 2. 使用devServer来配置代理解决跨域
proxy: 'http://localhost:3000/' // 这里写需要代理的URL,这里会告诉开发服务器,将任何未知请求匹配不到静态文件的请求,都代理到这个URL来满足跨域
}
}
~~~
然后修改一下 **axios配置文件** 中的 **baseURL** 判断逻辑,如果是 **开发环境 baseURL 就设置为空字符串** ,因为 **不设置为空字符串,baseURL 不会被代理里配置的URL更改**。
**src/config/index.js**
~~~
// 如果当前是生产环境用生产环境地址,如果是开发环境并且在vue.config.js中配置了代理,就用空字符串【''】,如果未配置代理就用开发环境地址
export const baseURL = process.env.NODE_ENV === 'production' ? 'http://production.com' : ''
~~~
**注意:如果 修改了webpack 的配置 ,必须要 重启前端的服务,才会生效 。**
>[success] ## Token过期处理
一个网站如果 **长时间没有操作** ,不可能让它 **登陆完一次,就一辈子不用再登陆了,就一直能用** ,我们会 **设置一个token的过期时间 ,过期之后需要重新登录** 。例如:用户 **登陆成功** 后获取到了 **token** ,**token 过期时间为一天** ,在这一天都在频繁的使用该网站,到一天了 **token过期** 就 **让用户跳出去重新登录** ,这样 **用户体验不好** ,我们希望 **当用户长时间使用网站时,我们应该给 token 续命,给它延长使用时间** ,每次 **页面的跳转** 都会调用 **authorization接口** ,**authorization接口** 每次都会返回一个 **新的token** ,它会 **重新计时**。
**src/store/module/user.js**
~~~
// 接口引入
import { authorization } from '@/api/user'
// 引入业务类方法
import { setToken } from '@/lib/util'
const state = {}
const mutations = {}
const actions = {
/**
* 校验token是否失效
* @param commit 执行一个mutation方法来修改state
* @param token token信息
*/
authorization({ commit }, token){
return new Promise((resolve, reject) => {
authorization().then(res => {
if(parseInt(res.code) === 401){
reject(new Error('token error'))
} else {
setToken(res.data.token) // 重新设置token
resolve()
}
}).catch(error => {
reject(error)
})
})
}
}
export default {
state,
mutations,
actions
}
~~~
>[success] ## 退出登陆
在 **首页** 写个 **退出按钮** ,**点击按钮** 时候执行 **setToken('')** 把 **cookie清空** ,但是为了 **保持代码的一致性** ,还是把这个 **登出方法** 写在 **vuex** 中进行调用, **清空cookie** 后跳转到 **登录页面** 即可。
1. **首页代码**
**src/views/Home.vue**
~~~
<template>
<div>
<h1>首页</h1>
<button @click="handleLogout">退出登录</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
methods: {
// 引入vuex中的logout方法
...mapActions([
'logout'
]),
// 退出登录
handleLogout(){
this.logout()
this.$router.push({
name: 'login'
})
}
}
}
</script>
~~~
2. **vuex模块中代码**
**src/store/module/user.js**
~~~
// 接口引入
import { login, authorization } from '@/api/user'
// 引入业务类方法
import { setToken } from '@/lib/util'
const state = {}
const mutations = {}
const actions = {
/**
* 登陆方法
* @param commit 执行一个mutation方法来修改state
* @param params this.$store.dispatch('login', '我是params')时传的参数
*/
login({ commit }, { userName, password }){
return new Promise((resolve, reject) => {
// 登陆接口
login({ userName, password }).then(res => {
if(res.code === 200 && res.data.token){
setToken(res.data.token)
resolve()
} else {
reject(new Error('错误'))
}
}).catch(error => {
reject(error)
})
})
},
/**
* 校验token是否失效
* @param commit 执行一个mutation方法来修改state
* @param token token信息
*/
authorization({ commit }, token){
return new Promise((resolve, reject) => {
authorization().then(res => {
if(parseInt(res.code) === 401){
reject(new Error('token error'))
} else {
resolve()
}
}).catch(error => {
reject(error)
})
})
},
/**
* 登出方法
*/
logout(){
setToken('')
}
}
export default {
state,
mutations,
actions
}
~~~
>[success] ## 总结
1. **安全性要求不高** :上面的方案适合用于 **安全性要求不高的情况** 。
2. **安全性要求高** :如果你的系统 **对安全性要求比较高** ,就不能使用上面的方案, **不能使用 js 取到 token 存入到 cookie 中,不能进行一些判断 cookie 里的 token 的逻辑** ,需要 **在服务端设置一个开启 httpOnly,httpOnly设置为 true 后,就只能通过服务端来把 token 设置到 cookie 中了,无法通过 js 脚本来读取操作这个 cookie了,这样就能避免一些跨站脚本攻击。**
- vue 26课
- Vue-cli3.0项目搭建
- Vue-ui 创建cli3.0项目
- Vue-ui 界面详解
- 项目目录详解
- public文件夹
- favicon.ico
- index.html
- src文件夹
- api文件夹
- assets文件夹
- components文件夹
- config文件夹
- directive文件夹
- lib文件夹
- mock文件夹
- mock简明文档
- router文件夹
- store文件夹
- views文件夹
- App.vue
- main.js
- .browserslistrc
- .editorconfig
- .eslintrc.js
- .gitignore
- babel.config.js
- package-lock.json
- package.json
- postcss.config.js
- README.en.md
- README.md
- vue.config.js
- Vue Router
- 路由详解(一)----基础篇
- 路由详解(二)----进阶篇
- Vuex
- Bus
- Vuex-基础-state&getter
- Vuex-基础-mutation&action/module
- Vuex-进阶
- Ajax请求
- 解决跨域问题
- 封装axios
- Mock.js模拟Ajax响应
- 组件封装
- 从数字渐变组件谈第三方JS库使用
- 从SplitPane组件谈Vue中如何【操作】DOM
- 渲染函数和JSX快速掌握
- 递归组件的使用
- 登陆/登出以及JWT认证
- 响应式布局
- 可收缩多级菜单的实现
- vue杂项
- vue递归组件
- vue-cli3.0多环境打包配置
- Vue+Canvas实现图片剪切
- vue3系统入门与项目实战
- Vue语法初探
- 初学编写 HelloWorld 和 Counter
- 编写字符串反转和内容隐藏功能
- 编写TodoList功能了解循环与双向绑定
- 组件概念初探,对 TodoList 进行组件代码拆分
- Vue基础语法
- Vue 中应用和组件的基础概念
- 理解 Vue 中的生命周期函数
- 常用模版语法讲解
- 数据,方法,计算属性和侦听器
- 样式绑定语法
- 条件渲染
- 列表循环渲染
- 事件绑定
- 表单中双向绑定指令的使用
- 探索组件的理念
- 组件的定义及复用性,局部组件和全局组件
- 组件间传值及传值校验
- 单向数据流的理解
- Non-Props 属性是什么
- 父子组件间如何通过事件进行通信
- 组件间双向绑定高级内容
- 使用匿名插槽和具名插槽解决组件内容传递问题
- 作用域插槽
- 动态组件和异步组件
- 基础语法知识点查缺补漏
- Vue 中的动画
- 使用 Vue 实现基础的 CSS 过渡与动画效果
- 使用 transition 标签实现单元素组件的过渡和动画效果
- 组件和元素切换动画的实现
- 列表动画
- 状态动画
- Vue 中的高级语法
- Mixin 混入的基础语法
- 开发实现 Vue 中的自定义指令
- Teleport 传送门功能
- 更加底层的 render 函数
- 插件的定义和使用
- 数据校验插件开发实例
- Composition API
- Setup 函数的使用
- ref,reactive 响应式引用的用法和原理
- toRef 以及 context 参数
- 使用 Composition API 开发TodoList
- computed方法生成计算属性
- watch 和 watchEffect 的使用和差异性
- 生命周期函数的新写法
- Provide,Inject,模版 Ref 的用法
- Vue 项目开发配套工具讲解
- VueCLI 的使用和单文件组件
- 使用单文件组件编写 TodoList
- Vue-Router 路由的理解和使用
- VueX 的语法详解
- CompositionAPI 中如何使用 VueX
- 使用 axios 发送ajax 请求
- Vue3.0(正式版) + TS
- 你好 Typescript: 进入类型的世界
- 什么是 Typescript
- 为什么要学习 Typescript
- 安装 Typescript
- 原始数据类型和 Any 类型
- 数组和元组
- Interface- 接口初探
- 函数
- 类型推论 联合类型和 类型断言
- class - 类 初次见面
- 类和接口 - 完美搭档
- 枚举(Enum)
- 泛型(Generics) 第一部分
- 泛型(Generics) 第二部分 - 约束泛型
- 泛型第三部分 - 泛型在类和接口中的使用
- 类型别名,字面量 和 交叉类型
- 声明文件
- 内置类型
- 总结