├── .env.dev ├── .gitignore ├── .prettierrc.js ├── .stylelintrc.js ├── LICENSE ├── README.md ├── package.json ├── src ├── actions │ └── index.ts ├── api.ts ├── assets │ ├── grm.png │ ├── logo.png │ └── money.svg ├── components │ ├── AddTodo │ │ ├── AddTodo.tsx │ │ └── index.ts │ ├── App.css │ ├── App.tsx │ ├── TodoItem │ │ ├── TodoItem.tsx │ │ └── index.ts │ ├── TodoList │ │ ├── TodoList.tsx │ │ └── index.ts │ └── loading │ │ ├── InstanceWrapper.ts │ │ ├── Loading.scss │ │ ├── Loading.tsx │ │ └── index.ts ├── constants │ └── index.ts ├── container │ ├── home │ │ ├── Home.tsx │ │ ├── home.scss │ │ └── index.ts │ └── result │ │ ├── Result.scss │ │ ├── Result.tsx │ │ └── index.ts ├── core │ ├── bootstrap │ │ ├── flexibleRem.ts │ │ └── index.ts │ └── request │ │ └── index.ts ├── entries │ ├── index.html │ ├── index.scss │ └── index.tsx ├── reducers │ ├── index.ts │ └── todoList.ts ├── store │ └── index.ts ├── styles │ └── variables.scss └── types │ └── index.ts ├── theme.ts ├── tools ├── paths.config.ts └── webpack.config.ts ├── tsconfig.json ├── tslint.json ├── typings ├── images.d.ts └── typed-css-modules.d.ts └── yarn.lock /.env.dev: -------------------------------------------------------------------------------- 1 | REACT_APP_API_BASE= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | /node_modules 5 | 6 | # Compiled output 7 | /build 8 | /dist 9 | 10 | yarn-error.log -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // Prettier configuration 2 | // https://io/docs/en/configuration.html 3 | module.exports = { 4 | stylelintIntegration: true, // 让prettier使用stylelint的代码格式校验 5 | tabWidth: 2, // 缩进字节数 6 | singleQuote: true, // 使用单引号替换双引号 7 | semi: true, // 句末加分号 8 | printWidth: 120, // 超过最大值换行 9 | arrowParens: "avoid", // (x) => {} 是否要有小括号 avoid:省略括号 10 | proseWrap: "preserve", //默认值。 是否要换行 11 | trailingComma: "all", // 在对象或数组最后一个元素是否加逗号 12 | tslintIntegration: true, // 让prettier使用tslint的代码格式进行校验 13 | }; -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "stylelint-config-standard", 3 | // 需要忽略掉一些检测文件类型 4 | ignoreFiles: [ 5 | "node_modules/**/*.scss", 6 | "node_modules/**/*.less", 7 | "node_modules/**/*.css", 8 | "**/*.md", 9 | "**/*.js", 10 | "**/*.ts", 11 | "**/*.tsx", 12 | ], 13 | plugins: ["stylelint-order", "stylelint-scss"], 14 | 15 | rules: { 16 | "at-rule-no-unknown": [ 17 | true, 18 | { 19 | ignoreAtRules: [ "at-root", 20 | "content", 21 | "extend", 22 | "include", 23 | "mixin", 24 | "function", 25 | "return", 26 | "debug", 27 | "warn", 28 | "error", 29 | "if", 30 | "else", 31 | "for", 32 | "each", 33 | "while", 34 | ], 35 | }, 36 | ], 37 | indentation: 2, 38 | // 不允许未知属性 39 | "property-no-unknown": [ 40 | true, 41 | { ignoreProperties: ["composes"] }, 42 | ], 43 | 44 | // 不允许未知的伪类选择器 45 | "selector-pseudo-class-no-unknown": [ 46 | true, 47 | { ignorePseudoClasses: ["global", "local"] }, 48 | ], 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 vettel-qin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 工具版本 2 | 3 | - node 版本 `v10.16.0` 4 | - npm 版本 `v6.12.0` 5 | 6 | ### 开发命令 7 | 8 | 1. 运行 `npm install` 安装依赖包 9 | 2. 运行 `npm run start` 启动 dev 服务器 10 | 3. 运行 `npm run build` 编译发布包 11 | 12 | ``` 13 | 本文详细介绍了如何从零开始搭建一个 Typescript + React 开发的脚手架,包含如何添加 Redux 以及 React Router 的环境。 14 | 15 | 建议将代码拉下来之后,配合本文一起查看,效果更佳。 16 | 17 | 代码下载命令:git clone https://github.com/vettel-qin/ts-react-redux.git 18 | ``` 19 | 20 | ## 相关文章目录 21 | 22 | 本文代码地址:[ts-react-redux](https://github.com/vettel-qin/ts-react-redux) 23 | 24 | 25 | [从零开始,一步一步搭建Typescript + React + Redux项目——创建项目结构(一)](https://www.jianshu.com/p/1798094ea2eb) 26 | 27 | [从零开始,一步一步搭建Typescript + React + Redux项目——开发环境配置(二)](https://www.jianshu.com/p/063535c0733e) 28 | 29 | [从零开始,一步一步搭建 Typescript + React + Redux项目——集成React(三)](https://www.jianshu.com/p/0e9092c40f11) 30 | 31 | [从零开始,一步一步搭建 Typescript + React + Redux项目——集成Redux(四)](https://www.jianshu.com/p/8a1fa4cdc12d) 32 | 33 | [从零开始,一步一步搭建 Typescript + React + Redux项目——项目打包(五)](https://www.jianshu.com/p/8c0bcc12ea12) 34 | 35 | [从零开始,一步一步搭建 Typescript + React + Redux项目——团队合作规范(六)](https://www.jianshu.com/p/769a037af6bf) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-react", 3 | "version": "1.0.0", 4 | "description": "从零开始,一步一步搭建Typescript+React+Redux项目模板", 5 | "main": "index.js", 6 | "repository": "git@github.com:vettel-qin/ts-react-redux.git", 7 | "author": "vettel-qin", 8 | "license": "MIT", 9 | "browserslist": [ 10 | "Android >= 4", 11 | "Chrome >= 29", 12 | "iOS >= 6", 13 | "Safari >= 7.1" 14 | ], 15 | "scripts": { 16 | "start": "webpack-dev-server --config tools/webpack.config.ts --progress --dev", 17 | "build": "webpack --config tools/webpack.config.ts --progress", 18 | "analyzer": "yarn run build --report --analyze", 19 | "lint:ts": "tslint \"{src, tools}/**/*.{ts,tsx}\"", 20 | "lint:css": "stylelint \"{src,tools}/**/*.{css,less,scss,sass}\"", 21 | "lint": "yarn lint:ts && yarn lint:css" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.9.0", 25 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 26 | "@babel/preset-env": "^7.9.5", 27 | "@babel/preset-react": "^7.9.4", 28 | "@hot-loader/react-dom": "^16.13.0", 29 | "@types/clean-webpack-plugin": "^0.1.3", 30 | "@types/html-webpack-plugin": "^3.2.0", 31 | "@types/mini-css-extract-plugin": "^0.9.1", 32 | "@types/node": "^13.13.0", 33 | "@types/optimize-css-assets-webpack-plugin": "^5.0.1", 34 | "@types/react": "^16.9.34", 35 | "@types/react-dom": "^16.9.6", 36 | "@types/react-router-dom": "^5.1.4", 37 | "@types/uglifyjs-webpack-plugin": "^1.1.0", 38 | "@types/webpack": "^4.41.11", 39 | "@types/webpack-bundle-analyzer": "^2.13.3", 40 | "@types/webpack-dev-server": "^3.10.1", 41 | "@types/webpack-env": "^1.15.1", 42 | "autoprefixer": "^9.7.6", 43 | "axios": "^0.19.2", 44 | "babel-loader": "^8.1.0", 45 | "cache-loader": "^4.1.0", 46 | "clean-webpack-plugin": "^3.0.0", 47 | "css-loader": "^3.5.2", 48 | "file-loader": "^6.0.0", 49 | "html-webpack-plugin": "^3.2.0", 50 | "husky": "^4.2.5", 51 | "lint-staged": "^10.1.6", 52 | "mini-css-extract-plugin": "^0.9.0", 53 | "node-sass": "^4.13.1", 54 | "optimize-css-assets-webpack-plugin": "^5.0.3", 55 | "postcss-loader": "^3.0.0", 56 | "prettier": "^2.0.4", 57 | "query-string": "^6.12.1", 58 | "react-hot-loader": "^4.12.20", 59 | "react-router-dom": "^5.1.2", 60 | "redux-thunk": "^2.3.0", 61 | "sass-loader": "^8.0.2", 62 | "style-loader": "^1.1.4", 63 | "stylelint": "^13.3.2", 64 | "stylelint-config-standard": "^20.0.0", 65 | "stylelint-order": "^4.0.0", 66 | "stylelint-scss": "^3.17.0", 67 | "ts-loader": "^7.0.0", 68 | "ts-node": "^8.8.2", 69 | "tsconfig-paths-webpack-plugin": "^3.2.0", 70 | "tslint": "^6.1.1", 71 | "tslint-react": "^4.2.0", 72 | "typescript": "^3.8.3", 73 | "uglifyjs-webpack-plugin": "^2.2.0", 74 | "url-loader": "^4.1.0", 75 | "webpack": "^4.42.1", 76 | "webpack-bundle-analyzer": "^3.7.0", 77 | "webpack-cli": "^3.3.11", 78 | "webpack-dev-server": "^3.10.3" 79 | }, 80 | "dependencies": { 81 | "@types/react-redux": "^7.1.7", 82 | "react": "^16.13.1", 83 | "react-dom": "^16.13.1", 84 | "react-redux": "^7.2.0", 85 | "redux": "^4.0.5" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { CHANGE_VALUE, ADD_ITEM, DELETE_ITEM } from '~/constants'; 2 | 3 | export interface IChangeValueAction { 4 | value: any; 5 | type: CHANGE_VALUE; 6 | } 7 | 8 | export interface IAddItemAction { 9 | index: number; 10 | value: any; 11 | type: ADD_ITEM; 12 | } 13 | 14 | export interface IDeleteItemAction { 15 | index: number; 16 | value: any; 17 | type: DELETE_ITEM; 18 | } 19 | 20 | // 定义 TodoAction类型,包含IChangeValueAction | IAddItemAction | IDeleteItemAction 21 | export type TodoAction = IChangeValueAction | IAddItemAction | IDeleteItemAction; 22 | 23 | export const changeValue = (value: any): IChangeValueAction => ({ 24 | type: CHANGE_VALUE, 25 | value, 26 | }); 27 | 28 | export const addItem = () => ({ 29 | type: ADD_ITEM, 30 | }); 31 | 32 | export const deleteItem = (index: number) => ({ 33 | index, 34 | type: DELETE_ITEM, 35 | }); 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import { get } from '~/core/request'; 2 | 3 | interface ListInfo { 4 | } 5 | 6 | export function getProvinceList(): any { 7 | return get('http://5d79e6fe9edf7400140a90cf.mockapi.io/api/findProvinceList'); 8 | } 9 | 10 | // export function login(data: object) { 11 | // return post('/scrm/auth/consumer/loginByPhonePwd', data, headersSet('25234234')); 12 | // } 13 | -------------------------------------------------------------------------------- /src/assets/grm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vettel-qin/ts-react-redux/4beda710cbefb6c3c8760a29c9f2f5ae9e4bb88f/src/assets/grm.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vettel-qin/ts-react-redux/4beda710cbefb6c3c8760a29c9f2f5ae9e4bb88f/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AddTodo/AddTodo.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | 3 | // 创建类型接口 4 | interface IProps { 5 | inputValue: string; 6 | onChange: (e: any) => void; 7 | submit: () => void; 8 | } 9 | 10 | const AddTodo = ({ onChange, inputValue, submit }: IProps) => ( 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | export default AddTodo; 18 | 19 | -------------------------------------------------------------------------------- /src/components/AddTodo/index.ts: -------------------------------------------------------------------------------- 1 | import AddTodo from './AddTodo'; 2 | 3 | export default AddTodo; -------------------------------------------------------------------------------- /src/components/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, 6 | *::before, 7 | *::after { 8 | box-sizing: inherit; 9 | } 10 | 11 | html, 12 | body { 13 | width: 100%; 14 | height: 100%; 15 | text-size-adjust: 100% !important; 16 | user-select: none; 17 | margin: 0; 18 | padding: 0; 19 | line-height: 1.15; 20 | } 21 | 22 | body { 23 | font-family: 'Helvetica Neue', Helvetica, STHeiTi, sans-serif; 24 | } 25 | 26 | ::-webkit-scrollbar { 27 | width: 0; 28 | background: transparent; 29 | } 30 | 31 | * { 32 | -webkit-tap-highlight-color: rbga(0, 0, 0, 0); /* 禁止可点元素的黑色块 */ 33 | } 34 | 35 | a, 36 | img { 37 | -webkit-touch-callout: none; /* 禁止长按链接与图片弹出菜单 */ 38 | } 39 | 40 | input { 41 | box-shadow: none; 42 | } 43 | 44 | :global(.flex-1) { 45 | flex: 1; 46 | } 47 | 48 | :global(.flex-2) { 49 | flex: 2; 50 | } 51 | 52 | :global(.flex-3) { 53 | flex: 3; 54 | } 55 | 56 | :global(.flex-4) { 57 | flex: 4; 58 | } 59 | 60 | :global(.clearfix::before), 61 | :global(.clearfix::after) { 62 | display: table; 63 | content: ' '; 64 | } 65 | 66 | :global(.clearfix::after) { 67 | clear: both; 68 | } 69 | 70 | :global(#react-root) { 71 | display: flex; 72 | flex-direction: column; 73 | height: 100%; 74 | position: relative; 75 | } 76 | 77 | /* @media screen and (min-width: 751px) { 78 | :global(#react-root) { 79 | width: 750px; 80 | margin: 0 auto; 81 | } 82 | } */ 83 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { hot } from 'react-hot-loader/root'; 2 | 3 | import React from 'react'; 4 | import './App.css'; 5 | 6 | interface IAppProps {} 7 | 8 | const App: React.SFC = ({ children }) => { 9 | return <>{children}; 10 | }; 11 | 12 | App.defaultProps = {}; 13 | 14 | export default hot(App); 15 | -------------------------------------------------------------------------------- /src/components/TodoItem/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // 创建类型接口 4 | interface IProps { 5 | item: string; 6 | deleteItem: () => void; 7 | } 8 | 9 | const TodoItem = ({ item, deleteItem }: IProps) =>
  • {item}
  • ; 10 | 11 | export default TodoItem; 12 | -------------------------------------------------------------------------------- /src/components/TodoItem/index.ts: -------------------------------------------------------------------------------- 1 | import TodoItem from './TodoItem'; 2 | 3 | export default TodoItem; 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/TodoList/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import TodoItem from '~/components/TodoItem'; 3 | 4 | // 创建类型接口 5 | interface IProps { 6 | todos: any[]; 7 | deleteItem: (num: number) => void; 8 | } 9 | 10 | class TodoList extends PureComponent { 11 | public render() { 12 | const { todos } = this.props; 13 | 14 | return ( 15 | 20 | ); 21 | } 22 | private handleItemDelete = (num: number) => () => { 23 | const { deleteItem } = this.props; 24 | deleteItem(num); 25 | }; 26 | } 27 | 28 | export default TodoList; -------------------------------------------------------------------------------- /src/components/TodoList/index.ts: -------------------------------------------------------------------------------- 1 | import TodoList from './TodoList'; 2 | 3 | export default TodoList; -------------------------------------------------------------------------------- /src/components/loading/InstanceWrapper.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Loading from './Loading'; 4 | 5 | class InstanceWrapper { 6 | private refCount: number = 0; 7 | private container: HTMLElement | null = null; 8 | private root: HTMLElement; 9 | 10 | constructor() { 11 | this.root = document.getElementById('react-modal'); 12 | } 13 | 14 | public show() { 15 | this.refCount += 1; 16 | if (this.refCount === 1) { 17 | this.create(); 18 | } 19 | } 20 | 21 | public hide() { 22 | this.refCount -= 1; 23 | if (this.refCount === 0) { 24 | this.destroy(); 25 | } else if (this.refCount < 0) { 26 | this.refCount = 0; 27 | } 28 | } 29 | 30 | public reset() { 31 | this.refCount = 0; 32 | this.destroy(); 33 | } 34 | 35 | private create() { 36 | if (!this.container) { 37 | this.container = document.createElement('div'); 38 | this.root.appendChild(this.container); 39 | 40 | ReactDOM.render(React.createElement(Loading), this.container); 41 | } 42 | } 43 | 44 | private destroy() { 45 | if (this.container) { 46 | this.root.removeChild(this.container); 47 | this.container = null; 48 | } 49 | } 50 | } 51 | 52 | export default InstanceWrapper; 53 | -------------------------------------------------------------------------------- /src/components/loading/Loading.scss: -------------------------------------------------------------------------------- 1 | .mask { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './Loading.scss'; 3 | 4 | function Loading() { 5 | return ( 6 |
    7 | 14 | 25 | 35 | 36 | 48 | 58 | 59 | 60 |
    61 | ); 62 | } 63 | 64 | export default Loading; 65 | -------------------------------------------------------------------------------- /src/components/loading/index.ts: -------------------------------------------------------------------------------- 1 | import Loading from './Loading'; 2 | 3 | import InstanceWrapper from './InstanceWrapper'; 4 | 5 | const instance = new InstanceWrapper(); 6 | 7 | export { Loading }; 8 | export default instance; 9 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const CHANGE_VALUE = 'CHANGE_VALUE'; 2 | export type CHANGE_VALUE = typeof CHANGE_VALUE; 3 | 4 | export const ADD_ITEM = 'ADD_ITEM'; 5 | export type ADD_ITEM = typeof ADD_ITEM; 6 | 7 | export const DELETE_ITEM = 'DELETE_ITEM'; 8 | export type DELETE_ITEM = typeof DELETE_ITEM; 9 | -------------------------------------------------------------------------------- /src/container/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { getProvinceList } from '~/api'; 4 | import AddTodo from '~/components/AddTodo'; 5 | import TodoList from '~/components/TodoList'; 6 | import { changeValue, addItem, deleteItem } from '~/actions'; 7 | 8 | // 创建类型接口 9 | export interface IHomeProps { 10 | inputValue: string; 11 | list: any[]; 12 | changeValue: (e: any) => void; 13 | addItem: () => void; 14 | deleteItem: (index: number) => void; 15 | } 16 | 17 | export interface IHomeState { 18 | provinceData: any[]; 19 | } 20 | 21 | // 使用类型接口代替PropTypes进行类型校验 22 | class Home extends PureComponent { 23 | constructor(props: Readonly) { 24 | super(props); 25 | this.state = { 26 | provinceData: [], 27 | }; 28 | } 29 | 30 | public componentDidMount() { 31 | getProvinceList() 32 | .then((res: any[]) => { 33 | this.setState(() => ({ provinceData: res })); 34 | }) 35 | .catch((err: any) => { console.error(err); 36 | }); 37 | } 38 | 39 | public render() { 40 | const { provinceData } = this.state; 41 | const { inputValue, list } = this.props; 42 | return ( 43 |
    44 |
      45 | { 46 | provinceData.map((data: { provinceId: number; provinceName: string }) => ( 47 |
    • {data.provinceName}
    • 48 | )) 49 | } 50 |
    51 | 52 | 53 |
    54 | ); 55 | } 56 | 57 | private handleChangeValue = (e: any) => { this.props.changeValue(e.target.value); }; 58 | 59 | private handleSubmit = () => { this.props.addItem(); }; 60 | 61 | private handleDeleteItem = (index: number) => { this.props.deleteItem(index); }; 62 | } 63 | 64 | const mapStateToProps = (state: any) => ({ inputValue: state.todoList.inputValue, list: [...state.todoList.list]}); 65 | 66 | const mapDispatchToProps = { changeValue, addItem, deleteItem }; 67 | 68 | export default connect(mapStateToProps, mapDispatchToProps)(Home); 69 | 70 | -------------------------------------------------------------------------------- /src/container/home/home.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | height: 5rem; 4 | // background: url('../../assets/grm.png'); 5 | background-repeat: no-repeat; 6 | transform: translate(0, 0); 7 | color: #333; 8 | } 9 | 10 | @mixin center { 11 | text-align: center; 12 | } 13 | -------------------------------------------------------------------------------- /src/container/home/index.ts: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | 3 | export default Home; 4 | 5 | -------------------------------------------------------------------------------- /src/container/result/Result.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | height: 5rem; 4 | // background: url('../../assets/grm.png'); 5 | background-repeat: no-repeat; 6 | transform: translate(0, 0); 7 | color: #333; 8 | } 9 | 10 | @mixin center { 11 | text-align: center; 12 | } 13 | -------------------------------------------------------------------------------- /src/container/result/Result.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | // 创建类型接口 4 | export interface IResultProps { 5 | } 6 | 7 | export interface IResultState { 8 | } 9 | 10 | // 使用类型接口代替PropTypes进行类型校验 11 | class Result extends Component { 12 | 13 | public render() { 14 | return ( 15 |
    16 | React Result 17 |
    18 | ); 19 | } 20 | } 21 | 22 | export default Result; 23 | 24 | -------------------------------------------------------------------------------- /src/container/result/index.ts: -------------------------------------------------------------------------------- 1 | import Result from './Result'; 2 | 3 | export default Result; 4 | 5 | -------------------------------------------------------------------------------- /src/core/bootstrap/flexibleRem.ts: -------------------------------------------------------------------------------- 1 | import { BASE_FONT_SIZE, BASE_SCREEN_WIDTH } from '~/config'; 2 | 3 | function scaleFontSize() { 4 | const docEl = document.documentElement; 5 | // 获取当前页面的宽度 6 | const screenWidth = docEl.getBoundingClientRect().width || window.innerWidth; 7 | // 设置页面的字体大小 8 | const fontSize = (screenWidth / BASE_SCREEN_WIDTH) * BASE_FONT_SIZE; 9 | // 给获取到的元素(docEl)的字体大小赋值 10 | docEl.style.fontSize = fontSize > BASE_FONT_SIZE ? `${BASE_FONT_SIZE}px` : `${fontSize}px`; 11 | } 12 | 13 | export default function flexibleRem() { 14 | scaleFontSize(); 15 | // 监听屏幕变化 16 | window.addEventListener('resize', scaleFontSize, false); 17 | } 18 | -------------------------------------------------------------------------------- /src/core/bootstrap/index.ts: -------------------------------------------------------------------------------- 1 | import flexibleRem from './flexibleRem'; 2 | 3 | export default function boostrap() { 4 | return Promise.all([flexibleRem()]); 5 | } 6 | -------------------------------------------------------------------------------- /src/core/request/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const service = axios.create({ 4 | baseURL: process.env.REACT_APP_API_BASE, 5 | timeout: 30000, 6 | }); 7 | 8 | /** 9 | * Content-Type 10 | */ 11 | function contentType(type: any) { 12 | switch (type) { 13 | case 'form': 14 | return { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' }; 15 | case 'json': 16 | return { 'Content-Type': 'application/json;charset=utf-8' }; 17 | default: 18 | return {}; 19 | } 20 | } 21 | 22 | function request(method: any, url: any, options: any) { 23 | const defered = service({ 24 | method, 25 | url, 26 | data: options.data, 27 | params: options.params, 28 | headers: { 29 | ...options.headers, 30 | ...contentType(options.type), 31 | }, 32 | }); 33 | 34 | return defered; 35 | } 36 | 37 | export function get(url: string, params = {}, options = {}) { 38 | return new Promise((resolve, reject) => { 39 | request('GET', url, { 40 | ...options, 41 | params, 42 | }) 43 | .then(response => response.data) 44 | .then(res => { 45 | resolve(res); 46 | }) 47 | .catch(err => { 48 | reject(err.response); 49 | }); 50 | }); 51 | } 52 | 53 | export function post(url: string, data = {}, options = {}) { 54 | return new Promise((resolve, reject) => { 55 | request('POST', url, { 56 | ...options, 57 | data, 58 | }) 59 | .then(response => { 60 | resolve(response.data); 61 | }) 62 | .catch(err => { 63 | reject(err.response); 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /src/entries/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ts-react-redux 9 | 10 | 11 | 12 |
    13 |
    14 | 15 | 16 | -------------------------------------------------------------------------------- /src/entries/index.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .wrapper { 4 | color: $blue; 5 | font-size: $base-font-size; 6 | display: flex; 7 | 8 | .logo { 9 | width: 100px; 10 | height: 100px; 11 | pointer-events: none; 12 | animation: logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | @keyframes logo-spin { 17 | from { 18 | transform: rotate(0deg); 19 | } 20 | 21 | to { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/entries/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HashRouter as Router, Switch, Route } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import App from '~/components/App'; 6 | import Home from '~/container/home'; 7 | import Result from '~/container/result'; 8 | import store from '~/store'; 9 | import s from './index.scss'; 10 | import logo from '~/assets/logo.png'; 11 | 12 | 13 | function render() { 14 | return ReactDOM.render( 15 | 16 | 17 |
    18 |

    Hello, react + redux

    19 | logo 20 |
    21 | 22 | 23 | 24 | 25 | 26 | 27 |
    28 |
    , 29 | document.getElementById('react-root') 30 | ); 31 | } 32 | 33 | render(); 34 | 35 | if (module.hot) { 36 | module.hot.accept(['~/components/App'], render); 37 | module.hot.accept(() => window.location.reload(true)); 38 | } 39 | -------------------------------------------------------------------------------- /src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import todoList from './todoList'; 3 | 4 | const reducer = combineReducers({ 5 | todoList, 6 | }); 7 | 8 | export default reducer; 9 | -------------------------------------------------------------------------------- /src/reducers/todoList.ts: -------------------------------------------------------------------------------- 1 | import { TodoAction } from '../actions'; 2 | import { CHANGE_VALUE, ADD_ITEM, DELETE_ITEM } from '../constants'; 3 | 4 | export interface IDefaultState { 5 | inputValue: string; 6 | list: any[]; 7 | } 8 | 9 | const defaultState: IDefaultState = { 10 | inputValue: '', 11 | list: [], 12 | }; 13 | 14 | // 处理并返回state 15 | 16 | export default function(state = defaultState, action: TodoAction) { 17 | switch (action.type) { 18 | case CHANGE_VALUE: 19 | return { ...state, inputValue: action.value }; 20 | case ADD_ITEM: 21 | return { ...state, list: [...state.list, state.inputValue], inputValue: '' }; 22 | case DELETE_ITEM: 23 | const newState = { ...state }; 24 | newState.list.splice(action.index, 1); 25 | return newState; 26 | default: 27 | return state; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import reducer from '~/reducers'; 4 | 5 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 6 | const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))); 7 | 8 | export default store; 9 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $base-font-size: 14px !default; 2 | 3 | $blue: blue; 4 | 5 | @mixin ellipsis { 6 | overflow: hidden; 7 | text-overflow: ellipsis; 8 | white-space: nowrap; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IStoreState { 2 | inputValue: string; 3 | list: any[]; 4 | } 5 | -------------------------------------------------------------------------------- /theme.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'primary-color': '#2ad295', 3 | }; 4 | -------------------------------------------------------------------------------- /tools/paths.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export const ROOT_DIR = path.resolve(__dirname, '../'); 4 | 5 | export const SRC_DIR = path.resolve(ROOT_DIR, 'src'); 6 | 7 | export const BUILD_DIR = path.resolve(ROOT_DIR, 'build'); 8 | 9 | export const STYLES_DIR = path.resolve(SRC_DIR, './styles'); -------------------------------------------------------------------------------- /tools/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 3 | import TsconfigPathsWebpackPlugin from 'tsconfig-paths-webpack-plugin'; 4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 5 | import UglifyjsPligin from 'uglifyjs-webpack-plugin'; 6 | import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin'; 7 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 8 | // tslint:disable-next-line:no-var-requires 9 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 10 | import * as paths from './paths.config'; 11 | 12 | const isDev = process.argv.includes('--dev'); 13 | 14 | const webpackConfig: webpack.Configuration = { 15 | mode: isDev ? 'development' : 'production', 16 | 17 | devtool: isDev ? 'cheap-module-eval-source-map' : 'cheap-module-source-map', 18 | 19 | entry: { 20 | main: ['react-hot-loader/patch', './src/entries/index.tsx'], 21 | }, 22 | 23 | output: { 24 | path: paths.BUILD_DIR, 25 | filename: isDev ? 'scripts/[name].js' : 'scripts/[name].[hash:8].js', 26 | chunkFilename: isDev ? 'scripts/[name].js' : 'scripts/[name].[hash:8].js', 27 | publicPath: '', 28 | }, 29 | 30 | resolve: { 31 | modules: ['node_modules'], 32 | extensions: ['.js', '.ts', '.tsx'], 33 | alias: { 34 | // Allow absolute paths in imports, e.g. import Button from '~/components/Button' 35 | // Keep in sync with tsconfig.json 36 | '~': paths.SRC_DIR, 37 | 'react-dom': '@hot-loader/react-dom' 38 | }, 39 | // plugins: [ 40 | // new TsconfigPathsWebpackPlugin({ 41 | // configFile: paths.SRC_DIR, 42 | // }), 43 | // ], 44 | }, 45 | 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.(ts|tsx)$/, 50 | use: [ 51 | { 52 | loader: 'cache-loader', 53 | }, 54 | { 55 | loader: 'babel-loader', 56 | options: { 57 | presets: ['@babel/preset-env', '@babel/preset-react'], 58 | babelrc: false, 59 | plugins: ['@babel/plugin-syntax-dynamic-import', 'react-hot-loader/babel'], 60 | }, 61 | }, 62 | { 63 | loader: 'ts-loader', 64 | options: { 65 | compilerOptions: { 66 | module: 'esNext', // for Tree-shaking 67 | sourceMap: isDev, 68 | }, 69 | }, 70 | }, 71 | ], 72 | include: paths.SRC_DIR, 73 | }, 74 | { 75 | test: /\.(css|scss)$/, 76 | use: [ 77 | isDev ? 'style-loader' : 78 | { 79 | loader: MiniCssExtractPlugin.loader, 80 | options: { 81 | publicPath: '../', 82 | }, 83 | }, 84 | 85 | { 86 | loader: 'cache-loader', 87 | }, 88 | 89 | { 90 | loader: 'css-loader', 91 | options: { 92 | sourceMap: isDev ? true : false, // 启用/禁用 Sourcemap 93 | modules:{ 94 | localIdentName: isDev ? '[name]-[local]-[hash:base64:5]' : '[hash:base64:5]', // 配置生成的标识符 95 | }, 96 | importLoaders: 2, // 在css-loader前应用的loader数量 97 | } 98 | }, 99 | { 100 | loader: 'sass-loader', 101 | options: { 102 | sourceMap: isDev ? true : false, // 启用/禁用 Sourcemap 103 | sassOptions: { 104 | includePaths: [paths.STYLES_DIR], 105 | } 106 | } 107 | }, 108 | { 109 | loader: 'postcss-loader', 110 | options: { 111 | plugins: [require('autoprefixer')], 112 | } 113 | } 114 | ] 115 | }, 116 | { 117 | test: /\.(png|jpe?g|gif|svg)$/, 118 | loader: 'url-loader', 119 | options: { 120 | limit: 8192, 121 | name: '[name].[hash:8].[ext]', 122 | outputPath: 'images/', 123 | }, 124 | }, 125 | { 126 | test: /\.(eot|ttf|woff|woff2|svg)$/, 127 | loader: 'file-loader', 128 | options: { 129 | name: '[hash].[ext]', 130 | outputPath: 'fonts/', 131 | }, 132 | }, 133 | { 134 | test: /\.(avi|mp3|mp4|mpg|ogg|wav|wmv)$/, 135 | loader: 'file-loader', 136 | options: { 137 | name: '[hash].[ext]', 138 | outputPath: 'media/', 139 | }, 140 | }, 141 | ] 142 | }, 143 | 144 | plugins: [ 145 | new CleanWebpackPlugin({ 146 | dry: false, // 默认false dry为true时,模拟删除,加删除,不会真的删掉文件 147 | verbose: true, // 默认false verbose为true时 显示日志, 当dry为true时,总是会打印日志,不管verbose是什么值 148 | cleanStaleWebpackAssets: true, // 自动删除未被使用的webpack资源 149 | cleanOnceBeforeBuildPatterns: ['**/*'] // 打包前做的一些事 150 | }), 151 | 152 | new BundleAnalyzerPlugin({ 153 | analyzerMode: 'server', 154 | analyzerHost: '127.0.0.1', 155 | analyzerPort: 8888, 156 | reportFilename: 'report.html', 157 | defaultSizes: 'parsed', 158 | openAnalyzer: false, 159 | generateStatsFile: false, 160 | statsFilename: 'stats.json', 161 | statsOptions: null, 162 | logLevel: 'info' 163 | }), 164 | 165 | new HtmlWebpackPlugin({ 166 | template: './src/entries/index.html', 167 | minify: { 168 | // 是对html文件进行压缩 169 | removeComments: true, 170 | collapseWhitespace: true, 171 | removeAttributeQuotes: true, // 去掉属性的双引号 172 | }, 173 | hash: true, 174 | }), 175 | 176 | new webpack.NamedModulesPlugin(), 177 |     new webpack.HotModuleReplacementPlugin(), 178 | 179 | new MiniCssExtractPlugin({ 180 | filename: 'styles/[name].[contenthash:8].css', 181 | }), 182 | ], 183 | 184 | optimization: { 185 | // 缓存webpack固定生成的代码块,该代码块通常不变。用于维系各个代码块关系的代码。 186 | runtimeChunk: { 187 | name: 'mainfest', 188 | }, 189 | 190 | // 指定node_modules中的第三方代码进行分离 191 | splitChunks: { 192 | cacheGroups: { 193 | default: false, 194 | commons: { 195 | test: /[\\/]node_modules[\\/]/, 196 | name: 'vendor', 197 | chunks: 'all', 198 | }, 199 | }, 200 | }, 201 | 202 | minimizer: [ 203 | new UglifyjsPligin({ 204 | // 文件缓存,当js文件没有变化时就使得缓存 205 | cache: true, 206 | // 采用多线程来加速压缩 207 | parallel: true, 208 | sourceMap: true, 209 | }), 210 | 211 | new OptimizeCssAssetsPlugin(), 212 | ], 213 | }, 214 | 215 | devServer: { 216 | port: 8088, 217 | host: 'localhost', 218 | contentBase: paths.SRC_DIR, 219 | hot: true, 220 | hotOnly: true, 221 | inline: true, 222 | } 223 | 224 | }; 225 | 226 | export default webpackConfig; 227 | 228 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* 编译的目标是什么版本 */ 5 | "module": "commonjs", /* 指定生成哪个模块系统代码 */ 6 | "moduleResolution": "node", /* 指定模块解析方式 */ 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | 11 | "strict": true, /* Enable all strict type-checking options. */ 12 | "noUnusedLocals": false, /* 若有未使用的局部变量则抛错 */ 13 | "noUnusedParameters": false, /* 若有未使用有参数则抛错 */ 14 | 15 | "jsx": "preserve", /* 在.tsx文件里支持jsx */ 16 | "jsxFactory": "React.createElement", /* 指定生成目标为 react JSX时,使用jSX工厂函数 */ 17 | "experimentalDecorators": false, /* 启用实验性的ES装饰器 */ 18 | "emitDecoratorMetadata": false, 19 | 20 | "baseUrl": "./src", 21 | "paths": { 22 | "~/*": ["*"] 23 | }, 24 | 25 | "typeRoots": ["node_modules/@types", "types"] 26 | }, 27 | "exclude": ["node_modules/**"] 28 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react"], 3 | 4 | "rules": { 5 | "arrow-parens": [true, "ban-single-arg-parens"], 6 | "curly": [true, "ignore-same-line"], //for if do while 要有花括号,ignore-same-line对单行实行进行例外处理 正确:if(x>0) doStuff(); 把doStuff()换行失败。 7 | "interface-name": false, //要求接口名称以大写”I“开头 8 | "no-console": [true, "log"], //禁止使用指定的console方法,要禁止的方法名称列表。如果未提供方法名称,则禁止所有控制台方法。 9 | "no-empty": false, //不允许空的块 10 | "no-empty-interface": false, //禁止空接口 {} 11 | "jsx-no-multiline-js": false, 12 | "object-literal-key-quotes": [true, "as-needed"], 13 | "object-literal-sort-keys": false, //检查对象中键的排序 14 | "ordered-imports": false, // 要求将import语句按字母排序排列并进行分组 15 | "prefer-template": true, 16 | "quotemark": [ 17 | true, 18 | "single", 19 | "jsx-double", 20 | "avoid-escape", 21 | "avoid-template" 22 | ], // 引号的使用规则, single强制执行单, jsx-double对JSX属性强制使用双引号, avoid-escape允许在通常需要转义的情况下使用”其他“引号, avoid-template禁止不包含字符串插值的单行未标记模板字符串。 23 | "semicolon": [true, "always", "strict-bound-class-methods"], //分号的使用规则 24 | "jsx-wrap-multiline": false 25 | } 26 | } -------------------------------------------------------------------------------- /typings/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.gif' { 2 |   const exports: string; 3 |   export default exports; 4 | } 5 | 6 | declare module '*.jpg' { 7 |   const exports: string; 8 |   export default exports; 9 | } 10 | 11 | declare module '*.jpeg' { 12 |   const exports: string; 13 |   export default exports; 14 | } 15 | 16 | declare module '*.png' { 17 |   const exports: string; 18 |   export default exports; 19 | } 20 | 21 | declare module '*.svg' { 22 |   const exports: string; 23 |   export default exports; 24 | } -------------------------------------------------------------------------------- /typings/typed-css-modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const content: any; 3 | export = content; 4 | } 5 | --------------------------------------------------------------------------------