рдЦрд░реЛрдВрдЪ рд╕реЗ рдПрдХ рдЖрдзреБрдирд┐рдХ рд╡реЗрдм рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛рдирд╛

рдЗрд╕рд▓рд┐рдП, рдЖрдкрдиреЗ рдПрдХ рдирдИ рдкрд░рд┐рдпреЛрдЬрдирд╛ рдмрдирд╛рдиреЗ рдХрд╛ рдирд┐рд░реНрдгрдп рд▓рд┐рдпрд╛ред рдФрд░ рдпрд╣ рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдПрдХ рд╡реЗрдм рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд╣реИред рдПрдХ рдореВрд▓ рдкреНрд░реЛрдЯреЛрдЯрд╛рдЗрдк рдмрдирд╛рдиреЗ рдореЗрдВ рдХрд┐рддрдирд╛ рд╕рдордп рд▓рдЧреЗрдЧрд╛? рдпрд╣ рдХрд┐рддрдирд╛ рдореБрд╢реНрдХрд┐рд▓ рд╣реИ? рдПрдХ рдЖрдзреБрдирд┐рдХ рд╡реЗрдмрд╕рд╛рдЗрдЯ рдХреЛ рд╢реБрд░реБрдЖрдд рд╕реЗ рдХреНрдпрд╛ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП?

рдЗрд╕ рд▓реЗрдЦ рдореЗрдВ, рд╣рдо рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рдЖрд░реНрдХрд┐рдЯреЗрдХреНрдЪрд░ рдХреЗ рд╕рд╛рде рдПрдХ рд╕рд╛рдзрд╛рд░рдг рд╡реЗрдм рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдмреЙрдпрд▓рд░рдкреНрд▓реЗрдЯ рдХреЛ рд░реЗрдЦрд╛рдВрдХрд┐рдд рдХрд░рдиреЗ рдХрд╛ рдкреНрд░рдпрд╛рд╕ рдХрд░реЗрдВрдЧреЗ:


рд╣рдо рдХреНрдпрд╛ рдХрд╡рд░ рдХрд░реЗрдВрдЧреЗ:

  • рдбреЙрдХ-рдХрдВрдкреЛрдЬрд╝ рдореЗрдВ рджреЗрд╡ рд╡рд╛рддрд╛рд╡рд░рдг рдХреА рд╕реНрдерд╛рдкрдирд╛ред
  • рдлреНрд▓рд╛рд╕реНрдХ рдкрд░ рдмреИрдХреЗрдВрдб рдирд┐рд░реНрдорд╛рдгред
  • рдПрдХреНрд╕рдкреНрд░реЗрд╕ рдкрд░ рдПрдХ рджреГрд╢реНрдп рдмрдирд╛рдирд╛ред
  • рд╡реЗрдмрдкреИрдХ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ JS рдмрдирд╛рдПрдБред
  • рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛, Redux рдФрд░ рд╕рд░реНрд╡рд░ рд╕рд╛рдЗрдб рд░реЗрдВрдбрд░рд┐рдВрдЧред
  • RQ рдХреЗ рд╕рд╛рде рдХрд╛рд░реНрдп рдХрддрд╛рд░ред

рдкрд░рд┐рдЪрдп


рд╡рд┐рдХрд╛рд╕ рд╕реЗ рдкрд╣рд▓реЗ, рдирд┐рд╢реНрдЪрд┐рдд рд░реВрдк рд╕реЗ, рдЖрдкрдХреЛ рдкрд╣рд▓реЗ рдпрд╣ рддрдп рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ рдХрд┐ рд╣рдо рдХреНрдпрд╛ рд╡рд┐рдХрд╕рд┐рдд рдХрд░ рд░рд╣реЗ рд╣реИрдВ! рдЗрд╕ рд▓реЗрдЦ рдХреЗ рд▓рд┐рдП рдПрдХ рдореЙрдбрд▓ рдЖрд╡реЗрджрди рдХреЗ рд░реВрдк рдореЗрдВ, рдореИрдВрдиреЗ рдПрдХ рдЖрджрд┐рдо рд╡рд┐рдХреА рдЗрдВрдЬрди рдмрдирд╛рдиреЗ рдХрд╛ рдлреИрд╕рд▓рд╛ рдХрд┐рдпрд╛ред рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рдорд╛рд░реНрдХрдбрд╛рдЙрди рдореЗрдВ рдЬрд╛рд░реА рдХрд┐рдП рдЧрдП рдХрд╛рд░реНрдб рд╣реЛрдВрдЧреЗ; рдЙрдиреНрд╣реЗрдВ рджреЗрдЦрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ рдФрд░ (рднрд╡рд┐рд╖реНрдп рдореЗрдВ рдХрднреА-рдХрднреА) рд╕рдВрдкрд╛рджрди рдкреНрд░рджрд╛рди рдХрд░рддреЗ рд╣реИрдВред рдпрд╣ рд╕рдм рд╣рдо рд╕рд░реНрд╡рд░-рд╕рд╛рдЗрдб рд░реЗрдВрдбрд░рд┐рдВрдЧ рдХреЗ рд╕рд╛рде рдПрдХ-рдкреЗрдЬ рдХреЗ рдЖрд╡реЗрджрди рдХреЗ рд░реВрдк рдореЗрдВ рд╡реНрдпрд╡рд╕реНрдерд┐рдд рдХрд░реЗрдВрдЧреЗ (рдЬреЛ рдХрд┐ рд╣рдорд╛рд░реЗ рднрд╡рд┐рд╖реНрдп рдХреА рд╕рд╛рдордЧреНрд░реА рдХреЗ рдЯреЗрд░рд╛рдмрд╛рдЗрдЯреНрд╕ рдХреЛ рдЕрдиреБрдХреНрд░рдорд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдмрд┐рд▓реНрдХреБрд▓ рдЖрд╡рд╢реНрдпрдХ рд╣реИ)ред

рдЖрдЗрдП рдЗрд╕рдХреЗ рд▓рд┐рдП рдЖрд╡рд╢реНрдпрдХ рдШрдЯрдХреЛрдВ рдкрд░ рдереЛрдбрд╝рд╛ рдФрд░ рд╡рд┐рд╕реНрддреГрдд рдирдЬрд╝рд░ рдбрд╛рд▓реЗрдВ:

  • рдХреНрд▓рд╛рдЗрдВрдЯред рдЖрдЗрдП рд░рд┐рдПрдХреНрдЯ + рд░реЗрдбрдХреНрд╕ рдмрдВрдбрд▓ рдкрд░ рдПрдХ рдкреЗрдЬ рдХрд╛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛рдПрдВ (рдпрд╛рдиреА рдкреЗрдЬ рдЯреНрд░рд╛рдВрд╕рдХреНрд╢рди рдХреЗ рд╕рд╛рде AJAX рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ), рдЬреЛ рдлреНрд░рдВрдЯ-рдПрдВрдб рд╡рд░реНрд▓реНрдб рдореЗрдВ рдмрд╣реБрдд рд╣реА рд╕рд╛рдорд╛рдиреНрдп рд╣реИред
  • рдЖрдЧреЗ рдХрд╛ рднрд╛рдЧ ред рдЖрдЗрдП рдПрдХ рд╕рд░рд▓ рдПрдХреНрд╕рдкреНрд░реЗрд╕ рд╕рд░реНрд╡рд░ рдмрдирд╛рдПрдВ рдЬреЛ рд╣рдорд╛рд░реЗ рд░рд┐рдПрдХреНрдЯ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдкреНрд░рд╕реНрддреБрдд рдХрд░реЗрдЧрд╛ (рдмреИрдХрдПрдВрдб рдореЗрдВ рд╕рднреА рдЖрд╡рд╢реНрдпрдХ рдбреЗрдЯрд╛ рдХреЛ рдЕрдирд┐рд╡рд╛рд░реНрдп рд░реВрдк рд╕реЗ рдЕрдиреБрд░реЛрдз рдХрд░рддреЗ рд╣реБрдП) рдФрд░ рдЗрд╕реЗ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреЛ рдЬрд╛рд░реА рдХрд░реЗрдЧрд╛ред
  • рдмреИрдХреЗрдВрдб ред рд╡реНрдпрд╛рдкрд╛рд░ рддрд░реНрдХ рдХреЗ рдорд╛рд╕реНрдЯрд░, рд╣рдорд╛рд░реЗ рдмреИрдХрдПрдВрдб рдПрдХ рдЫреЛрдЯрд╛ рдлреНрд▓рд╛рд╕реНрдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд╣реЛрдЧрд╛ред рд╣рдо рд▓реЛрдХрдкреНрд░рд┐рдп MongoDB рджрд╕реНрддрд╛рд╡реЗрдЬрд╝ рднрдВрдбрд╛рд░ рдореЗрдВ рдбреЗрдЯрд╛ (рд╣рдорд╛рд░реЗ рдХрд╛рд░реНрдб) рд╕рдВрдЧреНрд░рд╣реАрдд рдХрд░реЗрдВрдЧреЗ, рдФрд░ рдХрд╛рд░реНрдп рдХрддрд╛рд░ рдХреЗ рд▓рд┐рдП рдФрд░, рд╕рдВрднрд╡рддрдГ, рднрд╡рд┐рд╖реНрдп рдореЗрдВ, рдХреИрд╢рд┐рдВрдЧ рдореЗрдВ, рд╣рдо Redis рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВрдЧреЗред
  • рдХрд╛рдо рдХрд░рдиреЗрд╡рд╛рд▓рд╛ ред рдЖрд░рдХреНрдпреВ рд▓рд╛рдЗрдмреНрд░реЗрд░реА рджреНрд╡рд╛рд░рд╛ рднрд╛рд░реА рдХрд╛рд░реНрдпреЛрдВ рдХреЗ рд▓рд┐рдП рдПрдХ рдЕрд▓рдЧ рдХрдВрдЯреЗрдирд░ рд▓реЙрдиреНрдЪ рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред

рдЗрдиреНрдлреНрд░рд╛рд╕реНрдЯреНрд░рдХреНрдЪрд░: рдЧрд┐рдЯ


рд╢рд╛рдпрдж, рд╣рдо рдЗрд╕ рдмрд╛рд░реЗ рдореЗрдВ рдмрд╛рдд рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗ, рд▓реЗрдХрд┐рди, рдирд┐рд╢реНрдЪрд┐рдд рд░реВрдк рд╕реЗ, рд╣рдо рдЧрд┐рдЯ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдореЗрдВ рд╡рд┐рдХрд╛рд╕ рдХрд╛ рд╕рдВрдЪрд╛рд▓рди рдХрд░реЗрдВрдЧреЗред

git init git remote add origin git@github.com:Saluev/habr-app-demo.git git commit --allow-empty -m "Initial commit" git push 

(рдпрд╣рд╛рдВ рдЖрдкрдХреЛ рддреБрд░рдВрдд .gitignore рднрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред)

рдЕрдВрддрд┐рдо рдбреНрд░рд╛рдлреНрдЯ рдЧрд┐рддреБрдм рдкрд░ рджреЗрдЦрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред рд▓реЗрдЦ рдХрд╛ рдкреНрд░рддреНрдпреЗрдХ рднрд╛рдЧ рдПрдХ рдкреНрд░рддрд┐рдмрджреНрдз рд╕реЗ рдореЗрд▓ рдЦрд╛рддрд╛ рд╣реИ (рдореИрдВрдиреЗ рдЗрд╕реЗ рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдмрд╣реБрдд рдЕрдзрд┐рдХ рдкреБрдирд░реНрдЬрдиреНрдо рдХрд┐рдпрд╛!)ред

рдЗрдиреНрдлреНрд░рд╛рд╕реНрдЯреНрд░рдХреНрдЪрд░: docker- рд░рдЪрдирд╛


рдЖрдЗрдП рдкрд░реНрдпрд╛рд╡рд░рдг рдХреЛ рд╕реНрдерд╛рдкрд┐рдд рдХрд░рдХреЗ рд╢реБрд░реВ рдХрд░реЗрдВред рдШрдЯрдХреЛрдВ рдХреА рдкреНрд░рдЪреБрд░рддрд╛ рдХреЗ рд╕рд╛рде рдЬреЛ рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рд╣реИ, рдмрд╣реБрдд рддрд╛рд░реНрдХрд┐рдХ рд╡рд┐рдХрд╛рд╕ рд╕рдорд╛рдзрд╛рди рдХреЗ рд▓рд┐рдП docker-compose рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред

рдирд┐рдореНрди рд╕рд╛рдордЧреНрд░реА рдХреЗ docker-compose.yml рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдореЗрдВ docker-compose.yml рдлрд╝рд╛рдЗрд▓ рдЬреЛрдбрд╝реЗрдВ:

 version: '3' services: mongo: image: "mongo:latest" redis: image: "redis:alpine" backend: build: context: . dockerfile: ./docker/backend/Dockerfile environment: - APP_ENV=dev depends_on: - mongo - redis ports: - "40001:40001" volumes: - .:/code frontend: build: context: . dockerfile: ./docker/frontend/Dockerfile environment: - APP_ENV=dev - APP_BACKEND_URL=backend:40001 - APP_FRONTEND_PORT=40002 depends_on: - backend ports: - "40002:40002" volumes: - ./frontend:/app/src worker: build: context: . dockerfile: ./docker/worker/Dockerfile environment: - APP_ENV=dev depends_on: - mongo - redis volumes: - .:/code 

рдпрд╣рд╛рдБ рдХреНрдпрд╛ рд╣реЛ рд░рд╣рд╛ рд╣реИ рдкрд░ рдПрдХ рдирдЬрд╝рд░ рдбрд╛рд▓рддреЗ рд╣реИрдВред

  • рдПрдХ MongoDB рдХрдВрдЯреЗрдирд░ рдФрд░ рдПрдХ рд░реЗрдбрд┐рд╕ рдХрдВрдЯреЗрдирд░ рдмрдирд╛рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред
  • рд╣рдорд╛рд░реЗ рдмреИрдХрдПрдВрдб рдХреЗ рд▓рд┐рдП рдПрдХ рдХрдВрдЯреЗрдирд░ рдмрдирд╛рдпрд╛ рдЧрдпрд╛ рд╣реИ (рдЬрд┐рд╕рдХрд╛ рд╣рдо рдиреАрдЪреЗ рд╡рд░реНрдгрди рдХрд░рддреЗ рд╣реИрдВ)ред рдкрд░реНрдпрд╛рд╡рд░рдг рдЪрд░ APP_ENV = рджреЗрд╡ рдЗрд╕реЗ рдкрд╛рд░рд┐рдд рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ (рд╣рдо рдЗрд╕реЗ рд╕рдордЭрдиреЗ рдХреЗ рд▓рд┐рдП рджреЗрдЦреЗрдВрдЧреЗ рдХрд┐ рдлреНрд▓рд╛рд╕реНрдХ рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдХреЛ рдХреНрдпрд╛ рд▓реЛрдб рдХрд░рдирд╛ рд╣реИ), рдФрд░ рдЗрд╕рдХрд╛ рдкреЛрд░реНрдЯ 40001 рдмрд╛рд╣рд░ рдЦреБрд▓реЗрдЧрд╛ (рдЗрд╕рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рд╣рдорд╛рд░рд╛ рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреНрд▓рд╛рдЗрдВрдЯ рдПрдкреАрдЖрдИ рдкрд░ рдЬрд╛рдПрдЧрд╛)ред
  • рд╣рдорд╛рд░реЗ рдлреНрд░рдВрдЯреЗрдВрдб рдХрд╛ рдПрдХ рдХрдВрдЯреЗрдирд░ рдмрдирд╛рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред рд╡рд┐рднрд┐рдиреНрди рдкреНрд░рдХрд╛рд░ рдХреЗ рдкрд░реНрдпрд╛рд╡рд░рдг рдЪрд░ рднреА рдЗрд╕рдореЗрдВ рдЗрдВрдЬреЗрдХреНрдЯ рдХрд┐рдП рдЬрд╛рддреЗ рд╣реИрдВ, рдЬреЛ рдмрд╛рдж рдореЗрдВ рд╣рдорд╛рд░реЗ рд▓рд┐рдП рдЙрдкрдпреЛрдЧреА рд╣реЛрдВрдЧреЗ, рдФрд░ рдкреЛрд░реНрдЯ 40002 рдЦреБрд▓рддрд╛ рд╣реИред рдпрд╣ рд╣рдорд╛рд░реЗ рд╡реЗрдм рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХрд╛ рдореБрдЦреНрдп рдкреЛрд░реНрдЯ рд╣реИ: рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдореЗрдВ рд╣рдо http: // localhost: 40002 рдкрд░ рдЬрд╛рдПрдВрдЧреЗред
  • рд╣рдорд╛рд░реЗ рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рдХрд╛ рдХрдВрдЯреЗрдирд░ рдмрдирд╛рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред рдЙрд╕реЗ рдмрд╛рд╣рд░реА рдмрдВрджрд░рдЧрд╛рд╣реЛрдВ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рд╣реИ, рдФрд░ рдХреЗрд╡рд▓ MongoDB рдФрд░ Redis рдореЗрдВ рдПрдХреНрд╕реЗрд╕ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред

