React Hook + TypeScript + styled-component 建站
Posted 刻刻帝丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Hook + TypeScript + styled-component 建站相关的知识,希望对你有一定的参考价值。
技术选型
Vue 与 React 的对比
- 组件化
Vue 的组件化是将 UI 结构(template)、UI 样式(style)、数据与业务逻辑(script)都放在一个 .vue 的文件中,运行前 .vue 文件会被编译成真正的组件;
React 的组件化是直接通过 JS 代码的形式实现组件 - 模板引擎
Vue的视图模板使用类 html 的写法加上属性与指令,多数情况下要比 React 的 JSX 写法清晰且开发效率高,但是在复杂场景下,Vue 的写法有时会比 React 写起来更麻烦 - 数据监听
Vue 使用代理/拦截的方式使得我们直接修改 data 就可以,但 React 需要使用 setState API 改变数据
项目构建
目录结构
├─ mock #数据模拟
├─ public #静态
├─ scripts #脚本
└─ src
├─ common #工具库
├─ components #组件
├─ hooks #钩子
├─ pages #页面
├─ styles #样式
└─ router #路由
技术栈
- 开发框架:
React
- 构建工具:
Webpack
- 类型检查:
TypeScript
- 日志埋点:
@baidu/bend-sdk
- 视图样式:
styled-components
- 状态管理:
React-hook/useReducer
- 数据请求:
Umi-hook/useRequest + axios
- 规范检测:
Eslint + prettier + husky + lint-staged + commitlint
代码规范化提交
husky
注册 git 的钩子函数保证在 git 执行 commit 时调用代码扫描的动作lint-staged
保证只对当前 add 到 stage 区的文件进行扫描prettier
自动格式化代码eslint
按照配置扫描代码@commitlint/cli
规范 commit 提交@commitlint/config-conventional
commtlint 通用配置
工作流
- 待提交的代码
git add
添加到暂存区 - 执行
git commit
husky
注册在git pre-commit
的钩子函数被调用,执行lint-staged
lint-staged
取得所有被提交的文件依次执行写好的任务(ESLint 和 Prettier)- 如果有错误(没通过ESlint检查)则停止任务,同时打印错误信息,等待修复后再执行
git commit
- 成功 commit,可 push 到远程
package.json配置如下:
...
"husky":
"hooks":
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
,
"lint-staged":
"src/**/*.jsx,js,tsx,ts": [
"prettier --write",
"eslint --fix",
"git add"
]
.prettierrc.js
module.exports =
"printWidth": 100, // 一行的字符数,如果超过会进行换行,默认为80
"tabWidth": 4,
"useTabs": false, // 注意:makefile文件必须使用tab
"singleQuote": true,
"semi": true,
"trailingComma": "es5", //是否使用尾逗号,有三个可选值"<none|es5|all>"
"bracketSpacing": true, //对象大括号之间是否有空格,默认为true,效果: foo: bar
"endOfLine": "auto",
"arrowParens": "avoid"
;
.eslintrc.js
module.exports =
"root": true,
"env":
"browser": true,
"node": true,
"es6": true,
"jest": true,
"jsx-control-statements/jsx-control-statements": true
,
"parser": "@typescript-eslint/parser",
"parserOptions":
"sourceType": 'module',
"ecmaFeatures":
"jsx": true,
"experimentalObjectRestSpread": true
,
"globals":
// "wx": "readonly",
,
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:jsx-control-statements/recommended", // 需要另外配合babel插件使用
"prettier"
],
"overrides": [
"files": ["**/*.tsx"],
"rules":
"react/prop-types": "off"
],
"settings":
"react":
"version": "detect"
,
"plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-control-statements", "prettier"],
"rules":
"prettier/prettier": 1,
"no-extra-semi": 2, // 禁止不必要的分号
"quotes": ['error', 'single'], // 强制使用单引号
"no-unused-vars": 0, // 不允许未定义的变量
"jsx-control-statements/jsx-use-if-tag": 0,
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
;
.eslintignore/.prettierignore
**/*.js
!src/**/*.js
.commitlintrc.js
module.exports =
extends: ['@commitlint/config-conventional']
;
提交需遵循 conventional commit 格式,即:
type(scope?): subject
e.g. feat: 教培PC框架搭建(cvi-3000)
type 可以是:
feat
:新功能(feature)upd
:更新某功能fix
:修补bugdocs
:文档(documentation)style
: 格式(不影响代码运行的变动)refactor
:重构(即不是新增功能,也不是修改bug的代码变动)test
:增加测试chore
:构建过程或辅助工具的变动
样式方案
styled-components
是一个 CSS in JS 的类库,就是可以在 JS 中写 CSS 语法
使用 Sass/Less 等预处理语言需要在 Webpack 中进行各种 loader 的配置
而 styled-components 只需直接引用
import styled from 'styled-components';
- 样式化组件,主要作用是编写实际 CSS 代码来设计组件样式,无需组件和样式之间的映射,创建后实际就是一个 React 组件
- 使用后不再需要使用 className 来控制样式,而是写成更具语义化的组件
- 编译后的节点会随机生成 class,可以避免全局污染,但会增加维护难度
解决方案:在 babel 配置中加入 styled-components 插件
babel-plugin-styled-components
use: [
loader: 'babel-loader',
options:
...
plugins: [
'babel-plugin-styled-components',
...
]
,
...
]
编译后:
基本使用
// src/components/styles/index.ts
...
interface ICardProps
type?: string;
...
const typeMap = (type: string | undefined) =>
switch (type)
case 'b':
return 'block';
case 'i':
return 'inline-block';
case 'f':
return 'flex';
case 'n':
return 'none';
default:
return 'block';
;
/**
* 卡片容器
*/
const Card = styled.div`
display: $(props: ICardProps) => typeMap(props.type);
padding: 24px 24px 15px;
background-color: #fff;
`;
Card.defaultProps =
type: 'f',
;
...
// src/pages/shop/index.tsx
import styled from 'styled-components';
...
const Shop = () =>
...
return (
<Card type="b">
...
</Card>
);
全局样式
// src/style.ts
import createGlobalStyle from 'styled-components';
export const GlobalStyle = createGlobalStyle`
html,body,div,span,applet,object,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video,button
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
font-weight: normal;
vertical-align: baseline;
/* HTML5 display-role reset for older browsers */
article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section
display: block;
ol,ul,li
list-style:none;
...
`;
...
import GlobalStyle from '@/style';
...
const App: React.FC = () =>
return (
<Router>
<GlobalStyle />
...
</Router>
);
;
export default App;
代码片段
// src/components/styles/snippet.ts
import css from 'styled-components';
const mask = css`
&::after
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-image: radial-gradient(50% 129%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.1) 100%);
`;
...
export default
mask,
...
;
// src/components/styles/index.ts
import styled from 'styled-components';
import s from './snippet';
...
const Img = styled.div`
...
$(props: ICardImgProps) => (props.mask ? s.mask : '')
...
`
类型系统
TypeScript
微软开发的开源编程语言
是 JS 的一个超集,主要提供类型系统以及对尚未正式发布的 ECMAScript 新特性的支持,最终会被编译成纯 javascript 代码
优点:
- 编译阶段就能发现大部分错误
- 增加代码的可读性和可维护性,大部分函数看看类型定义就知道如何使用
- VSCode 对 TS 提供代码补全、接口提示、跳转定义等功能
缺点:
- 开发时要写很多类型定义,短期来看会增加开发成本,但对长期维护项目来看可以降低维护成本
- 集成到构建流程有一定工作量
- 与部分第三方库的结合使用可能不是很完美(比如styled-components.)
// tsconfig.json
"compilerOptions":
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"downlevelIteration": true,
"baseUrl": ".",
"paths":
"@/*": ["src/*"]
,
"plugins": [
"transform": "typescript-plugin-styled-components",
"type": "config"
]
,
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
React Hook
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其它的 React 特性。
- Hook 强化了 React 函数组件的能力,使得函数组件可以做到类组件中的 state 和生命周期。Hook 让类组件能实现的在函数组件中也都可以实现
- 语法更加简洁,解决了高阶组件使用困难以及难以理解的问题
- 向后兼容,类组件不会被舍弃
类组件与函数组件
类组件
import React, Component from 'react';
export default class Button extends Component
constructor()
super();
this.state = buttonText: 'Click' ;
this.handleClick = this.handleClick.bind(this);
handleClick()
this.setState(() =>
return buttonText: 'Click Done' ;
);
render()
const buttonText = this.state;
return <button onClick=this.handleClick>buttonText</button>;
函数组件
import React, useState from 'react';
export default function Button()
const [buttonText, setButtonText] = useState('Click');
function handleClick()
return setButtonText('Click Done');
return <button onClick=handleClick>buttonText</button>;
类组件的缺点:
- 需手动绑定this指向
- 大型组件难以拆分和重构
- 业务逻辑分散在各生命周期函数中,会导致逻辑重复(函数组件 useEffect 解决)
- 引入了复杂编程模式,例如渲染属性(Render Props)、高阶组件(HOC)(函数组件自定义 Hook 解决)
Render Props
使用一个值为函数的prop来传递需要动态渲染的组件
import UIDemo from 'components/demo';
class DataProvider extends React.Component
constructor(props)
super(props);
this.state = target: 'Payen';
render()
return (
<div>
this.props.render(this.state)
</div>
)
<DataProvider render=data => (
<UIDemo target=data.target />
)/>
<DataProvider>
data => (
<UIDemo target=data.target/>
)
</DataProvider>
HOC
函数接收一个组件作为参数,经过一系列加工,返回一个新组件
const withUser = WrappedComponent =>
const user = sessionStorage.getItem('user');
return props => <WrappedComponent user=user ...props/>;
const UserPage = props => (
<div>
<p>name: props.user</p>
</div>
);
内置 Hook API
Hook API 名字以 use 开头
基础Hook:
- useState
- useEffect
- useContext
附加Hook:
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
基础 Hook
useState
// 传入初始state值
const [state, setState] = useState(initState);
// 传入函数,函数返回值作为初始state,函数只会在初始渲染时被调用
const [state, setState] = useState(() =>
const initState = Func();
return initState;
);
useState
唯一参数是初始状态值,初始状态参数仅在第一次渲染时使用
返回当前的状态值(state)和用来变更状态的函数(setState)
类似于类组件中的this.setState
解构赋值语法允许我们将声明的状态赋予不同的名称
useEffect
useEffect(() =>
Func(state);
return () =>
// 组件卸载时以及后续渲染重新运行效果之前执行
;
, [state]);
useEffect
接收一个包含命令式且可能有副作用代码的函数
React渲染阶段在函数组件主体内改变 DOM、设置定时器等包含副作用的操作是不允许的,因为会影响其它组件
useEffect
和类组件中的componentDidMount
、componentDidUpdate
、 componentWillUnmount
有相同用途
import useState, useEffect from 'react';
function Example()
const [count, setCount] = useState(0);
useEffect(() =>
document.title = `You clicked $count times`;
);
return (
<div>
<p>You clicked count times</p>
<button onClick=() => setCount(count + 1)>
Click me
</button>
</div>
);
使用规则
- 只能在顶层调用 Hooks。不能在循环,条件和嵌套函数中使用 Hook API
- 仅在 React 函数组件中和自定义 Hooks 函数中使用 Hooks API
React 会根据调用 hook 的顺序依次将值存入数组
如果存在条件判断等可能会导致更新时不能获取对应的值,导致取值混乱
自定义Hook
为了与普通函数区分,自定义 Hook 命名以 use 开头
用来解决逻辑复用问题
// src/hooks/index.tsx
import React, useState, useEffect, ... from 'react';
...
// 元素可拖拽hook
function useDraggable(ref: React.RefObject<HTMLElement>)
const [ dx, dy , setOffset] = useState( dx: 0, dy: 0 );
useEffect(() =>
if (ref.current == null)
throw new Error('[useDraggable] ref未注册到组件中');
const el = ref.current;
const handleMouseDown = (event: MouseEvent) =>
const startX = event.pageX - dx;
const startY = event.pageY - dy;
const handleMouseMove = (event: MouseEvent) =>
const newDx = event.pageX - startX;
const newDy = event.pageY - startY;
setOffset( dx: newDx, dy: newDy );
;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener(
'mouseup',
() =>
document.removeEventListener('mousemove', handleMouseMove);
,
once: true
);
;
el.addEventListener('mousedown', handleMouseDown);
return () =>
el.removeEventListener('mousedown', handleMouseDown);
;
, [dx, dy, ref]);
useEffect(() =>
if (ref.current)
ref.current.style.transform = `translate3d($dxpx, $dypx, 0)`;
, [dx, dy, ref]);
// src/components/Usual/ButtonGroup.tsx
import useDraggable, ... from '@/hooks';
...
const ButtonGroup = (props: IButtonGroupProps) =>
...
// 预约弹窗可拖拽
const el = useRef<HTMLDivElement>(null);
useDraggable(el);
return (
<Space gap="b" nowrap>
...
<PopupWrapper ref=el>
<AppointPopup
show=showAppointBox
switchAppointBox=switchAppointBox
formid=formid
/>
</PopupWrapper>
</Space>
)
;
Hook 对比 HOC
import React from 'react';
function hocMatch(Component)
return class Match React.Component
componentDidMount()
this.getMatchInfo(this.props.matchId)
componentDidUpdate(prevProps)
if (prevProps.matchId !== this.props.matchId)
this.getMatchInfo(this.props.matchId)
getMatchInfo = (matchId) =>
// 请求后台接口获取赛事信息
render ()
return (
<Component ...this.props />
)
const MatchDiv=hocMatch(DivUIComponent)
const MatchSpan=hocMatch(SpanUIComponent)
<MatchDiv matchId=1 matchInfo=matchInfo />
<MatchSpan matchId=1 matchInfo=matchInfo />
function useMatch(matchId)
const [ matchInfo, setMatchInfo ] = useState('');
useEffect(() =>
// 请求后台接口获取赛事信息
// ...
setMatchInfo(serverResult) // serverResult后端返回数据
, [matchId]);
return [matchInfo];
...
export default function Match(matchId)
const [matchInfo] = useMatch(matchId);
return <div>matchInfo</div>;
其它问题
地图跳转
const jumpMap = (e: React.MouseEvent<HTMLParagraphElement, MouseEvent>) =>
// coord_type坐标类型选择国测局坐标(火星坐标系)
window.open(
`http://api.map.baidu.com/marker?location=$pos?.lat,$pos?.lng&title=$name ||
'我的位置'&content=$children&output=html&coord_type=gcj02`
);
e.stopPropagation();
;
二维码
import QRCode from 'qrcode.react';
...
<QRCode
value=qrCodeUrl
size=300
fgColor="#000"
imageSettings=
src: logo,
height: 60,
width: 60,
excavate: false,
/>
屏幕适配
CSS 适配
直接使用 CSS 媒体查询对窄屏下单独设置样式
const PageContent = styled.div`
width: 1200px;
padding-top: 16px;
margin: 0 auto;
@media (max-width: 900px)
width: 740px;
`;
JS 适配
实现一个自定义 Hook
// src/hooks/index.ts
...
function useNarrowScreen()
const isNarrow = () => window.innerWidth <= 900;
const [narrow, setNarrow] = useState(isNarrow);
useEffect(() =>
const resizeHandler = () => setNarrow(isNarrow());
window.addEventListener('resize', resizeHandler);
return () => window.removeEventListener('resize', resizeHandler);
);
return narrow;
// src/components/Base/PhotoAlbum.tsx
...
import useNarrowScreen from '@/hooks';
...
const PhotoAlbum = (props: IPhotoAlbum) =>
...
const [baseLen, setBaseLen] = useState(0);
const isNarrow = useNarrowScreen();
useEffect(() =>
setBaseLen(isNarrow ? 3 : 5);
...
, [isNarrow, ...]);
;
日志相关
click日志
import React, useEffect from 'react';
import sendLog from '@/common/log';
...
useEffect(() =>
const listenedEles = document.querySelectorAll('[data-mod]') || [];
listenedEles.forEach((ele: HTMLElement) =>
ele.onclick = function()
const mod = ele.getAttribute('data-mod');
...
;
);
, []);
// src/hooks/useLog.ts
function useClickLog(ref: React.RefObject<HTMLElement>)
useEffect(() =>
const el: any = ref.current;
el.onclick = function()
sendLog(el);
;
, [ref]);
const jumpMapRef = useRef(null);
useClickLog(jumpMapRef);
...
<MapHref
ref=jumpMapRef
onClick=jumpMap
data-mod="map_click"
>
查看
</MapHref>
show日志
页面进入埋点
在路由组件渲染回调函数中添加
// src/router/RouterWithSubRoutes.tsx
import React from 'react';
import Route, Redirect, RouteComponentProps from 'react-router-dom';
import RouteInterface from '@/types/route';
import sendLog from '@/common/log';
import routers from '@/common/router-config';
export const RouteWithSubRoutes = (
route: RouteInterface,
i: number,
authed: boolean,
authPath: string
) =>
return (
<Route
key=i
path=route.path
exact=route.exact
render=(props: RouteComponentProps) =>
const match = props;
const location = props.location;
if (routers[location.pathname])
sendLog(
mod: 'detail_show',
s_type: 'show',
);
if (!route.auth || authed || route.path === authPath)
return <route.component ...props routes=route.routes />;
return <Redirect to= pathname: authPath, state: from: props.location />;
/>
);
;
页面离开埋点
利用react-router
提供的离开确认组件Prompt
// src/App.tsx
import React from 'react';
import BrowserRouter as Router from 'react-router-dom';
import routes from '@/router/router';
import RenderRoutes from '@/router/RenderRoutes';
import GlobalStyle from '@/style';
import Prompt from 'react-router';
import sendLog from '@/common/log';
import routers from '@/common/router-config';
...
const App: React.FC = () =>
...
return (
<Router>
<GlobalStyle />
RenderRoutes(routes, authed, authPath)
<Prompt
message=location =>
// 退出页面前的逻辑
window.scrollTo(0, 0);
const pathname, search = window.location;
if (routers[pathname])
// sendLog
return true;
/>
</Router>
);
;
export default App;
页面状态缓存
Vue 中有keep-alive
的组件功能,但是 React 官方没有提供支持
使用<Route>
时,路由对应的组件在前进和后退无法被缓存,数据和行为会丢失
例如:列表页滚动到底后,点击跳转到详情页,返回后会回到列表顶部,数据以及滚动位置重置
<Route>
中配置的组件在路径不匹配时就会被卸载,对应真实节点也会从 DOM 树中移除
三种解决方案:
- 手动实现类似 Vue 的 keep-alive 功能
- 迁移其它第三方可以实现状态缓存的 Route 库
- 页面状态存入 sessionStorage
通过实现自定义钩子 useStorage,可以将数据代理到其它数据源
LocalStorage / SessionStorage
// src/pages/search/index.tsx
...
import useStorage from '@/hooks';
...
const Search = () =>
const location = useLocation();
const params = new URLSearchParams(location.search);
const tabSearch = params.get('tab') || 'shop';
...
// const [currentTab, changeCurrentTabState] = useState(tabSearch);
const [currentTab, changeCurrentTabState] = useStorage('zlhx_home_tab', tabSearch);
...
// src/hooks/index.ts
import React, useState, useEffect, useCallback, Dispatch, SetStateAction from 'react';
...
function useStorage<T>(
key: string,
defaultValue?: T | (() => T), // 默认值
keepOnWindowClosed: boolean = false // 是否在窗口关闭后保持数据
): [T | undefined, Dispatch<SetStateAction<T>>, () => void]
const storage = keepOnWindowClosed ? localStorage : sessionStorage;
// 尝试从Storage恢复值
const getStorageValue = () =>
try
const storageValue = storage.getItem(key);
if (storageValue != null)
return JSON.parse(storageValue);
else if (defaultValue != null)
// 设置默认值
const value =
typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue;
storage.setItem(key, JSON.stringify(value));
return value;
catch (err)
console.warn(`useStorage 无法获取$key: `, err);
return undefined;
;
const [value, setValue] = useState<T | undefined>(getStorageValue);
// 更新组件状态并保存到Storage
const save = useCallback<Dispatch<SetStateAction<T>>>(
value =>
setValue(prev =>
const finalValue =
typeof value === 'function'
? (value as (prev: T | undefined) => T)(prev)
: value;
storage.setItem(key, JSON.stringify(finalValue));
return finalValue;
);
,
[storage, key]
);
// 移除状态
const clear = useCallback(() =>
storage.removeItem(key);
setValue(undefined);
, [storage, key]);
return [value, save, clear];
以上是关于React Hook + TypeScript + styled-component 建站的主要内容,如果未能解决你的问题,请参考以下文章
React Hook + TypeScript 手把手打造类型安全的应用。
React:react-router-dom Typescript 中不存在 useLocation Hook
如何测试使用自定义 TypeScript React Hook 的组件?
如何测试使用自定义TypeScript React Hook的组件?