記錄--uniapp自定義相機 自定義界面拍照錄像閃光燈切換攝像頭

来源:https://www.cnblogs.com/smileZAZ/archive/2022/12/06/16955998.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 因公司業務需要,需要開發水印相機功能,而項目代碼用的uniapp框架,App端只能簡單調用系統的相機,無法自定義界面,在此基礎上,只能開發自定義插件來完成功能(自定義原生插件,即是用原生代碼來編寫組件實現功能,然後供uniapp項目調用) ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

因公司業務需要,需要開發水印相機功能,而項目代碼用的uniapp框架,App端只能簡單調用系統的相機,無法自定義界面,在此基礎上,只能開發自定義插件來完成功能(自定義原生插件,即是用原生代碼來編寫組件實現功能,然後供uniapp項目調用),經過半個月的研究和開發,完成了這款插件,以高度自由的形式提供了開發者相機自定義界面的需求,只需要在相機界面引入
            <!-- 相機原生插件 START -->

            <camera-view

                ref="cameraObj"

                class="camera_view"

                :defaultCamera="currentCamera"

                @receiveRatio="receiveRatio"

                @takePhotoSuccess="takePhotoSuccess"

                @takePhotoFail="takePhotoFail"

                @recordSuccess="recordSuccess"

                @recordFail="recordFail"

                @receiveInfo="onError"

                :style="'width:'+previewWidth+'px;height:'+previewHeight+'px;margin-left:-'+marginLeft+'px'"

                >

            </camera-view>

            <!-- 相機原生插件 END -->

這裡建議寬高設置為全屏,然後在界面上自定義疊加自己的按鈕文字等實現自己的界面功能,然後調用插件提供的api實現物理功能

// 拍照

takePhoto(){

    console.error("開始拍照")

    // 設置水印

    this.$refs.cameraObj.addWaterText({

        "date":this.tempDateStr || "",

        "logo":"·七彩雲·|水印相機",

        "address":(this.showAddress ? this.address:""),

        "time":this.tempTimeStr || "",

        "week":this.weekDay || "",

        "remark":(this.showRemark ? this.remark:"")

    });

    // 調用拍照api

    this.$refs.cameraObj.takePhoto();

},

// 切換閃光燈

    switchFlash(){

        if(this.flashStatus === 0){

            this.flashStatus = 1;

            this.$refs.cameraObj.openFlash();

        }else{

            this.flashStatus = 0;

            this.$refs.cameraObj.closeFlash();

        }

    },

// 切換攝像頭

switchCamera(){

    if(this.currentCamera === "0"){

        this.currentCamera = "1";

        this.$refs.cameraObj.openFront();

    }else{

        this.currentCamera = "0";

        this.$refs.cameraObj.openBack();

    }

},    

原生插件開發文檔

Android / IOS 原生插件都有兩種類型擴展

1、  Module 擴展 非 UI 的特定功能. ( 直白點說就是只註重功能 )

2、 Component 擴展 實現特別功能的 Native 控制項. ( 側重點在界面 )

比如我們想實現一個自定義的原生按鈕,那就得擴展Component,因為需要有界面,而想實現一個提供各種api的插件,比如加減乘除演算法等不需要界面顯示,只有結果數據的,這種就可以用Module

附上鏈接: 前往下載插件和demo實例

一、Android原生插件的實現

首先android類繼承uniapp的特殊類UniComponent

public class LuanQingCamera extends UniComponent<FrameLayout>

在initComponentHostView這個固定方法返回一個組件

