【Electron】酷家楽クライアント開発実践共有—ダウンロードマネージャ


作者:钟离,酷家楽PCクライアント担当者原文アドレス:https://webfe.kujiale.com/electron-ku-jia-le-ke-hu-duan-kai-fa-shi-jian-fen-xiang-jin-cheng-tong-xin/酷家楽クライアント:ダウンロードアドレスhttps://www.kujiale.com/activity/136文章の背景:クールな家楽クライアントがV 12の改版に成功した後、私たちは多くの貴重な経験とベストプラクティスを蓄積しました.フロントエンドコミュニティではElectronに関する知識が比較的少ないので,これらの内容をシリーズ記事の形で共有したい.シリーズ記事:
  • 【Electron】酷家楽クライアント開発実践分かち合い-ピット編
  • 【Electron】酷家楽クライアント開発実践共有-ソフトウェア自動更新
  • 【Electron】酷家楽クライアント開発実践共有-ブラウザ起動クライアント
  • 【Electron】酷家楽クライアント開発実践共有—プロセス通信
  • 【Electron】酷家楽クライアント開発実践共有-ダウンロードマネージャ
  • 不定期更新...

  • 背景
    クールファミリークライアントを開くと、左下のメニューからダウンロード管理という機能を見つけることができます.今日はElectronでダウンロードマネージャを実現する方法を見てみましょう.
    ダウンロード動作をトリガーする方法
    Electronレンダリングレイヤはchromiumに基づいているため、ダウンロードをトリガするロジックとchromiumは一致しており、ページ内のaラベルやjsジャンプなどの動作は、アクセスするリソースに応じてダウンロードをトリガする可能性があります.どのようなリソースがブラウザのダウンロード動作をトリガーしますか?
  • response headerのContent-Dispositionはattachmentです.参照MDN Content-Disposition
  • response headerのContent-Typeは、ブラウザが直接開くことができないファイルタイプ、例えばapplication/octet-streamであり、この場合、ブラウザの具体的な実装に依存する.例:IEはpdfファイルを開くことができず、chromeは直接pdfファイルを開くことができるので、pdfタイプのurlはchrome上で直接開くことができ、IEの下でダウンロード動作をトリガーします.

  • Electronではダウンロードをトリガーする方法もあります:webContents.download.chromiumの下位レベルのダウンロードロジックを直接呼び出すことに相当し、headersの判断を無視し、直接ダウンロードする.
    上記の2つのダウンロード動作は、セッションのwill-downloadイベントをトリガーし、ここで重要なdownloadItemオブジェクトを取得できます.
    全体の流れ
    ファイルパスの設定
    何の処理もしないと、ダウンロード動作がトリガーされるとElectronはシステムdialogをポップアップし、ファイルが格納されているディレクトリを選択させます.この体験はよくないので、まずこのシステムdialogを削除する必要があります.downloadItem.savePathを使えばいいです.
    // Set the save path, making Electron not to prompt a save dialog.
    downloadItem.setSavePath('/tmp/save.pdf');

    ファイルのデフォルトのダウンロードパスを設定するには、ファイル名が重複する場合を考慮する必要があります.一般的には、testなどのファイル名の自己増加論理が使用されます.jpg、test.jpg(1)というフォーマットです.ファイルのデフォルト保存ディレクトリも問題であり、app.getPath('downloads')をファイルダウンロードディレクトリとして統一して使用しています.ユーザー体験のために、ファイルのダウンロードディレクトリを変更する機能を提供すればいい.
    // in main.js     
    const { session } = require('electron');
    session.defaultSession.on('will-download', async (event, item) => {
        const fileName = item.getFilename();
        const url = item.getURL();
        const startTime = item.getStartTime();
        const initialState = item.getState();
        const downloadPath = app.getPath('downloads');
    
        let fileNum = 0;
        let savePath = path.join(downloadPath, fileName);
    
        // savePath    
        const ext = path.extname(savePath);
        const name = path.basename(savePath, ext);
        const dir = path.dirname(savePath);
    
        //        
        while (fs.pathExistsSync(savePath)) {
          fileNum += 1;
          savePath = path.format({
            dir,
            ext,
            name: `${name}(${fileNum})`,
          });
        }
    
        //       ,    dialog   
        item.setSavePath(savePath);
        
         //       ,         
        win.webContents.send('new-download-item', {
          savePath,
          url,
          startTime,
          state: initialState,
          paused: item.isPaused(),
          totalBytes: item.getTotalBytes(),
          receivedBytes: item.getReceivedBytes(),
        });
    
        //       
        item.on('updated', (e, state) => { // eslint-disable-line
          win.webContents.send('download-item-updated', {
            startTime,
            state,
            totalBytes: item.getTotalBytes(),
            receivedBytes: item.getReceivedBytes(),
            paused: item.isPaused(),
          });
        });
    
        //       
        item.on('done', (e, state) => { // eslint-disable-line
          win.webContents.send('download-item-done', {
            startTime,
            state,
          });
        });
      });

    ダウンロード動作がトリガーされると、ファイルはDownloadsディレクトリにダウンロードされ、ファイル名には自己増加ロジックがあります.同時に、ダウンロードウィンドウにキーイベントが送信され、ダウンロードウィンドウはこれらのイベントとデータに基づいてダウンロードタスクを作成、更新することができます.
    上記の手順では、レンダリングプロセスでremote実装を使用すると問題があり、リアルタイムのダウンロードデータを取得できません.したがって、メインプロセスで実装することを推奨します.
    レコードのダウンロード
    ダウンロード機能では、ダウンロード履歴をローカルにキャッシュする必要があり、ダウンロード履歴のデータが多いため、nedbをローカルデータベースとして使用します.
    //     nedb    
    const db = nedbStore({ filename, autoload: true });
    
    ipcRenderer.on('new-download-item', (e, item) => {
        //           
        db.insert(item);
        
        // UI         
        this.addItem(item);
    })
    
    //            
    ipcRenderer.on('download-item-updated', (e, item) => {
        this.updateItem(item)
    })
    
    
    //     ,    
    ipcRenderer.on('download-item-done', (e, item) => {
        //      
        db.update(item);
        
        //   UI       
        this.updateItem(item);
    });
    

    ローカル・データベースのデータは次のようになります.
    {"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/      -      -   .jpg","startTime":1560415098.731598,"state":"completed","totalBytes":236568,"url":"https://qhtbdoss.kujiale.com/fpimgnew/prod/3FO4EGX11S9S/op/LUBAVDQKN4BE6AABAAAAACY8.jpg?kpm=9V8.32a74ad82d44e7d0.3dba44f.1560415094020","_id":"6AorFZvpI0N8Yzw9"}
    {"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/Kujiale-12.0.2-stable(1).dmg","startTime":1560415129.488072,"state":"progressing","totalBytes":80762523,"url":"https://qhstaticssl.kujiale.com/download/kjl-software12/Kujiale-12.0.2-stable.dmg?timestamp=1560415129351","_id":"YAeWIy2xoeWTw0Ht"}
    {"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/      -      -   (1).jpg","startTime":1560418413.240669,"state":"progressing","totalBytes":236568,"url":"https://qhtbdoss.kujiale.com/fpimgnew/prod/3FO4EGX11S9S/op/LUBBLFYKN4BE6AABAAAAADY8.jpg?kpm=9V8.32a74ad82d44e7d0.3dba44f.1560418409875","_id":"obFLotKillhzTw09"}
    {"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/      -      -   (1).jpg","startTime":1560418413.240669,"state":"completed","totalBytes":236568,"url":"https://qhtbdoss.kujiale.com/fpimgnew/prod/3FO4EGX11S9S/op/LUBBLFYKN4BE6AABAAAAADY8.jpg?kpm=9V8.32a74ad82d44e7d0.3dba44f.1560418409875","_id":"obFLotKillhzTw09"}
    

    レンダリングプロセスが初期化されると、ダウンロードレコードを読み込む必要があり、データはダウンロード時間によって逆順序になります.読み取り数を制限する必要があります.そうしないと、パフォーマンスに影響し、一時的に50個を制限します.
    //      
    const db = nedbStore({ filename, autoload: true });
    
    //       
    const downloadHistory = await db.cfind({}).sort({
      startTime: -1,
    }).limit(50).exec()
      .catch(err => logger.error(err));
    if (downloadHistory) {
      this.setList(downloadHistory.map((d) => {
        const item = d;
        //      ,              
        if (item.state !== 'completed') { 
          item.state = 'cancelled';
        }
        return item;
      }));
    }

    ダウンロードディレクトリのカスタマイズ
    デフォルトのダウンロードディレクトリElectronでは、デフォルトでは本体上のDownloadsディレクトリであり、ユーザーがダウンロードディレクトリを設定する機能を提供し、ユーザーがカスタマイズしたダウンロードディレクトリをローカルでキャッシュする必要がある.この基礎構成はelectron-storeを用いて実現する
    // in config.json
    {
        "downloadsPath": "/Users/ww/Downloads/  "
    }

    ウィンドウの初期化時に、キャッシュにカスタムダウンロードディレクトリがあるかどうかを確認し、ある場合はappのデフォルトダウンロードディレクトリを変更します.
    componentDidMount() {
        const downloadsPath = store.get('downloadsPath');
        if (downloadsPath) {
            app.setPath('downloads', downloadsPath);
            // app.getPath('downloads'); -> /Users/ww/Downloads/  
        }
    }

    ユーザーがダウンロードディレクトリを変更するには、次の手順に従います.
  • ポップアップファイルディレクトリ選択dialog、dialog.showOpenDialogを使用して
  • を実現する.
  • ローカルキャッシュ内のカスタムダウンロードディレクトリ
  • を更新する.
  • 現在のappのデフォルトダウンロードディレクトリ
  • を変更
  • ダウンロードウィンドウのダウンロードディレクトリ文案を更新
  • //              
    changeDoiwnloadHandler = () => {
        const paths = dialog.showOpenDialog({
          title: '        ',
          properties: ['openDirectory'],
        });
        if (paths && paths.length) {
          //          
          store.set('downloadsPath', paths[0]);
          
          //          
          app.setPath('downloads', paths[0]);
          
          //         
          this.updateDownloadsPath();
        }
    }

    ダウンロードの進行状況の計算
    downloadItemを取得すると、ダウンロードしたバイト数とファイルの合計バイト数を取得してダウンロードの進捗を計算できます.
    const percent = item.getReceivedBytes() / item.getTotalBytes();

    操作ファイル
    ダウンロード管理ウィンドウで、ダウンロードタスクをダブルクリックしてファイルを開き、表示ボタンをクリックしてファイルがあるディレクトリを開きます.Electronのshellモジュールを統合して実装した.
    openFile = (path) => {
        if (!fs.pathExistsSync) return; //         
        shell.openItem(path); //     
    } 
    
    openFileFolder = async (path) => {
        if (!fs.pathExistsSync(path)) { //      
          return;
        }
        shell.showItemInFolder(path); //          
    }

    ファイル関連アイコンの取得
    ダウンロード管理ウィンドウをよく見ると、ファイルのアイコンはシステムから取得され、ファイルマネージャで見たファイルのアイコンと一致しています.
    上図ではdmg,jpgファイルともにシステム関連のファイルアイコンが示されており,ユーザ体験がよい.getFileIconを使用してシステムアイコンを取得できます.以下は実装コードです.
    const { app } = require('electron').remote;
    
    //       
    const getFileIcon = (path) => {
      return new Promise((resolve) => {
        const defaultIcon = 'some-default.jpg';
        if (!path) return resolve(defaultIcon);
        return app.getFileIcon(path, (err, nativeImage) => {
          if (err) {
            return resolve(defaultIcon);
          }
          return resolve(nativeImage.toDataURL()); //   base64    
        });
      });
    };
    
    //     
    const imgSrc = await getFileIcon('./test.jpg');

    最後に
    コメントコーナーでの議論、技術交流&プッシュを歓迎します->[email protected]