使用ReactWebpackNode.jsWebsocketElectronDva快速构建跨平台应用

Posted 前端速报

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用ReactWebpackNode.jsWebsocketElectronDva快速构建跨平台应用相关的知识,希望对你有一定的参考价值。

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


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

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

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


先说说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、Webpack、Node.js、Websocket、Electron、Dva快速构建跨平台应用

  • config文件放置webpack配置文件

  • server文件夹放置Node.js的后端服务器代码

  • src下放置源码

  • main.jsElectron的入口文件

  • json文件是脚本入口文件,也是包管理的文件~



开发模式项目启动思路:

  • 先启动webpack将代码打包到内存中,实现热更新


设置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},}

看看主进程的配置文件main.js

// Modules to control application life and create native browser windowconst { app, BrowserWindow, ipcMain, Tray, Menu } = require('electron')const path = require('path')// Keep a global reference of the window object, if you don't, the window will// be closed automatically when the JavaScript object is garbage collected.let mainWindowapp.disableHardwareAcceleration()// ipcMain.on('sync-message', (event, arg) => {// console.log("sync - message")// // event.returnValue('message', 'tanjinjie hello')// })function createWindow() { // Create the browser window. tray = new Tray(path.join(__dirname, './src/assets/bg.jpg')); tray.setToolTip('wechart'); tray.on('click', () => { mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show() }); const contextMenu = Menu.buildFromTemplate([ { label: '退出', click: () => mainWindow.quit() }, ]); tray.setContextMenu(contextMenu); mainWindow = new BrowserWindow({ width: 805, height: 500, webPreferences: { nodeIntegration: true }, // titleBarStyle: 'hidden' frame: false })
//自定义放大缩小托盘功能 ipcMain.on('changeWindow', (event, arg) => { if (arg === 'min') { console.log('min') mainWindow.minimize() } else if (arg === 'max') { console.log('max') if (mainWindow.isMaximized()) { mainWindow.unmaximize() } else { mainWindow.maximize() } } else if (arg === "hide") { console.log('hide') mainWindow.hide() } }) // and load the index.html of the app. // mainWindow.loadFile('index.html') mainWindow.loadURL('http://localhost:5000'); BrowserWindow.addDevToolsExtension( path.join(__dirname, './src/extensions/react-dev-tool'), );

// Open the DevTools. // mainWindow.webContents.openDevTools()
// Emitted when the window is closed. mainWindow.on('closed', function () { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. mainWindow = null BrowserWindow.removeDevToolsExtension( path.join(__dirname, './src/extensions/react-dev-tool'), ); })}
// This method will be called when Electron has finished// initialization and is ready to create browser windows.// Some APIs can only be used after this event occurs.app.on('ready', createWindow)
// Quit when all windows are closed.app.on('window-all-closed', function () { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') app.quit()})
app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (mainWindow === null) createWindow()})

// In this file you can include the rest of your app's specific main process// code. You can also put them in separate files and require them here.


今天只讲开发模式下的配置

在开发模式下启动项目:

  • 使用 "dev": "webpack-dev-server --config ./config/webpack.dev.js", 将代码打包到内存中

项目起来后,在入口处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 的联系: 可以通过 dispatch 向 store 注入 action, 促使 store 的状态进行变化, 同时又订阅了 store 的状态变化, 一旦状态有变, 被 connect 的组件也随之刷新,使用 dispatch 往 store 发送 action 的这个过程是可以被拦截的, 自然而然地就可以在这里增加各种 Middleware, 实现各种自定义功能, eg: logging这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好

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

然后是注入Redux-sage

  • 上面说了, 可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子:

  • 点击创建 Todo 的按钮, 发起一个 type == addTodo 的 action。

  • saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 type == addTodoSucc 的action, 提示创建成功, 反之则发送 type == addTodoFail 的 action 即可。



最后是: Dva