рдЕрдм dockerfiles рдмрдирд╛рддреЗ рд╣реИрдВред рдЕрднреА, рдбреЙрдХрд░ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЙрддреНрдХреГрд╖реНрдЯ рд▓реЗрдЦреЛрдВ рдХреЗ рдЕрдиреБрд╡рд╛рдж рдХреА рдПрдХ рд╢реНрд░реГрдВрдЦрд▓рд╛ рд╣реИрдмреЗрд░рд╛ рдореЗрдВ рдЖ рд░рд╣реА рд╣реИ - рдЖрдк рд╕рднреА рд╡рд┐рд╡рд░рдгреЛрдВ рдХреЗ рд▓рд┐рдП рд╕реБрд░рдХреНрд╖рд┐рдд рд░реВрдк рд╕реЗ рд╡рд╣рд╛рдВ рдЬрд╛ рд╕рдХрддреЗ рд╣реИрдВред

рдЪрд▓реЛ рдмреИрдХрдПрдВрдб рд╕реЗ рд╢реБрд░реВ рдХрд░рддреЗ рд╣реИрдВред

 # docker/backend/Dockerfile FROM python:stretch COPY requirements.txt /tmp/ RUN pip install -r /tmp/requirements.txt ADD . /code WORKDIR /code CMD gunicorn -w 1 -b 0.0.0.0:40001 --worker-class gevent backend.server:app 

рдпрд╣ рд╕рдордЭрд╛ рдЬрд╛рддрд╛ рд╣реИ рдХрд┐ рд╣рдо gunicorn рдлреНрд▓рд╛рд╕реНрдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдЪрд▓рд╛рддреЗ рд╣реИрдВ, рдЬреЛ рдмреИрдХрдПрдВрдб.рд╕рд░реНрд╡рд░ рдореЙрдбреНрдпреВрд▓ рдореЗрдВ рдирд╛рдо app рдиреАрдЪреЗ рдЫрд┐рдкрд╛ рд╣реИред

рдХреЛрдИ рдХрдо рдорд╣рддреНрд╡рдкреВрд░реНрдг docker/backend/.dockerignore :

 .git .idea .logs .pytest_cache frontend tests venv *.pyc *.pyo 

рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рдЖрдо рддреМрд░ рдкрд░ рдмреИрдХрдПрдВрдб рдХреЗ рд╕рдорд╛рди рд╣реЛрддрд╛ рд╣реИ, рдХреЗрд╡рд▓ рдЕрдВрдЧрд░рдЦрд╛ рдХреЗ рдмрдЬрд╛рдп рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рдПрдХ рдкрд┐рдЯ рдореЙрдбреНрдпреВрд▓ рдХрд╛ рд╕рд╛рдорд╛рдиреНрдп рдкреНрд░рдХреНрд╖реЗрдкрдг рд╣реЛрддрд╛ рд╣реИ:

 # docker/worker/Dockerfile FROM python:stretch COPY requirements.txt /tmp/ RUN pip install -r /tmp/requirements.txt ADD . /code WORKDIR /code CMD python -m worker 

рд╣рдо рд╕рднреА рдХрд╛рд░реНрдп worker/__main__.py ред

.dockerignore рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рдкреВрд░реА рддрд░рд╣ рд╕реЗ .dockerignore рдмреИрдХрдПрдВрдб рдХреЗ рд╕рдорд╛рди рд╣реИред

рдЕрдВрдд рдореЗрдВ, рджреГрд╢реНрдпред рд╣реИрдмреЗ рдкрд░ рдЙрдирдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдПрдХ рдЕрд▓рдЧ рд▓реЗрдЦ рд╣реИ , рд▓реЗрдХрд┐рди рд╕реНрдЯреИрдХрдУрд╡рд░рдлреНрд▓реЛ рдкрд░ рд╡реНрдпрд╛рдкрдХ рдЪрд░реНрдЪрд╛ рдФрд░ "рджреЛрд╕реНрддреЛрдВ рдХреА рднрд╛рд╡рдирд╛ рдореЗрдВ рдЯрд┐рдкреНрдкрдгрд┐рдпреЛрдВ рд╕реЗ рджреЗрдЦрддреЗ рд╣реБрдП, рдХреНрдпрд╛ рдпрд╣ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА 2018 рд╣реИ, рдХреНрдпрд╛ рдЕрднреА рднреА рдХреЛрдИ рд╕рд╛рдорд╛рдиреНрдп рд╕рдорд╛рдзрд╛рди рдирд╣реАрдВ рд╣реИ?" рд╡рд╣рд╛рдВ рд╕рдм рдХреБрдЫ рдЗрддрдирд╛ рд╕рд░рд▓ рдирд╣реАрдВ рд╣реИред рдореИрдВ рдбреЙрдХ рдлрд╝рд╛рдЗрд▓ рдХреЗ рдЗрд╕ рд╕рдВрд╕реНрдХрд░рдг рдкрд░ рдмрд╕ рдЧрдпрд╛ред

 # docker/frontend/Dockerfile FROM node:carbon WORKDIR /app #  package.json  package-lock.json   npm install,   . COPY frontend/package*.json ./ RUN npm install #       , #     PATH. ENV PATH /app/node_modules/.bin:$PATH #      . ADD frontend /app/src WORKDIR /app/src RUN npm run build CMD npm run start 

рдкреЗрд╢реЗрд╡рд░реЛрдВ:

  • рд╕рдм рдХреБрдЫ рдЕрдкреЗрдХреНрд╖рд╛ рдХреЗ рдЕрдиреБрд╕рд╛рд░ рдХреИрд╢ рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ (рдиреАрдЪреЗ рдХреА рдкрд░рдд рдкрд░ - рдирд┐рд░реНрднрд░рддрд╛рдПрдВ, рд╢реАрд░реНрд╖ рдкрд░ - рд╣рдорд╛рд░реЗ рдЖрд╡реЗрджрди рдХрд╛ рдирд┐рд░реНрдорд╛рдг);
  • docker-compose exec frontend npm install --save newDependency рдХрд╛рдо рдХрд░рддреА рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рдпрд╣ рд╣рдорд╛рд░реЗ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдореЗрдВ docker-compose exec frontend npm install --save newDependency рдХреЛ рд╕рдВрд╢реЛрдзрд┐рдд рдХрд░рддрд╛ рд╣реИ рдФрд░ рдЗрд╕реЗ рд╕рдВрд╢реЛрдзрд┐рдд рдХрд░рддрд╛ рд╣реИ (рдЬреЛ рдХрд┐ рдЕрдЧрд░ рд╣рдо COPY рдХрд╛ рдЙрдкрдпреЛрдЧ рдирд╣реАрдВ рдХрд░рддреЗ, рддреЛ рдмрд╣реБрдд рд╕реЗ рд▓реЛрдЧ рд╕реБрдЭрд╛рд╡ рджреЗрддреЗ рд╣реИрдВ)ред рд╡реИрд╕реЗ рднреА npm install --save newDependency рдХреЛ рдХрдВрдЯреЗрдирд░ рдХреЗ рдмрд╛рд╣рд░ рдЪрд▓рд╛рдиреЗ рдХреЗ рд▓рд┐рдП рдпрд╣ рд╡рд╛рдВрдЫрдиреАрдп рдирд╣реАрдВ рд╣реЛрдЧрд╛, рдХреНрдпреЛрдВрдХрд┐ рдирдП рдкреИрдХреЗрдЬ рдХреА рдХреБрдЫ рдирд┐рд░реНрднрд░рддрд╛рдПрдВ рдкрд╣рд▓реЗ рд╕реЗ рдореМрдЬреВрдж рд╣реЛ рд╕рдХрддреА рд╣реИрдВ рдФрд░ рдЙрдиреНрд╣реЗрдВ рдПрдХ рдЕрд▓рдЧ рдкреНрд▓реЗрдЯрдлрд╝реЙрд░реНрдо рдХреЗ рддрд╣рдд рдмрдирд╛рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ (docker рдХреЗ рдЕрдВрджрд░ рдПрдХ рдФрд░ рд╣рдорд╛рд░реЗ рдХрд╛рдо рдХрд░рдиреЗ рд╡рд╛рд▓реЗ macbook рдХреЗ рддрд╣рдд рдирд╣реАрдВ, рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП ), рдФрд░ рдлрд┐рд░ рднреА рд╣рдо рдЖрдо рддреМрд░ рдкрд░ рд╡рд┐рдХрд╛рд╕ рдорд╢реАрди рдкрд░ рдиреЛрдб рдХреА рдЙрдкрд╕реНрдерд┐рддрд┐ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рдЪрд╛рд╣рддреЗ рд╣реИрдВред рдПрдХ рдбреЙрдХрд░ рдЙрди рд╕рднреА рдкрд░ рд░рд╛рдЬ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП!

рдЦреИрд░ рдФрд░ рдирд┐рд╢реНрдЪрд┐рдд рд░реВрдк рд╕реЗ docker/frontend/.dockerignore :

 .git .idea .logs .pytest_cache backend worker tools node_modules npm-debug tests venv 

рддреЛ, рд╣рдорд╛рд░рд╛ рдХрдВрдЯреЗрдирд░ рдлреНрд░реЗрдо рддреИрдпрд╛рд░ рд╣реИ рдФрд░ рдЖрдк рдЗрд╕реЗ рд╕рд╛рдордЧреНрд░реА рд╕реЗ рднрд░ рд╕рдХрддреЗ рд╣реИрдВ!

рдмреИрдХрдПрдВрдб: рдлреНрд▓рд╛рд╕реНрдХ рдлреНрд░реЗрдорд╡рд░реНрдХ


рдЖрд╡рд╢реНрдпрдХрддрд╛рдУрдВ рдХреЗ рд▓рд┐рдП flask , flask-cors , gunicorn рдФрд░ gunicorn рдЬреЛрдбрд╝реЗрдВред gunicorn рдФрд░ backend/server.py gunicorn рдПрдХ рд╕рд╛рдзрд╛рд░рдг рдлреНрд▓рд╛рд╕реНрдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛рдПрдВред

 # backend/server.py import os.path import flask import flask_cors class HabrAppDemo(flask.Flask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # CORS        #    ,      # (  Access-Control-Origin  ). #   - . flask_cors.CORS(self) app = HabrAppDemo("habr-app-demo") env = os.environ.get("APP_ENV", "dev") print(f"Starting application in {env} mode") app.config.from_object(f"backend.{env}_settings") 

рд╣рдордиреЗ рдлреНрд▓рд╛рд╕реНрдХ рдХреЛ backend.{env}_settings рдлрд╝рд╛рдЗрд▓ рд╕реЗ рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдЦреАрдВрдЪрдиреЗ рдХреЗ рд▓рд┐рдП рдХрд╣рд╛ backend.{env}_settings , рдЬрд┐рд╕рдХрд╛ рдЕрд░реНрде рд╣реИ рдХрд┐ рд╣рдореЗрдВ рд╣рд░ рдЪреАрдЬрд╝ рдХреЛ backend/dev_settings.py рд▓рд┐рдП рдПрдХ (рдХрдо рд╕реЗ рдХрдо рдЦрд╛рд▓реА) рдлрд╝рд╛рдЗрд▓ backend/dev_settings.py рдмрдирд╛рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрдЧреАред

рдЕрдм рд╣рдо рдЖрдзрд┐рдХрд╛рд░рд┐рдХ рддреМрд░ рдкрд░ рд╣рдорд╛рд░реЗ рдмреИрдХрдПрдВрдб рдХреЛ рдмрдврд╝рд╛ рд╕рдХрддреЗ рд╣реИрдВ!

 habr-app-demo$ docker-compose up backend ... backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Starting gunicorn 19.9.0 backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Listening at: http://0.0.0.0:40001 (6) backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Using worker: gevent backend_1 | [2019-02-23 10:09:03 +0000] [9] [INFO] Booting worker with pid: 9 

рд╣рдо рдЖрдЧреЗ рдмрдврд╝рддреЗ рд╣реИрдВред

рд╕реАрдорд╛рд╡рд░реНрддреА: рдПрдХреНрд╕рдкреНрд░реЗрд╕ рд░реВрдкрд░реЗрдЦрд╛


рдЖрдЗрдП рдкреИрдХреЗрдЬ рдмрдирд╛рдХрд░ рд╢реБрд░реБрдЖрдд рдХрд░реЗрдВред рдлреНрд░рдВрдЯрдПрдВрдб рдлреЛрд▓реНрдбрд░ npm init рдФрд░ рдЙрд╕рдореЗрдВ npm init рдЪрд▓рд╛рдХрд░, рдХреБрдЫ рдЕрдкрд░рд┐рд╣рд╛рд░реНрдп рдкреНрд░рд╢реНрдиреЛрдВ рдХреЗ рдмрд╛рдж, рд╣рдореЗрдВ рддреИрдпрд╛рд░ рдкреИрдХреЗрдЬ рдорд┐рд▓рддрд╛ рд╣реИред рдЖрддреНрдорд╛ рдореЗрдВ рдЯреИрдЧ рдХрд░реЗрдВ

 { "name": "habr-app-demo", "version": "0.0.1", "description": "This is an app demo for Habr article.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/Saluev/habr-app-demo.git" }, "author": "Tigran Saluev <tigran@saluev.com>", "license": "MIT", "bugs": { "url": "https://github.com/Saluev/habr-app-demo/issues" }, "homepage": "https://github.com/Saluev/habr-app-demo#readme" } 

рднрд╡рд┐рд╖реНрдп рдореЗрдВ, рд╣рдореЗрдВ рдбреЗрд╡рд▓рдкрд░ рдХреА рдорд╢реАрди рдкрд░ Node.js рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рд╣реИ (рд╣рд╛рд▓рд╛рдВрдХрд┐ рд╣рдо рдЕрднреА рднреА рдЪрдХрдорд╛ рджреЗ рд╕рдХрддреЗ рд╣реИрдВ рдФрд░ рдбреЙрдХрд░ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ npm init рд╢реБрд░реВ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рд▓реЗрдХрд┐рди рдУрд╣ рдЕрдЪреНрдЫреА рддрд░рд╣ рд╕реЗ)ред

Dockerfile рд╣рдордиреЗ Dockerfile npm run build рдФрд░ npm run start npm run build рдЙрд▓реНрд▓реЗрдЦ рдХрд┐рдпрд╛ рд╣реИ - рдЖрдкрдХреЛ package.json рдореЗрдВ рдЙрдкрдпреБрдХреНрдд рдХрдорд╛рдВрдб рдЬреЛрдбрд╝рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред Dockerfile :

 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,6 +4,8 @@ "description": "This is an app demo for Habr article.", "main": "index.js", "scripts": { + "build": "echo 'build'", + "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { 

build рдХрдорд╛рдВрдб рдЕрднреА рддрдХ рдХреБрдЫ рднреА рдирд╣реАрдВ рдХрд░рддрд╛ рд╣реИ, рд▓реЗрдХрд┐рди рдпрд╣ рдЕрднреА рднреА рд╣рдорд╛рд░реЗ рд▓рд┐рдП рдЙрдкрдпреЛрдЧреА рд╣реЛрдЧрд╛ред

рдПрдХреНрд╕рдкреНрд░реЗрд╕ рдирд┐рд░реНрднрд░рддрд╛ рдЬреЛрдбрд╝реЗрдВ рдФрд░ index.js рдореЗрдВ рдПрдХ рд╕рд░рд▓ рдЕрдиреБрдкреНрд░рдпреЛрдЧ рдмрдирд╛рдПрдВ:

 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,5 +17,8 @@ "bugs": { "url": "https://github.com/Saluev/habr-app-demo/issues" }, - "homepage": "https://github.com/Saluev/habr-app-demo#readme" + "homepage": "https://github.com/Saluev/habr-app-demo#readme", + "dependencies": { + "express": "^4.16.3" + } } 

 // frontend/index.js const express = require("express"); app = express(); app.listen(process.env.APP_FRONTEND_PORT); app.get("*", (req, res) => { res.send("Hello, world!") }); 

рдЕрдм docker-compose up frontend рд╣рдорд╛рд░реЗ рд╕реАрдиреНрд╕ рдХреЛ рдмрдврд╝рд╛рддрд╛ рд╣реИ! рдЗрд╕рдХреЗ рдЕрд▓рд╛рд╡рд╛, http: // localhost: 40002 рдкрд░ , рдХреНрд▓рд╛рд╕рд┐рдХ "рд╣реИрд▓реЛ, рджреБрдирд┐рдпрд╛" рдкрд╣рд▓реЗ рд╣реА рджрд┐рдЦрд╛рд╡рд╛ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред

рд╕реАрдорд╛: рд╡реЗрдмрдкреИрдХ рдФрд░ рд░рд┐рдПрдХреНрдЯ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рд╕рд╛рде рдирд┐рд░реНрдорд╛рдг


рдпрд╣ рд╣рдорд╛рд░реЗ рдЖрд╡реЗрджрди рдореЗрдВ рд╕рд╛рджреЗ рдкрд╛рда рд╕реЗ рдЕрдзрд┐рдХ рдХреБрдЫ рдЪрд┐рддреНрд░рд┐рдд рдХрд░рдиреЗ рдХрд╛ рд╕рдордп рд╣реИред рдЗрд╕ рдЕрдиреБрднрд╛рдЧ рдореЗрдВ, рд╣рдо App рдХрд╛ рд╕рдмрд╕реЗ рд╕рд░рд▓ рд░рд┐рдПрдХреНрдЯ рдШрдЯрдХ рдЬреЛрдбрд╝реЗрдВрдЧреЗ рдФрд░ рдЕрд╕реЗрдВрдмрд▓реА рдХреЛ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░реЗрдВрдЧреЗред

