1. 程式人生 > 其它 >比較全面的ts + react 開發教程

比較全面的ts + react 開發教程

技術標籤:reactts前端reacttypescript

前言

本教程不用react-script這種工具,用的webpack + eslint(可選) + prettier(可選) + babel,使用編輯器用的vscode。
以下會涉及到ts語法和reduxreact-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了