桌面应用的实现方式有很多,但谈到多操作系统平台兼容的话,就不得不提到electron。electron是前端开发的利好,做过web前端的同学只要稍微迁移下自己的项目,就能够将原本的web前端变成桌面应用。
因此,本文以react为例,以antd为UI库支持,讲解基于react的electron应用该如何搭建。
首先,electron-react-boilerplate项目,就帮助我们初始化了基于react的electron应用。electron-react-boilerplate内置了flow静态类型检查机制、基于webpack的electron应用打包支持以及with redux的前端架构。故在此基础上再引入其它的lib,也不会过于困难。本文采用的electron-react-boilerplate版本为0.17.1。
通过yarn add antd
,就可以安装上antd库。要在内置的例子里以antd为layout的话,首先需要在app
文件夹下新建app.global.less
文件,填充内容:@import '../node_modules/antd/dist/antd.less';
,然后我们可以再观察到,app/containers/Root.js
是redux store的抽象层,wrap了路由;路由所在的文件为app/Route.js
,是以
标签为根的路由集合;
主界面所在的文件为app/containers/App.js
,我们通过更改其中的内容,就可以变换主界面的样式了。我们就以antd的layout为例,写一个App主界面:
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Layout, Card, Icon, Menu, Button } from 'antd';
import { shell } from 'electron';
import routes from '../constants/routes';
const { Sider, Content } = Layout;
type Props = {
children: React.Node
};
type State = {
menuKey: string
};
const menuList = [
{ key: 'note', text: '笔记', icon: 'book' },
{ key: 'setting', text: '设置', icon: 'setting' }
];
class App extends React.Component<Props, State> {
props: Props;
static propTypes = {
history: PropTypes.object.isRequired
};
state = {
menuKey: 'note'
};
handleMenuClick = (e: Event) => {
this.setState({ menuKey: e.key });
const { history } = this.props;
history.push(routes[e.key]);
};
render() {
const { children } = this.props;
const { menuKey } = this.state;
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible={false}>
<Menu
style={{ height: '100%' }}
theme="dark"
onClick={this.handleMenuClick}
selectedKeys={[menuKey]}
mode="inline"
>
{menuList.map(item => (
<Menu.Item key={item.key}>
<span>
<Icon type={item.icon} />
{`\t${item.text}`}
</span>
</Menu.Item>
))}
</Menu>
</Sider>
<Content>
<Card
title={menuList.find(i => i.key === menuKey).text}
style={{ height: '100%' }}
extra={
<Button
shape="circle"
onClick={() =>shell.openExternal('https://github.com')}
icon="github"
/>
}
>
{children}
</Card>
</Content>
</Layout>
);
}
}
export default withRouter(App);
写App主界面后,记得也得实时更新app/Route.js
以及其关联的app/constants/routes.json
数据喔~
在app/containers
中其它以Page
结尾的文件,约定俗成是各个子路由绑定redux的层次。比如在上面我们设置了笔记的menu,那么在app/containers/NotePage.js
中,就可以定义跟redux的绑定:
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Note from '../components/note/index';
import * as NoteActions from '../actions/note';
function mapStateToProps(state) {
return { ...state.note };
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(NoteActions, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Note);
之后在app/components/note/index.js
中,就可以编写笔记页面的样式了。
至于redux这块,由于flow增加了静态检查,因此会稍微麻烦一点。虽然在app/reducers/index.js
中已经帮我们完成了reducer的绑定,但在app/reducers/types.js
中,还需要初始化各个state的类型:
import type { Dispatch as ReduxDispatch, Store as ReduxStore } from 'redux';
// action
export type Action = {
+type: string
};
// note state
export type NoteState = {
notes: Array
};
// state type
export type StateType = {
note: NoteState
};
export type GetState = () => StateType;
export type Dispatch = ReduxDispatch<Action>;
export type Store = ReduxStore<GetState, Action>;
之后根据预定义的类型,再写action跟reducer。拿note笔记模块为例,action跟reducer如下:
// app/actions/note.js
import type { Dispatch } from '../reducers/types';
export function add() {
return (dispatch: Dispatch) => dispatch({ type: 'ADD_NOTE' });
}
export function edit(index: number) {
return (dispatch: Dispatch) => dispatch({ type: 'EDIT_NOTE', index });
}
export function remove() {
return (dispatch: Dispatch) => dispatch({ type: 'REMOVE_NOTE' });
}
export function clear() {
return (dispatch: Dispatch) => dispatch({ type: 'CLEAR_NOTE' });
}
// app/reducers/note.js
import type { Action, NoteState } from './types';
const defaultState: NoteState = {
notes: []
};
const addNote = state => ({
...state,
notes: [...state.notes, state.notes.length + 1]
});
const editNote = (state, payload) => {
const { notes } = state;
const { index } = payload;
console.log(payload);
if (index >= notes.length || index < 0) {
return state;
}
const newNotes = [...notes];
newNotes[index] *= 2;
return { ...state, notes: newNotes };
};
const removeNote = state => {
const { notes } = state;
if (notes.length === 0) {
return state;
}
const newNotes = [...notes];
newNotes.splice(newNotes.length - 1, 1);
return { ...state, notes: newNotes };
};
const clearNote = state => ({ ...state, notes: [] });
export default function note(state: NoteState = defaultState, action: Action) {
const { type, ...payload } = action;
switch (type) {
case 'ADD_NOTE': {
return addNote(state);
}
case 'EDIT_NOTE': {
return editNote(state, payload);
}
case 'REMOVE_NOTE': {
return removeNote(state);
}
case 'CLEAR_NOTE': {
return clearNote(state);
}
default: {
return state;
}
}
}
这样便初始化好了基本的笔记增删改查操作,整个桌面应用就有雏形了。
桌面软件一般会有标题与菜单栏,标题的修改是在app/app.html
的title
标签,而菜单的修改在app/menu.js
中。
antd的深色主题在桌面应用中显示会不错,因此我们想要最终产品为深色主题。值得注意的是,由于我们刚开始引入antd新建了less文件,但electron-react-boilerplate默认不支持less,因此需要我们在开发与生产环境的webpack配置中(configs/webpack.config.renderer.dev.babel.js
与configs/webpack.config.renderer.prod.babel.js
)先将antd的深色主题import进来,然后自行yarn add less-loader
,再在配置中的module.rules
列表中,追加一段就好:
import antdTheme from '@ant-design/dark-theme';
...
{
test: /\.less$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader' // translates CSS into CommonJS
},
{
loader: 'less-loader', // compiles Less to CSS
options: {
modifyVars: antdTheme,
javascriptEnabled: true
}
}
]
}
...
这样,electron+react+antd深色主题桌面应用的基础流程就打通了。