@Override

    protected FrameLayout initComponentHostView(Context context) {

        // 我們自定義了一個FrameLayout的組件(為了方便後面擴展水印)

        FrameLayout frameLayout = new FrameLayout(context);

        // 創建一個SurfaceView用來承載攝像頭預覽

        SurfaceView surfaceView = new SurfaceView(context);

        // 添加到佈局中

        frameLayout.addView(surfaceView);

        

        if (mHolder == null) {

            mHolder = surfaceView.getHolder();




            mHolder.addCallback(new SurfaceHolder.Callback() {

                @Override

                public void surfaceCreated(SurfaceHolder holder) {

                    // 檢查許可權 如果許可權滿足就將打開攝像頭,初始化預覽

                    checkPermission();

                }




                @Override

                public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {




                }




                @Override

                public void surfaceDestroyed(SurfaceHolder holder) {




                }

            });

        }




        return frameLayout;

    }

申請許可權,android 6.0起需一些危險許可權要動態申請,因此我們在使用攝像頭前申請

    @UniJSMethod

    public void checkPermission() {

        Context mContent = mUniSDKInstance.getContext();

        if(mContent instanceof Activity){




            // 用於請求許可權的列表

            List<String> permissions = new ArrayList<>();

            // 判斷許可權是否足夠的標識變數

            boolean isEnoughPermission = true;




            // 許可權檢查和判斷模塊 START

            List<PermissionEntity> checkList = new ArrayList<>();

            checkList.add(new PermissionEntity(Manifest.permission.CAMERA,"攝像頭相機許可權"));

            checkList.add(new PermissionEntity(Manifest.permission.RECORD_AUDIO,"錄音錄製許可權"));

            checkList.add(new PermissionEntity(Manifest.permission.WRITE_EXTERNAL_STORAGE,"文件讀寫許可權"));




            for (PermissionEntity p : checkList){

                // 判斷是否有許可權

                boolean isHas = ActivityCompat.checkSelfPermission(mUniSDKInstance.getContext(), p.getPermissionName()) == PackageManager.PERMISSION_GRANTED;

                if (isHas) {

                    // 已經有許可權(可能用戶在設置中開啟了)的話就把配置中的許可權狀態設置為已有許可權

                    SharedData.setParam(mUniSDKInstance.getContext(),p.getPermissionName(),1);

                }




                // 許可權狀態: 0|無許可權  1|有許可權  2|已拒絕

                int status = (int) SharedData.getParam(mUniSDKInstance.getContext(),p.getPermissionName(),0);

                if(status == 0){

                    // 添加到許可權請求列表

                    permissions.add(p.getPermissionName());

                    isEnoughPermission = false;

                }else if(status == 2){

                    isEnoughPermission = false;

                    backData("receiveInfo", 2003 ,"缺少"+p.getDescribe());

                }

            }




            // 如果許可權足夠了直接初始化相機

            if(isEnoughPermission){

                initCameraOption();

                return;

            }

            // 許可權檢查和判斷模塊 START




            if(permissions.size() > 0){

                EsayPermissions.with((Activity) mContent).permission(permissions).request(new OnPermission() {

                    @Override

                    public void hasPermission(List<String> granted, boolean isAll) {

                        if(isAll){

                            initCameraOption();

                        }else{

                            backData("receiveInfo", 2003 ,"缺少攝像頭|錄製錄音|文件讀寫許可權");

                        }

                    }




                    @Override

                    public void noPermission(List<String> denied, boolean quick) {

                        // 把已拒絕的許可權記錄,下次不再彈出許可權申請,因為不這樣做存在會被應用市場拒絕並下架的風險

                        for (String permission : denied){

                            // 用戶拒絕

                            SharedData.setParam(mUniSDKInstance.getContext(),permission,2);

                        }

                        backData("receiveInfo", 2003 ,"未授予攝像頭|錄製錄音|文件讀寫許可權");

                    }

                });

            }

        }

    }

攝像頭開始預覽,顯示可見的內容

// 開始預覽

    @UniJSMethod

    public void startPreview() {

        try {

            if(mCameraCaptureSession != null){

                mCameraCaptureSession.stopRepeating();//停止之前的會話操作,準備切換到預覽畫面

                mCameraCaptureSession.close();//關閉之前的會話

                mCameraCaptureSession = null;

            }



            //創建預覽請求

            mPreviewCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

            // 設置自動對焦模式

            mPreviewCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);




            //設置Surface作為預覽數據的顯示界面

            mPreviewCaptureRequestBuilder.addTarget(mHolder.getSurface());

            //創建相機捕獲會話,第一個參數是捕獲數據的輸出Surface列表,第二個參數是CameraCaptureSession的狀態回調介面,當它創建好後會回調onConfigured方法,第三個參數用來確定Callback在哪個線程執行,為null的話就在當前線程執行

            mCameraDevice.createCaptureSession(Arrays.asList(mHolder.getSurface(),mImageReader.getSurface()),new CameraCaptureSession.StateCallback() {

                @Override

                public void onConfigured(CameraCaptureSession session) {

                    mCameraCaptureSession = session;

                    try {

                        //開始預覽

                        mPreviewCaptureRequest = mPreviewCaptureRequestBuilder.build();

                        UniLogUtils.e("初始化開啟預覽");

                        //設置反覆捕獲數據的請求,這樣預覽界面就會一直有數據顯示

                        mCameraCaptureSession.setRepeatingRequest(mPreviewCaptureRequest, null, null);

                    } catch (CameraAccessException e) {

                        e.printStackTrace();

                    }

                }




                @Override

                public void onConfigureFailed(CameraCaptureSession session) {

                    UniLogUtils.e("預覽失敗");

                }

            }, null);

        } catch (CameraAccessException e) {

            e.printStackTrace();

        }

    }

執行拍照功能

@UniJSMethod

    public void takePhoto() {

        UniLogUtils.e("準備開始拍照");

        if (mCameraDevice == null) return;

        try {

            imageFileName = System.currentTimeMillis() + ".jpg";




            //首先我們創建請求拍照的CaptureRequest

            CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);




            Context context = mUniSDKInstance.getContext();

            if(context instanceof Activity){

                Activity activity = (Activity)mUniSDKInstance.getContext();

                //獲取屏幕方向

                int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();

                //一個 CaptureRequest 除了需要配置很多參數之外,還要求至少配置一個 Surface(任何相機操作的本質都是為了捕獲圖像),

                captureBuilder.addTarget(mImageReader.getSurface());




                // 自動對焦

//                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

//              // 自動曝光開

                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);

\


//                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF);

\


                // 這裡有個坑,設置閃光燈必須先設置曝光

                if(flashState == 0){

                    captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);

                }else{

                    captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE);

                }




//                captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);

//                captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE);

                captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));




                mCameraCaptureSession.stopRepeating();

                CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {

                    @Override

                    public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {

                        super.onCaptureCompleted(session, request, result);

                        UniLogUtils.e("拍照成功:");

                        backData("takePhotoSuccess", 200 ,"ok");

                        startPreview();

                    }




                    @Override

                    public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {

                        super.onCaptureFailed(session, request, failure);

                        UniLogUtils.e("拍照失敗:");

                        backData("takePhotoFail", 2001 ,"拍照操作失敗");

                    }

                };

                UniLogUtils.e("開始拍照");




                mCameraCaptureSession.capture(captureBuilder.build(), captureCallback, null);

            }

        } catch (CameraAccessException e) {

            e.printStackTrace();

        }

    }

二、IOS原生插件的實現

ios端相比較,更為簡單

頭部文件 .h

#import <AVFoundation/AVFoundation.h>

#import "DCUniComponent.h"

NS_ASSUME_NONNULL_BEGIN

@interface LQCamera : DCUniComponent

@end

NS_ASSUME_NONNULL_END

.m文件實現固定函數,並返回一個組件

- (UIView *)loadView {

    NSLog(@"插件日誌:loadView");

    return [UIView new];

}

初始化一些攝像頭參數

