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.css
và renderer.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 process
và main 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