在这个Demo里,我们会用到这些:
- React
- React-Router
- Jest
git仓库: github.com/Alexlangl/r…
cra新建项目
首先,我们暂时不必直接使用webpack来自己搭建一个项目,因为这太过费时,我们的目的是先能写
yarn create react-app react-demo --template typescript
你可以直接复制我上面的代码片段,去创建一个名为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,那么应该是可以正常运行,并自动打开以下页面:
如果,很不巧,它报了如下这种错误:
我相信你聪明的你一定可以猜到该如何解决这个问题了~
对的,没错,就是去/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
然后,我们开始建立配置文件
.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
然后我们需要在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 我们将看到如下效果:
2021.06.29,我们下周见
2021.7.5又是一个周一,因为我上周过于怠惰,所以这周就先写一个简单的button组件吧
需求
和之前一样,先提一个简单的需求,这个button组件,我们需要它做到以下几点:
- 我们需要用到less
- 我们需要用到ts,目的是可以检测类型
- 我们的button需要能够传入一些参数,这些参数可以在一定程度上改变组件的样式
- 我们传入的参数,可以做到互斥,希望可以使用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
我们先来确定一下我们需要传入哪些属性:
| 属性名 | 类型 | 是否必传 | 作用 |
|---|---|---|---|
| children | string | true | 按钮的文字 |
| type | "primary" | "warning" | "error" | false | 按钮类型,默认为primary |
| ghost | boolean | false | 是否开启ghost按钮 |
| text | boolean | false | 是否开启文字按钮,与ghost互斥 |
| href | string | false | 点击跳转链接 |
| onClick | Function | false | 点击按钮时执行的回调方法,与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
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,没问题
加个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就算是完成了~
如果有时间的话,大概会在仓库里再加些有的没的(估计是不可能了)