不折腾的前端,和咸鱼有什么区别
目录 |
---|
一 目录 |
二 前言 |
三 设置 |
四 多页面 |
五 链接 |
六 样式 |
七 共享组件 |
八 布局组件 |
九 实战 |
9.1 目录结构 |
9.2 UI 组件 |
9.3 Markdown 内容 |
9.4 Pages 入口和 API |
9.4.1 服务端渲染 |
9.5 Public 静态资源 |
9.6 resoruces |
十 参考文献 |
返回目录
使用 Next.js 好处:
返回目录
Next.js 可以在 Windows、Mac 和 Linux 运行,只需要在系统中安装 Node.js 即可开始构建 Next.js 应用程序。
那么开始创建项目:
mkdir next
cd next
npm init -y
npm i react react-dom next
此处标记下版本:
"dependencies": {
"next": "^10.0.4",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
pages
文件夹:mkdir pages
此时项目文件夹如下所示:
+ .next
+ node_modules
+ pages
- index.js
- package-lock.json
- package.json
首先,修改 package.json
:
package.json
{
"name": "next",
"version": "1.0.0",
"description": "next",
"main": "index.js",
+ "scripts": {
+ "dev": "next"
+ },
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"next": "^10.0.4",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}
然后,往 pages/index.js
添加内容:
pages/index.js
const Index = () => (
<div>
<h1>Hello next</h1>
</div>
);
export default Index;
最后,终端执行 npm run dev
,就可以看到页面显示:Hello next。
返回目录
当然,一个页面太孤独了,给它添加个小伙伴 pages/about.js
:
pages/about.js
const About = () => (
<div>
<h1>关于 next</h1>
</div>
);
export default About;
这样访问 about.js
就可以看到 关于 next 了。
返回目录
修改下 pages/index.js
,添加一个站内跳转:
pages/index.js
import Link from 'next/link';
const Index = () => (
Hello next
+
+ About Page
+
);
export default Index;
这样在 http://localhost:3000/
我们就可以看到 About Page
并点击跳转过去了。
Next.js 处理了
location.history
相关的内容,所以不需要再处理路由了
返回目录
下面再处理下 pages/index.js
,将链接换成 next 喜欢的深空蓝:
pages/index.js
import Link from 'next/link';
const Index = () => (
<div>
<h1>Hello next</h1>
<Link href='/about'>
<a style={{ textDecoration: 'none', color: 'deepskyblue' }}>About Page</a>
</Link>
</div>
);
export default Index;
注意:你并不能在
上添加样式,因为它是一个高阶组件,只能接收
href
属性
返回目录
新建一个好看的页头组件:
components/Header.js
import Link from 'next/link';
const linkStyle = {
marginRight: 15,
color: 'deepskyblue'
};
const Header = () => (
<div>
<Link href='/'>
<a style={linkStyle}>Home</a>
</Link>
<Link href='/about'>
<a style={linkStyle}>About</a>
</Link>
</div>
);
export default Header;
这个组件将我们 pages/about.js
和 pages/index.js
关联了起来,要怎么使用呢?
pages/index.js
import Header from '../components/Header';
const Index = () => (
<div>
<Header />
<h1>Hello next</h1>
</div>
);
export default Index;
pages/about.js
import Header from '../components/Header';
const About = () => (
<div>
<Header />
<h1>关于 next</h1>
</div>
);
export default About;
so easy~
这样就搞成公共的了。
注意:
pages
目录是特殊的,Next.js
会读取pages
作为入口,但是components
不是特殊的,你可以将你的组件文件夹命名成comps
也是可行的
返回目录
既然前面已经做到共享了,那么为何不丰富一下,将其作为一个布局组件。
将 Header
作为布局组件的一部分,新建 components/Layout.js
:
components/Layout.js
import Header from './Header';
const layoutStyle = {
margin: 20,
padding: 20,
border: '1px solid #DDD',
};
const Layout = (props) => (
<div style={layoutStyle}>
<Header />
{props.children}
</div>
);
export default Layout;
这样就可以在 index.js
和 about.js
中使用了。
pages/index.js
- import Header from '../components/Header';
+ import Layout from '../components/Layout';
const Index = () => (
-
-
- Hello next
-
+
+ Hello next
+
);
export default Index;
pages/about.js
- import Header from '../components/Header';
+ import Layout from '../components/Layout';
const About = () => (
-
-
- 关于 next
-
+
+ 关于 next
+
);
export default About;
在这里 {props.children}
是创建布局组件的一种手段。
创建布局组件还有其他方式:
方式一
import withLayout from '../lib/layout';
const Page = () => (
<p>Hello next</p>
);
export default withLayout(Page);
方式二
const Page = () => (
<p>Hello next</p>
)
export default () => (<Layout page={Page}/>)
方式三
const content = (<p>Hello next</p>);
export default () => (<Layout content={content}/>);
返回目录
返回目录
+ components ———————————————————— UI 组件
- Layout.jsx —— 布局
- Nav.jsx —— 导航
- Page.jsx —— 内容
+ markdown ———————————————————— Markdown 存放位置
- Next.md
- react-markdown.md
- README.md
+ pages ———————————————————— Next 规定页面目录
+ api ———————————————————— 本地 API 存放地址
- getCatelog.js —— API:获取目录
- getContent.js —— API:获取内容
+ pages ———————————————————— 子页面加载
- [...args].js —— [...args].js 是 Next.js 规则,可以解构成 A.js、B.js 等
- index.js ———————————————————— 入口
+ public ———————————————————— 静态资源
+ css ———————————————————— CSS
- global.css
+ img ———————————————————— 图片
- monkey.jpg
+ resoruces ———————————————————— 工具
+ react-syntax-highlighter —— 魔改 Code 样式
- dracula.js —— 使用这种样式
- index.js —— 工具入口
返回目录
Layout.jsx
import Head from 'next/head'; // 设置页面头部信息
import Nav from './Nav';
import Content from './Content';
const Layout = (props) => (
<>
{/* 设置头部信息 */}
<Head>
<link href='./css/global.css' rel='stylesheet'/>
<title>文档库</title>
</Head>
{/* 设置页面导航 */}
<Nav {...props} />
{/* 设置页面内容 */}
<Content {...props} />
</>
);
export default Layout;
这里存放了 UI 组件,然后希望传递的数据是这样的:
props = {
catelog: [{
id,
url,
title
}], // 目录信息
content: '', // 文本信息
}
所以 Nav.jsx
和 Content.jsx
如下:
Nav.jsx
import Link from 'next/link';
const Nav = (props) => {
return (
<nav>
<ul>
{
props.catelog && props.catelog.map((item) => (
<li key={item.id}><Link href={item.url}>{item.title}</Link></li>
))
}
</ul>
</nav>
);
};
export default Nav;
Content.jsx
import React from 'react'; // 导入 React
import ReactMarkdownWithHtml from 'react-markdown/with-html'; // 支持 HTML 代码
import gfm from 'remark-gfm'; // 处理删除线、表格、任务清单和 URL
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; // 设置代码高亮
import { dracula } from '../resoruces'; // 魔改样式
// 设置代码高亮
const renderers = {
code: ({ language, value }) => {
return (
<SyntaxHighlighter
style={dracula}
language={language}
children={value}
/>
);
},
};
const Page = (props) => (
<div className="content">
<ReactMarkdownWithHtml
allowDangerousHtml
renderers={renderers}
children={props.content}
plugins={[gfm]}
/>
</div>
);
export default Page;
返回目录
markdown
这个目录存放了需要展示的 Markdown
文件,这里就不贴内容了,随意放点都 OK。
返回目录
pages
这个目录是 Next.js
指定的,用来存放入口和 API 等内容的。
+ pages ———————————————————— Next 规定页面目录
+ api ———————————————————— 本地 API 存放地址
- getCatelog.js —— API:获取目录
- getContent.js —— API:获取内容
+ pages ———————————————————— 子页面加载
- [...args].js —— [...args].js 是 Next.js 规则,可以解构成 A.js、B.js 等
- index.js ———————————————————— 入口
首先,看主入口 index.js
:
pages/index.js
import Layout from '../components/Layout';
// Next 钩子
export async function getStaticProps() {
// 调用 3000 接口,这个请求会被 pages/api/xxx 获取到
const [catelogInfo, contentInfo] = await Promise.all([
fetch(`http://localhost:3000/api/getCatelog`),
fetch(`http://localhost:3000/api/getContent?args=/`),
]);
// 获取 200 响应
if (catelogInfo.status === 200 && contentInfo.status === 200) {
const [catelogData, contentData] = await Promise.all([
catelogInfo.json(),
contentInfo.json(),
]);
// 拿到接口数据并返回
return {
props: {
catelog: catelogData.catelog,
content: contentData.content,
},
}
};
};
const Index = (props) => (
<Layout {...props} />
);
export default Index;
在这里需要注意的是 getStaticProps
,如果是服务端渲染应该是 getInitialProps
,但是我这里希望直接将所有 Markdown
打包成 HTML
,所以就不需要 SSR 配置了。
但是为了避免有的小伙伴需要,后面会加个小节来演示。
然后,当用户通过 请求
pages/A.js
等页面的时候,就会进入 pages/[...args].js
文件:
pages/[…args].js
import Layout from '../../components/Layout';
const Index = (props) => (
<Layout {...props} />
);
// Next 钩子:预构建-获取目录
export async function getStaticPaths() {
// 调用 3000 接口,这个请求会被 pages/api/xxx 获取到
const [catelogInfo] = await Promise.all([
fetch(`http://localhost:3000/api/getCatelog`),
]);
// 获取 200 响应
if (catelogInfo.status === 200) {
const [catelogData] = await Promise.all([
catelogInfo.json(),
]);
// 拿到接口数据并返回
return {
paths: catelogData.catelog.map((item) => item.url),
fallback: false, // 如果为 false,其他路由为 404,否则不会 404
}
};
};
// Next 钩子
export async function getStaticProps({ params }) {
// 获取链接路径
const path = params.args[0];
// 调用 3000 接口,这个请求会被 pages/api/xxx 获取到
const [catelogInfo, contentInfo] = await Promise.all([
fetch(`http://localhost:3000/api/getCatelog`),
fetch(`http://localhost:3000/api/getContent?args=/${path}`),
]);
// 获取 200 响应
if (catelogInfo.status === 200 && contentInfo.status === 200) {
const [catelogData, contentData] = await Promise.all([
catelogInfo.json(),
contentInfo.json(),
]);
// 拿到接口数据并返回
return {
props: {
catelog: catelogData.catelog,
content: contentData.content,
},
}
};
};
export default Index;
最后,不管是 pages/index.js
和 pages/[...args].js
都会调用 api/getCatelog
和 api/getContent
,所以这两个直接写读取文件:
pages/api/getCatelog.js
const fs = require('fs');
const path = require('path');
const BASE_PATH = path.join(process.cwd());
// 接口
export default async (req, res) => {
// GET 操作
if (req.method === 'GET') {
// 读取文件夹并返回目录
const files = fs.readdirSync(`${BASE_PATH}/markdown`);
return res.json({
catelog: files.map((item, index) => {
const title = item.split('.')[0];
return {
id: index,
title,
url: `/pages/${title}`
};
})
});
}
}
pages/api/getContent.js
const fs = require('fs');
const path = require('path');
const BASE_PATH = path.join(process.cwd());
// 接口
export default async (req, res) => {
// GET 操作
if (req.method === 'GET') {
// 如果是 / 根路径,那么重定向为 /README
if (req.query.args === '/') {
req.query.args = '/README';
}
// 读取文件并返回数据
const data = fs.readFileSync(`${BASE_PATH}/markdown${req.query.args}.md`, 'utf-8');
return res.json({ content: data });
}
}
到此我们页面基本能跑起来了。
返回目录
+ pages ———————————————————— Next 规定页面目录
+ api ———————————————————— 本地 API 存放地址
- getCatelog.js —— API:获取目录
- getContent.js —— API:获取内容
- [...args].js ———————————————— [...args].js 是 Next.js 规则,可以解构成 A.js、B.js 等
- index.js ———————————————————— 入口
这里注意下 [...args]
直接放在一级目录即可,当然也可以放到 pages
里面。
index.js
import Index from './[...args]';
export default Index;
[…args].js
import Layout from '../components/Layout';
const Index = (props) => (
<Layout {...props} />
);
// Next 钩子:调用接口
Index.getInitialProps = async (ctx) => {
// 获取链接路径
const path = ctx.asPath;
// 调用 3000 接口,这个请求会被 pages/api.js 获取到
const [catelogInfo, contentInfo] = await Promise.all([
fetch(`http://localhost:3000/api/getCatelog`),
fetch(`http://localhost:3000/api/getContent?args=${path}`),
]);
// 获取 200 响应
if (catelogInfo.status === 200 && contentInfo.status === 200) {
const [catelogData, contentData] = await Promise.all([
catelogInfo.json(),
contentInfo.json(),
]);
// 拿到接口数据并返回
return {
catelog: catelogData.catelog,
content: contentData.content,
}
};
};
export default Index;
返回目录
public/css/global.css
blockquote {
padding: 0 15px;
color: #777;
border-left: 4px solid #ddd;
}
img/monkey.jpg
随便找个猴子图片
返回目录
resoruces/index.js
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "dracula", {
enumerable: true,
get: function get() {
return _dracula.default;
}
});
var _dracula = _interopRequireDefault(require("./react-syntax-highlighter/dracula"));
resoruces/react-syntax-highlighter/dracula.js
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = {
"code[class*=\"language-\"]": {
"color": "#f8f8f2",
"background": "none",
"textShadow": "0 1px rgba(0, 0, 0, 0.3)",
"fontFamily": "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace",
"textAlign": "left",
"whiteSpace": "pre",
"wordSpacing": "normal",
"wordBreak": "normal",
"wordWrap": "normal",
"lineHeight": "1.5",
"MozTabSize": "4",
"OTabSize": "4",
"tabSize": "4",
"WebkitHyphens": "none",
"MozHyphens": "none",
"msHyphens": "none",
"hyphens": "none"
},
"pre[class*=\"language-\"]": {
"color": "#f8f8f2",
"background": "#282a36",
"textShadow": "0 1px rgba(0, 0, 0, 0.3)",
"fontFamily": "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace",
"textAlign": "left",
"whiteSpace": "pre",
"wordSpacing": "normal",
"wordBreak": "normal",
"wordWrap": "normal",
"lineHeight": "1.5",
"MozTabSize": "4",
"OTabSize": "4",
"tabSize": "4",
"WebkitHyphens": "none",
"MozHyphens": "none",
"msHyphens": "none",
"hyphens": "none",
"padding": "1em",
"margin": ".5em 0",
"overflow": "auto",
"borderRadius": "0.3em"
},
":not(pre) > code[class*=\"language-\"]": {
"background": "#282a36",
"padding": ".1em",
"borderRadius": ".3em",
"whiteSpace": "normal"
},
"comment": {
"color": "#6272a4"
},
"prolog": {
"color": "#6272a4"
},
"doctype": {
"color": "#6272a4"
},
"cdata": {
"color": "#6272a4"
},
"punctuation": {
"color": "#f8f8f2"
},
".namespace": {
"Opacity": ".7"
},
"property": {
"color": "#ff79c6"
},
"tag": {
"color": "#00bfff"
},
"constant": {
"color": "#ff79c6"
},
"symbol": {
"color": "#ff79c6"
},
"deleted": {
"color": "#f50000"
},
"boolean": {
"color": "#bd93f9"
},
"number": {
"color": "#bd93f9"
},
"selector": {
"color": "#50fa7b"
},
"attr-name": {
"color": "#50fa7b"
},
"string": {
"color": "#50fa7b"
},
"char": {
"color": "#50fa7b"
},
"builtin": {
"color": "#50fa7b"
},
"inserted": {
"color": "#50fa7b"
},
"operator": {
"color": "#f8f8f2"
},
"entity": {
"color": "#f8f8f2",
"cursor": "help"
},
"url": {
"color": "#f8f8f2"
},
".language-css .token.string": {
"color": "#f8f8f2"
},
".style .token.string": {
"color": "#f8f8f2"
},
"variable": {
"color": "#f8f8f2"
},
"atrule": {
"color": "#f1fa8c"
},
"attr-value": {
"color": "#f1fa8c"
},
"function": {
"color": "#f1fa8c"
},
"class-name": {
"color": "#f1fa8c"
},
"keyword": {
"color": "#8be9fd"
},
"regex": {
"color": "#ffb86c"
},
"important": {
"color": "#ffb86c",
"fontWeight": "bold"
},
"bold": {
"fontWeight": "bold"
},
"italic": {
"fontStyle": "italic"
}
};
exports.default = _default;
SSR 即服务端渲染。
服务器呈现响应于导航为服务器上的页面生成完整的 HTML。
这样可以避免在客户端进行数据获取和模板化的其他往返过程,因为它是在浏览器获得响应之前进行处理的。
在服务器上运行页面逻辑和呈现可以避免向客户端发送大量 JavaScript,这有助于实现快速的交互时间。
SSG 即静态网站生成。
静态网站生成类似于服务器端渲染,不同之处在于构建时而不是在请求时渲染页面。
与服务器渲染不同,由于不必动态生成页面的 HTML,因此它还可以实现始终如一的快速到第一字节的时间。
通常,静态呈现意味着提前为每个 URL 生成单独的 HTML 文件。
借助预先生成的 HTML 响应,可以将静态渲染器部署到多个 CDN,以利用边缘缓存的优势。
SSR With hydration 即视图通过同时进行客户端渲染和服务端渲染,从而达成一种平衡。
导航请求(例如整页加载或重新加载)由服务器处理,该服务器将应用程序呈现为 HTML,然后将 JavaScript 和用于呈现的数据嵌入到生成的文档中。
理想状态下,就可以像服务器渲染一样实现快速的 First Contentful Paint,然后通过使用称为 hydration 的技术在客户端上再次渲染来修补。
从真实网站中收集的效果指标表明, 使用 SSR 水合模式效果并不好,强烈建议不要使用它。
原因归结为用户体验:最终很容易使用户陷入怪异的山谷。
CSR 即客户端渲染。
客户端渲染,意味着: 直接使用 JavaScript 在浏览器中渲染页面。
所有逻辑,数据获取,模板和路由均在客户端而不是服务器上处理。
CSR With Pre-rendering 即在构建阶段,就将 HTML 页面渲染完毕,不会进行二次渲染。
也就是说,当初打包时页面是怎么样,那么预渲染就是什么样。
等到 JS 下载并完成执行,如果页面上有数据更新,那么页面会再次渲染,这时会造成一种数据延迟的错觉。
Pre-render 利用 Chrome 官方出品的 Puppeteer 工具,对页面进行爬取。
它提供了一系列的 API, 可以在无 UI 的情况下调用 Chrome 的功能, 适用于爬虫、自动化处理等各种场景。
它很强大,所以很简单就能将运行时的 HTML 打包到文件中。
Trisomorphic Rendring 即三态渲染。
在三态渲染模型中,可以使用服务器流式渲染进行初始导航,然后让 Service Worker 在 HTML 加载完成后,继续进行导航 HTML 的渲染。
这样可以使缓存的组件和模板保持最新状态,并启用 SPA 样式的导航,以在同一会话中呈现新视图。
如果可以在服务器,客户端页面和 Service Worker 之间共享相同的模板和路由代码时,这种方法十分有效。
返回目录