从Demo开始进入React世界

1,062 阅读9分钟

在这个Demo里,我们会用到这些:

  • React
  • React-Router
  • Jest

git仓库: github.com/Alexlangl/r…

cra新建项目

首先,我们暂时不必直接使用webpack来自己搭建一个项目,因为这太过费时,我们的目的是先能写

yarn create react-app react-demo --template typescript

image.png

你可以直接复制我上面的代码片段,去创建一个名为react-demo的react+ts项目。

如果你不太喜欢用yarn,你也可以使用npm作为你的包管理器,代码如下:

npx create-react-app react-demo --template typescript

如果你的操作和我一致的话,你应该会得到以下目录结构

├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   └── setupTests.ts
├── tsconfig.json
└── yarn.lock

启动项目

运行以下命令

yarn start

如果你的电脑恰好没有安装高版本的webpack或者babel-loader,那么应该是可以正常运行,并自动打开以下页面:

image.png

如果,很不巧,它报了如下这种错误:

image.png

我相信你聪明的你一定可以猜到该如何解决这个问题了~

对的,没错,就是去/User/<yourName>/node_moudles下将提示的那一个依赖删除即可

需要注意的是,往往不会是只删除一个就会奏效,提示其它的依赖出现这种问题,也可以尝试使用这种方法去解决


做一些小改动

至此,我们的项目的初始化已经完成了

在接下来的这一步骤里,我们将完成一个小需求,具体如下:

写一个layout,layout中有两个按钮,点击这这两个按钮可以切换路由

接下来,我们需要做以下几件事:

  • 简单的配置一下环境
  • 添加路由
  • 写一个最简单的layout

环境配置

为了使我们的代码更加地规整,我们可以使用eslint+pritter来规范我们的代码

我们需要安装以下依赖

yarn add prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint -D

image.png

然后,我们开始建立配置文件

.eslintrc.js
module.exports = {
    parser: '@typescript-eslint/parser',
    extends: [
      'prettier',
    ],
    parserOptions: {
      ecmaVersion: 2020,
      sourceType: 'module',
      ecmaFeatures: {
        jsx: true,
        },
    },
    settings: {
      react: {
        version: 'detect',
        },
    },
};

.prettierrc
{
    "semi": false,
    "singleQuote": false,
    "arrowParens": "avoid",
    "trailingComma": "none"
}

另外,我本人还对vscode和editor做了一个配置

.vscode/settings.json
{
  "editor.formatOnSave": true,
  "files.autoSave": "onWindowChange",
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  }
}
.editorconfig
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf

[*.md]
trim_trailing_whitespace = false

pre-commit钩子

同时,我还期望当我们提交代码的时候,eslint可以自动执行并修复一些我们可能没有看到的问题

我们需要先安装以下依赖

yarn add husky lint-staged -D

image.png

然后我们需要在package.json里添加这样一段代码:

"lint-staged": {
    "*.+(ts|tsx|js|jsx)": [
      "eslint --fix",
      "git add"
    ],
    "*.+(json|css|md)": [
      "prettier --write",
      "git add"
    ]
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  }

至此,最初始的环境已经配的差不多了,下面就可以开始正经写代码啦~ 当然,随着后续代码的不断增加,我们肯定还是要做些更新的,不过这是以后的事了~

添加路由

在这里,我们会使用react-router5.x的版本

对应react-web端开发,我们需要安装 react-router-dom

同时,又因为我们使用的是typescript的缘故,所以,我们还需要安装 @types/react-router

执行以下操作:

yarn add react-router-dom
yarn add @types/react-router -D

在src目录下建三个文件夹 views route Layout

views

在views文件夹下

views/pageA/index.tsx

import React from "react"

export default function PageA() {
  return <div>this is pageA</div>
}

views/pageB/index.tsx

import React from "react"

export default function PageA() {
  return <div>this is pageB</div>
}

同时,在Layout下: Layout/Layout.tsx

import * as React from "react"
import { Link } from "react-router-dom"

const Layout = () => {
  return (
    <div>
      <div>this is a layout Component</div>
    </div>
  )
}

export default Layout

Layout/index.ts

export { default } from "./Layout"
route

我们建立一个router.config.ts的文件,用于存放我们的路由表 route/router.config.ts

