├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── jest.config.js ├── jest.mongodb.environment.js ├── jest.mongodb.setup.js ├── jest.mongodb.teardown.js ├── jest.setup.js ├── package.json ├── src ├── App.jsx ├── __tests__ │ ├── CheckboxWithLabelComponent.test.jsx │ ├── FileSummarizer.test.js │ ├── __snapshots__ │ │ ├── jest_snap_property.test.js.snap │ │ └── react_link.test.jsx.snap │ ├── jest_advance_timer_game.test.js │ ├── jest_async_await.test.js │ ├── jest_async_callback.test.js │ ├── jest_async_promise.test.js │ ├── jest_common.test.js │ ├── jest_infinite_timer_game.test.js │ ├── jest_mock_names.test.js │ ├── jest_mock_return_values.test.js │ ├── jest_mongodb.test.js │ ├── jest_run_all_timer.test.js │ ├── jest_setup_describe.js │ ├── jest_setup_describe_diff.js │ ├── jest_setup_describe_order.js │ ├── jest_setup_each_moretime.test.js │ ├── jest_setup_each_onetime.test.js │ ├── jest_setup_test_only.js │ ├── jest_snap_property.test.js │ ├── jest_sound_player.test.js │ ├── jest_sound_player_2.test.js │ ├── jest_sound_player_3.test.js │ ├── jest_sound_player_4.test.js │ ├── jest_timer_game.test.js │ ├── jest_use_mock_function.test.js │ ├── jest_use_mock_function_property.test.js │ ├── react_link.test.jsx │ ├── sum.test.js │ ├── useMatchMedia.test.js │ ├── user.test.js │ ├── user_async.test.js │ ├── user_async_await.test.js │ ├── user_async_await_error.test.js │ └── user_async_await_rejects.test.js ├── actions │ ├── counter.js │ └── index.js ├── components │ ├── AboutComponent.jsx │ ├── AppComponent.jsx │ ├── CheckboxWithLabelComponent.jsx │ ├── CounterComponent.jsx │ ├── HomeComponent.jsx │ ├── Link.react.jsx │ ├── LoadingComponent.jsx │ ├── RoleComponent.jsx │ ├── TopicComponent.jsx │ └── TopicsComponent.jsx ├── css │ ├── CounterComponent.css │ └── main.css ├── index.jsx ├── lib │ ├── FileSummarizer.js │ ├── __mocks__ │ │ ├── fs.js │ │ ├── request.js │ │ └── sound-player.js │ ├── infiniteTimerGame.js │ ├── matchMedia.mock.js │ ├── request.js │ ├── sound-player-consumer.js │ ├── sound-player.js │ ├── sum.js │ ├── task.js │ ├── timerGame.js │ ├── useMatchMedia.js │ └── user.js ├── reducers │ ├── counter.js │ └── index.js └── routes.jsx ├── webpack.common.js ├── webpack.config.js ├── webpack.dev.js └── webpack.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-async-to-generator", 4 | "transform-strict-mode", 5 | "transform-object-assign", 6 | "transform-decorators-legacy", 7 | "react-hot-loader/babel", 8 | "syntax-dynamic-import" 9 | ], 10 | "presets": [ 11 | "react", 12 | "stage-0", 13 | "env" 14 | ] 15 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "jquery": true, 7 | "jest": true 8 | }, 9 | "parser": "babel-eslint", 10 | "plugins": [ 11 | "react", 12 | "html" 13 | ], 14 | "extends": [ 15 | "airbnb" 16 | ], 17 | "rules": { 18 | "no-underscore-dangle": 0, 19 | "import/no-extraneous-dependencies": ["error", { 20 | "devDependencies": true 21 | }] 22 | 23 | }, 24 | "globals": { 25 | "Actions": true 26 | }, 27 | 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | package-lock.json 63 | 64 | dist 65 | globalConfig.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 durban zhang 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 | # webpack4-react16-reactrouter-demo 2 | webpack4-react16-reactrouter-demo 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React + ReactRouter Demo 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['./jest.setup.js'], 3 | snapshotSerializers: ['enzyme-to-json/serializer'], 4 | globalSetup: './jest.mongodb.setup.js', 5 | globalTeardown: './jest.mongodb.teardown.js', 6 | testEnvironment: './jest.mongodb.environment.js', 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /jest.mongodb.environment.js: -------------------------------------------------------------------------------- 1 | const NodeEnvironment = require('jest-environment-node'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | const globalConfigPath = path.join(__dirname, 'globalConfig.json'); 6 | 7 | class MongoEnvironment extends NodeEnvironment { 8 | constructor(config) { 9 | super(config); 10 | } 11 | 12 | async setup() { 13 | console.log('设置MongoDB测试环境'); 14 | 15 | const globalConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf-8')); 16 | 17 | this.global.__MONGO_URI__ = globalConfig.mongoUri; 18 | this.global.__MONGO_DB_NAME__ = globalConfig.mongoDBName; 19 | 20 | await super.setup(); 21 | } 22 | 23 | async teardown() { 24 | console.log('卸载MongoDB测试环境'); 25 | 26 | await super.teardown(); 27 | } 28 | 29 | runScript(script) { 30 | return super.runScript(script); 31 | } 32 | } 33 | 34 | module.exports = MongoEnvironment; 35 | -------------------------------------------------------------------------------- /jest.mongodb.setup.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const MongodbMemoryServer = require('mongodb-memory-server'); 4 | 5 | const globalConfigPath = path.join(__dirname, 'globalConfig.json'); 6 | const mongoServer = new MongodbMemoryServer.MongoMemoryServer(); 7 | 8 | module.exports = async function setupMongodb() { 9 | console.log('配置Jest Setup调用'); 10 | const mongoConfig = { 11 | mongoDBName: 'jest', 12 | mongoUri: await mongoServer.getConnectionString(), 13 | }; 14 | 15 | // 将配置写入本地配置文件以供所有测试都能调用的到 16 | fs.writeFileSync(globalConfigPath, JSON.stringify(mongoConfig)); 17 | 18 | // 设置对mongodb的引用,以便在拆卸期间关闭服务器。 19 | global.__MONGOD__ = mongoServer; 20 | }; 21 | -------------------------------------------------------------------------------- /jest.mongodb.teardown.js: -------------------------------------------------------------------------------- 1 | module.exports = async function tearDownMongodb() { 2 | console.log('配置Jest TearDown调用'); 3 | await global.__MONGOD__.stop(); 4 | }; 5 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ 5 | adapter: new Adapter(), 6 | }); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack4_react16_reactrouter4", 3 | "version": "1.0.0", 4 | "description": "Webpack4 + React16 + ReactRouter4", 5 | "private": true, 6 | "scripts": { 7 | "test": "jest --notify --watchman=false", 8 | "build": "npx webpack --config webpack.prod.js", 9 | "build:package": "NODE_ENV=production npx webpack --config webpack.prod.js", 10 | "start": "npx webpack-dev-server --open --hot --config webpack.dev.js", 11 | "server:run": "npx http-server dist" 12 | }, 13 | "keywords": [], 14 | "author": "durban.zhang ", 15 | "license": "ISC", 16 | "dependencies": { 17 | "axios": "^0.18.0", 18 | "connected-react-router": "^4.3.0", 19 | "enzyme-to-json": "^3.3.4", 20 | "history": "^4.7.2", 21 | "prop-types": "^15.6.1", 22 | "react": "^16.4.0", 23 | "react-dom": "^16.4.0", 24 | "react-hot-loader": "^4.3.0", 25 | "react-loadable": "^5.4.0", 26 | "react-redux": "^5.0.7", 27 | "react-router-dom": "^4.3.1", 28 | "redux": "^4.0.0", 29 | "redux-logger": "^3.0.6" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.26.3", 33 | "babel-eslint": "^8.2.3", 34 | "babel-jest": "^23.2.0", 35 | "babel-loader": "^7.1.4", 36 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 37 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 38 | "babel-plugin-transform-object-assign": "^6.22.0", 39 | "babel-plugin-transform-strict-mode": "^6.24.1", 40 | "babel-preset-env": "^1.7.0", 41 | "babel-preset-react": "^6.24.1", 42 | "babel-preset-stage-0": "^6.24.1", 43 | "clean-webpack-plugin": "^0.1.19", 44 | "css-loader": "^0.28.11", 45 | "enzyme": "^3.3.0", 46 | "enzyme-adapter-react-16": "^1.1.1", 47 | "eslint": "^4.19.1", 48 | "eslint-config-airbnb": "^16.1.0", 49 | "eslint-plugin-html": "^4.0.3", 50 | "eslint-plugin-import": "^2.12.0", 51 | "eslint-plugin-jsx-a11y": "^6.0.3", 52 | "eslint-plugin-react": "^7.9.1", 53 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 54 | "html-webpack-plugin": "^3.2.0", 55 | "inline-manifest-webpack-plugin": "^4.0.0", 56 | "jest": "^23.1.0", 57 | "jest-changed-files": "^23.2.0", 58 | "jest-diff": "^23.2.0", 59 | "jest-docblock": "^23.2.0", 60 | "jest-environment-node": "^23.4.0", 61 | "jest-get-type": "^22.4.3", 62 | "jest-validate": "^23.2.0", 63 | "jest-worker": "^23.2.0", 64 | "mongodb": "^3.1.1", 65 | "mongodb-memory-server": "^1.9.0", 66 | "pretty-format": "^23.2.0", 67 | "react-test-renderer": "^16.4.1", 68 | "regenerator-runtime": "^0.12.0", 69 | "style-loader": "^0.21.0", 70 | "uglifyjs-webpack-plugin": "^1.2.5", 71 | "url-loader": "^1.0.1", 72 | "webpack": "^4.11.1", 73 | "webpack-bundle-analyzer": "^3.6.1", 74 | "webpack-cli": "^3.0.3", 75 | "webpack-dev-server": "^3.1.4", 76 | "webpack-manifest-plugin": "^2.0.3", 77 | "webpack-merge": "^4.1.2", 78 | "workbox-webpack-plugin": "^3.3.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ConnectedRouter } from 'connected-react-router'; 4 | import routes from './routes'; 5 | 6 | const App = ({ history }) => ( 7 | 8 | {routes} 9 | 10 | ); 11 | 12 | App.propTypes = { 13 | history: PropTypes.objectOf(PropTypes.any).isRequired, 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/__tests__/CheckboxWithLabelComponent.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import CheckboxWithLabelComponent from '../components/CheckboxWithLabelComponent'; 4 | 5 | test('CheckboxWithLabelComponent changes the text after click', () => { 6 | // Render a checkbox with label in the document 7 | const checkbox = shallow(); 8 | 9 | expect(checkbox.text()).toEqual('Off'); 10 | 11 | checkbox.find('input').simulate('change'); 12 | 13 | expect(checkbox.text()).toEqual('On'); 14 | }); 15 | -------------------------------------------------------------------------------- /src/__tests__/FileSummarizer.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const FileSummarizer = require('../lib/FileSummarizer'); 3 | 4 | 5 | jest.mock('fs'); 6 | 7 | describe('listFilesInDirectorySync', () => { 8 | const MOCK_FILE_INFO = { 9 | '/path/to/file1.js': 'console.log("file1 contents");', 10 | '/path/to/file2.txt': 'file2 contents', 11 | }; 12 | 13 | beforeEach(() => { 14 | // Set up some mocked out file info before each test 15 | fs.__setMockFiles(MOCK_FILE_INFO); 16 | }); 17 | 18 | test('includes all files in the directory in the summary', () => { 19 | const fileSummary = FileSummarizer.summarizeFilesInDirectorySync('/path/to'); 20 | 21 | expect(fileSummary.length).toBe(2); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/jest_snap_property.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`检查匹配器并测试通过 1`] = ` 4 | Object { 5 | "createAt": Any, 6 | "id": Any, 7 | "name": "Durban", 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/react_link.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`正确的渲染 1`] = ` 4 | 10 | Walkerfree 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/__tests__/jest_advance_timer_game.test.js: -------------------------------------------------------------------------------- 1 | const timerGame = require('../lib/timerGame'); 2 | 3 | jest.useFakeTimers(); 4 | 5 | it('1秒钟后通过advanceTimersByTime调用回调函数', () => { 6 | const callback = jest.fn(); 7 | 8 | timerGame(callback); 9 | 10 | // callback还没有被执行 11 | expect(callback).not.toBeCalled(); 12 | 13 | // 提前1秒钟执行 14 | jest.advanceTimersByTime(1000); 15 | 16 | // 所有的callback被调用 17 | expect(callback).toBeCalled(); 18 | expect(callback).toHaveBeenCalledTimes(1); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__tests__/jest_async_await.test.js: -------------------------------------------------------------------------------- 1 | const fetchData = (err) => { 2 | const promise = new Promise((resolve, reject) => { 3 | if (err) { 4 | return reject('error'); 5 | } 6 | 7 | return setTimeout(() => resolve('gowhich'), 3000); 8 | }); 9 | return promise; 10 | }; 11 | 12 | 13 | test('data的数据是"gowhich"', async () => { 14 | const data = await fetchData(); 15 | expect(data).toBe('gowhich'); 16 | }); 17 | 18 | test('获取数据失败', async () => { 19 | try { 20 | await fetchData(true); 21 | } catch (err) { 22 | expect(err).toBe('error'); 23 | } 24 | }); 25 | 26 | test('data的数据是"gowhich"', async () => { 27 | await expect(fetchData()).resolves.toBe('gowhich'); 28 | }); 29 | 30 | test('获取数据失败', async () => { 31 | await expect(fetchData(true)).rejects.toMatch('error'); 32 | }); 33 | -------------------------------------------------------------------------------- /src/__tests__/jest_async_callback.test.js: -------------------------------------------------------------------------------- 1 | function fetchData(callback) { 2 | setTimeout(() => { 3 | callback('peanut butter'); 4 | }, 1000); 5 | } 6 | 7 | test('two section the data is peanut butter', (done) => { 8 | function callback(data) { 9 | // console.log('two section data = ', data); 10 | expect(data).toBe('peanut butter'); 11 | done(); 12 | } 13 | 14 | fetchData(callback); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__tests__/jest_async_promise.test.js: -------------------------------------------------------------------------------- 1 | const fetchData = (err) => { 2 | const promise = new Promise((resolve, reject) => { 3 | if (err) { 4 | return reject('error'); 5 | } 6 | 7 | return setTimeout(() => resolve('peanut butter'), 3000); 8 | }); 9 | return promise; 10 | }; 11 | 12 | test('the data is peanut butter', () => { 13 | const promise = fetchData().then((data) => { 14 | // console.log('third section data = ', data); 15 | expect(data).toBe('peanut butter'); 16 | }); 17 | 18 | return promise; 19 | }); 20 | 21 | test('the fetch fails with an error', () => { 22 | expect.assertions(1); 23 | const promise = fetchData(true).catch((err) => { 24 | expect(err).toMatch('error'); 25 | }); 26 | 27 | return promise; 28 | }); 29 | 30 | test('the data is peanut butter', () => { 31 | const promise = expect(fetchData()).resolves.toBe('peanut butter'); 32 | return promise; 33 | }); 34 | 35 | test('the fetch fails with an error', () => { 36 | const promise = expect(fetchData(true)).rejects.toMatch('error'); 37 | return promise; 38 | }); 39 | -------------------------------------------------------------------------------- /src/__tests__/jest_common.test.js: -------------------------------------------------------------------------------- 1 | test('two plus two is four', () => { 2 | expect(2 + 2).toBe(4); 3 | }); 4 | 5 | test('object assignment', () => { 6 | const data = { one: 1 }; 7 | data.two = 2; 8 | expect(data).toEqual({ one: 1, two: 2 }); 9 | }); 10 | 11 | test('adding positive numbers is not zero', () => { 12 | for (let a = 1; a < 10; a += 1) { 13 | for (let b = 1; b < 10; b += 1) { 14 | expect(a + b).not.toBe(0); 15 | } 16 | } 17 | }); 18 | 19 | test('null', () => { 20 | const n = null; 21 | expect(n).toBeNull(); 22 | expect(n).toBeDefined(); 23 | expect(n).not.toBeUndefined(); 24 | expect(n).not.toBeTruthy(); 25 | expect(n).toBeFalsy(); 26 | }); 27 | 28 | test('zero', () => { 29 | const z = 0; 30 | expect(z).not.toBeNull(); 31 | expect(z).toBeDefined(); 32 | expect(z).not.toBeUndefined(); 33 | expect(z).not.toBeTruthy(); 34 | expect(z).toBeFalsy(); 35 | }); 36 | 37 | test('two plus tow', () => { 38 | const value = 2 + 2; 39 | expect(value).toBeGreaterThan(3); 40 | expect(value).toBeGreaterThanOrEqual(3.5); 41 | expect(value).toBeLessThan(5); 42 | expect(value).toBeLessThanOrEqual(4.5); 43 | }); 44 | 45 | test('adding floating point numbers', () => { 46 | const value = 0.1 + 0.2; 47 | expect(value).toBeCloseTo(0.3); 48 | }); 49 | 50 | test('there is no I in team', () => { 51 | expect('team').not.toMatch(/I/); 52 | }); 53 | 54 | test('but there is a "stop" in Christoph', () => { 55 | expect('Christoph').toMatch(/stop/); 56 | }); 57 | 58 | test('the shopping list has beer on it', () => { 59 | const shopping = [ 60 | 'diapers', 61 | 'kleenex', 62 | 'trash bags', 63 | 'paper towels', 64 | 'beer', 65 | ]; 66 | 67 | expect(shopping).toContain('beer'); 68 | }); 69 | 70 | test('compiling android goes as expected', () => { 71 | function compileAndroidCode() { 72 | throw new Error('you are useing the wrong JDK'); 73 | } 74 | 75 | expect(compileAndroidCode).toThrow(); 76 | expect(compileAndroidCode).toThrow(Error); 77 | 78 | // 匹配错误信息 79 | expect(compileAndroidCode).toThrow('you are useing the wrong JDK'); 80 | expect(compileAndroidCode).toThrow(/JDK/); 81 | }); 82 | -------------------------------------------------------------------------------- /src/__tests__/jest_infinite_timer_game.test.js: -------------------------------------------------------------------------------- 1 | const infiniteTimerGame = require('../lib/infiniteTimerGame'); 2 | 3 | jest.useFakeTimers(); 4 | 5 | describe('infiniteTimerGame', () => { 6 | test('schedules a 10-second timer after 1 second', () => { 7 | const callback = jest.fn(); 8 | 9 | infiniteTimerGame(callback); 10 | 11 | // 在这里,会在意秒钟后执行callback的回调 12 | expect(setTimeout).toHaveBeenCalledTimes(1); 13 | expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); 14 | 15 | // 只有当前待定的计时器(但不是在该过程中创建的任何新计时器) 16 | jest.runOnlyPendingTimers(); 17 | 18 | // 此时,1秒钟的计时器应该已经被回调了 19 | expect(callback).toBeCalled(); 20 | 21 | // 它应该创建一个新的计时器,以便在10秒内启动游戏 22 | expect(setTimeout).toHaveBeenCalledTimes(2); 23 | expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/__tests__/jest_mock_names.test.js: -------------------------------------------------------------------------------- 1 | const myMockFunc = jest 2 | .fn() 3 | .mockReturnValue('default') 4 | .mockImplementation(v => 42 + v) 5 | .mockName('add42'); 6 | 7 | test('add 42', () => { 8 | expect(myMockFunc(1)).toEqual(43); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/jest_mock_return_values.test.js: -------------------------------------------------------------------------------- 1 | const myMock = jest.fn(); 2 | console.log(myMock); 3 | 4 | myMock 5 | .mockReturnValueOnce(10) 6 | .mockReturnValueOnce('x') 7 | .mockReturnValue(true); 8 | 9 | console.log(myMock(), myMock(), myMock(), myMock(), myMock()); 10 | 11 | 12 | const filterTestFn = jest.fn(); 13 | 14 | // 第一次mock返回true,第二次mock返回false 15 | filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false); 16 | 17 | const result = [11, 12].filter(filterTestFn); 18 | 19 | console.log(result); 20 | // > [11] 21 | console.log(filterTestFn.mock.calls); 22 | // > [ [11], [12] ] 23 | 24 | test('do test', () => { 25 | expect(true).toBe(true); 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /src/__tests__/jest_mongodb.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | MongoClient, 3 | } = require('mongodb'); 4 | 5 | let connection; 6 | let db; 7 | 8 | beforeAll(async () => { 9 | connection = await MongoClient.connect(global.__MONGO_URI__, { 10 | useNewUrlParser: true, 11 | }); 12 | db = await connection.db(global.__MONGO_DB_NAME__); 13 | }); 14 | 15 | afterAll(async () => { 16 | await connection.close(); 17 | }); 18 | 19 | it('从集合中汇总文档', async () => { 20 | const files = db.collection('files'); 21 | 22 | await files.insertMany([{ 23 | type: 'Document', 24 | }, 25 | { 26 | type: 'Video', 27 | }, 28 | { 29 | type: 'Image', 30 | }, 31 | { 32 | type: 'Document', 33 | }, 34 | { 35 | type: 'Image', 36 | }, 37 | { 38 | type: 'Document', 39 | }, 40 | ]); 41 | 42 | const topFiles = await files 43 | .aggregate([{ 44 | $group: { 45 | _id: '$type', 46 | count: { 47 | $sum: 1, 48 | }, 49 | }, 50 | }, 51 | { 52 | $sort: { 53 | count: -1, 54 | }, 55 | }, 56 | ]) 57 | .toArray(); 58 | 59 | expect(topFiles).toEqual([{ 60 | _id: 'Document', 61 | count: 3, 62 | }, 63 | { 64 | _id: 'Image', 65 | count: 2, 66 | }, 67 | { 68 | _id: 'Video', 69 | count: 1, 70 | }, 71 | ]); 72 | }); 73 | -------------------------------------------------------------------------------- /src/__tests__/jest_run_all_timer.test.js: -------------------------------------------------------------------------------- 1 | const timerGame = require('../lib/timerGame'); 2 | 3 | jest.useFakeTimers(); 4 | 5 | test('1分钟后调用回调callback', () => { 6 | const callback = jest.fn(); 7 | 8 | timerGame(callback); 9 | 10 | // 在这个时间点上,callback回调函数还没有被调用 11 | expect(callback).not.toBeCalled(); 12 | 13 | // 所有timers被执行 14 | jest.runAllTimers(); 15 | 16 | // 现在我们的callback回调函数被调用 17 | expect(callback).toBeCalled(); 18 | expect(callback).toHaveBeenCalledTimes(1); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__tests__/jest_setup_describe.js: -------------------------------------------------------------------------------- 1 | const citys = []; 2 | const foods = []; 3 | let time1 = 1; 4 | let time2 = 1; 5 | const isCity = (city) => { 6 | if (citys.indexOf(city) > -1) { 7 | return true; 8 | } 9 | return false; 10 | }; 11 | 12 | const isCityAndFood = (cityAndFood) => { 13 | let hasCity = false; 14 | let hasFood = false; 15 | 16 | if (citys.indexOf(cityAndFood.city) > -1) { 17 | hasCity = true; 18 | } 19 | 20 | if (foods.indexOf(cityAndFood.food) > -1) { 21 | hasFood = true; 22 | } 23 | 24 | if (hasCity && hasFood) { 25 | return true; 26 | } 27 | 28 | return false; 29 | }; 30 | const initCityDatabase = () => new Promise((resolve, reject) => { 31 | let promise; 32 | 33 | try { 34 | setTimeout(() => { 35 | // console.log('initCityDatabase time = ', time1); 36 | if (time1 === 1) { 37 | citys.push('Shanghai'); 38 | } else if (time1 === 2) { 39 | citys.push('Chifeng'); 40 | } 41 | time1 += 1; 42 | promise = resolve(citys); 43 | }, 1000); 44 | } catch (err) { 45 | return reject(err); 46 | } 47 | 48 | return promise; 49 | }); 50 | 51 | const initFoodDatabase = () => new Promise((resolve, reject) => { 52 | let promise; 53 | 54 | try { 55 | setTimeout(() => { 56 | // console.log('initFoodDatabase time = ', time2); 57 | if (time2 === 1) { 58 | foods.push('Banana'); 59 | } else if (time2 === 2) { 60 | foods.push('Apple'); 61 | } 62 | time2 += 1; 63 | promise = resolve(foods); 64 | }, 1000); 65 | } catch (err) { 66 | return reject(err); 67 | } 68 | 69 | return promise; 70 | }); 71 | 72 | beforeEach(() => initCityDatabase()); 73 | 74 | test('city database has Shanghai', () => { 75 | expect(isCity('Shanghai')).toBeTruthy(); 76 | }); 77 | 78 | test('city database has Chifeng', () => { 79 | expect(isCity('Chifeng')).toBeTruthy(); 80 | }); 81 | 82 | describe('matching cities to foods', () => { 83 | beforeEach(() => initFoodDatabase()); 84 | 85 | test('database has Shanghai and Banana', () => { 86 | expect(isCityAndFood({ 87 | city: 'Shanghai', 88 | food: 'Banana', 89 | })).toBe(true); 90 | }); 91 | 92 | test('database has Chifeng and Apple', () => { 93 | expect(isCityAndFood({ 94 | city: 'Chifeng', 95 | food: 'Apple', 96 | })).toBe(true); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/__tests__/jest_setup_describe_diff.js: -------------------------------------------------------------------------------- 1 | beforeAll(() => console.log('1 - beforeAll')); 2 | afterAll(() => console.log('1 - afterAll')); 3 | beforeEach(() => console.log('1 - beforeEach')); 4 | afterEach(() => console.log('1 - afterEach')); 5 | test('', () => console.log('1 - test')); 6 | describe('Scoped / Nested block', () => { 7 | beforeAll(() => console.log('2 - beforeAll')); 8 | afterAll(() => console.log('2 - afterAll')); 9 | beforeEach(() => console.log('2 - beforeEach')); 10 | afterEach(() => console.log('2 - afterEach')); 11 | test('', () => console.log('2 - test')); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/jest_setup_describe_order.js: -------------------------------------------------------------------------------- 1 | describe('outer', () => { 2 | console.log('describe outer-a'); 3 | 4 | describe('describe inner 1', () => { 5 | console.log('describe inner 1'); 6 | test('test 1', () => { 7 | console.log('test for describe inner 1'); 8 | expect(true).toEqual(true); 9 | }); 10 | }); 11 | 12 | console.log('describe outer-b'); 13 | 14 | test('test 1', () => { 15 | console.log('test for describe outer'); 16 | expect(true).toEqual(true); 17 | }); 18 | 19 | describe('describe inner 2', () => { 20 | console.log('describe inner 2'); 21 | test('test for describe inner 2', () => { 22 | console.log('test for describe inner 2'); 23 | expect(false).toEqual(false); 24 | }); 25 | }); 26 | 27 | console.log('describe outer-c'); 28 | }); 29 | -------------------------------------------------------------------------------- /src/__tests__/jest_setup_each_moretime.test.js: -------------------------------------------------------------------------------- 1 | let citys = []; 2 | let time1 = 1; 3 | let time2 = 1; 4 | const isCity = (city) => { 5 | if (citys.indexOf(city) > -1) { 6 | return true; 7 | } 8 | return false; 9 | }; 10 | 11 | const initCityDatabase = () => new Promise((resolve, reject) => { 12 | let promise; 13 | 14 | try { 15 | setTimeout(() => { 16 | // console.log('moretime -> init time = ', time1); 17 | if (time1 === 1) { 18 | citys.push('natasha1'); 19 | time1 += 1; 20 | } else if (time1 === 2) { 21 | citys.push('natasha2'); 22 | time1 += 1; 23 | } 24 | promise = resolve(citys); 25 | }, 1000); 26 | } catch (err) { 27 | return reject(err); 28 | } 29 | 30 | return promise; 31 | }); 32 | 33 | const clearCityDatabase = () => new Promise((resolve, reject) => { 34 | let promise; 35 | try { 36 | setTimeout(() => { 37 | // console.log('moretime -> clear time = ', time2); 38 | time2 += 1; 39 | citys = []; 40 | promise = resolve(citys); 41 | }, 1000); 42 | } catch (err) { 43 | return reject(err); 44 | } 45 | 46 | return promise; 47 | }); 48 | 49 | beforeEach(() => initCityDatabase()); 50 | 51 | afterEach(() => clearCityDatabase()); 52 | 53 | test('The city database has natasha1', () => { 54 | expect(isCity('natasha1')).toBeTruthy(); 55 | }); 56 | 57 | test('The city database has natasha2', () => { 58 | expect(isCity('natasha2')).toBeTruthy(); 59 | }); 60 | -------------------------------------------------------------------------------- /src/__tests__/jest_setup_each_onetime.test.js: -------------------------------------------------------------------------------- 1 | let citys = []; 2 | let time1 = 1; 3 | let time2 = 1; 4 | const isCity = (city) => { 5 | if (citys.indexOf(city) > -1) { 6 | return true; 7 | } 8 | return false; 9 | }; 10 | 11 | const initCityDatabase = () => new Promise((resolve, reject) => { 12 | let promise; 13 | 14 | try { 15 | setTimeout(() => { 16 | // console.log('onetime -> init time = ', time1); 17 | time1 += 1; 18 | citys.push('natasha1'); 19 | citys.push('natasha2'); 20 | promise = resolve(citys); 21 | }, 1000); 22 | } catch (err) { 23 | return reject(err); 24 | } 25 | 26 | return promise; 27 | }); 28 | 29 | const clearCityDatabase = () => new Promise((resolve, reject) => { 30 | let promise; 31 | try { 32 | // console.log('onetime -> clear time = ', time2); 33 | time2 += 1; 34 | setTimeout(() => { 35 | citys = []; 36 | promise = resolve(citys); 37 | }, 1000); 38 | } catch (err) { 39 | return reject(err); 40 | } 41 | 42 | return promise; 43 | }); 44 | 45 | beforeAll(() => initCityDatabase()); 46 | 47 | afterAll(() => clearCityDatabase()); 48 | 49 | test('The city database has natasha1', () => { 50 | expect(isCity('natasha1')).toBeTruthy(); 51 | }); 52 | 53 | test('The city database has natasha2', () => { 54 | expect(isCity('natasha2')).toBeTruthy(); 55 | }); 56 | -------------------------------------------------------------------------------- /src/__tests__/jest_setup_test_only.js: -------------------------------------------------------------------------------- 1 | test.only('this will be the only test that runs', () => { 2 | expect(true).toBe(true); 3 | }); 4 | 5 | test('this test will not run', () => { 6 | expect('A').toBe('A'); 7 | }); 8 | -------------------------------------------------------------------------------- /src/__tests__/jest_snap_property.test.js: -------------------------------------------------------------------------------- 1 | it('检查匹配器并测试通过', () => { 2 | const user = { 3 | createAt: new Date(), 4 | id: Math.floor(Math.random() * 20), 5 | name: 'Durban', 6 | }; 7 | 8 | expect(user).toMatchSnapshot({ 9 | createAt: expect.any(Date), 10 | id: expect.any(Number), 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/jest_sound_player.test.js: -------------------------------------------------------------------------------- 1 | import SoundPlayer from '../lib/sound-player'; 2 | import SoundPlayerConsumer from '../lib/sound-player-consumer'; 3 | 4 | jest.mock('../lib/sound-player'); // SoundPlayer 现在是一个模拟构造函数 5 | 6 | beforeEach(() => { 7 | // 清除所有实例并调用构造函数和所有方法: 8 | SoundPlayer.mockClear(); 9 | }); 10 | 11 | it('我们可以检查SoundPlayerConsumer是否调用了类构造函数', () => { 12 | const soundPlayerConsumer = new SoundPlayerConsumer(); 13 | expect(SoundPlayer).toHaveBeenCalledTimes(1); 14 | }); 15 | 16 | it('我们可以检查SoundPlayerConsumer是否在类实例上调用了一个方法', () => { 17 | // 检查 mockClear() 会否起作用: 18 | expect(SoundPlayer).not.toHaveBeenCalled(); 19 | 20 | const soundPlayerConsumer = new SoundPlayerConsumer(); 21 | // 类构造函数再次被调用 22 | expect(SoundPlayer).toHaveBeenCalledTimes(1); 23 | 24 | const coolSoundFileName = 'song.mp3'; 25 | soundPlayerConsumer.play(); 26 | 27 | // mock.instances可用于自动模拟 28 | const mockSoundPlayerInstance = SoundPlayer.mock.instances[0]; 29 | const mockChoicePlaySoundFile = mockSoundPlayerInstance.choicePlaySoundFile; 30 | expect(mockChoicePlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName); 31 | // 相当于上面的检查 32 | expect(mockChoicePlaySoundFile).toHaveBeenCalledWith(coolSoundFileName); 33 | expect(mockChoicePlaySoundFile).toHaveBeenCalledTimes(1); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/jest_sound_player_2.test.js: -------------------------------------------------------------------------------- 1 | import SoundPlayer, { mockChoicePlaySoundFile } from '../lib/sound-player'; 2 | import SoundPlayerConsumer from '../lib/sound-player-consumer'; 3 | 4 | jest.mock('../lib/sound-player'); // SoundPlayer 现在是一个模拟构造函数 5 | 6 | beforeEach(() => { 7 | // 清除所有实例并调用构造函数和所有方法: 8 | SoundPlayer.mockClear(); 9 | mockChoicePlaySoundFile.mockClear(); 10 | }); 11 | 12 | it('我们可以检查SoundPlayerConsumer是否调用了类构造函数', () => { 13 | const soundPlayerConsumer = new SoundPlayerConsumer(); 14 | expect(SoundPlayer).toHaveBeenCalledTimes(1); 15 | }); 16 | 17 | it('我们可以检查SoundPlayerConsumer是否在类实例上调用了一个方法', () => { 18 | const soundPlayerConsumer = new SoundPlayerConsumer(); 19 | const coolSoundFileName = 'song.mp3'; 20 | soundPlayerConsumer.play(); 21 | expect(mockChoicePlaySoundFile).toHaveBeenCalledWith(coolSoundFileName); 22 | }); 23 | -------------------------------------------------------------------------------- /src/__tests__/jest_sound_player_3.test.js: -------------------------------------------------------------------------------- 1 | import SoundPlayer from '../lib/sound-player'; 2 | import SoundPlayerConsumer from '../lib/sound-player-consumer'; 3 | 4 | jest.mock('../lib/sound-player'); // SoundPlayer 现在是一个模拟构造函数 5 | 6 | 7 | const mockPlaySoundFile = jest.fn(); 8 | const mockChoicePlaySoundFile = jest.fn(); 9 | 10 | jest.mock('../lib/sound-player', () => jest.fn().mockImplementation(() => ({ 11 | choicePlaySoundFile: mockChoicePlaySoundFile, 12 | playSoundFile: mockPlaySoundFile, 13 | }))); 14 | 15 | beforeEach(() => { 16 | // 清除所有实例并调用构造函数和所有方法: 17 | SoundPlayer.mockClear(); 18 | mockChoicePlaySoundFile.mockClear(); 19 | }); 20 | 21 | it('我们可以检查SoundPlayerConsumer是否调用了类构造函数', () => { 22 | const soundPlayerConsumer = new SoundPlayerConsumer(); 23 | expect(SoundPlayer).toHaveBeenCalledTimes(1); 24 | }); 25 | 26 | it('我们可以检查SoundPlayerConsumer是否在类实例上调用了一个方法', () => { 27 | const soundPlayerConsumer = new SoundPlayerConsumer(); 28 | const coolSoundFileName = 'song.mp3'; 29 | soundPlayerConsumer.play(); 30 | expect(mockChoicePlaySoundFile).toHaveBeenCalledWith(coolSoundFileName); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/jest_sound_player_4.test.js: -------------------------------------------------------------------------------- 1 | import SoundPlayer from '../lib/sound-player'; 2 | import SoundPlayerConsumer from '../lib/sound-player-consumer'; 3 | 4 | jest.mock('../lib/sound-player'); // SoundPlayer 现在是一个模拟构造函数 5 | 6 | describe('SoundPlayer被调用的时候抛出异常', () => { 7 | beforeAll(() => { 8 | SoundPlayer.mockImplementation(() => ({ 9 | playSoundFile: () => { 10 | throw new Error('Test error'); 11 | }, 12 | choicePlaySoundFile: () => { 13 | throw new Error('Test error'); 14 | }, 15 | })); 16 | }); 17 | 18 | it('play被调用的收抛出异常', () => { 19 | const soundPlayerConsumer = new SoundPlayerConsumer(); 20 | expect(() => soundPlayerConsumer.play()).toThrow(); 21 | }); 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /src/__tests__/jest_timer_game.test.js: -------------------------------------------------------------------------------- 1 | const timerGame = require('../lib/timerGame'); 2 | 3 | jest.useFakeTimers(); 4 | 5 | test('等待1分钟后结束游戏', () => { 6 | timerGame(); 7 | 8 | expect(setTimeout).toHaveBeenCalledTimes(1); 9 | expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/jest_use_mock_function.test.js: -------------------------------------------------------------------------------- 1 | function forEach(items, callback) { 2 | for (let i = 0; i < items.length; i += 1) { 3 | callback(items[i]); 4 | } 5 | } 6 | 7 | const mockFunction = jest.fn(); 8 | forEach([0, 1], mockFunction); 9 | 10 | 11 | test('mockFunction被调用2次', () => { 12 | expect(mockFunction.mock.calls.length).toBe(2); 13 | }); 14 | 15 | test('第一次调用第一个参数是0', () => { 16 | expect(mockFunction.mock.calls[0][0]).toBe(0); 17 | }); 18 | 19 | test('第一次调用第一个参数是1', () => { 20 | expect(mockFunction.mock.calls[1][0]).toBe(1); 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /src/__tests__/jest_use_mock_function_property.test.js: -------------------------------------------------------------------------------- 1 | const myMock = jest.fn(); 2 | 3 | const a = new myMock(); 4 | const b = {}; 5 | const bound = myMock.bind(b); 6 | bound(); 7 | 8 | console.log(myMock.mock.instances); 9 | 10 | test('do test', () => { 11 | expect(true).toBe(true); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/react_link.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import ALink from '../components/Link.react'; 4 | 5 | it('正确的渲染', () => { 6 | const tree = renderer 7 | .create(Walkerfree) 8 | .toJSON(); expect(tree).toMatchSnapshot(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/sum.test.js: -------------------------------------------------------------------------------- 1 | const sum = require('../lib/sum'); 2 | 3 | test('adds 1 + 2 to equal 3', () => { 4 | expect(sum(1, 2)).toBe(3); 5 | }); 6 | -------------------------------------------------------------------------------- /src/__tests__/useMatchMedia.test.js: -------------------------------------------------------------------------------- 1 | // import '../lib/matchMedia.mock'; 2 | import { useMatchMedia } from '../lib/useMatchMedia'; 3 | 4 | describe('useMatchMedia()', () => { 5 | it('useMatchMedia() 被调用', () => { 6 | const res = useMatchMedia(); 7 | expect(res).toBeUndefined(); 8 | // expect(res).toBeDefined(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/user.test.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Users from '../lib/user'; 3 | 4 | jest.mock('axios'); 5 | 6 | test('should fetch users', () => { 7 | const resp = { 8 | data: [ 9 | { 10 | name: 'Durban', 11 | }, 12 | ], 13 | }; 14 | 15 | axios.get.mockResolvedValue(resp); 16 | // 或者也可以使用下面的代码 17 | // axios.get.mockImplementation(() => Promise.resolve(resp)); 18 | 19 | 20 | return Users.all().then(users => expect(users).toEqual(resp.data)); 21 | }); 22 | -------------------------------------------------------------------------------- /src/__tests__/user_async.test.js: -------------------------------------------------------------------------------- 1 | import Users from '../lib/user'; 2 | 3 | jest.mock('../lib/request'); 4 | 5 | // The assertion for a promise must be returned. 6 | it('works with promises', () => 7 | // expect.assertions(1); 8 | Users 9 | .getUserName(4) 10 | .then(data => expect(data).toEqual('Mark'))); 11 | 12 | it('works with resolves', () => { 13 | expect.assertions(1); 14 | return expect(Users.getUserName(5)).resolves.toEqual('Paul'); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__tests__/user_async_await.test.js: -------------------------------------------------------------------------------- 1 | import Users from '../lib/user'; 2 | 3 | jest.mock('../lib/request'); 4 | 5 | it('works with async/await', async () => { 6 | const data = await Users.getUserName(4); 7 | expect(data).toEqual('Mark'); 8 | }); 9 | 10 | it('works with async/await and resolves', async () => { 11 | expect.assertions(1); 12 | await expect(Users.getUserName(5)).resolves.toEqual('Paul'); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/user_async_await_error.test.js: -------------------------------------------------------------------------------- 1 | import Users from '../lib/user'; 2 | 3 | jest.mock('../lib/request'); 4 | 5 | test('tests error with promises', async () => { 6 | expect.assertions(1); 7 | return Users.getUserName(2).catch(e => 8 | expect(e).toEqual({ 9 | error: 'User with 2 not found.', 10 | })); 11 | }); 12 | 13 | it('tests error with async/await', async () => { 14 | expect.assertions(1); 15 | try { 16 | await Users.getUserName(1); 17 | } catch (e) { 18 | expect(e).toEqual({ 19 | error: 'User with 1 not found.', 20 | }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/__tests__/user_async_await_rejects.test.js: -------------------------------------------------------------------------------- 1 | import Users from '../lib/user'; 2 | 3 | jest.mock('../lib/request'); 4 | it('tests error with rejects', () => { 5 | expect.assertions(1); 6 | return expect(Users.getUserName(3)).rejects.toEqual({ 7 | error: 'User with 3 not found.', 8 | }); 9 | }); 10 | 11 | it('tests error with async/await and rejects', async () => { 12 | expect.assertions(1); 13 | await expect(Users.getUserName(3)).rejects.toEqual({ 14 | error: 'User with 3 not found.', 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/actions/counter.js: -------------------------------------------------------------------------------- 1 | const increment = () => ({ 2 | type: 'INCREMENT', 3 | }); 4 | 5 | const decrement = () => ({ 6 | type: 'DECREMENT', 7 | }); 8 | 9 | export default { 10 | increment, 11 | decrement, 12 | }; 13 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import counter from './counter'; 2 | 3 | const actions = { 4 | counter, 5 | }; 6 | 7 | export default actions; 8 | export { counter }; 9 | -------------------------------------------------------------------------------- /src/components/AboutComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | import { Route, Link as ALink } from 'react-router-dom'; 4 | import RoleComponent from './RoleComponent'; 5 | 6 | class AboutComponent extends React.Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | 10 | this.state = {}; 11 | } 12 | 13 | render() { 14 | const { 15 | match, 16 | } = this.props; 17 | 18 | return ( 19 |
20 |

About

21 | 22 |
    23 |
  • 24 | 25 |
  • 26 |
  • 27 | 28 |
  • 29 |
  • 30 | 31 |
  • 32 |
33 | 34 | 35 |

选择一个角色.

} 39 | /> 40 |
41 | ); 42 | } 43 | } 44 | 45 | AboutComponent.propTypes = { 46 | match: PropTypes.objectOf(PropTypes.any).isRequired, 47 | }; 48 | 49 | export default AboutComponent; 50 | -------------------------------------------------------------------------------- /src/components/AppComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class AppComponent extends React.Component { 5 | constructor(props, context) { 6 | super(props, context); 7 | 8 | this.state = {}; 9 | } 10 | 11 | render() { 12 | return ( 13 |
{this.props.children}
14 | ); 15 | } 16 | } 17 | 18 | AppComponent.propTypes = { 19 | children: PropTypes.node.isRequired, 20 | }; 21 | 22 | export default AppComponent; 23 | -------------------------------------------------------------------------------- /src/components/CheckboxWithLabelComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class CheckboxWithLabelComponent extends React.Component { 5 | constructor(props, context) { 6 | super(props, context); 7 | 8 | this.state = { 9 | isChecked: false, 10 | }; 11 | 12 | this.onChange = this.onChange.bind(this); 13 | } 14 | 15 | onChange() { 16 | this.setState({ 17 | isChecked: !this.state.isChecked, 18 | }); 19 | } 20 | 21 | render() { 22 | return ( 23 | 31 | ); 32 | } 33 | } 34 | 35 | CheckboxWithLabelComponent.propTypes = { 36 | labelOn: PropTypes.string.isRequired, 37 | labelOff: PropTypes.string.isRequired, 38 | }; 39 | 40 | export default CheckboxWithLabelComponent; 41 | -------------------------------------------------------------------------------- /src/components/CounterComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { counter } from 'Actions'; 5 | 6 | const { 7 | increment, decrement, 8 | } = counter; 9 | 10 | class CounterComponent extends Component { 11 | constructor(props, context) { 12 | super(props, context); 13 | 14 | this.state = {}; 15 | } 16 | 17 | render() { 18 | const { 19 | count, 20 | } = this.props; 21 | 22 | return ( 23 |
24 |

数值【Look hot reloader】: {count}

25 | 26 | 27 |
28 | ); 29 | } 30 | } 31 | 32 | CounterComponent.propTypes = { 33 | count: PropTypes.number, 34 | doIncrement: PropTypes.func.isRequired, 35 | doDecrement: PropTypes.func.isRequired, 36 | }; 37 | 38 | CounterComponent.defaultProps = { 39 | count: 0, 40 | }; 41 | 42 | const mapStateToProps = state => ({ 43 | count: state.count, 44 | }); 45 | 46 | const mapDispatchToProps = dispatch => ({ 47 | doIncrement: () => dispatch(increment()), 48 | doDecrement: () => dispatch(decrement()), 49 | }); 50 | 51 | 52 | export default connect(mapStateToProps, mapDispatchToProps)(CounterComponent); 53 | -------------------------------------------------------------------------------- /src/components/HomeComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class HomeComponent extends React.Component { 5 | constructor(props, context) { 6 | super(props, context); 7 | 8 | this.state = {}; 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

首页

15 |
16 | ); 17 | } 18 | } 19 | 20 | export default HomeComponent; 21 | -------------------------------------------------------------------------------- /src/components/Link.react.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const STATUS = { 5 | HOVERED: 'hovered', 6 | NORMAL: 'normal', 7 | }; 8 | 9 | class Link extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.onMouseEnter = this.onMouseEnter.bind(this); 14 | this.onMouseleave = this.onMouseleave.bind(this); 15 | this.state = { 16 | class: STATUS.NORMAL, 17 | }; 18 | } 19 | 20 | onMouseEnter() { 21 | this.setState({ 22 | class: STATUS.HOVERED, 23 | }); 24 | } 25 | 26 | onMouseleave() { 27 | this.setState({ 28 | class: STATUS.NORMAL, 29 | }); 30 | } 31 | 32 | render() { 33 | return ( 34 | 40 | {this.props.children} 41 | 42 | ); 43 | } 44 | } 45 | 46 | Link.propTypes = { 47 | page: PropTypes.string.isRequired, 48 | children: PropTypes.oneOfType([ 49 | PropTypes.element, 50 | PropTypes.string, 51 | ]).isRequired, 52 | }; 53 | 54 | export default Link; 55 | -------------------------------------------------------------------------------- /src/components/LoadingComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class LoadingComponent extends Component { 4 | constructor(props, context) { 5 | super(props, context); 6 | 7 | this.state = {}; 8 | } 9 | 10 | render() { 11 | return ( 12 |
Loading......
13 | ); 14 | } 15 | } 16 | 17 | export default LoadingComponent; 18 | -------------------------------------------------------------------------------- /src/components/RoleComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class RoleComponent extends Component { 5 | constructor(props, context) { 6 | super(props, context); 7 | 8 | this.state = {}; 9 | } 10 | 11 | render() { 12 | const { 13 | match, 14 | } = this.props; 15 | 16 | const { 17 | role, 18 | } = match.params; 19 | 20 | let name; 21 | if (role === 'you') { 22 | name = '你'; 23 | } else if (role === 'me') { 24 | name = '我'; 25 | } else if (role === 'him') { 26 | name = '他'; 27 | } 28 | return ( 29 |
关于 {name} 的故事
30 | ); 31 | } 32 | } 33 | 34 | RoleComponent.propTypes = { 35 | match: PropTypes.objectOf(PropTypes.any).isRequired, 36 | }; 37 | 38 | export default RoleComponent; 39 | 40 | -------------------------------------------------------------------------------- /src/components/TopicComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | 4 | class TopicComponent extends React.Component { 5 | constructor(props, context) { 6 | super(props, context); 7 | 8 | this.state = {}; 9 | } 10 | 11 | render() { 12 | const { 13 | match, 14 | } = this.props; 15 | 16 | return ( 17 |
论题【{match.params.topicId}】
18 | ); 19 | } 20 | } 21 | 22 | TopicComponent.propTypes = { 23 | match: PropTypes.objectOf(PropTypes.any).isRequired, 24 | }; 25 | 26 | export default TopicComponent; 27 | -------------------------------------------------------------------------------- /src/components/TopicsComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link as ALink, Route } from 'react-router-dom'; 3 | import { PropTypes } from 'prop-types'; 4 | import TopicComponent from './TopicComponent'; 5 | 6 | class TopicsComponent extends React.Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | 10 | this.state = {}; 11 | } 12 | 13 | render() { 14 | const { 15 | match, 16 | } = this.props; 17 | 18 | return ( 19 |
20 |

论题

21 |
    22 |
  • 23 | 论题1 24 |
  • 25 |
  • 26 | 论题2 27 |
  • 28 |
  • 29 | 论题3 30 |
  • 31 |
  • 32 | 论题4 33 |
  • 34 |
35 | 36 |
37 | 38 |

请选择一个感兴趣的论题.

} 42 | /> 43 |
44 | ); 45 | } 46 | } 47 | 48 | TopicsComponent.propTypes = { 49 | match: PropTypes.objectOf(PropTypes.any).isRequired, 50 | }; 51 | 52 | export default TopicsComponent; 53 | -------------------------------------------------------------------------------- /src/css/CounterComponent.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | width: 60px; 3 | height: 30px; 4 | background: #673ab7; 5 | font-size: 16px; 6 | border: none; 7 | border-radius: 2px; 8 | outline: none; 9 | color: #fff; 10 | } 11 | 12 | .btn:focus { 13 | background: #3d51b5; 14 | } 15 | 16 | .btn.first-child { 17 | margin-right: 5px; 18 | } -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | @import './CounterComponent.css'; -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppContainer } from 'react-hot-loader'; 3 | import { createBrowserHistory } from 'history'; 4 | import { applyMiddleware, compose, createStore } from 'redux'; 5 | import { Provider } from 'react-redux'; 6 | import { createLogger } from 'redux-logger'; 7 | import { connectRouter, routerMiddleware } from 'connected-react-router'; 8 | import ReactDOM from 'react-dom'; 9 | import App from './App'; 10 | import rootReducer from './reducers'; 11 | import './css/main.css'; 12 | 13 | if ('serviceWorker' in navigator) { 14 | window.addEventListener('load', () => { 15 | navigator.serviceWorker.register('/service-worker.js').then((registration) => { 16 | console.log('SW registered: ', registration); 17 | }).catch((registrationError) => { 18 | console.log('SW registration failed: ', registrationError); 19 | }); 20 | }); 21 | } 22 | 23 | const history = createBrowserHistory(); 24 | const initialState = {}; 25 | const middleware = []; 26 | if (process.env.NODE_ENV !== 'production') { 27 | middleware.push(createLogger()); 28 | } 29 | 30 | middleware.push(routerMiddleware(history)); 31 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 32 | const store = createStore( 33 | connectRouter(history)(rootReducer), 34 | initialState, 35 | composeEnhancer(applyMiddleware(...middleware)), 36 | ); 37 | 38 | const render = () => { 39 | ReactDOM.render( 40 | ( 41 | 42 | 43 | 44 | 45 | 46 | ), 47 | document.getElementById('root'), 48 | ); 49 | }; 50 | 51 | render(); 52 | 53 | if (module.hot) { 54 | module.hot.addStatusHandler((status) => { 55 | console.log('status = ', status); 56 | }); 57 | 58 | module.hot.accept('./App', () => { 59 | // 这里是当前版本很重要的环节,不然的话react-hot-reload不起作用 60 | require('./App').default; 61 | render(); 62 | }); 63 | 64 | module.hot.accept('./reducers', () => { 65 | store.replaceReducer(connectRouter(history)(rootReducer)); 66 | render(); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/FileSummarizer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | function summarizeFilesInDirectorySync(directory) { 4 | return fs.readdirSync(directory).map(fileName => ({ 5 | directory, 6 | fileName, 7 | })); 8 | } 9 | 10 | exports.summarizeFilesInDirectorySync = summarizeFilesInDirectorySync; 11 | -------------------------------------------------------------------------------- /src/lib/__mocks__/fs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const fs = jest.genMockFromModule('fs'); 4 | 5 | let mockFiles = Object.create(null); 6 | 7 | function __setMockFiles(newMockFiles) { 8 | mockFiles = Object.create(null); 9 | 10 | const keys = Object.keys(newMockFiles); 11 | 12 | for (let index = 0; index < keys.length; index += 1) { 13 | const file = keys[index]; 14 | const dir = path.dirname(file); 15 | if (!mockFiles[dir]) { 16 | mockFiles[dir] = []; 17 | } 18 | mockFiles[dir].push(path.basename(file)); 19 | } 20 | } 21 | 22 | function readdirSync(directoryPath) { 23 | return mockFiles[directoryPath] || []; 24 | } 25 | 26 | fs.__setMockFiles = __setMockFiles; 27 | fs.readdirSync = readdirSync; 28 | 29 | module.exports = fs; 30 | -------------------------------------------------------------------------------- /src/lib/__mocks__/request.js: -------------------------------------------------------------------------------- 1 | const users = { 2 | 4: { 3 | name: 'Mark', 4 | }, 5 | 5: { 6 | name: 'Paul', 7 | }, 8 | }; 9 | 10 | export default function request(url) { 11 | return new Promise((resolve, reject) => { 12 | const userID = parseInt(url.substr('/users/'.length), 10); 13 | process.nextTick(() => { 14 | if (users[userID]) { 15 | return resolve(users[userID]); 16 | } 17 | return reject({ 18 | error: `User with ${userID} not found.`, 19 | }); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/__mocks__/sound-player.js: -------------------------------------------------------------------------------- 1 | export const mockChoicePlaySoundFile = jest.fn(); 2 | const mockPlaySoundFile = jest.fn(); 3 | 4 | const mock = jest.fn().mockImplementation(() => { 5 | const data = { 6 | choicePlaySoundFile: mockChoicePlaySoundFile, 7 | playSoundFile: mockPlaySoundFile, 8 | }; 9 | 10 | return data; 11 | }); 12 | 13 | export default mock; 14 | -------------------------------------------------------------------------------- /src/lib/infiniteTimerGame.js: -------------------------------------------------------------------------------- 1 | function infiniteTimerGame(callback) { 2 | console.log('Ready....go!'); 3 | 4 | setTimeout(() => { 5 | console.log('Times up! 10 seconds before the next game starts...'); 6 | 7 | if (callback) { 8 | callback(); 9 | } 10 | 11 | // 10秒钟后执行下一个 12 | setTimeout(() => { 13 | infiniteTimerGame(callback); 14 | }, 10000); 15 | }, 1000); 16 | } 17 | 18 | module.exports = infiniteTimerGame; 19 | -------------------------------------------------------------------------------- /src/lib/matchMedia.mock.js: -------------------------------------------------------------------------------- 1 | window.matchMedia = jest.fn().mockImplementation((query) => { 2 | const obj = { 3 | matches: false, 4 | media: query, 5 | onchange: null, 6 | addListener: jest.fn(), 7 | removeListener: jest.fn(), 8 | }; 9 | 10 | return obj; 11 | }); 12 | -------------------------------------------------------------------------------- /src/lib/request.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | export default function request(url) { 4 | return new Promise((resolve) => { 5 | http.get({ 6 | path: url, 7 | }, (response) => { 8 | let data = ''; 9 | response.on('data', (o) => { 10 | data += o; 11 | return data; 12 | }); 13 | response.on('end', () => resolve(data)); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/sound-player-consumer.js: -------------------------------------------------------------------------------- 1 | import SoundPlayer from './sound-player'; 2 | 3 | export default class SoundPlayerConsumer { 4 | constructor() { 5 | this.soundPlayer = new SoundPlayer(); 6 | } 7 | 8 | play() { 9 | const coolSoundFileName = 'song.mp3'; 10 | this.soundPlayer.choicePlaySoundFile(coolSoundFileName); 11 | this.soundPlayer.playSoundFile(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/sound-player.js: -------------------------------------------------------------------------------- 1 | export default class SoundPlayer { 2 | constructor() { 3 | this.name = 'Player1'; 4 | this.fileName = ''; 5 | } 6 | 7 | choicePlaySoundFile(fileName) { 8 | this.fileName = fileName; 9 | } 10 | 11 | playSoundFile() { 12 | console.log('播放的文件是:', this.fileName); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/sum.js: -------------------------------------------------------------------------------- 1 | function sum(a, b) { 2 | return a + b; 3 | } 4 | module.exports = sum; 5 | -------------------------------------------------------------------------------- /src/lib/task.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Task: args => args, 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /src/lib/timerGame.js: -------------------------------------------------------------------------------- 1 | function timerGame(callback) { 2 | console.log('Ready....go!'); 3 | setTimeout(() => { 4 | console.log('Times up -- stop!'); 5 | return callback && callback(); 6 | }, 1000); 7 | } 8 | 9 | module.exports = timerGame; 10 | -------------------------------------------------------------------------------- /src/lib/useMatchMedia.js: -------------------------------------------------------------------------------- 1 | const useMatchMedia = () => { 2 | const res = window.matchMedia; 3 | return res; 4 | }; 5 | 6 | module.exports = { 7 | useMatchMedia, 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/user.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import request from './request'; 3 | 4 | class Users { 5 | static all() { 6 | return axios.get('/user.json').then(resp => resp.data); 7 | } 8 | 9 | static getUserName(userID) { 10 | return request(`/users/${userID}`).then(user => user.name); 11 | } 12 | } 13 | 14 | export default Users; 15 | -------------------------------------------------------------------------------- /src/reducers/counter.js: -------------------------------------------------------------------------------- 1 | const counterReducer = (state = 0, action) => { 2 | switch (action.type) { 3 | case 'INCREMENT': 4 | return state + 1; 5 | case 'DECREMENT': 6 | return state - 1; 7 | default: 8 | return state; 9 | } 10 | }; 11 | 12 | export default counterReducer; 13 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counterReducer from './counter'; 3 | 4 | export default combineReducers({ 5 | count: counterReducer, 6 | }); 7 | -------------------------------------------------------------------------------- /src/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loadable from 'react-loadable'; 3 | import { 4 | BrowserRouter as Router, 5 | Route, 6 | Link as ALink, 7 | } from 'react-router-dom'; 8 | 9 | import AppComponent from './components/AppComponent'; 10 | import HomeComponent from './components/HomeComponent'; 11 | import LoadingComponent from './components/LoadingComponent'; 12 | 13 | const routes = ( 14 |
15 | 16 | 17 |
    18 |
  • 首页
  • 19 |
  • 关于
  • 20 |
  • 论题
  • 21 |
  • 计数器
  • 22 |
