引言:为什么选择ASP.NET与Vue.js的组合?
在现代Web开发中,前后端分离已经成为主流架构模式。ASP.NET作为微软强大的后端框架,以其稳定性、高性能和丰富的生态系统著称;而Vue.js作为轻量级前端框架,以其易学性、灵活性和出色的用户体验受到开发者青睐。将这两者结合,可以充分发挥各自优势,构建出高效、可维护的现代化Web应用。
前后端分离开发的核心优势包括:
职责分离
:前端专注于用户界面和交互,后端专注于业务逻辑和数据处理
独立开发
:前后端团队可以并行开发,通过API接口进行协作
技术栈灵活
:可以独立升级和维护前后端技术栈
性能优化
:前端可以利用现代浏览器特性,后端可以专注于服务优化
第一部分:基础环境搭建
1.1 开发环境准备
在开始集成之前,我们需要确保开发环境满足以下要求:
必备软件:
.NET 6.0或更高版本(推荐.NET 8.0)
Node.js 16.x或更高版本
Visual Studio 2022或VS Code
Vue CLI或Vite(推荐使用Vite)
验证安装:
# 检查.NET版本
dotnet –version
# 检查Node.js版本
node –version
# 检查npm版本
npm –version
1.2 创建ASP.NET Core Web API项目
首先创建后端项目,我们将使用ASP.NET Core Web API作为服务端:
# 创建项目目录
mkdir VueDotnetIntegration
cd VueDotnetIntegration
# 创建ASP.NET Core Web API项目
dotnet new webapi -n Backend.Api
# 进入项目目录
cd Backend.Api
# 还原依赖
dotnet restore
# 运行项目验证
dotnet run
项目结构说明:
Backend.Api/
├── Controllers/ # API控制器
├── Models/ # 数据模型
├── Services/ # 业务服务层
├── Program.cs # 应用入口
├── appsettings.json # 配置文件
└── Backend.Api.csproj # 项目文件
1.3 创建Vue.js前端项目
创建Vue.js前端项目,我们推荐使用Vite作为构建工具:
# 回到根目录
cd ..
# 创建Vue项目(使用Vite)
npm create vue@latest Frontend.Web
# 进入前端项目
cd Frontend.Web
# 安装依赖
npm install
# 运行前端项目
npm run dev
Vue项目结构:
Frontend.Web/
├── public/ # 静态资源
├── src/
│ ├── components/ # 可复用组件
│ ├── views/ # 页面组件
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ ├── services/ # API服务层
│ ├── utils/ # 工具函数
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── index.html # HTML模板
├── vite.config.ts # Vite配置
└── package.json # 依赖配置
第二部分:核心集成策略
2.1 跨域配置(CORS)
前后端分离开发中,跨域问题是首要解决的问题。在ASP.NET Core中配置CORS:
Program.cs配置:
using Microsoft.AspNetCore.Cors;
var builder = WebApplication.CreateBuilder(args);
// 添加服务
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 配置CORS策略
builder.Services.AddCors(options =>
{
options.AddPolicy(“VueAppPolicy”, policy =>
{
policy.WithOrigins(“http://localhost:5173”) // Vue开发服务器地址
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // 允许携带凭证
});
});
var app = builder.Build();
// 中间件配置
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// 必须在UseRouting和UseEndpoints之间
app.UseCors(“VueAppPolicy”);
app.UseAuthorization();
app.MapControllers();
app.Run();
重要提示:
CORS中间件的顺序非常重要,必须在UseRouting之后、UseEndpoints之前。
2.2 API设计最佳实践
创建一个完整的示例API,展示RESTful风格设计:
数据模型(Models/Product.cs):
namespace Backend.Api.Models
{
public class Product
{
public int Id { get; get; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; get; }
public DateTime CreatedAt { get; set; }
}
public class ProductCreateDto
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; get; }
}
public class ProductUpdateDto
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; get; }
}
}
服务层(Services/IProductService.cs):
using Backend.Api.Models;
namespace Backend.Api.Services
{
public interface IProductService
{
Task> GetAllAsync();
Task
Task
Task
Task
}
}
服务实现(Services/ProductService.cs):
using Backend.Api.Models;
namespace Backend.Api.Services
{
public class ProductService : IProductService
{
// 模拟数据库存储
private static readonly List
{
new Product { Id = 1, Name = “Laptop”, Description = “High-end laptop”, Price = 1200.00m, Stock = 50, CreatedAt = DateTime.UtcNow },
new Product { Id = 2, Name = “Mouse”, Description = “Wireless mouse”, Price = 25.00m, Stock = 200, CreatedAt = DateTime.UtcNow }
};
public async Task> GetAllAsync()
{
await Task.Delay(10); // 模拟异步操作
return _products.ToList();
}
public async Task
{
await Task.Delay(10);
return _products.FirstOrDefault(p => p.Id == id);
}
public async Task
{
await Task.Delay(10);
var newProduct = new Product
{
Id = _products.Max(p => p.Id) + 1,
Name = product.Name,
Description = product.Description,
Price = product.Price,
Stock = product.Stock,
CreatedAt = DateTime.UtcNow
};
_products.Add(newProduct);
return newProduct;
}
public async Task
{
await Task.Delay(10);
var existing = _products.FirstOrDefault(p => p.Id == id);
if (existing == null) return false;
existing.Name = product.Name;
existing.Description = product.Description;
existing.Price = product.Price;
existing.Stock = product.Stock;
return true;
}
public async Task
{
await Task.Delay(10);
var product = _products.FirstOrDefault(p => p.Id == id);
if (product == null) return false;
_products.Remove(product);
return true;
}
}
}
API控制器(Controllers/ProductsController.cs):
using Backend.Api.Models;
using Backend.Api.Services;
using Microsoft.AspNetCore.Mvc;
namespace Backend.Api.Controllers
{
[ApiController]
[Route(“api/[controller]”)]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task
{
var products = await _productService.GetAllAsync();
return Ok(products);
}
[HttpGet(“{id}”)]
public async Task
{
var product = await _productService.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
[HttpPost]
public async Task
{
var created = await _productService.CreateAsync(product);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
[HttpPut(“{id}”)]
public async Task
{
var success = await _productService.UpdateAsync(id, product);
if (!success)
return NotFound();
return NoContent();
}
[HttpDelete(“{id}”)]
public async Task
{
var success = await _productService.DeleteAsync(id);
if (!success)
return NotFound();
return NoContent();
}
}
}
注册服务(Program.cs):
// 在Program.cs中添加服务注册
builder.Services.AddScoped
2.3 前端API服务层设计
在Vue.js中,我们需要创建一个清晰的API服务层来处理与后端的通信。
安装HTTP客户端:
npm install axios
创建API基础配置(src/services/api.ts):
import axios, { AxiosInstance, AxiosResponse, AxiosError } from ‘axios’;
// 创建axios实例
const apiClient: AxiosInstance = axios.create({
baseURL: ‘https://localhost:7000/api’, // 你的API地址
timeout: 10000, // 请求超时时间
headers: {
‘Content-Type’: ‘application/json’,
},
});
// 请求拦截器
apiClient.interceptors.request.use(
(config) => {
// 可以在这里添加认证token
const token = localStorage.getItem(‘authToken’);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
console.log(‘请求发送:’, config.method?.toUpperCase(), config.url);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
apiClient.interceptors.response.use(
(response: AxiosResponse) => {
console.log(‘响应接收:’, response.status, response.config.url);
return response;
},
(error: AxiosError) => {
// 统一错误处理
if (error.response) {
const { status, data } = error.response;
console.error(‘API错误:’, status, data);
// 根据状态码处理错误
switch (status) {
case 401:
// 未授权,跳转登录页
window.location.href = ‘/login’;
break;
case 403:
// 无权限
console.error(‘没有访问权限’);
break;
case 404:
// 资源不存在
console.error(‘请求的资源不存在’);
break;
case 500:
// 服务器错误
console.error(‘服务器内部错误’);
break;
}
} else if (error.request) {
// 请求已发送但无响应
console.error(‘网络错误,请检查网络连接’);
} else {
// 发送请求时出错
console.error(‘请求配置错误:’, error.message);
}
return Promise.reject(error);
}
);
export default apiClient;
创建产品服务(src/services/productService.ts):
import apiClient from ‘./api’;
// 定义数据类型
export interface Product {
id: number;
name: string;
description: string;
price: number;
stock: number;
createdAt: string;
}
export interface ProductCreateDto {
name: string;
description: string;
price: number;
stock: number;
}
export interface ProductUpdateDto {
name: string;
description: string;
price: number;
stock: number;
}
// 产品服务类
class ProductService {
// 获取所有产品
async getAll(): Promise
const response = await apiClient.get
return response.data;
}
// 根据ID获取产品
async getById(id: number): Promise
const response = await apiClient.get
return response.data;
}
// 创建产品
async create(product: ProductCreateDto): Promise
const response = await apiClient.post
return response.data;
}
// 更新产品
async update(id: number, product: ProductUpdateDto): Promise
await apiClient.put(`/products/${id}`, product);
}
// 删除产品
async delete(id: number): Promise
await apiClient.delete(`/products/${id}`);
}
}
export const productService = new ProductService();
2.4 Vue组件集成示例
主应用组件(src/App.vue):
产品列表组件(src/views/ProductList.vue):
产品管理
| ID | 名称 | 描述 | 价格 | 库存 | 操作 |
|---|---|---|---|---|---|
| {{ product.id }} | {{ product.name }} | {{ product.description }} | ${{ product.price.toFixed(2) }} | {{ product.stock }} |
|
添加/编辑产品组件(src/views/ProductForm.vue):
{{ isEditMode ? ‘编辑产品’ : ‘添加产品’ }}
2.5 路由配置
路由配置(src/router/index.ts):
import { createRouter, createWebHistory } from ‘vue-router’;
import ProductList from ‘../views/ProductList.vue’;
import ProductForm from ‘../views/ProductForm.vue’;
const routes = [
{
path: ‘/’,
name: ‘product-list’,
component: ProductList
},
{
path: ‘/add’,
name: ‘add-product’,
component: ProductForm
},
{
path: ‘/edit/:id’,
name: ‘edit-product’,
component: ProductForm
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
主入口文件(src/main.ts):
import { createApp } from ‘vue’;
import App from ‘./App.vue’;
import router from ‘./router’;
const app = createApp(App);
app.use(router);
app.mount(‘#app’);
第三部分:高级集成技巧
3.1 认证与授权集成
JWT认证实现:
后端JWT服务(Services/JwtService.cs):
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Backend.Api.Services
{
public interface IJwtService
{
string GenerateToken(string username, string role);
}
public class JwtService : IJwtService
{
private readonly IConfiguration _configuration;
public JwtService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateToken(string username, string role)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration[“Jwt:Key”] ?? “your-secret-key-min-32-chars”)
);
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration[“Jwt:Issuer”],
audience: _configuration[“Jwt:Audience”],
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}
认证控制器(Controllers/AuthController.cs):
using Backend.Api.Services;
using Microsoft.AspNetCore.Mvc;
namespace Backend.Api.Controllers
{
[ApiController]
[Route(“api/[controller]”)]
public class AuthController : ControllerBase
{
private readonly IJwtService _jwtService;
public AuthController(IJwtService jwtService)
{
_jwtService = jwtService;
}
[HttpPost(“login”)]
public IActionResult Login([FromBody] LoginRequest request)
{
// 模拟用户验证(实际项目中应该查询数据库)
if (request.Username == “admin” && request.Password == “admin123”)
{
var token = _jwtService.GenerateToken(request.Username, “Admin”);
return Ok(new { token });
}
return Unauthorized(new { message = “用户名或密码错误” });
}
}
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
}
配置JWT(appsettings.json):
{
“Jwt”: {
“Key”: “your-super-secret-key-that-is-at-least-32-characters”,
“Issuer”: “VueDotnetAPI”,
“Audience”: “VueApp”
}
}
前端认证服务(src/services/authService.ts):
import apiClient from ‘./api’;
export interface LoginCredentials {
username: string;
password: string;
}
export interface AuthResponse {
token: string;
}
class AuthService {
async login(credentials: LoginCredentials): Promise
const response = await apiClient.post
// 保存token
localStorage.setItem(‘authToken’, response.data.token);
return response.data;
}
logout(): void {
localStorage.removeItem(‘authToken’);
}
getToken(): string | null {
return localStorage.getItem(‘authToken’);
}
isAuthenticated(): boolean {
return !!this.getToken();
}
}
export const authService = new AuthService();
路由守卫(src/router/guards.ts):
import { NavigationGuardNext, RouteLocationNormalized } from ‘vue-router’;
import { authService } from ‘../services/authService’;
export const authGuard = (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
// 需要认证的路由
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (requiresAuth && !authService.isAuthenticated()) {
next(‘/login’);
} else {
next();
}
};
// 在路由配置中使用
// router.beforeEach(authGuard);
3.2 状态管理集成(Pinia)
安装Pinia:
npm install pinia
创建Pinia Store(src/stores/productStore.ts):
import { defineStore } from ‘pinia’;
import { productService, type Product } from ‘../services/productService’;
interface ProductState {
products: Product[];
loading: boolean;
error: string | null;
}
export const useProductStore = defineStore(‘product’, {
state: (): ProductState => ({
products: [],
loading: false,
error: null
}),
getters: {
getProductById: (state) => {
return (id: number) => state.products.find(p => p.id === id);
},
totalProducts: (state) => state.products.length,
inStockProducts: (state) => state.products.filter(p => p.stock > 0).length
},
actions: {
async fetchProducts() {
this.loading = true;
this.error = null;
try {
this.products = await productService.getAll();
} catch (err) {
this.error = ‘获取产品列表失败’;
console.error(‘Fetch products error:’, err);
} finally {
this.loading = false;
}
},
async addProduct(productData: any) {
this.loading = true;
try {
const newProduct = await productService.create(productData);
this.products.push(newProduct);
return newProduct;
} catch (err) {
this.error = ‘添加产品失败’;
throw err;
} finally {
this.loading = false;
}
},
async updateProduct(id: number, productData: any) {
this.loading = true;
try {
await productService.update(id, productData);
const index = this.products.findIndex(p => p.id === id);
if (index !== -1) {
this.products[index] = { …this.products[index], …productData };
}
} catch (err) {
this.error = ‘更新产品失败’;
throw err;
} finally {
this.loading = false;
}
},
async deleteProduct(id: number) {
try {
await productService.delete(id);
this.products = this.products.filter(p => p.id !== id);
} catch (err) {
this.error = ‘删除产品失败’;
throw err;
}
},
clearError() {
this.error = null;
}
}
});
在main.ts中配置Pinia:
import { createApp } from ‘vue’;
import { createPinia } from ‘pinia’;
import App from ‘./App.vue’;
import router from ‘./router’;
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
app.mount(‘#app’);
使用Store的组件示例(src/views/ProductListWithStore.vue):
产品管理 (Pinia Store)
有货: {{ productStore.inStockProducts }}
| ID | 名称 | 价格 | 库存 | 操作 |
|---|---|---|---|---|
| {{ product.id }} | {{ product.name }} | ${{ product.price.toFixed(2) }} | {{ product.stock }} |
|
3.3 错误处理与日志记录
后端全局异常处理中间件:
// Middleware/ExceptionMiddleware.cs
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger
public ExceptionMiddleware(RequestDelegate next, ILogger
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, “An unhandled exception occurred”);
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = “application/json”;
var errorResponse = new
{
error = “Internal Server Error”,
message = ex.Message,
timestamp = DateTime.UtcNow
};
await context.Response.WriteAsJsonAsync(errorResponse);
}
}
}
// 在Program.cs中注册
app.UseMiddleware
前端错误监控(src/utils/errorHandler.ts):
import { AxiosError } from ‘axios’;
export class ErrorHandler {
static handle(error: any): string {
if (error instanceof AxiosError) {
const status = error.response?.status;
const message = error.response?.data?.message || error.message;
switch (status) {
case 400:
return `请求错误: ${message}`;
case 401:
return ‘未授权,请重新登录’;
case 403:
return ‘拒绝访问’;
case 404:
return ‘请求的资源不存在’;
case 500:
return ‘服务器内部错误’;
default:
return `网络错误: ${message}`;
}
}
if (error instanceof Error) {
return error.message;
}
return ‘未知错误’;
}
static log(error: any): void {
console.error(‘Error caught:’, {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
// 可以发送到日志服务
// this.sendToLoggingService(error);
}
static sendToLoggingService(error: any): void {
// 实现发送到后端日志服务
const errorData = {
message: error.message,
stack: error.stack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
};
// 使用navigator.sendBeacon或fetch发送
fetch(‘/api/logs/error’, {
method: ‘POST’,
body: JSON.stringify(errorData),
headers: { ‘Content-Type’: ‘application/json’ }
}).catch(() => {
// 如果发送失败,至少在控制台记录
console.error(‘Failed to send error log’, errorData);
});
}
}
// 全局错误处理器
export const setupGlobalErrorHandler = (app: any) => {
// 捕获Vue组件错误
app.config.errorHandler = (err: any, instance: any, info: string) => {
ErrorHandler.log({ message: `Vue Error: ${info}`, stack: err.stack });
};
// 捕获未处理的Promise拒绝
window.addEventListener(‘unhandledrejection’, (event) => {
ErrorHandler.log({ message: ‘Unhandled Promise Rejection’, stack: event.reason?.stack });
});
// 捕获资源加载错误
window.addEventListener(‘error’, (event) => {
if (event.target instanceof HTMLElement) {
ErrorHandler.log({ message: `Resource Error: ${event.target.tagName}`, stack: ” });
}
}, true);
};
第四部分:常见兼容性问题解决方案
4.1 跨域问题深度解决
问题现象:
浏览器控制台出现CORS错误,如”Access to fetch at ‘…’ from origin ‘…’ has been blocked by CORS policy”
解决方案:
1. 开发环境CORS配置优化:
// Program.cs – 开发环境专用配置
if (app.Environment.IsDevelopment())
{
app.UseCors(“VueAppPolicy”);
// 开发环境允许所有来源(仅开发使用)
builder.Services.AddCors(options =>
{
options.AddPolicy(“VueDevPolicy”, policy =>
{
policy.SetIsOriginAllowed(origin => true) // 开发环境允许所有来源
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
}
2. 预检请求(Preflight)处理:
// 在控制器中添加[EnableCors]特性
[ApiController]
[Route(“api/[controller]”)]
[EnableCors(“VueAppPolicy”)] // 启用CORS
public class ProductsController : ControllerBase
{
// …
}
3. 代理配置(Vite):
// vite.config.ts
import { defineConfig } from ‘vite’;
import vue from ‘@vitejs/plugin-vue’;
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
‘/api’: {
target: ‘https://localhost:7000’,
changeOrigin: true,
secure: false,
// 重写路径(可选)
// rewrite: (path) => path.replace(/^\/api/, ”)
}
}
}
});
4. Nginx代理配置(生产环境):
server {
listen 80;
server_name yourdomain.com;
location /api/ {
proxy_pass https://localhost:7000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection ‘upgrade’;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# 处理CORS
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods ‘GET, POST, PUT, DELETE, OPTIONS’ always;
add_header Access-Control-Allow-Headers ‘Authorization, Content-Type’ always;
if ($request_method = ‘OPTIONS’) {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods ‘GET, POST, PUT, DELETE, OPTIONS’;
add_header Access-Control-Allow-Headers ‘Authorization, Content-Type’;
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
}
location / {
root /var/www/vue-app;
try_files $uri $uri/ /index.html;
}
}
4.2 认证状态同步问题
问题:
刷新页面后,前端认证状态丢失,导致需要重新登录
解决方案:
1. 持久化认证状态:
// src/utils/authStorage.ts
export class AuthStorage {
private static readonly TOKEN_KEY = ‘authToken’;
private static readonly EXPIRY_KEY = ‘tokenExpiry’;
private static readonly USER_KEY = ‘user’;
static saveToken(token: string): void {
const expiry = Date.now() + (60 * 60 * 1000); // 1小时
localStorage.setItem(this.TOKEN_KEY, token);
localStorage.setItem(this.EXPIRY_KEY, expiry.toString());
}
static getToken(): string | null {
const token = localStorage.getItem(this.TOKEN_KEY);
const expiry = localStorage.getItem(this.EXPIRY_KEY);
if (!token || !expiry) return null;
// 检查是否过期
if (Date.now() > parseInt(expiry)) {
this.clear();
return null;
}
return token;
}
static saveUser(user: any): void {
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
}
static getUser(): any | null {
const user = localStorage.getItem(this.USER_KEY);
return user ? JSON.parse(user) : null;
}
static clear(): void {
localStorage.removeItem(this.TOKEN_KEY);
localStorage.removeItem(this.EXPIRY_KEY);
localStorage.removeItem(this.USER_KEY);
}
static isAuthenticated(): boolean {
return !!this.getToken();
}
}
2. 应用初始化时恢复状态:
// src/main.ts
import { createApp } from ‘vue’;
import { createPinia } from ‘pinia’;
import App from ‘./App.vue’;
import router from ‘./router’;
import { AuthStorage } from ‘./utils/authStorage’;
import { useAuthStore } from ‘./stores/authStore’;
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
// 恢复认证状态
const authStore = useAuthStore();
const token = AuthStorage.getToken();
const user = AuthStorage.getUser();
if (token && user) {
authStore.setAuthData(token, user);
}
app.mount(‘#app’);
3. 请求拦截器自动附加token:
// src/services/api.ts
apiClient.interceptors.request.use(
(config) => {
const token = AuthStorage.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
4.3 日期格式兼容性问题
问题:
ASP.NET返回的日期格式(ISO 8601)与JavaScript的Date对象处理差异
解决方案:
1. 后端统一日期格式:
// Program.cs – 配置JSON序列化
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter());
});
// 自定义日期转换器
public class JsonDateTimeConverter : JsonConverter
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.Parse(reader.GetString() ?? string.Empty);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToUniversalTime().ToString(“yyyy-MM-ddTHH:mm:ssZ”));
}
}
2. 前端日期处理工具:
// src/utils/dateUtils.ts
export class DateUtils {
// 将ASP.NET日期字符串转换为Date对象
static parseAspNetDate(dateString: string): Date {
// ASP.NET返回的日期格式:2024-01-15T10:30:00Z
return new Date(dateString);
}
// 格式化日期为本地字符串
static formatDate(date: Date | string, format: string = ‘yyyy-MM-dd HH:mm’): string {
const d = typeof date === ‘string’ ? this.parseAspNetDate(date) : date;
const year = d.getFullYear();
const month = (d.getMonth() + 1).toString().padStart(2, ‘0’);
const day = d.getDate().toString().padStart(2, ‘0’);
const hours = d.getHours().toString().padStart(2, ‘0’);
const minutes = d.getMinutes().toString().padStart(2, ‘0’);
const seconds = d.getSeconds().toString().padStart(2, ‘0’);
return format
.replace(‘yyyy’, year.toString())
.replace(‘MM’, month)
.replace(‘dd’, day)
.replace(‘HH’, hours)
.replace(‘mm’, minutes)
.replace(‘ss’, seconds);
}
// 相对时间显示
static relativeTime(date: Date | string): string {
const d = typeof date === ‘string’ ? this.parseAspNetDate(date) : date;
const now = new Date();
const diff = now.getTime() – d.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}天前`;
if (hours > 0) return `${hours}小时前`;
if (minutes > 0) return `${minutes}分钟前`;
if (seconds > 0) return `${seconds}秒前`;
return ‘刚刚’;
}
}
3. Vue组件中使用:
创建时间: {{ formattedDate }}
相对时间: {{ relativeTime }}
4.4 文件上传与下载处理
后端文件上传API:
[ApiController]
[Route(“api/[controller]”)]
public class FilesController : ControllerBase
{
private readonly IWebHostEnvironment _env;
public FilesController(IWebHostEnvironment env)
{
_env = env;
}
[HttpPost(“upload”)]
public async Task
{
if (file == null || file.Length == 0)
return BadRequest(“No file uploaded”);
var uploadsFolder = Path.Combine(_env.WebRootPath, “uploads”);
if (!Directory.Exists(uploadsFolder))
Directory.CreateDirectory(uploadsFolder);
var fileName = $”{Guid.NewGuid()}_{file.FileName}”;
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Ok(new {
fileName,
filePath,
size = file.Length
});
}
[HttpGet(“download/{fileName}”)]
public IActionResult DownloadFile(string fileName)
{
var filePath = Path.Combine(_env.WebRootPath, “uploads”, fileName);
if (!System.IO.File.Exists(filePath))
return NotFound();
var fileBytes = System.IO.File.ReadAllBytes(filePath);
var contentType = “application/octet-stream”;
return File(fileBytes, contentType, fileName);
}
}
前端文件上传组件:
文件上传
拖拽文件到此处或
文件名: {{ selectedFile.name }}
大小: {{ formatFileSize(selectedFile.size) }}
{{ uploadProgress }}%
上传成功!
文件名: {{ uploadResult.fileName }}
大小: {{ formatFileSize(uploadResult.size) }}