從 0 打造一個 React 的 TS 模板

Dcison發表於2019-02-01

前言

最近導師安排了一個任務,將一個已配置好的React、Webpack4等具有市場上常見功能的模板新增上TypeScript。俗話說:環境配置三小時,Coding不足5分鐘。配置環境真的非常浪費時間,因此寫個文章記錄下,希望能對各位要配置TS的前端們有所幫助。

從 0 打造一個 React 的 TS 模板

模板目錄

先放上完整配置好的專案地址: github.com/dcison/Reac…

├── .babelrc 
├── .eslintrc 
├── .gitignore 
├── index.html 
├── package.json
├── tsconfig.json 
├── src
│   ├── reducers 
│   ├── components 
│   ├── actions.ts
│   ├── global.styl
│   ├── index.tsx
│   └── store.ts
└── static
複製程式碼

文章目錄

  1. Babel、Webpack 基本配置
  2. TypeScript配置
  3. Eslint 校驗配置,基於Vscode
  4. React
  5. 樣式相關
  6. React-Router
  7. Code Splitting
  8. webpack 熱更新
  9. Redux
  10. Git 提交校驗

Babel、Webpack

  1. 首先在 src 中建立入口檔案 index.js, 隨便寫點程式碼,比如
// src/index.js
import type from './components/Apple'  
console.log(type)

// src/components/Apple.js
export default {
    type: 'apple'
}
複製程式碼
  1. 配置webpack.config.babel.js
import path from 'path';
export default {
	mode: 'development',
	devtool: 'eval',
	entry: {
		index: './src/index.js',
	},
	output: {
		publicPath: '/',
		filename: 'js/[name].js',
		chunkFilename: 'js/[name].js',
		path: path.resolve(__dirname, 'dist')
	},
	module: {
		rules: [
			{
			  test: /\.js$/,
			  exclude: /node_modules/,
			  use: {
				loader: 'babel-loader'
			  }
			}
		]
	},	
	devServer: {
		port: 3000
	},
	resolve: {
		extensions: [
			'.jsx',
			'.js',
			'.ts',
			'.tsx'
		]
	}
};
複製程式碼
  1. 編寫.babelrc
{
    "presets": [
        "@babel/preset-env"
    ],
    "plugins": [

    ]
}
複製程式碼
  1. 編寫package.json中的 "scripts" 與 "devDependencies"
{
  "scripts": {
    "start": "webpack-dev-server --content-base"
  },
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1",
    "@babel/register": "^7.0.0",
    "babel-loader": "^8.0.5",
    "webpack": "^4.29.0",
    "webpack-cli": "^3.2.1",
    "webpack-dev-server": "^3.1.14"
  },
}
複製程式碼
  1. 執行 yarn 或者 npm i 安裝依賴後 yarn start 或者 npm start,能看到success就表示Babel、Webpack配置成功了。

TypeScript

  1. 把剛才2個JS檔案字尾改為tsx
  2. 配置webpack.config.babel.js,包括rules,入口檔案的字尾:改為tsx。
entry: {
		index: './src/index.tsx',
},
{
   test: /\.tsx?$/,
   exclude: /node_modules/,
   use: {
   	loader: 'ts-loader'
   }
}
複製程式碼
  1. 在根目錄下建立一個 tsconfig.json
{
    "compilerOptions": {
      "module": "esnext",
      "target": "es5",
      "lib": ["es6", "dom"],
      "sourceMap": true,
      "jsx": "react",
      "noImplicitAny": true,
      "allowJs": true,
      "moduleResolution": "node"
    },
    "include": [
      "src/*"
    ],
    "exclude": [
      "node_modules"
    ]
  }
複製程式碼
  1. 安裝依賴 yarn add -D ts-loader typescript
  2. yarn start 如果依舊 success 即表示成功

