[ElectronJS] Bài 2 – Process Model & Communication

Ok… Sau khi quay trở lại Sub-Series NodeJS của Series Web để chuẩn bị thêm một chút kiến thức về Event & Process thì chúng ta đã có thể tiếp tục tìm hiểu về ElectronJS ở đây. Trong bài viết này, chúng ta sẽ tìm hiểu về mô hình quản lý process của framework này

Ok… Sau khi quay trở lại Sub-Series NodeJS của Series Web để chuẩn bị thêm một chút kiến thức về Event & Process thì chúng ta đã có thể tiếp tục tìm hiểu về ElectronJS ở đây. Trong bài viết này, chúng ta sẽ tìm hiểu về mô hình quản lý process của framework này và công cụ để thực hiện giao tiếp giữa các process.

Mục tiêu xây dựng

Trước khi bắt đầu tìm hiểu chi tiết về ElectronJS thì chúng ta cần xác định một mục tiêu cụ thể đã. Một framework hỗ trợ xây dựng các ứng dụng native thì hiển nhiên sẽ có rất rất nhiều thứ để tìm hiểu. Tuy nhiên chúng ta chắc chắn cũng không thể kể hết tất cả các chi tiết về giao diện lập trình được cung cấp trong tài liệu của ElectronJS được.

Khi nghĩ đến việc xây dựng một website, thiết kế ứng dụng đơn giản nhất hiển nhiên là một trang blog cá nhân, bởi đó là ứng dụng online phổ biến nhất mà mọi người đều sử dụng. Còn đối với một ứng dụng native, mình nghĩ nền tảng chung nhất là một ứng dụng đơn giản có thể chỉnh sửa nội dung của một tệp văn bản thuần ví dụ như Notepad của Windows. Bởi vì bất kỳ phần mềm nào khác phức tạp hơn, cũng đều sẽ phải thực hiện những chức năng cơ sở như:

  • Mở cửa sổ duyệt các thư mục và tệp dữ liệu.
  • Đọc nội dung của một tệp dữ liệu cần xử lý để nạp vào môi trường của ứng dụng.
  • Chỉnh sửa nội dung và lưu trở lại ở dạng tệp tĩnh.
  • Quản lý nhiều cửa sổ.

Vì vậy nên chúng ta hãy quyết định là xây dựng một ứng dụng như Notepad của Windows đi. 😄 Chúng ta sẽ có thể sử dụng khi cần ghi chú nhanh, phác họa các ý tưởng, hay soạn thảo nhanh các tệp code đơn giản. Sau đó chúng ta sẽ suy nghĩ về việc bổ sung thêm một vài tính năng so với Notepad nguyên bản; Ví dụ như: tự động lưu nội dung ở dạng nháp sau một khoảng thời gian, mở nhiều tệp trong cùng một cửa sổ thành các tab giống như các trình soạn thảo code, v.v… 😄

Mô hình quản lý process

Chúng ta sẽ xuất phát từ điểm khởi chạy phần mềm để theo dõi logic vận hành của bộ code “Hello World”. Ở đây mình gọi ứng dụng đang xây dựng là electron-code và sẽ đổi tên thư mục thành như vậy. Lý do là vì mình thường dùng các phần mềm Text Editor đơn giản để code nháp và ghi chú; và cũng rất muốn có một phần mềm đơn giản như Notepad nhưng có khoảng cách giữa các dòng code và khoảng cách với các lề thoáng hơn một chút. Nếu bạn có ý định xây dựng một ứng dụng soạn thảo code xịn như VisualStudio Code thì có thể đặt tên bạn vào thay từ electron cũng được. 😄

{"name":"electron-code","version":"1.0.0","description":"Simple text editor","main":"main.js","scripts":{"start":"electron ."},"repository":"https://github.com/semiarthanoian/electron-code","keywords":["nodejs","electronjs","text editor","tutorial","beginner"],"author":"Semi Art","license":"CC0-1.0","devDependencies":{"electron":"^18.2.3"}}

Vậy là lệnh npm start được thiết lập mặc định là chạy "electron ." ở thư mục cùng cấp, trỏ tới "main": "main.js".

Hmm… code trong tệp main.js có khá nhiều ghi chú. Mình có Google Translate qua rồi, nhưng dịch lại ở đây thì dài dòng quá. Bạn cũng Google Translate sơ qua rồi xem code thu gọn dưới đây nhé. 😄

const{ app, BrowserWindow }=require('electron')const path =require('path')// --- display main windowconstcreateWindow=()=>{var preload = path.join(__dirname,'preload.js')var mainWindow =newBrowserWindow({
      width:800,
      height:600,
      webPreferences:{ preload }})
   mainWindow.loadFile('index.html')}

app.on('ready',(event)=>{window()})// --- for unix-based OS

app.on('activate',(event)=>{var windowsCounter = BrowserWindow.getAllWindows().length
   if(windowsCounter !=0)/* do nothing */;elsecreateWindow()})

app.on('window-all-closed',(event)=>{var hostOS = process.platform
   if(hostOS =='darwin')/* do nothing */;else                       app.quit()})

Ở đây chúng ta có hai phần code: Phần đầu là để khởi tạo và để hiển thị cửa sổ của ứng dụng khi người dùng nhấn vào biểu tượng trên mà hình để mở ứng dụng; Phần thứ hai là thao tác xử lý bổ sung cho các hệ điều hành có tên mã darwin, cụ thể là Mac và các hệ điều hành dòng OpenBSD và chúng ta rất ít gặp. Do đó chúng ta sẽ không cần phải quan tâm chi tiết tới phần code thứ hai.

Chúng ta thấy app là một object dựng sẵn được export bởi module electron và được áp dụng giao diện Event Emitter hoặc NodeEventTarget. Có lẽ ElectronJS đã sử dụng nhiều thiết lập ban đầu cho app do đó việc khởi tạo và hiển thị cửa sổ ứng dụng chỉ được thực hiện khi sự kiện ready được phát động.

Trong hàm khởi tạo cửa sổ ứng dụng createWindow, chúng ta có thêm 2 tệp nữa được tải vào logic xử lý. Đầu tiên là tệp preload.js:

window.addEventListener('DOMContentLoaded',(event)=>{varreplaceText=(selector, text)=>{var element = document.getElementById(selector)if(element ==null)/* do nothing */;else                    element.innerText = text
   }for(var type of['chrome','node','electron']){replaceText(`${type}-version`, process.versions[type])}})// window.addEventListener

Ồ… một đoạn code gắn hàm xử lý sự kiện trong môi trường trình duyệt web để tìm tới các phần tử #chrome-version, #node-version, và #electron-version để chèn nội dung là thông tin truy xuất từ object process mô tả tiến trình chạy code chính của ứng dụng tạo ra bởi NodeJS.

Như vậy là preload.js vừa có khả năng truy xuất object process trong môi trường NodeJS, và vừa có thể can thiệp vào bên trong môi trường vận hành code JavaScript của cửa sổ trình duyệt Chromium. Bây giờ chúng ta hãy xem lại code template của tệp index.html.

<!doctypehtml><html><head><title>Hello World!</title><metacharset="utf-8"><metahttp-equiv="content-security-policy"content="default-src 'self'; script-src 'self'; style-src 'self''unsafe-inline'"><linkhref="./styles.css"rel="stylesheet"></head><body><h1>Hello World!</h1>

   We are using Node.js <spanid="node-version"></span>,
   Chromium <spanid="chrome-version"></span>,
   and Electron <spanid="electron-version"></span>.

   <!-- You can also require other files to run in this process --><scriptsrc="./renderer.js"></script></body></html>

Ở đây chúng ta lại có thêm các tệp styles.cssrenderer.js được nhúng vào. Tệp preload.js có lẽ được nạp vào nhờ code thực thi của class BrowserWindow và chúng ta sẽ tìm hiểu trong tài liệu sau. Còn ở đây thì chúng ta sẽ thử xem renderer.js có thể sử dụng các module do NodeJS cung cấp không.

À mà khỏi cần thử. 😄 Nếu có thì code truy vấn thông tin về main process đâu cần thiết phải viết trong preload.js. 😄 Như vậy là code trong tệp renderer.js sẽ hoạt động như code JavaScript client-side khi lập trình web. Thật kỳ lạ, nếu vậy thì chỉ cần preload.js thôi là đủ, ở đó chúng ta cũng có thể gắn các hàm xử lý sự kiện do thao tác người dùng tạo ra. Có lẽ là đến lúc phải mở tài liệu chính thức của ElectronJS để xem rồi. 😄

electronjs.org -> Docs -> Processes in Electron -> Process Model

ElectronJS nói rằng chúng ta có 2 loại tiến trình: một là main process, và hai là renderer process. Khi chúng ta bắt đầu chạy code tại tệp main.js thì dòng xử lý chính tạo ra main process. Sau đó cứ mỗi cửa sổ BrowserWindow hay mỗi tab trong trình duyệt Chromium sẽ tạo ra một tiến trình phụ renderer process – thực ra là một child process được tạo ra từ main process theo cách mà chúng ta đã biết đến trong Sub-Series NodeJS.

electronjs.org -> Docs -> Processes in Electron -> Context Isolation

Ngoài ra thì chúng ta còn có thêm một mục nội dung Context Isolation nói về giới hạn hạn tài nguyên được sử dụng bởi renderer process. Code JavaScript trong tệp này sẽ mặc định không thể truy xuất tới các tính năng do framework cung cấp và cả các module của NodeJS, tuy nhiên có thể được khai mở bởi preload.js. 😄

Giao tiếp giữa các process

Như vậy là chúng ta có một tệp main.js khởi chạy tiến trình chính main process, và code ở đây sẽ không thể trực tiếp gắn hàm xử lý sự kiện vào các phần tử của giao diện người dùng. Và tệp renderer.js sẽ được tải vào mỗi tab của trình duyệt Chromium và chạy trên các tiến trình phụ renderer process, nơi mà chúng ta không thể viết code trực tiếp require các module của NodeJS hay ElectronJS để sử dụng.

Lúc này nếu chúng ta đứng ở vai trò người sử dụng thì khi thực hiện một thao tác nào đó trên giao diện người dùng, một sự kiện sẽ được trình duyệt Chromium tạo ra và chúng ta chỉ có thể viết code gắn hàm xử lý tại preload.js hoặc renderer.js.

Nếu như là một thao tác người dùng muốn mở một tệp đã lưu trên máy tính để chỉnh sửa nội dung, thì hiển nhiên hàm xử lý sự kiện sẽ cần phải có khả năng sử dụng module File System của NodeJS. Và chúng ta sẽ cần phải viết hàm xử lý sự kiện trong tệp preload.js, hoặc tìm cách để giúp code ở renderer.js có thể giao tiếp được với main process.

Mặc dù phương án xử lý thứ nhất rất đơn giản và phù hợp với ứng dụng electron-code mà mình đang hướng tới. Tuy nhiên để dự phòng là bạn muốn xây dựng một ứng dụng có tính năng đa dạng hơn, và hơn nữa là để chúng ta có thể đọc hiểu code của các project open-source để học hỏi thêm từ cộng đồng – chúng ta sẽ tìm hiểu thêm phương án xử lý thứ hai. 😄

electronjs.org -> Docs -> Processes in Electron -> Inter-Process Communication

Ở đây code ví dụ mà tài liệu của ElectronJS cung cấp cho chúng ta có một hàm xử lý sự kiện được viết tại renderer.js và sử dụng một phương thức openFile() được cung cấp qua giao diện electronAPI.

const btn = document.getElementById('btn')const filePathElement = document.getElementById('filePath')

btn.addEventListener('click',async()=>{const filePath =await window.electronAPI.openFile()
  filePathElement.innerText = filePath
})

Và giao diện electronAPI được thiết lập bởi preload.js.

