10/7/2020, 9:40:01 PM
最近用Next.js + Editor.js 撸了一个博客系统。开发起来甚是畅滑,如果你喜欢React,又有同构的需求,不妨由此文入手。
如果你的应用需要兼容IE 9,10等浏览器,请出门左拐找找ejs等“传统”方式~~~
前端同构,就是一站式解决上述问题的方案:让一套的JavaScript代码,同时跑在服务端和客户端。
现代点的前端框架都有服务端渲染API,为什么我们还需要一个同构框架?
原因是,一个正常的同构需求,我们需要:
而React SSR的API只有四个函数: renderToString()
, renderToStaticMarkup()
,renderToNodeStream()
,renderToStaticNodeStream()
(Vue也类似),只能满足第一个需求,我们需要更多,而以Next.js为代表前端同构框架除了能满足上述基本的要求外,还能为我们带来:
换句话说,让开发越发动态灵活,让渲染越发静态高效。
举个例子:
其中Next.js可以说是前端同构中的开山,翘楚级框架,依赖React渲染组件。当然Vue有Nuxt.js,Angular有 Angular Universal,甚至Svelte也有Sapper 。
正式开始之前,强烈推荐Next.js的官方文档,挺清晰易懂。
Get started with Next.js in the official documentation, and learn more about all our features!
Next.js的官方Blog,也十分推荐,各个版本的更新详尽及时,堪称模范。
Next.js is the React framework for production
本文基于Next.js 9.3,这里不涉及原理,只是做个入门指导。
一般前端web应用都可以简化为,基于路由的页面和API两部分。Next的路由系统基于文件路径自动映射,不需要做中性化的配置。
一般都约定在根目录pages
文件夹内:
./pages/index.tsx
--> 首页 /
./pages/admin/index.tsx
--> /admin
./pages/admin/post.tsx
--> /admin/post
import styles from './style.module.css'
function About() {
return <div>About</div>
}
export default About
默认导出一个React的组件,Next就会帮你默认生成对应路由的页面。
import
方式。import Link from 'next/link'
function Home() {
return (
<ul>
<li>
<Link href="/about">
<a>About Us</a>
</Link>
</li>
</ul>
)
}
export default Home
注意,Link中最好独立包裹a元素。
import Head from 'next/head'
function About() {
return (
<div>
<Head>
<title> Hipo Log - {props.post?.name ?? ''}</title>
</Head>
content
</div>
);
}
export default About
Next也支持ES2020的dynamic import()语法,可以拆分代码,或者有些第三方组件依赖浏览器API时候精致服务端渲染(ssr: false
)
import dynamic from 'next/dynamic'
const DynamicComponentWithCustomLoading = dynamic(
() => import('../components/hello'),
{
loading: () => <p>...</p>,
ssr: false
}
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithCustomLoading />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
越多引入,上线访问后加载的js就越多,特别是下面钩子函数要注意,不要引入多余代码
API类型的路由约定在./pages/api
文件夹内,next会自动映射为/api/*
路径的API
import { NextApiRequest, NextApiResponse } from 'next'
export default (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({ name: 'John Doe' })
}
请求方法通过req中取到。
如此你就可以很轻松的生成一个API。
正常的应用,都有动态路由,next中讨巧使用文件命名的方式来支持。
./pages/post/create.js
--> /post/create
./pages/post/[pid].js
-->
/post/1
, /post/abc
等,但是不会匹配 /post/create
./pages/post/[...slug].js
--> /post/1/2
, /post/a/b/c
等,但是不会匹配 /post/create
, /post/abc
动态参数可以通过req.query
对象中获取({ pid }
, { slug: [ 'a', 'b' ] }
),在页面中可以通过router hook获取:
import { useRouter } from 'next/router';
function About() {
const router = useRouter();
const { bID, pID } = router.query
return <div>About</div>
}
大部分的应用内容,都不是纯静态的,我们需要数据查询才能渲染那个页面,而这些就需要同构钩子函数来满足,有了这些钩子函数,我们才可以在不同需求下作出极佳体验的web应用。
页面中export
一个async
的getServerSideProps
方法,next就会在每次请求时候在服务端调用这个方法。
getServerSideProps
方法Link
组件导航而来,Next会向服务端发一个请求,然后在服务端运行getServerSideProps
方法,然后返回JSON到浏览器。getServerSideProps
方法主要是升级了9.3之前的getInitialProps
方法9.3之前的getInitialProps
方法有一个很大的缺陷是在浏览器中req
和res
对象会是undefined
。也就是使用它的页面,如果是浏览器渲染你需要在组件内再显示地请求一次。开发体验不太好。
如果没有特殊问题,建议使用getServerSideProps
替代getInitialProps
方法。
示例:
import { GetServerSideProps, NextPage } from 'next'
interface PostProps {
list: Post[]
}
const App: NextPage<PostProps> = props => {
return <div></div>
}
export const getServerSideProps: GetServerSideProps<PostProps> = async context => {
const list = await context.req.service.post.getPost(context.params.postID)
return {
props: {
list
}
}
}
export default App
所谓的SSG也就是静态站点生成,类似像hexo或者gatsbyjs都是在build阶段将页面构建成静态的html文件,这样线上直接访问HTML文件,性能极高。
Next.js 再9.0的时候引入了自动静态优化的功能,也就是如果页面没有使用getServerSideProps
和getInitialProps
方法,Next在build阶段会生成html,以此来提升性能。
但是正如上文说的,一般应用页面都会需要动态的内容,因此自动静态优化局限性很大。
Next 在9.3中更近了一步,引入了getStaticProps
和getStaticPaths
方法来让开发者指定哪些页面可以做SSG优化。
getStaticProps
方法在build阶段返回页面所需的数据。getStaticPaths
方法来返回所有的路由参数,以及是否需要回落机制。export async function getStaticPaths() {
// Call an external API endpoint to get posts
const res = await fetch('https://.../posts')
const posts = await res.json()
// Get the paths we want to pre-render based on posts
const paths = posts.map(post => ({
params: { id: post.id },
}))
// We'll pre-render only these paths at build time.
// { fallback: false } means other routes should 404.
return { paths, fallback: true };
}
export const getStaticProps: GetStaticProps<InitProps> = async ({ params }) => {
const data = await fetch(
`http://.../api/p/${params.bookUUID }/${
params.postUUID }`
);
return {
props: {
post: data,
},
};
};
使用非常的简单,需要注意的是:
getStaticPaths
方法返回的fallback
很有用:如果fallback
是false
,访问该方法没有返回的路由会404fallback
为true
,Next在访问build中没有的动态路由时候,先浏览器loading,然后服务端开始build该页面的信息,然后再返回浏览器渲染,再次访问该路由该缓存就会生效,很强大!!getStaticProps
方法可以增加revalidate
的属性以此来重新生成缓存,这点就很强大:页面加载仍然很快,页面永不离线,即使重新生成失败,老的还可以访问,而且可以大幅减少数据库,server的负载。function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
)
}
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most once every second
revalidate: 1, // In seconds
}
}
export default Blog
getServerSideProps
方法的SSR。上面就是Next.js中主要的部分了,下面是一些可能用到的自定义配置。
用./pages/_app.tsx
来自定义应用App,可以配置全局的css,或者getServerSideProps
方法来给每个页面添加数据。
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
用./pages/_document.tsx
来自定义页面的Document,可以配置页面html,head属性,或者使用静态getInitialProps方法中renderPage方法来包括整个react 应用。
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
, <Html>
, <Head />
和 <Main />
<NextScript />
都是必须的。
getServerSideProps
或者getInitialProps
方法让整个应用都无法自动静态优化Next自然也可以自定义构建,根目录使用next.config.js
来配置webpack,可以用来支持less编译,按需加载,path alias等。
下面是Hipo Log中的配置,支持了Antd design的按需加载。
const withLess = require('@zeit/next-less');
const withCss = require('@zeit/next-css');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const fixMiniCss = (nextConfig = {}) => {
return Object.assign({}, nextConfig, {
webpack(_config, options) {
// force react-scripts to use newer version of `mini-css-extract-plugin` and ignore css ordering warnings
// (related to issue: https://github.com/facok/create-react-app/issues/5372)
let config = _config;
if (typeof nextConfig.webpack === 'function') {
config = nextConfig.webpack(_config, options);
}
let miniCssExtractOptions = {};
for (let i = 0; i < config.plugins.length; i++) {
const p = config.plugins[i];
if (
!!p.constructor &&
p.constructor.name === MiniCssExtractPlugin.name
) {
miniCssExtractOptions = { ...p.options, ignoreOrder: true };
// config.plugins[i] = new MiniCssExtractPlugin(miniCssExtractOptions);
break;
}
}
// config.plugins.push(new MiniCssExtractPlugin(miniCssExtractOptions));
config.resolve.alias = {
...config.resolve.alias,
'@pages': path.join(__dirname, './pages'),
'@components': path.join(__dirname, './components'),
'@entity': path.join(__dirname, './dbEntity'),
'@services': path.join(__dirname, './services'),
'@lib': path.join(__dirname, './lib'),
'@util': path.join(__dirname, './services/util'),
'@type': path.join(__dirname, './app.type'),
};
return config;
},
});
};
module.exports = fixMiniCss(
withLess({
cssModules: true,
...withCss({
webpack(config, options) {
const { dev, isServer } = options;
if (isServer) {
const antStyles = /antd\/.*?\/style\/css.*?/;
const origExternals = [...config.externals];
config.externals = [
(context, request, callback) => {
if (request.match(antStyles)) return callback();
if (typeof origExternals[0] === 'function') {
origExternals[0](context, request, callback);
} else {
callback();
}
},
...(typeof origExternals[0] === 'function' ? [] : origExternals),
];
config.module.rules.unshift({
test: antStyles,
use: 'null-loader',
});
}
return config;
},
}),
})
);
Next也支持node启动,以此来和其他框架配合实现更复杂的服务端功能,譬如Hipo Log 使用它来绑定数据库typeorm等。
/ server.js
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl
if (pathname === '/a') {
app.render(req, res, '/b', query)
} else if (pathname === '/b') {
app.render(req, res, '/a', query)
} else {
handle(req, res, parsedUrl)
}
}).listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})