- (void)viewDidLoad {

    NSLog(@"插件日誌:viewDidLoad");

    

    self.session = [[AVCaptureSession alloc] init];

    

    //創建一個AVCaptureMovieFileOutput 實例,用於將Quick Time 電影錄製到文件系統

    self.movieOutput = [[AVCaptureMovieFileOutput alloc]init];

    //輸出連接 判斷是否可用,可用則添加到輸出連接中去

    if ([self.session canAddOutput:self.movieOutput])

    {

        [self.session addOutput:self.movieOutput];

    }

    

    //     拿到的圖像的大小可以自行設定

    //    AVCaptureSessionPresetHigh

    //    AVCaptureSessionPreset320x240

    //    AVCaptureSessionPreset352x288

    //    AVCaptureSessionPreset640x480

    //    AVCaptureSessionPreset960x540

    //    AVCaptureSessionPreset1280x720

    //    AVCaptureSessionPreset1920x1080

    //    AVCaptureSessionPreset3840x2160

    self.session.sessionPreset = AVCaptureSessionPreset1920x1080;

    

    //AVCaptureStillImageOutput 實例 從攝像頭捕捉靜態圖片

    self.imageOutput = [[AVCaptureStillImageOutput alloc]init];

    //配置字典:希望捕捉到JPEG格式的圖片

    self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};

    if ([self.session canAddOutput:self.imageOutput]) {

        [self.session addOutput:self.imageOutput];

    }

\


    self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    NSError * error = nil;

    self.input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:&error];

    

    if (self.input) {

        [self.session addInput:self.input];

    }else{

        NSLog(@"Input Error:%@",error);

    }

\


    //預覽層的生成

    self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];

    

    // 直接取用本組件的bounds來做定位,因為本組件的bounds是uniapp傳過來的css寬高設置過的

    self.previewLayer.frame = self.view.bounds; //預覽層填充視圖

\


    // AVLayerVideoGravityResizeAspectFill 等比例填充,直到填充滿整個視圖區域,其中一個維度的部分區域會被裁剪

    // AVLayerVideoGravityResize 非均勻模式。兩個維度完全填充至整個視圖區域

    // AVLayerVideoGravityResizeAspect 等比例填充,直到一個維度到達區域邊界

    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

    [self.view.layer addSublayer:self.previewLayer];

\


    [self.session startRunning];

}

一些固定的標註寫法

/// 前端更新屬性回調方法

/// @param attributes 更新的屬性

- (void)updateAttributes:(NSDictionary *)attributes {

    // 解析屬性

    if (attributes[@"showsTraffic"]) {

//        _showsTraffic = [DCUniConvert BOOL: attributes[@"showsTraffic"]];

    }

}

\


/// 前端註冊的事件會調用此方法

/// @param eventName 事件名稱

- (void)addEvent:(NSString *)eventName {

    if ([eventName isEqualToString:@"mapLoaded"]) {

        

    }

}

\


/// 對應的移除事件回調方法

/// @param eventName 事件名稱

- (void)removeEvent:(NSString *)eventName {

    if ([eventName isEqualToString:@"mapLoaded"]) {

        

    }

}

ios端回調原生方法

// 返回給前端的信息回調

// 向前端發送事件,params 為傳給前端的數據 註:數據最外層為 NSDictionary 格式,需要以 "detail" 作為 key 值

- (void) returnFunc:(NSString *) func returnCode:(NSNumber *)code returnMess:(NSString *) message{

    NSString *imgUrl = self.imagePath ? self.imagePath : @"";

    NSString *vioUrl = self.videoPath ? self.videoPath : @"";

\


    [self fireEvent:func params:@{@"detail":@{@"code":code,@"message":message,@"videoPath":vioUrl,@"imagePath":imgUrl}} domChanges:nil];

}

拍照、錄像[開始、停止]、閃光燈切換、攝像頭鏡頭切換、設置水印內容等功能介面

// 下列為暴露出來的方法列表 START

// 通過 WX_EXPORT_METHOD 將方法暴露給前端

UNI_EXPORT_METHOD(@selector(openFlash))

// 開啟閃光燈

- (void)openFlash {

    [self setFlashMode:AVCaptureFlashModeOn];

}

