1

clipboard.png

原文地址:https://zhuanlan.zhihu.com/p/...,欢迎转载 :-)

? 关于

其实对于这个专栏的订阅用户感到非常抱歉,已经停更很久了,也没啥特别的理由就是懒 orz!不对,画风不能这样开头,是这样的,我觉得我应该用 React 去做点儿什么,写文章能够清晰我的思路,让我和别人有交流,但是并没有实际做产品那么性感,于是我决定要用 React 来做一些产品出来,于是就有了 “氢” http://origingroup.tech 相信我在这里对氢的介绍以及实现技术介绍也能对大家有帮助。

所以这篇文章我主要想介绍一些做这个东西的思路、想法和一些技术实现以及用到的一些工具。

? “氢” 是什么鬼

这个名字听着就很奇怪,为什么是个 H 元素作为名字,对,是这样的,先不管氢是什么东西,我的想法是我要用 React 去做一些产品出来那总得取个名字吧,“一些产品” 意味着我得费好多经历去想产品名称,好了,不如就按照元素周期表的顺序取名字吧,于是“氢”这个名字就这样定了。 :- ) (我都觉得我是天才了,hhhhhh ☞ )

好了,说正经的,其实做氢这个东西早有预谋,很久很久以前我做了一些产品,然后夭折了,eee...... 于是现在我决定把它们捡起来(感觉哪里不对。。)!

其中的一个夭折的是 “一个针对个人的项目管理工具”,本想提供 saas 的服务,结果被我改来改去最后tm很长时间没空理它不玩了,于是最终被改到现在的 “针对个人的笔记,任务,待办离线管理工具” ,于是就有了这个叫氢的家伙(当然之前给它取的名字不叫这个。。)

? 那氢提供的核心功能有哪些呢?

  • 提供 workflowy 功能的强大列表,能够将日常的琐碎的事情,用极致简单的交互去做增删改查

  • 核心目标基于任务管理来打造,所以参考了很多任务管理工具的 UX 以及自己的想法-结合 scrum 模型,让它在任务管理上也能简单极致(最开始核心目标太多,导致产品几乎难产,其实最后的氢是一个删减吧,这也让我懂得了做产品需要克制)

  • 最后我需要有一个 powerful 的编辑器,因为平时记录事情的时候总的写点儿什么呀,于是任务详情被打造成了一个 wysiwyg 编辑器,并且支持用 markdown 语法,可以直接粘贴 markdown 生成样式,后面还会提供导出 markdown 功能

  • 当然得漂亮

好了,说了辣么多,先上几个图,毕竟有句名言叫-meitushuogejb

第一张图就是我说的 列表 ,具体怎么好用可能还得自己体验过才知道,所见即所得,回车就创建一个任务,tab 一下任务就变为子任务,ctrl + command + up/down

clipboard.png

clipboard.png

clipboard.png

clipboard.png

⚒ 说一说技术实现

如你所见,上面的这个东西是一个 Desktop app ,具体实现方式是:

Electron + React

最开始的时候只是针对 web 版本的,所以技术全是围绕 React 来,后来决定该为 Desktop app ,当然第一选择是 electron,过程中也是遇到了不少坑的地方,下面分几个方便来分享一下

  1. React 技术栈的选择

  2. 项目结构与 Webpack 打包编译

  3. Electron 相关的使用细节

  4. 如何用 Electron 做 i18n
    React 技术栈选择

基本的技术使用和我在本专栏中提到的无差,具体为:

  • ES6 + JSX + Less :作为基本的语言层选择,这套路基本还是很常用的了

  • Ant.design: 使用 ant.design 作为基础库,这里感谢玉伯大大团队的贡献

  • Redux + Redux-Saga:现在熟悉了 redux 和 redux-saga 过后很难再改为其他的方式,因为感觉这种配套已经很极致了,在处理数据流转已经 UI 交互的时候,redux-saga 几乎是个 magic 的工具

  • draft.js + draft-plugin-js: 在编辑器的选择上使用了 Facebook 的 draft.js , 如果你愿意详细了解其设计和架构的话也会觉得这也是个伟大的项目,同时通过 draft-plugin-js 可以很快的将编辑器功能组件化,很容易通过 hook 定制自己的编辑器,氢中的编辑器有两个,第一个是列表项每个都是一个编辑器实例,任务详情,定制化的一个富文本编辑器

  • Immutable.js: 不可变数据,这对于列表来说真是太重要了,React 如果不进行优化的在特殊情况下会有严重的性能问题,氢种的列表就是这种特殊情况,一编辑某一个任务,处理不好会卡,会抖动。因此我在做列表的时候就决定由 immutable 重构了整个项目,同时列表的数据结构也是做了特殊的设计和优化的。(比如一个小问题,上下移动,如何确定顺序呢?)

  • React-intl:氢做了基本的国际化,也就支持英文,当然现在应该有很多语法错误还没检查,使用的是 React-intl。

  • React-vitualized:这个项目也是为了做性能优化用的,不过现在的版本因为优化了数据结构不需要了

