为什么 nextjs 项目, 访问不到 public 目录下的 html 文件?

发现问题

打算搞一个百度收录, 百度需要我在根目录放一个 baidu_verify_xxx.html 的文件, 用于验证。

我一想, 这不太简单了, 直接把 .html 文件扔到 public 目录下不就好了?

扔进去, 测试一下, 404... 当时就猜, 应该被 nextjs 拦截了, 一看源码, 果然...

瞄瞄源码

以下仅截取关键代码

class NextNodeServer extends BaseServer {
  /**
   * https://github.com/vercel/next.js/blob/canary/packages/next/src/server/next-server.ts#L642
   */
  protected async findPageComponents() {
    /**
     * https://github.com/vercel/next.js/blob/canary/packages/next/src/server/next-server.ts#L710
     */
    const components = await loadComponents({
      distDir: this.distDir,
      page: pagePath,
      isAppPath,
    })
  }
}

loadComponents 中:


function loadComponents() {
  /**
   * https://github.com/vercel/next.js/blob/canary/packages/next/src/server/load-components.ts#L135
   */
  const ComponentMod = await Promise.resolve().then(() =>
    requirePage(page, distDir, isAppPath)
  )
}

requirePage:

/**
 * https://github.com/vercel/next.js/blob/canary/packages/next/src/server/require.ts#L108
 */
function requirePage() {
  const pagePath = getPagePath(page, distDir, undefined, isAppPath)
  if (pagePath.endsWith('.html')) {
    return promises.readFile(pagePath, 'utf8').catch((err) => {
      throw new MissingStaticPage(page, err.message)
    })
  }
}

getPagePath:

/**
 * https://github.com/vercel/next.js/blob/canary/packages/next/src/server/require.ts#L93
 */
export function getPagePath(
  page: string,
  distDir: string,
  locales: string[] | undefined,
  isAppPath: boolean
): string {
  const pagePath = getMaybePagePath(page, distDir, locales, isAppPath)

  if (!pagePath) {
    throw new PageNotFoundError(page)
  }

  return pagePath
}

getMaybePagePath (此处完整截取了该函数代码):

/**
 * https://github.com/vercel/next.js/blob/canary/packages/next/src/server/require.ts#L25
 * 
 * 太直白了, 没什么好注释解释的了, 就是:
 * 1. 从缓存里取, 如果没取到:
 * 2. 构建的时候会生成 .next/server/app-paths-manifest.json 和 .next/server/pages-manifest.json, 看看这两个文件里面有没有这个路径, 并拼上 .next/server
 */
export function getMaybePagePath(
  page: string,
  distDir: string,
  locales: string[] | undefined,
  isAppPath: boolean
): string | null {
  const cacheKey = `${page}:${distDir}:${locales}:${isAppPath}`

  let pagePath = pagePathCache?.get(cacheKey)

  // If we have a cached path, we can return it directly.
  if (pagePath) return pagePath

  const serverBuildPath = path.join(distDir, SERVER_DIRECTORY)
  let appPathsManifest: undefined | PagesManifest

  if (isAppPath) {
    appPathsManifest = loadManifest(
      path.join(serverBuildPath, APP_PATHS_MANIFEST),
      !isDev
    ) as PagesManifest
  }
  const pagesManifest = loadManifest(
    path.join(serverBuildPath, PAGES_MANIFEST),
    !isDev
  ) as PagesManifest

  try {
    page = denormalizePagePath(normalizePagePath(page))
  } catch (err) {
    console.error(err)
    throw new PageNotFoundError(page)
  }

  const checkManifest = (manifest: PagesManifest) => {
    let curPath = manifest[page]

    if (!manifest[curPath] && locales) {
      const manifestNoLocales: typeof pagesManifest = {}

      for (const key of Object.keys(manifest)) {
        manifestNoLocales[normalizeLocalePath(key, locales).pathname] =
          pagesManifest[key]
      }
      curPath = manifestNoLocales[page]
    }
    return curPath
  }

  if (appPathsManifest) {
    pagePath = checkManifest(appPathsManifest)
  }

  if (!pagePath) {
    pagePath = checkManifest(pagesManifest)
  }

  if (!pagePath) {
    pagePathCache?.set(cacheKey, null)
    return null
  }

  pagePath = path.join(serverBuildPath, pagePath)

  pagePathCache?.set(cacheKey, pagePath)
  return pagePath
}

稍加分析

nextjs 对于 .html 后缀的访问, 直接去生成的 apppages 目录里面找, 找不到就直接返回 404 了, 根本不管你 public 目录里面有没有

解决方案

虽然可以从源码中添加一个 baidu_verify_xxx 文件, 但我感觉这样挺烦的, 干脆, 我们原来的在 nginx 中的做法不是把所有的东西都扔给 nextjs 处理嘛, 现在不了, nginx 先去 public 目录里面找一找, 有就直接返回, 没有再交给 nextjs

server {
+  try_files    /public$uri @nextjs;

-  location / {
+  location @nextjs {
    proxy_pass    http://nextjs_upstream;
  }
  # ...
}

p.s. 其实, 像这种情况, nginx 直接返回文本省事多了🙃 但我要是不用 try_files, 前面的源码不是白分析了吗 [狗头] 这样一搞, 什么 html 静态文件都能用了 [正色]

再 p.s. 其实, 不适合一股脑 try_files, 因为如果是图片, 交给 nextjs 处理, next/image 会做优化, 直接由 nginx 丢出去, 优化就无效了

题外话

配置好后, 页面仍然报 404, 哪里出了问题呢?

  1. 看看 nginx 是谁执行的

    • 执行 ps aux | grep '[n]ginx'
    • 哦, 原来是 www-data 用户
  2. 看看 www-data 用户能不能访问到相应的文件

    • 执行 sudo -u www-data stat /path-to-target-file
    • 也可以执行 sudo -u www-data namei /path-to-target-file, 这个命令能更清楚地看到是在哪一目录才 denied
    • md permission denied

哦哦, 查一下, 发现 nginx 需要 x 权限, 而我的项目放在 /home/xxx 目录下, 权限是 750...

教训就是, 项目别放到 /home 目录下!

warning

chatgpt 说:

对于普通的Web服务器,try_files 的影响通常是可以忽略不计的。但是,在高性能要求的环境中,需要谨慎使用,并进行性能测试以确保不会成为性能瓶颈。如果性能成为问题,可能需要考虑其他优化策略,如缓存、负载均衡等。

如非特别声明,本站作品均为原创,遵循【自由转载-保持署名-非商用-非衍生 创意共享 3.0 许可证】。

对于转载作品,如需二次转载,请遵循原作许可。