diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 03bc9466580..fa181119ca9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,43 +38,43 @@ CMakeLists.txt @ilikethese @etkmao /framework/android/**/src/main/cpp/ @etkmao @ilikethese # framework: ios -/framework/ios/ @ozonelmy @wwwcg +/framework/ios/ @wwwcg @ruifanyuan # framework: voltron -/framework/voltron/ @lvfen @skindhu +/framework/voltron/ @lvfen @henryjin0511 # dom: others /dom/ @etkmao @ilikethese # renderer: native /renderer/native/android/ @siguangli @iPel -/renderer/native/ios/ @ozonelmy @wwwcg +/renderer/native/ios/ @wwwcg @ruifanyuan # renderer: tdf /renderer/tdf/ @vimerzhao /renderer/tdf/android/ @siguangli @iPel /renderer/tdf/android/**/src/main/cpp/ @ilikethese @etkmao -/renderer/tdf/ios/ @ozonelmy @wwwcg +/renderer/tdf/ios/ @wwwcg @ruifanyuan # renderer: voltron -/renderer/voltron/ @lvfen @skindhu +/renderer/voltron/ @lvfen @henryjin0511 # module: vfs /modules/vfs/ @etkmao @ilikethese /modules/vfs/android/ @siguangli @iPel /modules/vfs/android/**/src/main/cpp/ @etkmao @ilikethese -/modules/vfs/ios/ @ozonelmy @wwwcg -/modules/vfs/voltron/ @lvfen @skindhu +/modules/vfs/ios/ @wwwcg @ruifanyuan +/modules/vfs/voltron/ @lvfen @henryjin0511 # module: voltron -/modules/voltron/ @lvfen @skindhu +/modules/voltron/ @lvfen @henryjin0511 # module: android /modules/android/ @siguangli @iPel /modules/android/jni/ @etkmao @ilikethese # module: ios -/modules/ios/ @ozonelmy @wwwcg +/modules/ios/ @wwwcg @ruifanyuan # module: footstone /modules/footstone/ @etkmao @ilikethese @@ -88,8 +88,9 @@ CMakeLists.txt @ilikethese @etkmao # doc: example /framework/examples/android-demo/ @siguangli @iPel /framework/examples/android-demo/res/ @zealotchen0 -/framework/examples/ios-demo/ @ozonelmy @wwwcg +/framework/examples/ios-demo/ @wwwcg @ruifanyuan /framework/examples/ios-demo/res/ @zealotchen0 +/framework/examples/voltron-demo/ @henryjin0511 # doc: pages /*.md @zealotchen0 @@ -104,9 +105,9 @@ CMakeLists.txt @ilikethese @etkmao /gradle/ @siguangli @iPel # build: xcode -/HippySDK.xcworkspace/ @ozonelmy @wwwcg -/hippy.podspec @ozonelmy @wwwcg -/xcodeinitscript.sh @ozonelmy @wwwcg +/HippySDK.xcworkspace/ @wwwcg @ruifanyuan +/hippy.podspec @wwwcg @ruifanyuan +/xcodeinitscript.sh @wwwcg @ruifanyuan # build: config /buildconfig/ @ilikethese @etkmao @@ -121,3 +122,6 @@ CMakeLists.txt @ilikethese @etkmao /.markdownlintrc.json @zealotchen0 /PUBLISH.md @zealotchen0 /README.md @zealotchen0 + +# test +/tests/ios/ @wwwcg @ruifanyuan diff --git a/.github/workflows/3rd_prebuilt_v8.yml b/.github/workflows/3rd_prebuilt_v8.yml index 3dff5c44002..1bb1ae7a720 100644 --- a/.github/workflows/3rd_prebuilt_v8.yml +++ b/.github/workflows/3rd_prebuilt_v8.yml @@ -156,6 +156,7 @@ jobs: runs-on: [self-hosted, linux, shared] container: image: ghcr.io/${{ needs.context_in_lowercase.outputs.repository_owner }}/android-release:latest + options: --user root strategy: matrix: cpu: [arm, arm64, x86, x64] @@ -169,8 +170,13 @@ jobs: - cpu: x64 arch: x86_64 steps: + - name: Setup GN + run: | + git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git /usr/local/opt/depot_tools + export PATH=/usr/local/opt/depot_tools:$PATH - name: Fetch v8 run: | + export PATH=/usr/local/opt/depot_tools:$PATH fetch v8 cd v8 git checkout ${{ github.event.inputs.v8_revision }} @@ -187,6 +193,7 @@ jobs: - name: Sync third_party working-directory: ./v8 run: | + export PATH=/usr/local/opt/depot_tools:$PATH echo "target_os = ['android']" >> ../.gclient gclient sync -D - name: Prepare android_ndk @@ -213,10 +220,13 @@ jobs: - name: Generate ${{ matrix.arch }} working-directory: ./v8 run: | + export PATH=/usr/local/opt/depot_tools:$PATH gn gen out --args="target_os=\"android\" target_cpu=\"${{ matrix.cpu }}\" v8_target_cpu=\"${{ matrix.cpu }}\" android_ndk_root=\"${ANDROID_NDK_HOME}\" is_component_build=false v8_monolithic=true android32_ndk_api_level=21 android64_ndk_api_level=21 clang_use_chrome_plugins=false use_thin_lto=false use_custom_libcxx=false ${{ github.event.inputs.build_type == 'release' && 'is_debug=false is_official_build=true' || 'is_debug=true' }} ${{ github.event.inputs.build_args }}" - name: Compile ${{ matrix.arch }} working-directory: ./v8 run: | + export PATH=/usr/local/opt/depot_tools:$PATH + apt-get install -y ninja-build ninja -C out v8_monolith - name: Prepare package working-directory: ./v8/out diff --git a/.github/workflows/ios_build_tests.yml b/.github/workflows/ios_build_tests.yml index 6c28967abf7..d5c2f7fa5c5 100644 --- a/.github/workflows/ios_build_tests.yml +++ b/.github/workflows/ios_build_tests.yml @@ -38,12 +38,8 @@ jobs: uses: actions/checkout@v3 with: lfs: true - - name: Xcodegen - uses: xavierLowmiller/xcodegen-action@1.1.2 - with: - spec: framework/examples/ios-demo/project.yml - version: '2.32.0' - name: Demo working-directory: framework/examples/ios-demo run: | + pod install xcodebuild build -destination "generic/platform=iOS" -workspace "HippyDemo.xcworkspace" -scheme "HippyDemo" -configuration ${{matrix.type}} CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO diff --git a/.github/workflows/project_artifact_compare.yml b/.github/workflows/project_artifact_compare.yml index 2c0980704e5..bdb37c8c999 100644 --- a/.github/workflows/project_artifact_compare.yml +++ b/.github/workflows/project_artifact_compare.yml @@ -89,16 +89,10 @@ jobs: with: ref: ${{ matrix.ref }} lfs: true - - name: Xcodegen - uses: xavierLowmiller/xcodegen-action@1.1.2 - with: - spec: framework/examples/ios-demo/project.yml - version: '2.32.0' - name: Build if: ${{ matrix.ref }} run: | pushd framework/examples/ios-demo - xcodegen pod install xcodebuild build \ -destination 'generic/platform=iOS' \ diff --git a/.github/workflows/project_artifact_snapshot.yml b/.github/workflows/project_artifact_snapshot.yml new file mode 100644 index 00000000000..cf92eb0a3c8 --- /dev/null +++ b/.github/workflows/project_artifact_snapshot.yml @@ -0,0 +1,134 @@ +name: '[project] artifact snapshot' + +on: + workflow_dispatch: + inputs: + git_ref: + description: 'Git Ref' + type: string + required: true + version_name: + description: 'Version name' + type: string + required: true + registry_choice: + description: 'Registry choice' + type: choice + required: true + default: 'Both' + options: + - Default + - Github + - Both + is_release_for_android: + description: 'Release for Android' + type: boolean + default: true + required: false + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + context_in_lowercase: + if: github.event.inputs.is_release_for_android == 'true' + runs-on: ubuntu-latest + outputs: + repository_owner: ${{ steps.get_owner.outputs.lowercase }} + steps: + - name: Get repo owner(in lowercase) + id: get_owner + uses: ASzc/change-string-case-action@v2 + with: + string: ${{ github.repository_owner }} + + android_release: + if: github.event.inputs.is_release_for_android == 'true' + needs: context_in_lowercase + runs-on: ubuntu-latest + strategy: + matrix: + build_type: [Release] + include: + - build_type: Release + artifact_id: hippy-snapshot + container: + image: ghcr.io/${{ needs.context_in_lowercase.outputs.repository_owner }}/android-release:latest + steps: + - name: Checkout (${{ github.event.inputs.git_ref }}) + uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.git_ref }} + lfs: true + - name: ${{ matrix.build_type }} build + env: + SIGNING_KEY_ID: ${{ secrets.ANDROID_SIGNING_KEY_ID }} + SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} + SIGNING_SECRET_KEY: ${{ secrets.ANDROID_SIGNING_SECRET_KEY }} + run: | + ./gradlew assemble${{ matrix.build_type }} -PVERSION_NAME=${{ github.event.inputs.version_name }} -PPUBLISH_ARTIFACT_ID=${{ matrix.artifact_id }} -PINCLUDE_ABI_X86=true -PINCLUDE_ABI_X86_64=true + ./gradlew signMavenAarPublication + - name: Pre Archive artifacts + shell: bash + run: | + pip3 install -U cos-python-sdk-v5 + - name: Archive artifacts + working-directory: ./framework/android/build + shell: python3 {0} + run: | + from qcloud_cos import CosConfig + from qcloud_cos import CosS3Client + from urllib.parse import urlencode + import os + import tempfile + import zipfile + + artifacts = [("outputs/aar/android-sdk.aar", "hippy/android/${{ matrix.artifact_id }}/${{ github.event.inputs.version_name }}/android-sdk.aar")] + for path, dirs, files in os.walk("intermediates/merged_native_libs/%s/out/lib" % "${{ matrix.build_type }}".lower()): + if files: + with zipfile.ZipFile(tempfile.mkstemp()[1], "w", zipfile.ZIP_DEFLATED) as zip_file: + for file in files: + zip_file.write(os.path.join(path, file), file) + artifacts.append((zip_file.filename, "hippy/android/${{ matrix.artifact_id }}/${{ github.event.inputs.version_name }}/symbols/%s.zip" % os.path.basename(path))) + + metadata = {} + metadata["ci-name"] = "Github Action" + metadata["ci-id"] = "${{ github.run_id }}" + metadata["ci-url"] = "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + metadata["artifact-author"] = "${{ github.event.sender.login }}" + metadata["git-ref"] = "${{ github.event.inputs.git_ref }}" + + config = CosConfig(Region="${{ secrets.COS_REGION }}", SecretId="${{ secrets.TC_SECRET_ID }}", SecretKey="${{ secrets.TC_SECRET_KEY }}") + client = CosS3Client(config) + for artifact in artifacts: + print("Uploading %s" % artifact[0]) + response = client.upload_file( + Bucket="${{ secrets.COS_BUCKET_ARTIFACTS_STORE }}", + Key=artifact[1], + LocalFilePath=artifact[0], + Metadata={"x-cos-tagging": urlencode(metadata)} + ) + print("Archived %s" % artifact[1]) + - name: Publish to Github Packages + if: github.event.inputs.registry_choice == 'Both' || github.event.inputs.registry_choice == 'Github' + env: + SIGNING_KEY_ID: ${{ secrets.ANDROID_SIGNING_KEY_ID }} + SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} + SIGNING_SECRET_KEY: ${{ secrets.ANDROID_SIGNING_SECRET_KEY }} + MAVEN_USERNAME: ${{ secrets.GITHUB_ACTOR }} + MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + MAVEN_URL: https://maven.pkg.github.com/${{ github.repository }} + run: | + ./gradlew publish -PVERSION_NAME=${{ github.event.inputs.version_name }} -PPUBLISH_ARTIFACT_ID=${{ matrix.artifact_id }} -PINCLUDE_ABI_X86=true -PINCLUDE_ABI_X86_64=true + - name: Publish to OSSRH + if: github.event.inputs.registry_choice == 'Both' || github.event.inputs.registry_choice == 'Default' + env: + SIGNING_KEY_ID: ${{ secrets.ANDROID_SIGNING_KEY_ID }} + SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} + SIGNING_SECRET_KEY: ${{ secrets.ANDROID_SIGNING_SECRET_KEY }} + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + run: | + ./gradlew publish -PVERSION_NAME=${{ github.event.inputs.version_name }} -PPUBLISH_ARTIFACT_ID=${{ matrix.artifact_id }} -PINCLUDE_ABI_X86=true -PINCLUDE_ABI_X86_64=true + diff --git a/.gitignore b/.gitignore index 01bdcb1af9d..33cc2630ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,6 @@ framework/examples/android-demo/src/main/assets/ framework/examples/android-demo/libs/* framework/examples/android-demo/maven-auth.properties framework/examples/android-demo/.cxx/ -framework/examples/ios-demo/HippyDemo.xcodeproj framework/examples/ios-demo/HippyDemo.xcworkspace framework/examples/ios-demo/Pods/* framework/examples/ios-demo/Podfile.lock diff --git a/.husky/post-checkout b/.husky/post-checkout index c37815e2b56..ca7fcb40088 100755 --- a/.husky/post-checkout +++ b/.husky/post-checkout @@ -1,3 +1,3 @@ #!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; } +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-checkout' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } git lfs post-checkout "$@" diff --git a/.husky/post-commit b/.husky/post-commit index e5230c305f9..52b339cb3f4 100755 --- a/.husky/post-commit +++ b/.husky/post-commit @@ -1,3 +1,3 @@ #!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; } +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-commit' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } git lfs post-commit "$@" diff --git a/.husky/post-merge b/.husky/post-merge index c99b752a527..a912e667aa3 100755 --- a/.husky/post-merge +++ b/.husky/post-merge @@ -1,3 +1,3 @@ #!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; } +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-merge' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } git lfs post-merge "$@" diff --git a/.husky/pre-push b/.husky/pre-push index 216e91527e6..0f0089bc25d 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,3 +1,3 @@ #!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; } +command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } git lfs pre-push "$@" diff --git a/PUBLISH.md b/PUBLISH.md index 4b0071c8fa0..022d839df95 100644 --- a/PUBLISH.md +++ b/PUBLISH.md @@ -44,6 +44,12 @@ Android * [gradle.properties](https://github.com/Tencent/Hippy/blob/master/android/sdk/gradle.properties#L25) +修改安卓的abi配置,支持armeabi-v7a和arm64-v8a +``` +INCLUDE_ABI_ARMEABI_V7A=true +INCLUDE_ABI_ARM64_V8A=true +``` + ## 4. Update built-in packages and verify functionality The new front-end SDK is then compiled with @@ -93,8 +99,8 @@ git tag -a [VERSION] -m "version release xxx" Commit the code and prepare to publish the PR merge into the master branch. ```bash -git push # 提交代码 -git push --tags # 提交 tag +git push origin branch # 提交代码 +git push origin tag # 提交 tag ``` ## 6. Publish diff --git a/PrivacyInfo.xcprivacy b/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..8fe86d36970 --- /dev/null +++ b/PrivacyInfo.xcprivacy @@ -0,0 +1,31 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTrackingDomains + + NSPrivacyTracking + + + diff --git a/README.md b/README.md index a3a9a3990f3..bbd6ae55a27 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Hippy Group](https://img.shields.io/badge/group-Hippy-blue.svg) [![license](https://img.shields.io/badge/license-Apache%202-blue)](https://github.com/Tencent/Hippy/blob/master/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Tencent/Hippy/pulls) ![node](https://img.shields.io/badge/node-%3E%3D10.0.0-green.svg) [![Actions Status](https://github.com/Tencent/Hippy/workflows/build/badge.svg?branch=master)](https://github.com/Tencent/Hippy/actions) [![Codecov](https://img.shields.io/codecov/c/github/Tencent/Hippy)](https://codecov.io/gh/Tencent/Hippy) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/Tencent/Hippy)](https://github.com/Tencent/Hippy/releases) -[Homepage](//tencent.github.io/Hippy/) +[Homepage](https://openhippy.com) ## 💡 Introduction @@ -62,7 +62,7 @@ For iOS, we recommend to use iOS simulator when first try. However, you can chan 3. Choose a demo to build with `npm run buildexample [hippy-react-demo|hippy-vue-demo|hippy-vue-next-demo]`. -4. Install Xcodegen with `brew install xcodegen`, install CocoaPods with `brew install cocoapods`, install cmake with `brew install cmake`, then execute `xcodegen` command at `framework/examples/ios-demo` directory, which will create `HippyDemo.xcodeproj` and `HippyDemo.xcworkspace` files and install Cocoapods dependencies. +4. Install CocoaPods with `brew install cocoapods`, install cmake with `brew install cmake`, then execute `pod install` command at `framework/examples/ios-demo` directory, which will create `HippyDemo.xcworkspace` files and install Cocoapods dependencies. 5. Start the Xcode and build the iOS app with opening `framework/examples/ios-demo/HippyDemo.xcworkspace`. diff --git a/devtools/devtools-backend/CMakeLists.txt b/devtools/devtools-backend/CMakeLists.txt index 40ca1d96707..e51e0c82a84 100644 --- a/devtools/devtools-backend/CMakeLists.txt +++ b/devtools/devtools-backend/CMakeLists.txt @@ -54,13 +54,6 @@ GlobalPackages_Add(footstone) target_link_libraries(${PROJECT_NAME} PRIVATE footstone) # endregion -# region base64 -InfraPackage_Add(base64 - REMOTE "global_packages/base64/v0.5.0/git-repo.tgz" - LOCAL "third_party/base64") -target_link_libraries(${PROJECT_NAME} PRIVATE base64) -# endregion - # region asio InfraPackage_Add(asio REMOTE "devtools/backend/third_party/asio/1.22.1/asio.tar.gz" @@ -82,6 +75,8 @@ add_compile_definitions( # no exception "_WEBSOCKETPP_NO_EXCEPTIONS_") target_include_directories(${PROJECT_NAME} PRIVATE ${websocketpp_SOURCE_DIR}) +# websocketpp/base64/base64.hpp has some implicit conversion warnings currently +target_compile_options(${PROJECT_NAME} PRIVATE -Wno-error) # endregion # region nlohmann_json diff --git a/devtools/devtools-backend/include/api/adapter/devtools_screen_adapter.h b/devtools/devtools-backend/include/api/adapter/devtools_screen_adapter.h index 91bde420dab..d40b1985812 100644 --- a/devtools/devtools-backend/include/api/adapter/devtools_screen_adapter.h +++ b/devtools/devtools-backend/include/api/adapter/devtools_screen_adapter.h @@ -22,6 +22,7 @@ #include #include +#include namespace hippy::devtools { /** diff --git a/devtools/devtools-backend/include/api/adapter/devtools_tracing_adapter.h b/devtools/devtools-backend/include/api/adapter/devtools_tracing_adapter.h index 42763756cd9..33d436ae73e 100644 --- a/devtools/devtools-backend/include/api/adapter/devtools_tracing_adapter.h +++ b/devtools/devtools-backend/include/api/adapter/devtools_tracing_adapter.h @@ -21,6 +21,7 @@ #pragma once #include +#include namespace hippy::devtools { /** diff --git a/devtools/devtools-backend/include/module/domain/base_domain.h b/devtools/devtools-backend/include/module/domain/base_domain.h index 8fe09f96d48..65195948c27 100644 --- a/devtools/devtools-backend/include/module/domain/base_domain.h +++ b/devtools/devtools-backend/include/module/domain/base_domain.h @@ -62,8 +62,8 @@ class BaseDomain { /** * @brief handle domain.enable and domain.disable switch - * @param id - * @param method + * @param id frontend id + * @param method event name * @return if switch enable or disable return true, else return false */ bool HandleDomainSwitchEvent(int32_t id, const std::string& method); @@ -77,15 +77,15 @@ class BaseDomain { /** * @brief handle domain.method fail response - * @param id - * @param error_code - * @param error_msg + * @param id frontend id + * @param error_code error code + * @param error_msg error msg */ void ResponseErrorToFrontend(int32_t id, const int32_t error_code, const std::string& error_msg); /** * @brief send event to frontend - * @param inspect event that need to implement ToJsonString + * @param event event that need to implement ToJsonString */ void SendEventToFrontend(InspectEvent&& event); diff --git a/devtools/devtools-backend/include/module/domain_dispatch.h b/devtools/devtools-backend/include/module/domain_dispatch.h index d1361d9a4b2..17ea86996ed 100644 --- a/devtools/devtools-backend/include/module/domain_dispatch.h +++ b/devtools/devtools-backend/include/module/domain_dispatch.h @@ -56,7 +56,7 @@ class DomainDispatch : public std::enable_shared_from_this { /** * @brief receive msg from frontend - * @param params passing from frontend + * @param data passing from frontend */ bool ReceiveDataFromFrontend(const std::string& data); diff --git a/devtools/devtools-backend/include/module/util/parse_json_util.h b/devtools/devtools-backend/include/module/util/parse_json_util.h index 806408ad6bf..16375492873 100644 --- a/devtools/devtools-backend/include/module/util/parse_json_util.h +++ b/devtools/devtools-backend/include/module/util/parse_json_util.h @@ -31,7 +31,6 @@ class TdfParseJsonUtil { public: /** * @brief get the corresponding key value from the Json structure - * @tparam T * @param json Json data * @param key the key that needs to be retrieved * @param default_value default value if no key exists diff --git a/devtools/devtools-backend/include/tunnel/net_channel.h b/devtools/devtools-backend/include/tunnel/net_channel.h index cb3a747d09b..ec84c7ba764 100644 --- a/devtools/devtools-backend/include/tunnel/net_channel.h +++ b/devtools/devtools-backend/include/tunnel/net_channel.h @@ -20,6 +20,7 @@ #pragma once #include +#include #include "api/devtools_config.h" #include "api/devtools_define.h" @@ -42,7 +43,7 @@ class NetChannel { /** * @brief send data to frontend - * @param rsp_data + * @param rsp_data string */ virtual void Send(const std::string &rsp_data) = 0; diff --git a/devtools/devtools-backend/include/tunnel/ws/web_socket_channel.h b/devtools/devtools-backend/include/tunnel/ws/web_socket_channel.h index 9d10f5a00a9..1da2925e013 100644 --- a/devtools/devtools-backend/include/tunnel/ws/web_socket_channel.h +++ b/devtools/devtools-backend/include/tunnel/ws/web_socket_channel.h @@ -31,7 +31,9 @@ #pragma clang diagnostic ignored "-Wconversion" #pragma clang diagnostic ignored "-Wunknown-warning-option" #pragma clang diagnostic ignored "-Wextra" +#pragma clang diagnostic ignored "-Wdocumentation" #define ASIO_STANDALONE +#include "asio.hpp" #include "websocketpp/client.hpp" #include "websocketpp/config/asio_no_tls_client.hpp" #pragma clang diagnostic pop diff --git a/devtools/devtools-backend/src/module/domain/network_domain.cc b/devtools/devtools-backend/src/module/domain/network_domain.cc index 7c2a38de9ae..51a4d793c32 100644 --- a/devtools/devtools-backend/src/module/domain/network_domain.cc +++ b/devtools/devtools-backend/src/module/domain/network_domain.cc @@ -23,7 +23,7 @@ #include "footstone/logging.h" #include "module/domain_register.h" #include "nlohmann/json.hpp" -#include "libbase64.h" +#include "websocketpp/base64/base64.hpp" namespace hippy::devtools { constexpr char kResponseBody[] = "body"; @@ -53,9 +53,7 @@ void NetworkDomain::GetResponseBody(const NetworkResponseBodyRequest& request) { auto body_data = response.GetBodyData(); response_json[kResponseBase64Encoded] = is_encode_base64; if (is_encode_base64) { - size_t out_len = 4 * ((body_data.length() + 2) / 3); - std::string encode_body(out_len, '\0'); - base64_encode(body_data.c_str(), body_data.length(), encode_body.data(), &out_len, 0); + std::string encode_body = websocketpp::base64_encode(body_data); response_json[kResponseBody] = encode_body; } else { response_json[kResponseBody] = body_data; diff --git a/devtools/devtools-backend/src/module/domain/tdf_runtime_domain.cc b/devtools/devtools-backend/src/module/domain/tdf_runtime_domain.cc index 5f7d7e2f4ed..6643725d888 100644 --- a/devtools/devtools-backend/src/module/domain/tdf_runtime_domain.cc +++ b/devtools/devtools-backend/src/module/domain/tdf_runtime_domain.cc @@ -24,7 +24,9 @@ #include "module/domain_register.h" namespace hippy::devtools { +#if defined(JS_V8) && !defined(V8_WITHOUT_INSPECTOR) constexpr const char kCmdChromeSocketClose[] = "chrome_socket_closed"; +#endif std::string TdfRuntimeDomain::GetDomainName() { return kFrontendKeyDomainNameTDFRuntime; } diff --git a/devtools/devtools-backend/src/tunnel/tcp/socket.cc b/devtools/devtools-backend/src/tunnel/tcp/socket.cc index 827f4bd571c..c70768105f2 100644 --- a/devtools/devtools-backend/src/tunnel/tcp/socket.cc +++ b/devtools/devtools-backend/src/tunnel/tcp/socket.cc @@ -1,3 +1,23 @@ +/* + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2017-2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /* * socket.c * @@ -235,7 +255,7 @@ int socket_connect_unix(const char *filename) { struct timeval timeout; timeout.tv_sec = CONNECT_TIMEOUT / 1000; - timeout.tv_usec = (CONNECT_TIMEOUT - (timeout.tv_sec * 1000)) * 1000; + timeout.tv_usec = (int32_t)(CONNECT_TIMEOUT - (timeout.tv_sec * 1000)) * 1000; if (select(sfd + 1, NULL, &fds, NULL, &timeout) == 1) { int so_error; socklen_t len = sizeof(so_error); @@ -392,7 +412,7 @@ int socket_connect_addr(struct sockaddr *addr, uint16_t port) { struct timeval timeout; timeout.tv_sec = CONNECT_TIMEOUT / 1000; - timeout.tv_usec = (CONNECT_TIMEOUT - (timeout.tv_sec * 1000)) * 1000; + timeout.tv_usec = (int32_t)(CONNECT_TIMEOUT - (timeout.tv_sec * 1000)) * 1000; if (select(sfd + 1, NULL, &fds, NULL, &timeout) == 1) { int so_error; socklen_t len = sizeof(so_error); @@ -472,7 +492,7 @@ int socket_connect(const char *addr, uint16_t port) { hints.ai_flags = 0; hints.ai_protocol = IPPROTO_TCP; - sprintf(portstr, "%d", port); + snprintf(portstr, sizeof(portstr), "%d", port); res = getaddrinfo(addr, portstr, &hints, &result); if (res != 0) { @@ -524,7 +544,7 @@ int socket_connect(const char *addr, uint16_t port) { struct timeval timeout; timeout.tv_sec = CONNECT_TIMEOUT / 1000; - timeout.tv_usec = (CONNECT_TIMEOUT - (timeout.tv_sec * 1000)) * 1000; + timeout.tv_usec = (int32_t)(CONNECT_TIMEOUT - (timeout.tv_sec * 1000)) * 1000; if (select(sfd + 1, NULL, &fds, NULL, &timeout) == 1) { int so_error; socklen_t len = sizeof(so_error); @@ -584,7 +604,7 @@ int socket_check_fd(int fd, fd_mode fdm, unsigned int timeout) { do { if (timeout > 0) { to.tv_sec = (time_t) (timeout / 1000); - to.tv_usec = (time_t) ((timeout - static_cast(to.tv_sec) * 1000) * 1000); + to.tv_usec = (int32_t)((timeout - static_cast(to.tv_sec) * 1000) * 1000); pto = &to; } else { pto = NULL; diff --git a/devtools/devtools-integration/native/include/devtools/devtools_utils.h b/devtools/devtools-integration/native/include/devtools/devtools_utils.h index 384a7c06e58..cce16310167 100644 --- a/devtools/devtools-integration/native/include/devtools/devtools_utils.h +++ b/devtools/devtools-integration/native/include/devtools/devtools_utils.h @@ -52,6 +52,8 @@ class DevToolsUtil { static void PostDomTask(const std::weak_ptr& weak_dom_manager, std::function func); + static bool ShouldAvoidPostDomManagerTask(const std::string& event_name); + private: static std::shared_ptr GetHitNode(const std::shared_ptr& root_node, const std::shared_ptr& node, double x, double y); static bool IsLocationHitNode(const std::shared_ptr& root_node, const std::shared_ptr& dom_node, double x, double y); diff --git a/devtools/devtools-integration/native/src/devtools_utils.cc b/devtools/devtools-integration/native/src/devtools_utils.cc index 058e118ea11..8a49d3ff605 100644 --- a/devtools/devtools-integration/native/src/devtools_utils.cc +++ b/devtools/devtools-integration/native/src/devtools_utils.cc @@ -413,4 +413,13 @@ void DevToolsUtil::PostDomTask(const std::weak_ptr& weak_dom_manager dom_manager->PostTask(hippy::dom::Scene(std::move(ops))); } } + +/** + * Specific methods like getLocationOnScreen should wait in the dom manager task runner. To avoid a deadlock, the + * callback must not be posted in the same task runner. + */ +bool DevToolsUtil::ShouldAvoidPostDomManagerTask(const std::string& event_name) { + return event_name == kGetLocationOnScreen; +} + } // namespace hippy::devtools diff --git a/docs/README.md b/docs/README.md index c820bf0815a..fd2dda610ec 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,12 +1,18 @@ # Hippy 简介 -Hippy 是 TDF 腾讯端框架(Tencent Device-oriented Framework)下的开源跨平台应用开发解决方案。 +版本:3.3.0 -Hippy 可以理解为一个精简版的浏览器,从底层做了大量工作,抹平了 iOS 和 Android 双端差异,提供了接近 Web 的开发体验,目前上层支持了 React 和 Vue 两套界面框架,前端开发人员可以通过它,将前端代码转换为终端的原生指令,进行原生终端 App 的开发。 +更新时间:2024-6-26 -同时,Hippy 从底层进行了大量优化,在启动速度、渲染性能、动画速度、内存占用、包体积等方面都提供了业内顶尖的性能表现。 +SDK介绍:Hippy 是 TDF 腾讯端框架(Tencent Device-oriented Framework)下的开源跨平台应用开发解决方案。Hippy 可以理解为一个精简版的浏览器,从底层做了大量工作,抹平了 iOS 和 Android 双端差异,提供了接近 Web 的开发体验,目前上层支持了 React 和 Vue 两套界面框架,前端开发人员可以通过它,将前端代码转换为终端的原生指令,进行原生终端 App 的开发。同时,Hippy 从底层进行了大量优化,在启动速度、渲染性能、动画速度、内存占用、包体积等方面都提供了业内顶尖的性能表现。 -在Hippy 3.0版本,我们对 Hippy 架构做了一次比较大的重构, 采用 Driver,Dom Manager,Renderer 分层解耦的设计理念,其设计目标就是希望框架在未来具有很好的可扩展性,以复用的 DOM 管理、排版布局为核心连接上层 Driver 和下层 Renderer,同时支持不同 Driver 和 Renderer 的接入和自由切换。 +更新日志:[Change log](https://github.com/Tencent/Hippy/releases) + +服务提供方:深圳市腾讯计算机系统有限公司 + +[接入指引](development/react-vue-integration-guidelines.md) + +[Hippy SDK隐私保护指引](development/privacy.md) ## 和 Web 接近的开发体验 @@ -53,7 +59,7 @@ Hippy 的包体积在业内也是非常具有竞争力的。 ## 可扩展的架构设计 -
+在Hippy 3.0版本,我们对 Hippy 架构做了一次比较大的重构, 采用 Driver,Dom Manager,Renderer 分层解耦的设计理念,其设计目标就是希望框架在未来具有很好的可扩展性,以复用的 DOM 管理、排版布局为核心连接上层 Driver 和下层 Renderer,同时支持不同 Driver 和 Renderer 的接入和自由切换。
3.0架构 ### 驱动层 diff --git a/docs/api/hippy-react/components.md b/docs/api/hippy-react/components.md index ff3a957f12a..146f66f9335 100644 --- a/docs/api/hippy-react/components.md +++ b/docs/api/hippy-react/components.md @@ -53,7 +53,7 @@ import icon from './qb_icon_new.png'; | onLayout | 当元素挂载或者布局改变的时候调用,参数为: `nativeEvent: { layout: { x, y, width, height } }`,其中 `x` 和 `y` 为相对父元素的坐标位置 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | onLoad | 加载成功完成时调用此回调函数。 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | onLoadStart | 加载开始时调用。 例如, `onLoadStart={() => this.setState({ loading: true })}` | `Function` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | -| onLoadEnd | 加载结束后,不论成功还是失败,调用此回调函数。 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | +| onLoadEnd | 加载结束后,不论成功还是失败,调用此回调函数。参数为:`nativeEvent: { success: number, width: number, height: number}` | `Function` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | resizeMode | 决定当组件尺寸和图片尺寸不成比例的时候如何调整图片的大小。`注意:hippy-react-web、Web-Renderer 不支持 repeat` | `enum (cover, contain, stretch, repeat, center)` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | onError | 当加载错误的时候调用此回调函数,参数为 `nativeEvent: { error }` | `Function` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | onProgress | 在加载过程中不断调用,参数为 `nativeEvent: { loaded: number, total: number }`, `loaded` 表示加载中的图片大小, `total` 表示图片总大小 | `Function` | `iOS、Voltron` | @@ -97,13 +97,12 @@ import icon from './qb_icon_new.png'; | 参数 | 描述 | 类型 | 支持平台 | | --------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | -------- | | bounces | 是否开启回弹效果,默认 `true`, Android `2.14.1` 版本后支持该属性,老版本使用 `overScrollEnabled` | `boolean` | `Android`、`iOS`、`Voltron` | -| overScrollEnabled | 是否开启回弹效果,默认 `true`,3.0 版本后即将废弃 | `boolean` | `Android、Voltron` | | getRowKey | 指定一个函数,在其中返回对应条目的 Key 值,详见 [React 官文](//reactjs.org/docs/lists-and-keys.html) | `(index: number) => any` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | getRowStyle | 设置 `ListViewItem` 容器的样式。当设置了 `horizontal=true` 启用横向 `ListView` 时,需显式设置 `ListViewItem` 宽度 | `(index: number) => styleObject` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | getHeaderStyle | 设置 `PullHeader` 容器的样式。当设置了 `horizontal=true` 启用横向 `ListView` 时,需显式设置 `PullHeader` 宽度。`最低支持版本2.14.1` | `() => styleObject` | `Android、iOS、Voltron` | | getFooterStyle | 设置 `PullFooter` 容器的样式。当设置了 `horizontal=true` 启用横向 `ListView` 时,需显式设置 `PullFooter` 宽度。`最低支持版本2.14.1` | `() => styleObject` | `Android、iOS、Voltron` | | getRowType | 指定一个函数,在其中返回对应条目的类型(返回Number类型的自然数,默认是0),List 将对同类型条目进行复用,所以合理的类型拆分,可以很好地提升 List 性能。`注意:同一 type 的 item 组件由于复用可能不会走完整组件创建生命周期` | `(index: number) => number` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | -| horizontal | 指定 `ListView` 是否采用横向布局。`default: undefined` 纵向布局,Android `2.14.1` 版本后可设置 `false` 显式固定纵向布局;iOS 暂不支持横向 `ListView`| `boolean \| undefined` | `Android、hippy-react-web、Voltron` | +| horizontal | 指定 `ListView` 是否采用横向布局。`default: undefined` 纵向布局,Android `2.14.1` 版本后可设置 `false` 显式固定纵向布局;iOS 从 `3.0` 开始支持横向 `ListView`| `boolean \| undefined` | `Android、iOS、hippy-react-web、Voltron` | | initialListSize | 指定在组件刚挂载的时候渲染多少行数据。用这个属性来确保首屏显示合适数量的数据,而不是花费太多帧时间逐步显示出来。 | `number` | `Android、iOS、Web-Renderer、Voltron` | | initialContentOffset | 初始位移值。在列表初始化时即可指定滚动距离,避免初始化后再通过 scrollTo 系列方法产生的闪动。Android 在 `2.8.0` 版本后支持 | `number` | `Android、iOS、Web-Renderer、Voltron` | | onAppear | 当有`ListViewItem`滑动进入屏幕时(曝光)触发,入参返回曝光的`ListViewItem`对应索引值。 | `(index) => void` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | @@ -176,13 +175,14 @@ import icon from './qb_icon_new.png'; | 参数 | 描述 | 类型 | 支持平台 | | --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -------- | -| animated | 弹出时是否需要带动画 | `boolean` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | animationType | 动画效果 | `enum (none, slide, fade, slide_fade)` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | supportedOrientations | 支持屏幕翻转方向 | `enum (portrait, portrait-upside-down, landscape, landscape-left, landscape-right)[]` | `iOS` | | immersionStatusBar | 是否是沉浸式状态栏。`default: false` | `boolean` | `Android、Voltron` | | darkStatusBarText | 是否是亮色主体文字,默认字体是黑色的,改成 true 后会认为 Modal 背景为暗色调,字体就会改成白色。 | `boolean` | `Android、iOS、Voltron` | +| autoHideStatusBar | 是否在`Modal`显示时自动隐藏状态栏。Android 中仅 api28 以上生效。 `default: false` | `boolean` | `Android` | +| autoHideNavigationBar | 是否在`Modal`显示时自动隐藏导航栏。 `default: false` | `boolean` | `Android` | | onShow | 在`Modal`显示时会执行此回调函数。 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | -| onOrientationChange | 屏幕旋转方向改变时执行会回调 | `Function` | `Android、iOS` | +| onOrientationChange | 屏幕旋转方向改变时执行会回调,返回当前屏幕显示方向 `{ orientation: portrait|landscape }` | `Function` | `Android、iOS` | | onRequestClose | 在 `Modal` 请求关闭时会执行此回调函数,一般时在 Android 系统里按下硬件返回按钮时触发,一般要在里面处理关闭弹窗。 | `Function` | `Android、hippy-react-web、Voltron` | | transparent | 背景是否是透明的。`default: true` | `boolean` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | visible | 是否显示。`default: true` | `boolean` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | @@ -327,6 +327,8 @@ import icon from './qb_icon_new.png'; | keyboardType | 决定弹出的何种软键盘的。 注意,`password`仅在属性 `multiline=false` 单行文本框时生效。 | `enum (default, numeric, password, email, phone-pad)` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | maxLength | 限制文本框中最多的字符数。使用这个属性而不用JS 逻辑去实现,可以避免闪烁的现象。 | `number` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | multiline | 如果为 `true` ,文本框中可以输入多行文字。 由于终端特性。 | `boolean` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | +| lineSpacingExtra | 多行显示时每行文字的额外行高,如果style里设置的lineHeight属性该属性设置无效 | `number` | `Android` | +| lineSpacingMultiplier | 多行显示时每行文字的行高乘积系数,如果style里设置的lineHeight属性该属性设置无效 | `number` | `Android` | | numberOfLines | 设置 `TextInput` 最大显示行数,如果 `TextInput` 没有显式设置高度,会根据 `numberOfLines` 来计算高度撑开。在使用的时候必需同时设置 `multiline` 参数为 `true`。 | `number` | `Android、hippy-react-web、Web-Renderer、Voltron` | | onBlur | 当文本框失去焦点的时候调用此回调函数。 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | onFocus | 当文本框获得焦点的时候调用此回调函数。 | `Function` | `Android、iOS、Voltron` | @@ -339,8 +341,7 @@ import icon from './qb_icon_new.png'; | onSelectionChange | 当输入框选择文字的范围被改变时调用。返回参数的样式如 `nativeEvent: { selection: { start, end } }`。 | `Function` | `Android、iOS、Web-Renderer、Voltron` | | placeholder | 如果没有任何文字输入,会显示此字符串。 | `string` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | placeholderTextColor | 占位字符串显示的文字颜色。(也可设置为 Style 属性)`最低支持版本2.13.4` | [`color`](api/style/color.md) | `Android、iOS、Web-Renderer、Voltron` | -| returnKeyType | 指定软键盘的回车键显示的样式。 | `enum (done, go, next, search, send)` | `Android、iOS、Web-Renderer、Voltron` | -| underlineColorAndroid | `TextInput` 下底线的颜色。 可以设置为 'transparent' 来去掉下底线。(也可设置为 Style 属性) | [`color`](api/style/color.md) | `Android` | +| returnKeyType | 指定软键盘的回车键显示的样式。(其中部分样式仅`multiline=false`时有效) | `enum (done, go, next, search, send)` | `Android、iOS、Web-Renderer、Voltron` | | value | 指定 `TextInput` 组件的值。 | `string` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | autoFocus | 组件渲染时自动获得焦点。 | `boolean` | `Android、iOS、hippy-react-web、Web-Renderer、Voltron` | | breakStrategy* | 设置Android API 23及以上系统的文本折行策略。`default: simple` | `enum(simple, high_quality, balanced)` | `Android(版本 2.14.2以上)` | @@ -517,15 +518,13 @@ import icon from './qb_icon_new.png'; | interItemSpacing | item 间的垂直间距 | `number` | `Android、iOS、Voltron` | | contentInset | 内容缩进 ,默认值 `{ top:0, left:0, bottom:0, right:0 }` | `Object` | `Android、iOS、Voltron` | | renderItem | 这里的入参是当前 item 的 index,在这里可以凭借 index 获取到瀑布流一个具体单元格的数据,从而决定如何渲染这个单元格。 | `(index: number) => React.ReactElement` | `Android、iOS、Voltron` | -| renderBanner | 如何渲染 Banner。 | `() => React.ReactElement` | `iOS、Voltron` +| renderBanner | 如何渲染 Banner。 | `() => React.ReactElement` | `Android、iOS、Voltron` | getItemStyle | 设置`WaterfallItem`容器的样式。 | `(index: number) => styleObject` | `Android、iOS、Voltron` | | getItemType | 指定一个函数,在其中返回对应条目的类型(返回Number类型的自然数,默认是0),List 将对同类型条目进行复用,所以合理的类型拆分,可以很好地提升list 性能。 | `(index: number) => number` | `Android、iOS、Voltron` | | getItemKey | 指定一个函数,在其中返回对应条目的 Key 值,详见 [React 官文](//reactjs.org/docs/lists-and-keys.html) | `(index: number) => any` | `Android、iOS、Voltron` | | preloadItemNumber | 滑动到瀑布流底部前提前预加载的 item 数量 | `number` | `Android、iOS、Voltron` | | onEndReached | 当所有的数据都已经渲染过,并且列表被滚动到最后一条时,将触发 `onEndReached` 回调。 | `Function` | `Android、iOS、Voltron` | -| containPullHeader | 是否包含`PullHeader`组件,默认 `false` ;`Android` 暂不支持,可暂时用 `RefreshWrapper` 组件替代 | `boolean` | `iOS、Voltron` | -| renderPullHeader | 如何渲染 `PullHeader`,此时 `containPullHeader` 默认设置成 `true` | `() => React.ReactElement` | `iOS、Voltron` | -| containPullFooter | 是否包含`PullFooter`组件,默认 `false` | `boolean` | `Android、iOS、Voltron` | +| renderPullHeader | 如何渲染 `PullHeader`,此时 `containPullHeader` 默认设置成 `true` | `() => React.ReactElement` | `Android、iOS、Voltron` | | renderPullFooter | 如何渲染 `PullFooter`,此时 `containPullFooter` 默认设置成 `true` | `() => React.ReactElement` | `Android、iOS、Voltron` | | onScroll | 当触发 `WaterFall` 的滑动事件时回调。`startEdgePos`表示距离 List 顶部边缘滚动偏移量;`endEdgePos`表示距离 List 底部边缘滚动偏移量;`firstVisibleRowIndex`表示当前可见区域内第一个元素的索引;`lastVisibleRowIndex`表示当前可见区域内最后一个元素的索引;`visibleRowFrames`表示当前可见区域内所有 item 的信息(x,y,width,height) | `nativeEvent: { startEdgePos: number, endEdgePos: number, firstVisibleRowIndex: number, lastVisibleRowIndex: number, visibleRowFrames: Object[] }` | `Android、iOS、Voltron` diff --git a/docs/api/hippy-react/modules.md b/docs/api/hippy-react/modules.md index 1c26bfff211..67acc1194d0 100644 --- a/docs/api/hippy-react/modules.md +++ b/docs/api/hippy-react/modules.md @@ -159,7 +159,7 @@ AsyncStorage 是一个简单的、异步的、持久化的 Key-Value 存储系 ### AsyncStorage.multiGet -`(key: string[]) => Promise<[key: string, value: value][]>` 一次性用多个 key 值的数组去批量请求缓存数据,返回值将在回调函数以键值对的二维数组形式返回。 +`(key: string[]) => Promise<[key: string, value: string][]>` 一次性用多个 key 值的数组去批量请求缓存数据,返回值将在回调函数以键值对的二维数组形式返回。 > - key: string[] - 需要获取值的目标 key 数组 @@ -171,9 +171,9 @@ AsyncStorage 是一个简单的、异步的、持久化的 Key-Value 存储系 ### AsyncStorage.multiSet -`(keyValuePairs: [key: string, value: value][]) => void` 调用这个函数可以批量存储键值对对象。 +`(keyValuePairs: [key: string, value: string][]) => void` 调用这个函数可以批量存储键值对对象。 -> - keyValuePairs: [key: string, value: value][] - 需要设置的储键值二维数组 +> - keyValuePairs: [key: string, value: string][] - 需要设置的储键值二维数组 ### AsyncStorage.removeItem @@ -256,7 +256,7 @@ AsyncStorage 是一个简单的、异步的、持久化的 Key-Value 存储系 `(target: 'window' | 'screen') => { height: number, width: number, scale: number, statusBarHeight, navigatorBarHeight }` Hippy Root View 尺寸或者屏幕尺寸。 > - target: 'window' | 'screen' - 指定丈量 Hippy Root View 或者屏幕尺寸。 -> - Android 特别说明:因为历史遗留问题,screen 下的 statusBarHeight 是按实际像素算的,window 下经过修正已经是 dp 单位。 +> - Android 特别说明:因为历史遗留问题,2.x 及以下版本的 screen 下的 statusBarHeight 是按物理像素算的,window 下经过修正已经是 dp 单位;3.0 及以上版本 screen 和 window 均为 dp 单位。 > - navigatorBarHeight: Android 底部 navigatorBar 高度;最低支持版本 2.3.4 --- diff --git a/docs/api/hippy-vue/components.md b/docs/api/hippy-vue/components.md index 55da7fdfd18..2798dc62468 100644 --- a/docs/api/hippy-vue/components.md +++ b/docs/api/hippy-vue/components.md @@ -186,7 +186,7 @@ | layout | 当元素挂载或者布局改变的时候调用,参数为: `nativeEvent: { layout: { x, y, width, height } }`,其中 `x` 和 `y` 为相对父元素的坐标位置 | `Function` | `Android、iOS、Web-Renderer、Voltron` | | load | 加载成功完成时调用此回调函数。 | `Function` | `Android、iOS、Web-Renderer、Voltron` | | loadStart | 加载开始时调用。 | `Function` | `Android、iOS、Web-Renderer、Voltron` | -| loadEnd | 加载结束后,不论成功还是失败,调用此回调函数。 | `Function` | `Android、iOS、Web-Renderer、Voltron` | +| loadEnd | 加载结束后,不论成功还是失败,调用此回调函数。参数为:`nativeEvent: { success: number, width: number, height: number}` | `Function` | `Android、iOS、Web-Renderer、Voltron` | | error | 当加载错误的时候调用此回调函数。| `Function` | `Android、iOS、Web-Renderer、Voltron` | | progress | 在加载过程中不断调用,参数为 `nativeEvent: { loaded: number, total: number }`, `loaded` 表示加载中的图片大小, `total` 表示图片总大小 | `Function` | `iOS、Voltron` | | touchstart | 触屏开始事件,最低支持版本 2.6.2,参数为 `evt: { touches: [{ clientX: number, clientY: number }] }`,`clientX` 和 `clientY` 分别表示点击在屏幕内的绝对位置 | `Function` | `Android、iOS、Web-Renderer、Voltron` | @@ -257,8 +257,7 @@ | numberOfLines | 设置 `input` 最大显示行数,如果 `input` 没有显式设置高度,会根据 `numberOfLines` 来计算高度撑开。在使用的时候必需同时设置 `multiline` 参数为 `true`。 | `number` | `Android、Voltron、Web-Renderer` | | placeholder | 如果没有任何文字输入,会显示此字符串。 | `string` | `Android、iOS、Web-Renderer、Voltron` | | placeholder-text-color | 占位字符串显示的文字颜色。(也可设置为 Style 属性) `最低支持版本2.13.4` | [`color`](api/style/color.md) | `Android、iOS、Web-Renderer、Voltron` | -| underline-color-android | `input` 下底线的颜色。 可以设置为 'transparent' 来去掉下底线。(也可设置为 Style 属性) `最低支持版本2.13.4` | [`color`](api/style/color.md) | `Android` | -| returnKeyType | 指定软键盘的回车键显示的样式。 | `enum(done, go, next, search, send)` | `Android、iOS、Web-Renderer` | +| returnKeyType | 指定软键盘的回车键显示的样式。(其中部分样式仅对单行文本组件有效) | `enum(done, go, next, search, send)` | `Android、iOS、Web-Renderer` | | value | 指定 `input` 组件的值。 | `string` | `Android、iOS、Web-Renderer、Voltron` | | break-strategy* | 设置Android API 23及以上系统的文本换行策略。`default: simple` | `enum(simple, high_quality, balanced)` | `Android(版本 2.14.2以上)` | @@ -344,14 +343,13 @@ Hippy 的重点功能,高性能的可复用列表组件,在终端侧会被 | --------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | -------- | | horizontal | 指定 `ul` 是否采用横向布局。`default: undefined` 纵向布局,Android `2.14.1` 版本后可设置 `false` 显式固定纵向布局;iOS 暂不支持横向 `ul` | `boolean \| undefined` | `Android、Voltron` | | initialContentOffset | 初始位移值。在列表初始化时即可指定滚动距离,避免初始化后再通过 scrollTo 系列方法产生的闪动。Android 在 `2.8.0` 版本后支持 | `number` | `Android、iOS、Web-Renderer、Voltron` | -| bounces | 是否开启回弹效果,默认 `true` | `boolean` | `iOS、Voltron` | -| overScrollEnabled | 是否开启回弹效果,默认 `true` | `boolean` | `Android、Voltron` | -| rowShouldSticky | 设置 `ul` 是否需要开启悬停效果能力,与 `li` 的 `sticky` 配合使用。 `default: false` | `boolean` | `Android、iOS、Web-Renderer、Voltron` +| bounces | 是否开启回弹效果,默认 `true`, Android `2.14.1` 版本后支持该属性,老版本使用 `overScrollEnabled` | `boolean` | `Android、iOS、Voltron` | +| rowShouldSticky | 设置 `ul` 是否需要开启悬停效果能力,与 `li` 的 `sticky` 配合使用。 `default: false` | `boolean` | `Android、iOS、Web-Renderer、Voltron`| | scrollEnabled | 滑动是否开启。`default: true` | `boolean` | `Android、iOS、Web-Renderer、Voltron` | | scrollEventThrottle | 指定滑动事件的回调频率,传入数值指定了多少毫秒(ms)组件会调用一次 `onScroll` 回调事件,默认 200ms | `number` | `Android、iOS、Web-Renderer、Voltron` | | showScrollIndicator | 是否显示滚动条。`default: true` | `boolean` | `iOS、Voltron` | | preloadItemNumber | 指定当列表滚动至倒数第几行时触发 `endReached` 回调。 | `number` | `Android、iOS、Web-Renderer、Voltron` | -| exposureEventEnabled | Android 曝光能力启用开关,如果要使用 `appear`、`disappear` 相关事件,Android 需要设置该开关(iOS无需设置), `default: true` | `boolean` | `Android、Voltron` +| exposureEventEnabled | Android 曝光能力启用开关,如果要使用 `appear`、`disappear` 相关事件,Android 需要设置该开关(iOS无需设置), `default: true` | `boolean` | `Android、Voltron`| | endReached | 当所有的数据都已经渲染过,并且列表被滚动到最后一条时,将触发 `endReached` 回调。 | `Function` | `Android、iOS、Web-Renderer、Voltron` | | editable | 是否可编辑,开启侧滑删除时需要设置为 `true`。`最低支持版本2.9.0` | `boolean` | `iOS` | | delText | 侧滑删除文本。`最低支持版本2.9.0` | `string` | `iOS` | diff --git a/docs/api/hippy-vue/external-components.md b/docs/api/hippy-vue/external-components.md index 8c175c52baa..1e47c97ee04 100644 --- a/docs/api/hippy-vue/external-components.md +++ b/docs/api/hippy-vue/external-components.md @@ -162,6 +162,9 @@ export default { | supportedOrientations | 支持屏幕翻转方向 | `enum(portrait, portrait-upside-down, landscape, landscape-left, landscape-right)[]` | `iOS` | | immersionStatusBar | 是否是沉浸式状态栏。`default: true` | `boolean` | `Android、Voltron` | | darkStatusBarText | 是否是亮色主体文字,默认字体是黑色的,改成 true 后会认为 Modal 背景为暗色调,字体就会改成白色。 | `boolean` | `Android、iOS、Voltron` | +| autoHideStatusBar | 是否在`Modal`显示时自动隐藏状态栏。Android 中仅 api28 以上生效。 `default: false` | `boolean` | `Android` | +| autoHideNavigationBar | 是否在`Modal`显示时自动隐藏导航栏。 `default: false` | `boolean` | `Android` | + | transparent | 背景是否是透明的。`default: true` | `boolean` | `Android、iOS、Web-Renderer、Voltron` | ## 事件 diff --git a/docs/api/hippy-vue/style.md b/docs/api/hippy-vue/style.md index c16ecf1ca14..93758bb6657 100644 --- a/docs/api/hippy-vue/style.md +++ b/docs/api/hippy-vue/style.md @@ -19,6 +19,15 @@ HippyVue 提供了 `beforeLoadStyle` 的 Vue options 勾子函数,供开发者 }); ``` +beforeLoadStyle 默认是对全局节点生效的,针对不需要执行 beforeLoadStyle 的节点,可以对节点设置属性 beforeLoadStyleDisabled。 +对于所有节点(如 div、p、button等均可使用) + +```js +
+
+``` + # CSS 选择器 目前已经实现了基本的 `Universal`、`Type`、`ID`、`Class`、`Grouping` 选择器,而且可以支持除兄弟组合器以外的基本组合关系。 diff --git a/docs/api/hippy-vue/vue-native.md b/docs/api/hippy-vue/vue-native.md index 6206b60cb25..da43ca625b4 100644 --- a/docs/api/hippy-vue/vue-native.md +++ b/docs/api/hippy-vue/vue-native.md @@ -120,7 +120,7 @@ Vue.Native.AsyncStorage.getItem('itemKey'); ### AsyncStorage.multiGet -`(key: string[]) => Promise<[key: string, value: value][]>` 一次性用多个 key 值的数组去批量请求缓存数据,返回值将在回调函数以键值对的二维数组形式返回。 +`(key: string[]) => Promise<[key: string, value: string][]>` 一次性用多个 key 值的数组去批量请求缓存数据,返回值将在回调函数以键值对的二维数组形式返回。 > * key: string[] - 需要获取值的目标 key 数组 @@ -132,9 +132,9 @@ Vue.Native.AsyncStorage.getItem('itemKey'); ### AsyncStorage.multiSet -`(keyValuePairs: [key: string, value: value][]) => void` 调用这个函数可以批量存储键值对对象。 +`(keyValuePairs: [key: string, value: string][]) => void` 调用这个函数可以批量存储键值对对象。 -> * keyValuePairs: [key: string, value: value][] - 需要设置的储键值二维数组 +> * keyValuePairs: [key: string, value: string][] - 需要设置的储键值二维数组 ### AsyncStorage.removeItem diff --git a/docs/api/hippy-vue/vue3.md b/docs/api/hippy-vue/vue3.md index cc199bb8215..4443e269301 100644 --- a/docs/api/hippy-vue/vue3.md +++ b/docs/api/hippy-vue/vue3.md @@ -154,6 +154,106 @@ const router: Router = createRouter({ }); ``` +# 服务端渲染 + +@hippy/vue-next 现已支持服务端渲染,具体代码可以查看[示例项目](https://github.com/Tencent/Hippy/tree/main/examples/hippy-vue-next-ssr-demo)中的 SSR +部分,关于 Vue SSR 的实现及原理,可以参考[官方文档](https://cn.vuejs.org/guide/scaling-up/ssr.html)。 + +## 如何使用SSR + +请参考[示例项目](https://github.com/Tencent/Hippy/tree/main/examples/hippy-vue-next-ssr-demo)说明文档中的 How To Use SSR + +## 实现原理 + +### SSR 架构图 + +hippy-vue-next SSR 架构图 + +### 详细说明 + +@hippy/vue-next SSR 的实现涉及到了编译时,客户端运行时,以及服务端运行时三个运行环境。在 vue-next ssr的基础上,我们开发了 @hippy/vue-next-server-renderer +用于服务端运行时节点的渲染,开发了 @hippy/vue-next-compiler-ssr 用于编译时 vue 模版文件的编译。以及 @hippy/vue-next-style-parser 用于服务端渲染得到的 +Native Node List 的样式插入。下面我们通过一个模版的编译和运行时过程来说明 @hippy/vue-next SSR 做了哪些事情 + +我们有形如`
`的一段模版 + +- 编译时 + + 模版经过 @hippy/vue-next-compiler-ssr 的处理,得到了形如 + + ```javascript + _push(`{"id":${ssrGetUniqueId()},"index":0,"name":"View","tagName":"div","props":{"class":"test-class","id": "test",},"children":[]},`) + ``` + + 的 render function + +- 服务端运行时 + + 在服务端运行时,编译时得到的 render function 执行后得到了对应节点的 json object。注意 render function 中的 + ssrGetUniqueId 方法,是在 @hippy/vue-next-server-renderer 中提供的,在这里 server-renderer 还会对 + 节点的属性值等进行处理,最后得到 Native Node 的 json object + + ```javascript + { "id":1,"index":0,"name":"View","tagName":"div","props":{"class":"test-class","id": "test",},"children":[] } + ``` + + > 对于手写的非 sfc 模版的渲染函数,在 compiler 中无法处理,也是在 server-renderer 中执行的 + +- 客户端运行时 + + 在客户端运行时,通过 @hippy/vue-next-style-parser,给服务端返回的节点插入样式,并直接调用 hippy native 提供的 + native API,将返回的 Native Node 对象作为参数传入,并完成节点的渲染上屏。 完成节点上屏之后,再通过系统提供的 + global.dynamicLoad 异步加载客户端异步版 jsBundle,完成客户端 Hydrate 并执行后续流程。 + +## 初始化差异 + +SSR 版本的 Demo 初始化与异步版的初始化有一些差异部分,这里对其中的差异部分做一个详细的说明 + +- src/main-native.ts 变更 + +1. 使用 createSSRApp 替换之前的 createApp,createApp 仅支持 CSR 渲染,而 createSSRApp 同时支持 CSR 和 SSR +2. 在初始化时候新增了 ssrNodeList 参数,作为 Hydrate 的初始化节点列表。这里我们服务端返回的初始化节点列表保存在了 global.hippySSRNodes 中,并将其作为参数在createSSRApp时传入 +3. 将 app.mount 放到 router.isReady 完成后调用,因为如果不等待路由完成,会与服务端渲染的节点有所不同,导致 Hydrate 时报错 + +```javascript +- import { createApp } from '@hippy/vue-next'; ++ import { createSSRApp } from '@hippy/vue-next'; +- const app: HippyApp = createApp(App, { ++ const app: HippyApp = createSSRApp(App, { + // ssr rendered node list, use for hydration ++ ssrNodeList: global.hippySSRNodes, +}); ++ router.isReady().then(() => { ++ // mount app ++ app.mount('#root'); ++ }); +``` + +- src/main-server.ts 新增 + +main-server.ts 是在服务端运行的业务 jsBundle,因此不需要做代码分割。整体构建为一个 bundle 即可。其核心功能就是在服务端完成首屏渲染逻辑,并将得到的首屏 Hippy 节点进行处理,插入节点属性和 store(如果存在)后返回, +以及返回当前已生成节点的最大 uniqueId 供客户端后续使用。 + +>注意,服务端代码是同步执行的,如果有数据请求走了异步方式,可能会出现还没有拿到数据,请求就已经返回了的情况。对于这个问题,Vue SSR 提供了专用 API 来处理这个问题: +>[onServerPrefetch](https://cn.vuejs.org/api/composition-api-lifecycle.html#onserverprefetch)。 +>在 [Demo](https://github.com/Tencent/Hippy/blob/main/examples/hippy-vue-next-ssr-demo/src/app.vue) 的 app.vue 中也有 onServerPrefetch 的使用示例 + +- server.ts 新增 + +server.ts 是服务端执行的入口文件,其作用是提供 Web Server,接收客户端的 SSR CGI 请求,并将结果作为响应数据返回给客户端,包括了渲染节点列表,store,以及全局的样式列表。 + +- src/main-client.ts 新增 + +main-client.ts 是客户端执行的入口文件,与之前纯客户端渲染不同,SSR的客户端入口文件仅包含了获取首屏节点请求、插入首屏节点样式、以及将节点插入终端完成渲染的相关逻辑。 + +- src/ssr-node-ops.ts 新增 + +ssr-node-ops.ts 封装了不依赖 @hippy/vue-next 运行时的 SSR 节点的插入,更新,删除等操作逻辑 + +- src/webpack-plugin.ts 新增 + +webpack-plugin.ts 封装了 SSR 渲染所需 Hippy App 的初始化逻辑 + # 其他差异说明 目前 `@hippy/vue-next` 与 `@hippy/vue` 功能上基本对齐,不过在 API 方面与 @hippy/vue 有一些区别,以及还有一些问题还没有解决,这里做些说明: @@ -266,7 +366,7 @@ const router: Router = createRouter({ } ``` - 更多信息可以参考 demo 里的 [extend.ts](https://github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/src/extend.ts). + 更多信息可以参考 demo 里的 [extend.ts](https://github.com/Tencent/Hippy/blob/main/examples/hippy-vue-next-demo/src/extend.ts). - whitespace 处理 @@ -305,6 +405,23 @@ const router: Router = createRouter({ `` 组件的第一个子元素不能设置 `{ position: absolute }` 样式,如果想将 `` 内容铺满全屏,可以给第一个子元素设置 `{ flex: 1 }` 样式或者显式设置 width 和 height 数值。这与 Hippy3.0 的逻辑保持一致。 +- 书写 SSR 友好的代码 + + 因 SSR 的渲染方式和生命周期等与客户端渲染方式有一些差异,因此需要在代码编写过程中注意,这里可以参考[Vue官方的SSR指引](https://cn.vuejs.org/guide/scaling-up/ssr.html#writing-ssr-friendly-code) + +- Vue 构建结果体积问题 + + 因为 @hippy/vue-next 项目使用的是已编译的 Vue 组件,所以并不依赖于 Vue 的编译器,而默认的 webpack 打包会使用完整版本的 Vue 进行构建,会将不需要的编译器也打包在构建产物中,因此需要指定使用运行时版本的 Vue 产物 + + ```javascript + // scripts/hippy-webpack.android.js + const aliases = { + // ...other options + // hippy 仅需要运行时的 Vue,在这里指定 + vue$: 'vue/dist/vue.runtime.esm-bundler.js', + }; + ``` + # 示例 -更多使用请参考 [示例项目](https://github.com/Tencent/Hippy/tree/master/examples/hippy-vue-next-demo). +更多使用请参考 [示例项目](https://github.com/Tencent/Hippy/tree/main/examples/hippy-vue-next-demo). diff --git a/docs/api/style/appearance.md b/docs/api/style/appearance.md index 5877ac8c00b..e96d56b7f03 100644 --- a/docs/api/style/appearance.md +++ b/docs/api/style/appearance.md @@ -174,6 +174,8 @@ | ------ | -------- | --- | | number \| string | 否 | Android、iOS +> Android API 28 以下仅支持 `normal`和 `bold` 两种字重,其它字重效果需配合 `fontFamily` 实现,Android API 28 及以上可以支持设置`1` - `1000`的字重值。 + # letterSpacing 文本字符间距 diff --git a/docs/api/style/transform.md b/docs/api/style/transform.md index 07b330273bb..85360cfb318 100644 --- a/docs/api/style/transform.md +++ b/docs/api/style/transform.md @@ -14,6 +14,23 @@ transform: [{ rotateX: '45deg' }, { rotateZ: '0.785398rad' }] 它与 CSS 的 transform 参数类似,请参考 [MDN](//developer.mozilla.org/zh-CN/docs/Web/CSS/transform) 上的详细信息。 -| 类型 | 必需 | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| array of object: { perspective: number }, object: { rotate: string }, object: { rotateX: string }, object: { rotateY: string }, object: { rotateZ: string }, object: { scale: number }, object: { scaleX: number }, object: { scaleY: number }, object: { translateX: number }, object: {translateY: number}, object: { skewX: string }, object: { skewY: string } | 否 | +| 参数 | 描述 | 类型 | 支持平台 | +| ----------- | ------------------------------------------------------------ | -------- | -------------- | +| perspective | 指定观察者与 z=0 平面的距离,默认值`1280`,Android 从 `3.2.0` 版本开始支持 | `number` | `Android、iOS` | +| rotate | 旋转,角度或弧度 | `string` | `Android、iOS` | +| rotateX | X轴旋转,角度或弧度 | `string` | `Android、iOS` | +| rotateY | Y轴旋转,角度或弧度 | `string` | `Android、iOS` | +| rotateZ | Z轴旋转(同rotate) | `string` | `Android、iOS` | +| scale | 缩放 | `number` | `Android、iOS` | +| scaleX | X轴缩放 | `number` | `Android、iOS` | +| scaleY | Y轴缩放 | `number` | `Android、iOS` | +| translateX | X轴平移 | `number` | `Android、iOS` | +| translateY | Y轴平移 | `number` | `Android、iOS` | +| skewX | X轴倾斜,角度或弧度 | `string` | `iOS` | +| skewY | Y轴倾斜,角度或弧度 | `string` | `iOS` | + +!> Android 不支持 `skewX` 和 `skewY` 。 + +!> Android 从 `3.2.0` 版本开始支持设置perspective,并把默认值改为和 iOS 一致。 + +!> Android 旧版本处理多个变形参数的顺序是反转的,从 `3.2.0` 开始改为和 iOS 一致。 diff --git a/docs/architecture/render/ios/native-render.md b/docs/architecture/render/ios/native-render.md index b07a5f1e91d..de0a6d12efc 100644 --- a/docs/architecture/render/ios/native-render.md +++ b/docs/architecture/render/ios/native-render.md @@ -4,9 +4,9 @@ Hippy抽象了RenderManager的接口,允许接入方自行实现RenderManager接口,并实现上屏操作。其中Native Renderer由Hippy默认实现,通过Native组件构建出整个Hippy界面。 -NativeRenderManager负责实现Hippy::RenderManager的抽象接口,并将Render树的构建与UI上屏的行为交由NativeRenderImpl处理。 +NativeRenderManager负责实现Hippy::RenderManager的抽象接口,并将Render树的构建与UI上屏的行为交由HippyUIManager处理。 -NativeRenderImpl负责处理以下行为: +HippyUIManager负责处理以下行为: - Render节点的创建与管理 - RooView与RootNode绑定与管理(不持有) @@ -58,7 +58,7 @@ NativeRenderImpl负责处理以下行为: 为此,需要设定render节点的懒加载属性,以保证UI的懒创建。 -在iOS中,此能力由`[NativeRenderObjectView creationType:NativeRenderCreationType]`属性控制。 +在iOS中,此能力由`[HippyShadowView creationType:NativeRenderCreationType]`属性控制。 ![image](../../../assets/img/lazy_load1.png) 对于懒加载组件,需要手动调用创建方法才会创建。 ![image](../../../assets/img/lazy_load2.png) @@ -141,13 +141,13 @@ Text组件算是一个比较特殊的组件,相对于其他组件,其有两 各业务会选择不同的图片格式就计入,而iOS api默认支持的图片格式有限。这种情况下,需要提供接口,处理默认不支持的图片格式解码。 -为此我们声明了一份协议HPImageProviderProtocol,专门处理各类型Image的解码工作。 +为此我们声明了一份协议HippyImageProviderProtocol,专门处理各类型Image的解码工作。 接入方如果有自定义格式,需要实现一份protocol。 -#### HPImageProviderProtocol +#### HippyImageProviderProtocol -`HPImageProviderProtocol`包含有两类方法:必须实现的和可选实现的。 +`HippyImageProviderProtocol`包含有两类方法:必须实现的和可选实现的。 必须实现的方法负责处理图片解码的基本操作,而可选实现的用于处理动图。 接入方可同时添加多个解码器,HippySDK 在需要时,会按照解码器添加反序询问各解码器能否处理当前数据。如果不能,则会询问下个解码器,直至获取了对应的解码器,或者使用默认解码器。 @@ -167,7 +167,7 @@ Text组件算是一个比较特殊的组件,相对于其他组件,其有两 | -(NSUIneger)loopCount | 返回动图循环次数 | | -(double)delayTimeAtFrame:(NSUInteger)frame | 返回指定帧延迟时长 | -#### HPDefaultImageProvider +#### HippyDefaultImageProvider Hippy3.0默认实现了一套decoder作为默认decoder,实现对系统支持的格式进行解码操作。任何没有decoder处理的数据,最终都会由HippyDefaultImageProvider调用系统API CGImageSource进行处理。 @@ -178,6 +178,6 @@ Hippy3.0默认实现了一套decoder作为默认decoder,实现对系统支持 - Hipp3.0SDK的动图逻辑由NativeRenderAnimatedImage和NativeRenderAnimatedImageView负责。 - 这是一个生产者-消费者模型。NativeRenderAnimatedImage负责生产,NativeRenderAnimatedImageView负责消费。 - NativeRenderAnimatedImageView实现一个vsync回调,每次回调向NativeRenderAnimatedImage询问当前帧对应的Image -- NativeRenderAnimatedImage持有HPImageProviderProtocol实例,负责解析动图,并返回对应帧的Image +- NativeRenderAnimatedImage持有HippyImageProviderProtocol实例,负责解析动图,并返回对应帧的Image ![image](../../../assets/img/animated_image.png) diff --git a/docs/assets/img/3.0-demo-create.png b/docs/assets/img/3.0-demo-create.png new file mode 100644 index 00000000000..23585f0848d Binary files /dev/null and b/docs/assets/img/3.0-demo-create.png differ diff --git a/docs/assets/img/3.0-demo-helloworld.png b/docs/assets/img/3.0-demo-helloworld.png new file mode 100644 index 00000000000..14dbe00df10 Binary files /dev/null and b/docs/assets/img/3.0-demo-helloworld.png differ diff --git a/docs/assets/img/3.0-demo-home.png b/docs/assets/img/3.0-demo-home.png new file mode 100644 index 00000000000..42637ec7454 Binary files /dev/null and b/docs/assets/img/3.0-demo-home.png differ diff --git a/docs/assets/img/3.0-demo-page-management.png b/docs/assets/img/3.0-demo-page-management.png new file mode 100644 index 00000000000..3df9604515e Binary files /dev/null and b/docs/assets/img/3.0-demo-page-management.png differ diff --git a/docs/assets/img/3.0-demo.png b/docs/assets/img/3.0-demo.png new file mode 100644 index 00000000000..f2be10f8bfe Binary files /dev/null and b/docs/assets/img/3.0-demo.png differ diff --git a/docs/assets/img/3.0-performance-fps.png b/docs/assets/img/3.0-performance-fps.png new file mode 100644 index 00000000000..53deac33d72 Binary files /dev/null and b/docs/assets/img/3.0-performance-fps.png differ diff --git a/docs/assets/img/3.0-performance-memory.png b/docs/assets/img/3.0-performance-memory.png new file mode 100644 index 00000000000..3ab10e004b6 Binary files /dev/null and b/docs/assets/img/3.0-performance-memory.png differ diff --git a/docs/assets/img/3.0-performance-start.png b/docs/assets/img/3.0-performance-start.png new file mode 100644 index 00000000000..021a9baf8e4 Binary files /dev/null and b/docs/assets/img/3.0-performance-start.png differ diff --git a/docs/assets/img/3.0-performance0.png b/docs/assets/img/3.0-performance0.png new file mode 100644 index 00000000000..7cc75f1b747 Binary files /dev/null and b/docs/assets/img/3.0-performance0.png differ diff --git a/docs/assets/img/3.0-performance1.png b/docs/assets/img/3.0-performance1.png new file mode 100644 index 00000000000..b94fe94598c Binary files /dev/null and b/docs/assets/img/3.0-performance1.png differ diff --git a/docs/assets/img/3.0-performance2.png b/docs/assets/img/3.0-performance2.png new file mode 100644 index 00000000000..2690dcec0eb Binary files /dev/null and b/docs/assets/img/3.0-performance2.png differ diff --git a/docs/assets/img/hippy-vue-next-ssr-arch-cn.png b/docs/assets/img/hippy-vue-next-ssr-arch-cn.png new file mode 100644 index 00000000000..df95922560e Binary files /dev/null and b/docs/assets/img/hippy-vue-next-ssr-arch-cn.png differ diff --git a/docs/development/3.0-upgrade-guidelines.md b/docs/development/3.0-upgrade-guidelines.md new file mode 100644 index 00000000000..fd71d70cc50 --- /dev/null +++ b/docs/development/3.0-upgrade-guidelines.md @@ -0,0 +1,201 @@ +# Hippy 3.0 架构升级指引 + +>这篇教程,主要介绍Hippy 2.0升级3.0版本如何进行适配以及2.0和3.0在使用上的一些差异化。 +
+ +# Hippy-React 3.0 SDK 升级指引 + +>如果业务目前使用 React 来开发 Hippy,可以参考当前章节升级指引。 +
+ +如果当前 @hippy/react 版本小于 2.12.0, 且 React 使用的 16 的版本,则需要升级如下版本: + +``` javascript +(1)删除 react-reconciler 依赖 +(2)@hippy/react 升级到 3.0.2-beta 以上 +(3)新增 @hippy/react-reconciler 依赖,使用react17的tag,即 @hippy/react-reconciler: react17 +(4)React 版本升级到 17,即 react: "^17.0.2" +(5)如果使用了 @hippy/react-web 包做h5同构,则需要升级 @hippy/react-web 到 3.0.2-beta 以上 +``` + +如果当前 @hippy/react 版本大于 2.12.0, 且 React 使用的 17 的版本,则需要升级如下版本: + +``` javascript +(1)@hippy/react 升级到 3.0.2-beta 以上 +(2)升级 @hippy/react-reconciler 依赖,使用react17的tag,即 @hippy/react-reconciler: react17 +(3)如果使用了 @hippy/react-web 包做h5同构,则需要升级 @hippy/react-web 到 3.0.2-beta 以上 +``` + +Hippy-React 在升级3.0可以完全兼容之前的版本,除了升级如上依赖,业务代码不需要做修改。 + +验证关注点: + +1. 界面的UI视图渲染正常 (UI结构、样式属性等) +2. UI事件(点击、滑动)等表现正常 +3. 自定义组件渲染正常 +4. 自定义模块通讯正常 +5. 动态加载js bundle流程正常 +6. 页面冷启动、卡顿等性能数据正常 +7. 页面曝光上报/日志上报正常 + +# Hippy-Vue 3.0 SDK 升级指引 + +>如果业务目前使用 Vue 2.x 来开发 Hippy,可以参考当前章节升级指引。 +
+ +需要升级如下版本依赖: + +``` javascript +(1)@hippy/vue 升级到 3.0.2-beta 以上 +(2)@hippy/vue-native-components 升级到 3.0.2-beta 以上 +(3)@hippy/vue-router 升级到 3.0.2-beta 以上 +(4)@hippy/vue-css-loader 升级到 3.0.2-beta 以上 +(5)@hippy/vue-loader 升级到 3.0.2-beta 以上 +(6)vue 和 vue-router等vue相关依赖无需升级 +``` + +Hippy-Vue 在升级3.0可以完全兼容之前的版本,除了升级如上依赖,业务代码不需要做修改。 + +验证关注点:(同Hippy React) + +1. 界面的UI视图渲染正常 (UI结构、样式属性等) +2. UI事件(点击、滑动)等表现正常 +3. 自定义组件渲染正常 +4. 自定义模块通讯正常 +5. 动态加载js bundle流程正常 +6. 页面冷启动、卡顿等性能数据正常 +7. 页面曝光上报/日志上报正常 + +# Hippy-Vue-Next 3.0 SDK 升级指引 + +>如果业务目前使用 Vue 3.x 来开发 Hippy,可以参考当前章节升级指引。 +
+ +需要升级如下版本依赖: + +``` javascript +(1)@hippy/vue-next 升级到 3.0.2-beta 以上 +(2)@hippy/vue-router-next-history 升级到 3.0.2-beta 以上 +(3)@hippy/vue-css-loader 升级到 3.0.2-beta 以上 +(4)vue 和 vue-router 等vue相关依赖无需升级 +``` + +Hippy-Vue-Next 在升级3.0可以完全兼容之前的版本,除了升级如上依赖,业务代码不需要做修改。 + +验证关注点:(同Hippy React) + +1. 界面的UI视图渲染正常 (UI结构、样式属性等) +2. UI事件(点击、滑动)等表现正常 +3. 自定义组件渲染正常 +4. 自定义模块通讯正常 +5. 动态加载js bundle流程正常 +6. 页面冷启动、卡顿等性能数据正常 +7. 页面曝光上报/日志上报正常 + + +# Android 3.0 SDK 升级指引 + +1. 废弃HippyImageLoader相关实现 + + HippyImageLoader在2.0中作为引擎初始化必设项是不合理的,在3.0版本中由于图片数据的网络拉取和解码解耦为不同的子模块,HippyImageLoader已经被移除,图片请求会和其它所有IO相关的资源请求统一走vfs模块进行分发,网络请求vfs最终会分发到HttpAdapter完成请求的处理。 + 获取到图片数据后,解码模块新增加ImageDecoderAdapter可选项设置(引擎初始化时候新增imageDecoderAdapter参数设置),用于支持开发者有自定义格式图片的解码需求,ImageDecoderAdapter的具体接口描述如下: + + ```java + // 解码image原始数据,解码的结果可以通过 image data holder提供的setBitmap或者setDrawable接口 + // 置到holder中,如果宿主decode adapter不处理,返回false由SDK走默认解码逻辑 + boolean preDecode(@NonNull byte[] data, + @Nullable Map initProps, + @NonNull ImageDataHolder imageHolder, + @NonNull BitmapFactory.Options options); + + // 解码结束后,宿主通过该接口回调还可以获得二次处理bitmap的机会,比如要对bitmap做高斯模糊。 + void afterDecode(@Nullable Map initProps, + @NonNull ImageDataHolder imageHolder, + @NonNull BitmapFactory.Options options); + + // 引擎退出销毁时调用,释放adapter可能占用的资源 + void destroyIfNeeded(); + ``` + +2. 引擎初始化完成callback线程变更 + + 2.0中initEngine初始化结果SDK内部会切换到UI线程再callback onInitialized给宿主,但我们发现在很多APP内业务反馈的使用场景下,callback切UI线程执行具有很大的延迟,所以3.0中callback onInitialized直接在子线程回调并继续执行loadModule会有更好的效率,之前2.0在callback中对hippyRootView相关的UI操作需要开发者自己来切UI线程保证。 + +3. 引擎销毁 + + 3.0中destroyModule增加了回调接口,destroyEngine需要等destroyModule执行完成回调以后才能调用,否则可能有CRASH的风险,宿主可以参考下面代码示例进行引擎销毁: + + ```java + fun destroyEngine(hippyEngine: HippyEngine?, hippyRootView: ViewGroup?) { + hippyEngine?.destroyModule(hippyRootView, + Callback { result, e -> hippyEngine.destroyEngine() }) + } + ``` + +4. HippyEngine中的接口不再直接引用HippyRootView + + destroyModule接口参数以及loadModule接口返回值均使用系统ViewGroup类型替代,尽量减少对SDK的耦合。 + +5. loadModule接口参数ModuleListener接口有所变更 + - 我们发现之前2.0在onLoadCompleted回调接口中返回的root view参数其实在各多业务场景都不会去用到,所以在3.0中我们简化了这个接口,移除了root view参数的返回 + - 增加onFirstViewAdded接口回调,返回第一view挂载到Hippy root view的回调时机 + +6. 引擎初始化参数增加资源请求自定义processor的设置 + + ```java + public List processors; + ``` + + 关于vfs特性以及Processor接口使用的介绍可以详见 [VFS](feature/feature3.0/vfs.md)。 + +7. 关于UI Component事件发送 + Hippy终端事件的发送分为全局事件和UI Component事件2种,全局事件和2.0保持一致,使用HippyEngine中暴露的sendEvent接口发送,而UI Component事件的发送可以使用在3.0新增工具类EventUtils中封装的事件发送接口: + + ```java + @MainThread + public static void sendComponentEvent(@Nullable View view, + @NonNull String eventName, + @Nullable Object params); + ``` + +8. HippyInstanceContext已经被废弃 + 2.0中基于系统ContextWrapper封了Hippy自己的HippyInstanceContext,并将其作为所有Hippy view的初始化参数,随着3.0 framework和renderer两个子模块的解耦,我们发现HippyInstanceContext设计过于臃肿,已经不再适用于最新的3.0架构,所以我们在最新的3.0版本中废弃了HippyInstanceContext,改用更加轻量化的NativeRenderContext取代,也就是说3.0中所有Hippy相关的view中保存的context都是NativeRenderContext类型。 + +9. HippyEngine中新增render node缓存特性接口 + 2.0中我们支持了dom node缓存特性,但dom node缓存针对复杂页面场景性能还是存在一定的性能瓶颈,所有我们在3.0重新实现了性能更好的render node缓存特性,关于render node缓存特性与接口使用的介绍可以详见 [RenderNode Snapshot](feature/feature3.0/render-node-snapshot.md)。 + +10. 关于自定义UI组件的Controller中dispatchFunction参数说明 + 在2.0中dispatchFunction接收事件属性的参数类型为HippyArray类型,由于在2.0的后续版本中HippyMap和HippyArray就已经被标记为@Deprecated,所以在3.0的重构中,SDK内部也逐渐替换一些使用HippyMap或HippyArray类型参数的接口,所以针对Controller的dispatchFunction接口SDK内部默认替换成List类型参数 + + ```java + public void dispatchFunction(@NonNull T view, + @NonNull String functionName, + @NonNull List params); + + public void dispatchFunction(@NonNull T view, + @NonNull String functionName, + @NonNull List params, + @NonNull Promise promise); + ``` + + 为了减低3.0升级的成本原来使用HippyArray类型的接口还是保留,只是标记为@Deprecated,所以升级3.0对于原来定义的dispatchFunction接口不需要做任何修改,但建议后续升级到3.0版本的同学,定义新UI组件的时候,直接Override使用List参数类型的新接口。 + +# iOS 3.0 SDK 升级指引 + +## 升级须知 + +从设计上,`Hippy3.0`尽可能保持了与`Hippy2.0`的兼容性。大部分`Hippy2.0`的自定义组件和自定义模块均可无需任何修改,兼容`Hippy3.0`。 + +同时在SDK接入API方面,`Hippy3.0`也尽可能保持了一致。因此,如果您未曾在业务中深度扩展`Hippy内置组件`或模块,升级SDK的过程将非常简单,一般情况下仅会遇到少许编译问题,甚至无需修改任何代码。 + +然而由于3.0的架构改进和一致性优化等原因,部分内在实现会不可避免的发生变化。如果您在业务中存在较多深度定制的自定义组件,如对ListView组件、Image组件进行了深度扩展,那将可能会遇到一些编译问题,本文将做详细说明。 + +改动较大的组件/模块的说明如下: + +1. 删除了`HippyVirtualNode`、`HippyVirtualList`、`HippyVirtualCell`等相关类和API:`HippyVirtualNode`在2.0中作为列表等组件的虚拟对象和数据源,其作用与`HippyShadowView`存在重复,因此`Hippy3.0`删除了这一冗余虚拟对象。 + +2. ListView组件:为支持横滑(`horizontal: true`)相关特性,ListView的渲染实现从`UITableView`切换为了`UICollectionView`。相应的,列表中Cell的基类也由`UITableViewCell`变更为了`UICollectionViewCell`。 + +3. Image组件source属性:由于3.0中关于image source的调用约定发生了变化(从 `NSArray` 类型的 `source` 调整为了 `NSString` 类型的 `src`),因此,如自定义了Image组件,请注意在对应的ViewManager中补充实现 `src` 属性,否则图片可能无法正常显示。 + +4. Image组件内置图片缓存:删除了2.0中内置的背景图片缓存管理类,即`HippyBackgroundImageCacheManager`,图片缓存逻辑交由业务方自行定制。 diff --git a/docs/development/_sidebar.md b/docs/development/_sidebar.md index 84034059b38..efbff78397d 100644 --- a/docs/development/_sidebar.md +++ b/docs/development/_sidebar.md @@ -1,13 +1,22 @@ -* [Demo体验](development/demo.md) -* [前端接入](development/web-integration.md) -* [环境搭建](development/native-integration.md) -* [自定义组件](development/native-component.md) -* [自定义模块](development/native-module.md) -* [事件](development/native-event.md) -* [终端能力适配](development/native-adapter.md) -* [数据类型映射](development/type-mapping.md) -* [V8 API](development/v8-api.md) -* [调试](development/debug.md) -* [技术支持](development/support.md) -* [隐私政策](development/privacy.md) +- [Demo体验](development/demo.md) +- 前端接入 + - [Hippy-React&Vue 3.x SDK集成指引](development/react-vue-3.0-integration-guidelines.md) + - [Hippy-React&Vue 3.x SDK升级指引](development/react-vue-3.0-upgrade-guidelines.md) + - [Web同构接入指引](development/web-integration-guidelines.md) +- 终端接入 + - [Android 3.x SDK集成指引](development/android-3.0-integration-guidelines.md) + - [Android 3.x SDK升级指引](development/android-3.0-upgrade-guidelines.md) + - [iOS 3.x SDK集成指引](development/ios-3.0-integration-guidelines.md) + - [iOS 3.x SDK升级指引](development/ios-3.0-upgrade-guidelines.md) + - [Voltron/Flutter集成指引](development/voltron-flutter-integration-guidelines.md) +- [自定义组件](development/native-component.md) +- [自定义模块](development/native-module.md) +- [事件](development/native-event.md) +- [终端能力适配](development/native-adapter.md) +- [数据类型映射](development/type-mapping.md) +- [V8 API](development/v8-api.md) +- [调试](development/debug.md) +- [技术支持](development/support.md) +- [隐私政策](development/privacy.md) +- [开发者合规指南](development/privacy-developer-guide.md) diff --git a/docs/development/android-3.0-integration-guidelines.md b/docs/development/android-3.0-integration-guidelines.md new file mode 100644 index 00000000000..1b39ea3445b --- /dev/null +++ b/docs/development/android-3.0-integration-guidelines.md @@ -0,0 +1,53 @@ +# Hippy Android 3.x SDK集成指引 + +这篇教程,讲述了如何将 Hippy 3.x SDK 集成到一个现有的 Android 工程。 + +> 注:以下文档都是假设您已经具备一定的 Android 开发经验。 + +--- + +## 前期准备 + +- 已经安装了 JDK version>=1.7 并配置了环境变量 +- 已经安装 Android Studio 最新版本 +- 运行 Demo 工程前需要完成 NDK,CMAKE,gradle 与相关插件的安装 + +## Demo 体验 + +若想快速体验,可以直接基于我们的 [Android Demo](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/examples/android-demo) 来开发 + +## 快速接入 + +1. 创建一个 Android 工程 + +2. Maven 集成 + + - 查询 [Maven Central Hippy 版本](https://search.maven.org/search?q=com.tencent.hippy) + + - 配置 build.gradle + + 下面引用Hippy最新版本号可在上述链接中查询 + + ```java + implementation 'com.tencent.hippy:release:3.3.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.viewpager:viewpager:1.0.0' + ``` + +3. 本地集成(可选) + + - [hippy-framework](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/android) 工程运行 Gradle Task `other => assembleRelease` 或者 `other => assembleDebug` 后会在 `framework/android/build/outputs/aar` 目录下生成 `release` 或者 `debug` 模式的`android-sdk.aar`,将 `android-sdk.aar` 拷贝到你项目的 `libs` 目录下。 + + !> 通过 `assembleRelease` task 生成的 AAR 默认不携带 `inspector` 模块,不能在前端通过 Devtools 对代码进行调试,若需要集成 `inspector`,请执行 `assembleDebug` task + + - 配置 build.gradle + + ```java + api (name:'android-sdk', ext:'aar') + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.viewpager:viewpager:1.0.0' + ``` + +4. 在宿主 APP 工程中增加引擎初始化与 `hippyRootView` 挂载逻辑,具体可以参考 [Demo](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/examples/android-demo) 工程中 `HippyEngineWrapper` 实现 diff --git a/docs/development/android-3.0-upgrade-guidelines.md b/docs/development/android-3.0-upgrade-guidelines.md new file mode 100644 index 00000000000..e49e5892009 --- /dev/null +++ b/docs/development/android-3.0-upgrade-guidelines.md @@ -0,0 +1,142 @@ +# Hippy Android 3.x SDK升级指引 + +> 这篇教程,主要介绍Hippy Android SDK 从2.x升级3.x版本如何进行适配以及2.x和3.x在使用上的一些差异化。 +
+ +--- + +# 升级依赖项变更 + +3.0 SDK集成依赖项相对2.0有所变更,开发者可以参照[Hippy Android 3.x SDK集成指引](development/android-3.0-integration-guidelines.md)更新自己工程中的依赖项。 +
+
+ +# 接入与使用方式变更 + +**3.0针对终端引擎接入,部分类及接口调用方式上做了以下调整:** + +1. 引擎初始化完成callback回调线程调整
+ 为了提升引擎初始化效率,3.0中引擎初始化完成callback onInitialized直接在子线程回调并继续执行loadModule。2.0在onInitialized中如果有对hippyRootView挂载等相关的UI操作,需要开发者自己增加UI线程的切换逻辑来保证。 + +2. 引擎销毁调用方式与顺序调整
+ 3.0中destroyModule增加了回调接口,destroyEngine需要等destroyModule执行完成回调以后才能调用,否则可能有CRASH的风险,新的释放流程流程参照以下代码示例: + + ```java + fun destroyEngine(hippyEngine: HippyEngine?, hippyRootView: ViewGroup?) { + hippyEngine?.destroyModule(hippyRootView, + Callback { result, e -> hippyEngine.destroyEngine() }) + } + ``` + +3. 废弃引擎初始化参数HippyImageLoader
+ HippyImageLoader在2.0中作为引擎初始化必设项是不合理的,在3.0中已经废弃,开发者需要在引擎初始化中移除HippyImageLoader相关的设置参数与实现。
+ 3.0图片资源拉取会和其它所有IO相关的资源一样统一走VFS模块进行分发,远程网络资源请求最终会由VFS模块会分发到HttpAdapter进行处理。 + +4. 废弃HippyInstanceContext定义及其相关实现
+ 随着3.0 framework和renderer两个子模块的解耦,我们发现HippyInstanceContext设计过于臃肿,已经不再适用于最新的3.0架构,所以我们在最新的3.0版本中废弃了HippyInstanceContext,改用更加轻量化的NativeRenderContext取代。 + +5. UI Component事件发送
+ 3.0 UI Component事件的发送,开发者需要统一使用使用3.0新增工具类EventUtils中封装的事件发送接口: + + ```java + @MainThread + public static void sendComponentEvent(@Nullable View view, + @NonNull String eventName, + @Nullable Object params); + ``` + +
+ +# 组件变更 + +**3.0针对部分组件做了相应的重构,如果开发者基于老组件扩展了自定义组件,需要做以下适配:** + +1. 废弃support ui下面RecyclerView及其派生类HippyListView组件
+ 3.0的HippyWaterfallView重构后已经不再依赖support ui相关组件,以下2个目录下所有实现文件已经从sdk中移除
+ com/tencent/mtt/supportui/views/recyclerview/
+ com/tencent/mtt/hippy/views/list/
+ 之前开发者如果基于HippyListView派生了自己定义的list view组件,需要修改并适配为继承于HippyRecyclerView + +
+ +# 接口定义变更 + +**3.0对部分接口定义及参数了做了调整,如果开发者有使用到以下接口需要做相应适配:** + +1. ModuleListener接口定义变更
+ - onLoadCompleted回调接口移除了root view参数的返回 + - 增加onFirstViewAdded接口回调,返回第一view挂载到Hippy root view的回调时机 + +2. HippyEngineContext类中部分接口调整
+ - 新增findViewById(int nodeId),可以通过node id查找对应的view + - 移除getDomManager()与getRenderManager()两个接口 + - getEngineId()接口转移至HippyEngine类下 + - 废弃getInstance(int id)接口,由新增getRootView()接口替代 + +3. 废弃HippyRootView中getLaunchParams接口
+ 使用HippyEngineContext下面getJsParams接口来替代。 + +4. HippyEngine类中接口参数调整
+ 为尽量减少接入方对SDK的耦合,destroyModule接口参数以及loadModule接口返回值由原来HippyRootView类型改为系统ViewGroup类型替代。 + +5. HippyHttpRequest类中接口定义变更
+ 由于mInitParams参数在HippyHttpRequest创建的时候就作为初始化参数传入,后续一些依赖mInitParams获取参数的逻辑封装在HippyHttpRequest内部更合理,所以我们移除了以下set接口,只保留get接口: + - public void setMethod(String method) + - public void setInstanceFollowRedirects(boolean instanceFollowRedirects) + - public void setBody(String body) + - public void setNativeParams(Map nativeParams) + - public void setInitParams(HippyMap initParams) + - public void setInstanceFollowRedirects(boolean instanceFollowRedirects) + +6. 废弃HippyViewController中onManageChildComplete接口
+ 统一使用onBatchComplete接口替代,之前2.x列表滚动过程中,view复用后默认都会调用onManageChildComplete,但3.x中为了减少一些无效的调用逻辑进一步提升列表滚动流畅性,列表滚动view复用的时候默认不会调用onBatchComplete,如果想接续接收onBatchComplete调用需要做以下适配:
+ (1) 定义自定义组件对应的RenderNode(类名自定义),并Override shouldNotifyNonBatchingChange接口 + + ```java + public class CustomRenderNode extends RenderNode { + + public CustomRenderNode(int rootId, int id, + @Nullable Map props, + String className, + ControllerManager componentManager, + boolean isLazyLoad) { + super(rootId, id, props, className, componentManager, isLazyLoad); + } + + @Override + protected boolean shouldNotifyNonBatchingChange() { + return !isBatching(); + } + } + ``` + + (2) 在自定义组件Controller中Override createRenderNode接口并增加自定义RenderNode的创建逻辑 + + ```java + @Override + public RenderNode createRenderNode(int rootId, int id, + @Nullable Map props, + @NonNull String className, + @NonNull ControllerManagercontrollerManager, + boolean isLazy) { + return new CustomRenderNode(rootId, id, props, className, controllerManager, isLazy); + } + ``` + +7. HippyDeviceAdapter中reviseDimensionIfNeed接口参数调整
+ 由于HippyRootView不再监听onSystemUiVisibilityChange消息,移除shouldUseScreenDisplay和systemUiVisibilityChanged两个无效参数。 + +
+ +# 新增特性 + +**3.0中新增以下新特性的支持,开发者可以根据自己的需求进行选择性适配:** + +1. 新增统一资源请求处理模块-VFS,具体使用方式可以详见 [VFS](feature/feature3.0/vfs.md) 特性文档介绍。 + +2. 新增ImageDecoderAdapter支持接入自定义图片解码器,具体使用方式可以详见 [ImageDecoderAdapter](feature/feature3.0/image-decoder-adapter.md) 特性文档介绍。 + +3. 新增Render Node缓存特性优化启动速度,具体使用方式可以详见 [RenderNode Snapshot](feature/feature3.0/render-node-snapshot.md) 特性文档介绍。 + +4. 新增Screenshot截屏特性,具体使用方式可以详见 [Screenshot for specific views](feature/feature3.0/screenshot.md) 特性文档介绍。 + diff --git a/docs/development/demo.md b/docs/development/demo.md index 34e6f4ab70d..bb970335588 100644 --- a/docs/development/demo.md +++ b/docs/development/demo.md @@ -2,7 +2,12 @@ Hippy 采用 `monorepo` 进行代码管理,多仓库 SDK 统一版本,前端可以直接引入对应的 NPM 包,终端可通过发布分支源码接入或通过对应的包管理仓库引入。 -Hippy 已经提供了完整的[前端和终端范例](//github.com/Tencent/Hippy/tree/master/examples),可直接基于我们现有的范例开始 App 开发。若想快速体验 Hippy,可按照本文档的步骤将 DEMO 运行起来 。 如果要在已有的 App 里整合 Hippy,请继续阅读下面的 `终端环境搭建` 等章节。 +Demo的Native工程代码位于framework/examples目录,前端工程代码位于driver/js/examples目录,如果你对阅读代码更感兴趣,可直接进入github查看 + +- [Demo Native 工程代码](https://github.com/Tencent/Hippy/tree/main/framework/examples) +- [Demo 前端工程代码](https://github.com/Tencent/Hippy/tree/main/driver/js/examples) + +如果已经已经了解Hippy,可跳过Demo体验,参考[Native工程集成章节](development/native-integration?)把Hippy集成到工程。 --- @@ -10,103 +15,217 @@ Hippy 已经提供了完整的[前端和终端范例](//github.com/Tencent/Hippy ## 环境准备 -请确保本地已安装 [git](https://git-scm.com/) 、[nodejs](https://nodejs.org/en/) 和 [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)。 +### macOS开发环境 + +可以在macOS上开发iOS,Android应用,请求根据需要进行环境配置。 -!> npm 最低要求 v7 版本, nodejs 最低要求 v16 版本 +首先,通过Homebrew包管理工具安装git, git-lfs, node(v16) and npm(v7) -Hippy 仓库根目录运行 `git clone https://github.com/Tencent/Hippy.git` 和 `npm install` 命令。 +```shell +brew install git git-lfs node@16 cmake +``` -!> Hippy 仓库应用了 [git-lfs](https://git-lfs.github.com/) 来管理 so、gz、otf 文件, 请确保你已安装 [git-lfs](https://git-lfs.github.com/)。 +#### 编译iOS Demo环境准备 -对于 macOS 开发者: +1. Xcode -* [Xcode](https://developer.apple.com/xcode/) 和 iOS SDK: 用于构建 iOS App。 -* [Cocoapods](https://cocoapods.org/): 用于管理iOS工程文件。 -* [Android Studio](https://developer.android.com/studio) 和 NDK: 用于构建 Android App。 + 通过Apple App Store安装[Xcode](https://apps.apple.com/cn/app/xcode/id497799835?l=en-GB&mt=12) -对 Windows 开发者: +2. 通过gem命令安装Cocoapods -* [Android Studio](https://developer.android.com/studio) 和 NDK: 用于构建 Android App。 + `sudo gem install cocoapods` -## 使用 js demo 构建 iOS App -对于首次进行 iOS 开发,我们推荐优先采用 iOS 模拟器。然而你也可以修改 Xcode 配置将 app 安装到 iPhone 上。 +#### 编译Android Demo环境准备 -1. 在Hippy driver/js目录执行命令 +1. Android Studio - ```bash - npm run init + 通过android开发者平台下载安装[Android Studio](https://developer.android.com/studio) - # 该命令由 `npm install && npx lerna bootstrap && npm run build` 组成,你也可以分别执行这几个命令。 - # - # npm install: 安装项目所需的脚本依赖。 - # - # `npx lerna bootstrap`: 安装每一个 JS 包的依赖。(Hippy 使用 [Lerna](https://lerna.js.org/) 管理多个 js 包) - # - # `npm run build`: 构建每一个 JS SDK 包。 - ``` - -2. 选择一个你想体验的 JS Demo,在 Hippy 项目 driver/js目录执行 - - ```bash - npm run buildexample [hippy-react-demo|hippy-vue-demo|hippy-vue-next-demo] +2. Android NDK + + 通过android开发者平台下载安装[NDK](https://developer.android.com/ndk?hl=en) + +### Windows开发环境 + +可以Windows上开发Android应用,请安装以下依赖。 + +1. Android Studio + + 通过android开发者平台下载安装[Android Studio](https://developer.android.com/studio) - # 方括号内选择你想构建的 JS Demo,执行后会将对应的 JS 相关资源文件生成到终端 Demo 目录下。 +2. Android NDK + + 通过android开发者平台下载安装[NDK](https://developer.android.com/ndk?hl=en) + +3. Git for Windows + + 通过(https://gitforwindows.org)下载安装Git for Windows + +4. Node和NPM + + 通过指引安装[nodejs和npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm),建议使用Node v16,NPM v7版本 + + +## 代码拉取 + +```shell +git clone https://github.com/Tencent/Hippy.git +``` + +## 编译运行Demo + +以下基于macOS平台,分别说明如何编译Android和iOS Demo。 + +### 编译运行iOS Demo + +```shell +# 进入Hippy源码目录 +cd ./framework/examples/ios-demo +# 使用 Cocoapods 生成工程 +pod install +# 打开 workspace,编译运行即可 +open HippyDemo.xcworkspace +``` + +### 编译运行Android Demo + +1. 使用 Android Studio 打开根目录 `Android Project` 项目。 +2. 使用 USB 线连接 Android 设备,并确保设备 USB 调试模式已经开启(电脑 Terminal 执行 `adb devices` 检查手机连接状态)。 +3. Android Studio 执行项目构建,并安装 APK运行。 + +### 效果预览 + +Demo运行起来后,可见Demo首页类似 + +Demo效果 + +点击"New Page",进入页面管理 + +Demo效果 + +点击"+"号创建新的Hippy页面 + +Demo效果 + +点击Create按钮后,显示出了Hippy渲染的页面 + +Demo效果 + + +## 动手尝试 + +如果你不满足于简单把Demo跑起来,还可以动手尝试修改前端代码,可以按照以下指引进行。 + +### 了解Demo项目代码 + +Demo项目Native代码位于framework/examples/,前端代码位于driver/js/examples目录 + +```shell +steven@STEVEN-MC Hippy % ls driver/js/examples +total 0 +drwxr-xr-x 8 steven staff 256 Oct 30 14:53 hippy-react-demo +drwxr-xr-x 9 steven staff 288 Oct 30 14:53 hippy-vue-demo +drwxr-xr-x 11 steven staff 352 Oct 30 14:53 hippy-vue-next-demo - # 如果该步骤出现异常,你也可以 `cd` 到 `examples` 下的任意一个 JS Demo 目录,执行 `npm install --legacy-peer-deps` 去安装 Demo 的依赖。 - ``` +``` -3. 打开`examples/ios-demo`目录, 使用`Cocoapods`生成工程项目文件 +其中前端hippy-react-demo、hippy-vue-demo、hippy-vue-next-demo这3者为前端项目Demo,分别演示基于hippy-react、hippy-vue、hippy-vue-next开发项目。 - ```bash - cd examples/ios-demo - pod install - ``` +### 修改前端工程 -4. 用`Xcode`打开上一步Cocoapods生成的 `HippyDemo.xcworkspace`工程文件,进行 iOS App 构建。 +以hippy-react-demo为例,打开hippy-react-demo/src/app.jsx,将内容替换为以下代码片段 -> 更多细节请参考 [iOS 集成章节](development/native-integration?id=ios). +```jsx +import React, { Component } from 'react'; +import { + View, + Text +} from '@hippy/react'; -## 使用 js demo 构建 Android App +export default class App extends Component { + render() { + return ( + + Hello World! + + ); + } +} +``` -1. Hippy driver/js 目录执行 `npm run init`。 +### 编译修改后的前端工程 - > 该命令由 `npm install && npx lerna bootstrap && npm run build` 组成,你也可以分别执行这几个命令。 - > - > `npm install`: 安装项目所需的脚本依赖。 - > - > `npx lerna bootstrap`: 安装每一个 JS 包的依赖。(Hippy 使用 [Lerna](https://lerna.js.org/) 管理多个 js 包) - > - > `npm run build`: 构建每一个 JS SDK 包。 +先在Hippy**根目录**执行命令 -2. 选择一个你想体验的 JS Demo,在 Hippy driver/js目录执行 `npm run buildexample [hippy-react-demo|hippy-vue-demo|hippy-vue-next-demo]`(方括号内选择你想构建的 JS Demo),执行后会将对应的 JS 相关资源文件生成到终端 Demo 目录下。 -3. 使用 Android Studio 打开根目录 `Android Project` 项目。 -4. 使用 USB 线连接 Android 设备,并确保设备 USB 调试模式已经开启(电脑 Terminal 执行 `adb devices` 检查手机连接状态)。 -5. Android Studio 执行项目构建,并安装 APK。 +```shell +npm install +``` -> 如果 `步骤二` 出现异常,你也可以 `cd` 到 `examples` 下的任意一个 JS Demo 目录,执行 `npm install --legacy-peer-deps` 去安装 Demo 的依赖。 -> -> 更多细节请参考 [Android 集成章节](development/native-integration?id=android)。 +以hippy-react-demo为例说明编译流程 -## 调试 js demo +```shell +cd driver/js/ +npm run init -1. 按照 `使用 js demo 构建 iOS App` 或 `使用 js demo 构建 Android App` 步骤构建 App。 -2. Hippy 项目driver/js目录执行 `npm run init:example [hippy-react-demo|hippy-vue-demo|hippy-vue-next-demo]`。 -3. Hippy 项目driver/js目录执行 `npm run debugexample [hippy-react-demo|hippy-vue-demo|hippy-vue-next-demo] dev`。 +# 该命令由 `npm install && npx lerna bootstrap && npm run build` 组成,你也可以分别执行这几个命令。 +# +# npm install: 安装项目所需的脚本依赖。 +# +# `npx lerna bootstrap`: 安装每一个 JS 包的依赖。(Hippy 使用 [Lerna](https://lerna.js.org/) 管理多个 js 包) +# +# `npm run build`: 构建每一个 JS SDK 包。 -> 或者你也可以 `cd` 到 `driver/js/examples` 下不同的 DEMO 目录执行 `npm run hippy:dev` 开启 JS Bundle 调试. -> -> 为了在 Demo 调试模式下便于修改SDK源码进行调试,@hippy/react、 @hippy/vue,、@hippy/vue-next 等 npm 包会链接到 `packages` > `[different package]` > `dist` 的生产文件下,所以一旦你修改了 JS SDK的源码并想要在目标 JS DEMO 里立即生效,请再次在 Hippy 项目driver/js目录执行 `npm run build`。 -> -> 更多调试细节请参考 [Hippy 调试文档](development/debug)。 +# 编译hippy-react-demo +npm run buildexample hippy-react-demo -## 构建生产环境 JS demo +# 如果上一条命令有异常,可以执行以下命令 +cd examples/hippy-react-demo +npm install --legacy-peer-deps +cd ../.. +npm run buildexample hippy-react-demo +``` -1. 按照 `使用 js demo 构建 iOS App` 或 `使用 js demo 构建 Android App` 步骤构建 App。 -2. `cd` 到 `examples` 下不同的 DEMO 目录(hippy-react-demo/hippy-vue-demo/hippy-vue-next-demo)。 -3. 执行 `npm install` 安装不同 DEMO 的依赖包。 -4. 执行 `npm run hippy:vendor` 和 `npm run hippy:build` 构建出生产环境所需的 `vendor.[android|ios].js` 和 `index.[android|ios].js`。 +执行完后,构建产物将会被打包放到examples/hippy-react-demo/dist目录中,目录内容类似 -> Hippy demo 使用 Webpack DllPlugin 分离出公共的 js common 包和业务包,以便多个业务能够复用 common 代码。 +```shell +driver/js/examples/hippy-react-demo/dist +├── android +│ ├── assets +│ │ ├── defaultSource.jpg +│ │ └── hippyLogoWhite.png +│ ├── asyncComponentFromHttp.android.js +│ ├── asyncComponentFromLocal.android.js +│ ├── index.android.js +│ ├── vendor-manifest.json +│ └── vendor.android.js +└── ios + ├── assets + │ ├── defaultSource.jpg + │ └── hippyLogoWhite.png + ├── asyncComponentFromHttp.ios.js + ├── asyncComponentFromLocal.ios.js + ├── index.ios.js + ├── vendor-manifest.json + └── vendor.ios.js +5 directories, 14 files +``` +### 运行前端编译产物 + +正常构建后产物会被拷贝到Android和iOS的res目录,如果发现未拷贝,可以手动执行。 +把examples/hippy-react-demo/dist/ios目录内容整体拷贝到ios demo的res/react目录,当用Android来跑时,注意拷贝到Android对应的目录。 + +```shell +cp -R driver/js/examples/hippy-react-demo/dist/ios/* framework/examples/ios-demo/res/react +cp -R driver/js/examples/hippy-react-demo/dist/android/* framework/examples/android-demo/res/react/ +``` + +接下来,按照[编译运行Demo](#编译运行demo)一节运行Demo。 +效果如图所示 + + +Demo效果 + +恭喜你完成了Hippy的初步体验,下一步参考[Native工程集成章节](development/native-integration)将Hippy接入到你现有的工程吧。 diff --git a/docs/development/ios-3.0-integration-guidelines.md b/docs/development/ios-3.0-integration-guidelines.md new file mode 100644 index 00000000000..f9ec4d4845e --- /dev/null +++ b/docs/development/ios-3.0-integration-guidelines.md @@ -0,0 +1,215 @@ +# Hippy iOS 3.x SDK集成指引 + +这篇教程,讲述了如何将 Hippy 3.x SDK 集成到一个现有的 iOS 工程。 + +> 注:以下文档都是假设您已经具备一定的 iOS 开发经验。 + +--- + +## 一、环境准备 + +- 安装 Xcode + +- 安装 [CMake](https://cmake.org/) + + 推荐使用Homebrew安装CMake,安装命令如下: + + ```shell + brew install cmake + ``` + +- 安装 [CocoaPods](https://cocoapods.org/) + + [CocoaPods](https://cocoapods.org/) 是一个iOS和macOS开发中流行的包管理工具。我们将使用它把Hippy的iOS Framework添加到现有iOS项目中。 + + 推荐使用Homebrew安装CocoaPods,安装命令如下: + + ```shell + brew install cocoapods + ``` + + > 若想快速体验,可以直接基于Hippy仓库中的 [iOS Demo](https://github.com/Tencent/Hippy/tree/main/framework/examples/ios-demo) 来开发 + +## 二、使用 Cocoapods 集成 iOS SDK + +具体的操作步骤如下: + +1. 首先,确定要集成的Hippy iOS SDK版本,如3.2.0,将其记录下来,接下来将在Podfile中用到。 + > 可到「[版本查询地址](https://github.com/Tencent/Hippy/releases)」查询最新的版本信息 + +2. 其次,准备好现有iOS工程的 Podfile 文件 + + Podfile 文件是CocoaPods包管理工具的配置文件,如果当前工程还没有该文件,最简单的创建方式是通过CocoaPods init命令,在iOS工程文件目录下执行如下命令: + + ```shell + pod init + ``` + + 生成的Podfile将包含一些demo设置,您可以根据集成的目的对其进行调整。 + + 为了将Hippy SDK集成到工程,我们需要修改Podfile,将 hippy 添加到其中,并指定集成的版本。修改后的Podfile应该看起来像这样: + + ```text + #use_frameworks! + platform :ios, '11.0' + + # TargetName大概率是您的项目名称 + target TargetName do + + # 在此指定步骤1中记录的hippy版本号,可访问 https://github.com/Tencent/Hippy/releases 查询更多版本信息 + pod 'hippy', '3.3.0' + + end + ``` + + > 请注意,动态库方式接入 `"3.0.0-beta" ~ "3.2.0-beta"` 版本的 `beta版` Hippy iOS SDK 时需在 `Podfile` 添加 `ENV["use_frameworks"]` 环境变量,`"3.3.0"` 版本起已无需添加,如已添加可直接移除。 + + > 默认配置下,Hippy SDK使用布局引擎是[Taitank](https://github.com/Tencent/Taitank),JS引擎是系统的`JavaScriptCore`,如需切换使用其他引擎,请参照下文[《引擎切换(可选)》](#四引擎切换可选)一节调整配置。 + +3. 最后,在命令行中执行 + + ```shell + pod install + ``` + + > 请注意,由于 `hippy.podspec` 中依赖 `CMake` 编译部分 `C++` 模块,因此请确保您的开发环境已经正确配置。具体来说,您需要确保已经安装了 `Xcode` 命令行工具。可以在命令行中执行如下指令来安装必要的工具: + + ```shell + sudo xcode-select --install + sudo xcode-select --reset + ``` + + 命令成功执行后,使用 CocoaPods 生成的 `.xcworkspace` 后缀名的工程文件来打开工程。 + +## 三、编写SDK接入代码,加载本地或远程的Hippy资源包 + +Hippy SDK的代码接入简单来说只需两步: + +1、初始化一个HippyBridge实例,HippyBridge是Hippy最重要的概念,它是终端渲染侧与前端驱动侧进行通信的`桥梁`,同时也承载了Hippy应用的主要上下文信息。 + +2、通过HippyBridge实例初始化一个HippyRootView实例,HippyRootView是Hippy应用另一个重要概念,Hippy应用将由它显示出来,因此可以说创建业务也就是创建一个 `HippyRootView`。 + +目前,Hippy 提供了分包加载接口以及不分包加载接口,使用方式分别如下: + +### 方式1. 使用分包加载接口 + +``` objectivec +/** 此方法适用于以下场景: + * 在业务还未启动时先准备好JS环境,并加载包1,当业务启动时加载包2,减少包加载时间 + * 我们建议包1作为基础包,与业务无关,只包含一些通用基础组件,所有业务通用 + * 包2作为业务代码加载 +*/ + +// 先加载包1,创建出一个HippyBridge实例 +// 假设commonBundlePath为包1的路径 +// Tips:详细参数说明请查阅头文件: HippyBridge.h +NSURL *commonBundlePath = getCommonBundlePath(); +HippyBridge *bridge = [[HippyBridge alloc] initWithDelegate:self + bundleURL:commonBundlePath + moduleProvider:nil + launchOptions:your_launchOptions + executorKey:nil]; + +// 再通过上述bridge以及包2地址创建HippyRootView实例 +// 假设businessBundlePath为包2的路径 +// Tips:详细参数说明请查阅头文件: HippyRootView.h +HippyRootView *rootView = [[HippyRootView alloc] initWithBridge:bridge + businessURL:businessBundlePath + moduleName:@"Your_Hippy_App_Name" + initialProperties:@{} + shareOptions:nil + delegate:nil]; + +// 最后,给生成的rootView设置好frame,并将其挂载到指定的VC上。 +rootView.frame = self.view.bounds; +rootView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; +[self.view addSubview:rootView]; + +// 至此,您已经完成一个Hippy应用的初始化,SDK内部将自动加载资源并开始运行Hippy应用。 +``` + +### 方式2. 使用不分包加载接口 + +``` objectivec +// 与上述使用分包加载接口类似,首先需要创建一个HippyBridge实例, +// 区别是在创建HippyRootView实例时,无需再传入业务包,即businessBundlePath,直接使用如下接口创建即可 +// Tips:详细参数说明请查阅头文件: HippyRootView.h +- (instancetype)initWithBridge:(HippyBridge *)bridge + moduleName:(NSString *)moduleName + initialProperties:(nullable NSDictionary *)initialProperties + shareOptions:(nullable NSDictionary *)shareOptions + delegate:(nullable id)delegate; +``` + +> 在Hippy仓库中提供了一个简易示例项目,包含上述全部接入代码,以及更多注意事项。 +> +> 建议参考该示例完成SDK到已有项目的集成:[iOS Demo](https://github.com/Tencent/Hippy/tree/main/framework/examples/ios-demo),更多设置项及使用方式请查阅上述头文件中的具体API说明。 + +!> 使用分包加载可以结合一系列策略,比如提前预加载bridge, 全局单bridge等来优化页面打开速度。 + +到这里,您已经完成了接入一个默认配置下的Hippy iOS SDK的全部过程。 + +## 四、引擎切换(可选) + +Hippy 3.x的一个重要特性是支持了多引擎的便捷切换,目前,可切换的引擎有两个,一是布局引擎,二是JS引擎。默认配置下,Hippy使用布局引擎是[Taitank](https://github.com/Tencent/Taitank),JS引擎是iOS系统内置的`JavaScriptCore`。 + +如需使用其他布局引擎,如[Yoga](https://github.com/facebook/yoga),或使用其他JS引擎,如V8,可参考如下指引调整Hippy接入配置。 + +> Hippy3.x提供了iOS环境下默认的v8引擎实现,如需使用其他JS引擎需用户自行实现相关napi接口。 + +### 4.1 切换JS引擎 + +如需使用V8引擎,在Podfile文件中添加如下环境变量即可: + +```ruby +ENV['js_engine'] = 'v8' +``` + +修改后的Podfile应该看起来像这样: + +```text +#use_frameworks! +platform :ios, '11.0' +ENV['js_engine'] = 'v8' #切换为V8引擎 + +# TargetName大概率是您的项目名称 +target TargetName do + + pod 'hippy', 'your_specified_version' + +end +``` + +之后,重新执行`pod install`命令更新项目依赖即可。 + +如需使用其他第三方JS引擎,需要做如下操作: + +#### 1.修改Podfile配置为第三方JS引擎 + +将Podfile中的js_engine环境变量配置为other,这样在拉取代码时,jsc或者v8的代码将不会被添加到工程中。 + +```ruby +ENV['js_engine'] = 'other' +``` + +> Hippy3.0中使用napi抽象了不同JS引擎的接口。其中,JSC与V8的接口进行了实现。用户若使用JSC或者V8,可直接切换,Hippy默认进行了实现。 + +#### 2.自行实现napi抽象接口 + +napi将js引擎接口抽象化,由js driver层调用。接入方自行实现napi接口,即可实现对第三方JS引擎的支持。 + +napi文件位于 `/driver/js/napi*` 目录下。 + +#### 3.将实现文件添加到工程中 + +接入方自行将对应的napi实现文件添加到工程中。 + +## 4.2 切换布局引擎 + +用户若想使用Yoga布局引擎,直接在Podfile文件中指定layout_engine为Yoga即可: + +```ruby +ENV['layout_engine'] = 'Yoga' +``` + +之后,重新执行`pod install`命令更新项目依赖即可。 diff --git a/docs/development/ios-3.0-upgrade-guidelines.md b/docs/development/ios-3.0-upgrade-guidelines.md new file mode 100644 index 00000000000..356c6612c61 --- /dev/null +++ b/docs/development/ios-3.0-upgrade-guidelines.md @@ -0,0 +1,109 @@ +# Hippy iOS 3.x SDK升级指引 + +> 这篇教程,主要介绍Hippy iOS SDK 从2.x升级3.x版本如何进行适配以及2.x和3.x在使用上的一些差异。 +
+ +--- + +## 兼容性说明 + +从设计上,`Hippy3.0`尽可能保持了与`Hippy2.0`的兼容性。大部分`Hippy2.0`的自定义组件和自定义模块均可无需任何修改,兼容`Hippy3.0`。 + +同时在SDK的接入API方面,`Hippy3.0`也与`2.17`保持了一致。因此,如果您未曾在业务中深度扩展`Hippy内置组件`或模块,升级SDK的过程将非常简单,一般情况下仅会遇到少许编译问题,甚至无需修改任何代码。 + +然而由于3.0的架构改进和一致性优化等原因,部分内在实现会不可避免的发生变化。如果您在业务中存在较多深度定制的自定义组件,如对ListView组件、Image组件进行了深度扩展,那将可能会遇到一些编译问题,本文将做详细说明。 + +## 升级操作步骤 + +1. 安装必要工具 + + 由于3.0的hippy podspec中使用到了CMake构建工具,因此除了必要的`Xcode`和`Cocoapods`外,您还需安装`CMake`。 详细安装方法可参考:[Hippy iOS 3.x SDK集成指引](development/ios-3.0-integration-guidelines.md)。 + +2. 升级依赖的Hippy iOS SDK版本 + + 如果您使用的是Cocoapods集成,那么仅需将Podfile中指定的Hippy版本升级至3.x即可(可访问 [版本发布地址](https://github.com/Tencent/Hippy/releases) 查询更多版本信息)。 + +3. 编译 & 运行。 + + 确保已经完成前端包的更新后,重新编译运行即可完成SDK的升级。请注意,Hippy3.0的前端包与2.0并不兼容,在Hippy3.x SDK中运行2.0的包将出现错误提示。 + + 如在编译阶段遇到问题,请参考如下说明进行适当的修改。 + +## 变更说明 + +### 接入与使用方式变更 + +**3.0在框架方面,部分类及接口调用方式上做了以下调整:** + +1. 删除了`HippyVirtualNode`、`HippyVirtualList`、`HippyVirtualCell`等相关类和API: + + `HippyVirtualNode`在2.0中作为列表等组件的虚拟对象和数据源,其作用与`HippyShadowView`存在重复,因此`Hippy3.0`删除了这一冗余虚拟对象。 + 如果您在扩展组件中使用到了这些类,请将其替换为对应的 `ShadowView`。 + +2. 新增节点优化算法 + + 3.0中对节点的操作(创建/删除/更新/移动)均应用了节点优化算法,该算法会将仅参与布局的View节点优化去除,从而提升渲染效率。 + + > 请注意,由于该算法的存在,可能导致依赖特定UI层级结构的Native组件发生异常。如 `ScrollView` 组件要求只能有一个一级子元素,如果前端UI结构经节点优化后,一级子元素数量大于1,`Hippy` 将提示渲染异常;此时,可通过给特定 View 增加 `{collapsable: 'false'}` 属性来禁止该节点被优化算法去除。 + +3. 删除了`PerformanceLogger`相关API + + 由于框架变化,删除了2.0中iOS端`HippyPerformanceLogger`类,升级为一致性更好的[Performance API](feature/feature3.0/performance.md)。如有依赖,需适配新的前端`Performance API`,或通过框架在生命周期各阶段提供的的`Hippy Notification`来实现原有能力。 + +### 组件变更 + +**3.0针对部分组件做了相应的重构,如果开发者基于老组件扩展了自定义组件,需要做以下适配:** + +1. ListView组件 - 基于UICollectionView重新实现了ListView组件,支持横滑列表 + + 为支持横滑(`horizontal: true`)相关特性,ListView的渲染实现从2.0中的`UITableView`切换为了3.0中的`UICollectionView`。相应的,列表中Cell的基类也由`UITableViewCell`变更为了`UICollectionViewCell`。 + 如果您有强依赖ListView实现细节的组件扩展逻辑,那么将需做一些适当的修改。 + +2. Image组件 - source属性调用约定变更为src + + 由于3.0中关于image source的调用约定发生了变化(从 `NSArray` 类型的 `source` 调整为了 `NSString` 类型的 `src`),因此,如自定义了Image组件,请注意在对应的ViewManager中补充实现 `src` 属性,否则图片可能无法正常显示。 + +3. Image组件 - 删除了Image组件的内置图片缓存 + + 鉴于内置缓存与第三方解码库的冲突问题,3.0中删除了2.0内置的背景图片缓存管理类,即HippyBackgroundImageCacheManager,图片缓存逻辑交由业务方自行定制。如果您有缓存图片的需求,请通过自定义ImageLoader来实现。 + +4. Image组件 - 更新了自定义图片加载器的协议 + + Hippy 2.0提供了`HippyImageViewCustomLoader`协议,用于业务按需定制图片资源加载器。通常,App一般使用第三方图片库实现该协议,如SDWebImage等,从而实现更灵活的图片加载和支持更多图片类型的解码。然而,2.0中的这一协议约定存在些许问题,无法达到最佳的性能表现,而且已经与3.0的VFS模块设计不再兼容,因此在3.0中我们更新了该协议的约定。 + + 注意,为便于及时发现该变更,在3.0中该协议名从`HippyImageViewCustomLoader`调整为了`HippyImageCustomLoaderProtocol`,协议方法也有一些变化,因此如果您使用了该协议,升级时将遇到少许编译问题,但其基本功能依旧保持不变。 + +5. 动画模块 - 动画模块内部重构,动画机制发生变化 + + 动画模块代码实现由OC模块重构为C++模块,因此如有对原动画模块的相关扩展均会产生编译问题,并不再有效。 + + 动画机制由系统驱动(2.17.2以前)变更为Hippy DOM更新驱动 + + 3.0 部分动画相比2.17.2以前有 `Breaking Change` (包括width、height动画及宽高与位移等组合动画),升级时请注意检查。 + +### 接口定义变更 + +**3.0对部分接口定义及参数了做了调整,如果开发者有使用到以下接口需要做相应适配:** + +1. 部分通知(`Notification`)变更 + + * HippyJavaScriptDidLoadNotification通知 + * HippyJavaScriptDidFailToLoadNotification通知 + + 变更说明: + + a) 发送时机变化:由于加载机制变化,上述通知在3.0中的发送时机已从vendor包加载发送变更为只要加载bundle包就发送,不再区分vendor还是business类型。 + + b) 通知内容变化:在新通知userInfo字段中,增加 `kHippyNotiBundleTypeKey` 等字段,用于按需判断bundle类型,详细说明参见 `HippyBridge.h` 中有关通知的详细说明。 + + c) 不再推荐使用 `HippySecondaryBundleDidLoadNotification` 等xxSecondary通知,可使用 `HippyJavaScriptDidLoadNotification` 通知替代。 + +### 新增特性 + +**3.0新增以下新特性,开发者可根据自身需求选择性适配:** + +1. 新增统一资源请求处理模块-VFS,具体使用方式可以详见 [VFS](feature/feature3.0/vfs.md) 特性文档介绍。 + +2. 新增Render Node缓存特性优化启动速度,具体使用方式可以详见 [RenderNode Snapshot](feature/feature3.0/render-node-snapshot.md) 特性文档介绍。 + +3. 新增Screenshot截屏特性,具体使用方式可以详见 [Screenshot for specific views](feature/feature3.0/screenshot.md) 特性文档介绍。 diff --git a/docs/development/native-adapter.md b/docs/development/native-adapter.md index 8be096d8c82..5e3a83d294f 100644 --- a/docs/development/native-adapter.md +++ b/docs/development/native-adapter.md @@ -65,52 +65,86 @@ Hippy SDK 提供默认空实现 `DefaultEngineMonitorAdapter`。当你需要查 --- -## HippyImageViewCustomLoader +## HippyImageCustomLoaderProtocol -在Hippy SDK中, 前端 `` 组件默认对应的 HippyImageView 会根据 source 属性使用默认行为下载图片数据并显示。但是某些情况下,业务方希望使用自定义的图片加载逻辑(比如业务使用了缓存,或者拦截特定URL的数据),为此 SDK 提供了`HippyImageViewCustomLoader` 协议。 +在Hippy SDK中, 前端 `` 组件默认对应的 HippyImageView 会根据 src 属性使用默认行为下载图片数据并显示。但是某些情况下,业务方希望使用自定义的图片加载逻辑(比如业务使用了缓存,或者拦截特定URL的数据),为此 SDK 提供了`HippyImageCustomLoaderProtocol` 协议。 -用户实现此协议,自行根据图片的URL返回数据即可,HippyImageView将根据返回的数据展示图片。 +用户实现此协议,自行根据图片的URL返回数据即可,HippyImageView将根据返回的数据展示图片。注意该支持返回待解码的NSData类型图片数据,也支持直接返回解码后的UIImage图片,请根据需要选择合适方案。 ```objectivec -@protocol HippyImageViewCustomLoader +/// A Resource Loader for custom image loading +@protocol HippyImageCustomLoaderProtocol + @required -/** -* imageView: -*/ -- (void)imageView:(HippyImageView *)imageView - loadAtUrl:(NSURL *)url - placeholderImage:(UIImage *)placeholderImage - context:(void *)context - progress:(void (^)(long long, long long))progressBlock - completed:(void (^)(NSData *, NSURL *, NSError *))completedBlock; - -- (void)cancelImageDownload:(HippyImageView *)imageView withUrl:(NSURL *)url; + +/// Load Image with given URL +/// Note that If you want to skip the decoding process lately, +/// such as using a third-party SDWebImage to decode, +/// Just set the ControlOptions parameters in the CompletionBlock. +/// +/// - Parameters: +/// - imageUrl: image url +/// - extraInfo: extraInfo +/// - progressBlock: progress block +/// - completedBlock: completion block +- (void)loadImageAtUrl:(NSURL *)imageUrl + extraInfo:(nullable NSDictionary *)extraInfo + progress:(nullable HippyImageLoaderProgressBlock)progressBlock + completed:(nullable HippyImageLoaderCompletionBlock)completedBlock; + @end ``` ## 协议实现 ```objectivec -@interface CustomImageLoader : NSObject +@interface CustomImageLoader : NSObject @end @implementation CustomImageLoader -HIPPY_EXPORT_MODULE() -- (void)imageView:(HippyImageView *)imageView loadAtUrl:(NSURL *)url placeholderImage:(UIImage *)placeholderImage context:(void *)context progress:(void (^)(long long, long long))progressBlock completed:(void (^)(NSData *, NSURL *, NSError *))completedBlock { - NSError *error = NULL; +HIPPY_EXPORT_MODULE() // 全局注册该模块至Hippy + +- (void)loadImageAtUrl:(NSURL *)url + extraInfo:(NSDictionary *)extraInfo + progress:(HippyImageLoaderProgressBlock)progressBlock + completed:(HippyImageLoaderCompletionBlock)completedBlock { + + // 1、如果获取的是NSData数据: // 业务方自行获取图片数据,返回数据或者错误 + NSError *error = NULL; NSData *imageData = getImageData(url, &error); - // 将结果通过block通知 - completedBlock(imageData, url, error); + // 将结果通过block回调 + completedBlock(imageData, url, error, nil, kNilOptions); + + // 2、如果可以直接获取UIImage数据,可跳过Hippy内置解码过程,避免重复解码: + UIImage *image = getImage(xxx); + // 传入控制参数,跳过内部解码 + HippyImageLoaderControlOptions options = HippyImageLoaderControl_SkipDecodeOrDownsample; + // 将结果通过block回调 + completedBlock(nil, url, error, image, options); } @end ``` -业务方需要务必添加 `HIPPY_EXPORT_MODULE()` 代码以便在 Hippy 框架中注册此 ImageLoader 模块,系统将自动寻找实现了`HippyImageViewCustomLoader` 协议的模块当做 ImageLoader。 +## 协议注册 + +与Hippy框架注册其他模块的方法一样,ImageLoader同样既可以选择通过Hippy框架提供的 `HIPPY_EXPORT_MODULE()` 宏注册到App全局(注意,全局注册的含义是App内的所有HippyBridge实例均会获取和使用该模块),又可通过 `HippyBridge` 初始化参数列表中的 `moduleProvider` 参数来注册到特定bridge。 + +除此之外,`HippyBridge` 还提供了一个注册方法,便于业务注册ImageLoader实例: + +```objectivec +/// Set a custom Image Loader for current `hippyBridge` +/// The globally registered ImageLoader is ignored when set by this method. +/// +/// - Parameter imageLoader: id +- (void)setCustomImageLoader:(id)imageLoader; +``` + +在上述实现代码中,我们使用了 `HIPPY_EXPORT_MODULE()` 宏来实现将此 ImageLoader 模块自动注册至 Hippy 框架中,框架内部将自动寻找实现了`HippyImageCustomLoaderProtocol` 协议的模块作为 ImageLoader。 -PS: 若有多个模块实现 `HippyImageViewCustomLoader` 协议,系统只会使用其中一个作为默认 ImageLoader +!> 注意,同时只可有一个ImageLoader生效。若有多个模块实现了 `HippyImageCustomLoaderProtocol` 协议,框架使用最后一个作为生效的 ImageLoader。Hippy框架优先使用通过 `setCustomImageLoader:` 方法注册的ImageLoader。 diff --git a/docs/development/native-component.md b/docs/development/native-component.md index 36bc31d7110..2bbf6c68c64 100644 --- a/docs/development/native-component.md +++ b/docs/development/native-component.md @@ -111,6 +111,46 @@ public List> getControllers() ## 更多特性 +### 自定义组件挂载纯native view的适配 + +在Hippy框架中,会将前端节点映射为终端的natvie view,view的显示尺寸和位置由框架自带的排版引擎根据前端设置的css计算得出,不需要走系统默认的measure和layout流程,所以我们在HippyRootView中对onMeasure和onLayout两个回调做了拦截: + +```java +@Override +protected void onLayout(boolean changed, int left, int top, int right, int bottom) +{ + // No-op since UIManagerModule handles actually laying out children. +} + +@Override +protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) +{ + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); +} +``` + +但一些业务场景中,自定义组件需要挂载一些非前端节点映射的纯native view,常见的比如video view,lottie view等,由于我们拦截了onMeasure和onLayout,这些视图无法获取到正确的显示尺寸和位置,导致显示异常,所以需要开发者自己手动调用measure和layout来解决这个问题,可以参考以下示例: +在自定义组件的Controller中Override onBatchComplete接口,在非前端节点映射的纯native view的父容器调用measure和layout + +```java +private final Handler mHandler = new Handler(Looper.getMainLooper()); + +@Override +public void onBatchComplete(@NonNull View view) +{ + super.onBatchComplete(view); + mHandler.post(new Runnable() { + @Override + public void run() + { + view.measure(View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(view.getHeight(), View.MeasureSpec.EXACTLY)); + view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + } + }); +} +``` + ### 处理组件方法调用 在有些场景,JS需要调用组件的一些方法,比如 `MyView` 的 `changeColor`。这个时候需要在 `HippyViewController`重载 `dispatchFunction` 方法来处理JS的方法调用。 @@ -183,13 +223,13 @@ protected void onAttachedToWindow() { ## 创建对应的ViewManager > ViewManager 是对应的视图管理组件,负责前端视图和终端视图直接进行属性、方法的调用。 -> SDK 中最基础的 `ViewManager` 是 `NativeRenderViewManager`,封装了基本的方法,负责管理 `NativeRenderView`。 -> 用户自定的 `ViewManager` 必须继承自 `NativeRenderViewManager`。 +> SDK 中最基础的 `ViewManager` 是 `HippyViewManager`,封装了基本的方法,负责管理 `NativeRenderView`。 +> 用户自定的 `ViewManager` 必须继承自 `HippyViewManager`。 NativeRenderMyViewManager.h ```objectivec -@interface NativeRenderMyViewManager:NativeRenderViewManager +@interface NativeRenderMyViewManager:HippyViewManager @end ``` @@ -198,15 +238,15 @@ NativeRenderMyViewManager.m ```objectivec @implementation NativeRenderMyViewManager -NATIVE_RENDER_EXPORT_VIEW(MyView) +HIPPY_EXPORT_MODULE(MyView) -NATIVE_RENDER_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) -NATIVE_RENDER_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat) +HIPPY_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) +HIPPY_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat) -NATIVE_RENDER_CUSTOM_VIEW_PROPERTY(overflow, CSSOverflow, NativeRenderView) +HIPPY_CUSTOM_VIEW_PROPERTY(overflow, CSSOverflow, HippyView) { if (json) { - view.clipsToBounds = [HPConvert CSSOverflow:json] != CSSOverflowVisible; + view.clipsToBounds = [HippyConvert CSSOverflow:json] != CSSOverflowVisible; } else { view.clipsToBounds = defaultView.clipsToBounds; } @@ -216,15 +256,15 @@ NATIVE_RENDER_CUSTOM_VIEW_PROPERTY(overflow, CSSOverflow, NativeRenderView) return [[NativeRenderMyView alloc] init]; } -- (NativeRenderObjectView *)shadowView { - return [[NativeRenderObjectView alloc] init]; +- (HippyShadowView *)shadowView { + return [[HippyShadowView alloc] init]; } -NATIVE_RENDER_COMPONENT_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag) { +HIPPY_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag) { // do sth } -NATIVE_RENDER_COMPONENT_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag callback:(RenderUIResponseSenderBlock)callback) { +HIPPY_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag callback:(HippyPromiseResolveBlock)callback) { // do sth NSArray *result = xxx; callback(result); @@ -233,42 +273,42 @@ NATIVE_RENDER_COMPONENT_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag callbac ## 类型导出 -`NATIVE_RENDER_EXPORT_VIEW()` 将`NativeRenderMyViewManager` 类注册,前端在对 `MyView` 进行操作时会通过 `NativeRenderMyViewManager` 进行实例对象指派。 +`HIPPY_EXPORT_MODULE()` 将`NativeRenderMyViewManager` 类注册,前端在对 `MyView` 进行操作时会通过 `NativeRenderMyViewManager` 进行实例对象指派。 -`NATIVE_RENDER_EXPORT_VIEW()`中的参数可选。代表的是 `ViewManager` 对应的View名称。 +`HIPPY_EXPORT_MODULE()`中的参数可选。代表的是 `ViewManager` 对应的View名称。 若用户不填写,则默认使用类名称。 ## 参数导出 -`NATIVE_RENDER_EXPORT_VIEW_PROPERTY` 将终端View的参数和前端参数绑定。当前端设定参数值时,会自动调用 setter 方法设置到终端对应的参数。 +`HIPPY_EXPORT_VIEW_PROPERTY` 将终端View的参数和前端参数绑定。当前端设定参数值时,会自动调用 setter 方法设置到终端对应的参数。 -`NATIVE_RENDER_REMAP_VIEW_PROPERTY()` 负责将前端对应的参数名和终端对应的参数名对应起来。以上述代码为例,前端的`opacity` 参数对应终端的`alpha`参数。此宏一共包含三个参数,第一个为前端参数名,第二个为对应的终端参数名称,第三个为参数类型。另外,此宏在设置终端参数时使用的是`keyPath`方法,即终端可以使用`keyPath`参数。 +`HIPPY_REMAP_VIEW_PROPERTY()` 负责将前端对应的参数名和终端对应的参数名对应起来。以上述代码为例,前端的`opacity` 参数对应终端的`alpha`参数。此宏一共包含三个参数,第一个为前端参数名,第二个为对应的终端参数名称,第三个为参数类型。另外,此宏在设置终端参数时使用的是`keyPath`方法,即终端可以使用`keyPath`参数。 -`NATIVE_RENDER_CUSTOM_VIEW_PROPERTY()` 允许终端自行解析前端参数。SDK将前端传递过来的原始json类型数据传递给函数体(用户可以使用`HPConvert`类中的方法解析对应的数据),用户获取后自行解析。 +`HIPPY_CUSTOM_VIEW_PROPERTY()` 允许终端自行解析前端参数。SDK将前端传递过来的原始json类型数据传递给函数体(用户可以使用`HippyConvert`类中的方法解析对应的数据),用户获取后自行解析。 >这个方法带有两个隐藏参数-`view`, `defaultView`。`view`是指当前前端要求渲染的view。`defaultView`指当前端渲染参数为nil时创建的一个临时view,使用其默认参数赋值。 ## 方法导出 -`NATIVE_RENDER_COMPONENT_EXPORT_METHOD` 能够使前端随时调用终端对应的方法。前端通过三种模式调用,分别是 `callNative`, `callNativeWithCallbackId`。终端调用这三种方式时,函数体写法可以参照上面的示例。 +`HIPPY_EXPORT_METHOD` 能够使前端随时调用终端对应的方法。前端通过三种模式调用,分别是 `callNative`, `callNativeWithCallbackId`。终端调用这三种方式时,函数体写法可以参照上面的示例。 - callNative:此方法不需要终端返回任何值。 -- callNativeWithCallbackId: 此方法需要终端在函数体中以单个block形式返回数据。block类型为 `RenderUIResponseSenderBlock`,参数为一个`id`变量。 +- callNativeWithCallbackId: 此方法需要终端在函数体中以单个block形式返回数据。block类型为 `HippyPromiseResolveBlock`,参数为一个`id`变量。 一个`ViewManager`可以管理一种类型的多个实例,为了在ViewManager中区分当前操作的是哪个View,每一个导出方法对应的第一个参数都是View对应的tag值,用户可根据这个tag值找到对应操作的view。 -> 由于导出方法并不会在主线程中调用,因此如果用户需要进行UI操作,则必须将其分配至主线程。推荐在导出方法中使用[NativeRenderImpl addUIBlock:]方法。其中的block类型为`NativeRenderRenderUIBlock`。 +> 由于导出方法并不会在主线程中调用,因此如果用户需要进行UI操作,则必须将其分配至主线程。推荐在导出方法中使用[HippyUIManager addUIBlock:]方法。其中的block类型为`HippyViewManagerUIBlock`。 -> `typedef void (^NativeRenderRenderUIBlock)(NativeRenderImpl *renderContext, NSDictionary *viewRegistry)`。第二个参数为字典,其中的key就是对应的view tag值,value就是对应的view。 +> `typedef void (^HippyViewManagerUIBlock)(HippyUIManager *uiManager, NSDictionary *viewRegistry)`。第二个参数为字典,其中的key就是对应的view tag值,value就是对应的view。 ## 创建RenderObject和View -在OC层,`NativeRenderImpl`负责构建Render树,对应的每一个节点都是一个RenderObjectView。Render树结构不保证与dom树一致,因为Render可能有自己的渲染逻辑。 +在OC层,`HippyUIManager`负责构建Render树,对应的每一个节点都是一个RenderObjectView。Render树结构不保证与dom树一致,因为Render可能有自己的渲染逻辑。 ->`NativeRenderView`会根据`NativeRenderObjectView`的映射结果构建真正的View视图。因此对于大多数情况下的自定义view manager来说,直接创建一个`NativeRenderObjectView`即可。 +>`NativeRenderView`会根据`HippyShadowView`的映射结果构建真正的View视图。因此对于大多数情况下的自定义view manager来说,直接创建一个`HippyShadowView`即可。 -`NativeRenderImpl`将调用[NativeRenderMyViewManager view]方法去创建一个真正的view,用户需要实现这个方法并返回自己所需要的`NativeRenderMyView`。 +`HippyUIManager`将调用[NativeRenderMyViewManager view]方法去创建一个真正的view,用户需要实现这个方法并返回自己所需要的`NativeRenderMyView`。 到此,一个简单的`NativeRenderMyViewManager`与`NativeRenderMyView`创建完成。 diff --git a/docs/development/native-integration.md b/docs/development/native-integration.md index 0604d28112b..c7d43e6ff83 100644 --- a/docs/development/native-integration.md +++ b/docs/development/native-integration.md @@ -58,464 +58,199 @@ 4. 在宿主 APP 工程中增加引擎初始化与 `hippyRootView` 挂载逻辑,具体可以参考 [Demo](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/examples/android-demo) 工程中 `HippyEngineWrapper` 实现 -## 3.0与2.0的接入区别 - -1. 引擎初始化参数 - - HippyImageLoader在2.0中是必设项,在最新3.0版本中由于图片数据的网络拉取和图片解码解耦为不同的子模块,HippyImageLoader已经被移除,新增加ImageDecoderAdapter可选项设置,用于支持开发者有自定义格式图片的解码需求,ImageDecoderAdapter的具体接口用法可以参考native renderer文档介绍 - -2. 引擎初始化完成callback线程变更 - - 2.0中initEngine初始化结果SDK内部会切换到UI线程再callback给宿主,但我们发现在部分APP启动就使用Hippy的场景下,callback切UI线程执行具有很大的延迟,所以3.0中callback直接在子线程回调,之前2.0在callback中对hippyRootView相关的UI操作需要开发者自己来切UI线程保证 - -3. 引擎销毁 - - 3.0中destroyModule增加了回调接口,destroyEngine需要等destroyModule执行完成回调以后才能调用,否则可能有CRASH的风险 - -4. HippyEngine中不再直接引用HippyRootView - - destroyModule接口参数以及loadModule接口返回值均使用系统ViewGroup类型替代,尽量减少对SDK的耦合 - -5. loadModule接口参数ModuleListener接口有所变更 - - onLoadCompleted回调接口remove root view参数 - - 增加onFirstViewAdded接口回调 - -
-
- # iOS >注:以下文档都是假设您已经具备一定的 iOS 开发经验。 -这篇教程,讲述了如何将 Hippy 集成到 iOS 工程。 +这篇教程,讲述了如何将 Hippy 集成到一个现有的 iOS 工程。 --- -## 使用 CocoaPods 集成 - -### 安装必要环境 - -使用`brew install cmake`安转[cmake](https://cmake.org/) - -使用`sudo gem install cocoapods`命令安装 [CocoaPods](https://cocoapods.org/) - -### 在用户自定义工程目录下创建 podfile 文件 - -```ruby - -install! "cocoapods", :generate_multiple_pod_projects => true, :deterministic_uuids => false -#hippy仅支持ios11及以上版本 -platform :ios, '11.0' -#TargetName替换成用户工程名 -target TargetName do -#使用hippy最新版本 -pod 'hippy' -#若想指定使用的hippy版本号,比如3.0.0版,请使用 -#pod 'hippy', '2.0.0' -end - -``` - -### Cocoapods接入可选参数 - -> hippy3.0模式使用了Taitank布局引擎,JavaScriptCore JS引擎,使用static library方式接入。 - -接入方可以自定义如下参数接入。 - -```ruby - -#使用Yoga布局引擎 -ENV["layout_engine"]="Yoga" -#使用v8引擎或者其他第三方js引擎 -ENV["js_engine"] = "{v8/other}" -#使用framework模式集成hippy -ENV["use_frameworks"] = "true" - -``` - -其中如果用户选择使用第三方js引擎,还需要额外实现对应js napi接口,以便hippy访问对应的实现逻辑。 - ->由于hippy3.0中使用了大量#include"path/to/file.h"方式引用c++头文件,因此如果选择使用framework方式接入,必须在podfile文件中指定 `ENV["use_frameworks"] = "true"` - -### 配置 force load 选项 - -Hippy中大量使用了反射调用。若以静态链接库形式编译Hippy代码,其中未显式调用的代码将会被编译器 dead code strip。 -因此若 App 使用静态链接库接入 hippy,务必设置 `force load` 强制加载 hippy 静态链接库所有符号。 - -> 2.13.0版本开始删除了 force load。若使用静态链接库接入,需要 app 自行配置。 - -App可使用多种方式达到 `force load` 目的,下列方式自行选择合适的一项进行配置。并要根据实际情况自行适配 - -- 直接在主工程对应的 target 的 Build Settings 中的 `Other Linker Flags` 配置中设置 `*-force_load "${PODS_CONFIGURATION_BUILD_DIR}/hippy/libhippy.a"*`。 - -- 在App工程的 Podfile 配置文件中添加 `post_install hook`,自行给 xcconfig 添加 `force load`。 - -- fork一份Hippy源码,并修改对应的 `hippy.podspec` 配置文件,并给 `user_target` 添加如下配置,再引用此源码。 - -```ruby - -s.user_target_xcconfig = {'OTHER_LDFLAGS' => '-force_load "${PODS_CONFIGURATION_BUILD_DIR}/hippy/libhippy.a"'} - -``` - -### 执行集成命令 - -完成以上工作,直接执行`pod update`即可完成集成 - -## 代码接入 - -相较于Hippy2.x版本,Hippy3.0支持了多driver与多render能力,用户可以根据需要自行选择driver与renderer。为此,与driver和renderer相关的模块,需要用户自行创建及持有。 - -当然,Hippy3.0提供了默认的JS Driver以及Native Render模块。 - -### 核心概念 - -把Hippy3.0组件集成到iOS应用中有如下几个主要步骤: - -1. 配置好集成Hippy3.0所需的依赖项,并选定集成方式 -2. 按需集成Hippy3.0所有模块,包括自定义模块与组件 -3. 准备好对应的业务代码 -4. 加载业务代码并执行 - -### 使用Hippy3.0默认组件接入 - -Hippy3.0默认提供了JS Driver驱动层以及Native Render渲染层。目前大部分业务也是使用这种方式接入。 - -### 代码集成 - -#### 1.创建HipppyBridge实例 - -我们根据HippyBridge的构造方法,创建一个HippyBridge实例 - -```objectivec - -//HippyBridge.h -/** - * Create A HippyBridge instance - * - * @param delegate HippyBridge代理对象 - * @param block 用于用户指定自定义模块 - * @param launchOptions Hippy实例初始化参数 - * @param engineKey JS引擎标识符,相同的参数将使对应JS引擎使用同一个JS VM - * @return HippyBridge实例 - */ -- (instancetype)initWithDelegate:(id)delegate - moduleProvider:(HippyBridgeModuleProviderBlock)block - launchOptions:(NSDictionary *)launchOptions - engineKey:(NSString *)engineKey; - -//调用方 -HippyBridgeModuleProviderBlock block = ^NSArray> *{ - return nil; -}; -NSDictioanry *launchOptions = @{@"key": @"value"}; -HippyBridge *bridge = [[HippyBridge alloc] initWithDelegate:self - moduleProvider:block - launchOptions:launchOptions - engineKey:@"Demo"] - -``` - ->HippyBridge是Hippy3.0默认提供的入口类,自动构建了JS Driver与Native Render的关联。 +## 一、环境准备 -#### 2.为HippyBridge配置必要的属性 +- 安装 Xcode -HippyBridge中有些必须属性,需要调用方设置。如果不设置,将会导致功能不完善。 +- 安装 [CMake](https://cmake.org/) -```objectivec + 推荐使用Homebrew安装CMake,安装命令如下: -//HippyBridge.h + ```shell + brew install cmake + ``` -//业务模块名。前端将校验此模块名。如果不匹配,Hippy实例无法启动。 -@property (nonatomic, strong) NSString *moduleName; - -//Hippy业务沙盒目录。Hippy业务方的资源相对路径。 -@property (nonatomic, strong) NSURL *sandboxDirectory; - -//VFS模块,负责Hippy实例的所有网络模块。用户可自行实现,或者使用默认 -@property(nonatomic, assign)std::weak_ptr VFSUriLoader; - -//添加Image -- (void)addImageProviderClass:(Class)cls; - -//调用方代码 -_bridge.moduleName = @"Demo" -_bridge.sandboxDirectory = [NSURL fileURLWithString:@"path/to/your/directory"]; - -//使用Hippy3.0系统默认的VFSLoader -auto demoHandler = std::make_shared(); -auto demoLoader = std::make_shared(); -demoLoader->PushDefaultHandler(demoHandler); -demoLoader->AddConvenientDefaultHandler(demoHandler); -auto fileHandler = std::make_shared(_bridge); -demoLoader->RegisterConvenientUriHandler(@"hpfile", fileHandler); - -_bridge.VFSUriLoader = demoLoader; //使用Hippy默认的vfs - -//使用系统默认的image解码器 -[_bridge addImageProviderClass:[HPDefaultImageProvider class]]; - -``` - -#### 3.创建DomManager - -```objectivec - -//可以使用下面的方法,根据engineKey获取对应的DomManager.EngineKey相同的实例获取同一个DomManager -auto engineResource = [[HippyJSEnginesMapper defaultInstance] JSEngineResourceForKey:_engineKey]; -auto domManager = engineResource->GetDomManager(); - -``` - -#### 4.创建NativeRender模块及其属性并设置给HippyBridge实例 - -```objectivec - -//先获取第三步创建的DomManager -auto engineResource = [[HippyJSEnginesMapper defaultInstance] JSEngineResourceForKey:_engineKey]; -auto domManager = engineResource->GetDomManager(); +- 安装 [CocoaPods](https://cocoapods.org/) + + [CocoaPods](https://cocoapods.org/) 是一个iOS和macOS开发中流行的包管理工具。我们将使用它把Hippy的iOS Framework添加到现有iOS项目中。 -auto nativeRenderManager = std::make_shared(); -nativeRenderManager->SetDomManager(domManager); + 推荐使用Homebrew安装CocoaPods,安装命令如下: -//设置Image解码类 -nativeRenderManager->AddImageProviderClass([HPDefaultImageProvider class]); -//设置额外的自定义组件 -nativeRenderManager->RegisterExtraComponent(_extraComponents); -//设置vfs系统 -nativeRenderManager->SetVFSUriLoader([self URILoader]); -domManager->SetRenderManager(nativeRenderManager); + ```shell + brew install cocoapods + ``` -``` + > 若想快速体验,可以直接基于Hippy仓库中的 [iOS Demo](https://github.com/Tencent/Hippy/tree/main/framework/examples/ios-demo) 来开发 -#### 5.创建指定RootView与RootNode,并赋予NativeRenderManager - -RootView可以是任意UIView实例 - -```objectivec - -//创建RootView,RootView可以是任意view实例 -UIView *rootView = [[UIView alloc] initWithFrame:frame]; -//获取第三步创建的DomManager -auto engineResource = [[HippyJSEnginesMapper defaultInstance] JSEngineResourceForKey:_engineKey]; -auto domManager = engineResource->GetDomManager(); -//获取rootview的componentTag,rootview必须实现此方法。 -NSNumber *rootTag = [rootView componentTag]; -//创建隶属于dom层的RootNode -auto rootNode = std::make_shared([rootTag unsignedIntValue]); -//设置动画管理模块的root node属性 -rootNode->GetAnimationManager()->SetRootNode(rootNode); -rootNode->SetDomManager(domManager); -//设置root node的布局节点的屏幕缩放尺度 -rootNode->GetLayoutNode()->SetScaleFactor([UIScreen mainScreen].scale); -//设置root node大小 -rootNode->SetRootSize(rootView.frame.size.width, rootView.frame.size.height); - -//给第四部创建的NativeRenderManager设置rootnode与rootview -nativeRenderManager->RegisterRootView(rootView, _rootNode); - -//设置rootview大小改变事件回调 -auto cb = [](int32_t tag, NSDictionary *params){ -}; -_nativeRenderManager->SetRootViewSizeChangedEvent(cb); +## 二、使用 Cocoapods 集成 iOS SDK -``` +具体的操作步骤如下: -#### 6.为HippyBridge设置DomManager实例与RootNode实例 +1. 首先,确定要集成的Hippy iOS SDK版本,如3.2.0,将其记录下来,接下来将在Podfile中用到。 + > 可到「[版本查询地址](https://github.com/Tencent/Hippy/releases)」查询最新的版本信息 -当DomManager和RootNode实例都创建完毕,并且所有属性都设置之后,直接调用下列方法即可绑定HippyBridge,DomManager,RootNode三者 +2. 其次,准备好现有iOS工程的 Podfile 文件 -```objectivec + Podfile 文件是CocoaPods包管理工具的配置文件,如果当前工程还没有该文件,最简单的创建方式是通过CocoaPods init命令,在iOS工程文件目录下执行如下命令: -[_bridge setupDomManager:domManager rootNode:_rootNode]; + ```shell + pod init + ``` -``` + 生成的Podfile将包含一些demo设置,您可以根据集成的目的对其进行调整。 -#### 7.加载JS bundle业务代码 + 为了将Hippy SDK集成到工程,我们需要修改Podfile,将 hippy 添加到其中,并指定集成的版本。修改后的Podfile应该看起来像这样: -当设置完HippyBridge所有配置项之后,就可以加载JS Bundle包了 + ```text + #use_frameworks! + platform :ios, '11.0' -```objectivec + # TargetName大概率是您的项目名称 + target TargetName do -NSURL *bundleURL = yourBundlePathURL; -//此方法可多次调用,加载不同的bundle包。且保证bundle包的加载顺序。 -[_bridge loadBundleURL:bundleUrl completion:completion]; + # 在此指定步骤1中记录的hippy版本号,可访问 https://github.com/Tencent/Hippy/releases 查询更多版本信息 + pod 'hippy', '3.2.0' -``` + end + ``` -#### 8.加载Hippy实例 + > 默认配置下,Hippy SDK使用布局引擎是[Taitank](https://github.com/Tencent/Taitank),JS引擎是系统的`JavaScriptCore`,如需切换使用其他引擎,请参照下文[《引擎切换(可选)》](#四引擎切换可选)一节调整配置。 -之后,使用下列方法即可加载Hippy实例 + !> 请注意,由于hippy3.x中大量使用了 #include"path/to/file.h" 的方式引用C++头文件,因此如果开启了 CocoaPods 的 framework 格式集成选项(即Podfile中 `use_frameworks!` 配置为开启状态),则必须在 Podfile 文件中加入如下配置: -```objectivec + ```text + # 工程开启 use_frameworks! 后需添加此环境变量,用于hippy使用正确设置项 + ENV["use_frameworks"] = "true" + ``` -NSNumber *rootViewTag = xxx; -NSDictionary *props = xxx;//初始化配置属性 -[_bridge loadInstanceForRootView:rootViewTag withProperties:props]; +3. 最后,在命令行中执行 -``` + ```shell + pod install + ``` -### 使用简化方法进行代码集成 - -HippyBridge提供了丰富的接口方便接入方使用各种自定义模块进行接入,当然过程也稍微繁琐一些。 -但我们预测,大部分接入方其实只会使用默认的模块接入,并不会进行自定义配置。为此我们使用HippyConvenientBridge类简化接入流程。满足条件的接入方,直接使用HippyConvenientBridge接口即可进行接入。 - ->HippyConvenient类将封装NativeRenderManager,RootNode,DomManager的创建,直接使用默认类型,接入方无需自定义。以牺牲灵活性为代价,简化接入流程。 - -#### 1.创建HippyConvenientBridge实例并配置必要的属性 - -HippyConvenientBridge封装了HippyBridge,NativeRenderManager,DomManager,RootNode之间的关系。 - -```objectivec - -//HippyConvenient.h -/** - * Create A HippyConvenient instance - * - * @param delegate HippyBridge代理对象 - * @param block 用于用户指定自定义模块 - * @param extraComponents 用于用户指定自定义组件 - * @param launchOptions Hippy实例初始化参数 - * @param engineKey JS引擎标识符,相同的参数将使对应JS引擎使用同一个JS VM - * @return HippyBridge实例 - */ -- (instancetype)initWithDelegate:(id _Nullable)delegate - moduleProvider:(HippyBridgeModuleProviderBlock _Nullable)block - extraComponents:(NSArray * _Nullable)extraComponents - launchOptions:(NSDictionary * _Nullable)launchOptions - engineKey:(NSString *_Nullable)engineKey; - -//接入方代码 -//构建HippyConvenientBridge实例 -HippyConvenientBridge *connector = [[HippyConvenientBridge alloc] initWithDelegate:self - moduleProvider:nil - extraComponents:nil - launchOptions:launchOptions - engineKey:engineKey]; -//设置沙盒目录 -connector.sandboxDirectory = sandboxDirectory; -//设置模块名 -connector.moduleName = @"Demo"; + 命令成功执行后,使用 CocoaPods 生成的 `.xcworkspace` 后缀名的工程文件来打开工程。 -``` +## 三、编写SDK接入代码,加载本地或远程的Hippy资源包 -#### 2.构建RootView并赋值给HippyConvenientBridge +Hippy SDK的代码接入简单来说只需两步: -````objectivec +1、初始化一个HippyBridge实例,HippyBridge是Hippy最重要的概念,它是终端渲染侧与前端驱动侧进行通信的`桥梁`,同时也承载了Hippy应用的主要上下文信息。 -//创建UIView实例作为RootView -//RootView实例必须实现componentTag方法 -UIView *rootView = [[UIView alloc] initWithFrame:frame]; +2、通过HippyBridge实例初始化一个HippyRootView实例,HippyRootView是Hippy应用另一个重要概念,Hippy应用将由它显示出来,因此可以说创建业务也就是创建一个 `HippyRootView`。 -//并赋值给convenientBridge -[convenientBridge setRootView:rootView]; +目前,Hippy 提供了分包加载接口以及不分包加载接口,使用方式分别如下: -```` +### 方式1. 使用分包加载接口 -#### 3.HippyConvenientBridge实例加载JS bundle包 +``` objectivec +/** 此方法适用于以下场景: + * 在业务还未启动时先准备好JS环境,并加载包1,当业务启动时加载包2,减少包加载时间 + * 我们建议包1作为基础包,与业务无关,只包含一些通用基础组件,所有业务通用 + * 包2作为业务代码加载 +*/ ->HippyConvenientBridge确保bundle加载顺序 +// 先加载包1,创建出一个HippyBridge实例 +// 假设commonBundlePath为包1的路径 +// Tips:详细参数说明请查阅头文件: HippyBridge.h +NSURL *commonBundlePath = getCommonBundlePath(); +HippyBridge *bridge = [[HippyBridge alloc] initWithDelegate:self + bundleURL:commonBundlePath + moduleProvider:nil + launchOptions:your_launchOptions + executorKey:nil]; -```objectivec +// 再通过上述bridge以及包2地址创建HippyRootView实例 +// 假设businessBundlePath为包2的路径 +// Tips:详细参数说明请查阅头文件: HippyRootView.h +HippyRootView *rootView = [[HippyRootView alloc] initWithBridge:bridge + businessURL:businessBundlePath + moduleName:@"Your_Hippy_App_Name" + initialProperties:@{} + shareOptions:nil + delegate:nil]; -[convenientBridge loadBundleURL:bundleURL1 completion:^(NSURL * _Nullable, NSError * _Nullable) { - NSLog(@"url %@ load finish", commonBundlePath); -}]; -[convenientBridge loadBundleURL:bundleURL2 completion:^(NSURL * _Nullable, NSError * _Nullable) { - NSLog(@"url %@ load finish", businessBundlePath); -}]; +// 最后,给生成的rootView设置好frame,并将其挂载到指定的VC上。 +rootView.frame = self.view.bounds; +rootView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; +[self.view addSubview:rootView]; +// 至此,您已经完成一个Hippy应用的初始化,SDK内部将自动加载资源并开始运行Hippy应用。 ``` -#### 4.HippyConvenientBridge实例加载Hippy业务实例 - -```objectivec - -[convenientBridge loadInstanceForRootViewTag:rootTag props:@{@"isSimulator": @(isSimulator)}]; - +### 方式2. 使用不分包加载接口 + +``` objectivec +// 与上述使用分包加载接口类似,首先需要创建一个HippyBridge实例, +// 区别是在创建HippyRootView实例时,无需再传入业务包,即businessBundlePath,直接使用如下接口创建即可 +// Tips:详细参数说明请查阅头文件: HippyRootView.h +- (instancetype)initWithBridge:(HippyBridge *)bridge + moduleName:(NSString *)moduleName + initialProperties:(nullable NSDictionary *)initialProperties + shareOptions:(nullable NSDictionary *)shareOptions + delegate:(nullable id)delegate; ``` -## 使用自定义模块接入 +> 在Hippy仓库中提供了一个简易示例项目,包含上述全部接入代码,以及更多注意事项。 +> +> 建议参考该示例完成SDK到已有项目的集成:[iOS Demo](https://github.com/Tencent/Hippy/tree/main/framework/examples/ios-demo),更多设置项及使用方式请查阅上述头文件中的具体API说明。 -Hippy3.0同样支持使用自定义Driver与Render层接入,接入方只需要调用对应接口或者实现对应的抽象方法即可。 +!> 使用分包加载可以结合一系列策略,比如提前预加载bridge, 全局单bridge等来优化页面打开速度。 +到这里,您已经完成了接入一个默认配置下的Hippy iOS SDK的全部过程。 -### 自定义driver层 +## 四、引擎切换(可选) -driver层负责驱动dom层构建Dom树结构,Dom将继续驱动Render层构建Render树,以及最终上屏结果。 +Hippy 3.x的一个重要特性是支持了多引擎的便捷切换,目前,可切换的引擎有两个,一是布局引擎,二是JS引擎。默认配置下,Hippy使用布局引擎是[Taitank](https://github.com/Tencent/Taitank),JS引擎是iOS系统内置的`JavaScriptCore`。 -其中Dom层并不关心driver的实现逻辑,它只会遵照driver层的命令,最终构建出UI树。 +如需使用其他布局引擎,如[Yoga](https://github.com/facebook/yoga),或使用其他JS引擎,如V8,可参考如下指引调整Hippy接入配置。 -因此,接入方实现driver层的目标,就是驱动Dom层逻辑。 +> Hippy3.x提供了iOS环境下默认的v8引擎实现,如需使用其他JS引擎需用户自行实现相关napi接口。 -```c++ +### 4.1 切换JS引擎 -//dom_manager.h -class DomManager : public std::enable_shared_from_this { - static void CreateDomNodes(const std::weak_ptr& weak_root_node, - std::vector>&& nodes); - static void UpdateDomNodes(const std::weak_ptr& weak_root_node, - std::vector>&& nodes); - static void MoveDomNodes(const std::weak_ptr& weak_root_node, - std::vector>&& nodes); - static void DeleteDomNodes(const std::weak_ptr& weak_root_node, - std::vector>&& nodes); -}; - -``` - -上面列出的是DomManager几个比较有代表性的方法,driver层通过调用上述方法来对Dom树进行增删改操作,并最终驱动UI进行更新。 - -### 自定义render层 - -render层负责UI上渲染行为。同driver一样,Hippy并不关心render层的具体实现逻辑。它只负责将dom层的信息发送给render层,由render层自行负责渲染。 - -接入方自定义的render manager只需要继承自抽象类RenderManager,并实现其虚方法,实现自定义的UI渲染能力。 - -接入方首先要做的,就是使用dom manager实例,注册自定义的render manager - -```c++ - -DomManager::SetRenderManager(const std::weak_ptr& render_manager); +如需使用V8引擎,在Podfile文件中添加如下环境变量即可: +```ruby +ENV['js_engine'] = 'v8' ``` ->注意,RenderManager实例由接入方持有。DomManager只保持弱引用。 - -之后,dom manager会将dom层的所有改变行为发送给render manager,由render manager负责渲染。 - -## 切换JS引擎接入 - -Hippy3.0默认使用JSC引擎。通过修改Podfile文件配置,[可以实现JS引擎的切换](#使用-cocoapods-集成)。 - ->Hippy3.0提供了v8引擎的实现,其他引擎需要用户实现napi接口。 ->Hippy同一时间只支持使用一种JS引擎。 - -### 切换为V8引擎 +修改后的Podfile应该看起来像这样: -用户若想使用V8引擎,直接在Podfile文件中指定js_engine为V8即可 +```text +#use_frameworks! +platform :ios, '11.0' +ENV['js_engine'] = 'v8' #切换为V8引擎 -```ruby +# TargetName大概率是您的项目名称 +target TargetName do -ENV['js_engine'] = 'v8' + pod 'hippy', 'your_specified_version' +end ``` -### 切换为自定义JS引擎 +之后,重新执行`pod install`命令更新项目依赖即可。 -用户若需要使用其他第三方JS引擎,需要做如下操作: +如需使用其他第三方JS引擎,需要做如下操作: #### 1.修改Podfile配置为第三方JS引擎 -将Podfile中的js_engine配置为other,这样在拉取代码时,不过将jsc或者v8的代码添加到工程中。 +将Podfile中的js_engine环境变量配置为other,这样在拉取代码时,jsc或者v8的代码将不会被添加到工程中。 ```ruby - ENV['js_engine'] = 'other' - ``` -> Hippy3.0中使用napi抽象了不同JS引擎的接口。其中,JSC与V8的接口进行了实现。用户若使用JSC或者V8,直接切换就好,Hippy默认进行了实现。 +> Hippy3.0中使用napi抽象了不同JS引擎的接口。其中,JSC与V8的接口进行了实现。用户若使用JSC或者V8,可直接切换,Hippy默认进行了实现。 #### 2.自行实现napi抽象接口 @@ -527,25 +262,15 @@ napi文件位于 `/driver/js/napi*` 目录下。 接入方自行将对应的napi实现文件添加到工程中。 -## 切换布局引擎接入 - -Hippy3.0默认使用Taitank布局引擎。通过修改Podfile文件配置,[可以切换使用Yoga引擎](#使用-cocoapods-集成)。 +## 4.2 切换布局引擎 -### 切换为Yoga引擎 - -用户若想使用Yoga布局引擎,直接在Podfile文件中指定layout_engine为Yoga即可 +用户若想使用Yoga布局引擎,直接在Podfile文件中指定layout_engine为Yoga即可: ```ruby - ENV['layout_engine'] = 'Yoga' - ``` -之后,直接执行`pod update`命令更新代码即可。 - -
-
-
+之后,重新执行`pod install`命令更新项目依赖即可。 # Voltron/Flutter @@ -777,4 +502,3 @@ engine.start({ }, }); ``` - diff --git a/docs/development/privacy-developer-guide.md b/docs/development/privacy-developer-guide.md new file mode 100644 index 00000000000..19af7447f14 --- /dev/null +++ b/docs/development/privacy-developer-guide.md @@ -0,0 +1,29 @@ +# Hippy SDK 开发者合规指南 + +合规三步走: + +## 第一步:确保集成Hippy SDK的App有《隐私政策》并合规展示 + +1. APP首次运行时应通过弹窗等明显方式提示用户阅读App的隐私政策 + +2. 请务必在APP隐私政策中增加以下内容,请参考[Hippy SDK个人信息保护规则](development/privacy.md): + +* 在APP《已收集个人信息清单》中展示SDK收集的用户个人信息基本情况,包括信息种类、使用目的、使用场景 + +* 在APP《与第三方共享个人信息清单》中展示提供SDK的法人主体名称、产品/类型、向SDK提供的个人信息种类、使用目的、使用场景、共享方式及SDK隐私政策链接 + +* 在APP的《第三方SDK目录》中展示接入的SDK名称、提供SDK的法人主体名称、SDK的使用目的、处理方式、采集的个人信息类型、联系方式以及隐私政策链接 + +## 第二步:APP合规接入Hippy SDK + +1. 由用户自主选择是否启用Hippy SDK + +2. Hippy SDK的使用需取得用户的有效同意 + +3. APP在非服务所必须或无合理应用场景下,不得启动Hippy SDK + +4. APP提供给SDK的个人信息,请务必保证已依法获得用户同意 + +## 第三步:延迟初始化Hippy SDK + +请务必在用户同意《隐私政策》后再初始化Hippy SDK。 diff --git a/docs/development/privacy.md b/docs/development/privacy.md index 5e68f77b2e9..597f9ef9c2c 100644 --- a/docs/development/privacy.md +++ b/docs/development/privacy.md @@ -1,66 +1,118 @@ -# Hippy SDK 个人信息保护规则 +# Hippy SDK个人信息保护规则 -生效日期:2022年4月24日 +*更新日期:24-04-10* +*生效日期:24-04-10* + +## 更新说明 + +我们对《Hippy SDK隐私保护指引》进行了更新,更新内容主要为: + +尊敬的开发者:您好,为了优化Hippy SDK 功能体验,提升Hippy SDK安全防护能力,落实监管最新要求,我们移除了剪切板module及相关功能访问接口,升级后的Hippy SDK 版本为 Android v3.2.0,iOS v3.2.0,该版本已于2024年4月10日发布。 ## 引言 -Hippy SDK (以下简称"SDK产品")由深圳市腾讯计算机系统有限公司(以下简称"我们"或称"腾讯")提供,注册地为深圳市南山区粤海街道麻岭社区科技中一路腾讯大厦35层。 +Hippy SDK(以下简称“SDK产品”)由深圳市腾讯计算机系统有限公司(以下简称“我们”)开发, 公司注册地为深圳市南山区粤海街道麻岭社区科技中一路腾讯大厦35层。 + +《Hippy SDK个人信息保护规则》(以下简称“本规则”)主要向开发者及其终端用户(“终端用户”)告知,为了实现SDK产品的相关功能,SDK产品需收集、使用和处理终端用户个人信息的情况。 + +请开发者及终端用户认真阅读本规则。如您是开发者,请您确认充分了解并同意本规则后再集成SDK产品,如果您不同意本规则及按照本规则履行对应的用户个人信息保护义务,应立即停止接入及使用SDK产品。 + +特别说明: +如您是开发者,您应当: + +1. 遵守法律、法规收集、使用和处理终端用户的个人信息, 包括但不限于制定和公布有关个人信息保护的隐私政策等; + +2. 告知终端用户SDK产品收集、使用和处理终端用户个人信息的情况,并依法征得终端用户同意,在征得终端用户同意后初始化SDK产品; -《Hippy SDK 个人信息保护规则》(以下简称"本规则")主要向开发者及其终端用户("终端用户")告知,为了实现SDK产品的相关功能,SDK产品需收集、使用和处理终端用户个人信息的情况。 +3. 在征得终端用户的同意前、以及在用户触发相应功能场景前,除非法律法规另有规定,不应收集任何终端用户的个人信息; -请开发者及终端用户认真阅读本规则。如您是开发者,请您确认充分了解并同意本规则后再集成SDK产品,如果您不同意本规则及按照本规则履行对应的用户个人信息保护义务,应立即停止接入及使用SDK产品;同时,您应仅在征得终端用户的同意后启动或使用SDK产品并处理终端用户的个人信息。 +4. 应按您的应用的具体功能场景,在用户触发具体功能场景时调用SDK的相应功能、调用相应权限或处理终端用户的个人信息,未到具体功能场景时不应调用相应的SDK功能、调用相应权限或处理终端用户的个人信息。 -特别说明:
-如您是开发者,您应当: +5. 向终端用户提供易于操作且满足法律法规要求的用户权利实现机制, 并告知终端用户如何查阅、复制、修改、删除个人信息, 撤回同意, 以及限制个人信息处理、转移个人信息、获取个人信息副本和注销账号; -1. 遵守法律、法规收集、使用和处理终端用户的个人信息, 包括但不限于制定和公布有关个人信息保护的隐私政策等;
-2. 在启动或使用SDK产品前,告知终端用户SDK产品收集、使用和处理终端用户个人信息的情况,并依法征得终端用户同意;
-3. 在征得终端用户的同意前,除非法律法规另有规定,不应收集任何终端用户的个人信息;
-4. 向终端用户提供易于操作且满足法律法规要求的用户权利实现机制,并告知终端用户如何查阅、复制、修改、删除个人信息,撤回同意,以及限制个人信息处理、转移个人信息、获取个人信息副本和注销账号;
-5. 遵守本规则的要求。
+6. 遵守本规则的要求,并详细阅读《SDK接入使用文档》查看详细操作指引。 如开发者和终端用户对本规则内容有任何疑问或建议, 可随时通过本规则第八条提供的方式与我们联系。 ## 一、我们收集的信息及我们如何使用信息 -### (一)为实现 SDK 产品功能所需的权限 +### (一) 为实现SDK产品功能所需收集的个人信息 + +为实现SDK产品的相应功能所必须,我们将向终端用户或开发者收集终端用户在使用与SDK产品相关的功能时产生的如下个人信息: + +#### 1. Hippy SDK iOS版 + +| 个人信息类型 | 使用目的及功能场景 | 处理方式 | +| --- | --- | --- | +|手机型号【必选】|提供调试服务时显示设备的基本信息,作为环境信息提供给开发框架的使用者|仅读取,不保存到本地,也不上传服务器| + +Hippy SDK iOS版当前不存在拓展功能,无可选个人信息。 + +#### 2. Hippy SDK Android版 + +Android版SDK不会为实现SDK产品功能而收集个人信息。 + +### (二) 为实现SDK产品功能所需的权限 + +SDK不会为实现SDK产品功能而申请敏感权限。 -为实现SDK产品的相应功能所必须,我们会通过开发者的应用申请所需权限。 +### (三) 根据法律法规的规定,以下是征得用户同意的例外情形 -| 操作系统 | 权限名称 | 使用目的 | 是否可选 | -| ------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -------- | -| iOS/Android | 剪切板 | 向终端用户或开发者提供访问剪切板的功能,帮助用户用户完成相关内容的复制粘贴 | 可选 +1. 为订立、履行与终端用户的合同所必需; -请注意,在不同设备和系统中,权限显示方式及关闭方式会有所不同,需同时参考其使用的设备及操作系统开发方的说明或指引。当终端用户关闭权限即代表其取消了相应的授权,我们和开发者将不会继续收集和使用相关权限所对应的个人信息, 也无法为终端用户提供需要终端用户开启权限才能提供的对应的功能。 +2. 为履行我们的法定义务所必需; -### (二)根据法律法规的规定,以下是征得用户同意的例外情形 +3. 为应对突发公共卫生事件, 或者紧急情况下为保护终端用户的生命健康和财产安全所必需; + +4. 为公共利益实施新闻报道、舆论监督等行为, 在合理的范围内处理终端用户的个人信息; + +5. 依照本法规定在合理的范围内处理终端用户自行公开或者其他已经合法公开的个人信息; -1. 为订立、履行与终端用户的合同所必需; -2. 为履行我们的法定义务所必需; -3. 为应对突发公共卫生事件,或者紧急情况下为保护终端用户的生命健康和财产安全所必需; -4. 为公共利益实施新闻报道、舆论监督等行为, 在合理的范围内处理终端用户的个人信息; -5. 依照本法规定在合理的范围内处理终端用户自行公开或者其他已经合法公开的个人信息; 6. 法律行政法规规定的其他情形。 -特别提示: 如我们收集的信息无法单独或结合其他信息识别到终端用户的个人身份,其不属于法律意义上的个人信息。 +特别提示: 如我们收集的信息无法单独或结合其他信息识别到终端用户的个人身份, 其不属于法律意义上的个人信息。 + +## 二、第三方数据处理及信息的公开披露 + +我们不会与我们的关联公司、合作伙伴及第三方共享(“接收方”)终端用户的个人信息。 + +我们不会将终端用户的个人信息转移给任何公司、组织和个人, 但以下情况除外: + +1. 事先告知终端用户转移个人信息的种类、目的、方式和范围,并征得终端用户的单独同意; + +2. 如涉及合并、分立、解散、被宣告破产等原因需要转移个人信息的, 我们会向终端用户告知接收方的名称或者姓名和联系方式, 并要求接收方继续履行个人信息处理者的义务。接收方变更原先的处理目的、处理方式的, 我们会要求接收方重新取得终端用户的同意。 + +我们不会公开披露终端用户的个人信息, 但以下情况除外: -### (三)如何使用 Cookie +1. 告知终端用户公开披露的个人信息的种类、目的、方式和范围并征得终端用户的单独同意后; -为实现APP产品功能所必需,SDK按照Cookie规范为开发者提供基于业务需要使用Cookie技术的设置,修改、删除、存储能力。SDK不会收集、上报、使用通过Cookie收集的任何信息。如您是终端用户,您需要通过开发者提供的渠道管理和删除Cookie。 +2. 在法律法规、法律程序、诉讼或政府主管部门强制要求的情况下。 -如您是开发者,您应当向终端用户告知与SDK产品相关的Cookie使用情况,包括但不限于Cookie的类型、收集的个人信息、使用目的、使用场景,征得终端用户的同意,并向终端用户提供管理和删除Cookie的机制。 +## 三、终端用户如何管理自己的信息 -## 二、终端用户如何管理自己的信息 +我们非常重视终端用户对其个人信息管理的权利, 并竭力帮助终端用户管理个人信息,包括个人信息查阅、复制、删除、注销账号以及设置隐私功能等, 以保障终端用户的权利。 -我们非常重视终端用户对其个人信息管理的权利, 并竭力帮助终端用户管理个人信息,包括个人信息查阅、复制、删除、注销账号以及设置隐私功能等, 以保障终端用户的权利。如您是开发者,您应当为终端用户提供实现查阅、复制、修改、删除个人信息、撤回同意和注销账号的方式。 +如您是开发者,您应当为终端用户提供实现查阅、复制、修改、删除个人信息、撤回同意和注销账号的方式。 -基于终端用户的同意而进行的个人信息处理活动, 终端用户有权撤回该同意。我们已向开发者提供关闭本SDK产品的能力,请开发者点击[此处](https://hippyjs.org/#/android/integration)查看操作指引。由于我们与终端用户无直接的交互对话界面,终端用户可以直接联系开发者停止使用本SDK产品。如您是终端用户,请您理解,特定的业务功能或服务需要您提供服务所需的信息才能得以完成,当您撤回同意后,我们无法继续为您提供对应的功能或服务,也不再处理您相应的个人信息。您撤回同意的决定,不会影响我们此前基于您的授权而开展的个人信息处理。 +开发者可以通过本规则第八条提供的方式联系我们,以便开发者帮助终端用户管理自己的信息。 -## 三、信息的存储 +如您是终端用户,由于我们与您无直接的交互对话界面, 您可以直接联系开发者管理您的个人信息,也可通过本规则第八条提供的方式与我们联系。请您理解,特定的业务功能或服务需要您提供服务所需的信息才能得以完成,当您撤回同意后,我们无法继续为您提供对应的功能或服务,也不再处理您相应的个人信息。您撤回同意的决定,不会影响我们此前基于您的授权而开展的个人信息处理。 -Hippy SDK不存储用户的任何信息。 +## 四、信息的存储 -## 四、信息安全 +(一) 存储信息的地点 + +我们遵守法律法规的规定,将在中华人民共和国境内收集和产生的个人信息存储在境内。 + +(二) 存储信息的期限 + +一般而言,我们仅在为实现目的所必需的最短时间内保留终端用户的个人信息,但下列情况除外: + +* 为遵守适用的法律法规等有关规定; +* 为遵守法院判决、裁定或其他法律程序的规定; +* 为遵守相关政府机关执法的要求。 + +## 五、信息安全 我们为终端用户的个人信息提供相应的安全保障,以防止信息的丢失、不当使用、未经授权访问或披露。 @@ -72,22 +124,20 @@ Hippy SDK不存储用户的任何信息。 若发生个人信息泄露等安全事件,我们会启动应急预案,阻止安全事件扩大,并以推送通知、公告等形式告知开发者。 -## 五、变更 - -我们会适时修订本规则的内容。 +## 六、未成年人保护 -如本规则的修订会导致终端用户在本规则项下权利的实质减损,我们将在变更生效前,通过网站公告等方式进行告知。如您是开发者,当更新后的本规则对处理终端用户的个人信息情况有变动的,您应当适时更新隐私政策,并以弹框形式通知终端用户并且征得其同意,如果终端用户不同意接受本规则, 请停止启用或使用SDK产品。 +本SDK产品主要面向成年人。 -## 六、联系我们 +若您是开发者,如果终端用户是未满14周岁的未成年人(“儿童”),您应当向儿童的父母或其他监护人告知本规则,并在征得儿童的父母或其他监护人同意的前提下处理儿童个人信息。如果我们发现开发者未征得儿童监护人同意向我们提供儿童个人信息的,我们将会采取措施尽快删除。 -我们设立了专门的个人信息保护团队和个人信息保护负责人, 如果开发者和/或终端用户对本规则或个人信息保护相关事宜有任何疑问或投诉、建议时, 可以通过以下方式与我们联系:
-(i)通过 与我们联系;
-(ii)将问题发送至
-(iii)邮寄信件至: 中国广东省深圳市南山区海天二路33号腾讯滨海大厦 数据隐私保护部(收)
-邮编: 518054。
-我们将尽快审核所涉问题, 并在15个工作日或法律法规规定的期限内予以反馈。 +若您是儿童监护人,当您对您所监护儿童个人信息保护有相关疑问或权利请求时,您可以联系开发者, 或通过本规则第八条提供的方式与我们联系。 +## 七、变更 +我们会适时修订本规则的内容。 +如本规则的修订会导致终端用户在本规则项下权利的实质减损,我们将在变更生效前,通过网站公告等方式进行告知。如您是开发者,当更新后的本规则对处理终端用户的个人信息情况有变动的,您应当适时更新隐私政策,并以弹框形式通知终端用户并且征得其同意,如果终端用户不同意接受本规则, 请停止集成SDK产品。 +## 八、联系我们 +我们设立了专门的个人信息保护团队和个人信息保护负责人, 如果开发者和/或终端用户对本规则或个人信息保护相关事宜有任何疑问或投诉、建议时, 可以通过以下方式与我们联系: (i)通过 https://kf.qq.com/ 与我们联系;(ii)将问题发送至 Dataprivacy@tencent.com;(iii)邮寄信件至: 中国广东省深圳市南山区海天二路33号腾讯滨海大厦 数据隐私保护部(收) 邮编: 518054。我们将尽快审核所涉问题, 并在15个工作日或法律法规规定的期限内予以反馈。 diff --git a/docs/development/react-vue-3.0-integration-guidelines.md b/docs/development/react-vue-3.0-integration-guidelines.md new file mode 100644 index 00000000000..5ad00db5761 --- /dev/null +++ b/docs/development/react-vue-3.0-integration-guidelines.md @@ -0,0 +1,479 @@ + +# Hippy React&Vue SDK接入指引 + +Hippy 同时支持 React 和 Vue 两种 UI 框架,通过 [@hippy/react](//www.npmjs.com/package/@hippy/react) 和 [@hippy/vue](//www.npmjs.com/package/@hippy/vue) 及 [@hippy/vue-next](//www.npmjs.com/package/@hippy/vue-next) 三个包提供实现。 + +# hippy-react + +[[hippy-react 介绍]](api/hippy-react/introduction.md) [[范例工程]](https://github.com/Tencent/Hippy/tree/master/examples/hippy-react-demo) + +hippy-react 工程暂时只能通过手工配置初始化,建议直接 clone 范例工程并基于它进行修改。 + +当然,也可以从头开始进行配置。 + +## 准备 hippy-react 运行时依赖 + +请使用 `npm i` 安装以下 npm 包。 + +| 包名 | 说明 | +| ------------------- | -------------------------- | +| @hippy/react | hippy-react 运行时和渲染层 | +| react | react 本体 | +| regenerator-runtime | async/await 转换运行时 | + +## 准备 hippy-react 编译时依赖 + +以官方提供的 [范例工程](//github.com/Tencent/Hippy/tree/master/examples/hippy-react-demo) 范例工程为例,需要使用 `npm i -D` 准备好以下依赖,当然开发者可以根据需要自行选择: + +必须的: + +| 包名 | 说明 | +| --------------------------------------- | ---------------------------------------------- | +| @babel/plugin-proposal-class-properties | Babel 插件 - 支持仍在草案的 Class Properties | +| @babel/preset-env | Babel 插件 - 根据所设置的环境选择 polyfill | +| @babel/preset-react | Babel 插件 - 转译 JSX 到 JS | +| @hippy/debug-server | Hippy 前终端调试服务 | +| @babel/core | Babel - 高版本 ES 转换为 ES6 和 ES5 的转译程序 | +| babel-loader | Webpack 插件 - 加载 Babel 转译后的代码 | +| webpack | Webpack 打包程序 | +| webpack-cli | Webpack 命令行 | + +可选的: + +| 包名 | 说明 | +| ----------------------------------- | ------------------------------------------ | +| @hippy/hippy-live-reload-polyfill | live-reload 必备脚本 - 会在调试模式编译时注入代码到工程里 | +| @hippy/hippy-dynamic-import-plugin | 动态加载插件 - 拆分出子包用于按需加载 +| @babel/plugin-x | Babel 其余相关插件,如 `@babel/plugin-proposal-nullish-coalescing-operator` 等 | +| case-sensitive-paths-webpack-plugin | Webpack 插件,对 import 文件进行大小写检查 | +| file-loader | 静态文件加载 | +| url-loader | 静态文件以 Base64 形式加载 | + +## hippy-react 编译配置 + +当前 hippy-react 采用 `Webpack 4`构建,配置全部放置于 [scripts](//github.com/Tencent/Hippy/tree/master/examples/hippy-react-demo/scripts) 目录下,其实只是 [webpack](//webpack.js.org/) 的配置文件,建议先阅读 [webpack](//webpack.js.org/) 官网内容,具备一定基础后再进行修改。 + +### hippy-react 开发调试编译配置 + +该配置展示了将 Hippy 运行于终端的最小化配置。 + +| 配置文件 | 说明 | +| ------------------------------------------------------------ | ---------- | +| [hippy-webpack.dev.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-react-demo/scripts/hippy-webpack.dev.js) | 调试用配置 | + +### hippy-react 生产环境编译配置 + +生产环境和开发调试的包主要有两个区别: + +1. 生产环境开启了 production 模式,去掉调试信息,关闭了 `watch`(watch 模式下会监听文件变动并重新打包)。 +2. 终端内很可能不止运行一个 Hippy 业务,所以将共享的部分单独拆出来做成了 `vendor` 包,这样可以有效减小业务包体积,这里使用了 [DllPlugin](//webpack.js.org/plugins/dll-plugin/) 和 [DllReferencePlugin](//webpack.js.org/plugins/dll-plugin/#dllreferenceplugin) 来实现。 + +| 配置文件 | 说明 | +| ------------------------------------------------------------ | ----------------------------- | +| [vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-react-demo/scripts/vendor.js) | vendor 包中需要包含的共享部分 | +| [hippy-webpack.ios.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-react-demo/scripts/hippy-webpack.ios.js) | iOS 业务包配置 | +| [hippy-webpack.ios-vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-react-demo/scripts/hippy-webpack.ios-vendor.js) | iOS Vendor 包配置 | +| [hippy-webpack.android.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-react-demo/scripts/hippy-webpack.android.js) | Android 业务包配置 | +| [hippy-webpack.android-vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-react-demo/scripts/hippy-webpack.android-vendor.js) | Android Vendor 包配置 | + +如果仔细观察 webpack 配置,可以看出 iOS 和 Android 配置相差不大,但因为 iOS 上受苹果政策影响只能使用 [JavaScriptCore](//developer.apple.com/documentation/javascriptcore)(以下简称 JSC)作为运行环境,而 JSC 是跟随 iOS 操作系统的,无法进行独立升级,低版本 iOS 带的 JSC 甚至无法完整支持 ES6,所以需要输出一份 ES5 版本的 JS 代码。而 Android 下可以使用独立升级的 [X5](//x5.tencent.com/) 中的 V8 作为运行环境,就可以直接使用 ES6 代码了。 + +!> **特别说明:** JS 可以使用的语法受到 iOS 覆盖的最低版本的影响,绝大多数能力可以通过 `@babel/preset-env` 自动安装 polyfill,但是部分特性不行,例如要使用 [Proxy](//caniuse.com/#feat=proxy),就无法覆盖 iOS 10 以下版本。 + +## hippy-react 入口文件 + +入口文件非常简单,只是从 hippy-react 里初始化一个 Hippy 实例。注意,入口文件组件需要通过单节点包裹,如下: + +```js +import { Hippy } from '@hippy/react'; +import App from './app'; + +new Hippy({ + appName: 'Demo', // 终端分配的业务名称 + entryPage: App, // 对应业务启动时的组件 + silent: false, // 设置为 true 可以关闭框架日志输出 +}).start(); + +// P.S. entryPage需要通过单节点包裹,不能用数组的形式,例如 +import React from 'react'; +import { + View, + Text, +} from '@hippy/react'; +export default function app() { + // 入口文件不要使用这种形式,非入口文件可以使用 + return [ + , + test test + ]; + // 修改成通过单节点包裹 + return ( + , + test test + ); +} + +``` + +## hippy-react npm 脚本 + +在 [package.json](//github.com/Tencent/Hippy/blob/master/examples/hippy-react-demo/package.json#L13) 中提供了几个以 `hippy:`开头的 npm 脚本,可用来启动 [@hippy/debug-server-next](//www.npmjs.com/package/@hippy/debug-server-next) 等调试工具。 + +```json + "scripts": { +"hippy:dev": "node ./scripts/env-polyfill.js hippy-dev --config ./scripts/hippy-webpack.dev.js", +"hippy:vendor": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.ios-vendor.js --config ./scripts/hippy-webpack.android-vendor.js", +"hippy:build": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.ios.js --config ./scripts/hippy-webpack.android.js" +} +``` + +## hippy-react 转 Web + +请参考专门的 [hippy-react 转 Web 章节](api/hippy-react/web.md)。 + +# hippy-vue + +>注意:因vue2.x版本将停止更新,建议用户升级至使用vue3.x版本的@hippy/vue-next + +[[hippy-vue 介绍]](api/hippy-vue/introduction.md) [[范例工程]](//github.com/Tencent/Hippy/tree/master/examples/hippy-vue-demo) + +hippy-vue 相对简单很多,hippy-vue 只是 [Vue](//vuejs.org) 在终端上的渲染层,组件也基本和浏览器保持一致。可以通过 [vue-cli](//cli.vuejs.org/) 先[创建一个 Web 项目](//cli.vuejs.org/zh/guide/creating-a-project.html),然后加上一些 hippy-vue 的内容就可以直接将网页渲染到终端了。 + +## 准备 hippy-vue 运行时依赖 + +请使用 `npm i` 安装以下 npm 包,保证运行时正常。 + +| 包名 | 说明 | +| --------------------------- | -------------------------------- | +| @hippy/vue | hippy-vue 运行时核心 | +| @hippy/vue-native-components | hippy-vue 的扩展终端组件 | +| @hippy/vue-router | vue-router 在 hippy-vue 上的移植 | + +## hippy-vue 编译时依赖 + +以官方提供的 [范例工程](//github.com/Tencent/Hippy/tree/master/examples/hippy-vue-demo) 范例工程为例,需要使用 `npm i -D` 准备好以下依赖,当然开发者可以根据需要自行选择: + +必须的: + +| 包名 | 说明 | +| -------------------- | ------------------------------------------ | +| @hippy/debug-server | Hippy 前终端调试服务 | +| @hippy/vue-css-loader | hippy-vue 的 CSS 文本到 JS 语法树转换 | +| @babel/preset-env | Babel 插件 - 根据所设置的环境选择 polyfill | +| @babel/core | Babel - 高版本 ES 转换为 ES6 和 ES5 的转译程序 | +| babel-loader | Webpack 插件 - 加载 Babel 转译后的代码 | +| webpack | Webpack 打包程序 | +| webpack-cli | Webpack 命令行 | + +可选的: + +| 包名 | 说明 | +| ----------------------------------- | ------------------------------------------ | +| case-sensitive-paths-webpack-plugin | Webpack 插件,对 import 文件进行大小写检查 | +| @hippy/hippy-live-reload-polyfill | live-reload 必备脚本 - 会在调试模式编译时注入代码到工程里 | +| @hippy/hippy-dynamic-import-plugin | 动态加载插件 - 拆分出子包用于按需加载 +| @babel/plugin-x | Babel 其余相关插件,如 `@babel/plugin-proposal-nullish-coalescing-operator` 等 | +| file-loader | 静态文件加载 | +| url-loader | 静态文件以 Base64 形式加载 | + +## hippy-vue 编译配置 + +当前 hippy-vue 采用 `Webpack 4`构建,配置全部放置于 [scripts](//github.com/Tencent/Hippy/tree/master/examples/hippy-vue-demo/scripts) 目录下,其实只是 [webpack](//webpack.js.org/) 的配置文件,建议先阅读 [webpack](//webpack.js.org/) 官网内容,具备一定基础后再进行修改。 + +### hippy-vue 开发调试编译配置 + +该配置展示了将 Hippy 运行于终端的最小化配置。 + +| 配置文件 | 说明 | +| ------------------------------------------------------------ | ---------- | +| [hippy-webpack.dev.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/scripts/hippy-webpack.dev.js) | 调试用配置 | + +### hippy-vue 生产环境编译配置 + +线上包和开发调试用包主要有两个区别: + +1. 开启了 production 模式,去掉调试信息,关闭了 `watch`(watch 模式下会监听文件变动并重新打包)。 +2. 终端内很可能不止运行一个 Hippy 业务,所以将共享的部分单独拆出来做成了 `vendor` 包,这样可以有效减小业务包体积,这里使用了 [DllPlugin](//webpack.js.org/plugins/dll-plugin/) 和 [DllReferencePlugin](//webpack.js.org/plugins/dll-plugin/#dllreferenceplugin) 来实现。 + +| 配置文件 | 说明 | +| ------------------------------------------------------------ | ----------------------------- | +| [vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/scripts/vendor.js) | vendor 包中需要包含的共享部分 | +| [hippy-webpack.ios.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/scripts/hippy-webpack.ios.js) | iOS 业务包配置 | +| [hippy-webpack.ios-vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/scripts/hippy-webpack.ios-vendor.js) | iOS Vendor 包配置 | +| [hippy-webpack.android.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/scripts/hippy-webpack.android.js) | Android 业务包配置 | +| [hippy-webpack.android-vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/scripts/hippy-webpack.android-vendor.js) | Android Vendor 包配置 | + +如果仔细观察 webpack 配置,可以看出 iOS 和 Android 配置相差不大,但因为 iOS 上受苹果政策影响只能使用 [JavaScriptCore](//developer.apple.com/documentation/javascriptcore)(以下简称 JSC)作为运行环境,而 JSC 是跟随 iOS 操作系统的,无法进行独立升级,低版本 iOS 带的 JSC 甚至无法完整支持 ES6,所以需要输出一份 ES5 版本的 JS 代码。而 Android 下可以使用独立升级的 [X5](//x5.tencent.com/) 中的 V8 作为运行环境,就可以直接使用 ES6 代码了。 + +!> **特别说明:** JS 可以使用的语法受到 iOS 覆盖的最低版本的影响,绝大多数能力可以通过 `@babel/preset-env` 自动安装 polyfill,但是部分特性不行,例如要使用 [Proxy](//caniuse.com/#feat=proxy),就无法覆盖 iOS 10 以下版本。 + +## hippy-vue 入口文件 + +hippy-cli 初始化的项目自带了一个 [Web 端入口文件](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/src/main.js),可以保留着用来启动 Web 端网页,但是因为 hippy-vue 的启动参数不一样,需要专门的 [终端入口文件](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/src/main-native.js)来加载一些终端上用到的模块。 + +```js +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import HippyVueNativeComponents from '@hippy/vue-native-components'; +import App from './app.vue'; +import routes from './routes'; +import { setApp } from './util'; + +// 禁止框架调试信息输出,取消注释即可使用。 +// Vue.config.silent = true; + +Vue.config.productionTip = false; + +// Hippy 终端组件扩展中间件,可以使用 modal、view-pager、tab-host、ul-refresh 等终端扩展组件了。 +Vue.use(HippyVueNativeComponents); +Vue.use(VueRouter); + +const router = new VueRouter(routes); + +/** + * 声明一个 app,这是同步生成的 + */ +const app = new Vue({ + // 终端指定的 App 名称 + appName: 'Demo', + // 根节点,必须是 Id,当根节点挂载时才会触发上屏 + rootView: '#root', + // 渲染自己 + render: h => h(App), + // iPhone 下的状态栏配置 + iPhone: { + // 状态栏配置 + statusBar: { + // 禁用状态栏自动填充 + // disabled: true, + + // 状态栏背景色,如果不配的话,会用 4282431619,也就是 #40b883 - Vue 的绿色 + // 因为运行时只支持样式和属性的实际转换,所以需要用下面的转换器将颜色值提前转换,可以在 Node 中直接运行。 + // hippy-vue-css-loader/src/compiler/style/color-parser.js + backgroundColor: 4283416717, + + // 状态栏背景图,要注意这个会根据容器尺寸拉伸。 + // backgroundImage: '//mat1.gtimg.com/www/qq2018/imgs/qq_logo_2018x2.png', + }, + }, + // 路由 + router, +}); + +/** + * $start 是 Hippy 启动完以后触发的回调 + * Vue 会在 Hippy 启动之前完成首屏 VDOM 的渲染,所以首屏性能非常高 + * 在 $start 里可以通知终端说已经启动完成,可以开始给前端发消息了。 + */ +app.$start((/* app */) => { + // 这里干一点 Hippy 启动后的需要干的事情,比如通知终端前端已经准备完毕,可以开始发消息了。 + // setApp(app); +}); + +/** + * 保存 app 供后面通过 app 接受来自终端的事件。 + * + * 之前是放到 $start 里的,但是有个问题时因为 $start 执行太慢,如果首页就 getApp() 的话可能会 + * 导致获得了 undefined,然后监听失败。所以挪出来了。 + * + * 但是终端事件依然要等到 $start 也就是 Hippy 启动之后再发,因为之前桥尚未建立,终端发消息前端也 + * 接受不到。 + */ +setApp(app); +``` + +## hippy-vue npm 脚本 + +在 [package.json](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-demo/package.json#L13) 中提供了几个以 `hippy:`开头的 npm 脚本,可用来启动 [@hippy/debug-server-next](//www.npmjs.com/package/@hippy/debug-server-next) 等调试工具。 + +```json + "scripts": { +"hippy:dev": "node ./scripts/env-polyfill.js hippy-dev --config ./scripts/hippy-webpack.dev.js", +"hippy:vendor": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.ios-vendor.js --config ./scripts/hippy-webpack.android-vendor.js", +"hippy:build": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.ios.js --config ./scripts/hippy-webpack.android.js" +}, +``` + +## hippy-vue 路由 + +`@hippy/vue-router` 完整支持 vue-router 中的跳转功能,具体请参考 [hippy-vue-router](api/hippy-vue/router.md) 文档。 + +# hippy-vue-next + +[[hippy-vue-next 介绍]](api/hippy-vue/vue3) [[范例工程]](//github.com/Tencent/Hippy/tree/master/examples/hippy-vue-next-demo) + +hippy-vue-next 是 [Vue](//cn.vuejs.org) 在终端上的渲染层,组件也基本和浏览器保持一致。可以通过脚手架 [vue-cli](//github.com/vuejs/vue-cli) 先[创建一个 Web 项目](//cli.vuejs.org/zh/guide/creating-a-project.html#vue-create),然后加上一些 hippy-vue-next 的内容就可以直接将网页渲染到终端了。也可以参考我们的范例项目来初始化你的项目。 +>注意这里使用vue-cli创建项目时构建工具要选择webpack,并且Router和Typescript需要勾选,我们的hippy-vue-next默认都是基于Typescript开发的 + +## 准备 hippy-vue-next 运行时依赖 + +请使用 `npm i` 安装以下 npm 包,保证运行时正常。 + +| 包名 | 说明 | +|---------------|-------------------------------------| +| @hippy/vue-next | hippy-vue-next 运行时核心 | + +## hippy-vue-next 编译时依赖 + +以官方提供的 [范例工程](//github.com/Tencent/Hippy/tree/master/examples/hippy-vue-next-demo) 范例工程为例,需要使用 `npm i -D` 准备好以下依赖,当然开发者可以根据需要自行选择: + +必须的: + +| 包名 | 说明 | +|------------------------------|------------------------------------| +| @hippy/debug-server-next | Hippy 前终端调试服务 | +| @hippy/vue-css-loader | hippy-vue-next 的 CSS 文本到 JS 语法树转换 | +| @hippy/vue-next-style-parser | hippy-vue-next 的样式 parser | +| @babel/preset-env | Babel 插件 - 根据所设置的环境选择 polyfill | +| @babel/core | Babel - 高版本 ES 转换为 ES6 和 ES5 的转译程序 | +| babel-loader | Webpack 插件 - 加载 Babel 转译后的代码 | +| webpack | Webpack 打包程序 | +| webpack-cli | Webpack 命令行 | + +可选的: + +| 包名 | 说明 | +|-------------------------------------|-----------------------------------------------------------------------| +| case-sensitive-paths-webpack-plugin | Webpack 插件,对 import 文件进行大小写检查 | +| @hippy/hippy-live-reload-polyfill | live-reload 必备脚本 - 会在调试模式编译时注入代码到工程里 | +| @hippy/hippy-dynamic-import-plugin | 动态加载插件 - 拆分出子包用于按需加载 | +| @hippy/vue-router-next-history | 支持按安卓物理返回键回退路由 | +| @babel/plugin-x | Babel 其余相关插件,如 `@babel/plugin-proposal-nullish-coalescing-operator` 等 | +| file-loader | 静态文件加载 | +| url-loader | 静态文件以 Base64 形式加载 | +| esbuild & esbuild-loader | 开发环境webpack支持使用esbuild构建,性能比babel更好 | + +## hippy-vue-next 编译配置 + +当前 hippy-vue-next 支持 `Webpack 4 或 Webpack 5`构建,配置全部放置于 [scripts](//github.com/Tencent/Hippy/tree/master/examples/hippy-vue-next-demo/scripts) 目录下,其实只是 [webpack](//webpack.js.org/) 的配置文件,建议先阅读 [webpack](//webpack.js.org/) 官网内容,具备一定基础后再进行修改。 + +### hippy-vue-next 开发调试编译配置 + +该配置展示了将 Hippy 运行于终端的最小化配置。 + +| 配置文件 | 说明 | +|--------------------------------------------------------------------------------------------------------------------------| ---------- | +| [hippy-webpack.dev.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/scripts/hippy-webpack.dev.js) | 调试用配置 | + +### hippy-vue-next 生产环境编译配置 + +线上包和开发调试用包主要有两个区别: + +1. 开启了 production 模式,去掉调试信息,关闭了 `watch`(watch 模式下会监听文件变动并重新打包)。 +2. 终端内很可能不止运行一个 Hippy 业务,所以将共享的部分单独拆出来做成了 `vendor` 包,这样可以有效减小业务包体积,这里使用了 [DllPlugin](//webpack.js.org/plugins/dll-plugin/) 和 [DllReferencePlugin](//webpack.js.org/plugins/dll-plugin/#dllreferenceplugin) 来实现。 + +| 配置文件 | 说明 | +|-------------------------------------------------------------------------------------------------------------------------------------------| ----------------------------- | +| [vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/scripts/vendor.js) | vendor 包中需要包含的共享部分 | +| [hippy-webpack.ios.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios.js) | iOS 业务包配置 | +| [hippy-webpack.ios-vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios-vendor.js) | iOS Vendor 包配置 | +| [hippy-webpack.android.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/scripts/hippy-webpack.android.js) | Android 业务包配置 | +| [hippy-webpack.android-vendor.js](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/scripts/hippy-webpack.android-vendor.js) | Android Vendor 包配置 | + +如果仔细观察 webpack 配置,可以看出 iOS 和 Android 配置相差不大,但因为 iOS 上受苹果政策影响只能使用 [JavaScriptCore](//developer.apple.com/documentation/javascriptcore)(以下简称 JSC)作为运行环境,而 JSC 是跟随 iOS 操作系统的,无法进行独立升级,低版本 iOS 带的 JSC 甚至无法完整支持 ES6,所以需要输出一份 ES5 版本的 JS 代码。而 Android 下可以使用独立升级的 [X5](//x5.tencent.com/) 中的 V8 作为运行环境,就可以直接使用 ES6 代码了。 + +!> **特别说明:** JS 可以使用的语法受到 iOS 覆盖的最低版本的影响,绝大多数能力可以通过 `@babel/preset-env` 自动安装 polyfill,但是部分特性不行,例如要使用 [Proxy](//caniuse.com/#feat=proxy),就无法覆盖 iOS 10 以下版本,而hippy-vue-next是基于vue-next的,因此使用hippy-vue-next iOS版本必须要10及以上。 + +## hippy-vue-next 入口文件 + +因为 hippy-vue-next 的启动参数与 web 页面不一样,所以我们需要专门的 [终端入口文件](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/src/main-native.ts)来加载一些终端上用到的模块,并作为项目的入口文件 + +```ts +// 首先导入所需模块 +import { + createApp, + type HippyApp, + EventBus, + setScreenSize, + BackAndroid, +} from '@hippy/vue-next'; + +import App from './app.vue'; +import { createRouter } from './routes'; +import { setGlobalInitProps } from './util'; + +// 创建 hippy app 实例 +const app: HippyApp = createApp(App, { + // hippy native module name + appName: 'Demo', + iPhone: { + // config of statusBar + statusBar: { + // disable status bar autofill + // disabled: true, + + // Status bar background color, if not set, it will use 4282431619, as #40b883, Vue default green + // hippy-vue-css-loader/src/compiler/style/color-parser.js + backgroundColor: 4283416717, + + // 状态栏背景图,要注意这个会根据容器尺寸拉伸。 + // backgroundImage: 'https://user-images.githubusercontent.com/12878546/148737148-d0b227cb-69c8-4b21-bf92-739fb0c3f3aa.png', + }, + }, + // do not print trace info when set to true + // silent: true, + /** + * whether to trim whitespace on text element, + * default is true, if set false, it will follow vue-loader compilerOptions whitespace setting + */ + trimWhitespace: true, +}); +// create router +const router = createRouter(); +app.use(router); + +// init callback +const initCallback = ({ superProps, rootViewId }) => { + setGlobalInitProps({ + superProps, + rootViewId, + }); + /** + * Because the memory history of vue-router is now used, + * the initial position needs to be pushed manually, otherwise the router will not be ready. + * On the browser, it is matched by vue-router according to location.href, and the default push root path '/' + */ + router.push('/'); + + // listen android native back press, must before router back press inject + BackAndroid.addListener(() => { + console.log('backAndroid'); + // set true interrupts native back + return true; + }); + + // mount first, you can do something before mount + app.mount('#root'); + + /** + * You can also mount the app after the route is ready, However, + * it is recommended to mount first, because it can render content on the screen as soon as possible + */ + // router.isReady().then(() => { + // // mount app + // app.mount('#root'); + // }); +}; + +// start hippy app +app.$start().then(initCallback); + +// you can also use callback to start app like @hippy/vue before +// app.$start(initCallback); +``` + +## hippy-vue-next npm 脚本 + +在 [package.json](//github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/package.json#L13) 中提供了几个以 `hippy:`开头的 npm 脚本,可用来启动 [@hippy/debug-server-next](//www.npmjs.com/package/@hippy/debug-server-next) 等调试工具。 + +```json + "scripts": { + "hippy:dev": "node ./scripts/env-polyfill.js hippy-dev --config ./scripts/hippy-webpack.dev.js", + "hippy:vendor": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.ios-vendor.js --config ./scripts/hippy-webpack.android-vendor.js", + "hippy:build": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.ios.js --config ./scripts/hippy-webpack.android.js" + }, +``` + +## hippy-vue-next 路由 + +`@hippy/vue-next` 无需侵入式修改vue-router,直接使用官方 vue-router 即可,如果需要支持安卓物理健回退时路由历史回退,则可以安装@hippy/vue-router-next-history模块。 diff --git a/docs/development/react-vue-3.0-upgrade-guidelines.md b/docs/development/react-vue-3.0-upgrade-guidelines.md new file mode 100644 index 00000000000..7e5c7b1d447 --- /dev/null +++ b/docs/development/react-vue-3.0-upgrade-guidelines.md @@ -0,0 +1,164 @@ +# Hippy React&Vue 3.x SDK升级指引 + +> 这篇教程,主要介绍 Hippy React&Vue&Vue-next 如何升级3.0版本以及升级后的相关验证关注点。 + +--- + +# 升级依赖项变更 + +## hippy-react + +>如果业务目前使用 React 来开发 Hippy,可以参考当前章节升级指引。 +
+ +如果当前 @hippy/react 版本小于 2.12.0, 且 React 使用的 16 的版本,则需要升级如下版本: + +``` javascript +(1)删除 react-reconciler 依赖 +(2)@hippy/react 升级到 3.3.0 及以上 +(3)新增 @hippy/react-reconciler 依赖,使用react17的tag,即 @hippy/react-reconciler: "react17" +(4)React 版本升级到 17,即 react: "^17.0.2" +(5)如果使用了 @hippy/react-web 包做h5同构,则需要升级 @hippy/react-web 到 3.3.0 及以上 +``` + +如果当前 @hippy/react 版本大于 2.12.0, 且 React 使用的 17 的版本,则需要升级如下版本: + +``` javascript +(1)@hippy/react 升级到 3.3.0 及以上 +(2)升级 @hippy/react-reconciler 依赖,使用react17的tag,即 @hippy/react-reconciler: "react17" +(3)如果使用了 @hippy/react-web 包做h5同构,则需要升级 @hippy/react-web 到 3.3.0 及以上 +``` + +需要业务使用新的 hippy-react 重编 js common包 + +## hippy-vue + +>如果业务目前使用 Vue 2.x 来开发 Hippy,可以参考当前章节升级指引。 +
+ +需要升级如下版本依赖: + +``` javascript +(1)@hippy/vue 升级到 3.3.1-rc.1 及以上 +(2)@hippy/vue-native-components 升级到 3.3.0 及以上 +(3)@hippy/vue-router 升级到 3.3.0 及以上 +(4)@hippy/vue-css-loader 升级到 3.3.0 及以上 +(5)@hippy/vue-loader 升级到 3.3.0 及以上 +(6)vue 和 vue-router 等vue相关依赖无需升级 +``` + +需要业务使用新的 hippy-vue 重编 js common包 + +## hippy-vue-next + +>如果业务目前使用 Vue 3.x 来开发 Hippy,可以参考当前章节升级指引。 +
+ +需要升级如下版本依赖: + +``` javascript +(1)@hippy/vue-next 升级到 3.3.0 及以上 +(2)@hippy/vue-css-loader 升级到 3.3.0 及以上 +(3)@hippy/vue-router-next-history 升级到 0.0.1 +(4)vue 和 vue-router 等vue相关依赖无需升级 +``` + +需要业务使用新的 hippy-vue-next 重编 js common包 + +
+
+ +# 接入与使用方式变更 + +接入 Hippy-React、Hippy-Vue、Hippy-Vue-Next SDK 代码无变化,可参考 [前端集成指引](development/react-vue-3.0-integration-guidelines.md) + +具体变化点如下: + +1. iOS 新增节点层级优化算法,Android 优化了现有的层级优化算法: +该算法会将仅参与布局的View节点优化去除,从而提升渲染效率。请注意 !!!由于该算法的存在,可能导致依赖特定UI层级结构的native组件发生找不到特定View的异常。 +此时,可以通过前端代码中给特定 View 增加 collapsable: 'false' 属性来禁止该节点被优化算法去除。 + + +# 组件变更 + +1. dialog 组件的第一个子元素不能设置 { position: absolute } 样式,如果想将 dialog 内容铺满全屏,可以给第一个子元素设置 { flex: 1 } 样式或者显式设置 width 和 height 数值 + +2. Image 组件废弃了 source、 sources、srcs 字段,建议使用 src 字段代表图片 url + +3. iOS Image 组件默认没有实现图片缓存 (由于实现机制的变化),需要业务 iOS端自行实现缓存管理,详细可参考 [iOS 升级指引](development/ios-3.0-upgrade-guidelines.md) + +4. hippy-vue 2.15以前的版本不支持 scoped 样式隔离,2.15-2.17的版本,如果没有开启 Vue.config.scoped = true; 也不支持 scoped 样式隔离。 +Vue3.0 默认开启 scoped 无需设置开关 + +5. hippy-vue 布局属性如 height、width 在 3.0 的版本将不支持放在自定义属性里,如: + +``` javascript +
+``` + +需要放在style属性中,如: + +``` javascript +
+``` + +# 接口定义变更 + +1. hippy-react 不再导出RNfqb、RNfqbRegister、RNfqbEventEmitter、RNfqbEventListener 方法 + +2. hippy-react animation 模块不再有 destory() 方法的错误写法兼容,统一用 destroy() + +3. hippy-react animation 事件监听不再支持 onRNfqbAnimationXX 兼容写法,统一用 onHippyAnimationXX 或者 onAnimationXX + +4. hippy-react 初始化动画对象(new Animation),需要在根节点渲染之后,否则会因为 Dom Manager未创建提示报错 + hippy-vue/hippy-vue-next 初始化动画对象(new Animation),需要在 Vue.start 回调之后,否则会因为 Dom Manager未创建提示报错 + +5. hippy-react/hippy-vue/hippy-vue-next 如果使用了颜色属性的渐变动画,需要显示指定 color 单位,添加 valueType:'color' 字段,例如: + +``` javascript + animation: new Animation({ + startValue: 'red', + toValue: 'yellow', + valueType: 'color', // 颜色动画需显式指定color单位 + duration: 1000, + delay: 0, + mode: 'timing', + timingFunction: 'linear', + }), +``` + + +# 验证关注点 + +一、Hippy 3.0 前端架构升级主要有如下改动点: +
+ +1. JS 驱动上屏的方式由 UIManagerModule 变为了 SceneBuilder。 +2. Node API 重新实现了 Move 计算逻辑。 +3. Event 由前端分发变为 DOM 分发。 +4. 动画由 bridge 模块变为 C++ DOM 模块实现。 + +二、需要验证关注点: +
+ +1. 界面的UI视图渲染正常 (UI结构、样式属性等),特别关注 Hippy-React/Vue 中因为条件渲染语句,产生的节点`Move`操作,表现是否正常。 +2. UI事件(点击、滑动)等表现正常,特别关注事件`冒泡`、`捕获`等表现是否正常。 +3. 关注`动画`表现是否正常。 + +
+
+ +# 新特性 + +## Performance API + +Hippy 3.0 我们实现了基于前端规范设计的性能 API,接入方式可参考 [Performance](feature/feature3.0/performance.md)。 + +## Layout 引擎支持切换 + +Hippy 3.0 我们支持了 Layout 引擎的无缝切换,项目可保持`Yoga`引擎,也可以选择Hippy自研的`Taitank`引擎。详情可参考 [Layout](feature/feature3.0/layout.md) diff --git a/docs/development/voltron-flutter-integration-guidelines.md b/docs/development/voltron-flutter-integration-guidelines.md new file mode 100644 index 00000000000..c2b5b0ff5d8 --- /dev/null +++ b/docs/development/voltron-flutter-integration-guidelines.md @@ -0,0 +1,78 @@ +# Voltron Flutter 集成指引 + +这篇教程,讲述了如何将 Hippy 3.x SDK 集成到 Flutter 工程。 + +> 注:以下文档都是假设您已经具备一定的 Flutter 开发经验。 + +--- + +## 前期准备 + +- 已经安装了 Flutter version>=3.0 并配置了环境变量 + +## Demo 体验 + +若想快速体验,可以直接基于我们的 `Voltron Demo` 来开发,我们提供以下两种 `Demo` + +- 如果您的应用完全通过 `Flutter` 进行开发,可以参考[flutter_proj](https://github.com/Tencent/Hippy/tree/master/framework/example/voltron-demo/flutter_proj),3.0正式发布前,请使用 [flutter_proj](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/example/voltron-demo/flutter_proj) + +- 如果您希望将 `Voltron` 集成进您的原生 `IOS` 或 `Android` 应用,可以使用 `flutter module` 进行集成 + + - `Android` 应用请参考[android-proj](https://github.com/Tencent/Hippy/tree/master/framework/example/voltron-demo/android-proj),3.0正式发布前,请使用 [flutter_proj](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/example/voltron-demo/android-proj) + - `IOS` 应用请参考[IOSProj](https://github.com/Tencent/Hippy/tree/master/framework/example/voltron-demo/IOSProj),3.0正式发布前,请使用 [flutter_proj](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/example/voltron-demo/IOSProj) + +> 注意,使用 `flutter_module` 方式进行开发时,原生工程和 `Flutter` 工程在两个目录,上面所提到的 `android-proj` 和 `IOSProj` 均需要配合 [flutter_module](https://github.com/Tencent/Hippy/tree/master/framework/example/voltron-demo/flutter_module)进行使用,3.0正式发布前,请使用 [flutter_proj](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/example/voltron-demo/flutter_module) + +## 快速接入 + +### 如果您的应用完全通过 `Flutter` 进行开发 + +1. 创建一个 Flutter 工程 + +2. Pub 集成 + + 在 `pubspec.yaml` 中添加 `Voltron` 依赖 + + ```yaml + dependencies: + voltron: ^0.0.1 + ``` + +3. 本地集成(可选) + + 1. 克隆 Hippy 源码 + + ```shell + git clone https://github.com/Tencent/Hippy.git + ``` + + > 注意使用相应的分支及tag,未合入主干前,请使用v3.0-dev分支 + + 2. 打开 Flutter 工程根目录下的 `pubspec.yaml` + + 在 `dependencies` 下添加 `voltron` 依赖 + + ```yaml + voltron: + path: Hippy路径/framework/voltron + ``` + +4. 安装依赖 + + ```shell + flutter pub get + ``` + +5. 使用 `Voltron` + + 建议参考[flutter_proj](https://github.com/Tencent/Hippy/tree/master/framework/example/voltron-demo/flutter_proj),3.0正式发布前,请使用 [flutter_proj](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/example/voltron-demo/flutter_proj) + + > Pub 集成方式在 Android 平台默认支持 `arm64-v8a` 和 `armeabi-v7a`,如需支持 `x86` 和 `x86_64`,请使用本地集成,iOS 无影响。 + + > 需要注意,如果 **debugMode** 为YES的情况下,会忽略所有参数,直接使用 npm 本地服务加载测试 bundle, + +### 如果您希望将 `Voltron` 集成进您的原生 `IOS` 或 `Android` 应用 + +1. 使用该方式进行集成时,要首先集成 Flutter Module,该部分可直接参考官网[Add Flutter to an existing app](https://docs.flutter.dev/add-to-app) + +2. 后续流程与完全通过 `Flutter` 进行开发保持一致即可。也可直接参考我们的[Demo](#demo-体验-1)工程 diff --git a/docs/development/web-integration-guidelines.md b/docs/development/web-integration-guidelines.md new file mode 100644 index 00000000000..6c2bf0812fa --- /dev/null +++ b/docs/development/web-integration-guidelines.md @@ -0,0 +1,151 @@ +# Web同构指引 + +这篇教程,讲述了如何将 Hippy 集成到 Web 页面中。 + +> 不同于 @hippy/react-web 和 @hippy/vue-web 方案,本方案(Web Renderer)不会替换 @hippy/react 和 @hippy/vue,而是将运行在原生环境下的 bundle 原封不动运行到 Web 上,与转译 Web 的方案各有利弊,业务可根据具体场景采用合适的方案 + +--- + +## 前期准备 + +- 模板文件:Web 运行需要一个 HTML 文件作为入口 +- 入口文件:WebRenderer 是作为 Hippy bundle 的一个运行环境,因此不共享入口 JS 文件,应为其创建独立的入口文件 + +### npm script + +在 demo 项目中,通过 `web:dev` 命令启动 WebRenderer 调试服务,通过 `web:build` 打包编译。 + +```json + "scripts": { + "web:dev": "npm run hippy:dev & node ./scripts/env-polyfill.js webpack serve --config ./scripts/hippy-webpack.web-renderer.dev.js", + "web:build": "node ./scripts/env-polyfill.js +webpack --config ./scripts/hippy-webpack.web-renderer.js" + } +``` + +### 启动调试 + +执行 `npm run web:dev` 启动 WebRenderer 调试,根据 demo 的 webpack 配置,WebRenderer 的 web 服务运行在`3000`端口,浏览器通过 `http://localhost:3000` 访问页面。 + +## 快速接入 + +WebRenderer 的执行应符合以下流程: + +1. 导入 WebRenderer:该阶段会初始化 Hippy 代码运行的环境 +2. 加载业务 bundle:这个 bundle 与 Native 侧运行的 bundle 包保持一致 +3. 启动 WebRenderer:该阶段会加载 Hippy 内置组件和模块,也可以加载自定义组件和模块 + +### 导入 WebRenderer + +#### 以 CDN 方式使用 + +在模板文件内添加: + +```html + + + + + + Example + + +
+ + + + + + +``` + +#### 以 NPM 包方式使用 + +```shell +npm install -S @hippy/web-renderer +``` + +在入口文件内添加: + +```javascript +// 1. 导入 web renderer +import { HippyWebEngine, HippyWebModule } from '@hippy/web-renderer'; + +// 2. 导入业务 bundle 的入口文件,需放在 web renderer 导入之后 + +// 3. 创建 web engine,如果有业务自定义模块和组件,从此处传入 +``` + +### 加载业务 Bundle + +加载 bundle 包有多种方式,可根据业务需要灵活选择,只需要确保引入顺序在 WebRenderer 之后即可 + +#### 在模板文件内引用加载 + +```html + + + + + +``` + +#### 在入口文件内动态加载 + +```javascript +import { HippyWebEngine } from '@hippy/web-renderer'; + +const engine = HippyWebEngine.create(); + + engine.load('https://xxxx.com/hippy-bundle/index.bundle.js').then(() => { + engine.start({ + id: 'root', + name: 'example', + }); +}); +``` + +#### 业务源码直接引用 + +```javascript +import { HippyCallBack, HippyWebEngine, HippyWebModule, View } from '@hippy/web-renderer'; +// 导入业务 bundle 的入口文件,需放在 web renderer 导入之后 +import './main'; + + +const engine = HippyWebEngine.create(); +``` + +### 启动 WebRenderer + +加载完业务 bundle 后,调用相关 API 创建并启动 WebRenderer + +```js +// 创建 web engine,如果有业务自定义模块和组件,从此处传入 +// 如果只使用官方模块和组件,则直接使用 const engine = HippyWebEngine.create() 即可 +const engine = HippyWebEngine.create({ + modules: { + CustomCommonModule, + }, + components: { + CustomPageView, + }, +}); + +// 启动 web renderer +engine.start({ + // 挂载的 dom id + id: 'root', + // 模块名 + name: 'module-name', + // 模块启动参数,业务自定义, + // hippy-react 可以从 入口文件props里获取,hippy-vue可以从 app.$options.$superProps 里获取 + params: { + path: '/home', + singleModule: true, + isSingleMode: true, + business: '', + data: { }, + }, +}); +``` diff --git a/docs/feature/_sidebar.md b/docs/feature/_sidebar.md index eb5c3870814..a2369a7d15f 100644 --- a/docs/feature/_sidebar.md +++ b/docs/feature/_sidebar.md @@ -1,9 +1,10 @@ -- 3.0+ +- 3.x - [VFS](feature/feature3.0/vfs.md) - [Layout 引擎切换](feature/feature3.0/layout.md) - [Snapshot](feature/feature3.0/render-node-snapshot.md) - [双端一致性](feature/feature3.0/cross-platform-consistency.md) -- 2.0+ + - [Performance API](feature/feature3.0/performance.md) +- 2.x - [动画](feature/feature2.0/animation.md) - [日志](feature/feature2.0/console.md) - [自定义字体](feature/feature2.0/custom-font.md) diff --git a/docs/feature/feature3.0/_sidebar.md b/docs/feature/feature3.0/_sidebar.md index eb5c3870814..6e346381eef 100755 --- a/docs/feature/feature3.0/_sidebar.md +++ b/docs/feature/feature3.0/_sidebar.md @@ -3,6 +3,8 @@ - [Layout 引擎切换](feature/feature3.0/layout.md) - [Snapshot](feature/feature3.0/render-node-snapshot.md) - [双端一致性](feature/feature3.0/cross-platform-consistency.md) + - [Performance API](feature/feature3.0/performance.md) + - [Screenshot](feature/feature3.0/screenshot.md) - 2.0+ - [动画](feature/feature2.0/animation.md) - [日志](feature/feature2.0/console.md) diff --git a/docs/feature/feature3.0/image-decoder-adapter.md b/docs/feature/feature3.0/image-decoder-adapter.md new file mode 100644 index 00000000000..988c67dd144 --- /dev/null +++ b/docs/feature/feature3.0/image-decoder-adapter.md @@ -0,0 +1,35 @@ +# ImageDecoderAdapter + +--- + +## 背景 + +3.0我们在HippyEngine引擎初始化参数中增加了ImageDecoderAdapter的设置,如果有开发者业务中有使用到特殊格式的图片,如SharpP、avif等,可以通过设置ImageDecoderAdapter来对接你的自定义图片解码器,具体接口描述如下: + +## 3.0 图片解码Adapter接口定义 + +### ImageDecoderAdapter Public methods + + ```java + boolean preDecode(@NonNull byte[] data, + @Nullable Map initProps, + @NonNull ImageDataHolder imageHolder, + @NonNull BitmapFactory.Options options); + ``` + + 该接口在拉取到图片原始数据的时候会调用,解码的结果可以通过image data holder提供的setBitmap或者setDrawable接口设置到holder中,并且返回true,表示不需要sdk再做解码操作,如果返回false表示需要sdk做默认的解码操作。 + + ```java + void afterDecode(@Nullable Map initProps, + @NonNull ImageDataHolder imageHolder, + @NonNull BitmapFactory.Options options); + ``` + + + 该接口在解码结束后会调用,为开发者提供二次处理bitmap的机会, 比如要对bitmap做高斯模糊。 + + ```java + void destroyIfNeeded(); + ``` + + 引擎退出销毁时调用,释放adapter可能占用的资源 diff --git a/docs/feature/feature3.0/performance.md b/docs/feature/feature3.0/performance.md index d7c6bd069e7..5778a9a682e 100644 --- a/docs/feature/feature3.0/performance.md +++ b/docs/feature/feature3.0/performance.md @@ -1 +1,90 @@ # Performance API + +## 背景 + +过去 Hippy SDK 缺乏对关键性能指标的获取和监控机制,各个业务都需自行打点或者魔改 SDK 进行统计,导致 Hippy 团队和接入业务均没有一个针对性能指标的统一基准,数据解读混乱,因此由 SDK 统一提供标准化的性能监控和指标显得非常有必要。 + +## 指标 + +### 2\.1 启动耗时 + +Web 设计了 Performance API ,其中包含了 PerformanceResourceTiming 和 PerformanceNavigationTiming 接口,用于检索和分析有关加载应用程序资源的详细网络计时数据和首屏加载耗时数据 + +0 +1 + +Hippy 3\.0 新架构参考 Web 标准设计了新的性能 API: + +start + +性能数据获取示例: + +global\.performance\.getEntries\(\): 获取所有的性能指标对象 (PerformanceResource、PerformanceNavigation等) + +global\.performance\.getEntriesByType\('navigation'\): 获取启动加载性能指标对象 + +global\.performance\.getEntriesByType\('resource'\): 获取资源加载性能指标对象 + +2 + +>PerformanceNavigationTiming: + +| 指标 | 对应 Key | +|----------------|---------------------| +| Hippy 引擎加载开始 | hippyNativeInitStart | +| JS 引擎加载开始 | hippyJsEngineInitStart | +| JS 引擎加载结束 | hippyJsEngineInitEnd | +| Hippy 引擎加载结束 | hippyNativeInitEnd | +| JS Bundle 自执行耗时 | bundleInfo[] | +| 业务入口执行开始 | hippyRunApplicationStart | +| 业务入口执行结束 | hippyRunApplicationEnd | +| 首帧绘制开始 | hippyFirstFrameStart | +| 首帧绘制结束 | hippyFirstFrameEnd | +| 启动耗时 | duration | +| 指标名称 | name | +| 指标类型 | entryType | + +>bundleInfo: + +| 指标 | 对应 Key | +|----------------|---------------------| +| 主包/分包地址 | url | +| 执行js包开始时间 | executeSourceStart | +| 执行js包结束时间 | executeSourceEnd | + +>PerformanceResourceTiming: + +| 指标 | 对应 Key | +|----------------|---------------------| +| 资源地址 | name | +| 请求资源开始时间 | loadSourceStart | +| 请求资源结束时间 | loadSourceEnd | +| 请求耗时 | duration | +| 指标类型 | entryType | + + +- 适用版本:3\.1 + +### 2\.2 内存 + +- 现状:2\.0 已支持 JS 层通过 Performance\.memory 获取到 V8 引擎的内存数据(Hermes 待定) + +memory + +- 适用版本:2\.0、3\.1 + +### 2\.3 流畅度 + +流畅度可通过 FPS 和 Janky Frame 指标来衡量 + +浏览器里提供了 requestAnimationFrame API,浏览器会在屏幕刷新(一帧)时机调用回调函数,JS 可在回调函数中执行动画等逻辑,也可用来计算 FPS 和 janky frame。Hippy 由于 JS 线程与 UI 线程独立,渲染异步执行,若通过终端实现 requestAnimationFrame 无法做到与浏览器一致的数据精确度(JS 层获取到的帧率会小于终端的真实帧率),但也可作为一个通用能力提供给业务作为参考。 + +fps + +- Hippy 3\.0 基于vsync信号重新实现了 requestAnimationFrame API +- 适用版本:3\.1 + +## 三、Aegis\-Hippy 接入 + +aegis\-sdk:1\.42\.4 + diff --git a/docs/feature/feature3.0/screenshot.md b/docs/feature/feature3.0/screenshot.md new file mode 100644 index 00000000000..3b5c59d7e01 --- /dev/null +++ b/docs/feature/feature3.0/screenshot.md @@ -0,0 +1,42 @@ +# Screenshot for specific views + +--- + +## 背景 + +过去在一些业务的使用场景中需要针对指定的Hippy view进行截图并做分享,之前截图逻辑都是由开发者自己实现,在3.0中我们针对图片解码使用了Android高版本的api,导致部分开发者自己创建canvas和bitmap,并用draw方法截图的方式无法正常截取图片,为了进一步降低开发者的适配成本,我们把截图能力下沉到了SDK,为开发者提供更为便捷的使用方式。 + +## 3.0 Screenshot接口定义 + +### HippyEngine Public methods + +```java +/** + * @throws IllegalArgumentException + */ +public abstract void getScreenshotBitmapForView(@Nullable Context context, + int id, @NonNull ScreenshotBuildCallback callback); +``` + + 针对指定id的View进行截图,context非空情况下需要是HippyRootView挂载容器所属的Activity + +```java +/** + * @throws IllegalArgumentException + */ +public abstract void getScreenshotBitmapForView(@Nullable Context context, + @NonNull View view, @NonNull ScreenshotBuildCallback callback); +``` + + 针对指定的View进行截图,context非空情况下需要是HippyRootView挂载容器所属的Activity + + > 注意:以上2个接口中的context参数如果传入null,会默认使用view的context,view设置的context是loadModule时候在ModuleLoadParams中传入的context,针对一些预加载场景,开发者有可能设置的是app的context,当context不是Activiy的时候会抛出IllegalArgumentException异常导致截图失败。除了context不满足条件,还有其它view不存在或者执行PixelCopy.request都有可能抛出IllegalArgumentException类型异常,需要开发者自行捕获处理。 + +```java +public interface ScreenshotBuildCallback { + + void onScreenshotBuildCompleted(Bitmap bitmap, int result); +} +``` + + 返回截图结果的回调,result为0代表截图成功,非0代表截图失败,失败错误值可以参考系统PixelCopy类中定义的错误码 diff --git a/dom/include/dom/animation/animation_manager.h b/dom/include/dom/animation/animation_manager.h index 05de7478bd0..af60274e547 100644 --- a/dom/include/dom/animation/animation_manager.h +++ b/dom/include/dom/animation/animation_manager.h @@ -61,6 +61,7 @@ class AnimationManager } void RemoveVSyncEventListener(); + void OnDomNodeCreate(const std::vector>& nodes) override; void OnDomNodeUpdate(const std::vector>& nodes) override; void OnDomNodeMove(const std::vector>& nodes) override; diff --git a/dom/include/dom/diff_utils.h b/dom/include/dom/diff_utils.h index 8f8ec570b0e..5b8c0b9f679 100644 --- a/dom/include/dom/diff_utils.h +++ b/dom/include/dom/diff_utils.h @@ -53,7 +53,7 @@ class DiffUtils { * k: 11, * } */ - static DiffValue DiffProps(const DomValueMap& old_props_map, const DomValueMap& new_props_map); + static DiffValue DiffProps(const DomValueMap& old_props_map, const DomValueMap& new_props_map, bool skip_style_diff); }; } // namespace dom } // namespace hippy diff --git a/dom/include/dom/dom_manager.h b/dom/include/dom/dom_manager.h index 9de80ee4f42..da57955f68e 100644 --- a/dom/include/dom/dom_manager.h +++ b/dom/include/dom/dom_manager.h @@ -41,6 +41,8 @@ #include "footstone/base_timer.h" #include "footstone/worker.h" +#define HIPPY_EXPERIMENT_LAYER_OPTIMIZATION + namespace hippy { inline namespace dom { @@ -96,7 +98,7 @@ class DomManager : public std::enable_shared_from_this { uint32_t id) ; static void CreateDomNodes(const std::weak_ptr& weak_root_node, - std::vector>&& nodes); + std::vector>&& nodes, bool needSortByIndex); static void UpdateDomNodes(const std::weak_ptr& weak_root_node, std::vector>&& nodes); static void MoveDomNodes(const std::weak_ptr& weak_root_node, @@ -140,8 +142,12 @@ class DomManager : public std::enable_shared_from_this { friend class DomNode; uint32_t id_; +#ifdef HIPPY_EXPERIMENT_LAYER_OPTIMIZATION std::shared_ptr optimized_render_manager_; - std::weak_ptr render_manager_; + std::shared_ptr render_manager_; +#else + std::shared_ptr render_manager_; +#endif std::unordered_map> timer_map_; std::shared_ptr task_runner_; std::shared_ptr worker_; diff --git a/dom/include/dom/dom_node.h b/dom/include/dom/dom_node.h index e56964c9deb..858b1d4e14b 100644 --- a/dom/include/dom/dom_node.h +++ b/dom/include/dom/dom_node.h @@ -55,6 +55,14 @@ enum RelativeType { kBack = 1, }; +struct DiffInfo { + bool skip_style_diff; + DiffInfo(bool skip_style_diff) : skip_style_diff(skip_style_diff) {} + + private: + friend std::ostream& operator<<(std::ostream& os, const DiffInfo& diff_info); +}; + struct RefInfo { uint32_t ref_id; int32_t relative_to_ref = RelativeType::kDefault; @@ -67,7 +75,8 @@ struct RefInfo { struct DomInfo { std::shared_ptr dom_node; std::shared_ptr ref_info; - DomInfo(std::shared_ptr node, std::shared_ptr ref) : dom_node(node), ref_info(ref) {} + std::shared_ptr diff_info; + DomInfo(std::shared_ptr node, std::shared_ptr ref, std::shared_ptr diff) : dom_node(node), ref_info(ref), diff_info(diff) {} private: friend std::ostream& operator<<(std::ostream& os, const DomInfo& dom_info); @@ -97,6 +106,7 @@ class DomNode : public std::enable_shared_from_this { uint32_t id = kInvalidId; // RenderNode的id uint32_t pid = kInvalidId; // 父RenderNode的id int32_t index = kInvalidIndex; // 本节点在父RenderNode上的索引 + int32_t depth = kInvalidIndex; // 本节点在父RenderNode上的深度 }; inline std::shared_ptr GetParent() { return parent_.lock(); } @@ -119,6 +129,8 @@ class DomNode : public std::enable_shared_from_this { inline void SetLayoutOnly(bool layout_only) { layout_only_ = layout_only; } inline bool IsVirtual() { return is_virtual_; } inline void SetIsVirtual(bool is_virtual) { is_virtual_ = is_virtual; } + inline bool IsEnableEliminated() { return enable_eliminated_; } + inline void SetEnableEliminated(bool enable_eliminated) { enable_eliminated_ = enable_eliminated; } inline void SetIndex(int32_t index) { index_ = index; } inline int32_t GetIndex() const { return index_; } inline void SetRootNode(std::weak_ptr root_node) { root_node_ = root_node; } @@ -131,6 +143,7 @@ class DomNode : public std::enable_shared_from_this { void MarkWillChange(bool flag); int32_t GetSelfIndex(); int32_t GetChildIndex(uint32_t id); + int32_t GetSelfDepth(); int32_t IndexOf(const std::shared_ptr& child); std::shared_ptr GetChildAt(size_t index); @@ -230,6 +243,10 @@ class DomNode : public std::enable_shared_from_this { bool is_virtual_{}; bool layout_only_ = false; + // Node can only be eliminated for the first time, + // and if they cannot be eliminated for the first time, they cannot be eliminated at all times. + bool enable_eliminated_ = true; + std::weak_ptr parent_; std::vector> children_; diff --git a/dom/include/dom/layer_optimized_render_manager.h b/dom/include/dom/layer_optimized_render_manager.h index a060bad94df..ce683c0cc47 100644 --- a/dom/include/dom/layer_optimized_render_manager.h +++ b/dom/include/dom/layer_optimized_render_manager.h @@ -28,6 +28,7 @@ inline namespace dom { class LayerOptimizedRenderManager : public RenderManager { public: LayerOptimizedRenderManager(std::shared_ptr render_manager); + inline std::shared_ptr GetInternalNativeRenderManager() { return render_manager_; } void CreateRenderNode(std::weak_ptr root_node, std::vector>&& nodes) override; void UpdateRenderNode(std::weak_ptr root_node, std::vector>&& nodes) override; @@ -73,10 +74,6 @@ class LayerOptimizedRenderManager : public RenderManager { void FindValidChildren(const std::shared_ptr& node, std::vector>& valid_children_nodes); - - // Record nodes that cannot be eliminated. Nodes can only be eliminated for the first time, - // and if they cannot be eliminated for the first time, they cannot be eliminated at all times. - std::set not_eliminated_node_ids_; }; } // namespace dom diff --git a/dom/include/dom/layout_node.h b/dom/include/dom/layout_node.h index 75ac510bc51..01a0ec3f684 100644 --- a/dom/include/dom/layout_node.h +++ b/dom/include/dom/layout_node.h @@ -20,6 +20,7 @@ #pragma once +#include #include #include "footstone/hippy_value.h" @@ -93,14 +94,14 @@ class LayoutNode { /** * @brief 插入子节点 - * @param child - * @param index + * @param child LayoutNode ptr + * @param index int32 */ virtual void InsertChild(std::shared_ptr child, uint32_t index) = 0; /** * @brief 删除子节点 - * @param child + * @param child LayoutNode ptr */ virtual void RemoveChild(const std::shared_ptr child) = 0; @@ -116,13 +117,14 @@ class LayoutNode { /** * @brief 设置属性 - * @param style_map 属性的map + * @param style_update 属性的map */ virtual void SetLayoutStyles( const std::unordered_map>& style_update, const std::vector& style_delete) = 0; }; +void InitLayoutConsts(); std::shared_ptr CreateLayoutNode(); } // namespace dom diff --git a/dom/include/dom/root_node.h b/dom/include/dom/root_node.h index a711fc8e578..2b8f505565b 100644 --- a/dom/include/dom/root_node.h +++ b/dom/include/dom/root_node.h @@ -22,13 +22,37 @@ #include +#include "dom/diff_utils.h" #include "dom/dom_node.h" -#include "footstone/task_runner.h" #include "footstone/persistent_object_map.h" +#include "footstone/task_runner.h" namespace hippy { inline namespace dom { +class RootNode; + +/** + * In HippyVue/HippyReact, updating node styles can be intricate. + * This class is specifically designed to compute the differences when updating DOM node styles. + */ +class DomNodeStyleDiffer { + public: + DomNodeStyleDiffer() = default; + ~DomNodeStyleDiffer() = default; + + bool Calculate(const std::shared_ptr& root_node, const std::shared_ptr& dom_info, + hippy::dom::DiffValue& style_diff, hippy::dom::DiffValue& ext_style_diff); + void Reset() { + node_ext_style_map_.clear(); + node_style_map_.clear(); + } + + private: + std::unordered_map>> node_style_map_; + std::unordered_map>> node_ext_style_map_; +}; + class RootNode : public DomNode { public: using TaskRunner = footstone::runner::TaskRunner; @@ -50,7 +74,7 @@ class RootNode : public DomNode { virtual void RemoveEventListener(const std::string& name, uint64_t listener_id) override; void ReleaseResources(); - void CreateDomNodes(std::vector>&& nodes); + void CreateDomNodes(std::vector>&& nodes, bool needSortByIndex); void UpdateDomNodes(std::vector>&& nodes); void MoveDomNodes(std::vector>&& nodes); void DeleteDomNodes(std::vector>&& nodes); @@ -71,7 +95,6 @@ class RootNode : public DomNode { void Traverse(const std::function&)>& on_traverse); void AddInterceptor(const std::shared_ptr& interceptor); - static footstone::utils::PersistentObjectMap>& PersistentMap() { return persistent_map_; } @@ -80,16 +103,12 @@ class RootNode : public DomNode { static void MarkLayoutNodeDirty(const std::vector>& nodes); struct DomOperation { - enum class Op { - kOpCreate, kOpUpdate, kOpDelete, kOpMove - } op; + enum class Op { kOpCreate, kOpUpdate, kOpDelete, kOpMove } op; std::vector> nodes; }; struct EventOperation { - enum class Op { - kOpAdd, kOpRemove - } op; + enum class Op { kOpAdd, kOpRemove } op; uint32_t id; std::string name; }; @@ -107,6 +126,7 @@ class RootNode : public DomNode { std::weak_ptr dom_manager_; std::vector> interceptors_; std::shared_ptr animation_manager_; + std::unique_ptr style_differ_; static footstone::utils::PersistentObjectMap> persistent_map_; }; diff --git a/dom/include/dom/scene_builder.h b/dom/include/dom/scene_builder.h index 5e594dfc45d..e55a668a033 100644 --- a/dom/include/dom/scene_builder.h +++ b/dom/include/dom/scene_builder.h @@ -49,7 +49,8 @@ class SceneBuilder { static void Create(const std::weak_ptr& dom_manager, const std::weak_ptr& root_node, - std::vector>&& nodes); + std::vector>&& nodes, + bool needSortByIndex); static void Update(const std::weak_ptr& dom_manager, const std::weak_ptr& root_node, std::vector>&& nodes); diff --git a/dom/include/dom/taitank_layout_node.h b/dom/include/dom/taitank_layout_node.h index c7ea9380f39..25c906344a4 100644 --- a/dom/include/dom/taitank_layout_node.h +++ b/dom/include/dom/taitank_layout_node.h @@ -49,7 +49,7 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 设置 Taitank Layout 的属性 - * @param style_map 属性的map + * @param style_update 属性的map */ void SetLayoutStyles( const std::unordered_map>& style_update, @@ -69,7 +69,7 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 设置 position 属性 - * @param css_direction(EdgeLeft|EdgeTop|EdgeRight|EdgeBottom|EdgeStart|EdgeEnd) + * @param edge (EdgeLeft|EdgeTop|EdgeRight|EdgeBottom|EdgeStart|EdgeEnd) * @param position 位置 */ void SetPosition(Edge edge, float position) override; @@ -126,21 +126,21 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 获取 margin 属性 - * @param edge + * @param edge Edge * @return left 属性 */ float GetMargin(Edge edge) override; /** * @brief 获取 padding 属性 - * @param edge + * @param edge Edge * @return padding 属性 */ float GetPadding(Edge edge) override; /** * @brief 获取 border 属性 - * @param edge + * @param edge Edge * @return border 属性 */ float GetBorder(Edge edge) override; @@ -159,7 +159,6 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 是否 overflow - * @param overflow * @return border 属性 */ bool LayoutHadOverflow(); @@ -172,14 +171,14 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 插入子节点 - * @param child - * @param index + * @param child LayoutNode ptr + * @param index uint32 */ void InsertChild(std::shared_ptr child, uint32_t index) override; /** * @brief 删除子节点 - * @param child + * @param child LayoutNode ptr */ void RemoveChild(const std::shared_ptr child) override; @@ -191,7 +190,7 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 设置 has new layout 属性 - * @param has_new_layout + * @param has_new_layout bool */ void SetHasNewLayout(bool has_new_layout) override; @@ -218,8 +217,6 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this */ bool Reset(); - int64_t GetKey() { return key_; } - private: /** * @brief 解析属性 @@ -259,7 +256,7 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 设置 flex basis 属性 - * @param flex basis + * @param flex_basis flex basis */ void SetFlexBasis(float flex_basis); @@ -271,59 +268,59 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 设置 flex grow属性 - * @param flex grow + * @param flex_grow flex grow */ void SetFlexGrow(float flex_grow); /** * @brief 设置 flex shrink属性 - * @param flex shrink + * @param flex_shrink flex shrink */ void SetFlexShrink(float flex_shrink); /** * @brief 设置 flex direction 属性 - * @param flex direction (FLexDirectionRow|FLexDirectionRowReverse|FLexDirectionColumn|FLexDirectionColumnReverse) + * @param flex_direction (FLexDirectionRow|FLexDirectionRowReverse|FLexDirectionColumn|FLexDirectionColumnReverse) */ void SetFlexDirection(FlexDirection flex_direction); /** * @brief 设置 position type 属性 - * @param position_type(PositionTypeRelative|PositionTypeAbsolute) + * @param position_type (PositionTypeRelative|PositionTypeAbsolute) */ void SetPositionType(PositionType position_type); /** * @brief 设置 position 属性 - * @param css_direction(CSSLeft|CSSTop|CSSRight|CSSBottom|CSSStart|CSSEnd) - * @param position + * @param css_direction (CSSLeft|CSSTop|CSSRight|CSSBottom|CSSStart|CSSEnd) + * @param position float */ void SetPosition(CSSDirection css_direction, float position); /** * @brief 设置 margin 属性 - * @param css_direction - * @param margin + * @param css_direction CSSDirection + * @param margin float */ void SetMargin(CSSDirection css_direction, float margin); /** * @brief 设置 margin auto属性 - * @param css_direction + * @param css_direction CSSDirection */ void SetMarginAuto(CSSDirection css_direction); /** * @brief 设置 padding 属性 - * @param css_direction - * @param padding + * @param css_direction CSSDirection + * @param padding float */ void SetPadding(CSSDirection css_direction, float padding); /** * @brief 设置 border 属性 - * @param css_direction - * @param border + * @param css_direction CSSDirection + * @param border float */ void SetBorder(CSSDirection css_direction, float border); @@ -335,25 +332,25 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 设置 justify content 属性 - * @param justify + * @param justify FlexAlign */ void SetJustifyContent(FlexAlign justify); /** * @brief 设置 align content 属性 - * @param align_content + * @param align_content FlexAlign */ void SetAlignContent(FlexAlign align_content); /** * @brief 设置 align items 属性 - * @param align_items + * @param align_items FlexAlign */ void SetAlignItems(FlexAlign align_items); /** * @brief 设置 align self 属性 - * @param align_self + * @param align_self FlexAlign */ void SetAlignSelf(FlexAlign align_self); @@ -377,7 +374,6 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this /** * @brief 分配节点 - * @param overflow_type (OverflowVisible|OverflowHidden|OverflowScroll) */ void Allocate(); @@ -391,7 +387,7 @@ class TaitankLayoutNode : public LayoutNode, public std::enable_shared_from_this std::vector> children_; TaitankNodeRef engine_node_; - int64_t key_; + MeasureFunction measure_function_ = nullptr; }; } // namespace dom diff --git a/dom/src/dom/animation/animation.cc b/dom/src/dom/animation/animation.cc index 08dde41a476..3b4b49b9bf3 100644 --- a/dom/src/dom/animation/animation.cc +++ b/dom/src/dom/animation/animation.cc @@ -254,7 +254,8 @@ void Animation::Run(uint64_t now, const AnimationOnRun& on_run) { case Animation::Status::kDestroy: default: { FOOTSTONE_LOG(ERROR) << "animation status = " << static_cast(status_); - FOOTSTONE_UNREACHABLE(); + FOOTSTONE_DCHECK(false); + return; } } diff --git a/dom/src/dom/animation/animation_manager.cc b/dom/src/dom/animation/animation_manager.cc index 66356b9979b..566c0379155 100644 --- a/dom/src/dom/animation/animation_manager.cc +++ b/dom/src/dom/animation/animation_manager.cc @@ -69,7 +69,9 @@ void AnimationManager::EmplaceNodeProp(const std::shared_ptr& node, con animation_nodes_map_.insert({animation_id, nodeIds}); } auto animation = GetAnimation(animation_id); - node->EmplaceStyleMap(prop, HippyValue(animation->GetStartValue())); + if (animation) { + node->EmplaceStyleMap(prop, HippyValue(animation->GetStartValue())); + } } void AnimationManager::ParseAnimation(const std::shared_ptr& node) { diff --git a/dom/src/dom/deserializer_unittests.cc b/dom/src/dom/deserializer_unittests.cc index 07e425d4a6d..40cd696b3d6 100644 --- a/dom/src/dom/deserializer_unittests.cc +++ b/dom/src/dom/deserializer_unittests.cc @@ -44,6 +44,7 @@ void CheckUint32(uint32_t value) { footstone::value::HippyValue hippy_value; deserializer.ReadObject(hippy_value); + footstone::value::SerializerHelper::DestroyBuffer(buffer); EXPECT_TRUE(hippy_value.GetType() == footstone::value::HippyValue::Type::kNumber); EXPECT_TRUE(hippy_value.GetNumberType() == footstone::value::HippyValue::NumberType::kUInt32); EXPECT_TRUE(hippy_value.ToUint32Checked() == value); @@ -60,6 +61,7 @@ void CheckInt32(int32_t value) { footstone::value::HippyValue hippy_value; deserializer.ReadObject(hippy_value); + footstone::value::SerializerHelper::DestroyBuffer(buffer); EXPECT_TRUE(hippy_value.GetType() == footstone::value::HippyValue::Type::kNumber); EXPECT_TRUE(hippy_value.GetNumberType() == footstone::value::HippyValue::NumberType::kInt32); EXPECT_TRUE(hippy_value.ToInt32Checked() == value); @@ -76,6 +78,7 @@ void CheckDouble(double value) { footstone::value::HippyValue hippy_value; deserializer.ReadObject(hippy_value); + footstone::value::SerializerHelper::DestroyBuffer(buffer); EXPECT_TRUE(hippy_value.GetType() == footstone::value::HippyValue::Type::kNumber); EXPECT_TRUE(hippy_value.GetNumberType() == footstone::value::HippyValue::NumberType::kDouble); EXPECT_TRUE(hippy_value.ToDoubleChecked() == value); @@ -92,6 +95,7 @@ void CheckString(std::string value) { footstone::value::HippyValue hippy_value; deserializer.ReadObject(hippy_value); + footstone::value::SerializerHelper::DestroyBuffer(buffer); EXPECT_TRUE(hippy_value.GetType() == footstone::value::HippyValue::Type::kString); EXPECT_TRUE(hippy_value.ToStringChecked() == value); EXPECT_TRUE(hippy_value.ToStringChecked().length() == value.length()); @@ -108,6 +112,7 @@ void CheckMap(footstone::value::HippyValue::HippyValueObjectType value) { footstone::value::HippyValue hippy_value; deserializer.ReadObject(hippy_value); + footstone::value::SerializerHelper::DestroyBuffer(buffer); EXPECT_TRUE(hippy_value.GetType() == footstone::value::HippyValue::Type::kObject); EXPECT_TRUE(hippy_value.IsObject()); EXPECT_TRUE(hippy_value.ToObjectChecked().size() == value.size()); @@ -134,6 +139,7 @@ void CheckArray(footstone::value::HippyValue::HippyValueArrayType value) { footstone::value::HippyValue hippy_value; deserializer.ReadObject(hippy_value); + footstone::value::SerializerHelper::DestroyBuffer(buffer); EXPECT_TRUE(hippy_value.GetType() == footstone::value::HippyValue::Type::kArray); EXPECT_TRUE(hippy_value.IsArray()); EXPECT_TRUE(hippy_value.ToArrayChecked().size() == value.size()); @@ -157,6 +163,7 @@ TEST(DeserializerTest, ReadHeader) { footstone::value::Deserializer deserializer(buffer.first, buffer.second); deserializer.ReadHeader(); EXPECT_EQ(deserializer.version_, tdf::base::kLatestVersion); + footstone::value::SerializerHelper::DestroyBuffer(buffer); } TEST(DeserializerTest, Uint32) { diff --git a/dom/src/dom/diff_utils.cc b/dom/src/dom/diff_utils.cc index d101a840bc6..7b11fa274a2 100644 --- a/dom/src/dom/diff_utils.cc +++ b/dom/src/dom/diff_utils.cc @@ -69,9 +69,13 @@ static bool ShouldUpdateProperty(const std::string& key, const DomValueMap& old_ return false; } -DiffValue DiffUtils::DiffProps(const DomValueMap& old_props_map, const DomValueMap& new_props_map) { +DiffValue DiffUtils::DiffProps(const DomValueMap& old_props_map, const DomValueMap& new_props_map, bool skip_style_diff) { std::shared_ptr update_props = std::make_shared(); std::shared_ptr> delete_props = std::make_shared>(); + if (skip_style_diff) { + // 跳过 style diff 计算 + return std::make_tuple(update_props, delete_props); + } // delete props // Example: diff --git a/dom/src/dom/dom_argument.cc b/dom/src/dom/dom_argument.cc index 10bd581d03e..e0b84233723 100644 --- a/dom/src/dom/dom_argument.cc +++ b/dom/src/dom/dom_argument.cc @@ -65,6 +65,7 @@ bool DomArgument::ConvertObjectToBson(const footstone::value::HippyValue& hippy_ std::pair pair = serializer.Release(); bson.resize(pair.second); memcpy(&bson[0], pair.first, sizeof(uint8_t) * pair.second); + footstone::value::SerializerHelper::DestroyBuffer(pair); return true; } diff --git a/dom/src/dom/dom_manager.cc b/dom/src/dom/dom_manager.cc index 3a9e947dd26..74d81826d92 100644 --- a/dom/src/dom/dom_manager.cc +++ b/dom/src/dom/dom_manager.cc @@ -18,8 +18,6 @@ * limitations under the License. */ -#define EXPERIMENT_LAYER_OPTIMIZATION - #include "dom/dom_manager.h" #include @@ -54,11 +52,11 @@ using Deserializer = footstone::value::Deserializer; using HippyValueArrayType = footstone::value::HippyValue::HippyValueArrayType; void DomManager::SetRenderManager(const std::weak_ptr& render_manager) { -#ifdef EXPERIMENT_LAYER_OPTIMIZATION +#ifdef HIPPY_EXPERIMENT_LAYER_OPTIMIZATION optimized_render_manager_ = std::make_shared(render_manager.lock()); render_manager_ = optimized_render_manager_; #else - render_manager_ = render_manager; + render_manager_ = render_manager.lock(); #endif } @@ -71,13 +69,14 @@ std::shared_ptr DomManager::GetNode(const std::weak_ptr& weak } void DomManager::CreateDomNodes(const std::weak_ptr& weak_root_node, - std::vector>&& nodes) { + std::vector>&& nodes, + bool needSortByIndex) { auto root_node = weak_root_node.lock(); if (!root_node) { return; } size_t create_size = nodes.size(); - root_node->CreateDomNodes(std::move(nodes)); + root_node->CreateDomNodes(std::move(nodes), needSortByIndex); FOOTSTONE_DLOG(INFO) << "[Hippy Statistic] create node size = " << create_size << ", total node size = " << root_node->GetChildCount(); } @@ -124,7 +123,7 @@ void DomManager::DeleteDomNodes(const std::weak_ptr& weak_root_node, } void DomManager::EndBatch(const std::weak_ptr& weak_root_node) { - auto render_manager = render_manager_.lock(); + auto render_manager = render_manager_; FOOTSTONE_DCHECK(render_manager); if (!render_manager) { return; @@ -186,7 +185,7 @@ void DomManager::DoLayout(const std::weak_ptr& weak_root_node) { if (!root_node) { return; } - auto render_manager = render_manager_.lock(); + auto render_manager = render_manager_; // check render_manager, measure text dependent render_manager FOOTSTONE_DCHECK(render_manager); if (!render_manager) { @@ -223,8 +222,10 @@ DomManager::byte_string DomManager::GetSnapShot(const std::shared_ptr& Serializer serializer; serializer.WriteHeader(); serializer.WriteValue(HippyValue(array)); - auto ret = serializer.Release(); - return {reinterpret_cast(ret.first), ret.second}; + auto buffer_pair = serializer.Release(); + byte_string bs = {reinterpret_cast(buffer_pair.first), buffer_pair.second}; + footstone::value::SerializerHelper::DestroyBuffer(buffer_pair); + return bs; } bool DomManager::SetSnapShot(const std::shared_ptr& root_node, const byte_string& buffer) { @@ -262,10 +263,10 @@ bool DomManager::SetSnapShot(const std::shared_ptr& root_node, const b if (dom_node->GetPid() == orig_root_id) { dom_node->SetPid(root_node->GetId()); } - nodes.push_back(std::make_shared(dom_node, nullptr)); + nodes.push_back(std::make_shared(dom_node, nullptr, nullptr)); } - CreateDomNodes(root_node, std::move(nodes)); + CreateDomNodes(root_node, std::move(nodes), false); EndBatch(root_node); return true; diff --git a/dom/src/dom/dom_manager_unittests.cc b/dom/src/dom/dom_manager_unittests.cc index 4c76cf745fb..f6987b3dba7 100644 --- a/dom/src/dom/dom_manager_unittests.cc +++ b/dom/src/dom/dom_manager_unittests.cc @@ -110,7 +110,8 @@ std::vector> ParserJson(const std::string& json_ ref = std::make_shared(id, ref_id); } - std::shared_ptr dom_info = std::make_shared(dom_node, ref); + auto diff_info = std::make_shared(false); + std::shared_ptr dom_info = std::make_shared(dom_node, ref, diff_info); nodes.push_back(dom_info); } return nodes; @@ -133,7 +134,7 @@ TEST(DomManagerTest, CreateDomNodes) { std::shared_ptr root_node = manager->GetNode(10); root_node->SetDomManager(manager); std::vector> infos = ParserFile("create_node.json", manager); - manager->CreateDomNodes(std::move(infos)); + manager->CreateDomNodes(std::move(infos), false); ASSERT_EQ(root_node->GetChildren().size(), 1); auto child = root_node->GetChildren(); @@ -155,7 +156,7 @@ TEST(DomManagerTest, UpdateDomNodes) { std::shared_ptr root_node = manager->GetNode(10); root_node->SetDomManager(manager); std::vector> infos = ParserFile("create_node.json", manager); - manager->CreateDomNodes(std::move(infos)); + manager->CreateDomNodes(std::move(infos), false); std::string json = "[[{\"id\":59,\"pId\":61,\"name\":\"Text\",\"props\":{\"numberOfLines\":1,\"text\":\"本地调试\"," "\"style\":{\"color\":4280558628,\"fontSize\":26}}},{}]]"; @@ -173,7 +174,7 @@ TEST(DomManagerTest, DeleteDomNodes) { std::shared_ptr root_node = manager->GetNode(10); root_node->SetDomManager(manager); std::vector> infos = ParserFile("create_node.json", manager); - manager->CreateDomNodes(std::move(infos)); + manager->CreateDomNodes(std::move(infos), false); std::string json = "[[{\"id\":63,\"pId\":10,\"name\":\"View\"},{}]]"; std::vector> delete_nodes = ParserJson(json, manager); diff --git a/dom/src/dom/dom_node.cc b/dom/src/dom/dom_node.cc index 97abf0bb850..18a7dc56d9b 100644 --- a/dom/src/dom/dom_node.cc +++ b/dom/src/dom/dom_node.cc @@ -103,13 +103,13 @@ std::shared_ptr DomNode::GetChildAt(size_t index) { } int32_t DomNode::AddChildByRefInfo(const std::shared_ptr& dom_info) { - std::shared_ptr ref_info = dom_info->ref_info; + std::shared_ptr& ref_info = dom_info->ref_info; if (ref_info) { if (children_.size() == 0) { children_.push_back(dom_info->dom_node); } else { for (uint32_t i = 0; i < children_.size(); ++i) { - auto child = children_[i]; + auto& child = children_[i]; if (ref_info->ref_id == child->GetId()) { if (ref_info->relative_to_ref == RelativeType::kFront) { children_.insert( @@ -145,7 +145,7 @@ int32_t DomNode::AddChildByRefInfo(const std::shared_ptr& dom_info) { int32_t DomNode::GetChildIndex(uint32_t id) { int32_t index = -1; for (uint32_t i = 0; i < children_.size(); ++i) { - auto child = children_[i]; + auto& child = children_[i]; if (child && child->GetId() == id) { index = static_cast(i); break; @@ -169,6 +169,13 @@ int32_t DomNode::GetSelfIndex() { return -1; } +int32_t DomNode::GetSelfDepth() { + if (auto parent = parent_.lock()) { + return 1 + parent->GetSelfDepth(); + } + return 1; +} + std::shared_ptr DomNode::RemoveChildAt(int32_t index) { auto child = children_[footstone::check::checked_numeric_cast(index)]; child->SetParent(nullptr); @@ -322,11 +329,14 @@ LayoutResult DomNode::GetLayoutInfoFromRoot() { void DomNode::TransferLayoutOutputsRecursive(std::vector>& changed_nodes) { auto not_equal = std::not_equal_to<>(); bool changed = layout_node_->IsDirty() || layout_node_->HasNewLayout(); - - layout_.left = layout_node_->GetLeft(); - layout_.top = layout_node_->GetTop(); - layout_.width = layout_node_->GetWidth(); - layout_.height = layout_node_->GetHeight(); + bool trigger_layout_event = + not_equal(layout_.left, layout_node_->GetLeft()) || not_equal(layout_.top, layout_node_->GetTop()) || + not_equal(layout_.width, layout_node_->GetWidth()) || not_equal(layout_.height, layout_node_->GetHeight()); + + layout_.left = std::isnan(layout_node_->GetLeft()) ? 0 : layout_node_->GetLeft(); + layout_.top = std::isnan(layout_node_->GetTop()) ? 0 : layout_node_->GetTop(); + layout_.width = std::isnan(layout_node_->GetWidth()) ? 0 : std::max(layout_node_->GetWidth(), .0); + layout_.height = std::isnan(layout_node_->GetHeight()) ? 0 : std::max(layout_node_->GetHeight(), .0); layout_.marginLeft = layout_node_->GetMargin(Edge::EdgeLeft); layout_.marginTop = layout_node_->GetMargin(Edge::EdgeTop); layout_.marginRight = layout_node_->GetMargin(Edge::EdgeRight); @@ -364,19 +374,19 @@ void DomNode::TransferLayoutOutputsRecursive(std::vector(kLayoutEvent, - weak_from_this(), - std::make_shared(std::move(layout_obj))); - auto root = root_node_.lock(); - if (root != nullptr) { - auto manager = root->GetDomManager().lock(); - if (manager != nullptr) { - std::vector> ops = {[WEAK_THIS, event] { - DEFINE_AND_CHECK_SELF(DomNode) - self->HandleEvent(event); - }}; - manager->PostTask(Scene(std::move(ops))); + if (trigger_layout_event) { + auto event = std::make_shared(kLayoutEvent, weak_from_this(), + std::make_shared(std::move(layout_obj))); + auto root = root_node_.lock(); + if (root != nullptr) { + auto manager = root->GetDomManager().lock(); + if (manager != nullptr) { + std::vector> ops = {[WEAK_THIS, event] { + DEFINE_AND_CHECK_SELF(DomNode) + self->HandleEvent(event); + }}; + manager->PostTask(Scene(std::move(ops))); + } } } } @@ -487,8 +497,8 @@ void DomNode::UpdateDiff(const std::unordered_map>& update_style, const std::unordered_map>& update_dom_ext) { - auto style_diff_value = DiffUtils::DiffProps(*this->GetStyleMap(), update_style); - auto ext_diff_value = DiffUtils::DiffProps(*this->GetExtStyle(), update_dom_ext); + auto style_diff_value = DiffUtils::DiffProps(*this->GetStyleMap(), update_style, false); + auto ext_diff_value = DiffUtils::DiffProps(*this->GetExtStyle(), update_dom_ext, false); auto style_update = std::get<0>(style_diff_value); auto ext_update = std::get<0>(ext_diff_value); std::shared_ptr diff_value = std::make_shared(); @@ -604,6 +614,13 @@ std::ostream& operator<<(std::ostream& os, const RefInfo& ref_info) { return os; } +std::ostream& operator<<(std::ostream& os, const DiffInfo& diff_info) { + os << "{"; + os << "\"skip_style_diff\": " << diff_info.skip_style_diff << ", "; + os << "}"; + return os; +} + std::ostream& operator<<(std::ostream& os, const DomNode& dom_node) { os << "{"; os << "\"id\": " << dom_node.id_ << ", "; @@ -630,10 +647,14 @@ std::ostream& operator<<(std::ostream& os, const DomNode& dom_node) { std::ostream& operator<<(std::ostream& os, const DomInfo& dom_info) { auto dom_node = dom_info.dom_node; auto ref_info = dom_info.ref_info; + auto diff_info = dom_info.diff_info; os << "{"; if (ref_info != nullptr) { os << "\"ref info\": " << *ref_info << ", "; } + if (diff_info != nullptr) { + os << "\"diff info\": " << *diff_info << ", "; + } if (dom_node != nullptr) { os << "\"dom node\": " << *dom_node << ", "; } diff --git a/dom/src/dom/layer_optimized_render_manager.cc b/dom/src/dom/layer_optimized_render_manager.cc index fa70cf2686f..3c744dc0010 100644 --- a/dom/src/dom/layer_optimized_render_manager.cc +++ b/dom/src/dom/layer_optimized_render_manager.cc @@ -78,6 +78,7 @@ void LayerOptimizedRenderManager::UpdateRenderNode(std::weak_ptr root_ std::vector moved_ids; moved_ids.reserve(moved_children.size()); for (const auto& moved_node : moved_children) { + UpdateRenderInfo(moved_node); moved_ids.push_back(footstone::check::checked_numeric_cast(moved_node->GetId())); } MoveRenderNode(root_node, std::move(moved_ids), @@ -112,7 +113,7 @@ void LayerOptimizedRenderManager::MoveRenderNode(std::weak_ptr root_no } FOOTSTONE_DLOG(INFO) << "[Hippy Statistic] move node size before optimize = " << nodes.size() << ", move node size after optimize = " << nodes_to_move.size(); - render_manager_->MoveRenderNode(root_node, std::move(nodes)); + render_manager_->MoveRenderNode(root_node, std::move(nodes_to_move)); } void LayerOptimizedRenderManager::DeleteRenderNode(std::weak_ptr root_node, @@ -128,19 +129,6 @@ void LayerOptimizedRenderManager::DeleteRenderNode(std::weak_ptr root_ FOOTSTONE_DLOG(INFO) << "[Hippy Statistic] delete node size before optimize = " << nodes.size() << ", delete node size after optimize = " << nodes_to_delete.size(); if (!nodes_to_delete.empty()) { - for (auto& node : nodes_to_delete) { - // Recursively delete all ids on the node tree. - std::vector> node_stack; - node_stack.push_back(node); - while (!node_stack.empty()) { - auto back_node = node_stack.back(); - node_stack.pop_back(); - not_eliminated_node_ids_.erase(back_node->GetId()); - for (auto& child : back_node->GetChildren()) { - node_stack.push_back(child); - } - } - } render_manager_->DeleteRenderNode(root_node, std::move(nodes_to_delete)); } } @@ -289,10 +277,9 @@ bool LayerOptimizedRenderManager::IsJustLayoutProp(const char *prop_name) const } bool LayerOptimizedRenderManager::CanBeEliminated(const std::shared_ptr& node) { - bool eliminated = (node->IsLayoutOnly() || node->IsVirtual()) && - (not_eliminated_node_ids_.find(node->GetId()) == not_eliminated_node_ids_.end()); + bool eliminated = (node->IsLayoutOnly() || node->IsVirtual()) && node->IsEnableEliminated(); if (!eliminated) { - not_eliminated_node_ids_.insert(node->GetId()); + node->SetEnableEliminated(false); } return eliminated; } diff --git a/dom/src/dom/root_node.cc b/dom/src/dom/root_node.cc index 31059bcf9bf..eb94863ea70 100644 --- a/dom/src/dom/root_node.cc +++ b/dom/src/dom/root_node.cc @@ -23,7 +23,6 @@ #include #include "dom/animation/animation_manager.h" -#include "dom/diff_utils.h" #include "dom/render_manager.h" #include "footstone/deserializer.h" #include "footstone/hippy_value.h" @@ -45,10 +44,68 @@ using Task = footstone::Task; footstone::utils::PersistentObjectMap> RootNode::persistent_map_; +// In Hippy Vue, there are some special cases where there are multiple update instructions for the same node. This can +// cause issues with the diff algorithm and lead to incorrect results. +// Example: +// +// Dom Node: +// |------|--------------------------------------| +// | id | style: {text: "a", color: "red"} | +// | 1 | diff: {} | +// |------|--------------------------------------| +// +// Previous update algorithm: +// |------|-----------------------| update instructions: |------|-----------------------| update instructions: |------|-------------------------------------| +// | id | style: {text: "a"} | { text: "b"} | id | style: {text: "b"} | { text: "b", fontsize: 12} | id | style: {text: "b", fontsize: 12} | +// | 1 | diff: {} | -------------------> | 1 | diff: {text: "b"} | --------------------------> | 1 | diff: {fontsize: "b"} | +// |------|-----------------------| |------|-----------------------| |------|-------------------------------------| +// In the previous diff algorithm, the differences were generated by comparing the DOM styles and update instructions. +// However, in Hippy Vue, two update instructions might be generated within the same batch. This can lead to incorrect diff results. +// The diff should be {text: "b", fontsize: 12}, but the previous diff algorithm cacluate {fontsize: "b"} +// +// To address this issue, the new update algorithm is as follows: +// 1. When a node's style needs to be updated for the first time, we save the current style. +// 2. Subsequent update differences are generated by comparing the saved styles with the update instructions. +// 3. At the end of the batch, we clear the saved styles. +bool DomNodeStyleDiffer::Calculate(const std::shared_ptr& root_node, + const std::shared_ptr& dom_info, hippy::dom::DiffValue& style_diff, + hippy::dom::DiffValue& ext_style_diff) { + if (!root_node) return false; + if (dom_info == nullptr || dom_info->dom_node == nullptr) return false; + + auto dom_node = root_node->GetNode(dom_info->dom_node->GetId()); + if (dom_node == nullptr) return false; + uint32_t dom_id = dom_node->GetId(); + + // 保存 batch 最早的 style 和 ext_style, 该批次中的所有的 diff 都由这个 style 比较产生 + if (node_style_map_.find(dom_id) == node_style_map_.end()) { + std::unordered_map> style; + std::unordered_map> ext_style; + auto dom_style = dom_node->GetStyleMap(); + for (const auto& pair : *dom_style) { + style[pair.first] = std::make_shared(*pair.second); + } + node_style_map_.insert({dom_id, style}); + auto dom_ext_style = dom_node->GetExtStyle(); + for (const auto& pair : *dom_ext_style) { + ext_style[pair.first] = std::make_shared(*pair.second); + } + node_ext_style_map_.insert({dom_id, ext_style}); + } + + auto base_style = node_style_map_.at(dom_id); + auto base_ext_style = node_ext_style_map_.at(dom_id); + style_diff = DiffUtils::DiffProps(base_style, *dom_info->dom_node->GetStyleMap(), false); + ext_style_diff = DiffUtils::DiffProps(base_ext_style, *dom_info->dom_node->GetExtStyle(), false); + return true; +} + RootNode::RootNode(uint32_t id) : DomNode(id, 0, 0, "", "", nullptr, nullptr, {}) { + InitLayoutConsts(); SetRenderInfo({id, 0, 0}); animation_manager_ = std::make_shared(); interceptors_.push_back(animation_manager_); + style_differ_ = std::make_unique(); } RootNode::RootNode() : RootNode(0) {} @@ -64,17 +121,15 @@ void RootNode::RemoveEventListener(const std::string& name, uint64_t listener_id RemoveEvent(GetId(), name); } -void RootNode::ReleaseResources() { - animation_manager_->RemoveVSyncEventListener(); -} +void RootNode::ReleaseResources() {} -void RootNode::CreateDomNodes(std::vector>&& nodes) { +void RootNode::CreateDomNodes(std::vector>&& nodes, bool needSortByIndex) { for (const auto& interceptor : interceptors_) { interceptor->OnDomNodeCreate(nodes); } std::vector> nodes_to_create; for (const auto& node_info : nodes) { - auto node = node_info->dom_node; + auto& node = node_info->dom_node; std::shared_ptr parent_node = GetNode(node->GetPid()); if (parent_node == nullptr) { continue; @@ -88,8 +143,33 @@ void RootNode::CreateDomNodes(std::vector>&& nodes) { OnDomNodeCreated(node); } for (const auto& node : nodes_to_create) { - node->SetRenderInfo({node->GetId(), node->GetPid(), node->GetSelfIndex()}); + if (needSortByIndex) { + node->SetRenderInfo({node->GetId(), node->GetPid(), node->GetSelfIndex(), node->GetSelfDepth()}); + } else { + // 如果不需要对 index 排序,其他场景目前没有用到 depth,避免冗余计算 + node->SetRenderInfo({node->GetId(), node->GetPid(), node->GetSelfIndex(), -1}); + } + } + + if (needSortByIndex) { + // 针对反向插入的场景 (比如先查 index = 15的节点,再插入 index = 14,13,12.. 的节点),先做排序。否则会导致 renderNode 节点位置错乱。详见: + // https://doc.weixin.qq.com/doc/w3_ANsAsgZ1ACckOPazHXERJqKHOCbP1?scode=AJEAIQdfAAogJJ2RicAMgAvQZ1ACc + // 排序要保证两个原则:1. 父节点在子节点前;2. 同一父节点的子节点,必须按照 index 从小到大的顺序排序 + // 同一层级,不同父节点的子节点,位置可以交叉,但要保证原则2,即同一父节点子节点 index 是从小到大的顺序 + std::stable_sort( + nodes_to_create.begin(), + nodes_to_create.end(), + [](const std::shared_ptr& a, const std::shared_ptr& b) + { + auto render_info_a = a->GetRenderInfo(); + auto render_info_b = b->GetRenderInfo(); + if (render_info_a.depth == render_info_b.depth) { + return render_info_a.index < render_info_b.index; + } + return render_info_a.depth < render_info_b.depth; + }); } + auto event = std::make_shared(kDomTreeCreated, weak_from_this(), nullptr); HandleEvent(event); @@ -103,51 +183,21 @@ void RootNode::UpdateDomNodes(std::vector>&& nodes) { interceptor->OnDomNodeUpdate(nodes); } - // In Hippy Vue, there are some special cases where there are multiple update instructions for the same node. This can - // cause issues with the diff algorithm and lead to incorrect results. - // Example: - // - // Dom Node Style: - // |------|----------------| - // | id | style | - // | 1 | text : {} | - // | 2 | some style | - // - // Update instructions: - // |------|-------------------------------|----------------|------------------------| - // | id | style | operation | diff style result | - // | 1 | text : { "color": "blue" } | compare | { "color": "blue" } | - // | 2 | some style | | | - // | 1 | text : { "color": "red" } | compare | { "color": "red" } | - // | 1 | text : { "color": "red" } | compare | { } | - // In last diff algroithm the diff_style = {} - // - // To Solve this case we should use the last update instruction to generate the diff style. - // Update instructions: - // |------|-------------------------------|----------------|------------------------| - // | id | style | operation | diff style result | - // | 1 | text : { "color": "blue" } | skip | { } | - // | 2 | some style | | | - // | 1 | text : { "color": "red" } | skip | { } | - // | 1 | text : { "color": "red" } | compare | { "color": "red" } | - // In new diff algroithm the diff_style = { "color": "red" } - std::unordered_map> skipped_instructions; - for (const auto& node_info : nodes) { - auto id = node_info->dom_node->GetId(); - skipped_instructions[id] = node_info; - } - std::vector> nodes_to_update; - for (const auto& [id, node_info] : skipped_instructions) { - std::shared_ptr dom_node = GetNode(node_info->dom_node->GetId()); + for (const auto& node : nodes) { + std::shared_ptr dom_node = GetNode(node->dom_node->GetId()); if (dom_node == nullptr) { continue; } - // diff props - auto style_diff_value = DiffUtils::DiffProps(*dom_node->GetStyleMap(), *node_info->dom_node->GetStyleMap()); - auto ext_diff_value = DiffUtils::DiffProps(*dom_node->GetExtStyle(), *node_info->dom_node->GetExtStyle()); - auto style_update = std::get<0>(style_diff_value); - auto ext_update = std::get<0>(ext_diff_value); + + hippy::dom::DiffValue style_diff, ext_style_diff; + if (!style_differ_->Calculate(std::static_pointer_cast(shared_from_this()), node, style_diff, + ext_style_diff)) { + continue; + } + + auto style_update = std::get<0>(style_diff); + auto ext_update = std::get<0>(ext_style_diff); std::shared_ptr diff_value = std::make_shared(); if (!style_update->empty()) { diff_value->insert(style_update->begin(), style_update->end()); @@ -155,12 +205,12 @@ void RootNode::UpdateDomNodes(std::vector>&& nodes) { if (!ext_update->empty()) { diff_value->insert(ext_update->begin(), ext_update->end()); } - dom_node->SetStyleMap(node_info->dom_node->GetStyleMap()); - dom_node->SetExtStyleMap(node_info->dom_node->GetExtStyle()); + dom_node->SetStyleMap(node->dom_node->GetStyleMap()); + dom_node->SetExtStyleMap(node->dom_node->GetExtStyle()); dom_node->SetDiffStyle(diff_value); - auto style_delete = std::get<1>(style_diff_value); - auto ext_delete = std::get<1>(ext_diff_value); + auto style_delete = std::get<1>(style_diff); + auto ext_delete = std::get<1>(ext_style_diff); std::shared_ptr> delete_value = std::make_shared>(); if (!style_delete->empty()) { delete_value->insert(delete_value->end(), style_delete->begin(), style_delete->end()); @@ -170,8 +220,6 @@ void RootNode::UpdateDomNodes(std::vector>&& nodes) { delete_value->insert(delete_value->end(), ext_delete->begin(), ext_delete->end()); } dom_node->SetDeleteProps(delete_value); - node_info->dom_node->SetDiffStyle(diff_value); - node_info->dom_node->SetDeleteProps(delete_value); if (!style_update->empty() || !style_delete->empty()) { dom_node->UpdateLayoutStyleInfo(*style_update, *style_delete); } @@ -207,7 +255,7 @@ void RootNode::MoveDomNodes(std::vector>&& nodes) { continue; } nodes_to_move.push_back(node); - parent_node->AddChildByRefInfo(std::make_shared(node, node_info->ref_info)); + parent_node->AddChildByRefInfo(std::make_shared(node, node_info->ref_info, nullptr)); } for (const auto& node : nodes_to_move) { node->SetRenderInfo({node->GetId(), node->GetPid(), node->GetSelfIndex()}); @@ -274,14 +322,21 @@ void RootNode::CallFunction(uint32_t id, const std::string& name, const DomArgum } void RootNode::SyncWithRenderManager(const std::shared_ptr& render_manager) { + TDF_PERF_DO_STMT_AND_LOG(unsigned long domCnt = dom_operations_.size();, "RootNode::SyncWithRenderManager"); + if (style_differ_ != nullptr) style_differ_->Reset(); FlushDomOperations(render_manager); + TDF_PERF_DO_STMT_AND_LOG(unsigned long evCnt = event_operations_.size(); + , "RootNode::FlushDomOperations Done, dom op count:%lld", domCnt); FlushEventOperations(render_manager); + TDF_PERF_LOG("RootNode::FlushEventOperations Done, event op count:%d", evCnt); DoAndFlushLayout(render_manager); + TDF_PERF_LOG("RootNode::DoAndFlushLayout Done"); auto dom_manager = dom_manager_.lock(); if (dom_manager) { dom_manager->RecordDomEndTimePoint(); } render_manager->EndBatch(GetWeakSelf()); + TDF_PERF_LOG("RootNode::SyncWithRenderManager End"); } void RootNode::AddEvent(uint32_t id, const std::string& event_name) { @@ -446,7 +501,7 @@ void RootNode::FlushDomOperations(const std::shared_ptr& render_m } void RootNode::MarkLayoutNodeDirty(const std::vector>& nodes) { - for (const auto& node: nodes) { + for (const auto& node : nodes) { if (node && node->GetLayoutNode() && !node->GetLayoutNode()->HasParentEngineNode()) { auto parent = node->GetParent(); while (parent) { diff --git a/dom/src/dom/scene_builder.cc b/dom/src/dom/scene_builder.cc index 5a79c09cb30..a122f8d87d9 100644 --- a/dom/src/dom/scene_builder.cc +++ b/dom/src/dom/scene_builder.cc @@ -30,11 +30,12 @@ inline namespace dom { void SceneBuilder::Create(const std::weak_ptr& weak_dom_manager, const std::weak_ptr& root_node, - std::vector>&& nodes) { + std::vector>&& nodes, + bool needSortByIndex) { auto dom_manager = weak_dom_manager.lock(); if (dom_manager) { dom_manager->RecordDomStartTimePoint(); - dom_manager->CreateDomNodes(root_node, std::move(nodes)); + dom_manager->CreateDomNodes(root_node, std::move(nodes), needSortByIndex); } } diff --git a/dom/src/dom/serializer_unittests.cc b/dom/src/dom/serializer_unittests.cc index 98babf341d0..33a433fd69d 100644 --- a/dom/src/dom/serializer_unittests.cc +++ b/dom/src/dom/serializer_unittests.cc @@ -1,3 +1,23 @@ +/* + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2022 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #define private public #define protected public @@ -124,7 +144,7 @@ void CheckString(footstone::value::Serializer& serializer, std::string value, si TEST(SerializerTest, Release) { footstone::value::Serializer serializer; - serializer.Release(); + footstone::value::SerializerHelper(serializer.Release()); EXPECT_EQ(serializer.buffer_ == nullptr, true) << "Serializer buffer is not equal to nullptr."; EXPECT_EQ(serializer.buffer_size_, 0) << "Serializer buffer_size is not equal to 0."; EXPECT_EQ(serializer.buffer_capacity_, 0) << "Serializer buffer_capacity is not equal to 0."; diff --git a/dom/src/dom/taitank_layout_node.cc b/dom/src/dom/taitank_layout_node.cc index 236bb0b253d..cd9f1766fcd 100644 --- a/dom/src/dom/taitank_layout_node.cc +++ b/dom/src/dom/taitank_layout_node.cc @@ -22,7 +22,6 @@ #include #include -#include #include "footstone/logging.h" @@ -31,79 +30,82 @@ namespace hippy { inline namespace dom { -static std::atomic global_measure_function_key{0}; -static std::map measure_function_map; -static std::mutex mutex; - -const std::map kOverflowMap = {{"visible", OverflowType::OVERFLOW_VISIBLE}, - {"hidden", OverflowType::OVERFLOW_HIDDEN}, - {"scroll", OverflowType::OVERFLOW_SCROLL}}; - -const std::map kFlexDirectionMap = { - {"row", FlexDirection::FLEX_DIRECTION_ROW}, - {"row-reverse", FlexDirection::FLEX_DIRECTION_ROW_REVERSE}, - {"column", FlexDirection::FLEX_DIRECTION_COLUMN}, - {"column-reverse", FlexDirection::FLEX_DIRECTION_COLUNM_REVERSE}}; - -const std::map kWrapModeMap = {{"nowrap", FlexWrapMode::FLEX_NO_WRAP}, - {"wrap", FlexWrapMode::FLEX_WRAP}, - {"wrap-reverse", FlexWrapMode::FLEX_WRAP_REVERSE}}; - -const std::map kJustifyMap = {{"flex-start", FlexAlign::FLEX_ALIGN_START}, - {"center", FlexAlign::FLEX_ALIGN_CENTER}, - {"flex-end", FlexAlign::FLEX_ALIGN_END}, - {"space-between", FlexAlign::FLEX_ALIGN_SPACE_BETWEEN}, - {"space-around", FlexAlign::FLEX_ALIGN_SPACE_AROUND}, - {"space-evenly", FlexAlign::FLEX_ALIGN_SPACE_EVENLY}}; - -const std::map kAlignMap = {{"auto", FlexAlign::FLEX_ALIGN_AUTO}, - {"flex-start", FlexAlign::FLEX_ALIGN_START}, - {"center", FlexAlign::FLEX_ALIGN_CENTER}, - {"flex-end", FlexAlign::FLEX_ALIGN_END}, - {"stretch", FlexAlign::FLEX_ALIGN_STRETCH}, - {"baseline", FlexAlign::FLEX_ALIGN_BASE_LINE}, - {"space-between", FlexAlign::FLEX_ALIGN_SPACE_BETWEEN}, - {"space-around", FlexAlign::FLEX_ALIGN_SPACE_AROUND}}; - -const std::map kMarginMap = {{kMargin, CSSDirection::CSS_ALL}, - {kMarginVertical, CSSDirection::CSS_VERTICAL}, - {kMarginHorizontal, CSSDirection::CSS_HORIZONTAL}, - {kMarginLeft, CSSDirection::CSS_LEFT}, - {kMarginRight, CSSDirection::CSS_RIGHT}, - {kMarginTop, CSSDirection::CSS_TOP}, - {kMarginBottom, CSSDirection::CSS_BOTTOM}}; - -const std::map kPaddingMap = {{kPadding, CSSDirection::CSS_ALL}, - {kPaddingVertical, CSSDirection::CSS_VERTICAL}, - {kPaddingHorizontal, CSSDirection::CSS_HORIZONTAL}, - {kPaddingLeft, CSSDirection::CSS_LEFT}, - {kPaddingRight, CSSDirection::CSS_RIGHT}, - {kPaddingTop, CSSDirection::CSS_TOP}, - {kPaddingBottom, CSSDirection::CSS_BOTTOM}}; - -const std::map kPositionMap = {{kLeft, CSSDirection::CSS_LEFT}, - {kRight, CSSDirection::CSS_RIGHT}, - {kTop, CSSDirection::CSS_TOP}, - {kBottom, CSSDirection::CSS_BOTTOM}}; - -const std::map kBorderMap = {{kBorderWidth, CSSDirection::CSS_LEFT}, - {kBorderLeftWidth, CSSDirection::CSS_LEFT}, - {kBorderTopWidth, CSSDirection::CSS_TOP}, - {kBorderRightWidth, CSSDirection::CSS_RIGHT}, - {kBorderBottomWidth, CSSDirection::CSS_BOTTOM}}; - -const std::map kPositionTypeMap = {{"relative", PositionType::POSITION_TYPE_RELATIVE}, - {"absolute", PositionType::POSITION_TYPE_ABSOLUTE}}; - -const std::map kDisplayTypeMap = {{"none", DisplayType::DISPLAY_TYPE_NONE}}; - -const std::map kDirectionMap = { - {"inherit", DIRECTION_INHERIT}, {"ltr", DIRECTION_LTR}, {"rtl", DIRECTION_RTL}}; +class TaitankLayoutConsts { +public: + const std::map kOverflowMap = {{"visible", OverflowType::OVERFLOW_VISIBLE}, + {"hidden", OverflowType::OVERFLOW_HIDDEN}, + {"scroll", OverflowType::OVERFLOW_SCROLL}}; + + const std::map kFlexDirectionMap = { + {"row", FlexDirection::FLEX_DIRECTION_ROW}, + {"row-reverse", FlexDirection::FLEX_DIRECTION_ROW_REVERSE}, + {"column", FlexDirection::FLEX_DIRECTION_COLUMN}, + {"column-reverse", FlexDirection::FLEX_DIRECTION_COLUNM_REVERSE}}; + + const std::map kWrapModeMap = {{"nowrap", FlexWrapMode::FLEX_NO_WRAP}, + {"wrap", FlexWrapMode::FLEX_WRAP}, + {"wrap-reverse", FlexWrapMode::FLEX_WRAP_REVERSE}}; + + const std::map kJustifyMap = {{"flex-start", FlexAlign::FLEX_ALIGN_START}, + {"center", FlexAlign::FLEX_ALIGN_CENTER}, + {"flex-end", FlexAlign::FLEX_ALIGN_END}, + {"space-between", FlexAlign::FLEX_ALIGN_SPACE_BETWEEN}, + {"space-around", FlexAlign::FLEX_ALIGN_SPACE_AROUND}, + {"space-evenly", FlexAlign::FLEX_ALIGN_SPACE_EVENLY}}; + + const std::map kAlignMap = {{"auto", FlexAlign::FLEX_ALIGN_AUTO}, + {"flex-start", FlexAlign::FLEX_ALIGN_START}, + {"center", FlexAlign::FLEX_ALIGN_CENTER}, + {"flex-end", FlexAlign::FLEX_ALIGN_END}, + {"stretch", FlexAlign::FLEX_ALIGN_STRETCH}, + {"baseline", FlexAlign::FLEX_ALIGN_BASE_LINE}, + {"space-between", FlexAlign::FLEX_ALIGN_SPACE_BETWEEN}, + {"space-around", FlexAlign::FLEX_ALIGN_SPACE_AROUND}}; + + const std::map kMarginMap = {{kMargin, CSSDirection::CSS_ALL}, + {kMarginVertical, CSSDirection::CSS_VERTICAL}, + {kMarginHorizontal, CSSDirection::CSS_HORIZONTAL}, + {kMarginLeft, CSSDirection::CSS_LEFT}, + {kMarginRight, CSSDirection::CSS_RIGHT}, + {kMarginTop, CSSDirection::CSS_TOP}, + {kMarginBottom, CSSDirection::CSS_BOTTOM}}; + + const std::map kPaddingMap = {{kPadding, CSSDirection::CSS_ALL}, + {kPaddingVertical, CSSDirection::CSS_VERTICAL}, + {kPaddingHorizontal, CSSDirection::CSS_HORIZONTAL}, + {kPaddingLeft, CSSDirection::CSS_LEFT}, + {kPaddingRight, CSSDirection::CSS_RIGHT}, + {kPaddingTop, CSSDirection::CSS_TOP}, + {kPaddingBottom, CSSDirection::CSS_BOTTOM}}; + + const std::map kPositionMap = {{kLeft, CSSDirection::CSS_LEFT}, + {kRight, CSSDirection::CSS_RIGHT}, + {kTop, CSSDirection::CSS_TOP}, + {kBottom, CSSDirection::CSS_BOTTOM}}; + + const std::map kBorderMap = {{kBorderWidth, CSSDirection::CSS_ALL}, + {kBorderLeftWidth, CSSDirection::CSS_LEFT}, + {kBorderTopWidth, CSSDirection::CSS_TOP}, + {kBorderRightWidth, CSSDirection::CSS_RIGHT}, + {kBorderBottomWidth, CSSDirection::CSS_BOTTOM}}; + + const std::map kPositionTypeMap = {{"relative", PositionType::POSITION_TYPE_RELATIVE}, + {"absolute", PositionType::POSITION_TYPE_ABSOLUTE}}; + + const std::map kDisplayTypeMap = {{"none", DisplayType::DISPLAY_TYPE_NONE}}; + + const std::map kDirectionMap = { + {"inherit", DIRECTION_INHERIT}, {"ltr", DIRECTION_LTR}, {"rtl", DIRECTION_RTL}}; +}; + +static std::shared_ptr global_layout_consts = nullptr; #define TAITANK_GET_STYLE_DECL(NAME, TYPE, DEFAULT) \ static TYPE GetStyle##NAME(const std::string& key) { \ - auto iter = k##NAME##Map.find(key); \ - if (iter != k##NAME##Map.end()) return iter->second; \ + if (global_layout_consts == nullptr) return DEFAULT; \ + auto &map = global_layout_consts->k##NAME##Map; \ + auto iter = map.find(key); \ + if (iter != map.end()) return iter->second; \ return DEFAULT; \ } @@ -162,7 +164,7 @@ TAITANK_GET_STYLE_DECL(Direction, TaitankDirection, TaitankDirection::DIRECTION_ } static void CheckValueType(footstone::value::HippyValue::Type type) { - if (type == footstone::value::HippyValue::Type::kNumber || type == footstone::value::HippyValue::Type::kObject) + if (type == footstone::value::HippyValue::Type::kString || type == footstone::value::HippyValue::Type::kObject) FOOTSTONE_DLOG(WARNING) << "Taitank Layout Node Value Type Error"; } @@ -208,17 +210,11 @@ static CSSDirection GetCSSDirectionFromEdge(Edge edge) { } } -TaitankLayoutNode::TaitankLayoutNode() : key_(global_measure_function_key.fetch_add(1)) { Allocate(); } +TaitankLayoutNode::TaitankLayoutNode() { Allocate(); } -TaitankLayoutNode::TaitankLayoutNode(TaitankNodeRef engine_node_) - : engine_node_(engine_node_), key_(global_measure_function_key.fetch_add(1)) {} +TaitankLayoutNode::TaitankLayoutNode(TaitankNodeRef engine_node_) : engine_node_(engine_node_) {} -TaitankLayoutNode::~TaitankLayoutNode() { - std::lock_guard lock(mutex); - const auto it = measure_function_map.find(key_); - if (it != measure_function_map.end()) measure_function_map.erase(it); - Deallocate(); -} +TaitankLayoutNode::~TaitankLayoutNode() { Deallocate(); } void TaitankLayoutNode::CalculateLayout(float parent_width, float parent_height, Direction direction, void* layout_context) { @@ -469,32 +465,30 @@ void TaitankLayoutNode::Parser( } } -static TaitankSize TaitankMeasureFunction(TaitankNodeRef node, float width, MeasureMode width_measrue_mode, - float height, MeasureMode height_measure_mode, void* context) { - auto taitank_node = reinterpret_cast(node->GetContext()); - int64_t key = taitank_node->GetKey(); - auto iter = measure_function_map.find(key); - if (iter != measure_function_map.end()) { - auto size = iter->second(width, ToLayoutMeasureMode(width_measrue_mode), height, - ToLayoutMeasureMode(height_measure_mode), context); - TaitankSize result; - result.width = size.width; - result.height = size.height; - return result; - } - return TaitankSize{0, 0}; -} - void TaitankLayoutNode::SetMeasureFunction(MeasureFunction measure_function) { assert(engine_node_ != nullptr); - measure_function_map[key_] = measure_function; + measure_function_ = measure_function; engine_node_->SetContext(reinterpret_cast(this)); - engine_node_->SetMeasureFunction(TaitankMeasureFunction); + auto func = [](TaitankNodeRef node, float width, MeasureMode width_measrue_mode, float height, + MeasureMode height_measure_mode, void* context) -> TaitankSize { + auto taitank_node = reinterpret_cast(node->GetContext()); + if (taitank_node->measure_function_) { + auto size = taitank_node->measure_function_(width, ToLayoutMeasureMode(width_measrue_mode), height, + ToLayoutMeasureMode(height_measure_mode), context); + TaitankSize result; + result.width = size.width; + result.height = size.height; + return result; + } + return TaitankSize{0, 0}; + }; + TaitankMeasureFunction taitank_measure_function = func; + engine_node_->SetMeasureFunction(taitank_measure_function); } bool TaitankLayoutNode::HasMeasureFunction() { assert(engine_node_ != nullptr); - return measure_function_map.find(key_) != measure_function_map.end(); + return measure_function_ != nullptr; } float TaitankLayoutNode::GetLeft() { @@ -649,7 +643,9 @@ void TaitankLayoutNode::SetPosition(Edge edge, float position) { void TaitankLayoutNode::SetScaleFactor(float sacle_factor) { assert(engine_node_ != nullptr); TaitankConfigRef config = engine_node_->GetConfig(); - config->SetScaleFactor(sacle_factor); + if (config) { + config->SetScaleFactor(sacle_factor); + } } void TaitankLayoutNode::SetMaxWidth(float max_width) { @@ -822,6 +818,13 @@ void TaitankLayoutNode::Allocate() { engine_node_ = new TaitankNode(); } void TaitankLayoutNode::Deallocate() { if (engine_node_ == nullptr) return; delete engine_node_; + engine_node_ = nullptr; +} + +void InitLayoutConsts() { + if (global_layout_consts == nullptr) { + global_layout_consts = std::make_shared(); + } } std::shared_ptr CreateLayoutNode() { return std::make_shared(); } diff --git a/dom/src/dom/yoga_layout_node.cc b/dom/src/dom/yoga_layout_node.cc index 303d721c702..7b681694fa5 100644 --- a/dom/src/dom/yoga_layout_node.cc +++ b/dom/src/dom/yoga_layout_node.cc @@ -762,6 +762,7 @@ void YogaLayoutNode::Deallocate() { YGConfigFree(yoga_config_); } +void InitLayoutConsts() {} std::shared_ptr CreateLayoutNode() { return std::make_shared(); } } // namespace dom diff --git a/driver/js/.eslintrc.js b/driver/js/.eslintrc.js index 99c8ddf95ee..d4ef3af80b6 100644 --- a/driver/js/.eslintrc.js +++ b/driver/js/.eslintrc.js @@ -68,6 +68,7 @@ module.exports = { '@typescript-eslint/consistent-type-assertions': 'off', '@typescript-eslint/naming-convention': 'off', '@typescript-eslint/prefer-for-of': 'off', + '@typescript-eslint/no-require-imports': 'off', }, parserOptions: { project: ['./**/tsconfig.json'], @@ -171,6 +172,8 @@ module.exports = { ['sfc', resolveVue('sfc')], ['he', path.resolve(__dirname, './packages/hippy-vue/src/util/entity-decoder')], ['@hippy-vue-next-style-parser', resolvePackage('hippy-vue-next-style-parser')], + ['@hippy-vue-next', resolvePackage('hippy-vue-next')], + ['@hippy-vue-next-server-renderer', resolvePackage('hippy-vue-next-server-renderer')], ], }, }, diff --git a/driver/js/examples/hippy-react-demo/package.json b/driver/js/examples/hippy-react-demo/package.json index d9e627eb039..e3225b0130c 100644 --- a/driver/js/examples/hippy-react-demo/package.json +++ b/driver/js/examples/hippy-react-demo/package.json @@ -22,9 +22,9 @@ "React" ], "dependencies": { - "@hippy/react": "v3.0-dev", + "@hippy/react": "v3.3-latest", "@hippy/react-reconciler": "react17", - "@hippy/react-web": "v3.0-dev", + "@hippy/react-web": "v3.3-latest", "@hippy/rmc-list-view": "latest", "@hippy/rmc-pull-to-refresh": "latest", "@hippy/web-renderer": "latest", diff --git a/driver/js/examples/hippy-react-demo/src/components/Modal/index.jsx b/driver/js/examples/hippy-react-demo/src/components/Modal/index.jsx index e08f6cce27a..1d59f0d1332 100644 --- a/driver/js/examples/hippy-react-demo/src/components/Modal/index.jsx +++ b/driver/js/examples/hippy-react-demo/src/components/Modal/index.jsx @@ -39,11 +39,13 @@ const styles = StyleSheet.create({ }, selectionText: { fontSize: 20, - color: SKIN_COLOR.mainLight, textAlign: 'center', textAlignVertical: 'center', marginLeft: 10, marginRight: 10, + padding: 5, + borderRadius: 5, + borderWidth: 2, }, }); @@ -55,6 +57,9 @@ export default class ModalExpo extends React.Component { visible: false, press: false, animationType: 'fade', + immerseStatusBar: false, + hideStatusBar: false, + hideNavigationBar: false, }; this.show = this.show.bind(this); this.hide = this.hide.bind(this); @@ -98,24 +103,64 @@ export default class ModalExpo extends React.Component { {this.setState({animationType: 'fade'})}} - style={[styles.selectionText, {backgroundColor: this.state.animationType === 'fade' ? 'rgba(255, 0, 0, 0.5)' : '#FFFFFF'}]} + style={[styles.selectionText, + {borderColor: this.state.animationType === 'fade' ? 'red' : SKIN_COLOR.mainLight}, + {color: this.state.animationType === 'fade' ? 'red' : SKIN_COLOR.mainLight} + ]} >fade {this.setState({animationType: 'slide'})}} - style={[styles.selectionText, {backgroundColor: this.state.animationType === 'slide' ? 'rgba(255, 0, 0, 0.5)' : '#FFFFFF'}]} + style={[styles.selectionText, + {borderColor: this.state.animationType === 'slide' ? 'red' : SKIN_COLOR.mainLight}, + {color: this.state.animationType === 'slide' ? 'red' : SKIN_COLOR.mainLight} + ]} >slide {this.setState({animationType: 'slide_fade'})}} - style={[styles.selectionText, {backgroundColor: this.state.animationType === 'slide_fade' ? 'rgba(255, 0, 0, 0.5)' : '#FFFFFF'}]} + style={[styles.selectionText, + {borderColor: this.state.animationType === 'slide_fade' ? 'red' : SKIN_COLOR.mainLight}, + {color: this.state.animationType === 'slide_fade' ? 'red' : SKIN_COLOR.mainLight} + ]} >slide_fade + + {this.setState({hideStatusBar: !this.state.hideStatusBar})}} + style={[styles.selectionText, + {borderColor: this.state.hideStatusBar ? 'red' : SKIN_COLOR.mainLight}, + {color: this.state.hideStatusBar ? 'red' : SKIN_COLOR.mainLight} + ]} + >autoHideStatusBar + + + {this.setState({immerseStatusBar: !this.state.immerseStatusBar})}} + style={[styles.selectionText, + {borderColor: this.state.immerseStatusBar ? 'red' : SKIN_COLOR.mainLight}, + {color: this.state.immerseStatusBar ? 'red' : SKIN_COLOR.mainLight} + ]} + >immersionStatusBar + + + {this.setState({hideNavigationBar: !this.state.hideNavigationBar})}} + style={[styles.selectionText, + {borderColor: this.state.hideNavigationBar ? 'red' : SKIN_COLOR.mainLight}, + {color: this.state.hideNavigationBar ? 'red' : SKIN_COLOR.mainLight} + ]} + >autoHideNavigationBar + { /* Trigger when hardware back pressed */ }} + onShow={() => { console.log('modal show'); }} + requestClose={() => { /* Trigger when hardware back pressed */ }} + orientationChange={(evt) => { console.log('orientation changed', evt.orientation); }} supportedOrientations={['portrait']} - immersionStatusBar={true} + immersionStatusBar={this.state.immerseStatusBar} + autoHideStatusBar={this.state.hideStatusBar} + autoHideNavigationBar={this.state.hideNavigationBar} > ( {title} ); return ( - + {renderTitle('shadow')} { let textShadowColor = 'red'; @@ -173,10 +207,13 @@ export default class TextExpo extends React.Component { This is red {renderTitle('fontSize')} - + { `Text fontSize is ${fontSize}` } + + 切换字体颜色 + 放大字体 @@ -184,6 +221,18 @@ export default class TextExpo extends React.Component { 缩小字体 + {renderTitle('fontWeight')} + + + { `Text fontWeight is ${fontWeight}` } + + + 加粗字体 + + + 减细字体 + + {renderTitle('fontStyle')} Text fontStyle is normal diff --git a/driver/js/examples/hippy-react-demo/src/components/TextInput/index.jsx b/driver/js/examples/hippy-react-demo/src/components/TextInput/index.jsx index 69923279f03..307800cef60 100644 --- a/driver/js/examples/hippy-react-demo/src/components/TextInput/index.jsx +++ b/driver/js/examples/hippy-react-demo/src/components/TextInput/index.jsx @@ -20,6 +20,9 @@ const styles = StyleSheet.create({ fontSize: 16, color: '#242424', height: 30, + // you can use lineHeight + // to control the space between lines in multi-line input. + // for example: lineHeight: 30, }, input_style_block: { diff --git a/driver/js/examples/hippy-react-demo/src/components/WaterfallView/index.jsx b/driver/js/examples/hippy-react-demo/src/components/WaterfallView/index.jsx index 2cc4cb790fb..816642785ae 100644 --- a/driver/js/examples/hippy-react-demo/src/components/WaterfallView/index.jsx +++ b/driver/js/examples/hippy-react-demo/src/components/WaterfallView/index.jsx @@ -5,7 +5,6 @@ import { StyleSheet, Text, Dimensions, - RefreshWrapper, } from '@hippy/react'; import mockDataTemp from '../../shared/UIStyles/mock'; @@ -55,8 +54,9 @@ export default class ListExample extends React.Component { super(props); this.state = { dataSource: [], - pullingText: '继续下拉触发刷新', - loadingState: '正在加载...', + headerRefreshText: '继续下拉触发刷新', + footerRefreshText: '正在加载...', + horizontal: undefined, }; this.numberOfColumns = 2; this.columnSpacing = 6; @@ -69,8 +69,14 @@ export default class ListExample extends React.Component { this.onRefresh = this.onRefresh.bind(this); this.getRefresh = this.getRefresh.bind(this); this.renderPullFooter = this.renderPullFooter.bind(this); + this.renderPullHeader = this.renderPullHeader.bind(this); + this.onHeaderReleased = this.onHeaderReleased.bind(this); + this.onHeaderPulling = this.onHeaderPulling.bind(this); + this.onFooterPulling = this.onFooterPulling.bind(this); this.renderBanner = this.renderBanner.bind(this); this.getItemStyle = this.getItemStyle.bind(this); + this.getHeaderStyle = this.getHeaderStyle.bind(this); + this.onScroll = this.onScroll.bind(this); } async componentDidMount() { @@ -80,7 +86,9 @@ export default class ListExample extends React.Component { /** * 页面加载更多时触发 - * 这里触发加载更多还可以使用 PullFooter 组件,主要看是否需要一个内容加载区。 + * + * 这里触发加载更多还可以使用 PullFooter 组件。 + * * onEndReached 更适合用来无限滚动的场景。 */ async onEndReached() { @@ -91,7 +99,7 @@ export default class ListExample extends React.Component { } this.loadMoreDataFlag = true; this.setState({ - loadingState: '加载更多...', + footerRefreshText: '加载更多...', }); let newData = []; try { @@ -99,21 +107,93 @@ export default class ListExample extends React.Component { } catch (err) {} if (newData.length === 0) { this.setState({ - loadingState: '没有更多数据', + footerRefreshText: '没有更多数据', }); } const newDataSource = [...dataSource, ...newData]; this.setState({ dataSource: newDataSource }); this.loadMoreDataFlag = false; + this.listView.collapsePullFooter(); + } + + /** + * 下拉超过内容高度,松手后触发 + */ + async onHeaderReleased() { + if (this.fetchingDataFlag) { + return; + } + this.fetchingDataFlag = true; + console.log('onHeaderReleased'); + this.setState({ + headerRefreshText: '刷新数据中,请稍等', + }); + let dataSource = []; + try { + dataSource = await this.mockFetchData(); + } catch (err) {} + this.fetchingDataFlag = false; + this.setState({ + dataSource, + headerRefreshText: '2秒后收起', + }, () => { + this.listView.collapsePullHeader({ time: 2000 }); + }); + } + + /** + * 下拉过程中触发 + * + * 事件会通过 contentOffset 参数返回拖拽高度,我们已经知道了内容高度, + * 简单对比一下就可以显示不同的状态。 + * + * 这里简单处理,其实可以做到更复杂的动态效果。 + */ + onHeaderPulling(evt) { + if (this.fetchingDataFlag) { + return; + } + console.log('onHeaderPulling', evt.contentOffset); + if (evt.contentOffset > styles.pullContent.height) { + this.setState({ + headerRefreshText: '松手,即可触发刷新', + }); + } else { + this.setState({ + headerRefreshText: '继续下拉,触发刷新', + }); + } + } + + onFooterPulling(evt) { + console.log('onFooterPulling', evt); } + /** + * 渲染 pullFooter 组件 + */ renderPullFooter() { - if (this.state.dataSource.length === 0) return null; - return ( + const { horizontal } = this.state; + return !horizontal ? {this.state.loadingState} - ); + }} + >{this.state.footerRefreshText} + : + {this.state.footerRefreshText} + ; } async onRefresh() { @@ -155,6 +235,10 @@ export default class ListExample extends React.Component { this.listView.scrollToIndex({ index, animation: true }); } + onScroll(obj) { + + } + // render banner(it is not supported on Android yet) renderBanner() { if (this.state.dataSource.length === 0) return null; @@ -215,12 +299,12 @@ export default class ListExample extends React.Component { } getWaterfallContentInset() { - return { top: 0, left: 5, bottom: 0, right: 5 }; + return { top: 0, left: 0, bottom: 0, right: 0 }; } getItemStyle() { const { numberOfColumns, columnSpacing } = this; - const screenWidth = Dimensions.get('screen').width; + const screenWidth = Dimensions.get('screen').width - 32; const contentInset = this.getWaterfallContentInset(); const width = screenWidth - contentInset.left - contentInset.right; return { @@ -228,40 +312,67 @@ export default class ListExample extends React.Component { }; } + getHeaderStyle() { + const { horizontal } = this.state; + return !horizontal ? {} : { + width: 50, + }; + } + + /** + * 渲染 pullHeader 组件 + */ + renderPullHeader() { + const { headerRefreshText, horizontal } = this.state; + return ( + !horizontal ? + {headerRefreshText} + : + {headerRefreshText} + + ); + } + render() { const { dataSource } = this.state; const { numberOfColumns, columnSpacing, interItemSpacing } = this; const contentInset = this.getWaterfallContentInset(); return ( - { - this.refresh = ref; - }} - style={{ flex: 1 }} - onRefresh={this.onRefresh} - bounceTime={100} - getRefresh={this.getRefresh} - > { this.listView = ref; }} - renderBanner={this.renderBanner} numberOfColumns={numberOfColumns} columnSpacing={columnSpacing} interItemSpacing={interItemSpacing} numberOfItems={dataSource.length} + preloadItemNumber={4} style={{ flex: 1 }} - renderItem={this.renderItem} + onScroll={this.onScroll} + renderBanner={this.renderBanner} + renderPullHeader={this.renderPullHeader} onEndReached={this.onEndReached} + onFooterReleased={this.onEndReached} + onHeaderReleased={this.onHeaderReleased} + onHeaderPulling={this.onHeaderPulling} + renderItem={this.renderItem} getItemType={this.getItemType} getItemKey={this.getItemKey} - contentInset={contentInset} getItemStyle={this.getItemStyle} - containPullFooter={true} - renderPullFooter={this.renderPullFooter} + getHeaderStyle={this.getHeaderStyle} + contentInset={contentInset} /> - ); } } diff --git a/driver/js/examples/hippy-react-demo/src/modules/Clipboard/index.jsx b/driver/js/examples/hippy-react-demo/src/modules/Clipboard/index.jsx deleted file mode 100644 index 1649265a119..00000000000 --- a/driver/js/examples/hippy-react-demo/src/modules/Clipboard/index.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { - ScrollView, - Text, - View, - StyleSheet, - Clipboard, -} from '@hippy/react'; - -const styles = StyleSheet.create({ - itemTitle: { - alignItems: 'flex-start', - justifyContent: 'center', - height: 40, - borderWidth: 1, - borderStyle: 'solid', - borderColor: '#e0e0e0', - borderRadius: 2, - backgroundColor: '#fafafa', - padding: 10, - marginTop: 10, - }, - defaultText: { - marginVertical: 4, - fontSize: 18, - lineHeight: 24, - color: '#242424', - }, - copiedText: { - color: '#aaa', - }, - button: { - backgroundColor: '#4c9afa', - borderRadius: 4, - height: 30, - marginVertical: 4, - paddingHorizontal: 6, - alignItems: 'center', - justifyContent: 'center', - }, - buttonText: { - fontSize: 16, - color: 'white', - }, -}); - -export default class ClipboardDemo extends React.Component { - constructor(props) { - super(props); - this.state = { - hasCopied: false, - text: 'Winter is coming', - clipboardText: '点击上面的按钮', - }; - } - - render() { - const renderTitle = title => ( - - {title} - - ); - const { hasCopied, text, clipboardText } = this.state; - const copiedText = hasCopied ? ' (已复制) ' : ''; - return ( - - {renderTitle('文本复制到剪贴板')} - {text} - { - Clipboard.setString(text); - this.setState({ - hasCopied: true, - }); - }} - > - {`点击复制以上文案${copiedText}`} - - {renderTitle('获取剪贴板内容')} - { - try { - const str = await Clipboard.getString(); - this.setState({ - clipboardText: str, - }); - } catch (err) { - console.error(err); - } - }} - > - 点击获取剪贴板内容 - - {clipboardText} - - ); - } -} diff --git a/driver/js/examples/hippy-react-demo/src/modules/index.js b/driver/js/examples/hippy-react-demo/src/modules/index.js index 029d9c8cb17..6ff316a0c40 100644 --- a/driver/js/examples/hippy-react-demo/src/modules/index.js +++ b/driver/js/examples/hippy-react-demo/src/modules/index.js @@ -2,7 +2,6 @@ export { default as Animation } from './Animation'; export { default as AsyncStorage } from './AsyncStorage'; -export { default as Clipboard } from './Clipboard'; export { default as NetInfo } from './NetInfo'; export { default as WebSocket } from './WebSocket'; export { default as UIManagerModule } from './UIManagerModule'; diff --git a/driver/js/examples/hippy-react-demo/src/pages/gallery.jsx b/driver/js/examples/hippy-react-demo/src/pages/gallery.jsx index b44b85f146b..ed57c079124 100644 --- a/driver/js/examples/hippy-react-demo/src/pages/gallery.jsx +++ b/driver/js/examples/hippy-react-demo/src/pages/gallery.jsx @@ -188,6 +188,7 @@ export class Gallery extends Component { renderRow={this.renderRow} getRowType={this.getRowType} getRowKey={this.getRowKey} + paintType="fcp" /> ); } diff --git a/driver/js/examples/hippy-react-demo/src/routes.js b/driver/js/examples/hippy-react-demo/src/routes.js index 49e104924c2..f829521fd03 100644 --- a/driver/js/examples/hippy-react-demo/src/routes.js +++ b/driver/js/examples/hippy-react-demo/src/routes.js @@ -32,14 +32,6 @@ export default [ type: Type.COMPONENT, }, }, - { - path: '/Clipboard', - name: ' 组件', - component: PAGE_LIST.Clipboard, - meta: { - type: Type.COMPONENT, - }, - }, { path: '/Text', name: ' 组件', diff --git a/driver/js/examples/hippy-vue-demo/package.json b/driver/js/examples/hippy-vue-demo/package.json index 419e63ee5fa..caef249c2b0 100644 --- a/driver/js/examples/hippy-vue-demo/package.json +++ b/driver/js/examples/hippy-vue-demo/package.json @@ -16,9 +16,9 @@ "web:build": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.web-renderer.js" }, "dependencies": { - "@hippy/vue": "v3.0-dev", - "@hippy/vue-native-components": "v3.0-dev", - "@hippy/vue-router": "v3.0-dev", + "@hippy/vue": "3.3.1-rc.1", + "@hippy/vue-native-components": "v3.3-latest", + "@hippy/vue-router": "v3.3-latest", "@hippy/web-renderer": "latest", "core-js": "^3.11.0", "vue": "^2.6.10", @@ -39,8 +39,8 @@ "@hippy/hippy-dynamic-import-plugin": "^2.0.0", "@hippy/hippy-hmr-plugin": "^0.1.0", "@hippy/rejection-tracking-polyfill": "^1.0.0", - "@hippy/vue-css-loader": "v3.0-dev", - "@hippy/vue-loader": "v3.0-dev", + "@hippy/vue-css-loader": "v3.3-latest", + "@hippy/vue-loader": "v3.3-latest", "@vue/cli-plugin-babel": "^4.1.0", "@vue/cli-plugin-eslint": "^4.1.0", "@vue/cli-service": "^4.1.0", diff --git a/driver/js/examples/hippy-vue-demo/src/components/demos/demo-p.vue b/driver/js/examples/hippy-vue-demo/src/components/demos/demo-p.vue index aa410456f41..966a5b7885c 100644 --- a/driver/js/examples/hippy-vue-demo/src/components/demos/demo-p.vue +++ b/driver/js/examples/hippy-vue-demo/src/components/demos/demo-p.vue @@ -1,5 +1,8 @@ @@ -343,8 +317,6 @@ export default { screenIsVertical, storageValue: '', storageSetStatus: 'ready to set', - clipboardString: 'ready to set', - clipboardValue: '', imageSize: '', netInfoText: '正在获取...', fetchText: '请求网址中...', @@ -439,18 +411,6 @@ export default { this.cookiesValue = cookies; }); }, - setString() { - Vue.Native.Clipboard.setString('hippy'); - this.clipboardString = 'copy "hippy" value succeed'; - }, - async getString() { - const value = await Vue.Native.Clipboard.getString(); - if (value) { - this.clipboardValue = value; - } else { - this.clipboardValue = 'undefined'; - } - }, }, }; diff --git a/driver/js/examples/hippy-vue-demo/src/components/native-demos/demo-waterfall.vue b/driver/js/examples/hippy-vue-demo/src/components/native-demos/demo-waterfall.vue index 1d0383f0878..a95b59a450a 100644 --- a/driver/js/examples/hippy-vue-demo/src/components/native-demos/demo-waterfall.vue +++ b/driver/js/examples/hippy-vue-demo/src/components/native-demos/demo-waterfall.vue @@ -1,33 +1,42 @@ @@ -73,10 +87,27 @@ export default { isRefreshing: false, Vue, STYLE_LOADING, - loadingState: '正在加载...', + headerRefreshText: '继续下拉触发刷新', + footerRefreshText: '正在加载...', isLoading: false, + isAndroid: Vue.Native.Platform === 'android', }; }, + mounted() { + // *** loadMoreDataFlag 是加载锁,业务请照抄 *** + // 因为 onEndReach 位于屏幕底部时会多次触发, + // 所以需要加一个锁,当未加载完成时不进行二次加载 + this.loadMoreDataFlag = false; + this.fetchingDataFlag = false; + this.dataSource = [...mockData]; + if (Vue.Native) { + this.$windowHeight = Vue.Native.Dimensions.window.height; + console.log('Vue.Native.Dimensions.window', Vue.Native.Dimensions); + } else { + this.$windowHeight = window.innerHeight; + } + this.$refs.pullHeader.collapsePullHeader({ time: 2000 }); + }, computed: { refreshText() { return this.isRefreshing ? '正在刷新' : '下拉刷新'; @@ -114,6 +145,35 @@ export default { }, 600); }); }, + onHeaderPulling(evt) { + if (this.fetchingDataFlag) { + return; + } + console.log('onHeaderPulling', evt.contentOffset); + if (evt.contentOffset > 30) { + this.headerRefreshText = '松手,即可触发刷新'; + } else { + this.headerRefreshText = '继续下拉,触发刷新'; + } + }, + onFooterPulling(evt) { + console.log('onFooterPulling', evt); + }, + onHeaderIdle() {}, + onFooterIdle() {}, + async onHeaderReleased() { + if (this.fetchingDataFlag) { + return; + } + this.fetchingDataFlag = true; + console.log('onHeaderReleased'); + this.headerRefreshText = '刷新数据中,请稍等'; + const dataSource = await this.mockFetchData(); + this.fetchingDataFlag = false; + this.headerRefreshText = '2秒后收起'; + // 要主动调用collapsePullHeader关闭pullHeader,否则可能会导致released事件不能再次触发 + this.$refs.pullHeader.collapsePullHeader({ time: 2000 }); + }, async onRefresh() { // 重新获取数据 this.isRefreshing = true; @@ -128,21 +188,18 @@ export default { async onEndReached() { const { dataSource } = this; // 检查锁,如果在加载中,则直接返回,防止二次加载数据 - if (this.isLoading) { + if (this.loadMoreDataFlag) { return; } - - this.isLoading = true; - this.loadingState = '正在加载...'; - + this.loadMoreDataFlag = true; + this.footerRefreshText = '加载更多...'; const newData = await this.mockFetchData(); - if (!newData) { - this.loadingState = '没有更多数据'; - this.isLoading = false; - return; + if (newData.length === 0) { + this.footerRefreshText = '没有更多数据'; } this.dataSource = [...dataSource, ...newData]; - this.isLoading = false; + this.loadMoreDataFlag = false; + this.$refs.pullFooter.collapsePullFooter(); }, onItemClick(index) { @@ -157,10 +214,28 @@ export default { flex: 1; } -#demo-waterfall .refresh-header { +#demo-waterfall .ul-refresh { background-color: #40b883; } +#demo-waterfall .ul-refresh-text { + color: white; + height: 50px; + line-height: 50px; + text-align: center; +} + +#demo-waterfall .pull-footer { + background-color: #40b883; + height: 40px; +} + +#demo-waterfall .pull-footer-text { + color: white; + line-height: 40px; + text-align: center; +} + #demo-waterfall .refresh-text { height: 40px; line-height: 40px; @@ -184,60 +259,60 @@ export default { align-items: center; } -#demo-waterfall >>> .list-view-item { +#demo-waterfall .list-view-item { background-color: #eeeeee; } -#demo-waterfall >>> .article-title { +#demo-waterfall .article-title { font-size: 12px; line-height: 16px; color: #242424; } -#demo-waterfall >>> .normal-text { +#demo-waterfall .normal-text { font-size: 10px; color: #aaa; align-self: center; } -#demo-waterfall >>> .image { +#demo-waterfall .image { flex: 1; height: 120px; resize: both; } -#demo-waterfall >>> .style-one-image-container { +#demo-waterfall .style-one-image-container { flex-direction: row; justify-content: center; margin-top: 8px; flex: 1; } -#demo-waterfall >>> .style-one-image { +#demo-waterfall .style-one-image { height: 60px; } -#demo-waterfall >>> .style-two { +#demo-waterfall .style-two { flex-direction: row; justify-content: space-between; } -#demo-waterfall >>> .style-two-left-container { +#demo-waterfall .style-two-left-container { flex: 1; flex-direction: column; justify-content: center; margin-right: 8px; } -#demo-waterfall >>> .style-two-image-container { +#demo-waterfall .style-two-image-container { flex: 1; } -#demo-waterfall >>> .style-two-image { +#demo-waterfall .style-two-image { height: 80px; } -#demo-waterfall >>> .refresh { +#demo-waterfall .refresh { background-color: #40b883; } diff --git a/driver/js/examples/hippy-vue-demo/src/pages/menu.vue b/driver/js/examples/hippy-vue-demo/src/pages/menu.vue index bf8032a3372..cafb229f430 100644 --- a/driver/js/examples/hippy-vue-demo/src/pages/menu.vue +++ b/driver/js/examples/hippy-vue-demo/src/pages/menu.vue @@ -31,7 +31,7 @@
  • -

    +

    终端组件 Demos

  • diff --git a/driver/js/examples/hippy-vue-next-demo/package.json b/driver/js/examples/hippy-vue-next-demo/package.json index e5dc6e09c2b..49bf7deac60 100644 --- a/driver/js/examples/hippy-vue-next-demo/package.json +++ b/driver/js/examples/hippy-vue-next-demo/package.json @@ -16,11 +16,12 @@ "web:build": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.web-renderer.js" }, "dependencies": { - "@hippy/vue-next": "latest", - "@hippy/vue-router-next-history": "latest", + "@hippy/vue-next": "v3.3-latest", + "@hippy/vue-router-next-history": "0.0.1", "@hippy/web-renderer": "latest", - "@vue/runtime-core": "^3.2.21", - "vue": "^3.2.21", + "@vue/runtime-core": "^3.4.32", + "@vue/shared": "^3.4.32", + "vue": "^3.4.32", "vue-router": "^4.0.12" }, "devDependencies": { @@ -39,10 +40,10 @@ "@hippy/hippy-dynamic-import-plugin": "^2.0.0", "@hippy/hippy-hmr-plugin": "^0.1.0", "@hippy/rejection-tracking-polyfill": "^1.0.0", - "@hippy/vue-css-loader": "^2.0.1", + "@hippy/vue-css-loader": "v3.3-latest", "@vitejs/plugin-vue": "^1.9.4", "@vue/cli-service": "^4.5.19", - "@vue/compiler-sfc": "^3.2.21", + "@vue/compiler-sfc": "^3.4.32", "babel-loader": "^8.1.0", "case-sensitive-paths-webpack-plugin": "^2.2.0", "clean-webpack-plugin": "^4.0.0", @@ -58,6 +59,7 @@ "url-loader": "^4.0.0", "vue-loader": "^17.0.0", "webpack": "^4.46.0", - "webpack-cli": "^4.7.2" + "webpack-cli": "^4.7.2", + "webpack-bundle-analyzer": "^4.10.2" } } diff --git a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.android-vendor.js b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.android-vendor.js index 54a2c99b29a..a8689aece99 100644 --- a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.android-vendor.js +++ b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.android-vendor.js @@ -23,6 +23,7 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), __PLATFORM__: JSON.stringify(platform), + __VUE_PROD_DEVTOOLS__: false, }), new CaseSensitivePathsPlugin(), new VueLoaderPlugin(), diff --git a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.android.js b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.android.js index d9810b35073..c6188b5a3ca 100644 --- a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.android.js +++ b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.android.js @@ -35,6 +35,7 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), __PLATFORM__: JSON.stringify(platform), + __VUE_PROD_DEVTOOLS__: false, }), new CaseSensitivePathsPlugin(), new VueLoaderPlugin(), @@ -141,6 +142,8 @@ module.exports = { alias: (() => { const aliases = { src: path.resolve('./src'), + // hippy 仅需要运行时的 Vue,在这里指定 + vue$: 'vue/dist/vue.runtime.esm-bundler.js', }; // If @vue/runtime-core was built exist in packages directory then make an alias diff --git a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios-vendor.js b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios-vendor.js index 7a383bda137..a23657ec9fb 100644 --- a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios-vendor.js +++ b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios-vendor.js @@ -23,6 +23,7 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), __PLATFORM__: JSON.stringify(platform), + __VUE_PROD_DEVTOOLS__: false, }), new CaseSensitivePathsPlugin(), new VueLoaderPlugin(), diff --git a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios.js b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios.js index 1791f9cf403..a8154d41539 100644 --- a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios.js +++ b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.ios.js @@ -35,6 +35,7 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), __PLATFORM__: JSON.stringify(platform), + __VUE_PROD_DEVTOOLS__: false, }), new CaseSensitivePathsPlugin(), new VueLoaderPlugin(), @@ -141,6 +142,8 @@ module.exports = { alias: (() => { const aliases = { src: path.resolve('./src'), + // hippy 仅需要运行时的 Vue,在这里指定 + vue$: 'vue/dist/vue.runtime.esm-bundler.js', }; // If @vue/runtime-core was built exist in packages directory then make an alias diff --git a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.web-renderer.js b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.web-renderer.js index 5646b76e44f..eec1d4a15e0 100644 --- a/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.web-renderer.js +++ b/driver/js/examples/hippy-vue-next-demo/scripts/hippy-webpack.web-renderer.js @@ -138,6 +138,8 @@ module.exports = { alias: (() => { const aliases = { src: path.resolve('./src'), + // hippy 仅需要运行时的 Vue,在这里指定 + vue$: 'vue/dist/vue.runtime.esm-bundler.js', }; // If @vue/runtime-core was built exist in packages directory then make an alias diff --git a/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-dialog.vue b/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-dialog.vue index ee1a6dfcbf5..5b83bb13871 100644 --- a/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-dialog.vue +++ b/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-dialog.vue @@ -19,6 +19,27 @@ > 显示对话框--slide_fade + + +
    @@ -54,6 +79,7 @@ :animationType="dialogAnimationType" :transparent="true" @requestClose="onClose" + @orientationChange="onOrientationChange" >
    { dialogIsVisible.value = !dialogIsVisible.value; dialogAnimationType.value = type; }; + const onClickDialogConfig = (option) => { + switch (option) { + case 'hideStatusBar': + autoHideStatusBar.value = !autoHideStatusBar.value; + break; + case 'immerseStatusBar': + immersionStatusBar.value = !immersionStatusBar.value; + break; + case 'hideNavigationBar': + autoHideNavigationBar.value = !autoHideNavigationBar.value; + break; + default: + break; + } + }; const onClickOpenSecond = (evt) => { evt.stopPropagation(); dialog2IsVisible.value = !dialog2IsVisible.value; @@ -106,7 +153,9 @@ export default defineComponent({ const onShow = () => { console.log('Dialog is opening'); }; - + const onOrientationChange = (evt) => { + console.log('orientation changed', evt.nativeParams); + }; const onClose = (evt) => { evt.stopPropagation(); /** @@ -139,11 +188,15 @@ export default defineComponent({ dialogIsVisible, dialog2IsVisible, dialogAnimationType, + immersionStatusBar, + autoHideStatusBar, + autoHideNavigationBar, stopPropagation, onClose, onShow, onClickView, onClickOpenSecond, + onClickDialogConfig, }; }, }); diff --git a/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-vue-native.vue b/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-vue-native.vue index 2897b62fa4d..6405d34d68e 100644 --- a/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-vue-native.vue +++ b/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-vue-native.vue @@ -293,32 +293,6 @@ {{ cookiesValue }}
    - -
    - -
    - - {{ clipboardString }} -
    -
    - - {{ clipboardValue }} -
    -
    -
    { - Native.Clipboard.setString('hippy'); - clipboardString.value = 'clipboard set "hippy" value succeed'; - }; - - /** - * get content of clipboard - */ - const getString = async () => { - const value = await Native.Clipboard.getString(); - if (value) { - clipboardValue.value = value; - } else { - clipboardValue.value = 'undefined'; - } - }; - const getBoundingClientRect = async (relToContainer = false) => { try { const rect = await Native.getBoundingClientRect(rectRef.value as HippyNode, { relToContainer }); @@ -482,8 +434,6 @@ export default defineComponent({ rectRef, storageValue, storageSetStatus, - clipboardString, - clipboardValue, imageSize, netInfoText, superProps, @@ -494,8 +444,6 @@ export default defineComponent({ setItem, getItem, removeItem, - setString, - getString, setCookie, getCookie, getBoundingClientRect, diff --git a/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-waterfall.vue b/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-waterfall.vue index 943f14753a8..d67ab9635e8 100644 --- a/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-waterfall.vue +++ b/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-waterfall.vue @@ -1,67 +1,74 @@ @@ -87,6 +94,7 @@ const interItemSpacing = 6; const numberOfColumns = 2; // inner content padding const contentInset = { top: 0, left: 5, bottom: 0, right: 5 }; +const isAndroid = Native.Platform === 'android'; const mockFetchData = async (): Promise => new Promise((resolve) => { setTimeout(() => { @@ -113,9 +121,14 @@ export default defineComponent({ ...mockData, ]); - let isLoading = false; + let loadMoreDataFlag = false; + let fetchingDataFlag = false; const isRefreshing = ref(false); const loadingState = ref('正在加载...'); + const pullHeader = ref(null); + const pullFooter = ref(null); + let headerRefreshText = '继续下拉触发刷新'; + let footerRefreshText = '正在加载...'; const refreshText = computed(() => (isRefreshing.value ? '正在刷新' : '下拉刷新')); const gridView = ref(null); const header = ref(null); @@ -141,26 +154,62 @@ export default defineComponent({ } }; + const onHeaderPulling = (evt) => { + if (fetchingDataFlag) { + return; + } + console.log('onHeaderPulling', evt.contentOffset); + if (evt.contentOffset > 30) { + headerRefreshText = '松手,即可触发刷新'; + } else { + headerRefreshText = '继续下拉,触发刷新'; + } + }; + const onFooterPulling = (evt) => { + console.log('onFooterPulling', evt); + }; + const onHeaderIdle = () => {}; + const onFooterIdle = () => {}; + const onHeaderReleased = async () => { + if (fetchingDataFlag) { + return; + } + fetchingDataFlag = true; + console.log('onHeaderReleased'); + headerRefreshText = '刷新数据中,请稍等'; + fetchingDataFlag = false; + headerRefreshText = '2秒后收起'; + // 要主动调用collapsePullHeader关闭pullHeader,否则可能会导致released事件不能再次触发 + if (pullHeader.value) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + pullHeader.value.collapsePullHeader({ time: 2000 }); + } + }; + // scroll to bottom callback const onEndReached = async () => { console.log('end Reached'); - if (isLoading) { + if (loadMoreDataFlag) { return; } - isLoading = true; - loadingState.value = '正在加载...'; + loadMoreDataFlag = true; + footerRefreshText = '加载更多...'; const newData = await mockFetchData(); - if (!newData) { - loadingState.value = '没有更多数据'; - isLoading = false; - return; + if (newData.length === 0) { + footerRefreshText = '没有更多数据'; } dataSource.value = [...dataSource.value, ...newData]; - isLoading = false; + loadMoreDataFlag = false; + if (pullFooter.value) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + pullFooter.value.collapsePullFooter(); + } }; const onClickItem = (index) => { @@ -199,6 +248,17 @@ export default defineComponent({ onRefresh, onEndReached, onClickItem, + isAndroid, + onHeaderPulling, + onFooterPulling, + onHeaderIdle, + onFooterIdle, + onHeaderReleased, + headerRefreshText, + footerRefreshText, + loadMoreDataFlag, + pullHeader, + pullFooter, }; }, }); @@ -209,8 +269,24 @@ export default defineComponent({ flex: 1; } -#demo-waterfall .refresh-header { +#demo-waterfall .ul-refresh { + background-color: #40b883; +} + +#demo-waterfall .ul-refresh-text { + color: white; + height: 50px; + line-height: 50px; + text-align: center; +} +#demo-waterfall .pull-footer { background-color: #40b883; + height: 40px; +} +#demo-waterfall .pull-footer-text { + color: white; + line-height: 40px; + text-align: center; } #demo-waterfall .refresh-text { diff --git a/driver/js/examples/hippy-vue-next-demo/src/main-native.ts b/driver/js/examples/hippy-vue-next-demo/src/main-native.ts index 69a3e0c7e3a..5e22a426ece 100644 --- a/driver/js/examples/hippy-vue-next-demo/src/main-native.ts +++ b/driver/js/examples/hippy-vue-next-demo/src/main-native.ts @@ -4,6 +4,7 @@ import { EventBus, setScreenSize, BackAndroid, + Native, } from '@hippy/vue-next'; import App from './app.vue'; @@ -34,6 +35,7 @@ const app: HippyApp = createApp(App, { backgroundColor: 4283416717, // 状态栏背景图,要注意这个会根据容器尺寸拉伸。 + // background image of status bar, scale with wrapper size // backgroundImage: 'https://user-images.githubusercontent.com/12878546/148737148-d0b227cb-69c8-4b21-bf92-739fb0c3f3aa.png', }, }, @@ -44,6 +46,25 @@ const app: HippyApp = createApp(App, { * default is true, if set false, it will follow vue-loader compilerOptions whitespace setting */ trimWhitespace: true, + styleOptions: { + beforeLoadStyle: (decl) => { + let { value } = decl; + // 比如可以对 rem 单位进行处理 + if (typeof value === 'string' && /rem$/.test(value)) { + // get the numeric value of rem + + const { screen } = Native.Dimensions; + // 比如可以对 rem 单位进行处理 + if (typeof value === 'string' && /rem$/.test(value)) { + const { width, height } = screen; + // 防止hippy 旋转后,宽度发生变化 + const realWidth = width > height ? width : height; + value = Number(parseFloat(`${(realWidth * 100 * Number(value.replace('rem', ''))) / 844}`).toFixed(2)); + } + } + return { ...decl, value }; + }, + }, }); // create router const router = createRouter(); diff --git a/driver/js/examples/hippy-vue-next-demo/src/pages/menu.vue b/driver/js/examples/hippy-vue-next-demo/src/pages/menu.vue index 0cd40adb689..0de84784e3e 100644 --- a/driver/js/examples/hippy-vue-next-demo/src/pages/menu.vue +++ b/driver/js/examples/hippy-vue-next-demo/src/pages/menu.vue @@ -31,7 +31,7 @@
  • -

    +

    终端组件 Demos

  • diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/.gitignore b/driver/js/examples/hippy-vue-next-ssr-demo/.gitignore new file mode 100644 index 00000000000..a0dddc6fb8c --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/.npmrc b/driver/js/examples/hippy-vue-next-ssr-demo/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/README.md b/driver/js/examples/hippy-vue-next-ssr-demo/README.md new file mode 100644 index 00000000000..6429d65e1ff --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/README.md @@ -0,0 +1,46 @@ +# @hippy/vue-next demo + + +### Introduction +This package is the demo project for @hippy/vue-next. Project include most use case for +@hippy/vue-next. Just try it. + +### Usage +Read the hippy framework [doc](https://github.com/Tencent/Hippy/blob/master/README.md#-getting-started) and learn +how to use. + +### How To Use SSR + +we were support SSR for @hippy/vue-next. here is only how to use SSR. how to use vue-next doc is [here](https://hippyjs.org/en-us/#/hippy-vue/vue3) + +1. Before running vue-next-ssr-demo, you should run `npm run init` at root directory to install dependencies and build front-end sdk packages. +2. Then run `cd examples/hippy-vue-next-ssr-demo` and `npm install --legacy-peer-deps` to install demo dependencies. + +Now determine which environment you want build + +> Because our server listening port 8080, so if you are using android device, you should run `adb reverse tcp:8080 tcp:8080` +> to forward mobile device port to pc port, iOS simulator doesn't need this step. + +ensure you were at `examples/hippy-vue-next-ssr-demo`. + +#### Development + +1. run `npm run ssr:dev-client` to build client entry & client bundle, then running hippy debug server +2. run `npm run ssr:dev-server` to build server bundle and start SSR web server to listen port **8080**. +3. debug your app with [reference](https://hippyjs.org/en-us/#/guide/debug) +> You can change server listen port 8080 in `server.ts` by your self, but you also need change request port 8080 in +> `src/main-client.ts` and modify the adb reverse port, ensure port is same at three place + +#### Production + +1. run `npm run ssr:prod-build` to build client entry, server bundle, client bundle +2. run `npm run ssr:prod-server` to start SSR web server to listen port **8080**. +3. test your app +> In production, you can use process manage tool to manage your NodeJs process, like pm2. +> +> And you should deploy you web server at real server with real domain, then you can request +> SSR cgi like https://xxx.com/getSsrFirstScreenData +> + +#### Tips +> Usage of non SSR is [here](https://hippyjs.org/en-us/#/guide/integration) diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/app.d.ts b/driver/js/examples/hippy-vue-next-ssr-demo/app.d.ts new file mode 100644 index 00000000000..d614236bf60 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/app.d.ts @@ -0,0 +1,10 @@ +declare module '*.jpg'; +declare module '*.png'; +declare module '*.vue' { + import { defineComponent } from 'vue'; + const Component: ReturnType; + export default Component; +} + +type NeedToTyped = any; + diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/package.json b/driver/js/examples/hippy-vue-next-ssr-demo/package.json new file mode 100644 index 00000000000..90a2f4ee02e --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/package.json @@ -0,0 +1,84 @@ +{ + "name": "hippy-vue-next-demo", + "version": "3.0.0", + "description": "A SSR Demo Example For Hippy-Vue-Next Library To Show.", + "private": true, + "webMain": "./src/main-web.ts", + "nativeMain": "./src/main-native.ts", + "serverMain": "./src/main-server.ts", + "serverEntry": "./server.ts", + "ssrMain": "./src/main.ts", + "repository": "https://github.com/Tencent/Hippy/tree/master/examples/hippy-vue-next-demo", + "license": "Apache-2.0", + "author": "OpenHippy Team", + "scripts": { + "hippy:dev": "node ./scripts/env-polyfill.js hippy-dev -c ./scripts/hippy-webpack.dev.js", + "web:dev": "npm run hippy:dev & node ./scripts/env-polyfill.js webpack serve --config ./scripts/hippy-webpack.web-renderer.dev.js", + "web:build": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.web-renderer.js", + "ssr:dev-client": "node ./scripts/env-polyfill.js hippy-dev -c ./scripts/webpack-ssr-config/client.dev.js", + "ssr:dev-server": "node ./scripts/env-polyfill.js && node ./scripts/webpack.ssr.dev.js", + "ssr:prod-build": "node ./scripts/webpack.ssr.build.js", + "ssr:prod-server": "node ./dist/server/index.js --mode production" + }, + "dependencies": { + "@hippy/vue-router-next-history": "latest", + "@hippy/web-renderer": "latest", + "@hippy/vue-next": "latest", + "@hippy/vue-next-server-renderer": "latest", + "@hippy/hippy-vue-next-style-parser": "latest", + "@vue/runtime-core": "^3.4.32", + "@vue/server-renderer": "^3.4.32", + "@vue/shared": "^3.4.32", + "core-js": "^3.20.2", + "vue": "^3.4.32", + "vue-router": "^4.0.12", + "express": "^4.18.2", + "pinia": "2.0.30" + }, + "devDependencies": { + "@babel/core": "^7.12.0", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-decorators": "^7.10.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.5.5", + "@babel/plugin-proposal-optional-chaining": "^7.10.4", + "@babel/plugin-transform-async-to-generator": "^7.5.0", + "@babel/plugin-transform-runtime": "^7.11.0", + "@babel/polyfill": "^7.12.0", + "@babel/preset-env": "^7.12.0", + "@babel/runtime": "^7.16.0", + "@hippy/debug-server-next": "latest", + "@hippy/hippy-dynamic-import-plugin": "^2.0.0", + "@hippy/hippy-hmr-plugin": "^0.1.0", + "@hippy/rejection-tracking-polyfill": "^1.0.0", + "@hippy/vue-css-loader": "^2.0.1", + "@vitejs/plugin-vue": "^1.9.4", + "@hippy/vue-next-compiler-ssr": "latest", + "@types/shelljs": "^0.8.5", + "@vue/cli-service": "^4.5.19", + "@vue/compiler-sfc": "^3.4.32", + "babel-loader": "^8.1.0", + "case-sensitive-paths-webpack-plugin": "^2.2.0", + "chokidar": "^3.5.3", + "clean-webpack-plugin": "^4.0.0", + "webpack-manifest-plugin": "^4.1.1", + "cross-env": "^7.0.3", + "cross-env-os": "^7.1.1", + "esbuild": "^0.13.14", + "esbuild-loader": "^2.18.0", + "file-loader": "^4.3.0", + "less": "^4.1.2", + "less-loader": "^7.1.0", + "shelljs": "^0.8.5", + "terser": "^4.8.0", + "ts-loader": "^8.4.0", + "@types/express": "^4.17.17", + "url-loader": "^4.0.0", + "vue-loader": "^17.0.0", + "webpack": "^4.46.0", + "webpack-cli": "^4.7.2" + }, + "engines": { + "node": ">=15" + } +} diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/env-polyfill.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/env-polyfill.js new file mode 100644 index 00000000000..11a413eef85 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/env-polyfill.js @@ -0,0 +1,61 @@ +const { exec } = require('shelljs'); + +const runScript = (scriptStr) => { + console.log(`Full command execute: "${scriptStr}"`); + const result = exec(scriptStr, { stdio: 'inherit' }); + if (result.code !== 0) { + console.error(`❌ Execute cmd - "${scriptStr}" error: ${result.stderr}`); + process.exit(1); + } +}; + +const toNum = (originalNum) => { + const num = `${originalNum}`; + const versionList = num.split('.'); + const currentSplitLength = versionList.length; + if (currentSplitLength !== 4) { + let index = currentSplitLength; + while (index < 4) { + versionList.push('0'); + index += 1; + } + } + const r = ['0000', '000', '00', '0', '']; + for (let i = 0; i < versionList.length; i += 1) { + let len = versionList[i].length; + if (len > 4) { + len = 4; + versionList[i] = versionList[i].slice(0, 4); + } + versionList[i] = r[len] + versionList[i]; + } + return versionList.join(''); +}; + +const versionCompare = (targetVer, currentVer) => { + if (!targetVer || !currentVer) return 1; + const numA = toNum(currentVer); + const numB = toNum(targetVer); + if (numA === numB) { + return 0; + } + return numA < numB ? -1 : 1; +}; + +const LEGACY_OPENSSL_VERSION = '3.0.0'; +const scriptString = process.argv.slice(2).join(' '); +let envPrefixStr = ''; + +console.log(`Start to execute cmd: "${scriptString}"`); +console.log(`Current openssl version: ${process.versions.openssl}`); + +const result = /^(\d+\.\d+\.\d+).*$/.exec(process.versions.openssl.toString().trim()); +if (result && result[1]) { + const currentVersion = result[1]; + const compareResult = versionCompare(LEGACY_OPENSSL_VERSION, currentVersion); + if (compareResult >= 0) { + envPrefixStr += 'NODE_OPTIONS=--openssl-legacy-provider'; + } +} + +runScript(`${envPrefixStr} ${scriptString}`); // start to execute cmd diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.dev.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.dev.js new file mode 100644 index 00000000000..1d5a65f07a7 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.dev.js @@ -0,0 +1,179 @@ +const fs = require('fs'); +const path = require('path'); +const webpack = require('webpack'); +const HippyDynamicImportPlugin = require('@hippy/hippy-dynamic-import-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const { VueLoaderPlugin } = require('vue-loader'); + +const pkg = require('../package.json'); +let cssLoader = '@hippy/vue-css-loader'; +const hippyVueCssLoaderPath = path.resolve(__dirname, '../../../packages/hippy-vue-css-loader/dist/css-loader.js'); +if (fs.existsSync(hippyVueCssLoaderPath)) { + console.warn(`* Using the @hippy/vue-css-loader in ${hippyVueCssLoaderPath}`); + cssLoader = hippyVueCssLoaderPath; +} else { + console.warn('* Using the @hippy/vue-css-loader defined in package.json'); +} + + +module.exports = { + mode: 'development', + devtool: 'eval-source-map', + watch: true, + watchOptions: { + aggregateTimeout: 1500, + }, + devServer: { + // remote debug server address + remote: { + protocol: 'http', + host: '127.0.0.1', + port: 38989, + }, + // support inspect vue components, store and router, by default is disabled + vueDevtools: false, + // support debug multiple project with only one debug server, by default is set false. + multiple: false, + // by default hot and liveReload option are true, you could set only liveReload to true + // to use live reload + hot: true, + liveReload: true, + client: { + overlay: false, + }, + devMiddleware: { + writeToDisk: true, + }, + }, + entry: { + index: ['@hippy/rejection-tracking-polyfill', path.resolve(pkg.nativeMain)], + }, + output: { + filename: 'index.bundle', + // chunkFilename: '[name].[chunkhash].js', + strictModuleExceptionHandling: true, + path: path.resolve('./dist/dev/'), + globalObject: '(0, eval)("this")', + }, + plugins: [ + new VueLoaderPlugin(), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('development'), + HOST: JSON.stringify(process.env.DEV_HOST || '127.0.0.1'), + PORT: JSON.stringify(process.env.DEV_PORT || 38989), + }, + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + __PLATFORM__: null, + }), + new HippyDynamicImportPlugin(), + // LimitChunkCountPlugin can control dynamic import ability + // Using 1 will prevent any additional chunks from being added + // new webpack.optimize.LimitChunkCountPlugin({ + // maxChunks: 1, + // }), + // use SourceMapDevToolPlugin can generate sourcemap file while setting devtool to false + // new webpack.SourceMapDevToolPlugin({ + // test: /\.(js|jsbundle|css|bundle)($|\?)/i, + // filename: '[file].map', + // }), + new CleanWebpackPlugin(), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // disable vue3 dom patch flag,because hippy do not support innerHTML + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + }, + }, + }, + ], + }, + { + test: /\.(le|c)ss$/, + use: [cssLoader, 'less-loader'], + }, + { + test: /\.t|js$/, + use: [ + { + loader: 'esbuild-loader', + options: { + target: 'es2015', + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + limit: true, + // limit: 8192, + // fallback: 'file-loader', + // name: '[name].[ext]', + // outputPath: 'assets/', + }, + }], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + appendTsSuffixTo: [/\.vue$/], + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + return aliases; + })(), + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.web-renderer.dev.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.web-renderer.dev.js new file mode 100644 index 00000000000..7fc0ec81035 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.web-renderer.dev.js @@ -0,0 +1,165 @@ +const fs = require('fs'); +const path = require('path'); +const webpack = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +// const HippyDynamicImportPlugin = require('@hippy/hippy-dynamic-import-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const { VueLoaderPlugin } = require('vue-loader'); + +const pkg = require('../package.json'); +let cssLoader = '@hippy/vue-css-loader'; +const hippyVueCssLoaderPath = path.resolve(__dirname, '../../../packages/hippy-vue-css-loader/dist/css-loader.js'); +if (fs.existsSync(hippyVueCssLoaderPath)) { + console.warn(`* Using the @hippy/vue-css-loader in ${hippyVueCssLoaderPath}`); + cssLoader = hippyVueCssLoaderPath; +} else { + console.warn('* Using the @hippy/vue-css-loader defined in package.json'); +} + +const platform = 'web'; +module.exports = { + mode: 'development', + bail: true, + devServer: { + port: 3000, + hot: true, + liveReload: true, + }, + devtool: 'source-map', + entry: { + index: ['regenerator-runtime', path.resolve(pkg.webMain)], + }, + output: { + // filename: `[name].${platform}.js`, + filename: 'index.bundle.js', + path: path.resolve(`./dist/${platform}/`), + strictModuleExceptionHandling: true, + globalObject: '(0, eval)("this")', + }, + plugins: [ + new VueLoaderPlugin(), + new HtmlWebpackPlugin({ + inject: true, + scriptLoading: 'blocking', + template: path.resolve('./public/web-renderer.html'), + }), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('development'), + HOST: JSON.stringify(process.env.DEV_HOST || '127.0.0.1'), + PORT: JSON.stringify(process.env.DEV_PORT || 38989), + }, + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + __PLATFORM__: platform, + }), + // new HippyDynamicImportPlugin(), + // LimitChunkCountPlugin can control dynamic import ability + // Using 1 will prevent any additional chunks from being added + // new webpack.optimize.LimitChunkCountPlugin({ + // maxChunks: 1, + // }), + // use SourceMapDevToolPlugin can generate sourcemap file while setting devtool to false + // new webpack.SourceMapDevToolPlugin({ + // test: /\.(js|jsbundle|css|bundle)($|\?)/i, + // filename: '[file].map', + // }), + new CleanWebpackPlugin(), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // disable vue3 dom patch flag,because hippy do not support innerHTML + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + }, + }, + }, + ], + }, + { + test: /\.(le|c)ss$/, + use: [cssLoader, 'less-loader'], + }, + { + test: /\.t|js$/, + use: [ + { + loader: 'esbuild-loader', + options: { + target: 'es2015', + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + limit: true, + // limit: 8192, + // fallback: 'file-loader', + // name: '[name].[ext]', + // outputPath: 'assets/', + }, + }], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + appendTsSuffixTo: [/\.vue$/], + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + return aliases; + })(), + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.web-renderer.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.web-renderer.js new file mode 100644 index 00000000000..4e2636dfde5 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/hippy-webpack.web-renderer.js @@ -0,0 +1,166 @@ +const path = require('path'); +const fs = require('fs'); +const { VueLoaderPlugin } = require('vue-loader'); +const webpack = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +// const HippyDynamicImportPlugin = require('@hippy/hippy-dynamic-import-plugin'); +const pkg = require('../package.json'); + +let cssLoader = '@hippy/vue-css-loader'; +const hippyVueCssLoaderPath = path.resolve(__dirname, '../../../packages/hippy-vue-css-loader/dist/css-loader.js'); +if (fs.existsSync(hippyVueCssLoaderPath)) { + console.warn(`* Using the @hippy/vue-css-loader in ${hippyVueCssLoaderPath}`); + cssLoader = hippyVueCssLoaderPath; +} else { + console.warn('* Using the @hippy/vue-css-loader defined in package.json'); +} + +const platform = 'web'; +module.exports = { + mode: 'production', + bail: true, + entry: { + index: ['regenerator-runtime', path.resolve(pkg.webMain)], + }, + output: { + filename: '[name].[contenthash:8].js', + path: path.resolve(`./dist/${platform}/`), + }, + plugins: [ + new VueLoaderPlugin(), + new HtmlWebpackPlugin({ + inject: true, + scriptLoading: 'blocking', + template: path.resolve('./public/web-renderer.html'), + }), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('production'), + }, + __PLATFORM__: null, + }), + // new HippyDynamicImportPlugin(), + // LimitChunkCountPlugin can control dynamic import ability + // Using 1 will prevent any additional chunks from being added + // new webpack.optimize.LimitChunkCountPlugin({ + // maxChunks: 1, + // }), + // use SourceMapDevToolPlugin can generate sourcemap file + // new webpack.SourceMapDevToolPlugin({ + // test: /\.(js|jsbundle|css|bundle)($|\?)/i, + // filename: '[file].map', + // }), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // disable vue3 dom patch flag,because hippy do not support innerHTML + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + }, + }, + }, + ], + }, + { + test: /\.(le|c)ss$/, + use: [cssLoader, 'less-loader'], + }, + { + test: /\.t|js$/, + use: [ + { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + presets: [ + [ + '@babel/preset-env', + { + targets: { + chrome: 57, + }, + }, + ], + ], + plugins: [ + ['@babel/plugin-proposal-class-properties'], + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-runtime', { regenerator: true }], + ], + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + limit: true, + // TODO local path not supported on defaultSource/backgroundImage + // limit: 8192, + // fallback: 'file-loader', + // name: '[name].[ext]', + // outputPath: 'assets/', + }, + }], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + appendTsSuffixTo: [/\.vue$/], + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + return aliases; + })(), + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.android.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.android.js new file mode 100644 index 00000000000..bfd96dce07b --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.android.js @@ -0,0 +1,3 @@ +const { getWebpackSsrBaseConfig } = require('./client.base'); + +module.exports = getWebpackSsrBaseConfig('android', 'production'); diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.android.vendor.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.android.vendor.js new file mode 100644 index 00000000000..85e8b860784 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.android.vendor.js @@ -0,0 +1,108 @@ +const fs = require('fs'); +const path = require('path'); +const webpack = require('webpack'); +const { VueLoaderPlugin } = require('vue-loader'); +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); + +const platform = 'android'; + +module.exports = { + mode: 'production', + bail: true, + entry: { + vendor: [path.resolve(__dirname, '../vendor.js')], + }, + output: { + filename: `[name].${platform}.js`, + path: path.resolve(`./dist/${platform}/`), + globalObject: '(0, eval)("this")', + library: 'hippyVueBase', + }, + plugins: [ + new webpack.NamedModulesPlugin(), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production'), + __PLATFORM__: JSON.stringify(platform), + }), + new CaseSensitivePathsPlugin(), + new VueLoaderPlugin(), + new webpack.DllPlugin({ + context: path.resolve(__dirname, '../..'), + path: path.resolve(__dirname, `../../dist/${platform}/[name]-manifest.json`), + name: 'hippyVueBase', + }), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // disable vue3 dom patch flag,because hippy do not support innerHTML + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + }, + }, + }, + ], + }, + { + test: /\.(js)$/, + use: [ + { + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-env', + { + targets: { + chrome: 57, + }, + }, + ], + ], + plugins: [ + ['@babel/plugin-proposal-class-properties'], + ], + }, + }, + ], + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + return aliases; + })(), + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.base.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.base.js new file mode 100644 index 00000000000..36b6537e19d --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.base.js @@ -0,0 +1,181 @@ +const path = require('path'); +const fs = require('fs'); +const HippyDynamicImportPlugin = require('@hippy/hippy-dynamic-import-plugin'); +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); +const { VueLoaderPlugin } = require('vue-loader'); +const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); +const webpack = require('webpack'); + +const pkg = require('../../package.json'); + +let cssLoader = '@hippy/vue-css-loader'; +const hippyVueCssLoaderPath = path.resolve(__dirname, '../../../../packages/hippy-vue-css-loader/dist/css-loader.js'); +if (fs.existsSync(hippyVueCssLoaderPath)) { + console.warn(`* Using the @hippy/vue-css-loader in ${hippyVueCssLoaderPath}`); + cssLoader = hippyVueCssLoaderPath; +} else { + console.warn('* Using the @hippy/vue-css-loader defined in package.json'); +} + +/** + * get webpack ssr base config + * + * @param platform build platform + * @param env build environment + */ +exports.getWebpackSsrBaseConfig = function (platform, env) { + // do not generate vendor at development + const manifest = require(`../../dist/${platform}/vendor-manifest.json`); + return { + mode: env, + bail: true, + devtool: false, + entry: { + home: [path.resolve(pkg.nativeMain)], + }, + output: { + filename: `[name].${platform}.js`, + path: path.resolve(`./dist/${platform}/`), + globalObject: '(0, eval)("this")', + // CDN path can be configured to load children bundles from remote server + // publicPath: 'https://xxx/hippy/hippyVueNextDemo/', + }, + plugins: [ + new webpack.NamedModulesPlugin(), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify(env), + }, + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + __PLATFORM__: JSON.stringify(platform), + }), + new CaseSensitivePathsPlugin(), + new VueLoaderPlugin(), + new HippyDynamicImportPlugin(), + new WebpackManifestPlugin({ + fileName: `manifest.${platform}.json`, + }), + new webpack.DllReferencePlugin({ + context: path.resolve(__dirname, '../..'), + manifest, + }), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // disable vue3 dom patch flag,because hippy do not support innerHTML + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + // do not generate html comment node + comments: false, + }, + }, + }, + ], + }, + { + test: /\.(le|c)ss$/, + use: [cssLoader, 'less-loader'], + }, + { + test: /\.t|js$/, + use: [ + { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + presets: [ + [ + '@babel/preset-env', + { + targets: platform === 'android' ? { + chrome: 57, + } : { + ios: 9, + }, + }, + ], + ], + plugins: [ + ['@babel/plugin-proposal-class-properties'], + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-runtime', { regenerator: true }], + ], + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + // if you would like to use base64 for picture, uncomment limit: true + // limit: true, + limit: 1024, + fallback: 'file-loader', + name: '[name].[ext]', + outputPath: 'assets/', + }, + }], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + appendTsSuffixTo: [/\.vue$/], + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + return aliases; + })(), + }, + }; +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.dev.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.dev.js new file mode 100644 index 00000000000..cab3754f326 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.dev.js @@ -0,0 +1,215 @@ +const path = require('path'); +const fs = require('fs'); +const HippyDynamicImportPlugin = require('@hippy/hippy-dynamic-import-plugin'); +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); +const { VueLoaderPlugin } = require('vue-loader'); +const webpack = require('webpack'); + +const pkg = require('../../package.json'); + +let cssLoader = '@hippy/vue-css-loader'; +const hippyVueCssLoaderPath = path.resolve(__dirname, '../../../../packages/hippy-vue-css-loader/dist/css-loader.js'); +if (fs.existsSync(hippyVueCssLoaderPath)) { + console.warn(`* Using the @hippy/vue-css-loader in ${hippyVueCssLoaderPath}`); + cssLoader = hippyVueCssLoaderPath; +} else { + console.warn('* Using the @hippy/vue-css-loader defined in package.json'); +} + +/** + * webpack ssr client dev config + */ +module.exports = { + mode: 'development', + bail: true, + devtool: 'eval-source-map', + watch: true, + watchOptions: { + // file changed, rebuild delay time + aggregateTimeout: 1000, + }, + devServer: { + remote: { + protocol: 'http', + host: '127.0.0.1', + port: 38989, + }, + // support vue dev tools,default is false + vueDevtools: false, + // not support one debug server debug multiple app + multiple: false, + // ssr do not support hot replacement now + hot: false, + // default is true + liveReload: false, + client: { + // hippy do not support error tips layer + overlay: false, + }, + devMiddleware: { + // write hot replacement file to disk + writeToDisk: true, + }, + }, + entry: { + // client async bundle + home: [path.resolve(pkg.nativeMain)], + // client ssr entry + index: [path.resolve(pkg.ssrMain)], + }, + output: { + filename: '[name].bundle', + path: path.resolve('./dist/dev/'), + globalObject: '(0, eval)("this")', + }, + plugins: [ + new webpack.NamedModulesPlugin(), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('development'), + HOST: JSON.stringify(process.env.DEV_HOST || '127.0.0.1'), + PORT: JSON.stringify(process.env.DEV_PORT || 38989), + }, + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + __PLATFORM__: null, + }), + new CaseSensitivePathsPlugin(), + new VueLoaderPlugin(), + new HippyDynamicImportPlugin(), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // disable vue3 dom patch flag,because hippy do not support innerHTML + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + // do not generate html comment node + comments: false, + }, + }, + }, + ], + }, + { + test: /\.(le|c)ss$/, + use: [cssLoader, 'less-loader'], + }, + { + test: /\.t|js$/, + use: [ + { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + presets: [ + [ + '@babel/preset-env', + { + targets: { + chrome: 57, + ios: 9, + }, + }, + ], + ], + plugins: [ + ['@babel/plugin-proposal-class-properties'], + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-runtime', { regenerator: true }], + ], + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + // if you would like to use base64 for picture, uncomment limit: true + // limit: true, + fallback: 'file-loader', + name: '[name].[ext]', + outputPath: 'assets/', + }, + }], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + appendTsSuffixTo: [/\.vue$/], + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + // If @hippy/vue-next-style-parser was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextStyleParserPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next-style-parser/dist'); + if (fs.existsSync(path.resolve(hippyVueNextStyleParserPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next-style-parser in ${hippyVueNextStyleParserPath} as @hippy/vue-next-style-parser alias`); + aliases['@hippy/vue-next-style-parser'] = hippyVueNextStyleParserPath; + } else { + console.warn('* Using the @hippy/vue-next-style-parser defined in package.json'); + } + + // If @hippy/vue-next-server-render was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextSsrPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next-server-renderer/dist'); + if (fs.existsSync(path.resolve(hippyVueNextSsrPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next-server-renderer in ${hippyVueNextSsrPath} as @hippy/vue-next-server-renderer alias`); + aliases['@hippy/vue-next-server-renderer'] = hippyVueNextSsrPath; + } else { + console.warn('* Using the @hippy/vue-next-server-renderer defined in package.json'); + } + + return aliases; + })(), + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.entry.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.entry.js new file mode 100644 index 00000000000..660cc908186 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.entry.js @@ -0,0 +1,122 @@ +const path = require('path'); +const webpack = require('webpack'); + +const pkg = require('../../package.json'); +const fs = require('fs') + +module.exports = { + mode: 'production', + devtool: false, + entry: { + index: path.resolve(pkg.ssrMain), + }, + output: { + filename: '[name].js', + strictModuleExceptionHandling: true, + path: path.resolve('./dist'), + globalObject: '(0, eval)("this")', + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('production'), + }, + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + __PLATFORM__: null, + }), + ], + module: { + rules: [ + { + test: /\.t|js$/, + use: [{ + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + presets: [ + [ + '@babel/preset-env', + { + targets: { + chrome: 57, + ios: 9, + }, + }, + ], + ], + plugins: [ + ['@babel/plugin-proposal-class-properties'], + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-runtime', { regenerator: true }], + ], + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + // comment line when production environment + // entry file do not have image asset + limit: true, + // limit: 8192, + // fallback: 'file-loader', + // name: '[name].[ext]', + // outputPath: 'assets/', + }, + }], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + appendTsSuffixTo: [/\.vue$/], + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', + }, + ], + }, + resolve: { + extensions: ['.js', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @hippy/vue-next-style-parser was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextStyleParserPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next-style-parser/dist'); + if (fs.existsSync(path.resolve(hippyVueNextStyleParserPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next-style-parser in ${hippyVueNextStyleParserPath} as @hippy/vue-next-style-parser alias`); + aliases['@hippy/vue-next-style-parser'] = hippyVueNextStyleParserPath; + } else { + console.warn('* Using the @hippy/vue-next-style-parser defined in package.json'); + } + + // If @hippy/vue-next-server-render was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextSsrPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next-server-renderer/dist'); + if (fs.existsSync(path.resolve(hippyVueNextSsrPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next-server-renderer in ${hippyVueNextSsrPath} as @hippy/vue-next-server-renderer alias`); + aliases['@hippy/vue-next-server-renderer'] = hippyVueNextSsrPath; + } else { + console.warn('* Using the @hippy/vue-next-server-renderer defined in package.json'); + } + + return aliases; + })(), + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.ios.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.ios.js new file mode 100644 index 00000000000..eec18701059 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.ios.js @@ -0,0 +1,3 @@ +const { getWebpackSsrBaseConfig } = require('./client.base'); + +module.exports = getWebpackSsrBaseConfig('ios', 'production'); diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.ios.vendor.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.ios.vendor.js new file mode 100644 index 00000000000..6783d5a86ba --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/client.ios.vendor.js @@ -0,0 +1,108 @@ +const fs = require('fs'); +const path = require('path'); +const webpack = require('webpack'); +const { VueLoaderPlugin } = require('vue-loader'); +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); + +const platform = 'ios'; + +module.exports = { + mode: 'production', + bail: true, + entry: { + vendor: [path.resolve(__dirname, '../vendor.js')], + }, + output: { + filename: `[name].${platform}.js`, + path: path.resolve(`./dist/${platform}/`), + globalObject: '(0, eval)("this")', + library: 'hippyVueBase', + }, + plugins: [ + new webpack.NamedModulesPlugin(), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production'), + __PLATFORM__: JSON.stringify(platform), + }), + new CaseSensitivePathsPlugin(), + new VueLoaderPlugin(), + new webpack.DllPlugin({ + context: path.resolve(__dirname, '../..'), + path: path.resolve(__dirname, `../../dist/${platform}/[name]-manifest.json`), + name: 'hippyVueBase', + }), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // disable vue3 dom patch flag,because hippy do not support innerHTML + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + }, + }, + }, + ], + }, + { + test: /\.(js)$/, + use: [ + { + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-env', + { + targets: { + ios: 9, + }, + }, + ], + ], + plugins: [ + ['@babel/plugin-proposal-class-properties'], + ], + }, + }, + ], + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + return aliases; + })(), + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/server.dev.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/server.dev.js new file mode 100644 index 00000000000..770a4d7b9f7 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/server.dev.js @@ -0,0 +1,207 @@ +const path = require('path'); +const fs = require('fs'); +const webpack = require('webpack'); + +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); + +const { VueLoaderPlugin } = require('vue-loader'); +const pkg = require('../../package.json'); + +let cssLoader = '@hippy/vue-css-loader'; +const hippyVueCssLoaderPath = path.resolve(__dirname, '../../../../packages/hippy-vue-css-loader/dist/css-loader.js'); +if (fs.existsSync(hippyVueCssLoaderPath)) { + console.warn(`* Using the @hippy/vue-css-loader in ${hippyVueCssLoaderPath}`); + cssLoader = hippyVueCssLoaderPath; +} else { + console.warn('* Using the @hippy/vue-css-loader defined in package.json'); +} + +let vueNext = '@hippy/vue-next'; +const hippyVueNextPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/dist/index.js'); +if (fs.existsSync(hippyVueNextPath)) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath}`); + vueNext = hippyVueNextPath; +} else { + console.warn('* Using the @hippy/vue-next defined in package.json'); +} + +let compilerSsrPkg = '@hippy/vue-next-compiler-ssr' +let compilerSsrPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next-compiler-ssr/dist/index.js'); +if (fs.existsSync(compilerSsrPath)) { + console.warn(`* Using the @hippy/vue-next-compiler-ssr in ${compilerSsrPath}`); + compilerSsrPkg = compilerSsrPath +} else { + console.warn('* Using the @hippy/vue-next-compiler-ssr defined in package.json'); +} + + +const { isNativeTag } = require(vueNext); +const compilerSsr = require(compilerSsrPkg); + +module.exports = { + mode: 'development', + bail: true, + devtool: 'source-map', + target: 'node', + watch: true, + watchOptions: { + // file changed, rebuild delay time + aggregateTimeout: 1000, + }, + entry: { + index: path.resolve(pkg.serverEntry), + }, + output: { + filename: 'index.js', + strictModuleExceptionHandling: true, + path: path.resolve('dist/server'), + }, + plugins: [ + // only generate one chunk at server side + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('development'), + HIPPY_SSR: true, + }, + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + }), + new CaseSensitivePathsPlugin(), + new VueLoaderPlugin(), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // because hippy do not support innerHTML, so we should close this feature + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + // Vue will recognize non-HTML tags as components, so for Hippy native tags, + // Vue needs to be informed to render them as custom elements + isCustomElement: tag => isNativeTag && isNativeTag(tag), + // real used ssr runtime package, render vue node at server side + ssrRuntimeModuleName: '@hippy/vue-next-server-renderer', + // do not generate html comment node + comments: false, + }, + // real used vue compiler + compiler: compilerSsr, + }, + }, + ], + }, + { + test: /\.(le|c)ss$/, + use: [cssLoader, 'less-loader'], + }, + { + test: /\.t|js$/, + use: [ + { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: '16.0', + }, + }, + ], + ], + plugins: [ + ['@babel/plugin-proposal-nullish-coalescing-operator'], + ], + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + // if you would like to use base64 for picture, uncomment limit: true + // limit: true, + limit: true, + fallback: 'file-loader', + name: '[name].[ext]', + outputPath: 'assets/', + }, + }], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + appendTsSuffixTo: [/\.vue$/], + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + // If @hippy/vue-next-server-render was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextSsrPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next-server-renderer/dist'); + if (fs.existsSync(path.resolve(hippyVueNextSsrPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next-server-renderer in ${hippyVueNextSsrPath} as @hippy/vue-next-server-renderer alias`); + aliases['@hippy/vue-next-server-renderer'] = hippyVueNextSsrPath; + } else { + console.warn('* Using the @hippy/vue-next-server-renderer defined in package.json'); + } + + return aliases; + })(), + }, + externals: { + express: 'commonjs express', // this line is just to use the express dependency in a commonjs way + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/server.entry.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/server.entry.js new file mode 100644 index 00000000000..b67c138c5ce --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack-ssr-config/server.entry.js @@ -0,0 +1,197 @@ +const path = require('path'); +const fs = require('fs'); +const webpack = require('webpack'); + +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); +const { VueLoaderPlugin } = require('vue-loader'); +const pkg = require('../../package.json'); + +let cssLoader = '@hippy/vue-css-loader'; +const hippyVueCssLoaderPath = path.resolve(__dirname, '../../../../packages/hippy-vue-css-loader/dist/css-loader.js'); +if (fs.existsSync(hippyVueCssLoaderPath)) { + console.warn(`* Using the @hippy/vue-css-loader in ${hippyVueCssLoaderPath}`); + cssLoader = hippyVueCssLoaderPath; +} else { + console.warn('* Using the @hippy/vue-css-loader defined in package.json'); +} + +let vueNext = '@hippy/vue-next'; +const hippyVueNextPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/dist/index.js'); +if (fs.existsSync(hippyVueNextPath)) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath}`); + vueNext = hippyVueNextPath; +} else { + console.warn('* Using the @hippy/vue-next defined in package.json'); +} + +let compilerSsrPkg = '@hippy/vue-next-compiler-ssr' +let compilerSsrPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next-compiler-ssr/dist/index.js'); +if (fs.existsSync(compilerSsrPath)) { + console.warn(`* Using the @hippy/vue-next-compiler-ssr in ${compilerSsrPath}`); + compilerSsrPkg = compilerSsrPath +} else { + console.warn('* Using the @hippy/vue-next-compiler-ssr defined in package.json'); +} + +const { isNativeTag } = require(vueNext); +const compilerSsr = require(compilerSsrPkg); + +module.exports = { + mode: 'production', + bail: true, + devtool: false, + target: 'node', + entry: { + index: path.resolve(pkg.serverEntry), + }, + output: { + filename: 'index.js', + strictModuleExceptionHandling: true, + path: path.resolve('dist/server'), + }, + plugins: [ + // only generate one chunk at server side + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('production'), + HIPPY_SSR: true, + }, + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + }), + new CaseSensitivePathsPlugin(), + new VueLoaderPlugin(), + ], + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + compilerOptions: { + // because hippy do not support innerHTML, so we should close this feature + hoistStatic: false, + // whitespace handler, default is 'condense', it can be set 'preserve' + whitespace: 'condense', + // Vue will recognize non-HTML tags as components, so for Hippy native tags, + // Vue needs to be informed to render them as custom elements + isCustomElement: tag => isNativeTag && isNativeTag(tag), + // real used ssr runtime package, render vue node at server side + ssrRuntimeModuleName: '@hippy/vue-next-server-renderer', + // do not generate html comment node + comments: false, + }, + // real used vue compiler + compiler: compilerSsr, + }, + }, + ], + }, + { + test: /\.(le|c)ss$/, + use: [cssLoader, 'less-loader'], + }, + { + test: /\.t|js$/, + use: [ + { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: '16.0', + }, + }, + ], + ], + plugins: [ + ['@babel/plugin-proposal-nullish-coalescing-operator'], + ], + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + // if you would like to use base64 for picture, uncomment limit: true + // limit: true, + limit: 8192, + fallback: 'file-loader', + name: '[name].[ext]', + outputPath: 'assets/', + }, + }], + }, + { + test: /\.(ts)$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + appendTsSuffixTo: [/\.vue$/], + }, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', + }, + ], + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.ts'], + alias: (() => { + const aliases = { + src: path.resolve('./src'), + }; + + // If @vue/runtime-core was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueRuntimeCorePath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/node_modules/@vue/runtime-core'); + if (fs.existsSync(path.resolve(hippyVueRuntimeCorePath, 'index.js'))) { + console.warn(`* Using the @vue/runtime-core in ${hippyVueRuntimeCorePath} as vue alias`); + aliases['@vue/runtime-core'] = hippyVueRuntimeCorePath; + } else { + console.warn('* Using the @vue/runtime-core defined in package.json'); + } + + // If @hippy/vue-next was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next/dist'); + if (fs.existsSync(path.resolve(hippyVueNextPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next in ${hippyVueNextPath} as @hippy/vue-next alias`); + aliases['@hippy/vue-next'] = hippyVueNextPath; + } else { + console.warn('* Using the @hippy/vue-next defined in package.json'); + } + + // If @hippy/vue-next-server-render was built exist in packages directory then make an alias + // Remove the section if you don't use it + const hippyVueNextSsrPath = path.resolve(__dirname, '../../../../packages/hippy-vue-next-server-renderer/dist'); + if (fs.existsSync(path.resolve(hippyVueNextSsrPath, 'index.js'))) { + console.warn(`* Using the @hippy/vue-next-server-renderer in ${hippyVueNextSsrPath} as @hippy/vue-next-server-renderer alias`); + aliases['@hippy/vue-next-server-renderer'] = hippyVueNextSsrPath; + } else { + console.warn('* Using the @hippy/vue-next-server-renderer defined in package.json'); + } + + return aliases; + })(), + }, +}; diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack.ssr.build.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack.ssr.build.js new file mode 100644 index 00000000000..fe0210af821 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack.ssr.build.js @@ -0,0 +1,109 @@ +/** + * build js script for ssr production + */ +const { arch } = require('os'); +const { exec, rm, cp } = require('shelljs'); + +let envPrefixStr = 'cross-env-os os="Windows_NT,Linux,Darwin" minVersion=17 NODE_OPTIONS=--openssl-legacy-provider'; +const isArmCpu = arch() + .toLowerCase() + .includes('arm'); +if (isArmCpu) { + envPrefixStr = ''; +} + +/** + * get executed script + * + * @param configFile - config file name + */ +function getScriptCommand(configFile) { + return `${envPrefixStr} webpack --config scripts/webpack-ssr-config/${configFile} --mode production`; +} + +/** + * execute script + * + * @param scriptStr - script content + * @param options - shelljs options + */ +function runScript(scriptStr, options = { silent: false }) { + const result = exec(scriptStr, options); + if (result.code !== 0) { + console.error(`❌ execute cmd - "${scriptStr}" error: ${result.stderr}`); + process.exit(1); + } +} + +/** + * build ssr client entry bundle + */ +function buildServerEntry() { + // build server entry + runScript(getScriptCommand('server.entry.js')); +} + +/** + * build ssr sever and client bundle + */ +function buildJsBundle() { + // build Android client bundle + runScript(getScriptCommand('client.android.js')); + // build iOS client bundle + runScript(getScriptCommand('client.ios.js')); + // 3. build client entry + runScript(getScriptCommand('client.entry.js')); +} + +/** + * build js vendor for production + */ +function buildJsVendor() { + // ios + runScript(getScriptCommand('client.ios.vendor.js')); + // android + runScript(getScriptCommand('client.android.vendor.js')); +} + +/** + * generate client entry js bundle for production + */ +function generateClientEntryForProduction() { + // copy js entry to every platform + // ios + cp('-f', './dist/index.js', './dist/ios/index.ios.js'); + // android + cp('-f', './dist/index.js', './dist/android/index.android.js'); +} + +/** + * copy generated files to native demo + */ +function copyFilesToNativeDemo() { + cp('-Rf', './dist/ios/*', '../ios-demo/res/'); // Update the ios demo project + cp('-Rf', './dist/android/*', '../android-demo/res/'); // # Update the android project +} + +/** + * build production bundle + */ +function buildProduction() { + // production, build all entry bundle, ssr server should execute by user + // first, remove dist directory + rm('-rf', './dist'); + // second, build js vendor + buildJsVendor(); + // third, build all js bundle + buildJsBundle(); + // fourth, build client entry + buildServerEntry(); + // fifth, build every platform's client entry + generateClientEntryForProduction(); + // last, copy all files to native demo + copyFilesToNativeDemo(); +} + +// build production bundle +buildProduction(); + + diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack.ssr.dev.js b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack.ssr.dev.js new file mode 100644 index 00000000000..4c225478b6f --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/scripts/webpack.ssr.dev.js @@ -0,0 +1,39 @@ +/** + * build script for ssr + */ + +const webpack = require('webpack'); +const { exec } = require('shelljs'); +const serverConfig = require('./webpack-ssr-config/server.dev'); + +const compiler = webpack(serverConfig); +let childProcess = null; + +/** + * execute script + * + * @param scriptStr - script content + * @param options - shelljs options + */ +function runScript(scriptStr, options) { + if (childProcess) { + // kill process first + childProcess.kill(); + } + childProcess = exec(scriptStr, options, (code, stdout, stderr) => { + if (code) { + console.error(`❌ execute cmd - "${scriptStr}" error: ${stderr}`); + process.exit(1); + } + }); +} + +compiler.hooks.done.tap('DonePlugin', () => { + // restart node process after build success + setTimeout(() => { + runScript('node ./dist/server/index.js', { async: true, silent: false }); + }, 0); +}); + +// watch server entry change +compiler.watch({}, () => {}); diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/server.ts b/driver/js/examples/hippy-vue-next-ssr-demo/server.ts new file mode 100644 index 00000000000..d92fd6ae8ca --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/server.ts @@ -0,0 +1,83 @@ +import express from 'express'; +import { render, HIPPY_GLOBAL_STYLE_NAME } from 'src/main-server'; + +interface MinifiedStyleDeclaration { + [key: number]: number | string; +} + +/** + * minify css content + */ +function minifyStyleContent(rawStyleContent): NeedToTyped[] | MinifiedStyleDeclaration[] { + if (rawStyleContent?.length && Array.isArray(rawStyleContent)) { + const minifiedStyle: MinifiedStyleDeclaration[] = []; + rawStyleContent.forEach((styleContent) => { + // minified style is array, 0 index is selectors, 1 index is declaration, no hash + minifiedStyle.push([ + styleContent.selectors, + // minify declarations + styleContent.declarations.map(declaration => [declaration.property, declaration.value]), + ]); + }); + return minifiedStyle; + } + + return rawStyleContent; +} + +/** + * get ssr style content + * + * @param globalStyleName - hippy global style name + */ +function getSsrStyleContent(globalStyleName): NeedToTyped[] { + if (global.ssrStyleContentList) { + return global.ssrStyleContentList; + } + // cache global style sheet, then non first request could return directly, unnecessary to + // serialize again + global.ssrStyleContentList = JSON.stringify(minifyStyleContent(global[globalStyleName])); + + return global.ssrStyleContentList; +} + +// server listen port +const serverPort = 8080; +// init http server +const server = express(); +// use json middleware +server.use(express.json()); + +// listen request +server.all('/getSsrFirstScreenData', (req, rsp) => { + // get hippy ssr node list and other const + render('/', { + appName: 'Demo', + iPhone: { + statusBar: { disabled: true }, + }, + }, req.body).then(({ + list, + store, + uniqueId, + }) => { + // send response + rsp.json({ + code: 0, + data: list, + store: store.state.value, + uniqueId, + styleContent: getSsrStyleContent(HIPPY_GLOBAL_STYLE_NAME), + }); + }) + .catch((error) => { + rsp.json({ + code: -1, + message: `get ssr data error: ${JSON.stringify(error)}`, + }); + }); +}); + +// start server +server.listen(serverPort); +console.log(`Server listen on:${serverPort}`); diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/app.vue b/driver/js/examples/hippy-vue-next-ssr-demo/src/app.vue new file mode 100644 index 00000000000..65058aea3ef --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/src/app.vue @@ -0,0 +1,238 @@ + + + diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/defaultSource.jpg b/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/defaultSource.jpg new file mode 100644 index 00000000000..833417ea2fb Binary files /dev/null and b/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/defaultSource.jpg differ diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/hippyLogoWhite.png b/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/hippyLogoWhite.png new file mode 100644 index 00000000000..20e428e2fa9 Binary files /dev/null and b/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/hippyLogoWhite.png differ diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/logo.png b/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/logo.png new file mode 100644 index 00000000000..f3d2503fc2a Binary files /dev/null and b/driver/js/examples/hippy-vue-next-ssr-demo/src/assets/logo.png differ diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/back-icon.png b/driver/js/examples/hippy-vue-next-ssr-demo/src/back-icon.png new file mode 100644 index 00000000000..fe1fbf1cd10 Binary files /dev/null and b/driver/js/examples/hippy-vue-next-ssr-demo/src/back-icon.png differ diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-button.vue b/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-button.vue new file mode 100644 index 00000000000..00c06a74eb8 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-button.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-div.vue b/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-div.vue new file mode 100644 index 00000000000..f0a3aa4ea1e --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-div.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-dynamicimport.vue b/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-dynamicimport.vue new file mode 100644 index 00000000000..1b231d3702e --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-dynamicimport.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-iframe.vue b/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-iframe.vue new file mode 100644 index 00000000000..36a57aeeae0 --- /dev/null +++ b/driver/js/examples/hippy-vue-next-ssr-demo/src/components/demo/demo-iframe.vue @@ -0,0 +1,126 @@ +