使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

栏目: Node.js · 发布时间: 7年前

内容简介:这里吐槽下,

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

目前 Electrongithub 上面的 star 量已经快要跟 React-native 一样多了

这里吐槽下, webpack 感觉每周都在偷偷更新,很糟心啊,还有 Angular 更新到了 8 , Vue 马上又要出正式新版本了, 5G 今年就要商用,华为的系统也要出来了, RN 还没有更新到正式的 1 版本,还有号称让前端开发者失业的技术 flutter 也在疯狂更新,前端真的是学不完的

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

回到正题,不能否认,现在的大前端,真的太牛了, PC 端可以跨三种平台开发,移动端可以一次编写,生成各种小程序以及 React-native 应用,然后跑在 ios 和安卓以及网页中 , 这里不得不说-------京东的 Taro 框架 这些人 已经把 Node.jswebpack

用上了天

webpack 不熟悉的,看我之前的文章 ,今天不把重点放在 webpack

欢迎关注我的专栏 《前端进阶》 都是百星高赞文章

先说说 Electron 官网介绍:

使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用 ,如果你可以建一个网站,你就可以建一个桌面应用程序。 Electron 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可。

  • 什么意思呢?
  • Electron = Node.js + 谷歌浏览器 + 平常的JS代码生成的应用 ,最终打包成安装包,就是一个完整的应用
  • Electron 分两个进程,主进程负责比较难搞的那部分,渲染进程(平常的 JS 代码)部分,负责 UI 界面展示
  • 两个进程之间可以通过 remote 模块,以及 IPCRenderIPCMain 之间通信,前者类似于挂载在全局的属性上进行通信(很像最早的命名空间模块化方案),后者是基于发布订阅机制,自定义事件的监听和触发实现两个进程的通信。
  • Electron 相当于给 React 生成的单页面应用套了一层壳,如果涉及到文件操作这类的复杂功能,那么就要依靠 Electron 的主进程,因为主进程可以直接调用 Node.jsAPI ,还可以使用 C++ 插件,这里 Node.js 的牛逼程度就凸显出来了,既可以写后台的 CRUD ,又可以做中间件,现在又可以写前端。

谈谈技术选型

  • 使用 React 去做底层的 UI 绘制,大项目首选 React+TS
  • 状态管理的最佳实践肯定不是 Redux ,目前首选 dva ,或者 redux-saga
  • 构建 工具 选择 webpack ,如果不会 webpack 真的很吃亏,会严重限制你的前端发展,所以建议好好学习 Node.jswebpack
  • 选择了普通的 Restful 架构,而不是 GraphQL ,可能我对 GraphQL 理解不深,没有领悟到精髓
  • 在通信协议这块,选择了 websoket 和普通的 http 通信方式
  • 因为是 demo ,很多地方并没有细化,后期会针对 electron 出一个网易云音乐的开源项目,这是一定要做到的

先开始正式的环境搭建

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

  • config 文件放置 webpack 配置文件
  • server 文件夹放置 Node.js 的后端服务器代码
  • src 下放置源码
  • main.jsElectron 的入口文件
  • json 文件是脚本入口文件,也是包管理的文件~

开发模式项目启动思路:

  • 先启动 webpack 将代码打包到内存中,实现热更新
  • 再启动 Electron 读取对应的 url 地址的文件内容,也实现热更新

设置 webpack 入口

app: ['babel-polyfill', './src/index.js', './index.html'],
        vendor: ['react']
        }

忽略 Electron 中的代码,不用 webpack 打包(因为 Electron 中有后台模块代码,打包就会报错)

externals: [
        (function () {
            var IGNORES = [
                'electron'
            ];
            return function (context, request, callback) {
                if (IGNORES.indexOf(request) >= 0) {
                    return callback(null, "require('" + request + "')");
                }
                return callback();
            };
        })()
    ]

加入代码分割

optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all'
        }
    },

设置热更新等

plugins: [
        new HtmlWebpackPlugin({
            template: './index.html'
        }),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NamedModulesPlugin(),
    ],
    mode: 'development',
    devServer: {
        contentBase: '../build',
        open: true,
        port: 5000,
        hot: true
    },

#### 加入 babel

