- vừa được xem lúc

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

0 0 11

Người đăng: Semi Art

Theo Viblo Asia

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 window const createWindow = () => { var preload = path.join(__dirname, 'preload.js') var mainWindow = new BrowserWindow({ 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 */; else createWindow()
}) 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) => { var replaceText = (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.

<!doctype html>
<html>
<head> <title>Hello World!</title> <meta charset="utf-8"> <meta http-equiv="content-security-policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"> <link href="./styles.css" rel="stylesheet">
</head>
<body> <h1>Hello World!</h1> We are using Node.js <span id="node-version"></span>, Chromium <span id="chrome-version"></span>, and Electron <span id="electron-version"></span>. <!-- You can also require other files to run in this process --> <script src="./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 window const createWindow = () => { var preload = path.join(__dirname, 'preload.js') var mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload } }) mainWindow.loadFile('index.html')
} const handleFileOpen = async () => { var { canceled, filePaths } = await dialog.showOpenDialog() if (canceled) return null else return 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> <button type="button" id="btn"> Open a File </button> File path: <strong id="filePath"></strong> <script src='./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 Đã 😄

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 525

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 433

- vừa được xem lúc

Một số phương thức với object trong Javascript

Trong Javascript có hỗ trợ các loại dữ liệu cơ bản là giống với hầu hết những ngôn ngữ lập trình khác. Bài viết này mình sẽ giới thiệu về Object và một số phương thức thường dùng với nó.

0 0 153

- vừa được xem lúc

Tìm hiểu về thư viện axios

Giới thiệu. Axios là gì? Axios là một thư viện HTTP Client dựa trên Promise.

0 0 145

- vừa được xem lúc

Imports và Exports trong JavaScript ES6

. Giới thiệu. ES6 cung cấp cho chúng ta import (nhập), export (xuất) các functions, biến từ module này sang module khác và sử dụng nó trong các file khác.

0 0 110

- vừa được xem lúc

Bài toán đọc số thành chữ (phần 2) - Hoàn chỉnh chương trình dưới 100 dòng code

Tiếp tục bài viết còn dang dở ở phần trước Phân tích bài toán đọc số thành chữ (phần 1) - Phân tích đề và những mảnh ghép đầu tiên. Bạn nào chưa đọc thì có thể xem ở link trên trước nhé.

0 0 245