Поэкспериментируйте с Next.js рендеринга на стороне сервера

React.js
Поэкспериментируйте с Next.js рендеринга на стороне сервера

предисловие

Фреймворк SSR для рендеринга на стороне сервера на основе ReactNext.js, Фреймворк SSR для рендеринга на стороне сервера на основе VueNuxt.js.

Зачем нужен серверный рендеринг?

  • Уменьшите время белого экрана в верхней части страницы

  • SEO: Search Engine Optimization — поисковая оптимизация;
    Проще говоря, когда запрашивается веб-страница, контент, отправленный с сервера, может быть просканирован поисковым роботом.
    В настоящее время ключевые слова, найденные в поисковой системе, включены в это содержимое, поэтому информацию об этом URL-адресе легче отображать в результатах поиска~

  • рендеринг на стороне клиента: с появлением внешнего SPA большая часть контента страницы получает данные через асинхронные запросы на стороне браузера, а затем отрисовывает их;
    А то, что присылается с сервера, это просто пустая оболочка без контента, и поисковые системы, естественно, ничего не сканируют~

  • рендеринг на стороне сервера: предыдущая внутренняя структура MCV использовала шаблоны для создания контента на сервере, а затем браузер получал данные и отображал их напрямую;
    В это время содержимое веб-страницы уже следует за веб-сайтом, и для получения основного содержимого не требуется дополнительный асинхронный запрос,
    Сканер поисковой системы может сканировать данные этих неасинхронных запросов~

  • SSR для интерфейсных фреймворков:в основномВнешний и внутренний изоморфизм,Агрегация интерфейса микросервисаИ т. д.; конечно, это самое простое, как уровень рендеринга, а интерфейс — это обязанность внутреннего босса;
    Например, у всех React/Vue/Angular есть фреймворки, использующие Node.js для рендеринга данных на стороне сервера;
    Внешний интерфейс также может продолжать использовать фреймворк React/Vue/Angular, но некоторые данные генерируются на стороне сервера.


Смотрите исходный код 👇здесь

Как видите, контент уже есть, когда он приходит с сервера:

预览图

Последующие изменения маршрутизации эквивалентныОдностраничное SPA, но обновляется по определенному маршруту, то этот маршрут такжерендеринг на стороне сервераиз

1. структура каталогов 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

预览图

2. нпм-скрипт

tsc должен быть установлен

yarn install typescript -g

3. Заголовок страницы

установить заголовок страницы

import Head from 'next/head';

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

预览图

Его можно использовать там, где вам нужно изменить заголовок страницы, например/aboutстраницу нужно изменитьtitleпримерно;
Это способ пошаговой модификации: если нет — добавить, если есть — изменить

// ...
<Head>
  <title>about</title>
</Head>
// ...

4. Маршрутизация

pagesМаршруты автоматически генерируются в каталоге, и уровень может быть только один уровень следующим образом:

// 第一种写法
pages / home.tsx;
pages / detail.tsx;

styles / home.scss;
styles / detail.scss;

// 而不是
// 第二种写法
pages / home / index.tsx;
pages / home / home.scss;
  • pages/Это не может быть папка (обычно есть css, но если вы используете папку, фаза разработки нормальна, и файл scss будет сообщен, когда сборка не является компонентом React... Поэтому создайте новый снаружиstylesпапку, поместите файлы scss соответствующих компонентов маршрутизации)
  • другие папки, такие какcomponentsОб ошибке не будет сообщено, вы можете использовать второй способ написания, стиль и компонент находятся в папке

4.1 Компоненты ссылки

  • href: маршрут, напримерhref="about", он будет отображатьpage/about.tsxСодержание
  • как:hrefПереименуйте, и адрес браузера покажет этот URL;
    Маршрутизация href должна быть правильной, должен быть файл, который действительно существует в каталоге страницы.
  • prefetch: предварительная выборка, текущая страница будет использовать

href/as

URL-адрес браузера отображается как about1 следующим образом: если сервер не перехватывает маршрут, фактически отображается файл page/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 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"иззапрашивать URL-адрес запросаПеревести вas="/detail/123"из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 События маршрутизатора

Слушайте внутренние события маршрута

  • routeChangeStart(url) — срабатывает, когда маршрут начинает меняться
  • routeChangeComplete(url) — срабатывает после завершения изменения маршрута.
  • routeChangeError(err, url) — срабатывает при возникновении ошибки смены маршрута
  • beforeHistoryChange(url) — срабатывает перед изменением истории браузера
  • hashChangeStart(url) — срабатывает, когда хеш начинает меняться
  • hashChangeComplete(url) — срабатывает, когда хэш изменился

Пример:

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');
  • Мелкая маршрутизация: Мелкая маршрутизация

Измените URL-адрес страницы без выполнения getInitialProps,

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

5. Приложение

Компонент _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. Компоненты

6.1 Свойство getInitialProps

Получите метод, вы можете получить данные в этом методе и отобразить их на сервере; он может использоваться только компонентами на страницах/Документация

Если текущий маршрут обновляется, он будет выполняться на стороне сервера, если он перескакивает с другого маршрута, он будет выполняться на стороне браузера без обновления страницы;

// 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 }:

  • имя пути: фиксированная часть имени пути URL, как определено/post/:id, тогда путь здесь/post
  • запрос: объект параметров запроса для URL
  • asPath - определенный маршрут, такой как/post/:id
  • req: HTTP request object (server only)
  • res: HTTP response object (server only)
  • err: ошибка во время рендеринга

6.2 Динамический импорт

использоватьdynamicа такжеimport()Реализовать динамические компоненты;
Второй параметр dynamic — это объект, а поле загрузки — это загрузка до завершения загрузки.

// 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;

7. Псевдоним пути

использоватьbabel-plugin-module-alias, настройка веб-пакета напрямую недействительна

yarn add babel-plugin-module-alias -D
  • настроить .babelrc
{
  "plugins": [
    ["module-alias", { "src": "./src", "expose": "@" }]
  ],
  "presets": [
    "next/babel",
  ]
}
  • tsconfig.json
{
  "compilerOptions": {
    ...
    "baseUrl": "src",
    "paths": {
      "@/*": ["./*"]
    },
  },
  ...
}

8. Используйте SCSS

Официальный плагин: @zeit/next-sass

8.1 SASS

Установить

npm install --save @zeit/next-sass node-sass

или

yarn add @zeit/next-sass node-sass

настроить

После настройки используйте то же, что и использование модулей CSS в реакции.

// 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,
  },
};

9. и тд

Обратитесь к этому парнюстатья, в основномcssModulesа такжеantd按需加载Для решения проблемы совместного использования другие можно сделать в соответствии с официальным сайтом 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;

10. Управление состоянием 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. Сервер

11.1 server.ts

server.tsПосле изменения нужно вручную выполнить его в командной строкеtsc server.tsгенерироватьserver.js, можно исполнить (давайте пока что), можно и прямо в npm-скрипте написать, как автоматом скомпилировать + перезапустить сервис не знаю

next(opts: object)

opts имеет следующие свойства:

  • dev (bool): Разрабатывать ли среду разработки — по умолчанию false
  • dir (строка): Расположение следующего элемента — по умолчанию '.'
  • тихий (bool): скрыть сообщение об ошибке, содержащее информацию о сервере — по умолчанию false
  • conf (объект): то же, что и объект в next.config.js — по умолчанию {}
const next = require('next');

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

Префикс динамического актива

Например, если вы используете 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 Написать интерфейс

использовать экспресс по умолчанию

本来是要改成 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.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), скриншот ответа на запрос:

预览图

12. Построить

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папка (следующая сборка — это настроенный выходной каталог)

13. Развертывание

Не знаю зачем, положил проект в только что созданную папку html под nginx(/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

сценарий

Root Directory Project deploy.sh:

#deploy.sh
pm2 start yarn --name "next-test" -- run start

Терминал входит в каталог проекта:

. deploy.sh

预览图

Несколько команд pm2:

  • pm2 show [id]: показать информацию о приложении pm2
  • Список pm2: показывает обзор всех приложений pm2.
  • pm2 stop [id]/all: остановить приложение, вы можете удалить несколько, через пробел или остановить все
  • pm2 delete [id]/all: удалить приложение, можно удалить несколько, разделенных пробелами или удалить все
  • pm2 monit: отслеживать статус приложения, запущенного pm2
  • перезапуск pm2: перезапустить
  • так далее

13.2 Nginx

pm2 service runner, nginx отвечает за открытие сервиса http, вот простое использование

Несколько команд nginx

  • нгинкс: запустить нгинкс
  • nginx -t: проверить правильность конфигурации nginx.conf
  • NGINX -S STOP: 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-адрес меняется, но содержимое остается прежним! это ошибка,issues#9378, решить

  • После обновления некоторых маршрутов ввод других маршрутов приводит к потере стилей, см. запросstyles.chunk.cssи нет соответствующего css, ноscss+css modulesНу собственно трансформируется (пока среда разработки вроде бы и не встречалась после упаковки)

  • Похоже проблема с next.js при использовании koa.Отрисовываемая сервером страница увидит 404 из сетевого запроса, но страница отрисовывается либо это динамическая маршрутизация(/detail/:id) 404. . . У сервисов Node.js, построенных только с помощью koa, таких проблем нет (здесь )

Ссылаться на

  • Документацияnextjs.org
  • В интернете тоже есть статьи