😶‍🌫️ Next.js 踩坑记(1)——路径体操

date
May 29, 2024
slug
path-middle
status
Published
tags
React
Next.js
Website
summary
如何愉快的将你的 Nextjs 项目部署上 Github Page?
type
Post

引言

前段时间入坑了 React 开发,学习了基本语法后就决定找个项目跟着写写,由于b站上相关的资源重复率实在是太高了,我实在不想再写一个苍穹外卖之类的东西,于是在 YouTube 上找到了该视频,该项目是一个作品集项目,我觉得作为我 React 的 startup 非常不错,而且这个网页的审美也很合我的品味。
Build and Deploy an Amazing Developer Portfolio with Next JS and Framer Motion
Learn how to build and deploy a modern developer portfolio with animations using Next.js. Perfect for showcasing your web development skills! ⭐ Hostinger - https://hostinger.com/mastery10 Use the code JAVASCRIPTMASTERY to save up to 91% on all yearly hosting plans. 🌟 Sentry - https://bit.ly/4abT6PG 🎨 Portfolio Figma Design - https://resource.jsmastery.pro/minimal-portfolio 📘 Portfolio Best Practices Guide - https://resource.jsmastery.pro/portfolio-best-practices 🤖 Special Discord Forum - https://resource.jsmastery.pro/portfolio-discord 🌟 Become a top 1% Next.js 14 developer: https://jsmastery.pro/next14 🚀 Skyrocket your career in 4 months: https://jsmastery.pro/masterclass 📚 Materials/References: GitHub Repository (give it a star ⭐): https://github.com/adrianhajdin/portfolio README (assets & code): https://github.com/adrianhajdin/portfolio/blob/main/README.md 💻 Join our Discord Community - https://discord.com/invite/n6EdbFJ 🐦 Follow us on Twitter: http://twitter.com/jsmasterypro 🖼️ Follow us on Instagram: http://instagram.com/javascriptmastery 💼 Business Inquiries: contact@jsmastery.pro Time Stamps 👇 00:00:00 — Intro 00:05:40 — Project Setup 00:14:48 — Hero Section 00:40:30 — Bento Grid 01:23:18 — Recent Projects 01:43:40 — Sentry 01:52:56 — Testimonials 02:06:00 — Work Experience 02:14:18 — My Approach Section 02:26:24 — Footer 02:35:13 — Fixing Bugs 02:39:19 — Deployment
Build and Deploy an Amazing Developer Portfolio with Next JS and Framer Motion
本着 up 敲一行我就敲一行的学习精神,两个半小时的项目也不难敲,最后项目上线的时候该博主使用 Vercel 进行部署,本着能省一点是一点的原则,我决定使用 Github Page 功能进行部署,主要原因如下
  1. 免费,真的良心
  1. 本来这东西就是静态的,没什么后端服务,我想着给他渲染成纯静态页面丢上去不就完了呗
于是,开搞!

困境

notion image
首先,我遇到了第一个问题,部署页面有两种方式,我到底该使用 Github Action 方式,还是 Deploy from a branch 方式?网上介绍的大多是第二种方式,这里笔者推荐使用第一种方式,我们可以看到这两种方式配置好之后的工作流是什么样的,孰优孰劣大家自有评判

Github Action 方式

以 main 分支为当前分支为例,只需要将当前 commit push 上去→触发 Github Action,直接完成 page 更新。

Deploy from a branch 方式

本地修改后,在本地进行编译→使用 gh-pages 将编译结果推送到 gh-pages 分支,完成更新,相比于前一种方式,除了修改代码,还需要手动编译,再推送,非常不优雅。

关于如何使用 Github Action 方式进行部署,选中该方式之后,github 会弹一个工作流文件出来,检测到当前项目为 Nextjs 项目后会自动推荐 Nextjs 的工作流,我们可以暂时先用这个工作流,后面再进行修改。
此时按理来说,只要你没有使用什么在服务端不可用的生命周期钩子,你的代码会顺利在 Github 的服务器上完成编译,并成为该项目的 Page,但事情并没有这么简单。

静态资源路径问题

当我第一次看到 Github Action 中的工作流中,编译成功的绿标出现后,我非常兴奋的打开了我的网页,它长这样:
这是我直接打开本地编译的结果,仅作演示
这是我直接打开本地编译的结果,仅作演示
坑爹呢这是 😵‍💫,此时打开开发者工具发现,除了当前页面的 html 外,不论是处于_next/static 路径下的静态资源(主要是js和css),还是处于public 路径下的静态资源(主要是图片),资源路径通通是错的,这个项目的路径应该是musherm.github.io/Portofolio/ ,但所有的资源都指向musherm.github.io/ ,换言之,我需要给所有的静态资源路径前面加上一个’Portofolio/’ ,查阅官方文档,我们可以发现,nextjs.config.mjs这一文件中,存在两个选项与路径有关,他们分别是:

basePath

该选项用于在一个子路径下部署项目,他主要在两种场景下生效:
  1. Links:当使用next/linknext/router 时,会自动添加basePath,也就是说<Link href="/about">About Page</Link> 会输出<a href="/docs/about">About Page</a>basePath=’/docs’
  1. Image:当使用next/image 时,需要手动添加basePath,说白了就是不管你引用图片用的是<img>还是<Image> ,你通通需要手动把这个路径写完整