\


UNI_EXPORT_METHOD(@selector(closeFlash))

// 關閉閃光燈

- (void)closeFlash {

    [self setFlashMode:AVCaptureFlashModeOff];

}

\


UNI_EXPORT_METHOD(@selector(autoFlash))

// 自動閃光燈

- (void)autoFlash {

    [self setFlashMode:AVCaptureFlashModeAuto];

}

\


UNI_EXPORT_METHOD(@selector(openFront))

// 切換前置攝像頭

- (void)openFront {

    [self switchCamer:AVCaptureDevicePositionFront];

}

\


UNI_EXPORT_METHOD(@selector(openBack))

// 切換後置攝像頭

- (void)openBack {

    [self switchCamer:AVCaptureDevicePositionBack];

}

\


// 通過 WX_EXPORT_METHOD 將方法暴露給前端

UNI_EXPORT_METHOD(@selector(takePhoto:))

// 拍照

- (void)takePhoto:(NSDictionary *)options {

    // options 為前端傳遞的參數

    NSLog(@"IOS收到開始拍照請求");

    

    //獲取連接

    AVCaptureConnection *connection = [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];

\


    //程式只支持縱向,但是如果用戶橫向拍照時,需要調整結果照片的方向

    //判斷是否支持設置視頻方向

    if (connection.isVideoOrientationSupported) {

        

        //獲取方向值

        connection.videoOrientation = [self currentVideoOrientation];

    }

\


    //定義一個handler 塊,會返回1個圖片的NSData數據

    id handler = ^(CMSampleBufferRef sampleBuffer,NSError *error)

                {

                    if (sampleBuffer != NULL) {

                        NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];

                        UIImage *image = [[UIImage alloc]initWithData:imageData];

                        

                        [self returnFunc:@"takePhotoSuccess" returnCode:@200 returnMess:@"拍照成功"];

                        //重點:捕捉圖片成功後,將圖片傳遞出去

                        [self saveImage:image];

                    }else

                    {

                        NSLog(@"保存出錯NULL sampleBuffer:%@",[error localizedDescription]);

                    }

                };

    

    //捕捉靜態圖片

    [self.imageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:handler];

}




UNI_EXPORT_METHOD(@selector(addWaterText:))

// 添加水印

- (void)addWaterText:(NSDictionary *)options{

    NSLog(@"接收到水印內容:%@",options);

    

    if(options[@"time"]){

        self.timeStr = options[@"time"];

    }

    if(options[@"date"]){

        self.dateStr = options[@"date"];

    }

    if(options[@"week"]){

        self.weekStr = options[@"week"];

    }

    if(options[@"address"]){

        self.addressStr = options[@"address"];

    }

    if(options[@"remark"]){

        self.remarkStr = options[@"remark"];

    }

    if(options[@"logo"]){

        self.logoStr = options[@"logo"];

    }

}

\


// 停止錄製

UNI_EXPORT_METHOD(@selector(stopRecord))

- (void)stopRecord {

    NSLog(@"停止錄像");

    [self.movieOutput stopRecording];

}




// 開始錄製

UNI_EXPORT_METHOD(@selector(startRecord))

- (void)startRecord {

        NSLog(@"開始錄像");




        // 獲取當前視頻捕捉連接信息,用於捕捉視頻數據配置一些核心屬性

        AVCaptureConnection * videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];

        

        //判斷是否支持設置videoOrientation 屬性。

        if([videoConnection isVideoOrientationSupported])

        {

            //支持則修改當前視頻的方向

            videoConnection.videoOrientation = [self currentVideoOrientation];

            

        }

        

        //判斷是否支持視頻穩定 可以顯著提高視頻的質量。只會在錄製視頻文件涉及

        if([videoConnection isVideoStabilizationSupported])

        {

            videoConnection.enablesVideoStabilizationWhenAvailable = YES;

        }

        

        

        AVCaptureDevice *device = self.input.device;

        

        //攝像頭可以進行平滑對焦模式操作。即減慢攝像頭鏡頭對焦速度。當用戶移動拍攝時攝像頭會嘗試快速自動對焦。

        if (device.isSmoothAutoFocusEnabled) {

            NSError *error;

            if ([device lockForConfiguration:&error]) {

                

                device.smoothAutoFocusEnabled = YES;

                [device unlockForConfiguration];

            }else

            {

//                [self.delegate deviceConfigurationFailedWithError:error];

            }

        }

        

        //查找寫入捕捉視頻的唯一文件系統URL.

