搭建一个自己的electron应用(electron+react+ts)

2,594 阅读7分钟

1.png 最近需要做一个桌面应用,相信很多前端选择制作桌面应用肯定是会选择electron,查了好久有一个electron-vue,由于最近主要写的react,所以想找找有没有react的,实在找不到了,只能自己搭一个了。 开发electron有两个进程,渲染进程和主进程,首先我们先搭建主进程。

主进程

初始化文件夹

mkdir electron-react&&npm init --yes

安装依赖

我们这边都是使用ts来开发,所以把ts也一起安装了,这边electron可能需要科学上网才能正常安装,不然装的可能会比较慢,这边我的electron安装的版本是15.1.2

yarn add electron typescript -D

安装完后初始化ts

tsc --init

这样就生成了一份tsconfig.json文件,我们对这一份tsconfig.json改个名字,改为tsconfig.main.json然后我们配置下几个基本的ts配置项

{
  "compilerOptions": {

    "target": "esNext",                                /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs",                           /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "jsx": "react",                           /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
    "outDir": "main-dist",                              /* Redirect output structure to the directory. */
    /* Strict Type-Checking Options */
    "strict": true,                                 /* Enable all strict type-checking options. */
    "noImplicitAny": true,                       /* Raise error on expressions and declarations with an implied 'any' type. */
    "strictNullChecks": true,                    /* Enable strict null checks. */

    "baseUrl": ".",                             /* Base directory to resolve non-absolute module names. */
    "paths": {},                                 /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    "esModuleInterop": true,                        /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

    /* Advanced Options */
    "skipLibCheck": true,                           /* Skip type checking of declaration files. */
    "forceConsistentCasingInFileNames": true        /* Disallow inconsistently-cased references to the same file. */
  },
  "include" :["./main/**/*"],
    "exclude": ["dist","node_modules"]
}

然后我们在根目录下新建main/index.ts
main/index.ts,从字面意思很好理解,就是主进程是控制窗口的,而渲染进程就是负责渲染的,主进程创建了窗口可以加载url,也可以加载本地文件,这边我们就直接打开本地文件了.
在新建的main/index.ts输入一下内容来打开一个窗口

import {BrowserWindow,app} from 'electron'
function createWindow(){
    const window:BrowserWindow=new BrowserWindow({
        height:1000,
        width:1000,
        webPreferences:{
            webSecurity:false,
            contextIsolation:false,
            nodeIntegration:true,
        }
    })
    window.loadFile('../main/index.html');
    return window;
}
app.on('ready',()=>{
    process.env['ELECTRON_DISABLE_SECURITY_WARNINGS']='true'//关闭web安全警告
    createWindow();
})

上述内容就是创建一个窗口并且加载同级下的index.html文件,我们来执行下主进程,首先我们需要编译ts,在我们的package.json下写入脚本命令

 "scripts": {
    "start:main": "tsc --p tsconfig.main.json" //--p指定ts配置文件
  },

然后执行yarn start:main,我们就可以看到根目录下多了一个main-dist文件,这个文件就是ts编译好后的js文件,我们需要的就是执行这个js文件,我们在添加一个命令

 "scripts": {
    "build:main": "tsc --p tsconfig.main.json",
    "start:electron": "electron ./main-dist/index"
  },

在执行这个命令前我们需要创建一个main/index.html文件,这个文件就是我们窗口要加载的html文件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">hello electron</div>
</body>
</html>

然后执行yarn build:electron,我们就会看到我们启动了一个窗口,窗口里显示的就是hello electron,到这为止我们的主进程就搭建完成啦,当然后续渲染进程搭建完后这边也会有所修改. 来现在我们来搭建渲染进程,

渲染进程

渲染进程我们使用react,这边我们就用webpack开搭建一个react项目,搭建之前我们先把之前的tsconfig.main.json复制一份改名为tsconfig.json

{
  "compilerOptions": {
    "target": "esNext",                                /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs",                           /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "jsx": "react",                           /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
    "outDir": "./render/dist",                              /* Redirect output structure to the directory. */
    /* Strict Type-Checking Options */
    "strict": true,                                 /* Enable all strict type-checking options. */
    "noImplicitAny": true,                       /* Raise error on expressions and declarations with an implied 'any' type. */
    "strictNullChecks": true,                    /* Enable strict null checks. */
    "baseUrl": ".",                             /* Base directory to resolve non-absolute module names. */
    "paths": {},                                 /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    "esModuleInterop": true,                        /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    /* Advanced Options */
    "skipLibCheck": true,                           /* Skip type checking of declaration files. */
    "forceConsistentCasingInFileNames": true        /* Disallow inconsistently-cased references to the same file. */
  },
  "include" :["./render/**/*"],
  "exclude": ["dist","node_modules"]
}

安装依赖

安装webpack依赖

yarn add webpack webpack-cli webpack-dev-server webpack-merge html-webpack-plugin clean-webpack-plugin  -D

安装react依赖

yarn add react react-dom react-router-dom @types/react @types/react-dom @types/react-router-dom

安装loader

yarn add ts-loader style-loader url-loader file-loader css-loader less-loader -D

在根目录下新建三个文件
render/config/webpack.common.js
render/config/webpack.dev.js
render/config/webpack.prod.js

webpack.common.js

const webpack = require("webpack");
const path = require("path");
module.exports = {
  entry: "./render/index",
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "../dist-render"),
  },
  // target:'web',
  target: "electron-renderer",
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
    alias: {},
  },
  module: {
    rules: [
      {
        test: [/\.js$/, /\.ts$/, /\.tsx$/],
        exclude: /node_modules/,
        use: [
          {
            loader: "ts-loader",
          },
        ],
      },
      {
        test: [/\.less$/, /\.css$/],
        // exclude:/node_modules/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              modules: false,
              sourceMap: false,
            },
           
          },
          "less-loader",
        ],
      },
      {
        test: /.(jpg|png|gif)$/,
        use: {
          loader: "file-loader",
          options: {
            //
            name: "[name]_[hash].[ext]",
          },
        },
      },
    ],
  },
  plugins: [],
};