рдЬрдм рд░рд┐рдПрдХреНрдЯ рдореЗрдВ рдкреНрд░реЛрдЧреНрд░рд╛рдорд┐рдВрдЧ рдХрд░рддреЗ рд╣реИрдВ, рддреЛ рдлреЙрд░реНрдо рдХреЗ рд╕рд┐рдВрдЯреИрдХреНрдЯрд┐рдХ рдХрдВрд╕реНрдЯреНрд░рдХреНрд╢рди рджреНрд╡рд╛рд░рд╛ рд╡рд┐рд╕реНрддрд╛рд░рд┐рдд рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдХреА рдПрдХ рдмреЛрд▓реА JSX рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдирд╛ рдмрд╣реБрдд рд╕реБрд╡рд┐рдзрд╛рдЬрдирдХ рд╣реИ

 render() { return <MyButton color="blue">{this.props.caption}</MyButton>; } 

рд╣рд╛рд▓рд╛рдВрдХрд┐, рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдЗрдВрдЬрди рдЗрд╕реЗ рдирд╣реАрдВ рд╕рдордЭрддреЗ рд╣реИрдВ, рдЗрд╕рд▓рд┐рдП рдЖрдорддреМрд░ рдкрд░ рдмрд┐рд▓реНрдб рдЪрд░рдг рдХреЛ рдлреНрд░рдВрдЯреЗрдВрдб рдореЗрдВ рдЬреЛрдбрд╝рд╛ рдЬрд╛рддрд╛ рд╣реИред рд╡рд┐рд╢реЗрд╖ рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рд╕рдВрдХрд▓рдХ (рд╣рд╛рдБ-рд╣рд╛рдБ) рдмрджрд▓реА рд╣реБрдИ рдХреНрд▓рд╛рд╕рд┐рдХ рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдореЗрдВ рд╕рд┐рдВрдЯреИрдХреНрдЯрд┐рдХ рдЪреАрдиреА рдХреЛ рдЪрд╛рд▓реВ рдХрд░рддреЗ рд╣реИрдВ, рдЖрдпрд╛рддреЛрдВ рдХреЛ рд╕рдВрднрд╛рд▓рддреЗ рд╣реИрдВ, рдЫреЛрдЯрд╛ рдХрд░рддреЗ рд╣реИрдВ рдФрд░ рдЗрд╕реА рддрд░рд╣ред



2014 рд╕рд╛рд▓ред apt-cache search рдЬрд╛рд╡рд╛

рддреЛ, рд╕рдмрд╕реЗ рд╕рд░рд▓ рд░рд┐рдПрдХреНрдЯ рдШрдЯрдХ рдмрд╣реБрдд рд╕рд░рд▓ рджрд┐рдЦрддрд╛ рд╣реИред

 // frontend/src/components/app.js import React, {Component} from 'react' class App extends Component { render() { return <h1>Hello, world!</h1> } } export default App 

рд╡рд╣ рдмрд╕ рдПрдХ рдЕрдзрд┐рдХ рдареЛрд╕ рдкрд┐рди рдХреЗ рд╕рд╛рде рд╣рдорд╛рд░реЗ рдЕрднрд┐рд╡рд╛рджрди рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░реЗрдЧрд╛ред

рд╣рдорд╛рд░реЗ рднрд╡рд┐рд╖реНрдп рдХреЗ рдЕрдиреБрдкреНрд░рдпреЛрдЧ рдХреА рдиреНрдпреВрдирддрдо HTML рд░реВрдкрд░реЗрдЦрд╛ рдпреБрдХреНрдд рдлрд╝рд╛рдЗрд▓ frontend/src/template.js рдЬреЛрдбрд╝реЗрдВ:

 // frontend/src/template.js export default function template(title) { let page = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>${title}</title> </head> <body> <div id="app"></div> <script src="/dist/client.js"></script> </body> </html> `; return page; } 

рдПрдХ рдЧреНрд░рд╛рд╣рдХ рдкреНрд░рд╡рд┐рд╖реНрдЯрд┐ рдмрд┐рдВрджреБ рдЬреЛрдбрд╝реЗрдВ:

 // frontend/src/client.js import React from 'react' import {render} from 'react-dom' import App from './components/app' render( <App/>, document.querySelector('#app') ); 

рдЗрд╕ рд╕рднреА рд╕реБрдВрджрд░рддрд╛ рдХрд╛ рдирд┐рд░реНрдорд╛рдг рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдореЗрдВ рдЪрд╛рд╣рд┐рдП:

рд╡реЗрдмрдкреИрдХ рдЬреЗрдПрд╕ рдХреЗ рд▓рд┐рдП рдПрдХ рдлреИрд╢рдиреЗрдмрд▓ рдпреБрд╡рд╛ рдмрд┐рд▓реНрдбрд░ рд╣реИ (рд╣рд╛рд▓рд╛рдВрдХрд┐ рдореИрдВрдиреЗ рддреАрди рдШрдВрдЯреЗ рдХреЗ рд▓рд┐рдП рджреГрд╢реНрдпрдкрдЯрд▓ рдкрд░ рд▓реЗрдЦ рдирд╣реАрдВ рдкрдврд╝рд╛ рд╣реИ, рдЗрд╕рд▓рд┐рдП рдореИрдВ рдлреИрд╢рди рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдирд┐рд╢реНрдЪрд┐рдд рдирд╣реАрдВ рд╣реВрдВ);
рдЬреЗрдПрд╕рдПрдХреНрд╕ рдЬреИрд╕реЗ рд╕рднреА рдкреНрд░рдХрд╛рд░ рдХреЗ рд▓реЛрд╢рди рдХреЗ рд▓рд┐рдП рдмреИрдмрд▓ рдПрдХ рдХрдВрдкрд╛рдЗрд▓рд░ рд╣реИ, рдФрд░ рдПрдХ рд╣реА рд╕рдордп рдореЗрдВ рд╕рднреА IE рдорд╛рдорд▓реЛрдВ рдХреЗ рд▓рд┐рдП рдПрдХ рдкреЙрд▓реАрдлрд╝рд┐рд▓ рдкреНрд░рджрд╛рддрд╛ рд╣реИред

рдпрджрд┐ рдлреНрд░рдВрдЯрдПрдВрдб рдХрд╛ рдкрд┐рдЫрд▓рд╛ рдЪрд▓рдирд╛ рдЕрднреА рднреА рдЪрд▓ рд░рд╣рд╛ рд╣реИ, рддреЛ рдЖрдкрдХреЛ рдмрд╕ рдЗрддрдирд╛ рдХрд░рдирд╛ рд╣реИ

 docker-compose exec frontend npm install --save \ react \ react-dom docker-compose exec frontend npm install --save-dev \ webpack \ webpack-cli \ babel-loader \ @babel/core \ @babel/polyfill \ @babel/preset-env \ @babel/preset-react 

рдирдИ рдирд┐рд░реНрднрд░рддрд╛ рд╕реНрдерд╛рдкрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдПред рдЕрдм рд╡реЗрдмрдкреИрдХ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░реЗрдВ:

 // frontend/webpack.config.js const path = require("path"); //  . clientConfig = { mode: "development", entry: { client: ["./src/client.js", "@babel/polyfill"] }, output: { path: path.resolve(__dirname, "../dist"), filename: "[name].js" }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] } }; //  .     : // 1. target: "node" -      import path. // 2.   ..,    ../dist --   //    ,   ! serverConfig = { mode: "development", target: "node", entry: { server: ["./index.js", "@babel/polyfill"] }, output: { path: path.resolve(__dirname, ".."), filename: "[name].js" }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] } }; module.exports = [clientConfig, serverConfig]; 

frontend/.babelrc рдХрд╛рд░реНрдп рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ frontend/.babelrc рдХреЛ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ:

 { "presets": ["@babel/env", "@babel/react"] } 

рдЕрдВрдд рдореЗрдВ, рд╣рдорд╛рд░реЗ npm run build рдХрдорд╛рдВрдб рдХреЛ рд╕рд╛рд░реНрдердХ рдмрдирд╛рдПрдВ:

 // frontend/package.json ... "scripts": { "build": "webpack", "start": "node /app/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, ... 

рдЕрдм рд╣рдорд╛рд░рд╛ рдореБрд╡рдХреНрдХрд┐рд▓, рдкреЙрд▓реАрдлрд╝рд┐рд▓реНрд╕ рдФрд░ рдЙрд╕рдХреЗ рд╕рднреА рдЖрд╢реНрд░рд┐рддреЛрдВ рдХреЗ рдПрдХ рдмрдВрдбрд▓ рдХреЗ рд╕рд╛рде, рдмреЗрдмрд▓, ../dist/client.js рдФрд░ рд╕рд┐рд▓рд╡рдЯреЛрдВ рдХреЛ рдПрдХ рдЕрдЦрдВрдб ../dist/client.js рдлрд╝рд╛рдЗрд▓ рдореЗрдВ ../dist/client.js ред рдЗрд╕реЗ рд╣рдорд╛рд░реЗ рдПрдХреНрд╕рдкреНрд░реЗрд╕ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдореЗрдВ рдПрдХ рд╕реНрдерд┐рд░ рдлрд╝рд╛рдЗрд▓ рдХреЗ рд░реВрдк рдореЗрдВ рдЕрдкрд▓реЛрдб рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдЬреЛрдбрд╝реЗрдВ, рдФрд░ рдбрд┐рдлрд╝реЙрд▓реНрдЯ рдорд╛рд░реНрдЧ рдореЗрдВ рд╣рдо рдЕрдкрдирд╛ HTML рд╡рд╛рдкрд╕ рдХрд░рдирд╛ рд╢реБрд░реВ рдХрд░ рджреЗрдВрдЧреЗ:

 // frontend/index.js // ,    , //  - . import express from 'express' import template from './src/template' let app = express(); app.use('/dist', express.static('../dist')); app.get("*", (req, res) => { res.send(template("Habr demo app")); }); app.listen(process.env.APP_FRONTEND_PORT); 

рд╕рдлрд▓рддрд╛! рдЕрдм, рдпрджрд┐ рд╣рдо docker-compose up --build frontend рдЪрд▓рд╛рддреЗ рд╣реИрдВ, рддреЛ рд╣рдо "рд╣реЗрд▓реЛ, рд╡рд░реНрд▓реНрдб!" рджреЗрдЦреЗрдВрдЧреЗред рдПрдХ рдирдП, рдЪрдордХрджрд╛рд░ рдЖрд╡рд░рдг рдореЗрдВ, рдФрд░ рдпрджрд┐ рдЖрдкрдХреЗ рдкрд╛рд╕ React Developer Tools рдПрдХреНрд╕рдЯреЗрдВрд╢рди рд╕реНрдерд╛рдкрд┐рдд рд╣реИ ( Chrome , Firefox ), рддреЛ рдбреЗрд╡рд▓рдкрд░ рдЯреВрд▓ рдореЗрдВ рдПрдХ React рдШрдЯрдХ рдЯреНрд░реА рднреА рд╣реИ:



рдмреИрдХрдПрдВрдб: MongoDB рдореЗрдВ рдбреЗрдЯрд╛


рд╣рдорд╛рд░реЗ рдЖрд╡реЗрджрди рдореЗрдВ рдЬреАрд╡рди рдХреЛ рдЖрдЧреЗ рдмрдврд╝рд╛рдиреЗ рдФрд░ рд╕рд╛рдБрд╕ рд▓реЗрдиреЗ рд╕реЗ рдкрд╣рд▓реЗ, рдЖрдкрдХреЛ рдкрд╣рд▓реЗ рдЗрд╕реЗ рдмреИрдХрдПрдВрдб рдореЗрдВ рд╕рд╛рдБрд╕ рд▓реЗрдирд╛ рдЪрд╛рд╣рд┐рдПред рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рд╣рдо рдорд╛рд░реНрдХрдбрд╛рдЙрди рдореЗрдВ рдЪрд┐рд╣реНрдирд┐рдд рдХрд┐рдП рдЧрдП рдХрд╛рд░реНрдб рдХреЛ рд╕реНрдЯреЛрд░ рдХрд░рдиреЗ рдЬрд╛ рд░рд╣реЗ рдереЗ - рдпрд╣ рдХрд░рдиреЗ рдХрд╛ рд╕рдордп рд╣реИред

рдЬрдмрдХрд┐ рдЕрдЬрдЧрд░ рдореЗрдВ MongoDB рдХреЗ рд▓рд┐рдП рдУрдЖрд░рдПрдо рд╣реИрдВ , рдореИрдВ рдУрдЖрд░рдПрдо рдХреЗ рдЙрдкрдпреЛрдЧ рдХреЛ рд╢рд╛рддрд┐рд░ рдорд╛рдирддрд╛ рд╣реВрдВ рдФрд░ рдореИрдВ рдЖрдкрдХреЗ рд▓рд┐рдП рдЙрдкрдпреБрдХреНрдд рд╕рдорд╛рдзрд╛рдиреЛрдВ рдХрд╛ рдЕрдзреНрдпрдпрди рдЫреЛрдбрд╝ рджреЗрддрд╛ рд╣реВрдВред рдЗрд╕рдХреЗ рдмрдЬрд╛рдп, рд╣рдо рдХрд╛рд░реНрдб рдФрд░ рд╕рд╛рде рд╡рд╛рд▓реЗ рдбреАрдПрдУ рдХреЗ рд▓рд┐рдП рдПрдХ рд╕рд╛рдзрд╛рд░рдг рд╡рд░реНрдЧ рдмрдирд╛рдПрдВрдЧреЗ:

 # backend/storage/card.py import abc from typing import Iterable class Card(object): def __init__(self, id: str = None, slug: str = None, name: str = None, markdown: str = None, html: str = None): self.id = id self.slug = slug #    self.name = name self.markdown = markdown self.html = html class CardDAO(object, metaclass=abc.ABCMeta): @abc.abstractmethod def create(self, card: Card) -> Card: pass @abc.abstractmethod def update(self, card: Card) -> Card: pass @abc.abstractmethod def get_all(self) -> Iterable[Card]: pass @abc.abstractmethod def get_by_id(self, card_id: str) -> Card: pass @abc.abstractmethod def get_by_slug(self, slug: str) -> Card: pass class CardNotFound(Exception): pass 

(рдпрджрд┐ рдЖрдк рдЕрднреА рднреА рдкрд╛рдпрдерди рдореЗрдВ рдЯрд╛рдЗрдк рдПрдиреЛрдЯреЗрд╢рди рдХрд╛ рдЙрдкрдпреЛрдЧ рдирд╣реАрдВ рдХрд░рддреЗ рд╣реИрдВ, рддреЛ рдЗрди рд▓реЗрдЦреЛрдВ рдХреЛ рдЕрд╡рд╢реНрдп рджреЗрдЦреЗрдВ!)

рдЕрдм рдЪрд▓рд┐рдП CardDAO рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдХрд╛ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдмрдирд╛рддреЗ рд╣реИрдВ рдЬреЛ pymongo рд╕реЗ Database рдСрдмреНрдЬреЗрдХреНрдЯ рд▓реЗрддрд╛ рд╣реИ (рд╣рд╛рдБ, pymongo рдХреЛ requirements.txt рдЬреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рдордпред pymongo ):

 # backend/storage/card_impl.py from typing import Iterable import bson import bson.errors from pymongo.collection import Collection from pymongo.database import Database from backend.storage.card import Card, CardDAO, CardNotFound class MongoCardDAO(CardDAO): def __init__(self, mongo_database: Database): self.mongo_database = mongo_database # , slug   . self.collection.create_index("slug", unique=True) @property def collection(self) -> Collection: return self.mongo_database["cards"] @classmethod def to_bson(cls, card: Card): # MongoDB     BSON.  #       BSON- #  ,      . result = { k: v for k, v in card.__dict__.items() if v is not None } if "id" in result: result["_id"] = bson.ObjectId(result.pop("id")) return result @classmethod def from_bson(cls, document) -> Card: #   ,     #     ,     #  .    id    # ,   -   . document["id"] = str(document.pop("_id")) return Card(**document) def create(self, card: Card) -> Card: card.id = str(self.collection.insert_one(self.to_bson(card)).inserted_id) return card def update(self, card: Card) -> Card: card_id = bson.ObjectId(card.id) self.collection.update_one({"_id": card_id}, {"$set": self.to_bson(card)}) return card def get_all(self) -> Iterable[Card]: for document in self.collection.find(): yield self.from_bson(document) def get_by_id(self, card_id: str) -> Card: return self._get_by_query({"_id": bson.ObjectId(card_id)}) def get_by_slug(self, slug: str) -> Card: return self._get_by_query({"slug": slug}) def _get_by_query(self, query) -> Card: document = self.collection.find_one(query) if document is None: raise CardNotFound() return self.from_bson(document) 

