咱們將 pages 下頁面導出爲靜態 HTML 頁面.首先,next.config.js 在應用程序的根目錄中建立一個名爲的文件,並添加如下內容javascript
exportPathMap: async function() { return { "/": { page: "/" }, "/books": { page: "/books" }, "/article": { page: "/article" }, "/write": { page: "/write" } }; },
而後打開 package.json 並添加 scripts 爲如下內容:css
"build": "next build", "export": "next export"
如今,您能夠 out 在項目內部的目錄中看到導出的 HTML 內容.html
如今須要在本地開啓一個靜態服務器,進行測試java
npm install -g serve cd out serve -p 8866
serve 是一個很是簡單的靜態 Web 服務器node
將如下內容添加到 next.config.js 文件中:react
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,併爲其添加新路徑並進行查詢.webpack
關閉本地服務器並在次執行git
npm run export cd out serve -p 8080
運行 next export 命令時,Next.js 不會構建應用程序.頁面/book/[id]已經存在於構建中,所以無需再次構建整個應用程序.可是,若是咱們對應用程序進行了任何更改,則須要再次構建應用程序以獲取這些更改,就是在執行一個 npm run buildgithub
npm install --save-dev typescript @types/react @types/node @types/react-dom
將 index.js 更改成 index.tsxweb
生成的 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;
總體項目代碼 官方案例
安裝依賴包
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 包的大小,帶來了更快的加載速度
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,有助於頁面快地加載.
新建 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 中引入
新建 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 位置流式傳輸到最終用戶.爲用戶帶來超快的加載體驗.