在TypeScript中使用风格化的组件:带实例的教程

406 阅读15分钟

样式设计是构建网络和移动应用程序的一个重要部分。为了使一个应用程序看起来对你的用户有吸引力,它需要进行造型。CSS是我们最常使用的美化应用程序的方法。

但是,尽管我们很喜欢CSS,但也有一些恼人的问题需要处理,如类名重复、类名拼写错误和未知、不确定类名是否合适等等。在有大量代码库的项目中,使用普通的CSS可能会变得费时又费力。这就是风格化组件的用武之地。

在本教程中,我们将向你展示如何使用styled-components构建和设计一个TypeScript应用程序。

为了用一个实际的例子来证明,我们将使用styled-components和TypeScript来创建一个类似于Amazon的电子商务页面。完成后的产品将看起来像这样。

Finished Ecommerce Page

你可以在我的GitHub资源库中获取这个演示所需的图片。在本教程结束时,你应该能够使用styled-components设计你自己的TypeScript项目。

什么是styled-components?

styled-components是一个CSS-in-JS库,它使你能够编写常规的CSS并将其附加到JavaScript组件上。有了styled-components,你可以使用你已经熟悉的CSS,而不必学习新的样式结构。

styled-components让你可以对你的工作进行样式设计,而不必担心类名是否冲突,或者它们是否适合于特定的使用情况。它还有助于调试,因为一旦你发现哪个组件有问题,你就知道在哪里可以找到它的样式。

设置一个TypeScript项目

我们将使用create-react-app ,为我们的项目生成一个模板。由于这是一个TypeScript项目,我们将在我们的命令中加入--template typescript

打开你的终端,输入以下内容,然后按回车。

 npx create-react-app --template typescript

创建你的项目后,通过键入以下内容进入项目目录。

cd amazon-clone

在你的终端键入code . ,在你的代码编辑器中打开该项目。另外,你也可以从文件管理器中打开它,方法是右键单击文件夹,选择用代码打开

我们需要安装一些依赖项,因为styled-components并没有随create-react-app一起出现。

运行下面的命令:

yarn add styled-components

当它完成安装后,运行下面的命令:

yarn add -D @types/styled-components 

这将为TypeScript安装styled-components类型,作为一个开发依赖。