import Layout from "../Layout"
import { lazy } from "react"
const RouteConfig = [
  {
    path: "/pages",
    component: Layout,
    children: [
      {
        path: "/pages/page-a",
        component: lazy(() => import("../views/pageA"))
      },
      {
        path: "/pages/page-b",
        component: lazy(() => import("../views/pageB"))
      },
      { path: "/pages", redirect: "/pages/paeg-a" }
    ]
  },
  {
    path: "/",
    redirect: "/pages/page-a"
  }
]

export default RouteConfig

然后,我们还需要建一个RouteView组件,用于渲染我们的路由

route/RouteView.tsx

import React from "react"
import { Redirect, Route, Switch } from "react-router-dom"

export interface IRouteViewProps {
  path?: string
  redirect?: string
  component?: any
  children?: IRouteViewProps[]
}

const RouteView: React.FC<IRouteViewProps> = props => {
  return (
    <Switch>
      {props.children &&
        props.children.map((item, index) => {
          if (item.redirect) {
            return (
              <Redirect
                key={index}
                from={item.path}
                to={item.redirect}
              ></Redirect>
            )
          }
          return (
            <Route
              key={index}
              path={item.path}
              render={(props: JSX.IntrinsicAttributes) => {
                return (
                  item.component && (
                    <item.component
                      children={item.children}
                      {...props}
                    ></item.component>
                  )
                )
              }}
            ></Route>
          )
        })}
    </Switch>
  )
}

export default RouteView

App && Layout

然后我们改造一下我们src下的App.tsx

需要注意的是,我这里使用的是BrowserRouter

src/App.tsx

import React, { Suspense } from "react"

import { BrowserRouter } from "react-router-dom"

import RouteConfig from "./route/router.config"
import RouteView from "./route/RouteView"

import "./App.css"

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Suspense fallback={<div>loading...</div>}>
          <RouteView children={RouteConfig}></RouteView>
        </Suspense>
      </BrowserRouter>
    </div>
  )
}

export default App

相应的App.css我也做了一点更改

src/App.css

.App {
  text-align: center;
}

.App a {
  margin-left: 12px;
}

最后,再让我们来改造一下我们的Layout

Layout/Layout.tsx

import * as React from "react"
import { Link } from "react-router-dom"
import { History } from "history"
import RouteView, { IRouteViewProps } from "../route/RouteView"

interface ILayoutProps extends IRouteViewProps {
  history: History
}

const Layout = (props: ILayoutProps) => {
  return (
    <div>
      <div>this is a layout Component</div>
      <Link to="/pages/page-a">page-A</Link>
      <Link to="/pages/page-b">page-B</Link>
      <RouteView {...props} />
    </div>
  )
}

export default Layout

当前目录结构如下:

.
├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── Layout
│   │   ├── Layout.tsx
│   │   └── index.ts
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   ├── route
│   │   ├── RouteView.tsx
│   │   └── router.config.ts
│   ├── setupTests.ts
│   └── views
│       ├── pageA
│       │   └── index.tsx
│       └── pageB
│           └── index.tsx
├── tsconfig.json
└── yarn.lock

运行代码

执行 yarn start 我们将看到如下效果:

QQ20210629-120221-HD.gif


2021.06.29,我们下周见


2021.7.5 又是一个周一,因为我上周过于怠惰,所以这周就先写一个简单的button组件吧

需求

和之前一样,先提一个简单的需求,这个button组件,我们需要它做到以下几点:

  1. 我们需要用到less
  2. 我们需要用到ts,目的是可以检测类型
  3. 我们的button需要能够传入一些参数,这些参数可以在一定程度上改变组件的样式
  4. 我们传入的参数,可以做到互斥,希望可以使用ts来做到这一点

配置一些环境相关的东西

安装craco & less

安装craco的主要目的是我们需要改写一些webpack的配置,但是我们又不想直接去eject把webpack直接暴露出来。

执行以下命令:

yarn add @craco/craco craco-less -D

安装完成之后,在根目录下新建craco.config.js

const CracoLessPlugin = require('craco-less');

module.exports = {
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          lessOptions: {
            javascriptEnabled: true,
          },
        },
      },
    }
  ]
};

写组件

在src下新建目录components/Button

新建文件index.tsx index.less

我们先来确定一下我们需要传入哪些属性:

属性名类型是否必传作用
childrenstringtrue按钮的文字
type"primary" | "warning" | "error"false按钮类型,默认为primary
ghostbooleanfalse是否开启ghost按钮
textbooleanfalse是否开启文字按钮,与ghost互斥
hrefstringfalse点击跳转链接
onClickFunctionfalse点击按钮时执行的回调方法,与href互斥

我们明确了,我们要有互斥的属性,先来写个ts的互斥吧

type WitOutType<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type MutuallyExclusive<T, U> = T | U extends object
  ? (WitOutType<T, U> & U) | (WitOutType<U, T> & T)
  : T | U

这样,我们定义我们的组件,在刚开始的时候可以这么写:


import React from "react"
import "./index.less"

type WitOutType<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type MutuallyExclusive<T, U> = T | U extends object
  ? (WitOutType<T, U> & U) | (WitOutType<U, T> & T)
  : T | U
interface ButtonProps {
  children: string
  type?: type?: "primary" | "warning" | "error"
}
const Button: React.FC<
  ButtonProps &
    MutuallyExclusive<{ ghost?: boolean }, { text?: boolean }> &
    MutuallyExclusive<
      { href?: string },
      { onClick?: React.MouseEventHandler<HTMLElement> }
    >
> = ({ children, type = "primary", ghost, text, href, onClick }) => {

  return <div>{children}</div>
}

export default Button


在pageA中使用调用组件

import React from "react"
import Button from "..//Button"

export default function PageA() {
  return (
    <div style={{ marginTop: "12px" }}>
      <Button>
        this is a button
      </Button>
    </div>
  )
}

然后我们来运行我们的代码,我们期望pageA页面中渲染出 this is a buton

image.png

ok,没问题,这样我们就搞定了第一个属性 children了

接下来,我们去搞定剩下的几个属性

首先,type可以确定组件类型,同一个类型的组件,它有可能是ghost类型,也有可能是text类型

我们可以用字符串拼接的方法来确定className,不同的类型组合会对应不同的className

我们定义两个变量 buttonClass typeDefaultClass

buttonClass 确定(ghost | text) + type + 按钮

typeDefault 确定各类型的基础样式

于是乎,有一下面这一段代码

let buttonClass, typeDefultClass
  ghost
    ? ((buttonClass = "ghost_" + type + "_button"),
      (typeDefultClass = "ghost_type_button"))
    : text
    ? ((buttonClass = "text_" + type + "_button"),
      (typeDefultClass = "text_type_button"))
    : ((buttonClass = type + "_button"),
      (typeDefultClass = "default_type_button"))

为了方便,我们可以通过用一个数组.join(' ')的方法来直接确定className

const classNames = ["button_default", buttonClass, typeDefultClass].join(" ")

button_default 是我们的按钮基础样式

我们似乎还有一个href的属性没解决,简单点,我们直接判断在href的时候,我们渲染一个a标签出来,给它加上样式,似乎也可以实现我们的需求

<a className={classNames + " href_button"} href={href} target="_blank">
  {children}
</a>

ok,接下来拼合一下代码

import React from "react"
import "./index.less"

type WitOutType<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type MutuallyExclusive<T, U> = T | U extends object
  ? (WitOutType<T, U> & U) | (WitOutType<U, T> & T)
  : T | U
interface ButtonProps {
  children: string
  type?: "primary" | "warning" | "error"
}
const Button: React.FC<
  ButtonProps &
    MutuallyExclusive<{ ghost?: boolean }, { text?: boolean }> &
    MutuallyExclusive<
      { href?: string },
      { onClick?: React.MouseEventHandler<HTMLElement> }
    >
> = ({ children, type = "primary", ghost, text, href, onClick }) => {
  let buttonClass, typeDefultClass
  ghost
    ? ((buttonClass = "ghost_" + type + "_button"),
      (typeDefultClass = "ghost_type_button"))
    : text
    ? ((buttonClass = "text_" + type + "_button"),
      (typeDefultClass = "text_type_button"))
    : ((buttonClass = type + "_button"),
      (typeDefultClass = "default_type_button"))
  const classNames = ["button_default", buttonClass, typeDefultClass].join(" ")
  if (href) {
    return (
      <a className={classNames + " href_button"} href={href} target="_blank">
        {children}
      </a>
    )
  }
  return (
    <div className={classNames} onClick={onClick}>
      {children}
    </div>
  )
}

