Azure静态Web Apps全栈应用部署构建问题求助
问题:Azure静态Web Apps部署后后端无法启动
我为客户开发了一款目录网站,技术栈如下:
- 前端:Vue.js + Vite
- 后端:Express/Node.js
- 数据库:Azure SQL
本地运行完全正常,但通过GitHub Actions自动部署到Azure静态Web Apps后,前端加载正常,后端却无法启动,没有任何服务器/API日志,请求所有端点都返回连接拒绝。
已尝试的解决方案:
- 修改
api_start_command用Node启动服务器,但服务器启动后无法关闭,导致部署停滞 - 尝试通过Vite启动,发现构建后服务器/API文件缺失
- 查阅微软官方文档调整构建配置,也试过Azure Web App,结果一致
目前怀疑是Vite配置或架构设置问题,想请教:是否需要单独部署后端?是否需要独立仓库?
以下是相关代码:
Vite.config.js
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import copy from 'rollup-plugin-copy'; export default defineConfig({ plugins: [ vue(), ], build: { rollupOptions: { plugins: [ copy({ targets: [ { src: 'server.js', dest: 'dist' }, { src: 'api.js', dest: 'dist' }, ], hook: 'writeBundle' }), ] } }, resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
GitHub Actions 工作流配置
name: Deploy web app to Azure Static Web Apps env: APP_LOCATION: "/evo" # location of your client code API_LOCATION: "/evo" # location of your api source code - optional OUTPUT_LOCATION: "dist" # location of client code build output DB_USER: ${{ secrets.DB_USER }} DB_PASS: ${{ secrets.DB_PASS }} on: push: branches: - main pull_request: types: [opened, synchronize, reopened, closed] branches: - main permissions: issues: write contents: read pull-requests: write jobs: build_and_deploy: if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') runs-on: ubuntu-latest name: Build and Deploy steps: - uses: actions/checkout@v4 with: submodules: true - name: Build And Deploy uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_PEBBLE_0E2297810 }} repo_token: ${{ secrets.GITHUB_TOKEN }} action: "upload" app_location: ${{ env.APP_LOCATION }} api_location: ${{ env.API_LOCATION }} output_location: ${{ env.OUTPUT_LOCATION }} close_pull_request: if: github.event_name == 'pull_request' && github.event.action == 'closed' runs-on: ubuntu-latest name: Close Pull Request steps: - name: Close Pull Request uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_PEBBLE_0E2297810 }} action: "close"
api.js
const express = require('express'); const app = express(); const cors = require('cors'); var sql = require('mssql'); // MS Sql Server client const port = process.env.PORT || 1433; const pool = require('./server'); // Import the database connection pool app.use(express.json()); // Enable CORS for all routes app.use(cors()); // Get All Categories app.get('/api/data/categories', async (req, res) => { try { const request = new sql.Request(pool); const result = await request.query('SELECT * FROM categories2'); res.json(result.recordset); } catch (err) { console.error('Error executing SQL query:', err); res.status(500).json({ error: 'Internal Server Error' }); } }); // Get Parent Categories app.get('/api/data/categories/parents', async (req, res) => { try { const request = new sql.Request(pool); const result = await request.query('SELECT * FROM categories2 WHERE parentCategory = 1'); res.json(result.recordset); } catch (err) { console.error('Error executing SQL query:', err); res.status(500).json({ error: 'Internal Server Error' }); } }); // Get Subcategories by Category app.get('/api/data/categories/:parent', async (req, res) => { const { parent } = req.params; // Retrieve the parent parameter from the URL try { const request = new sql.Request(pool); const query = `SELECT * FROM categories2 WHERE parentId = @parent`; // Use a parameterized query to filter by parent request.input('parent', sql.NVarChar, parent); const result = await request.query(query); res.json(result.recordset); } catch (err) { console.error('Error executing SQL query:', err); res.status(500).json({ error: 'Internal Server Error' }); } }); // Get All Products app.get(`/api/data/products`, async (req, res) => { try { const request = new sql.Request(pool); const result = await request.query('SELECT * FROM products'); res.json(result.recordset); } catch (err) { console.error('Error executing SQL query:', err); res.status(500).json({ error: 'Internal Server Error' }); } }); // Get Products by Category app.get('/api/data/products/:category', async (req, res) => { const { category } = req.params; // Retrieve the category parameter from the URL try { const request = new sql.Request(pool); const query = `SELECT * FROM products WHERE Category_ID = @category`; // Use a parameterized query to filter by category request.input('category', sql.NVarChar, category); const result = await request.query(query); res.json(result.recordset); } catch (err) { console.error('Error executing SQL query:', err); res.status(500).json({ error: 'Internal Server Error' }); } }); // Get Product by Part Number app.get('/api/data/product/:partNum', async (req, res) => { const { partNum } = req.params; // Retrieve the category parameter from the URL try { const request = new sql.Request(pool); console.log(request); const query = `SELECT * FROM products WHERE Part_Number = @partNum`; // Use a parameterized query to filter by category console.log(query); request.input('partNum', sql.NVarChar, partNum); const result = await request.query(query); if (result.recordset.length > 0) { res.json(result.recordset); // Return the results if any are found } else { res.status(404).json({ error: 'Product not found' }); // Return 404 if no product is found } } catch (err) { console.error('Error executing SQL query:', err); res.status(500).json({ error: 'Internal Server Error' }); } }); app.listen(port, () => { console.log(`Server is running on port ${port}`); });
server.js
var express = require('express'); // Web Framework var app = express(); var sql = require('mssql'); // MS Sql Server client var test = require('dotenv').config(); var config = { user: process.env.DB_USER, password: process.env.DB_PASS, server: process.env.SERVER_NAME, database: 'EvoDB' } // Create a pool of database connections const pool = new sql.ConnectionPool(config); // Connect to the database pool.connect() .then(() => console.log('Connected to SQL Server')) .catch((err) => console.error('Database connection error:', err)); module.exports = pool;
解决方案
1. 核心问题:Azure静态Web Apps的API部署规范
Azure静态Web Apps要求后端API必须放在单独的api目录下,且目录内需要包含package.json定义依赖和启动脚本,不能和前端代码混在同一目录。你的配置中API_LOCATION: "/evo"指向了前端目录,导致部署流程无法正确识别和构建后端代码。
2. 具体修复步骤
步骤1:调整项目结构
把后端代码(api.js、server.js)移动到项目根目录下的api文件夹中,并在该目录创建package.json:
项目根目录 ├── evo/ # 前端Vue代码 │ ├── src/ │ ├── vite.config.js │ └── package.json └── api/ # 后端Express代码 ├── api.js ├── server.js └── package.json
步骤2:配置后端package.json
在api/package.json中定义启动脚本和依赖:
{ "name": "catalog-api", "version": "1.0.0", "main": "api.js", "scripts": { "start": "node api.js" }, "dependencies": { "express": "^4.18.2", "cors": "^2.8.5", "mssql": "^10.0.2", "dotenv": "^16.3.1" } }
步骤3:修改GitHub Actions配置
更新环境变量中的API_LOCATION为后端目录:
env: APP_LOCATION: "/evo" API_LOCATION: "/api" # 修改为后端目录 OUTPUT_LOCATION: "dist"
步骤4:移除Vite中的copy插件
Azure静态Web Apps会单独处理API目录的部署,无需将后端文件复制到前端dist目录,删除Vite.config中的rollup-plugin-copy相关配置:
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
步骤5:配置后端环境变量
后端的环境变量需要在Azure门户的配置 > 应用程序设置中添加,而非GitHub Actions:
登录Azure门户,找到你的静态Web Apps资源,添加以下应用设置:
DB_USER:Azure SQL用户名DB_PASS:Azure SQL密码SERVER_NAME:Azure SQL服务器名称(格式:xxx.database.windows.net)
3. 是否需要单独部署后端/独立仓库?
- 不需要独立仓库:Azure静态Web Apps支持单仓库中前端+后端的部署,只要目录结构符合规范即可。
- 若后续后端复杂度提升,或需要独立的扩缩容、部署周期,可以考虑单独部署到Azure App Service,此时需要独立仓库或单独的部署流程。
内容的提问来源于stack exchange,提问作者user13618009
相关产品推荐
相关产品推荐