Eslint

  1. 新增.eslintrc 配置檔案,如下:
{
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaFeatures": {
          "experimentalObjectRestSpread": true,
          "jsx": true
        },
        "sourceType": "module"
    },
    "parser": "typescript-eslint-parser",
    "plugins": [
		"react",
        "typescript"
    ],
    "rules": {
        //... 自行新增規則
    }
}
複製程式碼
  1. 安裝依賴 yarn add -D eslint eslint-plugin-typescript typescript-eslint-parser eslint-plugin-react
  2. 配置Vscode:首選項 -> 設定(即setting.json),找到eslint.validate,沒有就自己寫,加入typescript與typescriptreact,分別用於監聽ts與tsx檔案,如下:
"eslint.validate": [
  "javascript",
  "javascriptreact",
  {
      "language": "typescript",
      "autoFix": true
  },
  {
      "language": "typescriptreact",
      "autoFix": true
  }
]
複製程式碼

React

  1. 使用配好 html-webpack-plugin 外掛 ,yarn add -D html-webpack-plugin
  2. 在webpack.config.babel.js中配置下
import HtmlWebpackPlugin from 'html-webpack-plugin';

plugins: [	
		new HtmlWebpackPlugin({
			title: '模板',
			hash: false,
			filename: 'index.html',
			template: './index.html',
		})
]
複製程式碼
  1. 在根目錄下寫一個 html 模板
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title><%= htmlWebpackPlugin.options.title || ''%></title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
複製程式碼
  1. 安裝React相關依賴: yarn add react @types/react react-dom @types/react-dom @babel/preset-react
  2. .babelrc 中新增一條規則
"presets": [
        "@babel/preset-env",
        "@babel/preset-react" // 新增這條
],
複製程式碼
  1. 修改src/index.tsx
import * as React from 'react';
import * as ReactDOM from "react-dom";
class SomeComponent extends React.Component<{}, {}> {
	render () {
		return <div>hello</div>;
	}
}
ReactDOM.render(
	<SomeComponent/>,
	document.getElementById('root')
);
複製程式碼
  1. 在根目錄執行yarn start ,在介面上看到hello就表示React的環境配好了

CSS 及 樣式預處理 (以Stylus為例)

  1. 安裝CSS相關loader:yarn add -D css-loader style-loader
  2. 配置webpack.config.babel.js 中的rules
{
	test: /\.css$/,
	use: [
		'style-loader',
		{
			loader: 'css-loader',
			options: {
				localIdentName: '[path][name]__[local]--[hash:base64:5]'
			}
		}
	]
}
複製程式碼
  1. 測試:在src下建一個css檔案,然後在index.tsx中引用
import './global.css';
複製程式碼
  1. 能看到樣式被應用就表示CSS配置正常了,接下來配置Stylus
  2. 安裝相關依賴: yarn add -D stylus-loader stylus
  3. 繼續配置webpack.config.babel.js 中的rules
{
	test: /\.styl$/,
	use: [
		'style-loader',
		{
			loader: 'css-loader',
			options: {
				modules: true,
				import: true,
				importLoaders: 1,
				localIdentName: '[path][name]__[local]--[hash:base64:5]'
			}
		},
		{ loader: 'stylus-loader' },
	]
},
複製程式碼
  1. 更改剛才CSS字尾檔案為styl,index引用中的檔案也改為相應的Styl檔案,如果樣式依舊有改動表示樣式前處理器配置成功。

2.15日 追加: 新增typings-for-css-modules-loader

  1. 安裝依賴yarn add -D typings-for-css-modules-loader css-loader@^1.0.1 (css-loader 2.x版本以上有問題)
  2. 修改webpack.config.babel.js 中的rules(此處只修改stylus)
{
	test: /\.styl$/,
	use: [
		'style-loader',
		{
			loader: 'typings-for-css-modules-loader',
			options: {
				modules: true,
				namedExport: true,
				camelCase: true,
				minimize: true,
				localIdentName: "[local]_[hash:base64:5]",
				stylus: true
			}
		},
		{ loader: 'stylus-loader' },
	]
},
複製程式碼
  1. 使用方式:
import * as styles from './apple.styl';

<div className={styles.redFont}>測試文字</div>
複製程式碼
    1. 這裡我們遇到一個坑,所有的css-moduels/typings-for-css-modules-loader教程並不會告訴你這裡include配置不寫會造成什麼影響。當我們此前沒有寫這句話的時候,d.ts因為索引速度不夠快,或者說沒有在內部自動建立聯絡(這裡原因比較迷),會導致我們命令列和視窗直接報未找到型別的錯誤,需要手動重新編譯一次,效率極低,當我們加上include後就可以隨便折騰了(迷)include:path.resolve('src/')

      不知為何,新增inclue配置後,依舊會報未找到對應的樣式模組錯(但此時對應的.d.ts宣告檔案已經生成)。由於開啟了熱更新,不需要再次重新編譯,改動一個小內容觸發一下webpack的熱更新即可正常使用。

    2. 第二個坑,如果你只引用,而不用,會無法生成對應的.d.ts檔案,因此會打包失敗

React-Router

  1. 安裝依賴:yarn add react-router-dom @types/react-router-dom
  2. 修改下webpack.config.babel.js中的devServer,新增historyApiFallback
devServer: {
   historyApiFallback: true
}
複製程式碼
  1. 在Component中的apple.tsx與xiaomi.tsx修改一下程式碼,如下
// src/components/Apple.tsx
import * as React from 'react';
class Apple extends React.Component<any, any> {
    state = {
    	text: 'iphone XR'
    }
    handleChange = () => {
    	this.setState({
    		text: 'iphone8'
    	});
    }
    render () {
    	return <>
            <button onClick={this.handleChange}>點我換機子</button>
            {this.state.text}
        </>;
    }
}
export default Apple;
// src/components/xiaomi.tsx 
// 程式碼結構類似,就只改了下text的內容,所以不放了
// text: 'xiaomi 8'
複製程式碼
  1. 修改index.tsx中的程式碼,如下
import * as React from 'react';
import * as ReactDOM from "react-dom";
import { BrowserRouter, Route, Redirect, Switch, Link } from 'react-router-dom';
import Apple from './components/Apple';
import Xiaomi from './components/XiaoMi';
import './global.styl';
const Header = () => (
	<ul>
		<li><Link to="/">蘋果</Link></li>
		<li><Link to="/phone/xiaomi">小米</Link></li>
	</ul>
);
ReactDOM.render(
	<div>
		<BrowserRouter>
			<>
			<Header />
			<Switch>
				<Route path="/phone/apple" component={Apple} />
				<Route path="/phone/xiaomi" component={Xiaomi} />
				<Redirect to="/phone/apple" />
			</Switch>
			</>
		</BrowserRouter>
	</div>,
	document.getElementById('root')
);
複製程式碼
  1. 啟動你的yarn start 進行測試吧

code splitting

  1. 在Component目錄下新增一個檔案,asyncComponent.tsx
import * as React from "react";
export default function asyncComponent (importComponent: any) {
	class asyncComponent extends React.Component<any, any> {
		constructor (props: any) {
			super(props);
  
			this.state = {
				component: null
			};
		}
  
		async componentDidMount () {
			const { default: component } = await importComponent();
  
			this.setState({
				component: component
			});
		}
  
		render () {
			const C = this.state.component;
  
			return C ? <C {...this.props} /> : null;
		}
	}
  
	return asyncComponent;
}
複製程式碼
  1. 在index.tsx引用的地方改為以下內容
import asyncComponent from "./components/asyncComponent";
const Apple =  asyncComponent(() => import("./components/Apple"));
const XiaoMi =  asyncComponent(() => import("./components/XiaoMi"));
複製程式碼
  1. 如果出現下圖情況則表示成功了