//        self.outputURL = [self uniqueURL];

        NSLog(@"開始錄像2");




        //在捕捉輸出上調用方法 參數1:錄製保存路徑  參數2:代理

        [self.movieOutput startRecordingToOutputFileURL:[self outPutFileURL] recordingDelegate:self];

}

// 下列為暴露出來的方法列表 END

到此一款包含Android+IOS兩端的Uniapp原生插件完成

附上鏈接: 前往下載插件和demo實例

效果圖:

在這裡插入圖片描述

https://juejin.cn/post/7107058762673815566

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • sysbench是一個開源的、基於LuaJIT(LuaJIT 是 Lua 的即時編譯器,可將代碼直接翻譯成機器碼,性能比原生 lua 要高) 的、可自定義腳本的多線程基準測試工具,也是目前用得最多的 MySQL 性能壓測工具。 基於 sysbench,我們可以對比 MySQL 在不同版本、不同硬體配 ...
  • 緩存管理器CacheManager 一、背景 ​ 代碼併發量因建行活動頁上升,大量請求打到Mongo導致資料庫cpu100%從而服務不可用,目前解決方案,使用編程式緩存,即對緩存的操作與業務代碼耦合。目前基本上可以解決併發問題。此次提出CacheManager主要是優化代碼。使用聲明式,即註解的方式 ...
  • 摘要:糟糕,資料庫異常不可用怎麼辦?挺著急的,線上等。 本文分享自華為雲社區《糟糕,資料庫異常不可用怎麼辦?》,作者:GaussDB 資料庫。 隨著數字化轉型的加速,數據量爆髮式增長,用戶對資料庫運維能力要求更高,實現對資料庫的高效智能管理,尤其是業務異常時,資料庫運維平臺能自動定位故障並修複,或者 ...
  • 作者:李浩 責編:宇亭 當我們選擇一款 HTAP 資料庫時,總是先被其相關文檔里所描述的優異性能所吸引。卓越的性能是我們選擇一款產品的出發點,因為我們希望該款產品能夠解決我們業務中的痛點。而大家使用 HTAP 產品的出發點就是希望該款資料庫能夠解決我們在事務處理過程中的實時分析痛點。不過,性能優勢只 ...
  • China Taiwan 中國臺灣 2022 年 11 月 1 日,BSMI和經濟部發佈了針對 18 種音像產品的修訂法定檢驗要求。 自發佈之日起,CNS 15598-1:2020 Audio/video, information and communication technology equip ...
  • 本次我把CSS中的重難點整理出來,總共54個核心知識點,供大家複習,希望能幫到大家。這些重難點是進階高薪必需要掌握的知識點,同時也是面試必問的內容。 ...
  • 以前真機調試手機頁面,都是使用數據線連接手機和電腦,近日身邊沒有USB數據線,折騰了下如何不依賴數據線只用無線調試手機頁面,教程如下。 本教程適用於安卓11以及以上版本。否則應該使用USB數據線連接。 一、安裝adb工具 下載地址:https://developer.android.com/stud ...
  • 前面的文章分享了組件庫的開發、example、組件庫文檔,本文分享組件庫 cli 開發。 1 為什麼要開發組件庫 cli 回顧一個新組件的完整開發步驟: 1 在 packages 目錄下創建組件目錄 xxx: 1.1 使用 pnpm 初始化 package.json,修改 name 屬性; 1.2 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...