├── __tests__ ├── __mocks__ │ └── fileMock.js ├── app.test.tsx └── __snapshots__ │ └── app.test.tsx.snap ├── .npmrc ├── resources ├── icon.icns ├── icon.ico ├── icon.png ├── tray.png └── icons │ ├── 128x128.png │ ├── 16x16.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 512x512.png │ ├── 64x64.png │ ├── 96x96.png │ └── 1024x1024.png ├── src ├── main │ ├── assets │ │ └── logo.png │ ├── App.module.scss │ ├── index.tsx │ ├── app.scss │ ├── App.tsx │ └── components │ │ ├── hero.scss │ │ └── hero.tsx ├── picture │ ├── assets │ │ └── logo.png │ ├── index.tsx │ ├── App.module.scss │ ├── App.tsx │ ├── app.scss │ └── components │ │ ├── hero.scss │ │ └── hero.tsx ├── update │ ├── assets │ │ └── logo.png │ ├── index.tsx │ ├── App.module.scss │ ├── app.scss │ ├── App.tsx │ └── components │ │ ├── hero.scss │ │ └── hero.tsx └── multiple │ ├── assets │ └── logo.png │ ├── app.scss │ ├── App.module.scss │ ├── index.tsx │ ├── App.tsx │ └── components │ ├── hero.scss │ └── hero.tsx ├── .travis.yml ├── .postcssrc.js ├── electron ├── utils.ts ├── index.ts ├── for-renderer-utils │ ├── ipc.ts │ ├── window.ts │ └── update.ts ├── set-dock-menu.ts ├── set-tray.ts ├── constants.ts ├── set-up.ts ├── set-update.ts ├── set-menu.ts ├── set-touchbar.ts └── windows-manager.ts ├── .gitignore ├── .prettierrc ├── type.d.ts ├── erb.config.js ├── jest.config.js ├── tsconfig.json ├── .babelrc ├── README.md ├── README_EN.md └── package.json /__tests__/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/tray.png -------------------------------------------------------------------------------- /src/main/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/src/main/assets/logo.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icons/512x512.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icons/64x64.png -------------------------------------------------------------------------------- /resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icons/96x96.png -------------------------------------------------------------------------------- /src/picture/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/src/picture/assets/logo.png -------------------------------------------------------------------------------- /src/update/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/src/update/assets/logo.png -------------------------------------------------------------------------------- /resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /src/multiple/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fantasticit/electron-react-boilerplate/HEAD/src/multiple/assets/logo.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10.6.0 4 | 5 | script: 6 | - npm run test 7 | 8 | branches: 9 | only: 10 | - master 11 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'postcss-url': {}, 5 | // to edit target browsers: use "browserslist" field in package.json 6 | autoprefixer: {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /electron/utils.ts: -------------------------------------------------------------------------------- 1 | export const getType = (objOrVal) => 2 | Object.prototype.toString.call(objOrVal).slice(8, -1).toLowerCase(); 3 | 4 | export const isProd = process.env.NODE_ENV === 'production'; 5 | export const isMac = process.platform === 'darwin'; 6 | -------------------------------------------------------------------------------- /src/multiple/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', 'Oxygen', 'Ubuntu', 9 | 'Cantarell', 'Open Sans', sans-serif; 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache-loader 2 | .DS_Store 3 | /.cache-loader/ 4 | node_modules/ 5 | /dist/ 6 | /release/ 7 | /coverage/ 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "quoteProps": "consistent", 4 | "bracketSpacing": true, 5 | "jsxBracketSameLine": false, 6 | "arrowParens": "always", 7 | "trailingComma": "es5", 8 | "tabWidth": 2, 9 | "semi": true, 10 | "printWidth": 100, 11 | "endOfLine": "lf" 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/app.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import App from '../src/main/App'; 4 | 5 | test('Hello, Electron & React', () => { 6 | const component = renderer.create(); 7 | let tree = component.toJSON(); 8 | expect(tree).toMatchSnapshot(); 9 | }); 10 | -------------------------------------------------------------------------------- /electron/index.ts: -------------------------------------------------------------------------------- 1 | import { setUp } from './set-up'; 2 | import { setMenu } from './set-menu'; 3 | import { setTray } from './set-tray'; 4 | import { setUpdate } from './set-update'; 5 | import { setTouchbar } from './set-touchbar'; 6 | import { setDockMenu } from './set-dock-menu'; 7 | 8 | setUp().then(({ windowsManger, mainWindow }) => { 9 | setMenu(); 10 | setTray(); 11 | setUpdate(windowsManger); 12 | setTouchbar(mainWindow); 13 | setDockMenu(); 14 | }); 15 | -------------------------------------------------------------------------------- /electron/for-renderer-utils/ipc.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | /** 4 | * 渲染进程向主进程发送消息 5 | * @param type 6 | * @param arg 7 | */ 8 | export function ipc(type, arg = null) { 9 | return new Promise((resolve, reject) => { 10 | ipcRenderer.once('success', (_, arg) => { 11 | resolve(arg); 12 | }); 13 | ipcRenderer.once('error', (_, arg) => { 14 | reject(arg); 15 | }); 16 | ipcRenderer.send(type, arg); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/App.module.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | font-size: 1.5rem; 3 | text-align: center; 4 | 5 | header { 6 | padding: 60px 0 40px; 7 | color: #fff; 8 | background: #282c34; 9 | } 10 | 11 | code { 12 | color: red; 13 | } 14 | 15 | @media (min-width: 992px) { 16 | p { 17 | font-size: 2.4rem; 18 | } 19 | } 20 | 21 | @media (min-width: 768px) { 22 | p { 23 | font-size: 1.8rem; 24 | } 25 | } 26 | 27 | p { 28 | color: #9feaf9; 29 | font-size: 1.4rem; 30 | font-weight: 300; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/multiple/App.module.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | font-size: 1.5rem; 3 | text-align: center; 4 | 5 | header { 6 | padding: 60px 0 70px; 7 | color: #fff; 8 | background: #282c34; 9 | } 10 | 11 | code { 12 | color: red; 13 | } 14 | 15 | @media (min-width: 992px) { 16 | p { 17 | font-size: 2.4rem; 18 | } 19 | } 20 | 21 | @media (min-width: 768px) { 22 | p { 23 | font-size: 1.8rem; 24 | } 25 | } 26 | 27 | p { 28 | color: #9feaf9; 29 | font-size: 1.4rem; 30 | font-weight: 300; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /type.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const content: { [className: string]: string }; 3 | export = content; 4 | } 5 | 6 | declare module '*.module.scss' { 7 | const content: { [className: string]: string }; 8 | export = content; 9 | } 10 | 11 | declare module '*.svg'; 12 | declare module '*.png'; 13 | declare module '*.jpg'; 14 | declare module '*.jpeg'; 15 | declare module '*.gif'; 16 | declare module '*.bmp'; 17 | declare module '*.tiff'; 18 | 19 | declare const PACKAGE_JSON; 20 | declare const RUNTIME_CONFIG; 21 | declare const DEV_RENDERER_PORT; 22 | -------------------------------------------------------------------------------- /electron/set-dock-menu.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu } from 'electron'; 2 | import { isMac } from './utils'; 3 | 4 | export const setDockMenu = () => { 5 | if (!isMac) return; 6 | 7 | const dockMenu = Menu.buildFromTemplate([ 8 | { 9 | label: 'New Window', 10 | click() { 11 | console.log('New Window'); 12 | }, 13 | }, 14 | { 15 | label: 'New Window with Settings', 16 | submenu: [{ label: 'Basic' }, { label: 'Pro' }], 17 | }, 18 | { label: 'New Command...' }, 19 | ]); 20 | 21 | app.dock.setMenu(dockMenu); 22 | }; 23 | -------------------------------------------------------------------------------- /electron/set-tray.ts: -------------------------------------------------------------------------------- 1 | import { Menu, Tray } from 'electron'; 2 | import path from 'path'; 3 | 4 | let tray = null; 5 | 6 | export const setTray = () => { 7 | tray = new Tray(path.join(__dirname, '../resources/tray.png')); 8 | const contextMenu = Menu.buildFromTemplate([ 9 | { label: 'Item1', type: 'radio' }, 10 | { label: 'Item2', type: 'radio' }, 11 | { label: 'Item3', type: 'radio', checked: true }, 12 | { label: 'Item4', type: 'radio' }, 13 | ]); 14 | tray.setToolTip('This is my application.'); 15 | tray.setContextMenu(contextMenu); 16 | }; 17 | -------------------------------------------------------------------------------- /erb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 31323, 3 | windows: [ 4 | { 5 | name: 'main', 6 | isMain: true, 7 | title: '主窗口', 8 | }, 9 | { 10 | name: 'multiple', 11 | multiple: true, 12 | title: '可多开窗口', 13 | }, 14 | { 15 | name: 'update', 16 | title: '更新窗口', 17 | width: 400, 18 | parent: 'main', 19 | modal: false, // 模态窗在 mac 系统上无法关闭 20 | }, 21 | { 22 | name: 'picture', 23 | width: 600, 24 | heigh: 300, 25 | title: '图片查看', 26 | }, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /src/main/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') as HTMLElement 11 | ); 12 | 13 | if (module['hot']) { 14 | module['hot'].accept('./App', () => { 15 | const NextApp = require('./App').default; 16 | 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | document.getElementById('root') as HTMLElement 22 | ); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/multiple/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') as HTMLElement 11 | ); 12 | 13 | if (module['hot']) { 14 | module['hot'].accept('./App', () => { 15 | const NextApp = require('./App').default; 16 | 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | document.getElementById('root') as HTMLElement 22 | ); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/picture/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') as HTMLElement 11 | ); 12 | 13 | if (module['hot']) { 14 | module['hot'].accept('./App', () => { 15 | const NextApp = require('./App').default; 16 | 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | document.getElementById('root') as HTMLElement 22 | ); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/update/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') as HTMLElement 11 | ); 12 | 13 | if (module['hot']) { 14 | module['hot'].accept('./App', () => { 15 | const NextApp = require('./App').default; 16 | 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | document.getElementById('root') as HTMLElement 22 | ); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverageDirectory: 'coverage', 3 | globals: { 4 | 'ts-jest': { 5 | tsConfig: 'tsconfig.json', 6 | }, 7 | }, 8 | moduleFileExtensions: ['js', 'ts', 'tsx'], 9 | moduleNameMapper: { 10 | '\\.(css|scss|less)$': 'identity-obj-proxy', 11 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 12 | '/__test__/__mocks__/fileMock.js', 13 | '^electron-utils(.*)$': '/electron/for-renderer-utils$1', 14 | }, 15 | testEnvironment: 'node', 16 | testMatch: ['**/__tests__/*.+(ts|tsx|js)'], 17 | transform: { 18 | '^.+\\.(ts|tsx)$': 'ts-jest', 19 | }, 20 | preset: 'ts-jest', 21 | }; 22 | -------------------------------------------------------------------------------- /electron/constants.ts: -------------------------------------------------------------------------------- 1 | export const WINDOW_OPEN = 'window-open'; //打开窗口 2 | export const WINDOW_CLOSE = 'window-close'; // 关闭窗口 3 | export const WINDOW_PREVENT_CLOSE = 'window-prevent-close'; // 阻止窗口关闭事件 4 | 5 | export const UPDATE_ERROR = 'update-error'; // 更新出错 6 | export const UPDATE_CHECK = 'update-check'; // 检查更新 7 | export const UPDATE_CHECKING = 'update-checking'; // 检查更新中 8 | export const UPDATE_AVAILABLE = 'update-available'; // 检查到可用更新 9 | export const UPDATE_NOT_AVAILABLE = 'update-not-available'; // 无可用更新 10 | export const UPDATE_DOWNLOAD = 'update-download'; // 下载更新 11 | export const UPDATE_DOWNLOADING = 'update-downloading'; // 下载更新中(可获取下载进度) 12 | export const UPDATE_DOWNLOADED = 'update-downloaded'; // 下载更新完成 13 | export const UPDATE_INSTALL = 'update-install'; // 安装更新 14 | -------------------------------------------------------------------------------- /src/multiple/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hero from './components/hero'; 3 | import style from './App.module.scss'; 4 | import './App.scss'; 5 | import { beforeWindowClose } from 'electron-utils/window'; 6 | 7 | export default class App extends React.Component { 8 | componentDidMount() { 9 | beforeWindowClose(() => { 10 | return new Promise((resolve, reject) => { 11 | const value = confirm('确认关闭?'); 12 | resolve(value); 13 | }); 14 | }); 15 | } 16 | render() { 17 | return ( 18 |
19 |
20 | 21 |

