@mx-space/api-client 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintcache +1 -1
- package/LICENSE +661 -0
- package/controllers/recently.ts +24 -3
- package/dist/index.cjs +18 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/index.min.cjs +1 -1
- package/dist/index.min.cjs.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.umd.js +18 -0
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/esm/adaptors/axios.d.ts +4 -0
- package/esm/adaptors/axios.js +37 -0
- package/esm/adaptors/axios.js.map +1 -0
- package/esm/adaptors/ky.d.ts +23 -0
- package/esm/adaptors/ky.js +62 -0
- package/esm/adaptors/ky.js.map +1 -0
- package/esm/adaptors/umi-request.d.ts +4 -0
- package/esm/adaptors/umi-request.js +31 -0
- package/esm/adaptors/umi-request.js.map +1 -0
- package/esm/controllers/aggregate.d.ts +64 -0
- package/esm/controllers/aggregate.js +42 -0
- package/esm/controllers/aggregate.js.map +1 -0
- package/esm/controllers/base.d.ts +43 -0
- package/esm/controllers/base.js +27 -0
- package/esm/controllers/base.js.map +1 -0
- package/esm/controllers/category.d.ts +114 -0
- package/esm/controllers/category.js +68 -0
- package/esm/controllers/category.js.map +1 -0
- package/esm/controllers/comment.d.ts +81 -0
- package/esm/controllers/comment.js +47 -0
- package/esm/controllers/comment.js.map +1 -0
- package/esm/controllers/index.d.ts +18 -0
- package/esm/controllers/index.js +55 -0
- package/esm/controllers/index.js.map +1 -0
- package/esm/controllers/link.d.ts +20 -0
- package/esm/controllers/link.js +21 -0
- package/esm/controllers/link.js.map +1 -0
- package/esm/controllers/note.d.ts +123 -0
- package/esm/controllers/note.js +68 -0
- package/esm/controllers/note.js.map +1 -0
- package/esm/controllers/page.d.ts +59 -0
- package/esm/controllers/page.js +42 -0
- package/esm/controllers/page.js.map +1 -0
- package/esm/controllers/post.d.ts +72 -0
- package/esm/controllers/post.js +52 -0
- package/esm/controllers/post.js.map +1 -0
- package/esm/controllers/project.d.ts +15 -0
- package/esm/controllers/project.js +12 -0
- package/esm/controllers/project.js.map +1 -0
- package/esm/controllers/recently.d.ts +110 -0
- package/esm/controllers/recently.js +49 -0
- package/esm/controllers/recently.js.map +1 -0
- package/esm/controllers/say.d.ts +37 -0
- package/esm/controllers/say.js +21 -0
- package/esm/controllers/say.js.map +1 -0
- package/esm/controllers/search.d.ts +247 -0
- package/esm/controllers/search.js +28 -0
- package/esm/controllers/search.js.map +1 -0
- package/esm/controllers/severless.d.ts +24 -0
- package/esm/controllers/severless.js +16 -0
- package/esm/controllers/severless.js.map +1 -0
- package/esm/controllers/snippet.d.ts +24 -0
- package/esm/controllers/snippet.js +19 -0
- package/esm/controllers/snippet.js.map +1 -0
- package/esm/controllers/topic.d.ts +26 -0
- package/esm/controllers/topic.js +18 -0
- package/esm/controllers/topic.js.map +1 -0
- package/esm/controllers/user.d.ts +70 -0
- package/esm/controllers/user.js +40 -0
- package/esm/controllers/user.js.map +1 -0
- package/esm/core/attach-request.d.ts +2 -0
- package/esm/core/attach-request.js +36 -0
- package/esm/core/attach-request.js.map +1 -0
- package/esm/core/client.d.ts +29 -0
- package/esm/core/client.js +186 -0
- package/esm/core/client.js.map +1 -0
- package/esm/core/error.d.ts +6 -0
- package/esm/core/error.js +9 -0
- package/esm/core/error.js.map +1 -0
- package/esm/core/index.d.ts +2 -0
- package/esm/core/index.js +3 -0
- package/esm/core/index.js.map +1 -0
- package/esm/dtos/comment.d.ts +6 -0
- package/esm/dtos/comment.js +3 -0
- package/esm/dtos/comment.js.map +1 -0
- package/esm/index.d.ts +7 -0
- package/esm/index.js +8 -0
- package/esm/index.js.map +1 -0
- package/esm/interfaces/adapter.d.ts +14 -0
- package/esm/interfaces/adapter.js +2 -0
- package/esm/interfaces/adapter.js.map +1 -0
- package/esm/interfaces/client.d.ts +13 -0
- package/esm/interfaces/client.js +2 -0
- package/esm/interfaces/client.js.map +1 -0
- package/esm/interfaces/controller.d.ts +4 -0
- package/esm/interfaces/controller.js +2 -0
- package/esm/interfaces/controller.js.map +1 -0
- package/esm/interfaces/instance.d.ts +7 -0
- package/esm/interfaces/instance.js +2 -0
- package/esm/interfaces/instance.js.map +1 -0
- package/esm/interfaces/options.d.ts +1 -0
- package/esm/interfaces/options.js +2 -0
- package/esm/interfaces/options.js.map +1 -0
- package/esm/interfaces/params.d.ts +4 -0
- package/esm/interfaces/params.js +2 -0
- package/esm/interfaces/params.js.map +1 -0
- package/esm/interfaces/request.d.ts +43 -0
- package/esm/interfaces/request.js +2 -0
- package/esm/interfaces/request.js.map +1 -0
- package/esm/interfaces/types.d.ts +2 -0
- package/esm/interfaces/types.js +2 -0
- package/esm/interfaces/types.js.map +1 -0
- package/esm/models/aggregate.d.ts +53 -0
- package/esm/models/aggregate.js +6 -0
- package/esm/models/aggregate.js.map +1 -0
- package/esm/models/base.d.ts +41 -0
- package/esm/models/base.js +2 -0
- package/esm/models/base.js.map +1 -0
- package/esm/models/category.d.ts +22 -0
- package/esm/models/category.js +6 -0
- package/esm/models/category.js.map +1 -0
- package/esm/models/comment.d.ts +37 -0
- package/esm/models/comment.js +13 -0
- package/esm/models/comment.js.map +1 -0
- package/esm/models/index.d.ts +14 -0
- package/esm/models/index.js +15 -0
- package/esm/models/index.js.map +1 -0
- package/esm/models/link.d.ts +22 -0
- package/esm/models/link.js +14 -0
- package/esm/models/link.js.map +1 -0
- package/esm/models/note.d.ts +38 -0
- package/esm/models/note.js +2 -0
- package/esm/models/note.js.map +1 -0
- package/esm/models/page.d.ts +14 -0
- package/esm/models/page.js +7 -0
- package/esm/models/page.js.map +1 -0
- package/esm/models/post.d.ts +17 -0
- package/esm/models/post.js +2 -0
- package/esm/models/post.js.map +1 -0
- package/esm/models/project.d.ts +11 -0
- package/esm/models/project.js +2 -0
- package/esm/models/project.js.map +1 -0
- package/esm/models/recently.d.ts +20 -0
- package/esm/models/recently.js +7 -0
- package/esm/models/recently.js.map +1 -0
- package/esm/models/say.d.ts +6 -0
- package/esm/models/say.js +2 -0
- package/esm/models/say.js.map +1 -0
- package/esm/models/setting.d.ts +66 -0
- package/esm/models/setting.js +2 -0
- package/esm/models/setting.js.map +1 -0
- package/esm/models/snippet.d.ts +18 -0
- package/esm/models/snippet.js +8 -0
- package/esm/models/snippet.js.map +1 -0
- package/esm/models/topic.d.ts +8 -0
- package/esm/models/topic.js +2 -0
- package/esm/models/topic.js.map +1 -0
- package/esm/models/user.d.ts +21 -0
- package/esm/models/user.js +2 -0
- package/esm/models/user.js.map +1 -0
- package/esm/utils/auto-bind.d.ts +4 -0
- package/esm/utils/auto-bind.js +36 -0
- package/esm/utils/auto-bind.js.map +1 -0
- package/esm/utils/camelcase-keys.d.ts +6 -0
- package/esm/utils/camelcase-keys.js +23 -0
- package/esm/utils/camelcase-keys.js.map +1 -0
- package/esm/utils/index.d.ts +5 -0
- package/esm/utils/index.js +43 -0
- package/esm/utils/index.js.map +1 -0
- package/esm/utils/path.d.ts +1 -0
- package/esm/utils/path.js +7 -0
- package/esm/utils/path.js.map +1 -0
- package/esm/vitest.config.d.ts +2 -0
- package/esm/vitest.config.js +12 -0
- package/esm/vitest.config.js.map +1 -0
- package/lib/adaptors/axios.d.ts +4 -0
- package/lib/adaptors/axios.js +43 -0
- package/lib/adaptors/axios.js.map +1 -0
- package/lib/adaptors/ky.d.ts +23 -0
- package/lib/adaptors/ky.js +69 -0
- package/lib/adaptors/ky.js.map +1 -0
- package/lib/adaptors/umi-request.d.ts +4 -0
- package/lib/adaptors/umi-request.js +34 -0
- package/lib/adaptors/umi-request.js.map +1 -0
- package/lib/controllers/aggregate.d.ts +64 -0
- package/lib/controllers/aggregate.js +46 -0
- package/lib/controllers/aggregate.js.map +1 -0
- package/lib/controllers/base.d.ts +43 -0
- package/lib/controllers/base.js +31 -0
- package/lib/controllers/base.js.map +1 -0
- package/lib/controllers/category.d.ts +114 -0
- package/lib/controllers/category.js +72 -0
- package/lib/controllers/category.js.map +1 -0
- package/lib/controllers/comment.d.ts +81 -0
- package/lib/controllers/comment.js +51 -0
- package/lib/controllers/comment.js.map +1 -0
- package/lib/controllers/index.d.ts +18 -0
- package/lib/controllers/index.js +72 -0
- package/lib/controllers/index.js.map +1 -0
- package/lib/controllers/link.d.ts +20 -0
- package/lib/controllers/link.js +25 -0
- package/lib/controllers/link.js.map +1 -0
- package/lib/controllers/note.d.ts +123 -0
- package/lib/controllers/note.js +72 -0
- package/lib/controllers/note.js.map +1 -0
- package/lib/controllers/page.d.ts +59 -0
- package/lib/controllers/page.js +46 -0
- package/lib/controllers/page.js.map +1 -0
- package/lib/controllers/post.d.ts +72 -0
- package/lib/controllers/post.js +56 -0
- package/lib/controllers/post.js.map +1 -0
- package/lib/controllers/project.d.ts +15 -0
- package/lib/controllers/project.js +16 -0
- package/lib/controllers/project.js.map +1 -0
- package/lib/controllers/recently.d.ts +110 -0
- package/lib/controllers/recently.js +53 -0
- package/lib/controllers/recently.js.map +1 -0
- package/lib/controllers/say.d.ts +37 -0
- package/lib/controllers/say.js +25 -0
- package/lib/controllers/say.js.map +1 -0
- package/lib/controllers/search.d.ts +247 -0
- package/lib/controllers/search.js +32 -0
- package/lib/controllers/search.js.map +1 -0
- package/lib/controllers/severless.d.ts +24 -0
- package/lib/controllers/severless.js +20 -0
- package/lib/controllers/severless.js.map +1 -0
- package/lib/controllers/snippet.d.ts +24 -0
- package/lib/controllers/snippet.js +23 -0
- package/lib/controllers/snippet.js.map +1 -0
- package/lib/controllers/topic.d.ts +26 -0
- package/lib/controllers/topic.js +22 -0
- package/lib/controllers/topic.js.map +1 -0
- package/lib/controllers/user.d.ts +70 -0
- package/lib/controllers/user.js +44 -0
- package/lib/controllers/user.js.map +1 -0
- package/lib/core/attach-request.d.ts +2 -0
- package/lib/core/attach-request.js +40 -0
- package/lib/core/attach-request.js.map +1 -0
- package/lib/core/client.d.ts +29 -0
- package/lib/core/client.js +190 -0
- package/lib/core/client.js.map +1 -0
- package/lib/core/error.d.ts +6 -0
- package/lib/core/error.js +13 -0
- package/lib/core/error.js.map +1 -0
- package/lib/core/index.d.ts +2 -0
- package/lib/core/index.js +19 -0
- package/lib/core/index.js.map +1 -0
- package/lib/dtos/comment.d.ts +6 -0
- package/lib/dtos/comment.js +7 -0
- package/lib/dtos/comment.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/interfaces/adapter.d.ts +14 -0
- package/lib/interfaces/adapter.js +3 -0
- package/lib/interfaces/adapter.js.map +1 -0
- package/lib/interfaces/client.d.ts +13 -0
- package/lib/interfaces/client.js +3 -0
- package/lib/interfaces/client.js.map +1 -0
- package/lib/interfaces/controller.d.ts +4 -0
- package/lib/interfaces/controller.js +3 -0
- package/lib/interfaces/controller.js.map +1 -0
- package/lib/interfaces/instance.d.ts +7 -0
- package/lib/interfaces/instance.js +3 -0
- package/lib/interfaces/instance.js.map +1 -0
- package/lib/interfaces/options.d.ts +1 -0
- package/lib/interfaces/options.js +3 -0
- package/lib/interfaces/options.js.map +1 -0
- package/lib/interfaces/params.d.ts +4 -0
- package/lib/interfaces/params.js +3 -0
- package/lib/interfaces/params.js.map +1 -0
- package/lib/interfaces/request.d.ts +43 -0
- package/lib/interfaces/request.js +3 -0
- package/lib/interfaces/request.js.map +1 -0
- package/lib/interfaces/types.d.ts +2 -0
- package/lib/interfaces/types.js +3 -0
- package/lib/interfaces/types.js.map +1 -0
- package/lib/models/aggregate.d.ts +53 -0
- package/lib/models/aggregate.js +9 -0
- package/lib/models/aggregate.js.map +1 -0
- package/lib/models/base.d.ts +41 -0
- package/lib/models/base.js +3 -0
- package/lib/models/base.js.map +1 -0
- package/lib/models/category.d.ts +22 -0
- package/lib/models/category.js +9 -0
- package/lib/models/category.js.map +1 -0
- package/lib/models/comment.d.ts +37 -0
- package/lib/models/comment.js +16 -0
- package/lib/models/comment.js.map +1 -0
- package/lib/models/index.d.ts +14 -0
- package/lib/models/index.js +31 -0
- package/lib/models/index.js.map +1 -0
- package/lib/models/link.d.ts +22 -0
- package/lib/models/link.js +17 -0
- package/lib/models/link.js.map +1 -0
- package/lib/models/note.d.ts +38 -0
- package/lib/models/note.js +3 -0
- package/lib/models/note.js.map +1 -0
- package/lib/models/page.d.ts +14 -0
- package/lib/models/page.js +10 -0
- package/lib/models/page.js.map +1 -0
- package/lib/models/post.d.ts +17 -0
- package/lib/models/post.js +3 -0
- package/lib/models/post.js.map +1 -0
- package/lib/models/project.d.ts +11 -0
- package/lib/models/project.js +3 -0
- package/lib/models/project.js.map +1 -0
- package/lib/models/recently.d.ts +20 -0
- package/lib/models/recently.js +10 -0
- package/lib/models/recently.js.map +1 -0
- package/lib/models/say.d.ts +6 -0
- package/lib/models/say.js +3 -0
- package/lib/models/say.js.map +1 -0
- package/lib/models/setting.d.ts +66 -0
- package/lib/models/setting.js +3 -0
- package/lib/models/setting.js.map +1 -0
- package/lib/models/snippet.d.ts +18 -0
- package/lib/models/snippet.js +11 -0
- package/lib/models/snippet.js.map +1 -0
- package/lib/models/topic.d.ts +8 -0
- package/lib/models/topic.js +3 -0
- package/lib/models/topic.js.map +1 -0
- package/lib/models/user.d.ts +21 -0
- package/lib/models/user.js +3 -0
- package/lib/models/user.js.map +1 -0
- package/lib/utils/auto-bind.d.ts +4 -0
- package/lib/utils/auto-bind.js +40 -0
- package/lib/utils/auto-bind.js.map +1 -0
- package/lib/utils/camelcase-keys.d.ts +6 -0
- package/lib/utils/camelcase-keys.js +28 -0
- package/lib/utils/camelcase-keys.js.map +1 -0
- package/lib/utils/index.d.ts +5 -0
- package/lib/utils/index.js +50 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/path.d.ts +1 -0
- package/lib/utils/path.js +11 -0
- package/lib/utils/path.js.map +1 -0
- package/lib/vitest.config.d.ts +2 -0
- package/lib/vitest.config.js +17 -0
- package/lib/vitest.config.js.map +1 -0
- package/package.json +26 -12
- package/types/adaptors/axios.d.ts +4 -0
- package/types/adaptors/ky.d.ts +23 -0
- package/types/adaptors/umi-request.d.ts +4 -0
- package/types/controllers/aggregate.d.ts +64 -0
- package/types/controllers/base.d.ts +43 -0
- package/types/controllers/category.d.ts +114 -0
- package/types/controllers/comment.d.ts +81 -0
- package/types/controllers/index.d.ts +18 -0
- package/types/controllers/link.d.ts +20 -0
- package/types/controllers/note.d.ts +123 -0
- package/types/controllers/page.d.ts +59 -0
- package/types/controllers/post.d.ts +72 -0
- package/types/controllers/project.d.ts +15 -0
- package/types/controllers/recently.d.ts +110 -0
- package/types/controllers/say.d.ts +37 -0
- package/types/controllers/search.d.ts +247 -0
- package/types/controllers/severless.d.ts +24 -0
- package/types/controllers/snippet.d.ts +24 -0
- package/types/controllers/topic.d.ts +26 -0
- package/types/controllers/user.d.ts +70 -0
- package/types/core/attach-request.d.ts +2 -0
- package/types/core/client.d.ts +29 -0
- package/types/core/error.d.ts +6 -0
- package/types/core/index.d.ts +2 -0
- package/types/dtos/comment.d.ts +6 -0
- package/types/index.d.ts +7 -0
- package/types/interfaces/adapter.d.ts +14 -0
- package/types/interfaces/client.d.ts +13 -0
- package/types/interfaces/controller.d.ts +4 -0
- package/types/interfaces/instance.d.ts +7 -0
- package/types/interfaces/options.d.ts +1 -0
- package/types/interfaces/params.d.ts +4 -0
- package/types/interfaces/request.d.ts +43 -0
- package/types/interfaces/types.d.ts +2 -0
- package/types/models/aggregate.d.ts +53 -0
- package/types/models/base.d.ts +41 -0
- package/types/models/category.d.ts +22 -0
- package/types/models/comment.d.ts +37 -0
- package/types/models/index.d.ts +14 -0
- package/types/models/link.d.ts +22 -0
- package/types/models/note.d.ts +38 -0
- package/types/models/page.d.ts +14 -0
- package/types/models/post.d.ts +17 -0
- package/types/models/project.d.ts +11 -0
- package/types/models/recently.d.ts +20 -0
- package/types/models/say.d.ts +6 -0
- package/types/models/setting.d.ts +66 -0
- package/types/models/snippet.d.ts +18 -0
- package/types/models/topic.d.ts +8 -0
- package/types/models/user.d.ts +21 -0
- package/types/utils/auto-bind.d.ts +4 -0
- package/types/utils/camelcase-keys.d.ts +6 -0
- package/types/utils/index.d.ts +5 -0
- package/types/utils/path.d.ts +1 -0
- package/types/vitest.config.d.ts +2 -0
- package/__tests__/adaptors/axios.spec.ts +0 -7
- package/__tests__/adaptors/ky.spec.ts +0 -9
- package/__tests__/adaptors/umi-request.spec.ts +0 -7
- package/__tests__/contronllers/aggregate.test.ts +0 -389
- package/__tests__/contronllers/category.test.ts +0 -284
- package/__tests__/contronllers/comment.test.ts +0 -113
- package/__tests__/contronllers/link.test.ts +0 -95
- package/__tests__/contronllers/note.test.ts +0 -120
- package/__tests__/contronllers/page.test.ts +0 -49
- package/__tests__/contronllers/post.test.ts +0 -77
- package/__tests__/contronllers/recently.test.ts +0 -49
- package/__tests__/contronllers/say.test.ts +0 -106
- package/__tests__/contronllers/search.test.ts +0 -81
- package/__tests__/contronllers/serverless.test.ts +0 -19
- package/__tests__/contronllers/snippet.test.ts +0 -38
- package/__tests__/contronllers/topic.test.ts +0 -17
- package/__tests__/contronllers/user.test.ts +0 -59
- package/__tests__/core/client.test.ts +0 -293
- package/__tests__/helpers/adaptor-test.ts +0 -108
- package/__tests__/helpers/e2e-mock-server.ts +0 -22
- package/__tests__/helpers/global-fetch.ts +0 -39
- package/__tests__/helpers/instance.ts +0 -11
- package/__tests__/helpers/response.ts +0 -76
- package/__tests__/mock/algolia.json +0 -446
- package/__tests__/utils/auto-bind.spec.ts +0 -27
- package/__tests__/utils/camelcase-keys.spec.ts +0 -86
- package/__tests__/utils/index.test.ts +0 -33
- package/__tests__/utils/path.spec.ts +0 -8
- package/adaptors/axios.ts +0 -44
- package/adaptors/ky.ts +0 -76
- package/adaptors/umi-request.ts +0 -38
- package/core/attach-request.ts +0 -43
- package/core/client.ts +0 -252
- package/core/error.ts +0 -10
- package/core/index.ts +0 -2
- package/coverage/lcov-report/__tests__/helpers/adaptor-test.ts.html +0 -405
- package/coverage/lcov-report/__tests__/helpers/e2e-mock-server.ts.html +0 -150
- package/coverage/lcov-report/__tests__/helpers/index.html +0 -236
- package/coverage/lcov-report/__tests__/helpers/instance.ts.html +0 -120
- package/coverage/lcov-report/__tests__/helpers/response.ts.html +0 -231
- package/rollup.config.js +0 -150
- package/utils/auto-bind.ts +0 -48
- package/utils/camelcase-keys.ts +0 -26
- package/utils/index.ts +0 -53
- package/utils/path.ts +0 -6
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"data": [
|
|
3
|
-
{
|
|
4
|
-
"id": "5fe97d1d5b11408f99ada0fd",
|
|
5
|
-
"title": "从零开始的 Swift UI (三)",
|
|
6
|
-
"slug": "swift-ui-meet_3",
|
|
7
|
-
"category_id": "5eb2c62a613a5ab0642f1f7a",
|
|
8
|
-
"modified": "2020-12-28T06:38:01.547Z",
|
|
9
|
-
"created": "2020-12-28T06:37:17.970Z",
|
|
10
|
-
"category": {
|
|
11
|
-
"id": "5eb2c62a613a5ab0642f1f7a",
|
|
12
|
-
"type": 0,
|
|
13
|
-
"count": 56,
|
|
14
|
-
"name": "编程",
|
|
15
|
-
"slug": "programming",
|
|
16
|
-
"created": "2020-05-06T14:14:02.339Z"
|
|
17
|
-
},
|
|
18
|
-
"type": "post"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "5fe951565b11408f99ad9edd",
|
|
22
|
-
"title": "从零开始的 Swift UI (二)",
|
|
23
|
-
"slug": "swift-ui-meet_2",
|
|
24
|
-
"category_id": "5eb2c62a613a5ab0642f1f7a",
|
|
25
|
-
"modified": "2021-01-01T10:50:18.036Z",
|
|
26
|
-
"created": "2020-12-28T03:30:30.615Z",
|
|
27
|
-
"category": {
|
|
28
|
-
"id": "5eb2c62a613a5ab0642f1f7a",
|
|
29
|
-
"type": 0,
|
|
30
|
-
"count": 56,
|
|
31
|
-
"name": "编程",
|
|
32
|
-
"slug": "programming",
|
|
33
|
-
"created": "2020-05-06T14:14:02.339Z"
|
|
34
|
-
},
|
|
35
|
-
"type": "post"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"id": "5fe933425b11408f99ad9d0c",
|
|
39
|
-
"title": "从零开始的 Swift UI (一)",
|
|
40
|
-
"slug": "swift-ui-meet_1",
|
|
41
|
-
"category_id": "5eb2c62a613a5ab0642f1f7a",
|
|
42
|
-
"modified": "2020-12-28T01:22:10.129Z",
|
|
43
|
-
"created": "2020-12-28T01:22:10.134Z",
|
|
44
|
-
"category": {
|
|
45
|
-
"id": "5eb2c62a613a5ab0642f1f7a",
|
|
46
|
-
"type": 0,
|
|
47
|
-
"count": 56,
|
|
48
|
-
"name": "编程",
|
|
49
|
-
"slug": "programming",
|
|
50
|
-
"created": "2020-05-06T14:14:02.339Z"
|
|
51
|
-
},
|
|
52
|
-
"type": "post"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"id": "5fd5d40adadfe8960b838356",
|
|
56
|
-
"title": "年终更新小记",
|
|
57
|
-
"slug": "update-2020.12",
|
|
58
|
-
"category_id": "5ed09730a0a8f94af569c96c",
|
|
59
|
-
"modified": "2020-12-13T08:42:50.819Z",
|
|
60
|
-
"created": "2020-12-13T08:42:50.823Z",
|
|
61
|
-
"category": {
|
|
62
|
-
"id": "5ed09730a0a8f94af569c96c",
|
|
63
|
-
"type": 0,
|
|
64
|
-
"count": 12,
|
|
65
|
-
"slug": "website",
|
|
66
|
-
"name": "站点日志",
|
|
67
|
-
"created": "2020-05-29T05:01:36.315Z"
|
|
68
|
-
},
|
|
69
|
-
"type": "post"
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"id": "5f6dbe08946127628d8a872c",
|
|
73
|
-
"title": "Vue 3 正式发布,再度踩坑",
|
|
74
|
-
"slug": "vue-3-one-piece-beginning",
|
|
75
|
-
"category_id": "5eb2c62a613a5ab0642f1f7c",
|
|
76
|
-
"created": "2020-09-25T09:53:12.835Z",
|
|
77
|
-
"modified": "2020-11-16T03:43:54.956Z",
|
|
78
|
-
"category": {
|
|
79
|
-
"id": "5eb2c62a613a5ab0642f1f7c",
|
|
80
|
-
"type": 0,
|
|
81
|
-
"count": 26,
|
|
82
|
-
"name": "学习",
|
|
83
|
-
"slug": "learning-process",
|
|
84
|
-
"created": "2020-05-06T14:14:02.364Z"
|
|
85
|
-
},
|
|
86
|
-
"type": "post"
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
"id": "5f5a142413d00b5a750d9054",
|
|
90
|
-
"title": "每天一个劝退小技巧之位操作",
|
|
91
|
-
"slug": "bit-operation",
|
|
92
|
-
"category_id": "5eb2c62a613a5ab0642f1f7c",
|
|
93
|
-
"created": "2020-09-10T11:55:16.758Z",
|
|
94
|
-
"modified": "2021-05-07T10:34:41.062Z",
|
|
95
|
-
"category": {
|
|
96
|
-
"id": "5eb2c62a613a5ab0642f1f7c",
|
|
97
|
-
"type": 0,
|
|
98
|
-
"count": 26,
|
|
99
|
-
"name": "学习",
|
|
100
|
-
"slug": "learning-process",
|
|
101
|
-
"created": "2020-05-06T14:14:02.364Z"
|
|
102
|
-
},
|
|
103
|
-
"type": "post"
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
"id": "5f54dae24f80b551b79583d1",
|
|
107
|
-
"title": "文字溢出边缘羽化 CSS Mask 实现",
|
|
108
|
-
"slug": "css-mask-text-overflow",
|
|
109
|
-
"category_id": "5eb2c62a613a5ab0642f1f7a",
|
|
110
|
-
"created": "2020-09-06T12:49:38.102Z",
|
|
111
|
-
"modified": "2020-11-16T06:49:38.179Z",
|
|
112
|
-
"category": {
|
|
113
|
-
"id": "5eb2c62a613a5ab0642f1f7a",
|
|
114
|
-
"type": 0,
|
|
115
|
-
"count": 56,
|
|
116
|
-
"name": "编程",
|
|
117
|
-
"slug": "programming",
|
|
118
|
-
"created": "2020-05-06T14:14:02.339Z"
|
|
119
|
-
},
|
|
120
|
-
"type": "post"
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
"id": "5f3f31c4e495ba011ac4b934",
|
|
124
|
-
"title": "编写更加简洁易阅读的代码",
|
|
125
|
-
"slug": "write-more-easy-code",
|
|
126
|
-
"category_id": "5eb2c62a613a5ab0642f1f7a",
|
|
127
|
-
"created": "2020-08-21T02:30:28.372Z",
|
|
128
|
-
"modified": "2020-11-15T10:11:32.945Z",
|
|
129
|
-
"category": {
|
|
130
|
-
"id": "5eb2c62a613a5ab0642f1f7a",
|
|
131
|
-
"type": 0,
|
|
132
|
-
"count": 56,
|
|
133
|
-
"name": "编程",
|
|
134
|
-
"slug": "programming",
|
|
135
|
-
"created": "2020-05-06T14:14:02.339Z"
|
|
136
|
-
},
|
|
137
|
-
"type": "post"
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
"id": "5f37c66ae495ba011ac4597d",
|
|
141
|
-
"title": "不同寻常的地址栏过渡",
|
|
142
|
-
"slug": "unusual-address-bar-transitions",
|
|
143
|
-
"category_id": "5eb2c62a613a5ab0642f1f7a",
|
|
144
|
-
"created": "2020-08-15T11:26:34.865Z",
|
|
145
|
-
"modified": "2020-11-15T23:51:34.116Z",
|
|
146
|
-
"category": {
|
|
147
|
-
"id": "5eb2c62a613a5ab0642f1f7a",
|
|
148
|
-
"type": 0,
|
|
149
|
-
"count": 56,
|
|
150
|
-
"name": "编程",
|
|
151
|
-
"slug": "programming",
|
|
152
|
-
"created": "2020-05-06T14:14:02.339Z"
|
|
153
|
-
},
|
|
154
|
-
"type": "post"
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
"id": "5f0dc4dbddf2006d12774b6a",
|
|
158
|
-
"title": "Electron 的打包与构建",
|
|
159
|
-
"slug": "electron-build-package",
|
|
160
|
-
"category_id": "5eb2c62a613a5ab0642f1f7a",
|
|
161
|
-
"created": "2020-07-14T14:44:43.841Z",
|
|
162
|
-
"modified": "2020-11-16T03:40:41.289Z",
|
|
163
|
-
"category": {
|
|
164
|
-
"id": "5eb2c62a613a5ab0642f1f7a",
|
|
165
|
-
"type": 0,
|
|
166
|
-
"count": 56,
|
|
167
|
-
"name": "编程",
|
|
168
|
-
"slug": "programming",
|
|
169
|
-
"created": "2020-05-06T14:14:02.339Z"
|
|
170
|
-
},
|
|
171
|
-
"type": "post"
|
|
172
|
-
}
|
|
173
|
-
],
|
|
174
|
-
"raw": {
|
|
175
|
-
"hits": [
|
|
176
|
-
{
|
|
177
|
-
"text": "接上文:[从零开始的 Swift UI (二)](https://innei.ren/posts/programming/swift-ui-meet_2)\n\n上篇文章介绍了如何使用 UserDefaults 和 ObserveableObject 来进行数据管理。\n\n这篇文章来完成 LikeView 的布局和功能实现。\n\n\n\n## Layout\n\n在 LikeView 中编写如下代码。\n\n```swift\nstruct LikeView: View {\n @EnvironmentObject var like: Like\n\n var likes: [LikeModel] {\n like.likes\n }\n\n var body: some View {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }\n }\n}\n\n```\n\n\n\n再修改 HomeView 中的 Like Button 代码。\n\n```swift\nButton(action: {\n like.add(hikotoko: model)\n}, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n})\n```\n\nLike.swift 中新增一个方法。\n\n```swift\nfunc add(hikotoko: HitokotoModel) -> Bool {\n let date = ISO8601DateFormatter().date(from: hikotoko.createdAt) ?? Date()\n\n return add(item: LikeModel(id: UUID(uuidString: hikotoko.uuid) ?? UUID(), text: hikotoko.hitokoto,\n createdAt: date, from: hikotoko.from, author: hikotoko.creator))\n}\n```\n\n上面 Like Button 时候被选中,可以根据 Like 中有没有存储判断。\n\n将 ActionView 修改为如下代码:\n\n```swift\nstruct ActionView: View {\n @Binding var model: HitokotoModel?\n @EnvironmentObject var like: Like\n\n var liked: Bool {\n guard let model = model else {\n return false\n }\n return like.has(uuid: UUID(uuidString: model.uuid))\n }\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n like.add(hikotoko: model)\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n\n```\n\n`liked` 计算属性根据 model 中的 uuid 推断状态。因为使用了 `@Binding` 所以上层 View 还需要传一个 Binding 给他。可以理解为 React 中的 Props。注意的是 只有加了 `@Binding` 的参数传递才是引用传递,也就是上层数据更新后下层也会被更新。\n\n在 HomeView 中修改为 `ActionView(model: $model).offset(x: 0, y: reader.size.height / 2 - 50)`\n\n被 `@State` 装饰的属性,取他的 Binding 只需要在前面加一个 `$`\n\n这样点击 Like Button 后 ❤就会变红啦。\n\n## Navigation\n\n为了实现能在各个 View 之间导航。使用 NavigationView 就可以做到啦。\n\n修改 HomeView,在外层加上 NavigationView。\n\n修改 LikeView,在外层加上 NavigationView。\n\n```swift\nvar body: some View {\n NavigationView {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }.navigationBarTitle(\"喜欢\")\n }\n}\n```\n\n注意在设定 `.navigationBarTitle` 必须加在 NavigationView 的子 View 上才会生效。\n\n\n\n接下来,调整一下 List 的 style,让 Item 撑满整个宽度。只需要使用内置的 `.listStyle(PlainListStyle())` 即可。\n\n其余知识点将通过小 Demo 描述。\n\n- Share\n- Sheet Modal\n\n完整 App:<https://github.com/Innei/meet-swift>\n\n(完)",
|
|
178
|
-
"title": "从零开始的 Swift UI (三)",
|
|
179
|
-
"id": "5fe97d1d5b11408f99ada0fd",
|
|
180
|
-
"type": "post",
|
|
181
|
-
"object_id": "5fe97d1d5b11408f99ada0fd",
|
|
182
|
-
"highlight_result": {
|
|
183
|
-
"text": {
|
|
184
|
-
"value": "接上文:[从零开始的 Swift UI (二)](https://innei.ren/posts/programming/swift-ui-meet_2)\n\n上篇文章介绍了如何使用 UserDefaults 和 ObserveableObject 来进行数据管理。\n\n这篇文章来完成 LikeView 的布局和功能实现。\n\n\n\n## Layout\n\n在 LikeView 中编写如下代码。\n\n```swift\nstruct LikeView: View {\n @EnvironmentObject var like: Like\n\n var likes: [LikeModel] {\n like.likes\n }\n\n var body: some View {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }\n }\n}\n\n```\n\n\n\n再修改 HomeView 中的 Like Button 代码。\n\n```swift\nButton(action: {\n like.add(hikotoko: model)\n}, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n})\n```\n\nLike.swift 中新增一个方法。\n\n```swift\nfunc add(hikotoko: HitokotoModel) -> Bool {\n let date = ISO8601DateFormatter().date(from: hikotoko.createdAt) ?? Date()\n\n return add(item: LikeModel(id: UUID(uuidString: hikotoko.uuid) ?? UUID(), text: hikotoko.hitokoto,\n createdAt: date, from: hikotoko.from, author: hikotoko.creator))\n}\n```\n\n上面 Like Button 时候被选中,可以根据 Like 中有没有存储判断。\n\n将 ActionView 修改为如下代码:\n\n```swift\nstruct ActionView: View {\n @Binding var model: HitokotoModel?\n @EnvironmentObject var like: Like\n\n var liked: Bool {\n guard let model = model else {\n return false\n }\n return like.has(uuid: UUID(uuidString: model.uuid))\n }\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n like.add(hikotoko: model)\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n\n```\n\n`liked` 计算属性根据 model 中的 uuid 推断状态。因为使用了 `@Binding` 所以上层 View 还需要传一个 Binding 给他。可以理解为 React 中的 Props。注意的是 只有加了 `@Binding` 的参数传递才是引用传递,也就是上层数据更新后下层也会被更新。\n\n在 HomeView 中修改为 `ActionView(model: $model).offset(x: 0, y: reader.size.height / 2 - 50)`\n\n被 `@State` 装饰的属性,取他的 Binding 只需要在前面加一个 `$`\n\n这样点击 Like Button 后 ❤就会变红啦。\n\n## Navigation\n\n为了实现能在各个 View 之间导航。使用 NavigationView 就可以做到啦。\n\n修改 HomeView,在外层加上 NavigationView。\n\n修改 LikeView,在外层加上 NavigationView。\n\n```swift\nvar body: some View {\n NavigationView {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }.navigationBarTitle(\"喜欢\")\n }\n}\n```\n\n注意在设定 `.navigationBarTitle` 必须加在 NavigationView 的子 View 上才会生效。\n\n\n\n接下来,调整一下 List 的 style,让 Item 撑满整个宽度。只需要使用内置的 `.listStyle(PlainListStyle())` 即可。\n\n其余知识点将通过小 Demo 描述。\n\n- Share\n- Sheet Modal\n\n完整 App:<https://github.com/Innei/meet-swift>\n\n(完)",
|
|
185
|
-
"match_level": "full",
|
|
186
|
-
"fully_highlighted": false,
|
|
187
|
-
"matched_words": ["1"]
|
|
188
|
-
},
|
|
189
|
-
"title": {
|
|
190
|
-
"value": "从零开始的 Swift UI (三)",
|
|
191
|
-
"match_level": "none",
|
|
192
|
-
"matched_words": []
|
|
193
|
-
},
|
|
194
|
-
"id": {
|
|
195
|
-
"value": "5fe97d1d5b11408f99ada0fd",
|
|
196
|
-
"match_level": "none",
|
|
197
|
-
"matched_words": []
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
"text": "接上文:[从零开始的 Swift UI (一)](https://innei.ren/posts/programming/swift-ui-meet_1)\n\n在上一篇文章中,我们完成了 HomeView 的基本布局。接下来我们来编写一下数据层(Model ViewModel)。\n\n大概包括两个方面:数据的获取(JSON URLSession) 和 UI ViewModel 的数据同步。\n\n## 数据的获取\n\n首先我们使用的 Api 是 [Hikotoko](http://v1.hitokoto.cn/)。随机获取一条 Hikotoko 的 JSON 如下。\n\n```json\n{\n\"id\": 5716,\n\"uuid\": \"71396790-6d06-49dd-bc72-2568311cdd7b\",\n\"hitokoto\": \"粗缯大布裹生涯,腹有诗书气自华。\",\n\"type\": \"i\",\n\"from\": \"和董传留别\",\n\"from_who\": \"苏轼\",\n\"creator\": \"a632079\",\n\"creator_uid\": 1044,\n\"reviewer\": 4756,\n\"commit_from\": \"web\",\n\"created_at\": \"1586333487\",\n\"length\": 16\n}\n```\n\n使用工具 JSON2Swift 将 JSON Model 转化为 Swift Struct。工具推荐使用: <https://app.quicktype.io/>\n\n右侧选项根据需要修改。仅参考。\n\n\n\n使用此工具的好处是,他把 URLSession 也自动构建好了。并给出了实例。\n\n新建一个 Swift 文件,命名为 `Model.swift` 将生成的代码复制到新文件。\n\n再新建一个 Swift 文件,命名为 `ViewModel.swift`,写入以下代码。\n\n```swift\nimport Foundation\n\nclass HitokotoViewModel {\n static func fetch(completion: @escaping (HitokotoModel) -> Void) {\n let task = URLSession.shared.hitokotoModelTask(with: URL(string: \"https://v1.hitokoto.cn/\")!) { hitokotoModel, _, _ in\n if let hitokotoModel = hitokotoModel {\n DispatchQueue.main.async {\n completion(hitokotoModel)\n }\n }\n }\n\n task.resume()\n }\n}\n```\n\n在 HomeView 中调用此方法。修改 HomeView 的代码为\n\n```swift\n//\n// HomeView.swift\n// Meet\n//\n// Created by Innei on 2020/12/28.\n//\n\nimport SwiftUI\n\nstruct HomeView: View {\n @State var model: HitokotoModel? = nil\n\n func fetch() {\n HitokotoViewModel.fetch {\n self.model = $0\n }\n }\n\n var body: some View {\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(model?.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model?.creator ?? \"\")\n }\n }.padding()\n\n ActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n\n Button(action: {\n fetch()\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n }\n .onAppear {\n fetch()\n }\n }\n }\n}\n\nstruct HomeView_Previews: PreviewProvider {\n static var previews: some View {\n HomeView()\n }\n}\n\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n\nstruct ActionView: View {\n @State var liked = false\n\n @ViewBuilder\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n\n```\n\n\n\n效果已经有了,但是没有加载完成时(受限于网络,弱网),会出现一片空白。如果未加载完成时,显示加载中.. 可能会比较好。\n\n在未加载完成时,`model` 为 `nil` ,那么只需要判断是不是 `nil` 就行了。我本来想用 `Group` 包裹 `if` 判断语句实现。理论上是可行的,但是由于 `Group ` 中 `if` 不支持使用 `Stack` 包裹。出现如下报错。\n\n\n\n换一种方法。转而使用 `@ViewBuilder`,首先提取组件。在这个 struct 里新增一个 `some View`。\n\n```swift\n @ViewBuilder\n var Preview: some View {\n if let model = model {\n VStack {\n Text(model.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model.creator ?? \"\")\n }\n }\n } else {\n Text(\"加载中\")\n }\n }\n```\n\n然后在 `body` 的合适地方替换成。\n\n```swift\nZStack {\n Preview\n \n // ....\n}\n```\n\n## 响应式数据流\n\n接下来我们实现保存 Hikotoko 到 喜欢。我们需要用到本地存储和响应式数据流。\n\n本地存储可以使用 `UserDefaults`,响应式数据流使用 `ObservableObject`。\n\n新建一个 Swift 文件,命名为 `Like.swift`\n\n```swift\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = []\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n \n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n \n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\n```\n\n使用 `ObservableObject` protocol 使得一个对象成为可被观察的,当被装饰 `@Published` 的属性改变时,会触发 UIView 更新。\n\n在 MeetApp.swift 中挂载 `Like` 为 `environmentObject`。增加如下代码。\n\n```git\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n\n+ let like = Like()\n\n var body: some Scene {\n WindowGroup {\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != 1 ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = 1\n }\n }\n .tag(1)\n }\n .accentColor(.pink)\n+ .environmentObject(like)\n }\n }\n}\n\n```\n\n在 HomeView 中,ActionView 中的 Like Button,修改 action 为\n\n```swift\nif like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n```\n\n在顶部增加\n\n```swift\n @EnvironmentObject var like: Like\n```\n\n完整如下\n\n```swift\nstruct ActionView: View {\n @EnvironmentObject var like: Like\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n if like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n\n }, label: {\n Image(systemName: \"suit.heart\")\n .foregroundColor(.primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n```\n\n装饰了 `@EnvironmentObject` 的属性会自动获取上层 View 挂载的 `environmentObject`,不需要层层传递。类似 React 中的 `Context`。\n\n## 数据的存储\n\n在 `Like.swift` 中新建一个 Class,代码如下。\n\n```swift\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n我们使用 `refreshStore` 方法把 Like 中 `likes` 数据保存到本地数据中。因为 `likes` 不是普通的 Array,所以不能直接使用 `Userdefaults.set()` 的方法写入,否则会 runtime crash。首先使用 `PropertyListEncoder` 将数据序列化。在此之前,请注意 `LikeModel` 实现了 `Codable` Protocol。\n\n同样在 Like init 的时候读取本地保存的数据。当然也需要先反序列化数据。\n\n```swift\ninit() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n```\n\n在修改 likes 后,同时写入到本地数据。可以使用 `didSet` 计算属性很容易完成。修改 likes 属性为。\n\n```swift\n@Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n}\n\n```\n\n之后完整的 `Like.swift` 如下:\n\n```swift\n//\n// Like.swift\n// Meet\n//\n// Created by Innei on 2020/12/27.\n//\n\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n }\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func has(uuid: UUID?) -> Bool {\n guard let uuid = uuid else { return false }\n return likes.first { $0.id == uuid } != nil\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n// Store.refreshStore()\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n下一篇文章,将构建 LikeView。\n\n(未待完续)",
|
|
203
|
-
"title": "从零开始的 Swift UI (二)",
|
|
204
|
-
"id": "5fe951565b11408f99ad9edd",
|
|
205
|
-
"type": "post",
|
|
206
|
-
"object_id": "5fe951565b11408f99ad9edd",
|
|
207
|
-
"highlight_result": {
|
|
208
|
-
"text": {
|
|
209
|
-
"value": "接上文:[从零开始的 Swift UI (一)](https://innei.ren/posts/programming/swift-ui-meet_<em>1</em>)\n\n在上一篇文章中,我们完成了 HomeView 的基本布局。接下来我们来编写一下数据层(Model ViewModel)。\n\n大概包括两个方面:数据的获取(JSON URLSession) 和 UI ViewModel 的数据同步。\n\n## 数据的获取\n\n首先我们使用的 Api 是 [Hikotoko](http://v1.hitokoto.cn/)。随机获取一条 Hikotoko 的 JSON 如下。\n\n```json\n{\n\"id\": 5716,\n\"uuid\": \"71396790-6d06-49dd-bc72-2568311cdd7b\",\n\"hitokoto\": \"粗缯大布裹生涯,腹有诗书气自华。\",\n\"type\": \"i\",\n\"from\": \"和董传留别\",\n\"from_who\": \"苏轼\",\n\"creator\": \"a632079\",\n\"creator_uid\": <em>1</em>044,\n\"reviewer\": 4756,\n\"commit_from\": \"web\",\n\"created_at\": \"<em>1</em>586333487\",\n\"length\": <em>1</em>6\n}\n```\n\n使用工具 JSON2Swift 将 JSON Model 转化为 Swift Struct。工具推荐使用: <https://app.quicktype.io/>\n\n右侧选项根据需要修改。仅参考。\n\n\n\n使用此工具的好处是,他把 URLSession 也自动构建好了。并给出了实例。\n\n新建一个 Swift 文件,命名为 `Model.swift` 将生成的代码复制到新文件。\n\n再新建一个 Swift 文件,命名为 `ViewModel.swift`,写入以下代码。\n\n```swift\nimport Foundation\n\nclass HitokotoViewModel {\n static func fetch(completion: @escaping (HitokotoModel) -> Void) {\n let task = URLSession.shared.hitokotoModelTask(with: URL(string: \"https://v1.hitokoto.cn/\")!) { hitokotoModel, _, _ in\n if let hitokotoModel = hitokotoModel {\n DispatchQueue.main.async {\n completion(hitokotoModel)\n }\n }\n }\n\n task.resume()\n }\n}\n```\n\n在 HomeView 中调用此方法。修改 HomeView 的代码为\n\n```swift\n//\n// HomeView.swift\n// Meet\n//\n// Created by Innei on 2020/<em>1</em>2/28.\n//\n\nimport SwiftUI\n\nstruct HomeView: View {\n @State var model: HitokotoModel? = nil\n\n func fetch() {\n HitokotoViewModel.fetch {\n self.model = $0\n }\n }\n\n var body: some View {\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(model?.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model?.creator ?? \"\")\n }\n }.padding()\n\n ActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n\n Button(action: {\n fetch()\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n }\n .onAppear {\n fetch()\n }\n }\n }\n}\n\nstruct HomeView_Previews: PreviewProvider {\n static var previews: some View {\n HomeView()\n }\n}\n\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n\nstruct ActionView: View {\n @State var liked = false\n\n @ViewBuilder\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n\n```\n\n\n\n效果已经有了,但是没有加载完成时(受限于网络,弱网),会出现一片空白。如果未加载完成时,显示加载中.. 可能会比较好。\n\n在未加载完成时,`model` 为 `nil` ,那么只需要判断是不是 `nil` 就行了。我本来想用 `Group` 包裹 `if` 判断语句实现。理论上是可行的,但是由于 `Group ` 中 `if` 不支持使用 `Stack` 包裹。出现如下报错。\n\n\n\n换一种方法。转而使用 `@ViewBuilder`,首先提取组件。在这个 struct 里新增一个 `some View`。\n\n```swift\n @ViewBuilder\n var Preview: some View {\n if let model = model {\n VStack {\n Text(model.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model.creator ?? \"\")\n }\n }\n } else {\n Text(\"加载中\")\n }\n }\n```\n\n然后在 `body` 的合适地方替换成。\n\n```swift\nZStack {\n Preview\n \n // ....\n}\n```\n\n## 响应式数据流\n\n接下来我们实现保存 Hikotoko 到 喜欢。我们需要用到本地存储和响应式数据流。\n\n本地存储可以使用 `UserDefaults`,响应式数据流使用 `ObservableObject`。\n\n新建一个 Swift 文件,命名为 `Like.swift`\n\n```swift\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = []\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n \n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n \n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\n```\n\n使用 `ObservableObject` protocol 使得一个对象成为可被观察的,当被装饰 `@Published` 的属性改变时,会触发 UIView 更新。\n\n在 MeetApp.swift 中挂载 `Like` 为 `environmentObject`。增加如下代码。\n\n```git\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n\n+ let like = Like()\n\n var body: some Scene {\n WindowGroup {\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != <em>1</em> ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = <em>1</em>\n }\n }\n .tag(<em>1</em>)\n }\n .accentColor(.pink)\n+ .environmentObject(like)\n }\n }\n}\n\n```\n\n在 HomeView 中,ActionView 中的 Like Button,修改 action 为\n\n```swift\nif like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n```\n\n在顶部增加\n\n```swift\n @EnvironmentObject var like: Like\n```\n\n完整如下\n\n```swift\nstruct ActionView: View {\n @EnvironmentObject var like: Like\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n if like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n\n }, label: {\n Image(systemName: \"suit.heart\")\n .foregroundColor(.primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n```\n\n装饰了 `@EnvironmentObject` 的属性会自动获取上层 View 挂载的 `environmentObject`,不需要层层传递。类似 React 中的 `Context`。\n\n## 数据的存储\n\n在 `Like.swift` 中新建一个 Class,代码如下。\n\n```swift\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n我们使用 `refreshStore` 方法把 Like 中 `likes` 数据保存到本地数据中。因为 `likes` 不是普通的 Array,所以不能直接使用 `Userdefaults.set()` 的方法写入,否则会 runtime crash。首先使用 `PropertyListEncoder` 将数据序列化。在此之前,请注意 `LikeModel` 实现了 `Codable` Protocol。\n\n同样在 Like init 的时候读取本地保存的数据。当然也需要先反序列化数据。\n\n```swift\ninit() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n```\n\n在修改 likes 后,同时写入到本地数据。可以使用 `didSet` 计算属性很容易完成。修改 likes 属性为。\n\n```swift\n@Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n}\n\n```\n\n之后完整的 `Like.swift` 如下:\n\n```swift\n//\n// Like.swift\n// Meet\n//\n// Created by Innei on 2020/<em>1</em>2/27.\n//\n\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n }\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func has(uuid: UUID?) -> Bool {\n guard let uuid = uuid else { return false }\n return likes.first { $0.id == uuid } != nil\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n// Store.refreshStore()\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n下一篇文章,将构建 LikeView。\n\n(未待完续)",
|
|
210
|
-
"match_level": "full",
|
|
211
|
-
"fully_highlighted": false,
|
|
212
|
-
"matched_words": ["1"]
|
|
213
|
-
},
|
|
214
|
-
"title": {
|
|
215
|
-
"value": "从零开始的 Swift UI (二)",
|
|
216
|
-
"match_level": "none",
|
|
217
|
-
"matched_words": []
|
|
218
|
-
},
|
|
219
|
-
"id": {
|
|
220
|
-
"value": "5fe951565b11408f99ad9edd",
|
|
221
|
-
"match_level": "none",
|
|
222
|
-
"matched_words": []
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
"text": "注: 本文编写时,使用 Xcode 12.3、Swift 5.3.2 来构建 App\n\n入门 Swift UI 已经有一段时间了,但是却一直没有写过什么练手项目,虽然之前跟着 Hackingwithswift 上找着写过几个 Demo。突然打算自己独立写一个练手项目,因为是练手项目,所以布局和功能上也很简单,App 的类型大概和 TODO 类似。\n\n\n\n## 准备\n\n打开 Xcode 新建一个项目在此不再展开。在左侧文件树中打开 `ContentView.swift`,这是 View 的入口文件。你可以看到如下代码。\n\n```swift\nimport SwiftUI\n\nstruct ContentView: View {\n var body: some View {\n Text(\"Hello, world!\")\n .padding()\n }\n}\n\nstruct ContentView_Previews: PreviewProvider {\n static var previews: some View {\n ContentView()\n }\n}\n```\n\n在 Swift UI 2.0 中,UI 主入口文件从复杂的 `AppDelegate.swift` 和 `SceneDelegate.swift` 转变为仅仅只有几行的 `xxApp.swift`,得益于 Swift 5.3 加入的 `@main` 关键字\n\n```swift\nimport SwiftUI\n\n@main\nstruct MeetApp: App {\n var body: some Scene {\n WindowGroup {\n ContentView()\n }\n }\n}\n```\n\n## 布局\n\n### HomeView\n\n首先新建一个 View,`Command + N` 选择 SwiftUI View,命名为 `HomeView.swift`。将 HomeView 修改为如下代码。\n\n```swift\nstruct HomeView: View {\n var body: some View {\n VStack {\n Text(\"我不去想,是否能够成功 ,既然选择了远方 ,便只顾风雨兼程。\")\n .foregroundColor(.blue)\n .padding(.vertical)\n \n Text(\"hasty\")\n }.padding()\n }\n}\n```\n\n\n\n接下来,绘制圆形 Button。在 Swift UI 中绘制图形十分简单,Swift UI 中内置了 `Circle` 组件,只要使用 ZStack 和 Circle 结合,很容易编写这个组件。\n\n```swift\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n```\n\n这个组件绘制了整个图形,其中 Image 接收一个 SFSymbol 字符串。SF Symbols 可以在[这里下载](https://developer.apple.com/sf-symbols/)。绘制完了图形接下来需要在 View 中使用这个图形,并定位到对应的地点。\n\n在 Swift UI 中,可以使用 ZStack 结合 `.postion` 定位到指定地点。为了获取到整个视窗的长宽,还需要 `GeometryReader` 去读取子 View 的长宽。在根 View 包裹可以获取到设备的长宽。\n\n```swift\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(\"我不去想,是否能够成功 ,既然选择了远方 ,便只顾风雨兼程。\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(\"hasty\")\n }\n }.padding()\n\n Button(action: {\n // TODO:\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n\n }\n }\n```\n\n\n\n\n\n接下来绘制底部的 ActionView。包含两个 Icon。\n\n```swift\nstruct ActionView: View {\n @State var liked = false\n\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n```\n\n在 HomeView 中 ZStack 末尾添加。\n\n```swift\nActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n```\n\n可以看到如图。\n\n\n\n这里使用了 `.offset` 而不是 `.position` 去定位,是因为使用 position 去定位会丢失 `width: 100%` ,可以理解为 CSS 中 `block` 使用 `absolute` 之后变成了 `inline-block`, 而使用 `.offset` 只是` relative` 中的定位。\n\n### TabView\n\n接下来,绘制底部 Tabbar。在 Swift UI 中使用默认的 Tabbar 极为简单。只需要使用 `TabView` 即可。\n\n在 `xxApp.swift` (为你的 project_nameApp.swift,比如我的 Project 为 Meet,则为 `MeetApp.swift`) 中增加 `TabView`\n\n```swift\nstruct MeetApp: App {\n var body: some Scene {\n WindowGroup {\n TabView {\n ContentView().tabItem { Label(\"遇见\", systemImage: \"circle\") }\n }\n }\n }\n}\n```\n\nTabView 中每个 View 都会在底部 tab 中存在一个 Item,使用 `.tabItem` 定义这个 item 的文字和 image。有且只有一个 text 和 image。我们再新建一个 SwiftUI View 文件,命名为 `LikeView.swift` 。在 `MeetApp.swift` 中增加一个 View。\n\n```swift\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: \"largecircle.fill.circle\")\n }\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: \"heart.circle.fill\")\n }\n }\n .accentColor(.pink) // 修改默认主题色\n```\n\n然后我们给 tabItem 增加 tag,让 Swift UI 知道当前选定的 tab 是哪个。如果被选中,修改为 Solid 的 Icon。当然我们可以使用 `@State` 和 `.onTapGesture` 实现。\n\n```swift\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n \n var body: some Scene {\n return WindowGroup {\n TabView(selection: $activeTabIndex) {\n HomeView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != 1 ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = 1\n }\n }\n .tag(1)\n }\n .accentColor(.pink)\n }\n }\n}\n```\n\n注意:`.tag` 是不可或缺的。否则无效。\n\n大功告成!\n\n\n\n下一篇文章,将构建数据层。\n\n(未待完续)",
|
|
228
|
-
"title": "从零开始的 Swift UI (一)",
|
|
229
|
-
"id": "5fe933425b11408f99ad9d0c",
|
|
230
|
-
"type": "post",
|
|
231
|
-
"object_id": "5fe933425b11408f99ad9d0c",
|
|
232
|
-
"highlight_result": {
|
|
233
|
-
"text": {
|
|
234
|
-
"value": "注: 本文编写时,使用 Xcode <em>1</em>2.3、Swift 5.3.2 来构建 App\n\n入门 Swift UI 已经有一段时间了,但是却一直没有写过什么练手项目,虽然之前跟着 Hackingwithswift 上找着写过几个 Demo。突然打算自己独立写一个练手项目,因为是练手项目,所以布局和功能上也很简单,App 的类型大概和 TODO 类似。\n\n\n\n## 准备\n\n打开 Xcode 新建一个项目在此不再展开。在左侧文件树中打开 `ContentView.swift`,这是 View 的入口文件。你可以看到如下代码。\n\n```swift\nimport SwiftUI\n\nstruct ContentView: View {\n var body: some View {\n Text(\"Hello, world!\")\n .padding()\n }\n}\n\nstruct ContentView_Previews: PreviewProvider {\n static var previews: some View {\n ContentView()\n }\n}\n```\n\n在 Swift UI 2.0 中,UI 主入口文件从复杂的 `AppDelegate.swift` 和 `SceneDelegate.swift` 转变为仅仅只有几行的 `xxApp.swift`,得益于 Swift 5.3 加入的 `@main` 关键字\n\n```swift\nimport SwiftUI\n\n@main\nstruct MeetApp: App {\n var body: some Scene {\n WindowGroup {\n ContentView()\n }\n }\n}\n```\n\n## 布局\n\n### HomeView\n\n首先新建一个 View,`Command + N` 选择 SwiftUI View,命名为 `HomeView.swift`。将 HomeView 修改为如下代码。\n\n```swift\nstruct HomeView: View {\n var body: some View {\n VStack {\n Text(\"我不去想,是否能够成功 ,既然选择了远方 ,便只顾风雨兼程。\")\n .foregroundColor(.blue)\n .padding(.vertical)\n \n Text(\"hasty\")\n }.padding()\n }\n}\n```\n\n\n\n接下来,绘制圆形 Button。在 Swift UI 中绘制图形十分简单,Swift UI 中内置了 `Circle` 组件,只要使用 ZStack 和 Circle 结合,很容易编写这个组件。\n\n```swift\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n```\n\n这个组件绘制了整个图形,其中 Image 接收一个 SFSymbol 字符串。SF Symbols 可以在[这里下载](https://developer.apple.com/sf-symbols/)。绘制完了图形接下来需要在 View 中使用这个图形,并定位到对应的地点。\n\n在 Swift UI 中,可以使用 ZStack 结合 `.postion` 定位到指定地点。为了获取到整个视窗的长宽,还需要 `GeometryReader` 去读取子 View 的长宽。在根 View 包裹可以获取到设备的长宽。\n\n```swift\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(\"我不去想,是否能够成功 ,既然选择了远方 ,便只顾风雨兼程。\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(\"hasty\")\n }\n }.padding()\n\n Button(action: {\n // TODO:\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n\n }\n }\n```\n\n\n\n\n\n接下来绘制底部的 ActionView。包含两个 Icon。\n\n```swift\nstruct ActionView: View {\n @State var liked = false\n\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n```\n\n在 HomeView 中 ZStack 末尾添加。\n\n```swift\nActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n```\n\n可以看到如图。\n\n\n\n这里使用了 `.offset` 而不是 `.position` 去定位,是因为使用 position 去定位会丢失 `width: <em>1</em>00%` ,可以理解为 CSS 中 `block` 使用 `absolute` 之后变成了 `inline-block`, 而使用 `.offset` 只是` relative` 中的定位。\n\n### TabView\n\n接下来,绘制底部 Tabbar。在 Swift UI 中使用默认的 Tabbar 极为简单。只需要使用 `TabView` 即可。\n\n在 `xxApp.swift` (为你的 project_nameApp.swift,比如我的 Project 为 Meet,则为 `MeetApp.swift`) 中增加 `TabView`\n\n```swift\nstruct MeetApp: App {\n var body: some Scene {\n WindowGroup {\n TabView {\n ContentView().tabItem { Label(\"遇见\", systemImage: \"circle\") }\n }\n }\n }\n}\n```\n\nTabView 中每个 View 都会在底部 tab 中存在一个 Item,使用 `.tabItem` 定义这个 item 的文字和 image。有且只有一个 text 和 image。我们再新建一个 SwiftUI View 文件,命名为 `LikeView.swift` 。在 `MeetApp.swift` 中增加一个 View。\n\n```swift\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: \"largecircle.fill.circle\")\n }\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: \"heart.circle.fill\")\n }\n }\n .accentColor(.pink) // 修改默认主题色\n```\n\n然后我们给 tabItem 增加 tag,让 Swift UI 知道当前选定的 tab 是哪个。如果被选中,修改为 Solid 的 Icon。当然我们可以使用 `@State` 和 `.onTapGesture` 实现。\n\n```swift\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n \n var body: some Scene {\n return WindowGroup {\n TabView(selection: $activeTabIndex) {\n HomeView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != <em>1</em> ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = <em>1</em>\n }\n }\n .tag(<em>1</em>)\n }\n .accentColor(.pink)\n }\n }\n}\n```\n\n注意:`.tag` 是不可或缺的。否则无效。\n\n大功告成!\n\n\n\n下一篇文章,将构建数据层。\n\n(未待完续)",
|
|
235
|
-
"match_level": "full",
|
|
236
|
-
"fully_highlighted": false,
|
|
237
|
-
"matched_words": ["1"]
|
|
238
|
-
},
|
|
239
|
-
"title": {
|
|
240
|
-
"value": "从零开始的 Swift UI (一)",
|
|
241
|
-
"match_level": "none",
|
|
242
|
-
"matched_words": []
|
|
243
|
-
},
|
|
244
|
-
"id": {
|
|
245
|
-
"value": "5fe933425b11408f99ad9d0c",
|
|
246
|
-
"match_level": "none",
|
|
247
|
-
"matched_words": []
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
"text": "记录 9月 至 12月 此网站的更新内容。\n\n# 前端\n\n- 增加了日记音乐自动播放\n- 利用 Socket 实时更新文章的最新内容\n- 利用 Socket 实时更新当前文章的评论\n- 其他优化\n\n# 后端\n\n- 增加了 GraphQL 的支持\n- 其他 Bug 修复",
|
|
253
|
-
"title": "年终更新小记",
|
|
254
|
-
"id": "5fd5d40adadfe8960b838356",
|
|
255
|
-
"type": "post",
|
|
256
|
-
"object_id": "5fd5d40adadfe8960b838356",
|
|
257
|
-
"highlight_result": {
|
|
258
|
-
"text": {
|
|
259
|
-
"value": "记录 9月 至 <em>1</em>2月 此网站的更新内容。\n\n# 前端\n\n- 增加了日记音乐自动播放\n- 利用 Socket 实时更新文章的最新内容\n- 利用 Socket 实时更新当前文章的评论\n- 其他优化\n\n# 后端\n\n- 增加了 GraphQL 的支持\n- 其他 Bug 修复",
|
|
260
|
-
"match_level": "full",
|
|
261
|
-
"fully_highlighted": false,
|
|
262
|
-
"matched_words": ["1"]
|
|
263
|
-
},
|
|
264
|
-
"title": {
|
|
265
|
-
"value": "年终更新小记",
|
|
266
|
-
"match_level": "none",
|
|
267
|
-
"matched_words": []
|
|
268
|
-
},
|
|
269
|
-
"id": {
|
|
270
|
-
"value": "5fd5d40adadfe8960b838356",
|
|
271
|
-
"match_level": "none",
|
|
272
|
-
"matched_words": []
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
"text": "Vue 3 终于在 2020.9.18 发布了第一个正式版「One Piece」,到现在已经一周了。终于有时间来体验一把正式版的 Vue 3 是什么样子了。\n\n## 准备\n\n### 初始化项目\n\n这次,我不再使用 vite 来建立项目,而使用 vue-cli。\n\n```sh\nvue create vue3-blog\ncd vue3-blog\nvue add typescript\nyarn add vue-router@next\nyarn add vuex@next\nyarn serve\n```\n\n注意在选择 vue 版本的时候选择 vue3-preview\n\n```sh\n? Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)\n```\n\n首先打开 App.vue,清理一下默认的模板,如下\n\n```vue\n<template>\n <router-view> </router-view>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue'\nexport default defineComponent({\n name: 'App',\n})\n</script>\n\n<style></style>\n\n```\n\n注:除了这个文件使用 `.vue` 后缀之外,其他一律文件采用 `tsx` 编写。\n\n### 引入路由\n\n在 `src` 目录新建一个 `router.ts`,写入如下代码\n\n```ts\n/*\n * @Author: Innei\n * @Date: 2020-09-25 15:16:26\n * @LastEditTime: 2020-09-25 15:31:18\n * @LastEditors: Innei\n * @FilePath: /vue3-blog/src/router.ts\n * @Mark: Coding with Love\n */\nimport { defineAsyncComponent } from 'vue'\nimport { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'\n\nconst routes: RouteRecordRaw[] = [\n {\n name: 'root',\n path: '/',\n component: () => import('./App.vue'),\n children: [\n {\n path: '/',\n component: defineAsyncComponent(() =>\n import('./views/home').then(mo => mo.HomeView),\n ),\n name: 'home',\n },\n ],\n },\n]\n\nexport const router = createRouter({\n history: createWebHashHistory(),\n routes,\n})\n\nrouter.beforeEach((before, to, next) => {\n // todo\n next()\n})\n\nexport default router\n\n```\n\n写法略微和 vue2-router 有点不同。\n\n接下来来写一个视图(view)。新建一个目录`views`,新建`home/index.tsx`。\n\n写如下代码。\n\n```tsx\nimport { defineComponent, ref } from 'vue'\n\nexport const HomeView = defineComponent({\n setup() {\n const names = ref([{ name: 'foo' }, { name: 'bar' }])\n\n return () => (\n <div class=\"\">\n <p>HomeView</p>\n\n <ul>\n {names.value.map(item => {\n return <li>{item.name}</li>\n })}\n </ul>\n </div>\n )\n },\n})\n\n```\n\n执行`yarn serve`之后,应该会显示如下。\n\n\n\n## 数据\n\n如果使用 vue 3 composition api 的写法,所有的数据操作都发生在 setup 函数。写法类似于 react hooks。\n\n接下来我以调用 api 获取文章标题,渲染一个列表为例,填一填遇到的坑。\n\n代码如下\n\n```tsx\nimport { useApi } from '@/hooks/useApi'\nimport { PostResModel } from '@/models/post'\nimport { defineComponent, ref } from 'vue'\n\nexport const HomeView = defineComponent({\n setup() {\n const api = useApi()\n const posts = ref<PostResModel[]>([])\n\n api('Post')\n .gets(1, 10)\n .then(res => {\n const data = res.data\n // posts.push(...data)\n posts.value = data\n })\n\n return () => (\n <div class=\"\">\n <p>HomeView</p>\n\n <ul>\n {posts.value.map(post => {\n return <li>{post.title}</li>\n })}\n </ul>\n </div>\n )\n },\n})\n\n```\n\napi 的部分暂时忽略,返回为的 response 为一个 `data` 的数组。包括了 `title` 的字段。像上面的写法是可以达到预期效果的。\n\n\n\n但是有几个达不到预期的写法,在这里也提一下。\n\n首先是数据的更改的时候。\n\n如果用了 `reactive` 包裹了 data,如:\n\n```ts\n// const posts = ref<PostResModel[]>([])\nlet posts = reactive<PostResModel[]>([])\n```\n\n那么,想要在获取数据之后改变 `posts` 中的值,貌似只能用 `posts.push()` 的方式,以下方式会失去响应式。\n\n```ts\nposts = res.data // 不能达到预期\nposts = reactive<PostResModel[]>(res.data) // 不能达到预期\nposts.push(...res.data) // 可以\n```\n\n但是如果用 `ref`。那就可以这样写了。\n\n```ts\nposts.value = res.data\n// or\nposts.value.push(...res.data)\n```\n\n注意,ref 需要通过 `.value` 获取被 proxy 的值。\n\n个人认为,一般的对象可以用 `reactive` wrap,而 array 以及原始类型可以用 `ref` wrap。`reactive` 的好处是不用多写一个 `.value`。\n\n\n\n\n\n**未待完续**",
|
|
278
|
-
"title": "Vue 3 正式发布,再度踩坑",
|
|
279
|
-
"id": "5f6dbe08946127628d8a872c",
|
|
280
|
-
"type": "post",
|
|
281
|
-
"object_id": "5f6dbe08946127628d8a872c",
|
|
282
|
-
"highlight_result": {
|
|
283
|
-
"text": {
|
|
284
|
-
"value": "Vue 3 终于在 2020.9.<em>1</em>8 发布了第一个正式版「One Piece」,到现在已经一周了。终于有时间来体验一把正式版的 Vue 3 是什么样子了。\n\n## 准备\n\n### 初始化项目\n\n这次,我不再使用 vite 来建立项目,而使用 vue-cli。\n\n```sh\nvue create vue3-blog\ncd vue3-blog\nvue add typescript\nyarn add vue-router@next\nyarn add vuex@next\nyarn serve\n```\n\n注意在选择 vue 版本的时候选择 vue3-preview\n\n```sh\n? Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)\n```\n\n首先打开 App.vue,清理一下默认的模板,如下\n\n```vue\n<template>\n <router-view> </router-view>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue'\nexport default defineComponent({\n name: 'App',\n})\n</script>\n\n<style></style>\n\n```\n\n注:除了这个文件使用 `.vue` 后缀之外,其他一律文件采用 `tsx` 编写。\n\n### 引入路由\n\n在 `src` 目录新建一个 `router.ts`,写入如下代码\n\n```ts\n/*\n * @Author: Innei\n * @Date: 2020-09-25 <em>1</em>5:<em>1</em>6:26\n * @LastEditTime: 2020-09-25 <em>1</em>5:31:<em>1</em>8\n * @LastEditors: Innei\n * @FilePath: /vue3-blog/src/router.ts\n * @Mark: Coding with Love\n */\nimport { defineAsyncComponent } from 'vue'\nimport { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'\n\nconst routes: RouteRecordRaw[] = [\n {\n name: 'root',\n path: '/',\n component: () => import('./App.vue'),\n children: [\n {\n path: '/',\n component: defineAsyncComponent(() =>\n import('./views/home').then(mo => mo.HomeView),\n ),\n name: 'home',\n },\n ],\n },\n]\n\nexport const router = createRouter({\n history: createWebHashHistory(),\n routes,\n})\n\nrouter.beforeEach((before, to, next) => {\n // todo\n next()\n})\n\nexport default router\n\n```\n\n写法略微和 vue2-router 有点不同。\n\n接下来来写一个视图(view)。新建一个目录`views`,新建`home/index.tsx`。\n\n写如下代码。\n\n```tsx\nimport { defineComponent, ref } from 'vue'\n\nexport const HomeView = defineComponent({\n setup() {\n const names = ref([{ name: 'foo' }, { name: 'bar' }])\n\n return () => (\n <div class=\"\">\n <p>HomeView</p>\n\n <ul>\n {names.value.map(item => {\n return <li>{item.name}</li>\n })}\n </ul>\n </div>\n )\n },\n})\n\n```\n\n执行`yarn serve`之后,应该会显示如下。\n\n\n\n## 数据\n\n如果使用 vue 3 composition api 的写法,所有的数据操作都发生在 setup 函数。写法类似于 react hooks。\n\n接下来我以调用 api 获取文章标题,渲染一个列表为例,填一填遇到的坑。\n\n代码如下\n\n```tsx\nimport { useApi } from '@/hooks/useApi'\nimport { PostResModel } from '@/models/post'\nimport { defineComponent, ref } from 'vue'\n\nexport const HomeView = defineComponent({\n setup() {\n const api = useApi()\n const posts = ref<PostResModel[]>([])\n\n api('Post')\n .gets(<em>1</em>, <em>1</em>0)\n .then(res => {\n const data = res.data\n // posts.push(...data)\n posts.value = data\n })\n\n return () => (\n <div class=\"\">\n <p>HomeView</p>\n\n <ul>\n {posts.value.map(post => {\n return <li>{post.title}</li>\n })}\n </ul>\n </div>\n )\n },\n})\n\n```\n\napi 的部分暂时忽略,返回为的 response 为一个 `data` 的数组。包括了 `title` 的字段。像上面的写法是可以达到预期效果的。\n\n\n\n但是有几个达不到预期的写法,在这里也提一下。\n\n首先是数据的更改的时候。\n\n如果用了 `reactive` 包裹了 data,如:\n\n```ts\n// const posts = ref<PostResModel[]>([])\nlet posts = reactive<PostResModel[]>([])\n```\n\n那么,想要在获取数据之后改变 `posts` 中的值,貌似只能用 `posts.push()` 的方式,以下方式会失去响应式。\n\n```ts\nposts = res.data // 不能达到预期\nposts = reactive<PostResModel[]>(res.data) // 不能达到预期\nposts.push(...res.data) // 可以\n```\n\n但是如果用 `ref`。那就可以这样写了。\n\n```ts\nposts.value = res.data\n// or\nposts.value.push(...res.data)\n```\n\n注意,ref 需要通过 `.value` 获取被 proxy 的值。\n\n个人认为,一般的对象可以用 `reactive` wrap,而 array 以及原始类型可以用 `ref` wrap。`reactive` 的好处是不用多写一个 `.value`。\n\n\n\n\n\n**未待完续**",
|
|
285
|
-
"match_level": "full",
|
|
286
|
-
"fully_highlighted": false,
|
|
287
|
-
"matched_words": ["1"]
|
|
288
|
-
},
|
|
289
|
-
"title": {
|
|
290
|
-
"value": "Vue 3 正式发布,再度踩坑",
|
|
291
|
-
"match_level": "none",
|
|
292
|
-
"matched_words": []
|
|
293
|
-
},
|
|
294
|
-
"id": {
|
|
295
|
-
"value": "5f6dbe08946127628d8a872c",
|
|
296
|
-
"match_level": "none",
|
|
297
|
-
"matched_words": []
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
"text": "为什么要使用位操作,因为位操作是直接操作二进制数,是所有语言中执行效率最高的运算。\n\n以下代码以 JavaScript 为例,部分代码在所有支持位操作的语言通用。\n\n注:JavaScript 中数值以 IEEE 754 双精度浮点数表示。\n\n**快速取整**:\n\n```js\nparseInt(2.2) === ~~2.2 // true\nparseInt('1.3') === ~~'1.3' // true\n1<<30 === ~~1<<30 // true\n\nMath.floor(1.2) == 1.2 | 0\nMath.floor(1.2) == 1.2 ^ 0\n\nMath.floor(-1.2) == -1.2 | 0\nMath.floor(-1.2) == -1.2 ^ 0\n\n// HINT\n\nparseInt('4294967296') === ~~4294967296 //false, 越界\nparseInt('4294967296') === ~~'4294967296' //false, 越界\n```\n\n**快速累加**\n\n```js\n-~undefined === 1 // true\n-~0 === 1 // true\n-~1 === 2 // true\n-~-1 === 0 // true, -~-1 为 -0\n\n-~2<<30 === 2<<29+1 //false\n```\n\n**是否奇数**\n\n```js\n1 & 1 === 1 // 1 为奇数\n2 & 1 === 0 // 0 为偶数\n3 & 1 === 1 // etc.\n```\n\n**权限**\n\n```js\n// 比如我有 2 3 4 号权限\npermission = 1 << 2 | 1 << 3 | 1 << 4\n// 现在判断我有没有 3 号权限\nhasPerssion3 = !!(permission & 1 << 3) // res is 8, true\nhasPerssion5 = !!(permission & 1 << 5) // res is 0, false\nhasPerssion0 = !!(permission & 1 << 0) // res is 0, false\n\n```\n\n注意最大边界为 `1 << 30`, 更大需要用 `BigInt`\n\n**获取数组中只出现一次的数字**\n\n1. 交换律:a ^ b ^ c <=> a ^ c ^ b\n1. 任何数于0异或为任何数 0 ^ n => n\n1. 相同的数异或为0: n ^ n => 0\n\n\n```js\nlet res = 0\narr.forEach(i => res ^= i)\n// return res\n```\n\n**0-1互转**\n\n```js\n0^1 == 1\n1^1 == 0\n```\n\n**两数中点**\n\n```js\nleft + ((right - left) >> 1) === (left + right) / 2 // int\n```\n<!-- \n**XOR**\n\n```js\n1 ^ 1 == 1 - 1\n3 ^ 1 == 3 - 1\n5 ^ 0 == 5\n-3 ^ 1 == -3 - 1\n\n``` -->\n\n\n**持续更新,有更好的用法欢迎评论区指出**",
|
|
303
|
-
"title": "每天一个劝退小技巧之位操作",
|
|
304
|
-
"id": "5f5a142413d00b5a750d9054",
|
|
305
|
-
"type": "post",
|
|
306
|
-
"object_id": "5f5a142413d00b5a750d9054",
|
|
307
|
-
"highlight_result": {
|
|
308
|
-
"text": {
|
|
309
|
-
"value": "为什么要使用位操作,因为位操作是直接操作二进制数,是所有语言中执行效率最高的运算。\n\n以下代码以 JavaScript 为例,部分代码在所有支持位操作的语言通用。\n\n注:JavaScript 中数值以 IEEE 754 双精度浮点数表示。\n\n**快速取整**:\n\n```js\nparseInt(2.2) === ~~2.2 // true\nparseInt('<em>1</em>.3') === ~~'<em>1</em>.3' // true\n<em>1</em><<30 === ~~<em>1</em><<30 // true\n\nMath.floor(<em>1</em>.2) == <em>1</em>.2 | 0\nMath.floor(<em>1</em>.2) == <em>1</em>.2 ^ 0\n\nMath.floor(-<em>1</em>.2) == -<em>1</em>.2 | 0\nMath.floor(-<em>1</em>.2) == -<em>1</em>.2 ^ 0\n\n// HINT\n\nparseInt('4294967296') === ~~4294967296 //false, 越界\nparseInt('4294967296') === ~~'4294967296' //false, 越界\n```\n\n**快速累加**\n\n```js\n-~undefined === <em>1</em> // true\n-~0 === <em>1</em> // true\n-~<em>1</em> === 2 // true\n-~-<em>1</em> === 0 // true, -~-<em>1</em> 为 -0\n\n-~2<<30 === 2<<29+<em>1</em> //false\n```\n\n**是否奇数**\n\n```js\n<em>1</em> & <em>1</em> === <em>1</em> // <em>1</em> 为奇数\n2 & <em>1</em> === 0 // 0 为偶数\n3 & <em>1</em> === <em>1</em> // etc.\n```\n\n**权限**\n\n```js\n// 比如我有 2 3 4 号权限\npermission = <em>1</em> << 2 | <em>1</em> << 3 | <em>1</em> << 4\n// 现在判断我有没有 3 号权限\nhasPerssion3 = !!(permission & <em>1</em> << 3) // res is 8, true\nhasPerssion5 = !!(permission & <em>1</em> << 5) // res is 0, false\nhasPerssion0 = !!(permission & <em>1</em> << 0) // res is 0, false\n\n```\n\n注意最大边界为 `<em>1</em> << 30`, 更大需要用 `BigInt`\n\n**获取数组中只出现一次的数字**\n\n<em>1</em>. 交换律:a ^ b ^ c <=> a ^ c ^ b\n<em>1</em>. 任何数于0异或为任何数 0 ^ n => n\n<em>1</em>. 相同的数异或为0: n ^ n => 0\n\n\n```js\nlet res = 0\narr.forEach(i => res ^= i)\n// return res\n```\n\n**0-<em>1</em>互转**\n\n```js\n0^<em>1</em> == <em>1</em>\n<em>1</em>^<em>1</em> == 0\n```\n\n**两数中点**\n\n```js\nleft + ((right - left) >> <em>1</em>) === (left + right) / 2 // int\n```\n<!-- \n**XOR**\n\n```js\n<em>1</em> ^ <em>1</em> == <em>1</em> - <em>1</em>\n3 ^ <em>1</em> == 3 - <em>1</em>\n5 ^ 0 == 5\n-3 ^ <em>1</em> == -3 - <em>1</em>\n\n``` -->\n\n\n**持续更新,有更好的用法欢迎评论区指出**",
|
|
310
|
-
"match_level": "full",
|
|
311
|
-
"fully_highlighted": false,
|
|
312
|
-
"matched_words": ["1"]
|
|
313
|
-
},
|
|
314
|
-
"title": {
|
|
315
|
-
"value": "每天一个劝退小技巧之位操作",
|
|
316
|
-
"match_level": "none",
|
|
317
|
-
"matched_words": []
|
|
318
|
-
},
|
|
319
|
-
"id": {
|
|
320
|
-
"value": "5f5a142413d00b5a750d9054",
|
|
321
|
-
"match_level": "none",
|
|
322
|
-
"matched_words": []
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
"text": "在很多时候都需要处理文字的溢出,尤其是对单行网格处理时,需要避免文字过长导致容器撑坏的情况。一般会固定文字最大宽度和 `overflow: hidden; text-overflow: ellipsis` 让溢出的文字显示成 `...`。但是现在可以用 CSS 的 mark 属性,让溢出的文字边缘羽化。\n\n如图 Chrome 的 tab。\n\n\n\n首先看看 `mark-image` 的兼容性。\n\n\n\n基本上都支持,需要注意的是我使用的 Chrome 85,还在试验性阶段,需要加上前缀 `-webkit-`\n\n`mask-image` 和 `background-image` 的值一样,和蒙版一样,黑色的显示,透明的不显示。我们可以很简单的用 `linear-gradient` 完成边缘羽化效果。\n\n我们来模仿一个 Chrome Tab 的样式。首先建立一个骨架。\n\n```html\n<div class=\"tab-wrap\">\n <div class=\"tab\">\n <span class=\"tab-text cursor\"\n >一个标题很长的标签 一个标题很长的标签 一个标题很长的标签\n 一个标题很长的标签</span\n >\n </div>\n <div class=\"close cursor\">×</div>\n</div>\n```\n\n确定好外层容器的宽高后,可以对 `span` 的父元素设置 `mask`。\n\n```css\n.tab-wrap .tab {\n width: 100%;\n overflow: hidden;\n -webkit-mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, 1) calc(100% - 2em),\n transparent\n );\n mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, 1) calc(100% - 2em),\n transparent\n );\n}\n.tab .tab-text {\n white-space: nowrap;\n}\n```\n\n最后再加亿点点小细节,大功告成啦。\n\n\n\n当然啦,如果遇到不支持的浏览器就显示直接截断的效果,很不好看,我们还想要让他显示 `...`,那么可以用 `@supports` 查询,是否支持这个属性,如果支持才使用,不支持就使用 `text-overflow: ellipsis;`。\n\n修改一下,`span` 的父级样式。\n\n```css\n.tab-wrap .tab {\n width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n@supports (-webkit-mask-image: inherit) or (mask-image: inherit) {\n .tab-wrap .tab {\n text-overflow: clip;\n -webkit-mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, 1) calc(100% - 2em),\n transparent\n );\n mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, 1) calc(100% - 2em),\n transparent\n );\n }\n}\n```\n\n\n\n完整的代码请戳: [Gist](https://gist.github.com/Innei/d8dcaebe9ac919c4a1d0462b2f0ef6b8)",
|
|
328
|
-
"title": "文字溢出边缘羽化 CSS Mask 实现",
|
|
329
|
-
"id": "5f54dae24f80b551b79583d1",
|
|
330
|
-
"type": "post",
|
|
331
|
-
"object_id": "5f54dae24f80b551b79583d1",
|
|
332
|
-
"highlight_result": {
|
|
333
|
-
"text": {
|
|
334
|
-
"value": "在很多时候都需要处理文字的溢出,尤其是对单行网格处理时,需要避免文字过长导致容器撑坏的情况。一般会固定文字最大宽度和 `overflow: hidden; text-overflow: ellipsis` 让溢出的文字显示成 `...`。但是现在可以用 CSS 的 mark 属性,让溢出的文字边缘羽化。\n\n如图 Chrome 的 tab。\n\n\n\n首先看看 `mark-image` 的兼容性。\n\n\n\n基本上都支持,需要注意的是我使用的 Chrome 85,还在试验性阶段,需要加上前缀 `-webkit-`\n\n`mask-image` 和 `background-image` 的值一样,和蒙版一样,黑色的显示,透明的不显示。我们可以很简单的用 `linear-gradient` 完成边缘羽化效果。\n\n我们来模仿一个 Chrome Tab 的样式。首先建立一个骨架。\n\n```html\n<div class=\"tab-wrap\">\n <div class=\"tab\">\n <span class=\"tab-text cursor\"\n >一个标题很长的标签 一个标题很长的标签 一个标题很长的标签\n 一个标题很长的标签</span\n >\n </div>\n <div class=\"close cursor\">×</div>\n</div>\n```\n\n确定好外层容器的宽高后,可以对 `span` 的父元素设置 `mask`。\n\n```css\n.tab-wrap .tab {\n width: <em>1</em>00%;\n overflow: hidden;\n -webkit-mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, <em>1</em>) calc(<em>1</em>00% - 2em),\n transparent\n );\n mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, <em>1</em>) calc(<em>1</em>00% - 2em),\n transparent\n );\n}\n.tab .tab-text {\n white-space: nowrap;\n}\n```\n\n最后再加亿点点小细节,大功告成啦。\n\n\n\n当然啦,如果遇到不支持的浏览器就显示直接截断的效果,很不好看,我们还想要让他显示 `...`,那么可以用 `@supports` 查询,是否支持这个属性,如果支持才使用,不支持就使用 `text-overflow: ellipsis;`。\n\n修改一下,`span` 的父级样式。\n\n```css\n.tab-wrap .tab {\n width: <em>1</em>00%;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n@supports (-webkit-mask-image: inherit) or (mask-image: inherit) {\n .tab-wrap .tab {\n text-overflow: clip;\n -webkit-mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, <em>1</em>) calc(<em>1</em>00% - 2em),\n transparent\n );\n mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, <em>1</em>) calc(<em>1</em>00% - 2em),\n transparent\n );\n }\n}\n```\n\n\n\n完整的代码请戳: [Gist](https://gist.github.com/Innei/d8dcaebe9ac919c4a1d0462b2f0ef6b8)",
|
|
335
|
-
"match_level": "full",
|
|
336
|
-
"fully_highlighted": false,
|
|
337
|
-
"matched_words": ["1"]
|
|
338
|
-
},
|
|
339
|
-
"title": {
|
|
340
|
-
"value": "文字溢出边缘羽化 CSS Mask 实现",
|
|
341
|
-
"match_level": "none",
|
|
342
|
-
"matched_words": []
|
|
343
|
-
},
|
|
344
|
-
"id": {
|
|
345
|
-
"value": "5f54dae24f80b551b79583d1",
|
|
346
|
-
"match_level": "none",
|
|
347
|
-
"matched_words": []
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
"text": "好的代码,往往不是又臭又长,而是小而精悍。用更少的代码,实现相同的功能。不管是自己还是同事日后阅读,都不会感到困惑。\n\n在这里,记录前端开发中,优化代码的几种方式。\n\n**噩梦地狱嵌套**\n\n大概大家都看过怎么一张图\n\n\n\n在遇到这个写法的时候, 首先要想一想这样到底有没有用,有没有办法优化。像图中的根本就没有必要去嵌套。\n\n再来看个例子。\n\n```ts\nconst payload: <Record, unknown> | undefined = {\n page: '1',\n size: 10\n} // payload from server via request\nif (payload) {\n if (typeof payload.page === 'number' && typeof payload.size === 'number') {\n // do anything..\n }\n}\n// do anything..\n```\n\n这是很简单的数据验证,一般用于后端防止 noSQL 注入。\n\n简单的写法如下\n\n```ts\nif (!payload) {\n // do anything..\n // throw a bad request error\n}\nif (typeof payload.page !== 'number' || typeof payload.size !== 'number') {\n // do anything..\n // throw a 422 error\n}\n// do anything\n```\n\n这样就有效避免了嵌套。\n\n**糟糕的 if 判断**\n\n不知道这种写法,你们见的多不多。\n\n```ts\nif (a === 1 || b === 1 || c === 1) {\n // do anything...\n}\n```\n\n可以转换成\n\n```ts\nif ([a, b, c].includes(1)) {\n}\n```\n\n**表格选择法**\n\n你还是使用大量 `if`, 或者 `switch`,作为分支选择值吗,那就显得很没水平了,快来试试表格选择。\n\n```ts\n// switch\nlet week;\n\nswitch (week) {\n case 0:\n week = '周日';\n break;\n case 1:\n week = '周一';\n // ...\n default:\n break;\n}\n\n// convert to \nlet week = 0\nweek = ['日', '一', '二', '三', '四', '五', '六'][week]\n\n```\n\n更复杂一点? 当然可以,而且更加简洁。这里以判断文件类型为例。\n\n```ts\n const checkTypes = (ext: string) => {\n const i18n = {\n VIDEO: '视频',\n TXT: '文本',\n MUSIC: '音乐',\n DOC: '文档',\n EXL: '表格',\n PPT: '幻灯片',\n PDF: '图书',\n CODE: '代码',\n IMG: '图片',\n FILE: '文件',\n };\n return i18n[\n Object.entries(FileTypes).find(([key, val]) => {\n if (val.includes(ext)) {\n return key;\n }\n return false;\n })?.[0] || 'FILE'\n ];\n};\n\nconst FileTypes = Object.freeze({\n VIDEO: ['.mp4', '.avi', '.mov', '.mpg'],\n TXT: ['.txt'],\n MUSIC: ['.mp3'],\n DOC: ['.doc', '.docx'],\n EXL: ['.xls'],\n PPT: ['.ppt', '.pptx'],\n PDF: ['.pdf'],\n CODE: [\n '.js',\n '.c',\n '.cpp',\n '.py',\n '.html',\n '.css',\n '.scss',\n '.xml',\n '.swift',\n '.ts',\n '.java',\n '.go',\n '.asp',\n '.aspx',\n '.class',\n '.clw',\n '.cs',\n '.dsp',\n '.dsw',\n '.frm',\n '.frx',\n '.h',\n '.hpp',\n '.jar',\n '.lib',\n '.ocx',\n '.pyc',\n '.vbp',\n '.vbs',\n '.xsl',\n ],\n IMG: ['.png', '.jpg', '.jpeg', '.gif', '.svg'],\n});\n\nexport default FileTypes;\n\n```\n\n以上是我临时想到的,之后继续补充。",
|
|
353
|
-
"title": "编写更加简洁易阅读的代码",
|
|
354
|
-
"id": "5f3f31c4e495ba011ac4b934",
|
|
355
|
-
"type": "post",
|
|
356
|
-
"object_id": "5f3f31c4e495ba011ac4b934",
|
|
357
|
-
"highlight_result": {
|
|
358
|
-
"text": {
|
|
359
|
-
"value": "好的代码,往往不是又臭又长,而是小而精悍。用更少的代码,实现相同的功能。不管是自己还是同事日后阅读,都不会感到困惑。\n\n在这里,记录前端开发中,优化代码的几种方式。\n\n**噩梦地狱嵌套**\n\n大概大家都看过怎么一张图\n\n\n\n在遇到这个写法的时候, 首先要想一想这样到底有没有用,有没有办法优化。像图中的根本就没有必要去嵌套。\n\n再来看个例子。\n\n```ts\nconst payload: <Record, unknown> | undefined = {\n page: '<em>1</em>',\n size: <em>1</em>0\n} // payload from server via request\nif (payload) {\n if (typeof payload.page === 'number' && typeof payload.size === 'number') {\n // do anything..\n }\n}\n// do anything..\n```\n\n这是很简单的数据验证,一般用于后端防止 noSQL 注入。\n\n简单的写法如下\n\n```ts\nif (!payload) {\n // do anything..\n // throw a bad request error\n}\nif (typeof payload.page !== 'number' || typeof payload.size !== 'number') {\n // do anything..\n // throw a 422 error\n}\n// do anything\n```\n\n这样就有效避免了嵌套。\n\n**糟糕的 if 判断**\n\n不知道这种写法,你们见的多不多。\n\n```ts\nif (a === <em>1</em> || b === <em>1</em> || c === <em>1</em>) {\n // do anything...\n}\n```\n\n可以转换成\n\n```ts\nif ([a, b, c].includes(<em>1</em>)) {\n}\n```\n\n**表格选择法**\n\n你还是使用大量 `if`, 或者 `switch`,作为分支选择值吗,那就显得很没水平了,快来试试表格选择。\n\n```ts\n// switch\nlet week;\n\nswitch (week) {\n case 0:\n week = '周日';\n break;\n case <em>1</em>:\n week = '周一';\n // ...\n default:\n break;\n}\n\n// convert to \nlet week = 0\nweek = ['日', '一', '二', '三', '四', '五', '六'][week]\n\n```\n\n更复杂一点? 当然可以,而且更加简洁。这里以判断文件类型为例。\n\n```ts\n const checkTypes = (ext: string) => {\n const i18n = {\n VIDEO: '视频',\n TXT: '文本',\n MUSIC: '音乐',\n DOC: '文档',\n EXL: '表格',\n PPT: '幻灯片',\n PDF: '图书',\n CODE: '代码',\n IMG: '图片',\n FILE: '文件',\n };\n return i18n[\n Object.entries(FileTypes).find(([key, val]) => {\n if (val.includes(ext)) {\n return key;\n }\n return false;\n })?.[0] || 'FILE'\n ];\n};\n\nconst FileTypes = Object.freeze({\n VIDEO: ['.mp4', '.avi', '.mov', '.mpg'],\n TXT: ['.txt'],\n MUSIC: ['.mp3'],\n DOC: ['.doc', '.docx'],\n EXL: ['.xls'],\n PPT: ['.ppt', '.pptx'],\n PDF: ['.pdf'],\n CODE: [\n '.js',\n '.c',\n '.cpp',\n '.py',\n '.html',\n '.css',\n '.scss',\n '.xml',\n '.swift',\n '.ts',\n '.java',\n '.go',\n '.asp',\n '.aspx',\n '.class',\n '.clw',\n '.cs',\n '.dsp',\n '.dsw',\n '.frm',\n '.frx',\n '.h',\n '.hpp',\n '.jar',\n '.lib',\n '.ocx',\n '.pyc',\n '.vbp',\n '.vbs',\n '.xsl',\n ],\n IMG: ['.png', '.jpg', '.jpeg', '.gif', '.svg'],\n});\n\nexport default FileTypes;\n\n```\n\n以上是我临时想到的,之后继续补充。",
|
|
360
|
-
"match_level": "full",
|
|
361
|
-
"fully_highlighted": false,
|
|
362
|
-
"matched_words": ["1"]
|
|
363
|
-
},
|
|
364
|
-
"title": {
|
|
365
|
-
"value": "编写更加简洁易阅读的代码",
|
|
366
|
-
"match_level": "none",
|
|
367
|
-
"matched_words": []
|
|
368
|
-
},
|
|
369
|
-
"id": {
|
|
370
|
-
"value": "5f3f31c4e495ba011ac4b934",
|
|
371
|
-
"match_level": "none",
|
|
372
|
-
"matched_words": []
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
"text": "前几天,我在推特上看到这样一张图。\n\n\n\n原来地址栏还能这么玩,瞬间就觉得自己弱爆了。然后我决定去实现一下这个效果,然后做成一个库。\n\n花了一个晚上,终于做好了。这是最后的成果。\n\n\n\n这个库使用非常的简单。\n\n你只需要,\n\n```sh\nyarn add animate-uri\n```\n\n然后\n\n```js\nimport { animateUriFactory, bindAllLink } from 'animate-uri'\n\nanimateUriFactory({ duration: 60, shouldPushState: false }).start(\n '/hello-world',\n '/',\n)\n```\n\n这样就是一个简单的过渡效果了。\n\n玩玩可没有意思,在项目中使用才有意思。\n\n接下来我们在 Next.js 项目中加入一个好玩的东西。\n\n在 nextjs 中的自定义 `_app.tsx` 中加入如下,监听路由变化。\n\n\n```tsx\nimport { animateUriFactory } from 'animate-uri/publish/index.esm'\nconst animateInstance = animateUriFactory()\n\n// componentDidMount(): void {\nRouter.events.on('routeChangeStart', (url) => {\n animateInstance?.start(url)\n})\n\nRouter.events.on('routeChangeComplete', () => {\n animateInstance?.stop()\n})\n\n// }\n```\n\n大功告成。\n\n随便偷偷说一下仓库地址:[animate-uri](https://github.com/Innei/animate-uri)",
|
|
378
|
-
"title": "不同寻常的地址栏过渡",
|
|
379
|
-
"id": "5f37c66ae495ba011ac4597d",
|
|
380
|
-
"type": "post",
|
|
381
|
-
"object_id": "5f37c66ae495ba011ac4597d",
|
|
382
|
-
"highlight_result": {
|
|
383
|
-
"text": {
|
|
384
|
-
"value": "前几天,我在推特上看到这样一张图。\n\n\n\n原来地址栏还能这么玩,瞬间就觉得自己弱爆了。然后我决定去实现一下这个效果,然后做成一个库。\n\n花了一个晚上,终于做好了。这是最后的成果。\n\n\n\n这个库使用非常的简单。\n\n你只需要,\n\n```sh\nyarn add animate-uri\n```\n\n然后\n\n```js\nimport { animateUriFactory, bindAllLink } from 'animate-uri'\n\nanimateUriFactory({ duration: 60, shouldPushState: false }).start(\n '/hello-world',\n '/',\n)\n```\n\n这样就是一个简单的过渡效果了。\n\n玩玩可没有意思,在项目中使用才有意思。\n\n接下来我们在 Next.js 项目中加入一个好玩的东西。\n\n在 nextjs 中的自定义 `_app.tsx` 中加入如下,监听路由变化。\n\n\n```tsx\nimport { animateUriFactory } from 'animate-uri/publish/index.esm'\nconst animateInstance = animateUriFactory()\n\n// componentDidMount(): void {\nRouter.events.on('routeChangeStart', (url) => {\n animateInstance?.start(url)\n})\n\nRouter.events.on('routeChangeComplete', () => {\n animateInstance?.stop()\n})\n\n// }\n```\n\n大功告成。\n\n随便偷偷说一下仓库地址:[animate-uri](https://github.com/Innei/animate-uri)",
|
|
385
|
-
"match_level": "full",
|
|
386
|
-
"fully_highlighted": false,
|
|
387
|
-
"matched_words": ["1"]
|
|
388
|
-
},
|
|
389
|
-
"title": {
|
|
390
|
-
"value": "不同寻常的地址栏过渡",
|
|
391
|
-
"match_level": "none",
|
|
392
|
-
"matched_words": []
|
|
393
|
-
},
|
|
394
|
-
"id": {
|
|
395
|
-
"value": "5f37c66ae495ba011ac4597d",
|
|
396
|
-
"match_level": "none",
|
|
397
|
-
"matched_words": []
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
"title": "Electron 的打包与构建",
|
|
403
|
-
"text": "说起 Electron,大家能定不会感觉到陌生,庞大的体积,内置浏览器,Hello World 都有 200+M... 我个人是很反感跨段应用的,虽然对于开发来说,节省了很多时间,但是站在用户的角度来讲,体验就不是那么称心如意了。但是最近一些业务需要用到 Electron,折腾过程中也踩了不少坑,总结一下。\n\n ## 开发环境的搭建\n\n平时我们在开发前端应用时,一般都是使用 Webpack 去打包,在开发环境中,也是由 Webpack dev server 来实现 HMR。在 Electron 中也是可以使用 Webpack 的。\n\n我们使用 `electron-wepack` 包,简单搭建一下环境。\n\n```shell\nyarn add source-map-support\nyarn add -D electron electron-webpack electron-builder webpack \n```\n\n然后我们参考这个项目结构建立目录:\n\n```\nproject/\n├─ resources/\n│ ├─ icon \t\t\t\t\t\t// 程序图标\n├─ src/\n│ ├─ main/ \t\t\t\t\t// 主进程\n│ │ └─ index.ts\n│ ├─ renderer/ \t\t\t// 渲染层(启动界面)\n│ └─ index.js\n└─ static/ \t\t\t\t\t\t // 静态资源\n```\n\n`src` 目录下的分别为存放 Electron 主进程逻辑(main) 和 渲染层(renderer)。入口文件必须为 `index` 或 `main`\n\n### TypeScript 支持 (可选)\n\n```shell\nyarn add electron-webpack-ts typescript -D\n```\n\n安装完以上依赖,`electron-webpack` 会识别支持 TypeScript。\n\n### 渲染层\n\n在 `src/renderer/index.ts` 中,你可以操作 DOM 树。`electron-wepack`默认会提供一个空白的 HTML 文档,只有一个 `#app` 节点供你使用,你无法通过一般操作自定义一个入口 `index.html`, 但是你也可以用其他手段达到这个目标,在此不多赘述 (参看 issue)。\n\n```ts\n// src/renderer/index.ts\nconst $app = document.getElementById('app')!\n\n$app.textContent = 'Hello World'\n\n```\n\n### 主进程\n\n在 `src/main/index.ts` 中, 简单建立一个 app\n\n```ts\nimport { app, BrowserWindow } from 'electron'\nimport { createWindow } from './common/window'\n\nlet mainWindow: BrowserWindow\napp.on('ready', () => {\n mainWindow = createWindow()\n app.show()\n})\n\napp.on('window-all-closed', () => {\n if (process.platform !== 'darwin') {\n app.quit()\n }\n})\n\napp.on('activate', function () {\n if (mainWindow === null) {\n createWindow()\n }\n})\n```\n\n其次是 `window.ts`,建立一个 window\n\n```ts\nimport { BrowserWindow } from 'electron'\nimport path from 'path'\nimport { isDev } from '../utils'\nimport { format } from 'url'\nexport function createWindow() {\n const mainWindow = new BrowserWindow({\n height: 620,\n width: 400,\n webPreferences: { nodeIntegration: true }, // 一定要加!!!\n })\n if (isDev) {\n mainWindow.loadURL(\n `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`, // 开发环境\n )\n } else {\n mainWindow.loadURL(\n format({\n pathname: path.join(__dirname, 'index.html'),\n protocol: 'file',\n slashes: true,\n }),\n )\n }\n return mainWindow\n}\n\n```\n\n### 脚本\n\n在 `package.json` 中添加。\n\n```json\n{\n \"scripts\": {\n \"prebuild\": \"rm -rf dist\",\n \"build\": \"cross-env NODE_ENV=production electron-webpack\",\n \"start\": \"electron-webpack dev\",\n \"package\": \"yarn build && electron-builder build --publish never\"\n }\n}\n```\n\n执行 `yarn start` 。发现正确显示了 Hello World。\n\n\n\n使用 `yarn package` 来生成 dmg 也是没有问题的。一般教程到此就结束了,但是我们的需求并不是这么简单,我们还需要配置其他,比如 app version,app icon,app sign key... 而这些配置也有很多坑。\n\n## 配置\n\n### 图标\n\n应用图标需要不同大小的几张 png 以及 icns 等格式的图片,手动操作比较麻烦,我们可以用一张 png 去生成,使用 ` electron-icon-builder` 工具就能轻松转换到我们想要的结果。\n\n```\nnpx electron-icon-builder -i ./path-your-icon-file.png -o output\n```\n\n```\n.\n├── icon.icns\n├── icon.ico\n├── icon.png\n└── icons\n ├── 1024x1024.png\n ├── 128x128.png\n ├── 16x16.png\n ├── 24x24.png\n ├── 256x256.png\n ├── 32x32.png\n ├── 48x48.png\n ├── 512x512.png\n └── 64x64.png\n```\n\n把生成的文件放入 `resources` 文件夹内,如不存在则新建。\n\n\n\n在 `package.json` 中加入 `build` 字段,用于配置 `electron-builder`。\n\n```json\n{\n \"build\": {\n \"appId\": \"com.innei.electron-template\",\n \"productName\": \"template\",\n \"extraMetadata\": {\n \"main\": \"main.js\" // **必须** \n },\n \"copyright\": \"Copyright © 2019-2020 ${author}\",\n \"mac\": {\n \"category\": \"public.app-category.utilities\"\n },\n \"files\": [\n \"package.json\",\n \"resources/**/*\", \t\t\t\t\t\t// **必须** \n \"static\", \n {\n \"from\": \"dist/main\" \t // **必须** \n },\t\n {\n \"from\": \"dist/renderer\" // **必须** \n }\n ],\n \"extends\": null,\n \"dmg\": {\n \"contents\": [\n {\n \"x\": 130,\n \"y\": 220\n },\n {\n \"x\": 410,\n \"y\": 220,\n \"type\": \"link\",\n \"path\": \"/Applications\"\n }\n ]\n },\n \"win\": {\n \"icon\": \"resources/icon.ico\",\n \"target\": [\n \"nsis\",\n \"msi\"\n ]\n },\n \"nsis\": {\n \"oneClick\": false,\n \"allowToChangeInstallationDirectory\": true,\n \"installerIcon\": \"resources/icon.ico\"\n },\n \"linux\": {\n \"target\": [\n \"deb\",\n \"rpm\",\n \"AppImage\"\n ],\n \"category\": \"Development\"\n },\n \"directories\": {\n \"buildResources\": \"resources\",\n \"output\": \"release\"\n },\n \"extraResources\": [\n {\n \"from\": \"resources/\", // **必须** \n \"to\": \"resources/\" // **必须** \n },\n {\n \"from\": \"static\",\n \"to\": \"static\"\n }\n ]\n }\n}\n```\n\n这里是个大坑,因为我们自定义了配置,覆盖了原来 `electron-webpack` 的配置,所以有几个地方是必须要这么写的,否则就会在打包之后无法显示 renderer 或者 找不到入口文件。这是我自己摸索出来的,比较 hack 的方法。因为我实在找不到答案。\n\n如果你需要使用 `__static ` 常量的话,\n\n```\n{ // 也是必须的\n \"from\": \"static\",\n \"to\": \"static\"\n}\n```\n\n最后,附上 GitHub 地址:\n\n<https://github.com/Innei/electron-typescript-starter>",
|
|
404
|
-
"id": "5f0dc4dbddf2006d12774b6a",
|
|
405
|
-
"type": "post",
|
|
406
|
-
"object_id": "5f0dc4dbddf2006d12774b6a",
|
|
407
|
-
"highlight_result": {
|
|
408
|
-
"title": {
|
|
409
|
-
"value": "Electron 的打包与构建",
|
|
410
|
-
"match_level": "none",
|
|
411
|
-
"matched_words": []
|
|
412
|
-
},
|
|
413
|
-
"text": {
|
|
414
|
-
"value": "说起 Electron,大家能定不会感觉到陌生,庞大的体积,内置浏览器,Hello World 都有 200+M... 我个人是很反感跨段应用的,虽然对于开发来说,节省了很多时间,但是站在用户的角度来讲,体验就不是那么称心如意了。但是最近一些业务需要用到 Electron,折腾过程中也踩了不少坑,总结一下。\n\n ## 开发环境的搭建\n\n平时我们在开发前端应用时,一般都是使用 Webpack 去打包,在开发环境中,也是由 Webpack dev server 来实现 HMR。在 Electron 中也是可以使用 Webpack 的。\n\n我们使用 `electron-wepack` 包,简单搭建一下环境。\n\n```shell\nyarn add source-map-support\nyarn add -D electron electron-webpack electron-builder webpack \n```\n\n然后我们参考这个项目结构建立目录:\n\n```\nproject/\n├─ resources/\n│ ├─ icon \t\t\t\t\t\t// 程序图标\n├─ src/\n│ ├─ main/ \t\t\t\t\t// 主进程\n│ │ └─ index.ts\n│ ├─ renderer/ \t\t\t// 渲染层(启动界面)\n│ └─ index.js\n└─ static/ \t\t\t\t\t\t // 静态资源\n```\n\n`src` 目录下的分别为存放 Electron 主进程逻辑(main) 和 渲染层(renderer)。入口文件必须为 `index` 或 `main`\n\n### TypeScript 支持 (可选)\n\n```shell\nyarn add electron-webpack-ts typescript -D\n```\n\n安装完以上依赖,`electron-webpack` 会识别支持 TypeScript。\n\n### 渲染层\n\n在 `src/renderer/index.ts` 中,你可以操作 DOM 树。`electron-wepack`默认会提供一个空白的 HTML 文档,只有一个 `#app` 节点供你使用,你无法通过一般操作自定义一个入口 `index.html`, 但是你也可以用其他手段达到这个目标,在此不多赘述 (参看 issue)。\n\n```ts\n// src/renderer/index.ts\nconst $app = document.getElementById('app')!\n\n$app.textContent = 'Hello World'\n\n```\n\n### 主进程\n\n在 `src/main/index.ts` 中, 简单建立一个 app\n\n```ts\nimport { app, BrowserWindow } from 'electron'\nimport { createWindow } from './common/window'\n\nlet mainWindow: BrowserWindow\napp.on('ready', () => {\n mainWindow = createWindow()\n app.show()\n})\n\napp.on('window-all-closed', () => {\n if (process.platform !== 'darwin') {\n app.quit()\n }\n})\n\napp.on('activate', function () {\n if (mainWindow === null) {\n createWindow()\n }\n})\n```\n\n其次是 `window.ts`,建立一个 window\n\n```ts\nimport { BrowserWindow } from 'electron'\nimport path from 'path'\nimport { isDev } from '../utils'\nimport { format } from 'url'\nexport function createWindow() {\n const mainWindow = new BrowserWindow({\n height: 620,\n width: 400,\n webPreferences: { nodeIntegration: true }, // 一定要加!!!\n })\n if (isDev) {\n mainWindow.loadURL(\n `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`, // 开发环境\n )\n } else {\n mainWindow.loadURL(\n format({\n pathname: path.join(__dirname, 'index.html'),\n protocol: 'file',\n slashes: true,\n }),\n )\n }\n return mainWindow\n}\n\n```\n\n### 脚本\n\n在 `package.json` 中添加。\n\n```json\n{\n \"scripts\": {\n \"prebuild\": \"rm -rf dist\",\n \"build\": \"cross-env NODE_ENV=production electron-webpack\",\n \"start\": \"electron-webpack dev\",\n \"package\": \"yarn build && electron-builder build --publish never\"\n }\n}\n```\n\n执行 `yarn start` 。发现正确显示了 Hello World。\n\n\n\n使用 `yarn package` 来生成 dmg 也是没有问题的。一般教程到此就结束了,但是我们的需求并不是这么简单,我们还需要配置其他,比如 app version,app icon,app sign key... 而这些配置也有很多坑。\n\n## 配置\n\n### 图标\n\n应用图标需要不同大小的几张 png 以及 icns 等格式的图片,手动操作比较麻烦,我们可以用一张 png 去生成,使用 ` electron-icon-builder` 工具就能轻松转换到我们想要的结果。\n\n```\nnpx electron-icon-builder -i ./path-your-icon-file.png -o output\n```\n\n```\n.\n├── icon.icns\n├── icon.ico\n├── icon.png\n└── icons\n ├── <em>1</em>024x1024.png\n ├── <em>1</em>28x128.png\n ├── <em>1</em>6x16.png\n ├── 24x24.png\n ├── 256x256.png\n ├── 32x32.png\n ├── 48x48.png\n ├── 512x512.png\n └── 64x64.png\n```\n\n把生成的文件放入 `resources` 文件夹内,如不存在则新建。\n\n\n\n在 `package.json` 中加入 `build` 字段,用于配置 `electron-builder`。\n\n```json\n{\n \"build\": {\n \"appId\": \"com.innei.electron-template\",\n \"productName\": \"template\",\n \"extraMetadata\": {\n \"main\": \"main.js\" // **必须** \n },\n \"copyright\": \"Copyright © 2019-2020 ${author}\",\n \"mac\": {\n \"category\": \"public.app-category.utilities\"\n },\n \"files\": [\n \"package.json\",\n \"resources/**/*\", \t\t\t\t\t\t// **必须** \n \"static\", \n {\n \"from\": \"dist/main\" \t // **必须** \n },\t\n {\n \"from\": \"dist/renderer\" // **必须** \n }\n ],\n \"extends\": null,\n \"dmg\": {\n \"contents\": [\n {\n \"x\": <em>1</em>30,\n \"y\": 220\n },\n {\n \"x\": 410,\n \"y\": 220,\n \"type\": \"link\",\n \"path\": \"/Applications\"\n }\n ]\n },\n \"win\": {\n \"icon\": \"resources/icon.ico\",\n \"target\": [\n \"nsis\",\n \"msi\"\n ]\n },\n \"nsis\": {\n \"oneClick\": false,\n \"allowToChangeInstallationDirectory\": true,\n \"installerIcon\": \"resources/icon.ico\"\n },\n \"linux\": {\n \"target\": [\n \"deb\",\n \"rpm\",\n \"AppImage\"\n ],\n \"category\": \"Development\"\n },\n \"directories\": {\n \"buildResources\": \"resources\",\n \"output\": \"release\"\n },\n \"extraResources\": [\n {\n \"from\": \"resources/\", // **必须** \n \"to\": \"resources/\" // **必须** \n },\n {\n \"from\": \"static\",\n \"to\": \"static\"\n }\n ]\n }\n}\n```\n\n这里是个大坑,因为我们自定义了配置,覆盖了原来 `electron-webpack` 的配置,所以有几个地方是必须要这么写的,否则就会在打包之后无法显示 renderer 或者 找不到入口文件。这是我自己摸索出来的,比较 hack 的方法。因为我实在找不到答案。\n\n如果你需要使用 `__static ` 常量的话,\n\n```\n{ // 也是必须的\n \"from\": \"static\",\n \"to\": \"static\"\n}\n```\n\n最后,附上 GitHub 地址:\n\n<https://github.com/Innei/electron-typescript-starter>",
|
|
415
|
-
"match_level": "full",
|
|
416
|
-
"fully_highlighted": false,
|
|
417
|
-
"matched_words": ["1"]
|
|
418
|
-
},
|
|
419
|
-
"id": {
|
|
420
|
-
"value": "5f0dc4dbddf2006d12774b6a",
|
|
421
|
-
"match_level": "none",
|
|
422
|
-
"matched_words": []
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
],
|
|
427
|
-
"nb_hits": 118,
|
|
428
|
-
"page": 1,
|
|
429
|
-
"nb_pages": 12,
|
|
430
|
-
"hits_per_page": 10,
|
|
431
|
-
"exhaustive_nb_hits": true,
|
|
432
|
-
"exhaustive_typo": true,
|
|
433
|
-
"query": "1",
|
|
434
|
-
"params": "query=1&page=1&hitsPerPage=10",
|
|
435
|
-
"rendering_content": {},
|
|
436
|
-
"processing_time_ms": 9
|
|
437
|
-
},
|
|
438
|
-
"pagination": {
|
|
439
|
-
"current_page": 1,
|
|
440
|
-
"total": 118,
|
|
441
|
-
"has_next_page": true,
|
|
442
|
-
"has_prev_page": false,
|
|
443
|
-
"size": 10,
|
|
444
|
-
"total_page": 12
|
|
445
|
-
}
|
|
446
|
-
}
|