让我们用NextJS?NestJS?PostgreSQL?Docker开发一个全栈应用程序。动手②(前端?Docker版)


介绍

谢谢您的等待!这是上篇动手文章的第二部分。

这次,我们将在前端(NextJS),Nginx服务器和Docker周围启动设置。

我写了这篇文章,希望对于那些从现在开始尝试使用该技术堆栈的人来说,它将是一个类似于模板的代码。

现在就开始吧!

先决条件

  • Docker已安装。 (安装Dockerhub更容易)
  • 已安装Node(为了方便使用软件包,建议使用v12.0.0。如果安装了nvm,则可以方便地切换版本。)
  • 如果您想动手操作,请阅读服务器端版本。

目标观众

  • 那些已经独立研究了Docker,React,NodeJS等的人,但是想全面地学习一切
  • 暂时,那些只想使用Typescript开发全栈应用程序的人
  • NextJS?NestJS?Docker,但还没有使用过?想要使用它的人

单击此处查看完整的代码

主要故事

上一次,我主要在服务器端(NestJS)上实现了它。使用称为NestJS的框架的特殊标记体系结构,我将与连接代码相对应的代码写入数据库和HTTP请求(GET和POST)。

这次我们使用NextJS从前端进行开发!

在此之前,作为最后一次回顾,我将发布该应用程序的配置图!

应用配置

Architecture.png

  • 要点(1):通过将nginx服务器夹在浏览器和系统之间,它被赋予了代理服务器/路由器的角色。如果在request参数中仅指定" /",则将其分配给NextJS,如果在参数中指定" / api",则将其分配给NestJS。
  • 要点(2):在Docker容器中启动应用程序。这样可以进行与环境无关的开发。

让我们用NextJS实现前端!

创建一个项目

首先,前端!点击以下命令创建一个项目!

单击此处以获取NextJS文档

1
2
3
4
# npm派はこちら
npx create-next-app
# yarn派はこちら
yarn create next-app

然后,将如下创建项目!

スクリーンショット 2021-01-05 17.26.34.png

创建typescript配置文件

但是,这次我想使用Typescript,所以让我们在根目录中创建一个tsconfig.json配置文件!

1
2
3
4
5
6
touch tsconfig.json

# yarn run devで試しに実行してみると、以下のdependenciesをインストールする
ように怒られるのでしておく

yarn add --dev typescript @types/react @types/node

如下修改tsconfig.json文件

tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "compilerOptions": {
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "esnext",
    "target": "es6",
    "jsx": "preserve",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "baseUrl": "./",
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "moduleResolution": "node"
  },
  "include": [
    "pages/**/*",
  ],
  "exclude": [
    "node_modules"
  ]
}

这一次,我设置为递归检查pages/下的所有文件夹和文件。如果要严格检查Typescript,请将其设置为strict:true

NextJS项目现在支持Typescript。之后,让我们将pages下的所有扩展名设置为.ts

同时设置ESLINT和Prettier

我想事先使开发代码更具可读性... ????

我认为有个人喜好,但让我们设置最低设置!

我使用VS Code作为编辑器,因此将包括ESLint和Prettier插件。

prettierrc.js

1
2
3
4
5
6
7
8
module.exports = {
  semi: true,
  trailingComma: 'es5',
  singleQuote: true,
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
}

eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
module.exports = {
  overrides: [
    {
      files: ['*.ts', '*.tsx'],
      parserOptions: {
        project: ['./tsconfig.json'],
        tsconfigRootDir: __dirname,
        sourceType: 'module',
      },
    },
  ],
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint',
  ],
  plugins: ['@typescript-eslint', 'react', 'prettier'],
  parser: '@typescript-eslint/parser',
  env: {
    browser: true,
    node: true,
    es6: true,
  },
  parserOptions: {
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  rules: {
    'react/prop-types': 'off',
    'react/react-in-jsx-scope': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    'prettier/prettier': 'error',
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
};

故障排除:如果ESLint或Prettier无法正常工作,请注意,在许多情况下,package.json没有必需的依赖项,或者在VS Code设置中"保存时格式化"为假。

固定页面文件

这次,我故意简化了应用程序的配置。因此,只需修改_app.tsindex.ts就足够了。

_app.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import type { AppProps /*, AppContext */ } from 'next/app';

import '../styles/global.scss';

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
  return <Component {...pageProps} />;
}