项目结构与 Webpack 打包编译

项目的目录结构设计和 webpack 打包编译才是头痛的问题,当 webpack 遇上 electron,各种环境不一致导致的奇怪 bug 我是不会跟别人说的,☝️个人默默的承受。。。

先上个项目结构的图

clipboard.png

好长的目录,其中关键的地方是 containers 的设计,项目中 containers 目前的设计是一个 cotainer 包含

  • reducer.js

  • sagas.js

  • index.jsx

  • styles.less

  • components

当然图中的项目结构中有专门的 reducers 目录和 sagas 目录,这是因为之前的设计没改,后面的 reducer ,style,sagas 都是组件自包含的。

说说 webpack 的打包

感谢这个项目帮我解决了不少坑 chentsulin/electron-react-boilerplate 正常情况下的 Electron ,React 项目基本就按照这个 boilerplate 来就行了,不过我自己在打包上做了很多自定义的修改,所以才会出现很多 webpack 的问题。

其中的一个优化点是,不使用 dll ,无论 production 还是 dev 都会使用 vendors.js ,而这个 vendors 是预先打包压缩好了的,所以每次打包实际都是只打包了业务代码。具体方式是看图就知道了

通过定义 externals 让打包过程忽略这些模块的打包,使用 external 方式引用

clipboard.png

专门打包 vendor 的webpack 配置,一定要注意 libraryTarget

clipboard.png

Electron 相关的使用细节

这里不想列举太多细节,说说其中的一个,初始化 loading 加载,因为 electron 每次打开都几乎是打开一个浏览器,在执行大的js 上也会花很多时间,所以会出现1s到2s的停顿,显得应用很卡。

氢中解决这个问题的方式使用一个专门负责 loading 的 window ,这个页面极其简单,很快就可以加载出来了,然后这个时候打开主要的 window,当这个window 加载完了再显示出来,再关闭负责 loading 的 window, 下面我直接贴出 main.js 的代码,有需要的可以拿走

app.on('ready', createWindow);

function createWindow () {
  locale = app.getLocale();
  landingWindow = new BrowserWindow({
    show: false,
    frame: false,
    width: 490,
    height: 400
  })

  landingWindow.once("show", () => {
    // Create the browser window.
    mainWindow = new BrowserWindow({
      width: 1000, 
      height: 740,
      titleBarStyle: 'hidden',
      icon: `file://${__dirname}/assets/imgs/logo.png`,
      show: false
    })

    mainWindow.once("show", () => {
      landingWindow.hide()
      landingWindow.close()
      landingWindow.removeAllListeners();
      mainWindow.show()
      landingWindow = null
    })

    mainWindow.webContents.on('did-finish-load', () => {
      if (!mainWindow) {
        throw new Error('"mainWindow" is not defined');
      }
      mainWindow.show();
      mainWindow.focus();
    });

     // 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.removeAllListeners();
      mainWindow = null;
    })

    // and load the index.html of the app.
    mainWindow.loadURL(`file://${__dirname}/app.html`);
    
    const menuBuilder = new MenuBuilder(mainWindow);
    menuBuilder.buildMenu(locale);
  })

  landingWindow.loadURL(`file://${__dirname}/landing.html`)
  landingWindow.once('ready-to-show', () => {
    landingWindow.show()
  })
}

如何用 Electron 做 i18n

在做国际化版本的时候有两种情况

main.js 主进程的国际化
window 内 renderer 进程的国际化
在 main.js 可以通过

locale = app.getLocale();
来获取当前系统的语言,不过需要注意的是,获取的地方一定要在 app.on('ready') 的注册函数中获取,不然默认会一直取到 “en-US”

在 window 中, 这就真的是前端的天下了

locale = navigator.language
和在 chrome 中无差,可以如上获取语言,然后再通过设置到 react-intl 的 provider 中,接下来就是琐碎的翻译工作了。。。

最后

氢还是花了我不少时间的,目前没打算 open source, 积累到了一定程度应该会开源,最后说一说做这个东西的收获:

  • 设计和 UX 是很重要的

  • 克制,在做产品上,在设计上一定要克制

  • linux 哲学,让产品只完成一个功能,不要想太多,想太多了什么都做不成

  • 哪怕还是个浑身 bug 的东西,也要尽快的推出来,不然你都找不到理由继续做下去


陈学家_6174
3.8k 声望1.1k 粉丝

[链接]