Implementation Guide for JWT Authentication, Vant UI Integration and Common Interactive Features in Vue + Node.js Mobile Fullstack Projects
Backend JWT Authentication Setup
Install jsonwebtokan dependency first:
npm install jsonwebtoken --save
Generate JWT on successful login
Add login endpoint handler in user route file:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
router.post('/api/user/login', async (req, res) => {
const { userTel, loginPwd } = req.body;
const matchedUsers = await sql.query(UserModel, { telephone: userTel }, { __v: 0 });
if (matchedUsers.length === 0) {
return res.json({
code: '20001',
msg: 'User not registered'
});
}
const storedPwd = matchedUsers[0].password;
const pwdValid = bcrypt.compareSync(loginPwd, storedPwd);
if (!pwdValid) {
return res.json({
code: '20002',
msg: 'Incorrect password'
});
}
const userId = matchedUsers[0].uid;
const token = jwt.sign({ userId }, 'fullstack_jwt_secret_2024', {
expiresIn: 7200 // Token valid for 2 hours
});
res.json({
code: '20000',
msg: 'Login successful',
authToken: token
});
});
Global JWT validation middleware
Add the following middleware in app.js before route registration to protect private interfaces:
const jwt = require('jsonwebtoken');
app.use((req, res, next) => {
// Exclude public interfaces from authentication
const publicPaths = ['/api/user/login', '/api/user/register', '/api/banner'];
if (!publicPaths.includes(req.path)) {
// Fetch token from header, query or request body
const token = req.headers.authtoken || req.query.authToken || req.body.authToken;
if (!token) {
return res.json({
code: '20003',
msg: 'No valid authentication credentials found'
});
}
jwt.verify(token, 'fullstack_jwt_secret_2024', (err, payload) => {
if (err) {
return res.json({
code: '20003',
msg: 'Authentication expired, please login again'
});
}
req.userInfo = payload;
next();
});
} else {
next();
}
});
Frontend Login Module Implementation
Login page development
Create views/login/index.vue file:
<template>
<div class="login-wrapper">
<div class="page-header">Account Login</div>
<div class="form-content">
<input type="tel" placeholder="Please enter mobile number" v-model="phoneNum" />
<p class="error-tip">{{ phoneErrMsg }}</p>
<input type="password" placeholder="Please enter password" v-model="loginPwd" />
<p class="error-tip">{{ pwdErrMsg }}</p>
<button class="submit-btn" @click="handleLogin">Login Now</button>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
phoneNum: '',
loginPwd: '',
submitErr: ''
}
},
computed: {
phoneErrMsg() {
if (!this.phoneNum) return '';
return /^1[3-9]\d{9}$/.test(this.phoneNum) ? '' : 'Mobile number format is incorrect';
},
pwdErrMsg() {
if (!this.loginPwd) return '';
return this.loginPwd.length >=6 ? '' : 'Password must be at least 6 characters long';
}
},
methods: {
async handleLogin() {
if (this.phoneErrMsg || !this.phoneNum) {
this.submitErr = 'Please enter valid mobile number';
return;
}
if (this.pwdErrMsg || !this.loginPwd) {
this.submitErr = 'Please enter valid password';
return;
}
const res = await axios.post('/api/user/login', {
userTel: this.phoneNum,
loginPwd: this.loginPwd
});
if (res.data.code === '20001') {
this.submitErr = 'Account not registered, please register first';
} else if (res.data.code === '20002') {
this.submitErr = 'Password is incorrect, please try again';
} else if (res.data.code === '20000') {
this.submitErr = '';
localStorage.setItem('auth_token', res.data.authToken);
this.$router.push('/home');
}
}
}
}
</script>
<style scoped lang="scss">
.login-wrapper {
width: 100%;
height: 100vh;
background: #fff;
}
.page-header {
height: 44px;
line-height: 44px;
text-align: center;
font-size: 16px;
font-weight: 500;
border-bottom: 1px solid #eee;
}
.form-content {
padding: 20px 15px;
}
input {
width: 100%;
height: 45px;
line-height: 45px;
border: none;
border-bottom: 1px solid #eee;
text-indent: 10px;
margin-bottom: 5px;
font-size: 14px;
}
.error-tip {
height: 18px;
line-height: 18px;
color: #ff4d4f;
font-size: 12px;
text-align: center;
}
.submit-btn {
width: 100%;
height: 45px;
line-height: 45px;
border: none;
border-radius: 4px;
background: #ff4d4f;
color: #fff;
font-size: 16px;
margin-top: 20px;
}
</style>
Login route configuration
Add login route entry in router/index.js:
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue')
}
]
Authentication for Private Pages
Homepage access control
Adjust homepage template to show content only after successful authentication:
<template>
<div class="home-page">
<div class="page-content">
<ProductList v-if="isLoggedIn" :productList="productList" />
<div v-else class="auth-tip">
<p>Please login to view product content</p>
<router-link to="/login" class="login-link">Go to login</router-link>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import ProductList from '@/components/ProductList.vue';
export default {
components: { ProductList },
data() {
return {
productList: [],
isLoggedIn: false
}
},
async created() {
const token = localStorage.getItem('auth_token');
const res = await axios.get(`/api/product?authToken=${token}`);
if (res.data.code === '20003') {
this.isLoggedIn = false;
} else {
this.isLoggedIn = true;
this.productList = res.data.data;
}
}
}
</script>
Product detail page access control
Adjust detail page logic to verify authentication before loading data:
<template>
<div class="detail-page">
<div class="page-content" v-if="isLoggedIn">
<img :src="productCover" alt="product cover" class="prod-img" />
<h2 class="prod-name">{{ productName }}</h2>
<p class="prod-price">¥ {{ productPrice }}</p>
<div class="prod-desc">{{ productDesc }}</div>
<div class="comment-section">
<div class="comment-item" v-for="item in commentList" :key="item.commentId">
<h4>{{ item.nickname }} <span class="rating">{{ item.rate }} stars</span></h4>
<p>{{ item.content }}</p>
</div>
</div>
</div>
<div v-else class="auth-tip">
<p>Please login to view product details</p>
<router-link to="/login" class="login-link">Go to login</router-link>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
isLoggedIn: false,
productId: '',
productName: '',
productCover: '',
productPrice: '',
productDesc: '',
commentList: []
}
},
async created() {
this.productId = this.$route.query.pid;
const token = localStorage.getItem('auth_token');
const [prodRes, commentRes] = await Promise.all([
axios.get(`/api/product/detail?pid=${this.productId}&authToken=${token}`),
axios.get(`/api/comment/list?pid=${this.productId}&authToken=${token}`)
]);
if (prodRes.data.code === '20003' || commentRes.data.code === '20003') {
this.isLoggedIn = false;
} else {
this.isLoggedIn = true;
const prodInfo = prodRes.data.data;
this.productName = prodInfo.name;
this.productCover = prodInfo.cover;
this.productPrice = prodInfo.price;
this.productDesc = prodInfo.desc;
this.commentList = commentRes.data.data;
}
}
}
</script>
Vant UI Integration
Vant is a lightweight mobile UI component library suitable for Vue-based mobile projects.
Install dependencies
npm install vant --save
npm install babel-plugin-import --save-dev
On-demand import configuration
Modify babel.config.js to enable on-demand component loading:
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
Note: Restart the local development server after modifying Babel configuraton to take effect.
Implement Common Interactive Features
Homepage carousel
- Register Swipe component in entry file or homepage:
import Vue from 'vue';
import { Swipe, SwipeItem } from 'vant';
Vue.use(Swipe).use(SwipeItem);
- Load banner data (public interface, no authentication required):
async created() {
const bannerRes = await axios.get('/api/banner');
this.bannerList = bannerRes.data.data;
// Other request logic
}
- Render carousel in template:
<van-swipe autoplay="3000" indicator-color="#fff">
<van-swipe-item v-for="item in bannerList" :key="item.bid">
<img :src="item.imgUrl" alt="banner" class="banner-img" />
</van-swipe-item>
</van-swipe>
Product detail page bottom action bar
Import and register GoodsAction component, add to detail page bottom:
import { GoodsAction, GoodsActionIcon, GoodsActionButton } from 'vant';
Vue.use(GoodsAction).use(GoodsActionIcon).use(GoodsActionButton);
<van-goods-action>
<van-goods-action-icon icon="chat-o" text="Customer Service" />
<van-goods-action-icon icon="cart-o" text="Cart" />
<van-goods-action-button type="warning" text="Add to Cart" />
<van-goods-action-button type="danger" text="Buy Now" />
</van-goods-action>
City selector page
- Add route for city selector:
{
path: '/city-select',
name: 'CitySelect',
component: () => import('@/views/city/index.vue')
}
- Implement city selector with Vant IndexBar:
<template>
<div class="city-page">
<div class="page-header">Select City</div>
<div class="page-content">
<van-index-bar>
<van-index-anchor v-for="(group, idx) in cityGroupList" :key="idx" :index="group.initial">
<van-cell v-for="city in group.cityList" :key="city.cid" :title="city.cityName" @click="selectCity(city)" />
</van-index-anchor>
</van-index-bar>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import { IndexBar, IndexAnchor, Cell } from 'vant';
import axios from 'axios';
Vue.use(IndexBar).use(IndexAnchor).use(Cell);
export default {
data() {
return {
cityGroupList: []
}
},
async created() {
const res = await axios.get('/static/city_data.json');
this.cityGroupList = res.data;
},
methods: {
selectCity(city) {
localStorage.setItem('selected_city', JSON.stringify(city));
this.$router.back();
}
}
}
</script>
Pull up to load more products
- Import and register List component:
import { List } from 'vant';
Vue.use(List);
- Add related data and methods in homepage:
data() {
return {
productList: [],
isLoggedIn: false,
isLoadingMore: false,
isAllLoaded: false,
currentPage: 1,
pageSize: 10
}
},
methods: {
async handleLoadMore() {
this.isLoadingMore = true;
const token = localStorage.getItem('auth_token');
const res = await axios.get(`/api/product?page=${this.currentPage}&size=${this.pageSize}&authToken=${token}`);
this.isLoadingMore = false;
if (res.data.code !== '20003') {
this.isLoggedIn = true;
const newProducts = res.data.data;
if (newProducts.length === 0) {
this.isAllLoaded = true;
} else {
this.productList = this.productList.concat(newProducts);
this.currentPage += 1;
}
} else {
this.isLoggedIn = false;
}
}
}
- Wrap product list with List component:
<van-list
v-model="isLoadingMore"
:finished="isAllLoaded"
finished-text="No more products"
@load="handleLoadMore"
>
<ProductList v-if="isLoggedIn" :productList="productList" />
<div v-else class="auth-tip">
<p>Please login to view product content</p>
<router-link to="/login" class="login-link">Go to login</router-link>
</div>
</van-list>
Pull down to refresh
- Import and register PullRefresh component:
import { PullRefresh } from 'vant';
Vue.use(PullRefresh);
- Add related data and methods:
data() {
return {
// Other existing data
isRefreshing: false
}
},
methods: {
async handleRefresh() {
this.isRefreshing = true;
const token = localStorage.getItem('auth_token');
const res = await axios.get(`/api/product?page=1&size=${this.pageSize}&authToken=${token}`);
this.isRefreshing = false;
if (res.data.code !== '20003') {
this.isLoggedIn = true;
this.productList = res.data.data;
this.currentPage = 2;
this.isAllLoaded = false;
} else {
this.isLoggedIn = false;
}
}
}
- Wrap page content with PullRefresh component:
<van-pull-refresh v-model="isRefreshing" @refresh="handleRefresh">
<van-swipe autoplay="3000" indicator-color="#fff">
<van-swipe-item v-for="item in bannerList" :key="item.bid">
<img :src="item.imgUrl" alt="banner" class="banner-img" />
</van-swipe-item>
</van-swipe>
<van-list
v-model="isLoadingMore"
:finished="isAllLoaded"
finished-text="No more products"
@load="handleLoadMore"
>
<ProductList v-if="isLoggedIn" :productList="productList" />
<div v-else class="auth-tip">
<p>Please login to view product content</p>
<router-link to="/login" class="login-link">Go to login</router-link>
</div>
</van-list>
</van-pull-refresh>
Back to top button
- Add iconfont import in
public/index.html:
<link rel="stylesheet" href="//at.alicdn.com/t/font_1476238_uph8zgimp3.css" />
- Add back to top button in homepage template:
<span
class="back-top iconfont icon-fanhuidingbu"
v-show="showBackTop"
@click="handleBackTop"
></span>
<style scoped>
.back-top {
position: fixed;
bottom: 60px;
right: 15px;
font-size: 32px;
color: #666;
z-index: 999;
}
</style>
- Add related logic:
data() {
return {
// Other existing data
showBackTop: false
}
},
watch: {
currentPage(newVal) {
this.showBackTop = newVal > 2;
}
},
methods: {
handleBackTop() {
document.querySelector('.page-content').scrollTop = 0;
this.showBackTop = false;
}
}