// Only uncomment this method if you have blocking data requirements for
// every single page in your application. This disables the ability to
// perform automatic static optimization, causing every page in your app to
// be server-side rendered.
//
// MyApp.getInitialProps = async (appContext: AppContext) => {
//   // calls page's `getInitialProps` and fills `appProps.pageProps`
//   const appProps = await App.getInitialProps(appContext);

//   return { ...appProps }
// }

export default MyApp;

index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import Head from 'next/head';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { MovieListType } from '../interface/movie';

export default function Home(): React.ReactElement {
  const [movieName, setMovieName] = useState<string>('');
  const [movieList, setMovieList] = useState<Array<MovieListType>>([]);

  useEffect(() => {
    console.log(movieList);
  }, [movieList]);

  useEffect(() => {
    let tmp: any = '';

    const fetchMovieList = async (): Promise<void> => {
      const { data } = await axios.get('/api/movielist');
      tmp = data;

      setMovieList(tmp);
    };

    fetchMovieList();
  }, []);

  function onMovieNameInput(event: React.ChangeEvent<HTMLInputElement>) {
    const inputValue = (event.target as HTMLInputElement).value;
    setMovieName(inputValue);
  }

  async function onClickWatchLater() {
    await axios.post('/api/movielist', {
      movieName,
    });

    const { data } = await axios.get('/api/movielist');

    setMovieList(data);
  }

  return (
    <div>
      <Head>
        I Theater
        <link rel="icon" href="/favicon.ico" />
        <link rel="preconnect" href="https://fonts.gstatic.com" />
        <link
          href="https://fonts.googleapis.com/css2?family=Unlock&display=swap"
          rel="stylesheet"
        />
      </Head>
      <div className="wrapper">
        <div className="search">
          <div>
            <h1 className="title">ITheater</h1>
          </div>
          <div>
            <input
              className="input u-margin-bottom-medium"
              value={movieName}
              onChange={onMovieNameInput}
            />
          </div>
          <div>
            <button className="btn" onClick={onClickWatchLater}>
              Watch Later!
            </button>
          </div>
        </div>
        <div>
          {movieList.map((el: MovieListType, index: number) => {
            if (movieList.length === 0) {
              return <div></div>;
            } else {
              return (
                <div key={index} className="result result__element">
                  <div className="result__row--number">{el.id}</div>
                  <div className="result__row--border"></div>
                  <div className="result__row--title">{el.movieName}</div>
                </div>
              );
            }
          })}
        </div>
      </div>
    </div>
  );
}

点是,注册新电影的名称时,请求目标的路径名称以" / api"开头。这是识别您稍后将实现的nginx服务器所需的代码!

接下来,让我们将接口定义为类型安全。基本上,它应该与服务器端实体类匹配。

movielist.ts

1
2
3
4
export type MovieListType = {
  id: number;
  movieName: string;
};

CSS设置

SCSS / SASS(CSS编译器)用于CSS文件。这次,我们采用" BEM"作为前端的设计符号。

该体系结构使用" 7-1"模式。

从此链接下载源代码并替换样式文件夹。

这样就完成了前端实现!

Nginx服务器实现

现在,假设您要开始实现nginx?

尽管我对

充满热情,但是nginx的设置仅涉及到w

真正基本的部分。

好吧,确切地说,我只能用w

触摸它

基本上,您充当中介程序,根据URI的路径决定是向客户端(∴NextJS)还是向服务器(∴NestJS)发送请求。

我想用一篇文章来更深入地研究nginx,但是root函数对于这个应用程序来说已经足够了...?,?????????????

让我们再来看一下

,在根目录中创建一个nginx文件夹,并创建一个配置文件default.conf

default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# NextJS
upstream client {
# ここでいう、clientは後ほど作成するdocker-compose.ymlで定義しているエンドポイントです。
  server client:3000;
}

# NestJS
upstream api {
# ここでいう、apiは後ほど作成するdocker-compose.ymlで定義しているエンドポイントです。
  server api:5000;
}

server {
  listen 80;

# ここで振り分けのルールを定義します
  location / {
  proxy_pass http://client;
  }

  location /api {
  rewrite /api/(.*) /$1 break;
  proxy_pass http://api;
  }
}