💡
官方既然考虑到了图片资源会出现这个问题,为啥不能通过 basePath 自动给他加上呢,无语了。

assetPrefix

该选项仅针对_next/static 下的静态js css资源,不会影响public下的资源,看来这一选项可以帮助我们解决js和css找不到的问题!
修改我们的nextconfig 为这样:
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "export",
  basePath: "/Portofolio",
  assetPrefix: "/Portofolio/",
  images: {
    unoptimized: true,
  },
};

export default nextConfig;
好,至少我们的js和css确实找到了,但是事情还没有解决,我们的img标签下的所有图片,无一例外,还是指向了根目录,根据官方文档,你必须手动给他加上。
比如一个img标签,你修改之前它长这样:
<img
  src={icon}
  alt={icon}
  className="p-2"
/>
修改之后呢,它应该长这样:
<img
  src={
    process.env.NEXT_PUBLIC_ASSET_PREFIX + icon
  }
  alt={icon}
  className="p-2"
/>
同时修改你的nextconfig,提供一下NEXT_PUBLIC_ASSET_PREFIX 这个环境变量,当然,你也可以通过.env .env.local 来提供,但我不喜欢把配置文件分散在好几个文件上,尽量集中管理比较清晰:
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "export",
  basePath: "/Portofolio",
  assetPrefix: "/Portofolio/",
  images: {
    unoptimized: true,
  },
  env: {
    NEXT_PUBLIC_ASSET_PREFIX: "/Portofolio",
  },
};

export default nextConfig;
到这里,你在编译的代码如果直接打开,资源路径应该已经加上了你的相对路径名,如果直接 push 上去,Github Action 部署还是会不成功,因为服务器端并不会读取你在 nextconfig 里定义的环境变量,你还需要在工作流中提供这个环境变量,所幸这个操作其实也很简单:打开用于定义工作流的文件,在我的环境下是.github/workflows/nextjs.yml,并在编译的时候提供环境变量,比如这样
- name: Build with Next.js
  run: ${{ steps.detect-package-manager.outputs.runner }} next build
  # Add this
  env:
    NEXT_PUBLIC_ASSET_PREFIX: /Portofolio
添加的这一行,会在服务器端编译的过程中提供这个环境变量,从而保证编译后对于public文件夹下的静态资源访问正确,到这里,我们就已经完成了对于 Nextjs 项目的 Github Page 部署。

后谈

细心的小伙伴可能发现,写死 basePath assetPrefix NEXT_PUBLIC_ASSET_PREFIX这些变量,当你在本地dev环境开发时,你的首页会从localhost:3000变成localhost:3000/Portofolio ,如果你能接受,那其实也还行,但如果想“不行,我在本地开发的时候,我就要在localhost:3000直接访问 !”那你可以继续看下去(没错,说的就是我自己,我真的忍受不了每次从终端点开这个链接,还需要手动补全后面的路径)。答案是,你可以像NEXT_PUBLIC_ASSET_PREFIX 一样,再引入一个NODE_ENV 来判断当前的开发环境,从而在生产环境使用子路径,但在开发环境不实用,此时nextconfig 修改如下:
/** @type {import('next').NextConfig} */
const isProd = process.env.NODE_ENV === "production";
const nextConfig = {
  output: "export",
  basePath: isProd ? "/Portofolio" : "",
  assetPrefix: isProd ? "/Portofolio/" : "",
  images: {
    unoptimized: true,
  },
  env: {
    NEXT_PUBLIC_ASSET_PREFIX: isProd ? "/Portofolio" : "",
  },
};

export default nextConfig;
同时修改img标签的src如下
<img 
	src={
    (process.env.NEXT_PUBLIC_ASSET_PREFIX || "") + "/profile.svg"
  }
  alt="profile"
/>
你可能想问为什么?按理来说我在dev环境已经指定了NEXT_PUBLIC_ASSET_PREFIX为空啊?这就是Nextjs 的又一大坑,Nextjs 的环境变量无法指定为空!(至少据我所知)如果你像这样试图指定为空,它会变成undefined ,因而你需要通过(process.env.NEXT_PUBLIC_ASSET_PREFIX || "") 这样的表达式体操来让undefined 变成真正的空。
此外,你还需要修改 Github Action 的流程定义文件,从而指定编译环境为 production
- name: Build with Next.js
  run: ${{ steps.detect-package-manager.outputs.runner }} next build
  env:
    NODE_ENV: production
    NEXT_PUBLIC_ASSET_PREFIX: /Portofolio
于是,经过这么一番折腾,我们终于调好了环境!
  1. 在 Github Action 侧,我们通过 basePath assetPrefix NEXT_PUBLIC_ASSET_PREFIX 三兄弟,完成了对css js img 这些静态资源的路径补全;
  1. 在本地 dev 侧,我们将这三兄弟全部设为空,从而保持原有的根路径访问

后后谈

总结这篇博客查阅资料时,我查到了这个东西
由于你可以像这样定义image loader
export default function myImageLoader({ src, width, quality }) {
  return `https://example.com/${src}?w=${width}&q=${quality || 75}`
}
所以理论上,你也可以通过在imageLoader内部拼接src,从而补全路径,但笔者懒得写了,就先放到 todo List 吧。

成果展示:
 

© Musher 2019 - 2024

powered by nobelium