比較全面的ts + react 開發教程
阿新 • • 發佈:2021-01-10
前言
本教程不用react-script
這種工具,用的webpack
+ eslint(可選)
+ prettier(可選)
+ babel
,使用編輯器用的vscode。
以下會涉及到ts語法和redux
、react-router-dom
的一些技巧,也是入門時學習到的。
準備工作
depend
# babel yarn add @babel/core @babel/preset-reac @babel/preset-typescript @babel/preset-env -D # webpack yarn add webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader css-loader fork-ts-checker-webpack-plugin -D # react-hot-reload
yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh type-fest -D # react 相關 yarn add react react-dom react-redux react-router-dom redux-thunk # 以上的這些react相關全部裝個type,例子 # yarn add @types/react @types/react-dom ... -D # eslint 可選 yarn add babel-eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-react-hooks -D
envfile
tsconfig.json
這裡我的工程檔案都放在./src
裡
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@root/*": ["src/*"]
},
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true ,
"lib": [
"es6",
"es7",
"dom"
],
"target": "es5",
"jsx": "react",
"allowJs": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true
},
"include": [
"./src/**/*"
]
}
.babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript",
"@babel/preset-react"
]
}
.eslintrc.js
module.exports = {
root: true,
env: {
commonjs: true,
es6: true,
browser: true,
node: true,
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
'prettier',
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
ignorePatterns: ['dist', 'lib'],
rules: {
'@typescript-eslint/no-var-requires': 0,
'@typescript-eslint/no-unused-vars': 0,
'prefer-const': 0,
'@typescript-eslint/no-explicit-any': 0,
'react-hooks/exhaustive-deps': 0,
},
}
.prettierrc
{
"semi": false,
"singleQuote": true
}
webpack.dev.js
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
module.exports = {
entry: './src/index.tsx',
output: {
filename: 'app.js',
path: path.join(__dirname, './dist'),
publicPath: '/',
},
context: path.resolve(__dirname, './src'),
devtool: 'cheap-module-source-map',
devServer: {
hot: true,
// enable HMR on the server
compress: true,
contentBase: path.resolve(__dirname, './src'),
// match the output path
publicPath: URL,
historyApiFallback: true,
},
mode: 'development',
module: {
rules: [
{
test: /\.[tj]sx?$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.css$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
mode: 'global',
localIdentName: '[name]__[local]--[hash:base64:5]',
},
importLoaders: 2,
},
},
],
},
],
},
resolve: {
alias: {
'@root': path.resolve(__dirname, './src'),
},
extensions: ['.ts', '.tsx', '.js', '.json'],
},
plugins: [
// 用來檢測開發中ts語法錯誤問題
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.resolve(__dirname, './tsconfig.json'),
},
}),
new ReactRefreshWebpackPlugin(),
new HTMLWebpackPlugin({
template: './src/app.html',
filename: 'index.html',
}),
],
}
基本開發
index.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from '@root/App'
ReactDOM.render(<App />, document.getElementById('app'))
App.tsx
import * as React from 'react'
import { Provider } from 'react-redux'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
// 這裡redux下面會給寫法
import store from './reducers'
const App: React.FC = function App(props) {
return (
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/a1">
<div>page a1</div>
</Route>
<Route path="/">
<div>home</div>
</Route>
</Switch>
</BrowserRouter>
</Provider>
)
}
export default App
redux相關
這裡以多個模組組合為例
/reducers/folder.ts
// 定義state
export type FolderDstate = {
folders: {
name: string
}[]
isLoading: boolean
loaded: boolean
}
let dstate: FolderDstate = {
folder: [],
isLoading: false,
loaded: false,
}
// 集合所有action
export type FolderAction =
| FolderLoad
| FolderUpdate
type FolderLoad = {
type: 'folder/load'
folders: {
name: string
}[]
}
type FolderUpdate = {
type: 'folder/update'
isLoading?: boolean
loaded?: boolean
}
let folder = (state = dstate, action: FolderAction): FolderDstate => {
switch (action.type) {
case 'folder/update': {
return {
...state,
isLoading: action.isLoading ?? state.isLoading,
loaded: action.loaded ?? state.loaded,
}
}
case 'folder/load': {
return {
...state,
folders: [...action.folders],
}
}
}
export default folder
這裡用的switch
是因為vscode會有case
提示
並且每塊case
中的變數都是相對獨立於上面定義的type的
/reducers/index.ts
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import folder, { FolderAction, FolderDstate } from './folder'
let rootReducer = combineReducers({
folder,
})
let store = (function configureStore() {
// 這裡是新增外掛使chrome的redux外掛能檢測並使用
// 這裡window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__會被ts爆出錯誤,以下會給解決方案
const middlewares = [thunkMiddleware]
const middlewaresEnhancer = applyMiddleware(...middlewares)
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
rootReducer,
composeEnhancers(middlewaresEnhancer)
)
return store
})()
export default store
// 這裡放進去所有的模組預設state type
export type StateBase = {
folder: FolderDstate
//other: otherDstate
}
// 這裡放進去所有的action集合,用|連線
export type RootActions = FolderAction /*| otherAction */
redux-connect用法
import * as React from 'react'
import { connect, ConnectedProps } from 'react-redux'
// --- 這裡的Dispatch最好用一個ts檔案集中管理,這裡方便就放一起了 ---
import { RootActions, StateBase } from '@root/reducers'
import { Dispatch } from 'redux'
export const loadFolder = (isForce = false) => (
dispatch: Dispatch<RootActions>,
getState: () => StateBase
): Promise<void> => {
const state = getState()
if (state.folder.isLoading || (!isForce && state.user.loaded)) return
dispatch({ type: 'folder/load', folders: [] })
dispatch({ type: 'folder/update', loaded:true })
}
// -------------
// 定義傳入引數,ConnectedProps是把connect裡的所有方法和變數都打包一起了
type Props = ConnectedProps<typeof connector> & {
//自定義引數
}
const Comp1: React.FC<Props> = (props) => {
React.useEffect(()=>{
props.loadFolder()
},[])
return <div>{`${props.loaded}`}</div>
}
const mapStateToProps = (state: StateBase) => {
return {
loaded: state.folder.loaded,
}
}
const mapDispatchToProps = {
loadFolder
}
let connector = connect(mapStateToProps, mapDispatchToProps)
export default connector(Comp1)
loadFolder
裡的dispatch
方法提示,都會根據type提示需要的引數
單獨帶提示的dispatch useDp.ts
import { RootActions } from '@root/reducers'
import { useDispatch } from 'react-redux'
export const useDp = (): React.Dispatch<RootActions> =>
useDispatch<React.Dispatch<RootActions>>()
效果
關於ts的一些東西
全域性變數
在./src
下隨便建個什麼的global.d.ts
,放一些ts沒法檢測的全域性變數
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: (...any: any) => any
}
將type變成全部可選的新type
export type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
// 這裡FolderDstate的所有屬性就變成全選了
type n = DeepPartial<FolderDstate>
動態key
如果用了eslint使用object型別他會報錯,這樣就是動態key了
type dykey = {
[key:string]:any
}
end
npx webpack serve --config webpack.dev.js
就ok了