项目启动
-
新建项目
npx create-react-app project-name --template typescript npm install typescript-plugin-css-modules --save-dev
若运行出错,可
npm config set registry https://registry.npmjs.org/
如需将 TypeScript 添加到现有的 Create React App 项目中:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
-
JSS 模块化
tsconfig.json
下增加配置// compilerOptions 下增加 "noImplicitAny": false, "plugins": [ { "name": "typescript-plugin-css-modules" } ]
新建文件
src/custom.d.ts
文件declare module '*.css' { const css: { [key: string]: string } export default css }
App.css
文件改成App.module.css
,并改变
App.tsx
中引入方式import styles from './App.module.css'
-
配置 eslint、 prettier 和 commitlint 规范工程
-
配置绝对路径
import logo from 'logo.svg' // src/logo.svg
tsconfig.json
下增加配置// compilerOptions 下增加 "baseUrl": "src",
-
配置格式化
npm install prettier --save-dev
新建文件
project-name/.prettierrc.json
,可自行配置新建文件
project-name/.prettierignore
build coverage
- 执行
npx mrm lint-staged
,lint-staged
是一个在git
暂存文件上运行linters
的工具。它将根据package.json
依赖项中的代码质量工具来安装和配置husky
和lint-staged
,因此请确保在此之前安装(npm install --save-dev
)并配置所有代码质量工具,如Prettier
和ESlint
。
"husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,css,md,ts,tsx,jsx}": "prettier --write" }
- 安装依赖
npm install eslint-config-prettier --save-dev
src/package.json
"eslintConfig": { "extends": [ "react-app", "react-app/jest", "prettier" ] },
-
配置 commitlint
安装:npm install --save-dev @commitlint/config-conventional @commitlint/cli // 生成配置文件commitlint.config.js,当然也可以是 .commitlintrc.js echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js
配置:在
husky
的配置加入CommitlIint
配置"husky": { "hooks": { "commit-msg": "commitlint -E $HUSKY_GIT_PARAMS" } },
-
引入 UI 框架 Ant Design
npm install antd @ant-design/icons --save
全局引用 Ant Design 样式文件
index.css
中@import '~antd/dist/antd.css'
按需引入引入 antd(具体用法见文档)
-
组件化思想(使用方便,易于独立)
src/components/index.ts
:组件导出export * from './header' export * from './footer' ......
src/components/footer
:封装 Footer 组件src/components/footer/Footer.tsx
:组件逻辑import React from 'react' import { Layout, Typography } from 'antd' export const Footer: React.FC = () => { return ( <Layout.Footer> <Typography.Title level={3} style={{ textAlign: 'center' }}> 版权所有 @ React 旅游网 </Typography.Title> </Layout.Footer> ) }
src/components/footer/index.tsx
:组件导出export * from './Footer'
src/App.tsx
:根组件import React from 'react'; import styles from './App.module.css'; import { Header, Footer } from './components' function App() { return ( <div className={styles.app}> <Header /> <Footer /> </div> ); } export default App;
引入路由系统
-
react-router-dom
用于浏览器,处理Web App
的路由- 安装
npm install react-router-dom --save-dev
- 会自动安装
react-router
核心框架 -
<Link />
组件可以渲染出<a/>
标签 -
<BrowserRouter/>
组件利用H5 API
实现路由切换 -
<HashRouter/>
组件则利用原生JS
中的window.location.hash
来实现路由切换
- 安装
react-router-native
用于React Native
,处理手机app
的路由react-router-redux
提供了路由中间件,处理redux
的集成react-router-config
用来静态配置路由
react-router-dom
-
配置路由
import { BrowserRouter, Route } from 'react-router-dom' // 若匹配到多个路由,则UI显示为多组件共存 // exact:精准匹配 <div className={styles.app}> <BrowserRouter> // HomePage 中,可通过 props 获取到路由信息 <Route exact path="/" component={HomePage} /> <Route path="/login" render={() => <h1>登录</h1>} /> </BrowserRouter> </div>
-
带参数路由匹配
// App.tsx import React from 'react' import { BrowserRouter, Route, Switch } from 'react-router-dom' import styles from './App.module.css' import { HomePage, LoginPage, RegisterPage, DetailPage } from './pages' function App() { return ( <div className={styles.app}> <BrowserRouter> <Switch> <Route exact path="/" component={HomePage} /> <Route path="/login" component={LoginPage} /> <Route path="/register" component={RegisterPage} /> <Route path="/detail/:id" component={DetailPage} /> </Switch> </BrowserRouter> </div> ) } export default App
// Detail.tsx import React from 'react' import { RouteComponentProps } from 'react-router-dom' interface MatchParams { id: string } export const DetailPage: React.FC<RouteComponentProps<MatchParams>> = (props) => { return ( <> <h1>详情{props.match.params.id}</h1> </> ) }
路由跳转
-
withRouter
import { RouteComponentProps, withRouter } from 'react-router-dom' interface PropsType extends RouteComponentProps { id: number ...... } const ProductImageComponent: React.FC<PropsType> = ({ id, history, location, match }) => { return ( <div onClick={() => history.push(`detail/${id}`)}> ...... </div> ) } export const ProductImage = withRouter(ProductImageComponent)
-
useHistory
import { useHistory } from 'react-router-dom' const history = useHistory() <Button onClick={() => history.push('/register')}>注册</Button> <Button onClick={() => history.push('/login')}>登录</Button>
-
Link
import { Link } from 'react-router-dom' <Link to={`detail/${id}`}>......</Link>
redux 状态管理
详见 react-redux
常见 MOCK 方案
代码入侵:直接在代码中写死
Mock
数据,或者请求本地的json
文件请求拦截,如
Mock.js
接口管理工具,如
rap
、swagger
、yapi
-
本地
node
服务,如json-server
,推荐~~- 安装:
sudo npm install json-server -g
- 配置:
project-name
下新建文件db.json(RESTful 规范接口,如增删改查资源)、middleware.js(非 RESTful 规范接口,如登录、注册等)
- 启动:
json-server json-server-mock/db.json --middlewares json-server-mock/middleware.js
- 使用:
postman
中使用restful
规范操作,json
联动 - 若在
package.json
中配置"scripts": { ...... "json-server": "PORT=3000 json-server json-server-mock/db.json --middlewares json-server-mock/middleware.js --watch" },
- 安装:
-
React
启动时自定义端口号
修改package.json
中start
一行// Linux or MacOS "start": "PORT=4003 react-scripts start" // or "start": "export PORT=3006 react-scripts start" // Windows "start": "set PORT=3006 && react-scripts start"
Ajax
npm install qs @types/qs --save
-
配置请求根路径,处理请求参数,必须为
REACT_APP_API_URL
,否则会报错
project-name/.envREACT_APP_API_URL = http://online.com
project-name/.env.development
REACT_APP_API_URL = http://localhost:3000
src/utils/index.js
// 是否虚值 export const isFalsy = (value: any): boolean => { return value === 0 ? true : !!value } // 在一个函数里,改变传入的对象本身是不好的 // 处理对象,去除空值 export const cleanObject = (object) => { const params = { ...object } Object.keys(params).forEach(key => { const value = params[key] if (!isFalsy(value)) { delete params[key] } }) return params }
发送请求
import qs from 'qs' const apiUrl = process.env.REACT_APP_API_URL useEffect(() => { fetch(`${apiUrl}/projects?${qs.stringify(cleanObject(param))}`).then(async response => { if (response.ok) { setList(await response.json()) } }) }, [param])
-
custom hook
-
useMount
export const useMount = (callback: () => void) => { useEffect(() => { callback() }, []) } useMount(() => { fetch(`${apiUrl}/users`).then(async response => { if (response.ok) { setUsers(await response.json()) } }) })
-
useDebounce
export const useDebounce = <T>(value: T, delay = 300) => { const [ debounceValue, setDebounceValue ] = useState(value) useEffect(() => { // 每次在value 或 delay 变化后,设置一个定时器 const timeout = setTimeout(() => { setDebounceValue(value) }, delay) // 每次在上一个 useEffect 处理完以后再执行,最后一个 timeout 得以执行 return () => { clearTimeout(timeout) } }, [value, delay]) // 返回最后一次执行后的 value return debounceValue } const debounceParam = useDebounce(param, 300) useEffect(() => { fetch(`${apiUrl}/projects?${qs.stringify(cleanObject(debounceParam))}`).then(async response => { if (response.ok) { setList(await response.json()) } }) }, [debounceParam])
-
useArray
import React from 'react' export const useArray = <T>(list: T[]) => { const [ value, setValue ] = useState(list) return { value, clear: () => setValue([]), add: (item: T) => setValue([ ...value, item ]), removeIndex: (index: number) => { const array = [ ...value ] array.splice(index, 1) setValue(array) }, } } interface Person { name: string age: number } export const TsReactTest = () => { const persons: Person[] = [ { name: 'Jack', age: 25 }, { name: 'Marry', age: 22 }, ] const { value, clear, removeIndex, add } = useArray(persons) return ( <> <div> <button onClick={ () => add({ name: 'John', age: 20 }) }>add John</button> <button onClick={ () => removeIndex(0) }>remove 0</button> <button onClick={ () => clear() }>clear Array</button> </div> <div> { value.map((item: Person, index: number) => { return ( <div key={index}> <span>{index}</span> <span>{item.name}</span> <span>{item.age}</span> </div> ) }) } </div> </> ) }
-
-
完成 login
json-server-mock/middleware.jsmodule.exports = (req, res, next) => { const { method, path, body } = req // 登录 if (method === 'POST' && path === '/login') { if (body.username === 'jack' && body.password === '123') { return res.status(200).json({ user: { token: '123' } }) } else { return res.status(400).json({ message: '用户名或密码错误' }) } next() } }
src/screens/login/index.tsx
import React, { FormEvent } from 'react' const apiUrl = process.env.REACT_APP_API_URL export const LoginScreen = () => { const handleSubmit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault() const username = (event.currentTarget.elements[0] as HTMLInputElement).value const password = (event.currentTarget.elements[1] as HTMLInputElement).value login({username, password}) } const login = (param: { username: string; password: string }) => { fetch(`${apiUrl}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(param) }).then(async response => { if (response.ok) { const result = await response.json() console.log(result) } }) } return ( <div> <h3>登录</h3> <form onSubmit={ handleSubmit }> <div> <label htmlFor="username">用户名</label> <input type="text" id="username" /> </div> <div> <label htmlFor="password">密码</label> <input type="password" id="password" /> </div> <button type="submit">登录</button> </form> </div> ) }
npx imooc-jira-tool