рдмреИрдХрдПрдВрдб рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдореЗрдВ рдореЛрдВрдЧрд╛ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░реЗрд╢рди рдХреЛ рдкрдВрдЬреАрдХреГрдд рдХрд░рдиреЗ рдХрд╛ рд╕рдордпред рд╣рдордиреЗ рдЕрдкрдиреЗ рдХрдВрдЯреЗрдирд░ рдХрд╛ рдирд╛рдо рдореЛрдВрдЧреЛ mongo рд╕рд╛рде рд░рдЦрд╛ рд╣реИ, рдЗрд╕рд▓рд┐рдП MONGO_HOST = "mongo"

 --- a/backend/dev_settings.py +++ b/backend/dev_settings.py @@ -0,0 +1,3 @@ +MONGO_HOST = "mongo" +MONGO_PORT = 27017 +MONGO_DATABASE = "core" 

рдЕрдм рд╣рдореЗрдВ MongoCardDAO рдмрдирд╛рдиреЗ рдХреА рдЬрд░реВрд░рдд рд╣реИ рдФрд░ рдлреНрд▓рд╛рд╕реНрдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдЗрд╕рдХреЗ рд▓рд┐рдП рдЙрдкрдпреЛрдЧ рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рд╣рд╛рд▓рд╛рдБрдХрд┐ рдЕрдм рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рдСрдмреНрдЬреЗрдХреНрдЯреНрд╕ (рд╕реЗрдЯрд┐рдВрдЧреНрд╕ тЖТ pymongo рдХреНрд▓рд╛рдЗрдВрдЯ тЖТ pymongo рдбреЗрдЯрд╛рдмреЗрд╕ тЖТ MongoCardDAO ) рдХреА рдПрдХ рдмрд╣реБрдд рд╣реА рд╕рд░рд▓ рдкрджрд╛рдиреБрдХреНрд░рдо рд╣реИ, рдЪрд▓реЛ рддреБрд░рдВрдд рдПрдХ рдХреЗрдВрджреНрд░реАрдХреГрдд рд░рд╛рдЬрд╛ рдШрдЯрдХ рдмрдирд╛рддреЗ рд╣реИрдВ рдЬреЛ рдирд┐рд░реНрднрд░рддрд╛ рдЗрдВрдЬреЗрдХреНрд╢рди рдХрд░рддрд╛ рд╣реИ (рдпрд╣ рдлрд┐рд░ рд╕реЗ рдХрд╛рдо рдореЗрдВ рдЖрдПрдЧрд╛ рдЬрдм рд╣рдо рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рдФрд░ рдЯреВрд▓ рдХрд░рддреЗ рд╣реИрдВ)ред

 # backend/wiring.py import os from pymongo import MongoClient from pymongo.database import Database import backend.dev_settings from backend.storage.card import CardDAO from backend.storage.card_impl import MongoCardDAO class Wiring(object): def __init__(self, env=None): if env is None: env = os.environ.get("APP_ENV", "dev") self.settings = { "dev": backend.dev_settings, # (    # ,   !) }[env] #        . #        DI,  . self.mongo_client: MongoClient = MongoClient( host=self.settings.MONGO_HOST, port=self.settings.MONGO_PORT) self.mongo_database: Database = self.mongo_client[self.settings.MONGO_DATABASE] self.card_dao: CardDAO = MongoCardDAO(self.mongo_database) 


рдлреНрд▓рд╛рд╕реНрдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдореЗрдВ рдПрдХ рдирдпрд╛ рдорд╛рд░реНрдЧ рдЬреЛрдбрд╝рдиреЗ рдФрд░ рджреГрд╢реНрдп рдХрд╛ рдЖрдирдВрдж рд▓реЗрдиреЗ рдХрд╛ рд╕рдордп!

 # backend/server.py import os.path import flask import flask_cors from backend.storage.card import CardNotFound from backend.wiring import Wiring env = os.environ.get("APP_ENV", "dev") print(f"Starting application in {env} mode") class HabrAppDemo(flask.Flask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) flask_cors.CORS(self) self.wiring = Wiring(env) self.route("/api/v1/card/<card_id_or_slug>")(self.card) def card(self, card_id_or_slug): try: card = self.wiring.card_dao.get_by_slug(card_id_or_slug) except CardNotFound: try: card = self.wiring.card_dao.get_by_id(card_id_or_slug) except (CardNotFound, ValueError): return flask.abort(404) return flask.jsonify({ k: v for k, v in card.__dict__.items() if v is not None }) app = HabrAppDemo("habr-app-demo") app.config.from_object(f"backend.{env}_settings") 

docker-compose up --build backend рд╕рд╛рде рдкреБрдирдГ рдЖрд░рдВрдн рдХрд░реЗрдВ:



рдЙрдлрд╝тАж рдУрд╣, рдмрд┐рд▓реНрдХреБрд▓ред рд╣рдореЗрдВ рд╕рд╛рдордЧреНрд░реА рдЬреЛрдбрд╝рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ! рд╣рдо рдЯреВрд▓ рдлрд╝реЛрд▓реНрдбрд░ рдХреЛ рдЦреЛрд▓реЗрдВрдЧреЗ рдФрд░ рдЙрд╕рдореЗрдВ рдПрдХ рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдЬреЛрдбрд╝реЗрдВрдЧреЗ рдЬрд┐рд╕рдореЗрдВ рдПрдХ рдЯреЗрд╕реНрдЯ рдХрд╛рд░реНрдб рдЬреЛрдбрд╝рд╛ рдЬрд╛рдПрдЧрд╛:

 # tools/add_test_content.py from backend.storage.card import Card from backend.wiring import Wiring wiring = Wiring() wiring.card_dao.create(Card( slug="helloworld", name="Hello, world!", markdown=""" This is a hello-world page. """)) 

docker-compose exec backend python -m tools.add_test_content рд╣рдорд╛рд░реЗ рдореЛрдВрдЧрд╛ рдХреЛ docker-compose exec backend python -m tools.add_test_content рдХрдВрдЯреЗрдирд░ рдХреЗ рдЕрдВрджрд░ рд╕реЗ рд╕рд╛рдордЧреНрд░реА рд╕реЗ рднрд░ docker-compose exec backend python -m tools.add_test_content ред



рд╕рдлрд▓рддрд╛! рдЕрдм рд╕рд╛рдордиреЗ рдХреЗ рдЫреЛрд░ рдкрд░ рдЗрд╕рдХрд╛ рд╕рдорд░реНрдерди рдХрд░рдиреЗ рдХрд╛ рд╕рдордп рд╣реИред

рд╕реАрдорд╛рд╡рд░реНрддреА: Redux


рдЕрдм рд╣рдо рд░реВрдЯ /card/:id_or_slug рдмрдирд╛рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ, рдЬрд┐рд╕рдХреЗ рджреНрд╡рд╛рд░рд╛ рд╣рдорд╛рд░рд╛ рд░рд┐рдПрдХреНрдЯ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдЦреБрд▓ рдЬрд╛рдПрдЧрд╛, рдПрдкреАрдЖрдИ рд╕реЗ рдХрд╛рд░реНрдб рдбреЗрдЯрд╛ рд▓реЛрдб рдХрд░реЗрдВ рдФрд░ рдЗрд╕реЗ рд╣рдореЗрдВ рдХрд┐рд╕реА рднреА рддрд░рд╣ рджрд┐рдЦрд╛рдПрдВред рдФрд░ рдпрд╣рд╛рдВ, рд╢рд╛рдпрдж, рд╕рдмрд╕реЗ рдХрдард┐рди рд╣рд┐рд╕реНрд╕рд╛ рд╢реБрд░реВ рд╣реЛрддрд╛ рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рд╣рдо рдЪрд╛рд╣рддреЗ рд╣реИрдВ рдХрд┐ рд╕рд░реНрд╡рд░ рддреБрд░рдВрдд рд╣рдореЗрдВ рдХрд╛рд░реНрдб рдХреА рд╕рд╛рдордЧреНрд░реА рдХреЗ рд╕рд╛рде HTML рджреЗ, рдЕрдиреБрдХреНрд░рдордг рдХреЗ рд▓рд┐рдП рдЙрдкрдпреБрдХреНрдд рд╣реИ, рд▓реЗрдХрд┐рди рдПрдХ рд╣реА рд╕рдордп рдореЗрдВ, рдЬрдм рдЖрд╡реЗрджрди рдХрд╛рд░реНрдб рдХреЗ рдмреАрдЪ рдиреЗрд╡рд┐рдЧреЗрдЯ рдХрд░рддрд╛ рд╣реИ, рддреЛ рдпрд╣ рдПрдкреАрдЖрдИ рд╕реЗ JSON рдХреЗ рд░реВрдк рдореЗрдВ рд╕рднреА рдбреЗрдЯрд╛ рдкреНрд░рд╛рдкреНрдд рдХрд░рддрд╛ рд╣реИ, рдФрд░ рдкреГрд╖реНрда рдЕрдзрд┐рднрд╛рд░ рдирд╣реАрдВ рджреЗрддрд╛ рд╣реИред рдФрд░ рдЗрд╕рд▓рд┐рдП рдпрд╣ рд╕рдм - рдмрд┐рдирд╛ рдХреЙрдкреА-рдкреЗрд╕реНрдЯ рдХреЗ!

Redux рдХреЛ рдЬреЛрдбрд╝рдХрд░ рд╢реБрд░реВ рдХрд░рддреЗ рд╣реИрдВред Redux рд░рд╛рдЬреНрдп рднрдВрдбрд╛рд░рдг рдХреЗ рд▓рд┐рдП рдПрдХ рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдкреБрд╕реНрддрдХрд╛рд▓рдп рд╣реИред рд╡рд┐рдЪрд╛рд░ рдпрд╣ рд╣реИ рдХрд┐ рд╣рдЬрд╛рд░ рдирд┐рд╣рд┐рдд рдХреЗ рдмрдЬрд╛рдп рдпрд╣ рдмрддрд╛рддрд╛ рд╣реИ рдХрд┐ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреЗ рдХрд╛рд░реНрдпреЛрдВ рдФрд░ рдЕрдиреНрдп рджрд┐рд▓рдЪрд╕реНрдк рдШрдЯрдирд╛рдУрдВ рдХреЗ рджреМрд░рд╛рди рдЖрдкрдХреЗ рдШрдЯрдХ рдмрджрд▓ рдЬрд╛рддреЗ рд╣реИрдВ, рдЙрдирдХреЗ рдкрд╛рд╕ рдПрдХ рдХреЗрдВрджреНрд░реАрдХреГрдд рд╕реНрдерд┐рддрд┐ рд╣реЛрддреА рд╣реИ, рдФрд░ рдХреНрд░рд┐рдпрд╛рдУрдВ рдХреЗ рдХреЗрдВрджреНрд░реАрдХреГрдд рддрдВрддреНрд░ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдХреЛрдИ рднреА рдкрд░рд┐рд╡рд░реНрддрди рд╣реЛрддрд╛ рд╣реИред рдЗрд╕рд▓рд┐рдП, рдЕрдЧрд░ рдкрд╣рд▓реЗ рдиреЗрд╡рд┐рдЧреЗрд╢рди рдХреЗ рд▓рд┐рдП рд╣рдо рдкрд╣рд▓реЗ рд▓реЛрдбрд┐рдВрдЧ GIF рдХреЛ рдЪрд╛рд▓реВ рдХрд░рддреЗ рд╣реИрдВ, рддреЛ рд╣рдордиреЗ AJAX рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдПрдХ рдЕрдиреБрд░реЛрдз рдХрд┐рдпрд╛ рдФрд░, рдЖрдЦрд┐рд░рдХрд╛рд░, рд╕рдлрд▓рддрд╛ рдХреЙрд▓рдмреИрдХ рдореЗрдВ, рд╣рдордиреЗ рдкреГрд╖реНрда рдХреЗ рдЖрд╡рд╢реНрдпрдХ рднрд╛рдЧреЛрдВ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд┐рдпрд╛, рдлрд┐рд░ Redux рдкреНрд░рддрд┐рдорд╛рди рдореЗрдВ рд╣рдореЗрдВ рдХрд╛рд░реНрд░рд╡рд╛рдИ рдХреЛ рдПрдиреАрдореЗрд╢рди рдХреЗ рд╕рд╛рде рдЬреАрдЖрдИрдПрдл рдореЗрдВ рдмрджрд▓рдиреЗ рдХреЗ рд▓рд┐рдП рдЖрдордВрддреНрд░рд┐рдд рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ, " рд╡реИрд╢реНрд╡рд┐рдХ рд╕реНрдерд┐рддрд┐ рдХреЛ рдмрджрд▓ рджреЗрдЧрд╛ рддрд╛рдХрд┐ рдЖрдкрдХрд╛ рдПрдХ рдШрдЯрдХ рдкрд┐рдЫрд▓реА рд╕рд╛рдордЧреНрд░реА рдХреЛ рдмрд╛рд╣рд░ рдирд┐рдХрд╛рд▓ рджреЗ рдФрд░ рдПрдиреАрдореЗрд╢рди рдбрд╛рд▓ рджреЗ, рдлрд┐рд░ рдПрдХ рдЕрдиреБрд░реЛрдз рдХрд░реЗрдВ, рдФрд░ рдЗрд╕рдХреА рд╕рдлрд▓рддрд╛ рдХреЗ рдХреЙрд▓рдмреИрдХ рдореЗрдВ рдПрдХ рдФрд░ рдХрд╛рд░реНрд░рд╡рд╛рдИ рднреЗрдЬреЗрдВ, "рд╕рд╛рдордЧреНрд░реА рдХреЛ рд▓реЛрдб рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдмрджрд▓реЗрдВ"ред рд╕рд╛рдорд╛рдиреНрдп рддреМрд░ рдкрд░, рдЕрдм рд╣рдо рдЗрд╕реЗ рд╕реНрд╡рдпрдВ рджреЗрдЦреЗрдВрдЧреЗред

рдЪрд▓реЛ рд╣рдорд╛рд░реЗ рдХрдВрдЯреЗрдирд░ рдореЗрдВ рдирдИ рдирд┐рд░реНрднрд░рддрд╛ рд╕реНрдерд╛рдкрд┐рдд рдХрд░рдХреЗ рд╢реБрд░реВ рдХрд░рддреЗ рд╣реИрдВред

 docker-compose exec frontend npm install --save \ redux \ react-redux \ redux-thunk \ redux-devtools-extension 

рдкрд╣рд▓рд╛, рд╡рд╛рд╕реНрддрд╡ рдореЗрдВ, Redux рд╣реИ, рджреВрд╕рд░рд╛ рд░рд┐рдПрдХреНрдЯ рдФрд░ Redux (рд╡рд┐рд╢реЗрд╖рдЬреНрдЮреЛрдВ рджреНрд╡рд╛рд░рд╛ рд▓рд┐рдЦрд┐рдд) рдХреЛ рдкрд╛рд░ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рд╡рд┐рд╢реЗрд╖ рдкреБрд╕реНрддрдХрд╛рд▓рдп рд╣реИ, рддреАрд╕рд░рд╛ рдПрдХ рдмрд╣реБрдд рд╣реА рдЖрд╡рд╢реНрдпрдХ рдЪреАрдЬ рд╣реИ, рдЬрд┐рд╕рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдЗрд╕рдХреЗ README рдореЗрдВ рдЕрдЪреНрдЫреА рддрд░рд╣ рд╕реЗ рдЙрдЪрд┐рдд рд╣реИ, рдФрд░, рдЖрдЦрд┐рд░рдХрд╛рд░, Redux DevTools рдХреЛ рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЖрд╡рд╢реНрдпрдХ рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдЪреМрдереА рд╣реИред рд╡рд┐рд╕реНрддрд╛рд░ ред

рдЪрд▓реЛ рдмреЙрдпрд▓рд░рдкреНрд▓реЗрдЯ Redux рдХреЛрдб рд╕реЗ рд╢реБрд░реВ рдХрд░рддреЗ рд╣реИрдВ: рдПрдХ reducer рдмрдирд╛рддреЗ рд╣реИрдВ рдЬреЛ рдХреБрдЫ рднреА рдирд╣реАрдВ рдХрд░рддрд╛ рд╣реИ, рдФрд░ рд░рд╛рдЬреНрдп рдХреЛ рдЗрдирд┐рд╢рд┐рдпрд▓рд╛рдЗрдЬрд╝ рдХрд░рддрд╛ рд╣реИред

 // frontend/src/redux/reducers.js export default function root(state = {}, action) { return state; } 

 // frontend/src/redux/configureStore.js import {createStore, applyMiddleware} from "redux"; import thunkMiddleware from "redux-thunk"; import {composeWithDevTools} from "redux-devtools-extension"; import rootReducer from "./reducers"; export default function configureStore(initialState) { return createStore( rootReducer, initialState, composeWithDevTools(applyMiddleware(thunkMiddleware)), ); } 

рд╣рдорд╛рд░рд╛ рдЧреНрд░рд╛рд╣рдХ рдереЛрдбрд╝рд╛ рдмрджрд▓ рдЬрд╛рддрд╛ рд╣реИ, Redux рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдорд╛рдирд╕рд┐рдХ рд░реВрдк рд╕реЗ рддреИрдпрд╛рд░ рд╣реЛрддрд╛ рд╣реИ:

 // frontend/src/client.js import React from 'react' import {render} from 'react-dom' import {Provider} from 'react-redux' import App from './components/app' import configureStore from './redux/configureStore' //      ... const store = configureStore(); render( // ...      , //     <Provider store={store}> <App/> </Provider>, document.querySelector('#app') ); 

