├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── doc.yml │ └── release-tag.yaml ├── .gitignore ├── .npmrc ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.js │ │ └── theme.scss ├── guide │ ├── api.md │ ├── component.md │ ├── di.md │ ├── index.md │ └── service.md └── index.md ├── eslint.config.js ├── package.json ├── playground ├── global.d.ts ├── index.html ├── package.json ├── src │ ├── common │ │ └── directive │ │ │ ├── focus │ │ │ └── focus.directive.ts │ │ │ └── index.ts │ ├── main.tsx │ ├── module │ │ └── basic │ │ │ ├── basic.module.tsx │ │ │ ├── basic.router.ts │ │ │ ├── hello-world │ │ │ ├── a.comp.tsx │ │ │ └── hello-world.view.tsx │ │ │ ├── hoc │ │ │ └── hoc.view.tsx │ │ │ ├── simple-component │ │ │ └── index.view.tsx │ │ │ └── user-input │ │ │ └── user-input.view.tsx │ ├── router │ │ ├── index.ts │ │ ├── router.service.ts │ │ └── routes.ts │ └── setup.ts ├── tsconfig.json └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js ├── src ├── components │ └── index.ts ├── decorators │ ├── computed.ts │ ├── hook.ts │ ├── link.ts │ ├── mut.ts │ └── util.ts ├── di │ └── index.ts ├── extends │ ├── component.ts │ └── service.ts ├── helper.ts ├── index.ts ├── simple-props │ ├── composables.ts │ ├── index.ts │ └── types.ts └── type.ts ├── tests ├── __snapshots__ │ └── component.test.tsx.snap ├── component.test.tsx ├── decorator │ ├── computed.test.tsx │ ├── hook.test.tsx │ └── mut.test.tsx └── di.test.tsx ├── tsconfig.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | end_of_line = lf 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: 发布文档到github pages 2 | on: 3 | push: 4 | paths: 5 | - '**/docs/**' 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: 获取源码 🛎️ 11 | uses: actions/checkout@v2.3.1 12 | 13 | - name: 安装依赖和构建 🔧 14 | run: | 15 | npm i pnpm -g 16 | pnpm install 17 | pnpm run docs:build 18 | 19 | - name: 部署 🚀 20 | uses: JamesIves/github-pages-deploy-action@4.1.5 21 | with: 22 | branch: gh-pages # The branch the action should deploy to. 23 | folder: docs/.vitepress/dist # The folder the action should deploy. 24 | -------------------------------------------------------------------------------- /.github/workflows/release-tag.yaml: -------------------------------------------------------------------------------- 1 | name: Create Release Tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release-tag: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@master 14 | 15 | - name: Create Release Tag 16 | id: release_tag 17 | uses: yyx990803/release-tag@master 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | tag_name: ${{ github.ref }} 22 | body: | 23 | 更新内容请查看[CHANGELOG](https://github.com/agileago/vue3-oop/blob/master/CHANGELOG.md)。 24 | Please refer to [CHANGELOG](https://github.com/agileago/vue3-oop/blob/master/CHANGELOG.md) for details. 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .idea 7 | .vscode 8 | types 9 | coverage 10 | docs/.vitepress/cache 11 | lib 12 | docs/.vitepress/dist 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | strict-peer-dependencies=false 3 | ignore-workspace-root-check=true 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .gitignore 3 | .prettierignore 4 | .idea 5 | node_modules 6 | dist 7 | pnpm-lock.yaml 8 | coverage 9 | .github 10 | .DS_Store 11 | docs 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.1.2](https://github.com/agileago/vue3-oop/compare/v1.1.1...v1.1.2) (2025-05-20) 2 | 3 | 4 | ### Features 5 | 6 | * add If component for conditional rendering ([5f8bd91](https://github.com/agileago/vue3-oop/commit/5f8bd9143aec10e7c00e568fe127fc960b73ba50)) 7 | 8 | 9 | 10 | ## [1.1.1](https://github.com/agileago/vue3-oop/compare/v1.1.0...v1.1.1) (2025-05-13) 11 | 12 | 13 | ### Performance Improvements 14 | 15 | * 提高性能 ([180a8b3](https://github.com/agileago/vue3-oop/commit/180a8b3ba8819684852ef4a21c85a138c667a2d8)) 16 | 17 | 18 | 19 | # [1.1.0](https://github.com/agileago/vue3-oop/compare/v1.0.21...v1.1.0) (2025-05-12) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * watch不生效 ([2669347](https://github.com/agileago/vue3-oop/commit/266934753d9732216cf0c0967ffaf8865eb1b2c6)) 25 | 26 | 27 | 28 | ## [1.0.21](https://github.com/agileago/vue3-oop/compare/v1.0.19...v1.0.21) (2025-04-04) 29 | 30 | 31 | 32 | ## [1.0.19](https://github.com/agileago/vue3-oop/compare/v1.0.18...v1.0.19) (2025-04-02) 33 | 34 | 35 | 36 | ## [1.0.18](https://github.com/agileago/vue3-oop/compare/v1.0.17...v1.0.18) (2025-04-02) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * 打包到dist ([df5554e](https://github.com/agileago/vue3-oop/commit/df5554e9f143280b3929a05e21a9ebf5d3e5dc39)) 42 | 43 | 44 | 45 | ## [1.0.17](https://github.com/agileago/vue3-oop/compare/v1.0.16...v1.0.17) (2025-04-02) 46 | 47 | 48 | 49 | ## [1.0.16](https://github.com/agileago/vue3-oop/compare/v1.0.15...v1.0.16) (2024-12-10) 50 | 51 | 52 | 53 | ## [1.0.15](https://github.com/agileago/vue3-oop/compare/v1.0.14...v1.0.15) (2024-12-10) 54 | 55 | 56 | 57 | ## [1.0.14](https://github.com/agileago/vue3-oop/compare/v1.0.13...v1.0.14) (2024-12-09) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * 支持 data-* ([364ac94](https://github.com/agileago/vue3-oop/commit/364ac940d99fe49905797277f385d9053e821c33)) 63 | 64 | 65 | 66 | ## [1.0.13](https://github.com/agileago/vue3-oop/compare/v1.0.12...v1.0.13) (2024-12-05) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * 更新slots类型 ([38f835d](https://github.com/agileago/vue3-oop/commit/38f835d71d68e51589d533f9bde5fae84925750a)) 72 | 73 | 74 | 75 | ## [1.0.12](https://github.com/agileago/vue3-oop/compare/v1.0.11...v1.0.12) (2024-11-25) 76 | 77 | 78 | ### Features 79 | 80 | * 导出类和样式 ([c7da5fe](https://github.com/agileago/vue3-oop/commit/c7da5fec551f4db14f9f8ac13a543e504f907cc1)) 81 | 82 | 83 | 84 | ## [1.0.11](https://github.com/agileago/vue3-oop/compare/v1.0.10...v1.0.11) (2024-11-22) 85 | 86 | 87 | ### Features 88 | 89 | * 增加simple-props组件定义 ([c45e169](https://github.com/agileago/vue3-oop/commit/c45e169b46a35eb3fc83c95fc6a9f4a1277f396f)) 90 | 91 | 92 | 93 | ## [1.0.10](https://github.com/agileago/vue3-oop/compare/v1.0.9...v1.0.10) (2024-11-13) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * provideservice correct value ([eaaf9bc](https://github.com/agileago/vue3-oop/commit/eaaf9bc3d2b18ef98b98cb60645b568363f784a4)) 99 | 100 | 101 | 102 | ## [1.0.9](https://github.com/agileago/vue3-oop/compare/v1.0.8...v1.0.9) (2024-10-17) 103 | 104 | 105 | 106 | ## [1.0.8](https://github.com/agileago/vue3-oop/compare/v1.0.7...v1.0.8) (2024-10-17) 107 | 108 | 109 | 110 | ## [1.0.7](https://github.com/agileago/vue3-oop/compare/v1.0.6...v1.0.7) (2024-10-14) 111 | 112 | 113 | ### Features 114 | 115 | * add provideService ([044aa65](https://github.com/agileago/vue3-oop/commit/044aa6554e9d4150fc3fdf4ca38035bcf3f91f11)) 116 | 117 | 118 | 119 | ## [1.0.6](https://github.com/agileago/vue3-oop/compare/v1.0.5...v1.0.6) (2023-09-01) 120 | 121 | 122 | ### Bug Fixes 123 | 124 | * 调整package.json导出配置,解决vue-tsc类型报错 ([947a107](https://github.com/agileago/vue3-oop/commit/947a10710981cf5b3b6eb79fefd88d1bdb5054cb)) 125 | 126 | 127 | 128 | ## [1.0.5](https://github.com/agileago/vue3-oop/compare/v1.0.4...v1.0.5) (2023-08-29) 129 | 130 | 131 | ### Features 132 | 133 | * injectService支持forwardRef ([3b7970a](https://github.com/agileago/vue3-oop/commit/3b7970a0e8ce7e913ecc781c9221b71a63628322)) 134 | 135 | 136 | 137 | ## [1.0.4](https://github.com/agileago/vue3-oop/compare/v1.0.2...v1.0.4) (2023-06-27) 138 | 139 | 140 | ### Bug Fixes 141 | 142 | * injectservice type ([dda63c6](https://github.com/agileago/vue3-oop/commit/dda63c6607e26f9134f910979c500f095593947f)) 143 | 144 | 145 | 146 | ## [1.0.2](https://github.com/agileago/vue3-oop/compare/v1.0.1...v1.0.2) (2022-09-26) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * remove postinstall script ([df8d817](https://github.com/agileago/vue3-oop/commit/df8d817e5d75d13e95416cfc0984811dd6475142)) 152 | 153 | 154 | 155 | ## [1.0.1](https://github.com/agileago/vue3-oop/compare/v1.0.0...v1.0.1) (2022-09-12) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * getcurrentinstance 在所有方法调用之前初始化 ([e69528e](https://github.com/agileago/vue3-oop/commit/e69528e4dbf2fd856f5d0d558d9e8102b40a5893)) 161 | 162 | 163 | 164 | # [1.0.0](https://github.com/agileago/vue3-oop/compare/v0.6.2...v1.0.0) (2022-08-17) 165 | 166 | 167 | ### Features 168 | 169 | * 删除 ProviderKey, 新增injectService ([0858f83](https://github.com/agileago/vue3-oop/commit/0858f83d0e72e1b9772ce139d2e5d7dd1d3953b2)) 170 | 171 | 172 | 173 | ## [0.6.2](https://github.com/agileago/vue3-oop/compare/v0.6.1...v0.6.2) (2022-08-10) 174 | 175 | 176 | ### Features 177 | 178 | * :fire: 增强组件类型 ([0544000](https://github.com/agileago/vue3-oop/commit/05440001a83bba43532c5e782e060a62b416c78e)) 179 | 180 | 181 | 182 | ## [0.6.1](https://github.com/agileago/vue3-oop/compare/v0.6.0...v0.6.1) (2022-07-15) 183 | 184 | 185 | ### Features 186 | 187 | * :fire: 支持devtool,增加工具函数 ([50ed2fb](https://github.com/agileago/vue3-oop/commit/50ed2fb95c1594a2d05534c1782d80a2090cbdf2)) 188 | 189 | 190 | 191 | # [0.6.0](https://github.com/agileago/vue3-oop/compare/v0.5.9...v0.6.0) (2022-06-27) 192 | 193 | 194 | ### Features 195 | 196 | * 支持多重组件继承,修改异步组件的定义 ([9139b42](https://github.com/agileago/vue3-oop/commit/9139b428bb625a9fa77d5c1ba63f72c4e8172195)) 197 | 198 | 199 | 200 | ## [0.5.9](https://github.com/agileago/vue3-oop/compare/v0.5.8...v0.5.9) (2022-06-16) 201 | 202 | 203 | ### Features 204 | 205 | * 支持异步组件,组件内增加init初始化方法 ([ce14cd3](https://github.com/agileago/vue3-oop/commit/ce14cd383f0f07cbbaace1a6e3566e2fc7cd2ac8)) 206 | 207 | 208 | 209 | ## [0.5.8](https://github.com/agileago/vue3-oop/compare/v0.5.7...v0.5.8) (2022-04-26) 210 | 211 | 212 | ### Bug Fixes 213 | 214 | * 支持组件继承 ([3d927d1](https://github.com/agileago/vue3-oop/commit/3d927d155e6529fb7885834aff1d613398dca538)) 215 | 216 | 217 | 218 | ## [0.5.7](https://github.com/agileago/vue3-oop/compare/v0.5.6...v0.5.7) (2022-04-26) 219 | 220 | 221 | ### Bug Fixes 222 | 223 | * 修复继承问题 ([75167c2](https://github.com/agileago/vue3-oop/commit/75167c27a41a0563ac421b2b36a554eee5df1650)) 224 | 225 | 226 | 227 | ## [0.5.6](https://github.com/agileago/vue3-oop/compare/v0.5.5...v0.5.6) (2022-04-18) 228 | 229 | 230 | ### Bug Fixes 231 | 232 | * 修复vuecomponent slots类型错误 ([047b4b9](https://github.com/agileago/vue3-oop/commit/047b4b95b266d45efa5bc8e916c92df6c6a6493d)) 233 | 234 | 235 | 236 | ## [0.5.5](https://github.com/agileago/vue3-oop/compare/v0.5.3...v0.5.5) (2022-04-15) 237 | 238 | 239 | ### Bug Fixes 240 | 241 | * hook装饰器未传递参数 ([44f87a0](https://github.com/agileago/vue3-oop/commit/44f87a03019bd7eee36d83c233d6467265daa90b)) 242 | 243 | 244 | 245 | ## [0.5.3](https://github.com/agileago/vue3-oop/compare/v0.5.2...v0.5.3) (2022-04-15) 246 | 247 | 248 | ### Bug Fixes 249 | 250 | * 修复联合类型错误提示 ([661acfe](https://github.com/agileago/vue3-oop/commit/661acfe1c1941f9023c9bfa8ebc03a2976d05ef4)) 251 | 252 | 253 | 254 | ## [0.5.2](https://github.com/agileago/vue3-oop/compare/v0.5.1...v0.5.2) (2022-03-23) 255 | 256 | 257 | ### Features 258 | 259 | * 增加vue实例的方法 ([aeb9ad9](https://github.com/agileago/vue3-oop/commit/aeb9ad9a82d090e2568c6db564d33c69246f7723)) 260 | 261 | 262 | 263 | ## [0.5.1](https://github.com/agileago/vue3-oop/compare/v0.3.2...v0.5.1) (2022-03-16) 264 | 265 | 266 | ### Features 267 | 268 | * 增加useForwardRef ([13cafd4](https://github.com/agileago/vue3-oop/commit/13cafd4c47dcb937919375d2631a53c840f77c9a)) 269 | 270 | 271 | 272 | ## [0.3.2](https://github.com/agileago/vue3-oop/compare/v0.3.1...v0.3.2) (2022-03-14) 273 | 274 | 275 | ### Features 276 | 277 | * 导出创建装饰器的函数 ([90f2ad1](https://github.com/agileago/vue3-oop/commit/90f2ad19f7c89f27791ecd02bfbcb1d3c5533c65)) 278 | 279 | 280 | 281 | ## [0.3.1](https://github.com/agileago/vue3-oop/compare/v0.3.0...v0.3.1) (2022-03-14) 282 | 283 | 284 | ### Features 285 | 286 | * hook支持绑定多个 ([c9699dc](https://github.com/agileago/vue3-oop/commit/c9699dc86e673e1e06625a5289e106faafc80a5d)) 287 | 288 | 289 | 290 | # [0.3.0](https://github.com/agileago/vue3-oop/compare/v0.2.8...v0.3.0) (2022-03-11) 291 | 292 | 293 | ### Bug Fixes 294 | 295 | * vuecomponent类型错误 ([c6199ed](https://github.com/agileago/vue3-oop/commit/c6199ed9145ef3062ef58d6959fce61c037e9981)) 296 | 297 | 298 | ### Features 299 | 300 | * 支持模板 ([9076d9c](https://github.com/agileago/vue3-oop/commit/9076d9ce92401bdc47e0bbbc6ed99c18e2ee0ebb)) 301 | 302 | 303 | 304 | ## [0.2.8](https://github.com/agileago/vue3-oop/compare/v0.2.7...v0.2.8) (2022-02-25) 305 | 306 | 307 | ### Features 308 | 309 | * computed增加eager,支持立即计算出值 ([508b1c0](https://github.com/agileago/vue3-oop/commit/508b1c096315cc66ab9f935a9030d346e0e14257)) 310 | 311 | 312 | 313 | ## [0.2.7](https://github.com/agileago/vue3-oop/compare/v0.2.5...v0.2.7) (2022-02-25) 314 | 315 | 316 | ### Features 317 | 318 | * 增加getCurrentInjector,在外部服务中获取注射器 ([e601189](https://github.com/agileago/vue3-oop/commit/e60118962c205c484fdfe3625892b2cff9d60eb3)) 319 | * 更新版本以及类型 ([c00d67c](https://github.com/agileago/vue3-oop/commit/c00d67c23d7c8efefefe95283fcd6afdb7c7d743)) 320 | 321 | 322 | 323 | ## [0.2.5](https://github.com/agileago/vue3-oop/compare/v0.2.3...v0.2.5) (2022-01-21) 324 | 325 | 326 | ### Features 327 | 328 | * expose $ alias getCurrentInstance ([7438234](https://github.com/agileago/vue3-oop/commit/743823473a4f7603d907a611f132014b4c0fc8ca)) 329 | * 依赖进行缓存,提高性能 ([fcf9969](https://github.com/agileago/vue3-oop/commit/fcf99693aec79cfc346eaec5466d4579d61ad030)) 330 | 331 | 332 | 333 | ## [0.2.3](https://github.com/agileago/vue3-oop/compare/v0.2.2...v0.2.3) (2022-01-16) 334 | 335 | 336 | ### Features 337 | 338 | * Mut装饰器支持shallow, customRef ([4cb5239](https://github.com/agileago/vue3-oop/commit/4cb5239c4187b975d466e1e8a95766c5f96ec7d9)) 339 | 340 | 341 | 342 | ## [0.2.2](https://github.com/agileago/vue3-oop/compare/v0.2.1...v0.2.2) (2021-12-23) 343 | 344 | 345 | ### Features 346 | 347 | * 更新组件属性类型 ([72048a2](https://github.com/agileago/vue3-oop/commit/72048a23cdb7d7ef43d8176ce64544236898d2c7)) 348 | 349 | 350 | 351 | ## [0.2.1](https://github.com/agileago/vue3-oop/compare/v0.2.0...v0.2.1) (2021-12-21) 352 | 353 | 354 | ### Features 355 | 356 | * Ref重命名Mut ([f13d1ef](https://github.com/agileago/vue3-oop/commit/f13d1efbc9ec45c765a60906c560cbcd054c87a1)) 357 | 358 | 359 | 360 | # [0.2.0](https://github.com/agileago/vue3-oop/compare/v0.1.3...v0.2.0) (2021-12-19) 361 | 362 | 363 | ### Features 364 | 365 | * Ref重命名Track ([455739a](https://github.com/agileago/vue3-oop/commit/455739a4127a53b9c7bfc74f89260ebc9fa3405c)) 366 | 367 | 368 | ### BREAKING CHANGES 369 | 370 | * Ref因与vue的Ref重名,故改名为Track 371 | 372 | 373 | 374 | ## [0.1.3](https://github.com/agileago/vue3-oop/compare/v0.1.2...v0.1.3) (2021-12-17) 375 | 376 | 377 | ### Bug Fixes 378 | 379 | * 删除测试依赖 ([c283891](https://github.com/agileago/vue3-oop/commit/c2838911f12dcf7500dfd2052352a8145bc51f91)) 380 | 381 | 382 | 383 | ## [0.1.2](https://github.com/agileago/vue3-oop/compare/v0.1.1...v0.1.2) (2021-12-17) 384 | 385 | 386 | 387 | ## [0.1.1](https://github.com/agileago/vue3-oop/compare/v0.1.0...v0.1.1) (2021-12-15) 388 | 389 | 390 | 391 | # [0.1.0](https://github.com/agileago/vue3-oop/compare/v0.0.19...v0.1.0) (2021-12-15) 392 | 393 | 394 | ### Bug Fixes 395 | 396 | * 自动解析识别抽象类 ([7c0dd98](https://github.com/agileago/vue3-oop/commit/7c0dd980b46cbbd0a4911d8440af2c4503ede6e7)) 397 | 398 | 399 | 400 | ## [0.0.19](https://github.com/agileago/vue3-oop/compare/v0.0.18...v0.0.19) (2021-12-15) 401 | 402 | 403 | ### Features 404 | 405 | * 自动解析依赖 ([9aeb9b7](https://github.com/agileago/vue3-oop/commit/9aeb9b7c9cd432b47fe158e2918ecc2ca94ef354)) 406 | 407 | 408 | 409 | ## [0.0.18](https://github.com/agileago/vue3-oop/compare/v0.0.17...v0.0.18) (2021-12-14) 410 | 411 | 412 | ### Bug Fixes 413 | 414 | * 修复providerkey类型 ([0d0ed40](https://github.com/agileago/vue3-oop/commit/0d0ed40ba02e813d6aca28f2d11cffb249241518)) 415 | 416 | 417 | 418 | ## [0.0.17](https://github.com/agileago/vue3-oop/compare/v0.0.16...v0.0.17) (2021-12-14) 419 | 420 | 421 | ### Bug Fixes 422 | 423 | * 修复npm-run-all在pnpm下无法使用 ([b390981](https://github.com/agileago/vue3-oop/commit/b390981cd22a2bb1a4d00f6c2de483531a8cbc0c)) 424 | 425 | 426 | 427 | ## [0.0.16](https://github.com/agileago/vue3-oop/compare/v0.0.15...v0.0.16) (2021-12-13) 428 | 429 | 430 | ### Features 431 | 432 | * webpack支持热更新 ([686672a](https://github.com/agileago/vue3-oop/commit/686672a3416719c6c440f64ce56182a3a91e26a3)) 433 | 434 | 435 | 436 | ## [0.0.15](https://github.com/agileago/vue3-oop/compare/v0.0.14...v0.0.15) (2021-12-12) 437 | 438 | 439 | ### Features 440 | 441 | * 初始化未在constructor声明的服务 ([83f0ef3](https://github.com/agileago/vue3-oop/commit/83f0ef3cbe1c209ab59fe7b1bc4b4e24f896ee2a)) 442 | 443 | 444 | 445 | ## [0.0.14](https://github.com/agileago/vue3-oop/compare/v0.0.13...v0.0.14) (2021-12-10) 446 | 447 | 448 | ### Bug Fixes 449 | 450 | * 修复v-model类型 ([5d69c59](https://github.com/agileago/vue3-oop/commit/5d69c592fb158d881d48b26e1f1dd3b15fbe3f14)) 451 | 452 | 453 | 454 | ## [0.0.13](https://github.com/agileago/vue3-oop/compare/v0.0.12...v0.0.13) (2021-12-09) 455 | 456 | 457 | ### Features 458 | 459 | * update component type ([9debbad](https://github.com/agileago/vue3-oop/commit/9debbad2525291badddfc97669e07af794c04128)) 460 | * update component type ([d124ec9](https://github.com/agileago/vue3-oop/commit/d124ec9c90590cc09e7b245b69895eeb6dd5d9df)) 461 | 462 | 463 | 464 | ## [0.0.12](https://github.com/agileago/vue3-oop/compare/v0.0.11...v0.0.12) (2021-12-09) 465 | 466 | 467 | ### Bug Fixes 468 | 469 | * 修复类型路径@导致vscode无法识别 ([8a88a32](https://github.com/agileago/vue3-oop/commit/8a88a3206913a0728d406c457056dad2a6ccc80d)) 470 | 471 | 472 | 473 | ## [0.0.11](https://github.com/agileago/vue3-oop/compare/v0.0.10...v0.0.11) (2021-12-09) 474 | 475 | 476 | 477 | ## [0.0.10](https://github.com/agileago/vue3-oop/compare/v0.0.9...v0.0.10) (2021-12-09) 478 | 479 | 480 | ### Features 481 | 482 | * 属性支持数组形式,并给与类型提示 ([d9b4856](https://github.com/agileago/vue3-oop/commit/d9b4856031b72c364904f48a545faf1be00d010c)) 483 | 484 | 485 | 486 | ## [0.0.9](https://github.com/agileago/vue3-oop/compare/v0.0.8...v0.0.9) (2021-12-09) 487 | 488 | 489 | 490 | ## [0.0.8](https://github.com/agileago/vue3-oop/compare/v0.0.7...v0.0.8) (2021-12-09) 491 | 492 | 493 | 494 | ## [0.0.7](https://github.com/agileago/vue3-oop/compare/v0.0.6...v0.0.7) (2021-12-05) 495 | 496 | 497 | ### Features 498 | 499 | * 增加类型帮助函数 ([07ef3a6](https://github.com/agileago/vue3-oop/commit/07ef3a606fcb91cce32655fe27f818aaa37796ef)) 500 | 501 | 502 | 503 | ## [0.0.6](https://github.com/agileago/vue3-oop/compare/v0.0.5...v0.0.6) (2021-12-04) 504 | 505 | 506 | ### Features 507 | 508 | * 增加app获取 ([22d2c31](https://github.com/agileago/vue3-oop/commit/22d2c31e5a1e3fc87c8a8d8627933dff3512e50d)) 509 | 510 | 511 | 512 | ## [0.0.3](https://github.com/agileago/vue3-oop/compare/v0.0.2...v0.0.3) (2021-11-09) 513 | 514 | 515 | ### Bug Fixes 516 | 517 | * **模块一:** 更改readme ([94cb61c](https://github.com/agileago/vue3-oop/commit/94cb61c685f1b567f407104a9c6921cbf151b897)) 518 | 519 | 520 | 521 | ## [0.0.2](https://github.com/agileago/vue3-oop/compare/553c9af87443170387ae04852cfee4891e2db2ab...v0.0.2) (2021-11-09) 522 | 523 | 524 | ### Features 525 | 526 | * 初始化 ([553c9af](https://github.com/agileago/vue3-oop/commit/553c9af87443170387ae04852cfee4891e2db2ab)) 527 | 528 | 529 | 530 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 vue3-oop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3 oop [文档](https://agileago.github.io/vue3-oop/) 2 | 3 | 类组件+自动化的依赖注入(可选) = 极致的代码体验 [DEMO](https://stackblitz.com/edit/vite-y7m4fy?file=main.tsx) 4 | 5 | 6 | ### 前提条件 7 | 8 | 需要**reflect-metadata** 的支持 9 | 10 | ```shell 11 | pnpm add @abraham/reflection injection-js 12 | ``` 13 | 14 | 项目入口需要引入 `reflect-metadata` 15 | 16 | ```typescript 17 | import '@abraham/reflection' 18 | ``` 19 | 20 | **`tsconfig.json`** 需要增加配置: 21 | 22 | ```json 23 | { 24 | "compilerOptions": { 25 | "experimentalDecorators": true, 26 | "emitDecoratorMetadata": true, 27 | "useDefineForClassFields": false 28 | } 29 | } 30 | ``` 31 | 32 | ### 安装 33 | 34 | ```shell 35 | pnpm add vue3-oop 36 | ``` 37 | 38 | ### vite配置 39 | 40 | 因为esbuild不支持装饰器的metadata属性,所以需要安装 `@vue3-oop/plugin-vue-jsx` 插件使用原始ts编译 41 | 42 | ### 定义组件 43 | 44 | ```typescript jsx 45 | import { Autobind, ComponentProps, Computed, Hook, Link, Mut, VueComponent } from 'vue3-oop' 46 | import { Directive, VNodeChild, watch } from 'vue' 47 | 48 | interface FooProps { 49 | size: 'small' | 'large' 50 | // 组件的slots 51 | slots: { 52 | item(name: string): VNodeChild 53 | } 54 | } 55 | 56 | class Foo extends VueComponent { 57 | // vue需要的运行时属性检查 58 | static defaultProps: ComponentProps = ['size'] 59 | 60 | constructor() { 61 | super() 62 | // watch在构造函数中初始化 63 | watch( 64 | () => this.count, 65 | () => { 66 | console.log(this.count) 67 | }, 68 | ) 69 | } 70 | 71 | // 组件自身状态 72 | @Mut() count = 1 73 | 74 | // 计算属性 75 | @Computed() 76 | get doubleCount() { 77 | return this.count * 2 78 | } 79 | 80 | add() { 81 | this.count++ 82 | } 83 | 84 | // 自动绑定this 85 | @Autobind() 86 | remove() { 87 | this.count-- 88 | } 89 | 90 | // 生命周期 91 | @Hook('Mounted') 92 | mount() { 93 | console.log('mounted') 94 | } 95 | 96 | // 对元素或组件的引用 97 | @Link() element?: HTMLDivElement 98 | 99 | render() { 100 | return ( 101 |
102 | {this.props.size} 103 | 104 | {this.count} 105 | 106 |
{this.context.slots.item?.('aaa')}
107 | 108 |
109 | ) 110 | } 111 | } 112 | 113 | ``` 114 | 115 | ### 定义服务 116 | 117 | 组件和服务的差距是缺少了render这一个表现UI的函数,其他都基本一样 118 | 119 | ```typescript 120 | class CountService extends VueService { 121 | @Mut() count = 1 122 | add() { 123 | this.count++ 124 | } 125 | remove() { 126 | this.count-- 127 | } 128 | } 129 | ``` 130 | 131 | 132 | ### 依赖注入 133 | 134 | Angular文档 135 | 136 | - [Angular 中的依赖注入](https://angular.cn/guide/dependency-injection) 137 | - [依赖提供者](https://angular.cn/guide/dependency-injection-providers) 138 | - [服务与依赖注入简介](https://angular.cn/guide/architecture-services) 139 | - [多级注入器](https://angular.cn/guide/hierarchical-dependency-injection) 140 | - [依赖注入实战](https://angular.cn/guide/dependency-injection-in-action) 141 | 142 | ```typescript jsx 143 | import { VueComponent, VueService } from 'vue3-oop' 144 | import { Injectable } from 'injection-js' 145 | 146 | // 组件DI 147 | @Component({ 148 | providers: [CountService] 149 | }) 150 | class Bar extends VueComponent { 151 | constructor(private countService: CountService) {super()} 152 | 153 | render() { 154 | return
{this.countService.count}
155 | } 156 | } 157 | 158 | @Injectable() 159 | class BarService extends VueService { 160 | constructor(private countService: CountService) {super()} 161 | } 162 | ``` 163 | 164 | ### 支持我 165 | 166 | #### 微信 167 | 168 | 169 | 170 | 171 | #### 支付宝 172 | 173 | 174 | 175 | ### QQ交流群 176 | 177 | 178 | 179 | ### License 180 | 181 | [MIT](https://opensource.org/licenses/MIT) -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | export default defineConfig({ 4 | lang: 'zh_CN', 5 | base: '/vue3-oop/', 6 | title: 'VUE3-OOP', 7 | description: 'vue3 oop是vue3开发进入面向对象阶段', 8 | markdown: { 9 | lineNumbers: false, 10 | }, 11 | themeConfig: { 12 | search: { 13 | provider: 'local', 14 | options: { 15 | translations: { 16 | button: { 17 | buttonText: '搜索文档', 18 | buttonAriaLabel: '搜索文档' 19 | }, 20 | modal: { 21 | noResultsText: '无法找到相关结果', 22 | resetButtonTitle: '清除查询条件', 23 | footer: { 24 | selectText: '选择', 25 | navigateText: '切换', 26 | closeText: '关闭' 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | nav: [ 33 | { text: '指南', link: '/guide/', activeMatch: '/guide/' }, 34 | { 35 | text: 'DEMO', 36 | link: 'https://stackblitz.com/edit/vite-y7m4fy?file=main.tsx', 37 | }, 38 | ], 39 | socialLinks: [ 40 | { icon: 'github', link: 'https://github.com/agileago/vue3-oop' }, 41 | ], 42 | sidebar: { 43 | '/guide/': [ 44 | { 45 | text: '介绍', 46 | collapsed: true, 47 | items: [ 48 | { text: '使用指南', link: '/guide/' }, 49 | { text: '组件', link: '/guide/component' }, 50 | { text: '服务', link: '/guide/service' }, 51 | ], 52 | }, 53 | { 54 | text: '依赖注入', 55 | items: [{ text: '服务注入', link: '/guide/di' }], 56 | }, 57 | { 58 | text: 'API', 59 | items: [{ text: 'API', link: '/guide/api' }], 60 | }, 61 | ], 62 | }, 63 | }, 64 | }) 65 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './theme.scss' 3 | 4 | export default DefaultTheme 5 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/theme.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | $color: #16a8a7; 4 | 5 | :root { 6 | --vp-c-brand-1: #{$color}; 7 | --vp-c-brand-2: #{color.adjust($color, $lightness: 5%)}; 8 | --vp-c-brand-3: #{color.adjust($color, $lightness: 10%)}; 9 | } 10 | -------------------------------------------------------------------------------- /docs/guide/api.md: -------------------------------------------------------------------------------- 1 | # 基础类 2 | 3 | ## VueComponent 4 | 5 | 可传入泛型 `VueComponent` 6 | 7 | 组件必须继承的类,并且必须实现`render`函数 8 | 9 | ## VueService 10 | 11 | 服务必须继承的类 12 | 13 | # 装饰器 14 | 15 | ## Mut 16 | - Type: 属性装饰器 17 | 18 | 声明变量为响应式 19 | 20 | ```tsx 21 | class Foo extends VueComponent { 22 | @Mut() count = 1 23 | } 24 | ``` 25 | 26 | ## Computed 27 | - Type: 存取器属性装饰器 28 | 29 | 计算属性,加了一层缓存 30 | 31 | ```tsx 32 | class Foo extends VueComponent { 33 | @Mut() count = 1 34 | 35 | @Computed() 36 | get doubleCount() { 37 | return this.count * 2 38 | } 39 | } 40 | ``` 41 | 42 | ## Hook 43 | - Type: 方法装饰器 44 | - 'BeforeMount' | 'Mounted' | 'BeforeUpdate' | 'Updated' | 'BeforeUnmount' | 'Unmounted' | 'Activated' | 'Deactivated' | 'ErrorCaptured' | 'RenderTracked' | 'RenderTriggered' | 'ServerPrefetch' 45 | 46 | 生命周期 47 | 48 | ```tsx 49 | class Foo extends VueComponent { 50 | @Hook('Mounted') 51 | mount() { 52 | console.log('mount') 53 | } 54 | } 55 | ``` 56 | 57 | ## Link 58 | - Type: 属性装饰器 59 | 60 | 链接到组件或者元素 61 | 62 | ```tsx 63 | class Foo extends VueComponent { 64 | @Link() div?: HTMLDivElement 65 | 66 | render() { 67 | return
68 | } 69 | } 70 | ``` 71 | 72 | ## Autobind 73 | - Type: 类装饰器或者方法装饰器 74 | 75 | 自动绑定方法的this 76 | 77 | ```tsx 78 | // 可以直接绑定一个类,类下面的所有方法都会自动绑定 79 | @Autobind() 80 | class Foo extends VueComponent { 81 | @Autobind() // 可单独绑定某个方法 82 | add() { 83 | 84 | } 85 | 86 | render() { 87 | return
88 | } 89 | } 90 | ``` 91 | 92 | # 帮助函数 93 | 94 | ## useProps 95 | 96 | 获取当前组件的属性 97 | 98 | ## useCtx 99 | 100 | 获取当前组件的上下文 101 | 102 | ## getCurrentApp 103 | 104 | 获取当前的应用app 105 | 106 | ## getCurrentInjector 107 | 108 | 在外部服务中获取当前的最近一级的注射器 109 | 110 | ## createCurrentInjector 111 | 112 | 在当前组件手动创建注射器 113 | 114 | ## injectService 115 | 116 | 手动注入服务,存在注射器或父级注射器 117 | 118 | ## useForwardRef 119 | 120 | 在HOC组件中使用这个方法可以转发真正的ref 121 | 122 | ## mergeRefs 123 | 124 | 支持多个不同类型的ref在同一个元素或组件上面 125 | 126 | ```typescript jsx 127 | import { mergeRefs } from './component' 128 | 129 | class App extends VueComponent { 130 | @Link() aa?: any 131 | 132 | cc = shallowRef() 133 | 134 | dd?: any 135 | 136 | render() { 137 | return
this.dd = v)}>
138 | } 139 | } 140 | ``` 141 | 142 | 143 | # 类型 144 | 145 | ## ComponentProps 146 | 147 | 定义组件的props时使用 148 | 149 | ```tsx 150 | interface FooProps { 151 | size: 'small' | 'large' 152 | } 153 | class Foo extends VueComponent { 154 | static defaultProps: ComponentProps = ['size'] 155 | 156 | render() { 157 | return
{this.props.size}
158 | } 159 | } 160 | ``` 161 | 162 | ## ComponentSlots 163 | 164 | 在render函数中定义slots的时候用到 165 | 166 | ```tsx 167 | const a: ComponentSlots = { 168 | 169 | } 170 | ``` 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /docs/guide/component.md: -------------------------------------------------------------------------------- 1 | ## 定义 2 | 3 | 组件必须继承 **`VueComponent`** 4 | 5 | ```typescript 6 | import { VueComponent } from 'vue3-oop' 7 | 8 | class Foo extends VueComponent { 9 | render() { 10 | return ( 11 |
我是组件UI
12 | ) 13 | } 14 | } 15 | ``` 16 | 17 | ## 属性 18 | 19 | 属性使用接口定义,在组件的 `props` 上面获取 20 | 21 | ```tsx 22 | import { VueComponent, ComponentProps } from 'vue3-oop' 23 | 24 | interface Foo_Props { 25 | size: 'small' | 'large' 26 | } 27 | 28 | class Foo extends VueComponent { 29 | static defaultProps: ComponentProps = ['size'] 30 | render() { 31 | return
{this.props.size}
32 | } 33 | } 34 | 35 | // 泛型组件 36 | interface Bar_Props { 37 | data: T 38 | onChange: (item: T) => void 39 | } 40 | class Bar extends VueComponent> { 41 | static defaultProps: ComponentProps = ['data', 'onChange'] 42 | render() { 43 | return
this.props.onChange?.(this.props.data)} 45 | >{this.props.data}
46 | } 47 | } 48 | 49 | ``` 50 | 51 | ::: warning 注意! 52 | 定义属性之后一定要在类的静态属性 `defaultProps` 定义 vue 需要的属性定义 53 | ::: 54 | 55 | ## 上下文 56 | 57 | 组件的 `context` 属性上面存储这个组件的 `emit`, `slots`, `attrs`, `expose`, 58 | 就是setup函数的第二个参数 59 | 60 | ```tsx 61 | import { VueComponent } from './component' 62 | 63 | class Foo extends VueComponent { 64 | static inheritAttrs = false 65 | 66 | render() { 67 | return
foo
68 | } 69 | } 70 | ``` 71 | 72 | ## 响应式变量 73 | 74 | 响应式变量使用装饰器注解一下,主要有2个 `Mut` 和 `Computed`,此时忘记 `.value` 的事情, 75 | 就是正常普通的变量定义,加上装饰器就是告诉框架当此变量变化的时候我要刷新视图 76 | 77 | ```tsx 78 | class Foo extends VueComponent { 79 | @Mut() count = 1 80 | 81 | @Computed() 82 | get doubleCount() { 83 | return this.count * 2 84 | } 85 | 86 | render() { 87 | return ( 88 |
this.count++}> 89 | {this.count} 90 |
91 | ) 92 | } 93 | } 94 | ``` 95 | 96 | ## 生命周期 97 | 98 | 生命周期使用 `Hook` 装饰器 99 | 100 | ```tsx 101 | class Foo extends VueComponent { 102 | @Hook('Mounted') 103 | mounted() { 104 | console.log('foo mounted') 105 | } 106 | 107 | render() { 108 | return foo 109 | } 110 | } 111 | ``` 112 | 113 | ## watch 114 | 115 | watch在构造函数中使用, 构造函数其实就认为是 setup,你可以做任何在setup中使用的方法 116 | 117 | ```tsx 118 | import { watch } from 'vue' 119 | 120 | class Foo extends VueComponent { 121 | constructor() { 122 | super() 123 | watch(() => this.count, (n, o) => console.log('change', n, o)) 124 | } 125 | 126 | @Mut() count = 1 127 | 128 | render() { 129 | return this.count++}>{this.count} 130 | } 131 | } 132 | ``` 133 | 134 | ## 插槽 135 | 136 | slots本质上其实是属性的一部分,为了模板的需要单独给拿出来,他其实就是类似于 `react` 中的 `renderProps`, 137 | 所以我们可以在属性定义的时候定义一下, 138 | 139 | ```tsx 140 | import { VNodeChild } from 'vue' 141 | import { VueComponent } from 'vue3-oop' 142 | 143 | interface Foo_Props { 144 | slots: { 145 | item(name: string): VNodeChild 146 | } 147 | } 148 | 149 | // 此时如果只有slots的话就可以不用定义 defaultProps 150 | class Foo extends VueComponent { 151 | render() { 152 | return ( 153 |
154 | {this.context.slots.item?.('aaaa')} 155 |
156 | ) 157 | } 158 | } 159 | ``` 160 | 161 | ## 异步组件 162 | 163 | 异步组件需配合`vue`提供的`Suspense`组件使用,需要标记组件的`async: true`, 并且组件内定义`init` 方法并且返回`promise`结果 164 | 165 | ```tsx 166 | class Foo extends VueComponent { 167 | static async = true 168 | 169 | async init() { 170 | await new Promise(r => setTimeout(r, 5000)) 171 | } 172 | 173 | render() { 174 | return ( 175 |
176 | {this.context.slots.item?.('aaaa')} 177 |
178 | ) 179 | } 180 | } 181 | ``` -------------------------------------------------------------------------------- /docs/guide/di.md: -------------------------------------------------------------------------------- 1 | ### 依赖注入 2 | 3 | 依赖注入是一种在后端非常常见的设计模式,具体的依赖注入学习请看 4 | 5 | - https://zhuanlan.zhihu.com/p/311184005 6 | - https://zhuanlan.zhihu.com/p/113299696 7 | 8 | 9 | 本库的依赖注入使用了 `Angular` 早期的基于动态的依赖注入 `injection-js` https://github.com/mgechev/injection-js, 10 | 结合vue3进行了集成和优化,使用请看下面例子 11 | 12 | ```tsx 13 | import { VueService, VueComponent } from 'vue3-oop' 14 | import { Injectable } from 'injection-js' 15 | 16 | // 定义服务 加上此装饰器表明我有需要其他服务,如果不需要,可以不加 17 | @Injectable() 18 | class CountService extends VueService { 19 | @Mut() count = 1 20 | 21 | add() { 22 | this.count++ 23 | } 24 | } 25 | 26 | // 加上此装饰器表明我有服务需要注入 27 | @Component() 28 | class Home extends VueComponent { 29 | constructor(private countService: CountService) {super()} 30 | 31 | render() { 32 | return ( 33 |
this.countService.add()}> 34 | {this.countService.count} 35 |
36 | ); 37 | } 38 | } 39 | ``` 40 | 41 | 在 `vue3-oop` 里面,依赖注入是分层的, 每一个应用 `@Component` 装饰器的组件都会生成注射器, 42 | 注射器在本级寻找不到服务会自动向上级寻找,不过需要加上 `@SkipSelf` 标识 43 | 44 | ```tsx 45 | import { VueComponent } from './component' 46 | import { SkipSelf } from 'injection-js' 47 | 48 | class CountService extends VueService { 49 | @Mut() count = 1 50 | 51 | add() { 52 | this.count++ 53 | } 54 | } 55 | 56 | // 加上此装饰器表明我有服务需要注入 57 | @Component({ 58 | // 如果某些服务只是在父级需要,而父级组件不需要可以直接写在这里 59 | providers: [CountService] 60 | }) 61 | class Home extends VueComponent { 62 | render() { 63 | return ( 64 |
65 | 66 |
67 | ); 68 | } 69 | } 70 | 71 | @Component() 72 | class HomeChild extends VueComponent { 73 | constructor( 74 | private countService: CountService, // 自身注入 75 | @SkipSelf() private c2: CountService // 从父级注入 76 | ) {super()} 77 | 78 | render() { 79 | return ( 80 |
81 | 82 |
83 | ); 84 | } 85 | } 86 | ``` 87 | 88 | 89 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # 如何使用 2 | 3 | ## 前提条件 4 | 5 | 1. 因为要用到ts的获取元数据的能力,所以需要安装`reflect-metada`的支持 6 | 7 | ```shell 8 | yarn add @abraham/reflection 9 | ``` 10 | 并且把这段代码放到入口引入,只需引入一次 11 | 12 | ```typescript 13 | import '@abraham/reflection' 14 | ``` 15 | 16 | 2. 安装依赖注入库 `injection-js`, 以及本库 17 | 18 | ```shell 19 | yarn add injection-js vue3-oop 20 | ``` 21 | 22 | 3. 设置 `tsconfig.json` 23 | 24 | 主要是涉及到装饰器以及类的设置 25 | 26 | ```json 27 | { 28 | "compilerOptions": { 29 | "experimentalDecorators": true, 30 | "emitDecoratorMetadata": true, 31 | "useDefineForClassFields": false, 32 | } 33 | } 34 | ``` 35 | 36 | 4. Vite设置 37 | 38 | 由于vite内部使用esbuild编译ts, esbuild不支持元数据 `metadata`, 所以需要使用tsc编译ts 39 | 40 | ```shell 41 | pnpm add @vue3-oop/plugin-vue-jsx -D 42 | ``` 43 | 44 | vite 插件配置 45 | ```typescript 46 | import vueJsx from '@vue3-oop/plugin-vue-jsx' 47 | export default { 48 | plugins: [vueJsx()] 49 | } 50 | ``` 51 | ## 装饰器 52 | 53 | 有关装饰器的知识请阅读 https://www.typescriptlang.org/docs/handbook/decorators.html#decorators 54 | 55 | ## 模板 56 | 57 | vite: [https://github.com/agileago/fe-template](https://github.com/agileago/fe-template) 58 | 59 | webpack: [https://github.com/agileago/fe-vue3-template](https://github.com/agileago/fe-vue3-template) 60 | 61 | -------------------------------------------------------------------------------- /docs/guide/service.md: -------------------------------------------------------------------------------- 1 | ## 定义 2 | 3 | 服务等价于vue3中`composition-api`,cpa本质上是用闭包把函数和变量包在一起,形成一个隔离区域,然后 4 | 再导出来给外界使用,但这样对类型不太友好,所以js提出了新的`class`写法去替代这种方式 5 | 6 | ```typescript 7 | import { onBeforeUnmount, ref } from 'vue' 8 | 9 | function usePosition() { 10 | const x = ref(0) 11 | const y = ref(0) 12 | 13 | function change(e: MouseEvent) { 14 | x.value = e.clientX 15 | y.value = e.clientY 16 | } 17 | 18 | document.addEventListener('mousemove', change) 19 | onBeforeUnmount(() => document.removeEventListener('mousemove', change)) 20 | 21 | return { 22 | x, 23 | y 24 | } 25 | } 26 | ``` 27 | 28 | 而使用服务同样能达到同样的效果 29 | 30 | ```typescript 31 | import { VueService, Autobind, VueComponent } from 'vue3-oop' 32 | import { onBeforeUnmount } from 'vue' 33 | 34 | class PositionService extends VueService { 35 | constructor() { 36 | super() 37 | window.addEventListener('mousemove', this.change) 38 | onBeforeUnmount(() => window.removeEventListener('mousemove', this.change)) 39 | } 40 | 41 | @Mut() x = 0 42 | @Mut() y = 0 43 | 44 | @Autobind() 45 | private change(e: MouseEvent) { 46 | this.x = e.clientX 47 | this.y = e.clientY 48 | } 49 | } 50 | 51 | class Foo extends VueComponent { 52 | postionService = new PositionService() 53 | 54 | render() { 55 | return
{ this.postionService.x }
56 | } 57 | } 58 | ``` 59 | 60 | 61 | 而双方的区别只是初始化的不一样, 但类天生自带类型,所以对类型非常友好,并且把 `.value`的问题解决掉, 62 | 服务与组件的区别: 63 | 64 | - 继承的类不一样 65 | - 少了几个组件特有的属性比如 `props`, `context`, 以及 `render` 函数, 其他都一样, 66 | 67 | 68 | 但假如你想在服务中获取属性和上下文可以使用 `useProps` 和 `useCtx`, 以及获取当前应用的实例 `getCurrentApp` 69 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: VUE3-OOP 6 | text: VUE3面向对象编程 7 | tagline: 极致优秀的编码体验 8 | actions: 9 | - theme: brand 10 | text: 开始使用 11 | link: /guide/ 12 | 13 | features: 14 | - title: 类组件 15 | details: 功能与类型融为一体,无需多次声明类型,独立的属性类型声明,各种HOC组合轻而易举 16 | - title: 自动的依赖注入 17 | details: 基于动态解析的 injection-js 依赖注入,让使用服务丝般柔滑 18 | - title: vue3无ref编程 19 | details: 无需关注ref及其value,正常声明变量,编程体验更自然 20 | --- 21 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@configurajs/eslint' 2 | 3 | export default defineConfig({ 4 | ignores: ['docs'], 5 | rules: { 6 | curly: 'off', 7 | eqeqeq: 'warn', 8 | 'vue/multi-word-component-names': 'off', 9 | 'vue/no-deprecated-v-on-native-modifier': 'off', 10 | 'vue/no-mutating-props': 'off', 11 | 'no-unused-vars': 'off', 12 | 'no-loss-of-precision': 'off', 13 | 'no-undef': 'off', 14 | 'no-empty': 'off', 15 | 'no-func-assign': 'off', 16 | 'no-prototype-builtins': 'off', 17 | 'no-cond-assign': 'off', 18 | 'no-debugger': 'off', 19 | 'eslint-comments/no-unlimited-disable': 'off', 20 | '@typescript-eslint/ban-ts-comment': 'off', 21 | '@typescript-eslint/no-unused-vars': 'warn', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-empty-function': 'warn', 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | '@typescript-eslint/no-floating-promises': 'off', 26 | 'require-await': 'warn', 27 | '@typescript-eslint/no-useless-constructor': 'warn', 28 | '@typescript-eslint/no-unsafe-function-type': 'warn', 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-oop", 3 | "version": "1.1.2", 4 | "packageManager": "pnpm@9.1.1", 5 | "engines": { 6 | "pnpm": ">=9.0" 7 | }, 8 | "description": "vue3-oop take class component and di into vue", 9 | "author": { 10 | "name": "agileago", 11 | "email": "593728759@qq.com" 12 | }, 13 | "keywords": [ 14 | "vue-oop", 15 | "oop", 16 | "vue", 17 | "di", 18 | "vue-di", 19 | "ioc", 20 | "vue class component" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/agileago/vue3-oop.git" 25 | }, 26 | "homepage": "https://agileago.github.io/vue3-oop#readme", 27 | "bugs": { 28 | "url": "https://github.com/agileago/vue3-oop/issues" 29 | }, 30 | "license": "MIT", 31 | "type": "module", 32 | "exports": { 33 | ".": { 34 | "types": "./dist/index.d.ts", 35 | "import": "./dist/index.js", 36 | "require": "./dist/index.cjs" 37 | } 38 | }, 39 | "main": "dist/index.cjs", 40 | "module": "dist/index.js", 41 | "types": "dist/index.d.ts", 42 | "files": [ 43 | "dist" 44 | ], 45 | "publishConfig": { 46 | "registry": "https://registry.npmjs.org", 47 | "access": "public" 48 | }, 49 | "scripts": { 50 | "build": "tsup src/index.ts --format esm,cjs --out-dir=dist --dts --clean", 51 | "dev": "tsup src/index.ts --format esm --out-dir=dist --watch --dts", 52 | "typecheck": "tsc --noEmit", 53 | "lint": "eslint --fix .", 54 | "format": "prettier --write .", 55 | "release": "vr release", 56 | "prepublishOnly": "pnpm build", 57 | "docs:dev": "vitepress dev docs", 58 | "docs:build": "vitepress build docs", 59 | "docs:preview": "vitepress preview docs", 60 | "test:dev": "vitest --coverage", 61 | "test": "vitest run --coverage", 62 | "prepare": "simple-git-hooks", 63 | "demo:dev": "pnpm -C playground dev" 64 | }, 65 | "commitlint": { 66 | "extends": [ 67 | "@commitlint/config-conventional" 68 | ] 69 | }, 70 | "simple-git-hooks": { 71 | "pre-commit": "enpm exec lint-staged --allow-empty --concurrent false", 72 | "commit-msg": "pnpm exec commitlint -e $1" 73 | }, 74 | "lint-staged": { 75 | "*.{ts,tsx}": "eslint --fix", 76 | "*.{vue,ts,tsx,js,jsx,less,css}": "prettier --write" 77 | }, 78 | "devDependencies": { 79 | "@abraham/reflection": "^0.12.0", 80 | "@commitlint/cli": "^19.8.0", 81 | "@commitlint/config-conventional": "^19.8.0", 82 | "@configurajs/eslint": "^0.1.2", 83 | "@configurajs/prettier": "^0.1.4", 84 | "@varlet/release": "^0.3.3", 85 | "autobind-decorator": "^2.4.0", 86 | "eslint": "^9.23.0", 87 | "injection-js": "^2.4.0", 88 | "lint-staged": "^14.0.1", 89 | "prettier": "^3.5.3", 90 | "sass-embedded": "^1.86.1", 91 | "simple-git-hooks": "^2.9.0", 92 | "tsup": "^8.4.0", 93 | "typescript": "^5.8.2", 94 | "vitepress": "^1.4.1", 95 | "vue": "^3.5.13", 96 | "vitest": "^3.1.1", 97 | "jsdom": "^25.0.1", 98 | "@vue3-oop/plugin-vue-jsx": "^1.4.6", 99 | "@vue/test-utils": "^2.4.6", 100 | "@vitest/coverage-istanbul": "^3.1.1" 101 | }, 102 | "peerDependencies": { 103 | "injection-js": "*", 104 | "vue": "3" 105 | } 106 | } -------------------------------------------------------------------------------- /playground/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue3 oop demo 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite" 8 | }, 9 | "dependencies": { 10 | "@abraham/reflection": "^0.12.0", 11 | "injection-js": "^2.4.0", 12 | "vue": "^3.5.13", 13 | "vue-router": "^4.5.0", 14 | "vue3-oop": "workspace:*", 15 | "ant-design-vue": "^4.2.6", 16 | "tslib": "^2.8.1" 17 | }, 18 | "devDependencies": { 19 | "vite": "^6.2.4", 20 | "typescript": "^5.8.2", 21 | "vite-plugin-tsconfig-paths": "^1.4.1", 22 | "@vue3-oop/plugin-vue-jsx": "^1.4.6", 23 | "@vue/runtime-dom": "^3.5.13" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/src/common/directive/focus/focus.directive.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from 'vue' 2 | 3 | const focusDirective: Directive & { name: string } = { 4 | name: 'focus', 5 | mounted(el, binding) { 6 | el.focus() 7 | console.log(binding.instance) 8 | }, 9 | } 10 | 11 | export default focusDirective 12 | -------------------------------------------------------------------------------- /playground/src/common/directive/index.ts: -------------------------------------------------------------------------------- 1 | // 由于tsx的缘故,指令不能挂载在组件上面 https://github.com/vuejs/babel-plugin-jsx/issues/541 2 | // 所以所有指令需挂载在app上 3 | 4 | import type { App, Directive } from 'vue' 5 | 6 | const dirs = import.meta.glob('./**/*.directive.ts', { eager: true }) 7 | 8 | export function setupDirective(app: App) { 9 | Reflect.ownKeys(dirs).forEach(k => { 10 | const module: any = dirs[k as string] 11 | if (!module?.default) return 12 | const dir = module.default as Directive & { name: string } 13 | app.directive(dir.name, dir) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /playground/src/main.tsx: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | import 'ant-design-vue/dist/reset.css' 3 | import { Component, Hook, Link, mergeRefs, Mut, VueComponent } from 'vue3-oop' 4 | import { createApp, shallowRef } from 'vue' 5 | import { ConfigProvider, Layout, Menu } from 'ant-design-vue' 6 | import { RouterLink, RouterView } from 'vue-router' 7 | import { RouterStartService } from './router' 8 | import { routes } from './router/routes' 9 | import zhCN from 'ant-design-vue/lib/locale/zh_CN' 10 | import { setup } from './setup' 11 | 12 | @Component({ 13 | providers: [RouterStartService], 14 | }) 15 | class App extends VueComponent { 16 | @Mut() collapsed = false 17 | 18 | cc = shallowRef() 19 | 20 | @Hook('ErrorCaptured') 21 | error(...args: any[]) { 22 | console.log(args) 23 | } 24 | 25 | @Link() aaa?: any 26 | 27 | @Hook('Mounted') 28 | mounted() { 29 | console.log(this.aaa, this.cc) 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 | 36 | 37 |

{ 40 | throw new Error('hahaha') 41 | }} 42 | style={{ color: '#fff', textAlign: 'center', lineHeight: '40px' }} 43 | > 44 | VUE 示例 45 |

46 | 47 | {routes.map(r => { 48 | return ( 49 | 50 | {'children' in r && 51 | r.children?.map(i => { 52 | return ( 53 | 54 | 55 | {i.meta?.title} 56 | 57 | 58 | ) 59 | })} 60 | 61 | ) 62 | })} 63 | 64 |
65 | 66 | 67 | 68 |
69 |
70 | ) 71 | } 72 | } 73 | 74 | const app = createApp(App) 75 | setup(app) 76 | app.mount('#app') 77 | -------------------------------------------------------------------------------- /playground/src/module/basic/basic.module.tsx: -------------------------------------------------------------------------------- 1 | import { Spin } from 'ant-design-vue' 2 | import { Suspense } from 'vue' 3 | import { RouterView } from 'vue-router' 4 | import { Hook, VueComponent } from 'vue3-oop' 5 | 6 | export default class BasicModule extends VueComponent { 7 | @Hook('Mounted') 8 | mounted() { 9 | console.log(this.$parent) 10 | } 11 | render() { 12 | return ( 13 | 14 | {({ Component }: { Component: any }) => { 15 | return ( 16 |
17 |

Suspense容器

18 | [Component], 21 | fallback: () => loading...., 22 | }} 23 | > 24 |
25 | ) 26 | }} 27 |
28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /playground/src/module/basic/basic.router.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw = { 4 | path: '/basic', 5 | component: () => import('./basic.module'), 6 | meta: { 7 | title: '特色功能', 8 | }, 9 | children: [ 10 | { 11 | path: '/basic/simple-component', 12 | component: () => import('./simple-component/index.view'), 13 | meta: { 14 | title: '简单组件', 15 | }, 16 | }, 17 | { 18 | path: '/basic/class-component', 19 | component: () => import('./user-input/user-input.view'), 20 | meta: { 21 | title: '类组件', 22 | }, 23 | }, 24 | { 25 | path: '/basic/hoc', 26 | component: () => import('./hoc/hoc.view'), 27 | meta: { 28 | title: '高阶组件', 29 | }, 30 | }, 31 | { 32 | path: '/basic/service', 33 | component: () => import('./hoc/hoc.view'), 34 | meta: { 35 | title: '简单服务', 36 | }, 37 | }, 38 | { 39 | path: '/basic/inject', 40 | component: () => import('./hoc/hoc.view'), 41 | meta: { 42 | title: '复杂服务注入', 43 | }, 44 | }, 45 | ], 46 | } 47 | 48 | export default routes 49 | -------------------------------------------------------------------------------- /playground/src/module/basic/hello-world/a.comp.tsx: -------------------------------------------------------------------------------- 1 | import { Autobind, Component, type ComponentProps, Computed, Hook, Mut, VueComponent, VueService } from 'vue3-oop' 2 | import type { VNodeChild } from 'vue' 3 | import { Injectable } from 'injection-js' 4 | 5 | // 服务,可复用的服务 6 | @Autobind() 7 | @Injectable() 8 | class CountService extends VueService { 9 | @Mut() count = 1 10 | 11 | @Computed() // 计算属性 12 | get double() { 13 | return this.count * 2 14 | } 15 | 16 | add() { 17 | this.count++ 18 | } 19 | 20 | remove() { 21 | this.count-- 22 | } 23 | 24 | // 生命周期钩子 25 | @Hook('Mounted') 26 | mount() { 27 | console.log('mounted') 28 | } 29 | } 30 | 31 | // 组件属性类型定义 32 | interface CountProps { 33 | size: 'large' | 'small' 34 | age?: number 35 | // 插槽类似react render props 36 | slots: { 37 | item(name: string): VNodeChild 38 | } 39 | } 40 | // 组件 组件和服务差异仅仅只是组件有render 41 | @Component() 42 | class Count extends VueComponent { 43 | // vue需要的 runtime props 44 | static defaultProps: ComponentProps = ['size', 'age'] 45 | 46 | // 注入服务 47 | constructor(private cs: CountService) { 48 | super() 49 | } 50 | 51 | render() { 52 | const { cs, props, context } = this 53 | return ( 54 |
55 | 56 |

{cs.count}

57 | 58 |

{props.size}

59 | {context.slots.item?.('111')} 60 |
61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /playground/src/module/basic/hello-world/hello-world.view.tsx: -------------------------------------------------------------------------------- 1 | import { Hook, injectService, Link, Mut, VueComponent } from 'vue3-oop' 2 | import { Button, Card, Input } from 'ant-design-vue' 3 | import { RouterService } from '@/router/router.service' 4 | 5 | export class Base extends VueComponent { 6 | @Mut() count = 1 7 | render() { 8 | return
base{this.count}
9 | } 10 | } 11 | 12 | function Foo() { 13 | return
aaaaaaa
14 | } 15 | 16 | declare module '@vue/runtime-dom' { 17 | interface HTMLAttributes { 18 | onClickOnce?: () => void 19 | 'v-focus'?: any 20 | } 21 | } 22 | 23 | export class Child1 extends Base { 24 | render() { 25 | return
this.count++}>{super.render()}
26 | } 27 | } 28 | 29 | export class Child2 extends Child1 { 30 | render() { 31 | return ( 32 | <> 33 |

this is child2

34 | {super.render()} 35 | 36 | ) 37 | } 38 | } 39 | 40 | export default class HelloWorldView extends VueComponent { 41 | @Mut() count = 1 42 | 43 | async init() { 44 | await new Promise(r => setTimeout(r, 5000)) 45 | } 46 | 47 | router = injectService(RouterService)! 48 | 49 | @Link() abc?: any 50 | 51 | @Hook('Mounted') 52 | mounted() { 53 | console.log(this.abc.provides) 54 | } 55 | 56 | render() { 57 | console.log(this.router) 58 | return ( 59 | 60 | 61 | 64 | 65 | 68 | 69 | 70 | 71 | 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /playground/src/module/basic/hoc/hoc.view.tsx: -------------------------------------------------------------------------------- 1 | import { type ComponentProps, Hook, Link, Mut, useForwardRef, VueComponent } from 'vue3-oop' 2 | import { nextTick } from 'vue' 3 | import { Card } from 'ant-design-vue' 4 | 5 | interface OriginProps { 6 | size?: 'small' | 'large' 7 | data?: number[] 8 | } 9 | 10 | class Origin extends VueComponent { 11 | static defaultProps: ComponentProps = ['size', 'data'] 12 | 13 | @Mut() count = 1 14 | 15 | render() { 16 | console.log(this) 17 | const { props } = this 18 | return ( 19 |
20 |

this.count++}>{this.count}

21 | {props.data?.map(k =>
  • {k}
  • ) ||
    nodata
    } 22 |
    23 | ) 24 | } 25 | } 26 | 27 | class OriginWithData extends Origin { 28 | @Mut() data?: number[] 29 | render(...args: any[]) { 30 | if (!this.data) return
    loading...
    31 | 32 | return super.render() 33 | } 34 | } 35 | 36 | // 自带请求数据的组件 37 | function WithDataOrigin( 38 | Comp: T, 39 | request: (...args: any[]) => Promise, 40 | ): T { 41 | class CompWithData extends VueComponent { 42 | // 处理属性 43 | static inheritAttrs = false 44 | // @ts-ignore 45 | static displayName = Comp.displayName || Comp.name 46 | // 处理 ref 47 | forwardRef = useForwardRef() 48 | 49 | @Mut() data?: number[] 50 | 51 | @Hook('Mounted') 52 | async mounted() { 53 | this.data = await request() 54 | // 处理父组件的ref指向 55 | nextTick(() => this.$parent?.$forceUpdate()) 56 | } 57 | 58 | render() { 59 | if (!this.data) return
    loading....
    60 | return 61 | } 62 | } 63 | 64 | return CompWithData as unknown as T 65 | } 66 | 67 | // 自带请求数据的组件 68 | function WithDataOriginExtends( 69 | Comp: T, 70 | request: (...args: any[]) => Promise, 71 | ): T { 72 | class CompWithData extends Comp { 73 | @Mut() data?: number[] 74 | 75 | @Hook('Mounted') 76 | async mounted() { 77 | this.data = await request() 78 | } 79 | 80 | render() { 81 | if (!this.data) return
    loading....
    82 | return super.render() 83 | } 84 | } 85 | 86 | return CompWithData 87 | } 88 | 89 | const OriginData = WithDataOriginExtends( 90 | Origin, 91 | () => 92 | new Promise(resolve => { 93 | setTimeout(() => resolve([1, 2, 3]), 3000) 94 | }), 95 | ) 96 | 97 | export default class HocView extends VueComponent { 98 | constructor() { 99 | super() 100 | // const num = setInterval(() => { 101 | // console.log(this.origin) 102 | // if (this.origin instanceof Origin) clearInterval(num) 103 | // }, 1000) 104 | } 105 | 106 | @Link() origin?: Origin 107 | 108 | render() { 109 | return ( 110 | 111 | 112 | 113 | ) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /playground/src/module/basic/simple-component/index.view.tsx: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import { defineComponent, useClassAndStyle, type ClassAndStyleProps } from 'vue3-oop' 3 | 4 | // region 函数组件 5 | export interface SimpleFuncComponentProps extends ClassAndStyleProps { 6 | count?: number 7 | } 8 | export function SimpleFuncComponent(props: SimpleFuncComponentProps) { 9 | return
    函数组件:{props.count}
    10 | } 11 | // endregion 12 | 13 | // region 状态带属性组件 14 | export interface SimpleStateComponentProps { 15 | initialValue?: number 16 | } 17 | 18 | export const SimpleStateComponent = defineComponent(function SimpleStateComponent(props: SimpleStateComponentProps) { 19 | const classAndStyle = useClassAndStyle() 20 | const count = ref(props.initialValue || 0) 21 | return () => ( 22 |
    23 | 24 |
    25 | ) 26 | }) 27 | 28 | export const SimpleStateWithDefaultValueComponent = defineComponent( 29 | function SimpleStateWithDefaultValueComponent(props: SimpleStateComponentProps, { attrs }) { 30 | const classAndStyle = useClassAndStyle() 31 | watch( 32 | () => props.initialValue, 33 | (n, o) => console.log(555555, n, o), 34 | ) 35 | const count = ref(props.initialValue || 0) 36 | return () => { 37 | console.log(2222, props.initialValue, attrs) 38 | console.log(3333, { ...props }) 39 | return ( 40 |
    41 |

    带默认属性参数的组件

    42 | 43 |
    44 | ) 45 | } 46 | }, 47 | { 48 | // props: { 49 | // initialValue: { 50 | // type: Number, 51 | // default: 20, 52 | // }, 53 | // }, 54 | }, 55 | ) 56 | 57 | // endregion 58 | 59 | // 简单状态组件定义 60 | const SimpleComponent = defineComponent(() => { 61 | const init = ref(10) 62 | 63 | return () => ( 64 |
    65 |

    简单组件定义

    66 |

    函数组件

    67 | 68 | 69 | 70 | 76 |
    77 | ) 78 | }) 79 | 80 | export default SimpleComponent 81 | -------------------------------------------------------------------------------- /playground/src/module/basic/user-input/user-input.view.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Form, Input, Modal } from 'ant-design-vue' 2 | import { Autobind, Mut, VueComponent } from 'vue3-oop' 3 | 4 | class FormModel { 5 | name = '' 6 | age?: number 7 | } 8 | 9 | @Autobind() 10 | export default class UserInputView extends VueComponent { 11 | @Mut() showModal = false 12 | @Mut() model = new FormModel() 13 | 14 | add() { 15 | this.showModal = true 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 |
    22 | 25 |
    26 | 27 |
    28 | 29 | 30 | 31 | 32 | 33 | 34 |
    35 |
    36 |
    37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /playground/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from 'injection-js' 2 | import { RouterService } from './router.service' 3 | import { routes } from './routes' 4 | 5 | @Injectable() 6 | export class RouterStartService { 7 | constructor(private rs: RouterService) { 8 | rs.init(routes) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /playground/src/router/router.service.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentApp, Hook, VueService } from 'vue3-oop' 2 | import type { Router, RouteRecordRaw } from 'vue-router' 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | 5 | export class RouterService extends VueService { 6 | history = createWebHistory() 7 | router!: Router 8 | app = getCurrentApp() 9 | init(routes: RouteRecordRaw[]) { 10 | this.router = createRouter({ 11 | history: this.history, 12 | routes, 13 | }) 14 | this.app?.use(this.router) 15 | } 16 | @Hook('BeforeUnmount') 17 | beforeUnmount() { 18 | this.history.destroy() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /playground/src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | // 自动收集子模块的路由 4 | const moduleRoutes = import.meta.glob('../module/**/*.router.ts', { 5 | eager: true, 6 | }) 7 | 8 | export const routes: RouteRecordRaw[] = Reflect.ownKeys(moduleRoutes) 9 | .map(k => (moduleRoutes[k as string] as any).default as RouteRecordRaw) 10 | .filter(Boolean) 11 | -------------------------------------------------------------------------------- /playground/src/setup.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { setupDirective } from './common/directive' 3 | 4 | export function setup(app: App) { 5 | setupDirective(app) 6 | } 7 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "importHelpers": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vueJsx from '@vue3-oop/plugin-vue-jsx' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | plugins: [vueJsx()], 6 | }) 7 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'playground' 3 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@configurajs/prettier' 2 | 3 | export default defineConfig({ 4 | arrowParens: 'avoid', 5 | }) 6 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import type { SetupContext } from 'vue' 2 | import type { JSX } from 'vue/jsx-runtime' 3 | 4 | interface IfProps { 5 | /*判断条件*/ 6 | condition: any 7 | } 8 | export function If(props: IfProps, ctx: SetupContext) { 9 | if (!props.condition) return null 10 | return ctx.slots.default?.() as unknown as JSX.Element 11 | } 12 | -------------------------------------------------------------------------------- /src/decorators/computed.ts: -------------------------------------------------------------------------------- 1 | import type { WatchOptionsBase } from 'vue' 2 | import { computed, shallowRef, watchEffect } from 'vue' 3 | import type { Hanlder } from '../type' 4 | import { createDecorator, getProtoMetadata } from './util' 5 | 6 | export const Computed: ComputedDecorator = createDecorator('Computed') 7 | 8 | type EagerType = true | WatchOptionsBase['flush'] 9 | 10 | export interface ComputedDecorator { 11 | /** 12 | * @param eager 是否是急切的获取值 13 | */ 14 | (eager?: EagerType): MethodDecorator 15 | MetadataKey: string | symbol 16 | } 17 | 18 | function handler(targetThis: Record) { 19 | const list = getProtoMetadata(targetThis, Computed.MetadataKey, true) 20 | if (!list || !list.length) return 21 | for (const item of list) { 22 | const desc = item.desc 23 | const option = item.options 24 | if (!desc) continue 25 | let keyVal: any 26 | if (option) { 27 | // eager computed 28 | keyVal = shallowRef() 29 | watchEffect( 30 | () => { 31 | try { 32 | keyVal.value = desc.get?.call(targetThis) 33 | } finally { 34 | } 35 | }, 36 | { 37 | flush: option === true ? 'sync' : option, 38 | }, 39 | ) 40 | } else { 41 | keyVal = computed({ 42 | get: () => desc.get?.call(targetThis), 43 | set: (v: any) => desc.set?.call(targetThis, v), 44 | }) 45 | } 46 | Object.defineProperty(targetThis, item.key, { 47 | enumerable: desc?.enumerable, 48 | configurable: true, 49 | get() { 50 | return keyVal.value 51 | }, 52 | set(v: any) { 53 | keyVal.value = v 54 | }, 55 | }) 56 | } 57 | } 58 | 59 | export const ComputedHandler: Hanlder = { 60 | key: 'Computed', 61 | handler, 62 | } 63 | -------------------------------------------------------------------------------- /src/decorators/hook.ts: -------------------------------------------------------------------------------- 1 | import { 2 | onActivated, 3 | onBeforeMount, 4 | onBeforeUnmount, 5 | onBeforeUpdate, 6 | onDeactivated, 7 | onErrorCaptured, 8 | onMounted, 9 | onRenderTracked, 10 | onRenderTriggered, 11 | onServerPrefetch, 12 | onUnmounted, 13 | onUpdated, 14 | } from 'vue' 15 | import type { Hanlder } from '../type' 16 | import { createDecorator, getProtoMetadata } from './util' 17 | 18 | type Lifecycle = 19 | | 'BeforeMount' 20 | | 'Mounted' 21 | | 'BeforeUpdate' 22 | | 'Updated' 23 | | 'BeforeUnmount' 24 | | 'Unmounted' 25 | | 'Activated' 26 | | 'Deactivated' 27 | | 'ErrorCaptured' 28 | | 'RenderTracked' 29 | | 'RenderTriggered' 30 | | 'ServerPrefetch' 31 | 32 | export const Hook: HookDecorator = createDecorator('Hook', true) 33 | 34 | export interface HookDecorator { 35 | (lifecycle: Lifecycle | Lifecycle[]): MethodDecorator 36 | MetadataKey: string | symbol 37 | } 38 | 39 | function handler(targetThis: any) { 40 | const list = getProtoMetadata<(Lifecycle | Lifecycle[])[]>(targetThis, Hook.MetadataKey) 41 | if (!list?.length) return 42 | for (const item of list) { 43 | let vueFn: any 44 | const doneLife: Record = {} 45 | const options = item.options.slice() 46 | for (const option of options) { 47 | if (Array.isArray(option)) { 48 | options.push(...option) 49 | continue 50 | } 51 | if (doneLife[option]) continue 52 | switch (option) { 53 | case 'BeforeMount': 54 | vueFn = onBeforeMount 55 | break 56 | case 'Mounted': 57 | vueFn = onMounted 58 | break 59 | case 'BeforeUpdate': 60 | vueFn = onBeforeUpdate 61 | break 62 | case 'Updated': 63 | vueFn = onUpdated 64 | break 65 | case 'BeforeUnmount': 66 | vueFn = onBeforeUnmount 67 | break 68 | case 'Unmounted': 69 | vueFn = onUnmounted 70 | break 71 | case 'Activated': 72 | vueFn = onActivated 73 | break 74 | case 'Deactivated': 75 | vueFn = onDeactivated 76 | break 77 | case 'ErrorCaptured': 78 | vueFn = onErrorCaptured 79 | break 80 | case 'RenderTracked': 81 | vueFn = onRenderTracked 82 | break 83 | case 'RenderTriggered': 84 | vueFn = onRenderTriggered 85 | break 86 | case 'ServerPrefetch': 87 | vueFn = onServerPrefetch 88 | break 89 | } 90 | doneLife[option] = true 91 | vueFn((...args: any[]) => targetThis[item.key](...args)) 92 | } 93 | } 94 | } 95 | 96 | export const HookHandler: Hanlder = { 97 | key: 'Hook', 98 | handler, 99 | } 100 | -------------------------------------------------------------------------------- /src/decorators/link.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from 'vue' 2 | import type { Hanlder } from '../type' 3 | import { createDecorator, getProtoMetadata } from './util' 4 | 5 | export const Link: LinkDecorator = createDecorator('Link') 6 | export interface LinkDecorator { 7 | (refName?: string): PropertyDecorator 8 | MetadataKey: symbol | string 9 | } 10 | 11 | function handler(targetThis: Record) { 12 | const list = getProtoMetadata(targetThis, Link.MetadataKey) 13 | if (!list || !list.length) return 14 | for (const item of list) { 15 | const { key, options } = item 16 | const instance = getCurrentInstance() 17 | Object.defineProperty(targetThis, key, { 18 | enumerable: true, 19 | configurable: true, 20 | get() { 21 | return instance?.refs?.[(options || key) as string] 22 | }, 23 | }) 24 | } 25 | } 26 | 27 | export const LinkHandler: Hanlder = { 28 | key: 'Link', 29 | handler, 30 | } 31 | -------------------------------------------------------------------------------- /src/decorators/mut.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { customRef, ref, shallowRef } from 'vue' 3 | import type { Hanlder } from '../type' 4 | import { createDecorator, getProtoMetadata } from './util' 5 | 6 | export const Mut: MutDecorator = createDecorator('Mut') 7 | 8 | type MutOptions = void | true | Parameters[0] 9 | type RefFactory = Parameters[0] 10 | export interface MutDecorator { 11 | /** 12 | * @param shallowOrRefFactory 13 | */ 14 | (shallowOrRefFactory?: true | RefFactory): PropertyDecorator 15 | MetadataKey: string | symbol 16 | } 17 | 18 | function handler(targetThis: Record) { 19 | const list = getProtoMetadata(targetThis, Mut.MetadataKey) 20 | if (!list || !list.length) return 21 | for (const item of list) { 22 | const { options, key } = item 23 | defMut(targetThis, key, options) 24 | } 25 | } 26 | 27 | export function defMut(targetThis: Record, key: string | symbol, options: any) { 28 | let keyVal: Ref 29 | if (options === true) { 30 | keyVal = shallowRef() 31 | } else if (typeof options === 'function') { 32 | keyVal = customRef(options) 33 | } else { 34 | keyVal = ref() 35 | } 36 | Object.defineProperty(targetThis, key, { 37 | enumerable: true, 38 | configurable: true, 39 | get() { 40 | return keyVal.value 41 | }, 42 | set(v) { 43 | keyVal.value = v 44 | }, 45 | }) 46 | } 47 | 48 | export const MutHandler: Hanlder = { 49 | key: 'Mut', 50 | handler, 51 | } 52 | -------------------------------------------------------------------------------- /src/decorators/util.ts: -------------------------------------------------------------------------------- 1 | import { createSymbol } from '../helper' 2 | 3 | export interface DecoratorFn { 4 | (options: T): IsMethod extends false ? PropertyDecorator : MethodDecorator 5 | MetadataKey: string | symbol 6 | } 7 | export interface MetadataStore { 8 | key: string | symbol 9 | options: T 10 | desc?: PropertyDescriptor | null 11 | } 12 | 13 | export function createDecorator(name: string, allowRepeat = false) { 14 | const metaName = `VUE3-OOP_${name.toUpperCase()}` 15 | const MetadataKey = createSymbol(metaName) 16 | const decoratorFn: DecoratorFn = function (options: T) { 17 | return function (target: any, key: string | symbol) { 18 | let list: MetadataStore[] = Reflect.getMetadata(MetadataKey, target) || [] 19 | // 处理继承 20 | list = list.slice() 21 | const hasIndex = list.findIndex(k => k.key === key) 22 | if (hasIndex === -1) { 23 | list.push({ key, options: allowRepeat ? [options] : options }) 24 | } else { 25 | // 处理继承 26 | const item = Object.assign({}, list[hasIndex]) 27 | if (!allowRepeat) { 28 | item.options = options 29 | } else if (Array.isArray(item.options)) { 30 | item.options = item.options.concat(options) 31 | } 32 | list[hasIndex] = item 33 | } 34 | Reflect.defineMetadata(MetadataKey, list, target) 35 | } 36 | } 37 | decoratorFn.MetadataKey = MetadataKey 38 | Object.defineProperty(decoratorFn, 'MetadataKey', { 39 | get() { 40 | return MetadataKey 41 | }, 42 | }) 43 | return decoratorFn 44 | } 45 | export function getProtoMetadata(target: any, key: symbol | string, withDesc = false): MetadataStore[] { 46 | const proto = Object.getPrototypeOf(target) 47 | if (!proto) return [] 48 | const res: MetadataStore[] = Reflect.getMetadata(key, proto) || [] 49 | if (withDesc) { 50 | res.forEach(k => (k.desc = getDeepOwnDescriptor(proto, k.key))) 51 | } 52 | return res 53 | } 54 | export function getDeepOwnDescriptor(proto: any, key: string | symbol): PropertyDescriptor | null { 55 | if (!proto) return null 56 | const desc = Object.getOwnPropertyDescriptor(proto, key) 57 | if (desc) return desc 58 | return getDeepOwnDescriptor(Object.getPrototypeOf(proto), key) 59 | } 60 | -------------------------------------------------------------------------------- /src/di/index.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue' 2 | import { getCurrentInstance, inject, provide } from 'vue' 3 | import type { ClassProvider, Provider, ResolvedReflectiveProvider, Type, TypeProvider } from 'injection-js' 4 | import { Injectable, InjectionToken, ReflectiveInjector, resolveForwardRef, SkipSelf } from 'injection-js' 5 | import { createSymbol } from '../helper' 6 | 7 | export const InjectorKey: InjectionKey = createSymbol('VUE3-OOP_ReflectiveInjector') as symbol 8 | 9 | const MetadataKey = createSymbol('VUE3-OOP_Component') 10 | const MetadataProviderKey = createSymbol('VUE3-OOP_ResolveProviders') 11 | 12 | declare module 'vue' { 13 | interface App { 14 | getStore(): any 15 | getService(token: any): any 16 | } 17 | } 18 | 19 | export interface ComponentOptions { 20 | /** 21 | * 依赖的服务 22 | */ 23 | providers?: Provider[] 24 | /** 25 | * 排除掉的服务 26 | */ 27 | exclude?: Provider[] 28 | /** 29 | * 自动分析依赖 30 | */ 31 | autoResolveDeps?: boolean 32 | /** 33 | * 此注入器是否作为全局的store 34 | */ 35 | globalStore?: boolean 36 | /** 37 | * option是否是稳定的, 38 | * 依赖解析只会在第一次的时候解析并且缓存下来,所以options如果是动态变化的,请标记 39 | */ 40 | stable?: boolean 41 | } 42 | 43 | export function Component(options?: ComponentOptions): ClassDecorator { 44 | return function (target: any) { 45 | Reflect.defineMetadata(MetadataKey, options, target) 46 | return Injectable()(target) 47 | } 48 | } 49 | 50 | export function resolveComponent(target: { new (...args: []): any }) { 51 | // 如果没有使用 injection-js 则不创建注入器 52 | if (!Reflect.getMetadata('annotations', target)) return new target() 53 | const parent = inject(InjectorKey, undefined) 54 | // 从缓存中拿到解析过得依赖 55 | let resolveProviders = Reflect.getOwnMetadata(MetadataProviderKey, target) 56 | const options: ComponentOptions | undefined = Reflect.getOwnMetadata(MetadataKey, target) 57 | if (!resolveProviders || options?.stable === false) { 58 | // 依赖 59 | let deps: Provider[] = [target] 60 | if (options?.providers?.length) { 61 | deps = deps.concat(options.providers) 62 | } 63 | // 自动解析依赖的依赖 64 | if (options?.autoResolveDeps !== false) { 65 | deps = resolveDependencies(deps) 66 | } 67 | // 排除掉某些依赖 68 | if (options?.exclude?.length) { 69 | deps = deps.filter(k => !options.exclude?.includes(k)) 70 | } 71 | resolveProviders = ReflectiveInjector.resolve(deps) 72 | // 缓存解析过的依赖, 提高性能 73 | Reflect.defineMetadata(MetadataProviderKey, resolveProviders, target) 74 | } 75 | const injector = ReflectiveInjector.fromResolvedProviders(resolveProviders, parent) 76 | if (options?.globalStore) { 77 | // 如果作为全局的服务,则注入到根上面 78 | 79 | const current = getCurrentInstance()! 80 | const app = current.appContext.app 81 | 82 | app.provide(InjectorKey, injector) 83 | app.getStore = () => injector 84 | app.getService = token => injector.get(token) 85 | } else { 86 | provide(InjectorKey, injector) 87 | } 88 | const compInstance = injector.get(target) 89 | // 处理一下providers中的未创建实例的服务 90 | resolveProviders.forEach(k => injector.get(k.key.token)) 91 | return compInstance 92 | } 93 | 94 | export function resolveDependencies(inputs: Provider[]) { 95 | // 处理抽象类 96 | const noConstructor: Exclude[] = [] 97 | 98 | for (const input of inputs) { 99 | if (!(input instanceof Function) && !Array.isArray(input)) { 100 | noConstructor.push(input) 101 | } 102 | } 103 | 104 | const deps = new Set() 105 | 106 | function resolver(klass: Provider) { 107 | if (deps.has(klass) || noConstructor.find(k => k !== klass && k.provide === klass)) return 108 | deps.add(klass) 109 | const resolves = ReflectiveInjector.resolve([klass]) 110 | for (const item of resolves) { 111 | for (const fact of item.resolvedFactories) { 112 | for (const dep of fact.dependencies) { 113 | if ( 114 | dep.optional || 115 | dep.visibility instanceof SkipSelf || 116 | dep.key.token instanceof InjectionToken || 117 | typeof dep.key.token !== 'function' 118 | ) { 119 | continue 120 | } 121 | resolver(dep.key.token as unknown as ClassProvider) 122 | } 123 | } 124 | } 125 | } 126 | 127 | for (const input of inputs) resolver(input) 128 | 129 | return Array.from(deps) 130 | } 131 | 132 | /** 133 | * 获取当前的注射器,可用于外部使用 134 | */ 135 | export function getCurrentInjector(): ReflectiveInjector { 136 | const instance = getCurrentInstance() 137 | // @ts-ignore 138 | return instance?.provides[InjectorKey] || inject(InjectorKey) 139 | } 140 | /** 手动创建当前注射器, 只能用在 setup 中 */ 141 | export function createCurrentInjector(providers: Provider[], exclude?: Provider[]): ReflectiveInjector { 142 | let deps = resolveDependencies(providers) 143 | if (exclude?.length) { 144 | deps = deps.filter(k => exclude?.includes(k)) 145 | } 146 | const resolveProviders = ReflectiveInjector.resolve(deps) 147 | const parent = inject(InjectorKey, undefined) 148 | const injector = ReflectiveInjector.fromResolvedProviders(resolveProviders, parent) 149 | provide(InjectorKey, injector) 150 | // 实例化 151 | resolveProviders.forEach(k => injector.get(k.key.token)) 152 | return injector 153 | } 154 | 155 | /** 156 | * 从当前容器中获取服务 157 | * @param token 158 | * @param notFoundValue 159 | */ 160 | function injectService>(token: T, notFoundValue?: any): InstanceType 161 | function injectService(token: string | number | symbol | Type, notFoundValue?: any): T 162 | function injectService(token: any, notFoundValue?: any) { 163 | const currentInjector = getCurrentInjector() 164 | if (!currentInjector) return notFoundValue 165 | if (typeof token === 'function') token = resolveForwardRef(token) 166 | return currentInjector.get(token, notFoundValue) 167 | } 168 | 169 | interface Constructable { 170 | constructor: Function 171 | } 172 | function provideService(...service: T[]) { 173 | const instance = getCurrentInstance()! 174 | // @ts-ignore 175 | let injector: ReflectiveInjector 176 | if (Reflect.has(instance, InjectorKey as symbol)) { 177 | // @ts-ignore 178 | injector = instance.provides[InjectorKey] 179 | } 180 | // @ts-ignore 181 | if (!injector) { 182 | injector = ReflectiveInjector.resolveAndCreate([], inject(InjectorKey)) 183 | // @ts-ignore 184 | instance.provides[InjectorKey] = injector 185 | } 186 | 187 | ReflectiveInjector.resolve(service.map(k => ({ provide: k.constructor, useValue: k }))).forEach((provider, i) => { 188 | // @ts-ignore 189 | const index = injector._providers.length 190 | // @ts-ignore 191 | injector._providers[index] = provider 192 | // @ts-ignore 193 | injector.keyIds[index] = provider.key.id 194 | // @ts-ignore 195 | injector.objs[index] = service[i] 196 | }) 197 | return injector 198 | } 199 | 200 | export { injectService, provideService } 201 | -------------------------------------------------------------------------------- /src/extends/component.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentOptions, ComponentPublicInstance, VNodeChild, VNodeRef } from 'vue' 2 | import { getCurrentInstance, isRef, markRaw } from 'vue' 3 | import { ComputedHandler } from '../decorators/computed' 4 | import { HookHandler } from '../decorators/hook' 5 | import { LinkHandler } from '../decorators/link' 6 | import { MutHandler } from '../decorators/mut' 7 | import { resolveComponent } from '../di' 8 | import { getEmitsFromProps, useCtx, useProps } from '../helper' 9 | import type { Hanlder, VueComponentProps, WithSlotTypes } from '../type' 10 | 11 | export const GlobalStoreKey = 'GlobalStoreKey' 12 | const hasOwnProperty = Object.prototype.hasOwnProperty 13 | const hasOwn = (val: any, key: string) => hasOwnProperty.call(val, key) 14 | 15 | export class VueComponent { 16 | /** 装饰器处理 */ 17 | static handler: Hanlder[] = [MutHandler, ComputedHandler, LinkHandler, HookHandler] 18 | /** 是否自定义解析组件 */ 19 | static resolveComponent = resolveComponent 20 | /** 热更新使用 */ 21 | static __hmrId?: string 22 | /** 组件显示名字 */ 23 | static displayName?: string 24 | /** 组件的属性定义 */ 25 | static defaultProps?: any 26 | /** vue options emits */ 27 | static emits?: string[] 28 | /** 29 | * 是否继承多余的属性 30 | */ 31 | static inheritAttrs?: boolean 32 | 33 | static __vccOpts__value?: ComponentOptions 34 | /** 组件option定义,vue3遇到类组件会从此属性获取组件的option */ 35 | static __vccOpts: ComponentOptions 36 | /** 组件属性 */ 37 | public props = useProps>() 38 | /** 组件上下文 */ 39 | public context = useCtx() as WithSlotTypes 40 | 41 | // 公开的一些属性 publicPropertiesMap 42 | /** 组件内部实例,如果使用组件实例请 this.$.proxy */ 43 | public $ = getCurrentInstance()! 44 | /** 主要给jsx提示用 */ 45 | get $props() { 46 | return this.props 47 | } 48 | get $el() { 49 | return this.$.proxy!.$el 50 | } 51 | get $data() { 52 | return this.$.proxy!.$data 53 | } 54 | get $attrs() { 55 | return this.$.proxy!.$attrs 56 | } 57 | get $slots(): Omit['v-slots']>, '$stable'> { 58 | return this.$.proxy!.$slots as any 59 | } 60 | get $options() { 61 | return this.$.proxy!.$options 62 | } 63 | get $refs() { 64 | return this.$.proxy!.$refs 65 | } 66 | get $parent() { 67 | return this.$.proxy!.$parent 68 | } 69 | get $root() { 70 | return this.$.proxy!.$root 71 | } 72 | get $emit() { 73 | return this.$.proxy!.$emit 74 | } 75 | get $forceUpdate() { 76 | return this.$.proxy!.$forceUpdate 77 | } 78 | get $nextTick() { 79 | return this.$.proxy!.$nextTick 80 | } 81 | get $watch() { 82 | return this.$.proxy!.$watch 83 | } 84 | 85 | constructor() { 86 | markRaw(this) 87 | // 处理ref 88 | const current = this.$ 89 | // 由于vue会包装一层,会自动代理ref,导致类型错误, 还导致不能修改变量 90 | current.exposed = this 91 | current.exposeProxy = this 92 | 93 | VueComponent.handler.forEach(handler => handler.handler(this)) 94 | } 95 | 96 | /** 渲染函数 */ 97 | render?(ctx: ComponentPublicInstance, cache: any[]): VNodeChild 98 | 99 | /** 100 | * 组件初始化逻辑 101 | * 如果设置 static async = true,并且返回promise则为异步setup组件 102 | * 需配合 suspense 组件使用 103 | */ 104 | init?(): any 105 | 106 | /** 107 | * 标记为异步初始化组件, 配合init使用 108 | */ 109 | static async?: boolean 110 | } 111 | // 某些浏览器不支持 static get 112 | Object.defineProperty(VueComponent, '__vccOpts', { 113 | enumerable: true, 114 | configurable: true, 115 | get() { 116 | if (this === VueComponent) { 117 | console.warn('base VueComponent only to be extends') 118 | return 119 | } 120 | if (hasOwn(this, '__vccOpts__value')) return this.__vccOpts__value 121 | 122 | // 处理多重继承 123 | const parent = Object.getPrototypeOf(this) 124 | const parentOpt = parent === VueComponent ? null : parent.__vccOpts 125 | const CompConstructor = this as typeof VueComponent 126 | 127 | const { displayName, defaultProps, emits, ...rest } = CompConstructor 128 | 129 | let optValue: ComponentOptions 130 | 131 | const setup = (props: any, ctx: any) => { 132 | const instance = VueComponent.resolveComponent(CompConstructor) 133 | // 支持 devtool 134 | getCurrentInstance()!.data = instance 135 | // 支持模板 136 | if (CompConstructor.__vccOpts__value!.render) return instance 137 | const render = instance.render.bind(instance) 138 | // 支持异步组件 139 | if (typeof instance.init === 'function') { 140 | const res = instance.init() 141 | if (typeof res?.then === 'function' && CompConstructor.async) { 142 | return res.then(() => render) 143 | } 144 | } 145 | return render 146 | } 147 | 148 | // 处理继承 149 | if (parentOpt) { 150 | optValue = { 151 | ...parentOpt, 152 | ...rest, 153 | name: displayName || CompConstructor.name, 154 | setup, 155 | } 156 | if (defaultProps) optValue.props = defaultProps 157 | if (emits) optValue.emits = emits 158 | } else { 159 | optValue = { 160 | ...rest, 161 | name: displayName || CompConstructor.name, 162 | props: defaultProps || {}, 163 | // 放到emits的on函数会自动缓存 164 | emits: (emits || []).concat(getEmitsFromProps(CompConstructor.defaultProps || {})), 165 | setup, 166 | } 167 | } 168 | 169 | Object.defineProperty(this, '__vccOpts__value', { 170 | configurable: true, 171 | enumerable: false, 172 | value: optValue, 173 | writable: true, 174 | }) 175 | return optValue 176 | }, 177 | }) 178 | 179 | // 处理forwardref 180 | export function useForwardRef() { 181 | const instance = getCurrentInstance()! 182 | function forwardRef(ref: any) { 183 | instance.exposed = ref 184 | instance.exposeProxy = ref 185 | } 186 | return forwardRef 187 | } 188 | 189 | // 合并ref 190 | export function mergeRefs(...values: VNodeRef[]) { 191 | return function (ref: Element | ComponentPublicInstance | null, refs: Record) { 192 | for (const r of values) { 193 | if (typeof r === 'string') { 194 | refs[r] = ref 195 | } else if (typeof r === 'function') { 196 | r(ref, refs) 197 | } else if (isRef(r)) { 198 | r.value = ref 199 | } 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/extends/service.ts: -------------------------------------------------------------------------------- 1 | import { markRaw } from 'vue' 2 | import { ComputedHandler } from '../decorators/computed' 3 | import { HookHandler } from '../decorators/hook' 4 | import { LinkHandler } from '../decorators/link' 5 | import { MutHandler } from '../decorators/mut' 6 | import type { Hanlder } from '../type' 7 | 8 | export class VueService { 9 | static handler: Hanlder[] = [MutHandler, ComputedHandler, LinkHandler, HookHandler] 10 | constructor() { 11 | markRaw(this) 12 | VueService.handler.forEach(handler => handler.handler(this)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | import type { SetupContext } from 'vue' 2 | import { getCurrentInstance } from 'vue' 3 | import autobind from 'autobind-decorator' 4 | 5 | /** 6 | * 自动绑定this !!!此装饰器必须放在最上面 7 | */ 8 | export function Autobind() { 9 | return autobind 10 | } 11 | 12 | export function useProps() { 13 | const instance = getCurrentInstance() 14 | return instance?.props as T 15 | } 16 | export function useCtx() { 17 | const instance = getCurrentInstance() 18 | // @ts-ignore 19 | return instance?.setupContext as SetupContext 20 | } 21 | export function getCurrentApp() { 22 | return getCurrentInstance()?.appContext.app 23 | } 24 | export function getEmitsFromProps(defaultProps: Record | string[]) { 25 | const keys = Array.isArray(defaultProps) ? defaultProps : Object.keys(defaultProps) 26 | const emits: string[] = [] 27 | 28 | for (let key of keys) { 29 | if (!/^on/.test(key)) continue 30 | key = key.slice(2).replace(/Once$/, '') 31 | emits.push(key[0].toLowerCase() + key.slice(1)) 32 | } 33 | return emits 34 | } 35 | export function createSymbol(name: string) { 36 | return typeof Symbol === 'undefined' ? name : Symbol(name) 37 | } 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { VueComponent, GlobalStoreKey, useForwardRef, mergeRefs } from './extends/component' 2 | export { VueService } from './extends/service' 3 | export { Mut, defMut } from './decorators/mut' 4 | export { Computed } from './decorators/computed' 5 | export { Link } from './decorators/link' 6 | export { Hook } from './decorators/hook' 7 | export { createDecorator, getProtoMetadata } from './decorators/util' 8 | export * from './helper' 9 | export * from './simple-props' 10 | export * from './components' 11 | export { Component, InjectorKey, getCurrentInjector, createCurrentInjector, injectService, provideService } from './di' 12 | export type { ComponentOptions } from './di' 13 | export type { 14 | ComponentProps, 15 | ComponentSlots, 16 | ClassType, 17 | WithVModel, 18 | ComponentPropsObject, 19 | Hanlder, 20 | ClassAndStyleProps, 21 | } from './type' 22 | -------------------------------------------------------------------------------- /src/simple-props/composables.ts: -------------------------------------------------------------------------------- 1 | import { camelize, getCurrentInstance, useAttrs, useSlots } from 'vue' 2 | import type { ClassAndStyleProps } from '../type' 3 | 4 | export function camelizePropKey(p: string | symbol): string | symbol { 5 | if (typeof p === 'string') { 6 | if (p.startsWith('data-') || p.startsWith('aria-')) return p 7 | return camelize(p) 8 | } 9 | return p 10 | } 11 | 12 | export function useProps(): T { 13 | const instance = getCurrentInstance() 14 | if (!instance) { 15 | throw new Error('useProps must be called inside setup()') 16 | } 17 | 18 | const slots = useSlots() 19 | const getProps = () => { 20 | return Object.fromEntries(Object.entries(instance.vnode.props || {}).map(([k, v]) => [camelizePropKey(k), v])) 21 | } 22 | 23 | return new Proxy( 24 | {}, 25 | { 26 | get(target, p, receiver) { 27 | const slotName = getSlotName(p) 28 | if (slotName) { 29 | const slot = Reflect.get(slots, slotName, receiver) 30 | if (slot) return slot 31 | } 32 | const key = camelizePropKey(p) 33 | if (key in instance.props) { 34 | // @ts-ignore 35 | return instance.props[key] 36 | } else { 37 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 38 | instance.proxy?.$attrs 39 | 40 | return Reflect.get(getProps(), key, receiver) 41 | } 42 | }, 43 | ownKeys() { 44 | return [ 45 | ...new Set([ 46 | ...Reflect.ownKeys(instance.props), 47 | ...Reflect.ownKeys(getProps()), 48 | ...Reflect.ownKeys(slots).map(k => (typeof k === 'string' ? camelize(`render-${k}`) : k)), 49 | ]), 50 | ] 51 | }, 52 | has(target, p) { 53 | const slotName = getSlotName(p) 54 | if (slotName) { 55 | return Reflect.has(slots, slotName) 56 | } 57 | return Reflect.has(getProps(), camelizePropKey(p)) 58 | }, 59 | getOwnPropertyDescriptor(target, p) { 60 | const slotName = getSlotName(p) 61 | if (slotName) { 62 | const descriptor = Reflect.getOwnPropertyDescriptor(slots, slotName) 63 | if (descriptor) return descriptor 64 | } 65 | return Reflect.getOwnPropertyDescriptor(getProps(), camelizePropKey(p)) 66 | }, 67 | }, 68 | ) as any 69 | } 70 | 71 | function getSlotName(p: PropertyKey) { 72 | if (typeof p === 'string' && p.startsWith('render')) return p.slice(6).replace(/^[A-Z]/, s => s.toLowerCase()) 73 | } 74 | 75 | export function useClassAndStyle(): ClassAndStyleProps { 76 | const instance = getCurrentInstance() 77 | if (!instance) { 78 | throw new Error('useClassAndStyle must be called inside setup()') 79 | } 80 | 81 | const attrs = useAttrs() 82 | const keys = ['class', 'style'] 83 | 84 | return new Proxy(attrs, { 85 | get(target, p, receiver) { 86 | if (keys.includes(p as string)) { 87 | return Reflect.get(target, p, receiver) 88 | } 89 | }, 90 | ownKeys: () => keys, 91 | has: (target, p) => keys.includes(p as string), 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /src/simple-props/index.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent as vueDefineComponent, type ComponentOptions } from 'vue' 2 | import { useProps } from './composables' 3 | import type { ComponentType, FunctionalComponent } from './types' 4 | 5 | export { useClassAndStyle, camelizePropKey } from './composables' 6 | export * from './types' 7 | 8 | export function defineComponent< 9 | T extends Record, 10 | S extends Record = {}, 11 | M extends Record = {}, 12 | >(comp: FunctionalComponent, extraOptions?: ComponentOptions): ComponentType { 13 | const fn: FunctionalComponent = (_props, ctx) => { 14 | const props = useProps() 15 | return comp(props as any, ctx as any) 16 | } 17 | Object.keys(comp).forEach(key => { 18 | // @ts-expect-error 19 | fn[key] = comp[key] 20 | }) 21 | return vueDefineComponent(fn, { 22 | inheritAttrs: false, 23 | name: comp.name || comp.displayName, 24 | ...extraOptions, 25 | }) as any 26 | } 27 | -------------------------------------------------------------------------------- /src/simple-props/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ComponentPropsOptions, 3 | DefineSetupFnComponent, 4 | EmitsOptions, 5 | EmitsToProps, 6 | SetupContext, 7 | ShortEmitsToObject, 8 | Slots, 9 | SlotsType, 10 | FunctionalComponent as VueFunctionalComponent, 11 | } from 'vue' 12 | 13 | type RemovePrefix = K extends `${P}${infer Event}` ? Uncapitalize : never 14 | 15 | export type ExtractProps = Omit & Partial> 16 | export type ExtractEvent = { 17 | [P in keyof T as RemovePrefix]: T[P] 18 | } 19 | export type ExtractSlots = { 20 | [P in keyof T as RemovePrefix]: T[P] 21 | } 22 | 23 | export type ComponentType< 24 | T extends Record, 25 | S extends Record, 26 | Expose extends Record, 27 | > = DefineSetupFnComponent, ExtractEvent, SlotsType & S>> & { 28 | new (...args: any[]): Expose 29 | } 30 | 31 | type IfAny = 0 extends 1 & T ? Y : N 32 | export interface FunctionalComponent< 33 | P = {}, 34 | E extends EmitsOptions | Record = {}, 35 | S extends Record = any, 36 | EE extends EmitsOptions = ShortEmitsToObject, 37 | > { 38 | (props: P & EmitsToProps, ctx: SetupContext>>): any 39 | props?: ComponentPropsOptions

    40 | emits?: EE | (keyof EE)[] 41 | slots?: IfAny> 42 | expose?: string[] 43 | inheritAttrs?: boolean 44 | displayName?: string 45 | compatConfig?: VueFunctionalComponent['compatConfig'] 46 | } 47 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentCustomProps, Prop, SetupContext, StyleValue, VNodeChild, VNodeProps } from 'vue' 2 | 3 | /** 4 | * 装饰器处理 5 | */ 6 | export interface Hanlder { 7 | key: string 8 | handler: (targetThis: any) => void 9 | } 10 | 11 | type KeysOfUnion = T extends T ? keyof T : never 12 | 13 | type DefaultSlots = { 14 | default(): VNodeChild 15 | } 16 | 17 | type MixDefaultSlots = 'default' extends keyof T ? {} : DefaultSlots 18 | 19 | // 处理tsx slots 类型问题 20 | export type WithVSlots> = { 21 | 'v-slots'?: 'slots' extends keyof T 22 | ? Partial> 23 | : Partial<{ $stable: boolean; default(): VNodeChild }> 24 | } 25 | 26 | export type WithSlotTypes = Omit & { 27 | slots: NonNullable['v-slots']> 28 | } 29 | 30 | type ModelProps = Exclude< 31 | { 32 | [Prop in keyof T]: T extends { 33 | [k in Prop as `onUpdate:${k & string}`]?: any 34 | } 35 | ? Prop 36 | : never 37 | }[keyof T], 38 | undefined 39 | > 40 | 41 | export type WithVModel> = TransformModelValue<{ 42 | [k in U as `v-model:${k & string}`]?: T[k] | [T[k], string[]] 43 | }> 44 | export type TransformModelValue = 'v-model:modelValue' extends keyof T 45 | ? Omit & { ['v-model']?: T['v-model:modelValue'] } 46 | : T 47 | 48 | export type ComponentProps = ComponentPropsObject | Array>> 49 | 50 | export type ComponentPropsObject = { 51 | [U in KeysOfUnion>]-?: Prop 52 | } 53 | 54 | export type ComponentSlots = NonNullable 55 | 56 | /** 为了阻止ts把不相关的类也解析到metadata数据中,用这个工具类型包装一下类 */ 57 | export type ClassType = T 58 | 59 | export interface ClassAndStyleProps { 60 | class?: any 61 | style?: StyleValue 62 | [name: string]: any 63 | } 64 | 65 | export type DistributiveOmit = T extends T ? Omit : never 66 | 67 | type DistributiveVModel = T extends T ? WithVModel : never 68 | type DistributiveVSlots = T extends T ? WithVSlots : never 69 | 70 | export type VueComponentProps = DistributiveOmit & 71 | DistributiveVModel & 72 | DistributiveVSlots & 73 | VNodeProps & 74 | ClassAndStyleProps & 75 | ComponentCustomProps 76 | -------------------------------------------------------------------------------- /tests/__snapshots__/component.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`class component render 1`] = `"

    hello world 1

    "`; 4 | 5 | exports[`class component render 2`] = `"

    hello world 2

    "`; 6 | -------------------------------------------------------------------------------- /tests/component.test.tsx: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | import { expect, test } from 'vitest' 3 | import { type ComponentProps, type ComponentSlots, Mut, VueComponent } from 'vue3-oop' 4 | import { mount } from '@vue/test-utils' 5 | import { nextTick, type VNodeChild } from 'vue' 6 | 7 | test('class component render', async () => { 8 | class CountComponent extends VueComponent { 9 | @Mut() count = 1 10 | render() { 11 | return

    hello world {this.count}

    12 | } 13 | } 14 | 15 | const wrapper = mount(CountComponent) 16 | expect(wrapper.html()).toMatchSnapshot() 17 | wrapper.vm.count++ 18 | await nextTick() 19 | expect(wrapper.html()).toMatchSnapshot() 20 | }) 21 | 22 | test('class with props', async () => { 23 | interface CountProps { 24 | count: number 25 | } 26 | class CountComponent extends VueComponent { 27 | static defaultProps: ComponentProps = ['count'] 28 | render() { 29 | return
    {this.props.count}
    30 | } 31 | } 32 | 33 | const wrapper = mount(CountComponent, { props: { count: 1 } }) 34 | expect(wrapper.text()).toContain('1') 35 | 36 | wrapper.vm.$props.count = 2 37 | await nextTick() 38 | expect(wrapper.text()).toContain('2') 39 | }) 40 | 41 | test('带slots的组件', async () => { 42 | interface CountProps { 43 | slots: { 44 | item(name: string): VNodeChild 45 | } 46 | } 47 | class Count extends VueComponent { 48 | render() { 49 | return
    {this.context.slots.item?.('aaa')}
    50 | } 51 | } 52 | 53 | const slots: ComponentSlots = { 54 | item(name: string): VNodeChild { 55 | return name 56 | }, 57 | } 58 | 59 | // @ts-ignore 60 | const wrapper = mount(Count, { slots }) 61 | expect(wrapper.text()).toContain('aaa') 62 | }) 63 | 64 | test('init 组件初始化调用', async () => { 65 | class Count extends VueComponent { 66 | @Mut() count = 0 67 | 68 | init() { 69 | this.count = 1 70 | } 71 | 72 | render() { 73 | return
    {this.count}
    74 | } 75 | } 76 | const wrapper = mount(Count) 77 | expect(wrapper.text()).toContain('1') 78 | }) 79 | -------------------------------------------------------------------------------- /tests/decorator/computed.test.tsx: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | import { expect, test } from 'vitest' 3 | import { Computed, Mut, VueComponent } from 'vue3-oop' 4 | import { mount } from '@vue/test-utils' 5 | import { nextTick } from 'vue' 6 | 7 | class CountComponent extends VueComponent { 8 | @Mut() count = 1 9 | 10 | @Computed() 11 | get double() { 12 | return this.count * 2 13 | } 14 | 15 | render() { 16 | return ( 17 |
    18 |

    {this.double}

    19 |
    20 | ) 21 | } 22 | } 23 | 24 | test('Computed decorator should work', async () => { 25 | const wrapper = mount(CountComponent) 26 | const vm = wrapper.vm 27 | 28 | const p = wrapper.get('p') 29 | expect(p.text()).toContain('2') 30 | vm.count++ 31 | await nextTick() 32 | expect(p.text()).toContain('4') 33 | }) 34 | -------------------------------------------------------------------------------- /tests/decorator/hook.test.tsx: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | import { mount } from '@vue/test-utils' 3 | import { expect, test, vi } from 'vitest' 4 | import { Hook, VueComponent } from 'vue3-oop' 5 | import { nextTick } from 'vue' 6 | 7 | test('hook should work', async () => { 8 | const log: any = vi.spyOn(console, 'log') 9 | 10 | class Count extends VueComponent { 11 | @Hook('Mounted') 12 | mounted() { 13 | log(1) 14 | } 15 | 16 | render() { 17 | return
    1111
    18 | } 19 | } 20 | 21 | const wrapper = mount(Count) 22 | await nextTick() 23 | expect(log).toHaveBeenCalled() 24 | }) 25 | -------------------------------------------------------------------------------- /tests/decorator/mut.test.tsx: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | import { expect, test } from 'vitest' 3 | import { Mut, VueComponent } from 'vue3-oop' 4 | import { mount } from '@vue/test-utils' 5 | import type { CustomRefFactory } from 'vue' 6 | 7 | const debounceRef: CustomRefFactory = (track, trigger) => { 8 | let value: any 9 | let timeout: number 10 | return { 11 | get() { 12 | track() 13 | return value 14 | }, 15 | set(v) { 16 | if (value === undefined) { 17 | value = v 18 | return 19 | } 20 | clearTimeout(timeout) 21 | timeout = window.setTimeout(() => { 22 | value = v 23 | trigger() 24 | }, 1000) 25 | }, 26 | } 27 | } 28 | const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)) 29 | 30 | class CountComponent extends VueComponent { 31 | @Mut() count = 1 32 | // shallow ref 33 | @Mut(true) obj = { count: 1 } 34 | // custom ref 35 | @Mut(debounceRef) count1 = 1 36 | 37 | render() { 38 | return ( 39 |
    40 |

    this.count++}>{this.count}

    41 |
    this.obj.count++}> 42 | {this.obj.count} 43 |
    44 |
    this.count1++}> 45 | {this.count1} 46 |
    47 |
    48 | ) 49 | } 50 | } 51 | 52 | test('Mut decorator should work', async () => { 53 | const wrapper = mount(CountComponent) 54 | 55 | const p = wrapper.get('p') 56 | expect(p.text()).toContain('1') 57 | await p.trigger('click') 58 | expect(p.text()).toContain('2') 59 | }) 60 | 61 | test('mut: shallow ref', async () => { 62 | const wrapper = mount(CountComponent) 63 | const vm = wrapper.vm as unknown as CountComponent 64 | 65 | const shallow = wrapper.get('#shallow') 66 | expect(shallow.text()).toContain('1') 67 | await shallow.trigger('click') 68 | expect(shallow.text()).toContain('1') 69 | 70 | vm.obj = { count: 2 } 71 | await vm.$nextTick() 72 | console.log(shallow.text()) 73 | expect(shallow.text()).toContain('2') 74 | }) 75 | 76 | test('mut: custom ref', async () => { 77 | const wrapper = mount(CountComponent) 78 | // custom ref 79 | const custom = wrapper.get('#custom') 80 | expect(custom.text()).toContain('1') 81 | await custom.trigger('click') 82 | expect(custom.text()).toContain('1') 83 | await delay(1000) 84 | expect(custom.text()).toContain('2') 85 | }) 86 | -------------------------------------------------------------------------------- /tests/di.test.tsx: -------------------------------------------------------------------------------- 1 | import '@abraham/reflection' 2 | import { expect, test } from 'vitest' 3 | import { Component, getCurrentInjector, Mut, VueComponent, VueService } from 'vue3-oop' 4 | import { mount } from '@vue/test-utils' 5 | import { Injector } from 'injection-js' 6 | 7 | test('di should work', () => { 8 | class CountService { 9 | @Mut() count = 1 10 | add = () => this.count++ 11 | } 12 | @Component() 13 | class CountComponent extends VueComponent { 14 | constructor(private countService: CountService) { 15 | super() 16 | } 17 | 18 | render() { 19 | const { countService } = this 20 | return
    {countService.count}
    21 | } 22 | } 23 | 24 | const wrapper = mount(CountComponent) 25 | expect(wrapper.text()).toContain('1') 26 | }) 27 | 28 | test('外部服务应该可以获取到注射器', () => { 29 | class OutSideService extends VueService { 30 | injector = getCurrentInjector() 31 | } 32 | 33 | @Component() 34 | class Foo extends VueComponent { 35 | constructor(public injector: Injector) { 36 | super() 37 | } 38 | outSideService = new OutSideService() 39 | render() { 40 | return
    1111
    41 | } 42 | } 43 | 44 | const wrapper = mount(Foo) 45 | const vm = wrapper.vm 46 | expect(vm.injector).toStrictEqual(vm.outSideService.injector) 47 | }) 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "strict": true, 5 | "declaration": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "downlevelIteration": true, 9 | "resolveJsonModule": true, 10 | "jsx": "preserve", 11 | "lib": ["esnext", "dom"], 12 | "types": ["@abraham/reflection", "vue/jsx"], 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "verbatimModuleSyntax": true, 16 | "isolatedModules": true, 17 | "useDefineForClassFields": false, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "vue3-oop": ["src"] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import vueJsx from '@vue3-oop/plugin-vue-jsx' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | plugins: [vueJsx()], 6 | test: { 7 | environment: 'jsdom', 8 | coverage: { 9 | provider: 'istanbul', 10 | include: ['src/**/*.ts'], 11 | }, 12 | }, 13 | }) 14 | --------------------------------------------------------------------------------