使用 Electron 和 React 构建跨平台的桌面应用 demo

22 |
23 |

可多开窗口

24 |

25 | To get started, edit src/multiple/App.tsx and save to reload. 26 |

27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/picture/App.module.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | font-size: 1.5rem; 3 | text-align: center; 4 | 5 | header { 6 | padding: 60px 0 70px; 7 | color: #fff; 8 | background: #282c34; 9 | } 10 | 11 | code { 12 | color: red; 13 | } 14 | 15 | @media (min-width: 992px) { 16 | p { 17 | font-size: 2.4rem; 18 | } 19 | } 20 | 21 | @media (min-width: 768px) { 22 | p { 23 | font-size: 1.8rem; 24 | } 25 | } 26 | 27 | p { 28 | color: #9feaf9; 29 | font-size: 1.4rem; 30 | font-weight: 300; 31 | } 32 | 33 | .infoWrapper { 34 | padding: 0 1rem; 35 | 36 | > div { 37 | text-align: left; 38 | 39 | pre { 40 | word-break: break-word; 41 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; 42 | border-radius: 5px; 43 | box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; 44 | font-size: 12px; 45 | padding: 12px 10px; 46 | border-radius: 5px; 47 | border-radius: 0; 48 | padding-top: 9px; 49 | overflow: auto; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/picture/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | import { beforeWindowClose } from 'electron-utils/window'; 4 | import style from './App.module.scss'; 5 | import './App.scss'; 6 | 7 | const App = () => { 8 | const [images, setImages] = useState([]); 9 | 10 | const listener = useCallback((_, arg) => { 11 | console.log(arg); 12 | setImages(arg.img || []); 13 | }, []); 14 | 15 | useEffect(() => { 16 | ipcRenderer.on('picture:view', listener); 17 | return () => { 18 | ipcRenderer.removeListener('picture:view', listener); 19 | }; 20 | }, []); 21 | 22 | useEffect(() => { 23 | beforeWindowClose(() => { 24 | return new Promise((resolve) => { 25 | const value = confirm('确认关闭?'); 26 | resolve(value); 27 | }); 28 | }); 29 | }, []); 30 | 31 | return ( 32 |
33 |

图片查看窗口

34 | {images.map((img) => ( 35 | 36 | ))} 37 |
38 | ); 39 | }; 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /src/update/App.module.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | font-size: 1.5rem; 3 | text-align: center; 4 | 5 | header { 6 | padding: 60px 0 70px; 7 | color: #fff; 8 | background: #282c34; 9 | } 10 | 11 | code { 12 | color: red; 13 | } 14 | 15 | @media (min-width: 992px) { 16 | p { 17 | font-size: 2.4rem; 18 | } 19 | } 20 | 21 | @media (min-width: 768px) { 22 | p { 23 | font-size: 1.8rem; 24 | } 25 | } 26 | 27 | p { 28 | color: #9feaf9; 29 | font-size: 1.4rem; 30 | font-weight: 300; 31 | } 32 | 33 | .infoWrapper { 34 | padding: 0 1rem; 35 | 36 | > div { 37 | text-align: left; 38 | 39 | pre { 40 | word-break: break-word; 41 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; 42 | border-radius: 5px; 43 | box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; 44 | font-size: 12px; 45 | padding: 12px 10px; 46 | border-radius: 5px; 47 | border-radius: 0; 48 | padding-top: 9px; 49 | overflow: auto; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "noEmit": true, 12 | "jsx": "react", 13 | "baseUrl": "src", 14 | "paths": { 15 | "@/*": ["./renderer/*"], 16 | "electron-utils/*": ["../electron/for-renderer-utils/*"] 17 | }, 18 | "noUnusedParameters": false, 19 | "preserveConstEnums": true, 20 | "removeComments": false, 21 | "skipLibCheck": true, 22 | "strict": false, 23 | "experimentalDecorators": true, 24 | "plugins": [ 25 | { 26 | "name": "typescript-plugin-css-modules", 27 | "options": { 28 | "customMatcher": "\\.(c|le|sa|sc)ss$" 29 | } 30 | } 31 | ] 32 | }, 33 | "include": [ 34 | "type.d.ts", 35 | "src/*.ts", 36 | "src/*.tsx", 37 | "src/**/*", 38 | "*.js", 39 | "node_modules/@types", 40 | "electron" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env" 5 | ], 6 | "@babel/preset-typescript", 7 | "@babel/preset-react" 8 | ], 9 | "plugins": [ 10 | "@babel/plugin-transform-runtime", 11 | "react-hot-loader/babel", 12 | "@babel/plugin-syntax-dynamic-import", 13 | "@babel/plugin-syntax-import-meta", 14 | "@babel/plugin-proposal-class-properties", 15 | "@babel/plugin-proposal-json-strings", 16 | [ 17 | "@babel/plugin-proposal-decorators", 18 | { 19 | "legacy": true 20 | } 21 | ], 22 | "@babel/plugin-proposal-function-sent", 23 | "@babel/plugin-proposal-export-namespace-from", 24 | "@babel/plugin-proposal-numeric-separator", 25 | "@babel/plugin-proposal-throw-expressions", 26 | "@babel/plugin-proposal-export-default-from", 27 | "@babel/plugin-proposal-logical-assignment-operators", 28 | "@babel/plugin-proposal-optional-chaining", 29 | [ 30 | "@babel/plugin-proposal-pipeline-operator", 31 | { 32 | "proposal": "minimal" 33 | } 34 | ], 35 | "@babel/plugin-proposal-nullish-coalescing-operator", 36 | "@babel/plugin-proposal-do-expressions", 37 | "@babel/plugin-proposal-function-bind" 38 | ], 39 | "comments": false 40 | } 41 | -------------------------------------------------------------------------------- /src/picture/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', 'Oxygen', 'Ubuntu', 9 | 'Cantarell', 'Open Sans', sans-serif; 10 | } 11 | 12 | button { 13 | line-height: 1.5715; 14 | position: relative; 15 | display: inline-block; 16 | font-weight: 400; 17 | white-space: nowrap; 18 | text-align: center; 19 | background-image: none; 20 | -webkit-box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); 21 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); 22 | cursor: pointer; 23 | -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 24 | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 25 | -webkit-user-select: none; 26 | -moz-user-select: none; 27 | -ms-user-select: none; 28 | user-select: none; 29 | -ms-touch-action: manipulation; 30 | touch-action: manipulation; 31 | height: 32px; 32 | padding: 4px 15px; 33 | font-size: 14px; 34 | border-radius: 2px; 35 | color: rgba(0, 0, 0, 0.85); 36 | background: #fff; 37 | border: 1px solid #d9d9d9; 38 | outline: none; 39 | 40 | &[disabled] { 41 | cursor: not-allowed; 42 | color: rgba(0, 0, 0, 0.25); 43 | background: #f5f5f5; 44 | border-color: #d9d9d9; 45 | text-shadow: none; 46 | -webkit-box-shadow: none; 47 | box-shadow: none; 48 | } 49 | 50 | &.primary { 51 | color: #fff; 52 | background: #1890ff; 53 | border-color: #1890ff; 54 | } 55 | 56 | + button { 57 | margin-left: 1rem; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/update/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', 'Oxygen', 'Ubuntu', 9 | 'Cantarell', 'Open Sans', sans-serif; 10 | } 11 | 12 | button { 13 | line-height: 1.5715; 14 | position: relative; 15 | display: inline-block; 16 | font-weight: 400; 17 | white-space: nowrap; 18 | text-align: center; 19 | background-image: none; 20 | -webkit-box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); 21 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); 22 | cursor: pointer; 23 | -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 24 | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 25 | -webkit-user-select: none; 26 | -moz-user-select: none; 27 | -ms-user-select: none; 28 | user-select: none; 29 | -ms-touch-action: manipulation; 30 | touch-action: manipulation; 31 | height: 32px; 32 | padding: 4px 15px; 33 | font-size: 14px; 34 | border-radius: 2px; 35 | color: rgba(0, 0, 0, 0.85); 36 | background: #fff; 37 | border: 1px solid #d9d9d9; 38 | outline: none; 39 | 40 | &[disabled] { 41 | cursor: not-allowed; 42 | color: rgba(0, 0, 0, 0.25); 43 | background: #f5f5f5; 44 | border-color: #d9d9d9; 45 | text-shadow: none; 46 | -webkit-box-shadow: none; 47 | box-shadow: none; 48 | } 49 | 50 | &.primary { 51 | color: #fff; 52 | background: #1890ff; 53 | border-color: #1890ff; 54 | } 55 | 56 | + button { 57 | margin-left: 1rem; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-react-boilerplate 2 | 3 | > [English](./README_EN.md) 4 | 5 | ### 使用 React.js 开发 Electron App 应用 6 | 7 | [![Build Status](https://travis-ci.org/fantasticit/electron-react-boilerplate.svg?branch=master)](https://travis-ci.org/fantasticit/electron-react-boilerplate) 8 | 9 | ## 截图 10 | 11 | ![electron-react-boilerplate](https://user-images.githubusercontent.com/26452939/100304935-dce05f00-2fda-11eb-98f5-5af5bfd46a1a.gif) 12 | 13 | ## 特性 14 | 15 | - 支持 React:各窗口均采用 React 开发,也可根据需要更改为其他框架 16 | - 支持多窗口:修改 `erb.config.js` 中 windows 配置(配置和 electron BrowserWindows 构造参数一致) 17 | - 支持 Touchbar:可根据需要在 `electron/set-touchbar.ts` 更改 18 | - 支持 Tray:可根据需要在 `electron/set-tray.ts` 更改 19 | - 支持 Dock:可根据需要在 `electron/set-dock-menu` 更改 20 | - 支持更新:`package.json` 中的 `build` 配置 21 | 22 | ## 安装 23 | 24 | 1. 使用 git clone: 25 | 26 | ```bash 27 | git clone --depth=1 https://github.com/fantasticit/electron-react-boilerplate.git your-project-name 28 | ``` 29 | 30 | 2. 安装依赖 31 | 32 | ```bash 33 | cd your-project-name 34 | npm install 35 | ``` 36 | 37 | ## 运行 38 | 39 | 开发模式下,运行本项目会开启一个 `renderer` 进程(支持模块热替换,即: **hot-module-replacement**)和一个 `electron` 主进程. 40 | 41 | ```bash 42 | npm run dev 43 | ``` 44 | 45 | 端口默认为: `8080`,如果需要指定其他端口,命令如下: 46 | 47 | ```bash 48 | npm run dev other-port // such as npm run dev 9090 49 | ``` 50 | 51 | ## 测试 52 | 53 | 本项目使用 `Jest` 进行测试: 54 | 55 | ```bash 56 | npm test 57 | ``` 58 | 59 | 编辑 `jest.config.js` 以更改测试配置. 60 | 61 | ## 打包 62 | 63 | 运行: 64 | 65 | ```bash 66 | npm run build 67 | ``` 68 | 69 | 编辑 `package.json` 中相关字段,可以使用其他图标: 70 | 71 | ```json 72 | "build": { 73 | "mac": { 74 | "icon": "icons/icon.icns" 75 | }, 76 | "win": { 77 | "icon": "icons/icon.ico" 78 | }, 79 | "linux": { 80 | "icon": "icons" 81 | } 82 | } 83 | ``` 84 | 85 | 修改 `index.html` 的标题,即可修改打包后 App 名称. 86 | 87 | ## License 88 | 89 | MIT 90 | -------------------------------------------------------------------------------- /src/main/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', 'Oxygen', 'Ubuntu', 9 | 'Cantarell', 'Open Sans', sans-serif; 10 | } 11 | 12 | button { 13 | line-height: 1.5715; 14 | position: relative; 15 | display: inline-block; 16 | font-weight: 400; 17 | white-space: nowrap; 18 | text-align: center; 19 | background-image: none; 20 | -webkit-box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); 21 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); 22 | cursor: pointer; 23 | -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 24 | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 25 | -webkit-user-select: none; 26 | -moz-user-select: none; 27 | -ms-user-select: none; 28 | user-select: none; 29 | -ms-touch-action: manipulation; 30 | touch-action: manipulation; 31 | height: 32px; 32 | padding: 4px 15px; 33 | font-size: 14px; 34 | border-radius: 2px; 35 | color: rgba(0, 0, 0, 0.85); 36 | background: #fff; 37 | border: 1px solid #d9d9d9; 38 | outline: none; 39 | 40 | &.primary { 41 | color: #fff; 42 | background: #1890ff; 43 | border-color: #1890ff; 44 | } 45 | 46 | + button { 47 | margin-left: 1rem; 48 | } 49 | } 50 | 51 | input { 52 | -webkit-box-sizing: border-box; 53 | box-sizing: border-box; 54 | margin: 0; 55 | font-variant: tabular-nums; 56 | list-style: none; 57 | -webkit-font-feature-settings: 'tnum'; 58 | font-feature-settings: 'tnum'; 59 | position: relative; 60 | display: inline-block; 61 | width: 400px; 62 | min-width: 0; 63 | padding: 4px 11px; 64 | color: rgba(0, 0, 0, 0.85); 65 | font-size: 14px; 66 | line-height: 1.5715; 67 | background-color: #fff; 68 | background-image: none; 69 | border: 1px solid #d9d9d9; 70 | border-radius: 2px; 71 | outline: none; 72 | } 73 | -------------------------------------------------------------------------------- /electron/for-renderer-utils/window.ts: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | import { WINDOW_OPEN, WINDOW_CLOSE, WINDOW_PREVENT_CLOSE } from '../constants'; 3 | import { ipc } from './ipc'; 4 | 5 | /** 6 | * 开启窗口 7 | * @param name 8 | */ 9 | export const openWindow = (name: string) => ipc(WINDOW_OPEN, { name }); 10 | 11 | /** 12 | * 关闭窗口 13 | * @param name 14 | */ 15 | export const closeWindow = (name: string) => ipc(WINDOW_CLOSE, { name }); 16 | 17 | /** 18 | * 向指定窗口发送消息 19 | * @param name 20 | * @param message 21 | * @param body 22 | */ 23 | export const sendToWindow = (name: string, message: string, body: any) => { 24 | name = name.toLowerCase(); 25 | const window = remote.BrowserWindow.getAllWindows().find((window) => name === window['name']); 26 | if (!window) return; 27 | if (window.isVisible()) { 28 | window.webContents.send(message, body); 29 | } else { 30 | window.once('show', () => { 31 | window.webContents.send(message, body); 32 | }); 33 | } 34 | }; 35 | 36 | /** 37 | * 向所有窗口发送消息 38 | * @param message 39 | * @param body 40 | */ 41 | export const sendToAllWindow = (message: string, body: any) => { 42 | remote.BrowserWindow.getAllWindows().forEach((window) => { 43 | if (window.isVisible()) { 44 | window.webContents.send(message, body); 45 | } else { 46 | window.once('show', () => { 47 | window.webContents.send(message, body); 48 | }); 49 | } 50 | }); 51 | }; 52 | /** 53 | * 当关闭窗口前执行回调 54 | * @param callback () => Promise 55 | */ 56 | export const beforeWindowClose = (callback: () => Promise) => { 57 | const window = remote.getCurrentWindow(); 58 | const listener = async (evt) => { 59 | evt.preventDefault(); 60 | 61 | try { 62 | const value = await callback(); 63 | if (value) { 64 | window.removeListener('close', listener); 65 | window.destroy(); 66 | } 67 | } catch (e) {} 68 | }; 69 | // 通知主进程阻止关闭窗口 70 | window.on('close', listener); 71 | ipc(WINDOW_PREVENT_CLOSE, { name: window['name'] }); 72 | }; 73 | -------------------------------------------------------------------------------- /src/main/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { openWindow, closeWindow, sendToWindow } from 'electron-utils/window'; 3 | import Hero from './components/hero'; 4 | import style from './App.module.scss'; 5 | import './App.scss'; 6 | 7 | const DEFAULT_IMG = 8 | 'https://user-images.githubusercontent.com/26452939/100304935-dce05f00-2fda-11eb-98f5-5af5bfd46a1a.gif'; 9 | 10 | export default class App extends React.Component { 11 | state = { 12 | img: DEFAULT_IMG, 13 | }; 14 | 15 | open = (name) => { 16 | openWindow(name).then(() => console.log('ok')); 17 | }; 18 | 19 | close = (name) => { 20 | closeWindow(name).then(() => console.log('ok')); 21 | }; 22 | 23 | viewPicture = () => { 24 | openWindow('picture').then(() => { 25 | sendToWindow('picture', 'picture:view', { img: [this.state.img] }); 26 | }); 27 | }; 28 | 29 | render() { 30 | return ( 31 |
32 |
33 | 34 |

使用 Electron 和 React 构建跨平台的桌面应用

35 |
36 |
37 | 40 | 41 | 42 | 45 | 46 |
47 |
48 | this.setState({ img: e.target.value })} 52 | /> 53 | 56 |
57 |