23 |
24 | 25 | 26 | import('./components/AboutComponent'), 30 | loading: LoadingComponent, 31 | })} 32 | /> 33 | import('./components/TopicsComponent'), 37 | loading: LoadingComponent, 38 | })} 39 | /> 40 | import('./components/CounterComponent'), 44 | loading: LoadingComponent, 45 | })} 46 | /> 47 |
48 |
49 |
50 | ); 51 | 52 | export default routes; 53 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | module.exports = { 7 | plugins: [ 8 | new CleanWebpackPlugin(['dist']), 9 | new HtmlWebpackPlugin({ 10 | title: 'React + ReactRouter Demo', 11 | filename: './index.html', // 调用的文件 12 | template: './index.html', // 模板文件 13 | }), 14 | ], 15 | output: { 16 | filename: '[name].bundle.js', 17 | chunkFilename: '[chunkhash].bundle.js', 18 | path: path.resolve(__dirname, 'dist'), 19 | publicPath: '/', 20 | }, 21 | module: { 22 | rules: [{ 23 | test: /\.(js|jsx)$/, 24 | loader: 'babel-loader', 25 | exclude: [ 26 | path.resolve(__dirname, 'node_modules'), 27 | ], 28 | options: { 29 | plugins: ['transform-async-to-generator', 'transform-strict-mode', 'transform-object-assign', 'transform-decorators-legacy', 'react-hot-loader/babel'], 30 | presets: ['env', 'react', 'stage-0'], 31 | }, 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: ExtractTextPlugin.extract({ 36 | fallback: 'style-loader', 37 | use: 'css-loader', 38 | }), 39 | }, 40 | { 41 | test: /\.(png|svg|jpg|gif)$/, 42 | use: [ 43 | 'file-loader', 44 | ], 45 | }, 46 | { 47 | test: /\.(woff|woff2|eot|ttf|otf)$/, 48 | use: [ 49 | 'file-loader', 50 | ], 51 | }, 52 | { 53 | test: /\.(csv|tsv)$/, 54 | use: [ 55 | 'csv-loader', 56 | ], 57 | }, 58 | { 59 | test: /\.xml$/, 60 | use: [ 61 | 'xml-loader', 62 | ], 63 | }, 64 | ], 65 | }, 66 | resolve: { 67 | extensions: ['.js', '.jsx'], // 这里是必须要加的,不然默认的值加载['.js','.json']为后缀的文件 68 | alias: { 69 | Actions: path.resolve(__dirname, 'src/actions'), 70 | }, 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | 6 | let config = { 7 | entry: { 8 | app: [ 9 | 'webpack/hot/only-dev-server', 10 | 'react-hot-loader/patch', 11 | './src/index.jsx', 12 | ], 13 | }, 14 | plugins: [ 15 | new CleanWebpackPlugin(['dist']), 16 | new HtmlWebpackPlugin({ 17 | title: 'React + ReactRouter Demo', 18 | filename: './index.html', // 调用的文件 19 | template: './index.html', // 模板文件 20 | }), 21 | ], 22 | output: { 23 | filename: '[name].bundle.js', 24 | chunkFilename: '[chunkhash].bundle.js', 25 | path: path.resolve(__dirname, 'dist'), 26 | publicPath: '/', 27 | }, 28 | module: { 29 | rules: [{ 30 | test: /\.(js|jsx)$/, 31 | loader: 'babel-loader', 32 | exclude: [ 33 | path.resolve(__dirname, 'node_modules'), 34 | ], 35 | options: { 36 | plugins: ['transform-async-to-generator', 'transform-strict-mode', 'transform-object-assign', 'transform-decorators-legacy', 'react-hot-loader/babel'], 37 | presets: ['env', 'react', 'stage-0'], 38 | }, 39 | }, 40 | { 41 | test: /\.css$/, 42 | use: [ 43 | 'style-loader', 44 | 'css-loader', 45 | ], 46 | }, 47 | { 48 | test: /\.(png|svg|jpg|gif)$/, 49 | use: [ 50 | 'file-loader', 51 | ], 52 | }, 53 | { 54 | test: /\.(woff|woff2|eot|ttf|otf)$/, 55 | use: [ 56 | 'file-loader', 57 | ], 58 | }, 59 | { 60 | test: /\.(csv|tsv)$/, 61 | use: [ 62 | 'csv-loader', 63 | ], 64 | }, 65 | { 66 | test: /\.xml$/, 67 | use: [ 68 | 'xml-loader', 69 | ], 70 | }, 71 | ], 72 | }, 73 | resolve: { 74 | extensions: ['.js', '.jsx'], // 这里是必须要加的,不然默认的值加载['.js','.json']为后缀的文件 75 | alias: { 76 | Actions: path.resolve(__dirname, 'src/actions'), 77 | }, 78 | }, 79 | }; 80 | 81 | if (process.env.NODE_ENV === 'production') { 82 | config = Object.assign({}, config, { 83 | mode: 'production', 84 | }); 85 | } else { 86 | const { 87 | plugins, 88 | } = config; 89 | plugins.push(new webpack.DefinePlugin({ 90 | 'global.GENTLY': false, 91 | __DEV__: true, 92 | })); 93 | config = Object.assign({}, config, { 94 | mode: 'development', 95 | devtool: 'eval', 96 | devServer: { 97 | hot: true, 98 | contentBase: path.join(__dirname, 'dist'), 99 | compress: true, 100 | port: 8083, 101 | historyApiFallback: { 102 | rewrites: [{ 103 | from: /^\/$/, 104 | to: './index.html', 105 | }], 106 | }, 107 | }, 108 | plugins, 109 | }); 110 | } 111 | 112 | module.exports = config; 113 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const ManifestPlugin = require('webpack-manifest-plugin'); 7 | const path = require('path'); 8 | const common = require('./webpack.common'); 9 | 10 | module.exports = merge(common, { 11 | mode: 'development', 12 | devtool: 'eval', 13 | entry: { 14 | app: [ 15 | 'webpack/hot/only-dev-server', 16 | 'react-hot-loader/patch', 17 | './src/index.jsx', 18 | ], 19 | vendor: [ 20 | 'react', 21 | 'react-dom', 22 | 'redux', 23 | ], 24 | }, 25 | output: { 26 | filename: '[name].[hash].bundle.js', 27 | chunkFilename: '[name].[hash].bundle.js', 28 | path: path.resolve(__dirname, 'dist'), 29 | publicPath: '/', 30 | }, 31 | devServer: { 32 | hot: true, 33 | contentBase: path.join(__dirname, 'dist'), 34 | compress: true, 35 | port: 8083, 36 | historyApiFallback: { 37 | rewrites: [{ 38 | from: /^\/$/, 39 | to: './index.html', 40 | }], 41 | }, 42 | }, 43 | plugins: [ 44 | new CleanWebpackPlugin(['dist']), 45 | new HtmlWebpackPlugin({ 46 | title: 'React + ReactRouter Demo', 47 | filename: './index.html', // 调用的文件 48 | template: './index.html', // 模板文件 49 | }), 50 | new webpack.DefinePlugin({ 51 | 'process.env.NODE_ENV': JSON.stringify('development'), 52 | }), 53 | new ExtractTextPlugin({ 54 | filename: '[name].[hash].bundle.css', 55 | }), 56 | new ManifestPlugin(), 57 | new webpack.NamedModulesPlugin(), 58 | ], 59 | optimization: { 60 | splitChunks: { 61 | chunks: 'initial', // 必须三选一: "initial" | "all"(默认就是all) | "async" 62 | minSize: 0, // 最小尺寸,默认0 63 | minChunks: 1, // 最小 chunk ,默认1 64 | maxAsyncRequests: 1, // 最大异步请求数, 默认1 65 | maxInitialRequests: 1, // 最大初始化请求书,默认1 66 | name: () => {}, // 名称,此选项可接收 function 67 | cacheGroups: { // 这里开始设置缓存的 chunks 68 | priority: '0', // 缓存组优先级 false | object | 69 | vendor: { // key 为entry中定义的 入口名称 70 | chunks: 'initial', // 必须三选一: "initial" | "all" | "async"(默认就是异步) 71 | test: /react|lodash|react-dom|redux/, // 正则规则验证,如果符合就提取 chunk 72 | name: 'vendor', // 要缓存的 分隔出来的 chunk 名称 73 | minSize: 0, 74 | minChunks: 1, 75 | enforce: true, 76 | maxAsyncRequests: 1, // 最大异步请求数, 默认1 77 | maxInitialRequests: 1, // 最大初始化请求书,默认1 78 | reuseExistingChunk: true, // 可设置是否重用该chunk(查看源码没有发现默认值) 79 | }, 80 | }, 81 | }, 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 6 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | const ManifestPlugin = require('webpack-manifest-plugin'); 9 | const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin'); 10 | const WebpackBundleAnalyzer = require('webpack-bundle-analyzer'); 11 | const common = require('./webpack.common'); 12 | const WorkboxWebpackPlugin = require('workbox-webpack-plugin'); 13 | 14 | const { 15 | BundleAnalyzerPlugin, 16 | } = WebpackBundleAnalyzer; 17 | 18 | module.exports = merge(common, { 19 | mode: 'production', 20 | devtool: 'source-map', 21 | entry: { 22 | app: [ 23 | './src/index.jsx', 24 | ], 25 | vendor: [ 26 | 'react', 27 | 'react-dom', 28 | 'redux', 29 | ], 30 | }, 31 | output: { 32 | filename: '[name].[chunkhash].bundle.js', 33 | chunkFilename: '[name].[chunkhash].bundle.js', 34 | path: path.resolve(__dirname, 'dist'), 35 | publicPath: '/', 36 | }, 37 | plugins: [ 38 | new CleanWebpackPlugin(['dist']), 39 | new HtmlWebpackPlugin({ 40 | title: 'React + ReactRouter', 41 | filename: './index.html', // 调用的文件 42 | template: './index.html', // 模板文件 43 | }), 44 | new InlineManifestWebpackPlugin(), 45 | new UglifyJsPlugin({ 46 | sourceMap: true, 47 | }), 48 | new webpack.DefinePlugin({ 49 | 'process.env.NODE_ENV': JSON.stringify('production'), 50 | }), 51 | new ExtractTextPlugin({ 52 | filename: 'main.[chunkhash].css', 53 | }), 54 | new ManifestPlugin(), 55 | new webpack.NamedModulesPlugin(), 56 | // new BundleAnalyzerPlugin(), 57 | new WorkboxWebpackPlugin.GenerateSW({ 58 | clientsClaim: true, 59 | skipWaiting: true, 60 | exclude: [/\.map$/], 61 | }), 62 | ], 63 | optimization: { 64 | splitChunks: { 65 | chunks: 'initial', // 必须三选一: "initial" | "all"(默认就是all) | "async" 66 | minSize: 0, // 最小尺寸,默认0 67 | minChunks: 1, // 最小 chunk ,默认1 68 | maxAsyncRequests: 1, // 最大异步请求数, 默认1 69 | maxInitialRequests: 1, // 最大初始化请求书,默认1 70 | name: () => {}, // 名称,此选项可接收 function 71 | cacheGroups: { // 这里开始设置缓存的 chunks 72 | priority: '0', // 缓存组优先级 false | object | 73 | vendor: { // key 为entry中定义的 入口名称 74 | chunks: 'initial', // 必须三选一: "initial" | "all" | "async"(默认就是异步) 75 | test: /react|lodash|react-dom|redux/, // 正则规则验证,如果符合就提取 chunk 76 | name: 'vendor', // 要缓存的 分隔出来的 chunk 名称 77 | minSize: 0, 78 | minChunks: 1, 79 | enforce: true, 80 | maxAsyncRequests: 1, // 最大异步请求数, 默认1 81 | maxInitialRequests: 1, // 最大初始化请求书,默认1 82 | reuseExistingChunk: true, // 可设置是否重用该chunk(查看源码没有发现默认值) 83 | }, 84 | }, 85 | }, 86 | runtimeChunk: { 87 | name: 'manifest', 88 | }, 89 | }, 90 | }); 91 | --------------------------------------------------------------------------------