本文不对webpack配置做过多说明,这边我们对ts、js、tsx使用ts-loader来进行编译,css、less和静态文件都使用最常用的loader.

需要注意的是,由于electron这边可以在web中使用node,所以target我们不能设置成web而要使用electron-renderer webpack.dev.js

const webpack = require("webpack");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const mode = "development";
module.exports = merge(common, {
  mode: "development",
  devtool: "inline-source-map",
  devServer: {
    hot: true,
    static: path.join(__dirname, "../dist-main"),
    historyApiFallback: {
      index: "./index.html",
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "../public/index.html"),
      filename: "index.html",
    }),
    new webpack.HotModuleReplacementPlugin(),
  ],
});

webpack.prod.js

const webpack = require("webpack");
const  common  = require("./webpack.common");
const { merge } = require("webpack-merge");
const path=require('path')
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = merge(common, {
  mode: "production",
  devtool: "source-map",
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "../public/index.html"),
      filename: "index.html",
    }),
  ],
});

新建render/public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

新建render/src/app.tsx,初始化一个根组件

import { App } from "electron";
import React from "react";

export default function App() {
  return <div>hello react</div>;
}

新建render/index.tsx

import React from "react";
import ReactDOM from "react-dom";
import App from "./src/app";
ReactDOM.render(<App />, document.getElementById("App"));

接下来我们就可以来配置渲染进程脚本命令啦

 "scripts": {
    "start:react": "webpack serve --config render/config/webpack.dev.js",
    "build:react": "webpack --config render/config/webpack.prod.js",
    "start:main": "tsc --p tsconfig.main.json",
    "start:electron": "electron ./main-dist/index"
  },

我们这边执行yarn start:react,然后打开8080端口就会看到下面这个报错

image.png

这个报错呢,是正常的,因为我们使用electron-renderer打包出来的只能在electron环境下才可以运行,所以我们修改我们的主进程让他来加载我们本地服务,我们需要安装一个依赖叫做electron-is-dev,当我们不是开发环境的时候就要加载yarn build:react打包出来的文件了。

yarn add electron-is-dev

然后修改我们的main/index.ts

import {BrowserWindow,app} from 'electron'
import isDev from 'electron-is-dev'
import {resolve} from 'path'
function createWindow(){
    const window:BrowserWindow=new BrowserWindow({
        height:1000,
        width:1000,
        webPreferences:{
            webSecurity:false,
            contextIsolation:false,
            nodeIntegration:true,
        }
    })
    if(isDev){
         window.webContents.openDevTools();//打开控制台
        window.loadURL('http://localhost:8080')
    }else{
         window.loadFile(resolve(__dirname,"../render/dist-render/index.html"));
    }
   
    return window;
}
app.on('ready',()=>{
    process.env['ELECTRON_DISABLE_SECURITY_WARNINGS']='true'//关闭web安全警告
    createWindow();
})

然后我们在执行一下yarn build:main打包主进程,我们每次都要修改主进程都需要重新打包这就很麻烦,所以我们给主进程加上热更新,怎么做呢,很简单只要使用tsc -w就行了,w意思就是watch监听

   "start:main":"tsc -w --p tsconfig.main.json",

我们每次就可以执行yarn start:main了,这样修改主进程文件后他就会自动重新打包了
接下来我们执行yarn start:electron 我们会发现我们能看到hello react了,这就说明我们的整个electron应用就搭建完成咯。

热重载

electron热重载我们可以使用electron-reloader

yarn add electron-reloader -D

然后在main.ts中添加一下代码就行啦

 if(isDev){
        try {
            require('electron-reloader')(module, {});
        } catch (_) { }
        window.webContents.openDevTools();
        window.loadURL('http://localhost:8080')
    }

打包

如果我们开发完了然后要打包我们该怎么办呢,我们需要安装一个electron-builder

yarn add electron-builder -D

然后我们需要在package.json中做一些配置,首先需要指定项目主入口,然后就是打包配置了

"main":"./main-dist/index.js",
  "build": {
    "productName": "test",
    "appId": "org.react.vas",
    "mac": {
      "icon": "assets/icon.icns",
      "type": "distribution",
      "target": [
        "dmg",
        "zip"
      ]
    },
    "win": {
      "icon": "build/favicon.ico",
      "target": [
        "nsis"
      ]
    },
    "nsis": {
      "oneClick": false, 
      "perMachine": true,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "createStartMenuShortcut": false
    }
  },

这边需要注意icon这里mac一般用的icns,icns的生成可以参考快速生成 Mac App icns 图标,生成后放入指定文件夹里就行,我这就直接放在了根目录下的assets下。 然后我们加入打包命令,build:mac是打包mac安装包的,build:win是打包windows安装包的,我们都是打的对应系统的安装包

  "build:mac": "electron-builder --mac",
  "build:win": "electron-builder --win"

执行yarn build:mac,耐心等待后你会发现根目录下多了一个dist文件,里面就有你打包出来的安装包咯,打开安装包就是下面这个样子的咯

image.png

拖入后然后打开我们的test应用你就会发现打开的应用是一片空白,这是什么原因呢,其实是老生常谈的问题了,修改我们webpack配置,在webpack.common.js中添加一段代码,再重新打包就行啦

output: {
    publicPath: './',
    filename: "[name].js",
    path: path.join(__dirname, "../dist-render"),
  },

到这边我们自己搭建的electron应用就搭建完成啦
仓库地址