рдЕрдм рд╣рдо рдбреЙрдХрдЯрд░-рдХрдВрдкреЛрдЬрд╝ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ - рдпрд╣ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐ рдХреЛрдИ рднреА рдЪреАрдЬрд╝ рдЯреВрдЯреА рдирд╣реАрдВ рд╣реИ, рдФрд░ рд╣рдорд╛рд░реА рдЖрджрд┐рдо рд╕реНрдерд┐рддрд┐ Redux DevTools рдореЗрдВ рджрд┐рдЦрд╛рдИ рджреЗрддреА рд╣реИ:



рдлреНрд░рдВрдЯреЗрдВрдб: рдХрд╛рд░реНрдб рдкреЗрдЬ


рдЗрд╕рд╕реЗ рдкрд╣рд▓реЗ рдХрд┐ рдЖрдк рдПрд╕рдПрд╕рдЖрд░ рдХреЗ рд╕рд╛рде рдкреЗрдЬ рдмрдирд╛ рд╕рдХреЗрдВ, рдЖрдкрдХреЛ рдПрд╕рдПрд╕рдЖрд░ рдХреЗ рдмрд┐рдирд╛ рдкреЗрдЬ рдмрдирд╛рдиреЗ рдХреА рдЬрд░реВрд░рдд рд╣реИ! рдЪрд▓реЛ рдЕрдВрдд рдореЗрдВ рдХрд╛рд░реНрдб рддрдХ рдкрд╣реБрдВрдЪ рдХреЗ рд▓рд┐рдП рд╣рдорд╛рд░реЗ рд╕рд░рд▓ рдПрдкреАрдЖрдИ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ рдФрд░ рд╕рд╛рдордиреЗ рдХреЗ рдЫреЛрд░ рдкрд░ рдХрд╛рд░реНрдб рдкреЗрдЬ рдмрдирд╛рдПрдВред

рдмреБрджреНрдзрд┐рдорддреНрддрд╛ рдХрд╛ рд▓рд╛рдн рдЙрдард╛рдиреЗ рдФрд░ рд╣рдорд╛рд░реА рд░рд╛рдЬреНрдп рд╕рдВрд░рдЪрдирд╛ рдХреЛ рдирдпрд╛ рд╕реНрд╡рд░реВрдк рджреЗрдиреЗ рдХрд╛ рд╕рдордпред рдЗрд╕ рд╡рд┐рд╖рдп рдкрд░ рдмрд╣реБрдд рд╕рд╛рд░реА рд╕рд╛рдордЧреНрд░рд┐рдпрд╛рдВ рд╣реИрдВ, рдЗрд╕рд▓рд┐рдП рдореЗрд░рд╛ рд╕реБрдЭрд╛рд╡ рд╣реИ рдХрд┐ рдмреБрджреНрдзрд┐рдорддрд╛ рдХрд╛ рджреБрд░реБрдкрдпреЛрдЧ рди рдХрд░реЗрдВ рдФрд░ рд╕рд░рд▓ рдкрд░ рдзреНрдпрд╛рди рджреЗрдВред рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдЬреИрд╕реЗ:

 { "page": { "type": "card", //     //       type=card: "cardSlug": "...", //     "isFetching": false, //      API "cardData": {...}, //   (  ) // ... }, // ... } 

рдЖрдЗрдП "рдХрд╛рд░реНрдб" рдШрдЯрдХ рдкреНрд░рд╛рдкреНрдд рдХрд░реЗрдВ, рдЬреЛ рдХрд╛рд░реНрдбрдбреИрдЯ рдХреА рд╕рд╛рдордЧреНрд░реА рдХреЛ рдкреНрд░реЙрдкреНрд╕ рдХреЗ рд░реВрдк рдореЗрдВ рд▓реЗрддрд╛ рд╣реИ (рдпрд╣ рд╡рд╛рд╕реНрддрд╡ рдореЗрдВ рд╣рдорд╛рд░реЗ рдХрд╛рд░реНрдб рдХреА рд╕рд╛рдордЧреНрд░реА рдХреЗ рд░реВрдк рдореЗрдВ рд╣реИ):

 // frontend/src/components/card.js import React, {Component} from 'react'; class Card extends Component { componentDidMount() { document.title = this.props.name } render() { const {name, html} = this.props; return ( <div> <h1>{name}</h1> <!---,  HTML  React  - !--> <div dangerouslySetInnerHTML={{__html: html}}/> </div> ); } } export default Card; 

рдЕрдм рдЪрд▓реЛ рдХрд╛рд░реНрдб рдХреЗ рд╕рд╛рде рдкреВрд░реЗ рдкреГрд╖реНрда рдХреЗ рд▓рд┐рдП рдПрдХ рдШрдЯрдХ рдкреНрд░рд╛рдкреНрдд рдХрд░реЗрдВред рд╡рд╣ рдПрдкреАрдЖрдИ рд╕реЗ рдЖрд╡рд╢реНрдпрдХ рдбреЗрдЯрд╛ рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдФрд░ рдЗрд╕реЗ рдХрд╛рд░реНрдб рдореЗрдВ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЬрд┐рдореНрдореЗрджрд╛рд░ рд╣реЛрдЧрд╛ред рдФрд░ рд╣рдо React-Redux рддрд░реАрдХреЗ рд╕реЗ рдбреЗрдЯрд╛ рд▓рд╛рдиреЗ рдХрд╛ рдХрд╛рдо рдХрд░реЗрдВрдЧреЗред

рд╕рдмрд╕реЗ рдкрд╣рд▓реЗ, рдлрд╝рд╛рдЗрд▓ frontend/src/redux/actions.js рдмрдирд╛рдПрдВ рдФрд░ рдПрдХ рдПрдХреНрд╢рди рдмрдирд╛рдПрдВ, рдЬреЛ рдПрдкреАрдЖрдИ рд╕реЗ рдХрд╛рд░реНрдб рдХреА рд╕рд╛рдордЧреНрд░реА рдХреЛ рдирд┐рдХрд╛рд▓рддрд╛ рд╣реИ, рдпрджрд┐ рдкрд╣рд▓реЗ рд╕реЗ рдирд╣реАрдВ:

 export function fetchCardIfNeeded() { return (dispatch, getState) => { let state = getState().page; if (state.cardData === undefined || state.cardData.slug !== state.cardSlug) { return dispatch(fetchCard()); } }; } 

fetchCard рдХреНрд░рд┐рдпрд╛, рдЬреЛ рд╡рд╛рд╕реНрддрд╡ рдореЗрдВ fetchCard рдмрдирд╛рддреА рд╣реИ, рдереЛрдбрд╝рд╛ рдФрд░ рдЬрдЯрд┐рд▓:

 function fetchCard() { return (dispatch, getState) => { //    ,    . //     , , //    . dispatch(startFetchingCard()); //    API. let url = apiPath() + "/card/" + getState().page.cardSlug; // , ,   ,  //    . , ,  //    . return fetch(url) .then(response => response.json()) .then(json => dispatch(finishFetchingCard(json))); }; // ,  redux-thunk   //     . } function startFetchingCard() { return { type: START_FETCHING_CARD }; } function finishFetchingCard(json) { return { type: FINISH_FETCHING_CARD, cardData: json }; } function apiPath() { //    .    server-side // rendering,   API     -  //         localhost, //   backend. return "http://localhost:40001/api/v1"; } 

рдУрд╣, рд╣рдореЗрдВ рдПрдХ рдХрд╛рд░реНрд░рд╡рд╛рдИ рдорд┐рд▓реА рд╣реИ рдХрд┐ рдЬреЛ рдХреБрдЫ рдХрд░рддрд╛ рд╣реИ! рдпрд╣ reducer рдореЗрдВ рд╕рдорд░реНрдерд┐рдд рд╣реЛрдирд╛ рдЪрд╛рд╣рд┐рдП:

 // frontend/src/redux/reducers.js import { START_FETCHING_CARD, FINISH_FETCHING_CARD } from "./actions"; export default function root(state = {}, action) { switch (action.type) { case START_FETCHING_CARD: return { ...state, page: { ...state.page, isFetching: true } }; case FINISH_FETCHING_CARD: return { ...state, page: { ...state.page, isFetching: false, cardData: action.cardData } } } return state; } 

(рдмрджрд▓рддреЗ рдХреНрд╖реЗрддреНрд░реЛрдВ рдХреЗ рд╕рд╛рде рдХрд┐рд╕реА рд╡рд╕реНрддреБ рдХреЛ рдХреНрд▓реЛрди рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЯреНрд░реЗрдВрдбреА рд╕рд┐рдВрдЯреИрдХреНрд╕ рдкрд░ рдзреНрдпрд╛рди рджреЗрдВред)

рдЕрдм рдЬрдм рд╕рднреА рддрд░реНрдХ Redux рдХрд╛рд░реНрдпреЛрдВ рдореЗрдВ рдХрд┐рдП рдЬрд╛рддреЗ рд╣реИрдВ, рддреЛ рдШрдЯрдХ рд╕реНрд╡рдпрдВ рд╣реА CardPageрдЕрдкреЗрдХреНрд╖рд╛рдХреГрдд рд╕рд░рд▓ рджрд┐рдЦрд╛рдИ рджреЗрдЧрд╛:

 // frontend/src/components/cardPage.js import React, {Component} from 'react'; import {connect} from 'react-redux' import {fetchCardIfNeeded} from '../redux/actions' import Card from './card' class CardPage extends Component { componentWillMount() { //   ,  React  //   .      //   ,    " // "   ,    //  - .    -   //       HTML  // renderToString,      SSR. this.props.dispatch(fetchCardIfNeeded()) } render() { const {isFetching, cardData} = this.props; return ( <div> {isFetching && <h2>Loading...</h2>} {cardData && <Card {...cardData}/>} </div> ); } } //       ,   //  .        //  react-redux.   page    //  dispatch,   . function mapStateToProps(state) { const {page} = state; return page; } export default connect(mapStateToProps)(CardPage); 

рд╣рдорд╛рд░реЗ рд░реВрдЯ рдРрдк рдШрдЯрдХ рдореЗрдВ рдПрдХ рд╕рд╛рдзрд╛рд░рдг рдкреЗрдЬ рдЯрд╛рдЗрдк рдХрд░реЗрдВред

 // frontend/src/components/app.js import React, {Component} from 'react' import {connect} from "react-redux"; import CardPage from "./cardPage" class App extends Component { render() { const {pageType} = this.props; return ( <div> {pageType === "card" && <CardPage/>} </div> ); } } function mapStateToProps(state) { const {page} = state; const {type} = page; return { pageType: type }; } export default connect(mapStateToProps)(App); 

рдФрд░ рдЕрдм, рдПрдХ рдЕрдВрддрд┐рдо рдмрд╛рдд - рдпрд╣ рдЖрд╡рд╢реНрдпрдХ рд╣реИ рдкреНрд░рд╛рд░рдВрдн рдХрд░рдиреЗ page.typeрдФрд░ page.cardSlugрдпреВрдЖрд░рдПрд▓ рдкрд░ рдирд┐рд░реНрднрд░ рдХрд░рддрд╛ рд╣реИред

рд▓реЗрдХрд┐рди рдЗрд╕ рд▓реЗрдЦ рдореЗрдВ рдЕрднреА рднреА рдХрдИ рдЦрдВрдб рд╣реИрдВ, рд▓реЗрдХрд┐рди рд╣рдо рдЕрднреА рдПрдХ рдЙрдЪреНрдЪ-рдЧреБрдгрд╡рддреНрддрд╛ рдХрд╛ рд╕рдорд╛рдзрд╛рди рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред рдЪрд▓реЛ рдЕрдм рдХреЗ рд▓рд┐рдП рдмреЗрд╡рдХреВрдл рдмрдирд╛рддреЗ рд╣реИрдВред рдпрд╣ рдкреВрд░реА рддрд░рд╣ рд╕реЗ рдмреЗрд╡рдХреВрдл рд╣реИред рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдЖрд╡реЗрджрди рдХреЛ рд╢реБрд░реВ рдХрд░рддреЗ рд╕рдордп рдПрдХ рдирд┐рдпрдорд┐рдд!

 // frontend/src/client.js import React from 'react' import {render} from 'react-dom' import {Provider} from 'react-redux' import App from './components/app' import configureStore from './redux/configureStore' let initialState = { page: { type: "home" } }; const m = /^\/card\/([^\/]+)$/.exec(location.pathname); if (m !== null) { initialState = { page: { type: "card", cardSlug: m[1] }, } } const store = configureStore(initialState); render( <Provider store={store}> <App/> </Provider>, document.querySelector('#app') ); 

рдЕрдм рд╣рдо docker-compose up --build frontendрдЕрдкрдиреЗ рдХрд╛рд░реНрдб рдХрд╛ рдЖрдирдВрдж рд▓реЗрдиреЗ рдХреЗ рд▓рд┐рдП рдлреНрд░рдВрдЯрдПрдВрдб рдХрд╛ рдкреБрдирд░реНрдирд┐рд░реНрдорд╛рдг рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ helloworld...



рдЗрд╕рд▓рд┐рдП, рдПрдХ рд╕реЗрдХрдВрдб рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░реЗрдВ ... рдФрд░ рд╣рдорд╛рд░реА рд╕рд╛рдордЧреНрд░реА рдХрд╣рд╛рдВ рд╣реИ? рдУрд╣, рд╣рдо рдорд╛рд░реНрдХрдбрд╛рдЙрди рдХреЛ рдкрд╛рд░реНрд╕ рдХрд░рдирд╛ рднреВрд▓ рдЧрдП!

рдХрд╛рд░реНрдпрдХрд░реНрддрд╛: рдЖрд░рдХреНрдпреВ


рдорд╛рд░реНрдХрд╛рдбрд╛рдЙрди рдХреЛ рдкрд╛рд░реНрд╕ рдХрд░рдирд╛ рдФрд░ рд╕рдВрднрд╛рд╡рд┐рдд рдЕрд╕реАрдорд┐рдд рдЖрдХрд╛рд░ рдХреЗ рдХрд╛рд░реНрдб рдХреЗ рд▓рд┐рдП HTML рдЙрддреНрдкрдиреНрди рдХрд░рдирд╛ рдПрдХ рд╡рд┐рд╢рд┐рд╖реНрдЯ "рднрд╛рд░реА" рдХрд╛рд░реНрдп рд╣реИ, рдЬреЛ рдмрджрд▓рд╛рд╡реЛрдВ рдХреЛ рд╕рд╣реЗрдЬрддреЗ рд╕рдордп рдмреИрдХрдПрдВрдб рдкрд░ рд╕реАрдзреЗ рд╣рд▓ рд╣реЛрдиреЗ рдХреЗ рдмрдЬрд╛рдп, рдЖрдорддреМрд░ рдкрд░ рдХрддрд╛рд░рдмрджреНрдз рдФрд░ рдЕрд▓рдЧ-рдЕрд▓рдЧ рдХрд╛рдо рдХрд░рдиреЗ рд╡рд╛рд▓реА рдорд╢реАрдиреЛрдВ рдкрд░ рдирд┐рд╖реНрдкрд╛рджрд┐рдд рд╣реЛрддрд╛ рд╣реИред

рдХрд╛рд░реНрдп рдХрддрд╛рд░реЛрдВ рдХреЗ рдХрдИ рдЦреБрд▓реЗ рд╕реНрд░реЛрдд рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рд╣реИрдВ; рд╣рдо рд░реЗрдбрд┐рд╕ рдФрд░ рдПрдХ рд╕рд╛рдзрд╛рд░рдг рдкреБрд╕реНрддрдХрд╛рд▓рдп рдЖрд░рдХреНрдпреВ (рд░реЗрдбрд┐рд╕ рдХреНрдпреВ) рд▓реЗрдВрдЧреЗ , рдЬреЛ рдЕрдЪрд╛рд░ рдкреНрд░рд╛рд░реВрдк рдореЗрдВ рдХрд╛рд░реНрдп рдорд╛рдкрджрдВрдбреЛрдВ рдХреЛ рдкреНрд░рд╕рд╛рд░рд┐рдд рдХрд░рддрд╛ рд╣реИ рдФрд░ рд╣рдореЗрдВ рдЙрдирдХреЗ рдкреНрд░рд╕рдВрд╕реНрдХрд░рдг рдХреЗ рд▓рд┐рдП рд╕реНрдкреЙрдирд┐рдВрдЧ рдкреНрд░рдХреНрд░рд┐рдпрд╛рдУрдВ рдХрд╛ рдЖрдпреЛрдЬрди рдХрд░рддрд╛ рд╣реИред

рдореВрд▓реА, рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдФрд░ рд╡рд╛рдпрд░рд┐рдВрдЧ рдХреЗ рдЖрдзрд╛рд░ рдкрд░ рдЬреЛрдбрд╝рдиреЗ рдХрд╛ рд╕рдордп!

 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ flask-cors gevent gunicorn pymongo +redis +rq --- a/backend/dev_settings.py +++ b/backend/dev_settings.py @@ -1,3 +1,7 @@ MONGO_HOST = "mongo" MONGO_PORT = 27017 MONGO_DATABASE = "core" +REDIS_HOST = "redis" +REDIS_PORT = 6379 +REDIS_DB = 0 +TASK_QUEUE_NAME = "tasks" --- a/backend/wiring.py +++ b/backend/wiring.py @@ -2,6 +2,8 @@ import os from pymongo import MongoClient from pymongo.database import Database +import redis +import rq import backend.dev_settings from backend.storage.card import CardDAO @@ -21,3 +23,11 @@ class Wiring(object): port=self.settings.MONGO_PORT) self.mongo_database: Database = self.mongo_client[self.settings.MONGO_DATABASE] self.card_dao: CardDAO = MongoCardDAO(self.mongo_database) + + self.redis: redis.Redis = redis.StrictRedis( + host=self.settings.REDIS_HOST, + port=self.settings.REDIS_PORT, + db=self.settings.REDIS_DB) + self.task_queue: rq.Queue = rq.Queue( + name=self.settings.TASK_QUEUE_NAME, + connection=self.redis) 

рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рдХреЗ рд▓рд┐рдП рдмреЙрдпрд▓рд░ рдХреЛрдб рдХрд╛ рдПрдХ рд╕рд╛ред

 # worker/__main__.py import argparse import uuid import rq import backend.wiring parser = argparse.ArgumentParser(description="Run worker.") #   ,      #  .  ,       rq. parser.add_argument( "--burst", action="store_const", const=True, default=False, help="enable burst mode") args = parser.parse_args() #       Redis. wiring = backend.wiring.Wiring() with rq.Connection(wiring.redis): w = rq.Worker( queues=[wiring.settings.TASK_QUEUE_NAME], #         # ,    . name=uuid.uuid4().hex) w.work(burst=args.burst) 

рдкрд╛рд░реНрд╕рд┐рдВрдЧ рдХреЗ рд▓рд┐рдП, рдорд┐рд╕реНрдЯреНрдпреВрди рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдХрдиреЗрдХреНрдЯ рдХрд░реЗрдВ рдФрд░ рдПрдХ рд╕рд╛рдзрд╛рд░рдг рдлрд╝рдВрдХреНрд╢рди рд▓рд┐рдЦреЗрдВ:

 # backend/tasks/parse.py import mistune from backend.storage.card import CardDAO def parse_card_markup(card_dao: CardDAO, card_id: str): card = card_dao.get_by_id(card_id) card.html = _parse_markdown(card.markdown) card_dao.update(card) _parse_markdown = mistune.Markdown(escape=True, hard_wrap=False) 

рддрд╛рд░реНрдХрд┐рдХ рд░реВрдк рд╕реЗ: рд╣рдореЗрдВ CardDAOрдХрд╛рд░реНрдб рдХрд╛ рд╕реНрд░реЛрдд рдХреЛрдб рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдФрд░ рдкрд░рд┐рдгрд╛рдо рдмрдЪрд╛рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред рд▓реЗрдХрд┐рди рдмрд╛рд╣рд░реА рднрдВрдбрд╛рд░рдг рд╕реЗ рд╕рдВрдмрдВрдз рд░рдЦрдиреЗ рд╡рд╛рд▓реА рд╡рд╕реНрддреБ рдХреЛ рдЕрдЪрд╛рд░ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдХреНрд░рдордмрджреНрдз рдирд╣реАрдВ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ - рдЬрд┐рд╕рдХрд╛ рдЕрд░реНрде рд╣реИ рдХрд┐ рдпрд╣ рдХрд╛рд░реНрдп рддреБрд░рдВрдд рдирд╣реАрдВ рд▓рд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ рдФрд░ рдЗрд╕реЗ рдЖрд░рдХреНрдпреВ рджреНрд╡рд╛рд░рд╛ рдкрдВрдХреНрддрд┐рдмрджреНрдз рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред рдПрдХ рдЕрдЪреНрдЫреЗ рддрд░реАрдХреЗ рд╕реЗ, рд╣рдореЗрдВ WiringрдкрдХреНрд╖ рдореЗрдВ рдПрдХ рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рдмрдирд╛рдиреЗ рдФрд░ рдЗрд╕реЗ рд╣рд░ рддрд░рд╣ рд╕реЗ рдлреЗрдВрдХрдиреЗ рдХреА рдЬрд░реВрд░рдд рд╣реИ ... рдЖрдЗрдП рдЗрд╕реЗ рдХрд░реЗрдВ:

 --- a/worker/__main__.py +++ b/worker/__main__.py @@ -2,6 +2,7 @@ import argparse import uuid import rq +from rq.job import Job import backend.wiring @@ -16,8 +17,23 @@ args = parser.parse_args() wiring = backend.wiring.Wiring() + +class JobWithWiring(Job): + + @property + def kwargs(self): + result = dict(super().kwargs) + result["wiring"] = backend.wiring.Wiring() + return result + + @kwargs.setter + def kwargs(self, value): + super().kwargs = value + + with rq.Connection(wiring.redis): w = rq.Worker( queues=[wiring.settings.TASK_QUEUE_NAME], - name=uuid.uuid4().hex) + name=uuid.uuid4().hex, + job_class=JobWithWiring) w.work(burst=args.burst) 

рд╣рдордиреЗ рдЕрдкрдиреА рдиреМрдХрд░рд┐рдпреЛрдВ рдХреА рд╢реНрд░реЗрдгреА рдШреЛрд╖рд┐рдд рдХреА, рд╡рд╛рдпрд░рд┐рдВрдЧ рдХреЛ рд╕рднреА рд╕рдорд╕реНрдпрд╛рдУрдВ рдореЗрдВ рдЕрддрд┐рд░рд┐рдХреНрдд kwargs рддрд░реНрдХ рдХреЗ рд░реВрдк рдореЗрдВ рдлреЗрдВрдХ рджрд┐рдпрд╛ред (рдзреНрдпрд╛рди рджреЗрдВ рдХрд┐ рдпрд╣ рд╣рд░ рдмрд╛рд░ рдПрдХ рдирдИ рд╡рд╛рдпрд░рд┐рдВрдЧ рдмрдирд╛рддрд╛ рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рдХреБрдЫ рдХреНрд▓рд╛рдЗрдВрдЯ рдХрд╛рд░реНрдп рд╢реБрд░реВ рд╣реЛрдиреЗ рд╕реЗ рдкрд╣рд▓реЗ рдЖрд░рдХреНрдпреВ рдХреЗ рдЕрдВрджрд░ рд╣реЛрдиреЗ рд╡рд╛рд▓реЗ рдХрд╛рдВрдЯреЗ рд╕реЗ рдкрд╣рд▓реЗ рдирд╣реАрдВ рдмрдирд╛рдП рдЬрд╛ рд╕рдХрддреЗ рд╣реИрдВред) рддрд╛рдХрд┐ рд╣рдорд╛рд░реЗ рд╕рднреА рдХрд╛рд░реНрдп рд╡рд╛рдпрд░рд┐рдВрдЧ рдкрд░ рдирд┐рд░реНрднрд░ рди рд╣реЛрдВ - рдЕрд░реНрдерд╛рдд, рд╣рдорд╛рд░реА рд╕рднреА рд╡рд╕реНрддреБрдУрдВ рдкрд░ - рдЪрд▓реЛ рдЖрдЗрдП рдПрдХ рдРрд╕рд╛ рдбреЗрдХреЛрд░реЗрдЯрд░ рдмрдирд╛рдПрдВ рдЬреЛ рд╡рд╛рдпрд░рд┐рдВрдЧ рд╕реЗ рдХреЗрд╡рд▓ рдЖрд╡рд╢реНрдпрдХ рд╣реЛ:

 # backend/tasks/task.py import functools from typing import Callable from backend.wiring import Wiring def task(func: Callable): #    : varnames = func.__code__.co_varnames @functools.wraps(func) def result(*args, **kwargs): #  .  .pop(),     # ,        . wiring: Wiring = kwargs.pop("wiring") wired_objects_by_name = wiring.__dict__ for arg_name in varnames: if arg_name in wired_objects_by_name: kwargs[arg_name] = wired_objects_by_name[arg_name] #          #   ,  -   . return func(*args, **kwargs) return result 

рд╣рдорд╛рд░реЗ рдХрд╛рд░реНрдп рдореЗрдВ рдПрдХ рдбреЗрдХреЛрд░реЗрдЯрд░ рдЬреЛрдбрд╝реЗрдВ рдФрд░ рдЬреАрд╡рди рдХрд╛ рдЖрдирдВрдж рд▓реЗрдВ:

 import mistune from backend.storage.card import CardDAO from backend.tasks.task import task @task def parse_card_markup(card_dao: CardDAO, card_id: str): card = card_dao.get_by_id(card_id) card.html = _parse_markdown(card.markdown) card_dao.update(card) _parse_markdown = mistune.Markdown(escape=True, hard_wrap=False) 

рдЬреАрд╡рди рдХрд╛ рдЖрдирдВрдж рд▓реЗрдВ? рдК, рдореИрдВ рдХрд╣рдирд╛ рдЪрд╛рд╣рддрд╛ рдерд╛, рд╣рдо рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рд╢реБрд░реВ рдХрд░рддреЗ рд╣реИрдВ:

 $ docker-compose up worker ... Creating habr-app-demo_worker_1 ... done Attaching to habr-app-demo_worker_1 worker_1 | 17:21:03 RQ worker 'rq:worker:49a25686acc34cdfa322feb88a780f00' started, version 0.13.0 worker_1 | 17:21:03 *** Listening on tasks... worker_1 | 17:21:03 Cleaning registries for queue: tasks 

III ... рд╡рд╣ рдХреБрдЫ рдирд╣реАрдВ рдХрд░рддрд╛ рд╣реИ! рдмреЗрд╢рдХ, рдХреНрдпреЛрдВрдХрд┐ рд╣рдордиреЗ рдПрдХ рднреА рдХрд╛рд░реНрдп рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдирд╣реАрдВ рдХрд┐рдпрд╛ рдерд╛!

рдЖрдЗрдП рд╣рдорд╛рд░реЗ рдЯреВрд▓ рдХреЛ рдлрд┐рд░ рд╕реЗ рд▓рд┐рдЦреЗрдВ, рдЬреЛ рдПрдХ рдЯреЗрд╕реНрдЯ рдХрд╛рд░реНрдб рдмрдирд╛рддрд╛ рд╣реИ, рддрд╛рдХрд┐ рдпрд╣: рдХ) рдХрд╛рд░реНрдб рдкрд╣рд▓реЗ рд╕реЗ рд╣реА рдмрдирд╛ рд╣реБрдЖ рдирд╣реАрдВ рд╣реИ (рдЬреИрд╕рд╛ рдХрд┐ рд╣рдорд╛рд░реЗ рдорд╛рдорд▓реЗ рдореЗрдВ рд╣реИ); b) рдорд╛рд░реНрдХрд╛рдбрд╛рдЙрди рдХреЗ рдкрд╛рд░реНрд╕рд┐рдВрдЧ рдкрд░ рдХрд╛рд░реНрдп рдХрд░рдирд╛ред

 # tools/add_test_content.py from backend.storage.card import Card, CardNotFound from backend.tasks.parse import parse_card_markup from backend.wiring import Wiring wiring = Wiring() try: card = wiring.card_dao.get_by_slug("helloworld") except CardNotFound: card = wiring.card_dao.create(Card( slug="helloworld", name="Hello, world!", markdown=""" This is a hello-world page. """)) # ,   card_dao.get_or_create,  #      ! wiring.task_queue.enqueue_call( parse_card_markup, kwargs={"card_id": card.id}) 