從 0 打造一個 React 的 TS 模板

webpack 熱更新

  1. 安裝依賴 yarn add -D @types/webpack-env
  2. 開啟熱更新,修改webpack.config.babel.js的plugins、devServer ,新增兩個屬性
import webpack from 'webpack';
plugins: [	
		new webpack.HotModuleReplacementPlugin()
],
devServer: {
		hot: true
},
複製程式碼
  1. 在入口檔案中加入以下程式碼,更多詳情可參考官網
if (module.hot) {
	module.hot.accept();
}
複製程式碼
  1. 隨意修改一些文字內容,如果有下圖型別情況出現表示配置成功

從 0 打造一個 React 的 TS 模板

Redux

  1. 安裝依賴:yarn add @types/react-redux react-redux redux redux-thunk
  2. 在src/下新增一個actions.ts,用於存放所有actions
export const initPhone = (data: object) => {
	return {
		type: 'INIT_PHONE',
		data
	};
};
export const setPhoneMoney = (data: object) => {
	return {
		type: 'SET_MONEY',
		data
	};
};
複製程式碼
  1. 在src/下新建一個 reducers 目錄,存放各業務reducer
// src/reducers/Phone.ts
var initState = {
	name: '',
	money: 0
};
export default function (state = initState, action: any) {
	switch (action.type) {
			case 'INIT_PHONE':
				return {
					...state,
					name: action.data.name,
					money: action.data.money
				};
			case 'SET_MONEY':
				return {
					...state,
					money: action.data.money
				};
			default:
				return state;
	}
}
複製程式碼
  1. 在src/下建store.ts
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import Phone from './reducers/Phone';
const rootReducer =  combineReducers({
	Phone
});
const createStoreWithMiddleware = applyMiddleware(
	thunkMiddleware,
)(createStore);
function configureStore (initialState?: any) {
	return createStoreWithMiddleware(rootReducer, initialState);
}
export default configureStore();
複製程式碼
  1. 修改元件中的程式碼,這裡以Component/Apple.tsx為例
import * as React from 'react';
import { connect } from 'react-redux';
import * as action from '../actions';
class Apple extends React.Component<any, any> {
	componentDidMount () {
		this.props.initPhone({ name: '蘋果8', money: 10000 });
	}
    handleChange = () => {
    	this.props.setPhoneMoney({ money: this.props.money - 20 });
    }
    render () {
    	return <>
            <button onClick={this.handleChange}>點我降價</button>
            {this.props.name} 現在僅售價 {this.props.money}
        </>;
    }
}
function mapStateToProps (state: any) {
	return {
		name: state.Phone.name,
		money: state.Phone.money
	};
}
export default connect(mapStateToProps, action)(Apple);
複製程式碼
  1. 修改入口的程式碼(src/index.tsx)
import { Provider } from 'react-redux';
import store from './store';

<Provider store={store}> //用該容器包裹一下
		<BrowserRouter>
			<>
			<Header />
			<Switch>
				<Route path="/phone/apple" component={Apple} />
				<Route path="/phone/xiaomi" component={XiaoMi} />
				<Redirect to="/phone/apple" />
			</Switch>
			</>
		</BrowserRouter>
	</Provider>
複製程式碼

Husky 與 lint-staged

  1. 安裝依賴 yarn add -D lint-staged husky
  2. 在package.json中新增以下程式碼即可
"scripts": {
    "precommit": "lint-staged"
  },
"lint-staged": {
 "*.{ts,tsx}": [
   "eslint --fix",
   "git add"
 ]
}
複製程式碼
  1. 自行提交測試下吧~

參考

後話

由於該文是邊配置編寫的,時間跨度也有點大,導致寫文的思路斷斷續續,也未做過多的文字潤色,給看到最後的讀者們遞眼藥水。

從 0 打造一個 React 的 TS 模板

相關文章