玩玩服務端渲染之 Next.js

前言

基於 React+Node.js 的服務端渲染SSR框架 Next.jscss

爲何須要服務端渲染?

  • 減小首屏白屏時間html

  • SEO:Search Engine Optimization 即搜索引擎優化;
    簡單說就是,網頁在被請求的時候,從服務器發出的內容能夠被搜索引擎的爬蟲爬到數據,
    這個時候從搜索引擎搜索的關鍵字包含在這些內容中,那麼這個網址的信息就更容易顯示在搜索結果中~前端

  • 客戶端渲染:前端SPA的出現,大部分的頁面內容在瀏覽器端經過異步請求拿到數據,而後渲染生成;
    而從服務器發出的只是一個沒有內容的空殼,搜索引擎天然爬不到東西~node

  • 服務端渲染:之前的後端MCV框架就是使用模板在服務器上將內容生成,而後瀏覽器接收到數據直接渲染就能夠了;
    這個時候網頁的內容已經跟隨網站過來,主要內容不須要額外的異步請求獲取,
    搜索引擎的爬蟲就能夠爬到這些非異步請求的數據~react

  • 前端框架的SSR:主要是 先後端同構微服務接口聚合 等;固然只是看成渲染層是最簡單的,接口這些就由後端大佬負責吧;
    如 React/Vue/Angular 都有使用 Node.js 在服務端渲染數據的框架;
    前端也能夠繼續使用 React/Vue/Angular 框架,只是某些數據放在了服務端生成linux


源碼看👇這裏webpack

能夠看到,從服務器過來的時候已經有內容了: nginx

後續的路由變化就至關於 單頁面SPA 了,可是在某個路由下刷新,那麼這個路由也就是 服務端渲染c++

一、src 目錄結構

.
├── components
│   ├── header
│   ├── hello-world
│   └── layout
├── pages
│   ├── _app.tsx
│   ├── _error.tsx
│   ├── about.tsx
│   ├── detail.tsx
│   └── index.tsx
├── store
│   ├── home.ts
│   └── index.ts
└── styles
    ├── _error.scss
    ├── about.scss
    ├── detail.scss
    └── index.scss
複製代碼

二、npm script

tsc 須要安裝git

yarn install typescript -g
複製代碼

三、頁面 head

設置頁面 head

import Head from 'next/head';

const Header = () => (
  <Head> <title>Next.js test</title> <meta name="viewport" content="width=device-width,initial-scale=1" /> </Head> ); 複製代碼

能夠在須要修改頁面 head 的地方使用,如 /about 須要修改 頁面title 爲 about;
是增量修改的方式:沒有則添加,有則修改

// ...
<Head>
  <title>about</title>
</Head>
// ...
複製代碼

四、路由

pages 目錄下自動生成路由,層級只能是一層 以下:

// 第一種寫法
pages/home.tsx
pages/detail.tsx

styles/home.scss
styles/detail.scss

// 而不是
// 第二種寫法
pages/home/index.tsx
pages/home/home.scss
複製代碼
  • pages/ 下 不能是文件夾(正常來講還有個 css,可是使用文件夾的話,開發階段正常,build 時 會報 scss 文件不是 React 組件。。。因此在外面新建 styles 文件夾,放各自路由組件的 scss 文件)
  • 其餘文件夾如 components 不會報錯,可使用第二種寫法,樣式跟組件在一個文件夾

4.1 <link>組件:

  • href: 路由,如 href="about" ,則會渲染 page/about.tsx 的內容
  • as: 將 href 重命名而後瀏覽器地址顯示的是這個URL;
    href的路由必須正確,須要有一個實際上在 page 目錄中存在的文件
  • prefetch: 預取,當前頁面會用到

href/as

以下將瀏覽器URL顯示爲 about1,若是服務端不另外攔截路由的話,實際上渲染的是 pages/about.tsx 文件,實際的路由也是如此

import Link from 'next/link';