我们还将使用Material Icons,因此让我们安装 [material-ui package](https://www.npmjs.com/package/@material-ui/core).输入以下内容并按回车键。

yarn add @material-ui/icons @material-ui/core

VS Code有一个styled-components扩展,它使我们看起来像在输入实际的CSS,尽管它是一个TypeScript文件。它被称为 [vs-code-styled-components](https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components)如果你还没有安装,我建议你在继续之前安装它。

你的文件夹现在应该是这样的:

Folder Structure

现在是时候清理我们的项目了。我们不需要由Create React App生成的所有文件,所以让我们删除多余的文件:

  • 在public文件夹中,删除logo文件
  • index.html 文件中,把标题改成你想要的样子,并删除从元描述到标题标签开始的所有信息
  • src 文件夹中,删除所有文件,除了App.tsxindex.tsx
  • index.tsx ,删除导入index.cssreportWebVitals 的行。同时删除所有与webVitals有关的行
  • 删除App.tsx 中的所有内容,除了我们导入React和导出应用的地方。

说完这些,让我们开始构建。

创建项目结构

首先,让我们计划一下我们页面的布局。

我们的页面由三个主要部分组成:导航栏、菜单栏和主体。我们将从顶部到底部开始。

App.tsx ,创建一个React功能组件。它应该看起来像这样:

 import React from 'react';

 const App:React.FC = ()=> (
   <> </>
 )

 export default App;

在你的src 文件夹中,创建一个style文件夹。你可以随心所欲地命名它;我的名字是styles

在style文件夹中,创建一个文件并命名为global.ts 。这个文件将包含我们的全局样式。全局样式是一个页面中所有元素都使用的样式。

global.ts ,我们导入createGlobalStyle ,并创建全局样式。你的global.ts 文件应该看起来像这样。

 import {createGlobalStyle} from "styled-components"

 export default createGlobalStyle`
    *{
        margin: 0;
        padding: 0;
        outline:0;
        box-sizing:border-box;
        font-family: 'Open Sans', sans-serif; 
    }
    #root{
        margin:0 auto;
    }
 `

这些样式确保页面上没有边距或填充,除非在明确说明的地方。

src 文件夹中,我们将为组件创建一个文件夹。在组件文件夹中,我们将再创建三个文件夹,为页面的每个部分创建一个。我们将为我们的资产再创建一个文件夹。所有的图片都应该存放在这个文件夹里。

在导航条文件夹中,创建两个文件:index.tsxstyles.tsx 。在index.tsx ,导入以下项目。

import React from 'react'
import {
    Container, 
    Logo, 
    Flag, 
    Text, 
    Wrapper, 
    Searchbox,
    Select,
    SearchIconWrapper
} from "./styles"
import { ShoppingCartOutlined, SearchOutlined, ArrowDropDown, RoomOutlined } from '@material-ui/icons';
import logo from "../assets/logo.png"
import flag from "../assets/flag.png"

导入React是很重要的,因为我们需要它来创建我们的函数。

在第二行,我们从样式文件中导入了一些组件(我们很快会看到它们的作用)。然后,我们从Material Icons导入我们的图标。最后,我们从assets文件夹中导入了我们的图片。

在index文件夹中,让我们创建一个功能组件,并把我们的容器组件放在其中。

import React from 'react';

const App:React.FC = ()=> (
  <>
    <Container></Container>
  </>
)

export default App;

如果你不习惯使用风格化的组件,这可能看起来很奇怪。

容器组件代表了我们将使用的其他组件的整体包装。在不使用styled-components时,它的使用方式与div 。当我们开始进行样式设计时,你会了解到容器组件实际上是一个div

让我们看看样式文件是什么样子的。

import styled from "styled-components"

export const Container = styled.div`
    display: flex;
    justify-content:space-evenly;
    align-items: center;
    color: white;
    background-color: #131A22;
`

在顶部,我们从styled-components 中导入了styledstyled 让我们可以选择使用任何HTML元素,如divsectionspaninputs 等。我们可以访问它们全部。

对于我们的容器组件,我们使用了一个div 。在这个元素的名字后面,我们添加了template literals ,作为组件样式的包装。然后我们在模板字面上写下我们的CSS样式。

注意到这看起来像是在写普通的CSS吗?这是因为我前面提到的扩展。它对颜色和自动完成有帮助。

这些组件是可重复使用的,它们使每个组件与它的样式相关联变得更容易。这是因为每个组件在其主体中已经有了它的样式,所以当你需要调试时,你马上就知道该在哪里检查。

帮助性组件

让我们再创建一个文件夹:一个帮助者文件夹。在helper文件夹中,我们将创建两个文件:PageText.tsxItemWrapper.tsx

这个页面上有很多文字,有不同的大小和颜色。为了避免重复,我们需要创建TypeScript组件,并将一些CSS属性作为props传递。这将使我们能够重复使用这些组件,并根据我们的需要修改它们,而不需要在我们的样式文件中写新的代码行。

PageText 组件将用于页面上的所有文本。ItemWrapper 组件将作为一个容器来包装页面上的各种元素。

PageText.tsx ,插入以下内容。

import React from 'react'

interface Props{
    className?: string;
    fontSize?: any;
    color?: string;
}
export const PageText:React.FC <Props> = ({className, children}) => {
    return (
       <span className={className}>{children}</span>
    )
}

注意到组件中传递的道具了吗?这些是使用该组件时将被接受的道具。如果你不包括className 道具,无论你试图在组件主体中声明什么样式,都不会生效。在使用它的时候,你可以删除className 道具,看看它抛出的错误。子项道具代表我们将在组件内编写或使用的任何项目。

ItemWrapper.tsx ,插入以下几行代码。

import React from 'react'

interface Props{
    className?: string;
    display?: string;
    flexDirection?: string;
    alignItems?: string;
    maxWidth?: string;
    margin?: string;
}
export const PageItemWrapper:React.FC <Props> = ({className, children}) => {
    return (
       <div className={className}>{children}</div>
    )
}

设计导航条的样式

现在我们已经完成了辅助工具,让我们来创建我们的导航条。示例页面的导航条以亚马逊的标志开始。我们已经在索引文件中导入了它,所以让我们为它创建一个组件,并对它进行样式设计。

import{ PageText as NavText} from "../helpers/PageText"
import{ PageItemWrapper as NavItemWrapper} from "../helpers/ItemWrapper"

export const Logo = styled.img`
    width: 6em;
    border:1px solid #131A22;
    padding: .2em .1em;
    cursor:pointer;

    &:hover{
        border:1px solid #ffffff;
        border-radius: .2em;
    }
`

我们为宽度、边框、填充和光标行为创建了样式。我们还为标志被悬停时添加了额外的样式。不像在普通的CSS中,我们会使用像a:hover ,这里我们使用& 符号来表示我们正在为元素被悬停时创建样式。

这个过程在为::before::after 和其他伪元素创建样式时也是如此。样式化组件最好的一点是,你可以在一个地方编写所有的样式。

现在我们已经完成了标志的设计,让我们继续讨论导航条的其他部分。

styles.tsx ,添加以下内容:

import{ PageText as NavText} from "../helpers/PageText"

export const Text = styled(NavText)`
    color:${(props)=>props.color ? props.color :"#ffffff" };
    font-size: ${(props)=>props.fontSize ? props.fontSize  : ".9em"
};    
`

这里,我们以另一种方式使用了styled-components。我们把PageText 作为NavText ,从帮助文件夹中导入。在元素名称的位置上,我们使用了NavText

注意,当使用一个现有的组件时,我们没有包括一个点。我们没有写styled.NavText ,而是写了styled(NavText) 。我们还为我们的组件创建了默认样式。

组件内的这几行代码意味着,如果在使用该组件时指定了一个colorfont-size 的道具,那么colorfont-size 就等于指定的道具,否则,文本组件就使用其中的默认值。

import{ PageItemWrapper as NavItemWrapper} from "../helpers/ItemWrapper"

export const Wrapper = styled(NavItemWrapper)`
    display: flex;
    flex-direction: ${(props)=>props.flexDirection ?  props.flexDirection  : "column"};
    align-items:  ${(props)=>props.alignItems ? props.alignItems  : "flex-start"};
    padding: .1em;
    cursor:pointer;
    border:1px solid #131A22;

    &:hover{
        border:1px solid #ffffff;
        border-radius: .2em;
    }
     @media(max-width:850px){
        display: none;
    }

`

现在让我们在我们的页面中使用它们。在index.tsx ,添加以下几行代码:

const Navbar:React.FC = () => {
    return (
        <>
            <Container>
                    <Logo src={logo}/>

                    <Wrapper flexDirection="row"
                    alignItems="center">
                        <RoomOutlined/>


                        <Wrapper >
                        <Text fontSize=".7em">Deliver in 
                        </Text>
                        <Text >Nigeria</Text>
                        </Wrapper>
                    </Wrapper>

                 {/* the search button */}
                    <Select>
                    <option value="All">All</option>
                    </Select>
                    <Searchbox/>
                    <SearchIconWrapper>
                        <SearchOutlined/>
                    </SearchIconWrapper>

                    {/* flag image */}
                    <Wrapper flexDirection="row"
                    alignItems="flex-start"
                    >
                        <Flag src={flag}/>
                        <ArrowDropDown/>
                    </Wrapper>

                    <Wrapper>
                        <Text fontSize=".7em">Hello, Sign   in</Text>
                        <Wrapper flexDirection="row" alignItems="center">
                            <Text>Account & Lists </Text>
                            <ArrowDropDown/>
                        </Wrapper>
                    </Wrapper>


                    <Wrapper>
                        <Text fontSize=".7em">Returns</Text>
                        <Text >& Orders</Text>
                    </Wrapper>

                    <Wrapper flexDirection="row"
                    alignItems="center">
                        <Wrapper alignItems="center">
                        <Text color="#ff9900">0</Text>
                        <ShoppingCartOutlined/>
                        </Wrapper>

                        <Text>Cart</Text>
                    </Wrapper>


              </Container>
        </>
    )
}

export default Navbar

这是很多代码,但不要被吓倒。让我们一起看一下。

我们使用了我们的WrapperNavText 组件,你可以看到如何根据需要修改它们。我们还使用了我们在本教程开始时导入的图标。这里唯一的新东西是Select,Searchbox,SearchIconWrapper, 和Flag 组件。让我们解释一下它们是什么,以及我们如何创建它们。

在页面的导航栏中,有一个搜索框和它两边的两个元素。让我们来创建它们。

styles.tsx

export const Searchbox = styled.input`
    background-color: #ffffff;
    padding: .78em;
    width: 47%;
    border: none;

    @media(max-width:850px){
       border-radius: .2em;
       margin: .3em 0;
    }

`
export const Select = styled.select`
    background-color:#ddd;
    margin-right: -1.2em;
    padding: .72em .5em;
    border-radius: .2em 0em 0em .2em;
    border: none;
    cursor: pointer;

@media(max-width:850px){
        display: none;
    }

` 
export const SearchIconWrapper = styled.span`
    background-color:#fabd60;
    color: #131A22;
    margin-left: -1em;
    border-radius: 0em .2em .2em 0em ;
    padding: .32em .5em;
    cursor: pointer;
    transition: all 250ms ease;

    &:hover{
        background-color:#ff9900;
    }

     @media(max-width:850px){
        display: none;
    }

` 
export const Flag = styled.img`
    width:2em;
`

我们为Searchbox 组件使用了一个input 标签,为Select 组件使用了一个select 标签,为Flag 组件使用了一个img 标签,为SearchIconWrapper 使用了一个span 。我们还为SearchIconWrapper 添加了一个悬停效果,并为新组件添加了媒体查询。

这就是导航条的全部内容。现在你的页面应该看起来像这样。

Navbar Example

现在,让我们来建立菜单栏。

构建菜单栏

在我们之前创建的Menubar 文件夹中,创建一个index.tsx 文件和一个styles.tsx 文件。

index.tsx ,创建一个功能组件,并在其中添加以下一行:

import React from 'react'
import {Container, Wrapper, Text, LeftText} from "./styles"
import {Menu} from '@material-ui/icons';

const Menubar:React.FC = () => {
    return (
        <></>
    )
}

export default Menubar

我们从styles.tsx 中导入了一些组件,从Material Icons中导入了菜单图标。就像在我们的Navbar 文件夹中一样,我们将从帮助文件夹中导入包装器和文本组件,并在我们的 styles.tsx 文件中使用它。

import styled from "styled-components"
import{ PageText as MenuText} from "../helpers/PageText"
import{ PageItemWrapper as MenuItemWrapper} from "../helpers/ItemWrapper"

 export const Container = styled.div`
   display: flex;
   justify-content:space-between;
   color: white;
   padding: .3em;
   background-color: #232f3e;
`

export const Text = styled(MenuText)`
    color:${(props)=>props.color ? props.color :"#ffffff" };
    font-size: ${(props)=>props.fontSize ? props.fontSize  : ".9em"};
    margin-right: 1em;
    border:1px solid #232f3e;
    padding: .5em .1em;
    cursor:pointer;

    &:hover{
        border:1px solid #ffffff;
        border-radius: .2em;
    }
    @media(max-width:850px){
        display: none;
    }

`
export const LeftText = styled(Text)`
    @media(max-width:850px){
        display: block;
    }
`

export const Wrapper = styled(MenuItemWrapper)`
    display: flex;
    flex-direction: row;
    align-items: center;
    margin-right: 1em;
`

我们对我们的Container,Wrapper, 和Text 组件进行了样式设计。我们还使Text 组件的颜色和字体大小变得可定制。让我们在我们的index.tsx 文件中使用它们。

const Menubar:React.FC = () => {
    return (
        <Container>


            <Wrapper>
                <Wrapper>
                        <Menu/>
                        <Text>All</Text>
                </Wrapper>

                <Wrapper>
                    <Text>Today's Deals</Text>
                    <Text>Customer Service</Text>
                    <Text>Gift Cards</Text>
                    <Text>Sell</Text>
                    <Text>Registry</Text>
                </Wrapper>
            </Wrapper>

            <Wrapper>
                <LeftText>Amazon's response to COVID-19</LeftText>
            </Wrapper>


        </Container>
    )
}

export default Menubar

现在让我们建立我们的页面主体。

铺设页面主体

在页面的左侧,有一个显示各种部门名称的下拉菜单。我们在页面上也有产品的详细信息。让我们在我们的PageBody 文件夹中创建一个新文件来存储这些信息。

PageInfo.ts

import battery from "../assets/cell-battery.jpg"
import headset from "../assets/headset.jpg"
import screen from "../assets/screen-protector.jpg"

export  const departmentList = [
    "Electronics",
    "Cell Phones & Accessories",
    "Industrial & Scientific",
   " Office Products",
   " Video Games",
   " Health, Household & Baby Care",
   " Automotive Parts & Accessories",
   " Clothing, Shoes & Jewelry",
    "Sports & Outdoors",
   " Tools & Home Improvement",
    "Home & Kitchen",
    "Musical Instruments",
    "Pet Supplies",
    "Appliances",
    "Arts, Crafts & Sewing",
   " Toys & Games",
   " CDs & Vinyl"
]

export const productDetails=[
    {
        name:"Amazon Basics 4 Pack CR2032 3 Volt Lithium Coin Cell Battery",
        by: "by Amazon Basics",
        starcount: "59,531",
        price: "",
        shipping: "",
        available: "Currently unavailable.",
        src: battery
    },
    {
        name: "Arae Screen Protector for iPhone 12 Pro Max, HD Tempered Glass Anti Scratch Work with Most Case, 6.7 inch, 3 Pack",
        by: "by Arae",
        starcount: "321",
        price: "$6.99",
        shipping: " + $17.27 shipping",
        available: "Arrives: Thursday, May 20",
        src: screen
    },
    {
        name: "SENZER SG500 Surround Sound Pro Gaming Headset with Noise Cancelling Microphone - Detachable Memory Foam Ear Pads - Portable Foldable",
        by: "by SENZER",
        starcount: "1,613",
        price: "$23.99 ",
        shipping: " + $23.39 shipping",
        available: "Arrives: Thursday, May 20",
        src: headset
    }
]

PageInfo 文件中,我们有departmentList ,它是一个部门名称的数组,和productDetails ,一个包含产品信息的对象数组。在顶部,我们从assets文件夹中导入了产品图片。

让我们在PageBody 文件夹中创建一些新文件。创建一个index.tsx 文件和一个styles.tsx 文件。在index.tsx ,添加以下内容。

import React from 'react'
import {
    Container, 
    LeftContainer, 
    RightContainer, 
    Wrapper, 
    Image, 
    Text, 
    Paragraph,
    ProductContainer,
    ImageContainer,
    SearchResultDiv,
    IconWrapper,
    BoldText
} from "./styles"
import {KeyboardArrowLeft, Star, StarHalf, KeyboardArrowDown} from '@material-ui/icons';
import {departmentList, productDetails} from "./PageInfo"

我们从样式文件中导入我们的组件,从PageInfo.ts 中导入我们的图标和页面的信息。

现在让我们来创建我们的组件并为它们设计样式。

在你的 styles.tsx 文件中,添加以下几行代码。

import styled from "styled-components"
import {PageText} from "./PageText"
import {PageItemWrapper} from "./PageItemWrapper"

export const Container = styled.div`
    display: flex;
    padding: 1em;
`

export const LeftContainer = styled.aside`
    height:80vh;
    width: 18vw;
    border-right: 2px solid #ddd;

    @media(max-width:650px){
        display: none
   }

`

export const RightContainer = styled.section`
    height:80vh;
    width: 82vw;
    display: flex;
    flex-direction: column;
    margin-left: 1.5em;

`
export const Image = styled.img`
    width: 13em;
`
export const Text = styled(PageText)`
    color:${(props)=>props.color ? props.color :"#131A22" };
    font-size:${(props)=>props.fontSize ? props.fontSize :".9em" };
`
export const BoldText = styled(Text)`
    font-weight: bold;
    padding: .4em;
`

export const Paragraph = styled.p`
    font-size:.9em;
    display: flex;
    align-items: center;
    padding-bottom: .1em;
`
export const SearchResultDiv = styled.div`
    border: 1px solid #ddd;
    padding: .6em;
    width: 95%;
    border-radius: 4px;
`
export const ProductContainer = styled.div`
    display: grid;
    grid-template-columns: repeat(auto-fit, 22em);
    margin-top: 2em;

    @media(max-width:915px){
        grid-template-columns: repeat(auto-fit, 15em);
        align-items: center;
        justify-content: center;
    }

`
export const ImageContainer = styled.div`
    height: 14em;
    display: flex;
    align-items: center;
`

export const Wrapper = styled(PageItemWrapper)`
    display: flex;
    margin-right: 1em;
    flex-direction: ${(props)=>props.flexDirection ? props.flexDirection  : "row"};
    align-items:  ${(props)=>props.alignItems ? props.alignItems  : "left"};
    margin:  ${(props)=>props.margin ? props.margin : ""};
`

export const IconWrapper = styled.div`
    color: #ff9900;
`

在上面的代码中,我们为我们的页面主体创建了一个容器,然后将页面分为两个部分。左边的部分包含部门列表,右边的部分包含产品及其信息。

然后我们为我们的产品图片创建了一个组件,也使用了我们导入的辅助组件。

如果你看一下BoldText 组件,你会发现它是PageText 组件的扩展,里面有一些新的样式。我们用它来创建比页面上的文字更粗的文本。通过样式化组件,你可以扩展另一个组件。这就像复制带有所有样式的组件,然后给它添加额外的样式。

SearchResultDiv 组件是产品图片上方的长条,包含搜索结果的细节。

我们还创建了一个ProductContainer 组件,包含我们的产品和它们的信息,然后是产品图片的图像容器。

我们的样式文件中的最后一个组件是我们的明星图标的包装器。

如果你愿意的话,你可以花点时间研究一下这些样式,了解每一行的作用。

现在,让我们在我们的索引文件中使用这些组件。

const Menubar:React.FC = () => {
    return (
        <Container>
            <LeftContainer>
                <BoldText>Department</BoldText >
                {departmentList.map(item =>(
                    <Paragraph> <KeyboardArrowLeft/>{item}</Paragraph>
                ))}
            </LeftContainer>
            <RightContainer>
                <SearchResultDiv> 
                    <BoldText>1-12 of over 2,000 results for
                    </BoldText>
                    <BoldText color="#c45500"> All Departments
                    </BoldText> 
                </SearchResultDiv>

                <ProductContainer>
                {productDetails.map(item =>(
                    <Wrapper 
                    flexDirection="column"
                    >
                        <ImageContainer>
                            <Image src={item.src}/>
                        </ImageContainer>
                        <Text>{item.name}</Text>
                        <Text fontSize=".8em" color="grey" >{item.by}</Text>

                        {/* stars */}
                        <Wrapper 
                        margin=".3em 0 0 0"
                        alignItems="center"
                        >
                            <IconWrapper>
                                <Star/>
                                <Star/>
                                <Star/>
                                <Star/>
                                <StarHalf/>
                            </IconWrapper>

                            <KeyboardArrowDown/>
                            <Text color="blue">{item.starcount}</Text>
                        </Wrapper>

                        <Wrapper 
                        alignItems="center"
                        margin=".3em 0 .3em 0"
                        >
                            <Text fontSize="1.3em">{item.price }</Text>
                            <Text color="grey">{ item.shipping}</Text>
                        </Wrapper>
                        <Text>{item.available}</Text>

                    </Wrapper>
                ))}
                </ProductContainer>
            </RightContainer>
        </Container>
    )
}

export default Menubar

上面,我们使用了我们创建的组件来建立我们的页面主体,并定制了它们以适应它们应该出现的地方。

我们利用了我们的产品细节,这些细节是从pageInfo 文件中导入的。在LeftContainer 组件中,我们通过departmentList 数组进行了映射。而在ProductContainer 组件中,我们通过productDetails 数组进行映射。通过数组映射而不是一个接一个地写细节,使我们能够写出更干净、更少重复的代码。

这就是页面主体的全部内容。我们的页面现在看起来应该是这样的:

Final Ecommerce Page

结语

恭喜你!你现在已经掌握了所有的基础知识。你现在已经掌握了所有的基础知识和技能,可以用styled-components为你自己的TypeScript项目设计样式。你可以在这里查看完成的示例页面。