├── .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 |
16 | {todos.map((item, index) => (
17 |
18 | ))}
19 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------