<Link href="/about"> <a>About</a> </Link>
複製代碼

若是設置了 as 別名,而且與原來的路由不同了,須要在服務端另外設置路由;

以下:

  • 客戶端
// ...
  return (
    <div> <Link href="/detail?id=123" as="/detail/123"> <a style={linkStyle}>Detail</a> </Link> </div>
  )
// ...
複製代碼
  • 服務端
// server.ts
  server.get('/detail/:id', (req: Req, res: http.ServerResponse) => {
    app.render(req, res, '/detail', {
      id: req.params.id
    })
  });
複製代碼

prefetch(來自文檔):

預取數據,如列表頁能夠預取詳情頁

  • Link 組件
<Link href="/about" prefetch={true}>
  <a>About</a>
</Link>
複製代碼
  • 命令式
import { withRouter } from 'next/router';

function MyLink({ router }) {
  return (
    <div> <a onClick={() => setTimeout(() => router.push('/dynamic'), 100)}> A route transition will happen after 100ms </a> {// but we can prefetch it! router.prefetch('/dynamic')} </div>
  );
}

export default withRouter(MyLink);
複製代碼

push/replace

  • 對象
import Router from 'next/router'

const handler = () => {
  Router.push({
    pathname: '/about',
    query: { name: 'Zeit' },
  })
}

function ReadMore() {
  return (
    <div> Click <span onClick={handler}>here</span> to read more </div>
  )
}

export default ReadMore
複製代碼

4.2 非路由組件獲取路由參數

這個和 React 同樣,使用 withRouter 獲取路由參數,不過這個是從 next/router 導出的;函數組件也可使用 useRouter

// src/components/header/index.tsx
import Link from 'next/link';
import Head from 'next/head';
import React from 'react'; 
import styles from './header.scss';

const Header = () => {
  return (
    <div> <Head> <title>Next.js test</title> <meta name="viewport" content="width=device-width,initial-scale=1" /> </Head> <Link href="/"> <a className={styles.tag}>Home</a> </Link> <Link href="/about"> <a className={styles.tag}>About</a> </Link> <Link href="/detail?id=123" as="/detail/123"> <a className={styles.tag}>Detail</a> </Link> </div> ) }; export default Header; 複製代碼

4.3 路由轉換

  • href="/detail?id=123"query查詢的URL 轉換爲 as="/detail/123"params 式 URL
    href/as 是靜態的,有須要的話動態生成 <Link> 便可

import Link from 'next/link';

<Link href="/detail?id=123" as="/detail/123"> <a className={styles.tag}>Detail</a> </Link>
複製代碼
  • 服務端匹配URL爲 /detail/:id 的路由,
    添加 params 參數,而後渲染 /detail 對應的 page/detail.tsx 文件;
    若是不在服務端設置對應的路由攔截的話,刷新會致使404
// server.ts
// ...

function serverRun() {
  const server = express();
  // api接口
  const controllers = require('./server/controller');
  const apiRoute = ''; // '/web';
  server.use(apiRoute, controllers);

  // 匹配URL爲 `/` 的路由,而後渲染 `/` 對應的 `page/index.tsx` 文件
  server.get('/', (req: Req, res: http.ServerResponse) => {
    app.render(req, res, '/')
  });

  // 匹配URL爲 `/about` 的路由,而後渲染 `/about` 對應的 `page/about.tsx` 文件
  server.get('/about', (req: Req, res: http.ServerResponse) => {
    app.render(req, res, '/about')
  });

  // 匹配URL爲 `/detail/:id` 的路由,添加 `params 參數`,而後渲染 `/detail` 對應的 `page/detail.tsx` 文件
  server.get('/detail/:id', (req: Req, res: http.ServerResponse) => {
    app.render(req, res, '/detail', {
      id: req.params.id
    })
  });

  server.get('*', (req: http.IncomingMessage, res: http.ServerResponse) => {
    return handle(req, res);
  });

  server.listen(3000, (err: any) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
}
複製代碼

4.4 路由方法

路由攔截 Router.beforePopState

在瀏覽器端執行,須要組件加載完 componentDidMount/useEffect 執行,否則會報錯

// src/pages/index.tsx
import Link from 'next/link';
import /* Router, */ { useRouter } from 'next/router';
import React, { useEffect } from 'react';
import Layout from '@/components/layout';
import styles from '@/styles/index.scss';

/** * 首頁,路由爲 '/' * @param props */
const Home = () => {
  const router = useRouter();

  useEffect(() => {
    // 路由攔截,會影響瀏覽器前進後退的渲染結果
    // Router.beforePopState(({ url, as, options }: any) => {
    // console.log('url: ', url);
    // console.log('as: ', as);
    // console.log('options: ', options);
      
    // if (as === '/about') {
    // console.log('about');
    // return true;
    // }
    // return true;
    // });
  });

  return (
    <Layout> <h1>{router.query.title}</h1> <img className={styles.img} src="/static/images/4k-wallpaper-alps-cold-2283757.jpg" /> <div className={styles.content}> <p> This is our blog post. Yes. We can have a <Link href="/link"><a>link</a></Link>. And we can have a title as well. </p> <h3>This is a title</h3> <p>And here's the content.</p> </div> </Layout> ); }; export default Home; 複製代碼

4.5 Router 事件

監聽路由的內部事件:

  • routeChangeStart(url) - 路由開始變化的時候觸發
  • routeChangeComplete(url) - 路由完成變化以後觸發
  • routeChangeError(err, url) - 路由變化發生錯誤時觸發
  • beforeHistoryChange(url) - 改變瀏覽器歷史紀錄以前觸發
  • hashChangeStart(url) - hash 開始變化的時候觸發
  • hashChangeComplete(url) - hash完成變化以後觸發

示例:

import Router from 'next/router';

const handleRouteChange = url => {
  console.log('App is changing to: ', url);
};

Router.events.on('routeChangeStart', handleRouteChange);
複製代碼

路由跳轉

// 正常路由跳轉,在about頁面獲取路由信息的時候,id爲a11,
// 刷新頁面則id爲asss,因此儘可能兩者一致,避免沒必要要的問題
Router.push('/about?id=a11', '/about/asss')
複製代碼
  • Shallow Routing:淺路由

不執行 getInitialProps 的狀況下修改頁面 URL,

Router.push('/about?id=a11', '/about/asss', { shallow: true });
複製代碼

五、App

_app 組件不會被銷燬,除非手動刷新

// src/pages/_app.tsx
import React from 'react';
import { NextComponentType } from "next";
import { Router } from 'next/router';
import App, { AppProps } from 'next/app';

interface Props {
  Component: NextComponentType,
  pageProps: AppProps,
  router: Router
}

/** * App */
class myApp extends App<Props> {

  public constructor(props: Props) {
    super(props);
  }
  
  public componentDidUpdate() {
    console.log('router: ', this.props.router);
  }
  
  public componentDidMount() {
    console.log('router: ', this.props.router);
  }

  public render() {
    const { Component, pageProps } = this.props;

    return (
      <React.Fragment>
        <Component {...pageProps} />
      </React.Fragment>
    );
  }
}

export default myApp;
複製代碼

六、組件

6.1 getInitialProps 屬性:

接收一個方法,能夠在這個方法裏面獲取數據,在服務端渲染;只能在 pages/ 下的組件使用 文檔

在當前路由刷新纔會在服務端執行,若是是從其餘路由跳轉過來的,沒有刷新頁面就會在瀏覽器端執行的;

// src/pages/detail.tsx
import Head from 'next/head';
import { useRouter } from 'next/router';
import React from 'react';
import { inject, observer } from 'mobx-react';
import { homeStoreType } from '@/store/home';
import { Button, Row } from 'antd';
import Layout from '@/components/layout';
import styles from '@/styles/detail.scss';

function Detail(props: any) {
  const router = useRouter();
  const homeStore: homeStoreType = props.homeStore;

  return (
    <Layout> <Head> <title>Detail</title> </Head> <p className={styles.detail}>This is the detail page!</p> id: { router.query.id } <Row> count: { homeStore.count } </Row> <Button onClick={() => homeStore.setCount(homeStore.count+1)} >count++</Button> </Layout>
  );
}

Detail.getInitialProps = async function(context: any) {
  /** * 在當前路由刷新的話,context.req 爲真,服務端纔有 req/res,在命令行打印 'broswer'; * 若是是其餘路由跳轉過來沒有刷新頁面的話,context.req 爲假,在瀏覽器控制檯打印, * 此時 document.title 是 跳轉以前的頁面 title; */
  console.log('render-type: ', context.req ? 'server' : 'broswer');

  return {
    // data: 'detail'
  };
}

const DetailWithMobx = inject('homeStore')(
  observer(Detail)
);

export default DetailWithMobx;
複製代碼

接收的方法有一個形參 context = { pathname, query, asPath, req, res, err }

  • pathname: URL pathname 中的固定的部分,如 定義的/post/:id,則這裏pathname爲/post
  • query: URL的查詢參數的對象
  • asPath - 定義的路由,如 /post/:id
  • req: HTTP request object (server only)
  • res: HTTP response object (server only)
  • err: 渲染期間的報錯

6.2 動態 import

使用 dynamicimport() 實現動態組件;
dynamic 第二個參數是一個對象,loading 字段是加載完成前的 loading

// src/components/about.tsx
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import Layout from '@/components/layout';
import styles from '@/styles/about.scss';

const Hello = dynamic(
  () => import('../components/hello-world/index'),
  { loading: () => <p>...</p> }
);

function About() {
  return (
    <Layout>
      <Head>
        <title>about</title>
      </Head>
      <p className={styles.about}>This is the about page</p>
      <Hello />
    </Layout>
  );
}

// About.getInitialProps = async function(context: any) {
//   return {
//     data: 'about'
//   };
// }

export default About;
複製代碼

七、路徑別名

使用 babel-plugin-module-alias,直接配置 webpack 是無效的

yarn add babel-plugin-module-alias -D
複製代碼
  • 配置 .babelrc
{
  "plugins": [
    ["module-alias", { "src": "./src", "expose": "@" }]
  ],
  "presets": [
    "next/babel",
  ]
}
複製代碼
  • tsconfig.json
{
  "compilerOptions": {
    ...
    "baseUrl": "src",
    "paths": {
      "@/*": ["./*"]
    },
  },
  ...
}
複製代碼

八、使用 SCSS

官方插件: @zeit/next-sass

8.1 SASS

安裝

npm install --save @zeit/next-sass node-sass
複製代碼

yarn add @zeit/next-sass node-sass
複製代碼

配置

配置後,使用跟react裏面使用CSS modules同樣

// next.config.js
const withSass = require('@zeit/next-sass')
module.exports = withSass({
  cssModules: true, // 默認 false,即全局有效
  cssLoaderOptions: {
    importLoaders: 1,
    localIdentName: "[local]___[hash:base64:5]",
  }
})
複製代碼

或者自定義

// next.config.js
const withSass = require('@zeit/next-sass')
module.exports = withSass({
  webpack(config, options) {
    return config
  }
})
複製代碼

使用 postcss

項目根目錄下,新建一個文件 postcss.config.js

// postcss.config.js
module.exports = {
  plugins: {
    'autoprefixer': true
  }
}
複製代碼

九、antd

參考這位大佬的 文章, 主要就是 cssModulesantd按需加載 一塊兒使用的問題,其餘的按照 antd 官網的搞就能夠,antd-mobile 只要將 相應的antd 改成 antd-mobile 就能夠了

.babelrc

{
  "plugins": [
    // "transform-decorators-legacy",
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["module-alias", { "src": "./src", "expose": "@" }],
    ["import", { "libraryName": "antd", "style": "css" }]
  ],
  "presets": [
    "next/babel",
  ]
}
複製代碼

next.config.js

如下是公共配置,會合併到 next.config.js 的配置中

// config/config.common.js
const path = require('path');
const cssLoaderGetLocalIdent = require('css-loader/lib/getLocalIdent.js');
// const isProd = process.env.NODE_ENV === 'production';

if (typeof require !== 'undefined') {
  require.extensions['.css'] = file => { }
}

/* 公共配置 */
let configCommon = {
  // assetPrefix: isProd ? 'https://cdn.mydomain.com' : '',
  crossOrigin: 'anonymous',
  cssModules: true,
  cssLoaderOptions: {
    importLoaders: 1,
    localIdentName: "[local]___[hash:base64:5]",
    getLocalIdent: (context, localIdentName, localName, options) => {
      const hz = context.resourcePath.replace(context.rootContext, '');
      // 排除 node_modules 下的樣式
      if (/node_modules/.test(hz)) {
        return localName;
      }
      return cssLoaderGetLocalIdent(context, localIdentName, localName, options);
    }
  },
  distDir: 'next-build', // 構建輸出目錄,默認 '.next'
  generateEtags: true, // 控制緩存的 etag,默認 true
  pageExtensions: ['tsx', 'jsx', 'js', 'scss'], // pages文件夾下的文件後綴
  webpack(config){
    if(config.externals){
      // 解決 打包css報錯問題
      const includes = [/antd/];
      config.externals = config.externals.map(external => {
        if (typeof external !== 'function') return external;
        return (ctx, req, cb) => {
          return includes.find(include =>
            req.startsWith('.')
              ? include.test(path.resolve(ctx, req))
              : include.test(req)
          )
            ? cb()
            : external(ctx, req, cb);
        };
      });
    }
    return config;
  }
};

module.exports = configCommon;
複製代碼

十、狀態管理 MobX

跟這篇 React+Typescript 的單頁面SPA項目裏的差很少,只是服務端渲染 沒有 window;因此緩存這裏先判斷一下是否瀏覽器,而後再去使用瀏覽器 API( sessionStorage );

不過刷新會有一個數據變化的過程,由於實際上 _app.txs 是在服務端渲染的,緩存是在瀏覽器恢復的,有個時間差,並且會有警告(其實能夠忽略,服務器跟客戶端的這個緩存不須要同步,使用 sessionStorage 也是由於不須要長久緩存,固然能夠根據須要改成 localStorage ),根據需求取捨吧

單頁面SPA 由於是在瀏覽器渲染因此不會有這樣的問題

const isBroswer: boolean = process.browser;
複製代碼

10.1 項目入口

// src/pages/_app.tsx
import { NextComponentType } from "next";
import { Router } from 'next/router';
import App, { AppProps } from 'next/app';
import React from 'react';
import { Provider } from 'mobx-react';
import store from '../store';

interface Props {
  Component: NextComponentType,
  pageProps: AppProps,
  router: Router
}

/** * App */
class myApp extends App<Props> {

  public constructor(props: Props) {
    super(props);
  }
  
  public componentDidUpdate() {
    console.log('router: ', this.props.router);
  }
  
  public componentDidMount() {
    console.log('router: ', this.props.router);
  }

  public render() {
    const { Component, pageProps } = this.props;

    return (
      <React.Fragment>
        <Provider {...store}>
          <Component {...pageProps} />
        </Provider>
      </React.Fragment>
    );
  }
}

export default myApp;
複製代碼

10.2 模塊

監聽數據使用 autorun,會在數據變化時執行一次;而後用 toJS 將模塊轉化爲 JS對象

// src/store/home.ts
import * as mobx from 'mobx';

// 禁止在 action 外直接修改 state 
mobx.configure({ enforceActions: "observed"});
const { observable, action, computed, runInAction, autorun } = mobx;

const isBroswer: boolean = process.browser;

/** * 因此緩存這裏先判斷一下是否瀏覽器,而後再去使用瀏覽器 API( `sessionStorage` ); * 不過會有一個閃現的過程,由於實際上 `_app.txs` 是在服務端渲染的,緩存是在瀏覽器恢復的, * 有個時間差,並且會有警告,根據需求取捨吧 */
let cache = isBroswer && window.sessionStorage.getItem('homeStore');

// 初始化數據
let initialState = {
  count: 0,
  data: {
    time: '2019-11-20'
  },
};

// 緩存數據
if (isBroswer && cache) {
  initialState = {
    ...initialState,
    ...JSON.parse(cache)
  }
}

class Home {
  @observable
  public count = initialState.count;

  @observable
  public data = initialState.data;

  @computed
  public get getTime() {
    return this.data.time;
  }

  @action
  public setCount = (_count: number) => {
    this.count = _count;
  }

  @action
  public setCountAsync = (_count: number) => {
    setTimeout(() => {
      runInAction(() => {
        this.count = _count;
      })
    }, 1000);
  }

  // public setCountFlow = flow(function *(_count: number) {
  // yield setTimeout(() => {}, 1000);
  // this.count = _count;
  // })
}

const homeStore = new Home();

// 數據變化後觸發,數據緩存
autorun(() => {
  const obj = mobx.toJS(homeStore);
  isBroswer && window.sessionStorage.setItem('homeStore', JSON.stringify(obj));
});

export type homeStoreType = typeof homeStore;
export default homeStore;
複製代碼

10.3 模塊管理輸出

// src/store/index.ts
import homeStore from './home';

/** * 使用 mobx 狀態管理 */
export default {
  homeStore
}
複製代碼

10.4 組件使用

這裏是函數組件的使用,類組件的使用能夠看 這裏

// src/pages/detail.tsx
import Head from 'next/head';
import { useRouter } from 'next/router';
import React from 'react';
import { inject, observer } from 'mobx-react';
import { homeStoreType } from '@/store/home';
import { Button, Row } from 'antd';
import Layout from '@/components/layout';
import styles from '@/styles/detail.scss';

function Detail(props: any) {
  const router = useRouter();
  const homeStore: homeStoreType = props.homeStore;

  return (
    <Layout> <Head> <title>Detail</title> </Head> <p className={styles.detail}>This is the detail page!</p> id: { router.query.id } <Row> count: { homeStore.count } </Row> <Button onClick={() => homeStore.setCount(homeStore.count+1)} >count++</Button> <Button onClick={() => homeStore.setCountAsync(homeStore.count+1)} >countAsync++</Button> </Layout>
  );
}

Detail.getInitialProps = async function(context: any) {
  /** * 在當前路由刷新的話,context.req 爲真,服務端纔有 req/res,在命令行打印 'broswer'; * 若是是其餘路由跳轉過來沒有刷新頁面的話,context.req 爲假,在瀏覽器控制檯打印, * 此時 document.title 是 跳轉以前的頁面 title; */
  console.log('render-type: ', context.req ? 'server' : 'broswer');

  return {
    data: 'detail'
  };
}

const DetailWithMobx = inject('homeStore')(
  observer(Detail)
);

export default DetailWithMobx;
複製代碼

十一、服務端

11.1 server.ts

server.ts 改動後,須要在命令行手動執行 tsc server.ts 生成 server.js,才能執行(暫時就這麼搞吧);在 npm script 裏面直接寫好就ok了;不知道怎麼自動編譯+重啓服務

next(opts: object)

opts有如下屬性:

  • dev (bool): 是否開發環境 development - 默認 false
  • dir (string): Next 項目的位置 - 默認 '.'
  • quiet (bool): 隱藏包含服務器信息的錯誤消息 - 默認 false
  • conf (object): 與在 next.config.js 中的對象同樣 - 默認 {}
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ 
  dev,
  dir: '.',
  quiet: false,
  conf: {}
});
複製代碼

動態資源前綴 assetPrefix

好比本地、線上使用cdn的話資源就和頁面的服務器不是同一臺了

// server.ts
const express = require('express');
const next = require('next');
import * as http from "http";

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

interface Req extends http.IncomingMessage {
  params?: any,
}

app
  .prepare()
  .then(() => {
    serverRun();
  })
  .catch((ex: any) => {
    console.log(ex.stack);
    process.exit(1);
  });
  
function serverRun() {
  const server = express();
  // api接口
  const controllers = require('./server/controller');
  const apiRoute = ''; // '/web';
  server.use(apiRoute, controllers);

  // 匹配URL爲 `/` 的路由,而後渲染 `/` 對應的 `page/index.tsx` 文件
  server.get('/', (req: Req, res: http.ServerResponse) => {
    app.render(req, res, '/')
  });

  // 匹配URL爲 `/about` 的路由,而後渲染 `/about` 對應的 `page/about.tsx` 文件
  server.get('/about', (req: Req, res: http.ServerResponse) => {
    app.render(req, res, '/about')
  });

  // 匹配URL爲 `/detail/:id` 的路由,添加 `params 參數`,而後渲染 `/detail` 對應的 `page/detail.tsx` 文件
  server.get('/detail/:id', (req: Req, res: http.ServerResponse) => {
    app.render(req, res, '/detail', {
      id: req.params.id
    })
  });

  server.get('*', (req: http.IncomingMessage, res: http.ServerResponse) => {
    return handle(req, res);
  });

  server.listen(3000, (err: any) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
}
複製代碼

11.2 寫個接口

使用默認的 express

原本是要改爲 koa 的,可是路由那裏頁面會渲染,服務端渲染的頁面看網絡請求確是404,內容卻是渲染出來了(在 server-koa.ts )。。。

模塊

// server/controllers/user.js
const Router = require('express').Router();

Router.get('/userInfo/:id', (req, res) => {
  console.log('id: ', req.params.id);
  
  res.send({
    status: 200,
    data: {
      name: '小屁孩',
      sex: '男',
      age: '3'
    },
    message: ''
  });
});

module.exports = Router;
複製代碼

模塊管理

// server/controller.js
const Router = require('express').Router();
const user = require('./controllers/user');

Router.use('/user', user);

module.exports = Router;
複製代碼

掛載到 server 服務

// server.ts
// ...

function serverRun() {
  const server = express();
  // api接口
  const controllers = require('./server/controller');
  const apiRoute = ''; // '/web';
  server.use(apiRoute, controllers);
  
  // ...
}
複製代碼

接口調用

// src/pages/about.tsx
fetch('/user/userInfo/2').then(res => res.json())
.then(res => {
  console.log('fetch: ', res);
})
複製代碼

nginx 在本地部署,請求被代理(proxy_pass)到 next-test 的服務(pm2 啓動),請求響應的截圖:

十二、構建

npm script:

// package.json
...
 "scripts": {
    "dev": "node server.js",
    "dev:tsc": "tsc server.ts",
    "build": "next build",
    "start": "cross-env NODE_ENV=production node server.js"
  },
...
複製代碼

開發環境:

yarn dev
複製代碼

打包:

yarn build
複製代碼

而後產生的 next-build 文件夾 (next-build 是配置好的輸出目錄)

1三、部署

不知道爲何,把項目放在 nginx 下新建的 html 文件夾下( /usr/local/etc/nginx/html/next-test ) 啓動項目和nginx,瀏覽器訪問一直都是502(單頁面SPA的卻是正常)。。。放到別的目錄下啓動就能夠( /usr/local/website/next-test )。。。不知道是否是 macOS 的緣由,找時間在 linux 上面試試~ 因此把項目都放 /usr/local/website/ 下面了

13.1 pm2

使用 pm2,能夠在本機測試,可是最終部署仍是服務器

yarn global add pm2
複製代碼

終端進入項目目錄下,而後:

全命令

pm2 start yarn --name "next-test" -- run start
複製代碼

腳本

項目根目錄下 deploy.sh:

#deploy.sh
pm2 start yarn --name "next-test" -- run start
複製代碼

終端進入項目目錄下:

. deploy.sh 
複製代碼

幾個 pm2 命令:

  • pm2 show [id]: 顯示某個 pm2 應用的信息
  • pm2 list: 顯示全部的 pm2 應用的概覽
  • pm2 stop [id]/all: 中止某個應用,能夠多個刪除,空格隔開或者所有中止
  • pm2 delete [id]/all: 刪除某個應用,能夠多個刪除,空格隔開或者所有刪除
  • pm2 monit: 監聽 pm2 啓動的應用狀態
  • pm2 restart: 重啓
  • 等等

13.2 Nginx

pm2 服務運行程序,nginx 負責開啓 http 服務,這裏只是簡單的使用

幾個 nginx 命令

  • nginx: 啓動 nginx
  • nginx -t: 測試 nginx.conf 配置是否正確
  • nginx -s stop: 中止 nginx
  • nginx -s reload: 重啓 nginx
  • 等等

在本地測試的配置(mac)

網站代碼都放 /usr/local/website

nginx.conf 配置:

#user nobody;
worker_processes  1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    # '$status $body_bytes_sent "$http_referer" '
    # '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log logs/access.log main;

    sendfile        on;
    #tcp_nopush on;

    #keepalive_timeout 0;
    keepalive_timeout  65;

    gzip  on;

    upstream next-test {
	    server 127.0.0.1:3000; # next-test 啓動的服務端口
    }

    # next-test
    server {
        listen       80;
        server_name  localhost; #這裏配置域名

        #charset koi8-r;

        #access_log logs/host.access.log main;

        error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        location / {
            proxy_pass http://127.0.0.1:3000; #請求將會轉發到next-test的node服務下
            proxy_redirect off;
            
            # root html;
            index  index.html index.htm;
        }
    }

    # movie-db: 單頁面SPA
    server {
        listen       81;
        server_name  localhost; #這裏配置域名

        root /usr/local/website/movie-db/;

        location / {
            try_files $uri $uri/ @router;
        }

        location @router {
            rewrite ^.*$/index.html last;
        }

        # error_page 404 /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }


    # HTTPS server
    #
    #server {
    # listen 443 ssl;
    # server_name localhost;

    # ssl_certificate cert.pem;
    # ssl_certificate_key cert.key;

    # ssl_session_cache shared:SSL:1m;
    # ssl_session_timeout 5m;

    # ssl_ciphers HIGH:!aNULL:!MD5;
    # ssl_prefer_server_ciphers on;

    # location / {
    # root html;
    # index index.html index.htm;
    # }
    #}
    include servers/*;
}
複製代碼

後續要搞的東西

數據庫

MySQL/MongoDB 這兩個吧

GrahpQL

好像叫 API查詢語言,主要作接口聚合的一個東西吧;爲的是對 後端微服務接口的聚合,而後前端頁面只要請求 通過聚合的接口,就不須要屢次請求 後端微服務小接口 了;不知道理解的對不對

一些問題

  • 從一級路由進入二級路由而後刷新,瀏覽器後退,URL變了可是內容不變!這是個 bug, issues#9378, 解決

  • 在某些路由刷新後,進入其餘路由致使樣式丟失,查看請求 styles.chunk.css 並無相關的 css,可是 scss+css modules 卻是轉化好了(貌似只要開發環境會,打包後沒遇到過)

  • next.js 使用 koa 好像有點問題,服務端渲染的頁面看網絡請求那裏會是 404,可是頁面渲染出來了;要麼就是動態路由(/detail/:id) 404。。。單獨使用 koa 搭建的 node.js 服務並不會有這樣的問題( 這裏 )

參考

  • 文檔 nextjs.org
  • 還有網上一些大佬的文章
相關文章
相關標籤/搜索