рдЙрдкрдХрд░рдг рдЕрдм рди рдХреЗрд╡рд▓ рдмреИрдХрдПрдВрдб рдкрд░, рдмрд▓реНрдХрд┐ рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рдкрд░ рднреА рдЪрд▓рд╛рдП рдЬрд╛ рд╕рдХрддреЗ рд╣реИрдВред рд╕рд┐рджреНрдзрд╛рдВрдд рд░реВрдк рдореЗрдВ, рдЕрдм рд╣рдореЗрдВ рдкрд░рд╡рд╛рд╣ рдирд╣реАрдВ рд╣реИред рд╣рдо рдЗрд╕реЗ рд▓реЙрдиреНрдЪ рдХрд░рддреЗ рд╣реИрдВ docker-compose exec worker python -m tools.add_test_contentрдФрд░ рдЯрд░реНрдорд┐рдирд▓ рдХреЗ рдПрдХ рдкрдбрд╝реЛрд╕реА рдЯреИрдм рдореЗрдВ рд╣рдореЗрдВ рдПрдХ рдЪрдорддреНрдХрд╛рд░ рджрд┐рдЦрд╛рдИ рджреЗрддрд╛ рд╣реИ - рдХрд╛рд░реНрдпрдХрд░реНрддрд╛ рдиреЗ SOMETHING рдХрд┐рдпрд╛!

 worker_1 | 17:34:26 tasks: backend.tasks.parse.parse_card_markup(card_id='5c715dd1e201ce000c6a89fa') (613b53b1-726b-47a4-9c7b-97cad26da1a5) worker_1 | 17:34:27 tasks: Job OK (613b53b1-726b-47a4-9c7b-97cad26da1a5) worker_1 | 17:34:27 Result is kept for 500 seconds 

рдмреИрдХрдПрдВрдб рдХреЗ рд╕рд╛рде рдХрдВрдЯреЗрдирд░ рдХреЗ рдкреБрдирд░реНрдирд┐рд░реНрдорд╛рдг рдХреЗ рдмрд╛рдж, рд╣рдо рдЕрдВрдд рдореЗрдВ рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдореЗрдВ рдЕрдкрдиреЗ рдХрд╛рд░реНрдб рдХреА рд╕рд╛рдордЧреНрд░реА рджреЗрдЦ рд╕рдХрддреЗ рд╣реИрдВ:



рд╕реАрдорд╛рдВрдд рдиреЗрд╡рд┐рдЧреЗрд╢рди


рдЗрд╕рд╕реЗ рдкрд╣рд▓реЗ рдХрд┐ рд╣рдо SSR рдкрд░ рдЖрдЧреЗ рдмрдврд╝реЗрдВ, рд╣рдореЗрдВ рдЕрдкрдиреЗ рд╕рднреА React рдЙрдкрджреНрд░рд╡ рдХреЛ рдереЛрдбрд╝рд╛ рд╕рд╛рд░реНрдердХ рдХрд░рдиреЗ рдХреА рдЬрд░реВрд░рдд рд╣реИ рдФрд░ рд╣рдорд╛рд░реЗ рдПрдХрд▓ рдкреГрд╖реНрда рдХреЗ рдЖрд╡реЗрджрди рдХреЛ рд╕рд╣реА рдорд╛рдпрдиреЗ рдореЗрдВ рдПрдХ рд╣реА рдкреГрд╖реНрда рдмрдирд╛рдирд╛ рд╣реИред рдЖрдЗрдП рджреЛ (рдПрдХ рдирд╣реАрдВ, рдФрд░ рджреЛ!) рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП рд╣рдорд╛рд░реЗ рдЯреВрд▓ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░реЗрдВ, I рдЕрдм рдмрд┐рдЧ рдбреЗрдЯ рдбреЗрд╡рд▓рдкрд░!) рдХрд╛рд░реНрдб рдЬреЛ рдПрдХ рджреВрд╕рд░реЗ рд╕реЗ рд▓рд┐рдВрдХ рдХрд░рддреЗ рд╣реИрдВ, рдФрд░ рдлрд┐рд░ рд╣рдо рдЙрдирдХреЗ рдмреАрдЪ рдиреЗрд╡рд┐рдЧреЗрд╢рди рд╕реЗ рдирд┐рдкрдЯреЗрдВрдЧреЗред

рдЫрд┐рдкрд╛ рд╣реБрдЖ рдкрд╛рда
 # tools/add_test_content.py def create_or_update(card): try: card.id = wiring.card_dao.get_by_slug(card.slug).id card = wiring.card_dao.update(card) except CardNotFound: card = wiring.card_dao.create(card) wiring.task_queue.enqueue_call( parse_card_markup, kwargs={"card_id": card.id}) create_or_update(Card( slug="helloworld", name="Hello, world!", markdown=""" This is a hello-world page. It can't really compete with the [demo page](demo). """)) create_or_update(Card( slug="demo", name="Demo Card!", markdown=""" Hi there, habrovchanin. You've probably got here from the awkward ["Hello, world" card](helloworld). Well, **good news**! Finally you are looking at a **really cool card**! """ )) 


рдЕрдм рд╣рдо рд▓рд┐рдВрдХ рдХрд╛ рдЕрдиреБрд╕рд░рдг рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдФрд░ рдЪрд┐рдВрддрди рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рд╣рд░ рдмрд╛рд░ рд╣рдорд╛рд░рд╛ рдЕрджреНрднреБрдд рдЖрд╡реЗрджрди рдХреИрд╕реЗ рд╢реБрд░реВ рд╣реЛрддрд╛ рд╣реИред рдЗрд╕реЗ рд░реЛрдХреЛ!