{
    loader: 'babel-loader',
    options: {   //jsx语法
        presets: ["@babel/preset-react",
            //tree shaking 按需加载babel-polifill  presets从后到前执行 
            ["@babel/preset-env", {
                "modules": false,
                "useBuiltIns": "false", "corejs": 2,
            }],
        ],

    plugins: [
        //支持import 懒加载    plugin从前到后
        "@babel/plugin-syntax-dynamic-import",
        //andt-mobile按需加载  true是less,如果不用less style的值可以写'css' 
        ["import", { libraryName: "antd-mobile", style: true }],
        //识别class组件
        ["@babel/plugin-proposal-class-properties", { "loose": true }],
        //
    ],
    cacheDirectory: true
},
}

今天只讲开发模式下的配置,因为实在太多,得分两篇文章写了~ 剩下的配置去 git 仓库看

在开发模式下启动项目:

  • 使用 "dev": "webpack-dev-server --config ./config/webpack.dev.js", 将代码打包到内存中
  • 使用 "start": "electron ." 开启 electron ,读取对应的内存地址中的资源,实现热更新

项目起来后,在入口处 index.js 文件中,注入 dva

import React from 'react'
import App from './App'
import dva from 'dva'
import Homes from './model/Homes'
import main from './model/main'
const app = dva()
app.router(({ history, app: store }) => (
  <App
    history={history}
    getState={store._store.getState}
    dispatch={store._store.dispatch}
  />
));
app.model(Homes)
app.model(main)
app.start('#root')

这里不得不说 redux,redux-sage,dva 的区别 直接看图

首先是 Redux

  • React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑可以从中单独抽取出来, 变成 store ,状态及页面逻辑从 <App/> 里面抽取出来, 成为独立的 store ,
  • 页面逻辑就是 reducer,<TodoList/> 及<AddTodoBtn/> 都是 Pure Component , 通过 connect 方法可以很方便地给它俩加一层 wrapper 从而建立起与 store 的联系: 可以通过 dispatchstore 注入 action , 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态有变, 被 connect 的组件也随之刷新,使用 dispatchstore 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware , 实现各种自定义功能, eg: logging这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

然后是注入 Redux-sage

  • 上面说了, 可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子:
  • 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action
  • saga 拦截这个 action , 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSuccaction , 提示创建成功, 反之则发送 type == addTodoFailaction 即可

最后是: Dva

  • 有了前面的三步铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:
  • storesaga 统一为一个 model 的概念, 写在一个 js 文件里面
  • 增加了一个 Subscriptions , 用于收集其他来源的 action, eg: 键盘操作
  • model 写法很简约, 类似于 DSL 或者 RoR , coding 快得飞起:airplane:
  • 约定优于配置, 总是好的:laughing:

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

在入口 APP 组件中,注入 props ,实现状态树的管理

import React from 'react'
import { HashRouter, Route, Redirect, Switch } from 'dva/router';
import Home from './pages/home'
const Router = (props) => {
    return (
        <HashRouter>
            <Switch>
                <Route path="/home" component={Home}></Route>
                <Redirect to="/home"></Redirect>
            </Switch>
        </HashRouter>
    )
}
export default Router

在组件中 connect 连接状态树即可

import React from 'react'
import { ipcRenderer } from 'electron'
import { NavLink, Switch, Route, Redirect } from 'dva/router'
import Title from '../../components/title'
import Main from '../main'
import Friend from '../firend'
import More from '../more'
import { connect } from 'dva'
import './index.less'
class App extends React.Component {
    componentDidMount() {
        ipcRenderer.send('message', 'hello electron')
        ipcRenderer.on('message', (event, arg) => {
            console.log(arg, new Date(Date.now()))
        })
        const ws = new WebSocket('ws://localhost:8080');
        ws.onopen = function () {
            ws.send('123')
            console.log('open')
        }
        ws.onmessage = function () {
            console.log('onmessage')
        }
        ws.onerror = function () {
            console.log('onerror')
        }
        ws.onclose = function () {
            console.log('onclose')
        }
    }
    componentWillUnmount() {
        ipcRenderer.removeAllListeners()
    }
    render() {
        console.log(this.props)
        return (
            <div className="wrap">
                <div className="nav">
                    <NavLink to="/home/main">Home</NavLink>
                    <NavLink to="/home/firend">Friend</NavLink>
                    <NavLink to="/home/more">More</NavLink>
                </div>
                <div className="content">
                    <Title></Title>
                    <Switch>
                        <Route path="/home/main" component={Main}></Route>
                        <Route path="/home/firend" component={Friend}></Route>
                        <Route path="/home/more" component={More}></Route>
                        <Redirect to="/home/main"></Redirect>
                    </Switch>
                </div>
            </div>
        )
    }
}
export default connect(
    ({ main }) => ({
        test: main.main
    })
)(App)
// ipcRenderer.sendSync('sync-message','sync-message')

