本文将会讲述一个完整的跨端桌面应用 代码画板 的构建,会涉及到整个软件开发流程,从开始的设计、编码、到最后产品成型、包装等。php
本文不只仅是一篇技术方面的专业文章,更会有不少产品方面的设计思想和将技术转换成生产力的思考,我将结合我本身的使用场景彻底的讲解整个开发流程,固然涉及到设计方面的不必定具备广泛实用性,多数状况下都是我本身的一些喜爱,我只关心本身的需求。css
同时本文只从总体上讲思路,也会有个别的技术细节和常规套路,有兴趣的也能够直接去 github 上看 源码,文章会比较长,若是你只想知道一些拿来即用的「干货」,或许这篇文章并非一个好的选择html
事情的原由是这样的,由于咱们内部会有一些培训会议。会常常现场演示一些代码片断。好比说咱们讲到 React 的时候会现场写一些组件,让你们能直观的感觉到 React 的一些功能。前端
可是一般因为条件所所限,会议总会遇到一些意外。好比断网、投影分辨率低看不清文字等node
起初咱们用的是在线版的 codepen,可是感受并非那么好用。好比不能方便的修改字体大小,必需要在连网的状况下才能使用。另外它的 UI 设计不是很紧凑,一般咱们展现代码的时候都投影是寸土寸金的,应该有一个简洁又不失功能的 UI 界面,能全屏展现…react
因而我解决本身实现一个这样的轮子,那么大概的需求目标是有了:webpack
代码画板解决的是 临时性 的一些 演示代码 的需求,因此它的本质属性是一个拿来即用的工具,它不该该有更复杂的功能,好比用户登陆、代码片断的管理等。这些需求不是它要解决的。代码画板会提供一个简单的导出成 HTML 文件的功能,能够方便用户存储整个 HTML 文件。git
既然是用来演示代码的,那么它的界面上应该只有两个东西,一个是 代码,一个就是 预览。像代码/控制台切换的功能都作成 tab 的形式,正常状况不须要让他们展现出来。像 codepen 那样把全部的代码编辑器功能都展现出来我认为是不对的。程序员
codepen 的界面给人感受很是复杂,有不少功能点。固然我并非在批评它,codepen 作为一个须要商业化运营的软件,势必会作的很是复杂,这样才能知足更多用户的需求。然而程序员写软件则能够彻底按照本身的想法来,哪怕这个应用只给本身一我的用呢。es6
桌面应用的设计和 web 界面的设计仍是有些细微区别的,一样的基于 electron 的应用,有的应用会让人感受很「原生」,有的则一眼就能看出来是用 CSS 画的。我在设计代码画板的时候也尽可能向原生靠近,避免产生落差感。好比禁用鼠标手型图标、在按钮或者非可选元素上禁止用户选择:
cursor: default;
user-select: none
复制代码
由于实际上用户在使用一款应用的时候感性的因素影响占很大一部分,好比说有人不喜欢 electron 可能就是由于看到过 electron 里面嵌一个完整的 web 页面的操做,这就让人很反感。可是这不是 electron 的问题,而是应用设计者的问题。
说实话应用 logo 设计我也是业余水平,可是聊胜于无。既然水平不行,那就尽可能设计的不难看就好了。能够参考一些好的设计。我用 sketch 画出 logo 的外形,sketch 有不少 macOS 的模块能够从网上下载下来,直接基于模板修改就能够了。
代码画板主要的界面是分割开的两个面板,左边是代码,右边是预览。因此我就大概画了一个形状
这个 logo 有个问题就是线条过多,小尺寸的时候看不清楚。这个问题我暂时先忽略了,毕竟我还不是专业的,后续有好的创意能够再改
代码画板也 不会有 设置界面,由于经常使用的设置都预约义好了,你不须要配置。顶多改变下代码字体的大小。使用编辑器的通用快捷键 command
++/-
就解决了,或者插入三方库,直接使用编辑器的通用命令快捷键 command
+p
调出。咱们的思路就是把复杂的东西帮用户隐藏在后台,观众只须要关注演员台上的一分钟,而没必要了解其它细节。
因为代码画板的界面很是简单,在一些细小的必要功能就得添加一些快捷键。好比:切换 HTML/CSS/JS/Console 代码编辑器,我在每一个 tab 上加了数字标号,暗示它是有顺序有快捷键的,并且这个切换方式和 Chrome tab 切换的逻辑一致,使用 command
+数字
就能够实现,万一仍是有人不会用的话,能够去看帮助文档。里面有全部的快捷键。
界面中间的分割条能够自定义拖动,双击重置平分界面
刚开始的时候我把每一个 tab 页签都分割成单独的面板,由于我以为这个能拖动自定义面板大小的交互实在是太爽了,忍不住想去拖动它。可是后来想一想,其实并无必要,咱们写代码时应该更专一于代码自己,若是只有两个面板,那么这个界面不管是认知仍是使用起来就没有任何困难。
由于咱们并不须要把一堆的功能的界面摔给用户,让他们本身去选择。
经过使用流行的几款在线代码运行工具,我发现他们有一个共同的问题:控制台很难用。没法像 Chrome Console 那样展现任意类型的 JS 值。好比我想 log 一段嵌套的 JS 对象:
console.log({ a: { b: 1, c: { d: [1, 2, 3] } }})
复制代码
大多数都展现成这样的:
[object Object] {
a: [object Object] {
b: 1
}
}
复制代码
Chrome 是这样的:
显然 Chrome 控制台中更直观。因此咱们须要在前面的基础上加一个需求,即:实现一个基于 DOM 的日志展现界面(无限级联选择)
日志界面应该有下面这些功能:
现代化的前端写页面确定不是 HTML/CSS/JS 一把梭了,至少应该有 Sass/Babel 的支持吧。
Sass 嵌套能让你少写不少选择器,固然 Less 也能够,可是在咱们的这个应用里面区别不大,通常来讲临时性的写一些代码不多会用到它们的细节功能。有 变量 和 选择器 嵌套就够了
Babel 主要是解决了写 React 的问题,不用再安装一大堆的构建工具了,直接使用 UMD
的 React/ReactDOM
就能够了,并且 electron 内嵌的 chromium 也支持了 es6 的 class 写法,实际上 Babel 主要的目的仍是用来转译 JSX
注意这里是有一个我认为是 刚性 的需求,好比临时突然有个想法,或者想验证一段代码的话,正常状况是使用你的编辑器,新建 demo.html/demo.css/demo.js 等这些操做。可是这些动做太浪费时间了。有了代码画板之后,直接打应用就能够开始 coding 了,真正能作到开箱即用。
咱们在写 demo 页面时一般是要引用不少第三方类库的,好比:Bootstrp/jQuery 等。我但愿有一种方法能够方便的引用到这些库,直接把库文件的 link/script 标签插入到代码画板的 HTML 中,可是前端框架真的是太多了,又不能一个个去扣来写死到页面,就算是写死了随着框架版本的升级,可能就没法知足咱们的需求。
之前写页面时常常会用到 bootcdn,无心中发现它提供了相关 API,能够直接拿来使用。接下来就得想办法让用户经过界面选择便可。
这个 API 有三层数据结构:库 - 版本 - 资源连接
。这个功能要用界面来实现确定会很是臃肿,界面上可能会放不少按钮。这就违背了「更简洁」的需求目标。
这时就得参考下咱们常用的一些软件是如何解决 简洁性 和 功能性 需求之间的矛盾问题的,我比较喜欢 Sublime Text 的一些界面设计,Command Palette 是我常用的,因此我决定再模拟一个 Command Palette 来实现插入第三方库的需求。并且重要的是这个 Command Palette 并不必定只用来实现这一个功能,或者后期会有一些别的功能须要添加,那这个 Command Palette 也是个很好的入口。
实现离线可用不少方法,好比使用 PWA 技术。可是 PWA 并不能给我带来一种原生应用的那种可靠感,相反 electron 恰好能够解决个人顾虑。同时它能够把你的应用打包成各个平台(macOS/Window/Linux)的原生应用。惟一的缺点就是安装包确实很大,通常来说一个 electron 应用 安装完 至少要 100 多兆,不过我以为还能接受,毕竟硬盘存储如今已经很廉价了。
有人可能对 electron 有抗拒,以为 electron 应用太庞大、占系统资源什么的,不过咱们作的这个应用并不须要常驻系统,临时性的使用一下,用完就关闭,正常写生产环境的代码确定仍是要换回 编辑器/IDE 的。同时由于 electron 下降了写桌面应用的门槛,确实有不少人把一个完整的在线的网页直接嵌进去,这也是有问题的。
electron 还有一个好处,由于它彻底基于 HTML/CSS/JS 来实现 UI(可使用 Chrome only 的一些新功能),那咱们理论上能够在作桌面应用时顺手把 web 应用也作了。这就能够同时支持各个系统下的原生应用,而且有 web 在线版本。若是你不肯意使用原生应用,直接登陆 web.code-sketch.com 使用在线版也没是一种选择。这样就使得咱们的应用具备真正的 跨端 能力。
因为咱们团队都使用了 macbook,因此我优先支持 macOS 的开发,另外 macOS Mojave 的系统级别的暗色主题我也比较喜欢,恰好实现支持 mojave 暗色主题这个需求也作上。
大方向肯定了,像框架选择这个就简单了,基于 electron 的应用,须要你区分开 render/main process 来选择。
渲染进程 就是 electron 中界面的实现部分 ,通常来讲就是一个 webview,选本身喜欢的框架便可。我使用 React 来实现界面。样式方面就再也不使用框架了,由于咱们的界面原则上没有复杂的元素,直接手写 CSS,300 行内基本上就能够解决问题。可能有人会以为这不可能,实际状况是当你写样式只跑在 Chrome 里面的时候那感受彻底爽到飞起,CSS variable/flex/grid/calc/vh/rem 什么的均可以拿来用,实现一个功能的成本就下降了不少。
我使用 Codemirror 来作为主界面的代码编辑器,Monaco 也是一个好选择,可是它有点过于庞大了,并且若是想要自定义功能得本身写不少实现
主界面上的分割组件,使用了 React-split。
主进程 就是 electron 应用程序的进程,主要的区别在于主进程中能够调用一些与原生操做系统交互的 API,好比对话框、系统风格主题等。而且有 node 的运行时,能够引用 NPM 包。固然渲染进程也能够有 node 支持,可是我建议渲染进程中就只放一些纯前端的逻辑,这样的话方便后期把应用分离成 web 版
由于咱们要集成 Sass 编译功能,若是你也经历过 node-sass 的各类问题,那就应该果断选择 dart-sass — 使用 dart 实现,编译成了原生的 JS,没有依赖问题。dart-sass 我放在了 main process 中,由于我试过放在 render process 中会有各类报错。若是 web 端要实现这个功能就须要其它的解决办法了,好比作成一个 http 服务,让 web 调 http 服务。
Babel 的话我是放在了 渲染进程 中以 script 标签的方式调用,这样即便在 web 端 Babel 编译也是可用的。
总之若是你使用 electron 构建应用而且引入的第三方 NPM 包能够 支持 运行在客户端(浏览器)上,那就尽可能把包放在渲染进程里面。
我使用 Parcel 来构建 React 而不是 Create React App。后者用来写个小应用还能够,稍微大一点的,须要定制化一些东西你就得 eject 出来一大堆 webpack 配置文件,即使是我已经用 webpack 开发过几个项目了,可是说实话我仍是没用会 webpack。写 webpack 配置的时间足够我本身写 npm script 来知足本身的需求了。
使用 electron-builder 来打包到平台原生应用,而且若是你有 Apple 开发者帐号的话应用还能够提交到 AppStore 上去。
我目前的打包参数是这么配置的:
{
"build": {
"productName": "Code Sketch",
"extends": null,
"directories": { "output": "release" },
"files": [
"icon.icns",
"main.js",
"src/*.js",
"全部须要的文件",
"package.json",
"node_modules/@babel",
"node_modules/sass"
],
"mac": {
"icon": "icon.icns",
"category": "public.app-category.productivity",
"target": [ "dmg" ]
}
}
}
复制代码
在你的 package.json 中添加 build 字段,productName, directories 这些按本身须要更改便可
代码画板项目开过过程当中涉及两个关键环境
import
这样的语句的,可是渲染进程因为你使用了 Parcel 编译,则无需考虑这里舒适提示下:想要作到 electron 中的 渲染进程与主进程之间共享 JS 代码是很是困难的。就算是有办法也会特别的别扭,个人建议是尽可能分离这两个进程中的代码,主进程主要作一些系统级别的 API 调用、事件分发等,业务逻辑尽可能放在渲染进程中去作
若是非要共享,那建议单独作成一个 NPM 包分别作为主进程运行时依赖,和渲染进程的 Parcel 编译依赖,惟一的缺点就是实际上共享的代码会有两份。
渲染进程中调用 node API 可能会和 Parcel 打包工具冲突,通常在调用好比文件模块时,能够加上 window.require(‘fs’)
这样就能够兼容两个环境:
get ipc() {
if (window.require) {
return window.require('electron').ipcRenderer
} else {
return { on() {}, send() {}, sendToHost() {} }
}
}
this.ipc.send('event', data)
复制代码
这样的话你在浏览器端调试也不会产生报错。通常状况下,建议当你用渲染进程中的 JS 引用(require
)包的时候都加上 window.
前缀就能够了。由于渲染进程中 window 是全局变量,调用 require
和调用 window.require
是等价的
一般在测试的时候应用会调用一些 electron 内置的系统级别 API,这部分调用一般须要启动 electron,可是有时候只有渲染进程中 UI 界面上的改动,就不用再启动 electron 了,直接在浏览器里面测试便可。使用 Parcel 运行一个本地的服务,这样就能够在浏览器里面调试页面。整个开发过程须要两个命令(NPM Script):
启动 Parcel 编译服务器
"scripts": {
"start": "./node_modules/.bin/parcel index.html -p 2044"
}
复制代码
调试 electron 原生功能,注意设置 ELECTRON_START_URL
"scripts": {
"dev": "ELECTRON_START_URL=http://localhost:2044 yarn electron",
}
复制代码
整个应用只有两个功能是须要咱们本身写代码实现的:日志控制台,Sublime 命令行。咱们分别来分析下这两个模块的难点。
日志控制台 的难点在于,咱们须要打印任意类型的 JS 值。若是你对 JS 了解比较多的话天然会想到在 JS 中全部的东西都是 对象,即 Object,那么实际上当你想打印一个变量的时候,其实你只要把整个 Object 递归的遍历出来,而后作成一个无限级的下拉菜单就能够了。看起来大概想下面这样:
Sublime 命令行 实际上开发起来仍是比较简单的,使用 React 很简单就实现了功能,比较麻烦的是调用 bootcdn 的接口,过程当中我发现接口返回数据量仍是挺大的,有必要作上一层 localStorage 缓存,加快二次打开速度。
然而在使用的过程当中你会发现当我想插入一个前端库须要不少操做,由于有 三级选择:库-版本-CDN 连接。虽然这个流程解决了 全部用户 的使用问题,可是却损害了 大部分 用户的体验。这个时候插入一个经常使用库的成本就很高了,因此咱们就要加上一些快捷入口,来实现一键插入流行框架。
咱们写代码的思路是知足全部用户的使用需求,可是一个好产品的思路是先知足大多数用户(80%)的常规需求,再让其他的用户(20%)能够有选择
还有一个问题比较典型就是 React 这类框架在渲染大列表而且进行过滤(关键字查询)时性能的问题。注意这个性能问题 并非 引入框架产生的,真正的缘由是当你渲染的 HTML 节点数以千计的时候,批量操做 DOM 会使得 DOM Render 特别慢。
因此说当咱们遇到性能问题的时候应该去查找问题的根源,而不是停留在框架使用上,实际上在 DOM 操做这个层面来说 jQuery 提供了更多的性能优化,好比自身的缓存系统,以至于当你在使用的时候很难发现有性能问题。可是在类 React 框架中它们框架自己的重点并不在于解决你应用的性能问题。
相似咱们上面讲到的,实际上 jQuery 帮助你屏蔽了不少舞台背后的东西,以至于你能够不用操心技术细节,你甚至能够把 jQuery 当作一个 产品 来使用,而类 React 框架你却要亲力亲为的用他来设计你的代码。
话题再转回性能问题。这时候须要咱们去实现一个相似于 react-window 的功能,让列表元素根据滚动按需加载。这多是一种通用的解决大列表加载的方案,可是个人解决方法更粗暴,由于咱们的下拉过滤功能使用时用户只关注 最佳的匹配项 便可,后面匹配程度不高的项能够直接限制数量裁剪就好了嘛。不多有用户会一直滚动到下面去查找某个选项,若是有,那就说明咱们这个匹配作的有问题。
slice() {
const idx = (this.props.itemsPerPage || 50) * (this.state.activeFrame + 1)
return this.props.items.slice(0, idx)
}
复制代码
整个匹配筛选的状态大概是这样的:
this.state = {
// 当前第N步选择
step: 0,
// 当前步骤数据
items: [],
// 是否显示
active: false,
// 当前选中项
current: {},
// 过滤关键字
keyword: ''
}
复制代码
这个 items
是当前步骤的全部数据,实际上咱们这个组件是支持无限级的扩展的,那么咱们经过组件的 props 传入全部层级的数据,而后持久存储在内存中。这个 全部层级的数据 是数据结构层面的,实际上它多是经过异步接口获取的。
再来看看咱们组件提供的全部 props
:
static defaultProps = {
step: 0,
active: false,
data: [[]], // 无限层级数据 [[], [], [], ...]
// 数据的主键,用于钩子函数返回用户选择的结果集
pk: 'id',
autoFocus: true,
activeCls: 'active',
delay: 300,
defaultSelected: 0,
placeholder: '',
async: false,
alias: [],
done: () => {}
}
复制代码
这些数据均可以经过组件的 props 传入,这就意味着咱们的这个组件才是真正的组件,别人也可使用这样的功能,而他们并不用在乎里面的细节,使用者只须要作好相似调用本身接口的这种业务逻辑。
组件的调用大概是这样的:
<CommandPalette step={0}
key="CommandPalette"
async={injectData}
done={this.done.bind(this)}
alias={alias}
aliasClick={this.aliasClick.bind(this)}
data={[ [], [], [] ]}
复制代码
async
这个 props 其实是一个异步调用的钩子方法,它会回传给你组件上当前操做的相关数据状态,经过这些数据使用者就能够按本身的需求在不一样的步骤上调用不一样的方法
export const injectData = (step, item, results, cb) => {
const API = 'https://api.bootcdn.cn/libraries'
if (step === 0) {
fetchData(`${API}.min.json`)
.then(processLibraryData)
.then(cb)
} else if (step === 1) {
// ...
} else if (step === 2) {
// ...
}
}
复制代码
另外关于 React 这里安利下本身翻译过的一个教程:React 模式,里面讲到 18 种短小精悍的 React 模式案例,很是简单易懂。
还有一个小窍门,咱们在适配暗色主题时,传统的方法是直接写两套主题 CSS 代码,实际上咱们要使用 CSS Variable 的话彻底不必生成两套了,背景色,字体都作成 CSS 变量,切换的时候只须要动态往页面插入更新过的 CSS 变量值便可
系统的一些参数想直接传给渲染进程也是比较麻烦的,个人作法是直接从主进程中的 loadUrl 方法上以 queryString 的方式传到渲染页面的 URL 上
const query = {
theme: osTheme,
app_path: app.getAppPath(),
home_dir: app.getPath('home')
}
mainWindow.loadURL(process.env.ELECTRON_START_URL ? url.format({
slashes: true,
protocol: 'http:',
hostname: 'localhost',
port: 2044,
query
}) : url.format({
slashes: true,
protocol: 'file:',
pathname: path.resolve(app.getAppPath(), './dist/index.html'),
query
}))
复制代码
像程序运行时的一些参数(好比程序的根目录)也能够这么动态传过去,并且还有一个好处就是你甚至能够在渲染进程中测试与这些参数相关的功能。
我会把最终全部功能的使用方法录制成一个视频,万一有人不不想下载你的软件,只是要了解一下,这就是个很好的方法。我同时上传到了 Youtube 和 bilibili 这两个平台,其它的都有广告就不必了
使用 Quicktime Player 便可,录制完使用 iMovie 转码成两倍速率的 mp4。若是你有兴趣还能够加上一段音乐什么的,让视频看起来更灵动
域名是一个能让用户记住你产品的方法,若是你作的是一个成型的产品,那就必定要申请个域名。
我老是有这样的体验,有的时候看到一个很是不错的产品但因为当时没需求就忽略了,想起来或者忽然有需求的时候缺记不起来名字叫什么了。
事实上代码画板最开始我给他起的名字是 code playground,这个更直观,可是名字太长,并且想用到的一些域名呀、Github 名、NPM 包都被注册了。
想来想去就换成了 code sketch,这和符合咱们的设计初衷,即:一边是代码,一边是效果/草图
域名申请我通常会上 Godaddy,不用备案,.com 域名一年 ¥65.00,而后 DNS 服务器转到了 cloudflare,后续域名也会直接转到 cloudflare。由于听说之后在 cloudflare 上续费域名最便宜
宣传网站直接放在 github pages 上,作个自定义域便可,实在是太方便了。并且还有 SSL 支持,Github 真的是业界良心
web 版的代码画板,因为咱们把渲染进程中的代码分离开发,因此直接把 parcel 打包出来的静态文件也作成 github pages 就能够了,爽歪歪,网站就等于一分钱不花了。后续作一些 web 版的加强功能时,能够作成先后端分离的 http 服务,这就是后话了
GA 可让你了解网站的用户分布状况,清楚的知道网站访问的波动。好比说你把本身的连接放到某个网站上分享了,GA 里面就能看出来全部的推荐来源和波动,对于运营来讲是很是有必要的
这个我还真想了好长时间,基于我对于代码画板的定义,我以为它应该是一个咱们有一个想法的时候须要快速去实现一个 demo 的地方,想来想去就定了一段看起来文邹邹的话,虽然听名字根本不知道它是干啥用的,可是不要紧,程序员写东西就是要有个性,由于个人受众只有本身。
First place where the code was written... 一个你最初写代码的地方...
麻雀虽小,五脏俱全。咱们来看下代码画板总共用到了多少东西:
实事上我本身的开发这个应用的时候并无严格按照这篇文章的顺序执行,而是想到一些实现一些,可能一个功能实现了后来以为很差又干掉了,是不断的取舍、提炼的结果。
开发中我也不断的问本身这个功能是否有必要,若是无关紧要那是否是能够去掉,这样才能使得用户更加关注于代码自己。
整个开发过程当中本身实现的功能模块并很少,只有控制台、命令行窗口是本身实现的,其它的功能基本上都是靠社区现有的工具库来完成的,从这一点来讲前端技术的生态仍是挺好的。这使得当我从总体上构思一个产品时我没必要在乎那些细节,虽然过程当中仍是能感受到前端工具/库的割裂感,可是总体而言仍是向好的,毕竟工具对于开发者只是一种选择的。
8、引用