рд╕рдмрд╕реЗ рдкрд╣рд▓реЗ, рд▓рд┐рдВрдХ рдкрд░ рдХреНрд▓рд┐рдХ рдкрд░ рдЕрдкрдирд╛ рд╣реИрдВрдбрд▓рд░ рд▓рдЧрд╛рдПрдВред рдЪреВрдВрдХрд┐ рд▓рд┐рдВрдХ рд╡рд╛рд▓реЗ HTML рдмреИрдХрдПрдВрдб рд╕реЗ рдЖрддреЗ рд╣реИрдВ, рдФрд░ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд░рд┐рдПрдХреНрдЯ рдХреЗ рд╕рд╛рде рд╣реИ, рдЗрд╕рд▓рд┐рдП рд╣рдореЗрдВ рдереЛрдбрд╝рд╛ рд░рд┐рдПрдХреНрдЯ-рд╡рд┐рд╢рд┐рд╖реНрдЯ рдлреЛрдХрд╕ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред

 // frontend/src/components/card.js class Card extends Component { componentDidMount() { document.title = this.props.name } navigate(event) { //       .  //      ,    . if (event.target.tagName === 'A' && event.target.hostname === window.location.hostname) { //     event.preventDefault(); //      this.props.dispatch(navigate(event.target)); } } render() { const {name, html} = this.props; return ( <div> <h1>{name}</h1> <div dangerouslySetInnerHTML={{__html: html}} onClick={event => this.navigate(event)} /> </div> ); } } 

рдЪреВрдВрдХрд┐ рд╣рдорд╛рд░реЗ рдШрдЯрдХ рдореЗрдВ рдХрд╛рд░реНрдб рд▓реЛрдб рдХрд░рдиреЗ рдХреЗ рд╕рд╛рде рд╕рднреА рддрд░реНрдХ CardPage, рдХрд╛рд░реНрд░рд╡рд╛рдИ рдореЗрдВ рд╣реА (рдЖрд╢реНрдЪрд░реНрдпрдЬрдирдХ!), рдХреЛрдИ рдХрд╛рд░реНрд░рд╡рд╛рдИ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рд╣реИ:

 export function navigate(link) { return { type: NAVIGATE, path: link.pathname } } 

рдЗрд╕ рдорд╛рдорд▓реЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдореВрд░реНрдЦ reducer рдЬреЛрдбрд╝реЗрдВ:

 // frontend/src/redux/reducers.js import { START_FETCHING_CARD, FINISH_FETCHING_CARD, NAVIGATE } from "./actions"; function navigate(state, path) { //     react-router,    ! // (       SSR.) let m = /^\/card\/([^/]+)$/.exec(path); if (m !== null) { return { ...state, page: { type: "card", cardSlug: m[1], isFetching: true } }; } return state } export default function root(state = {}, action) { switch (action.type) { case START_FETCHING_CARD: return { ...state, page: { ...state.page, isFetching: true } }; case FINISH_FETCHING_CARD: return { ...state, page: { ...state.page, isFetching: false, cardData: action.cardData } }; case NAVIGATE: return navigate(state, action.path) } return state; } 

рдЪреВрдВрдХрд┐ рдЕрдм рд╣рдорд╛рд░реЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреА рд╕реНрдерд┐рддрд┐ рдмрджрд▓ рд╕рдХрддреА рд╣реИ, CardPageрдЗрд╕рд▓рд┐рдП componentDidUpdateрд╣рдореЗрдВ рдкрд╣рд▓реЗ рд╕реЗ рдЬреЛрдбрд╝реЗ рдЧрдП рддрд░реАрдХреЗ рдХреЗ рд╕рдорд╛рди рдПрдХ рд╡рд┐рдзрд┐ рдЬреЛрдбрд╝рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ componentWillMountред рдЕрдм, рдЧреБрдгреЛрдВ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рдХреЗ рдмрд╛рдж CardPage(рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, cardSlugрдиреЗрд╡рд┐рдЧреЗрд╢рди рдХреЗ рджреМрд░рд╛рди рдЧреБрдг ), рдмреИрдХрдПрдВрдб рд╕реЗ рдХрд╛рд░реНрдб рдХреА рд╕рд╛рдордЧреНрд░реА рдХрд╛ рднреА рдЕрдиреБрд░реЛрдз рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ (рдпрд╣ componentWillMountрдХреЗрд╡рд▓ рддрдм рдХрд┐рдпрд╛ рдЧрдпрд╛ рдерд╛ рдЬрдм рдШрдЯрдХ рдЖрд░рдВрднреАрдХреГрдд рдХрд┐рдпрд╛ рдЧрдпрд╛ рдерд╛)ред

рдареАрдХ рд╣реИ, docker-compose up --build frontendрдФрд░ рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рдПрдХ рдХрд╛рдо рдХрд░рдиреЗ рд╡рд╛рд▓рд╛ рдиреЗрд╡рд┐рдЧреЗрд╢рди рд╣реИ!



рдПрдХ рдЪреМрдХрд╕ рдкрд╛рдардХ рдпрд╣ рдиреЛрдЯ рдХрд░реЗрдЧрд╛ рдХрд┐ рдХрд╛рд░реНрдб рдХреЗ рдмреАрдЪ рдиреЗрд╡рд┐рдЧреЗрдЯ рдХрд░рддреЗ рд╕рдордп рдкреГрд╖реНрда рдХрд╛ URL рдирд╣реАрдВ рдмрджрд▓реЗрдЧрд╛ - рдпрд╣рд╛рдВ рддрдХ тАЛтАЛрдХрд┐ рд╕реНрдХреНрд░реАрдирд╢реЙрдЯ рдореЗрдВ рд╣рдо рд╣реИрд▓реЛ, рдбреЗрдореЛ-рдХрд╛рд░реНрдб рдХреЗ рдкрддреЗ рдкрд░ рд╡рд┐рд╢реНрд╡-рдХрд╛рд░реНрдб рджреЗрдЦрддреЗ рд╣реИрдВред рддрджрдиреБрд╕рд╛рд░, рдЖрдЧреЗ-рдкреАрдЫреЗ рдХрд╛ рдиреЗрд╡рд┐рдЧреЗрд╢рди рднреА рдмрдВрдж рд╣реЛ рдЧрдпрд╛ред рдЖрдЗрдП рдЗрд╕реЗ рдареАрдХ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЗрддрд┐рд╣рд╛рд╕ рдХреЗ рд╕рд╛рде рдХреБрдЫ рдХрд╛рд▓рд╛ рдЬрд╛рджреВ рдЬреЛрдбрд╝реЗрдВ!

рд╕рдмрд╕реЗ рд╕рд░рд▓ рдмрд╛рдд рдЬреЛ рдЖрдк рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рд╡рд╣ рд╣реИ рдХрд╛рд░реНрд░рд╡рд╛рдИ рдореЗрдВ рдЬреЛрдбрд╝рдирд╛редnavigateрдПрдХ рдЪреБрдиреМрддреА history.pushStateред

 export function navigate(link) { history.pushState(null, "", link.href); return { type: NAVIGATE, path: link.pathname } } 

рдЕрдм, рд▓рд┐рдВрдХ рдкрд░ рдХреНрд▓рд┐рдХ рдХрд░рдиреЗ рдкрд░, рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреЗ рдПрдбреНрд░реЗрд╕ рдмрд╛рд░ рдореЗрдВ URL рд╡рд╛рд╕реНрддрд╡ рдореЗрдВ рдмрджрд▓ рдЬрд╛рдПрдЧрд╛ред рд╣рд╛рд▓рд╛рдБрдХрд┐, рдмреИрдХ рдмрдЯрди рдЯреВрдЯ рдЬрд╛рдПрдЧрд╛ !

рдЗрд╕реЗ рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдореЗрдВ popstateрдСрдмреНрдЬреЗрдХреНрдЯ рдХреА рдШрдЯрдирд╛ рдХреЛ рд╕реБрдирдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ windowред рдЗрд╕рдХреЗ рдЕрд▓рд╛рд╡рд╛, рдЕрдЧрд░ рдЗрд╕ рдШрдЯрдирд╛ рдореЗрдВ рд╣рдо рдиреЗрд╡рд┐рдЧреЗрд╢рди рдХреЛ рдЖрдЧреЗ рдФрд░ рдкреАрдЫреЗ (рдпрд╛рдиреА рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ dispatch(navigate(...))) рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ, рддреЛ рд╣рдореЗрдВ рдлрд╝рдВрдХреНрд╢рди рдореЗрдВ navigateрдПрдХ рд╡рд┐рд╢реЗрд╖ pushState" рдбреЛрдВрдЯ " рдлреНрд▓реИрдЧ рдЬреЛрдбрд╝рдирд╛ рд╣реЛрдЧрд╛ (рдЕрдиреНрдпрдерд╛ рд╕рдм рдХреБрдЫ рдФрд░ рднреА рдЕрдзрд┐рдХ рдЯреВрдЯ рдЬрд╛рдПрдЧрд╛!)ред рдЗрд╕рдХреЗ рдЕрд▓рд╛рд╡рд╛, "рд╣рдорд╛рд░реЗ" рд░рд╛рдЬреНрдпреЛрдВ рдХреЗ рдмреАрдЪ рдЕрдВрддрд░ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдореЗрдВ pushStateрдореЗрдЯрд╛рдбреЗрдЯрд╛ рдХреЛ рдмрдЪрд╛рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП ред рдмрд╣реБрдд рд╕рд╛рд░рд╛ рдЬрд╛рджреВ рдФрд░ рдбрд┐рдмрдЧ рд╣реИ, рддреЛ рдЪрд▓реЛ рдХреЛрдб рдХреЛ рд╕рд╣реА рдХрд░реЗрдВ! рдпрд╣рд╛рдВ рдмрддрд╛рдпрд╛ рдЧрдпрд╛ рд╣реИ рдХрд┐ рдРрдк рдХреИрд╕рд╛ рджрд┐рдЦреЗрдЧрд╛:

 // frontend/src/components/app.js class App extends Component { componentDidMount() { //     --   //      "". history.replaceState({ pathname: location.pathname, href: location.href }, ""); //     . window.addEventListener("popstate", event => this.navigate(event)); } navigate(event) { //    "" ,   //        ,    //   (or is it a good thing?..) if (event.state && event.state.pathname) { event.preventDefault(); event.stopPropagation(); //      "  pushState". this.props.dispatch(navigate(event.state, true)); } } render() { // ... } } 

рдФрд░ рдпрд╣рд╛рдБ рдиреЗрд╡рд┐рдЧреЗрдЯ рдХрд╛рд░реНрд░рд╡рд╛рдИ рд╣реИ:

 // frontend/src/redux/actions.js export function navigate(link, dontPushState) { if (!dontPushState) { history.pushState({ pathname: link.pathname, href: link.href }, "", link.href); } return { type: NAVIGATE, path: link.pathname } } 

рдЕрдм рдХрд╣рд╛рдиреА рдХрд╛рдо рдХрд░реЗрдЧреАред

рдЦреИрд░, рдЕрдВрддрд┐рдо рд╕реНрдкрд░реНрд╢: рдЪреВрдВрдХрд┐ рдЕрдм рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рдПрдХ рдХрд╛рд░реНрд░рд╡рд╛рдИ рд╣реИ navigate, рдЗрд╕рд▓рд┐рдП рд╣рдо рдЙрд╕ рдХреНрд▓рд╛рдЗрдВрдЯ рдореЗрдВ рдЕрддрд┐рд░рд┐рдХреНрдд рдХреЛрдб рдХреНрдпреЛрдВ рдирд╣реАрдВ рдЫреЛрдбрд╝рддреЗ рд╣реИрдВ рдЬреЛ рдкреНрд░рд╛рд░рдВрднрд┐рдХ рд╕реНрдерд┐рддрд┐ рдХреА рдЧрдгрдирд╛ рдХрд░рддрд╛ рд╣реИ? рд╣рдо рд╡рд░реНрддрдорд╛рди рд╕реНрдерд╛рди рдкрд░ рдиреЗрд╡рд┐рдЧреЗрдЯ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХреЙрд▓ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ:

 --- a/frontend/src/client.js +++ b/frontend/src/client.js @@ -3,23 +3,16 @@ import {render} from 'react-dom' import {Provider} from 'react-redux' import App from './components/app' import configureStore from './redux/configureStore' +import {navigate} from "./redux/actions"; let initialState = { page: { type: "home" } }; -const m = /^\/card\/([^\/]+)$/.exec(location.pathname); -if (m !== null) { - initialState = { - page: { - type: "card", - cardSlug: m[1] - }, - } -} const store = configureStore(initialState); +store.dispatch(navigate(location)); 

рдХреЙрдкреА-рдкреЗрд╕реНрдЯ рдирд╖реНрдЯ!

рдлрд╝реНрд░рдВрдЯреЗрдВрдб: рд╕рд░реНрд╡рд░-рд╕рд╛рдЗрдб рд░реЗрдВрдбрд░рд┐рдВрдЧ


рдпрд╣ рд╣рдорд╛рд░реЗ рдореБрдЦреНрдп (рдореЗрд░реА рд░рд╛рдп рдореЗрдВ) рдЪрд┐рдкреНрд╕ рдХреЗ рд▓рд┐рдП рд╕рдордп рд╣реИ - рдПрд╕рдИрдУ-рдорд┐рддреНрд░рддрд╛ред рддрд╛рдХрд┐ рдЦреЛрдЬ рдЗрдВрдЬрди рд╣рдорд╛рд░реА рд╕рд╛рдордЧреНрд░реА рдХреЛ рдЕрдиреБрдХреНрд░рдордгрд┐рдд рдХрд░ рд╕рдХреЗрдВ, рдЬреЛ рдкреВрд░реА рддрд░рд╣ рд╕реЗ рдЧрддрд┐рд╢реАрд▓ рд░реВрдк рд╕реЗ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛-рдШрдЯрдХреЛрдВ рдореЗрдВ рдмрдирд╛рдИ рдЧрдИ рд╣реИ, рд╣рдореЗрдВ рдЙрдиреНрд╣реЗрдВ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рджреЗрдиреЗ рдХреЗ рдкрд░рд┐рдгрд╛рдо рджреЗрдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реЛрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ, рдФрд░ рдпрд╣ рднреА рд╕реАрдЦреЗрдВ рдХрд┐ рдЗрд╕ рдкрд░рд┐рдгрд╛рдо рдХреЛ рдлрд┐рд░ рд╕реЗ рдЗрдВрдЯрд░реИрдХреНрдЯрд┐рд╡ рдХреИрд╕реЗ рдмрдирд╛рдпрд╛ рдЬрд╛рдПред

рд╕рд╛рдорд╛рдиреНрдп рдпреЛрдЬрдирд╛ рд╕рд░рд▓ рд╣реИред рдкрд╣рд▓рд╛: рд╣рдореЗрдВ рдЕрдкрдиреЗ рд░рд┐рдПрдХреНрдЯ рдШрдЯрдХ рджреНрд╡рд╛рд░рд╛ рдЙрддреНрдкрдиреНрди HTML рдХреЛ рд╣рдорд╛рд░реЗ HTML рдЯреЗрдореНрдкрд▓реЗрдЯ рдореЗрдВ рд╕рдореНрдорд┐рд▓рд┐рдд рдХрд░рдирд╛ рд╣реЛрдЧрд╛ Appред рдпрд╣ HTML рд╕рд░реНрдЪ рдЗрдВрдЬрди (рдФрд░ рдЬреЗрдПрд╕ рдмрдВрдж рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреЗ рд╕рд╛рде, рд╣реЗрд╣реЗ) рджреНрд╡рд╛рд░рд╛ рджреЗрдЦрд╛ рдЬрд╛рдПрдЧрд╛ред рджреВрд╕рд░рд╛: рдЯреЗрдореНрдкрд▓реЗрдЯ рдХреЛ рдПрдХ рдЯреИрдЧ рдЬреЛрдбрд╝реЗрдВ рдЬреЛ <script>рдХрд╣реАрдВ рдмрдЪрд╛рддрд╛ рд╣реИ (рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдПрдХ рдСрдмреНрдЬреЗрдХреНрдЯ window) рдПрдХ рд░рд╛рдЬреНрдп рдбрдВрдк рдЬрд┐рд╕рдореЗрдВ рд╕реЗ рдпрд╣ HTML рдкреНрд░рджрд╛рди рдХрд┐рдпрд╛ рдЧрдпрд╛ рдерд╛ред рдлрд┐рд░ рд╣рдо рддреБрд░рдВрдд рдЗрд╕ рд░рд╛рдЬреНрдп рдХреЗ рд╕рд╛рде рдЧреНрд░рд╛рд╣рдХ рдкрдХреНрд╖ рдкрд░ рдЕрдкрдиреЗ рдЖрд╡реЗрджрди рдХреЛ рдЗрдирд┐рд╢рд┐рдпрд▓рд╛рдЗрдЬрд╝ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдФрд░ рджрд┐рдЦрд╛ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рдХреНрдпрд╛ рдЬрд░реВрд░рдд рд╣реИ (рд╣рдо рд╣рд╛рдЗрдбреНрд░реЗрдЯ рдХрд╛ рдЙрдкрдпреЛрдЧ рднреА рдХрд░ рд╕рдХрддреЗ рд╣реИрдВрдЬрдирд░реЗрдЯ рдХрд┐рдП рдЧрдП HTML рдореЗрдВ, рддрд╛рдХрд┐ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ DOM рдЯреНрд░реА рдХреЛ рдлрд┐рд░ рд╕реЗ рди рдмрдирд╛рдпрд╛ рдЬрд╛рдП)ред

