Nếu bạn biết về lập trình web với ReactJS, HTML,CSS nhưng lại muốn làm một desktop app thì phải làm như thế nào? Trong bài viết hôm nay, mình sẽ hướng dẫn các bạn làm desktop app bằng ElectronJS kết hợp với ReactJS, HTML và CSS
Bài viết này là phần thứ nhất trong chuỗi bài về ElectronJS
- Giới thiệu chung, setup React, Electron, Webpack
- Quản lý state với redux và routing trong Electron App.
1. Giới thiệu sơ lược về ElectronJS
ElectronJS là gì?
ElectronJS là một framework dùng để tạo ra Desktop native application. Đây là một open-source và cross - platform framework. Nhờ có ElectronJS bạn hoàn toàn có thể build một native app chạy trên được cả Window, MacOS và Linux chỉ cần sử dụng HTML, CSS và Javascript.
Trong bài viết này, mình sẽ hướng dẫn các bạn khởi tạo một Desktop app với ElectronJS, ReactJS và Webpack
2. Khởi tạo dự án
Bước đầu tiên là khởi tạo project bằng lệnh npm
hoặc yarn
. Bạn có thể dùng flag -y
để cho gọn. Khi đó 1 file package.json
sẽ được tạo ra.
$ mkdir <folder-name> && cd <folder-name> $ yarn -y # You can use npm instead $ npm -y
Sau khi file package.json
được tạo ra, bạn có thể thay đổi các trường thông tin theo ý mình, tuy nhiên phải cần có các field sau:
productName
: Tên của app, sẽ cần dùng bởi packageelectron-packager
version
: Version cho app mỗi lần release (1.0.0
)homePage
: URL to project home page, thường có giá trị là (./
)main
: entry point của app (main.js
)
3. Tạo react app
Trong phần này, chúng ta sẽ khởi tạo React App. Có 2 cách để tạo React app trong trường hợp này:
- Sử dụng
create-react-app
generator - Tự tạo starter template và tự cấu hình từ đầu.
Chúng ta sẽ chọn hướng tiếp cận thứ 2 vì:
- Khi tạo react app từ ban đầu, ta sẽ tự do cấu hình webpack và customize hơn so với create react app
- Với
create-react-app
khi ta kết hợp với Electron phải chạy 2 process (1 là npm script và 1 là process của Electron) đồng thời. Trong khi nếu cách thứ 2, chỉ cần chạy 1 process đã có thể handle cả Electron và React
Đầu tiên chúng ta sẽ cài 2 package căn bản của React là react
và react-dom
$ yarn add react react-dom
Tiếp theo chúng ta sẽ tạo 2 component đầu tiên của ứng dụng.
Tạo file src/index.jsx
với nội dung như sau để render component và gắn vào DOM
// src/index.jsx import React from 'react' import { render } from 'react-dom' // Import main App component import App from './components/app' // Since we are using HtmlWebpackPlugin WITHOUT a template, we should create our own root node in the body element before rendering into it let root = document.createElement('div') // Append root div to body root.id = 'root' document.body.appendChild(root) // Render the app into the root div render(<App />, document.getElementById('root'))
Tiếp theo ta sẽ tạo App component. Trong thư mục src
tạo thư mục components
và file App.jsx như sau
// src/components/App.jsx import React from 'react' // Create main App component const App = () => ( <div> <h1>Hello, electron!</h1> <p>Let's start building your awesome desktop app with electron and React!</p> </div> ) // Export the App component export default App
4. Thêm electronJS và config main process
Sau khi đã xử lý xong với React, bước tiếp theo ta sẽ “take care” đến Electron. Cài đặt electron
và electron-packager
.
electron
thì khỏi phải bàn, package này giúp chúng ta chạy được electronelectron-packager
cho phép chúng ta build ứng dụng electron về các file bundle tương ứng với từng hệ điều hành như .exe (Windows) hoặc .dmg (MacOS)
$ yarn add electron electron-packager
Ngoài ra chúng ta có thể cài thêm electron-devtools-installer
để có thể cài đặt devtool vào electron app, nhăm hỗ trợ debug tốt hơn trong quá trình code
$ yarn add electron-devtools-installer
Sau khi đã cài đặt xong, chúng ta tiến hành config main process của electron. Trong thư mục root của project, tạo file main.js
(có thể đổi tên khác nhưng lưu ý là phải đúng với đường dẫn ở field main
trong package.json đã nói ở trên)
// main.js 'use strict' const { app, BrowserWindow, remote } = require('electron'); const path = require('path'); const url = require('url'); const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); let mainWindow; console.log(`Running in ${process.env.NODE_ENV} mode`); let dev = process.env.NODE_ENV === 'development'; if (process.platform === 'win32') { app.commandLine.appendSwitch('high-dpi-support', 'true') app.commandLine.appendSwitch('force-device-scale-factor', '1') } function createWindow() { mainWindow = new BrowserWindow({ width: 1024, height:768, show: false, webPreferences: { nodeIntegration: true } }); let indexPath; if (dev && process.argv.indexOf('--noDevServer') === -1) { indexPath = url.format({ protocol: 'http:', host: 'localhost:8080', pathname: 'index.html', slashes: true }) } else { indexPath = url.format({ protocol: 'file:', pathname: path.join(__dirname, 'dist', 'index.html'), slashes: true }) } mainWindow.loadURL(indexPath); mainWindow.once('ready-to-show', () => { mainWindow.show(); if (dev) { installExtension(REACT_DEVELOPER_TOOLS) .catch(err => console.log('Error loading React DevTools: ', err)) mainWindow.webContents.openDevTools() } }); mainWindow.once('closed', () => { mainWindow = null; }) } app.on('ready', createWindow); app.on('window-all-closed', () => { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit() } }); app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (mainWindow === null) { createWindow() } })
5. Cấu hình webpack
Sau khi React và Electron đã sẵn sàng, công việc tiếp theo của chúng ta là cấu hình webpack. Ở đây chúng ta sẽ tạo ra 2 file webpack config cho 2 môi trường là development và production.
Nội dung của 2 file này phần lớn là như nhau, chỉ khác nhau một số điểm như sau:
webpack.dev.config.js
: sử dụng devServer và sourcemapwebpack.build.config.js
: sẽ sử dụngmini-css-extract-plugin
để optimize css style và dùngbabili-webpack-plugin
như babel minifier
Cụ thể như sau:
Trong thư mục của project, tạo file webpack.dev.config.js
const webpack = require('webpack');
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { spawn } = require('child_process');
const defaultInclude = path.resolve(__dirname, 'src');
module.exports = {
mode: 'developement',
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader'
}
],
include: defaultInclude,
},
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', 'sass-loader'],
include: defaultInclude,
},
{
test: /\.jsx?$/,
use: ['babel-loader'],
include: defaultInclude,
},
{
test: /\.(jpe?g|png|gif)$/,
use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }],
include: defaultInclude,
},
{
test: /\.(eot|svg|ttf|woff|woff2)$/,
use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }],
include: defaultInclude
}
]
},
target: 'electron-renderer',
plugins: [
new HtmlWebpackPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
})
],
devtool: 'cheap-source-map',
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
stats: {
colors: true,
chunks: false,
children: false
},
before() {
spawn(
'electron',
['.'],
{ shell: true, env: process.env, stdio: 'inherit' }
)
.on('close', code => process.exit(0))
.on('error', spawnError => console.error(spawnError))
}
},
resolve: {
extensions: ['.js', '.json', '.jsx']
}
}
Và tương tự tạo file webpack.build.config.js
cho production build
const webpack = require('webpack'); const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const BabiliPlugin = require('babili-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin') const defaultInclude = path.resolve(__dirname, 'src') module.exports = { mode: 'production', module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader' } ], include: defaultInclude, }, { test: /\.s[ac]ss$/i, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], include: defaultInclude, }, { test: /\.jsx?$/, use: ['babel-loader'], include: defaultInclude, }, { test: /\.(jpe?g|png|gif)$/, use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }], include: defaultInclude, }, { test: /\.(eot|svg|ttf|woff|woff2)$/, use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }], include: defaultInclude } ] }, target: 'electron-renderer', plugins: [ new HtmlWebpackPlugin(), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: 'bundle.css', chunkFilename: '[id].css' }), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), new BabiliPlugin() ], stats: { colors: true, children: false, chunks: false, modules: false }, resolve: { extensions: ['.js', '.json', '.jsx'] } }
- Lưu ý: trong project này mình sẽ dùng SCSS để style thay vì dùng CSS, nên sẽ cấu hình thêm việc đọc file .scss
Sau khi config webpack xong, chúng ta sẽ cái các package cần thiết, bao gồm webpack, các loader, và các plugin
$ yarn add -D webpack webpack-cli webpack-dev-server @babel/core @babel/preset-react babili-webpack-plugin babel-loader css-loader sass-loader file-loader style-loader html-webpack-plugin mini-css-extract-plugin
Chúng ta biết rằng, webpack sử dụng babel và @babel/preset-react
để build code React, nhưng webpack không tự nhận diện ra plugin này 1 cách tự động. Vì vậy chúng ta phải dùng file .babelrc
để đảm bảo webpack sử dụng plugin mà chúng ta đã cài. Trong thư mục gốc của project, tạo file .babelrc
với nội dung như sau:
{ "presets": [ "@babel/preset-react" ] }
6. Thêm các script để hoàn chỉnh package.json
Bước cuối cùng, sau khi đã chuẩn bị sẵn sàng từ React, Electron cho đến Webpack configuration, chúng ta sẽ thêm các npm script trong package.json để sử dụng trong quá trình code cũng như build production.
Chúng ta sẽ có 5 script như sau được để trong mục scripts của package.json
{
"scripts:" {
"start": "NODE_ENV=development webpack-dev-server --hot --host 0.0.0.0 --config=./webpack.dev.config.js --mode=development",
"prod": "webpack --mode=production --config webpack.build.config.js && NODE_ENV=production electron --noDevServer .",
"build": "NODE_ENV=production webpack --config webpack.build.config.js --mode production",
"package": "npm run build",
"postpackage": "NODE_ENV=production electron-packager ./ --out=./builds"
}
}
Để chạy app ở development mode thì chỉ cần yarn start
Kết
Như vậy là mình và các bạn đã vừa hoàn thành xong 1 boilerplate để phát triển 1 ứng dụng desktop sử dụng ElectronJS, ReactJS và Webpack. Trong phần 2 của bài viết này, mình sẽ thêm Redux và React-router vào boilerplate này. Cùng chờ đón phần 2 nhé. Thanks