部署在o2switch的Node.js+Vue.js应用路由404问题求助
问题:Vue路由刷新/直接访问出现404(Cannot GET /exemple)
我在o2switch上部署了Node.js后端与Vue.js前端应用,直接点击链接访问特定路由或刷新页面(F5)时频繁出现404错误(提示Cannot GET /exemple),本地运行正常。已检查代码中的路由定义,确认无误。创建Node应用时o2switch自动生成了包含环境变量的.htaccess文件,相关配置文件如下:
.htaccess配置
# DO NOT REMOVE. CLOUDLINUX PASSENGER CONFIGURATION BEGIN PassengerAppRoot "/home/sc3ievr8380/quiz-server" PassengerBaseURI "/" PassengerNodejs "/home/sc3ievr8380/nodevenv/quiz-server/20/bin/node" PassengerAppType node PassengerStartupFile server.js # DO NOT REMOVE. CLOUDLINUX PASSENGER CONFIGURATION END # DO NOT REMOVE OR MODIFY. CLOUDLINUX ENV VARS CONFIGURATION BEGIN <IfModule Litespeed> SetEnv DATABASE_HOST localhost SetEnv DATABASE_PASS nonono SetEnv DATABASE_USER nonono_quiz SetEnv EMAIL_PASS nonono SetEnv EMAIL_USER noreply@nonono.fr SetEnv SERVER_CORS http://quiz.nonono.universe.wf SetEnv SERVER_PORT 3000 SetEnv DATABASE_NAME nonono_bdd_quiz </IfModule> # DO NOT REMOVE OR MODIFY. CLOUDLINUX ENV VARS CONFIGURATION END
前端路由配置(router/index.js)
import { createWebHistory, createRouter } from 'vue-router'; import { useAuthentificationStore } from '@/stores/authentification'; const routes = [{ name: 'login', path: '/', alias: '/login', component: () => import('@/views/QuizLogin'), meta: { title: 'Keepcod - Login' } }, { name: 'quiz', path: '/quiz', component: () => import('@/views/QuizGame'), meta: { requiresAuth: true, requiresAdmin: false, title: 'Keepcod' } }, { name: 'admin', path: '/admin/login', component: () => import('@/views/AdminLogin'), meta: { title: 'Keepcod - Login' } }, { name: 'candidat', path: '/admin/candidat', component: () => import('@/views/AdminCandidate'), meta: { requiresAuth: true, requiresAdmin: true, title: 'Candidats' } }, { name: 'recruiter', path: '/admin/recruiter', component: () => import('@/views/AdminRecruiter'), meta: { requiresAuth: true, requiresAdmin: true, title: 'Recruteurs' } }, { name: 'quizs', path: '/admin/quiz', component: () => import('@/views/AdminQuiz'), meta: { requiresAuth: true, requiresAdmin: true, title: 'Quiz' } }, { name: 'ranking', path: '/admin/rankings', component: () => import('@/views/AdminRanking'), meta: { requiresAuth: true, requiresAdmin: true, title: 'Classements' } }, { name: 'profile', path: '/admin/profile', component: () => import('@/views/AdminProfile'), meta: { requiresAuth: true, requiresAdmin: true, title: 'Profil' } }, { name: 'check', path: '/admin/quiz/:id/:email?', component: () => import('@/views/QuizGame'), meta: { requiresAuth: true, requiresAdmin: true, title: 'Quiz' } }, { name: '404', path: '/:pathMatch(.*)*', component: () => import('@/views/NotFound'), meta: { title: 'Page introuvable' } } ]; const router = createRouter({ history: createWebHistory(), routes }); router.beforeEach((to, from, next) => { const store = useAuthentificationStore(); document.title = to.meta.title; if (to.name == "admin" && store.isAdmin) { next('/admin/candidat'); } else { if (to.meta.requiresAuth && !store.isAuthenticated) { next('/login'); } else { next(); } } }); export default router;
后端应用配置(src/app.mjs)
import express, { Router } from 'express'; import cors from 'cors'; import bodyParser from 'body-parser'; import servConfig from './config/server.config.mjs'; import { sendError } from './controllers/common.controller.mjs'; import quizController from './controllers/quiz.controller.mjs'; import adminController from './controllers/admin.controller.mjs'; import manageController from './controllers/manage.controller.mjs'; const app = express(); // Permet d'autoriser un domaine à accéder aux ressources if (servConfig.CORS) { app.use( cors({ origin: servConfig.CORS }) ); } // parse requests of content-type - application/json app.use(bodyParser.json()); // parse requests of content-type - application/x-www-form-urlencoded app.use(bodyParser.urlencoded({ extended: true })); // Permet de définir l'ensemble des routes du serveur route(app, quizController, '/quiz'); route(app, adminController, '/admin'); route(app, manageController, '/manage'); /** * La route en cas d'erreur */ // La signature de la function doit toujours comporter 4 arguments // eslint-disable-next-line no-unused-vars app.use(function (err, req, res, next) { console.error(res); sendError(res); }); export default app; /** * @private * Permet de créer une nouvelle route * * @param {Express} app - L'application express * @param {Function} controller - Le controller définissant les différentes chemins * @param {string} path - Le chemin racine */ function route(app, controller, path) { let router = Router(); controller(router); app.use(path, router); }
后端启动文件(server.js)
loadServer(); async function loadServer() { const { default: app } = await import ('./src/app.mjs'); const { default: servConfig } = await import ('./src/config/server.config.mjs'); const { default: db } = await import ('./src/models/database.mjs'); db.sequelize.sync(); const PORT = process.env.PORT || servConfig.PORT; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}.`); }); }
Vite配置文件(vite.config.js)
import { fileURLToPath, URL } from "node:url"; import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; // https://vitejs.dev/config/ export default defineConfig({ base: "/", plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => ['lord-icon'].includes(tag), } } })], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)), }, extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"], // add .vue extension } });
解决方案:适配Vue History模式的服务器路由重写
问题核心:Vue使用createWebHistory()(HTML5 History模式)时,刷新或直接访问前端路由,服务器会尝试匹配物理文件或后端路由,而这些路由仅存在于前端,因此返回404。需要修改服务器配置,将前端路由请求转发到Vue入口文件。
1. 修改.htaccess添加重写规则
在现有配置末尾添加URL重写规则,排除后端API路由,将其他所有非静态资源请求转发到Vue的index.html:
# 添加Vue History模式重写规则 <IfModule mod_rewrite.c> RewriteEngine On # 排除后端API路由,确保API请求正常被处理 RewriteCond %{REQUEST_URI} !^/quiz/ RewriteCond %{REQUEST_URI} !^/admin/ RewriteCond %{REQUEST_URI} !^/manage/ # 排除真实存在的文件和目录(比如静态资源) RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # 将所有符合条件的请求转发到index.html RewriteRule ^ index.html [L] </IfModule>
2. 配置Express托管前端静态资源
在后端src/app.mjs中添加静态资源托管,指向Vue打包后的dist目录(需确保前端已执行npm run build并将dist文件部署到服务器对应路径):
// 引入path模块 import path from 'path'; // 在API路由定义之前添加静态资源托管 app.use(express.static(path.join(__dirname, '../dist')));
3. 确认路由优先级
确保后端API路由(/quiz、/admin、/manage)的定义在静态资源托管之后,这样API请求会优先被后端处理,其余请求则由Vue Router接管。
完成以上配置后,刷新或直接访问前端路由时,服务器会自动将请求转发到Vue入口文件,由前端路由系统处理,即可解决404问题。
内容的提问来源于stack exchange,提问作者Raphaël Dev
相关产品推荐
相关产品推荐

