diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php index 6a7be2c74..1d7b589dc 100755 --- a/app/Http/Controllers/IndexController.php +++ b/app/Http/Controllers/IndexController.php @@ -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]); diff --git a/cmd b/cmd index 85d535449..7432e8997 100755 --- a/cmd +++ b/cmd @@ -379,18 +379,6 @@ web_build() { if [ ! -d "./node_modules" ]; then npm install fi - if [ "$type" = "dev" ]; then - echo "" > ./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 "" > ./index.html - fi - env_set VSCODE_PROXY_URI "${VSCODE_PROXY_URI:-}" - fi switch_debug "$type" # if [ "$type" = "prod" ]; then diff --git a/docker-compose.yml b/docker-compose.yml index c735a6d0b..5a3084d12 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/vite.config.js b/vite.config.js index a89b2b2c3..06dc20225 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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: {