refactor(dev): 开发模式改由 nginx 同源反代 vite,移除 VSCODE_PROXY_URI 拼接

- vite 开发 base 改为 /__vite/,nginx 通过 host.docker.internal 反代(含 HMR websocket),任意访问地址/代理下均可用
- vite 端口默认 5173,被占用自动顺延,实际端口由 devProxyPlugin 写入 nginx 片段并 reload(先 nginx -t 校验,失败不保留片段)
- HMR 客户端改连当前页面源;启动输出改为应用访问地址
- public/hot 只写 base/port,不再泄露整个 .env
- cmd dev 移除随机 APP_DEV_PORT、VSCODE_PROXY_URI 与 index.html 跳转;docker-compose nginx 增加 extra_hosts
This commit is contained in:
kuaifan 2026-10-21 17:52:41 +00:00
parent c64fb1d98f
commit aca8fc6b24
4 changed files with 99 additions and 50 deletions

View File

@ -63,11 +63,8 @@ class IndexController extends InvokeController
if (file_exists($hotFile)) {
$array = Base::json2array(file_get_contents($hotFile));
$style = null;
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
$proxyUri = Base::liveEnv('VSCODE_PROXY_URI');
if (is_string($proxyUri) && preg_match('/^https?:\/\//i', $proxyUri)) {
$script = preg_replace('/^(https?:\/\/|\/\/)[^\/]+/', rtrim($proxyUri, '/'), $script, 1);
}
// 开发模式:vite 经 nginx 反代在同源的 base 路径下(见 vite.config.js devProxyPlugin)
$script = asset_main(trim($array['base'] ?? '/__vite/', '/') . "/resources/assets/js/app.js");
} else {
$array = Base::json2array(file_get_contents($manifestFile));
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);

12
cmd
View File

