fix: first commit
This commit is contained in:
parent
7fcd6724e1
commit
e287f6c9d2
|
@ -21,3 +21,5 @@
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.idea
|
|
@ -12,9 +12,14 @@
|
|||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^5.9.2",
|
||||
"web-vitals": "^2.1.4"
|
||||
}
|
||||
},
|
||||
|
@ -2506,6 +2511,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/diff-sequences": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
|
||||
"integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/environment/-/environment-27.5.1.tgz",
|
||||
|
@ -2520,6 +2533,17 @@
|
|||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/expect-utils": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/expect-utils/-/expect-utils-30.0.5.tgz",
|
||||
"integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==",
|
||||
"dependencies": {
|
||||
"@jest/get-type": "30.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/fake-timers": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz",
|
||||
|
@ -2536,6 +2560,14 @@
|
|||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/get-type": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/get-type/-/get-type-30.0.1.tgz",
|
||||
"integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==",
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/globals": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/globals/-/globals-27.5.1.tgz",
|
||||
|
@ -2549,6 +2581,26 @@
|
|||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/pattern": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/pattern/-/pattern-30.0.1.tgz",
|
||||
"integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"jest-regex-util": "30.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/pattern/node_modules/jest-regex-util": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
|
||||
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/reporters": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/reporters/-/reporters-27.5.1.tgz",
|
||||
|
@ -3483,6 +3535,202 @@
|
|||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest": {
|
||||
"version": "30.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/jest/-/jest-30.0.0.tgz",
|
||||
"integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==",
|
||||
"dependencies": {
|
||||
"expect": "^30.0.0",
|
||||
"pretty-format": "^30.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/@jest/schemas": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-30.0.5.tgz",
|
||||
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/@jest/types": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@jest/types/-/types-30.0.5.tgz",
|
||||
"integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==",
|
||||
"dependencies": {
|
||||
"@jest/pattern": "30.0.1",
|
||||
"@jest/schemas": "30.0.5",
|
||||
"@types/istanbul-lib-coverage": "^2.0.6",
|
||||
"@types/istanbul-reports": "^3.0.4",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chalk": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.38",
|
||||
"resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.34.38.tgz",
|
||||
"integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA=="
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.33.tgz",
|
||||
"integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
|
||||
"dependencies": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/ci-info": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-4.3.0.tgz",
|
||||
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/expect": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/expect/-/expect-30.0.5.tgz",
|
||||
"integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==",
|
||||
"dependencies": {
|
||||
"@jest/expect-utils": "30.0.5",
|
||||
"@jest/get-type": "30.0.1",
|
||||
"jest-matcher-utils": "30.0.5",
|
||||
"jest-message-util": "30.0.5",
|
||||
"jest-mock": "30.0.5",
|
||||
"jest-util": "30.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-diff": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/jest-diff/-/jest-diff-30.0.5.tgz",
|
||||
"integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==",
|
||||
"dependencies": {
|
||||
"@jest/diff-sequences": "30.0.1",
|
||||
"@jest/get-type": "30.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"pretty-format": "30.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-matcher-utils": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz",
|
||||
"integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==",
|
||||
"dependencies": {
|
||||
"@jest/get-type": "30.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"jest-diff": "30.0.5",
|
||||
"pretty-format": "30.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-message-util": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/jest-message-util/-/jest-message-util-30.0.5.tgz",
|
||||
"integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@jest/types": "30.0.5",
|
||||
"@types/stack-utils": "^2.0.3",
|
||||
"chalk": "^4.1.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"micromatch": "^4.0.8",
|
||||
"pretty-format": "30.0.5",
|
||||
"slash": "^3.0.0",
|
||||
"stack-utils": "^2.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-mock": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/jest-mock/-/jest-mock-30.0.5.tgz",
|
||||
"integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.0.5",
|
||||
"@types/node": "*",
|
||||
"jest-util": "30.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/jest-util": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/jest-util/-/jest-util-30.0.5.tgz",
|
||||
"integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.0.5",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^4.2.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/pretty-format": {
|
||||
"version": "30.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-30.0.5.tgz",
|
||||
"integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==",
|
||||
"dependencies": {
|
||||
"@jest/schemas": "30.0.5",
|
||||
"ansi-styles": "^5.2.0",
|
||||
"react-is": "^18.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jest/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
|
@ -3539,6 +3787,22 @@
|
|||
"resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react/-/react-19.1.9.tgz",
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.1.7.tgz",
|
||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
|
@ -5888,6 +6152,11 @@
|
|||
"resolved": "https://registry.npmmirror.com/cssom/-/cssom-0.3.8.tgz",
|
||||
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
|
@ -15096,16 +15365,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"peer": true,
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
|
|
|
@ -7,9 +7,14 @@
|
|||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^5.9.2",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
170
src/App.css
170
src/App.css
|
@ -1,38 +1,158 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
.App-header h1 {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 700;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
}
|
||||
|
||||
/* 按钮悬停效果 */
|
||||
button {
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 卡片阴影效果 */
|
||||
div[style*="border"] {
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
div[style*="border"]:hover {
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
input[type="text"] {
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #667eea !important;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.App-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.App-header p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
nav {
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
nav button {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.App-header {
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 15px 30px 15px !important;
|
||||
}
|
||||
|
||||
.App-header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
main > div {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* 代码块样式 */
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #e83e8c;
|
||||
}
|
||||
|
||||
/* 高亮文本 */
|
||||
strong {
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #764ba2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
25
src/App.js
25
src/App.js
|
@ -1,25 +0,0 @@
|
|||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,198 @@
|
|||
import React, { useState } from 'react';
|
||||
import './App.css';
|
||||
import JSXExample from './examples/JSXExample';
|
||||
import TSXExample from './examples/TSXExample';
|
||||
import ReactVsVueExamples from './examples/ReactVsVueExamples';
|
||||
import VueStyleExamples from './examples/VueStyleExamples';
|
||||
import AdvancedConcepts from './examples/AdvancedConcepts';
|
||||
import PerformanceDemo from './examples/PerformanceDemo';
|
||||
import InteractiveGames from './examples/InteractiveGames';
|
||||
|
||||
type ViewType = 'jsx-tsx' | 'react-vue' | 'advanced' | 'performance' | 'games';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [currentView, setCurrentView] = useState<ViewType>('jsx-tsx');
|
||||
|
||||
const navigationItems = [
|
||||
{ key: 'jsx-tsx', label: 'JSX vs TSX', emoji: '📚' },
|
||||
{ key: 'react-vue', label: 'React vs Vue', emoji: '⚔️' },
|
||||
{ key: 'advanced', label: '高级概念', emoji: '🚀' },
|
||||
{ key: 'performance', label: '性能优化', emoji: '⚡' },
|
||||
{ key: 'games', label: '交互游戏', emoji: '🎮' }
|
||||
];
|
||||
|
||||
const renderContent = () => {
|
||||
switch (currentView) {
|
||||
case 'jsx-tsx':
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ textAlign: 'center', color: '#333', marginBottom: '30px' }}>
|
||||
📚 JSX 和 TSX 概念对比
|
||||
</h2>
|
||||
<JSXExample />
|
||||
<TSXExample title="TypeScript JSX 示例" initialCount={10} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'react-vue':
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ textAlign: 'center', color: '#333', marginBottom: '30px' }}>
|
||||
⚔️ React vs Vue 设计思想对比
|
||||
</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(500px, 1fr))',
|
||||
gap: '20px'
|
||||
}}>
|
||||
<ReactVsVueExamples />
|
||||
<VueStyleExamples />
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '10px',
|
||||
border: '2px solid #dee2e6'
|
||||
}}>
|
||||
<h3>🎯 核心差异总结</h3>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: '20px'
|
||||
}}>
|
||||
<div>
|
||||
<h4 style={{ color: '#007bff' }}>🚀 React 哲学</h4>
|
||||
<ul style={{ textAlign: 'left' }}>
|
||||
<li><strong>函数式编程</strong>:UI = f(state)</li>
|
||||
<li><strong>显式控制</strong>:开发者完全掌控</li>
|
||||
<li><strong>组合优于继承</strong>:通过组合构建复杂UI</li>
|
||||
<li><strong>单向数据流</strong>:可预测的数据流向</li>
|
||||
<li><strong>不可变性</strong>:状态变化通过新对象</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style={{ color: '#4FC08D' }}>🌟 Vue 哲学</h4>
|
||||
<ul style={{ textAlign: 'left' }}>
|
||||
<li><strong>渐进式框架</strong>:逐步增强现有项目</li>
|
||||
<li><strong>响应式系统</strong>:数据变化自动更新视图</li>
|
||||
<li><strong>约定优于配置</strong>:合理的默认值</li>
|
||||
<li><strong>开发者友好</strong>:降低学习曲线</li>
|
||||
<li><strong>模板语法</strong>:接近HTML的声明式语法</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'advanced':
|
||||
return <AdvancedConcepts />;
|
||||
|
||||
case 'performance':
|
||||
return <PerformanceDemo />;
|
||||
|
||||
case 'games':
|
||||
return <InteractiveGames />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header" style={{ padding: '20px 20px 10px 20px' }}>
|
||||
<h1 style={{ margin: '0 0 10px 0' }}>React 全面学习演示</h1>
|
||||
<p style={{ margin: '0 0 20px 0', color: '#666' }}>
|
||||
深入理解JSX、TSX、设计思想、高级概念、性能优化和实战应用
|
||||
</p>
|
||||
|
||||
{/* 导航按钮 */}
|
||||
<nav style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '10px'
|
||||
}}>
|
||||
{navigationItems.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => setCurrentView(item.key as ViewType)}
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
backgroundColor: currentView === item.key ? '#007bff' : '#6c757d',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
transition: 'all 0.3s ease',
|
||||
transform: currentView === item.key ? 'translateY(-2px)' : 'none',
|
||||
boxShadow: currentView === item.key
|
||||
? '0 4px 12px rgba(0, 123, 255, 0.3)'
|
||||
: '0 2px 4px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentView !== item.key) {
|
||||
e.currentTarget.style.backgroundColor = '#5a6268';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentView !== item.key) {
|
||||
e.currentTarget.style.backgroundColor = '#6c757d';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.emoji} {item.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* 视图描述 */}
|
||||
<div style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#e9ecef',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '20px',
|
||||
fontSize: '14px',
|
||||
color: '#495057'
|
||||
}}>
|
||||
{currentView === 'jsx-tsx' && '学习JSX和TSX的语法差异、类型安全和最佳实践'}
|
||||
{currentView === 'react-vue' && '对比React和Vue的设计理念、架构特点和适用场景'}
|
||||
{currentView === 'advanced' && '掌握Context、自定义Hooks、HOC、Render Props等高级模式'}
|
||||
{currentView === 'performance' && '学习React性能优化技巧:memo、虚拟滚动、懒加载等'}
|
||||
{currentView === 'games' && '通过交互游戏实践React状态管理和事件处理'}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main style={{ padding: '0 20px 40px 20px', minHeight: 'calc(100vh - 200px)' }}>
|
||||
{renderContent()}
|
||||
</main>
|
||||
|
||||
{/* 页脚 */}
|
||||
<footer style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderTop: '1px solid #dee2e6',
|
||||
textAlign: 'center',
|
||||
color: '#6c757d',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<span style={{ marginRight: '20px' }}>🛠️ 技术栈: React 19 + TypeScript</span>
|
||||
<span style={{ marginRight: '20px' }}>📚 包含: 基础语法 + 设计模式 + 性能优化</span>
|
||||
<span>🎯 目标: 全面掌握React开发</span>
|
||||
</div>
|
||||
<div>
|
||||
💡 <strong>学习建议</strong>: 按顺序学习各个章节,动手实践每个示例,观察控制台输出
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,178 @@
|
|||
# JSX vs TSX 概念详解
|
||||
|
||||
## 📖 基本概念
|
||||
|
||||
### JSX (JavaScript XML)
|
||||
JSX是React的语法扩展,允许你在JavaScript中编写类似HTML的代码。
|
||||
|
||||
**特点:**
|
||||
- 语法简洁,易于理解
|
||||
- 编译时转换为普通的JavaScript函数调用
|
||||
- 支持在{}中嵌入JavaScript表达式
|
||||
- 没有类型检查
|
||||
|
||||
### TSX (TypeScript JSX)
|
||||
TSX是JSX的TypeScript版本,在JSX基础上添加了类型安全。
|
||||
|
||||
**特点:**
|
||||
- 所有JSX的功能
|
||||
- 编译时类型检查
|
||||
- 更好的IDE支持(自动补全、错误提示)
|
||||
- 更安全的代码
|
||||
|
||||
## 🔄 语法对比
|
||||
|
||||
### 1. 基本组件定义
|
||||
|
||||
**JSX (JavaScript):**
|
||||
```jsx
|
||||
function MyComponent() {
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**TSX (TypeScript):**
|
||||
```tsx
|
||||
const MyComponent: React.FC = () => {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Props类型定义
|
||||
|
||||
**JSX:**
|
||||
```jsx
|
||||
function UserCard({ name, age }) {
|
||||
return (
|
||||
<div>
|
||||
<h3>{name}</h3>
|
||||
<p>年龄: {age}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**TSX:**
|
||||
```tsx
|
||||
interface UserCardProps {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
const UserCard: React.FC<UserCardProps> = ({ name, age }) => {
|
||||
return (
|
||||
<div>
|
||||
<h3>{name}</h3>
|
||||
<p>年龄: {age}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 状态管理
|
||||
|
||||
**JSX:**
|
||||
```jsx
|
||||
const [count, setCount] = useState(0);
|
||||
```
|
||||
|
||||
**TSX:**
|
||||
```tsx
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
```
|
||||
|
||||
### 4. 事件处理
|
||||
|
||||
**JSX:**
|
||||
```jsx
|
||||
const handleClick = (event) => {
|
||||
console.log(event.target.value);
|
||||
};
|
||||
```
|
||||
|
||||
**TSX:**
|
||||
```tsx
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
console.log(event.target.value);
|
||||
};
|
||||
```
|
||||
|
||||
## 🎯 主要区别
|
||||
|
||||
| 特性 | JSX | TSX |
|
||||
|------|-----|-----|
|
||||
| 类型安全 | ❌ | ✅ |
|
||||
| 编译时错误检查 | 基础 | 完整 |
|
||||
| IDE支持 | 基础 | 增强 |
|
||||
| 学习曲线 | 简单 | 中等 |
|
||||
| 运行时性能 | 相同 | 相同 |
|
||||
| 代码维护性 | 一般 | 更好 |
|
||||
|
||||
## 🔍 实际示例中的区别
|
||||
|
||||
### 类型安全示例
|
||||
|
||||
**JSX可能的问题:**
|
||||
```jsx
|
||||
// 运行时才能发现错误
|
||||
function displayAge(user) {
|
||||
return user.age; // 如果user为null会报错
|
||||
}
|
||||
```
|
||||
|
||||
**TSX的解决方案:**
|
||||
```tsx
|
||||
interface User {
|
||||
age: number;
|
||||
}
|
||||
|
||||
function displayAge(user: User | null): number | null {
|
||||
return user?.age || null; // 编译时就能发现潜在问题
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 何时使用哪种
|
||||
|
||||
### 选择JSX当:
|
||||
- 小型项目或原型开发
|
||||
- 团队对TypeScript不熟悉
|
||||
- 需要快速开发
|
||||
|
||||
### 选择TSX当:
|
||||
- 中大型项目
|
||||
- 团队协作开发
|
||||
- 需要高代码质量
|
||||
- 复杂的数据结构
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### JSX最佳实践:
|
||||
1. 使用PropTypes进行运行时类型检查
|
||||
2. 编写详细的注释
|
||||
3. 使用ESLint进行代码检查
|
||||
|
||||
### TSX最佳实践:
|
||||
1. 定义清晰的接口
|
||||
2. 使用泛型提高复用性
|
||||
3. 利用TypeScript的严格模式
|
||||
4. 合理使用联合类型和可选属性
|
||||
|
||||
## 🔧 项目配置
|
||||
|
||||
### 启用TSX支持需要:
|
||||
1. 安装TypeScript:`npm install typescript`
|
||||
2. 安装类型定义:`npm install @types/react @types/react-dom`
|
||||
3. 创建tsconfig.json配置文件
|
||||
4. 将.js文件重命名为.tsx
|
||||
|
||||
### tsconfig.json关键配置:
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,405 @@
|
|||
import React, { useState, useEffect, useContext, createContext, useCallback, useMemo } from 'react';
|
||||
|
||||
// ========== 高级React概念示例 ==========
|
||||
|
||||
// 1. React Context 示例
|
||||
interface ThemeContextType {
|
||||
theme: 'light' | 'dark';
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | null>(null);
|
||||
|
||||
const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
theme,
|
||||
toggleTheme
|
||||
}), [theme, toggleTheme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const ThemedComponent: React.FC = () => {
|
||||
const themeContext = useContext(ThemeContext);
|
||||
|
||||
if (!themeContext) {
|
||||
throw new Error('ThemedComponent must be used within ThemeProvider');
|
||||
}
|
||||
|
||||
const { theme, toggleTheme } = themeContext;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
backgroundColor: theme === 'light' ? '#ffffff' : '#2d3748',
|
||||
color: theme === 'light' ? '#000000' : '#ffffff',
|
||||
border: `2px solid ${theme === 'light' ? '#e2e8f0' : '#4a5568'}`,
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>🎨 Context API 示例</h4>
|
||||
<p>当前主题: {theme === 'light' ? '☀️ 浅色' : '🌙 深色'}</p>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: theme === 'light' ? '#4299e1' : '#63b3ed',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
切换主题
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 2. 自定义Hook示例
|
||||
const useCounter = (initialValue: number = 0) => {
|
||||
const [count, setCount] = useState(initialValue);
|
||||
|
||||
const increment = useCallback(() => setCount(prev => prev + 1), []);
|
||||
const decrement = useCallback(() => setCount(prev => prev - 1), []);
|
||||
const reset = useCallback(() => setCount(initialValue), [initialValue]);
|
||||
|
||||
return { count, increment, decrement, reset };
|
||||
};
|
||||
|
||||
const useLocalStorage = <T,>(key: string, initialValue: T) => {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = useCallback((value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, [key, storedValue]);
|
||||
|
||||
return [storedValue, setValue] as const;
|
||||
};
|
||||
|
||||
const CustomHooksDemo: React.FC = () => {
|
||||
const { count, increment, decrement, reset } = useCounter(0);
|
||||
const [name, setName] = useLocalStorage('userName', '');
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #38a169',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>🎣 自定义Hooks示例</h4>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<h5>计数器Hook:</h5>
|
||||
<p>计数: {count}</p>
|
||||
<button onClick={increment} style={{ margin: '2px', padding: '5px 10px' }}>+</button>
|
||||
<button onClick={decrement} style={{ margin: '2px', padding: '5px 10px' }}>-</button>
|
||||
<button onClick={reset} style={{ margin: '2px', padding: '5px 10px' }}>重置</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5>本地存储Hook:</h5>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="输入你的名字(会保存到localStorage)"
|
||||
style={{ padding: '5px', marginRight: '10px' }}
|
||||
/>
|
||||
<p>存储的名字: {name}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 3. 高阶组件 (HOC) 示例
|
||||
interface WithLoadingProps {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const withLoading = <P extends object>(Component: React.ComponentType<P>) => {
|
||||
return (props: P & WithLoadingProps) => {
|
||||
const { isLoading, ...otherProps } = props;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#f7fafc',
|
||||
border: '1px dashed #cbd5e0',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
<div>🔄 加载中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Component {...(otherProps as P)} />;
|
||||
};
|
||||
};
|
||||
|
||||
const DataDisplay: React.FC<{ data: string[] }> = ({ data }) => (
|
||||
<div style={{ padding: '10px' }}>
|
||||
<h5>数据列表:</h5>
|
||||
<ul>
|
||||
{data.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DataDisplayWithLoading = withLoading(DataDisplay);
|
||||
|
||||
const HOCDemo: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [data, setData] = useState<string[]>(['项目1', '项目2', '项目3']);
|
||||
|
||||
const loadData = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setData(['新数据1', '新数据2', '新数据3', '新数据4']);
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #805ad5',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>🔗 高阶组件 (HOC) 示例</h4>
|
||||
<button
|
||||
onClick={loadData}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#805ad5',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '10px'
|
||||
}}
|
||||
>
|
||||
加载新数据
|
||||
</button>
|
||||
|
||||
<DataDisplayWithLoading isLoading={isLoading} data={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 4. Render Props 模式示例
|
||||
interface MouseTrackerProps {
|
||||
children: (position: { x: number; y: number }) => React.ReactNode;
|
||||
}
|
||||
|
||||
const MouseTracker: React.FC<MouseTrackerProps> = ({ children }) => {
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
setPosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
return () => document.removeEventListener('mousemove', handleMouseMove);
|
||||
}, []);
|
||||
|
||||
return <>{children(position)}</>;
|
||||
};
|
||||
|
||||
const RenderPropsDemo: React.FC = () => {
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #ed8936',
|
||||
borderRadius: '8px',
|
||||
margin: '10px',
|
||||
minHeight: '100px'
|
||||
}}>
|
||||
<h4>🖱️ Render Props 模式示例</h4>
|
||||
<MouseTracker>
|
||||
{({ x, y }) => (
|
||||
<div>
|
||||
<p>鼠标位置: ({x}, {y})</p>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: x - 10,
|
||||
top: y - 10,
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundColor: '#ed8936',
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none',
|
||||
transition: 'all 0.1s ease'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</MouseTracker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 5. 错误边界示例
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<
|
||||
{ children: React.ReactNode },
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: { children: React.ReactNode }) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.log('错误边界捕获到错误:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#fed7d7',
|
||||
border: '2px solid #e53e3e',
|
||||
borderRadius: '8px',
|
||||
color: '#742a2a'
|
||||
}}>
|
||||
<h4>⚠️ 出错了!</h4>
|
||||
<p>组件渲染时发生错误</p>
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false })}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
backgroundColor: '#e53e3e',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const BuggyComponent: React.FC<{ shouldThrow: boolean }> = ({ shouldThrow }) => {
|
||||
if (shouldThrow) {
|
||||
throw new Error('这是一个故意的错误!');
|
||||
}
|
||||
return <div>✅ 组件正常工作</div>;
|
||||
};
|
||||
|
||||
const ErrorBoundaryDemo: React.FC = () => {
|
||||
const [shouldThrow, setShouldThrow] = useState(false);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #e53e3e',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>🛡️ 错误边界示例</h4>
|
||||
<button
|
||||
onClick={() => setShouldThrow(!shouldThrow)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: shouldThrow ? '#38a169' : '#e53e3e',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '10px'
|
||||
}}
|
||||
>
|
||||
{shouldThrow ? '修复组件' : '触发错误'}
|
||||
</button>
|
||||
|
||||
<ErrorBoundary>
|
||||
<BuggyComponent shouldThrow={shouldThrow} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 主组件
|
||||
const AdvancedConcepts: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>🚀 React高级概念演示</h2>
|
||||
|
||||
<ThemeProvider>
|
||||
<ThemedComponent />
|
||||
</ThemeProvider>
|
||||
|
||||
<CustomHooksDemo />
|
||||
<HOCDemo />
|
||||
<RenderPropsDemo />
|
||||
<ErrorBoundaryDemo />
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
padding: '20px',
|
||||
backgroundColor: '#ebf8ff',
|
||||
borderRadius: '10px',
|
||||
border: '2px solid #3182ce'
|
||||
}}>
|
||||
<h3>💡 高级概念总结</h3>
|
||||
<ul style={{ textAlign: 'left', lineHeight: '1.6' }}>
|
||||
<li><strong>Context API</strong>: 跨组件层级共享状态,避免prop drilling</li>
|
||||
<li><strong>自定义Hooks</strong>: 封装可复用的状态逻辑</li>
|
||||
<li><strong>高阶组件(HOC)</strong>: 增强组件功能的设计模式</li>
|
||||
<li><strong>Render Props</strong>: 通过函数prop共享代码</li>
|
||||
<li><strong>错误边界</strong>: 捕获和处理组件渲染错误</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedConcepts;
|
|
@ -0,0 +1,528 @@
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
// ========== 交互式游戏演示 ==========
|
||||
|
||||
// 1. 记忆卡片游戏
|
||||
interface Card {
|
||||
id: number;
|
||||
value: string;
|
||||
isFlipped: boolean;
|
||||
isMatched: boolean;
|
||||
}
|
||||
|
||||
const MemoryGame: React.FC = () => {
|
||||
const [cards, setCards] = useState<Card[]>([]);
|
||||
const [flippedCards, setFlippedCards] = useState<number[]>([]);
|
||||
const [score, setScore] = useState(0);
|
||||
const [moves, setMoves] = useState(0);
|
||||
const [gameComplete, setGameComplete] = useState(false);
|
||||
|
||||
const cardValues = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼'];
|
||||
|
||||
const initializeGame = useCallback(() => {
|
||||
const shuffledCards = [...cardValues, ...cardValues]
|
||||
.map((value, index) => ({
|
||||
id: index,
|
||||
value,
|
||||
isFlipped: false,
|
||||
isMatched: false
|
||||
}))
|
||||
.sort(() => Math.random() - 0.5);
|
||||
|
||||
setCards(shuffledCards);
|
||||
setFlippedCards([]);
|
||||
setScore(0);
|
||||
setMoves(0);
|
||||
setGameComplete(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
initializeGame();
|
||||
}, [initializeGame]);
|
||||
|
||||
useEffect(() => {
|
||||
if (flippedCards.length === 2) {
|
||||
const [first, second] = flippedCards;
|
||||
const firstCard = cards[first];
|
||||
const secondCard = cards[second];
|
||||
|
||||
if (firstCard.value === secondCard.value) {
|
||||
setTimeout(() => {
|
||||
setCards(prev => prev.map(card =>
|
||||
card.id === first || card.id === second
|
||||
? { ...card, isMatched: true }
|
||||
: card
|
||||
));
|
||||
setFlippedCards([]);
|
||||
setScore(prev => prev + 10);
|
||||
}, 1000);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setCards(prev => prev.map(card =>
|
||||
card.id === first || card.id === second
|
||||
? { ...card, isFlipped: false }
|
||||
: card
|
||||
));
|
||||
setFlippedCards([]);
|
||||
}, 1000);
|
||||
}
|
||||
setMoves(prev => prev + 1);
|
||||
}
|
||||
}, [flippedCards, cards]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cards.length > 0 && cards.every(card => card.isMatched)) {
|
||||
setGameComplete(true);
|
||||
}
|
||||
}, [cards]);
|
||||
|
||||
const handleCardClick = (cardId: number) => {
|
||||
if (flippedCards.length >= 2) return;
|
||||
if (cards[cardId].isFlipped || cards[cardId].isMatched) return;
|
||||
|
||||
setCards(prev => prev.map(card =>
|
||||
card.id === cardId ? { ...card, isFlipped: true } : card
|
||||
));
|
||||
setFlippedCards(prev => [...prev, cardId]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #9f7aea',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>🧠 记忆卡片游戏</h4>
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<span style={{ marginRight: '20px' }}>得分: {score}</span>
|
||||
<span style={{ marginRight: '20px' }}>步数: {moves}</span>
|
||||
<button
|
||||
onClick={initializeGame}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
backgroundColor: '#9f7aea',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
重新开始
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{gameComplete && (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
backgroundColor: '#c6f6d5',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '15px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
🎉 恭喜!游戏完成!用了 {moves} 步,得分 {score}!
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: '8px',
|
||||
maxWidth: '320px'
|
||||
}}>
|
||||
{cards.map((card) => (
|
||||
<div
|
||||
key={card.id}
|
||||
onClick={() => handleCardClick(card.id)}
|
||||
style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
backgroundColor: card.isFlipped || card.isMatched ? '#fff' : '#4a5568',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '24px',
|
||||
border: '2px solid #e2e8f0',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{(card.isFlipped || card.isMatched) ? card.value : '?'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 2. 井字棋游戏
|
||||
type Player = 'X' | 'O' | null;
|
||||
|
||||
const TicTacToe: React.FC = () => {
|
||||
const [board, setBoard] = useState<Player[]>(Array(9).fill(null));
|
||||
const [currentPlayer, setCurrentPlayer] = useState<'X' | 'O'>('X');
|
||||
const [winner, setWinner] = useState<Player>(null);
|
||||
const [gameOver, setGameOver] = useState(false);
|
||||
|
||||
const winningCombinations = [
|
||||
[0, 1, 2], [3, 4, 5], [6, 7, 8], // 行
|
||||
[0, 3, 6], [1, 4, 7], [2, 5, 8], // 列
|
||||
[0, 4, 8], [2, 4, 6] // 对角线
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const checkWinner = () => {
|
||||
for (const combination of winningCombinations) {
|
||||
const [a, b, c] = combination;
|
||||
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
|
||||
setWinner(board[a]);
|
||||
setGameOver(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (board.every(cell => cell !== null)) {
|
||||
setGameOver(true);
|
||||
}
|
||||
};
|
||||
|
||||
checkWinner();
|
||||
}, [board, winningCombinations]);
|
||||
|
||||
const handleCellClick = (index: number) => {
|
||||
if (board[index] || gameOver) return;
|
||||
|
||||
const newBoard = [...board];
|
||||
newBoard[index] = currentPlayer;
|
||||
setBoard(newBoard);
|
||||
setCurrentPlayer(currentPlayer === 'X' ? 'O' : 'X');
|
||||
};
|
||||
|
||||
const resetGame = () => {
|
||||
setBoard(Array(9).fill(null));
|
||||
setCurrentPlayer('X');
|
||||
setWinner(null);
|
||||
setGameOver(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #4299e1',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>⭕ 井字棋游戏</h4>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
{gameOver ? (
|
||||
<div>
|
||||
{winner ? `🎉 玩家 ${winner} 获胜!` : '🤝 平局!'}
|
||||
</div>
|
||||
) : (
|
||||
<div>当前玩家: {currentPlayer}</div>
|
||||
)}
|
||||
<button
|
||||
onClick={resetGame}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
backgroundColor: '#4299e1',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
重置游戏
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: '4px',
|
||||
width: '180px',
|
||||
height: '180px'
|
||||
}}>
|
||||
{board.map((cell, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleCellClick(index)}
|
||||
style={{
|
||||
backgroundColor: '#f7fafc',
|
||||
border: '2px solid #e2e8f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
cursor: gameOver || cell ? 'default' : 'pointer',
|
||||
transition: 'background-color 0.2s ease'
|
||||
}}
|
||||
>
|
||||
{cell}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 3. 简单贪吃蛇游戏
|
||||
interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const SnakeGame: React.FC = () => {
|
||||
const [snake, setSnake] = useState<Position[]>([{ x: 10, y: 10 }]);
|
||||
const [food, setFood] = useState<Position>({ x: 5, y: 5 });
|
||||
const [direction, setDirection] = useState<Position>({ x: 0, y: -1 });
|
||||
const [gameRunning, setGameRunning] = useState(false);
|
||||
const [score, setScore] = useState(0);
|
||||
const [gameOver, setGameOver] = useState(false);
|
||||
|
||||
const gameAreaSize = 20;
|
||||
const gameLoopRef = useRef<number | null>(null);
|
||||
|
||||
const generateFood = useCallback(() => {
|
||||
const newFood = {
|
||||
x: Math.floor(Math.random() * gameAreaSize),
|
||||
y: Math.floor(Math.random() * gameAreaSize)
|
||||
};
|
||||
setFood(newFood);
|
||||
}, []);
|
||||
|
||||
const resetGame = () => {
|
||||
setSnake([{ x: 10, y: 10 }]);
|
||||
setDirection({ x: 0, y: -1 });
|
||||
setScore(0);
|
||||
setGameOver(false);
|
||||
setGameRunning(false);
|
||||
generateFood();
|
||||
};
|
||||
|
||||
const startGame = () => {
|
||||
if (!gameRunning && !gameOver) {
|
||||
setGameRunning(true);
|
||||
}
|
||||
};
|
||||
|
||||
const gameLoop = useCallback(() => {
|
||||
setSnake(prevSnake => {
|
||||
const newSnake = [...prevSnake];
|
||||
const head = { ...newSnake[0] };
|
||||
|
||||
head.x += direction.x;
|
||||
head.y += direction.y;
|
||||
|
||||
// 检查边界碰撞
|
||||
if (head.x < 0 || head.x >= gameAreaSize || head.y < 0 || head.y >= gameAreaSize) {
|
||||
setGameOver(true);
|
||||
setGameRunning(false);
|
||||
return prevSnake;
|
||||
}
|
||||
|
||||
// 检查自身碰撞
|
||||
for (const segment of newSnake) {
|
||||
if (head.x === segment.x && head.y === segment.y) {
|
||||
setGameOver(true);
|
||||
setGameRunning(false);
|
||||
return prevSnake;
|
||||
}
|
||||
}
|
||||
|
||||
newSnake.unshift(head);
|
||||
|
||||
// 检查是否吃到食物
|
||||
if (head.x === food.x && head.y === food.y) {
|
||||
setScore(prev => prev + 10);
|
||||
generateFood();
|
||||
} else {
|
||||
newSnake.pop();
|
||||
}
|
||||
|
||||
return newSnake;
|
||||
});
|
||||
}, [direction, food, generateFood]);
|
||||
|
||||
useEffect(() => {
|
||||
if (gameRunning) {
|
||||
gameLoopRef.current = window.setInterval(gameLoop, 200);
|
||||
} else {
|
||||
if (gameLoopRef.current) {
|
||||
window.clearInterval(gameLoopRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (gameLoopRef.current) {
|
||||
window.clearInterval(gameLoopRef.current);
|
||||
}
|
||||
};
|
||||
}, [gameRunning, gameLoop]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (!gameRunning) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
if (direction.y === 0) setDirection({ x: 0, y: -1 });
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (direction.y === 0) setDirection({ x: 0, y: 1 });
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (direction.x === 0) setDirection({ x: -1, y: 0 });
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (direction.x === 0) setDirection({ x: 1, y: 0 });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||
}, [direction, gameRunning]);
|
||||
|
||||
useEffect(() => {
|
||||
generateFood();
|
||||
}, [generateFood]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #38a169',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>🐍 贪吃蛇游戏</h4>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<span style={{ marginRight: '20px' }}>得分: {score}</span>
|
||||
<button
|
||||
onClick={startGame}
|
||||
disabled={gameRunning || gameOver}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
backgroundColor: gameRunning ? '#cbd5e0' : '#38a169',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: gameRunning ? 'not-allowed' : 'pointer',
|
||||
marginRight: '10px'
|
||||
}}
|
||||
>
|
||||
开始游戏
|
||||
</button>
|
||||
<button
|
||||
onClick={resetGame}
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
backgroundColor: '#e53e3e',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{gameOver && (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
backgroundColor: '#fed7d7',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '15px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
💀 游戏结束!最终得分: {score}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: '14px', color: '#666' }}>
|
||||
使用方向键控制蛇的移动
|
||||
</p>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${gameAreaSize}, 1fr)`,
|
||||
gap: '1px',
|
||||
width: '300px',
|
||||
height: '300px',
|
||||
backgroundColor: '#2d3748',
|
||||
padding: '2px'
|
||||
}}>
|
||||
{Array.from({ length: gameAreaSize * gameAreaSize }).map((_, index) => {
|
||||
const x = index % gameAreaSize;
|
||||
const y = Math.floor(index / gameAreaSize);
|
||||
|
||||
const isSnake = snake.some(segment => segment.x === x && segment.y === y);
|
||||
const isFood = food.x === x && food.y === y;
|
||||
const isHead = snake[0] && snake[0].x === x && snake[0].y === y;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: isSnake
|
||||
? (isHead ? '#e53e3e' : '#38a169')
|
||||
: isFood
|
||||
? '#ed8936'
|
||||
: '#f7fafc',
|
||||
transition: 'background-color 0.1s ease'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 主组件
|
||||
const InteractiveGames: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>🎮 交互式游戏演示</h2>
|
||||
<p style={{ color: '#666', marginBottom: '20px' }}>
|
||||
通过小游戏学习React的状态管理、事件处理和副作用处理
|
||||
</p>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))',
|
||||
gap: '20px'
|
||||
}}>
|
||||
<MemoryGame />
|
||||
<TicTacToe />
|
||||
<SnakeGame />
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
padding: '20px',
|
||||
backgroundColor: '#fef5e7',
|
||||
borderRadius: '10px',
|
||||
border: '2px solid #ed8936'
|
||||
}}>
|
||||
<h3>🎯 游戏开发中的React概念</h3>
|
||||
<ul style={{ textAlign: 'left', lineHeight: '1.6' }}>
|
||||
<li><strong>状态管理</strong>: 使用useState管理游戏状态(分数、位置、游戏状态等)</li>
|
||||
<li><strong>副作用处理</strong>: 使用useEffect处理游戏循环、键盘事件监听</li>
|
||||
<li><strong>事件处理</strong>: 处理鼠标点击、键盘输入等用户交互</li>
|
||||
<li><strong>条件渲染</strong>: 根据游戏状态显示不同的UI元素</li>
|
||||
<li><strong>列表渲染</strong>: 渲染游戏网格、卡片列表等重复元素</li>
|
||||
<li><strong>性能优化</strong>: 使用useCallback避免不必要的重新渲染</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InteractiveGames;
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
// JSX示例组件
|
||||
function JSXExample() {
|
||||
// 状态管理
|
||||
const [count, setCount] = useState(0);
|
||||
const [name, setName] = useState('');
|
||||
|
||||
// 事件处理函数
|
||||
const handleIncrement = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
const handleNameChange = (event) => {
|
||||
setName(event.target.value);
|
||||
};
|
||||
|
||||
// JSX语法示例
|
||||
return (
|
||||
<div style={{ padding: '20px', border: '1px solid #ccc', margin: '10px' }}>
|
||||
<h2>JSX 示例</h2>
|
||||
|
||||
{/* 条件渲染 */}
|
||||
{name && <p>你好, {name}!</p>}
|
||||
|
||||
{/* 表单元素 */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="输入你的名字"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
|
||||
{/* 计数器 */}
|
||||
<div>
|
||||
<p>计数: {count}</p>
|
||||
<button onClick={handleIncrement}>增加</button>
|
||||
</div>
|
||||
|
||||
{/* 列表渲染 */}
|
||||
<ul>
|
||||
{[1, 2, 3, 4, 5].map(num => (
|
||||
<li key={num}>项目 {num}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* 样式对象 */}
|
||||
<div style={{
|
||||
backgroundColor: count > 5 ? 'lightgreen' : 'lightblue',
|
||||
padding: '10px',
|
||||
borderRadius: '5px'
|
||||
}}>
|
||||
{count > 5 ? '计数超过5了!' : '继续点击按钮'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JSXExample;
|
|
@ -0,0 +1,389 @@
|
|||
import React, { useState, useMemo, useCallback, memo, useRef, useEffect } from 'react';
|
||||
|
||||
// ========== React性能优化演示 ==========
|
||||
|
||||
// 1. memo优化示例
|
||||
interface ExpensiveChildProps {
|
||||
value: number;
|
||||
onUpdate: (value: number) => void;
|
||||
}
|
||||
|
||||
// 未优化的组件
|
||||
const ExpensiveChildNormal: React.FC<ExpensiveChildProps> = ({ value, onUpdate }) => {
|
||||
console.log('🔄 普通组件重新渲染:', value);
|
||||
|
||||
// 模拟昂贵的计算
|
||||
const expensiveCalculation = () => {
|
||||
let result = 0;
|
||||
for (let i = 0; i < 1000000; i++) {
|
||||
result += i;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const calculatedValue = expensiveCalculation();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
border: '2px solid #e53e3e',
|
||||
borderRadius: '8px',
|
||||
margin: '5px',
|
||||
backgroundColor: '#fed7d7'
|
||||
}}>
|
||||
<h5>❌ 未优化组件</h5>
|
||||
<p>值: {value}</p>
|
||||
<p>计算结果: {calculatedValue}</p>
|
||||
<button onClick={() => onUpdate(value + 1)}>增加</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// memo优化的组件
|
||||
const ExpensiveChildMemo = memo<ExpensiveChildProps>(({ value, onUpdate }) => {
|
||||
console.log('✅ memo组件重新渲染:', value);
|
||||
|
||||
const expensiveCalculation = useMemo(() => {
|
||||
console.log('💚 执行昂贵计算');
|
||||
let result = 0;
|
||||
for (let i = 0; i < 1000000; i++) {
|
||||
result += i;
|
||||
}
|
||||
return result;
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
border: '2px solid #38a169',
|
||||
borderRadius: '8px',
|
||||
margin: '5px',
|
||||
backgroundColor: '#c6f6d5'
|
||||
}}>
|
||||
<h5>✅ memo优化组件</h5>
|
||||
<p>值: {value}</p>
|
||||
<p>计算结果: {expensiveCalculation}</p>
|
||||
<button onClick={() => onUpdate(value + 1)}>增加</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ExpensiveChildMemo.displayName = 'ExpensiveChildMemo';
|
||||
|
||||
const MemoDemo: React.FC = () => {
|
||||
const [count1, setCount1] = useState(0);
|
||||
const [count2, setCount2] = useState(0);
|
||||
const [otherState, setOtherState] = useState(0);
|
||||
|
||||
// 使用useCallback优化回调函数
|
||||
const handleUpdate1 = useCallback((value: number) => {
|
||||
setCount1(value);
|
||||
}, []);
|
||||
|
||||
const handleUpdate2 = useCallback((value: number) => {
|
||||
setCount2(value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #3182ce',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>⚡ React.memo 性能优化演示</h4>
|
||||
|
||||
<button
|
||||
onClick={() => setOtherState(otherState + 1)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#3182ce',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '10px'
|
||||
}}
|
||||
>
|
||||
更新父组件状态 (不影响子组件) - {otherState}
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
<ExpensiveChildNormal value={count1} onUpdate={handleUpdate1} />
|
||||
<ExpensiveChildMemo value={count2} onUpdate={handleUpdate2} />
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '14px', color: '#666', marginTop: '10px' }}>
|
||||
💡 打开控制台查看重新渲染日志。点击"更新父组件状态"按钮时,memo组件不会重新渲染!
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 2. 虚拟滚动演示
|
||||
interface VirtualListProps {
|
||||
items: string[];
|
||||
itemHeight: number;
|
||||
containerHeight: number;
|
||||
}
|
||||
|
||||
const VirtualList: React.FC<VirtualListProps> = ({ items, itemHeight, containerHeight }) => {
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
|
||||
const visibleStart = Math.floor(scrollTop / itemHeight);
|
||||
const visibleEnd = Math.min(
|
||||
visibleStart + Math.ceil(containerHeight / itemHeight) + 1,
|
||||
items.length
|
||||
);
|
||||
|
||||
const visibleItems = items.slice(visibleStart, visibleEnd);
|
||||
const totalHeight = items.length * itemHeight;
|
||||
const offsetY = visibleStart * itemHeight;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #805ad5',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>📜 虚拟滚动演示</h4>
|
||||
<p>总共 {items.length} 个项目,只渲染可见的 {visibleItems.length} 个</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: containerHeight,
|
||||
overflow: 'auto',
|
||||
border: '1px solid #ccc',
|
||||
position: 'relative'
|
||||
}}
|
||||
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
|
||||
>
|
||||
<div style={{ height: totalHeight, position: 'relative' }}>
|
||||
<div style={{ transform: `translateY(${offsetY}px)` }}>
|
||||
{visibleItems.map((item, index) => (
|
||||
<div
|
||||
key={visibleStart + index}
|
||||
style={{
|
||||
height: itemHeight,
|
||||
padding: '10px',
|
||||
borderBottom: '1px solid #eee',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: (visibleStart + index) % 2 === 0 ? '#f9f9f9' : 'white'
|
||||
}}
|
||||
>
|
||||
第 {visibleStart + index + 1} 项: {item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 3. 防抖搜索演示
|
||||
const useDebounce = <T,>(value: T, delay: number): T => {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
|
||||
const DebounceSearchDemo: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchCount, setSearchCount] = useState(0);
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearchTerm) {
|
||||
setSearchCount(prev => prev + 1);
|
||||
console.log('🔍 执行搜索:', debouncedSearchTerm);
|
||||
}
|
||||
}, [debouncedSearchTerm]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #ed8936',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>🔍 防抖搜索演示</h4>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="输入搜索内容..."
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
width: '200px'
|
||||
}}
|
||||
/>
|
||||
<p>搜索词: {debouncedSearchTerm}</p>
|
||||
<p>搜索次数: {searchCount}</p>
|
||||
<p style={{ fontSize: '14px', color: '#666' }}>
|
||||
💡 输入会延迟500ms后触发搜索,减少不必要的API调用
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 4. 图片懒加载演示
|
||||
interface LazyImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const LazyImage: React.FC<LazyImageProps> = ({ src, alt, width, height }) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isInView, setIsInView] = useState(false);
|
||||
const imgRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsInView(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
if (imgRef.current) {
|
||||
observer.observe(imgRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={imgRef}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
backgroundColor: '#f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '10px',
|
||||
border: '1px solid #ddd',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{!isInView ? (
|
||||
<div>📷 图片占位符</div>
|
||||
) : !isLoaded ? (
|
||||
<div>🔄 加载中...</div>
|
||||
) : null}
|
||||
|
||||
{isInView && (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
display: isLoaded ? 'block' : 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LazyLoadDemo: React.FC = () => {
|
||||
const images = [
|
||||
'https://picsum.photos/200/150?random=1',
|
||||
'https://picsum.photos/200/150?random=2',
|
||||
'https://picsum.photos/200/150?random=3',
|
||||
'https://picsum.photos/200/150?random=4',
|
||||
'https://picsum.photos/200/150?random=5',
|
||||
'https://picsum.photos/200/150?random=6'
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
border: '2px solid #38a169',
|
||||
borderRadius: '8px',
|
||||
margin: '10px'
|
||||
}}>
|
||||
<h4>🖼️ 图片懒加载演示</h4>
|
||||
<p>向下滚动查看图片懒加载效果</p>
|
||||
<div style={{ height: '300px', overflow: 'auto' }}>
|
||||
{images.map((src, index) => (
|
||||
<LazyImage
|
||||
key={index}
|
||||
src={src}
|
||||
alt={`懒加载图片 ${index + 1}`}
|
||||
width={200}
|
||||
height={150}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 主组件
|
||||
const PerformanceDemo: React.FC = () => {
|
||||
// 生成大量测试数据
|
||||
const largeDataSet = useMemo(() => {
|
||||
return Array.from({ length: 10000 }, (_, i) => `数据项 ${i + 1}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>⚡ React性能优化演示</h2>
|
||||
|
||||
<MemoDemo />
|
||||
<VirtualList
|
||||
items={largeDataSet}
|
||||
itemHeight={60}
|
||||
containerHeight={300}
|
||||
/>
|
||||
<DebounceSearchDemo />
|
||||
<LazyLoadDemo />
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
padding: '20px',
|
||||
backgroundColor: '#f0fff4',
|
||||
borderRadius: '10px',
|
||||
border: '2px solid #38a169'
|
||||
}}>
|
||||
<h3>⚡ 性能优化技巧总结</h3>
|
||||
<ul style={{ textAlign: 'left', lineHeight: '1.6' }}>
|
||||
<li><strong>React.memo</strong>: 避免不必要的组件重新渲染</li>
|
||||
<li><strong>useMemo</strong>: 缓存昂贵的计算结果</li>
|
||||
<li><strong>useCallback</strong>: 缓存函数引用,避免子组件重新渲染</li>
|
||||
<li><strong>虚拟滚动</strong>: 处理大量数据时只渲染可见项</li>
|
||||
<li><strong>防抖(Debounce)</strong>: 减少频繁操作的执行次数</li>
|
||||
<li><strong>懒加载</strong>: 延迟加载非关键资源</li>
|
||||
<li><strong>代码分割</strong>: 使用React.lazy和Suspense按需加载</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformanceDemo;
|
|
@ -0,0 +1,250 @@
|
|||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
|
||||
// ========== React 设计思想示例 ==========
|
||||
|
||||
// 1. 纯函数组件 - 相同输入永远产生相同输出
|
||||
interface UserProps {
|
||||
name: string;
|
||||
age: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const PureUserCard: React.FC<UserProps> = ({ name, age, isActive }) => {
|
||||
// 纯函数:给定相同的props,总是返回相同的JSX
|
||||
return (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
border: isActive ? '2px solid green' : '2px solid gray',
|
||||
margin: '5px',
|
||||
borderRadius: '5px'
|
||||
}}>
|
||||
<h4>{name}</h4>
|
||||
<p>年龄: {age}</p>
|
||||
<span>{isActive ? '🟢 在线' : '⚫ 离线'}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 2. 单向数据流示例
|
||||
interface CounterProps {
|
||||
value: number;
|
||||
onIncrement: () => void;
|
||||
onDecrement: () => void;
|
||||
}
|
||||
|
||||
const Counter: React.FC<CounterProps> = ({ value, onIncrement, onDecrement }) => {
|
||||
// 数据只能从父组件流入,事件通过回调函数流出
|
||||
return (
|
||||
<div style={{ padding: '10px', border: '1px solid #ccc', margin: '10px' }}>
|
||||
<h3>计数器: {value}</h3>
|
||||
<button onClick={onDecrement}>-</button>
|
||||
<span style={{ margin: '0 10px' }}>{value}</span>
|
||||
<button onClick={onIncrement}>+</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 3. 不可变性 - 状态更新通过创建新对象
|
||||
interface TodoItem {
|
||||
id: number;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
const TodoList: React.FC = () => {
|
||||
const [todos, setTodos] = useState<TodoItem[]>([
|
||||
{ id: 1, text: '学习React', completed: false },
|
||||
{ id: 2, text: '理解设计思想', completed: false }
|
||||
]);
|
||||
|
||||
// 不可变更新:创建新数组而不是修改原数组
|
||||
const toggleTodo = useCallback((id: number) => {
|
||||
setTodos(prevTodos =>
|
||||
prevTodos.map(todo =>
|
||||
todo.id === id
|
||||
? { ...todo, completed: !todo.completed } // 创建新对象
|
||||
: todo
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const addTodo = useCallback((text: string) => {
|
||||
const newTodo: TodoItem = {
|
||||
id: Date.now(),
|
||||
text,
|
||||
completed: false
|
||||
};
|
||||
setTodos(prevTodos => [...prevTodos, newTodo]); // 创建新数组
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '15px', border: '2px solid #007bff', margin: '10px' }}>
|
||||
<h3>待办事项 (不可变性示例)</h3>
|
||||
{todos.map(todo => (
|
||||
<div key={todo.id} style={{ margin: '5px 0' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={todo.completed}
|
||||
onChange={() => toggleTodo(todo.id)}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
textDecoration: todo.completed ? 'line-through' : 'none',
|
||||
marginLeft: '8px'
|
||||
}}
|
||||
>
|
||||
{todo.text}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => addTodo(`新任务 ${todos.length + 1}`)}
|
||||
style={{ marginTop: '10px' }}
|
||||
>
|
||||
添加任务
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 4. 组合优于继承 - 通过组合构建复杂UI
|
||||
interface WithLoadingProps {
|
||||
isLoading: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const WithLoading: React.FC<WithLoadingProps> = ({ isLoading, children }) => {
|
||||
if (isLoading) {
|
||||
return <div>🔄 加载中...</div>;
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const DataFetcher: React.FC = () => {
|
||||
const [data, setData] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟数据获取
|
||||
setTimeout(() => {
|
||||
setData(['React', 'Vue', 'Angular']);
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WithLoading isLoading={loading}>
|
||||
<div style={{ padding: '15px', border: '2px solid #28a745', margin: '10px' }}>
|
||||
<h3>框架列表</h3>
|
||||
<ul>
|
||||
{data.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</WithLoading>
|
||||
);
|
||||
};
|
||||
|
||||
// 5. 记忆化和性能优化
|
||||
const ExpensiveCalculation: React.FC<{ numbers: number[] }> = ({ numbers }) => {
|
||||
// 使用useMemo避免不必要的重计算
|
||||
const sum = useMemo(() => {
|
||||
console.log('🔄 重新计算总和...');
|
||||
return numbers.reduce((acc, num) => acc + num, 0);
|
||||
}, [numbers]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '10px', backgroundColor: '#f8f9fa', margin: '10px' }}>
|
||||
<h4>性能优化示例</h4>
|
||||
<p>数字列表: {numbers.join(', ')}</p>
|
||||
<p>总和: {sum}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 主组件 - 展示React设计思想
|
||||
const ReactVsVueExamples: React.FC = () => {
|
||||
const [count, setCount] = useState(0);
|
||||
const [numbers, setNumbers] = useState([1, 2, 3]);
|
||||
|
||||
// 回调函数 - 单向数据流
|
||||
const handleIncrement = useCallback(() => {
|
||||
setCount(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
const handleDecrement = useCallback(() => {
|
||||
setCount(prev => prev - 1);
|
||||
}, []);
|
||||
|
||||
const addRandomNumber = useCallback(() => {
|
||||
const randomNum = Math.floor(Math.random() * 100);
|
||||
setNumbers(prev => [...prev, randomNum]);
|
||||
}, []);
|
||||
|
||||
const users: UserProps[] = [
|
||||
{ name: '张三', age: 25, isActive: true },
|
||||
{ name: '李四', age: 30, isActive: false },
|
||||
{ name: '王五', age: 28, isActive: true }
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>🚀 React 设计思想示例</h2>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>1. 纯函数组件</h3>
|
||||
<p>相同的props总是产生相同的输出:</p>
|
||||
{users.map((user, index) => (
|
||||
<PureUserCard key={index} {...user} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>2. 单向数据流</h3>
|
||||
<p>数据从父组件流向子组件,事件通过回调函数向上传递:</p>
|
||||
<Counter
|
||||
value={count}
|
||||
onIncrement={handleIncrement}
|
||||
onDecrement={handleDecrement}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>3. 不可变性</h3>
|
||||
<TodoList />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>4. 组合优于继承</h3>
|
||||
<DataFetcher />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3>5. 性能优化 (useMemo)</h3>
|
||||
<ExpensiveCalculation numbers={numbers} />
|
||||
<button onClick={addRandomNumber}>
|
||||
添加随机数字
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
padding: '15px',
|
||||
backgroundColor: '#e3f2fd',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<h3>💡 React设计思想总结</h3>
|
||||
<ul>
|
||||
<li><strong>函数式编程</strong>:组件是纯函数,可预测且易测试</li>
|
||||
<li><strong>单向数据流</strong>:数据流向清晰,便于调试</li>
|
||||
<li><strong>不可变性</strong>:通过创建新对象来更新状态</li>
|
||||
<li><strong>组合式架构</strong>:通过组合小组件构建复杂UI</li>
|
||||
<li><strong>显式优化</strong>:开发者需要手动进行性能优化</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactVsVueExamples;
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
// 定义接口(类型)
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
initialCount?: number; // 可选属性
|
||||
}
|
||||
|
||||
// TSX示例组件
|
||||
const TSXExample: React.FC<Props> = ({ title, initialCount = 0 }) => {
|
||||
// 带类型的状态管理
|
||||
const [count, setCount] = useState<number>(initialCount);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
|
||||
// 带类型的事件处理函数
|
||||
const handleIncrement = (): void => {
|
||||
setCount(prevCount => prevCount + 1);
|
||||
};
|
||||
|
||||
const handleAddUser = (): void => {
|
||||
const newUser: User = {
|
||||
id: users.length + 1,
|
||||
name: `用户${users.length + 1}`,
|
||||
email: `user${users.length + 1}@example.com`,
|
||||
age: Math.floor(Math.random() * 50) + 18
|
||||
};
|
||||
setUsers(prevUsers => [...prevUsers, newUser]);
|
||||
};
|
||||
|
||||
const handleSelectUser = (selectedUser: User): void => {
|
||||
setUser(selectedUser);
|
||||
};
|
||||
|
||||
// 输入框变化处理
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
console.log(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
border: '2px solid #007bff',
|
||||
margin: '10px',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<h2>{title}</h2>
|
||||
|
||||
{/* 类型安全的计数器 */}
|
||||
<div>
|
||||
<p>计数: {count}</p>
|
||||
<button onClick={handleIncrement}>增加</button>
|
||||
</div>
|
||||
|
||||
{/* 用户管理 */}
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<button onClick={handleAddUser}>添加用户</button>
|
||||
|
||||
{/* 条件渲染当前选中的用户 */}
|
||||
{user && (
|
||||
<div style={{
|
||||
backgroundColor: '#f8f9fa',
|
||||
padding: '10px',
|
||||
margin: '10px 0',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
<h4>选中的用户:</h4>
|
||||
<p>姓名: {user.name}</p>
|
||||
<p>邮箱: {user.email}</p>
|
||||
<p>年龄: {user.age}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户列表 */}
|
||||
<ul>
|
||||
{users.map((userItem: User) => (
|
||||
<li
|
||||
key={userItem.id}
|
||||
onClick={() => handleSelectUser(userItem)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '5px',
|
||||
marginBottom: '5px',
|
||||
backgroundColor: user?.id === userItem.id ? '#e7f3ff' : 'transparent'
|
||||
}}
|
||||
>
|
||||
{userItem.name} ({userItem.age}岁)
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 类型安全的输入框 */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="带类型的输入框"
|
||||
onChange={handleInputChange}
|
||||
style={{ marginTop: '10px', padding: '5px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TSXExample;
|
|
@ -0,0 +1,297 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
// ========== 模拟Vue设计思想的React实现 ==========
|
||||
// 注意:这里是用React语法来展示Vue的设计理念
|
||||
|
||||
// 1. 模拟Vue的响应式数据绑定概念
|
||||
const VueStyleReactivity: React.FC = () => {
|
||||
// 在Vue中,这种数据变化会自动触发视图更新,无需手动优化
|
||||
const [message, setMessage] = useState('Hello Vue!');
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// Vue风格:数据变化自动反映到视图
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setMessage(e.target.value); // Vue中通过v-model实现双向绑定
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '15px', border: '2px solid #4FC08D', margin: '10px' }}>
|
||||
<h3>📱 响应式数据绑定 (Vue风格概念)</h3>
|
||||
|
||||
{/* 模拟Vue模板语法 */}
|
||||
<div>
|
||||
<p>消息: {message}</p>
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={handleInputChange}
|
||||
placeholder="修改消息 (类似v-model)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '15px' }}>
|
||||
<p>计数: {count}</p>
|
||||
<button onClick={() => setCount(count + 1)}>
|
||||
点击增加 (类似@click)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '15px', backgroundColor: '#f0f8f0', padding: '10px' }}>
|
||||
<h4>💡 Vue设计理念体现:</h4>
|
||||
<ul>
|
||||
<li>数据变化自动更新视图 (响应式)</li>
|
||||
<li>简单的双向数据绑定</li>
|
||||
<li>声明式的事件处理</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 2. 模拟Vue的模板指令概念
|
||||
const VueStyleDirectives: React.FC = () => {
|
||||
const [showElement, setShowElement] = useState(true);
|
||||
const [items, setItems] = useState(['Apple', 'Banana', 'Orange']);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const addItem = () => {
|
||||
if (inputValue.trim()) {
|
||||
setItems([...items, inputValue]);
|
||||
setInputValue('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '15px', border: '2px solid #4FC08D', margin: '10px' }}>
|
||||
<h3>🎯 模板指令风格 (Vue概念)</h3>
|
||||
|
||||
{/* 模拟 v-if */}
|
||||
<div>
|
||||
<button onClick={() => setShowElement(!showElement)}>
|
||||
{showElement ? '隐藏' : '显示'} 元素 (类似v-if)
|
||||
</button>
|
||||
{showElement && (
|
||||
<p style={{ color: 'green' }}>🎉 这个元素是条件渲染的!</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 模拟 v-for */}
|
||||
<div style={{ marginTop: '15px' }}>
|
||||
<h4>列表渲染 (类似v-for):</h4>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="添加新项目"
|
||||
/>
|
||||
<button onClick={addItem}>添加</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '15px', backgroundColor: '#f0f8f0', padding: '10px' }}>
|
||||
<h4>💡 Vue模板语法特点:</h4>
|
||||
<ul>
|
||||
<li>v-if: 条件渲染</li>
|
||||
<li>v-for: 列表渲染</li>
|
||||
<li>v-model: 双向数据绑定</li>
|
||||
<li>@click: 事件监听</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 3. 模拟Vue的计算属性概念
|
||||
const VueStyleComputed: React.FC = () => {
|
||||
const [firstName, setFirstName] = useState('张');
|
||||
const [lastName, setLastName] = useState('三');
|
||||
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
|
||||
|
||||
// 模拟Vue的computed属性 - 在Vue中这会自动缓存和响应式更新
|
||||
const fullName = `${firstName} ${lastName}`; // Vue: computed: { fullName() { ... } }
|
||||
const sum = numbers.reduce((acc, num) => acc + num, 0); // Vue会自动优化这种计算
|
||||
|
||||
return (
|
||||
<div style={{ padding: '15px', border: '2px solid #4FC08D', margin: '10px' }}>
|
||||
<h3>⚡ 计算属性概念 (Vue风格)</h3>
|
||||
|
||||
<div>
|
||||
<label>姓: </label>
|
||||
<input
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<label>名: </label>
|
||||
<input
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '15px' }}>
|
||||
<p><strong>全名 (计算属性): {fullName}</strong></p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '15px' }}>
|
||||
<p>数字列表: {numbers.join(', ')}</p>
|
||||
<p><strong>总和 (计算属性): {sum}</strong></p>
|
||||
<button onClick={() => setNumbers([...numbers, Math.floor(Math.random() * 10)])}>
|
||||
添加随机数
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '15px', backgroundColor: '#f0f8f0', padding: '10px' }}>
|
||||
<h4>💡 Vue计算属性特点:</h4>
|
||||
<ul>
|
||||
<li>基于依赖自动更新</li>
|
||||
<li>自动缓存结果</li>
|
||||
<li>声明式定义</li>
|
||||
<li>无需手动优化</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 4. 模拟Vue的生命周期概念
|
||||
const VueStyleLifecycle: React.FC = () => {
|
||||
const [data, setData] = useState<string>('');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// 模拟Vue的mounted生命周期
|
||||
useEffect(() => {
|
||||
console.log('🔄 组件挂载了 (类似Vue的mounted)');
|
||||
setMounted(true);
|
||||
|
||||
// 模拟数据获取
|
||||
setTimeout(() => {
|
||||
setData('从API获取的数据');
|
||||
}, 1000);
|
||||
|
||||
// 模拟Vue的beforeUnmount
|
||||
return () => {
|
||||
console.log('🔄 组件即将卸载 (类似Vue的beforeUnmount)');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '15px', border: '2px solid #4FC08D', margin: '10px' }}>
|
||||
<h3>🔄 生命周期概念 (Vue风格)</h3>
|
||||
|
||||
<div>
|
||||
<p>挂载状态: {mounted ? '✅ 已挂载' : '⏳ 挂载中'}</p>
|
||||
<p>数据: {data || '⏳ 加载中...'}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '15px', backgroundColor: '#f0f8f0', padding: '10px' }}>
|
||||
<h4>💡 Vue生命周期特点:</h4>
|
||||
<ul>
|
||||
<li>created: 实例创建后</li>
|
||||
<li>mounted: DOM挂载后</li>
|
||||
<li>updated: 数据更新后</li>
|
||||
<li>beforeUnmount: 卸载前</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 5. 模拟Vue的组件通信概念
|
||||
interface ChildProps {
|
||||
message: string;
|
||||
onUpdateMessage: (newMessage: string) => void;
|
||||
}
|
||||
|
||||
const VueStyleChild: React.FC<ChildProps> = ({ message, onUpdateMessage }) => {
|
||||
return (
|
||||
<div style={{ padding: '10px', backgroundColor: '#e8f5e8', border: '1px solid #4FC08D' }}>
|
||||
<h4>子组件</h4>
|
||||
<p>从父组件接收: {message}</p>
|
||||
<button onClick={() => onUpdateMessage('来自子组件的消息')}>
|
||||
向父组件发送消息 (类似$emit)
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VueStyleParent: React.FC = () => {
|
||||
const [parentMessage, setParentMessage] = useState('来自父组件的消息');
|
||||
|
||||
return (
|
||||
<div style={{ padding: '15px', border: '2px solid #4FC08D', margin: '10px' }}>
|
||||
<h3>👨👩👧👦 组件通信 (Vue风格)</h3>
|
||||
|
||||
<div>
|
||||
<h4>父组件</h4>
|
||||
<p>当前消息: {parentMessage}</p>
|
||||
<VueStyleChild
|
||||
message={parentMessage}
|
||||
onUpdateMessage={setParentMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '15px', backgroundColor: '#f0f8f0', padding: '10px' }}>
|
||||
<h4>💡 Vue组件通信特点:</h4>
|
||||
<ul>
|
||||
<li>props down: 父到子传递数据</li>
|
||||
<li>events up: 子到父发送事件</li>
|
||||
<li>v-model: 双向绑定语法糖</li>
|
||||
<li>provide/inject: 跨层级通信</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 主组件 - 展示Vue设计思想概念
|
||||
const VueStyleExamples: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>🌟 Vue设计思想概念展示</h2>
|
||||
<p style={{
|
||||
backgroundColor: '#fffbe6',
|
||||
padding: '10px',
|
||||
borderLeft: '4px solid #4FC08D',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<strong>注意:</strong>以下示例用React语法展示Vue的设计理念,
|
||||
帮助理解两个框架的不同思维方式。
|
||||
</p>
|
||||
|
||||
<VueStyleReactivity />
|
||||
<VueStyleDirectives />
|
||||
<VueStyleComputed />
|
||||
<VueStyleLifecycle />
|
||||
<VueStyleParent />
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
padding: '15px',
|
||||
backgroundColor: '#f0f8f0',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<h3>🌟 Vue设计思想总结</h3>
|
||||
<ul>
|
||||
<li><strong>响应式系统</strong>:数据变化自动更新视图</li>
|
||||
<li><strong>模板语法</strong>:声明式的HTML扩展语法</li>
|
||||
<li><strong>渐进式</strong>:可以逐步集成到现有项目</li>
|
||||
<li><strong>约定优于配置</strong>:提供合理的默认值</li>
|
||||
<li><strong>开发者友好</strong>:降低学习曲线,提高开发效率</li>
|
||||
<li><strong>自动优化</strong>:框架层面的性能优化</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VueStyleExamples;
|
|
@ -4,7 +4,9 @@ import './index.css';
|
|||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
|
@ -0,0 +1,275 @@
|
|||
# Vue vs React 设计思想对比
|
||||
|
||||
## 🎯 核心设计理念
|
||||
|
||||
### React 设计思想
|
||||
**"函数式编程 + 单向数据流"**
|
||||
|
||||
React将UI视为状态的函数:`UI = f(state)`
|
||||
|
||||
**核心理念:**
|
||||
- 组件是纯函数,给定相同的props和state,永远返回相同的结果
|
||||
- 数据单向流动,从父组件流向子组件
|
||||
- 不可变性(Immutability)- 状态变化通过创建新对象实现
|
||||
- 声明式编程 - 描述"是什么"而不是"怎么做"
|
||||
|
||||
### Vue 设计思想
|
||||
**"渐进式框架 + 响应式系统"**
|
||||
|
||||
Vue强调渐进式增强和开发者友好性
|
||||
|
||||
**核心理念:**
|
||||
- 响应式数据绑定 - 数据变化自动更新视图
|
||||
- 模板语法 - 更接近HTML的声明式模板
|
||||
- 渐进式 - 可以部分采用,不需要重写整个应用
|
||||
- 约定优于配置 - 提供合理的默认值和约定
|
||||
|
||||
## 🏗️ 架构设计对比
|
||||
|
||||
### React 架构特点
|
||||
|
||||
```jsx
|
||||
// React: 函数式组件 + Hooks
|
||||
function UserProfile({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser(userId).then(userData => {
|
||||
setUser(userData);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
if (loading) return <div>加载中...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{user.name}</h2>
|
||||
<p>{user.email}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**特点:**
|
||||
- 显式状态管理
|
||||
- 手动优化渲染
|
||||
- 函数式编程范式
|
||||
- 灵活但需要更多样板代码
|
||||
|
||||
### Vue 架构特点
|
||||
|
||||
```vue
|
||||
<!-- Vue: 单文件组件 + 选项式API -->
|
||||
<template>
|
||||
<div v-if="loading">加载中...</div>
|
||||
<div v-else>
|
||||
<h2>{{ user.name }}</h2>
|
||||
<p>{{ user.email }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['userId'],
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
userId: {
|
||||
immediate: true,
|
||||
handler(newId) {
|
||||
this.fetchUserData(newId);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchUserData(id) {
|
||||
this.loading = true;
|
||||
this.user = await fetchUser(id);
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**特点:**
|
||||
- 自动响应式更新
|
||||
- 模板语法直观
|
||||
- 约定式结构
|
||||
- 更少的样板代码
|
||||
|
||||
## 📊 具体差异对比
|
||||
|
||||
| 维度 | React | Vue |
|
||||
|------|-------|-----|
|
||||
| **学习曲线** | 陡峭,需要理解函数式编程 | 平缓,更接近传统前端开发 |
|
||||
| **数据绑定** | 单向数据流,手动处理 | 双向数据绑定,自动响应式 |
|
||||
| **模板语法** | JSX (JavaScript in HTML) | 模板语法 (HTML + 指令) |
|
||||
| **状态管理** | 显式,需要useState/Redux | 隐式,响应式系统 |
|
||||
| **组件通信** | Props + 回调函数 | Props + 事件 + v-model |
|
||||
| **性能优化** | 手动优化 (memo, useMemo) | 自动优化 (响应式依赖追踪) |
|
||||
| **生态系统** | 庞大但分散 | 官方维护,更集中 |
|
||||
| **灵活性** | 极高,函数式编程 | 中等,约定优于配置 |
|
||||
|
||||
## 🔄 数据流设计
|
||||
|
||||
### React: 单向数据流
|
||||
|
||||
```jsx
|
||||
// 父组件
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Counter
|
||||
value={count}
|
||||
onChange={setCount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 子组件
|
||||
function Counter({ value, onChange }) {
|
||||
return (
|
||||
<div>
|
||||
<p>计数: {value}</p>
|
||||
<button onClick={() => onChange(value + 1)}>
|
||||
增加
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**特点:**
|
||||
- 数据只能从父组件流向子组件
|
||||
- 子组件通过回调函数通知父组件
|
||||
- 数据流向清晰可追踪
|
||||
|
||||
### Vue: 响应式双向绑定
|
||||
|
||||
```vue
|
||||
<!-- 父组件 -->
|
||||
<template>
|
||||
<div>
|
||||
<Counter v-model="count" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return { count: 0 }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 子组件 -->
|
||||
<template>
|
||||
<div>
|
||||
<p>计数: {{ modelValue }}</p>
|
||||
<button @click="increment">增加</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
increment() {
|
||||
this.$emit('update:modelValue', this.modelValue + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**特点:**
|
||||
- 支持双向数据绑定
|
||||
- 响应式系统自动追踪依赖
|
||||
- 语法糖简化父子组件通信
|
||||
|
||||
## 🎨 开发体验对比
|
||||
|
||||
### React 开发特点
|
||||
|
||||
**优势:**
|
||||
- 函数式编程,逻辑清晰
|
||||
- 组合优于继承
|
||||
- 测试友好
|
||||
- 社区生态丰富
|
||||
|
||||
**挑战:**
|
||||
- 需要理解函数式编程概念
|
||||
- 状态管理复杂项目需要额外工具
|
||||
- 性能优化需要手动处理
|
||||
|
||||
### Vue 开发特点
|
||||
|
||||
**优势:**
|
||||
- 学习成本低,易于上手
|
||||
- 官方工具链完整
|
||||
- 模板语法直观
|
||||
- 自动性能优化
|
||||
|
||||
**挑战:**
|
||||
- 大型项目可能缺乏约束
|
||||
- 模板语法限制了一些高级用法
|
||||
- TypeScript支持相对较晚
|
||||
|
||||
## 🚀 适用场景
|
||||
|
||||
### 选择React的场景:
|
||||
- **大型复杂应用**:需要精细控制和性能优化
|
||||
- **团队有函数式编程经验**:能充分利用React的设计理念
|
||||
- **需要丰富生态**:React社区更大,第三方库更多
|
||||
- **移动端开发**:React Native生态成熟
|
||||
|
||||
### 选择Vue的场景:
|
||||
- **快速原型开发**:Vue的学习曲线更平缓
|
||||
- **团队技术栈偏传统**:更容易从jQuery等过渡
|
||||
- **中小型项目**:Vue的约定能提高开发效率
|
||||
- **注重开发体验**:Vue的工具链更完整统一
|
||||
|
||||
## 🔮 设计思想演进
|
||||
|
||||
### React的演进:
|
||||
1. **类组件时代**:面向对象编程
|
||||
2. **Hooks时代**:函数式编程回归
|
||||
3. **并发模式**:性能和用户体验优化
|
||||
4. **Server Components**:全栈React
|
||||
|
||||
### Vue的演进:
|
||||
1. **Vue 1.x**:简单的数据绑定
|
||||
2. **Vue 2.x**:组件化 + 虚拟DOM
|
||||
3. **Vue 3.x**:Composition API + 更好的TypeScript支持
|
||||
4. **渐进式增强**:从简单到复杂的平滑过渡
|
||||
|
||||
## 💭 设计哲学总结
|
||||
|
||||
### React哲学:
|
||||
> "学会一次,随处编写" - 通过函数式编程原则构建可预测的UI
|
||||
|
||||
- 显式优于隐式
|
||||
- 组合优于继承
|
||||
- 不可变性带来可预测性
|
||||
- 单一数据源
|
||||
|
||||
### Vue哲学:
|
||||
> "渐进式框架" - 让开发者能够渐进式地采用和学习
|
||||
|
||||
- 约定优于配置
|
||||
- 开发者体验优先
|
||||
- 性能优化自动化
|
||||
- 灵活性与易用性平衡
|
||||
|
||||
两个框架都很优秀,选择取决于团队背景、项目需求和个人偏好。React更适合大型团队和复杂项目,Vue更适合快速开发和团队技术栈迁移。
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es6"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue