You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

部署在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 07:18:12