@ -379,18 +379,6 @@ web_build() {
if [ ! -d "./node_modules" ]; then
npm install
fi
if [ "$type" = "dev" ]; then
echo "<script>window.location.href=window.location.href.replace(/:\d+/, ':' + $(env_get APP_PORT))</script>" > ./index.html
if [ -z "$(env_get APP_DEV_PORT)" ]; then
env_set APP_DEV_PORT $(rand 20001 30000)
fi
if [ -n "${VSCODE_PROXY_URI:-}" ]; then
APP_REAL_URI=$(TARGET_PORT="$(env_get APP_PORT)" node -p "process.env.VSCODE_PROXY_URI.replace(/\{\{port\}\}/g, process.env.TARGET_PORT || '')")
VSCODE_PROXY_URI=$(APP_DEV_PORT="$(env_get APP_DEV_PORT)" node -p "process.env.VSCODE_PROXY_URI.replace(/\{\{port\}\}/g, process.env.APP_DEV_PORT || '')")
echo "<script>window.location.href='${APP_REAL_URI}'</script>" > ./index.html
fi
env_set VSCODE_PROXY_URI "${VSCODE_PROXY_URI:-}"
fi
switch_debug "$type"
#
if [ "$type" = "prod" ]; then

View File

@ -45,6 +45,8 @@ services:
- "${APP_SSL_PORT:-0}:443"
environment:
APP_SCHEME: "${APP_SCHEME:-auto}"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/templates/default.conf.template
- ./:/var/www

128
vite.config.js vendored
View File

@ -18,28 +18,99 @@ if (!argv.includes('fromcmd')) {
process.exit()
}
/**
* 开发模式:由 nginx 把 /__vite/ 反代到本机 vite,页面与 vite 同源,任意访问地址/代理下均可用
* - 监听成功后写入 nginx 反代片段并 reload,同时写 public/hot 通知后端走开发资源
* - 退出时清理并 reload
*/
function devProxyPlugin(env, devBase) {
const hotFile = path.resolve(__dirname, 'public/hot')
const confFile = path.resolve(__dirname, 'docker/nginx/site/vite-dev.conf')
const nginxName = `dootask-nginx-${env['APP_ID']}`
const reloadNginx = () => {
try {
// nginx -s reload 在配置有误时也返回成功,先 -t 校验
execSync(`docker exec ${nginxName} sh -c "nginx -t -q && nginx -s reload"`, {stdio: 'pipe'})
return true
} catch (e) {
console.warn(`[dev-proxy] reload ${nginxName} failed: ${String(e.stderr || e.message).trim()}`)
return false
}
}
const cleanup = () => {
const exists = fs.existsSync(confFile)
fs.rmSync(hotFile, {force: true})
fs.rmSync(confFile, {force: true})
exists && reloadNginx()
}
return {
name: 'dootask-dev-proxy',
configureServer(server) {
// 浏览器经 nginx 访问应用,不直接访问 vite,替换 vite 默认打印的地址
server.printUrls = () => {
const {info, warn} = server.config.logger
if (!fs.existsSync(hotFile)) {
warn(` > Proxy unavailable`)
return
}
// 与 vite 默认输出一致:地址青色、端口加粗
const useColor = process.stdout.isTTY && !('NO_COLOR' in process.env)
const ansi = (open, close) => text => useColor ? `\x1b[${open}m${text}\x1b[${close}m` : text
const cyan = ansi(36, 39), bold = ansi(1, 22)
const appPort = env['APP_PORT']
info(` > Local: ${cyan(`http://localhost:${bold(appPort)}/`)}`)
const proxyUri = process.env['VSCODE_PROXY_URI']
if (proxyUri && proxyUri.includes('{{port}}')) {
info(` > Network: ${cyan(proxyUri.replace(/\{\{port\}\}/g, bold(appPort)))}`)
}
}
server.httpServer.once('listening', () => {
const {port} = server.httpServer.address()
fs.writeFileSync(confFile, [
`location ^~ ${devBase} {`,
` proxy_http_version 1.1;`,
` proxy_set_header Host $host;`,
` proxy_set_header Upgrade $http_upgrade;`,
` proxy_set_header Connection $connection_upgrade;`,
` proxy_read_timeout 1d;`,
` proxy_buffering off;`,
` proxy_pass http://host.docker.internal:${port};`,
`}`,
``,
].join('\n'))
if (reloadNginx()) {
fs.writeFileSync(hotFile, JSON.stringify({base: devBase, port}))
} else {
// 不保留无效片段,否则 nginx 容器重启时会因配置错误起不来
fs.rmSync(confFile, {force: true})
}
})
// 修改配置后 vite 会重新加载本文件并重启服务,退出钩子挂在 globalThis 上只注册一次
if (!globalThis.__dootaskDevProxyHooked) {
globalThis.__dootaskDevProxyHooked = true
process.on('exit', cleanup)
for (const signal of ['SIGINT', 'SIGHUP', 'SIGTERM']) {
process.on(signal, () => process.exit())
}
}
},
transform(code, id) {
// vite 2 的 HMR 客户端固定连 location.hostname:端口,改为连当前页面的源(经 nginx 转发)
if (id.includes('/vite/dist/client/client.mjs')) {
return code.replace(
/const socketHost = `\$\{__HMR_HOSTNAME__ \|\| location\.hostname\}:\$\{__HMR_PORT__\}`;/,
`const socketHost = location.host + ${JSON.stringify(devBase)};`
)
}
},
}
}
export default defineConfig(({command, mode}) => {
const env = loadEnv(mode, process.cwd(), '')
const host = "0.0.0.0"
const port = parseInt(env['APP_DEV_PORT'])
const proxy_uri = env['VSCODE_PROXY_URI']
if (command === 'serve') {
const hotFile = path.resolve(__dirname, 'public/hot')
const hotClean = (exit) => {
if (fs.existsSync(hotFile)) {
fs.unlinkSync(hotFile);
}
if (exit) {
process.exit()
}
}
hotClean(false)
fs.writeFileSync(hotFile, JSON.stringify(env));
process.on('exit', () => hotClean(true));
process.on('SIGINT', () => hotClean(true));
process.on('SIGHUP', () => hotClean(true));
}
const port = parseInt(env['APP_DEV_PORT']) || 5173
const devBase = '/__vite/'
const plugins = [
createVuePlugin({
@ -52,6 +123,9 @@ export default defineConfig(({command, mode}) => {
vitePluginRequire(),
vitePluginFileCopy([staticDir])
]
if (command === 'serve') {
plugins.push(devProxyPlugin(env, devBase))
}
if (mode === "development") {
plugins.push({
name: 'watch-copy',
@ -81,24 +155,13 @@ export default defineConfig(({command, mode}) => {
})
}
const serverHmr = {}
if (/^https?:\/\//i.test(proxy_uri)) {
const proxyUri = new URL(proxy_uri)
if (proxyUri) {
Object.assign(serverHmr, {
host: proxyUri.host,
clientPort: proxyUri.port || (/^https/.test(proxy_uri) ? 443 : 80)
})
}
}
return {
base: basePath,
base: command === 'serve' ? devBase : basePath,
publicDir: publicPath,
server: {
host,
port,
strictPort: false,
strictPort: false, // 端口被占用时自动顺延,实际端口由 devProxyPlugin 同步给 nginx
watch: {
ignored: [
'**/node_modules/**',
@ -111,7 +174,6 @@ export default defineConfig(({command, mode}) => {
'**/electron/**',
]
},
hmr: serverHmr
},
resolve: {
alias: {