[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f_1vBIMMEmeLEh2Zx7uyBXBB3GBddJFiUHtcqSoJRRFA":3},{"code":4,"message":5,"data":6},200,"成功",{"pageSize":7,"pageIndex":8,"list":9,"total":297},20,1,[10,33,49,64,79,92,105,120,135,147,160,173,186,201,214,228,243,257,270,284],{"id":11,"createdAt":12,"title":13,"content":14,"summary":15,"image":16,"uid":17,"user":18,"categoryId":24,"category":25,"subCategoryId":27,"subCategory":28,"comments":30,"status":8,"reason":22,"notice":22,"visitCount":31,"commentCount":8,"keywords":32},87,"2024-02-23T08:00:20.265Z","ruby - Your Ruby version is 2.6.3, but your Gemfile specified >= 2.6.10，或者 Error running '__rvm_make install'","当运行iOS工程时，遇到gemfile文件，对ruby的版本是有要求的。\n\n或者你因为其他原因导致需要安装或者升级ruby版本。\n\n有时候会遇到以下报错：\n\n报错：\n1. ruby版本过低\n> Your Ruby version is 2.6.3, but your Gemfile specified >= 2.6.10\n\n![ruby version fail](https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-02-23%20151708673908208.png)\n\n2. ruby安装失败\n> Error running '__rvm_make install',\nplease read /Users/hanweixing/.rvm/log/1708659865_ruby-2.7.5/install.log\nThere has been an error while running make install. Halting the installation.\n\n![ruby install fail](https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-02-23%20151708673957020.png)\n\n当遇到这样的错误时，你可以按照以下步骤进行解决：\n1. 查看本地ruby版本情况：\n```\nruby -v\n```\n\n2. 清理本地rvm下载安装的缓存\n```\nrvm cleanup all\n```\n\n3. 获取rvm的稳定版本\n```\nrvm get stable --verbose\n```\n\n4. 如果你需要重新安装rvm\n```\ncurl -sSL https://get.rvm.io | bash -s stable --ruby\n```\n\n5. 尝试安装ruby\n```\nrvm install ruby --verbose\n```\n\n6. 如果安装失败，可以使用以下命令获取可用的ruby版本\n```\nrvm list known\n```\n\n7. 指定ruby版本安装\n```\nrvm install ruby-3.2.2 --verbose\n```\n或者\n```\nrvm install 3.2.2 -C --with-openssl-dir=/opt/homebrew/opt/openssl@3.1 --verbose\n```\n这2个命令基本可以让安装ruby成功\n![ruby install success](https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-02-23%20151708674829430.png)\n\n8. 通过`ruby -v`命令查看ruby对应版本是否是刚安装的那个版本。\n\n9. 查看本地安装的ruby版本\n```\nrvm list\n```\n![](https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-02-23%20151708675069734.png)\n\n10. 如果不是，可以使用rvm的use命令来切换\n![ruby切换](https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-02-23%20151708674940438.png)\n\n```\nrvm use 3.2.2\n```","这篇文章针对ruby的安装或者报错给出对应的解决方案，当运行iOS工程时，遇到gemfile文件，对ruby的版本是有要求的。","https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-02-23%20151708673908208.png",499668042977349,{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},"13121171998","全栈老韩","https://image.xinwei.ltd/images/IMG_5430.JPG","","2024-01-01T16:14:30.305Z",2,{"id":24,"name":26},"IT技术",18,{"id":27,"name":29,"parentId":24},"Linux",[],6530,"ruby,ruby install,ruby error,ruby开发,ruby issue,ruby编码",{"id":34,"createdAt":35,"title":36,"content":37,"summary":38,"image":39,"uid":17,"user":40,"categoryId":24,"category":41,"subCategoryId":42,"subCategory":43,"comments":45,"status":8,"reason":22,"notice":22,"visitCount":46,"commentCount":47,"keywords":48},145,"2025-01-24T04:52:55.324Z","git Authentication failed - remote: Https克隆账号或密码错误，如何查看克隆账号或密码","## 问题\n> Pushing to https://codeup.aliyun.com/66f152d5c/h5/xxx.git\nremote: Https克隆账号或密码错误，如何查看克隆账号或密码：https://help.aliyun.com/document_detail/158880.html\nfatal: Authentication failed for 'https://codeup.aliyun.com/66f152d5c/h5/xxx.git/'\n\n问题截图：\n![git error截图](https://image.xinwei.ltd/31737693420722.png)\n\n## 简要描述一下背景\n本地新开发了一个web项目，当开发完之后，需要上传到git仓库，于是我选择了阿里云的codeup平台，在codeup上新建了一个仓库目录，然后通过`git init`、`git remote add`等操作将代码推送到codeup上，但是出现了`git push`的报错，如上面的问题和截图所示。\n\n## 问题所在\n即是本地的`git config`中username和password需要和阿里的codeup上的https进行验证，但是本地的username和password在验证的时候输入不正确，所以出现如此报错。\n\n## 解决问题\n### 1. 确认username和password\n去阿里的codeup平台上的【个人设置】中，找到【HTTPS密码】设置的地方\n![codeup location](https://image.xinwei.ltd/image1737693779286.png)\n\n确保你是知道克隆账号username和密码password的，如果已经忘记了，可以点击编辑图标修改对应的username和password。\n\n修改的过程中，会需要提供邮箱收到的验证码。\n![codeup_修改密码](https://image.xinwei.ltd/image1737694118357.png)\n\n\n所以需要你确保你的邮箱已经在codeup上验证过了。\n![codeup email](https://image.xinwei.ltd/image1737693948700.png)\n\n\n### 2. 执行git push\n已经知道了https的username和password后，那么操作git时遇到验证输入你就可以解决了。\n![git push username](https://image.xinwei.ltd/21737694191169.png)\n如图所示，我在vs code的终端中对项目git push，在顶部出现了username的输入框，输入username后，紧接着弹出password输入框，输入password，那么验证就通过了。\n![git push success](https://image.xinwei.ltd/image1737694310587.png)\n\n以上就是这么多了。\n\n","简单记录一个git操作失败的问题吧。喜欢记录，喜欢分享，希望对需要的同学有所帮助。","https://image.xinwei.ltd/31737693420722.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},31,{"id":42,"name":44,"parentId":24},"git&svn",[],3712,0,"git,git Authentication,git Authentication failed,git https,git https error",{"id":50,"createdAt":51,"title":52,"content":53,"summary":54,"image":55,"uid":17,"user":56,"categoryId":24,"category":57,"subCategoryId":58,"subCategory":59,"comments":61,"status":8,"reason":22,"notice":22,"visitCount":62,"commentCount":8,"keywords":63},123,"2024-08-18T18:08:17.125Z","web网页集成Apple Sign in苹果授权登录","## 一、概要\n现在越来越多的应用都集成了第三方授权登录了，而且由于苹果的iOS app上架审核规则，即如果有第三方授权登录，那么必须接入苹果Apple授权登录。那么针对苹果账号，网页端的苹果授权登录自然而然也变得很重要。\n\n## 二、Apple Sign In官方文档\n[https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/)\n\n## 三、步骤\n### 3.1 web接入Apple Sign In所需的资料\n- 1. 一个Apple 开发者账号\n- 2. 开发者账号下配置一个App Id\n- 3. 开发者账号下配置一个Service ID\n- 4. 开发者账号下给Service ID的sign in Apple编辑中配置domains和website\n> 针对一个Service ID，公司账号可以注册上限100个的网站，个人账号上限10个\n- 5. 开发者账号下给新建一个key（可以下载导出p8格式文件，服务端会需要到）\n- 6. 如果是公司账号要给服务端team id（就是右上角账号名字中的一组字符串）.\n\n针对上述所需的资料，创建App Id的流程很简单，基本和Service ID类似，所以这里不赘述。\n\n### 3.2 创建Service ID\n在Identifier下，选择Service Ids类型，然后点击Identifiers旁边的“+”号\n![创建Service ID - 1](https://image.xinwei.ltd/11724001528643.jpg)\n\n输入service id的描述，identifier（就是website网页上配置的**client ID**），要记得勾选 `Sign in with Apple`.\n![创建Service ID - 2](https://image.xinwei.ltd/image1724002021317.png)\n\n配置`Sign in with Apple`，点击`Configure`\n![点击Configure](https://image.xinwei.ltd/21724002446590.jpg)\n- 选择一个App ID\n- 配置网站的域名（我这里是测试和正式环境都配置了，localhost配置不了）\n- 配置Return URLs（这里是需要提供一个服务端的接口，因为Apple会给这里面的url发送post请求）\n- 注意点：domains和return urls都是用英文逗号`,`隔开的。domains中不要加http或者https，return urls中是要加http和https。\n![配置域名、网址](https://image.xinwei.ltd/31724002623205.jpg)\n\n点击Next，点击Done就ok了。\n![确认website urls](https://image.xinwei.ltd/41724002974138.jpg)\n\n### 3.3 新建一个key\n![新建一个key](https://image.xinwei.ltd/image1724003234060.png)\n\n可以下载p8文件出来，给服务端\n![download key](https://image.xinwei.ltd/71724003479138.jpg)\n\n\n### 3.4 网页的html中配置\n如果要使用service id中的return urls，那就需要服务端提供post接口。如果不用服务端，只用前端介入的话，可以使用pop up的弹出窗口模式。\n> 如果使用pop up方式，那么在Facebook等第三方app中显示网页时，授权弹窗会被阻止，导致流程中断，切记\n\n在html中配置script脚本\n```html\n\u003Cscript type=\"text/javascript\" src=\"https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js\">\u003C/script>\n```\n\n使用js或者ts脚本\n```js\nAppleID.auth.init({\n      clientId : '[CLIENT_ID]',\n      scope : '[SCOPES]', // 比如[ 'name', 'email' ]\n      redirectURI : '[REDIRECT_URI]',\n      state : '[STATE]',\n      nonce : '[NONCE]',\n      usePopup : true // 这里如果是true，那么就是使用弹窗授权, return urls就用不到\n});\n\n// 直接使用promise的方式，当然也有使用document监听的\nAppleID.auth.signIn()\n.then(res => {\n  \n})\n.catch(err => {\n  \n});\n\n// 可选，document监听\n// Listen for authorization success.\ndocument.addEventListener('AppleIDSignInOnSuccess', (event) => {\n    // Handle successful response.\n    console.log(event.detail.data);\n});\n\n\n// Listen for authorization failures.\ndocument.addEventListener('AppleIDSignInOnFailure', (event) => {\n     // Handle error.\n     console.log(event.detail.error);\n});\n```\n\n使用signin方法得到的数据就是包括code、id_token参数的res，将这2个参数传给自己公司的接口，让服务端去从Apple端获取用户信息。\n\n\n## 四、服务端所需的资料\n- Apple开发者账号的team id\n- Apple开发者账号的service id\n- Apple开发者账号的key文件（p8文件）\n\n以上大概就是这些了，希望对大家有帮助。\n","现在越来越多的应用都集成了第三方授权登录了，而且由于苹果的iOS app上架审核规则，即如果有第三方授权登录，那么必须接入苹果Apple Sign in授权登录。那么针对苹果账号，网页端的苹果授权登录自然而然也变得很重要。","https://image.xinwei.ltd/11724001528643.jpg",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},32,{"id":58,"name":60,"parentId":24},"前端",[],3590,"web apple sign in,apple sign in,apple web sign,apple sign in web,apple登录,apple登录网页集成,apple集成",{"id":65,"createdAt":66,"title":67,"content":68,"summary":69,"image":70,"uid":17,"user":71,"categoryId":24,"category":72,"subCategoryId":73,"subCategory":74,"comments":76,"status":8,"reason":22,"notice":22,"visitCount":77,"commentCount":47,"keywords":78},148,"2025-02-13T04:40:30.945Z","flutter Notification Service Extension errors(Error output from CocoaPods,unknown ISA PBXFileSystemSynchronized)...","## 一、背景\n在flutter工程中为iOS添加Notification Service Extension时，遇到了一系列的编译和运行问题，在此做一个记录。\n\n本地环境`environment`:\n- Xcode: 16.2\n- Simulator: iOS 17.5\n- Flutter 3.27.0-0.1.pre • channel beta • https://github.com/flutter/flutter.git\nFramework • revision 2e2c358c9b (4 months ago) • 2024-10-22 11:02:13 -0400\nEngine • revision af0f0d559c\nTools • Dart 3.6.0 (build 3.6.0-334.3.beta) • DevTools 2.40.1\n- ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-darwin23]\n- CocoaPods : 1.15.2\n\n## 二、问题\n### 问题1: \n> Error output from CocoaPods:\n↳\n    Searching for inspections failed: undefined method `map' for nil\nError running pod install\nError launching application on iPhone SE (3rd generation).\n\n![issue-1 pod error](https://image.xinwei.ltd/image1739418777754.png)\n\n### 问题2:\n> Launching lib/main.dart on iPhone SE (3rd generation) in debug mode...\nRunning pod install...\nRunning Xcode build...\nXcode build done.                                           304.5s\nFailed to build iOS app\nError (Xcode): Cycle inside Runner; building could produce unreliable results.\nCycle details:\n→ That command depends on command in Target 'Runner': script phase “[CP] Copy Pods Resources”\n○ That command depends on command in Target 'Runner': script phase “[CP] Embed Pods Frameworks”\n○ That command depends on command in Target 'Runner': script phase “Thin Binary”\n○ Target 'Runner' has process command with output '/Users/edy/Desktop/xxxproject/build/ios/Debug-iphonesimulator/Runner.app/Info.plist'\n○ Target 'Runner' has copy command from '/Users/edy/Desktop/xxxproject/build/ios/Debug-iphonesimulator/xxxPushExtension.appex' to '/Users/edy/Desktop/xxxproject/build/ios/Debug-iphonesimulator/Runner.app/PlugIns/xxxPushExtension.appex'\n\n![issue-2 cycle inside runner](https://image.xinwei.ltd/image1739419782879.png)\n\n\n### 问题3（可能会遇到这样的问题，我也是在这个过程中碰到过）:\n> RuntimeError - `PBXGroup` attempted to initialize an object with unknown ISA `PBXFileSystemSynchronizedRootGroup`\n\n这个问题需要删除掉Notification Service Extension，然后重新新建.\n\n\n\n## 三、步骤：\n### 3.1 给iOS工程添加Notification Service Extension\n添加Notification Service Extension\n![add Notification Service Extension](https://image.xinwei.ltd/image1739418168922.png)\n\n![choose Notification Service Extension](https://image.xinwei.ltd/image1739418252399.png)\n\n添加完之后的状态folder结构\n![](https://image.xinwei.ltd/image1739418361790.png)\n\n### 3.2 编译flutter运行，出现报错：\n```run flutter error\nLaunching lib/main.dart on iPhone SE (3rd generation) in debug mode...\nUpdating project for Xcode compatibility.\nUpgrading project.pbxproj\nUpgrading Runner.xcscheme\nRunning pod install...\nCocoaPods' output:\n↳\n      Preparing\n\n    Analyzing dependencies\n\n    Inspecting targets to integrate\n      CDN: trunk Relative path: CocoaPods-version.yml exists! Returning local because checking is only performed in repo update\n\n    ――― MARKDOWN TEMPLATE ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n\n    ### Command\n\n    ```\n    /Users/edy/.rvm/rubies/ruby-3.3.5/bin/pod install --verbose\n    ```\n\n    ### Report\n\n    * What did you do?\n\n    * What did you expect to happen?\n\n    * What happened instead?\n\n\n    ### Stack\n\n    ```\n       CocoaPods : 1.15.2\n            Ruby : ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-darwin23]\n        RubyGems : 3.5.16\n            Host : macOS 14.6.1 (23G93)\n           Xcode : 16.2 (16C5032a)\n             Git : git version 2.39.5 (Apple Git-154)\n    Ruby lib dir : /Users/edy/.rvm/rubies/ruby-3.3.5/lib\n    Repositories : cocoapods - git - https://github.com/CocoaPods/Specs.git @ 3574d4d220177427a4fa77c221b01e55996aa84d\n\n                   trunk - CDN - https://cdn.cocoapods.org/\n    ```\n\n    ### Plugins\n\n    ```\n    cocoapods-deintegrate : 1.0.5\n    cocoapods-plugins     : 1.0.0\n    cocoapods-search      : 1.0.1\n    cocoapods-trunk       : 1.6.0\n    cocoapods-try         : 1.2.0\n    ```\n\n    ### Podfile\n\n    ```ruby\n    # Uncomment this line to define a global platform for your project\n    platform :ios, '13.0'\n\n    # CocoaPods analytics sends network stats synchronously affecting flutter build latency.\n    ENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\n    project 'Runner', {\n      'Debug' => :debug,\n      'Profile' => :release,\n      'Release' => :release,\n    }\n\n    def flutter_root\n      generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n      unless File.exist?(generated_xcode_build_settings_path)\n        raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n      end\n\n      File.foreach(generated_xcode_build_settings_path) do |line|\n        matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n        return matches[1].strip if matches\n      end\n      raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\n    end\n\n    require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\n    flutter_ios_podfile_setup\n\n    target 'Runner' do\n      use_frameworks!\n      use_modular_headers!\n\n      pod 'KeychainAccess'\n      pod 'FBAudienceNetwork','= 6.15.0'\n\n      flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\n      target 'RunnerTests' do\n        inherit! :search_paths\n      end\n    end\n\n    post_install do |installer|\n      installer.pods_project.targets.each do |target|\n        flutter_additional_ios_build_settings(target)\n        target.build_configurations.each do |config|\n          config.build_settings['SWIFT_VERSION'] = '5.0'  # required by simple_permission\n          config.build_settings['ENABLE_BITCODE'] = 'NO'\n        end\n      end\n    end\n    ```\n\n    ### Error\n\n    ```\n    RuntimeError - `PBXGroup` attempted to initialize an object with unknown ISA `PBXFileSystemSynchronizedRootGroup` from attributes: `{\"isa\"=>\"PBXFileSystemSynchronizedRootGroup\", \"exceptions\"=>[\"0453440D2D5DA235003E9BF2\"], \"explicitFileTypes\"=>{}, \"explicitFolders\"=>[], \"path\"=>\"xxxPushExtension\", \"sourceTree\"=>\"\u003Cgroup>\"}`\n    If this ISA was generated by Xcode please file an issue: https://github.com/CocoaPods/Xcodeproj/issues/new\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:359:in `rescue in object_with_uuid'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:349:in `object_with_uuid'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:300:in `block (2 levels) in configure_with_plist'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:299:in `each'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:299:in `block in configure_with_plist'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:296:in `each'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:296:in `configure_with_plist'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project.rb:272:in `new_from_plist'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:350:in `object_with_uuid'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:290:in `block in configure_with_plist'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:287:in `each'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project/object.rb:287:in `configure_with_plist'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project.rb:272:in `new_from_plist'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project.rb:213:in `initialize_from_file'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/xcodeproj-1.25.1/lib/xcodeproj/project.rb:113:in `open'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1194:in `block (2 levels) in inspect_targets_to_integrate'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1193:in `each'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1193:in `block in inspect_targets_to_integrate'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/user_interface.rb:64:in `section'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1188:in `inspect_targets_to_integrate'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:107:in `analyze'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:422:in `analyze'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:244:in `block in resolve_dependencies'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/user_interface.rb:64:in `section'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:243:in `resolve_dependencies'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:162:in `install!'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/command/install.rb:52:in `run'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/claide-1.1.0/lib/claide/command.rb:334:in `run'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/lib/cocoapods/command.rb:52:in `run'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/lib/ruby/gems/3.3.0/gems/cocoapods-1.15.2/bin/pod:55:in `\u003Ctop (required)>'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/bin/pod:25:in `load'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/bin/pod:25:in `\u003Cmain>'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/bin/ruby_executable_hooks:22:in `eval'\n    /Users/edy/.rvm/rubies/ruby-3.3.5/bin/ruby_executable_hooks:22:in `\u003Cmain>'\n    ```\n\n    ――― TEMPLATE END ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n\n    [!] Oh no, an error occurred.\n\n    Search for existing GitHub issues similar to yours:\n    https://github.com/CocoaPods/CocoaPods/search?q=%60PBXGroup%60+attempted+to+initialize+an+object+with+unknown+ISA+%60PBXFileSystemSynchronizedRootGroup%60+from+attributes%3A+%60%7B%22isa%22%3D%3E%22PBXFileSystemSynchronizedRootGroup%22%2C+%22exceptions%22%3D%3E%5B%220453440D2D5DA235003E9BF2%22%5D%2C+%22explicitFileTypes%22%3D%3E%7B%7D%2C+%22explicitFolders%22%3D%3E%5B%5D%2C+%22path%22%3D%3E%22xxxPushExtension%22%2C+%22sourceTree%22%3D%3E%22%3Cgroup%3E%22%7D%60%0AIf+this+ISA+was+generated+by+Xcode+please+file+an+issue%3A+https%3A%2F%2Fgithub.com%2FCocoaPods%2FXcodeproj%2Fissues%2Fnew&type=Issues\n\n    If none exists, create a ticket, with the template displayed above, on:\n    https://github.com/CocoaPods/CocoaPods/issues/new\n\n    Be sure to first read the contributing guide for details on how to properly submit a ticket:\n    https://github.com/CocoaPods/CocoaPods/blob/master/CONTRIBUTING.md\n\n    Don't forget to anonymize any private data!\n\n    Looking for related issues on cocoapods/cocoapods...\n\nError output from CocoaPods:\n↳\n    Searching for inspections failed: undefined method `map' for nil\n\nError running pod install\nError launching application on iPhone SE (3rd generation).\n```\n\n### 3.3 解决pod install失败的问题\n从问题3.1的folder结构中可以看出，文件夹的icon标识不是灰色gray的，而是淡蓝色blue的icon，这个应该是Xcode的bug，需要将新建的Extension文件夹改成group。\n\n右键Extension文件夹，选择`Convert to Group`\n![convert to Group](https://image.xinwei.ltd/image1739419003315.png)\n\n这个解决方法，感谢链接：[https://github.com/CocoaPods/CocoaPods/issues/12456](https://github.com/CocoaPods/CocoaPods/issues/12456)\n![github fix](https://image.xinwei.ltd/image1739419140817.png)\n\n### 3.4 接着运行代码，遇到build的报错\n![issue-2 cycle inside runner](https://image.xinwei.ltd/image1739419782879.png)\n这个问题有点奇怪，我也查了好久，才从网上找到一个解决办法：\n- 选择主target，进入到Build Phrases的设置下\n![build phrases setting](https://image.xinwei.ltd/image1739420069053.png)\n- 将Embed Foundation Extensions这项，移动到CopyBundle Resources这项正下面.\n![move position result](https://image.xinwei.ltd/image1739420184527.png)\n- 然后运行，没有报错。\n![run success](https://image.xinwei.ltd/image1739420382663.png)\n\n参考链接：[https://stackoverflow.com/questions/77138968/handling-cycle-inside-runner-building-could-produce-unreliable-results-after-up](https://stackoverflow.com/questions/77138968/handling-cycle-inside-runner-building-could-produce-unreliable-results-after-up)\n\n## 四、添加即时通知 - time sensitive notifications\n### 4.1 设置push notification capability\n按照下面截图中的步骤，添加push notifications\n![push notifications](https://image.xinwei.ltd/image1739420598289.png)\n\n会生成entitlements文件和capability\n![push notifications result](https://image.xinwei.ltd/image1739420695587.png)\n\n### 4.2 配置ios deployment target\n因为Xcode默认会配置ios deployment target，所以我们要检查适配版本，修改为我们想要的版本.\n截图是默认的18.2，但现阶段不可能配置到这个版本。\n![default ios deployment target](https://image.xinwei.ltd/image1739420828256.png)\n修改为15.0\n![set to ios15.0](https://image.xinwei.ltd/image1739420981066.png)\n\n### 4.3 配置NotificationService方法\n配置如下：\n```NotificationService.swift\n//\n//  NotificationService.swift\n//  xxxPushExtension\n//\n//  Created by Hamry on 2/13/25.\n//\nimport UserNotifications\nimport UIKit\nimport Intents\n\nclass NotificationService: UNNotificationServiceExtension {\n\n    var contentHandler: ((UNNotificationContent) -> Void)?\n    var bestAttemptContent: UNMutableNotificationContent?\n    \n    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {\n            self.contentHandler = contentHandler\n            bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)\n            \n            if let bestAttemptContent = bestAttemptContent {\n                // Modify the notification content here...\n                // 获取通信消息\n                if let options = bestAttemptContent.userInfo[\"fcm_options\"] as? [String: Any], let senderImageURLString = options[\"image\"] as? String\n                {\n                    // Here you need to download the image data from the URL.\n                    self.getMediaAttachment(for: senderImageURLString) { image in\n                        guard let groupIcon = image else {\n                            contentHandler(bestAttemptContent)\n                            return\n                        }\n                        let avatar = INImage(imageData: groupIcon.pngData()!)\n                        // 消息发送方\n                        let messageSender = INPerson(\n                            personHandle: INPersonHandle(value: nil, type: .unknown),\n                            nameComponents: try? PersonNameComponents(bestAttemptContent.title),\n                            displayName: bestAttemptContent.title,\n                            image: avatar,\n                            contactIdentifier: nil,\n                            customIdentifier: nil,\n                            isMe: false,\n                            suggestionType: .none\n                        )\n                        \n                        // 消息接收方\n                        let mePerson = INPerson(\n                            personHandle: INPersonHandle(value: \"\", type: .unknown),\n                            nameComponents: nil,\n                            displayName: nil,\n                            image: nil,\n                            contactIdentifier: nil,\n                            customIdentifier: nil,\n                            isMe: true,\n                            suggestionType: .none\n                        )\n                        \n                        let intent = INSendMessageIntent(recipients: [mePerson],\n                                                         outgoingMessageType: .outgoingMessageText,\n                                                         content: bestAttemptContent.body,\n                                                         speakableGroupName: INSpeakableString(spokenPhrase: bestAttemptContent.title),\n                                                         conversationIdentifier: nil,\n                                                         serviceName: nil,\n                                                         sender: messageSender,\n                                                         attachments: nil)\n//                        sender\n                        intent.setImage(avatar, forParameterNamed: \\.speakableGroupName)\n\n                        let interaction = INInteraction(intent: intent, response: nil)\n                        interaction.direction = .incoming\n                        interaction.donate(completion: nil)\n                        do {\n                            let messageContent = try request.content.updating(from: intent)\n                            contentHandler(messageContent)\n                        } catch {\n                            print(error.localizedDescription)\n                            contentHandler(bestAttemptContent)\n                        }\n                    }\n                }else {\n                    contentHandler(bestAttemptContent)\n                }\n            }\n        }\n    \n    override func serviceExtensionTimeWillExpire() {\n        // Called just before the extension will be terminated by the system.\n        // Use this as an opportunity to deliver your \"best attempt\" at modified content, otherwise the original push payload will be used.\n        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {\n            contentHandler(bestAttemptContent)\n        }\n    }\n\n}\n\nextension NotificationService {\n\n    //  通过本地 url 地址 获取图片资源\n    private func getMediaAttachment(for urlString: String, completion: @escaping (UIImage?) -> Void) {\n        // 1\n        guard let url = URL(string: urlString) else {\n            completion(nil)\n            return\n        }\n        \n        // 2 通过远程图片URL下载图片\n        downloadImage(forURL: url) { result in\n            // 3\n            guard let image = try? result.get() else {\n                completion(nil)\n                return\n            }\n            \n            // 4\n            completion(image)\n        }\n    }\n    \n    // 通过远程图片url地址 下载图片文件\n    public enum DownloadError: Error {\n        case emptyData\n        case invalidImage\n    }\n\n    private func downloadImage(forURL url: URL, completion: @escaping (Result\u003CUIImage, Error>) -> Void) {\n        let task = URLSession.shared.dataTask(with: url) { data, response, error in\n            if let error = error {\n                completion(.failure(error))\n                return\n            }\n            \n            guard let data = data else {\n                completion(.failure(DownloadError.emptyData))\n                return\n            }\n            \n            guard let image = UIImage(data: data) else {\n                completion(.failure(DownloadError.invalidImage))\n                return\n            }\n            \n            completion(.success(image))\n        }\n        \n        task.resume()\n    }\n}\n\n```\n\n### 4.4 运行，没有问题\n注意`NotificationService.swift`中的`INPerson`等是iOS 15.0才开始使用的特性，所以需要4.2种的ios deployment target配置。\n\n## 五、总结\n其实也反反复复遇到过其他问题，但是解决了，这里篇幅有限，暂时记录这些，希望对有的同学有帮助.\n\n","记录一下在flutter工程中添加iOS的Notification Service Extension遇到的编译以及运行问题，希望对大家有所帮助。","https://image.xinwei.ltd/image1739418777754.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},11,{"id":73,"name":75,"parentId":24},"Flutter",[],3255,"Notification Service Extension,flutter Notification Service Extension,unknown ISA PBXFileSystemSynchronizedRootGroup,Error output from CocoaPods",{"id":80,"createdAt":81,"title":82,"content":83,"summary":84,"image":85,"uid":17,"user":86,"categoryId":24,"category":87,"subCategoryId":58,"subCategory":88,"comments":89,"status":8,"reason":22,"notice":22,"visitCount":90,"commentCount":47,"keywords":91},118,"2024-07-20T11:14:14.489Z","web开发-从facebook的in-app browser浏览器中网页，唤起Safari 或者 谷歌chrome浏览器","## 一、概要\n安卓Android系统是可以实现从Facebook中的网页，唤起默认的浏览器。\n但是iOS最多能实现打开Safari，但是不能指定具体的网址。\n\n## 二、在安装了facebook的iPhone中，通过网页唤起Safari\n### 2.1 【不成功】：直接使用https 或者 http链接\n```js\nwindow.location.href = 'https://www.baidu.com'\n```\n\n没有Facebook的“跳转外部浏览器”的弹窗出现，依然还是在Facebook的browser中刷新\n\n### 2.2 【不成功】：通过ftp协议使用中转用的index.html\nfacebook中打开的网页\n```js\nwindow.location.href = `ftp://43.xxx.xxx.xxx/index.html`\n```\n中转网页中\n```js\nwindow.open(”https://www.baidu.com”, “_self”);\n```\nSafari已经不支持ftp协议。\n![safari的ftp不支持.png](https://image.xinwei.ltd/11721472897038.png)\n\n能弹出Facebook的“跳转外部浏览器”的弹窗，点“确定”后可以唤起Safari，但是Safari中的中转index.html不能解析，Safari的白色提示页面提示“ftp url is blocked”\n\n### 2.3 【半成功】：通过x-web-search://协议\n```js\nconst currentLink = location.href\nconst link = currentLink.replace('https://', '').replace('http://', '').replace('www.', '')\nwindow.location.href = `x-web-search://${link}`\n```\n能弹出Facebook的“跳转外部浏览器”的弹窗，点“确定”后可以唤起Safari，但是进入的是Safari的默认搜素引擎的搜索界面，搜索输入框中是link的参数部分\n\n如果使用以下的方式，那么只会出现一个网址是空的Safari界面\n```js\nwindow.location.href = `x-web-search://`\n```\n## 三、在iOS上唤起iOS版谷歌浏览器\n```js\nwindow.location = `googlechrome://${link}`// ios to chrome\n```\n\n## 四、在安装了Facebook的Android手机中唤起chrome\n```js\nconst currentLink = location.href\nconst link = currentLink.replace('https://', '').replace('http://', '').replace('www.', '')\n\nif (ua.isAndroid()) {\n    window.location.href = `intent://${link}#Intent;scheme=https;end`// android\n}\n```\n\n或者使用：\n```js\n\u003Cscript>\n    function isFacebookApp() {\n        var ua = navigator.userAgent || navigator.vendor || window.opera;\n        return (ua.indexOf(\"FBAV\") > -1) || (ua.indexOf(\"FBAN\") > -1);\n    }\nif (isFacebookApp()) {\n    var currentLink = location.href;\n    if (currentLink.indexOf('https') > -1) {\n        var currentLink = currentLink.replace('https://', '');\n        currentLink = currentLink.replace('www.', '');\n        var chromeLink = \"intent://\" + currentLink + \"#Intent;scheme=https;package=com.android.chrome;end\";\n        window.location.href = chromeLink;\n    }\n    if (currentLink.indexOf('http') > -1) {\n        var currentLink = currentLink.replace('http://', '');\n        currentLink = currentLink.replace('www.', '');\n        var chromeLink = \"intent://\" + currentLink + \"#Intent;scheme=http;package=com.android.chrome;end\";\n        window.location.href = chromeLink;\n    }\n} \n\u003C/script> \n```\n\n## 五、 以下是一些测试过跳转的不成功代码逻辑\n```js\n// tryOpenDefault(() => {\n        //     window.open(url, '_blank');\n        // }, 1000)\n        \n        // tryOpenDefault(() => {\n        //     window.location.href = url;\n        // }, 2000)\n        \n        // tryOpenDefault(() => {\n        //     window.open(url, '_system');\n        // }, 3000)\n        \n        // tryOpenDefault(() => {\n        //     window.location.href = 'intent://' + url + '#Intent;' + 'scheme=https;end';\n        // }, 4000)\n        \n        // 会弹出跳转box，但是又快速退出回到帖子页\n        // tryOpenDefault(() => {\n        //     var a = document.createElement('a');\n        //     a.setAttribute('href', url);\n        //     a.setAttribute('target', '_blank'); // Ensures it opens in a new tab/window\n        //     a.click();\n        // }, 5000)\n  \n  \n  \n  // window.location.href = `prefs://${link}`\n                \n                // window.location.href = `x-safari-https://${link}` // box but not jump\n                \n                // window.location.href = `site://${link}` // not work\n                \n                // not work\n                // var a = document.createElement('a');\n                // a.setAttribute('href', currentLink);\n                // a.setAttribute('target', '_blank'); // Ensures it opens in a new tab/window\n                // a.click();\n                \n                // not work again\n                // var a = document.createElement('a');\n                // a.setAttribute('href', currentLink);\n                // a.setAttribute('target', '_blank'); // Ensures it opens in a new tab/window\n                // var dispatch = document.createEvent(\"HTMLEvents\");\n                // dispatch.initEvent(\"click\", true, true);\n                // a.dispatchEvent(dispatch);\n                    \n                // window.open(location.href, '_blank') // not work\n                // window.location.href = location.href // not work\n                // window.location.href = `safari://${currentLink}` // can prompt box, but can not jump still\n                // window.location.href = `safari://${link}`// can prompt box, but can not jump\n                // window.location.href = `googlechrome://${link}`// can open chrome\n```\n\n## 六、总结\n目前经过各种尝试发现，在安卓上确实是可以通过intent的方式唤起系统的浏览器，但是iOS的Safari浏览器，并没有合适的方法唤起浏览器并打开对应的网址。\n\n所以如果在iOS上的Facebook或者是其他app的内置浏览器（即in-app browser）上，想仅仅只通过web中来实现是做不到的。除非这个in-app浏览器所在的app是可以内置我们自己的代码的。\n\n因为在iOS系统中，app打开Safari的方式都是通过iOS的系统API：\n```js\n[[UIApplication sharedInstance] openUrl:@\"https://xxx.xxx.xxx\"]\n```\n这样的方式来实现跳转Safari的。所以除非web和app有通信机制，调用iOS原生代码的这个API。\n\n而且即使通过在Mac上的应用程序右键Safari浏览器，点击“查看内容”，打开Safari应用的`info.plist`，查看Safari的`URL Scheme`，也就只有有限的`http`、`https`、`ftp`等深链接。\n我在Mac上测试时，发现是可以通过以下代码：（有点忘了是不是safari开头，应该还有一个`x-safari-http`的scheme头，还是`webkit:`这个）\n```js\nwindow.location.href = `safari://43.xxx.xxx.xxx/index.html`\n```\n在Mac上是可以从谷歌Chrome浏览器跳转打开Safari的，但是在移动端是不行的。\n\n\n所以在iOS的第三方app的内置浏览器中，想打开系统Safari浏览器，最好还是要做一个引导的浮层，指向右上角的三个点，引导用户主动点击Facebook等第三方app的“打开外部浏览器”选项。\n\n\n\n","在一些面向海外的项目中，有时候经常绕不开Google和Facebook这2个巨大的平台。最常见的就是Google登录和Facebook登录，但是在一些项目推广和广告相关的需求，难免会遇到打开mobile的默认浏览器，以提高web项目的用户体验或者避免Google和Facebook的内置浏览器的限制。","https://image.xinwei.ltd/11721472897038.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":58,"name":60,"parentId":24},[],3212,"web facebook,facebook web,facebook integrate,facebook开发,facebook集成",{"id":93,"createdAt":94,"title":95,"content":96,"summary":97,"image":98,"uid":17,"user":99,"categoryId":24,"category":100,"subCategoryId":73,"subCategory":101,"comments":102,"status":8,"reason":22,"notice":22,"visitCount":103,"commentCount":47,"keywords":104},135,"2024-11-09T09:53:38.596Z","flutter中如何实现Apple登录","## 一、现有的Apple登录第三方库\n众所周知，当你想要快速实现某一个功能的时候，最快的方法还是要看一下有没有便捷的第三方库，可以极大的提高效率，而flutter第三方库查找的地方就在：[https://pub.dev/](https://pub.dev/)，这个是flutter官方支持库网址。\n\n目前来说，flutter的生态已经非常完善，而且支持的平台也很多，所以大多数项目所需的功能，都是可以找到现成的库来解决的。\n\n对于苹果的授权登录，最流行的库当属：`sign_in_with_apple`，连接是：[https://pub.dev/packages/sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple)，这个库是aboutyou团队开发和维护的库。\n\n## 二、集成Apple login\n1. 在flutter的pubspec.yaml中手动引入，或者通过命令引入：\n- 手动引入：\n```pubspec.yaml\nsign_in_with_apple: ^6.1.0\n```\n![flutter apple登录](https://image.xinwei.ltd/image1731144292470.png)\n- 终端命令集成\n```terminal\nflutter pub add sign_in_with_apple    \n```\n\n2. 自定义你的登录按钮，点击事件中添加以下代码：\n```dart\nimport 'package:sign_in_with_apple/sign_in_with_apple.dart';\n\nclass LoginPage extends StatefulWidget {\n  const LoginPage({super.key});\n\n  @override\n  State\u003CLoginPage> createState() => _LoginPageState();\n}\n\nclass _LoginPageState extends State\u003CLoginPage> {\n  ...\n\n  /// Apple 登录，解析这个credential\n  void login(AuthorizationCredentialAppleID appleID) async {\n    BaseViewModel.showLoadingDialog();\n    DefaultRepository repository = DefaultRepository();\n    var result = await repository.loginApple(\n        appleID.authorizationCode, appleID.identityToken ?? \"\", appleID.userIdentifier ?? \"\");\n    await BaseViewModel.hideLoadingDialog();\n    if (result.isSucc) {\n      handleLoginResult(result);\n    }\n  }\n\n  ...\n  // 登录按钮\n  GestureDetector(\n              onTap: () async {\n                final credential = await SignInWithApple.getAppleIDCredential(\n                  scopes: [\n                    AppleIDAuthorizationScopes.email,\n                    AppleIDAuthorizationScopes.fullName,\n                  ],\n                );\n                login(credential);\n              },\n              child: Container(\n                decoration: const BoxDecoration(\n                  color: Colours.btnEnableColor,\n                  borderRadius: BorderRadius.all(Radius.circular(28)),\n                ),\n                constraints: BoxConstraints.expand(width: sWidth - 64, height: 56),\n                child: const Center(\n                  child: Text(\"Continue with Apple\", style: TextStyle(\n                    color: Colors.black,\n                    fontFamily: 'SF',\n                    fontWeight: FontWeight.w700,\n                  ),),\n                ),\n              ),\n            )\n  ...\n}\n\n```\n3. 将苹果登录获取到的credential通过接口，发送给后端服务器去调用Apple服务。\n4. Apple开发者账号的证书中心网站中，需要配置开发证书和发布证书支持Sign In with Apple的功能。这个直接打开证书配置中心网站操作即可：[https://developer.apple.com/account/resources/](https://developer.apple.com/account/resources/)\n5. 如果想要同时支持flutter打出web、Android的包，那么需要这个证书中心配置service id，这个id用来做clientid。这个在我之前的uni-app项目中有提到，可以参考：[https://www.xinwei.ltd/article/123](https://www.xinwei.ltd/article/123)\n6. 在证书中心需要配置一个key，配置key的操作，也可以参考我之前的文章：[https://www.xinwei.ltd/article/123](https://www.xinwei.ltd/article/123)，key创建成功后，需要下载下来给后端，后端服务需要用到这个文件。\n7. 使用Xcode打开flutter中的iOS工程，选中Runner的target，添加Sign In with Apple功能。\n![sign in with apple](https://image.xinwei.ltd/image1731145116775.png)\n8.完整的代码就是上面贴出来的代码，步骤也很简单，集成进去，然后测试，一切应该都OK的。\n\n到此就结束了，祝集成顺利。\n","本文介绍如何在flutter中实现Apple的第三方登录，非常容易实现，这里简单记录一下。","https://image.xinwei.ltd/image1731144292470.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":73,"name":75,"parentId":24},[],2530,"flutter apple login,apple login,flutter apple,flutter ios,flutter苹果登录,flutter",{"id":106,"createdAt":107,"title":108,"content":109,"summary":110,"image":111,"uid":17,"user":112,"categoryId":24,"category":113,"subCategoryId":114,"subCategory":115,"comments":117,"status":8,"reason":22,"notice":22,"visitCount":118,"commentCount":47,"keywords":119},111,"2024-05-17T01:32:02.657Z","npm报错：node: --openssl-legacy-provider is not allowed in NODE_OPTIONS","## 出现的问题\n问题1:\n> ERROR  Failed to compile with 1 error                                        \n error  in ./src/modules/stu/views/member/MemberModelTest.vue?vue&type=style&index=0&id=7b0ab969&scoped=true&lang=scss\nSyntax Error: Error: Node Sass version 9.0.0 is incompatible with ^4.0.0.\n @ ./node_modules/vue-style-loader??ref--8-oneOf-1-0!./node_modules/css-loader??ref--8-oneOf-1-1!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/src??ref--8-oneOf-1-2!./node_modules/sass-loader/dist/cjs.js??ref--8-oneOf-1-3!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/modules/stu/views/member/MemberModelTest.vue?vue&type=style&index=0&id=7b0ab969&scoped=true&lang=scss 4:14-520 15:3-20:5 16:22-528\n @ ./src/modules/stu/views/member/MemberModelTest.vue?vue&type=style&index=0&id=7b0ab969&scoped=true&lang=scss\n @ ./src/modules/stu/views/member/MemberModelTest.vue\n @ ./src/modules/stu/router/practise.js\n @ ./src/modules/stu/router sync nonrecursive .js$\n @ ./src/modules/stu/router/index.js\n @ ./src/modules/stu/main.js\n @ multi (webpack)-dev-server/client?http://xxx.xxx.xxx.13:8080/sockjs-node (webpack)/hot/dev-server.js ./src/modules/stu/main.js\n\n问题2\n> node: --openssl-legacy-provider is not allowed in NODE_OPTIONS\n\n问题3\n> Error: spawn vue-cli-service ENOENT\n\n## 问题分析\n### 问题1\n这个问题往往出现在当你的电脑中安装了vue3的环境，而vue2的环境比如vue-cli-service等被移除，并且你运行vue2工程的时候。为什么这么说呢，你可以看报错信息中，node-sass这个sass库，通常是用于vue2的，并且这个node-sass已经被废弃，官方建议是使用dart-sass即（sass)来替代node-sass。\n详细线索可以查看：[https://www.npmjs.com/package/node-sass](https://www.npmjs.com/package/node-sass)\n而node-sass依赖的环境如下：\n![node-sass.png](https://image.xinwei.ltd/image1715906803789.png)\n\n你可以查看你电脑中现有的node版本：\n```terminal\nnode -v\n```\n由于我的node版本是20以上，而且使用的是vue3开发所以，报错了问题1.\n\n### 问题2\n出现问题2的原因很简单，那就是我在将本地的node版本降级到node 14.18.1的时候，终端报错了，并且由于我之前开发时对react-native做优化，在bash配置中增加了`--openssl-legacy-provider`的环境配置，这个原因造成的。\n\n### 问题3\n这个问题更简单，就是我喜欢使用vue3来开发，从而删除了本地的vue2中的`vue-cli-service`，导致本地没有这个bash命令。\n\n## 解决方案\n其实第1个和第2个问题是相辅相成的，在某些特殊场景下（比如你的电脑是Mac，并且系统相匹配的openssl版本是3.x）。所以有时候，你解决了第1个问题，还需要继续解决第2个问题。\n### 问题1\n针对vue2中配置了node-sass，而本地node环境比较高（比如14以上），那么有以下几个方案：\n- 使用nvm来使用本地的node，可以自由切换node（推荐）\n- 降级电脑上本地的node版本\n- 使用虚拟机或者docker技术来配置一个另类的\n\n这里的3个方法都可以配置一个低版本的node环境，我个人推荐使用第一个，因为比较简单。\n所以这里我解释一下第一个怎么做：\n1. 安装nvm\n```terminal\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash\n```\n\n2. 查看本地的nvm\n```terminal\nnvm ls\n```\n![nvm ls命令.png](https://image.xinwei.ltd/image1715908341860.png)\n\n3. 如果你需要安装某个node版本\n```terminal\nnvm install 14.18.1 --verbose\n```\n\n4. 如果你想要切换某一个node版本\n```terminal\nnvm use 14.18.1\n```\n\n### 问题2\n这个问题在你的mac电脑系统内置的openssl命令在3.x，而node环境配置了`--openssl-legacy-provider`的情况。\n1. 查看本地openssl版本\n```terminal\nopenssl -v\n```\n![](https://image.xinwei.ltd/image1715908806020.png)\n\n2. 检查本地的bash环境配置中的`NODE_OPTIONS`，如果有就修改一下\n有的同学是直接在终端中执行以下命令，但是如果bash文件中明确配置了这个项，其实是无效的\n```terminal\nset NODE_OPTIONS=\n```\n所以还需要检查在Mac上的资源库目录下.bashrc和.zshrc（如果安装的是item2的终端）这2个文件\n![node_options的配置.png](https://image.xinwei.ltd/21715909356941.png)\n如图所示，如果有`NODE_OPTIONS`这个配置，那么就设为空值。\n```.zshrc\nexport NODE_OPTIONS=\n```\n\n### 问题3\n这个问题比较简单，直接重新安装vue-cli就好。详细说明可以参考：[vue-cli官网](https://cli.vuejs.org/zh/#%E8%B5%B7%E6%AD%A5)\n```terminal\nnpm install -g @vue/cli\n```\n\n更多信息，大家可以关注我的公众号“新卫网络科技”。\n\n\n\n\n\n\n\n\n\n","很多时候，我们在开发前端项目中，会遇到node版本和项目中依赖不匹配的问题，正如标题上所写的那样，在执行node -v相关的node命令时，可能出现如此报错，这篇文章就来探讨一下如何解决这个问题。","https://image.xinwei.ltd/image1715906803789.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},17,{"id":114,"name":116,"parentId":24},"Node",[],2334,"node,npm,node版本,node issue,npm问题,npm nvm",{"id":121,"createdAt":122,"title":123,"content":124,"summary":125,"image":126,"uid":17,"user":127,"categoryId":24,"category":128,"subCategoryId":129,"subCategory":130,"comments":132,"status":8,"reason":22,"notice":22,"visitCount":133,"commentCount":47,"keywords":134},104,"2024-03-25T17:17:22.421Z","微信游戏小程序制作工具","\n官网：[https://gamemaker.weixin.qq.com/](https://gamemaker.weixin.qq.com/)\n## 概述\n微信提供了一个无须安装且无须写代码，只需要拖拽和参数设置的可视化小游戏在线开发工具。网站地址就是上面记录的官网地址。\n\n官网中已经提供了很多demo游戏，可以查看了解具体应该如何去制作一个游戏。\n\n这篇文章会具体从游戏中的基本概念和基础操作让大家对制作小游戏有一个基本的了解。\n\n![微信游戏小程序.jpg](https://image.xinwei.ltd/17113845921191711384625793.jpg)\n\n\n### 1.1  场景scene\n游戏中的一个页面。一个游戏至少要有一个场景，否则游戏就是一个静态的黑色界面，什么也不存在。\n在一个场景中，可以定义很多精灵，后面会解释。\n在场景中可以给精灵设置多种不同的动画、背景色、坐标、循环执行、碰撞、事件监听等。可以给物体设置坐标，当然直接拖动到某一个位置也是可以的。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/17113846040351711384658046.jpg)\n\n### 1.2 精灵sprite\n这是游戏中的通常叫法，指的是一个场景中包含的所有元素（主要是图片，比如背景色图片、子弹图片、飞机图片等等），都被叫精灵。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/17113846780451711384691666.jpg)\n\n### 1.3 动画animation\n可以给精灵设置动画。包括水平/垂直平移、面向某个精灵移动、移动的步数、旋转、循环、透明度等等。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/41711384741588.jpg)\n\n### 1.4 素材asset\n主要指的是构成一个游戏的所有图片、音频。素材可以通过导入平台上的素材库或者导入自己的图片、音频文件。导入到资源管理器之后，就可以进行使用。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/41711384773732.jpg)\n\n### 1.5 坐标系统\n打开官网的编辑器的时候，可以看到屏幕的中心点是（0，0）的位置，所以学过数学中象限概念的人就可以知道一个点在屏幕中的坐标可以如何设置正数和负数。当然用户在屏幕上的鼠标移动位置，也会显示在屏幕标尺的上方，也可以通过这个知道任何位置的具体坐标。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/51711384808664.jpg)\n\n### 1.6 造型\n对于游戏中的精灵来说，可以有好几个造型。造型可以理解成一张图片，在不同条件下，可以展示成其他状态的图片或者形态。比如一个敌机正常状态下是一个正常的飞机图片，在被子弹击中的时候，可以切换成爆炸状态的图片。\n在制作工具界面，可以通过点击外观，就可以看到切换造型的事件。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/61711384858794.jpg)\n\n### 1.7 事件和通知\n指的是给精灵添加的在某些时刻执行什么动作的钩子。\n对于程序员来说，这是很容易理解的。也可以从通俗的话来描述，比如当进入当前场景时、被点击时、发生碰撞时、收到通知时、屏幕被点击时等等很多时刻。\n添加了事件后，就可以在这个事件包裹的框框内，拖入其他的比如运动、动画等逻辑操作。\n比如可以设置添加一个事件“当场景启动时”，然后拖入一个动画“《自己》在《1》秒内滑行到X:《0》Y《0》”。尖括号内的时可以下拉选择精灵，或者输入数字的。那么预览场景的时候，就可以看到，当页面被加载出来时，精灵滑行到了你设置的(x, y)坐标位置。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/71711384899412.jpg)\n\n创建通知之后，那么肯定有地方会接收这个通知做某些事情。接收通知时间在“添加事件”的位置。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/71711384929416.jpg)\n\n这样就可以在通知的包裹块里，设置某些其他动作。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/71711384964652.jpg)\n\n### 1.8  逻辑\n非常重要，这里包含循环和条件循环的一些设置，相当于编程中的do-white和if-else以及sleep等操作。精灵的动画效果依托这些条件进行设置时，体验会更加的丰富。\n\n从拖拽的动作堆叠面板中可以看出。逻辑块一般都是在某个条件下，包含一组操作。\n\n跟平时我们画流程图一样，制作工具中拖动出来的一些操作都是使用不同形状来表示。常见的比如三角内包含的就是判断条件，长方形表示具体执行等。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/81711385029797.jpg)\n\n\n### 1.9 其他\n游戏中还可以设置局部或者全局变量。\n变量可以通过\"运算\"中的数据部分，设置变量的加减，然后给某一个数字图片精灵，通过“外观”的“修改《自己》的值为《》”，拖入“数据”中的“变量《》”，就可以实现外观上显示数字。\n\n整个操作界面中有很多元素，大家不妨自己动手试一试。自己动手才能更深入的理解每一个设置的作用。\n![小游戏变量设置.png](https://image.xinwei.ltd/image1711387026687.png)\n\n\n### 2.0 预览和发布\n![小游戏基础.png](https://image.xinwei.ltd/image1711386876600.png)\n\n\n在左上角的“作品设置”中可以进行作品设置和上传以及下载源码的操作。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/211711385075658.jpg)\n\n预览场景或者整个项目，也是在左上角的位置。\n![微信游戏小程序.jpg](https://image.xinwei.ltd/221711385101919.jpg)\n\n\n\n\n","这篇文章介绍如何开发一个微信上的游戏小程序，主要是利用了微信的在线开发制作工具，无须安装和代码编写，通过素材和逻辑堆砌，良好的设计出一个微信的游戏小程序，并且可以发布到微信进行审核。","https://image.xinwei.ltd/17113845921191711384625793.jpg",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},13,{"id":129,"name":131,"parentId":24},"小程序",[],2284,"wechat miniprogram,wechat game,wechat game play,game play,mini programe,mini game develop,game develop,小程序游戏,小程序游戏开发",{"id":73,"createdAt":136,"title":137,"content":138,"summary":139,"image":140,"uid":17,"user":141,"categoryId":24,"category":142,"subCategoryId":58,"subCategory":143,"comments":144,"status":8,"reason":22,"notice":22,"visitCount":145,"commentCount":47,"keywords":146},"2024-01-11T14:50:23.553Z","midwayjs - 相关基础知识","[midwayjs官网](https://midwayjs.org/docs/intro)\n\n### 一、开始\n初始化项目（将项目创建在你所想的文件夹下）\n```terminal\nnpm init midway\n```\n![](https://image.xinwei.ltd/image_11704984101233.png)\n![](https://image.xinwei.ltd/image_21704984114259.png)\n\n### 二、目录结构\n![](https://image.xinwei.ltd/image_31704984162591.png)\nconfiguration：组件配置等\ninterface：参数约束等\nconfig：缓存、数据库等\ncontroller：写请求响应api\nfilter：过滤器，比如异常、404等\nmiddleware：中间件，请求的日志记录、安全拦截等\nservice：写业务逻辑\ntest: 单元测试\n\n### 三、数据库typeorm\n具体步骤参考：[midwayjs-orm](https://midwayjs.org/docs/extensions/orm)\n#### 3.1、安装\n```terminal\nnpm i @midwayjs/typeorm@3 typeorm --save\n```\n#### 3.2、配置configuration\n```configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as orm from '@midwayjs/typeorm';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    orm                                                         // 加载 typeorm 组件\n  ],\n  importConfigs: [\n    join(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n#### 3.3、安装数据库 Driver\n```language\n# for MySQL or MariaDB，也可以使用 mysql2 替代\nnpm install mysql --save\n```\n\n#### 3.4、配置config.default.ts\nps：需要改成你在自己电脑上的mysql对应的ip、port、用户名、密码\n```config.default.ts\n// orm配置\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'mysql',\n        host: '127.0.0.1',\n        port: 3306,\n        username: 'root',\n        password: '123456',\n        database: 'midway_boot',\n        synchronize: true,\n        logging: true,\n      },\n    },\n  },\n```\n![](https://image.xinwei.ltd/image_41704984485475.png)\n\n\n","[midwayjs官网](https://midwayjs.org/docs/intro) 一、开始 初始化项目（将项目创建在你所想的文件夹下） terminal npm init midway (https://image.xinwei.ltd/image_11704984101233.png) (https://image.xinwei.ltd/image_21704984114259.png","https://image.xinwei.ltd/image_11704984101233.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":58,"name":60,"parentId":24},[],2084,"midwayjs,midwayjs基础,midwayjs官网",{"id":148,"createdAt":149,"title":150,"content":151,"summary":152,"image":153,"uid":17,"user":154,"categoryId":24,"category":155,"subCategoryId":73,"subCategory":156,"comments":157,"status":8,"reason":22,"notice":22,"visitCount":158,"commentCount":47,"keywords":159},131,"2024-10-19T06:11:42.227Z","flutter pub get时报错pod install error: Error in the HTTP2 framing layer","在flutter工程中，flutter pub get时iOS工程的pod install报错问题。\n\n> Downloading dependencies\n-> Installing Adjust (4.38.4)\n > Git download\n > Git download\n     $ /usr/bin/git clone https://github.com/adjust/ios_sdk.git /var/folders/kx/zj85bxgx4td2tw0137gxb9vr0000gn/T/d20241019-31559-79f4ta --template= --single-branch --depth 1 --branch v4.38.4\n     Cloning into '/var/folders/kx/zj85bxgx4td2tw0137gxb9vr0000gn/T/d20241019-31559-79f4ta'...\n     fatal: unable to access 'https://github.com/adjust/ios_sdk.git/': Error in the HTTP2 framing layer\n[!] Error installing Adjust\n[!] /usr/bin/git clone https://github.com/adjust/ios_sdk.git /var/folders/kx/zj85bxgx4td2tw0137gxb9vr0000gn/T/d20241019-31559-79f4ta --template= --single-branch --depth 1 --branch v4.38.4\nCloning into '/var/folders/kx/zj85bxgx4td2tw0137gxb9vr0000gn/T/d20241019-31559-79f4ta'...\nfatal: unable to access 'https://github.com/adjust/ios_sdk.git/': Error in the HTTP2 framing layer\n\n\n截图：\n![flutter pub get error](https://image.xinwei.ltd/image1729318210433.png)\n\n\n解决办法：\n将git的http version切换成1.1:\n```terminal\ngit config --global http.version HTTP/1.1\n```\n","在flutter工程中，flutter pub get时iOS工程的pod install报错问题。","https://image.xinwei.ltd/image1729318210433.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":73,"name":75,"parentId":24},[],2010,"flutter pod install error,pod install error,adjust error,flutter",{"id":161,"createdAt":162,"title":163,"content":164,"summary":165,"image":166,"uid":17,"user":167,"categoryId":24,"category":168,"subCategoryId":58,"subCategory":169,"comments":170,"status":8,"reason":22,"notice":22,"visitCount":171,"commentCount":47,"keywords":172},141,"2024-11-28T07:00:21.332Z","web中实现Google登录和firebase登录以及google token转firebase token","## 一、前言\n在web的前端开发中，实现第三方授权登录的场景和平台很多，最常见的比如Google登录、Apple登录、Facebook登录，国内常见的有比如微信登录、支付宝登录，这些都是需要依赖他们这些第三方平台的授权流程才能拿到登录用户的身份id或者access token的。\n\n在web开发中，实现第三方登录的形式，我的经验告诉我大概可以分为3种：\n- 1. 引入第三方平台的库，在vue或者react或者原生div等按钮组件中，加一些标签属性或者配置。当点击后，第三方平台的函数会去执行授权流程。\n\n这种场景比如，使用谷歌的统一登录按钮：\n```示例\n\u003Cdiv id=\"g_id_onload\"\n     data-client_id=\"YOUR_GOOGLE_CLIENT_ID\"\n     data-login_uri=\"https://your.domain/your_login_endpoint\"\n     data-your_own_param_1_to_login=\"any_value\"\n     data-your_own_param_2_to_login=\"any_value\">\n\u003C/div>\n```\n![谷歌登录按钮](https://image.xinwei.ltd/11732766626888.png)\n\n具体解释可自行参考：[https://developers.google.com/identity/gsi/web/tools/configurator?hl=zh-cn](https://developers.google.com/identity/gsi/web/tools/configurator?hl=zh-cn)\n\n- 2. 引入第三方平台的库，使用javascript方法去调用第三方库的js api实现授权流程。\n\n还是拿Google登录举例：\n```示例\n\u003Cscript src=\"https://accounts.google.com/gsi/client\" async>\u003C/script>\n```\n```示例脚本\n\u003Cscript>\n  window.onload = function () {\n    google.accounts.id.initialize({\n      client_id: 'YOUR_GOOGLE_CLIENT_ID',\n      callback: handleCredentialResponse\n    });\n    google.accounts.id.prompt((notification) => {\n        if (notification.isNotDisplayed() || notification.isSkippedMoment()) {\n            // try next provider if OneTap is not displayed or skipped\n        }\n    });\n  }\n\u003C/script>\n```\n具体可自行查看：[https://developers.google.com/identity/gsi/web/guides/use-one-tap-js-api?hl=zh-cn](https://developers.google.com/identity/gsi/web/guides/use-one-tap-js-api?hl=zh-cn)\n\n- 3. 使用重定向的方式，在web当前域名网页下，跳转第三方平台的授权页，用户登录同意授权后，第三方平台的授权页会重定向回web的域名网页，重定向回来的web网页中会携带第三方平台加上的授权结果参数。前端开发同学，需要自己从网址参数读取参数值，并且将参数值通过接口请求发送给自己的后端同学进行处理。\n\n还是拿Google登录举例，通过重定向授权流程，最后重定向到的网址是：\n```示例授权返回后的网址\nhttps://oauth2.example.com/callback#access_token=4/P7q7W91&token_type=Bearer&expires_in=3600\n```\n如上所示，会有一些参数拼接在前端同学的授权返回网址上。\n\n以上大概是目前在web前端开发中遇到的几种授权登录场景。\n\n> 提示：显示网页的设备环境是多种多样的，对于在网页授权过程中，如果是弹出式授权窗口(iframe)的形式，经常会被一些设备或者一些app的webkit给拦截掉，从而导致授权失败。\n\n\n## 二、前端web开发中实现Google登录的方法\n根据前言中所提到的一些信息，相信大家对授权登录的集成方式有了大概的理解，在这一节，我们聊一下如何优雅的实现一个不会被webkit拦截的授权登录方式。所以我选择重定向的方式。\n\nGoogle对OAuth 2.0 的授权流程是比较支持的，当然你也可以继续使用js API的方式，这里我选择OAuth 2.0的标准。\n\n1. 需要在Google console中创建项目，得到一个client id；\n2. 需要在Google console的项目中配置重定向网址；\n3. web前端中实现重定向的代码。\n\n官方有给出文档：[https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow?hl=zh-cn](https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow?hl=zh-cn)\n\n### 2.1 实现Google登录必要的配置\n需要在Google的console控制台，新建一个项目，然后配置web的一些授权网域。\nGoogle的console网址：[https://console.cloud.google.com/](https://console.cloud.google.com/)\n\n![创建Google项目](https://image.xinwei.ltd/image1732767629549.png)\n\n在Google console中创建一个项目后，需要配置apis & services\n![apis & services](https://image.xinwei.ltd/image1732767724800.png)\n\n刚创建的项目是没有oauth client id的，所以需要创建一个新的：\n![OAuth client ID](https://image.xinwei.ltd/image1732767839169.png)\n\n创建完成后会得到client Id（我截图中有firebase自动创建的，这个在下一节会提到，大家可以忽略）\n![Client ID位置](https://image.xinwei.ltd/image1732768187572.png)\n\n配置授权回调网址，需要进入到OAuth consent Screen，然后点击进入编辑 \n![进入编辑](https://image.xinwei.ltd/image1732772143251.png)\n填写授权域名，这里可以填写重定向回调的网址域名，而且只能是顶级域名，当重定向成功后，会跳转到这个顶级域名下的网页（重定向的uri必须是这个顶级域名下的）\n![authorized domains](https://image.xinwei.ltd/image1732772314649.png)\n\n到这里，配置上的事情就结束了。\n\n### 2.2 发起重定向以及解析授权参数\n```google.ts\nfunction generateNonce() {\n  return [...Array(30)]\n    .map(() => Math.floor(Math.random() * 36).toString(36))\n    .join('');\n}\n// 实例化谷歌登录\nexport const googleSignAction = (callback: (success: boolean, msg?: string, res?: any) => void) => {\n            const clientId = \"xxx-xxx.apps.googleusercontent.com\";\n            const url = location.protocol.replaceAll(\":\", \"\") + \"://\" + location.host;\n\t    const redirectUri = encodeURIComponent(url);\n\t    const scope = encodeURIComponent(\"email profile openid\");\n\t    const responseType = \"id_token\"; // code、access_token\n      const nonce = generateNonce(); // id_token 必须要传此字段，且随机\n\t\t\n\t    const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=${responseType}&state=google&nonce=${nonce}`;\n\t    window.location.href = authUrl;\n}\n```\n> 提示：这里的ClientId，就是OAuth 2.0 Client IDs中的值\n\n![client id](https://image.xinwei.ltd/image1732772707047.png)\n\n接下来，当重定向成功后，解析我们自己网页中的参数：\n```google.ts\n/**\n * 当Google授权登录成功之后，读取path，从path中获取code，然后发送post请求，获取access token以及id token。\n * - id token用来和firebase进行授权，获取firebase的token。\n * - success：返回从firebase获取到的id token\n */\nexport const HandleGoogleAuthRedirect = async (path: string, success: (token: string) => void, fail: (err: any) => void) => {\n\tlet searchParams = path\n\tlet searchList = path.split(\"/#/\")\n\tif (searchList.length > 0) {\n\t\tsearchParams = searchList[searchList.length - 1]\n\t}\n\tsearchList = searchParams.split(\"?\")\n\tif (searchList.length > 0) {\n\t\tsearchParams = searchList[searchList.length - 1]\n\t}\n\tconst params = new URLSearchParams(searchParams)\n\tconst idToken = params.get('id_token')\n  ...\n}\n```\n这里的path参考前言一节中的重定向部分，网址携带参数的示例。\n\n这里的idToken就是Google的身份验证得到的标识，然后可以调用后端接口让服务端同学去处理了。\n\n## 三、前端web开发中实现firebase的Google登录的方法\n众所周知，firebase是Google的一个大一统集成平台，firebase上可以集成Crashly、firestore、login等一系列的功能，使用firebase可以很清晰的查看各种统计数据，各个功能的数据可以同步进行对比分析。\n\n所以firebase自然提供了Google登录，当然他还支持Facebook、Apple、Twitter登录等平台。\n\nfirebase的web登录同样是会得到一个id token，但是这个id token和Google得到的id token不是同一个字符串，是只针对firebase平台的。\n\n使用firebase登录需要在firebase的console平台上创建项目并且进行配置。\nfirebase console的链接是：[https://console.firebase.google.com/](https://console.firebase.google.com/)\n\n### 3.1 创建firebase项目并且配置\n进入控制台，创建一个新项目\n![创建新项目](https://image.xinwei.ltd/image1732773565975.png)\n\n创建完项目后，给这个项目添加web平台\n![添加应用](https://image.xinwei.ltd/image1732773746221.png)\n\n选择添加web平台\n![firebase web](https://image.xinwei.ltd/image1732773709899.png)\n\n根据以下截图，按照序号顺序，选择使用google作为身份提供方\n![firebase google](https://image.xinwei.ltd/image1732773858431.png)\n\n添加完后，进行编辑，一定要选择启用Google\n![firebase google](https://image.xinwei.ltd/image1732773945666.png)\n\n添加授权网域，需要把我们的网页域名添加到这里的“已获授权的网域”\n![auth domains](https://image.xinwei.ltd/image1732774056910.png)\n\n至此，前端相关的配置到这里就结束了，另外提一句，后端需要的文件也在这个控制台里，以下是截图：\n![server side usage](https://image.xinwei.ltd/image1732774393582.png)\n\n### 3.2 代码使用firebase\n查看前端的firebase集成代码，还是在刚刚的“项目设置”页面下，在“常规”这一项下，滑动到最底部，查看web的集成代码\n![project setting](https://image.xinwei.ltd/image1732774591497.png)\n\n滚动到最底部的地方，就有集成方式介绍\n![npm firebase](https://image.xinwei.ltd/image1732774714790.png)\n\n集成后查看package.json文件：\n```package.json\n{\n  \"dependencies\": {\n    ...\n    \"firebase\": \"^10.12.3\",\n    ...\n  },\n  ...\n}\n```\n\n我先把firebase的示例代码贴到这里：\n```firebase.ts\n// Import the functions you need from the SDKs you need\nimport { initializeApp } from \"firebase/app\";\nimport { getAnalytics } from \"firebase/analytics\";\n// TODO: Add SDKs for Firebase products that you want to use\n// https://firebase.google.com/docs/web/setup#available-libraries\n\n// Your web app's Firebase configuration\n// For Firebase JS SDK v7.20.0 and later, measurementId is optional\nconst firebaseConfig = {\n  apiKey: \"AxxxxQk\",\n  authDomain: \"xxx.firebaseapp.com\",\n  projectId: \"xxxx\",\n  storageBucket: \"xxx.firebasestorage.app\",\n  messagingSenderId: \"xxxx\",\n  appId: \"1:xxxx:web:xxxx\",\n  measurementId: \"G-xxx\"\n};\n\n// Initialize Firebase\nconst app = initializeApp(firebaseConfig);\nconst analytics = getAnalytics(app);\n```\n\n我们这里由于使用到了Google的身份验证，所以其实需要引入firebase的Google提供API，所以我贴一下我自己的代码：（`GetCurrentFirebase`表示的就是上面示例中的`firebaseConfig`）\n```示例.ts\nimport { initializeApp } from \"firebase/app\";\nimport { getAuth, signInWithPopup, GoogleAuthProvider } from \"firebase/auth\";\nimport { GetCurrentFirebase } from \"../../../env/firebase\";\n\nconst firebaseConfig = GetCurrentFirebase()\n\nconst app = initializeApp(firebaseConfig);\nconst auth = getAuth(app);\nconst provider = new GoogleAuthProvider();\n\nexport { auth, provider, signInWithPopup, GoogleAuthProvider };\n```\n\n然后我们在点击按钮后，要唤起授权弹窗：\n```示例.ts\n...\nimport { auth, provider, signInWithPopup, GoogleAuthProvider  } from \"./firebase\";\n...\n\nconst onClickGoogleBtn = (callback: (success: boolean, msg?: string, res?: any) => void) => {\n  signInWithPopup(auth, provider)\n\t  .then(async (result) => {\n\t\t  console.log(\"Google授权登录获取到的值是：\", res);\n\t\t  if (result && result.user) {\n\t\t\t  const user = result.user;\n\t\t\t  user.getIdToken(true).then((id_token) => {\n                              callback(true, id_token, res)\n\t\t\t  }).catch(error => {\n                              callback(false, typeof error == 'string' ? error : (error.message || error.msg));\n\t\t\t  });\n\t\t  } else {\n\t\t\t  callback(false, TXT_PleaseTryAgain);\n\t\t  }\n\t  })\n\t  .catch((error) => {\n\t\tconsole.log(\"Google授权登录失败是：\", error);\n\t\tcallback(false, typeof error == 'string' ? error : (error.message || error.msg));\n\t  });\n}\n```\n\n这样就能获取到firebase的id token了，但是这种方式就是通过弹窗授权的方式，而弹窗授权在一些设备的浏览器或者某些app的webview中显示时，弹窗会被禁止，从而导致弹窗授权失败。\n\n所以我们必须得使用一个不使用弹窗的授权方式，保证授权登录能成功。\n\n我看了firebase的很多文档，他们虽然提供了fireabse的重定向登录方式，但是对于fireabse的验证有诸多限制，是不容易实现的。\n\nfirebase的官方集成文档的几种方式：[https://firebase.google.com/docs/auth/web/redirect-best-practices?hl=zh-cn](https://firebase.google.com/docs/auth/web/redirect-best-practices?hl=zh-cn)\n\n总结来说，firebase的重定向方式有以下几种：\n![firebase授权示例](https://image.xinwei.ltd/image1732776357150.png)\n\n其实就一句话，那就是如果只使用firebase的方式进行Google授权，而且使用重定向的方式，那么服务端要么使用firebase Hosting，要么自行托管firebase授权文件。\n\n所以仅仅靠firebase实现Google的授权重定向登录，是不够的，下一节我们会说到。\n\n## 四、结合Google授权登录、firebase身份转换达到获取firebase的id token\n从这一节的标题看，相信大家已经知道如何去优雅的实现一个firebase的Google重定向登录。而且只需要使用第二、三种的代码，稍加改进就可以实现。\n\n总的流程就是：\n1. 使用Google的重定向获取Google的id token；\n2. 使用firebase的身份credential将Google的id token转成firebase的id token。\n\n第一步，通过第二节的代码，就可以得到，所以这里不再赘述。\n\n第二步，将firebase的授权登录代码，改造一下：\n```firebase.ts\n...\nimport { signInWithCredential } from \"firebase/auth\";\nimport { auth, GoogleAuthProvider  } from \"./firebase\";\n...\n\n        const params = new URLSearchParams(searchParams)\n\tconst idToken = params.get('id_token')\n\tif (idToken != null && idToken.length > 0) {\n\t\tconst credential = GoogleAuthProvider.credential(idToken);\n\t\tconst result = await signInWithCredential(auth, credential);\n\t\tconst user = result.user;\n\t\tuser.getIdToken(true).then((token) => {\n\t\t\t// 这里获取到的就是firebase 的 id token\n\t\t\tsuccess(token);\n\t\t}).catch(error => {\n\t\t\t// 这里就是出错了\n\t\t\tfail(error)\n\t\t});\n\t}\n...\n```\n上面的这个代码，大家应该能看懂吧，`auth`和`GoogleAuthProvider`，是在第三章的代码中`export`的，这里从`firebase`库中引入了`signInWithCredential`了。\n\n\n\n全篇就写到这里，希望对大家有帮助。\n\n\n\n\n\n\n\n\n\n","今天简单记录一下如何在web前端开发中实现Google登录和firebase登录，重点在如何使用谷歌登录后，转成firebase的登录方法。","https://image.xinwei.ltd/11732766626888.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":58,"name":60,"parentId":24},[],1910,"google login,firebase google login,firebase login,firebase google,firebase,google,google login token,web firebase,web google,web谷歌登录,web firebase登录,谷歌登录,firebase登录",{"id":174,"createdAt":175,"title":176,"content":177,"summary":178,"image":179,"uid":17,"user":180,"categoryId":24,"category":181,"subCategoryId":58,"subCategory":182,"comments":183,"status":8,"reason":22,"notice":22,"visitCount":184,"commentCount":47,"keywords":185},139,"2024-11-27T11:10:01.438Z","element plus使用范围为一周的日期选择组件(el-date-picker), 周一到周日 和 周日到周六","在引入element-plus组件库的项目中，使用date picker组件选择周范围的日期时，如何设置周范围从周日到周六。\n\n在默认情况下，尤其是中文时区的项目中，默认的周范围选择是周一到周日的7天，但是在出海业务中，经常会遇到要使用周日到周六的7天范围。\n\nelment-plus中有一个非常方便的组件`el-date-picker`\n\n解释一下，我们使用的周选择器效果图：\n![周一到周日](https://image.xinwei.ltd/11732704674655.png)\n这是默认情况下的的情况，如图所示，选择的是周一到周日的时间范围，只要点击其中某一个日期，那么范围会廊括这个日期在内的一周。\n代码是：\n```vue\n...\n\u003Cel-date-picker\n    v-model=\"dates\"\n    type=\"week\"\n    :format=\"`${searchData.st} - ${searchData.et}`\"\n    placeholder=\"Pick a week\"\n    @change=\"handleWeekChange\"\n/>\n...\n\nimport {dayjs, ElMessage} from 'element-plus';\n...\nconst dates = ref();\nconst searchData = reactive({\n    ...\n    st: '',\n    et: '',\n    ...\n})\n...\n\nfunction handleWeekChange(value){\n    if (value) {\n        getTime(value)\n    }\n}\n\nfunction getTime(value){\n    const start = dayjs(value).startOf('week').format('YYYY-MM-DD');\n    const end = dayjs(value).endOf('week').format('YYYY-MM-DD');\n    searchData.st = start;\n    searchData.et = end;\n}\n```\n\n那么如何将上面截图中的“周一到周日”的范围，改成“周日到周六”的范围呢？\n\n其实elment plus在官网中已经说明了：[https://element-plus.org/zh-CN/guide/i18n.html](https://element-plus.org/zh-CN/guide/i18n.html)\n\n![element-plus datepicker](https://image.xinwei.ltd/21732705335452.png)\n\n那么我们需要做的操作就是将dayjs的的locale进行修改，其他的组件使用完全不用动。\n\n在`main.ts`中直接`import` `dayjs`的locale文件即可：\n```main.ts\nimport 'dayjs/locale/en'\n```\n\n当然你还可以直接从element-plus中export出dayjs，然后调用方法也行：\n```main.ts\n...\nimport ElementPlus, { dayjs } from 'element-plus';\n...\n\ndayjs.locale('en');\n...\n```\n\n得到的效果如下：\n![周日到周六](https://image.xinwei.ltd/31732705632008.png)\n\n\n欢迎大家多多关注公众号\"新卫网络科技\".\n\n\n","在引入element-plus组件库的项目中，使用date picker组件选择周范围的日期时，如何设置周范围从周日到周六。","https://image.xinwei.ltd/11732704674655.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":58,"name":60,"parentId":24},[],1718,"el-date-picker,element plus date picker,week type date picker,vue date picker,vue week picker,element-plus",{"id":187,"createdAt":188,"title":189,"content":190,"summary":191,"image":192,"uid":17,"user":193,"categoryId":24,"category":194,"subCategoryId":195,"subCategory":196,"comments":198,"status":8,"reason":22,"notice":22,"visitCount":199,"commentCount":8,"keywords":200},88,"2024-02-24T08:44:18.075Z","curl error: RPC failed; curl 18 HTTP/2 stream 5 was reset8.00 KiB/s","不管在什么类型的项目中，当使用git相关的pull或者clone时，有时候会遇到以下错误：\n> error: RPC failed; curl 18 HTTP/2 stream 5 was reset8.00 KiB/s\n> Early EOF\n> Expect 5032 Kib\n> \n>  或者\n>\n> error time out\n\n忘记给错误截图了，但是问题基本是那个类似是上面的报错。\n\n从上面的报错来看，其实是在curl的时候，网络disconnect或者其他类似的io出错。\n\n这个问题的出现其实就是由于网络超时导致的package没有传输完成，或者是由于缓冲区小了导致的io error。\n\n面对这样的问题，有2种思路是可以思考的。\n1. 增加git clone的时间，防止由于time out而导致的终止链接，即增大buffer。\n2. 升级curl到8.2.1以上。【**推荐先尝试这个方式**】\n\n## 1. 升级curl\n我们先来说升级curl的方式，其实在curl的github上（[https://github.com/curl/curl/issues/11353](https://github.com/curl/curl/issues/11353)）就已经有相关的issue提出来，而且作者也说了8.2.1之前的版本确实会复现这样的问题，所以很简单，直接查看本地的curl版本，如果低于8.2.1版本，那么直接去升级就好了。\n\n\n查看curl的版本：\n```\ncurl --version\n```\n![](https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-02-24%20161708763014958.png)\n\n升级本地的curl版本：\n```\nbrew install curl\n```\n注意：\n在Mac上通过brew安装的curl，有可能没有自动配置环境变量，甚至安装之后查看curl的版本，还是之前的版本。所以需要进一步配置一下环境变量。\n\n通过brew info查看刚才安装的curl：\n```\nbrew info curl\n```\n![](https://image.xinwei.ltd/Snipaste_2024-02-24_16-28-061708763303402.png)\n\n然后，按照截图的蓝色框框中的内容，配置一下环境变量：\n```终端命令\necho 'export PATH=\"/usr/local/opt/curl/bin:$PATH\"' >> ~/.zshrc\n```\n然后再检查curl的版本：\n![](https://image.xinwei.ltd/Snipaste_2024-02-24_16-30-081708763434718.png)\n\n确认版本正确之后，再去执行git相关的操作，验证是否可行。\n我自己的问题是在更新curl后得到解决。\n\n## 2. 增加git clone的时间\n很容易理解，增加clone时间其实就是希望哪怕网速再慢，也可以慢慢clone下来，而不会导致time out而链接中断。\n那么常用的设置命令是：\n```终端命令\ngit config --global http.postBuffer 157286400\n```\n这里是把缓冲区设置为150M大小，git默认的是1M。\n\n其实很多程序员对此存在争议，认为这个设置是没有必要的，但仍然有程序员通过这个设置解决了自己的问题。\n\n对于这个设置，由于我自己通过这个命令没有解决我遇到的问题，所以暂时不敢苟同。\n\n## 3. 另外还有一种解法，就是使用http/1.1而不是http/2来进行传输\n这个方式我没尝试过，但是有一定的道理，http/2的协议比http/1.1确实多了很多限制，那么更改成http/1.1，或许可以解决transfer中遇到的问题，仅作参考吧。\n\n这种方式的解法是：\n先设置：\n```\ngit config --global http.version HTTP/1.1\n```\n当执行完git相关的操作后，再设置：\n```\ngit config --global http.version HTTP/2\n```\n\n","这个问题是在使用git相关的库clone时较容易遇到的问题，给大家一个解决问题的思路。","https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-02-24%20161708763014958.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},39,{"id":195,"name":197,"parentId":24},"软件",[],1711,"git,git clone,git error,git issue,git使用,git克隆,git使用",{"id":202,"createdAt":203,"title":204,"content":205,"summary":206,"image":207,"uid":17,"user":208,"categoryId":24,"category":209,"subCategoryId":58,"subCategory":210,"comments":211,"status":8,"reason":22,"notice":22,"visitCount":212,"commentCount":47,"keywords":213},120,"2024-07-31T03:45:41.753Z","uni-app中的输入框input: uni-easyinput，自动填充账号密码的背景色","## 背景\n在前端开发中，输入框input标签，在Safari或者Chrome浏览器上显示网页时，经常会出现keychain或者Google密码的自动填充提示。当使用这个填充提示时，浏览器会把账号和密码自动填充到输入框中，并且改变输入框的背景色，但是背景色就容易和现有的网页的背景色不协调。所以需要去解决这个问题。\n\n## 示例\n未输入状态：\n![未输入状态](https://image.xinwei.ltd/image1722396490031.png)\n\n在Safari中准备输入时，Safari自动在输入框右侧显示了账号/密码填充按钮：\n![Safari的账号密码填充](https://image.xinwei.ltd/image1722396537516.png)\n\n选择自动填充：\n![Safari账号密码填充](https://image.xinwei.ltd/image1722396597898.png)\n\n填充之后的样式，发现被Safari修改了：\n![Safari自动填充后样式被修改了](https://image.xinwei.ltd/image1722396642478.png)\n\n这种UI显示极其不协调，所以需要去修改。但是在检查html元素时，你会发现，直接在css中对input标签设置背景色也解决不了。\n![safari查看input标签](https://image.xinwei.ltd/1%20%281%291722397264339.png)\n\n\n## 解决方案\n在网上查找一些解决方案时，很多是直接在input标签设置auto-fill相关的样式，比如以下：\n```css\ninput:-webkit-autofill,\ninput:-webkit-autofill:hover, \ninput:-webkit-autofill:focus, \ninput:-webkit-autofill:active  {\n    transition: background-color 5000s;\n    -webkit-text-fill-color: #fff !important;\n}\n```\n\n但是我当前是使用的uni-app这个框架，直接设置input的标签并不会直接生效，所以采用以下的方法：\n```scss\n:deep(.uni-input-input) {\n  color: white !important;\n  -webkit-text-fill-color: white !important;\n  -webkit-box-shadow: 0 0 0px 1000px transparent inset !important;\n  background-color:transparent !important;\n  background-image: none !important;\n  transition: background-color 50000s ease-in-out 0s !important; //背景色透明 生效时长 过渡效果 启用时延迟的时间\n}\n```\n\n我在uni-app框架下使用的是`uni-easyinput`输入框:\n```uni-app, xxx.vue\n\u003Cuni-easyinput\n  class=\"input\"\n  :class=\"{ 'input-empty' : email.trim().length \u003C= 0, 'input-error' : emailError.trim().length > 0 }\"\n  placeholder=\"Email\"\n  placeholder-style=\"color:#8C8C8C;font-family:'gilroy-medium';font-size:32rpx;text-align:left\"\n  style=\"color: white;\"\n  :clearable=\"false\"\n  v-model=\"email\"\n/>\n```\n\n效果：\n![输入框input修改后](https://image.xinwei.ltd/image1722397520758.png)\n\n\n希望对大家有帮助。\n\n\n\n\n\n\n","在前端开发中，输入框input标签，在Safari或者Chrome浏览器上显示网页时，经常会出现keychain或者Google密码的自动填充提示。当使用这个填充提示时，浏览器会把账号和密码自动填充到输入框中，并且改变输入框的背景色，但是背景色就容易和现有的网页的背景色不协调。所以需要去解决这个问题。","https://image.xinwei.ltd/image1722396490031.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":58,"name":60,"parentId":24},[],1669,"input tag,input color,input tag web,web input,input颜色,输入框修改颜色",{"id":27,"createdAt":215,"title":216,"content":217,"summary":218,"image":219,"uid":17,"user":220,"categoryId":24,"category":221,"subCategoryId":222,"subCategory":223,"comments":225,"status":8,"reason":22,"notice":22,"visitCount":226,"commentCount":47,"keywords":227},"2024-01-22T15:01:27.42Z","Mac上操作，使用yml文件制作docker compose nginx","docker nginx image site: https://hub.docker.com/_/nginx  \n\n本文是在Mac OS上使用docker的桌面版来操作docker，以Mac OS模拟宿主机. \n\n### 一、在宿主机上自定义目录作为挂载目录\n比如在centos上可以使用home目录，大家也可以使用docker拉取centos镜像运行容器，在centos中操作，也可以在实际的云服务器上操作。  \n\n本文直接使用Mac OS模拟宿主机，所以在desktop新建了我自己的目录： \n```terminal\n/Users/hanweixing/Desktop/docker_projects/pro_2\n```\n![docker目录](https://image.xinwei.ltd/11705932893134.png)\n1.目录下的nginx空文件夹，将来存放挂载nginx容器中的目录/文件;\n2.docker-compose.yml文件后面会提到;\n3.所以自己准备一个空文件夹备用即可，这里新建nginx文件夹为例。\n### 二、在宿主机上部署nginx的注意点 \n1.当在宿主机上挂载nginx容器中的目录和配置文件时，宿主机需要提供一个nginx.conf文件，否则会报错： \n> pro_2-nginx0101-1  | nginx: [emerg] open() \"/etc/nginx/nginx.conf\" failed (2: No such file or directory)\n\n![](https://image.xinwei.ltd/21705932985538.png)  \n### 三、如何获取一个默认的nginx.conf文件\n当然你可以在网上搜索一个文件。 \n也可以使用临时容器获取&拷贝出来： \n```terminal \n$ docker container create --name tmpNginx nginx\n$ mkdir -p /Users/hanweixing/Desktop/docker_projects/pro_2/nginx/conf/\n$ docker cp tmpNginx:/etc/nginx/conf.d/default.conf /Users/hanweixing/Desktop/docker_projects/pro_2/nginx/nginx.conf\n$ docker rm tmpNginx\n```\n注：这里使用创建容器然后删除的做法，其实还有一个就是如下的用法，即不会保存的容器，这里不单独讲了。 \n```language\ndocker run --rm\n```\n这样就可以在我们的目录下得到一个nginx.conf文件。 \n```查看nginx.conf\nhanweixing@192  ~/Desktop/docker_projects/pro_2  cat nginx/nginx.conf\n```   \n```nginx.conf\nuser  nginx;\nworker_processes  auto;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include       /etc/nginx/mime.types;\n    default_type  application/octet-stream;\n\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log  /var/log/nginx/access.log  main;\n\n    sendfile        on;\n    #tcp_nopush     on;\n\n    keepalive_timeout  60;\n\n    gzip  on;\n    \n    include /etc/nginx/conf.d/*.conf;\n\n    server{\n       listen 80;\n       charset utf-8;\n\n       location / {\n          root   /usr/share/nginx/html;\n          try_files $uri $uri/ =404;\n          index  index.html index.htm;\n       }\n\n       #error_page  404              /404.html;\n\n       # redirect server error pages to the static page /50x.html\n       #\n       error_page   500 502 503 504  /50x.html;\n       location = /50x.html {\n           root   html;\n       }\n    }\n}\n\n```\n### 四、编写docker compose yml文件\ndocker hub上的示例是：\n![](https://image.xinwei.ltd/31705934403764.png)\n仿照写一份完整的nginx配置：\n```terminal\n hanweixing@192  ~/Desktop/docker_projects/pro_2  vi docker-compose.yml\n```\n```docker-compose.yml\nversion: '1.0'\n\nservices:\n  nginx:\n    restart: always\n    container_name: blog_nginx\n    image: nginx\n    ports:\n      - 80:80 # expose outside\n      - 443:443 # ssl\n    volumes:\n      - ./nginx/nginx.conf/:/etc/nginx/nginx.conf\n      - ./nginx/conf.d:/etc/nginx/conf.d\n      - ./nginx/html:/usr/share/nginx/html\n      - ./nginx/www:/var/www\n      - ./nginx/logs:/var/log/nginx\n      - ./nginx/etc/cert:/etc/nginx/cert\n    environment:\n      - NGINX_PORT=80\n      - TZ=Asia/Shanghai # 时区\n    privileged: true\n\n```\n### 五、运行docker compose.\n```terminal\n hanweixing@192  ~/Desktop/docker_projects/pro_2  docker compose up -d\n```\n![](https://image.xinwei.ltd/41705934536583.png)\n挂载目录下自动生成对应容器的目录：\n![](https://image.xinwei.ltd/51705934561761.png)\n\n### 六、查看容器状态\n```terminal\ndocker container ls // 这是查看running状态的容器\n```\n建议使用\n```terminal\ndocker ps -a // check all\n```\n桌面版直接看docker\n![](https://image.xinwei.ltd/61705935467755.png)\n\n### 七、访问localhost:80\n![](https://image.xinwei.ltd/71705935497791.png)\n因为我们的nginx中的conf配置中挂载目录下没有提供html或php网页:\n![](https://image.xinwei.ltd/81705935533640.png)\n\n### 八、提供一个html文件测试\n随便编写一个html文件：index.html\n```index.html\n\u003Chtml>\n\u003Cbody>\n\u003Ch1>\n测试标题\n\u003C/h1>\n\u003Cdiv>\n测试\n\u003Cdiv>\n\u003C/body>\n\u003C/html>\n```\n![](https://image.xinwei.ltd/91705935578009.png)\n\n### 九、测试网页\n访问成功\n![](https://image.xinwei.ltd/101705935673279.png)\n\n\n\n\n\n\n\n\n\n","docker nginx image site: https://hub.docker.com/_/nginx  本文是在Mac OS上使用docker的桌面版来操作docker，以Mac OS模拟宿主机.  一、在宿主机上自定义目录作为挂载目录 比如在centos上可以使用home目录，大家也可以使用docker拉取centos镜像运行容器，在centos中操作，也可以在实际的云服务器上操作。 ","https://image.xinwei.ltd/11705932893134.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},36,{"id":222,"name":224,"parentId":24},"Docker",[],1626,"docker,docker yml,mac上yml,docker compose,docker compose nginx,nginx",{"id":229,"createdAt":230,"title":231,"content":232,"summary":233,"image":234,"uid":17,"user":235,"categoryId":24,"category":236,"subCategoryId":237,"subCategory":238,"comments":240,"status":8,"reason":22,"notice":22,"visitCount":241,"commentCount":47,"keywords":242},21,"2024-01-26T13:49:03.139Z","如何在nuxt3中上传FormData文件","nuxt3中进行网络请求时，往往使用的是useFetch，当然其他使用方式如$fetch的。\n\n使用useFetch时，有和axios类似的网络interceptor，可以使用Promise在interceptor中自行决定返回某种格式的response，这里不赘述，只简单记一些可能出错的点。\n\n下面是一个简单的封装上传FormData的例子。\n\n```typescript\n// path example：/api/upload\nfunction upload\u003CT>(path: string, file: File) {\n  const formData = new FormData();\n  formData.append('file', file);\n  return useFetch\u003CT>(path, {\n    baseURL: 'https://www.xxxx.com', \n    key: path,\n    method: 'POST',\n    body: FormData,\n  })\n};\n```\n在这个例子中，将file文件挂载在名为“file”的formData中，然后传给useFetch的option中的body。\n\n服务端就可以通过post类型的该上传接口路由从MultipartForm中取出名为file的文件，然后进行上传处理，然后组装返回给客户端的response。\n\n有时候可能遇到的问题：no multipart boundary param in Content-Type\n如下截图：\n![报错截图](https://image.xinwei.ltd/11706276565695.png)\n\n这个问题目前我遇到的就是由于从Vue3的前端项目，重构为nuxt3项目时，老代码中的错误。\n\n错误的起因是在headers中多了一个内容类型Content-Type的声明（因为nuxt3在服务端渲染时，会自动添加含有boundary=的Content-Type）：\n```typescript\nreturn useFetch\u003CT>(path, {\n    baseURL: 'https://www.xxxx.com', \n    key: path,\n    method: 'POST',\n    body: FormData,\n    headers: { // 这里不要加Content-Type\n        \"Content-Type\": \"multiple/form-data\"  \n    }\n  })\n```\n截图为：\n![代码报错的地方截图](https://image.xinwei.ltd/21706276648780.png)\n\n对比一下在headers中添加Content-Type和不添加的效果对比：\n1. 添加Content-Type，如下图红框所示：\n![没有添加content-type的抓包](https://image.xinwei.ltd/31706276697489.png)\n\n2. 不添加Content-Type，nuxt3自动添加的是如下图所示：\n![](https://image.xinwei.ltd/41706276741721.png)\n\n从这2个对比的截图，我们可以看出，nuxt3在使用FormData类型的值给body字段时，会自动在Content-Type中加boundary信息。\n\n\n\n","在nuxt3中该如何使用网络请求API以FormData方式上传文件呢，这里会给大家提供演示的代码，以做参考。","https://image.xinwei.ltd/11706276565695.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},37,{"id":237,"name":239,"parentId":24},"全栈",[],1577,"nuxt,nuxt3,nuxt3 formdata,nuxt上传,nuxt知识,formdata",{"id":244,"createdAt":245,"title":246,"content":247,"summary":248,"image":22,"uid":17,"user":249,"categoryId":24,"category":250,"subCategoryId":251,"subCategory":252,"comments":254,"status":8,"reason":22,"notice":22,"visitCount":255,"commentCount":47,"keywords":256},89,"2024-03-01T09:46:34.56Z","CDN: trunk URL couldn't be downloaded: https://cdn.jsdelivr.net/cocoa/Specs/0/9/9/libevent/2.1.12/libevent.podspec.json","不管是react native还是单独的iOS中，当使用到iOS的cocoapods管理库时，有时候会遇到\"CDN: trunk URL couldn't be downloaded\"的错误，这大概是由于jsdelivr的安全策略导致的网络download fail\n\n错误类似如下：\n![CDN: trunk URL couldn't be downloaded](https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-03-01%20171709286240361.png)\n\n这个时候的解决办法是，修改pod的source，指定从cocoapods的官方仓库进行install。\n\n具体如下：\n```Podfile\nsource 'https://github.com/CocoaPods/Specs.git'\n```\n\n截图如下：\n![source 'https://github.com/CocoaPods/Specs.git'](https://image.xinwei.ltd/%E6%88%AA%E5%B1%8F2024-03-01%20171709286356627.png)\n\n","不管是react native还是单独的iOS中，当使用到iOS的cocoapods管理库时，有时候会遇到\"CDN: trunk URL couldn't be downloaded\"的错误，这大概是由于jsdelivr的安全策略导致的网络download fail。",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},3,{"id":251,"name":253,"parentId":24},"iOS",[],1353,"react native,react native ios,react native库,react native开发",{"id":258,"createdAt":259,"title":260,"content":261,"summary":262,"image":263,"uid":17,"user":264,"categoryId":24,"category":265,"subCategoryId":58,"subCategory":266,"comments":267,"status":8,"reason":22,"notice":22,"visitCount":268,"commentCount":47,"keywords":269},140,"2024-11-27T11:41:32.455Z","前端开发-如何生成并导出xlsx或者csv文件","在前端开发中，尤其是一些管理后台的开发项目中，经常有需要生成并导出表格的场景。最常见的表格格式莫过于xlsx和csv这两种格式的excel文件。\n\n这种场景有两种实践：\n1. 服务端生成了excel表格，将文件uri网络地址通过接口传给前端，前端使用`window.location.href`或者`window.open`等方法使用跳转下载这个uri对应的表格文件。\n2. 前端根据已有的数据，自行生成excel表格，实现自动下载文件。\n\n第1个实践对于前端来说是比较方便的，今天我们介绍第2个实践。介绍的包含2种格式（xlsx、csv）的文件生成和下载，推荐`xlsx`格式的excel。\n\n## 一、xlsx格式excel文件生成和下载\n这种方式通过使用前端库`xlsx`来实现：\n```package.json\n...\n\"dependencies\": {\n  ...\n  \"xlsx\": \"^0.18.5\",\n  ...\n}\n...\n```\n封装代码：\n```xlsxFileHandle.ts\nimport * as XLSX from 'xlsx';\n\nexport interface excelType {\n  json: object;\n  name: string;\n  titleArr: string[];\n  sheetName: string;\n}\n\n/**\n * 导出成xlsx文件\n * @param params 传入的数据格式\n */\nexport const exportExcel = (params: excelType) => {\n    const keyArray = [];\n    let data = [];\n    const getLength = function (obj: object) {\n        let count = 0;\n        for (const i in obj) {\n          if (Object.prototype.hasOwnProperty.call(obj, i)) {\n            count++;\n          }\n        }\n        return count;\n    };\n\t\n    for (const key1 in params.json) {\n      if (Object.prototype.hasOwnProperty.call(params.json, key1)) {\n        const element = (params.json as { [key: string]: object })[key1];\n        const rowDataArray = [];\n        for (const key2 in element) {\n          if (Object.prototype.hasOwnProperty.call(element, key2)) {\n            const element2 = (element as { [key: string]: object })[key2];\n            rowDataArray.push(element2);\n            if (keyArray.length \u003C getLength(element)) {\n              keyArray.push(key2);\n            }\n          }\n        }\n        data.push(rowDataArray);\n      }\n    }\n    data.splice(0, 0, params.titleArr as any);\n    data = data.filter(e => e.length > 0)\n\n    console.log(data);\n    let ws = XLSX.utils.aoa_to_sheet(data);\n    let wb = XLSX.utils.book_new();\n    // 隐藏英文字段表头\n    const wsrows = [{ hidden: false }];\n      /* 设置worksheet每列的最大宽度 */\n      const colWidth = data.filter(e => e.length > 0).map((row) =>\n        row.map((val) => {\n          /* 先判断是否为null/undefined */\n          if (val == null) {\n            return {\n              wch: 20,\n            };\n          } else if (val.toString().charCodeAt(0) > 255) {\n            /* 再判断是否为中文 */\n            return {\n              wch: val.toString().length * 2,\n            };\n          } else {\n            return {\n              wch: val.toString().length*2,\n            };\n          }\n        })\n      );\n      /* 以第一行为初始值 */\n      const result = colWidth[1];\n      for (let i = 1; i \u003C colWidth.length; i++) {\n        for (let j = 0; j \u003C colWidth[i].length; j++) {\n          if (result[j].wch \u003C colWidth[i][j].wch) {\n            result[j].wch = colWidth[i][j].wch;\n          }\n        }\n      }\n      ws['!cols'] = result;\n      ws['!rows'] = wsrows; // ws - worksheet\n      XLSX.utils.book_append_sheet(wb, ws, params.sheetName);\n      /* generate file and send to client */\n      XLSX.writeFile(wb, `${params.name}.xlsx`);\n};\n\n```\n使用实例代码：\n```payExport.ts\nimport { exportExcel } from \"../xlsxFileHandle\"\n\n/**\n * 导出成xlsx文件\n */\nexport const ExportForXlsx = (batch_id: number, valueList: Array\u003CxxxListItem>) => {\n    const titleList = [\n        \"ID\",\n        \"订单ID\",\n        \"姓\",\n        \"名\",\n        \"收款人名字\",\n        \"电话\",\n        \"账号号码\",\n        \"银行名字\",\n        \"账号名字\",\n        \"收款金额\",\n        \"选填，附言\",\n        \"扣款金额\",\n        \"汇率\",\n        \"手续费$\",\n        \"创建时间\",\n        \"完成时间\",\n        \"订单状态\",\n    ]\n    const getStatusDesc = (status: number) => {\n        if (status == 1) {\n            return '待付款'\n        }\n        if (status == 2) {\n            return '失败'\n        }\n        if (status == 3) {\n            return '成功'\n        }\n        if (status == 4) {\n            return '处理中'\n        }\n        if (status == 5) {\n            return '失败-系统'\n        }\n        return '未知'\n    }\n    const newValueList = valueList.map(e => {\n        return {\n            \"ID\" : e.payee_id + '',\n            \"订单ID\": e.order_id + '',\n            \"姓\": e.last_name + '',\n            \"名\": e.first_name + '',\n            \"收款人名字\": e.payee_name + '',\n            \"电话\": e.phone_number + '',\n            \"账号号码\": e.account_id + '',\n            \"银行名字\": e.back_name + '',\n            \"账号名字\": e.bank_account_number + '',\n            \"收款金额\": e.receive_amount + '',\n            \"选填，附言\": e.remark + '',\n            \"扣款金额\": e.buckle_amount + '',\n            \"汇率\": e.exchange_rate + '',\n            \"手续费$\": e.handling_fee + '',\n            \"创建时间\": e.create_time + '',\n            \"完成时间\": (e.complete_time > 0 ? moment(e.complete_time).format('YYYY-MM-DD HH:mm:ss') : e.complete_time) + '',\n            \"订单状态\": getStatusDesc(e.status) + ''\n        }\n    })\n    const tableName = '支付-' + batch_id //表名\n    exportExcel({\n        json: newValueList,//数据\n        name: tableName, // 表名\n        titleArr: titleList,//表头\n        sheetName: batch_id + '',//页签\n    })\n}\n```\n以上方法得到的效果：\n![导出excel](https://image.xinwei.ltd/41732707449352.png)\n\n## 二、csv格式excel文件生成和下载\n不使用任何库，而是将数据合成字符串。\n封装代码：\n```csvFile.ts\n/**\n * 导出成.csv格式的表格\n * @param titleList 比如：['ID', '姓名', '年龄']\n * @param valueList 比如：[ [1, '张三', 28], [2, '李四', 22] ]\n */\nexport const extractCsvFileFromData = (titleList: Array\u003Cstring>, valueList: Array\u003Cany>) => {\n    const totalList = [titleList, ...valueList]\n    // 将数据转换为CSV格式的字符串  \n    let csvContent = \"data:text/csv;charset=utf-8,\";  \n    totalList.forEach((row, index) => {  \n      if (index > 0) {  \n        csvContent += \"\\n\";  \n      }  \n      csvContent += row.join(\",\");  \n    });  \n\n    // 创建一个可下载的链接  \n    const encodedUri = encodeURI(csvContent);  \n    const link = document.createElement(\"a\");  \n    link.setAttribute(\"href\", encodedUri);  \n    link.setAttribute(\"download\", \"数据.csv\");  \n    document.body.appendChild(link); // 需要添加到body  \n    link.click();\n}\n```\n使用示例代码：\n```pay.ts\nimport moment from \"moment\"\n\n/**\n * 将明细列表，生成csv文件\n * @param valueList 明细列表数据\n */\nexport const GenerateCSVFileFrom = (valueList: Array\u003CxxxListItem>) => {\n    const titleList = [\n        \"ID\",\n        \"订单ID\",\n        \"姓\",\n        \"名\",\n        \"收款人名字\",\n        \"电话\",\n        \"账号号码\",\n        \"银行名字\",\n        \"账号名字\",\n        \"收款金额\",\n        \"选填，附言\",\n        \"扣款金额\",\n        \"汇率\",\n        \"手续费$\",\n        \"创建时间\",\n        \"完成时间\",\n        \"订单状态\",\n    ]\n\n    const getStatusDesc = (status: number) => {\n        if (status == 1) {\n            return '待付款'\n        }\n        if (status == 2) {\n            return '失败'\n        }\n        if (status == 3) {\n            return '成功'\n        }\n        if (status == 4) {\n            return '处理中'\n        }\n        if (status == 5) {\n            return '失败-系统'\n        }\n        return '未知'\n    }\n    \n    const newValueList = valueList.map(e => {\n        return [\n            e.payee_id + '', e.order_id + '', e.last_name + '', e.first_name + '',\n            e.payee_name + '', e.phone_number + '', e.account_id + '', e.back_name + '',\n            e.bank_account_number + '', e.receive_amount + '', e.remark + '', e.buckle_amount + '',\n            e.exchange_rate + '', e.handling_fee + '', e.create_time + '', (e.complete_time > 0 ? moment(e.complete_time).utcOffset(-5).format('YYYY-MM-DD HH:mm:ss') : e.complete_time) + '',\n            getStatusDesc(e.status) + ''\n        ]\n    })\n   \n    extractCsvFileFromData(titleList, newValueList)\n}\n```\n\n以上就是在前端开发中，生成和导出xlsx和csv格式的excel文件的简单实践，希望对大家有帮助。\n","今天介绍一下如何在前端开发中，实现生成xlsx或者csv文件的方法和步骤，算是记录一下，供大家参考。","https://image.xinwei.ltd/41732707449352.png",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":58,"name":60,"parentId":24},[],1331,"xlsx,vue xlsx,csv xlsx,vue csv,vue生成csv,vue生成xlsx,导出xlsx,下载excel,下载xlsx,下载csv",{"id":271,"createdAt":272,"title":273,"content":274,"summary":275,"image":22,"uid":17,"user":276,"categoryId":24,"category":277,"subCategoryId":278,"subCategory":279,"comments":281,"status":8,"reason":22,"notice":22,"visitCount":282,"commentCount":47,"keywords":283},126,"2024-10-05T16:48:19.276Z","com.google.android.recaptcha的sdk附注警告","## 一、概要\n当在Android应用中引入了recaptcha的sdk时，如果版本号过低，在提审到Android Google play的时候，会出现sdk的警告。警告中提出要更新sdk，为了应对Google play的审核，这个问题还是需要解决的，防止引起不必要的审核失败问题。\n\n## 二、问题截图\n![recaptcha警告](https://image.xinwei.ltd/img_v3_02f1_9fe17a9c-2f3e-4fd4-b957-e5cd52509e3g1728145995035.JPG)\n\n> A critical security vulnerability was discovered in reCAPTCHA Enterprise for Mobile. The vulnerability has been patched in the latest SDK release. Customers will need to update their Android application with the reCAPTCHA Enterprise for Mobile SDK, version 18.4.0 or above. We strongly recommend you update to the latest version as soon as possible.\n\n## 三、分析\n从截图中分析，这个问题是由recaptcha库的版本出现严重的安全漏洞所致。只要将版本号升至18.4.0及以上即可解决。\n\n那么在有明确引入dependency的gradle中修改版本号，更新一下即可。\n\n```\n// Add the following build rule to the dependencies section of your app-level build.gradle file:\n\nimplementation 'com.google.android.recaptcha:recaptcha:18.4.0'\n\n// Make sure to use reCAPTCHA SDK version 18.4.0 or later.\n```\n\n而我的问题，确实并没有直接引入`recaptcha`，但是我的项目中引入了`firebase-bom`，而`firebase-bom`中就包含了`recaptcha`，所以这种情况，就需要升级`firebase-bom`的版本了。\n\n## 四、解决\n我的项目中`firebase-bom`的版本是\n```gradle\nimplementation platform('com.google.firebase:firebase-bom:32.3.1')\n```\n改为：\n```gradle\nimplementation platform('com.google.firebase:firebase-bom:32.8.0')\n```\n\n以上解决了我的问题，经过上传到google playd的内测轨道，没有`recaptcha`的警告了。\n","当在Android应用中引入了recaptcha的sdk时，如果版本号过低，在提审到Android Google play的时候，会出现sdk的警告。警告中提出要更新sdk，为了应对Google play的审核，这个问题还是需要解决的，防止引起不必要的审核失败问题。",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},10,{"id":278,"name":280,"parentId":24},"Android",[],1318,"recaptcha,google play,android",{"id":285,"createdAt":286,"title":287,"content":288,"summary":289,"image":290,"uid":17,"user":291,"categoryId":24,"category":292,"subCategoryId":251,"subCategory":293,"comments":294,"status":8,"reason":22,"notice":22,"visitCount":295,"commentCount":47,"keywords":296},103,"2024-03-23T05:52:23.185Z","Swift知识一览（二）","## 一、枚举\n### 1.1 定义如下：\n```swift\nenum Season {\n    case spring\n    case summer\n    case autumn\n    case winter\n}\n或者写成\nenum Season {\n    case spring, summer, autumn, winter\n}\n\nvar current = Season.spring\ncurrent = .summer\nprint(current) // summer\n```\n\n\n### 1.2 枚举关联值(associate value)\n就是在枚举的声明中，关联某一个类型，配合switch使用枚举时，就可以从枚举中解析出关联值。\n```swift\nenum Direction {\n    case angle(Float)\n    case name(String)\n}\nvar dir = Direction.angle(45)\ndir = .name(\"东北方向\")\n\n// 从枚举中取关联值\nswitch dir {\n    case .angle(let pt):\n        print(\"当前方向角度：\\(pt)\")\n    case .name(let name):\n        print(\"当前方向是：\\(name)\")\n}\n```\n\n\n### 1.3 枚举的原始值(raw value)\n指的是在定义枚举的时候，可以给enum指定一个类型，然后可以给case中的枚举成员指定一个该类型的默认值，这个默认值就是枚举成员的关联值。\n```swift\nenum Direction : String { // 定义原始值的类型\n    case east = \"东\" // \"东\"就是原始值，依此类推\n    case south = \"南\"\n    case west = \"西\"\n    case north = \"北\"\n}\nvar dir = Direction.east\nprint(dir) // east\nprint(dir.rawValue) // 东\nprint(Direction.east.rawValue) // 东\n\n// 有一种直接创建枚举变量的方式\nlet dir = Direction(rawValue: \"南\")\n// 如果不包括在枚举成员的所有原始值中，那么此时的dir就是一个Direction?可选类型\n// 可能是nil，比如: Direction(rawValue: \"南北\")\n// 可选类型在后面会写到\n```\n\n还有隐式原始值的概念，如下：\n【如果枚举的原始值类型为Int、String，那么Swift会给枚举成员分配原始值。】\n```swift\nenum Direction : String {\n    case east, south, west, north\n}\n// 那么枚举成员的原始值就是对应的成员名称字符串\nprint(Direction.east) // east\nprint(Direction.north.rawValue) // east\n\nenum Direction: Int {\n    case east, south, west, north\n}\nprint(Direction.east.rawValue) // 0，后面成员的原始值依次加1\n\n// 如果给case其中的某几个case赋原始值，那么没赋原始值的枚举成员，\n// 它的原始值是紧靠它的前一个枚举成员原始值加1\n// 比如\nenum Direction: Int {\n    case east = 1, south, west = 8, winter\n}\nprint(Direction.east.rawValue) // 1\nprint(Direction.south.rawValue) // 2\nprint(Direction.west.rawValue) // 8\nprint(Direction.north.rawValue) // 9\n```\n\n### 1.4 枚举的递归(recursive enumeration)\n也就是指枚举的case枚举成员，如果有关联值，且其关联值类型就是当前枚举，这种情况就是递归枚举。\n递归枚举在定义时一定要在enum前加`indirect`修饰，或者在递归的枚举成员case前加`indirect`修饰。\n```swift\nindirect enum Direction {\n    case angle(Float)\n    case twoDirection(Direction, Direction)\n}\n\n// 或者\nenum Direction {\n    case angle(Float)\n    indirect case twoDirection(Direction, Direction)\n}\n```\n\n\n## 二、MemoryLayout获取数据类型占用的内存大小\n如果在Objective-C中，使用的是sizeof来获取类型占用的内存大小，Swift中使用的是MemoryLayout。\n### 2.1 具体使用\n```swift\nMemoryLayout\u003CInt>.size // 64位系统输出8，32位系统输出4\n\n// 针对变量获取内存大小\nlet num = 8;\nMemoryLayout.size(ofValue:num) // 8\n2.2 其他使用\n`MemoryLayout\u003CInt>.stride`和`MemoryLayout\u003CInt>.alignment`\n- stride 是指系统分配的内存大小\n- alignment是内存对齐的字节数\n- size是指在分配的内存中实际占用了多少字节\n// 和上面一样，都可以获取类型或者变量的类型的内存大小\nMemoryLayout\u003CInt>.stride\nMemoryLayout\u003CInt>.alignment\n\nlet num = 8;\nMemoryLayout.stride(ofValue:num)\nMemoryLayout.alignment(ofValue:num)\n```\n\n \n### 2.3 关联值和原始值在enum中的内存存储\n- 关联值存储在枚举变量类型中，所以需要能够存储所有大小的关联值的类型，也就是说，枚举类型的内存大小是所有关联值类型内存大小的和。\n举例如下：\n```swift\nenum Direction {\n    case ange(Int, Int)\n    case name // 这个地方如果没有指定关联类型，那么使用1个字节就可以存储\n}\n// 那么会是以下输出\nprint(MemoryLayout\u003CDirection>.alignment) // 8\nprint(MemoryLayout\u003CDirection>.size) // 17 解释：2 * 8 + 1 = 17\nprint(MemoryLayout\u003CDirection>.stride) // 24 解释：，内存对齐是8，所以取8的倍数24\n```\n\n\n- 原始值，比较特殊，不会占用到枚举类型的内存中去，具体的原始值存储在其他地方。\n```swift\nenum Direction : String {\n    case east, south, west, north\n}\n\nenum Direction: Int {\n    case east = 1, south, west, north\n}\n\n// 上面2种情况，枚举类型占用的内存都是1个字节。\n// 因为只用一个字节就可以表示出不同的枚举。\n// 可以想象使用以下方式，表示上面的默认1的原始值写法，直接返回某个具体的值就可以，没有必要存储原始值的类型。\nenum Direction: Int {\n    case east, south, west, north\n    \n    func rawValue() -> Int {\n        if self == 0 return 1\n        if self == 1 return 2\n        ...\n    }\n}\n```\n\n\n## 三、可选项（optional）\n也就是常说的可选类型，指一个变量的值可以是nil，或者是有确定的值。\n### 3.1 定义\n通过在类型名称后加一个？号，表示这个变量是可选类型\n```swift\nvar name: String? = nil // 等同于 var name: String?，因为可选类型未赋值时，默认就是nil\nname = \"小明\"\n\n// 输出的是Optional包裹的值\nprint(name) // Optional(\"小明\")\n```\n\n\n### 3.2 强制解包 & 可选项绑定\n切记，对值为nil的可选变量强制解包，会runtime error\n\n使用可选项时，需要判断是否为nil，或者复制给临时变量。这种方式叫可选项绑定。\n\n```swift\nvar name: String? = \"小明\"\nprint(name!) // \"小明\" 强制解包出来的就是直接值，不会有Optional包裹\n\nlet name1: String = name!\n\n// 使用可选项时，需要进行判断是否为nil，或者进行赋值\n// 这就叫做可选项绑定\nif name != nil {\n    // do something with name!\n}\n// 或者\nif let sureName = name { // sureName的作用域仅在这个大括号内\n    // do something with sureName\n}\n```\n\n\n这里还有一个概念，叫隐式解包，也就是直接使用感叹号!，例子如下：\n```swift\nvar a: Int! = 1 // 这个叫隐式解包的可选项\n// 这种方式可以给a置为nil，当然在使用之前必须赋值非nil，否则解包crash\na = nil\n\n// 这种有时候用在跟assert的情况\n```\n\n### 3.3 空合运算符??(nil-coalescing operator)\n指的是，用在可选类型变量后面，加一个??，紧跟一个同类型的变量，表示如果??前的可选项的值为nil，那么使用??后面的变量，否则使用??之前的变量。\n```swift\nlet a: String?\nlet b = a ?? \"未知名字\" // 因为b的值为\"未知名字\"，所以b的类型不是可选项类型\nprint(\"\\(a)\") // nil\nprint(\"\\(b)\") // \"未知名字\"\n\nlet a: String?\nlet b: String? = \"1\"\nlet c = a ?? b // 此时的c是Optional(\"1\")\n\nlet num: Int? = 1\nlet num1: Int = 2\nlet result = num ?? num1 // 此时的result是Int，非可选项\n```\n\n小结一下：\n- ??前的变量必须是可选项；\n- 如果??前后的2个变量都是可选类型，不管这2个可选类型是否有值，那么被赋值变量也是可选项；\n- 如果??前的可选项有值，??后的变量为非可选项，那么返回左边变量的时候就会对左边的可选项进行强制解包；\n总之一句话，对于??这样的空合运算符，左侧的被赋值变量的类型，取决于??后面的变量类型。\n\n另外，空合运算符可以联合使用，如下：\n```swift\nlet a: Int? = 1\nlet b: Int? = 2\nlet c = a ?? b ?? 3 // 那么c是Int，非可选项\n// 这种连续使用，左侧的c取决于最右侧的最后那个变量或者值\n```\n\n**特别注意的地方**：\n- 如果从字典中通过key取值，被赋值的变量是可选项（如果没有对应的key，那么返回的是nil）\n- 如果从数组中通过index取值，被赋值的变量是非可选项（如果有越界，程序直接crash）\n```swift\nlet dic = [\"name\": \"小明\", \"age\": 1]\nlet name = dic[\"name\"] // Optional(\"小明\")\nlet gender = dic[\"gender\"] // nil\n\nlet arr = [1, 2, 3]\nlet a = arr[0] // 1\nlet b = arr[3] // crash\n```\n\n\n### 3.4 guard\n和if正好相反，if是true才去执行大括号内的语句，guard是false才去执行紧接着的大括号内语句。\n在guard的else语句中，一定要写退出当前作用域(return、throw error、break、continue等），否则会报错\n```swift\nif condition {\n    // 如果condition为true，进入这个大括号\n}\n\nguard condition else {\n    // 如果condition为false，进入这个大括号\n    return\n}\n// 如果condition为true，会执行下面的代码\n\n\nvar name: String?\n...\nguard let sureName = name else {\n    print(\"name 不存在\")\n    return\n}\nprint(\"name is: \\(name)\")\n```\n\n\n### 3.5 多重可选项\n指大的是使用2个??来声明可选项\n```swift\nlet a: Int? = 1\nlet b: Int?? = a\nlet c: Int?? = 1\n\n// 在这种情况下，a 和 c 是等价的\nprint(b == c) // true\n\nlet a: Int? = nil\nlet b: Int?? = a\nlet c: Int?? = nil // 表示c中存储的是nil，而不是Optional(nil)\n// 在这种情况下，a 和 c 是不等价的\nprint(b == 3) // false\n```\n\n千万留心下面的这种情况：\n```swift\nlet a: Int? = nil\nlet b: Int?? = a\nlet c: Int?? = nil\n\nprint((b ?? 1) ?? 2) // 2，解释：首先b中存储了a这样一个可选项，那么b第一次解包出来的是a，然后再解1次包是nil\nprint((c ?? 1) ?? 2) // 1，解释：首先c中存储了nil，那么第一次解包出来就是nil，所以将1返回。\n```\n\n\n### 3.6 对多重可选项，可以使用断点调试来探究其构成\n断点使用llvm调试命令：`frame variable -R xxx`，xxx是变量\n```xcode-llvm\n(lldb) frame variable -R xxx\n// 或者简写\n(lldb) fr v -R xxx\n```\n![llvm指令.jpg](https://image.xinwei.ltd/17111730313711711173051270.jpg)\n可以看到每一个可选变量，它们的stack frame结构。\n双重可选变量内部是包裹的一个可选变量。\n![llvm指令1.jpg](https://image.xinwei.ltd/17111730698921711173097908.jpg)\n\n\n这是《Swift知识一览》系列的第二篇文章，若感兴趣，请关注后续发表的文章。","这篇文章是《Swift知识一览》的第二篇文章。该篇文章主要介绍Swift的枚举（关联值、原始值、递归枚举）、MemoryLayout、可选项等内容。根据案例来分析枚举、可选项的用法还有注意点。","https://image.xinwei.ltd/17111730313711711173051270.jpg",{"phone":19,"userId":17,"nickName":20,"vipType":8,"avatar":21,"sign":22,"createdAt":23},{"id":24,"name":26},{"id":251,"name":253,"parentId":24},[],1255,"swift,swift develop,swift knowledge,swift知识,swift开发",187]