рдЖрдЗрдП рдПрдХ рдлрд╝рдВрдХреНрд╢рди рд▓рд┐рдЦрдХрд░ рд╢реБрд░реВ рдХрд░реЗрдВ рдЬреЛ HTML рдФрд░ рдЕрдВрддрд┐рдо рд╕реНрдерд┐рддрд┐ рдкреНрд░рджрд╛рди рдХрд░рддрд╛ рд╣реИред

 // frontend/src/server.js import "@babel/polyfill" import React from 'react' import {renderToString} from 'react-dom/server' import {Provider} from 'react-redux' import App from './components/app' import {navigate} from "./redux/actions"; import configureStore from "./redux/configureStore"; export default function render(initialState, url) { //  store,    . const store = configureStore(initialState); store.dispatch(navigate(url)); let app = ( <Provider store={store}> <App/> </Provider> ); // ,        ! // ,         ? let content = renderToString(app); let preloadedState = store.getState(); return {content, preloadedState}; }; 

рд╣рдорд╛рд░реЗ рдЯреЗрдореНрдкрд▓реЗрдЯ рдореЗрдВ рдирдП рддрд░реНрдХ рдФрд░ рддрд░реНрдХ рдЬреЛрдбрд╝реЗрдВ, рдЬрд┐рдирдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рд╣рдордиреЗ рдКрдкрд░ рдмрддрд╛рдпрд╛ рд╣реИ:

 // frontend/src/template.js function template(title, initialState, content) { let page = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>${title}</title> </head> <body> <div id="app">${content}</div> <script> window.__STATE__ = ${JSON.stringify(initialState)} </script> <script src="/dist/client.js"></script> </body> </html> `; return page; } module.exports = template; 

рд╣рдорд╛рд░рд╛ рдПрдХреНрд╕рдкреНрд░реЗрд╕ рд╕рд░реНрд╡рд░ рдереЛрдбрд╝рд╛ рдФрд░ рдЬрдЯрд┐рд▓ рд╣реЛ рдЧрдпрд╛ рд╣реИ:

 // frontend/index.js app.get("*", (req, res) => { const initialState = { page: { type: "home" } }; const {content, preloadedState} = render(initialState, {pathname: req.url}); res.send(template("Habr demo app", preloadedState, content)); }); 

рд▓реЗрдХрд┐рди рдЧреНрд░рд╛рд╣рдХ рдЖрд╕рд╛рди рд╣реИ:

 // frontend/src/client.js import React from 'react' import {hydrate} from 'react-dom' import {Provider} from 'react-redux' import App from './components/app' import configureStore from './redux/configureStore' import {navigate} from "./redux/actions"; //         ! const store = configureStore(window.__STATE__); // render   hydrate. hydrate    // DOM tree,       . hydrate( <Provider store={store}> <App/> </Provider>, document.querySelector('#app') ); 

рдЕрдЧрд▓рд╛, рдЖрдкрдХреЛ "рдЗрддрд┐рд╣рд╛рд╕ рдкрд░рд┐рднрд╛рд╖рд┐рдд рдирд╣реАрдВ рд╣реИ" рдЬреИрд╕реЗ рдХреНрд░реЙрд╕-рдкреНрд▓реЗрдЯрдлрд╝реЙрд░реНрдо рддреНрд░реБрдЯрд┐рдпреЛрдВ рдХреЛ рд╕рд╛рдл рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред рдРрд╕рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдПрдХ рд╕рд░рд▓ (рдЕрдм рддрдХ) рдлрд╝рдВрдХреНрд╢рди рдХреЛ рдХрд╣реАрдВ рдкрд░ рдЬреЛрдбрд╝реЗрдВ utility.jsред

 // frontend/src/utility.js export function isServerSide() { //   ,      process, //     -   . return process.env.APP_ENV !== undefined; } 

рдлрд┐рд░ рдПрдХ рдирд┐рд╢реНрдЪрд┐рдд рд╕рдВрдЦреНрдпрд╛ рдореЗрдВ рджрд┐рдирдЪрд░реНрдпрд╛ рдореЗрдВ рдмрджрд▓рд╛рд╡ рд╣реЛрдВрдЧреЗ рдЬреЛ рдореИрдВ рдпрд╣рд╛рдВ рдирд╣реАрдВ рд▓рд╛рдКрдВрдЧрд╛ (рд▓реЗрдХрд┐рди рдЙрдиреНрд╣реЗрдВ рдЗрд╕рдХреЗ рдЕрдиреБрд░реВрдк рдкреНрд░рддрд┐рдмрджреНрдз рдкрд╛рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ )ред рдкрд░рд┐рдгрд╛рдорд╕реНрд╡рд░реВрдк, рд╣рдорд╛рд░рд╛ рд░рд┐рдПрдХреНрдЯ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдФрд░ рд╕рд░реНрд╡рд░ рджреЛрдиреЛрдВ рдореЗрдВ рд░реЗрдВрдбрд░ рдХрд░рдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реЛрдЧрд╛ред

рдпрд╣ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИ!рд▓реЗрдХрд┐рди рд╡рд╣рд╛рдБ рд╣реИ, рдЬреИрд╕рд╛ рдХрд┐ рд╡реЗ рдХрд╣рддреЗ рд╣реИрдВ, рдПрдХ рдЪреЗрддрд╛рд╡рдиреА ...



рд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ? рдореЗрд░реА рд╕реБрдкрд░-рдХреВрд▓ рдлреИрд╢рди рд╕реЗрд╡рд╛ рдкрд░ Google рдЬреЛ рджреЗрдЦ рд░рд╣рд╛ рд╣реИ рд╡рд╣ рд╕рднреА LO LOING рд╣реИред

рдЦреИрд░, рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рд╣рдорд╛рд░реЗ рд╕рднреА рдЕрддреБрд▓реНрдпрдХрд╛рд▓рд┐рдХрддрд╛ рд╣рдорд╛рд░реЗ рдЦрд┐рд▓рд╛рдл рдЦреЗрд▓реЗ рд╣реИрдВред рдЕрдм рд╣рдореЗрдВ рд╕рд░реНрд╡рд░ рдХреЛ рдпрд╣ рд╕рдордЭрдиреЗ рдХрд╛ рдПрдХ рддрд░реАрдХрд╛ рдЪрд╛рд╣рд┐рдП рдХрд┐ рдХрд╛рд░реНрдб рдХреА рд╕рд╛рдордЧреНрд░реА рдХреЗ рд╕рд╛рде рдмреИрдХрдПрдВрдб рд╕реЗ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдХреЛ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдПрдХ рд╕реНрдЯреНрд░рд┐рдВрдЧ рдореЗрдВ рд░реЗрдВрдбрд░ рдХрд░рдиреЗ рдФрд░ рдХреНрд▓рд╛рдЗрдВрдЯ рдХреЛ рднреЗрдЬрдиреЗ рд╕реЗ рдкрд╣рд▓реЗ рдЗрдВрддрдЬрд╛рд░ рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рдФрд░ рдпрд╣ рд╡рд╛рдВрдЫрдиреАрдп рд╣реИ рдХрд┐ рдпрд╣ рд╡рд┐рдзрд┐ рдХрд╛рдлреА рд╕рд╛рдорд╛рдиреНрдп рд╣реЛред

рдХрдИ рд╕рдорд╛рдзрд╛рди рд╣реЛ рд╕рдХрддреЗ рд╣реИрдВред рдПрдХ рджреГрд╖реНрдЯрд┐рдХреЛрдг рдПрдХ рдЕрд▓рдЧ рдлрд╝рд╛рдЗрд▓ рдореЗрдВ рд╡рд░реНрдгрди рдХрд░рдирд╛ рд╣реИ рдХрд┐ рдХрд┐рд╕ рдкрде рдХреЗ рд▓рд┐рдП рдбреЗрдЯрд╛ рд╕реБрд░рдХреНрд╖рд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдП, рдФрд░ рдЖрд╡реЗрджрди ( рд▓реЗрдЦ ) рдкреНрд░рджрд╛рди рдХрд░рдиреЗ рд╕реЗ рдкрд╣рд▓реЗ рдРрд╕рд╛ рдХрд░реЗрдВ ред рдЗрд╕ рдШреЛрд▓ рдХреЗ рдХрдИ рдлрд╛рдпрджреЗ рд╣реИрдВред рдпрд╣ рд╕рд░рд▓ рд╣реИ, рдпрд╣ рд╕реНрдкрд╖реНрдЯ рд╣реИ, рдФрд░ рдпрд╣ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИред

рдПрдХ рдкреНрд░рдпреЛрдЧ рдХреЗ рд░реВрдк рдореЗрдВ (рдореВрд▓ рд╕рд╛рдордЧреНрд░реА рдХрдо рд╕реЗ рдХрдо рдХрд╣реАрдВ рд▓реЗрдЦ рдореЗрдВ рд╣реЛрдиреА рдЪрд╛рд╣рд┐рдП!) рдореИрдВ рдПрдХ рдФрд░ рдпреЛрдЬрдирд╛ рдХрд╛ рдкреНрд░рд╕реНрддрд╛рд╡ рдХрд░рддрд╛ рд╣реВрдВред рд╣рд░ рдмрд╛рд░ рдЬрдм рд╣рдо рдХреБрдЫ рдЕрддреБрд▓реНрдпрдХрд╛рд▓рд┐рдХ рдЪрд▓рд╛рддреЗ рд╣реИрдВ, рдЬрд┐рд╕рдХрд╛ рд╣рдореЗрдВ рдЗрдВрддрдЬрд╛рд░ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП, рддреЛ рдЙрдкрдпреБрдХреНрдд рд╡рд╛рджрд╛ (рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдЬреЛ рд╡рд╛рдкрд╕ рд▓рд╛рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ) рдХреЛ рд╣рдорд╛рд░реЗ рд░рд╛рдЬреНрдп рдореЗрдВ рдХрд╣реАрдВ рди рдХрд╣реАрдВ рдЬреЛрдбрд╝реЗрдВред рдЗрд╕рд▓рд┐рдП рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рдПрдХ рдЬрдЧрд╣ рд╣реЛрдЧреА рдЬрд╣рд╛рдВ рдЖрдк рд╣рдореЗрд╢рд╛ рджреЗрдЦ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рдХреНрдпрд╛ рд╕рдм рдХреБрдЫ рдбрд╛рдЙрдирд▓реЛрдб рд╣реЛ рдЪреБрдХрд╛ рд╣реИред

рджреЛ рдирдИ рдХреНрд░рд┐рдпрд╛рдПрдВ рдЬреЛрдбрд╝реЗрдВред

 // frontend/src/redux/actions.js function addPromise(promise) { return { type: ADD_PROMISE, promise: promise }; } function removePromise(promise) { return { type: REMOVE_PROMISE, promise: promise, }; } 


рдЬрдм рднреНрд░реВрдг рд▓реЙрдиреНрдЪ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ, рддреЛ рдкрд╣рд▓реЗ рдХреЛ рдмреБрд▓рд╛рдпрд╛ рдЬрд╛рдПрдЧрд╛, рджреВрд╕рд░рд╛ - рдЗрд╕рдХреЗ рдЕрдВрдд рдореЗрдВ .then()ред

рдЕрдм рдЙрдирдХреЗ рдкреНрд░рд╕рдВрд╕реНрдХрд░рдг рдХреЛ reducer рдореЗрдВ рдЬреЛрдбрд╝реЗрдВ:

 // frontend/src/redux/reducers.js export default function root(state = {}, action) { switch (action.type) { case ADD_PROMISE: return { ...state, promises: [...state.promises, action.promise] }; case REMOVE_PROMISE: return { ...state, promises: state.promises.filter(p => p !== action.promise) }; ... 

рдЕрдм рд╣рдо рдХрд╛рд░реНрд░рд╡рд╛рдИ рдореЗрдВ рд╕реБрдзрд╛рд░ рдХрд░реЗрдВрдЧреЗ fetchCard:

 // frontend/src/redux/actions.js function fetchCard() { return (dispatch, getState) => { dispatch(startFetchingCard()); let url = apiPath() + "/card/" + getState().page.cardSlug; let promise = fetch(url) .then(response => response.json()) .then(json => { dispatch(finishFetchingCard(json)); // " ,  " dispatch(removePromise(promise)); }); // "  ,  " return dispatch(addPromise(promise)); }; } 

рдпрд╣ initialStateрдЦрд╛рд▓реА рд╕рд░рдгреА рдореЗрдВ рд╡рд╛рджреЛрдВ рдХреЛ рдЬреЛрдбрд╝рдиреЗ рдФрд░ рд╕рд░реНрд╡рд░ рдХреЛ рдЙрди рд╕рднреА рдХреЗ рд▓рд┐рдП рдЗрдВрддрдЬрд╛рд░ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдмрдиреА рд╣реБрдИ рд╣реИ ! рд░реЗрдВрдбрд░ рдлрд╝рдВрдХреНрд╢рди рдЕрддреБрд▓реНрдпрдХрд╛рд▓рд┐рдХ рд╣реЛ рдЬрд╛рддрд╛ рд╣реИ рдФрд░ рдирд┐рдореНрди рд░реВрдк рд▓реЗрддрд╛ рд╣реИ:

 // frontend/src/server.js function hasPromises(state) { return state.promises.length > 0 } export default async function render(initialState, url) { const store = configureStore(initialState); store.dispatch(navigate(url)); let app = ( <Provider store={store}> <App/> </Provider> ); //  renderToString     // (  ). CardPage     . renderToString(app); // ,   !    - //    (  // , ),     //    . let preloadedState = store.getState(); while (hasPromises(preloadedState)) { await preloadedState.promises[0]; preloadedState = store.getState() } //  renderToString.    HTML. let content = renderToString(app); return {content, preloadedState}; }; 

рдЕрдзрд┐рдЧреНрд░рд╣реАрдд renderрдЕрддреБрд▓реНрдпрдХрд╛рд▓рд┐рдХрддрд╛ рдХреЗ рдХрд╛рд░рдг , рдЕрдиреБрд░реЛрдз рд╣реИрдВрдбрд▓рд░ рднреА рдереЛрдбрд╝рд╛ рдЕрдзрд┐рдХ рдЬрдЯрд┐рд▓ рд╣реИ:

 // frontend/index.js app.get("*", (req, res) => { const initialState = { page: { type: "home" }, promises: [] }; render(initialState, {pathname: req.url}).then(result => { const {content, preloadedState} = result; const response = template("Habr demo app", preloadedState, content); res.send(response); }, (reason) => { console.log(reason); res.status(500).send("Server side rendering failed!"); }); }); 

Et voil├а!



рдирд┐рд╖реНрдХрд░реНрд╖


рдЬреИрд╕рд╛ рдХрд┐ рдЖрдк рджреЗрдЦ рд╕рдХрддреЗ рд╣реИрдВ, рд╣рд╛рдИ-рдЯреЗрдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛рдирд╛ рдЗрддрдирд╛ рд╕рд░рд▓ рдирд╣реАрдВ рд╣реИред рд▓реЗрдХрд┐рди рдЗрддрдирд╛ рдореБрд╢реНрдХрд┐рд▓ рдирд╣реАрдВ! рдЕрдВрддрд┐рдо рдЖрд╡реЗрджрди рдЧрд┐рддреБрдм рдкрд░ рднрдВрдбрд╛рд░ рдореЗрдВ рд╣реИ рдФрд░, рд╕реИрджреНрдзрд╛рдВрддрд┐рдХ рд░реВрдк рд╕реЗ, рдЖрдкрдХреЛ рдЗрд╕реЗ рдЪрд▓рд╛рдиреЗ рдХреЗ рд▓рд┐рдП рдХреЗрд╡рд▓ рдбреЙрдХрд░ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред

рдпрджрд┐ рд▓реЗрдЦ рдорд╛рдВрдЧ рдореЗрдВ рд╣реИ, рддреЛ рдЗрд╕ рднрдВрдбрд╛рд░ рдХреЛ рднреА рдирд╣реАрдВ рдЫреЛрдбрд╝рд╛ рдЬрд╛рдПрдЧрд╛! рд╣рдо рдЗрд╕реЗ рдЕрдиреНрдп рдЬреНрдЮрд╛рди рд╕реЗ рдХреБрдЫ рдХреЗ рд╕рд╛рде рджреЗрдЦ рдкрд╛рдПрдВрдЧреЗ рдЬреЛ рдЖрд╡рд╢реНрдпрдХ рд╣реИ:

  • рд▓реЙрдЧрд┐рдВрдЧ, рдирд┐рдЧрд░рд╛рдиреА, тАЛтАЛрд▓реЛрдб рдкрд░реАрдХреНрд╖рдгред
  • рдкрд░реАрдХреНрд╖рдг, рд╕реАрдЖрдИ, рд╕реАрдбреАред
  • рдХреВрд▓рд░ рдореЗрдВ рдкреНрд░рд╛рдзрд┐рдХрд░рдг рдпрд╛ рдкреВрд░реНрдг-рдкрд╛рда рдЦреЛрдЬ рдЬреИрд╕реА рд╡рд┐рд╢реЗрд╖рддрд╛рдПрдВ рд╣реИрдВред
  • рдЙрддреНрдкрд╛рджрди рд╡рд╛рддрд╛рд╡рд░рдг рдХрд╛ рд╕реЗрдЯрдЕрдк рдФрд░ рд╡рд┐рдХрд╛рд╕ред

рдЖрдкрдХрд╛ рдзреНрдпрд╛рди рдХреЗ рд▓рд┐рдП рдзрдиреНрдпрд╡рд╛рдж!

Source: https://habr.com/ru/post/hi444446/


All Articles