OpenFin:一个使用它的简小示例应用程序





5.00/5 (12投票s)
一个尝试 OpenFin 的小程序。
概述
距离我上次在这里写文章已经过去很久了。我实际上一直在忙于我的博客,所以最近在那里写得更多。问题是,我几年前听说过这项技术,也大致知道它的作用,但从未真正使用过它。
这项技术叫做 OpenFin,其口号是“OpenFin OS实现了金融桌面的现代化,实现了即时分发、强大的安全性和应用程序互操作性”。
碰巧我从事编写交易应用程序的业务,这大概就是我做的。认识我的人都知道,我以前经常使用WPF。那些日子似乎已经很久远了。我不再接触任何UI相关的东西了。说实话,我写了大约1.5年的Scala和Akka,所以根本没有UI方面的代码,而我的新工作是深入的服务器端/云。
尽管如此,我还是喜欢一些UI工作,并努力跟上最新技术,而且无法抗拒查看 OpenFin,即使它可能被一些人认为是相当老的技术了。
在开始之前,我在codeproject上快速搜索了一下,说实话,我真不敢相信竟然没有人写过它。所以我想,好吧,我得做这件事了。
因此,本文将介绍 OpenFin,还将展示它的优点和缺点。我们还将看到一个使用OpenFin编写的演示应用程序,并了解更多关于 OpenFin 的信息。
简而言之,本文将介绍 OpenFin 并通过演示应用程序进行讲解。
最终产品看起来怎么样?
我认为,要了解最终产品看起来是什么样的,最好的方式是通过这个小视频。
点击下载视频
基本上,我构建的是一个非常非常精简的交易应用程序的演示应用程序,它具有以下功能:
- 交易员,用于查看过去的交易(以网格形式)
- 带有假价格数据的磁贴
- 用于磁贴的图表,或用于过去交易的图表。
尽管只有3个窗口,但这足以让我们真正尝试Openfin。
沿途我们将使用哪些工具
这是一个相当庞大的演示应用程序,如果您一直跟读到最后,您将看到或可以在源代码中看到以下技术的使用:
- React
- Redux
- Redux thunk
- ES6
- SCSS
- TypeScript
- Express JS
- Parcel JS
- OpenFin
代码在哪里?
本文的代码实际上托管在Github上: https://github.com/sachabarber/OpenFinWithReactReduxParcel
Openfin究竟是什么,它试图解决什么问题?
正如上面所说,OpenFin 认为自己是金融领域的操作系统。为什么是这样?传统上,金融应用程序富含数据和实时信息,并需要许多丰富的UI元素,如图表、停靠、桌面集成(通知图标、系统托盘、弹出通知)等。
我认为公平地说,这以前是通过Winforms/WPF以及可能的MFC混合实现的。
然而,情况已经发生了变化,许多传统上使用这些技术栈实现的UI现在正使用HTML5来实现。但是,交易员们是善变的,仍然想要鱼与熊掌兼得。他们喜欢他们的桌面型应用程序。
关键是HTML5提供了独一无二的东西,即一次编写,随处运行。
这很棒,许多地方已经切换到HTML5并愉快地使用最新的单页应用程序(SPA)框架。但仍然需要真正的窗口化/工作区驱动的应用程序。SPA并不适合所有应用程序。
这正是 OpenFin 的用武之地,它允许您编写HTML5应用程序,然后将这些应用程序托管在 OpenFin 容器类型的框架内,其中 OpenFin 运行时将注入到您正在运行的HTML5代码中。还有针对不同语言的其他 OpenFin API,但HTML JavaScript API确实是主要的用例。
您可以这样理解它:
其中 OpenFin 利用了Chromium浏览器堆栈。您可以将您的源代码入口点HTML看作是单个单页应用程序,您仍然可以在其中拥有所有常规功能,如路由等,但如果您试图模拟传统的桌面应用程序,将一个HTML页面看作是传统桌面应用程序中的一个传统表单/窗口可能更有意义。
OpenFin 包含一些核心功能,namely these
它聪明之处在于,它能够运行在Windows/Mac/Linux上,并模仿所选操作系统的原生窗口。对我来说,另一个重要方面是进程间总线,我们稍后会详细介绍。
这不就是Electron吗?
这正是我提出的问题,在互联网上搜索了一些东西后,它确实归结为以下几点:
需要注意的是,OpenFin是Electron之上的一个相当薄的层,所以从技术角度来看,它们非常相似。
安全性,Electron默认不安全,它将Node API直接添加到渲染器线程。因此,XSS攻击可能会造成严重破坏!然而,Electron已经做了大量工作来创建一个健壮的、选择加入的安全模型(此问题跟踪了许多这些更改 https://github.com/electron/electron/issues/6712)。OpenFin有一个Electron的分支,默认是安全的,您根本无法访问Node API。但是,如果您不托管第三方内容到您的应用程序中,XSS不太可能发生,并且Electron可以相当安全地使用。
观看这段Chuck(OpenFin首席技术官)谈论安全性的视频:https://www.youtube.com/watch?v=jhY4kdY_0Ho
分发,OpenFin的分发模型涉及一次安装其运行时,然后通过配置文件从互联网下载应用程序。使用Electron,您需要将整个运行时与每个应用程序一起打包。OpenFin模型在安装桌面应用程序很麻烦的受监管环境中使事情变得更容易。
OpenFin的重要部分有哪些?
正如我上面提到的,有几个核心功能使 OpenFin 具有吸引力:
- 原生操作系统窗口
- 由于不为每个应用程序打包Chromium,Electron的分发量很小
- 进程间总线非常有用
- JavaScript API易于使用
- 应用程序配置文件
我们将在下一节剖析演示应用程序时看到所有这些元素。
一个演示应用程序
本节将详细介绍演示应用程序,但在深入细节之前,让我们先用文字回顾一些事情。
- 有一个小条是主窗口,这被称为Launcher.html。从这里您可以:
- 显示交易员窗口
- 显示图表窗口
- 显示磁贴窗口
- 磁贴窗口可能会用于通过总线向交易员发送消息,以模拟交易的发生,此时交易员将在其交易列表中添加新行。
- 磁贴窗口还可以显示图表,或通过向总线发送消息来更新当前图表的货币对。
- 交易员窗口将监听来自磁贴窗口的消息,并在收到此消息后添加一笔新交易。
- 交易员窗口还可以通过向总线发送消息来显示图表或更新当前图表的货币对。
- 图表窗口仅显示请求URL或通过总线请求的图表。
这就是我们试图构建的东西,如果您回去再看一遍演示视频,您将看到这一切。
如何运行演示应用程序
所以,如果您想运行演示应用程序,您需要这样做:
构建它
- 打开代码文件夹(找到包含
server.js
的文件夹) - 打开一个node命令提示符
npm install
npm install -g parcel-bundler
npm install -g openfin-cli
cd public
parcel launcher.html blotter.html chart.html tiles.html
- 构建完成后,**CTRL+C**,因为我们**不**想使用Parcel的Web主机。
- 如果成功,您应该会在`public\dist`文件夹中看到大量文件。
然后运行它
- 导航到包含
server.js
的根文件夹。 - 打开一个node命令提示符
node server
捆绑器
到目前为止,我们已经确定演示应用程序将是一个HTML5项目。如今,没有一个体面的HTML5项目会缺少捆绑器。Webpack是目前这场圣战中的事实标准,我曾沉浸在创建Webpack配置文件中的荣耀之中,例如:
let _ = require('lodash');
let webpack = require('webpack');
let path = require('path');
let fs = require("fs");
let WebpackOnBuildPlugin = require('on-build-webpack');
let ExtractTextPlugin = require('extract-text-webpack-plugin');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let babelOptions = {
"presets": ["es2015", "react"]
};
function isVendor(module) {
return module.context && module.context.indexOf('node_modules') !== -1;
}
let entries = {
index: './src/index.tsx',
indexCss: './scss/index.scss'
};
//build it to the Play Framework public folder, which is services by the assets controller
let buildDir = path.resolve(__dirname, '../public/dist');
module.exports = {
context: __dirname,
entry: entries,
output: {
filename: '[name].bundle.[hash].js',
path: buildDir,
//this is to make it play nice with the Play Framework Assets controllers
//that deals with static data
publicPath: '/assets/dist'
},
// these break for node 5.3+ when building WS stuff
node: {
fs: 'empty'
},
watch: true,
devServer: {
open: true, // to open the local server in browser
contentBase: __dirname,
},
// Enable sourcemaps for debugging webpack's output.
devtool: "source-map",
resolve: {
extensions: [".tsx", ".ts", ".js", ".jsx"],
modules: [path.resolve(__dirname, "src"), "node_modules"]
},
plugins: [
//The ProvidePlugin makes a module available as a variable in every other
//module required by webpack
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery"
}),
// creates a common vendor js file for libraries in node_modules
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor'],
minChunks: function (module, count) {
return isVendor(module);
}
}),
// creates a common vendor js file for libraries in node_modules
new webpack.optimize.CommonsChunkPlugin({
name: "commons",
chunks: _.keys(entries),
minChunks: function (module, count) {
return !isVendor(module) && count > 1;
}
}),
//will unlink unused files on a build
//http://stackoverflow.com/questions/40370749/how-to-remove-old-files-from-the-build-dir-when-webpack-watch
new WebpackOnBuildPlugin(function (stats) {
const newlyCreatedAssets = stats.compilation.assets;
const unlinked = [];
fs.readdir(path.resolve(buildDir), (err, files) => {
files.forEach(file => {
if (file != "fonts") {
if (!newlyCreatedAssets[file]) {
fs.unlink(path.resolve(buildDir + '\\' + file));
unlinked.push(file);
}
}
});
if (unlinked.length > 0) {
console.log('Removed old assets: ', unlinked);
}
})
}),
//scss/sass files extracted to common css bundle
new ExtractTextPlugin({
filename: '[name].bundle.[hash].css',
allChunks: true,
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'template.html',
})
],
module: {
rules: [
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader' 1st
// then 'babel-loader'
// NOTE : loaders run right to left (think of them as a cmd line pipe)
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: babelOptions
},
{
loader: 'awesome-typescript-loader'
}
]
},
// All files with a .css extenson will be handled by 'css-loader'
{
test: /\.css$/,
loader: ExtractTextPlugin.extract(['css-loader?importLoaders=1']),
},
// All files with a .scss|.sass extenson will be handled by 'sass-loader'
{
test: /\.(sass|scss)$/,
loader: ExtractTextPlugin.extract(['css-loader', 'sass-loader'])
},
// All files with a '.js' extension will be handled by 'babel-loader'.
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: babelOptions
}
]
},
{
test: /\.png$/,
loader: "url-loader?limit=100000"
},
{
test: /\.jpg$/,
loader: "file-loader"
},
{
test: /\.woff(\?.*)?$/,
loader: 'url-loader?prefix=fonts/&name=fonts/[name].[ext]&limit=10000&mimetype=application/font-woff'
},
{
test: /\.woff2(\?.*)?$/,
loader: 'url-loader?prefix=fonts/&name=fonts/[name].[ext]&limit=10000&mimetype=application/font-woff2'
},
{
test: /\.ttf(\?.*)?$/,
loader: 'url-loader?prefix=fonts/&name=fonts/[name].[ext]&limit=10000&mimetype=application/octet-stream'
},
{
test: /\.eot(\?.*)?$/, loader: 'file-loader?prefix=fonts/&name=fonts/[name].[ext]'
},
{
test: /\.svg(\?.*)?$/,
loader: 'url-loader?prefix=fonts/&name=fonts/[name].[ext]&limit=10000&mimetype=image/svg+xml'
},
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader"
}
]
}
};
此配置将为我提供以下功能:
- Babel转译为JS
- TypeScript转译为JS
- SCSS转译为CSS
- 我可以利用入口点来正确理解依赖图。
- 我可以利用SourceMaps
- 我可以使用的图片,如果小于一定大小,会被编码为base64字符串。
- 我可以利用一个HTML模板,该模板可以与我的可散列资产一起使用。
很棒,确实都是很棒的东西……然后我一位更聪明的Web开发同事向我展示了 https://parcel.node.org.cn/,它只需要付出这么少的努力就能实现上述相同的功能:
您能看到那个白色方框中的所有努力吗?没错,是零。根本没有。没有任何配置。这是我付出零努力就得到的结果:
看,我得到了散列的JS文件(即使演示应用程序使用TypeScript),多个入口点,每个文件一个(所以每个入口点一个依赖图),我得到了CSS(即使演示应用程序使用SCSS),哦,我还得到了源映射。
点击查看大图
不信?好吧,这是正在运行的演示应用程序,我已经调试过它,您可以看到我们确实将源映射发送到了浏览器(对于生产环境,有另一个程序 https://parcel.node.org.cn/production.html)。
所有这些都通过这个简单的命令行 parcel launcher.html blotter.html chart.html tiles.html
实现了,这真是太棒了。
Express服务器端代码
OpenFin 可能是一个容器,但它不是魔法,对于任何HTML内容,必须有一个负责提供这些数据的对象。当我们谈论这个话题时,OpenFin 还会创建桌面快捷方式和菜单图标,但这些只是指向app.json
配置文件的快捷方式。如果您没有实际运行应用程序的后端,OpenFin 的快捷方式将根本无法工作。
对于演示应用程序,这是使用Node和Express完成的。Express在此演示应用程序中的作用相当重要,因为它是在其中发生 OpenFin 引导的地方,它读取配置文件,并且还提供各种根目录来为整个演示应用程序提供服务。在此Express代码中,您将找到演示应用程序的所有可能路由。
理想情况下,我会将所有内容链接回一个 proper backend store,但对于这个演示应用程序,我将Express服务器代码用作内存存储库。
function formatDate(date) {
var d = new Date(date);
return [(d.getMonth() + 1).padLeft(),
d.getDate().padLeft(),
d.getFullYear()].join('/') + ' ' +
[d.getHours().padLeft(),
d.getMinutes().padLeft(),
d.getSeconds().padLeft()].join(':');
}
function createGuid() {
var r = (new Date()).getTime().toString(16) + Math.random().toString(16).substring(2) + "0".repeat(16);
return r.substr(0, 8) + '-' + r.substr(8, 4) + '-4000-8' + r.substr(12, 3) + '-' + r.substr(15, 12);
}
function formatTo2Places(theNum) {
var result = Math.round(theNum * 100) / 100
return parseFloat(result.toString(10)).toFixed(2);
}
function copyFile(filename) {
fs.copyFile('public/data/' + filename, 'public/dist/' + filename, (err) => {
if (err) throw err;
});
}
Number.prototype.padLeft = function (base, chr) {
var len = (String(base || 10).length - String(this).length) + 1;
return len > 0 ? new Array(len).join(chr || '0') + this : this;
}
var blotterData = [
{ "pair": "BTCEUR", "price": 5646.00 , "dateCreated": formatDate(Date.now()), "internalId": createGuid() },
{ "pair": "BTCGBP", "price": 5046.54, "dateCreated": formatDate(Date.now()), "internalId": createGuid() }
];
var express = require('express')
, http = require('http')
, path = require('path')
, openfinLauncher = require('openfin-launcher')
, bodyParser = require("body-parser")
, fs = require('fs');
var app = express();
//Here we are configuring express to use body-parser as middle-ware.
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.set('title','Express Parcel app');
app.use(express.static(path.join(__dirname, 'public/dist')));
copyFile('BTCEUR_d.csv');
copyFile('BTCGBP_d.csv');
copyFile('BTCUSD_d.csv');
copyFile('ETHEUR_d.csv');
copyFile('ETHUSD_d.csv');
copyFile('LTCEUR_d.csv');
copyFile('LTCUSD_d.csv');
copyFile('XRPUSD_d.csv');
/* serves main page */
app.get('/', function (req, res) {
res.sendFile("public/dist/launcher.html", { "root": __dirname });
});
app.get('/blotter', function (req, res) {
res.sendFile("public/dist/blotter.html", { "root": __dirname });
});
app.post('/trade', function (req, res) {
blotterData.push({
"pair": req.body.pair,
"price": formatTo2Places(req.body.price),
"dateCreated": formatDate(Date.now()),
"internalId": createGuid() });
res.sendStatus(200);
});
app.get('/chart', function (req, res) {
res.sendFile("public/dist/chart.html", { "root": __dirname });
});
app.get('/tiles', function (req, res) {
res.sendFile("public/dist/tiles.html", { "root": __dirname });
});
app.get('/tileInfos', function (req, res) {
res.send(
[
{ "tilePair": "BTCEUR", "tilePrice": 5646.00 },
{ "tilePair": "BTCGBP", "tilePrice": 5046.54 },
{ "tilePair": "BTCUSD", "tilePrice": 3799.92 },
{ "tilePair": "ETHEUR", "tilePrice": 195.3 },
{ "tilePair": "ETHUSD", "tilePrice": 134.46 },
{ "tilePair": "LTCEUR", "tilePrice": 51.61 },
{ "tilePair": "LTCUSD", "tilePrice": 44.96 },
{ "tilePair": "XRPUSD", "tilePrice": 0.3121 },
]
);
});
app.get('/blotterInfos', function (req, res) {
res.send(blotterData);
});
app.get('/csvdata/:pair', function (req, res) {
var pair = req.params["pair"]
res.sendFile('public/dist/' + pair + '_d.csv', { "root": __dirname });
});
/* process.env.PORT is used in case you want to push to Heroku,
for example, here the port will be dynamically allocated */
var port = process.env.PORT || 1234;
const configPath = path.join(__dirname, 'public', 'app.json');
const localServer = http.createServer(app).listen(port, function(){
console.log('Express server listening on port ' + port);
openfinLauncher.launchOpenFin({ configPath }).then(() => {
localServer.close();
});
});
OpenFin配置文件
为了让 OpenFin 能够运行应用程序,我们需要为其提供一个配置文件。这个文件应该叫做App.json,并包含关于如何引导应用程序的信息。这是演示应用程序的配置文件:
{
"devtools_port": 9090,
"startup_app": {
"name": "OpenfinPOC",
"description": "OpenFin POC",
"url": "https://:1234",
"showTaskbarIcon": true,
"taskbarIcon": "http://cdn.openfin.co/hyperblotter/favicon.ico",
"icon": "http://cdn.openfin.co/hyperblotter/favicon.ico",
"uuid": "Openfin react-redux-parcel demo",
"autoShow": true,
"contextMenu": true,
"defaultWidth": 480,
"maxWidth": 480,
"minWidth": 480,
"defaultHeight": 120,
"maxHeight": 120,
"minHeight": 120,
"frame": true,
"defaultCentered": true,
"resizable": false
},
"runtime": {
"arguments": "--enable-crash-reporting --no-sandbox",
"version": "stable"
},
"shortcut": {
"company": "OpenFin",
"description": "Openfin POC",
"name": "Openfin POC"
}
}
启动器
Launcher是演示应用程序的主窗口,它由Express后端为GET https://:1234 提供服务,看起来是这样的:
这也是Parcel.JS的入口点之一。您可以将每个窗口视为一个独立的应用程序,因此它需要自己的Parcel.js入口点,并且应该有一个HTML页面。在 OpenFin 的行话中,一个窗口等于一个新的HTML页面。对于Parcel来说,这意味着新的入口点/新的依赖图。
因此,这是一个典型的入口点设置(Tiles/Blotter/Graph都遵循此模式):
此页面旨在做什么?
此页面旨在用作一个简单的启动器(顾名思义),它使用 OpenFin JS API来启动其他HTML窗口,其中页面内容通过Express JS提供服务。
HTML
<html>
<head>
</head>
<body id="launcher">
<div id="root"></div>
<script src="src/LauncherEntry.tsx"></script>
</body>
</html>
看它如何引用src/LauncherEntry.tsx
TypeScript
其中src/LauncherEntry.tsx
看起来是这样的:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { LauncherComponent } from './Launcher';
if (window['module'] && window['module'].hot) {
window['module'].hot.dispose(function() {});
window['module'].hot.accept(function() {
ReactDOM.render(<LauncherComponent />, document.getElementById('root'));
});
}
ReactDOM.render(<LauncherComponent />, document.getElementById('root'));
它在挂载React LauncherComponent
时使用热模块加载,这是Launcher
窗口的视图,看起来是这样的:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { interval } from 'rxjs';
//scss
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import { Button } from 'react-bootstrap';
import '../scss/index.scss';
//images
import chartColoredLogo from '../img/chartColored.png';
import chartGrayLogo from '../img/chartGray.png';
import tableColoredLogo from '../img/tableColored.png';
import tableGrayLogo from '../img/tableGray.png';
import tilesColoredLogo from '../img/tilesColored.png';
import tilesGrayLogo from '../img/tilesGray.png';
import HoverImage from "react-hover-image"
//components
import { showChartWindow } from "./utils/ChartUtils"
import { LayoutService } from "./utils/LayoutUtils"
document.addEventListener("DOMContentLoaded", function () {
init();
});
function init() {
console.log("Dom Loaded ", this);
try {
fin.desktop.main(function () {
initWithOpenFin();
})
} catch (err) {
initNoOpenFin();
}
};
async function initWithOpenFin() {
const app = await fin.Application.getCurrent();
const mainWindow = await app.getWindow();
console.log("mainWindow",mainWindow);
await LayoutService.getInstance().hydrateWindows(mainWindow);
interval(1000).subscribe(async x => {
await LayoutService.getInstance().persistWindows(mainWindow);
});
}
function initNoOpenFin() {
alert("OpenFin is not available - you are probably running in a browser.");
}
class Launcher extends React.Component<undefined, undefined> {
constructor(props: any) {
super(props);
}
render() {
return (
<div id="launcherDiv">
<HoverImage className="launcherImages"
src={tableGrayLogo}
hoverSrc={tableColoredLogo}
onClick={this.handleTableClick} />
<HoverImage className="launcherImages"
src={chartGrayLogo}
hoverSrc={chartColoredLogo}
onClick={this.handleChartClick} />
<HoverImage className="launcherImages"
src={tilesGrayLogo}
hoverSrc={tilesColoredLogo}
onClick={this.handleTilesClick} />
</div>
);
}
handleTableClick = async (e) => {
await LayoutService.getInstance().showChildWindow("Blotter", '/blotter', 800, 200, true);
}
handleChartClick = async (e) => {
await showChartWindow('BTCEUR');
}
handleTilesClick = async (e) => {
await LayoutService.getInstance().showChildWindow("Tiles", '/tiles', 560, 350, false);
}
}
export const LauncherComponent = () => (
<Launcher/>
);
这一切都是相当标准的React
内容,很高兴JS终于成熟了,并使用了Lambda(对JS爱好者来说是箭头函数)和async
-await
,万岁!
上面代码中需要特别注意的几点是:
在Init()
方法中,我们检查 OpenFin 是否存在。这几乎是检测您是否处于 OpenFin 模式或尝试在浏览器中运行此应用程序的标准方法(在这种情况下,它可能无法正常工作,因为它毕竟是设计为 OpenFin 应用程序)。
另一个是如何启动新的 OpenFin 窗口,这可以通过此代码实现:
showChildWindow = async (name: string, url: string, width: number, height: number, resizable: boolean) => {
return await fin.Window.create({
name: name,
url: url,
defaultWidth: width,
defaultHeight: height,
width: width,
height: height,
resizable: resizable,
autoShow: true
});
}
磁贴
此页面旨在做什么?
- 此页面旨在显示一系列静态对(这些由Express后端作为静态数据数组提供)。
- 此页面获取初始价格,然后为看到的每个对创建一个新的
Tile
。 Tile
本身利用随机性使当前价格上下波动。Tile
还可以向“blotter
”窗口发送“已创建交易”消息。Tile
还可以显示其在“chart
”窗口中的对。
这就是整个TilesInner
组件的外观:
HTML/React根组件
它与我们上面看到的src/LauncherEntry.tsx
的工作方式大致相同。
TilesTypeScript
这个TilesComponent
负责渲染所有单个Tile
组件,基于传入的数据,它看起来是这样的:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
//scss
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../scss/index.scss';
//components
import { Button } from 'react-bootstrap';
import HoverImage from "react-hover-image"
import { Tile } from './Tile';
import { TileInfo } from './common/commonModels';
//Redux
import { Provider } from 'react-redux'
import { store } from './redux/store';
import { RootState } from './redux/root';
import { connect } from 'react-redux';
import { fetchTilesFromEndpoint } from './redux/tiles';
//images
import loaderLogo from '../img/ajax-loader.gif';
interface TilesProps {
tileInfos: TileInfo[];
tileLoadingError: Boolean
}
interface TilesActions {
fetchTilesFromEndpoint: any;
}
interface TilesState { }
class TilesInner extends React.Component<TilesProps & TilesActions, TilesState> {
constructor(props: any) {
super(props);
}
render() {
return (
(this.props.tileInfos === undefined)
? <div className="Loader">
<div className="LoaderImage">
<span>
<img src={loaderLogo} />
<br />
<span className="Text">Loading</span>
</span>
</div>
</div>
: <div id="tileContainer">
<ul id="listOfTiles">
{
this.props.tileInfos.map(d => <Tile tilePair={d.tilePair} tilePrice={d.tilePrice} />)
}
</ul>
</div>
);
}
componentDidMount() {
this.props.fetchTilesFromEndpoint();
}
//NOTE : This method will be deprecated in near future should use above methods
componentWillReceiveProps = (nextProps) => {
var wasTileLoadingError = nextProps.tileLoadingError;
if (wasTileLoadingError) {
alert("Could not load tiles");
}
}
}
export const Tiles = connect<TilesProps, TilesActions, RootState>(
state => ({
tileInfos: state.tiles.tileInfos,
tileLoadingError: state.tiles.tileLoadingError
}),
{
fetchTilesFromEndpoint: fetchTilesFromEndpoint,
}
)(TilesInner);
export const TilesComponent = () => (
<Provider store={store}>
<Tiles />
</Provider>
);
可以看到,我们利用Redux从store获取状态,并将其派发回TilesInner
组件的props。
我们来看看它是如何工作的。
磁贴Redux工作流程
我们通过this.props.fetchTilesFromEndpoint();
将调用派发到store
以加载状态。然后,这会被派发到Redux store。所以,现在让我们来看看redux store。
import { createStore, applyMiddleware } from 'redux';
import { rootReducer } from './root';
import { convertToPlainAction } from '../utils/redux-with-class/convert-to-plain-action';
import thunk from 'redux-thunk'
let store = createStore(rootReducer, applyMiddleware(convertToPlainAction, thunk));
export { store };
在这里,我们使用这个rootReducer
来保存全局状态。
import * as redux from 'redux';
import { TilesInfoState, tilesReducer } from './tiles';
import { BlotterInfoState, blotterReducer } from './blotter';
import { ChartInfoState, chartReducer } from './chart';
export interface RootState {
tiles: TilesInfoState;
blotter: BlotterInfoState;
chart: ChartInfoState;
}
export const rootReducer = redux.combineReducers({
tiles: tilesReducer,
blotter: blotterReducer,
chart: chartReducer
});
其中TilesInner
组件的实际reducer看起来是这样的(请注意,我们使用 Redux-Thunk 来派发函数),可以看到这段代码使用fetch api调用Express JS后端(基本上是服务器端代码),然后使用promises来处理要派发的内容(即错误或好的派发到连接到React store的组件TilesInner
)。
import { ActionWrapper } from '../utils/redux-with-class/actionwrapper';
import { TileInfo } from '../common/commonModels';
export interface TilesInfoState {
tileInfos: TileInfo[];
tileLoadingError: boolean;
}
const initialState: TilesInfoState = {
tileInfos: TileInfo[0],
tileLoadingError: false
};
function fetchTileInfos() {
return fetch('/tileInfos').then(x => {
return x.json();
});
}
export function fetchTilesFromEndpoint() {
return function (dispatch) {
return fetchTileInfos()
.then(
jsonTiles => dispatch(new TileLoadedAction(jsonTiles)),
reason => dispatch(new TileLoadingErrorAction()
));
};
}
export function tilesReducer(state: TilesInfoState = initialState, wrapper: ActionWrapper) {
const action = wrapper.action;
if (action instanceof TileLoadedAction) {
return {
...state,
tileInfos: action.tileInfos,
tileLoadingError: false
}
}
if (action instanceof TileLoadingErrorAction) {
return {
...state,
tileInfos: new Array<TileInfo>(),
tileLoadingError: true
};
}
return state;
}
class TileLoadedAction {
type = 'My-App/Tile-Infos-Loaded';
constructor(public tileInfos: TileInfo[]) {
}
}
class TileLoadingErrorAction {
type = 'My-App/Tile-Infos-Loading-Error';
constructor() {
}
}
其他窗口“blotter”和“chart”也以类似的方式使用React-Redux,所以我不会再次讨论Redux的工作原理,因为它只是相同的方式。
好的,现在我们已经看到了Tiles
,一个单独的Tile
是如何工作的?
此组件旨在做什么?
Tile
本身利用随机性使当前价格上下波动。Tile
还可以向“blotter
”窗口发送“已创建交易”消息。Tile
还可以显示其在“chart
”窗口中的对。
这就是整个Tile
组件的外观:
好吧,让我们看看它的React标记。
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { interval } from 'rxjs';
//scss
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../scss/index.scss';
//images
import tickGrayLogo from '../img/tickGray.png';
import tickColoredLogo from '../img/tickColored.png';
import chart16GrayLogo from '../img/chart16Gray.png';
import chart16ColoredLogo from '../img/chart16Colored.png';
import blackTriangleUpLogo from '../img/BlackTriangleUp.png';
import greenTriangleUpLogo from '../img/GreenTriangleUp.png';
import blackTriangleDownLogo from '../img/BlackTriangleDown.png';
import redTriangleDownLogo from '../img/RedTriangleDown.png';
//components
import { Button } from 'react-bootstrap';
import HoverImage from "react-hover-image"
import { TileInfo } from './common/commonModels';
import { formatTo2Places } from './common/commonFunctions';
import { showChartWindow } from "./utils/ChartUtils"
interface TileProps {
tilePair: string;
tilePrice: number;
}
interface TileState {
tilePriceRaw: number;
tileFraction: string;
tilePriceDigit: string;
isInitaialised: boolean;
isUp: boolean;
}
export class Tile extends React.Component<TileProps, TileState> {
constructor(props: any) {
super(props);
this.state = {
tilePriceDigit: "0",
tileFraction: "0",
tilePriceRaw: 0,
isInitaialised: false,
isUp:false
};
}
render() {
return (
<li key={this.props.tilePair}>
<div className="card">
<div className="tileDescription">
<p className="tilePair">{this.props.tilePair}</p>
<div id="tileNumbers">
<span>
<span className="tileLittleNumbers">{this.state.tilePriceDigit}</span>
<span className="tileLittleNumbers">.</span>
<span className="tileBigNumbers">{this.state.tilePriceFraction}</span>
</span>
</div>
<div className="tileArrowUp">
{this.state.isUp ? <img src={greenTriangleUpLogo} /> : <img src={blackTriangleUpLogo} />}
</div>
<div className="tileArrowDown">
{this.state.isUp ? <img src={blackTriangleDownLogo} /> : <img src={redTriangleDownLogo} />}
</div> </div>
<div className="tileCommands">
<span>
<HoverImage className="tileImages"
src={tickGrayLogo}
hoverSrc={tickColoredLogo}
onClick={this.handleTilePlaceTradeClick} />
</span>
<span>
<HoverImage className="tileImages2"
src={chart16GrayLogo}
hoverSrc={chart16ColoredLogo}
onClick={this.handleChartClick} />
</span>
</div>
</div>
</li>
);
}
componentDidMount() {
interval(500).subscribe(x => {
this.randomlyJiggleState();
});
this.randomlyJiggleState();
}
handleTilePlaceTradeClick = async (e) => {
this.publishMessage();
}
handleChartClick = async (e) => {
await showChartWindow(this.props.tilePair);
}
publishMessage = () => {
fetch('/trade', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
pair: this.props.tilePair,
price: this.state.tilePriceRaw
})
})
.then(
status => {
fin.desktop.InterApplicationBus.publish("created-trade-from-tile", {
pair: this.props.tilePair,
price: this.state.tilePriceRaw
});
},
reason => {
alert('something went wrong while saving trade');
}
));
}
generateRandomNumber = (divisor) => {
var self = this;
return Math.random() / divisor
}
randomlyJiggleState = () => {
var self = this;
if (self.state === undefined || self.state == null)
return;
if (self.state.isInitaialised === true) {
var isNegativeChance = self.generateRandomNumber(1.0);
var fiddleFactor = self.generateRandomNumber(20.0);
if (isNegativeChance > 0.5) {
fiddleFactor = -fiddleFactor;
}
var newTilePriceRaw = self.state.tilePriceRaw + fiddleFactor;
var newIsUp = newTilePriceRaw > self.state.tilePriceRaw;
var newTilePrice = formatTo2Places(newTilePriceRaw);
var newTilePriceDigit = newTilePrice.substring(0, newTilePrice.indexOf('.'));
var newTilePriceFraction = newTilePrice.substring(newTilePrice.indexOf('.') + 1);
self.setState((state, props) => ({
tilePriceDigit: newTilePriceDigit,
tilePriceFraction: newTilePriceFraction,
tilePriceRaw: newTilePriceRaw,
isUp: newIsUp
}));
}
else {
var newTilePriceRaw = self.props.tilePrice
var newTilePrice = formatTo2Places(newTilePriceRaw)
var newTilePriceDigit = newTilePrice.substring(0, newTilePrice.indexOf('.'));
var newTilePriceFraction = newTilePrice.substring(newTilePrice.indexOf('.'));
self.setState((state, props) => ({
tilePriceDigit: newTilePriceDigit,
tilePriceFraction: newTilePriceFraction,
tilePriceRaw: newTilePriceRaw,
isInitaialised: true,
isUp:true
}));
}
}
};
正如之前所示,此组件能够显示一个新的 OpenFin 窗口,它还利用 OpenFin 进程间总线,如果您查看这些行,这就是发布的工作方式:
fin.desktop.InterApplicationBus.publish("created-trade-from-tile", {
pair: this.props.tilePair,
price: this.state.tilePriceRaw
});
交易员
此页面旨在做什么?
- 此页面旨在显示Express端点的交易列表。
- 它还监听来自
Tile
的新交易。 - 当点击一行时,它还可以显示一个新的图表,其中该行的货币对将用于显示“
chart
”窗口。
这就是整个BlotterInner
组件的外观:
HTML/React根组件
它与我们上面看到的src/LauncherEntry.tsx
的工作方式大致相同。
TilesTypeScript
这个BlotterComponent
负责渲染所有交易,其代码如下:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import ReactTable from "react-table";
//scss
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../scss/index.scss';
import 'react-table/react-table.css'
//components
import { Button } from 'react-bootstrap';
import HoverImage from "react-hover-image"
import { Tile } from './Tile';
import { BlotterInfo } from './common/commonModels';
import { formatTo2Places } from './common/commonFunctions';
import { showChartWindow } from "./utils/ChartUtils"
//Redux
import { Provider } from 'react-redux'
import { store } from './redux/store';
import { RootState } from './redux/root';
import { connect } from 'react-redux';
import { fetchBlotterFromEndpoint } from './redux/blotter';
//images
import loaderLogo from '../img/ajax-loader.gif';
interface BlotterProps {
blotterInfos: BlotterInfo[];
blotterLoadingError: Boolean
}
interface BlotterActions {
fetchBlotterFromEndpoint: any;
}
interface BlotterState {
selectedRow: number;
}
Number.prototype.padLeft = function (base, chr) {
var len = (String(base || 10).length - String(this).length) + 1;
return len > 0 ? new Array(len).join(chr || '0') + this : this;
}
class BlotterInner extends React.Component<BlotterProps & BlotterActions, BlotterState> {
constructor(props: any) {
super(props);
}
render() {
const columns = [
{
Header: () => (
<div
style={{
textAlign: "left"
}}
>InternalId</div>),
accessor: 'internalId',
width:270
},
{
Header: () => (
<div
style={{
textAlign: "left"
}}
>Pair</div>),
accessor: 'pair',
width:100
},
{
Header: () => (
<div
style={{
textAlign: "left"
}}
>Price</div>),
accessor: 'price',
width:100
},
{
Header: () => (
<div
style={{
textAlign: "left"
}}
>Date Created</div>),
accessor: 'dateCreated',
width:200
}
]
return (
(typeof this.props === "undefined" || this.props === null
|| typeof this.props.blotterInfos === "undefined" || this.props.blotterInfos === null))
? <div className="Loader">
<div className="LoaderImage">
<span>
<img src={loaderLogo} />
<br />
<span className="Text">Loading</span>
</span>
</div>
</div>
:
<ReactTable
data={this.props.blotterInfos}
columns={columns}
getTrProps={(state, rowInfo, column, instance) => {
if (typeof rowInfo !== "undefined") {
return {
onClick: (e, handleOriginal) => {
this.setState({
...this.state,
selectedRow: rowInfo.index
})
this.handleRowClick(rowInfo, instance,"if")
},
style: {
background: this.checkRowIsSelected(rowInfo) ? 'cornflowerblue' : '#2c2c2c',
color: this.checkRowIsSelected(rowInfo) ? 'black' : 'white'
},
}
}
else {
return {
onClick: (e, handleOriginal) => {
this.handleRowClick(rowInfo, instance, "else")
},
style: {
background: '#2c2c2c',
color: 'white'
},
}
}
}}
/>
);
}
componentDidMount = () => {
this.props.fetchBlotterFromEndpoint();
this.initInterApp();
}
//NOTE : This method will be deprecated in near future should use above methods
componentWillReceiveProps = (nextProps) => {
var wasBlotterLoadingError = nextProps.blotterLoadingError;
if (wasBlotterLoadingError) {
alert("Could not load blotter");
}
}
checkRowIsSelected = (rowInfo) => {
var result = false;
if (typeof rowInfo !== "undefined" && typeof this.state !== "undefined" && this.state != null) {
result= rowInfo.index === this.state.selectedRow ? true : false;
}
console.log("result", result);
return result;
}
handleRowClick = async (rowInfo, instance, fromwhere) => {
console.log(rowInfo);
if (typeof rowInfo !== "undefined") {
await showChartWindow(rowInfo.row.pair);
}
}
isSelected = (rowInfo) => {
return false;
}
initInterApp = () => {
self = this;
fin.desktop.InterApplicationBus.subscribe("*","created-trade-from-tile",
function (message, uuid) {
self.props.fetchBlotterFromEndpoint();
});
};
}
export const Blotter = connect<BlotterProps, BlotterActions, RootState>(
state => ({
blotterInfos: state.blotter.blotterInfos,
blotterLoadingError: state.blotter.blotterLoadingError
}),
{
fetchBlotterFromEndpoint: fetchBlotterFromEndpoint,
}
)(BlotterInner);
export const BlotterComponent = () => (
<Provider store={store}>
<Blotter/>
</Provider>
);
其中真正重要的部分是:
- 它使用 React-Table 来渲染网格。
- 它像这样订阅 OpenFin 进程间总线:
fin.desktop.InterApplicationBus.subscribe("*","created-trade-from-tile",
function (message, uuid) {
self.props.fetchBlotterFromEndpoint();
});
图表
此页面旨在做什么?
- 此页面旨在显示给定货币对的图表,其中图表数据是由Express JS提供的静态CSV文件。
- 它还监听来自
Tile
的总线消息。 - 它还监听来自
Blotter
的总线消息。 - 它还可以显示来自
Launcher
的新图表窗口,其中货币对是Chart
窗口URL的一部分。
这就是整个CHartInner
组件的外观:
HTML/React根组件
它与我们上面看到的src/LauncherEntry.tsx
的工作方式大致相同。
TilesTypeScript
这个ChartComponent
负责渲染所有交易,其代码如下:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
//scss
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../scss/index.scss';
import 'react-table/react-table.css'
//Redux
import { Provider } from 'react-redux'
import { store } from './redux/store';
import { RootState } from './redux/root';
import { connect } from 'react-redux';
import { fetchChartDataFromWeb } from './redux/chart';
//images
import loaderLogo from '../img/ajax-loader.gif';
//chart
import { FixedWidthChart } from './CryptoChart';
import { getData } from "./utils/ChartUtils"
interface ChartProps {
chartData: any;
chartLoadingError: boolean;
}
interface ChartActions {
fetchChartDataFromWeb: any;
}
interface ChartState {
chartData: any;
pair: string;
}
export class ChartInner extends React.Component<ChartProps & ChartActions, ChartState> {
constructor(props: any) {
super(props);
}
render() {
return (
(typeof this.state === "undefined" || this.state === null
|| typeof this.state.chartData === "undefined" || this.state.chartData === null)
? <div className="Loader">
<div className="LoaderImage">
<span>
<img src={loaderLogo} />
<br />
<span className="Text">Loading</span>
</span>
</div>
</div>
:
<div className="chartContainer">
<FixedWidthChart data={this.state.chartData} pair={this.state.pair} />
</div>
);
}
initInterApp = () => {
self = this;
fin.desktop.InterApplicationBus.subscribe("*", "view-chart-for-pair",
function (message, uuid) {
window.location.assign("/chart?pair=" + message.pair);
document.title = window.location.hostname + ':' + window.location.port + "/chart?pair=" + message.pair;
});
};
componentDidMount = () => {
this.initInterApp();
const searchParams = new URLSearchParams(location.search);
var reqPair = searchParams.get('pair') || 'BTCEUR'
console.log("Query string pair", reqPair);
this.setState({ pair:reqPair });
this.props.fetchChartDataFromWeb(reqPair);
}
//static getDerivedStateFromProps(nextProps, prevState) {
//
//}
//componentDidUpdate(prevProps, prevState) {
//
//}
//NOTE : This method will be deprecated in near future should use above methods
componentWillReceiveProps = (nextProps) => {
console.log("props data", nextProps.chartData);
var wasChartLoadingError = nextProps.chartLoadingError;
if (wasChartLoadingError) {
alert("Could not load chart data");
return;
}
//NOTE :Dont know why I have to use sta ebyt react-stockcharts doesnt like using the react state
//transferred to props at all. Hey ho though
if (nextProps.chartData != null) {
this.setState({ chartData: nextProps.chartData });
}
}
}
export const TheChart = connect<ChartProps, ChartActions, RootState>(
state => ({
chartData: state.chart.chartData,
chartLoadingError: state.chart.chartLoadingError
}),
{
fetchChartDataFromWeb: fetchChartDataFromWeb
}
)(ChartInner);
export const ChartComponent = () => (
<Provider store={store}>
<TheChart />
</Provider>
);
其中创建图表的核心功能由这个React组件处理:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { format } from "d3-format";
import { timeFormat } from "d3-time-format";
import { ChartCanvas, Chart, ZoomButtons } from "react-stockcharts";
import {
BarSeries,
CandlestickSeries,
} from "react-stockcharts/lib/series";
import { XAxis, YAxis } from "react-stockcharts/lib/axes";
import {
CrossHairCursor,
MouseCoordinateX,
MouseCoordinateY,
} from "react-stockcharts/lib/coordinates";
import { discontinuousTimeScaleProvider } from "react-stockcharts/lib/scale";
import {
OHLCTooltip,
} from "react-stockcharts/lib/tooltip";
import { fitWidth } from "react-stockcharts/lib/helper";
import { last } from "react-stockcharts/lib/utils";
interface CandleStickChartWithZoomPanProps {
data: any[],
width: number,
ratio: number,
pair: string
};
class CandleStickChartWithZoomPan extends React.Component<CandleStickChartWithZoomPanProps, undefined> {
constructor(props) {
super(props);
this.saveNode = this.saveNode.bind(this);
this.resetYDomain = this.resetYDomain.bind(this);
this.handleReset = this.handleReset.bind(this);
}
componentWillMount() {
this.setState({
suffix: "XXX"
});
}
saveNode(node) {
this.node = node;
}
resetYDomain() {
this.node.resetYDomain();
}
handleReset() {
this.setState({
suffix: this.state.suffix + 1
});
}
render() {
const { data: initialData, width, ratio } = this.props;
const type = 'svg';
const mouseMoveEvent, panEvent, zoomEvent = true;
const zoomAnchor, clamp = false;
const xScaleProvider = discontinuousTimeScaleProvider
.inputDateAccessor(d => d.date);
const {
data,
xScale,
xAccessor,
displayXAccessor,
} = xScaleProvider(initialData);
const start = xAccessor(last(data));
const end = xAccessor(data[Math.max(0, data.length - 150)]);
const xExtents = [start, end];
const margin = { left: 70, right: 70, top: 20, bottom: 30 };
const height = 400;
const gridHeight = height - margin.top - margin.bottom;
const gridWidth = width - margin.left - margin.right;
const showGrid = true;
const yGrid = showGrid ? { innerTickSize: -1 * gridWidth, tickStrokeOpacity: 0.2 } : {};
const xGrid = showGrid ? { innerTickSize: -1 * gridHeight, tickStrokeOpacity: 0.2 } : {};
return (
<div>
<h1>{this.props.pair}</h1>
<ChartCanvas ref={this.saveNode} height={height}
ratio={ratio}
width={width}
margin={{ left: 70, right: 70, top: 10, bottom: 30 }}
mouseMoveEvent={mouseMoveEvent}
panEvent={panEvent}
zoomEvent={zoomEvent}
clamp={clamp}
zoomAnchor={zoomAnchor}
type={type}
seriesName={`${this.state.suffix}`}
data={data}
xScale={xScale}
xExtents={xExtents}
xAccessor={xAccessor}
displayXAccessor={displayXAccessor}
>
<Chart id={1} yExtents={d => [d.high, d.low]}>
<XAxis axisAt="bottom"
orient="bottom"
zoomEnabled={zoomEvent}
{...xGrid} />
<YAxis axisAt="right"
orient="right"
ticks={5}
zoomEnabled={zoomEvent}
{...yGrid}
/>
<MouseCoordinateY
at="right"
orient="right"
displayFormat={format(".2f")} />
<CandlestickSeries />
<OHLCTooltip origin={[-40, 0]} />
<ZoomButtons
onReset={this.handleReset}
/>
</Chart>
<Chart id={2}
yExtents={d => d.volume}
height={150} origin={(w, h) => [0, h - 150]}>
<YAxis
axisAt="left"
orient="left"
ticks={5}
tickFormat={format(".2s")}
zoomEnabled={zoomEvent}
/>
<MouseCoordinateX
at="bottom"
orient="bottom"
displayFormat={timeFormat("%Y-%m-%d")} />
<MouseCoordinateY
at="left"
orient="left"
displayFormat={format(".4s")} />
<BarSeries yAccessor={d => d.volume} fill={(d) => d.close > d.open ? "#6BA583" : "#FF0000"} />
</Chart>
<CrossHairCursor />
</ChartCanvas>
</div>
);
}
}
export const FixedWidthChart: any = fitWidth(CandleStickChartWithZoomPan);
既然我已经向您展示了总线订阅的工作原理,这个组件就没有太多可补充的了。
持久化状态
OpenFin 提供了这个很酷的 Layouts API,它负责窗口的停靠、吸附,允许您持久化窗口状态/位置等。
手动解决方案
听起来很棒。唯一的障碍是,如果您像我一样,拥有一个小的笔记本电脑,但分辨率为4K,这与 OpenFin 配合不佳。如果您尝试运行 OpenFin 的 Layouts API,同时您的缩放值不是100%,您将收到来自 OpenFin Layouts API 的启动错误消息。
当然,您可以将此缩放调整为100%,但这样您的PC就无法使用了。
您可以通过显示设置更改缩放,如下所示:
对我来说,这真是一件令人沮丧的事情。我想能够保存/恢复状态,那么我能做什么呢?好吧,我考虑了一下,觉得好吧,它不会像原生的 OpenFin 那样花哨,但我可以只使用本地存储来保存/恢复我的窗口布局,所以我就是这样做的。
import { PersistedWindowInfo } from './../common/commonModels';
export class LayoutService {
private static instance: LayoutService;
private static isLoading: boolean;
private constructor() {
}
static getInstance() {
if (!LayoutService.instance) {
LayoutService.instance = new LayoutService();
}
return LayoutService.instance;
}
hydrateWindows = async (mainWindow) => {
if (typeof (Storage) === "undefined") {
console.log('browser doesnt support local storage');
return;
}
try {
LayoutService.isLoading = true;
var persistedPersistedWindowsJson = localStorage.getItem('persisted-app-state');
if (persistedPersistedWindowsJson !== null) {
var persistedPersistedWindows = [] as PersistedWindowInfo[];
persistedPersistedWindows = JSON.parse(persistedPersistedWindowsJson);
console.log('Hydrating using this from storage', persistedPersistedWindows);
//deal with main window
var mainWindowInfo = await mainWindow.getInfo();
for (var i = 0; i > persistedPersistedWindows.length; i++) {
var persistedPersistedWindow = persistedPersistedWindows[i];
if (persistedPersistedWindow.isChildWindow === true) {
const theChildWindow = await this.showChildWindow(
persistedPersistedWindow.name,
persistedPersistedWindow.url,
persistedPersistedWindow.width,
persistedPersistedWindow.height,
persistedPersistedWindow.resizable);
await theChildWindow.setBounds({
height: persistedPersistedWindow.height,
width: persistedPersistedWindow.width,
top: persistedPersistedWindow.top,
left: persistedPersistedWindow.left
});
}
}
}
}
catch (e) {
console.log(e);
}
finally {
LayoutService.isLoading = false;
}
}
persistWindows = async (mainWindow) => {
if (typeof (Storage) === "undefined") {
console.log('browser doesnt support local storage');
return;
}
if (LayoutService.isLoading)
return;
var persistedPersistedWindows = [] as PersistedWindowInfo[];
//obtain main window details
var persistedWindowInfo = await this.extractInfoForWindow(mainWindow, false);
persistedPersistedWindows.push(persistedWindowInfo);
//obtain child window details
const app = await fin.Application.getCurrent();
var childWindows = await app.getChildWindows();
for (var i = 0; i > childWindows.length; i++) {
persistedWindowInfo = await this.extractInfoForWindow(childWindows[i], true);
var windowUrlWithoutQueryString = persistedWindowInfo.name;
if (persistedWindowInfo.name.indexOf("?") > 0) {
windowUrlWithoutQueryString = persistedWindowInfo.name.substring(0, persistedWindowInfo.name.indexOf("?"));
}
console.log("windowUrlWithoutQueryString",windowUrlWithoutQueryString);
persistedPersistedWindows = persistedPersistedWindows.filter(function (value, index, arr) {
return !value.name.startsWith(windowUrlWithoutQueryString);
});
persistedPersistedWindows.push(persistedWindowInfo);
}
console.log(persistedPersistedWindows);
console.log("has this many items",persistedPersistedWindows.length);
localStorage.removeItem('persisted-app-state');
localStorage.setItem('persisted-app-state', JSON.stringify(persistedPersistedWindows));
var fromStorage = JSON.parse(localStorage.getItem('persisted-app-state'));
console.log(fromStorage);
console.log("from storage has this many items", fromStorage.length);
}
extractInfoForWindow = async (theWindow, isChild) => {
var bounds = await theWindow.getBounds();
var info = await theWindow.getInfo();
var options = await theWindow.getOptions();
return new PersistedWindowInfo(
info.title,
info.url,
bounds.width,
options.defaultWidth,
options.defaultHeight,
bounds.height,
bounds.left,
bounds.top,
options.resizable,
isChild);
}
showChildWindow = async (name: string, url: string, width: number, height: number, resizable: boolean) => {
return await fin.Window.create({
name: name,
url: url,
defaultWidth: width,
defaultHeight: height,
width: width,
height: height,
resizable: resizable,
autoShow: true
});
}
}
实际上效果非常好。
使用OpenFin布局API
所以我决定在我的工作机器上试用它,我的显示器有100%的缩放。下面的代码是修改后的LayoutService
。
注意:如果您想从演示代码中运行它,它在“using-layouts-api”分支中。
import * as Layouts from "openfin-layouts"
import { interval } from 'rxjs';
export class LayoutService {
private static instance: LayoutService;
private constructor() {
Layouts.workspaces.setRestoreHandler(this.appRestoreHandler);
Layouts.workspaces.setGenerateHandler(() => {
//return custom data
//return {"foo":"bar"};
return {}
});
Layouts.workspaces.ready();
}
static getInstance() {
if (!LayoutService.instance) {
LayoutService.instance = new LayoutService();
}
return LayoutService.instance;
}
persistWindows = async () => {
const workspaceObject = await Layouts.workspaces.generate();
localStorage.setItem('persisted-app-state', JSON.stringify(workspaceObject));
}
hydrateWindows = async () => {
var persistedData = localStorage.getItem('persisted-app-state');
if (persistedData !== null) {
console.log('found old state restoring using it')
var workspaceObject = JSON.parse(persistedData);
await Layouts.workspaces.restore(workspaceObject);
}
interval(5000).subscribe(async x => {
await LayoutService.getInstance().persistWindows();
});
}
appRestoreHandler = async (workspaceApp) => {
const sleep = m => new Promise(r => setTimeout(r, m))
//iterate through the child windows of the workspaceApp data
for (var i = 0; i < workspaceApp.childWindows.length; i++) {
await this.openChild(workspaceApp.childWindows[i].name, workspaceApp.childWindows[i].url, workspaceApp.childWindows[i].bounds);
}
return layoutApp;
}
openChild = async (name: string, url: string, bounds : any) => {
const win = await fin.Window.create({
name: name,
url: url,
defaultWidth: 100,
defaultHeight: 100,
width: 100,
height: 100,
resizable: false,
autoShow: true
});
await this.positionWindow(win, bounds);
}
positionWindow = async (theWindow: any, bounds: any) => {
await theWindow.setBounds({
height: bounds.height,
width: bounds.width,
top: bounds.top,
left: bounds.left
});
}
showChildWindow = async (name: string, url: string, width: number, height: number) => {
await fin.Window.create({
name: name,
url: url,
defaultWidth: width,
defaultHeight: height,
width: width,
height: height,
resizable: false,
autoShow: true
});
}
}
调试
作为开发人员,您迟早会想要调试您的代码,所以您可能需要知道如何做到这一点。对于 OpenFin 来说,最简单的方法是允许您的应用程序中的上下文菜单,您可以在上面看到的 OpenFin 配置文件app.json
中设置。您基本上设置了这一行 "contextMenu": true
。
启用此选项后,您可以看到一个菜单并轻松启动Chrome调试工具。
结论
我确实认为 OpenFin 很酷,并且有一些很棒的应用程序是用Electron/OpenFin编写的,我真的很喜欢他们试图带回桌面类应用程序的方式(交易员实际上喜欢,与所有人说的相反,能够选项卡/分离、固定、在不同显示器上移动东西确实很有用)。
话虽如此,我上面提到的OpenFin-Layouts API问题以及它如何与Windows(即使是Windows 10)的缩放配合工作,使得它在跨不同显示器/不同分辨率运行时完全无法使用。这意味着这个相当吸引人的服务,除非您确切知道将要运行的硬件,并且它的缩放比例恰好是100%,否则无法使用。
所以这方面真是太糟糕了。
好的部分是我发现API非常容易使用,并且发现我仍然可以轻松地运行我的React/Redux和Typescript。
所以,对我来说,总体上可能是75%好25%坏。
我把结论留给你们。
如果您喜欢这篇文章,请在此投票或给仓库点星。