【實作記錄】Netflix Clone:首頁

Live Demo
Github

Netflix Clone Demo

簡介

首頁由 5 個部分組成:Header 首頁大圖、Jumbotron 資訊塊、Accordion 常見問題、Otp form 訂閱表單、Footer 頁尾。每一塊都為單獨的component,組合起來後才渲染到瀏覽器。每一個 component 的創建邏輯相似:創建需要的元件 > 創建組裝元件的 container,把需要的元件排進去 > 添加樣式。

分開創建 component 除了便於維護外,最大的功能在於可以重複使用,就如 part 4 的 Opt form 在 part 5 的 header 中可以直接套用。


Part 1 : Jumbotron 資訊塊 + Global style

在首頁裡一共有 3 個 Jumbotron,其組成有:照片、標題、副標題。會先創需要的元件,再將他們放進 container 裡,最後進行樣式調整。

樣式部分不會全部都放在筆記裡,主要記下架構邏輯的部分。


創建基本檔案

  • components > jumbotron > index.js : 處理 Jumbotron
  • components > jumbotron > styles > jumbotron.js : 引入 styled ,在裡面處理 styled component
  • Home.js : 首頁大圖會出現的地方
  • containers > jumbotron.js

index.js 中創立 jumbotron 需要的所有元件:container, title, subtitle, Image 和後來添加的 pane.

index.js (Component > Jumbotron) >folded
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
import React from "react"
import { Item, Inner, Container, Title, Subtitle } from "./styles/Jumbotron"

function Jumbotron({ children, direction = "row", ...restProps }){
return (
<Item {...restProps}>
<Inner direction={direction}>{children}</Inner>
</Item>
)
}

Jumbotron.Container = function JumbotronContainer({ children, ...restProps }){
return <Container {...restProps}>{children}</Container>
}

Jumbotron.Title = function JumbotronTitle({ children, ...restProps }){
return <Title {...restProps}>{children}</Title>
}

Jumbotron.SubTitle = function JumbotronSubTitle({ children, ...restProps }){
return <Subtitle {...restProps}>{children}</Subtitle>
}

Jumbotron.Pane = function JumbotronPane({ children, ...restProps }){
return <Pane {...restProps}>{children}</Pane>
}

Jumbotron.Image = function JumbotronSubTitle({ ...restProps }){
return <Image {...restProps}/>
}

export default Jumbotron
  • 第 4 行 : 引入 Jumbotron.js 中設定的每個區塊
  • 第 6 行 : 創建一個會傳入 children , direction (jumbo.json中有出現的) 以及剩餘的 props 參數的 function.
  • 第 7 -12 行 : 這個 function 會返回一個 < Item>, 他的 children 是 < Inner>, < Inner > 的 children 可以是下面的 container/ title/ subtitle
  • 第 14 行 : 創造新的變數,這個變數是一個 function,傳入的參數為 children 以及剩餘的 props. 後返回一個 < Container > element.
  • 第 15 行 : < Container > element 傳入的參數說所有的 props, children 為 children
    • 應該會是字串,顯示在熒幕上的字等等的
    • 比如說 Home.js 中的 < Jumbotron.Title > 包的東西就是 < Title > 的children
Home.js (Pages) >folded
1
2
3
4
5
6
7
8
9
10
11
12
13
// filename : Home.js (Pages)

import React from 'react'
import Jumbotron from '../components/jumbotron'

export default function Home() {
return (
<Jumbotron.Container>
<Jumbotron.Title>Hello</Jumbotron.Title>
<Jumbotron.SubTitle>Huala</Jumbotron.SubTitle>
</Jumbotron.Container>
)
}
  • 第 4 行 : 引入上面建立好的 Jumbotron
  • 第 8 行 : 可以直接使用 index.js 中創造好的變數

把首頁需要的資訊渲染到畫面上

  • 把在 Home.js 中的 container component 移到另一個檔案 : Jumbotron.js ( container > Jumbotron.js),再引入 Home.js 中
jumbotron.js (Containers > jumbotron.js) >folded
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

// filename : jumbotron.js (Containers > jumbotron.js)

import React from "react"
import Jumbotron from "../components/jumbotron"
import jumboData from "../fixtures/jumbo.json"