export default Button

加样式

代码有了,我们似乎到现在还只是定义了我们的样式名,但是并没有写任何样式,接下来我们将在index.less里书写我们的样式

定义三种type类型 primary warning error

定义了三个less函数 #buttonCss #ghostButton #textButton

@primary_color: #288efc;
@warning_color: #e6a23c;
@error_color: #f12c5d;

.button_default {
  display: inline-block;
  padding: 6px 12px;
  border-radius: 6px;
  cursor: pointer;
  transition: background-color 0.2s linear;
}

.default_type_button {
  color: #fff;
}
.ghost_type_button {
  color: #333;
}
.href_button {
  text-decoration: none;
}

#buttonCss(@defaultBgc,@hoverBgc) {
  background-color: @defaultBgc;
  &:hover,
  &:focus {
    background-color: @hoverBgc;
  }
}

#ghostButton(@color) {
  border: 1px solid @color;
  &:hover,
  &:focus {
    border: 1px solid @color;
    background-color: rgba(@color, 0.1);
    color: @color;
  }
}

#textButton(@color) {
  color: @color;
  &:hover,
  &:focus {
    color: rgba(@color, 0.8);
  }
}
.primary_button {
  #buttonCss(@primary_color, #67b4eb);
}

.warning_button {
  #buttonCss(@warning_color, #f5dab1);
}

.error_button {
  #buttonCss(@error_color, #eb8686);
}

.ghost_primary_button {
  #ghostButton(@primary_color);
}

.ghost_warning_button {
  #ghostButton(@warning_color);
}

.ghost_error_button {
  #ghostButton(@error_color);
}

.text_primary_button {
  #textButton(@primary_color);
}

.text_warning_button {
  #textButton(@warning_color);
}

.text_error_button {
  #textButton(@error_color);
}

ok,这样我们的组件似乎就已经完工了,我们来测试一下,还是在我们的pageA中,我们期望有一个warning类型的ghost按钮,点击可以输出event

import React from "react"
import Button from "../../components/Button"

export default function PageA() {
  const handleClick = (event: React.MouseEvent<HTMLElement> | undefined) => {
    console.log(event)
  }
  return (
    <div style={{ marginTop: "12px" }}>
      <Button type="error" onClick={handleClick}>
        Primary
      </Button>
    </div>
  )
}

运行代码,点击按钮~

ok,没问题

image.png

加个alias

我们的组件的功能,大体上已经实现了,但是每次导入组件都需要通过../../ 这样的去引入

现在代码少,还无所谓,如果以后代码量上去了,会很麻烦

我们可以通过配置alias的方法去解决

因为我们使用的craco,我们可以通过 craco-alias 来配置

我们先执行以下命令

yarn add -D craco-alias

然后在根目录下新建文件 tsconfig.extend.json

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@components/*": [
        "./components/*"
      ]
    }
  }
}

然后修改tsconfig.json

{
  "extends": "./tsconfig.extend.json",
  ........
}

最后,我们修改craco.config.js

const CracoLessPlugin = require('craco-less');
const CracoAlias = require("craco-alias");


module.exports = {
  plugins: [
    .....
    {
      plugin: CracoAlias,
      options: {
        source: "tsconfig",
        baseUrl: "./src",
        tsConfigPath: "./tsconfig.extend.json"
      }
    }
  ],
};

然后更改pageA中Button的引入

....
import Button from "@components/Button"
....

运行代码,不出意外的话,应该是可以成功的~

此时,如果你的操作与我一致,那么你的当前目录结果应该是这样的:

.
├── README.md
├── craco.config.js
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── Layout
│   │   ├── Layout.tsx
│   │   └── index.ts
│   ├── components
│   │   └── Button
│   │       ├── index.less
│   │       └── index.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   ├── route
│   │   ├── RouteView.tsx
│   │   └── router.config.ts
│   ├── setupTests.ts
│   ├── utils
│   │   └── useAsyncEffect.ts
│   └── views
│       ├── pageA
│       │   └── index.tsx
│       └── pageB
│           └── index.tsx
├── tsconfig.extend.json
├── tsconfig.json
└── yarn.lock


至此,一个超级简单的小demo就算是完成了~

如果有时间的话,大概会在仓库里再加些有的没的(估计是不可能了)