const{ contextBridge, ipcRenderer }=require('electron')

contextBridge.exposeInMainWorld('electronAPI',{openFile:()=> ipcRenderer.invoke('dialog:openFile')})

Ở đây preload.js sử dụng hai object do module electron cung cấp.

Đầu tiên, là object contextBridge – cầu nối giữa renderer process và môi trường bên ngoài. Trong đó phương thức exposeInMainWorld sẽ expose khai mở một thuộc tính electronAPI trong môi trường Main World. Ở đây chúng ta lưu ý là ElectronJS sử dụng từ “main world” để nói về môi trường của renderer process chứ không phải là của main process nhé. 😄 Còn thế giới bên ngoài renderer process được gọi là môi trường được tách biệt isolated world. Họ đặt tên như vậy là bởi vì phương thức này được xây dựng và đặt trong module tiện ích dành cho renderer process.

Thứ hai, là object ipcRenderer – được thiết kế để hỗ trợ gửi tương tác tới main process. Trong đó phương thức invoke sẽ gửi một mảng dữ liệu (nếu cần thiết) qua một kênh sự kiện channel. Và ở phía main process sẽ có thể gắn một hàm listener ở kênh sự kiện này bằng phương thức ipcMain.handle().

const{ app, BrowserWindow, ipcMain, dialog }=require('electron')const path =require('path')// --- display main windowconstcreateWindow=()=>{var preload = path.join(__dirname,'preload.js')var mainWindow =newBrowserWindow({
      width:800,
      height:600,
      webPreferences:{ preload }})
   mainWindow.loadFile('index.html')}consthandleFileOpen=async()=>{var{ canceled, filePaths }=await dialog.showOpenDialog()if(canceled)returnnullelsereturn filePaths[0]}

app.on('ready',(event)=>{// --- gắn listener cho kênh sự kiện 'dialog:openFile'
   ipcMain.handle('dialog:openFile', handleFileOpen)createWindow()})// --- for unix-based OS ...

Ok… cũng không quá rườm rà. Thế nhưng chúng ta vẫn cần cope/paste thêm code template ở index.html để chạy thử. 😄

<body><buttontype="button"id="btn"> Open a File </button>
   File path: <strongid="filePath"></strong><scriptsrc='./renderer.js'></script></body>
npm start

Ô… như vậy là chúng ta còn học được luôn cách mở cửa sổ duyệt các thư mục và các tệp để tìm tới tệp cần chỉnh sửa. 😄

Kết thúc bài viết

Như vậy là chúng ta đã có được hiểu biết tổng quan về các tệp code cơ bản của một ứng dụng ElectronJS, mô hình quản lý các renderer process được tách biệt khỏi tiến trình chính main process. Đồng thời, chúng ta cũng đã biết cách mở ra một giao diện lập trình kết nối giữa renderer processmain process, biết thêm luôn cách mở cửa sổ duyệt các thư mục và các tệp để tìm tới tệp cần chỉnh sửa nữa. 😄

Công việc tiếp theo là viết code tạo giao diện người dùng chi tiết và định nghĩa các kiểu sự kiện người dùng cần xử lý để viết code điều hành logic hoạt động của phần mềm. Tới đây thì mình nghĩ có khả năng là bạn sẽ có nhiều ý tưởng hơn mình; Và trong trường hợp bạn rất muốn nhanh chóng thực hiện những ý tưởng đang có thì điểm cần quan tâm tiếp theo trong tài liệu của ElectronJS cung cấp là chỉ mục Examples:

electronjs.org -> Docs -> Examples

Ở đây có code ví dụ minh họa về những tính năng phổ biến để bạn có thể tích hợp ngay vào ứng dụng đang xây dựng. Còn hạng mục tài liệu API thì mình nghĩ là nên sử dụng để tham khảo thông tin về các phương thức khi đọc code tại Examples và Google Search. 😄

[ElectronJS] Bài 3 – Từ Từ Để Mình Nghĩ Tên Cho Bài Viết Tiếp Theo Đã 😄

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