Docker设置

终于结束了。将每个Dockerfile放置在指定位置!

1
2
3
4
5
6
7
8
9
10
11
12
13
client/
│   ├ ...
│   └ Dockerfile.dev

nginx/
│   ├ default.conf
│   └ Dockerfile.dev

server/
│   ├ ...
│   └ Dockerfile.dev

└ docker-compose.yml

它不是用于生产的Dockerfile,因此将.dev添加到扩展名。部署到AWS或Heroku时,通常会创建一个不带Dockerfile.dev扩展名的Dockerfile。

  • /client/Dockerfile.dev

1
2
3
4
5
6
FROM node:alpine
WORKDIR /app
COPY ./package.json ./
RUN yarn
COPY . .
CMD ["yarn", "dev"]
  • /nginx/Dockerfile.dev

1
2
FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf
  • /server/Dockerfile.dev

1
2
3
4
5
6
FROM node:alpine
WORKDIR /app
COPY ./package.json ./
RUN yarn
COPY . .
CMD ["yarn", "start:dev"]

点(1):节点:alpine用于客户端和服务器。由于已在该映像中安装了节点,因此在使用节点应用程序时具有优势。
要点(2):在客户端和服务器的Dockerfile中分两步执行COPY。

1
2
3
COPY ./package.json ./
RUN yarn
COPY . .

这是为了防止您在修改主机上的代码时一一重装(学习)软件包。您可以减少开发成本。

  • docker-compose.yml

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
version: "3.9"
services:
  postgres:
    image: "postgres:latest"
    environment:
      - POSTGRES_PASSWORD=postgres_password
  client:
    build:
      context: ./client
      dockerfile: Dockerfile.dev
    volumes:
      - /app/node_modules
      - ./client:/app
  api:
    build:
      context: ./server
      dockerfile: Dockerfile.dev
    volumes:
      - /app/node_modules
      - ./server:/app
    environment:
      - PGUSER=postgres
      - PGHOST=postgres
      - PGDATABASE=postgres
      - PGPASSWORD=postgres_password
      - PGPORT=5432
  nginx:
    depends_on:
      - client
      - api
    restart: always
    build:
      context: ./nginx
      dockerfile: Dockerfile.dev
    ports:
      - "3050:80"

点(1):在客户端和api容器中设置了volumes值。通过将文件挂载在Docker的主机路径上,修改后的代码将在保存时自动更新。

1
2
3
    volumes:
      - /app/node_modules
      - ホストパス:/app

点(2):预先在api环境变量中注册postgres设置所需的值。这将允许nest.js(服务器端)从环境变量获取所需的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
  api:
    build:
      context: ./server
      dockerfile: Dockerfile.dev
    volumes:
      - /app/node_modules
      - ./server:/app
    environment:
      - PGUSER=postgres
      - PGHOST=postgres
      - PGDATABASE=postgres
      - PGPASSWORD=postgres_password
      - PGPORT=5432

服务器端,您可以像process.env.PGUSER一样获得它。

点③:将nginx与客户端和api相关联。

1
2
3
4
5
  nginx:
    depends_on:
      - client
      - api
    restart: always

通过指定

restart: always,您可以继续重新启动,直到其他容器完成启动为止。

让我们开始吧!

感谢所有阅读本文的人。谢谢你!

最后,在命运的时候...

让我们运行它!

1
2
3
4
5
docker-compose up --build

# 一回でnginxが起動できない場合があります。その場合は、もう一度以下のコマンドを叩いてください!

docker-compose down && docker-compose up

如果成功,您应该可以注册电影!

ezgif.com-video-to-gif.gif

在最后

到目前为止与我们在一起的每个人!感谢你的努力工作!
您现在是具有

的全职工??程师

这次写了这个博客,我觉得我加深了对这一领域的了解。

我希望这篇博客文章能为那些正在考虑使用NextJS,NestJS,Typescript,Docker等进行开发的人提供帮助。

老实说,NestJS仍然很深。它还支持最热门的技术堆栈,例如GraphQL,MicroService,Prisma等。

我想写一篇文章,深入探讨。

这是一个动手系列,但是下一次我将使用Kubernetes撰写有关该版本的文章。老实说,我认为这比Docker更实用!

下次在

见!