58 | To get started, edit src/main/App.tsx and save to reload. 59 |

60 |
61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /electron/for-renderer-utils/update.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import { 3 | UPDATE_DOWNLOAD, 4 | UPDATE_DOWNLOADING, 5 | UPDATE_DOWNLOADED, 6 | UPDATE_CHECK, 7 | UPDATE_ERROR, 8 | UPDATE_CHECKING, 9 | UPDATE_AVAILABLE, 10 | UPDATE_NOT_AVAILABLE, 11 | UPDATE_INSTALL, 12 | } from '../constants'; 13 | import { ipc } from './ipc'; 14 | 15 | const noop = (arg = null) => {}; 16 | 17 | let isUpdateDownloaded = false; 18 | /** 19 | * 检查更新 20 | * @param param0 21 | */ 22 | export const checkUpdate = ({ 23 | onChecking = noop, 24 | onAvailable = noop, 25 | onNoAvailable = noop, 26 | onError = noop, 27 | }: { 28 | onChecking?: (arg) => void; // 正在检查 29 | onAvailable?: (arg) => void; // 可以更新 30 | onNoAvailable?: (arg) => void; // 无更新 31 | onError?: (arg) => void; // 更新出错 32 | }) => { 33 | ipcRenderer.once(UPDATE_CHECKING, (_, arg) => onChecking(arg)); 34 | ipcRenderer.once(UPDATE_AVAILABLE, (_, arg) => onAvailable(arg)); 35 | ipcRenderer.once(UPDATE_NOT_AVAILABLE, (_, arg) => onNoAvailable(arg)); 36 | ipcRenderer.once(UPDATE_ERROR, (_, arg) => onError(arg)); 37 | ipc(UPDATE_CHECK); 38 | }; 39 | 40 | /** 41 | * 下载更新 42 | * @param param0 43 | */ 44 | export const downloadUpdate = ({ 45 | onProgress = noop, 46 | onDownloaded = noop, 47 | onError = noop, 48 | }: { 49 | onProgress?: (arg) => void; // 更新进度 50 | onDownloaded?: (arg) => void; // 下载完成 51 | onError?: (arg) => void; // 更新出错 52 | }) => { 53 | ipcRenderer.once(UPDATE_ERROR, (_, arg) => onError(arg)); 54 | ipcRenderer.once(UPDATE_DOWNLOADING, (_, arg) => onProgress(arg)); 55 | ipcRenderer.once(UPDATE_DOWNLOADED, (_, arg) => { 56 | isUpdateDownloaded = true; 57 | onDownloaded(arg); 58 | }); 59 | ipc(UPDATE_DOWNLOAD); 60 | }; 61 | 62 | /** 63 | * 安装更新 64 | */ 65 | export const installUpdate = () => { 66 | if (!isUpdateDownloaded) return; 67 | isUpdateDownloaded = false; 68 | void [ 69 | UPDATE_CHECKING, 70 | UPDATE_AVAILABLE, 71 | UPDATE_NOT_AVAILABLE, 72 | UPDATE_ERROR, 73 | UPDATE_DOWNLOADING, 74 | UPDATE_DOWNLOADED, 75 | ].forEach((type) => ipcRenderer.removeAllListeners(type)); 76 | ipc(UPDATE_INSTALL); 77 | }; 78 | -------------------------------------------------------------------------------- /electron/set-up.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, globalShortcut } from 'electron'; 2 | import { isMac, isProd } from './utils'; 3 | import { WindowsManager } from './windows-manager'; 4 | 5 | const mainWindowName = (() => { 6 | const target = RUNTIME_CONFIG.windows.find((window) => window.isMain); 7 | return (target && target.name) || 'main'; 8 | })(); 9 | const windows = RUNTIME_CONFIG.windows.reduce((all, window) => { 10 | all[window.name] = { 11 | url: isProd 12 | ? `file://${__dirname}/${window.name}.html` 13 | : `http://localhost:${DEV_RENDERER_PORT}/${window.name}.html`, 14 | isMain: window.name === mainWindowName, 15 | ...window, 16 | }; 17 | return all; 18 | }, {}); 19 | let mainWindow: BrowserWindow | null = null; 20 | let isQuiting; 21 | 22 | export const setUp = (): Promise<{ windowsManger: WindowsManager; mainWindow: BrowserWindow }> => { 23 | return new Promise((resolve, reject) => { 24 | if (!mainWindowName) reject(`No main window find!`); 25 | 26 | const windowsManger = new WindowsManager(windows); 27 | const createMainWindow = () => { 28 | mainWindow = windowsManger.show(mainWindowName); 29 | mainWindow.on('close', function (event) { 30 | if (isProd) { 31 | if (!isQuiting) { 32 | event.preventDefault(); 33 | mainWindow.hide(); 34 | return false; 35 | } 36 | } else { 37 | mainWindow = null; 38 | } 39 | }); 40 | mainWindow.addListener('close', () => { 41 | windowsManger.closeAll(mainWindowName); 42 | }); 43 | }; 44 | 45 | app.on('activate', () => { 46 | if (mainWindow) { 47 | mainWindow.show(); 48 | } else { 49 | createMainWindow(); 50 | } 51 | }); 52 | app.on('ready', () => { 53 | globalShortcut.register('CommandOrControl+Shift+L', () => { 54 | let focusWin = BrowserWindow.getFocusedWindow(); 55 | focusWin && (focusWin).toggleDevTools(); 56 | }); 57 | createMainWindow(); 58 | resolve({ windowsManger, mainWindow }); 59 | }); 60 | app.on('before-quit', () => { 61 | isQuiting = true; 62 | }); 63 | app.on('window-all-closed', () => !isMac && app.quit()); 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /electron/set-update.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import { autoUpdater } from 'electron-updater'; 3 | import { 4 | UPDATE_DOWNLOAD, 5 | UPDATE_DOWNLOADING, 6 | UPDATE_DOWNLOADED, 7 | UPDATE_CHECK, 8 | UPDATE_ERROR, 9 | UPDATE_CHECKING, 10 | UPDATE_AVAILABLE, 11 | UPDATE_NOT_AVAILABLE, 12 | UPDATE_INSTALL, 13 | } from './constants'; 14 | import { WindowsManager } from './windows-manager'; 15 | 16 | const UPDATE_URL = RUNTIME_CONFIG.update || (PACKAGE_JSON.build && PACKAGE_JSON.build.publish); 17 | 18 | export const setUpdate = (windowManger: WindowsManager) => { 19 | if (!UPDATE_URL) { 20 | console.log('未配置 update.url,不配置自动更新机制'); 21 | return; 22 | } 23 | const send = (cb) => { 24 | const windows = windowManger.getAllWindows(); 25 | windows.forEach((window) => { 26 | cb(window); 27 | }); 28 | }; 29 | autoUpdater.autoDownload = false; 30 | autoUpdater.setFeedURL(UPDATE_URL); 31 | // 检查更新出错 32 | autoUpdater.on('error', function (error) { 33 | send((window) => 34 | window.webContents.send(UPDATE_ERROR, { 35 | msg: error.message || error, 36 | }) 37 | ); 38 | }); 39 | // 正在检查更新 40 | autoUpdater.on('checking-for-update', function (info) { 41 | send((window) => window.webContents.send(UPDATE_CHECKING, info)); 42 | }); 43 | // 检测到新版本,正在下载 44 | autoUpdater.on('update-available', function (info) { 45 | send((window) => window.webContents.send(UPDATE_AVAILABLE, info)); 46 | }); 47 | // 无可用更新 48 | autoUpdater.on('update-not-available', function (info) { 49 | send((window) => window.webContents.send(UPDATE_NOT_AVAILABLE, info)); 50 | }); 51 | // 下载进度 52 | autoUpdater.on('download-progress', function (progress) { 53 | send((window) => window.webContents.send(UPDATE_DOWNLOADING, progress)); 54 | }); 55 | autoUpdater.on( 56 | 'update-downloaded', 57 | function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) { 58 | // 渲染进程要求安装时,进行安装 59 | ipcMain.on(UPDATE_INSTALL, () => { 60 | autoUpdater.quitAndInstall(); 61 | }); 62 | // 向渲染进程发送下载完成信息 63 | send((window) => 64 | window.webContents.send(UPDATE_DOWNLOADED, { 65 | releaseNotes, 66 | releaseName, 67 | releaseDate, 68 | updateUrl, 69 | }) 70 | ); 71 | } 72 | ); 73 | // 渲染进程要求检查更新时,检查更新 74 | ipcMain.on(UPDATE_CHECK, () => { 75 | autoUpdater.checkForUpdates(); 76 | }); 77 | // 下载 78 | ipcMain.on(UPDATE_DOWNLOAD, () => { 79 | autoUpdater.downloadUpdate(); 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /electron/set-menu.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu, MenuItemConstructorOptions, shell } from 'electron'; 2 | import { isMac } from './utils'; 3 | 4 | const template = [ 5 | ...(isMac 6 | ? [ 7 | { 8 | label: app.getName(), 9 | submenu: [ 10 | { role: 'about' }, 11 | { type: 'separator' }, 12 | { role: 'services' }, 13 | { type: 'separator' }, 14 | { role: 'hide' }, 15 | { role: 'hideothers' }, 16 | { role: 'unhide' }, 17 | { type: 'separator' }, 18 | { role: 'quit' }, 19 | ], 20 | }, 21 | ] 22 | : []), 23 | { 24 | label: 'File', 25 | submenu: [isMac ? { role: 'close' } : { role: 'quit' }], 26 | }, 27 | { 28 | label: 'Edit', 29 | submenu: [ 30 | { role: 'undo' }, 31 | { role: 'redo' }, 32 | { type: 'separator' }, 33 | { role: 'cut' }, 34 | { role: 'copy' }, 35 | { role: 'paste' }, 36 | ...(isMac 37 | ? [ 38 | { role: 'pasteAndMatchStyle' }, 39 | { role: 'delete' }, 40 | { role: 'selectAll' }, 41 | { type: 'separator' }, 42 | { 43 | label: 'Speech', 44 | submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }], 45 | }, 46 | ] 47 | : [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]), 48 | ], 49 | }, 50 | { 51 | label: 'View', 52 | submenu: [ 53 | { role: 'reload' }, 54 | { role: 'forceReload' }, 55 | { role: 'toggleDevTools' }, 56 | { type: 'separator' }, 57 | { role: 'resetZoom' }, 58 | { role: 'zoomIn' }, 59 | { role: 'zoomOut' }, 60 | { type: 'separator' }, 61 | { role: 'togglefullscreen' }, 62 | ], 63 | }, 64 | { 65 | label: 'Window', 66 | submenu: [ 67 | { role: 'minimize' }, 68 | { role: 'zoom' }, 69 | ...(isMac 70 | ? [{ type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' }] 71 | : [{ role: 'close' }]), 72 | ], 73 | }, 74 | { 75 | role: 'help', 76 | submenu: [ 77 | { 78 | label: 'Github', 79 | click: () => { 80 | shell.openExternal('https://github.com/fantasticit/electron-react-boilerplate'); 81 | }, 82 | }, 83 | { 84 | label: 'Learn More', 85 | click: () => { 86 | shell.openExternal('https://electronjs.org'); 87 | }, 88 | }, 89 | ], 90 | }, 91 | ]; 92 | 93 | export const setMenu = () => { 94 | const menu = Menu.buildFromTemplate(template as Array); 95 | Menu.setApplicationMenu(menu); 96 | }; 97 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # electron-react-boilerplate 2 | 3 | ### A boilerplate for developing Electron App with React and TypeScript 4 | 5 | [![Build Status](https://travis-ci.org/fantasticit/electron-react-boilerplate.svg?branch=master)](https://travis-ci.org/fantasticit/electron-react-boilerplate) 6 | 7 | ## Screenshot 8 | 9 | ![electron-react-boilerplate](https://user-images.githubusercontent.com/26452939/100304935-dce05f00-2fda-11eb-98f5-5af5bfd46a1a.gif) 10 | 11 | ## Features 12 | 13 | - React Support: every window is developed by React, and you can switch to other fe framework 14 | - Multiple Windows Support: the `windows` config in `erb.config.js` file, config is same like electron BrowserWindows Construct Options 15 | - Touchbar Support:change it in `electron/set-touchbar.ts` if you need 16 | - Tray Support:change it in `electron/set-tray.ts` if you need 17 | - Dock Support:change it in `electron/set-dock-menu.ts` if you need 18 | - Update Support: the `build` config in `package.json` file 19 | 20 | ## Install 21 | 22 | 1. clone the repo via git: 23 | 24 | ```bash 25 | git clone --depth=1 https://github.com/fantasticit/electron-react-boilerplate.git your-project-name 26 | ``` 27 | 28 | 2. install dependencies with npm(or yarn). 29 | 30 | ```bash 31 | cd your-project-name 32 | npm install 33 | ``` 34 | 35 | ## Run 36 | 37 | Start the app in the `dev` enviroment. This starts the renderer proess in **hot-module-replacement** mode and starts a webpack dev server that sends hot updates to the renderer process: 38 | 39 | ```bash 40 | npm run dev 41 | ``` 42 | 43 | The app will run at `http://localhost:8080` default, if you want to run it at other port, just run: 44 | 45 | ```bash 46 | npm run dev other-port // such as npm run dev 9090 47 | ``` 48 | 49 | ## Test 50 | 51 | The project uses `Jest` to test: 52 | 53 | ```bash 54 | npm test 55 | ``` 56 | 57 | If you want to change the test config, edit the `jest.config.js`. 58 | 59 | ## Packaging 60 | 61 | To package the app: 62 | 63 | ```bash 64 | npm run build 65 | ``` 66 | 67 | If you want alter the icon, you can edit the `package.json`: 68 | 69 | ```json 70 | "build": { 71 | "mac": { 72 | "icon": "icons/icon.icns" 73 | }, 74 | "win": { 75 | "icon": "icons/icon.ico" 76 | }, 77 | "linux": { 78 | "icon": "icons" 79 | } 80 | } 81 | ``` 82 | 83 | To alter the app's name, just edit the `index.html`'s title. 84 | 85 | ## Module Structure 86 | 87 | In develeopment, this boilerpolate uses two process to run app. One is `main process`, anoter is `renderer process`. If you edit the `src/main/index.js`, the electron will restart automatically, and you can see information in the console. As same, edit the `src/renderer`, the app's UI will hot update. 88 | 89 | ## License 90 | 91 | MIT 92 | -------------------------------------------------------------------------------- /electron/set-touchbar.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, TouchBar } from 'electron'; 2 | import { isMac } from './utils'; 3 | const { TouchBarLabel, TouchBarButton, TouchBarSpacer } = TouchBar; 4 | 5 | export const setTouchbar = (window: BrowserWindow) => { 6 | if (!isMac) return; 7 | 8 | let spinning = false; 9 | let timer = null; 10 | 11 | // Reel labels 12 | const reel1 = new TouchBarLabel({}); 13 | const reel2 = new TouchBarLabel({}); 14 | const reel3 = new TouchBarLabel({}); 15 | 16 | // Spin result label 17 | const result = new TouchBarLabel({}); 18 | 19 | // Spin button 20 | const spin = new TouchBarButton({ 21 | label: '🎰 Spin', 22 | backgroundColor: '#7851A9', 23 | click: () => { 24 | // Ignore clicks if already spinning 25 | if (spinning) { 26 | clearTimeout(timer); 27 | finishSpin(); 28 | return; 29 | } 30 | 31 | spinning = true; 32 | result.label = ''; 33 | 34 | let timeout = 10; 35 | const spinLength = 4 * 1000; // 4 seconds 36 | const startTime = Date.now(); 37 | 38 | const spinReels = () => { 39 | updateReels(); 40 | 41 | if (Date.now() - startTime >= spinLength) { 42 | clearTimeout(timer); 43 | finishSpin(); 44 | } else { 45 | // Slow down a bit on each spin 46 | timeout *= 1.1; 47 | timer = setTimeout(spinReels, timeout); 48 | } 49 | }; 50 | 51 | spinReels(); 52 | }, 53 | }); 54 | 55 | const getRandomValue = () => { 56 | const values = ['🍒', '💎', '7️⃣', '🍊', '🔔', '⭐', '🍇', '🍀']; 57 | return values[Math.floor(Math.random() * values.length)]; 58 | }; 59 | 60 | const updateReels = () => { 61 | reel1.label = getRandomValue(); 62 | reel2.label = getRandomValue(); 63 | reel3.label = getRandomValue(); 64 | }; 65 | 66 | const finishSpin = () => { 67 | const uniqueValues = new Set([reel1.label, reel2.label, reel3.label]).size; 68 | if (uniqueValues === 1) { 69 | // All 3 values are the same 70 | result.label = '💰 Jackpot!'; 71 | result.textColor = '#FDFF00'; 72 | } else if (uniqueValues === 2) { 73 | // 2 values are the same 74 | result.label = '😍 Winner!'; 75 | result.textColor = '#FDFF00'; 76 | } else { 77 | // No values are the same 78 | result.label = '🙁 Spin Again'; 79 | result.textColor = null; 80 | } 81 | spinning = false; 82 | }; 83 | 84 | const touchBar = new TouchBar({ 85 | items: [ 86 | spin, 87 | new TouchBarSpacer({ size: 'large' }), 88 | reel1, 89 | new TouchBarSpacer({ size: 'small' }), 90 | reel2, 91 | new TouchBarSpacer({ size: 'small' }), 92 | reel3, 93 | new TouchBarSpacer({ size: 'large' }), 94 | result, 95 | ], 96 | }); 97 | window.setTouchBar(touchBar); 98 | }; 99 | -------------------------------------------------------------------------------- /src/update/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { checkUpdate, installUpdate, downloadUpdate } from 'electron-utils/update'; 3 | import Hero from './components/hero'; 4 | import style from './App.module.scss'; 5 | import './App.scss'; 6 | 7 | const App = () => { 8 | const [checking, setChecking] = useState(false); 9 | const [downloaded, setDownloaded] = useState(false); 10 | const [availableInfo, setAvailableInfo] = useState(null); 11 | const [notAvailableInfo, setNotAvailableInfo] = useState(null); 12 | const [error, setError] = useState(null); 13 | const [progress, setProgress] = useState(null); 14 | 15 | const check = useCallback(() => { 16 | checkUpdate({ 17 | onChecking(arg) { 18 | setChecking(true); 19 | console.log('checking', arg); 20 | }, 21 | onAvailable(arg) { 22 | setChecking(false); 23 | setAvailableInfo(arg); 24 | console.log('available', arg); 25 | }, 26 | onNoAvailable(arg) { 27 | setChecking(false); 28 | setNotAvailableInfo(arg); 29 | console.log('not available', arg); 30 | }, 31 | onError(error) { 32 | setError(error); 33 | console.error(error); 34 | }, 35 | }); 36 | }, []); 37 | 38 | const download = useCallback(() => { 39 | downloadUpdate({ 40 | onDownloaded(arg) { 41 | console.log('downloaded', arg); 42 | setDownloaded(true); 43 | }, 44 | onError(error) { 45 | setError(error); 46 | console.error('download', error); 47 | }, 48 | onProgress(progress) { 49 | setProgress(progress); 50 | console.log('progress', progress); 51 | }, 52 | }); 53 | }, []); 54 | 55 | return ( 56 |
57 |
58 | 59 |

使用 Electron 和 React 构建跨平台的桌面应用 demo

60 |
61 |

更新界面

62 |
63 | 64 | 65 | 73 |
74 |

75 | To get started, edit src/update/App.tsx and save to reload. 76 |

77 |
78 | {checking &&
检查更新中...
} 79 | {error && ( 80 |
81 |

出错信息:

82 |
{JSON.stringify(error, null, 2)}
83 |
84 | )} 85 | {availableInfo && ( 86 |
87 |

更新可用:

88 |
{JSON.stringify(availableInfo, null, 2)}
89 |
90 | )} 91 | {notAvailableInfo && ( 92 |
93 |

无可用更新:

94 |
{JSON.stringify(notAvailableInfo, null, 2)}
95 |
96 | )} 97 | {progress && ( 98 |
99 |

下载进度:

100 |
{JSON.stringify(progress, null, 2)}
101 |
102 | )} 103 | {downloaded &&
下载完成
} 104 |
105 |
106 | ); 107 | }; 108 | 109 | export default App; 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-react-boilerplate", 3 | "version": "1.0.0", 4 | "main": "./output/dist/electron.js", 5 | "scripts": { 6 | "test": "jest", 7 | "dev": "node build/dev-runner.js", 8 | "build": "node build/build.js && electron-builder" 9 | }, 10 | "build": { 11 | "productName": "ElectronReact", 12 | "appId": "******", 13 | "files": [ 14 | "output/dist/", 15 | "node_modules/", 16 | "package.json" 17 | ], 18 | "dmg": { 19 | "sign": false, 20 | "contents": [ 21 | { 22 | "x": 410, 23 | "y": 220, 24 | "type": "link", 25 | "path": "/Applications" 26 | }, 27 | { 28 | "x": 130, 29 | "y": 220, 30 | "type": "file" 31 | } 32 | ] 33 | }, 34 | "win": { 35 | "target": [ 36 | "nsis", 37 | "msi" 38 | ] 39 | }, 40 | "linux": { 41 | "target": [ 42 | "deb", 43 | "rpm", 44 | "AppImage" 45 | ], 46 | "category": "Development" 47 | }, 48 | "directories": { 49 | "buildResources": "resources", 50 | "output": "output/release" 51 | }, 52 | "extraResources": [ 53 | "./resources/**" 54 | ], 55 | "publish": { 56 | "provider": "github", 57 | "owner": "fantasticit", 58 | "repo": "electron-react-boilerplate", 59 | "private": false 60 | } 61 | }, 62 | "keywords": [ 63 | "electron", 64 | "boilerplate", 65 | "react", 66 | "redux", 67 | "sass", 68 | "webpack4", 69 | "hot", 70 | "reload" 71 | ], 72 | "author": { 73 | "name": "fantasticit", 74 | "email": "fantasticit@163.com", 75 | "url": "https://github.com/fantasticit" 76 | }, 77 | "license": "MIT", 78 | "devDependencies": { 79 | "@babel/cli": "^7.0.0", 80 | "@babel/core": "^7.0.0", 81 | "@babel/plugin-proposal-class-properties": "^7.0.0", 82 | "@babel/plugin-proposal-decorators": "^7.0.0", 83 | "@babel/plugin-proposal-do-expressions": "^7.0.0", 84 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 85 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 86 | "@babel/plugin-proposal-function-bind": "^7.0.0", 87 | "@babel/plugin-proposal-function-sent": "^7.0.0", 88 | "@babel/plugin-proposal-json-strings": "^7.0.0", 89 | "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", 90 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", 91 | "@babel/plugin-proposal-numeric-separator": "^7.0.0", 92 | "@babel/plugin-proposal-optional-chaining": "^7.0.0", 93 | "@babel/plugin-proposal-pipeline-operator": "^7.0.0", 94 | "@babel/plugin-proposal-throw-expressions": "^7.0.0", 95 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 96 | "@babel/plugin-syntax-import-meta": "^7.0.0", 97 | "@babel/plugin-transform-runtime": "^7.12.1", 98 | "@babel/preset-env": "^7.12.7", 99 | "@babel/preset-react": "^7.0.0", 100 | "@babel/preset-typescript": "^7.12.7", 101 | "@babel/runtime": "^7.0.0", 102 | "@types/enzyme": "^3.1.12", 103 | "@types/jest": "^23.3.5", 104 | "@types/react": "^16.3.14", 105 | "autoprefixer": "^8.5.0", 106 | "babel-core": "^7.0.0-bridge.0", 107 | "babel-jest": "^23.4.2", 108 | "babel-loader": "^8.0.0", 109 | "cache-loader": "^1.2.2", 110 | "chalk": "^2.4.1", 111 | "cross-env": "^5.2.0", 112 | "css-hot-loader": "^1.3.9", 113 | "css-loader": "^0.28.11", 114 | "electron": "3", 115 | "electron-builder": "^22.9.1", 116 | "electron-packager": "^12.1.0", 117 | "enzyme": "^3.3.0", 118 | "enzyme-adapter-react-16": "^1.1.1", 119 | "file-loader": "^1.1.11", 120 | "html-webpack-plugin": "^3.2.0", 121 | "identity-obj-proxy": "^3.0.0", 122 | "jest": "23", 123 | "mini-css-extract-plugin": "^0.4.0", 124 | "node-sass": "^4.9.0", 125 | "optimize-css-assets-webpack-plugin": "^4.0.1", 126 | "ora": "^2.1.0", 127 | "postcss-import": "^11.1.0", 128 | "postcss-loader": "^2.1.5", 129 | "postcss-url": "^7.3.2", 130 | "react-hot-loader": "^4.2.0", 131 | "react-test-renderer": "^17.0.1", 132 | "rimraf": "^2.6.2", 133 | "sass-loader": "^7.0.1", 134 | "terser-webpack-plugin": "4", 135 | "thread-loader": "^1.1.5", 136 | "ts-jest": "^23.0.1", 137 | "ts-loader": "^4.3.0", 138 | "typescript": "^2.8.3", 139 | "typescript-plugin-css-modules": "^2.4.0", 140 | "uglifyjs-webpack-plugin": "^1.2.5", 141 | "url-loader": "^1.0.1", 142 | "webpack": "^4.20.2", 143 | "webpack-cli": "^3.1.2", 144 | "webpack-dev-server": "^3.1.4", 145 | "webpack-hot-middleware": "^2.22.2", 146 | "webpack-merge": "^4.1.2" 147 | }, 148 | "dependencies": { 149 | "electron-updater": "^4.3.5", 150 | "react": "^17.0.1", 151 | "react-dom": "^17.0.1" 152 | }, 153 | "browserslist": [ 154 | "> 1%", 155 | "last 2 versions", 156 | "not ie <= 8" 157 | ] 158 | } 159 | -------------------------------------------------------------------------------- /src/main/components/hero.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | margin: 0 auto; 3 | backface-visibility: hidden; 4 | max-width: 900px; 5 | animation: hero-animation 1s forwards; 6 | } 7 | @media (min-width: 1200px) { 8 | .hero { 9 | max-width: 100%; 10 | } 11 | } 12 | 13 | @keyframes hero-animation { 14 | 0% { 15 | opacity: 0; 16 | transform: scale(0.96); 17 | } 18 | 100% { 19 | opacity: 1; 20 | transform: scale(1); 21 | } 22 | } 23 | 24 | .hero-icon { 25 | transform-origin: 50% 50%; 26 | stroke: #9feaf9; 27 | stroke-width: 5; 28 | stroke-linecap: round; 29 | } 30 | 31 | .hero-icon.dot { 32 | fill: #9feaf9; 33 | stroke: none; 34 | } 35 | 36 | .hero-icon--line { 37 | stroke-dasharray: 170; 38 | stroke-dashoffset: 170; 39 | } 40 | 41 | .hero-icon--circle { 42 | stroke-dasharray: 70; 43 | stroke-dashoffset: 70; 44 | } 45 | 46 | .hero-icon-1 { 47 | animation: hero-icon-animation 1s 0.7s cubic-bezier(0.05, 0.35, 0.2, 1) 48 | forwards; 49 | } 50 | 51 | .hero-icon-2 { 52 | animation: hero-icon-animation 1s 0.8s cubic-bezier(0.05, 0.35, 0.2, 1) 53 | forwards; 54 | } 55 | 56 | .hero-icon-3 { 57 | animation: hero-icon-animation 1s 0.9s cubic-bezier(0.05, 0.35, 0.2, 1) 58 | forwards; 59 | } 60 | 61 | .hero-icon-4 { 62 | animation: hero-icon-animation 1s 1s cubic-bezier(0.05, 0.35, 0.2, 1) forwards; 63 | } 64 | 65 | .hero-icon-5 { 66 | animation: hero-icon-animation 1s 1.1s cubic-bezier(0.05, 0.35, 0.2, 1) 67 | forwards; 68 | } 69 | 70 | .hero-icon-6 { 71 | animation: hero-icon-animation 1s 1.2s cubic-bezier(0.05, 0.35, 0.2, 1) 72 | forwards; 73 | } 74 | 75 | .hero-icon-7 { 76 | animation: hero-icon-animation 1s 1.3s cubic-bezier(0.05, 0.35, 0.2, 1) 77 | forwards; 78 | } 79 | 80 | .hero-icon-8 { 81 | animation: hero-icon-animation 1s 1.4s cubic-bezier(0.05, 0.35, 0.2, 1) 82 | forwards; 83 | } 84 | 85 | .hero-icon-9 { 86 | animation: hero-icon-animation 1s 1.5s cubic-bezier(0.05, 0.35, 0.2, 1) 87 | forwards; 88 | } 89 | 90 | @keyframes hero-icon-animation { 91 | 100% { 92 | stroke-dashoffset: 0; 93 | } 94 | } 95 | 96 | .hero-app { 97 | fill: #6798a2; 98 | transform-origin: 50% 50%; 99 | } 100 | 101 | @keyframes hero-app-animate-left { 102 | 0% { 103 | transform: scale(1) translateX(30px); 104 | opacity: 0.1; 105 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 0.5); 106 | } 107 | 20% { 108 | transform: scale(1.1) translateX(20px); 109 | animation-timing-function: cubic-bezier(0.1, 0.4, 0.2, 1); 110 | } 111 | 100% { 112 | transform: scale(1) translateX(0px); 113 | opacity: 1; 114 | } 115 | } 116 | 117 | @keyframes hero-app-animate-right { 118 | 0% { 119 | transform: scale(1) translateX(-30px); 120 | opacity: 0.1; 121 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 0.5); 122 | } 123 | 20% { 124 | transform: scale(1.1) translateX(-20px); 125 | animation-timing-function: cubic-bezier(0.1, 0.4, 0.2, 1); 126 | } 127 | 100% { 128 | transform: scale(1) translateX(0px); 129 | opacity: 1; 130 | } 131 | } 132 | 133 | .hero-app-1 { 134 | fill: #597f89; 135 | animation: hero-app-animate-right 0.8s 1.54s both; 136 | } 137 | 138 | .hero-app-2 { 139 | fill: #567a85; 140 | animation: hero-app-animate-left 0.8s 1.58s both; 141 | } 142 | 143 | .hero-app-3 { 144 | fill: #547681; 145 | animation: hero-app-animate-right 0.8s 1.62s both; 146 | } 147 | 148 | .hero-app-4 { 149 | fill: #51727d; 150 | animation: hero-app-animate-left 0.8s 1.66s both; 151 | } 152 | 153 | .hero-app-5 { 154 | fill: #4f6e79; 155 | animation: hero-app-animate-right 0.8s 1.7s both; 156 | } 157 | 158 | .hero-app-6 { 159 | fill: #4d6975; 160 | animation: hero-app-animate-left 0.8s 1.74s both; 161 | } 162 | 163 | .hero-app-7 { 164 | fill: #4a6571; 165 | animation: hero-app-animate-right 0.8s 1.78s both; 166 | } 167 | 168 | .hero-app-8 { 169 | fill: #48616d; 170 | animation: hero-app-animate-left 0.8s 1.82s both; 171 | } 172 | 173 | .hero-app-9 { 174 | fill: #455c68; 175 | animation: hero-app-animate-right 0.8s 1.86s both; 176 | } 177 | 178 | .hero-app-10 { 179 | fill: #435864; 180 | animation: hero-app-animate-left 0.8s 1.9s both; 181 | } 182 | 183 | .hero-app-11 { 184 | fill: #415460; 185 | animation: hero-app-animate-right 0.8s 1.94s both; 186 | } 187 | 188 | .hero-app-12 { 189 | fill: #3e505c; 190 | animation: hero-app-animate-left 0.8s 1.98s both; 191 | } 192 | 193 | .hero-app-13 { 194 | fill: #3c4b58; 195 | animation: hero-app-animate-right 0.8s 2.02s both; 196 | } 197 | 198 | .hero-app-14 { 199 | fill: #394754; 200 | animation: hero-app-animate-left 0.8s 2.06s both; 201 | } 202 | 203 | .hero-app-15 { 204 | fill: #374350; 205 | animation: hero-app-animate-right 0.8s 2.1s both; 206 | } 207 | 208 | .docs-version { 209 | display: inline-block; 210 | padding: 0 0.4em; 211 | margin-left: 10px; 212 | vertical-align: middle; 213 | font-size: 15.2px; 214 | line-height: 1.8; 215 | font-weight: 200; 216 | -webkit-font-smoothing: initial; 217 | background-color: white; 218 | border: 1px solid #d1d9db; 219 | border-radius: 3px; 220 | } 221 | -------------------------------------------------------------------------------- /src/multiple/components/hero.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | margin: 0 auto; 3 | backface-visibility: hidden; 4 | max-width: 900px; 5 | animation: hero-animation 1s forwards; 6 | } 7 | @media (min-width: 1200px) { 8 | .hero { 9 | max-width: 100%; 10 | } 11 | } 12 | 13 | @keyframes hero-animation { 14 | 0% { 15 | opacity: 0; 16 | transform: scale(0.96); 17 | } 18 | 100% { 19 | opacity: 1; 20 | transform: scale(1); 21 | } 22 | } 23 | 24 | .hero-icon { 25 | transform-origin: 50% 50%; 26 | stroke: #9feaf9; 27 | stroke-width: 5; 28 | stroke-linecap: round; 29 | } 30 | 31 | .hero-icon.dot { 32 | fill: #9feaf9; 33 | stroke: none; 34 | } 35 | 36 | .hero-icon--line { 37 | stroke-dasharray: 170; 38 | stroke-dashoffset: 170; 39 | } 40 | 41 | .hero-icon--circle { 42 | stroke-dasharray: 70; 43 | stroke-dashoffset: 70; 44 | } 45 | 46 | .hero-icon-1 { 47 | animation: hero-icon-animation 1s 0.7s cubic-bezier(0.05, 0.35, 0.2, 1) 48 | forwards; 49 | } 50 | 51 | .hero-icon-2 { 52 | animation: hero-icon-animation 1s 0.8s cubic-bezier(0.05, 0.35, 0.2, 1) 53 | forwards; 54 | } 55 | 56 | .hero-icon-3 { 57 | animation: hero-icon-animation 1s 0.9s cubic-bezier(0.05, 0.35, 0.2, 1) 58 | forwards; 59 | } 60 | 61 | .hero-icon-4 { 62 | animation: hero-icon-animation 1s 1s cubic-bezier(0.05, 0.35, 0.2, 1) forwards; 63 | } 64 | 65 | .hero-icon-5 { 66 | animation: hero-icon-animation 1s 1.1s cubic-bezier(0.05, 0.35, 0.2, 1) 67 | forwards; 68 | } 69 | 70 | .hero-icon-6 { 71 | animation: hero-icon-animation 1s 1.2s cubic-bezier(0.05, 0.35, 0.2, 1) 72 | forwards; 73 | } 74 | 75 | .hero-icon-7 { 76 | animation: hero-icon-animation 1s 1.3s cubic-bezier(0.05, 0.35, 0.2, 1) 77 | forwards; 78 | } 79 | 80 | .hero-icon-8 { 81 | animation: hero-icon-animation 1s 1.4s cubic-bezier(0.05, 0.35, 0.2, 1) 82 | forwards; 83 | } 84 | 85 | .hero-icon-9 { 86 | animation: hero-icon-animation 1s 1.5s cubic-bezier(0.05, 0.35, 0.2, 1) 87 | forwards; 88 | } 89 | 90 | @keyframes hero-icon-animation { 91 | 100% { 92 | stroke-dashoffset: 0; 93 | } 94 | } 95 | 96 | .hero-app { 97 | fill: #6798a2; 98 | transform-origin: 50% 50%; 99 | } 100 | 101 | @keyframes hero-app-animate-left { 102 | 0% { 103 | transform: scale(1) translateX(30px); 104 | opacity: 0.1; 105 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 0.5); 106 | } 107 | 20% { 108 | transform: scale(1.1) translateX(20px); 109 | animation-timing-function: cubic-bezier(0.1, 0.4, 0.2, 1); 110 | } 111 | 100% { 112 | transform: scale(1) translateX(0px); 113 | opacity: 1; 114 | } 115 | } 116 | 117 | @keyframes hero-app-animate-right { 118 | 0% { 119 | transform: scale(1) translateX(-30px); 120 | opacity: 0.1; 121 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 0.5); 122 | } 123 | 20% { 124 | transform: scale(1.1) translateX(-20px); 125 | animation-timing-function: cubic-bezier(0.1, 0.4, 0.2, 1); 126 | } 127 | 100% { 128 | transform: scale(1) translateX(0px); 129 | opacity: 1; 130 | } 131 | } 132 | 133 | .hero-app-1 { 134 | fill: #597f89; 135 | animation: hero-app-animate-right 0.8s 1.54s both; 136 | } 137 | 138 | .hero-app-2 { 139 | fill: #567a85; 140 | animation: hero-app-animate-left 0.8s 1.58s both; 141 | } 142 | 143 | .hero-app-3 { 144 | fill: #547681; 145 | animation: hero-app-animate-right 0.8s 1.62s both; 146 | } 147 | 148 | .hero-app-4 { 149 | fill: #51727d; 150 | animation: hero-app-animate-left 0.8s 1.66s both; 151 | } 152 | 153 | .hero-app-5 { 154 | fill: #4f6e79; 155 | animation: hero-app-animate-right 0.8s 1.7s both; 156 | } 157 | 158 | .hero-app-6 { 159 | fill: #4d6975; 160 | animation: hero-app-animate-left 0.8s 1.74s both; 161 | } 162 | 163 | .hero-app-7 { 164 | fill: #4a6571; 165 | animation: hero-app-animate-right 0.8s 1.78s both; 166 | } 167 | 168 | .hero-app-8 { 169 | fill: #48616d; 170 | animation: hero-app-animate-left 0.8s 1.82s both; 171 | } 172 | 173 | .hero-app-9 { 174 | fill: #455c68; 175 | animation: hero-app-animate-right 0.8s 1.86s both; 176 | } 177 | 178 | .hero-app-10 { 179 | fill: #435864; 180 | animation: hero-app-animate-left 0.8s 1.9s both; 181 | } 182 | 183 | .hero-app-11 { 184 | fill: #415460; 185 | animation: hero-app-animate-right 0.8s 1.94s both; 186 | } 187 | 188 | .hero-app-12 { 189 | fill: #3e505c; 190 | animation: hero-app-animate-left 0.8s 1.98s both; 191 | } 192 | 193 | .hero-app-13 { 194 | fill: #3c4b58; 195 | animation: hero-app-animate-right 0.8s 2.02s both; 196 | } 197 | 198 | .hero-app-14 { 199 | fill: #394754; 200 | animation: hero-app-animate-left 0.8s 2.06s both; 201 | } 202 | 203 | .hero-app-15 { 204 | fill: #374350; 205 | animation: hero-app-animate-right 0.8s 2.1s both; 206 | } 207 | 208 | .docs-version { 209 | display: inline-block; 210 | padding: 0 0.4em; 211 | margin-left: 10px; 212 | vertical-align: middle; 213 | font-size: 15.2px; 214 | line-height: 1.8; 215 | font-weight: 200; 216 | -webkit-font-smoothing: initial; 217 | background-color: white; 218 | border: 1px solid #d1d9db; 219 | border-radius: 3px; 220 | } 221 | -------------------------------------------------------------------------------- /src/picture/components/hero.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | margin: 0 auto; 3 | backface-visibility: hidden; 4 | max-width: 900px; 5 | animation: hero-animation 1s forwards; 6 | } 7 | @media (min-width: 1200px) { 8 | .hero { 9 | max-width: 100%; 10 | } 11 | } 12 | 13 | @keyframes hero-animation { 14 | 0% { 15 | opacity: 0; 16 | transform: scale(0.96); 17 | } 18 | 100% { 19 | opacity: 1; 20 | transform: scale(1); 21 | } 22 | } 23 | 24 | .hero-icon { 25 | transform-origin: 50% 50%; 26 | stroke: #9feaf9; 27 | stroke-width: 5; 28 | stroke-linecap: round; 29 | } 30 | 31 | .hero-icon.dot { 32 | fill: #9feaf9; 33 | stroke: none; 34 | } 35 | 36 | .hero-icon--line { 37 | stroke-dasharray: 170; 38 | stroke-dashoffset: 170; 39 | } 40 | 41 | .hero-icon--circle { 42 | stroke-dasharray: 70; 43 | stroke-dashoffset: 70; 44 | } 45 | 46 | .hero-icon-1 { 47 | animation: hero-icon-animation 1s 0.7s cubic-bezier(0.05, 0.35, 0.2, 1) 48 | forwards; 49 | } 50 | 51 | .hero-icon-2 { 52 | animation: hero-icon-animation 1s 0.8s cubic-bezier(0.05, 0.35, 0.2, 1) 53 | forwards; 54 | } 55 | 56 | .hero-icon-3 { 57 | animation: hero-icon-animation 1s 0.9s cubic-bezier(0.05, 0.35, 0.2, 1) 58 | forwards; 59 | } 60 | 61 | .hero-icon-4 { 62 | animation: hero-icon-animation 1s 1s cubic-bezier(0.05, 0.35, 0.2, 1) forwards; 63 | } 64 | 65 | .hero-icon-5 { 66 | animation: hero-icon-animation 1s 1.1s cubic-bezier(0.05, 0.35, 0.2, 1) 67 | forwards; 68 | } 69 | 70 | .hero-icon-6 { 71 | animation: hero-icon-animation 1s 1.2s cubic-bezier(0.05, 0.35, 0.2, 1) 72 | forwards; 73 | } 74 | 75 | .hero-icon-7 { 76 | animation: hero-icon-animation 1s 1.3s cubic-bezier(0.05, 0.35, 0.2, 1) 77 | forwards; 78 | } 79 | 80 | .hero-icon-8 { 81 | animation: hero-icon-animation 1s 1.4s cubic-bezier(0.05, 0.35, 0.2, 1) 82 | forwards; 83 | } 84 | 85 | .hero-icon-9 { 86 | animation: hero-icon-animation 1s 1.5s cubic-bezier(0.05, 0.35, 0.2, 1) 87 | forwards; 88 | } 89 | 90 | @keyframes hero-icon-animation { 91 | 100% { 92 | stroke-dashoffset: 0; 93 | } 94 | } 95 | 96 | .hero-app { 97 | fill: #6798a2; 98 | transform-origin: 50% 50%; 99 | } 100 | 101 | @keyframes hero-app-animate-left { 102 | 0% { 103 | transform: scale(1) translateX(30px); 104 | opacity: 0.1; 105 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 0.5); 106 | } 107 | 20% { 108 | transform: scale(1.1) translateX(20px); 109 | animation-timing-function: cubic-bezier(0.1, 0.4, 0.2, 1); 110 | } 111 | 100% { 112 | transform: scale(1) translateX(0px); 113 | opacity: 1; 114 | } 115 | } 116 | 117 | @keyframes hero-app-animate-right { 118 | 0% { 119 | transform: scale(1) translateX(-30px); 120 | opacity: 0.1; 121 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 0.5); 122 | } 123 | 20% { 124 | transform: scale(1.1) translateX(-20px); 125 | animation-timing-function: cubic-bezier(0.1, 0.4, 0.2, 1); 126 | } 127 | 100% { 128 | transform: scale(1) translateX(0px); 129 | opacity: 1; 130 | } 131 | } 132 | 133 | .hero-app-1 { 134 | fill: #597f89; 135 | animation: hero-app-animate-right 0.8s 1.54s both; 136 | } 137 | 138 | .hero-app-2 { 139 | fill: #567a85; 140 | animation: hero-app-animate-left 0.8s 1.58s both; 141 | } 142 | 143 | .hero-app-3 { 144 | fill: #547681; 145 | animation: hero-app-animate-right 0.8s 1.62s both; 146 | } 147 | 148 | .hero-app-4 { 149 | fill: #51727d; 150 | animation: hero-app-animate-left 0.8s 1.66s both; 151 | } 152 | 153 | .hero-app-5 { 154 | fill: #4f6e79; 155 | animation: hero-app-animate-right 0.8s 1.7s both; 156 | } 157 | 158 | .hero-app-6 { 159 | fill: #4d6975; 160 | animation: hero-app-animate-left 0.8s 1.74s both; 161 | } 162 | 163 | .hero-app-7 { 164 | fill: #4a6571; 165 | animation: hero-app-animate-right 0.8s 1.78s both; 166 | } 167 | 168 | .hero-app-8 { 169 | fill: #48616d; 170 | animation: hero-app-animate-left 0.8s 1.82s both; 171 | } 172 | 173 | .hero-app-9 { 174 | fill: #455c68; 175 | animation: hero-app-animate-right 0.8s 1.86s both; 176 | } 177 | 178 | .hero-app-10 { 179 | fill: #435864; 180 | animation: hero-app-animate-left 0.8s 1.9s both; 181 | } 182 | 183 | .hero-app-11 { 184 | fill: #415460; 185 | animation: hero-app-animate-right 0.8s 1.94s both; 186 | } 187 | 188 | .hero-app-12 { 189 | fill: #3e505c; 190 | animation: hero-app-animate-left 0.8s 1.98s both; 191 | } 192 | 193 | .hero-app-13 { 194 | fill: #3c4b58; 195 | animation: hero-app-animate-right 0.8s 2.02s both; 196 | } 197 | 198 | .hero-app-14 { 199 | fill: #394754; 200 | animation: hero-app-animate-left 0.8s 2.06s both; 201 | } 202 | 203 | .hero-app-15 { 204 | fill: #374350; 205 | animation: hero-app-animate-right 0.8s 2.1s both; 206 | } 207 | 208 | .docs-version { 209 | display: inline-block; 210 | padding: 0 0.4em; 211 | margin-left: 10px; 212 | vertical-align: middle; 213 | font-size: 15.2px; 214 | line-height: 1.8; 215 | font-weight: 200; 216 | -webkit-font-smoothing: initial; 217 | background-color: white; 218 | border: 1px solid #d1d9db; 219 | border-radius: 3px; 220 | } 221 | -------------------------------------------------------------------------------- /src/update/components/hero.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | margin: 0 auto; 3 | backface-visibility: hidden; 4 | max-width: 900px; 5 | animation: hero-animation 1s forwards; 6 | } 7 | @media (min-width: 1200px) { 8 | .hero { 9 | max-width: 100%; 10 | } 11 | } 12 | 13 | @keyframes hero-animation { 14 | 0% { 15 | opacity: 0; 16 | transform: scale(0.96); 17 | } 18 | 100% { 19 | opacity: 1; 20 | transform: scale(1); 21 | } 22 | } 23 | 24 | .hero-icon { 25 | transform-origin: 50% 50%; 26 | stroke: #9feaf9; 27 | stroke-width: 5; 28 | stroke-linecap: round; 29 | } 30 | 31 | .hero-icon.dot { 32 | fill: #9feaf9; 33 | stroke: none; 34 | } 35 | 36 | .hero-icon--line { 37 | stroke-dasharray: 170; 38 | stroke-dashoffset: 170; 39 | } 40 | 41 | .hero-icon--circle { 42 | stroke-dasharray: 70; 43 | stroke-dashoffset: 70; 44 | } 45 | 46 | .hero-icon-1 { 47 | animation: hero-icon-animation 1s 0.7s cubic-bezier(0.05, 0.35, 0.2, 1) 48 | forwards; 49 | } 50 | 51 | .hero-icon-2 { 52 | animation: hero-icon-animation 1s 0.8s cubic-bezier(0.05, 0.35, 0.2, 1) 53 | forwards; 54 | } 55 | 56 | .hero-icon-3 { 57 | animation: hero-icon-animation 1s 0.9s cubic-bezier(0.05, 0.35, 0.2, 1) 58 | forwards; 59 | } 60 | 61 | .hero-icon-4 { 62 | animation: hero-icon-animation 1s 1s cubic-bezier(0.05, 0.35, 0.2, 1) forwards; 63 | } 64 | 65 | .hero-icon-5 { 66 | animation: hero-icon-animation 1s 1.1s cubic-bezier(0.05, 0.35, 0.2, 1) 67 | forwards; 68 | } 69 | 70 | .hero-icon-6 { 71 | animation: hero-icon-animation 1s 1.2s cubic-bezier(0.05, 0.35, 0.2, 1) 72 | forwards; 73 | } 74 | 75 | .hero-icon-7 { 76 | animation: hero-icon-animation 1s 1.3s cubic-bezier(0.05, 0.35, 0.2, 1) 77 | forwards; 78 | } 79 | 80 | .hero-icon-8 { 81 | animation: hero-icon-animation 1s 1.4s cubic-bezier(0.05, 0.35, 0.2, 1) 82 | forwards; 83 | } 84 | 85 | .hero-icon-9 { 86 | animation: hero-icon-animation 1s 1.5s cubic-bezier(0.05, 0.35, 0.2, 1) 87 | forwards; 88 | } 89 | 90 | @keyframes hero-icon-animation { 91 | 100% { 92 | stroke-dashoffset: 0; 93 | } 94 | } 95 | 96 | .hero-app { 97 | fill: #6798a2; 98 | transform-origin: 50% 50%; 99 | } 100 | 101 | @keyframes hero-app-animate-left { 102 | 0% { 103 | transform: scale(1) translateX(30px); 104 | opacity: 0.1; 105 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 0.5); 106 | } 107 | 20% { 108 | transform: scale(1.1) translateX(20px); 109 | animation-timing-function: cubic-bezier(0.1, 0.4, 0.2, 1); 110 | } 111 | 100% { 112 | transform: scale(1) translateX(0px); 113 | opacity: 1; 114 | } 115 | } 116 | 117 | @keyframes hero-app-animate-right { 118 | 0% { 119 | transform: scale(1) translateX(-30px); 120 | opacity: 0.1; 121 | animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 0.5); 122 | } 123 | 20% { 124 | transform: scale(1.1) translateX(-20px); 125 | animation-timing-function: cubic-bezier(0.1, 0.4, 0.2, 1); 126 | } 127 | 100% { 128 | transform: scale(1) translateX(0px); 129 | opacity: 1; 130 | } 131 | } 132 | 133 | .hero-app-1 { 134 | fill: #597f89; 135 | animation: hero-app-animate-right 0.8s 1.54s both; 136 | } 137 | 138 | .hero-app-2 { 139 | fill: #567a85; 140 | animation: hero-app-animate-left 0.8s 1.58s both; 141 | } 142 | 143 | .hero-app-3 { 144 | fill: #547681; 145 | animation: hero-app-animate-right 0.8s 1.62s both; 146 | } 147 | 148 | .hero-app-4 { 149 | fill: #51727d; 150 | animation: hero-app-animate-left 0.8s 1.66s both; 151 | } 152 | 153 | .hero-app-5 { 154 | fill: #4f6e79; 155 | animation: hero-app-animate-right 0.8s 1.7s both; 156 | } 157 | 158 | .hero-app-6 { 159 | fill: #4d6975; 160 | animation: hero-app-animate-left 0.8s 1.74s both; 161 | } 162 | 163 | .hero-app-7 { 164 | fill: #4a6571; 165 | animation: hero-app-animate-right 0.8s 1.78s both; 166 | } 167 | 168 | .hero-app-8 { 169 | fill: #48616d; 170 | animation: hero-app-animate-left 0.8s 1.82s both; 171 | } 172 | 173 | .hero-app-9 { 174 | fill: #455c68; 175 | animation: hero-app-animate-right 0.8s 1.86s both; 176 | } 177 | 178 | .hero-app-10 { 179 | fill: #435864; 180 | animation: hero-app-animate-left 0.8s 1.9s both; 181 | } 182 | 183 | .hero-app-11 { 184 | fill: #415460; 185 | animation: hero-app-animate-right 0.8s 1.94s both; 186 | } 187 | 188 | .hero-app-12 { 189 | fill: #3e505c; 190 | animation: hero-app-animate-left 0.8s 1.98s both; 191 | } 192 | 193 | .hero-app-13 { 194 | fill: #3c4b58; 195 | animation: hero-app-animate-right 0.8s 2.02s both; 196 | } 197 | 198 | .hero-app-14 { 199 | fill: #394754; 200 | animation: hero-app-animate-left 0.8s 2.06s both; 201 | } 202 | 203 | .hero-app-15 { 204 | fill: #374350; 205 | animation: hero-app-animate-right 0.8s 2.1s both; 206 | } 207 | 208 | .docs-version { 209 | display: inline-block; 210 | padding: 0 0.4em; 211 | margin-left: 10px; 212 | vertical-align: middle; 213 | font-size: 15.2px; 214 | line-height: 1.8; 215 | font-weight: 200; 216 | -webkit-font-smoothing: initial; 217 | background-color: white; 218 | border: 1px solid #d1d9db; 219 | border-radius: 3px; 220 | } 221 | -------------------------------------------------------------------------------- /electron/windows-manager.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain } from 'electron'; 2 | import { WINDOW_OPEN, WINDOW_CLOSE, WINDOW_PREVENT_CLOSE } from './constants'; 3 | import { getType } from './utils'; 4 | 5 | const createWindow = ({ name, url, width = 1096, height = 728, ...rest }, onClose) => { 6 | const webPreferences = { 7 | nodeIntegration: true, 8 | }; 9 | 10 | rest.webPreferences = Object.assign({}, webPreferences, rest.webPreferences || {}); 11 | 12 | const options: BrowserWindowConstructorOptions & { name?: string } = { 13 | name, 14 | width, 15 | height, 16 | show: false, 17 | ...rest, 18 | }; 19 | 20 | if (!(rest.parent || rest.modal)) { 21 | let x, y; 22 | const currentWindow = BrowserWindow.getFocusedWindow(); //获取当前活动的浏览器窗口。 23 | if (currentWindow) { 24 | // 如果上一步中有活动窗口,则根据当前活动窗口的右下方设置下一个窗口的坐标 25 | const [currentWindowX, currentWindowY] = currentWindow.getPosition(); 26 | x = currentWindowX + 40; 27 | y = currentWindowY + 40; 28 | } 29 | options.x = x; 30 | options.y = y; 31 | } 32 | 33 | let newWindow = new BrowserWindow(options); 34 | newWindow['name'] = options.name.toLowerCase(); 35 | newWindow.loadURL(url); 36 | newWindow.once('ready-to-show', () => { 37 | newWindow.show(); 38 | }); 39 | newWindow.on('closed', () => { 40 | onClose(); 41 | newWindow = null; 42 | }); 43 | return newWindow; 44 | }; 45 | 46 | export class WindowsManager { 47 | private windows: Map>; 48 | private config: Record; 49 | 50 | constructor(config) { 51 | this.windows = new Map(); 52 | this.config = config; 53 | this.detectIpcMain(); 54 | return this; 55 | } 56 | 57 | private detectIpcMain(): void { 58 | ipcMain.on(WINDOW_OPEN, async (evt, arg) => { 59 | const { name } = arg; 60 | this.show(name); 61 | evt.sender.send('success'); 62 | }); 63 | ipcMain.on(WINDOW_CLOSE, async (evt, arg) => { 64 | const { name } = arg; 65 | this.close(name); 66 | evt.sender.send('success'); 67 | }); 68 | ipcMain.on(WINDOW_PREVENT_CLOSE, (_, arg) => { 69 | const window = this.windows.get(arg.name); 70 | const prevent = (window) => { 71 | window.on('close', (evt) => { 72 | evt.preventDefault(); 73 | }); 74 | }; 75 | if (getType(window) === 'set') { 76 | Array.from(window as Set).forEach(prevent); 77 | } else { 78 | prevent(window); 79 | } 80 | }); 81 | } 82 | 83 | getAllWindows(): Array { 84 | return [...this.windows.keys()].reduce((all, name) => { 85 | const windowOrSet = this.windows.get(name); 86 | if (getType(windowOrSet) === 'set') { 87 | all.push.apply(all, Array.from(windowOrSet as Set)); 88 | } else { 89 | all.push(windowOrSet); 90 | } 91 | return all; 92 | }, []); 93 | } 94 | 95 | show(name): BrowserWindow { 96 | const config = this.config[name]; 97 | 98 | // 父子窗口 https://www.electronjs.org/docs/api/browser-window#%E7%88%B6%E5%AD%90%E7%AA%97%E5%8F%A3 99 | if (config.parent) { 100 | const parent = this.windows.get(config.parent); 101 | if (parent && getType(parent) !== 'set') { 102 | config.parent = parent; 103 | } 104 | } 105 | 106 | let window; 107 | let hasCreated = false; 108 | 109 | if (this.windows.has(name)) { 110 | if (config.multiple) { 111 | const set = this.windows.get(name) as Set; 112 | window = createWindow(config, () => { 113 | set.delete(window); 114 | }); 115 | set.add(window); 116 | } else { 117 | window = this.windows.get(name); 118 | hasCreated = true; 119 | } 120 | } else { 121 | if (config.multiple) { 122 | const set = new Set(); 123 | window = createWindow(config, () => { 124 | set.delete(window); 125 | }); 126 | set.add(window); 127 | this.windows.set(name, set); 128 | } else { 129 | window = createWindow(config, () => { 130 | this.windows.delete(name); 131 | }); 132 | this.windows.set(name, window); 133 | } 134 | } 135 | 136 | window.once('did-finish-load', () => { 137 | window.show(); 138 | window.focus(); 139 | }); 140 | 141 | if (hasCreated) { 142 | window.show(); 143 | window.focus(); 144 | } 145 | 146 | return window; 147 | } 148 | 149 | close(name): void { 150 | if (!this.windows.has(name)) return; 151 | const close = (window) => { 152 | if (window.isClosable()) { 153 | window.close(); 154 | } 155 | }; 156 | const windowOrSet = this.windows.get(name); 157 | if (getType(windowOrSet) === 'set') { 158 | (windowOrSet as Set).forEach(close); 159 | } else { 160 | close(windowOrSet); 161 | } 162 | this.windows.delete(name); 163 | } 164 | 165 | closeAll(current): void { 166 | void [...this.windows.keys()].filter((name) => name !== current).forEach(this.close.bind(this)); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/main/components/hero.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './hero.scss' 3 | 4 | const Hero: React.SFC = () => ( 5 |
6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 61 | 65 | 66 | 67 | 71 | 75 | 81 | 82 | 86 | 90 | 96 | 97 | 101 | 105 | 111 | 112 | 118 | 119 | 120 | 121 |
122 | ) 123 | 124 | export default Hero 125 | -------------------------------------------------------------------------------- /src/multiple/components/hero.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './hero.scss' 3 | 4 | const Hero: React.SFC = () => ( 5 |
6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 61 | 65 | 66 | 67 | 71 | 75 | 81 | 82 | 86 | 90 | 96 | 97 | 101 | 105 | 111 | 112 | 118 | 119 | 120 | 121 |
122 | ) 123 | 124 | export default Hero 125 | -------------------------------------------------------------------------------- /src/picture/components/hero.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './hero.scss' 3 | 4 | const Hero: React.SFC = () => ( 5 |
6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 61 | 65 | 66 | 67 | 71 | 75 | 81 | 82 | 86 | 90 | 96 | 97 | 101 | 105 | 111 | 112 | 118 | 119 | 120 | 121 |
122 | ) 123 | 124 | export default Hero 125 | -------------------------------------------------------------------------------- /src/update/components/hero.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './hero.scss' 3 | 4 | const Hero: React.SFC = () => ( 5 |
6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 61 | 65 | 66 | 67 | 71 | 75 | 81 | 82 | 86 | 90 | 96 | 97 | 101 | 105 | 111 | 112 | 118 | 119 | 120 | 121 |
122 | ) 123 | 124 | export default Hero 125 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/app.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hello, Electron & React 1`] = ` 4 |
7 |
8 |
11 | 15 | 19 | 22 | 26 | 30 | 34 | 38 | 42 | 46 | 50 | 54 | 58 | 62 | 66 | 70 | 74 | 78 | 79 | 83 | 87 | 91 | 97 | 101 | 105 | 111 | 115 | 119 | 125 | 131 | 132 | 133 | 134 |
135 |

136 | 使用 Electron 和 React 构建跨平台的桌面应用 137 |

138 |
139 |
146 | 152 | 157 | 165 | 171 | 176 |
177 |
184 | 189 | 199 |
200 |

201 | To get started, edit 202 | 203 | src/main/App.tsx 204 | 205 | and save to reload. 206 |

207 |
208 | `; 209 | --------------------------------------------------------------------------------