Coba pikir deh, hampir semua aplikasi desktop pasti butuh akses file. Pengguna pengen buka dokumen, ngedit isinya, terus nyimpen lagi. Nah, di Electron, semua itu dikerjain lewat dialog sistem bawaan — kotak "Open" dan "Save" yang pasti udah nggak asing lagi buat kamu. Kali ini kita bakal ngulik cara buka, baca, dan simpan file dengan cara yang aman dan bener.
Soalnya alurnya bakal ngelibatin dua proses, jadi kamu bakal sering pakai jembatan yang udah kita bahas di IPC di Electron. Makanya, mending kamu paham dulu konsep itu sebelum lanjut.
Kenapa akses file lewat main process
Electron itu punya dua dunia yang kepisah. Ada main process yang jalanin Node.js secara penuh, dan ada renderer process yang jalanin halaman web (HTML, CSS, JavaScript). Modul Node kayak fs dan dialog cuma ada di main process.
Renderer memang sengaja dibatasin. Kalau contextIsolation aktif (dan ini udah default di Electron modern), kode di halaman nggak bisa langsung manggil require("fs"). Alasannya soal keamanan. Bayangin aja kalau halaman web yang muat konten dari internet bisa baca atau bahkan hapus file di komputer pengguna. Serem, kan? Mimpi buruk banget itu.
Makanya, jangan sekali-kali nyalain nodeIntegration: true cuma biar renderer bisa akses file. Itu jalan pintas yang malah ngebuka lubang gede. Cara yang bener gini: UI yang micu permintaan lewat IPC, terus main process yang ngerjain tugas file-nya dan ngirim hasilnya balik.
Membuka file dengan dialog (showOpenDialog)
Buat nampilin kotak pilih file, kita pakai dialog.showOpenDialog. Fungsi ini cuma jalan di main process ya. Dia bakal ngembaliin sebuah Promise yang isinya objek hasil.
// main.js
const { dialog } = require("electron");
async function pilihFile() {
const hasil = await dialog.showOpenDialog({
title: "Pilih file teks",
properties: ["openFile"],
filters: [
{ name: "Teks", extensions: ["txt", "md"] },
{ name: "Semua File", extensions: ["*"] },
],
});
// hasil.canceled = true kalau pengguna menutup dialog
// hasil.filePaths = array berisi path yang dipilih
return hasil;
}
Beberapa opsi yang sering kepakai:
properties: misalnya["openFile"]buat satu file, atau tambahin"multiSelections"biar bisa pilih banyak sekaligus. Pakai"openDirectory"kalau yang kamu butuhin itu folder.filters: buat batasin jenis file yang muncul, biar pengguna nggak salah pilih.title: judul jendela dialognya.
Yang penting kamu inget nih: hasilnya itu bukan isi file, tapi path ke file tersebut. Baca isinya itu langkah yang beda lagi.
Membaca isi file (fs/promises)
Nah, setelah dapet path-nya, baru deh kita baca isinya. Pakai modul fs/promises bawaan Node. Versi yang ini ngembaliin Promise, jadi enak dipaduin sama async/await dan gampang juga nangani error-nya.
// main.js
const fs = require("fs/promises");
async function bacaFile(filePath) {
const isi = await fs.readFile(filePath, "utf-8");
return isi;
}
Argumen kedua "utf-8" bikin hasilnya jadi string biasa. Tanpa itu, kamu bakal dapet Buffer (data mentah), yang sebenernya berguna buat file biner kayak gambar, tapi ya nggak nyaman kalau buat teks.
Yuk kita gabungin proses buka dan baca jadi satu fungsi yang rapi:
// main.js
async function bukaDanBaca() {
const hasil = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Teks", extensions: ["txt", "md"] }],
});
if (hasil.canceled || hasil.filePaths.length === 0) {
return null; // pengguna batal memilih
}
const filePath = hasil.filePaths[0];
const isi = await fs.readFile(filePath, "utf-8");
return { filePath, isi };
}
Perhatiin pengecekan hasil.canceled ya. Ini penting banget biar aplikasi nggak crash pas pengguna mencet tombol Batal.
Menyimpan file (showSaveDialog + writeFile)
Nyimpen file itu kebalikannya. Kita tampilin dialog "Save As" pakai dialog.showSaveDialog, dapetin path tujuannya, terus tulis isinya pakai fs.writeFile.
// main.js
async function simpanFile(teks) {
const hasil = await dialog.showSaveDialog({
title: "Simpan sebagai",
defaultPath: "catatan.txt",
filters: [{ name: "Teks", extensions: ["txt"] }],
});
if (hasil.canceled || !hasil.filePath) {
return false; // pengguna batal menyimpan
}
await fs.writeFile(hasil.filePath, teks, "utf-8");
return true;
}
Bedanya sama showOpenDialog: di sini hasilnya hasil.filePath (tunggal, bukan array), soalnya kan nyimpen cuma ke satu lokasi. Opsi defaultPath gunanya ngisi nama file awal biar pengguna nggak mulai dari kosongan.
Fungsi fs.writeFile bakal nimpa file kalau filenya udah ada. Tapi kalau kamu pengen nambahin di akhir file tanpa ngehapus isi lama, pakai fs.appendFile aja.
Menghubungkan ke UI lewat IPC (alur lengkap)
Sekarang bagian intinya nih: nyambungin tombol di UI sama logika file di main process. Karena renderer nggak boleh akses fs langsung, kita pakai IPC sebagai jembatannya. Alurnya kira-kira begini: tombol diklik → renderer manggil ipcRenderer.invoke → main nangani lewat ipcMain.handle → hasilnya dikirim balik ke renderer.
Pertama-tama, siapin dulu jembatannya di preload script. File ini jalan di lingkungan khusus yang punya akses terbatas dan aman ke Node, terus dia ngekspos fungsi-fungsi tertentu ke renderer.
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("fileAPI", {
buka: () => ipcRenderer.invoke("file:buka"),
simpan: (teks) => ipcRenderer.invoke("file:simpan", teks),
});
Terus di main process, daftarin handler yang bakal jalanin logika file tadi:
// main.js
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const fs = require("fs/promises");
const path = require("path");
function buatJendela() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
win.loadFile("index.html");
}
ipcMain.handle("file:buka", async () => {
const hasil = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Teks", extensions: ["txt", "md"] }],
});
if (hasil.canceled) return null;
const isi = await fs.readFile(hasil.filePaths[0], "utf-8");
return isi;
});
ipcMain.handle("file:simpan", async (event, teks) => {
const hasil = await dialog.showSaveDialog({ defaultPath: "catatan.txt" });
if (hasil.canceled) return false;
await fs.writeFile(hasil.filePath, teks, "utf-8");
return true;
});
app.whenReady().then(buatJendela);
Terakhir, tinggal panggil dari renderer lewat objek fileAPI yang tadi udah kita ekspos:
// renderer.js
const tombolBuka = document.getElementById("buka");
const tombolSimpan = document.getElementById("simpan");
const area = document.getElementById("isi");
tombolBuka.addEventListener("click", async () => {
const isi = await window.fileAPI.buka();
if (isi !== null) {
area.value = isi;
}
});
tombolSimpan.addEventListener("click", async () => {
const berhasil = await window.fileAPI.simpan(area.value);
if (berhasil) {
alert("File berhasil disimpan.");
}
});
Coba perhatiin, rapi banget kan pembagian tugasnya. Renderer cuma ngurusin tampilan sama klik-klik doang. Main process yang nyentuh disk. Nggak ada akses fs yang bocor ke halaman, jadi keamanan tetep kejaga.
Tips: tangani error & batal pilih, perhatikan path
Ada beberapa hal kecil yang bisa bikin aplikasimu jauh lebih kokoh:
- Selalu cek
canceled. Pengguna tuh sering banget buka dialog terus nutupnya lagi gitu aja. Tanpa pengecekan ini, kode bakal nyoba baca path yang nggak ada dan ujung-ujungnya crash. - Bungkus operasi file pakai
try/catch. File bisa aja kekunci, nggak ada izin akses, atau malah kehapus di tengah jalan. Tangkep error-nya dan balikin pesan yang ramah ke UI.
ipcMain.handle("file:buka", async () => {
try {
const hasil = await dialog.showOpenDialog({ properties: ["openFile"] });
if (hasil.canceled) return { ok: true, isi: null };
const isi = await fs.readFile(hasil.filePaths[0], "utf-8");
return { ok: true, isi };
} catch (err) {
return { ok: false, pesan: err.message };
}
});
- Hati-hati sama path. Jangan nyusun path dengan cara nyambungin string biasa, apalagi kalau datangnya dari input pengguna. Pakai
path.joinbiar pemisah foldernya bener, baik di Windows maupun di Linux dan macOS. - Jangan percaya path dari renderer. Kalau renderer ngirim path, validasi dulu di main process. Baca file sembarangan cuma gara-gara perintah dari halaman itu bisa bahaya lho.
Kesimpulan
Nah, sekarang kamu udah lihat alur lengkap akses file di Electron: tombol di UI micu permintaan lewat IPC, main process buka dialog sistem pakai showOpenDialog atau showSaveDialog, terus baca atau nulis isinya pakai fs/promises, dan hasilnya dibalikin ke renderer. Kuncinya satu: semua sentuhan ke disk itu kejadiannya di main process, sementara renderer tetep bersih dan aman.
Dialog itu pas banget buat file yang dipilih sendiri sama pengguna. Tapi gimana caranya nyimpen data aplikasi, kayak pengaturan atau riwayat, tanpa harus repot nanya lokasi tiap kali? Nah, itu kebutuhan yang beda lagi. Lanjut aja ke menyimpan data lokal di Electron buat belajar nyimpen data otomatis di folder aplikasi. Oh iya, semua tutorial ini bagian dari seri Belajar Electron kalau kamu mau ngulang lagi dari awal.