function JumbotronContainer(){
return(
<Jumbotron.Container>
{jumboData.map((item) => (
// 會使用 index.js 中的 Jumbotron function 作為模板,title/subtitle這類的會變成children
<Jumbotron key={item.id} direction={item.direction}>
<Jumbotron.Pane> //style component 需要的,為標題和照片增加padding
<Jumbotron.Title >{item.title}</Jumbotron.Title>
<Jumbotron.SubTitle>{item.subTitle}</Jumbotron.SubTitle>
</Jumbotron.Pane>

<Jumbotron.Pane>
<Jumbotron.Image src={item.image} alt={item.name}></Jumbotron.Image>
</Jumbotron.Pane>
</Jumbotron>
))}
</Jumbotron.Container>
)
}

export { JumbotronContainer }
jumbo.json ( fixtures > jumbo.json ) >folded
1
2
3
4
5
6
7
8
9
10
11
// filename : jumbo.json ( fixtures > jumbo.json )
//裡面共有三筆資料,每筆資料長這樣

{
"id": 1,
"title": "Enjoy on your TV.",
"subTitle": "Watch on smart TVs, PlayStation, Xbox, Chromecast, Apple TV, Blu-ray players and more.",
"image": "/images/misc/home-tv.png",
"alt": "Tiger King on Netflix",
"direction": "row" //styled > jumbotron.js 中會用到
}

設計首頁每一個 Jumbotron

  • 需要 title、Subtitle、Image
Jumbotron.js (Component > Jumbotron > styles) >folded
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

// filename : Jumbotron.js (Component > Jumbotron > styles)
// 處理 styled component,順便設置不同裝置熒幕尺寸

import styled from 'styled-components'

export const Inner = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: ${({ direction }) => direction}; //根據jumbo.json 裡的 direction 改變
max-width: 1100px;
margin: auto;
width: 100%;

@media(max-width: 1000px) {
flex-direction: column;
}
`

//標題和圖片間增加 padding
export const Pane = styled.div`
width : 50%;

@media (max-width: 1000px) {
width:100%;
padding: 0 45px;
text-alighn: center;
}
`

export const Item = styled.div`
display: flex;
border-bottom: 8px solid #222;
padding: 50px 5%;
color: white;
overflow: hidden;
`
//手機裝置最後一個 jumbotron 會太靠近底部,增加bottom
export const Container = styled.section`
background-color: black;

@media (max-width: 1000px) {
${Item}:last-of-type h2 {
margin-bottom: 50px;
}
text-align: center;
}
`

export const Title = styled.h1`
font-size: 50px;
line-height: 1.1;
margin-bottom: 8px;

@media (max-width: 600px) {
font-size: 35px;
}
`

export const SubTitle = styled.h2`
font-size: 26px;
font-weight: normal;
line-height: normal;