捋一捋上面的组件做了什么

  • 上来在组件挂载的生命周期函数中,启动了 websocket 连接,并且挂载了响应的事件监听,对主线程发送了消息,并且触发了主线程的 message 事件。
  • 在组件即将卸载的时候,移除了所有的跨进程通信的事件监听
  • 使用了 dva 进行路由跳转
  • 连接了状态树,读取了状态树 main 模块的 main 状态数据

进入上一个组件的子组件

import React from 'react'
import { connect } from 'dva'
class App extends React.Component {
    handleAdd = () => {
        this.props.dispatch({
            type: 'home/add',
            val: 5,
            res: 1
        })
    }
    handleDel = () => {
    }
    render() {
        const { homes } = this.props
        console.log(this.props)
        return (
            <div>
                <button onClick={this.handleAdd}>add</button>
                <button onClick={this.handleDel}>{homes}</button>
            </div>
        )
    }
}
export default connect(
    ({ home, main }) => ({
        homes: home.num,
        mains: main.main
    })
)(App)

同样看看,这个组件做了什么

  • 连接状态树,读取了 home,main 模块的状态数据,并且转换成了 props
  • 绑定了事件,如果点击按钮, dispatch 给对应的 effects ,更新状态树的数据,进而更新页面

我们一起去 dva 中管理的 model 看看

  • home 模块
export default {
    namespace: 'home',
    state: {
        homes: [1, 2, 3],
        num: 0
    },
    reducers: {
        adds(state, { newNum }) {
            return {
                ...state,
                num: newNum
            }
        }
    },
    effects: {
        * add({ res, val }, { put, select, call }) {
            const { home } = yield select()
            console.log(home.num)
            yield console.log(res, val)
            const newNum = home.num + 1
            yield put({ type: 'adds', newNum })
        }
    },
}

dva 真的可以给我们省掉很多很多代码,而且更好维护,也更容易阅读

  • 它的大概流程

使用React、Electron、Dva、Webpack、Node.js、Websocket快速构建跨平台应用

  • 如果不会的话建议去官网看例子,一般来说不会像 RXJS 学习路线那么陡峭

Node.js 中代码

const express = require('express')
const { Server } = require("ws");
const app = express()
const wsServer = new Server({ port: 8080 })
wsServer.on('connection', (ws) => {
    ws.onopen = function () {
        console.log('open')
    }
    ws.onmessage = function (data) {
        console.log(data)
        ws.send('234')
        console.log('onmessage' + data)
    }
    ws.onerror = function () {
        console.log('onerror')
    }
    ws.onclose = function () {
        console.log('onclose')
    }
});

app.listen(8000, (err) => {
    if (!err) { console.log('监听OK') } else {
        console.log('监听失败')
    }
})

上来先给一个 websocket 8080 端口监听,绑定事件,并且使用 express 监听原生端口 8000

  • 这样好处,一个应用并不一定全部需要实时通讯,根据需求来决定什么时候进行实时通讯
  • Restful 架构依然存在, Node.js 作为中间件或者 IO 输出比较多的底层服务器进行 CRUD 都可以

今天先写到这里,套路都一样,基本的架子已经已经搭好,可以把代码 clone 下去慢慢玩,加功能。 可以的话给个 star 和赞,谢谢

本文git仓库源码地址,欢迎star


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Sovereign Individual

The Sovereign Individual

James Dale Davidson、William Rees-Mogg / Free Press / 1999-08-26 / USD 16.00

Two renowned investment advisors and authors of the bestseller The Great Reckoning bring to light both currents of disaster and the potential for prosperity and renewal in the face of radical changes ......一起来看看 《The Sovereign Individual》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具