带有URL重写的代理后面的NextJS

时间:2020-08-01 04:02:24

标签: nginx next.js nginx-reverse-proxy

上下文:

我在Nginx后面有一个NextJS部署。想法是使用NextJS创建在不同域中托管的多个网站。每个域在Nginx中都有一个条目,并且将指向NextJS中的特定路径pages/cafes/[cafeId]。所有网站将只有一个NextJs部署,并且每个域都将使用nginx中的static代理进行路由。

nginx.conf

   server {
        listen 80;
        server_name www.cafe-one.local;
        location = / {
            proxy_pass http://localhost:3000/cafes/cafe_id_1;
            ...
        }
        location / {
            proxy_pass http://localhost:3000/;
            ...
        }
    }
    
    server {
        listen 80;
        server_name www.cafe-two.local;
        location = / {
            proxy_pass http://localhost:3000/cafes/cafe_id_2;
            ...
        }
        location / {
            proxy_pass http://localhost:3000/;
            ...
        }
    }

pages / [cafeId] /index.js

export const getStaticPaths = async () => {
  return {
    paths: [], // no website rendered during build time, there are around 1000+ sites
    fallback: true
  };
};

export const getStaticProps = async context => {

  const cafeId = context.params.cafeId;
  const cafe = await ... // get data from server

  return {
    props: {
      cafe
    },
    revalidate: 10 // revalidate every 10 seconds
  };
};

export default function CafeWebsite(props) {
  const router = useRouter();

  // getStaticProps() is not finished
  if (router.isFallback) {
    return <div>Loading...</div>;
  }
  
  return <div>{props.cafe.name}</div>;
}

问题:

当我访问www.cafe-one.local时,我会进入加载屏幕,但是NextJS抛出关于The provided as value (/) is incompatible with the href value (/cafes/[cafeId])客户端错误。这是可以理解的,因为当前URL不是NextJS期望的。

enter image description here

问题:

如何解决该问题,以便可以在Nginx反向代理之前使用NextJS?

感谢您的帮助。

谢谢。

2 个答案:

答案 0 :(得分:0)

我一直在处理相同的问题,只是要在NextJS应用程序中将不同的子域映射到动态路由。

我无法为The provided as value (/) is incompatible with the href value错误找到合适的解决方案,但是我发现了一个有点棘手的解决方法。

首先,您必须将请求从my-domain.com重定向到my-domain.com/path-to-dynamic-route。然后,您必须将来自my-domain.com/path-to-dynamic-route的所有请求反向代理到NextJS应用程序中的同一动态路由,例如localhost:3000/path-to-dynamic-route

您可以结合使用return 301proxy_pass从NGINX手动进行操作,也可以通过在proxy_pass指令中将动态路由传递给末尾来让NextJS自动执行操作斜线。

nginx.conf


server {
    listen 80;
    server_name www.cafe-one.local;
    location = / {
        # When a url to a route has a trailing slash, NextJS responds with a "308 Permanent redirect" to the path without the slash.
        # In this case from /cafes/cafe_id_1/ to /cafes/cafe_id_1
        proxy_pass http://localhost:3000/cafes/cafe_id_1/;
        # If you also want to be able to pass parameters in the query string, you should use the variable $request_uri instead of "/"
        # proxy_pass http://localhost:3000/cafes/cafe_id_1$request_uri;
        ...
    }
    location / {
        # Any other request to www.cafe-one.local will keep the original route and query string
        proxy_pass http://localhost:3000$request_uri;
        ...
    }
}

这应该可以,但是现在我们地址栏中的网址有问题。任何访问www.cafe-one.local的用户都将被重定向到www.cafe-one.local/cafes/cafe_id_1,这看起来不太好。

我发现解决此问题的唯一方法是使用JavaScript通过用window.history.replaceState()重写浏览历史记录来删除路径。

pages/[cafeId]/index.js

...
export default function CafeWebsite(props) {
  if (typeof window !== "undefined") {
    window.history.replaceState(null, "", "/")
  }
...

如果您不想删除所有域的路径,则可以使用window.location.hostname来检查当前网址。

...
export default function CafeWebsite(props) {
  if (typeof window !== "undefined") {
    const hostname = window.location.hostname
    const regex = /^(www\.my-domain\.|my-domain\.)/
    if (!regex.test(hostname)) {
      window.history.replaceState(null, "", "/")
    }
  }
...

答案 1 :(得分:0)

感谢 Ale 的 replaceState 想法,这就是我们现在处理它的方式:

  1. Nginx 将所有初始请求代理到我们的 vercel 部署,其中 /original.host.com/ 作为路径中的第一项:
server {
    listen 8080;

    # redirect HTTP to HTTPS
    if ($http_x_forwarded_proto = "http") {
        return 301 https://$host$request_uri;
    }

    # (not needed if you set assetPrefix in next.config.js to https://myapp.com)
    location /_next {
        proxy_pass https://myapp.com;
    }

    # a separate line for root is needed to bypass nginx messing up the request uri
    location = / {
        proxy_pass https://myapp.com/$host;
    }

    # this is the primary proxy
    location / {
        proxy_pass https://myapp.com/$host$request_uri;
    }
}
  1. _app.tsx 中,我们注册了一个在 Next.js 完成更改路由后运行的效果(这不会在第一次渲染时触发)。
useEffect(() => {
  const handleRouteChange = (url: string) => {
    const paths = url.split('/')

    if (paths[1] === location.host) {
      // remove /original.host.com/ from the path
      // note that passing history.state as the first argument makes back/forward buttons work correctly
      history.replaceState(history.state, '', `/${paths.slice(2).join('/')}`)
    }
  }

  router.events.on('routeChangeComplete', handleRouteChange)

  return () => {
    router.events.off('routeChangeComplete', handleRouteChange)
  }
}, [])
  1. 所有特定于域的页面都在 pages/[domain]/ 下,例如pages/[domain]/mypage.tsx

  2. 最后,我们在每个 href 前面加上原始主机名,例如

    • <Link href="/original.host.com/mypage">...</Link>
    • Router.push('/original.host.com/mypage')

    不再需要使用 as

  3. Next.js 现在将导航到 https://original.host.com/original.host.com/mypage 一瞬间,然后在转换完成后将其替换为 https://original.host.com/mypage

  4. 为了使 SSR 适用于每个特定于域的页面,我们在每个页面中添加了 getStaticPaths/getStaticProps,以便 Next.js 知道为每个页面生成单独版本的页面每个域(否则 router.query.domain 在 SSR 中将为空,我们会得到路径不匹配的错误)。请注意,这不会对性能产生负面影响,因为页面将在第一次请求后缓存。

// In pages/[domain]/mypage.tsx

export default function MyPage(params: MyPageProps) {
  const router = useRouter()
  // SSR provides domain as a param, client takes it from router.query
  const domain = params.domain || router.query.domain

  // ... rest of your page
}

export const getStaticPaths: GetStaticPaths = async () => ({
  fallback: 'blocking',
  paths: [],
})

export const getStaticProps: GetStaticProps<SpaceEditPageProps> = async ({
  params,
}) => {
  return {
    props: {
      domain: params?.domain as string,
    },
  }
}
  1. 额外提示:要在用户悬停链接时查看最终 URL,我们不使用 passHref,而是将假 URL 设置为 href,取消 onClickCapture 中的默认操作,并在 onClick 中触发路由器(您可以将其提取到可重用的组件中):
<Link href="/original.host.com/mypage">
  <a
    href="/mypage"
    onClick={() => Router.push("/original.host.com/mypage")}
    onClickCapture={e => e.preventDefault()}
  >
    // ...
  </a>
</Link>