快速掌握Next.js:搭建你的第一个Next.js应用指南
架构介绍
Next.js是一个开源的JavaScript框架,由Vercel创建,用于增强React应用程序的功能,如 服务器端渲染和 静态网站生成.
传统上,React被用来创建 ***单页面应用程序(SPA)***其内容是在客户端渲染的。Next.js扩展了这一点,允许开发者创建可以执行服务器端操作的应用程序,预取路由,并支持TypeScript。最重要的是--它默认不需要任何额外的配置!
在本指南中,我们将了解Next.js的相关功能和工作原理。为了巩固新知识,我们将建立一个完整的多页面天气应用程序,与外部API对话,允许用户记录给定的状态。
**注意:**这个应用程序的完整代码可以在GitHub上找到。
安装和设置
创建一个新的Next.js应用程序的最简单方法是使用create-next-app
CLI工具。你可以通过npm
来安装它。
$ npm install create-next-app
安装后,你可以通过调用该工具并为你的项目提供一个名称来初始化一个新的Next.js应用程序。
$ npx create-next-app weather-app
**注意:**如果你还没有安装create-next-app
-npx
会提示你自动安装。
一旦工具完成了对一个骨架项目的初始化,让我们移到目录中,看一看里面的情况。
$ cd weather-app
$ ls
README.md node_modules/ package.json public/
next.config.js package-lock.json pages/ styles/
标准的package.json
、package-lock.json
和node_modules
,然而,我们还有/pages
、/public
和/styles
目录,以及一个next.config.js
文件!
让我们来看看这些是什么。
Next.js的特点
Next.js最终是React的一个扩展,它确实引入了一些新的东西,使React应用开发更加简单和快速--首先是Next.js页面。
页面
Next.js让使用React创建多页面应用程序变得非常容易,其默认的 基于文件系统的路由器.你不需要安装任何额外的包,如react-router-dom
,也不需要配置路由器。
所有Next.js项目都包括一个默认的/pages
目录,它是你将使用的所有React组件的家园。对于每个组件 - 路由器将根据该组件提供一个页面。
一个React组件在Next眼里就是一个页面,并自动在其文件名对应的路径上提供服务。
例如,假设我们在/pages
目录下创建了一个组件,contact.js
。
const Contact = () => {
return (
<div>
Contact
</div>
)
}
export default Contact
Next.js所采用的基于文件系统的路由器会使这个页面在/contact
路径下被访问!这个规则的唯一例外是index.js
,它不在/index
路由下,而是在/
。
此外,你可以用Next.js嵌套路由,所以你可以通过创建/weather
文件夹,和一个动态的[city].js
组件来充当页面,从而轻松地动态创建一个/weather/berlin
。
**注意:**对于动态路由,你需要用一组方括号来命名文件。没有方括号,它就是一个静态的字面字符串,它将被解析成这样。city.js
将解析到/weather/city
路由,而不会与其他东西匹配。另一方面,[city.js]
将与/weather/berlin
,/weather/london
,/weather/lagos
等匹配。
*<链接>*组件
<Link>
组件可以用来在你的应用程序中的页面之间进行导航。假设我们的项目页面结构在/pages
目录下有几个页面。
- pages
- index.js
- weather.js
- contact.js
<Link>
组件的href
属性可以用来指向每个页面的相对路径,从/pages
目录开始。
import Link from "next/link";
export default function Home() {
return (
<div>
<Link href="/">Home</Link>
<Link href="/weather">Weather</Link>
<Link href="/contact">Contact</Link>
</div>
)
}
当然,如果你有一个嵌套的层次结构的文件,你也可以链接到嵌套的页面。
- pages
- weather
- [city].js
import Link from "next/link";
export default function Weather() {
return (
<div>
<Link href="/weather/berlin">Berlin</Link>
<Link href="/weather/london">London</Link>
<Link href="/weather/lagos">Lagos</Link>
</div>
)
}
<Link>
组件还可以预取页面!一旦一个页面被加载,并且有多个指向其他页面的链接--如果你知道某个页面要经常被访问,或者想确保该页面尽快被加载(不影响初始页面),你可以预取与<Link>
相关的页面,以使过渡更快、更顺畅!
事实上,Next.js默认会预取所有页面。为此,如果你想加快过渡到某个页面的速度,你就把其他页面的
prefetch
属性设置为false
。
例如,可以想象,在一个天气应用程序中,人们更可能从主页导航到/weather
,而不是/about
。没有必要预取about.js
页面/组件,因为你会为一个不太可能被点击的页面进一步加重服务器的负担。另一方面--weather.js
最有可能是人们访问的下一个路线,所以你可以通过预取它来减少一些过渡时间。
import Link from "next/link";
export default function Home() {
return (
<div>
<Link prefetch=true href="/weather">Weather</Link>
<Link prefetch=false href="/about">About Us</Link>
</div>
)
}
其他一些属性包括scroll
属性(默认为true
),当用户用<Link>
重新定位时,它会将用户导航到页面的顶部。这是一个非常合理的默认值,尽管你可能想要关闭它,以达到你想要的更具体的效果。
另一个值得注意的属性是replace
,它的默认值是false
。如果设置为true
,当你用<Link>
,导航到一个新的页面/路径时,它会替换历史栈中的最新条目,而不是推送一个新条目。
预先渲染页面
说到预取和预渲染页面--Next.js的这一功能是比较贴切的。同样,在默认情况下,Next.js会预取你链接的所有页面,以便在它们之间进行平滑、快速的转换。
对于每个页面,你可以选择 服务器端渲染或 静态生成而使用哪种技术则取决于你用来获取数据的函数。你不会被强迫在整个应用程序中坚持使用这些技术中的一种
在SSR和SG之间的选择是一个关于你想把负载放在哪里的问题,这两种技术都是对页面进行预渲染,只是方式不同。
如果你在服务器端渲染你的页面,它们将在每个请求中被渲染,使用你的服务器资源,并发送给终端用户。如果你静态地生成一个页面,它就会被生成一次,并且在构建时间之后可以重复使用。
**注意:**一般来说,你想在没有必要使用 静态生成只要不需要使用 服务器端渲染因为页面可以被缓存和重用,从而节省宝贵的计算。每当页面上的组件频繁出现时,就需要进行服务器端渲染,在请求时用新数据(有些可能取决于请求本身)渲染页面。
你也可以决定让页面上的一些页面或元素通过以下方式呈现 客户端渲染渲染,这将负载放在最终用户的机器上,但你不能保证也不能控制他们的资源,所以通常情况下,你要避免在他们那里进行任何密集的计算。
预渲染在实践中意味着什么?
这对终端用户有什么影响,它是如何改善一个普通的React应用的?预渲染允许用户在加载任何JavaScript代码之前看到页面。JavaScript的加载需要很短的时间--但这几毫秒会不经意地影响我们的感知。尽管一旦所有组件被加载进来,页面就会以用户看到的样子显示出来--但它们都还没有工作。
只有当页面被显示出来后,这些组件才会被处理和加载,成为交互式组件。这个过程被称为 补水.
如果没有Next.js,当JavaScript加载时,页面将是空的,而组件正在被初始化。
由于预渲染是Next.js的一个组成部分,我们将看一下你可以用来促进预渲染的一些功能,包括通过SSR和SG。
获取服务器端数据 -getServerSideProps()
getServerSideProps()
函数被用来执行与服务器相关的操作,比如从外部API获取数据。同样,当页面上的数据快速变化时,你想执行SSR,而缓存它是没有意义的。例如,一个API可能会响应更新的股票价格,或每秒钟的时钟时间,以及用户的每次请求--这些都应该是最新的。
下面是一个例子,它向一个样本API发送请求,并将收到的数据作为一个道具传递给我们的页面组件。
const Weather = ({temperature}) => {
// display temperature
}
export default Weather
export async function getServerSideProps() {
const res = fetch('http://example.com/api')
...
const temperature = res.temperature
return {
props: {temperature},
}
}
getServerSideProps()
收到一个context
对象,其中包含与服务器相关的信息,如传入的请求、服务器响应、查询。这很关键,因为渲染本身可能取决于context
。
静态生成路径 -getStaticPaths()
我们使用getStaticPaths()
函数来定义一个动态路由应该静态生成的路径列表。假设我们有一个动态路由pages/weather/[city].js
,我们在这个文件中导出一个getStaticPaths()
函数,如下图所示。
export async function getStaticPaths() {
return {
paths: [{ params: { id: "paris" } }, { params: { id: "london" } }],
};
}
Next.js会在构建时自动为我们静态生成/weather/paris
和/weather/london
。
静态生成道具 -getStaticProps()
getStaticProps()
函数与getServerSideProps()
类似,我们用它来加载预渲染页面上的道具。然而,在这种情况下,道具是在构建时静态生成的,并在以后的所有请求中重复使用,而不是在请求时渲染。
export async function getStaticProps() {
const res = await fetch('http://someapi/toget/cities')
...
const cities = await res.json()
return {
props: {
cities,
},
}
}
注意: getStaticPaths()
不能与getServerSideProps()
一起使用--相反,请使用getStaticProps()
。最好只在需要预渲染的数据快速加载或可以公开缓存的情况下使用这个功能。
*<头部/>*与SEO
由于单页应用程序很难被搜索引擎抓取,因此为搜索引擎优化React应用程序可能是困难的。虽然Next.js的服务器端渲染解决了这个问题,但该框架还包括一个特殊的<Head />
组件,可以简单地将元素附加到页面的头部。
因此,更新你的应用程序页面的SEO设置,如标题标签、元描述以及你在标准HTML<head>
标签中包含的任何其他元素,都变得更加容易。
import Head from "next/head";
const Contact = () => {
return (
<div>
<Head>
<title>Contact</title>
<meta name="description" content="Welcome to our contact page!"/>
</Head>
</div>
);
};
export default Contact;
用Next.js创建API路由
Next.js还提供了一个功能,可以在你的项目中开发你自己的API,其过程与创建页面的过程相似首先,你需要在/pages
(即/pages/api
)下创建一个新的api
子目录,这个目录中的任何文件都将被路由到/api/*
。
比如,我们创建一个文件
/pages/api/weather.js
。一个端点可以立即被访问为/api/weather
。
为了让这些端点工作,你必须为每个端点导出一个默认的handler()
函数(请求处理程序),它接收两个参数:req
(传入请求),和res
(服务器响应)。
为了尝试这一点,让我们用以下内容更新我们的/pages/api/weather.js
例子。
export default function handler(req, res) {
res.status(200)
res.json({
city: "London",
temperature: "20",
description: "sunny",
});
}
如果我们访问或发送请求到/api/weather
,我们应该看到返回伦敦的假天气信息,以及一个200
响应代码。
静态资产
在某些时候,你可能想加载图片、视频、字体等资产。所有Next.js项目都有一个名为/public
的目录用于此目的。
/public
目录中的文件可以从你的应用程序中的任何地方访问,只要在源文件前面加上基本的URL (/
)即可。
例如,如果我们在/public/weather-icon.svg
下有一个文件,我们可以在任何组件中访问它。
const WeatherIcon = () => {
return <img src="/weather-icon.svg" alt="Weather Icon"/>
}
export default WeatherIcon
Next.js 环境变量
环境变量是在我们的应用程序之外设置的变量,我们主要用它们来保存敏感数据,如API密钥或服务器配置,以避免将它们推送到Github、GitLab等版本控制工具。
Next.js提供了对环境变量工作的支持,通过一个.env.local
文件。这个文件中的所有变量都被映射到process.env
。
如果我们有一个带有以下变量的.env.local
文件。
WEATHER_API_KEY=abcd123
CITY_API_KEY=123abc
我们现在可以通过process.env.WEATHER_API_KEY
和process.env.CITY_API_KEY
来访问它们。
另外,环境变量默认不在浏览器中暴露,只能在Node.js环境中访问(在服务器端)。然而,我们可以选择将它们暴露在客户端,方法是在首选变量前加上NEXT_PUBLIC_
。例如,如果我们有一个变量。
NEXT_PUBLIC_CITY_API_KEY=123abc
这个变量现在可以通过process.env.NEXT_PUBLIC_CITY_API_KEY
,在我们应用程序的任何地方访问。
用Next.js构建一个天气应用程序
我们已经讲了很多内容了!巩固新知识最好是通过实践,所以创建一个应用程序来补充新理论是很有帮助的。
我们将建立一个天气应用程序,检测用户的城市,并根据该信息显示天气信息。此外,我们将实现一个功能,允许用户在任何时候保存特定的天气信息,并在以后访问它。
该应用程序将看起来像这样。
如果你还没有,用下面的命令创建一个新的Next.js应用程序。
$ npx create-next-app weather-app
我们可以用以下命令启动我们的应用程序。
$ npm run dev
为了简洁起见,我们将使用Bootstrap来设置我们应用程序的界面,而不是编写自定义CSS。你可以用下面的命令安装Bootstrap。
$ npm install bootstrap
安装完成后,让我们打开pages/_app.js
,并为Bootstrap加入一个条目。
import "bootstrap/dist/css/bootstrap.css";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
注意: _app.js
文件是Next.js用来初始化页面的默认App组件。它是你所有页面组件的起点。
现在,我们可以通过改变默认字体和添加漂亮的背景颜色,使我们的应用程序在视觉上更吸引人。让我们打开styles/global.css
,并做如下修改。
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@100;200;300;400;500;800;900&display=swap');
body {
background: #4F32FF;
color: #fff;
font-family: 'Be Vietnam Pro', sans-serif;
}
这就足以让我们开始行动了!让我们来定义我们的页面结构和通过API获取数据时的占位符。
页面标记
对于我们的前端,让我们打开pages/index.js
,定义我们主页的标记(结构)。
import Link from "next/link";
export default function Home() {
return (
<div>
<div
className="d-flex justify-content-center align-items-center"
style={{ minHeight: "100vh" }}
>
<div>
<div>
<h1 className="fw-bolder" style={{ fontSize: "60px" }}>
Null City.
</h1>
13 January, 2022
</div>
<div className="d-flex justify-content-between align-items-center mt-4">
<div className="pe-5">
<h2 className="d-inline">0</h2>
<sup>°C</sup>
<p className="text-info">Cloudy</p>
</div>
<div>
<img src="/1.png" alt="" width={100} draggable="false" />
</div>
</div>
<hr />
<div className="d-md-flex justify-content-between align-items-center mt-4">
<button className="btn btn-success border-0 save-btn px-4 py-3">
Timestamp
</button>
<Link href="/history">
<button className="btn btn-danger border-0 history-btn px-4 py-3 ms-auto">
My History
</button>
</Link>
</div>
</div>
</div>
</div>
);
}
**注意:**你需要从我们的GitHub上下载天气图标,并将其纳入你的项目/public
。
而且,在这一点上,如果我们在浏览器中预览我们的应用程序,我们应该看到以下带有假数据的输出。
获取天气信息
我们将使用一个免费的天气API来获取用户当前的天气信息,但由于我们想显示用户当前所在城市的天气信息,我们需要使用另一个API来获取用户当前的城市,并将这个参数传递给天气API,以便获取所需的信息。
下面的图片描述了这个过程
为了获得天气信息,我们将利用 OpenWeather API,虽然他们提供了一个免费的计划,但你需要创建一个账户,以便获得API密钥。
为了检索用户的城市,我们将使用一个免费的 IP地理定位API它不需要API密钥来使用。
另外,我们要确保在页面加载后立即显示天气信息,所以Next.jsgetServerSideProps()
在这里确实派上了用场
现在,让我们在index.js
,以执行上述所有的功能。
export async function getServerSideProps() {
const ipRequest = await fetch(`http://ip-api.com/json/`);
const ipData = await ipRequest.json();
const city = ipData.regionName;
const api_key = "YOUR_OPEN-WEATHER_API_KEY";
const url = `http://api.openweathermap.org/data/2.5/weather?q=${city},&appid=${api_key}&units=metric`;
const weatherRequest = await fetch(url);
const weatherInfo = await weatherRequest.json();
console.log(weatherInfo);
return { props: { weatherInfo, city } };
}
上面的代码执行了两个异步操作。
- 第一个是检索用户的城市,我们将其存储在一个名为
city
的变量中。 - 第二个是向天气API发送一个请求。
最后,我们把从天气API返回的结果,以及城市作为一个道具传递给我们的索引页。
**注意:**你需要用你自己的OpenWeather API密钥替换YOUR_OPEN-WEATHER_API_KEY
。
所需的信息现在作为我们的索引页的道具存储在weatherInfo
和city
,我们可以通过它们来访问。
...
export default function Home({ weatherInfo, city }) {
...
}
如果你尝试将weatherInfo
到控制台,你会注意到有很多信息被返回,包括用户的坐标和其他一些对我们的应用不需要的信息。根据我们的应用设计,我们将只需要以下数据。
- 用户的城市
- 当前温度
- 天气描述(如阴天、小雨、下雪等)。
最后,一个基于当前温度的天气图标。当前温度在weatherInfo.main.temp
,而天气描述在weatherInfo.weather[0].description
。
所以,让我们继续用这些信息替换我们标记中的假数据。
{/* ... */}
<div>
<h1 className="fw-bolder" style={{fontsize: "60px"}}>
{city}
</h1>
13 January, 2022
</div>
<div className="d-flex justify-content-between align-items-center mt-4">
<div className="pe-5">
<h2 className="d-inline">
{Math.round(weatherInfo.main.temp)}</h2>
<sup>°C</sup>
<p className="text-info text-capitalize">
{weatherInfo.weather[0].description}
</p>
</div>
<div><img src='/1.png' alt="" width={100} draggable="false" /></div>
</div>
{/* ... */}
我们还可以使用OpenWeather API来获得一个取决于当前温度的天气图标,只需将图标名称作为一个参数传递,幸运的是这也可以在$weatherInfo.weather[0].icon
。
因此,让我们继续用下面的代码替换图标的<img>
标签。
{/* ... */}
<img
src={`http://openweathermap.org/img/wn/${weatherInfo.weather[0].icon}@2x.png`}
/>
{/* ... */}
我们的应用程序应该可以完全运行,根据我们目前所在的城市显示当前的天气信息。
本地保存数据
现在,让我们创建一个函数来保存当前的天气信息,以及保存在浏览器localStorage
的日期和时间。每个条目将被保存为一个对象,其结构如下。
{
date: 'Current Date',
time: 'Current Time',
city: “User's City”,
temperature: “User's city temperature”,
description: 'Weather Description',
};
要做到这一点,创建一个新的函数saveWeather()
(仍然在我们的index.js
文件内),代码如下。
const saveWeather = () => {
const date = new Date();
let data = {
date: `${date.getDate()} ${date.getMonth() + 1} ${date.getFullYear()}`,
time: date.toLocaleTimeString(),
city: city,
temperature: weatherInfo.main.temp,
description: weatherInfo.weather[0].description,
};
let previousData = localStorage.getItem("weatherHistory");
previousData = JSON.parse(previousData);
if (previousData === null) {
previousData = [];
}
previousData.push(data);
localStorage.setItem("weatherHistory", JSON.stringify(previousData));
alert("Weather saved successfully");
};
上面的代码将把以前存储在localStorage.weatherHistory
中的任何数据解析为JSON,根据返回的数据类型,我们将新条目推送到一个数组中,将这个数组转换为字符串,并在localStorage.weatherHistory
中恢复它。我们需要这样做,因为localStorage
只能存储字符串,而不是任何其他数据类型。
如果你想了解更多关于
localStorage
的信息--请阅读我们的《使用本地存储在浏览器中存储数据的指南》!
当然,我们想在用户点击时间戳按钮时调用这个函数,所以我们给按钮添加一个onClick
属性。
<button onClick={saveWeather}>Timestamp</button>
天气历史页面
最后,我们需要创建一个专门的页面来访问保存在我们浏览器的localStorage
中的所有天气信息。
**注意:**我们将不能使用Next.js的数据获取功能,因为localStorage
或任何其他文档对象在服务器上是不可用的,因此我们将不得不依赖客户端的数据获取。
在pages
目录下创建一个新的history.js
文件,内容如下。
import { useState, useEffect } from "react";
const History = ({}) => {
const [weatherHistory, setweatherHistory] = useState([]);
useEffect(() => {
setweatherHistory(
localStorage.weatherHistory !== undefined
? JSON.parse(localStorage.weatherHistory)
: []
);
}, []);
return (
<div
className="d-flex justify-content-center align-items-center p-3"
style={{ minHeight: "100vh" }}
>
<div>
{" "}
<h2>My Weather History</h2>
<div className="mt-5">
{weatherHistory.length > 0 ? (
weatherHistory.map((weather, index) => {
return (
<div
key={index}
className="card mb-3"
style={{ width: "450px" }}
>
<div className="card-body text-dark">
<h5 className="card-title ">
{weather.city} - {weather.date}
</h5>
<small>{weather.time}</small>
<hr />
<p className="card-text">
<span className="font-weight-bold">Temperature: </span>
{weather.temperature}
<sup>°C</sup>
</p>
<p className="card-text">
<span className="font-weight-bold">Condition: </span>
{weather.description}
</p>
</div>
</div>
);
})
) : (
<p>Nothing to see here - yet</p>
)}
</div>
</div>
</div>
);
};
export default History;
上面的代码检查localStorage.weatherHistory
是否存在,如果存在--我们解析数据并将其设置为一个新的变量weatherHistory
。如果它不存在,我们就把这个变量设置为一个空数组。
在我们的标记中,我们检查我们的weatherHistory
数组中是否至少有一个数据条目,并使用JavaScript的.map()
函数,我们遍历weatherHistory
中的所有项目,将它们显示在我们的网页上。
让我们继续点击索引页上的时间戳按钮来记录当前的天气信息,当你回到历史页面时,你应该看到这样的东西。
结论
Next.js是一个JavaScript框架,专门用于加强和促进高性能React应用程序的开发。
在本指南中,我们介绍了该库的相关功能--如何通过Next.js的文件路由系统创建和路由,<Link>
组件如何工作,什么是预取和预渲染以及如何利用它来增强用户体验,如何轻松创建API路由和请求处理程序以及如何使用环境变量。
最重要的是,我们建立了一个天气应用程序,与外部API进行通信,以获取数据并将其显示给最终用户,允许他们将任何给定的时间戳保存到他们的本地存储。
同样,该应用程序的完整源代码可以在GitHub上找到。