@media (max-width: 600px) {
font-size: 18px;
}
`

export const Image = styled.img`
max-width: 100%;
height: auto;
`


整理 Component Library & Implementing Global Styles With Styled Components

  • Component Library
    • 之後會有很多 component 要加進來,如果每次都單獨 import 檔案進來看起來就會冗冗的
    • 在 index.js (Component) 把路徑設成 {Jumbotron} 之後要引用這個路徑就可以直接寫 Jumbotron
    • 更改 jumbotron.js (containers) 的路徑
index.js (Component) >folded
1
export { default as Jumbotron } from './jumbotron';
jumbotron.js (containers) >folded
1
2
3
4
// filename : jumbotron.js (containers)

//原本的 : import Jumbotron from "../components/jumbotron"
import { Jumbotron } from "../components"
  • Global Styles
    • 使用 styled component 會難以估計每個元件預設的值是多少
    • 可以創建一個新的 conponent 來儲存 global 的樣式
    • 這個 component 要加在最根部,進行渲染的那一頁 (index.js)
filename : global-style.js >folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// filename : "global-style.js"

import { createGlobalStyle } from 'styled-components';

export const GlobalStyles = createGlobalStyle`
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #000000;
color: #333333;
font-size: 16px;
margin: 0;
}
`;
index.js >folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//filename : index.js

import React from "react"
import ReactDOM from "react-dom"
import { BrowserRouter } from "react-router-dom"
import { GlobalStyles } from "./global-style"
import "./index.css"
import App from "./App"


ReactDOM.render(
<>
<GlobalStyles/>
<BrowserRouter>
<App />
</BrowserRouter>
</>,
document.getElementById('root')
);

Footer 一共有 4 欄,其排版會使用 gird 來進行。在縮小的時候會先變成 3 欄 ,再變為兩欄。


創建基本檔案

  • components > footer > index.js : footer 會在裡面處理
  • components > footer > styles > footer.js : 引入 styled ,在裡面處理 styled component
  • containers > footer.js
  • 在 index.js (components 的) 添加 export { default as Footer } from './footer'; , 會在 footer.js (containers) 中引入
  • Home.js 中增加 footer container

建立需要的元件

index.js 中創立 footer 需要的所有元件:container, row, column, title, link 和 break.

index.js (conponents > footer) >folded
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
//filename : index.js (conponents > footer)

import React from "react"
import { Container, Row, Column, Link, Title, Break, Text } from "./styles/footer"

function Footer({ children, ...restProps }) {
return <Container {...restProps}>{children}</Container>
}

Footer.Row = function FooterRow({ children, ...restProps }) {
return <Row {...restProps}>{children}</Row>
}

Footer.Column = function FooterColumn ({ children, ...restProps }) {
return <Column {...restProps}>{children}</Column>
}

Footer.Link = function FooterLink({ children, ...restProps }) {
return <Link {...restProps}>{children}</Link>
}

Footer.Title = function FooterTitle({ children, ...restProps }) {
return <Title {...restProps}>{children}</Title>
}

Footer.Text = function FooterText({ children, ...restProps }) {
return <Text {...restProps}>{children}</Text>
}

Footer.Break = function FooterBreak ({ ...restProps }) {
return <Break {...restProps}/>
}

export { FooterContainer }
  • 第 4 行 : 引入 styled component
  • 第 6 行 : 設置 Footer container, children 是下面的一大串 (row, column…)
  • 第 10 行 : 設置 Row function ,會傳入 children 和其他 props, return < Row >,其他的同理
  • 第 30 行 : 排版的時候會用到空行,所以這裡設置 break component,排版的時候就可以用

把每一個需要的鏈接都放在 container 裡. 因為使用 grid 所以需要 Column 和 Row.

footer.js (containers > footer.js) >folded
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
// filename : footer.js (containers > footer.js)
// 處理 footer links 的部分

import React from "react"
import { Footer } from "../components"
//這裡在 index.js (components) 處理過了

function FooterContainer() {
return (
<Footer>
<Footer.Title>Question?Contact us.</Footer.Title>
<Footer.Break />
<Footer.Row>
<Footer.Column>
<Footer.Link href="#">FAQs</Footer.Link>
<Footer.Link href="#">Investor Relations</Footer.Link>
<Footer.Link href="#">Ways to watch</Footer.Link>
<Footer.Link href="#">Corparate Informations</Footer.Link>
<Footer.Link href="#">Netflix Originals</Footer.Link>
</Footer.Column>
·
·
·
</Footer.Column>
</Footer.Row>
<Footer.Break/>
</Footer>
)
}

export default FooterContainer

Part 3: Accordion 常見問題

常見問題的部分會使用手風琴式選單來製作(可以點開收起)。其中需要的元件有標題、問題、內容(回答)、點擊按鈕。

常見問題 (FAQ) 與 Jumbotron 製作的方式類似,都有幾筆類型相同、格式也相同的資料,因此渲染到畫面上的方法是一樣的。


創建基本檔案

  • components > Accordion > index.js : Accordion 會在裡面處理
  • components > Accordion > styles > accordion .js : 引入 styled ,在裡面處理 styled component
  • containers > faq.js
  • 在 index.js (components 的) 添加 export { default as Accordion } from './accordion'; , 會在 accordion .js (containers) 中引入
  • Home.js 中增加 Accordion container

建立需要的元件 + 把需要的資料渲染到瀏覽器上 (直到這裡的步驟都與 jumbotron 的相同)

  • 建立 index.js (之後會詳細處理 state 的部分,這裡不放程式碼) ,創建基本的架構
  • 建立 accordion.js (component > accrodion > styles),確保index.js 中需要的元件都有在 accordion.js 中出現,避免報錯
  • FAQ 資料保存在 faqs.json (fixtures) 中,一共有4筆。資料形態如下:
faqs.json >folded
1
2
3
4
5
6
7
// filename : faqs.json

{
"id": 1,
"header": "What is Netflix?",
"body": "Netflix is a streaming service that offers a wide variety of award-winning TV programmes, films, anime, documentaries and more – on thousands of internet-connected devices.\n\nYou can watch as much as you want, whenever you want, without a single advert – all for one low monthly price. There's always something new to discover, and new TV programmes and films are added every week!"
},
faqs.js (containers) >folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// filename : faqs.js (containers)

import React from "react"
import { Accordion } from "../components"
import faqsData from "../fixtures/faqs.json"

function FaqsContainer(){
return(
<Accordion >
<Accordion.Title>Frequently Asked Questions</Accordion.Title>
<Accordion.Frame> //這是用來補充 margin 的
{faqsData.map((item) => (
<Accordion.Item key={item.id}>
<Accordion.Header>{item.header}</Accordion.Header>
<Accordion.Body>{item.body}</Accordion.Body>
</Accordion.Item>
))}
</Accordion.Frame>
</Accordion>

)
}
export { FaqsContainer}

Part 4 : Otp form 訂閱表單 + Router 處理

提供用戶訂閱的 Opt Form 會放在 FAQs 那一個部分裡,因此只需要創建 Opt Form 的 component,不需要將其 container 獨立出來。

另外文末也會進行 Router 處理。


創建基本檔案

  • components > Opt-Form > index.js : opt-form 會在裡面處理
  • components > Opt-Form > styles > opt-form .js : 引入 styled ,在裡面處理 styled component
  • 在 index.js (components 的) 添加 export { default as Accordion } from ‘./Opt-Form’;
index.js (components > opt-form) >folded
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
//filename : index.js (components > opt-form )

import React from 'react';
import { Container, Input, Break , Button, Text } from './styles/opt-form';

function OptForm({ children, ...restProps }) {
return <Container {...restProps}>{children}</Container>
}

OptForm.Input = function OptFormInput({ ...restProps }) {
return <Input {...restProps} />
}

OptForm.Button = function OptFormButton({ children, ...restProps }) {
return (
<Button {...restProps}>
{children} <i className="fas fa-chevron-right"> </i>
</Button>
)
}

OptForm.Break = function OptBreak({ ...restProps }) {
return <Break {...restProps} />;
}


OptForm.Text = function OptFormText({ children, ...restProps }) {
return <Text {...restProps}>{children}</Text>
}

export default OptForm

處理 components (pages) 的 router

  • 如果之後要更換路徑,只要更新一個地方就可以了,比較容易維護
  • 創建好之後就可以更換 App.js 中的路徑
Routes.js (constants) >folded
1
2
3
4
5
6
// filename :Routes.js (constants)

export const HOME = '/'
export const BROWSE = '/browse'
export const SIGN_UP = '/signup'
export const SIGN_IN = '/signin'
App.js >folded
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
//filename : App.js

import React from 'react'
import { Switch, Route } from 'react-router-dom'
import * as ROUTES from './constants/routes'; //引入
import Home from "./pages/Home"

function App() {
return (
<Switch>

<Route path={ROUTES.SIGN_IN}> //這樣使用
<p>Sign in page</p>
</Route>

<Route path={ROUTES.SIGN_UP}>
<p>Sign up page</p>
</Route>

<Route path={ROUTES.BROWSE}>
<p>browse page</p>
</Route>

<Route path={ROUTES.HOME}>
<Home />
</Route>

</Switch>
);
}

export default App;

Part 5 : Header 首頁大圖


創建基本檔案

  • components > header > index.js : header 會在裡面處理
  • components > header > styles > header .js : 引入 styled ,在裡面處理 styled component
  • containers > header.js
  • 在 index.js (components 的) 添加 export { default as Header } from ‘./header’; , 會在 header .js (containers) 中引入
  • Home.js 中增加 header container

建構需要的元件

頁首需要的元件:背景, Logo, Sign In button, opt form . Logo 和 Sign In button 點擊後會跳往指定的頁。Opt form 上一個部分已經做了,所以可以直接使用。

index.js (containers > header) >folded
1
2
3
4
5
6
7
<p>//filename : index.js (containers &gt; header)</p>
<p>import React from “react”<br>import { Link as ReachRouterLink } from “react-router-dom”<br>import { Background, Container, Logo, ButtonLink } from “./styles/header”</p>
<p>function Header({ bg= true, children, …restProps }){<br> return bg ? &lt;Background {…restProps}&gt;{children}</Background> : children<br>}</p>
<p>Header.Frame = function HeaderFrame({ children, …restProps }) {<br> return &lt;Container {…restProps}&gt;{children}</Container><br>}</p>
<p>Header.Logo = function HeaderLogo({ to, …restProps }){<br> return(<br> <ReachRouterLink to={to}><br> &lt;Logo {…restProps} /&gt;<br> </ReachRouterLink><br> )<br>}</p>
<p>Header.ButtonLink = function HeaderButtonLink({ children, …restProps }){<br> return &lt;ButtonLink {…restProps}&gt;{children}</ButtonLink><br>}</p>
<p>export default Header</p>
  • 第 4 行 : logo & signin button 需要使用 Link
  • 第 7 行 : bg 為 true ,會顯示背景圖片
  • 第 11 行 : Header.Frame 會返回 Container
  • 第 15 行 : 使用 Link 把 Logo 包起來,讓他變成點擊後會跳轉的
  • 第 23 行 : Header.ButtonLink 是 for sign in button 的

把需要的元件排進 container 裡

header.js (components > header) >folded
1
2
3
4
<p>//filename : header.js (components &gt; header)</p>
<p>import React from “react”<br>import { Header } from “../components”<br>import * as ROUTES from “../constants/routes”;</p>
<p>function HeaderContainer({ children }){<br> return(<br> <Header><br> &lt;Header.Frame&gt;<br> &lt;Header.Logo<br> to={ROUTES.HOME}<br> src=”/images/misc/logo.png”<br> alt=”Netflix”<br> /&gt;<br> &lt;Header.ButtonLink to={ROUTES.SIGN_IN}&gt;Sign In&lt;/Header.ButtonLink&gt;<br> &lt;/Header.Frame&gt;<br> {children}<br> </Header><br> )<br>}</p>
<p>export default HeaderContainer</p>
  • 第 5 行 : 引入 route.js,為 logo & sign in button 加上 Link
  • 第 11 行 : Header.Logo 是個 Link 所以需要 to
  • 第 16 行 : 與第 11 行 同理

加入 Opt Form

會有 title, subtitle, form. 為了方便,title, subtitle 會另外創一個 container 來裝。

跟上面的步驟一樣,在 containers 中創建 feature. feature 中創建 index.js 以及 styles, styles 中創建 feature.js.

index.js (components > feature) >folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//filename : index.js (components > feature)

import React from "react"
import { Container, Title, SubTitle } from "./styles/feature"

function Feature({ children, ...restProps }){
return <Container {...restProps}>{children}</Container>
}

Feature.Title = function FeatureTitle({ children, ...restProps }){
return <Title {...restProps}>{children}</Title>
}

Feature.SubTitle = function FeatureSubTitle({ children, ...restProps }){
return <SubTitle {...restProps}>{children}</SubTitle>
}

export default Feature
Home.js >folded
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
//filename : Home.js

import React from 'react'
import { Feature, OptForm } from "../components"
import { FaqsContainer } from "../containers/faqs"
import { JumbotronContainer } from "../containers/jumbotron"
import { FooterContainer } from "../containers/footer"
import { HeaderContainer } from "../containers/header"

export default function Home() {
return (
<>
<HeaderContainer>
<Feature>
<Feature.Title>Unlimited films, TV programmes and more.</Feature.Title>
<Feature.SubTitle>Watch anywhere. Cancel at any time.</Feature.SubTitle>
<OptForm>
<OptForm.Input placeholder="Email Address" />
<OptForm.Button>Try it now</OptForm.Button>
<OptForm.Break />
<OptForm.Text>Ready to watch? Enter your email to create or restart
your membership.</OptForm.Text>
</OptForm>
</Feature>
</HeaderContainer>

<JumbotronContainer />
<FaqsContainer />
<FooterContainer />
</>
)
}
  • 第 4 行 : 把 feature component 傳進來
  • 第 14 行 : 把中間整塊視為 feature, 所以用 feature component 把整塊包起來
  • 第 15,16 行 : 傳進 title 和 subtitle
  • 第 17 行 : 把上一個 part 做好的 Opt Form 整個傳進來


導覽頁

Netflix Clone : 主頁
Netflix Clone : 首頁
Netflix Clone : 用戶登入頁
Netflix Clone : Browser 頁
Netflix Clone : 最後整理

Comments