導出 html 並開啟服務
我們將 pages 下頁面導出為靜態 HTML 頁面.首先,next.config.js 在應用程序的根目錄中創建一個名為的文件,並添加以下內容
exportPathMap: async function() {
return {
"/": { page: "/" },
"/books": { page: "/books" },
"/article": { page: "/article" },
"/write": { page: "/write" }
};
},
然后打開 package.json 並添加 scripts 為以下內容:
"build": "next build",
"export": "next export"
現在,您可以 out 在項目內部的目錄中看到導出的 HTML 內容.
現在需要在本地開啟一個靜態服務器,進行測試
npm install -g serve
cd out
serve -p 8866
serve 是一個非常簡單的靜態 Web 服務器
導出其他頁面
將以下內容添加到 next.config.js 文件中:
exportPathMap: async function() {
const paths = {
"/": { page: "/" },
"/books": { page: "/books" },
"/article": { page: "/article" },
"/write": { page: "/write" }
};
const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
const data = await res.json();
const shows = data.map(entry => entry.show);
shows.forEach(show => {
paths[`/book/${show.id}`] = {
page: "/book/[id]",
query: { id: show.id }
};
});
return paths;
},
為了渲染詳情頁面,我們首先獲取數據列表.然后,我們循環獲取 id,並為其添加新路徑並進行查詢.
關閉本地服務器並在次執行
npm run export
cd out
serve -p 8080
運行 next export 命令時,Next.js 不會構建應用程序.頁面/book/[id]已經存在於構建中,因此無需再次構建整個應用程序.但是,如果我們對應用程序進行了任何更改,則需要再次構建應用程序以獲取這些更改,就是在執行一個 npm run build
添加 typescript
npm install --save-dev typescript @types/react @types/node @types/react-dom
將 index.js 更改為 index.tsx
生成的 tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
index.tsx 頁面提示缺少類型,因為我們沒有告訴 TypeScript 它是 Next.js 頁面,在 strict 模式下不允許隱式 any 類型.
import { NextPage } from 'next';
const Home: NextPage<{ userAgent: string }> = ({ userAgent }) => (
<h1>Hello world! - user agent: {userAgent}</h1>
);
Home.getInitialProps = async ({ req }) => {
const userAgent = req ? req.headers['user-agent'] || '' : navigator.userAgent;
return { userAgent };
};
export default Home;
懶加載模塊
創建 firebase 頁面
整體項目代碼 官方案例
添加 analyzer
安裝依賴包
npm install firebase @zeit/next-bundle-analyzer cross-env --save
然后打開 package.json 並添加 scripts 為以下內容:
"analyze": "cross-env ANALYZE=true next build",
"analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build
現在的 next.config.js 所有配置
const fetch = require("isomorphic-unfetch");
const withBundleAnalyzer = require("@zeit/next-bundle-analyzer");
const withLess = require("@zeit/next-less");
const FilterWarningsPlugin = require("webpack-filter-warnings-plugin");
if (typeof require !== "undefined") {
require.extensions[".less"] = file => {};
}
function HACK_removeMinimizeOptionFromCssLoaders(config) {
config.module.rules.forEach(rule => {
if (Array.isArray(rule.use)) {
rule.use.forEach(u => {
if (u.loader === "css-loader" && u.options) {
delete u.options.minimize;
}
});
}
});
}
module.exports = withBundleAnalyzer(
withLess({
analyzeServer: ["server", "both"].includes(process.env.BUNDLE_ANALYZE),
analyzeBrowser: ["browser", "both"].includes(process.env.BUNDLE_ANALYZE),
bundleAnalyzerConfig: {
server: {
analyzerMode: "static",
reportFilename: "../bundles/server.html"
},
browser: {
analyzerMode: "static",
reportFilename: "../bundles/client.html"
}
},
exportPathMap: async function() {
const paths = {
"/": { page: "/" },
"/books": { page: "/books" },
"/article": { page: "/article" },
"/write": { page: "/write" }
};
const res = await fetch("https://api.tvmaze.com/search/shows?q=batman");
const data = await res.json();
const shows = data.map(entry => entry.show);
shows.forEach(show => {
paths[`/book/${show.id}`] = {
page: "/book/[id]",
query: { id: show.id }
};
});
return paths;
},
lessLoaderOptions: {
javascriptEnabled: true
},
webpack(config) {
config.plugins.push(
new FilterWarningsPlugin({
exclude: /mini-css-extract-plugin[^]*Conflicting order between:/
})
);
HACK_removeMinimizeOptionFromCssLoaders(config);
return config;
}
})
);
直接執行
npm run analyze
服務器文件分析
客戶端文件分析
firebase 文件分析詳情
可以看到當前 firebase 和 firebase/[id].js 存在對 firebase 模塊的引用
延遲加載
僅當用戶嘗試導航到其他頁面時,我們才使用 firebase 模塊.可以使用 Next.js 的動態導入功能輕松地做到這一點.
修改 lib/load-db.js
export default async function loadDb() {
const firebase = await import('firebase/app');
await import('firebase/database');
try {
firebase.initializeApp({
databaseURL: 'https://hacker-news.firebaseio.com'
});
} catch (err) {
// we skip the "already exists" message which is
// not an actual error when we're hot-reloading
if (!/already exists/.test(err.message)) {
console.error('Firebase initialization error', err.stack);
}
}
return firebase.database().ref('v0');
}
使用 import()函數加載 firebase 模塊,用 await 來等待並解析模塊.
再次執行
npm run analyze
firebase 模塊具有自己的 bundle,static/chunks/[a-random-string].js
.當您嘗試導入 firebase/app 和 firebase/database 模塊時,將加載此 bundle.
可以看到,firebse 和 firebase/[id].js 文件縮小了不少
進行測試
由於需要更真實的測試在線上的表現,我們需要重新構建.
npm run build
npm run start
然后輸入 localhost:8866 (與 dev 不一樣),之后進入 firebase 頁面在進入 firebase 詳情頁面.
實際上只會第一次瀏覽頁面時加載,當 firebase 頁面導入 firebase/app 和 firebase/database 模塊,會加載 firebase 的 bundle.等再次進入的時候,改 bundle 已經加載過,就不會再次加載`
如圖,再次加載沒有請求
延遲加載的模塊減少了主要 JavaScript 包的大小,帶來了更快的加載速度
使用 import 的要點
async componentDidMount() {
const SimpleMDE = await import("simplemde");
const marked = await import("marked");
const hljs = await import("highlight.js");
......
new SimpleMDE.default()
hljs.default.highlightAuto(code).value
marked.default
}
與正常的 import 加載不用的是,import('xxx')加載的形式會將返回的模塊放到一個 default 字段中進行保存
延遲加載組件
在一個組件里面同時使用 3 個 markdown 相關組件
import Markdown from "react-markdown";
import marked from "marked";
import Highlight from "react-highlight";
導致這個頁面過於龐大
執行npm run analyze
看看 markdown/[id]大小
但是我們不需要在一開始就使用這些模塊,只有需要加載 markdown 文本時才需要.因此,如果我們僅在使用時才加載,那將大大減少初始 bundle,有助於頁面快地加載.
使用 HOC 高階組件抽離渲染
新建 lib/with-post.js
import Layout from "../components/MyLayout";
import dynamic from "next/dynamic";
import marked from "marked";
const Highlight = dynamic(() => import("react-highlight"));
marked &&
marked.setOptions({
gfm: true,
tables: true,
breaks: true
});
function WithPost(InnerComponent, options) {
return class extends React.Component {
constructor(props) {
super(props);
this.renderMarkdown = this.renderMarkdown.bind(this);
}
renderMarkdown(id) {
// If a code snippet contains in the markdown content
// then use Highlight component
if (id === 1 || id === "1") {
return (
<Layout>
<h1>{options.title}</h1>
<h3>當前id=>{id}</h3>
<div className="markdown">
<Highlight innerHTML>{marked(options.content)}</Highlight>
</div>
</Layout>
);
}
// If not, simply render the generated HTML from markdown
return (
<Layout>
<h1>{options.title}</h1>
<h3>當前id=>{id}</h3>
<div className="markdown">
<div dangerouslySetInnerHTML={{ __html: marked(options.content) }} />
</div>
</Layout>
);
}
render() {
return <InnerComponent renderMarkdown={this.renderMarkdown}></InnerComponent>;
}
};
}
export default WithPost;
修改 marked/[id].js
import React, { Component } from "react";
import withPost from "../../lib/with-post";
import { withRouter } from "next/router";
const data = {
title: "Deploy apps with ZEIT now",
content: `
Deploying apps to ZEIT now is pretty easy.
Simply run the following command from your app root:
~~~bash
npm i -g now # one time command
now
~~~
`
};
class Post extends Component {
constructor(props) {
super(props);
}
render() {
return <div>{this.props.renderMarkdown(this.props.router.query.id)}</div>;
}
}
Post = withRouter(Post);
Post = withPost(Post, data);
export default Post;
現在需要使用 Next.js 中的動態導入將 react-highlight 組件轉換為動態組件.最終實現這些組件僅在將要在頁面中呈現時才加載.可以使用該 next/dynamic 來創建動態組件.
動態組件
//import Highlight from 'react-highlight'
import dynamic from 'next/dynamic';
const Highlight = dynamic(() => import('react-highlight'));
訪問 localhost:6688,可以在 network 中找到單次 Highlight 的 bundle 引入
僅在需要時加載
if (id === 1 || id === "1") {
return (
<Layout>
<h1>{options.title}</h1>
<h3>當前id=>{id}</h3>
<div className="markdown">
<Highlight innerHTML>{marked(options.content)}</Highlight>
</div>
</Layout>
);
}
當前判斷 id 是否為 1,如果是就加載 Highlight,否則就正常插入 html
使用動態組件后,就會將組件單獨實現一個 bundle,加載時候直接加載這一個 bundle 就行了
效果也是實現了 javascript 主文件的精簡,同時所以 marked/[id].js 的大小,能夠根據實際來判斷是否加載一大段可能不需要的代碼.
為了模擬真實的服務器渲染效果,需要重新構建
npm run build
npm run start
上圖中可以看到,highlight 的 bundle 名稱是 16.[chunkname].js
輸入http://localhost:8866/marked/1,可以在head里面發現<link rel="preload" href="/_next/static/chunks/commons.972eca8099a2576b25d9.js" as="script">
的存在.之后再切換為其它的 id,這一個 js 文件就沒有在 head 中引入
創建 awp 頁面
新建 pages/awp.js
export const config = { amp: true };
export default function Awp(props) {
return <p>Welcome to the AMP only Index page!!</p>;
}
通過添加 amp: 'hybrid'以下內容來創建混合 AMP 頁面
import { useAmp } from 'next/amp';
export const config = { amp: 'hybrid' };
export default function Awp(props) {
const isAmp = useAmp();
return <p>Welcome to the {isAmp ? 'AMP' : 'normal'} version of the Index page!!</p>;
}
自動靜態優化
如果沒有阻塞數據要求,則 Next.js 會自動確定頁面為靜態頁面(可以預呈現).判斷標准就是 getInitialProps 在頁面中是否存在.
如果 getInitialProps 存在,則 Next.js 不會靜態優化頁面.相反,Next.js 將使用其默認行為並按請求呈現頁面(即服務器端呈現).
如果 getInitialProps 不存在,則 Next.js 會通過將其預呈現為靜態 HTML 來自動靜態優化您的頁面.在預渲染期間,路由器的 query 對象將為空,因為 query 在此階段我們沒有信息要提供.query 水合后,將在客戶端填充任何值.
此功能允許 Next.js 發出包含服務器渲染頁面和靜態生成頁面的混合應用程序.這樣可以確保 Next.js 始終默認發出快速的應用程序.
靜態生成的頁面仍然是反應性的:Next.js 將對您的應用程序客戶端進行水化處理,使其具有完全的交互性.
優點是優化的頁面不需要服務器端計算,並且可以立即從 CDN 位置流式傳輸到最終用戶.為用戶帶來超快的加載體驗.
- 在大多數情況下,你並不需要一個自定義的服務器,所以嘗試添加 target: 'serverless'
- getInitialProps 是頁面是否靜態的主要決定因素,如果不需要 SSR,請不要添加到頁面
- 並非所有動態數據都必須具有 SSR,例如,如果它在登錄后,或者您不需要 SEO,那么在這種情況下,最好在外部進行獲取 getInitialProps 使用靜態 HTML 加載速度