本文撰写于 Vite-0.9.1 版本。
借用作者的原话:
Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打包。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。
注意到两个点:
因此,要实现上述目标,需要要求项目里只使用原生 ES imports,如果使用了 require 将失效,所以要用它完全替代掉 Webpack 就目前来说还是不太现实的。上面也说了,生产模式下的打包不是 Vite 自身提供的,因此生产模式下如果你想要用 Webpack 打包也依然是可以的。从这个角度来说,Vite 可能更像是替代了 webpack-dev-server 的一个东西。
Vite 的实现离不开现代浏览器原生支持的 模块功能。如下:
1 | <script type="module"> |
当声明一个 script
标签类型为 module
时,浏览器将对其内部的 import
引用发起 HTTP
请求获取模块内容。比如上述,浏览器将发起一个对 HOST/a.js
的 HTTP 请求,获取到内容之后再执行。
Vite 劫持了这些请求,并在后端进行相应的处理(比如将 Vue 文件拆分成 template
、style
、script
三个部分),然后再返回给浏览器。
由于浏览器只会对用到的模块发起 HTTP 请求,所以 Vite 没必要对项目里所有的文件先打包后返回,而是只编译浏览器发起 HTTP 请求的模块即可。这里是不是有点按需加载的味道?
看到这里,可能有些朋友不免有些疑问,编译和打包有什么区别?为什么 Vite 号称「热更新的速度不会随着模块增多而变慢」?
简单举个例子,有三个文件 a.js
、b.js
、c.js
1 | // a.js |
1 | // c.js |
如果以 c 文件为入口,那么打包就会变成如下(结果进行了简化处理):(假定打包文件名为 bundle.js
)
1 | // bundle.js |
值得注意的是,打包也需要有编译的步骤。
Webpack 的热更新原理简单来说就是,一旦发生某个依赖(比如上面的 a.js
)改变,就将这个依赖所处的 module
的更新,并将新的 module
发送给浏览器重新执行。由于我们只打了一个 bundle.js
,所以热更新的话也会重新打这个 bundle.js
。试想如果依赖越来越多,就算只修改一个文件,理论上热更新的速度也会越来越慢。
而如果是像 Vite 这种只编译不打包会是什么情况呢?
只是编译的话,最终产出的依然是 a.js
、b.js
、c.js
三个文件,只有编译耗时。由于入口是 c.js
,浏览器解析到 import { a } from './a'
时,会发起 HTTP 请求 a.js
(b 同理),就算不用打包,也可以加载到所需要的代码,因此省去了合并代码的时间。
在热更新的时候,如果 a
发生了改变,只需要更新 a
以及用到 a
的 c
。由于 b
没有发生改变,所以 Vite 无需重新编译 b
,可以从缓存中直接拿编译的结果。这样一来,修改一个文件 a
,只会重新编译这个文件 a
以及浏览器当前用到这个文件 a
的文件,而其余文件都无需重新编译。所以理论上热更新的速度不会随着文件增加而变慢。
当然这样做有没有不好的地方?有,初始化的时候如果浏览器请求的模块过多,也会带来初始化的性能问题。不过如果你能遇到初始化过慢的这个问题,相信热更新的速度会弥补很多。当然我相信以后尤大也会解决这个问题。
上面说了这么多的铺垫,可能还不够直观,我们可以先跑一个 Vite 项目来实际看看。
按照官网的说明,可以输入如下命令(<project-name>
为自己想要的目录名即可)
1 | $ npx create-vite-app <project-name> |
如果一切都正常你将在 localhost:3000
(Vite 的服务器起的端口) 看到这个界面:
并得到如下的代码结构:
1 | . |
接下来开始说一下 Vite 实现的核心——拦截浏览器对模块的请求并返回处理后的结果。
我们知道,由于是在 localhost:3000
打开的网页,所以浏览器发起的第一个请求自然是请求 localhost:3000/
,这个请求发送到 Vite 后端之后经过静态资源服务器的处理,会进而请求到 /index.html
,此时 Vite 就开始对这个请求做拦截和处理了。
首先,index.html
里的源码是这样的:
1 | <div id="app"></div> |
但是在浏览器里它是这样的:
注意到什么不同了吗?是的, import { createApp } from 'vue'
换成了 import { createApp } from '/@modules/vue
。
这里就不得不说浏览器对 import
的模块发起请求时的一些局限了,平时我们写代码,如果不是引用相对路径的模块,而是引用 node_modules
的模块,都是直接 import xxx from 'xxx'
,由 Webpack 等工具来帮我们找这个模块的具体路径。但是浏览器不知道你项目里有 node_modules
,它只能通过相对路径去寻找模块。
因此 Vite 在拦截的请求里,对直接引用 node_modules
的模块都做了路径的替换,换成了 /@modules/
并返回回去。而后浏览器收到后,会发起对 /@modules/xxx
的请求,然后被 Vite 再次拦截,并由 Vite 内部去访问真正的模块,并将得到的内容再次做同样的处理后,返回给浏览器。
上面说的这步替换来自 src/node/serverPluginModuleRewrite.ts
:
1 | // 只取关键代码: |
如果并没有在 script
标签内部直接写 import
,而是用 src
的形式引用的话如下:
1 | <script type="module" src="/main.js"></script> |
那么就会在浏览器发起对 main.js
请求的时候进行处理:
1 | // 只取关键代码: |
替换逻辑 rewriteImports
就不展开了,用的是 es-module-lexer
来进行的语法分析获取 imports
数组,然后再做的替换。
如果 import
的是 .vue
文件,将会做更进一步的替换:
原本的 App.vue
文件长这样:
1 | <template> |
替换后长这样:
1 | // localhost:3000/App.vue |
这样就把原本一个 .vue
的文件拆成了三个请求(分别对应 script
、style
和template
) ,浏览器会先收到包含 script
逻辑的 App.vue
的响应,然后解析到 template
和 style
的路径后,会再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。
如下:
不得不说这个思路是非常巧妙的。
这一步的拆分来自 src/node/serverPluginVue.ts
,核心逻辑是根据 URL 的 query 参数来做不同的处理(简化分析如下):
1 | // 如果没有 query 的 type,比如直接请求的 /App.vue |
上面只涉及到了替换的逻辑,解析的逻辑来自 src/node/serverPluginModuleResolve.ts
。这一步就相对简单了,核心逻辑就是去 node_modules
里找有没有对应的模块,有的话就返回,没有的话就报 404:(省略了很多逻辑,比如对 web_modules
的处理、缓存的处理等)
1 | // ... |
上面已经说完了 Vite 是如何运行一个 Web 应用的,包括如何拦截请求、替换内容、返回处理后的结果。接下来说一下 Vite 热更新的实现,同样实现的非常巧妙。
我们知道,如果要实现热更新,那么就需要浏览器和服务器建立某种通信机制,这样浏览器才能收到通知进行热更新。Vite 的是通过 WebSocket
来实现的热更新通信。
客户端的代码在 src/client/client.ts
,主要是创建 WebSocket
客户端,监听来自服务端的 HMR 消息推送。
Vite 的 WS 客户端目前监听这几种消息:
connected
: WebSocket 连接成功vue-reload
: Vue 组件重新加载(当你修改了 script 里的内容时)vue-rerender
: Vue 组件重新渲染(当你修改了 template 里的内容时)style-update
: 样式更新style-remove
: 样式移除js-update
: js 文件更新full-reload
: fallback 机制,网页重刷新其中针对 Vue 组件本身的一些更新,都可以直接调用 HMRRuntime
提供的方法,非常方便。其余的更新逻辑,基本上都是利用了 timestamp
刷新缓存重新执行的方法来达到更新的目的。
核心逻辑如下,我感觉非常清晰明了:
1 | import { HMRRuntime } from 'vue' // 来自 Vue3.0 的 HMRRuntime |
服务端的实现位于 src/node/serverPluginHmr.ts
。核心是监听项目文件的变更,然后根据不同文件类型(目前只有 vue
和 js
)来做不同的处理:
1 | watcher.on('change', async (file) => { |
对于 Vue
文件的热更新而言,主要是重新编译 Vue
文件,检测 template
、script
、style
的改动,如果有改动就通过 WS 服务端发起对应的热更新请求。
简单的源码分析如下:
1 | async function handleVueReload( |
对于热更新 js
文件而言,会递归地查找引用这个文件的 importer
。比如是某个 Vue
文件所引用了这个 js
,就会被查找出来。假如最终发现找不到引用者,则会返回 hasDeadEnd: true
。
1 | const vueImporters = new Set<string>() // 查找并存放需要热更新的 Vue 文件 |
如果 hasDeadEnd
为 true
,则直接发送 full-reload
。如果 vueImporters
或 jsHotImporters
里查找到需要热更新的文件,则发起热更新通知:
1 | if (hasDeadEnd) { |
写到这里,还有一个问题是,我们在自己的代码里并没有引入 HRM
的 client
代码,Vite 是如何把 client
代码注入的呢?
回到上面的一张图,Vite 重写 App.vue
文件的内容并返回时:
注意这张图里的代码区第一句话 import { updateStyle } from '/@hmr'
,并且在左侧请求列表中也有一个对 @hmr
文件的请求。这个请求是啥呢?
可以发现,这个请求就是上面说的客户端逻辑的 client.ts
的内容。
在 src/node/serverPluginHmr.ts
里,有针对 @hmr
文件的解析处理:
1 | export const hmrClientFilePath = path.resolve(__dirname, './client.js') |
至此,热更新的整体流程已经解析完毕。
这个项目最近在以惊人的速度迭代着,因此没过多久以后再回头看这篇文章,可能代码、实现已经过时。不过 Vite 的整体思路是非常棒的,在早期源码不多的情况下,能学到更贴近作者原始想法的东西,也算是很不错的收获。希望本文能给你学习 Vite 一些参考,有错误也欢迎大家指出。
]]>更新 Typora 的最新版,可以在设置-图像处找到自定义图片上传服务的设置区域:
Typora 官方关于图像自定义上传相关的配置、介绍的页面 在这里。
如上图,你可以选择自己喜欢用的图片上传工具,可选的工具如下图:
同时 Typora 提供了上传测试功能,如下图你可以找到 Test Uploader
按钮来测试你的上传功能是否正常:
Typora 会上传的图片就是它家的 Logo 了,如下:
当测试成功之后,还别忘了开启图片自动上传功能:
注意选则第三项,即允许通过读取 YAML 配置来决定是否自动上传图片。经过测试,在 macOS 上必须开启这个选项,同时在文章的顶部写下如下的 YAML 配置:
1 |
|
这样才可以开启自动上传图片的功能。应该是 Typora 的一个 bug,后续版本不知道会不会修复。
说了这么多,Typora 里引入图片即上传的效果是怎么样的呢?我录制了一个 gif:
可以说整体效果还是比较流畅的。
同时如果你未开启自动上传图片的功能,把图片拖入 Typora 或者粘贴到 Typora,右键图片看到一个上传图片的选项:
这样也能根据你配置的上传服务来上传图片。
Typora 支持了两种 PicGo 的上传模式,作为 PicGo 的开发者,我觉得有有必要跟朋友们说说区别。Typora 支持的两种 PicGo 上传模式分别是:PicGo-Core(命令行)以及 PicGo.app(图形界面)
PicGo.app 就是用户平时经常使用的图形化界面的 PicGo。而 Typora 对接的上传服务来自于 PicGo v2.2.0+提供的 PicGo-Server 的功能,它是一个小型的 HTTP 服务器,会默认开启 36677 端口来监听上传的请求。而 Typora 则会往 36677 端口发送请求来上传图片。所以如果你的 PicGo 版本过低或者 PicGo-Server 功能没有开启,或者端口不是 36677,都无法通过 Typora 的这个功能上传图片。
这个是 PicGo 底层依赖的 核心库,是 PicGo 上传图片、插件机制的核心。它是一个 npm 包,意味着你可以通过 npm 全局安装来实现上传。同时 Typora 也提供了预编译的二进制文件,它是把 PicGo-Core 所有依赖都打包成了一个可执行的文件。
Typora 对这两种 PicGo-Core 的用法都支持,官方的文档对此有详细的 配置说明。不过需要注意的是 macOS 由于系统的原因,不支持预编译的二进制文件那个使用方法,而只能使用 npm 全局安装的方式,再通过 custom command
自定义命令的方式来使用 PicGo-Core:
官方 文档 里对二者的区别有做出描述,我觉得写得挺到位的。不过还是跟大家聊聊这二者的区别:
跪求 T T 有兴趣的小伙伴一起来翻译,如果对 PicGo 的国际化有意向的小伙伴,可以加入官方 gitter 频道一起来聊。
就我自己的使用来说,我是更喜欢直接用 PicGo 来上传的,因为配置什么的不用再调了,可视化界面也更容易操作~
前不久 PicGo2.0 发布的时候,PicGo-Core 还收到了来自 Typora 官方的 PR。我以为需要好几个月的时间才能支持自定义图床,没想到支持来得这么快。我觉得对于一个 Markdown 编辑器而言,图片的管理、上传一定是一种刚需。而此次开放了自定义上传的功能,想必也是戳中了很多 Typora 用户的痛点。另外这次 PicGo 能够作为官方指定的上传工具,我觉得非常开心,同时它也是 Typora 三个平台都支持的上传工具(uPic 和 iPic 都很棒,不过只支持 macOS),希望有了这个功能以后能够给你们带来更好码字体验~
]]>reduce实现map
、用xxx实现yyy
的题目其实都挺有意思,考察融会贯通的本领。不过相比之下这道题可能更有实际意义。比如我们经常会用 setTimeout
来实现倒计时。下面来说说我对这个问题的思考。首先我们先用 setTimeout
实现一个简单版本的 setInterval
。
setInterval
需要不停循环调用,这让我们想到了递归调用自身:
1 | const mySetInterval = (cb, time) => { |
让我们来写段代码测试一下:
1 | mySetInterval(() => { |
嗯,没啥问题,实现了我们想要的功能。。。等一下,怎么停下来?总不能执行了就不管了吧。。。
平时如果用到了 setInterval
的同学应该都知道 clearInterval
的存在(不然你怎么停下 interval
呢)。
clearInterval
的用法是 clearInterval(id)
。而这个 id
是 setInterval
的返回值,通过这个 id
值就能够清除指定的定时器。
1 | const id = setInterval(() => { |
不过你有没有想到 clearInterval
是如何实现的?回答这个问题之前,我们需要先实现 mySetInterval
的返回值。
回到我们简单版本的 mySetInterval
:
1 | const mySetInterval = (cb, time) => { |
现在它的返回值因为没有显示指定,所以是 undefined
。因此第一步,我们先要返回一个 id
出去。
那么直接 return setTimeout(fn, time)
可以吗?因为我们知道 setTimeout
也会返回一个id,那么初步构想就是通过 setTimeout
返回的 id
,然后调用 clearTimeout(id)
来实现我们的 myClearInterval
。
如下:
1 | const mySetInterval = (cb, time) => { |
这显然是不行的。因为 mySetInterval
返回的 id
是第一个 setTimeout
的 id
,然而2秒后,要 clearTimeout
时,递归执行的第二个、第三个 setTimeout
等等的 id
已经不再是第一个 id
了。因此此时无法清除。
所以我们需要每次执行 setTimeout
的时候把新的 id
存下来。怎么存?我们应该会想到用闭包:
1 | const mySetInterval = (cb, time) => { |
很不错,到这步我们已经能够将 timeId
进行更新了。不过还有问题,那就是执行 mySetInterval
的时候返回的 id
依然不是最新的 timeId
。因为 timeId
只在 fn
内部被更新了,在外部并不知道它的更新。那有什么办法让 timeId
的更新也让外部知道呢?
有的,答案就是用全局变量。
1 | let timeId // 全局变量 |
但是这样有个问题,由于 timeId
是Number
类型,当我们这样使用的时候:
1 | const id = mySetInterval(() => { // 此处id是Number类型,是值的拷贝而不是引用 |
由于 id
是 Number
类型,我们拿到的是全局变量 timeId
的值拷贝而不是引用,所以上面那段代码依然无效。不过我们已经可以通过全局变量 timeId
来清除计时器了:
1 | setTimeout(() => { // 2秒后清除定时器 |
但是上面的实现,不仅与我们平时使用的 clearInterval
的用法有所出入,并且由于 timeId
是一个 Number
类型的变量,导致同一时刻全局只能有一个 mySetInterval
的 id
存在,也即无法做到清除多个 mySetInterval
的计时器。
所以我们需要一种类型,既能支持多个 timeId
存在,又能实现 mySetInterval
返回的 id
能够被我们的 myClearInterval
使用。你应该能想到,我们要用一个全局的 Object
来做。
修改代码如下:
1 | let timeMap = {} |
我们的 mySetInterval
依然返回了一个 id
值。只不过这个 id
值是全局变量 timeMap
里的一个键的内容。
我们每次更新 setTimeout
的 id
并不是去更新 timeId
,相应的,我们去更新 timeMap[timeId]
里的值。
这样实现后,我们调用 mySetInterval
虽然获取到的 timeId
是不变的,但是我们通过 timeMap[timeId]
获取到的真正的 setTimeout
的 id
值是会一直更新的。
另外为了保证 timeId
的唯一性,在这里我简单用了一个自增的全局变量 id
来保证唯一。
好了,id
值有了,剩下的就是 myClearInterval
的实现了。
由于我们的 mySetInterval
返回的 timeId
并不是真正的 setTimeout
返回的 id
,所以并不能简单地通过 clearTimeout(timeId)
来清除计时器。
不过其实原理也是很类似的,我们只要能拿到真正的 id
就行了:
1 | const myClearInterval = (id) => { |
测试一下:
没毛病~
至此我们就用 setTimeout
和 clearTimeout
简单实现了 setInterval
与clearInterval
。当然本文说的是简单实现,毕竟还有一些东西没有完成,比如setTimeout
的 args
参数、Node和浏览器端的 setTimeout
差异等等。也只是一个抛砖引玉,重点在一步步如何实现。感谢阅读~
我自己是北邮研二的学生,「主修」前端。我自己的面试经历不多,从1月份到现在总共只面了3家:头条,腾讯·微信和蚂蚁金服·支付宝,很幸运都拿到了offer。其实我觉得主要还是内推对我的帮助特别大,没有内推的话我估计也很难拿offer了。所以经验第一条:能找内推尽量通过内推来获取面试资格。帮你内推的学长学姐一般会帮你查看(甚至修改)简历,有的可以直接部门直推给leader,等于省去了HR筛简历的步骤,所以能找到内推就尽量走内推而不是单纯走网申吧。
1月份的时候有个头条的学长通过邮件联系到我,对我的做的PicGo很感兴趣。跟我要了非常简陋的简历,就把我内推了。
不过后来面试邮件发来后我才知道给我的推的职位是iOS研发工程师
。他们组是移动端的组,要招前端,但是可能没有前端名额,就用iOS
的职位给我内推了。然后我也就稀里糊涂的去面试了。说实话毕竟是第一次面试,并且当时周边的同学也都没有开始找实习,在仅有的几天时间里我准备的特别不充分。
头条总共面了我三面,都是视频面。其中一面二面是连着的(一面一结束,马上二面面试官来面我)。由于这个组的性质比较特殊,来面我的面试官都不是写前端的,因此问的网络、计算机相关的问题会更多点。我事后(3月份面完微信和蚂蚁之后)才觉得当时1月份面头条的时候简直回答得一塌糊涂。
不过感觉自己做的很正确的一件事就是面试完马上把问题记下来了。从中也看出三家公司的侧重点不同。
头条一面是个年轻的小哥,是做移动端的。先问了我的项目,因为都是前端的他也没太了解,就开始问问题了:
一面算法题虽然思路说对了,但是没写出来的时候我觉得自己已经凉了。结果居然面试官说「你等一下,我去叫二面面试官」。
和一面就隔了3分钟。二面是交叉面,是另外一个部门的面试官来面的。这个面试官年龄一看就比一面面试官大。简单自我介绍之后,他就开始问我问题了:
算法题2现在看来真的超级简单。当时我真的没刷过题,平时对算法训练也很少,所以说的思路能过但是不是最优解。面试官说「行吧」(当时觉得凉了哈哈)。
三面和二面隔了大概几天吧。其实面完二面觉得还是很悬,结果还是收到HR的三面约时间电话。三面面试官是部门leader了。这个面试相对来说最轻松,基本没有问什么复杂的问题:
由于卡着1月底快过年了,所以HR那边在年前给了我口头offer,年后回来就给我发了正式的offer。
作为人生中第一份offer,还是挺激动的。不过不是 前端开发
的职位让我心里一直有点不舒服。我想去的其实是专业的前端团队,以及之后入职后做的东西也不是自己特别喜欢的,所以我在想着年后回学校再找找有没有自己更喜欢的实习岗位。当然头条这个岗位也很棒了!
经验总结二:算法、数据结构和计算机、网络基础知识很重要,哪怕是前端研发工程师,也是一名工程师。 所以我寒假回去后就开始针对自己薄弱的算法和数据结构部分开始了恶补。
腾讯今年春招(暑期实习)开始的时间特别早,从2月底就开始能网申、内推了。尤其3月份一整个月是提前批,并且4月1号之前没走完流程的同学,都要必须参加4月份的笔试。所以理论上是越早内推越好,越到后面简历越多而且万一4月前流程没走完,就得参加笔试了。
本来我想着先面几家小一点的公司攒攒经验再去投腾讯和蚂蚁金服的,毕竟这两家门槛还是相当高的。原本打算投北京微信的前端岗,但是问了上一届的一个学长说北京的微信不招前端,于是我的重心就放在蚂蚁金服的北京实习了。不过有件事情的发生打破了我原本的规划。
我有个在微信工作的学长,听说了我的情况之后帮我从微信HR那边问到北京微信今年招前端的情况,但是HC很少。我一听,呀,好机会。赶紧修改了简历发给了学长。内推后没两天我就收到了北京微信的现场面试邀请,心里还是很忐忑的,毕竟那可是微信啊。而且也是我的第一次现场面试。
到现场后,有个年龄跟我相仿的学长找到了我,说「我是你的一面面试官」。微信的现场面试没有我想象中那么拘谨(两个人一间小屋子那种),是在开阔的大厅里,有很多小圆桌,光线也很好。总之面试体验还是很好的。同时我还看到了很多其他来面试的人。
一面面试官说他也是北邮毕业的,一下子就感觉放松了不少。接下去就基本是他拿着我的简历开始问问题了。
一面的问题基本都答上来了,面试官也觉得很满意,就让我等会,叫来了二面面试官,跟我说是专门搞算法的。(心里一凉)
面试官跟我说他是北师毕业的,跟我的学校(北邮)很近(哈哈)。然后说,「我们来到简单的算法题吧,不需要你写,只需要你说说思路」
算法题又是没做出来(虽然说了最蠢的解法)心里又是一凉,感觉gg。结果面试官说「小伙子思维还挺灵活」(有么!)然后让我等会,叫来了三面面试官。
三面是个女leader,她对我说「前面的面试官对你的评价很高啊」。于是开始问我的个人经历和项目相关。最后问了我什么时候能来?我一听奇怪,我不是投的暑期实习么?然后她说最近有个项目急着要上线,所以缺人,就额外要了一个前端的HC。我说我实验室暑假前并不放人…所以需要再考虑一下。并且这个时候我听闻他们组实际是做AI的,而前端如果我去了也只有两个人。到这时我感觉有点不对劲,不过leader说之后还有一个广州的电面要我准备一下。
没过两天就是4面,也是我第一次电话面试。四面就纯粹围绕着我做的项目PicGo开始说了。问的比较注重的部分是我对于PicGo的思考。从开发者和使用者和产品的角度去说明。比如如何维护、如何打磨产品,遇到的问题如何克服,与用户的意见不同时如何应对等等。我感觉更考量我对PicGo的认知和未来的规划,到底是一个用心做的产品还仅仅只是一个star收集者。
四面面完,没两天三面的leader就打电话过来问我啥时候能去实习。然而在四面面完的这几天里,我就决定了不去了。首先实验室6月底前放不了人;第二个跟我预期的有所出入,我以为是微信的前端团队招实习生(但不是),因为我其实想在前端这块能继续做深入一些,所以就还是把这个offer给拒了。当时想法是如果北京微信这边没有喜欢的岗位,那也没事,好好准备一下蚂蚁金服的面试就好。
回宿舍我跟舍友一说我把微信的offer拒了,他们只丢过来一句「暴殄天物」。舍得舍得,有舍才有得,后面会再说。
在面微信的面试阶段前,有个支付宝的北邮师兄通过微信联系上我。他说关注我的GitHub好久了,想给我内推到支付宝的前端团队那边。我自然是开心地答应了。不过我当时想着先完善简历+先把微信面完。不然一下子准备两个大厂的面试,压力大不说,万一时间撞上了反而更尴尬。在拒了微信后我把简历发给了师兄,开始了支付宝那边的内推。
支付宝这边技术面总共三面+HR一面。全程电话面试。
内推没多久,一面面试官就通过微信联系我,跟我约好了面试时间(第二天晚上7点半)并说「我这一面很轻松的」。在面试之前我有听说蚂蚁金服的面试是比较难的,虽然师兄说很简单但也是做好了被挂的准备。
7点半准时电话响起。面试官说他也是北邮毕业的,让我稍稍有所放松。然后接下来的问题就让我冷汗直冒。
一面的难度应该是面的这三个大厂以来最难的。面试过程中我还是比较紧张的,不过一开始确实紧张,后面说开了就好多了。面试官面完之后说等二面联系我吧。二面面试官是他们部门的leader。
一面面完的第二天面试官就加我了,直接约了当天晚上7点半的电面。(等于昨天一面今天二面…)事前我从内推我的师兄那里了解到二面面试官是很厉害的一个人,所以难度应该会比一面面试官高。听到这个消息不觉咽了一下口水,难受。
7点半准时电话响起。二面面试官的声音和语气给我的感觉是一开始比较低沉的,感觉比较严肃。然后后面的问题果然「没让我失望」地难。
面完感觉很凉,问题的深度是真的深。之前的面试很少有完全答不上来的,而这次二面对 vue-hot-reload
的问题就基本没有答上来。面试官最后给我的反馈大概还是不错的,所以我就在忐忑中等待三面的通知。
过了几天,三面面试官通过电话跟我约了时间,听声音还是很和善的。不过,问题还是依然很有难度啊!问题不多,总共问了三个问题,但是第一问就让我很难受:
这个面试总共只有45分钟不到,面试官说不能太长否则影响我的评价。我就说我第一题答得不够好。面试官说「不是不够好兄弟,是很不好!你第二题答得很不错,第三题有所偏差,但是你第一题答地太差了」
哈哈,当时听完觉得应该是凉了吧~然后面试官最后说了一句,「等之后HR会联系你」。噫,所以还是有戏?
经验总结三:只知其然不知其所以然是不行的,要对原理了解更深才能更好地解决问题。
不过人生总是有所波澜。
在我面支付宝结束前后,微信那边的HR小姐姐联系到我问我为什么把北京的岗位拒了。我说了之前我考虑的理由(主要是团队不符合预期啊啥的)。本来以为跟微信的缘分就这样了。然后HR小姐姐不死心,帮我联系了广州微信小程序的前端部门,问我去不去那边实习。我跟妹子商量了一下,暑期实习去广州两个月也能接受。于是就答应了。不过小程序那边还需要加面
。小程序这个部门做的是小程序开发者工具的,我觉得很合我的胃口,正好我也比较喜欢写工具类。
一波三折,在等待支付宝HR给我电话的这段时间里,我在两天内就拿到了微信小程序的offer。
三月最后一周的周一下午,我记得很清楚。3点开始一面。面试官给了我一个链接,让我一小时内做完题然后他再跟我电话聊。
一个小时总共两道题,这两个笔试题做完,面试官电话就过来了,简单问了一些问题:
面试官问了大概半小时,就说之后二面的leader会联系我。由于笔试题都做出来,所以感觉还是比较良好的。只是不知道二面来得这么快。
二面面试官隔了大概半小时就打电话来了,主要就看着我的PicGo这个项目在问,可能是因为技术栈(Electron)和小程序开发者工具(NW.js)比较接近吧。
面试官的语气非常和善,跟我探讨的时候也是基本以商量的语气。末了还夸了一下这个项目做得还是挺完整的。(其实还有一个很重要的「测试」部分没写。。。)考察的重点问题已经不是功能问题,而且类似安全、更新策略等这些平时可能写东西的时候不会太注意的问题。所以如果只是一个玩具项目,可能确实谈不上来。还好之前很多坑自己踩过,所以跟面试官聊起来也比较愉快。
经验总结四:一个好的(开源)项目非常加分。好的意思不是star多,而是你对它的思考、实践多。
经验总结五:如果你有一个做得很好的项目,一定要让面试官看到,并引导他问你的项目来把你熟悉的东西说出来。
第二天收到HR电话联系说已经通过面试了,第三天就发了Offer。
由于小程序这个组做的东西是开发者工具,很合我的胃口,于是我就接了这个Offer,而此时我还没接到支付宝的HR电话。微信的这个「抢人」速度是真的快。
支付宝HR电话在后面好久才打来。此时我已经接了小程序的offer了,于是暑期就没办法去支付宝实习了。我说了一下我暑假可能没法去实习,但是秋招还要回北京秋招。所以问能否保留秋招终面资格(跟去年一样)。支付宝的HR给我的反馈就是不一定,不好说。我想想反正如果不保留资格,到时候回北京再面就是了。
于是前两天终于发来的offer,也只能拒掉了。同时我也只能跟头条的HR说了一下情况,真的很不好意思,秋招还有机会。
我的春招(暑期实习)之旅也就这样结束。其实我大可接受支付宝的offer实习然后直接转正,不过我想着既然有一个更喜欢的机会去尝试一下又何尝不可呢。其实从第一次拒绝微信的offer到后面又接了小程序的offer,我觉得都是因为我想做自己喜欢做的事吧。
最后经验总结六:Do what you love, love what you do.
希望这份经历也能给你带来帮助。
我自己的主要开源项目
以及PicGo-Group的项目。
我参与的开源项目
:
命令支持】等等。
]]>前段时间,我用electron-vue开发了一款跨平台(目前支持主流三大桌面操作系统)的免费开源的图床上传应用——PicGo,在开发过程中踩了不少的坑,不仅来自应用的业务逻辑本身,也来自electron本身。在开发这个应用过程中,我学了不少的东西。因为我也是从0开始学习electron,所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历,用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。
预计将会从几篇系列文章或方面来展开:
PicGo
是采用electron-vue
开发的,所以如果你会vue
,那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如react
、angular
,那么纯按照本教程虽然在render端(可以理解为页面)的构建可能学习到的东西不多,不过在main端(Electron
的主进程)应该还是能学习到相应的知识的。
如果之前的文章没阅读的朋友可以先从之前的文章跟着看。本文主要是基于PicGo v2.1.0版本更新的重要内容做的讲述。
我们在使用一些Electron
开发的应用程序的时候,可以发现有些程序是可以通过命令行唤起的。比如VSCode
,在macOS的.bash_profile
里可以设置alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'
,这样就可以在命令行里通过code xxx.js
来调用VSCode打开文件了。如果想打开当前目录,可以通过code .
,如果想打开某个目录code xxx
等等。
命令行调用里其实还涉及到一个问题,有的时候我们的应用是个「单例应用」,也就是不能「多开」。如何在只能单开的应用里,也实现命令行调用呢?比如PicGo
,在软件打开的时候,命令行调用它也能上传图片,而不是打开一个新的PicGo
窗口。没事,下面会详细说明。
首先我们要来实现命令行调用。其实Electron
的命令行调用没有什么特殊的地方,与在Node.js
端很类似。我以PicGo
举例:
当我们在Windows下安装好了PicGo
之后,可以在安装目录里找到PicGo.exe
。你有没有想过在命令行里运行这个exe
会怎么样呢?在安装目录里打开powershell
,输入.\PicGo.exe
,你会发现PicGo
已经被打开了。如果我是加了一些参数打开会怎么样呢.\PicGo.exe upload
我们可以在main
进程里的ready
事件里把命令行参数打印出来:
1 | app.on('ready', () => { |
关键出现了,我们可以通过process.argv
这个在Node.js
端获取命令行参数的关键变量同样获得Electron
被命令行打开后的命令行参数。那么我们就可以在main
进程的ready
阶段通过获取的process.argv
参数来实现我们对应的功能。
对于PicGo而言,如果通过命令行打开它,并且传递了upload xxx.jpg
的话,我们就可以认为用户需要调用PicGo来实现上传一张图片。那么我们可以这么做(以下是实例代码):
1 | import path from 'path' |
拿到图片列表后就执行自带的上传逻辑即可。下面说说单开应用的命令行调用注意事项。
Electron
的发展很快,本文讲述的Electron
版本为当前最新的v4.1.4
,所以关于实现单例应用的api
也是跟随官方文档走的,如果你的Electron版本不是v4.x
,那么需要找对应版本的Electron
文档。
当前版本下实现单例应用的官方例子是:
1 | const { app } = require('electron') |
注意有个second-instance
事件。当我们试图在打开一个单例应用之后再打开这个应用的时候,就会触发这个事件。并且这个事件的回调函数里,有commandLine
和workingDeirectory
,实际上它们就是process.argv
和对应的cwd
(执行路径)。因此我们可以在这个事件里书写当应用试图被二次打开的时候应该做的事的逻辑。以下依然以PicGo举例:
1 | app.on('second-instance', (event, commandLine, workingDirectory) => { |
这里我们通过读取commandLine
参数,来判断用户是用命令行来调用PicGo
上传图片的,还是仅仅是通过PicGo
的图标再次打开PicGo
的。关键的逻辑就是判断commandLine
里有没有关键的参数,从而得出是否是从命令行调用我们的应用的。如果用户仅仅是通过PicGo
图标再次打开PicGo
,那么我们应该把之前打开过的窗口复原并激活,告诉用户你之前已经打开过这个应用了。当然具体的业务逻辑不能一概而论,这里只是我对PicGo
的一点理解,只需知道核心是监听second-instance
事件即可。
以下是上述实现的截图,注意命令行输出都只在第一个终端进程里,说明我们实现了单例应用的命令行调用:
其实这个章节到上面基本结束。不过我想起我演示的是在Windows下做的,相对简单。而macOS下的命令行调用Electron
应用会有个坑,所以还是要说一下为好。(由于我没有Linux机器,所以Linux部分就不说明了,有兴趣的朋友可以测试一下跟我反馈!)
大家都知道macOS
的应用基本是放在Application
下的,所以我们会很自然想到直接命令行调用它们:
1 | open /Applications/PicGo.app |
但是这样做并不能传递参数进去,因为执行命令的是open
。
所以我们需要到更深层次的路径启动PicGo
并传递参数进去:
1 | /Applications/PicGo.app/Contents/MacOS/PicGo upload xxx.jpg |
只有这样才能像Windows那样类似PicGo.exe
来实现调用。
值得注意的是,Electron
的macOS应用想要在生产阶段打开debug
模式查看console
的输出也是到上述应用的对应目录下:
1 | /Applications/PicGo.app/Contents/MacOS/PicGo --debug |
而Widnows
相对简单,只需要:
1 | .\PicGo.exe --debug |
(Linux请自测)
在实现了命令行调用的功能之后,我就在考虑给PicGo加上原生的系统右键菜单。这样做的好处是用户可以直接在一张图片上右键->通过PicGo上传。例如:
Windows下:
macOS下:
接下来说说二者在实现上不同的地方。(Linux没有测试,欢迎有兴趣的小伙伴测试一下跟我说说~)
Windows的右键菜单的原理其实很简单,在注册表里写入值就行。篇幅原因不会对Windows注册表的知识做过多的展开。我们只关注往哪里写值,写哪些值才能实现我们要的效果。
首先我们可以看看VScode是如何实现右键菜单「Open with Code」的。
在系统里按快捷键WIN+R
然后输入regedit
打开注册表编辑器,我们来找到VSCode
的右键菜单所在地:
HKEY_CLASSES_ROOT
→ *
→ shell
→ VSCode
:
可以看到一个「默认」的属性下的数据为「Open w&ith Code」,这个就是我们看到的菜单名。而一个叫「Icon」的属性下的数据为VSCode
的exe
安装路径。所以可以认为这个Icon
可以获取exe
的Icon
并显示到菜单上。
不过这里还没有看到如何将文件路径作为参数传入VScode
的。继续看:
HKEY_CLASSES_ROOT
→ *
→ shell
→ VSCode
→ command
:
在command
目录下我们看到了如下数据:
"C:\Users\PiEgg\AppData\Local\Programs\Microsoft VS Code\Code.exe" "%1"
可以看出这个%1
就是作为参数传给Code.exe
的。有了VSCode
作为参考,给自己的Electron
应用实现一个系统级别的右键菜单也不难了。有人可能会说我可以在应用启动阶段通过某些npm
包(比如windows-registry)来实现对注册表的写入。
不过实际上,在Windows
平台,如果你是用electron-builder
打包的话有一个更简洁的解决方案,那就是编写NSIS
脚本来实现,对此electron-builder
官方给出的文档可以一看。
本文不对NSIS
脚本做过多的描述,你只需要知道它是用来生成Windows
安装界面的一门脚本语言,你可以通过它来控制安装(卸载)界面都有哪些元素。并且它可以接入安装的生命周期,做一些操作,比如写入注册表。我们利用这个特性,来给PicGo做一个安装阶段写入注册表的操作,实现系统级别的右键菜单。
electron-builder
给NSIS
暴露的钩子主要有customHeader
, preInit
, customInit
, customInstall
, customUnInstall
,等等。
我们可以在customInstall
阶段通过获取用户安装PicGo的路径$INSTDIR
来实现对注册表关键值的写入。自己书写的installer.nsh
默认放在项目的build
目录下,那么electron-builder
在构建Windows
应用的时候将会自动读取这个文件以及package.json
里的配置来生成安装界面。
写入注册表的格式大概是这样:
1 | WriteRegStr <reg-path> <your-reg-path> <attr-name> <value> |
以下是PicGo的installer.nsh
,仅供参考:
1 | !macro customInstall |
注意HKCR
即是注册表目录HKEY_CLASSES_ROOT
的缩写。在写value
的时候如果要写多个参数,可以用单引号包起来。attr-name
不写即为默认。相信有了VSCode
的右键菜单注册表说明,你也能看得懂上面的PicGo的脚本了。同时注意我们应该在卸载阶段将之前写的注册表删除,以免用户卸载了应用之后菜单还在,上述脚本的后面部分是是在做这个事情。
因为上一章实现了命令行调用,所以我们的菜单就可以通过'"$INSTDIR\PicGo.exe" "upload" "%1"'
来实现菜单调用命令了。
macOS的话可以通过实现自动化脚本来生成右键菜单。打开automator
:
然后新建一个快速操作
:
将快速操作的工作流程限制到图像文件
,并且只作用于访达.app
里,同时在左侧菜单里找到shell
组件,将其拖拽到右侧编辑区:
将shell
选择成/bin/bash
,传递输入选成作为自变量
。
然后将默认的内容改成如下(实际上就差不多是之前说的macOS
下如何命令行调用Electron
应用的写法):
1 | /Applications/PicGo.app/Contents/MacOS/PicGo upload "$@" > /dev/null 2>&1 & |
其中macOS的快捷操作里,是通过"$@"
来作为参数传递的。
如何作为右键菜单?只要把你生成的这个workflow文件(夹),放到~/Library/Services
这个目录下就行了。
这样你就在你右键菜单里看到它:
如果你的服务项过多的话,会在服务的二级菜单里看到它:
其中,菜单名就是你生成的这个workflow的文件(夹)名。
那么生成了这个workflow之后,我们如何实现不让用户手动创建,而是自动帮他们放到~/Library/Services
目录下呢?macOS没有Windows那么方便的安装工具脚本语言,那么我们可以在main
进程里手动来实现这个功能。下面是PicGo的beforeOpen.js,其中我们将我们生成的workflow
文件(夹)放到项目的static
目录下。
1 | import fs from 'fs-extra' |
然后在主进程里加入这个方法,并判断是否在macOS下运行:
1 | // main/index.js |
这样用户在安装PicGo之后,打开软件之后,他的右键菜单就多了一个「Upload pictures with PicGo」项了。
至此,一个Electron
应用的命令行调用以及系统级别右键菜单的实现就讲述完了。当然可能还有其他实现的方式,以及更细致的实现(比如还能支持文件夹右键等等)。我在这里也只是一个抛砖引玉,其他的实现或者更好的实现方式需要自己摸索啦。当然本文没有Linux的相关内容,主要是我时间有限并且没有Linux机器,所以也希望有兴趣的朋友自己在Linux下实现了本文的功能后也能跟我说说~
本文很多都是我在开发PicGo
的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的electron-vue
开发带来一些启发。文中相关的代码,你都可以在PicGo和PicGo-Core的项目仓库里找到,欢迎star~如果本文能够给你带来帮助,那么将是我最开心的地方。如果喜欢,欢迎关注我的博客以及本系列文章的后续进展。(PS: 下一篇文章应该会讲述一下如何构建一个Electron应用 可扩展的快捷键系统 。)
注:文中的图片除未特地说明之外均属于我个人作品,需要转载请私信
感谢这些高质量的文章、问题等:
]]>前段时间,我用electron-vue开发了一款跨平台(目前支持主流三大桌面操作系统)的免费开源的图床上传应用——PicGo,在开发过程中踩了不少的坑,不仅来自应用的业务逻辑本身,也来自electron本身。在开发这个应用过程中,我学了不少的东西。因为我也是从0开始学习electron,所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历,用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。
预计将会从几篇系列文章或方面来展开:
PicGo
是采用electron-vue
开发的,所以如果你会vue
,那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如react
、angular
,那么纯按照本教程虽然在render端(可以理解为页面)的构建可能学习到的东西不多,不过在main端(Electron
的主进程)应该还是能学习到相应的知识的。
如果之前的文章没阅读的朋友可以先从之前的文章跟着看。并且如果没有看过前一篇CLI插件系统构建的朋友,需要先行阅读,本文涉及到的部分内容来自上一篇文章。
我们之前构建的插件系统是基于Node.js
端的。对于Electron
而言,main进程可以认为拥有Node.js
环境,所以我们首先要在main进程里将其引入。而对于PicGo而言,由于上传流程已经完全抽离到PicGo-Core
这个库里了,所以原本存在于Electron端的上传部分就可以精简整合成调用PicGo-Core
的api来实现上传部分的逻辑了。
而在引入PicGo-Core
的时候会遇到一个问题。在Electron
端,由于我使用的脚手架是Electron-vue
,它会将main
进程和renderer
进程都通过Webapck
进行打包。由于PicGo-Core
用于加载插件的部分使用的是require
,在Node.js端很正常没问题。但是Webpack并不知道这些require
是在运行时才需要调用的,它会认为这是构建时的「常规」require
,也就会在打包的时候把你require
的插件也打包进来。这样明显是不合理的,我们是运行时才require
插件的,所以需要做一些手段来「绕开」Webpack
的打包机制:
1 | // eslint-disable-next-line |
关于
__non_webpack_require__
的说明,可以查看文档。
打包之后会变成如下:
1 | const requireFunc = true ? require : require |
这样就可以避免PicGo-Core内部的require
被Webpack
也打包进去了。
Electron
的main
进程和renderer
进程实际上你可以把它们看成我们平时Web开发的后端和前端。二者交流的工具也不再是Ajax
,而是ipcMain
和ipcRenderer
。当然renderer
本身能做的事情也不少,只不过这样说一下可能会好理解一点。相应的,我们的插件系统原本实现在Node.js
端,是一个没有界面的工具,想要让它拥有「脸面」,其实也不过是在renderer
进程里调用来自main
进程里的插件系统暴露出来的api而已。这里我们举几个例子来说明。
在以前PicGo上传图片需要经过很多步骤:
Base64
编码。imgUploader
(比如qiniu
比如weibo
等)来上传到指定的图床。而如今整个底层上传流程系统已经被抽离出来,因此我们可以直接使用PicGo-Core实现的api来上传图片,只需定义一个Uploader类即可(下面的代码是简化版本):
1 | import { |
可以看出,由于在设计CLI插件系统的时候我们有考虑到设计好插件的生命周期,所以很多功能都可以通过生命周期的钩子、以及相应的一些事件来实现。比如图片上传完成就是通过picgo.on('finished', callback)
监听finished
事件来实现的,而上传的进度与进度条显示就是通过picgo.on('progress')
来实现的。它们的效果如下:
而且我们还可以通过接入picgo
的生命周期,实现一些以前实现起来比较麻烦的功能,比如上传前重命名:
1 | picgo.helper.beforeUploadPlugins.register('renameFn', { |
通过注册一个beforeUploadPlugin
,在上传前判断是否需要「上传前重命名」,如果是,就创建窗口并等待用户输入重命名的结果,然后将重命名的name
赋值给item.fileName
供后续的流程使用。
我们还可以在beforeTransform
阶段通知用户当前正在准备上传了:
1 | picgo.on('beforeTransform', ctx => { |
等等。所以实际上我们只需要在main
进程完成相应的api,那么renderer
进程做的事只不过是通过ipcRenderer
来通过main
进程调用这些api而已了。比如:
ipcRenderer
通知main
进程:1 | this.$electron.ipcRenderer.send('uploadChoosedFiles', sendFiles) |
main
进程监听事件并调用Uploader
的upload
方法:1 | ipcMain.on('uploadChoosedFiles', async (evt, files) => { |
就完成了一次「前后端」交互。其他方式上传(比如剪贴板上传)也同理,就不再赘述。
光有插件系统没有插件也不行,所以我们需要实现一个插件管理的界面。而插件管理的功能(比如安装、卸载、更新)已经在CLI版本里实现了,所以这些功能我们只需要通过向上一节里说的调用ipcRenderer
和ipcMain
来调用相应api即可。
在GUI界面我们需要一个很重要的功能就是「插件搜索」的功能。由于PicGo的插件统一是发布到npm的,所以其实我们可以通过npm的api来打到搜索插件的目的:
1 | getSearchResult (val) { |
通过搜索然后把结果显示到界面上就是如下:
没有安装的插件就会在右下角显示「安装」两个字样。
当我们安装好插件之后,需要从本地获取插件列表。这个部分需要做一些处理。由于插件是安装在Node.js端的,所以我们需要通过ipcRenderer
去向main
进程发起获取插件列表的「请求」:
1 | this.$electron.ipcRenderer.send('getPluginList') // 发起获取插件的「请求」 |
而获取插件列表以及相应信息我们需要在main
端进行,并发送回去:
1 | ipcMain.on('getPluginList', event => { |
注意到由于ipcMain
和ipcRenderer
里收发数据的时候会自动经过JSON.stringify
和JSON.parse
,所以对于原来的一些属性是function
之类无法被序列化的属性,我们要做一些处理,比如先执行它们得到结果:
1 | const handleConfigWithFunction = config => { |
这样,在renderer
进程里才能拿到完整的数据。
当然光有安装、查看还不够,还需要让插件管理界面拥有其他功能,比如「卸载」、「更新」或者是配置功能,所以在每个安装成功后的插件卡片的右下角有个配置按钮可以弹出相应的菜单:
菜单这个部分就是用Electron
的Menu
模块去实现了(我在之前的文章里已经有涉及,不再赘述),并没有特别复杂的地方。而这里比较关键的地方,就是当我点击配置plugin-xxx
的时候,会弹出一个配置的对话框:
这个配置对话框内的配置内容来自前文《开发CLI插件系统》里我们要求开发者定义好的config
方法返回的配置项。由于插件开发者定义的config
内容是Inquirer.js所要求的格式,便于在CLI环境下使用。但是它和我们平时使用的form
表单的一些格式可能有些出入,所以需要「转义」一下,通过原始的config
动态生成表单项:
1 | <div id="config-form"> |
上面是针对config
里不同的type
转换成不同的Web表单控件的代码。下面是初始化的时候处理config
的一些工作:
1 | watch: { |
经过上述处理,就可以将原本用于CLI的配置项,近乎「无缝」地迁移到Web(GUI)端了。其实这也是vue-cli3的ui版本实现的思路,大同小异。
不过既然是GUI软件了,只通过调用CLI实现的功能明显是不够丰富的。因此我也为PicGo
实现了一些特有的guiApi
提供给插件的开发者,让插件的可玩性更强。当然不同的软件给予插件的GUI能力是不一样的,因此不能一概而论。我仅以PicGo
为例,讲述我对于PicGo
所提供的guiApi
的理解和看法。下面我就来说说这部分是如何实现的。
由于PicGo本质是一个上传系统,所以用户在上传图片的时候,很多插件底层的东西和功能实际上是看不到的。如果要让插件的功能更加丰富,就需要让插件有自己的「可视化」入口让用户去使用。因此对于PicGo而言,我给予插件的「可视化」入口就放在插件配置的界面里——除了给插件默认的配置菜单之外,还给予插件自己的菜单项供用户使用:
这个实现也很容易,只要插件在自己的index.js
文件里暴露一个guiMenu
的选项,就可以生成自己的菜单:
1 | const guiMenu = ctx => { |
可以看到菜单项可以自定义,点击之后的操作也可以自定义,因此给予了插件很大的自由度。可以注意到,在点击菜单的时候会触发handle
函数,这个函数里会传入一个guiApi
,这个就是本节的重点了。就目前而言,guiApi
实现了如下功能:
showInputBox([option])
调用之后打开一个输入弹窗,可以用于接受用户输入。showFileExplorer([option])
调用之后打开一个文件浏览器,可以得到用户选择的文件(夹)路径。upload([file])
调用之后使用PicGo底层来上传,可以实现自动更新相册图片、上传成功后自动将URL写入剪贴板。showNotificaiton(option)
调用之后弹出系统通知窗口。上面api我们可以通过诸如guiApi.showInputBox()
、guiApi.showFileExplorer()
等来实现调用。这里面的例子实现思路都差不多,我简单以guiApi.showFileExplorer()
来做讲解。
当我们在renderer
界面点击插件实现的某个菜单之后,实际上是通过调用ipcRenderer
向main
进程传播了一次事件:
1 | if (plugin.guiMenu) { |
于是在main
进程,我们通过监听这个事件,来调用相应的guiApi
:
1 | const handlePluginActions = (ipcMain, CONFIG_PATH) => { |
而guiApi
的实现类GuiApi其实特别简单:
1 | import { |
实际上就是去调用一些Electron
的方法,甚至是你自己封装的一些方法,返回值是一个新的Promise
对象。这样插件开发者就可以通过async
和await
来方便获取这些方法的返回值了:
1 | const guiMenu = ctx => { |
至此,一个GUI插件系统的关键部分我们就基本实现了。除了整合了CLI插件系统的几乎所有功能之外,我们还提供了独特的guiApi
给插件开发者无限的想象空间,也给用户带来更好的插件体验。可以说插件系统的实现,让PicGo
有了更多的可玩性。关于PicGo
目前的插件,欢迎查看Awesome-PicGo的列表。以下罗列一些我觉得比较有用或者有意思的插件:
如果你也想为PicGo开发插件,欢迎阅读开发文档,PicGo有你更精彩哈哈!
本文很多都是我在开发PicGo
的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的electron-vue
开发带来一些启发。文中相关的代码,你都可以在PicGo和PicGo-Core的项目仓库里找到,欢迎star~如果本文能够给你带来帮助,那么将是我最开心的地方。如果喜欢,欢迎关注我的博客以及本系列文章的后续进展。
注:文中的图片除未特地说明之外均属于我个人作品,需要转载请私信
感谢这些高质量的文章:
]]>祝大家2019年猪年新年快乐!本文较长,需要一定耐心看完哦~
前段时间,我用electron-vue开发了一款跨平台(目前支持主流三大桌面操作系统)的免费开源的图床上传应用——PicGo,在开发过程中踩了不少的坑,不仅来自应用的业务逻辑本身,也来自electron本身。在开发这个应用过程中,我学了不少的东西。因为我也是从0开始学习electron,所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历,用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。
预计将会从几篇系列文章或方面来展开:
PicGo
是采用electron-vue
开发的,所以如果你会vue
,那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如react
、angular
,那么纯按照本教程虽然在render端(可以理解为页面)的构建可能学习到的东西不多,不过在main端(electron的主进程)应该还是能学习到相应的知识的。
如果之前的文章没阅读的朋友可以先从之前的文章跟着看。
说在前面,其实这篇文章写起来真的很难。如何构建一个插件系统,我花了半年的时间。要在一篇或者两篇文章里把这个东西说好是真的不容易。所以可能行文上会有一些瑕疵,后续会不断打磨。
相信很多人平时更多的是给其他框架诸如Vue
、React
或者Webpack
等写插件。我们可以把提供插件系统的框架称为「容器」,通过容器暴露出来的API,插件可以挂载到容器上,或者接入容器的生命周期来实现一些更定制化的功能。
比如Webpack
本质上是一个流程系统,它通过Tapable暴露了很多生命周期的钩子,插件可以通过接入这些生命周期钩子实现流水线作业——比如babel
系列的插件把ES6
代码转义成ES5
;SASS
、LESS
、Stylus
系列的插件把预处理的CSS
代码编译成浏览器可识别的正常CSS
代码等等。
我们要实现一个插件系统,本质上也是实现这么一个容器。这个容器以及对应的插件需要具备如下基本特征:
第一点应该很容易理解。如果一个插件系统因为没有第三方插件的存在就无法运行,那么这个插件系统有什么用呢?不过有别于第三方插件,很多插件系统有自己内置的插件,比如vue-cli
、Webpack
的一系列内置插件。这个时候插件系统本身的一些功能就会由内置的插件去实现。
第二点,插件的独立性是指插件本身运行时不会 主动 影响其他插件的运作。当然某个插件可以依赖于其他插件的运行结果。
第三点,插件如果不能配置不能管理,那么从安装插件阶段就会遇到问题。所以容器需要有设计良好的入口给予插件注册。
接下来的部分,我将结合PicGo-Core与PicGo来详细说明CLI插件系统与GUI插件系统如何构建与实现。
其实CLI插件系统可以认为是无GUI的插件系统,也就是运行在命令行或者不带有可视化界面的插件系统。为什么我们开发Electron的插件系统,需要扯到CLI插件系统呢?这里需要简单回顾一下Electron的结构:
可以看到除了Renderer
的界面渲染,大部分的功能是由Main
进程提供的。对于PicGo而言,它的底层应该是一个上传流程系统,如下:
所以理论上它的底层应该在Node.js端就能实现。而Electron的Renderer
进程只是实现了GUI界面,去调用底层Node.js端实现的流程系统提供的API而已。类似于我们平时在开发网页时候的前后端分离,只不过现在这个后端是基于Node.js实现的插件系统。基于这个思路,我开始着手PicGo-Core的实现。
通常来说一个插件系统都有自己的一个生命周期,比如Vue
有beforeCreate
、created
、mounted
等等,Webpack
有beforeRun
、run
、afterCompile
等等。这个也是一个插件系统的灵魂所在,通过接入系统的生命周期,赋予了插件更多的自由度。
因此我们可以先来实现一个生命周期类。代码可以参考Lifecycle.ts。
生命周期流程可以参考上面的流程图。
1 | class Lifecycle { |
在实际使用中,我们可以通过:
1 | const lifeCycle = new LifeCycle() |
来运行整个上传流程的生命周期。不过到这里我们还没有看到任何跟插件相关的东西。这是为了实现我们说的第一个条件: 容器在没有 第三方插件 接入的情况下也能 实现基本功能。
很多时候我们需要将一些事件以某种方式传递出去。就像发布订阅模型一样,由容器发布,由插件订阅。这个时候我们可以直接让Lifecycle
这个类继承Node.js自带的EventEmmit
:
1 | class Lifecycle extends EventEmitter { |
那么Lifecycle
也就拥有了EventEmitter
的emit
和on
方法了。对于容器来说,我们只需要emit
事件出去即可。
比如在PicGo-Core
里,上传的整个流程都会往外广播事件,通知插件当前进行到什么阶段,并且将当前的输入或者输出在广播的时候发送出去。
1 | private async beforeTransform (input) { |
插件可以自由选择监听想要监听的事件。比如插件想要知道上传结束后的结果(伪代码):
1 | plugin.on('finished', (output) => { |
在开发PicGo-Core的时候,有一些很有用的事件。在这里我也想分享出来,虽然不是所有插件系统都会有这样的事件,但是结合自己和项目的实际需要,他们有的时候很有用。
平时我们上传或者下载文件的时候,都会注意一个东西:进度条。同样,在PicGo-Core里也暴露了一个事件,叫做uploadProgress
,用于告诉用户当前的上传进度。不过在PicGo-Core,上传进度是从beforeTransform
就开始算了,为了方便计算,划分了5个固定的值。
1 | private async beforeTransform (input) { |
如果上传失败的话就返回-1
:
1 | async start (input: any[]): Promise<void> { |
通过监听这个事件,PicGo就能做出如下的上传进度条:
如果上传出了问题,或者有些信息需要通过系统级别的通知告诉用户的话,可以发布notification
事件。通过监听这个事件可以调用系统通知来发布。插件也可以发布这个事件,让PicGo监听。如上图上传成功后右上角的通知。
上部分讲到了生命周期中的事件广播,可以发现事件广播是只管发不管结果的。也就是PicGo-Core只管发布这个事件,至于有没有插件监听,监听后做了什么都不用关心。(怎么有点像UDP一样)。但是实际上很多时候我们需要接入生命周期做一些事情的。
就拿上传流程来说,我要是想要上传前压缩图片,那么监听beforeUpload
事件是做不到的。因为在beforeUpload
事件里就算你把图片已经压缩了,恐怕上传的流程早就走完了,emit
事件出去后生命周期照旧运行。
因此我们需要在容器的生命周期里实现一个功能,能够让插件接入它的生命周期,在执行完当前生命周期的插件的动作后,才把结果送往下一个生命周期。可以发现,这里有一个「等待」插件执行的动作。因此PicGo-Core使用最简易而直观的async
函数配合await
来实现「等待」。
我们先不用考虑插件是如何注册的,后文会说到。我们先来实现怎么让插件接入生命周期。
下面以生命周期beforeUpload
为例:
1 | private async beforeUpload (input) { |
可以看到我们通过await
等待生命周期方法handlePlugins
(下文会说明如何实现)的执行结束。而我们运行的插件列表是通过beforeUploadPlugins.getList()
(下文会说明如何实现)获取的,说明这些是只针对beforeUpload
这个生命周期的插件。然后将输入input
传入handlePlugins
让插件们调用即可。
现在我们实现一下handlePlugins
:
1 | private async handlePlugins (plugins: Plugin[], input: any[]) { |
我们通过Promise.all
以及await
来「等待」所有插件执行。这里需要注意的是,每个PicGo插件需要实现一个handle
方法来供PicGo-Core
调用。可以看到,这里实现我们说的第二个特征: 插件具有独立性。
从这里也能看到我们通过async
和await
构建了一个能够「等待」插件执行结束的环境。这样就解决了光是通过广播事件无法接入插件系统的生命周期的问题。
不,等等,这里还有一个问题。beforeUploadPlugins.getList()
是哪来的?上面只是一个示例代码。实际上PicGo-Core根据上传流程里的不同生命周期预留了五种不同的插件:
分别在上传的5个周期里调用。虽然这5种插件调用的时机不一样,但是它们的实现是同样的:有同样的注册机制、同样的方法用于获取插件列表、获取插件信息等等。所以我们接下去来实现一个生命周期的插件类。
这个是插件系统里很关键的一环,这个类的实现了插件应该以什么方式注册到我们的插件系统里,以及插件系统如何获取他们。这块的代码可以参考 LifecyclePlugins.ts。
以下是实现:
1 | class LifecyclePlugins { |
对于插件而言最重要的是register
方法,它是插件注册的入口。通过register
注册后,会在Lifecycle
内部的list
以id:plugin
形式里写入这个插件。注意到,PicGo-Core要求每个插件需要实现一个handle
的方法,用于之后在生命周期里调用。
这里用伪代码说明一下插件要如何注册:
1 | beforeTransformPlugins.register('test', { |
这里我们就注册了一个id
叫做test
的插件,它是一个beforeTransform
阶段的插件,它的作用就是打印传入的信息。
然后在不同的生命周期里,调用LifeCyclePlugins.getList()
的方法就能获取这个生命周期对应的插件的列表了。
如果仅仅是实现一个能够在Node.js项目里运行的插件系统,上面两个部分基本就够了:
不过一个良好的CLI插件系统还需要至少如下的部分(至少我觉得):
此处可以参考vue-cli3这个工具。
因此我们至少还需要如下的部分:
这上面的几个部分都跟生命周期类本身没有特别强的耦合关系,所以可以不必将它们都放到生命周期类里实现。
相对的,我们抽离出一个Core
作为核心,将上述这些类包含到这个核心类中,核心类负责命令行命令的注册、插件的加载、优化日志信息以及调用生命周期等等。
最后再将这个核心类暴露出去,供使用者或者开发者使用。这个就是PicGo-Core的核心 PicGo.ts 的实现。
PicGo本身的实现并不复杂,基本上只是调用上述几个类实例的方法。
不过注意到这里有一个之前一直没有提到的东西。PicGo-Core除了核心PicGo之外的几个子类里,基本上在constructor
构建函数阶段都会传入一个叫做ctx
的参数。这个参数是什么?这个参数是PicGo这个类自身的this
。通过传入this
,PicGo-Core的子类也能使用PicGo核心类暴露出来的方法了。
比如Logger
类实现了美观的命令行日志输出:
那么在其他子类里想要调用Logger
的方法也很容易:
1 | ctx.log.success('Hello world!') |
其中ctx
就是我们上面说的,PicGo自身的this
指针。
我们接下去介绍的每个类具体的实现。
先从这个类开始说起是因为这个类是最简单而且侵入性最小的一个类。有它没它都行,但是有它自然是锦上添花。
PicGo实现美化日志输出的库是chalk,它的作用就是用来输出花花绿绿的命令行文字:
用起来也很简单:
1 | const log = chalk.green('Success') |
我们打算实现4种输出类型,success、warn、info和error:
于是创建如下的类:
1 | import chalk from 'chalk' |
之后再将Logger
这个类挂载到PicGo核心类上:
1 | import Logger from '../lib/Logger' |
这样其他挂载到PicGo核心类上的类就能使用ctx.log
来调用log里的方法了。
很多时候我们的所写的系统也好、插件也好,或多或少需要一些配置之后才能更好地使用。比如vue-cli3
的vue.config.js
,比如hexo
的_config.yml
等等。而PicGo也不例外。默认情况下它可以直接使用,但是如果想要做些其他操作,自然就需要配置了。所以配置文件是插件系统很重要的一个组成部分。
之前我在Electron版的PicGo上使用了lowdb作为JSON配置文件的读写库,体验不错。为了向前兼容PicGo的配置,写PicGo-Core的时候我依然采用了这个库。关于lowdb的一些具体用法,我在之前的一篇文章里有提及,有兴趣的可以看看——传送门。
由于lowdb做的是类似MySQL一样的持久化配置,它需要磁盘上一个具体的JSON文件作为载体,所以无法通过创建一个配置对象去初始化配置。因此一切都从这个配置文件展开:
PicGo-Core采用一个默认的配置文件:homedir()/.picgo/config.json
,如果在实例化PicGo没提供配置文件路径那么就会使用这个文件。如果使用者提供了具体的配置文件,那么就会使用所提供的配置文件。
下面来实现一下PicGo初始化的过程:
1 | import fs from 'fs-extra' |
那么在实例化PicGo的时候就是如下这样:
1 | const PicGo = require('picgo') |
有了配置文件之后,我们只需要实现三个基本操作:
一般来说我们的系统都会有一些默认的配置,PicGo也不例外。我们可以选择把默认配置写到代码里,也可以选择把默认配置写到代码里。因为PicGo的配置文件有持久化的需求,所以把一些关键的默认配置写入配置文件是合理的。
初始化配置的时候会用到lowdb的一些知识,这里就不展开了:
1 | import lowdb from 'lowdb' |
那么在PicGo初始化阶段就可以将configPath
传入,来实现配置的初始化,以及获取配置。
1 | init () { |
一旦初始化配置之后,要获取配置就很容易了:
1 | import { get } from 'lodash' |
这里用到了lodash
的get
方法,主要是为了方便获取如下情况:
比如配置内容长这样:
1 | { |
往常我们要获取a.b
需要:
1 | let b = this.config.a.b |
万一遇到a
不存在的时候,那么上面那句话就会报错了。因为a
不存在,那么a.b
就是undefined.b
自然会报错了。而用lodash
的get
方法则可以避免这个问题,并且可以很方便的获取:
1 | let b = get(this.config, 'a.b') |
如果a
不存在,那么获取到的结果b
也不会报错,而是undefined
。
有了上面的铺垫,写入内容也很简单。通过lowdb
提供的接口,写入配置如下:
1 | const saveConfig = (configPath: string, config: any): void => { |
我们可以用:
1 | saveConfig(this.configPath, { a: { b: true } }) |
或者:
1 | saveConfig(this.configPath, { 'a.b': true }) |
上面两种写法都会生成如下配置:
1 | { |
可以看到明显后者更简洁点。这多亏了lowdb里由lodash提供的set
方法。
至此我们已经将配置文件相关的操作实现完了。其实可以把这堆操作封装成一个类的,PicGo-Core在一开始实现的时候觉得东西不多不复杂,所以只是抽成了一个小工具来调用的。当然这个不是关键,关键在于实现了配置文件的相关操作后,你的系统和这个系统的插件都能因此受益。系统可以把跟配置文件相关的操作的API暴露给插件使用。接下去我们一步步来完善这个插件系统。
暂时没想好这个类要取的名字是啥,代码里我写的是pluginHandler
,那么就叫它插件操作类吧。这个类主要目的就三个:
npm
安装插件 —— installnpm
卸载插件 —— uninstallnpm
更新插件 —— update用npm
来分发插件,这是大多数Node.js插件系统会选择的解决方案。毕竟在没有自己的插件商店(比如VSCode)的基础上,npm
就是一个天然的「插件商店」。当然发布到npm
之上好处还有很多,比如可以十分方便地来对插件进行安装、更新和卸载,比如对Node.js用户来说是0成本的上手。这也是pluginHandler
这个类要做的事。
pluginHandler
相关的实现思路来自feflow,特此感谢。
平时我们安装一个npm模块的时候,很简单:
1 | npm install xxxx --save |
不过我们是在当前项目目录的上来安装的。PicGo由于引入了配置文件,所以我们可以直接在配置文件所在的目录里进行插件的安装,这样如果你要卸载PicGo,只要把。但是每次都让用户打开PicGo的配置文件所在的路径去安装插件未免太累了。这样也不优雅。
相对的,如果我们全局安装了picgo
之后,在文件系统任何一个角落里只需要通过picgo install xxx
就能安装一个picgo
的插件,而不需要定位到PicGo的配置文件所在的文件夹,这样用户体验会好不少。这里大家可以类比vue-cli3
安装插件的步骤。
为了实现这个效果,我们需要通过代码的方式去调用npm
这个命令。那么Node.js要如何通过代码去实现命令行调用呢?
这里我们可以使用cross-spawn来实现跨平台的、通过代码来调用命令行的目的。
spawn
这个方法Node.js原生也有(在child_process里),不过cross-spawn
解决了一些跨平台的问题。使用上是一样的。
1 | const spawn = require('cross-spawn') |
可以看到,它的参数是以数组的形式传入的。
而我们要实现的插件操作,除了主要命令install
、update
、uninstall
不一样之外,其他的参数都是一样的。所以我们抽离出一个execCommand
的方法来实现它们背后的公共逻辑:
1 | execCommand (cmd: string, modules: string[], where: string, proxy: string = ''): Promise<Result> { |
关键的部分基本都已经在代码里给出了注释。当然这里还是有一些需要注意的地方。注意这句话:
1 | const npm = spawn('npm', args, { cwd: where }) // 执行npm,并通过 cwd指定执行的路径——配置文件所在文件夹 |
里面的{cwd: where}
,这个where
是会从外部传进来的值,表示这个npm
命令会在哪个目录下执行。这个也是我们要做这个插件操作类最关键的地方——不用让用户主动打开配置文件所在目录去安装插件,在系统任何地方都可以轻松安装PicGo的插件。
接下去我们实现一下install
方法,这样另外两个就可以类推了。
1 | async install (plugins: string[], proxy: string): Promise<void> { |
别看代码很多,关键就一句const result = await this.execCommand('install', plugins, this.ctx.baseDir, proxy)
,剩下的都是日志输出而已。好了,插件也安装完了,如何加载呢?
上面说了,我们会将插件安装在配置文件所在目录里。值得注意的是,由于npm
的特点,如果目录里有个叫做package.json
的文件,那么安装插件、更新插件等操作会同时修改package.json
文件。因此我们可以通过读取package.json
文件来得知当前目录下有什么PicGo的插件。这也是Hexo的插件加载机制里的很重要的一环。
pluginLoader
相关的实现思路来自hexo,特此感谢。
关于插件的命名,PicGo这里有个约束(这也是很多插件系统选择的方式),必须以picgo-plugin-
开头。这样才能方便插件加载类识别它们。
这里有一个小坑。如果我们配置文件所在的目录里没有package.json
的话,那么执行安装插件的命令会有报错信息。但是我们不想让用户看到这个报错,于是在初始化插件加载类
的时候,需要判断一下这个文件存不存在,如果不存在那么我们就要创建一个:
1 | class PluginLoader { |
接下来我们要实现最关键的load
方法了。我们需要如下步骤:
package.json
来找到所有合法的插件require
来加载插件picgoPlugins
配置来判断插件是否被禁用register
方法来实现插件注册1 | import PicGo from '../core/PicGo' |
load
这个方法是整个插件系统加载的最关键的部分。光看上面的步骤和代码可能没办法很好理解。我们下面用一个具体的插件例子来说明。
假设我写了一个picgo-plugin-xxx
的插件。我的代码如下:
1 | // 插件系统会传入picgo的ctx,方便插件调用picgo暴露出来的api |
我们从前文已经大概知道插件运行流程:
beforeTransform
,那么这个阶段就去获取beforeTransformPlugins
这些插件beforeTransformPlugins
这些插件由ctx.helper.beforeTransformPlugins.register
方法注册,并可以通过ctx.helper.beforeTransformPlugins.getList()
获取beforeTransformPlugins
的handle
方法,并传入ctx
供插件使用注意上面的第三步,ctx.helper.beforeTransformPlugins.register
这个方法是在什么时候被调用的?答案就是在本小节介绍的插件的加载阶段,pluginLoader
调用了每个插件的register
方法,那么在插件的register
方法里,我们写了:
1 | ctx.helper.beforeTransformPlugins.register('xxx', { |
也就是在这个时候,ctx.helper.beforeTransformPlugins.register
这个方法被调用。
于是乎,在生命周期开始之前,整个插件以及每个生命周期的插件已经预先被注册了。所以在生命周期开始运作的时候,只需要通过getList()
就可以获取注册过的插件,从而执行整个流程了。
也因此,我以前在跑Hexo
生成博客的时候曾经遇到的问题就得到解释了。我以前安装过一些Hexo
的插件,但是不知道为什么总是无法生效。后来发现是安装的时候没有使用--save
,导致它们没被写入package.json
的依赖字段。而Hexo
加载插件的第一步就是从package.json
里获取合法的插件列表,如果插件不在package.json
里,哪怕在node_modules
里有,也不会生效了。
有了插件,接下去我们讲讲如何在命令行调用和配置了。
PicGo的命令行操作类主要依赖于两个库:commander.js和Inquirer.js。这两个也是做Node.js命令行应用很常用的库了。前者负责命令行解析、执行相关命令。后者负责提供与用户交互的命令行界面。
比如你可以输入:
1 | picgo use uploader |
这个时候由commander.js
去解析这句命令,告诉我们这个时候调用的是use
这个命令,参数是uploader
,那么就进入Inquirer.js
提供的交互式界面了:
如果你用过诸如vue-cli3
或者create-react-app
等类似的命令行工具一定类似的情况很熟悉。
首先我们写一个命令行操作类,用于暴露api给其他部分注册命令,此处源码可以参考Commander.ts。
1 | import PicGo from '../core/PicGo' |
然后我们在PicGo-Core的核心类里将其实例化:
1 | import Commander from '../lib/Commander' |
这样其他部分就可以使用ctx.cmd.program
来调用commander.js
以及使用ctx.cmd.inquirer
来调用Inquirer.js
了。
这两个库的使用,网络上有很多教程了。此处简单举个例子,我们从PicGo最基本的功能——命令行上传图片开始说起。
为了与之前的插件结构统一,我们把命令注册也写到handle
函数里。
1 | import PicGo from '../../core/PicGo' |
这样我们如果通过某种方式把命令注册进去:
1 | import PicGo from '../../core/PicGo' |
当代码写到这里,可能大家觉得已经大功告成了。实际上还差了最后一步,我们缺少一个入口来接纳我们输入的命令。就比如现在我们写完了命令,也写完了命令的注册,然后我们要怎么在命令行里使用呢?
这个时候要简单说下package.json
里的两个字段bin
和main
。其中main
字段指向的文件,是你const xxx = require('xxx')
的时候拿到的东西。而bin
字段指向的文件,就是你在全局安装了之后,可以在命令行里直接输入的命令。
举个例子,PicGo-Core的bin
字段如下:
1 | // ... |
那么用户如果全局安装了picgo,就可以通过picgo
这个命令来使用picgo了。类似安装@vue/cli
之后,可以使用vue
这个命令一样。
那么我们来看看./bin/picgo
做了啥。源码在这里。
1 |
|
关键部分就在picgo.cmd.program.parse(process.argv)
这句话,这句话调用了commander.js
来解析process.argv
,也就是命令行里命令以及参数。
那么我们在开发阶段就可以用./bin/picgo upload
这样来调用命令,而在生产环境下,也就是用户全局安装后,就可以通过picgo upload
这样来调用命令了。
前文提到了,配置项是插件系统里很重要的一个组成部分。不同插件系统的配置项处理不太一样。比如Hexo
提供了_config.yml
供用户配置,vue-cli3
提供了vue.config.js
供用户配置。PicGo也提供了config.json
供用户配置,不过在此基础上,我想提供一个更方便的方式来让用户直接在命令行里完成配置,而不需要专门打开这个配置文件。
比如我们可以通过命令行来选择当前上传的图床是什么:
1 | $ picgo use |
这种在命令行里的交互,需要之前提到的Inquirer.js
来辅助我们达到这个效果。
它的用法也很简单,传入一个prompts
(可以理解为一个问题数组),然后它会将问题的结果再以对象的形式返回出来,我们通常将这个结果记为answer
。
而PicGo为了简化这个过程,只需要插件提供一个config
方法,这个方法只需返回一个合法的prompts
问题数组,然后PicGo会自动调用Inquirer.js
去执行它,并自动将结果写入配置文件里。
举个例子,PicGo内置的Imgur
图床的config
代码如下:
1 | const config = (ctx: PicGo): PluginConfig[] => { |
然后我们用代码实现能够在命令行里调用它,源码传送门:
以下代码有所精简
1 | import PicGo from '../../core/PicGo' |
上面是针对Uploader的config方法进行的配置处理,对于其他插件也是同理的,就不再赘述。这样我们就实现了能够通过命令行快速对配置文件进行配置,用户体验又是++。
讲了那么多,我们都是在本地书写的插件系统,如何发布让别人能够安装使用呢?关于往npm发布模块有很多相关文章,比如参考这篇文章。我在这里想讲的是如何发布一个既能在命令行使用,又可以通过比如const picgo = require('picgo')
在Node.js项目里使用API调用的库。
其实这个上面的部分里也提到了。我们在发布一个npm库的时候通常是在package.json
里的main
字段指定这个库的入口文件。那么这样使用者就可以通过比如const picgo = require('picgo')
在Node.js项目里使用。
如果我们想要让这个库安装之后能够注册一个命令,那么我们可以在bin
字段里指定这个命令已经对应的入口文件。比如:
1 | // ... |
这样我们在全局安装之后就会在系统里注册一个叫做picgo
的命令了。
当然这个时候bin
和main
的入口文件通常是不一样的。bin
的入口文件需要做好解析命令行的功能。所以通常我们会使用一些命令行解析的库例如minimist
或者commander.js
等等来解析命令行里的参数。
至此,一个CLI插件系统的关键部分我们就基本实现了。那么我们在Electron项目里,可以在main
进程里使用我们所写的插件系统,并通过这个插件暴露的API来打造应用的插件系统了。下一篇文章会详细讲述如何把CLI插件系统整合进Electron,实现GUI插件系统,并加入一些额外的机制,使得在GUI上的插件系统更加灵活而强大。
本文很多都是我在开发PicGo
的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的electron-vue
开发带来一些启发。文中相关的代码,你都可以在PicGo和PicGo-Core的项目仓库里找到,欢迎star~如果本文能够给你带来帮助,那么将是我最开心的地方。如果喜欢,欢迎关注我的博客以及本系列文章的后续进展。
注:文中的图片除未特地说明之外均属于我个人作品,需要转载请私信
感谢这些高质量的文章:
]]>今年和去年一样,也是格外忙。不仅实验室活多,还要兼顾研究生的开题等。跟去年一样,列一个今年学习成果清单:
2019.01.13(插播) PicGo 发布v2.0版本,正式支持插件系统。star数破3200,下载量破26k。【Electron】
2018.08.28 PicGo star数破2000,下载量破12k。【Electron】
2018.07.19 PicGo-Core 开坑PicGo底层流程系统,将支持插件系统【Node+TypeScript】
2018.07.11 PicGo 更新v1.6版本,支持阿里云OSS,imgur,mini窗口,批量删除等功能。【Electron】
2018.05.23 为VSCode的amVim-for-VSCode插件提交的支持:
呼出Command Palette
并实现部分Vim命令的PR被合并。【TypeScript】
2018.05.17 PicGo star数破800,下载数破5k。【Electron】
2018.05.15 开发推来推趣3期后台时遇到微信二维码支付相关功能的开发,总结了一篇《基于Koa2开发微信二维码扫码支付相关流程》的经验文。【Koa】
2018.05.09 PicGo 更新v1.5版本,支持腾讯云COSv5、GitHub图床、重命名等新功能。【Electron】
2018.03.28 node-github-profile-summary和vue-koa-demo的Docker话。【Docker】
2018.03.10~2018.05.31 推来推趣3期后台(全栈)迭代。【Vue+Koa+Graphql】
2018.03.06 hexo-theme-melody 更新v1.5版本,支持iframe、支持slides等特性。【hexo+hexo-theme】
2018.01.17~2018.03.28 开坑node-github-profile-summary,可以生成漂亮的GitHub总结报告。【Vue+Koa+Chart.js+Graphql】
2018.01.11~2018.05.08 写了Electron-vue开发实战系列教程,用于记录自己开发PicGo的坑以及帮助新人入门Electron开发。【Electron】
对比去年给自己立的目标:
感觉完成度不够高,不及去年同期对2016年的目标的实现。主要是没有预料到下半年研究生的开题的战线耗时这么久。从2018年8月开始我就没有发过笔记或者技术文章了,真的非常惭愧。
依然要写下2019年需要学习的东西:
感觉把目标缩小点应该完成度会更高。毕竟19年要开始找实习和正式工作+写研究生毕设了。
这一年来的前端的学习之路,收获还是不少的。比起2017年来说,我感觉最大的收获就是阅读源码的能力提高了。虽然不是什么高深的源码,不过相比之前对阅读源码有恐惧心理的自己,还是好了不少。
5月份的时候,那段时间我的Mac上的VSCode的Vim插件变得异常卡,可以参考这个issue。无奈之下只能把官方的Vim插件替换掉,换成了amVim-for-VSCode,当初刚换上的时候,操作如丝般顺滑!不过当时发现它不支持:
带来的一系列操作,比如:w
保存,:q
退出等。于是我萌生了一个想法,能不能把VSCodeVim的操作移植到amVim上?在阅读了VSCodeVim的源码之后,我也模仿了它的实现,把一部分常用的命令移植到了amVim上,并最终成功被作者合并。
这次提交PR的过程,我也发了一篇文章作为记录。应该说这次经历过后我对阅读源码的恐惧感减轻了不少,这也为之后的PicGo-Core的开发带来很大的帮助。
8月份之后很长的一段时间里,除了在做研究生开题相关的东西,我基本就是在开发PicGo-Core了。如果你有用过PicGo,那么你应该知道它的1.x版本是不支持插件系统的。而且内置的只有有限的8个图床。(如果你不知道PicGo,欢迎使用,对你的文章写作有很大帮助~)。PicGo
里我收到最多的issue,应该就是能否支持XXX图床
。如果是一开始写PicGo的时候,我一般会在下一个版本里更新新的图床支持。但是支持到第8个的时候我发现这样无限地支持下去不是一个办法。正巧有个用户提出一个想法:能否将对各种图床的支持,做成插件化的管理,类似 Core + Plugins 这样的模式。
我为此思考了好久,发现这样是可行而且非常合理的。于是我开始找相关的资料——我一开始的想法只是在Electron内部实现一个插件系统。为此我去找了不少例子,比如VSCode、Kap、Atom、Hyper等用Electron写的工具,想看看他们的插件系统是如何实现的。发现他们的实现相对比较复杂。对我来说我是想要实现一个底层的上传流程系统。
后来我想到了Hexo也是有插件系统的,于是就去阅读了Hexo的插件系统如何实现。在看Hexo插件系统实现的同时,我还发现了另外一个工具feflow的插件系统实现。不过我后来发现,feflow的插件体系其实底层大部分是「抄」的hexo的源码的,尤其一个很经典的例子…
于是我就把feflow的文章当做hexo插件系统实现的解析文章了哈哈。
在充分理解了hexo插件如何实现了之后,我也开始着手我自己的PicGo-Core了。当然我并没有完全照搬hexo的实现,因为我发现那样的话不利于插件开发者开发插件(主要是语法提示),hexo的插件机制是暴露全局的hexo
变量去实现的。
PicGo-Core
的流程大概如下:
输入路径或者变量等->经过转换器转换->上传器上传->输出结果。中间包含着三个生命周期钩子。这样的话用户开发插件可以只实现其中的某个部分,也可以实现其中的某几个部分,来实现PicGo
原先不能实现的一些功能:
等等。
我也正式使用了TypeScript
作为PicGo-Core
的开发语言,使用起来一开始确实很不习惯,但是后来越用越顺手,学习新东西的过程大概都是这样吧!
在开发PicGo-Core
的过程中,我也做了很多除了上面流程系统之外的工作。比如:
PicGo-Core
加上了命令行支持,同样插件也能支持注册命令等操作。vue-cli2
和vue-cli3
对于模板生成的实现,写了一个下载模板、生成模板的命令init,好让插件开发者能够快速创建插件模板进行插件开发。npm
命令来下载插件。PicGo-Core
的文档,配图、示例一应俱全。开发完Node版本的PicGo-Core
之后,我还要将它和Electron版本的PicGo
整合起来,使得Electron版本的PicGo
也能拥有插件系统。并且还得通过ipcMain
等方式,将主进程的信息通知给渲染进程,从而渲染出插件页面里的插件列表:
为了让插件开发者能够更好地利用GUI版本的优势,我还为GUI版本的PicGo插件加了GUI插件特有的guiApi
、guiMenu
等功能:
这样插件拥有自己的菜单,可以执行自己的操作,那么能做的事就更多了,比如:
等等。。
终于,在2019年1月13号,PicGo迎来了2.0版本的更新。
回顾这些工作,都是我一个人在半年的时间里通过课余的时间做出来的,其实还是很自豪的。更关键的是,通过开放了插件系统,可以让更多的人参与到PicGo软件的完善中来,通过插件可以实现很多本体不提供或者不足的功能,也是让PicGo更加强大的一个条件。我也希望它日后也能形成自己的一个小生态。
实际上,PicGo-Core以及PicGo2.0发布之后,就已经有第三方开发者开发插件了,速度之快让我始料未及。为此我也迅速加上了Awesome-PicGo的仓库,这样能让更多的开发者的作品让用户看到:
你已经可以在VSCode里搜索PicGo,就能发现VSCode版的PicGo扩展了,实现了三种在Markdown里快速上传图片的方式:
2019年,我会更新几篇文章,主要讲讲如何实现一个插件系统,如何将Node端实现的插件系统整合到Electron端,如何实现一个模板下载、生成功能,如何实现良好的命令行交互等等。
2019年也是我找实习、找正式工作的一年,希望今年一切都顺利吧!
]]>距离上次更新(v1.6.2)已经过去了5个月,很抱歉2.0版本来得这么晚。本来想着在18年12月(PicGo一周年的时候)发布2.0版本,但是无奈正值研究生开题期间,需要花费不少时间(不然毕不了业了T T),所以这个大版本姗姗来迟。不过从这个版本开始,正式支持插件系统,发挥你们的无限想象,PicGo也能成为一个极致的效率工具。
除了发布PicGo 2.0本体,一同发布的还有PicGo-Core(PicGo 2.0的底层,支持CLI和API调用),以及VSCode的PicGo插件vs-picgo等。
PicGo的底层核心其实是PicGo-Core
。这个核心主要就是一个流程系统。(它支持在Node.js环境下全局安装,可以通过命令行上传图片文件、也可以接入Node.js项目中调用api实现上传。)
PicGo-Core
的上传流程如下:
Input
一般是文件路径,经过Transformer
读取信息,传入Uploader
进行上传,最后通过 Output
输出结果。而插件可以接入三个生命周期(beforeTransform
、beforeUpload
、afterUpload
)以及两种部件(Transformer
和Uploader
)。
换句话说,如果你书写了合适的Uploader
,那么可以上传到不同的图床。如果你书写了合适的Transformer
,你可以通过URL先行下载文件再通过Uploader
上传等等。
另外,如果你不想下载PicGo的electron版本,也可以通过npm安装picgo来实现命令行一键上传图片的快速体验。
PicGo除了PicGo-Core
提供的核心功能之外,额外给GUI插件给予一些自主控制权。
比如插件可以拥有自己的菜单项:
因此GUI插件除了能够接管PicGo-Core
给予的上传流程,还可以通过PicGo提供的guiApi等接口,在插件页面实现一些以前单纯通过上传区
实现不了的功能:
比如可以通过打开一个InputBox
获取用户的输入:
可以通过打开一个路径来执行其他功能(而非只是上传文件):
甚至还可以直接在插件面板通过调用api实现上传。
另外插件可以监听相册里图片删除的事件:
这个功能就可以写一个插件来实现相册图片和远端存储里的同步删除了。
通过如上介绍,我现在甚至就已经能想到插件系统能做出哪些有意思的插件了。
比如:
希望这个插件系统能够给PicGo带来更强大的威力,也希望它能够成为你的极致的效率工具。
需要注意的是,想要使用PicGo 2.0的插件系统,需要先行安装Node.js环境,因为PicGo的插件安装依赖npm
。
除了上面说的插件系统,PicGo 2.0还更新了如下内容:
base64
值的将会提升不少速度。比如SM.MS
图床等。而原本就通过base64
上传的图床速度不变。在PicGo-Core发布不久,就有人根据PicGo-Core的API编写了VSCode版的PicGo插件。使用起来也非常方便:
配置项与PicGo的图床的配置项基本保持一致。在VSCode插件栏搜索PicGo即可下载安装与体验!
PicGo第一个稳定版本是在少数派上发布的,详见PicGo:基于 Electron 的图片上传工具。支持macOS、Windows、Linux三平台,开源免费,界面美观,也得到了很多朋友的认可。如果你对它有什么意见或者建议,也欢迎在issues里指出。如果你喜欢它,不妨给它点个star。如果对你真的很有帮助,不妨请我喝杯咖啡(PicGo的GitHub首页有赞助的二维码)?
Windows用户请下载
.exe
文件,macOS用户请下载.dmg
文件,Linux用户请下载.AppImage
文件。
Happy uploading!
]]>网络迷踪——————————————Searching
Hi,各位好久不见!(最近在忙毕设开题的事,所以一直没办法按期完成推送。等忙过这一段就能大致恢复正常。)这部电影可以说是小成本制作的典范之作了。全片很有意思,大部分用的镜头来自手机、电脑的前置摄像头,然后配合电脑、手机屏幕的聊天记录、网页记录等来描述故事、展现角色心理状态。很多时候刚敲完的文字,然后想了想又删掉的光标;在屏幕前停留的视线等等都会让你身临其境——因为这些场景在我们当今的生活中,真的司空见惯。
可以说手机和电脑加上互联网已经占据了很多人一天的大部分。本片也聚焦在当前的网络环境下的人与人之间,父母和孩子之间的关系。我们经常会对父母隐藏自己的某一面,而在互联网上却又是另一副的面孔。所以很多时候本该最了解我们的人,却成了最熟悉的陌生人。当然,意外的惊喜是本片还加入了很不错的悬疑元素,真相大白的那刻,总算把你觉得不对劲的地方说了出来,但是却让你依然感觉很过瘾。好电影,值得一看!
奇迹男孩——————————————Wonder
Hi,各位好久不见!本周给大家推荐的是一部来自美国的《奇迹男孩》。从片名就可以看出是讲述一个小男孩的故事。温馨的故事很多,不过各有各打动人的地方。本片讲述的故事可能并没有什么出奇的地方,甚至你也有可能遇到类似的例子。影片中的的主要角色都有自己的一段独白戏。而从独白戏中,你才可以看到那些角色真实的自己。
就像行星绕着恒星转一样,我们的生活中也或多或少会围着某个人转。在关心他人的同时不得不遮盖自己的伤疤。但其实很多时候跟对方坦诚相待能获得更好的效果。要成为一个善良的人,要做善良的事。温馨的电影,值得一看~
谍影重重——————————————The Bourne Identity
Hi,各位好久不见!继上次看完《碟中谍6》之后,在经过舍友的推荐后我找来了另外一部讲述特工的电影《谍影重重》。跟《碟中谍》系列不同的是,《谍影重重》系列的男主角马特达蒙并没有阿汤哥那样帅到让你印象深刻。相反他一开始并不吸引人。
如果说《007》的看点是特工+美女,《碟中谍》的看点是阿汤哥的颜和拼命,那么《谍影重重》的看点就真的是一个特工的自我救赎了。我想推荐的并不是这一部电影,而是这整个系列(1、2、3、5部)。并且这里面每一部的水平、评分都很高。可以说是荧幕上「最为真实」的讲述间谍、特工的电影了。在这里面你是能真的学习到一些常人并不会特意关注到的细节。伯恩的招式可能没有那么华丽,但是是招招制敌,干净利落不拖泥带水,剧情的发展也是一波三折,紧凑而牵动人心。作为一部动作、悬疑电影我觉得虽然动作戏不如《碟中谍》那么华丽但是也已经足够帮。
其实第一部已经做得很出色,没想到第二部、第三部也同样出彩。好电影,值得一看。
走到尽头——————————————끝까지 간다
Hi,各位好久不见!本周给大家推荐的是一部来自韩国的《走到尽头》。这部电影我在3年前曾看过一次,不过最近重新又看了一遍依然感觉十分不错。
从影片一开始就开始就把观众带入非常紧张、刺激的情节,让人不由自主地为主角捏一把汗。而后的矛盾冲突又依然保持着高度的紧张和不突兀的幽默镜头。而随着剧情的推进,不断地反转也是让人看得很是过瘾。可以说是不停地用新的错误掩盖旧的错误。我想虽然电影有所夸张,但是现实中的我们却总会有类似的时刻。环环相扣的剧情在影片的最后达到高潮。开放式的结局也能让你思考良多。而比起我们的电影结局大多是阳光美好而言,这部电影的结局可以说带着一些黑色气息了。好电影,值得一看!
碟中谍6:全面瓦解——————————————Mission: Impossible - Fallout
Hi,各位好久不见!本周给大家推荐的是一部最近正在热映的电影《碟中谍6:全面瓦解》。动作片系列,我觉得如今只有《速度与激情》系列能与《碟中谍》系列比拼了。
阿汤哥依然是拼命三郎。本片全程无尿点。虽然剧情依然是跟核弹有关(哈哈)。不过不管是跳伞、飙车、开飞机甚至是「屋顶跑酷」都让人看得热血沸腾。22年了,阿汤哥依然是那个阿汤哥,不过当年看他电影的人已经长大了。熟悉的片头曲,琳琅满目的「黑科技」,剧情也是不停地反转反转。整部电影几乎一直处于神经紧绷的状态,让人看了大呼过瘾!
不知道还能再看到阿汤哥的碟中谍多少次,这部好电影,我想你一定要去看看。
游戏之夜——————————————Game Night
Hi,各位好久不见~本周给大家推荐的是来自英国的喜剧电影《游戏之夜》。听名字可能并不知道是什么意思,甚至有点「游戏人生」的感觉。但是看完之后却能把你笑得人仰马翻。这部结合了悬疑、犯罪的喜剧电影从分类上来说就让人忍俊不禁。其实同样类型的国内电影还有比如《唐人街探案》系列。不过我更加推荐这一部电影。
原因?原因在于这部电影的笑点和反转总是让你措手不及,反转会给你会心一击,笑点会让你笑掉下巴。我觉得一部喜剧电影的成功在于它能不用老梗把观众欢笑地送出电影院。而每部喜剧电影或多或少都会有些荤段子。有些电影处理地不好反而让人反感。而这部电影处理起来就让人看完很舒服。它不是什么很高内涵的电影,但是确是一部老少咸宜,适合一群人一起观看一起欢笑的好电影。值得一看~
华盛顿邮报——————————————The Post
Hi,各位好久不见。本周给大家推荐的,是来自美国的电影《华盛顿邮报》。我记得此前给大家推荐过《聚焦》,那部电影讲的是波士顿环球报的故事。而本片从片名上你就能看出来,讲述的是华盛顿邮报的故事。同样都是讲报社的电影,两部电影讲出了各自不同的风格,不过同样都很精彩。
本片基于真实事件改编,剧情总体并不复杂,讲述的是华盛顿邮报揭露美国当时的越战黑幕,与尼克松政府「对着干」的故事。如果说《聚焦》的风格是尽力的克制,那么《华盛顿邮报》的风格就是与之相反的锋芒毕露。美国当时深陷越战泥潭,而政府却把战局的节节败退告知公众于步步胜利。在明知打不赢这场战争的情况下还依然偷偷往越南派兵。如果没有有良知的记着冒死将机密文件从五角大厦里偷出,华盛顿邮报将其公之于众,恐怕越战还将继续持续很久。
不得不提的是报社与政府之间的较量。他们捍卫着新闻自由,捍卫着美国宪法赋予新闻工作者的权利。「报纸不应为统治者服务,而是应该为被统治者服务」。这部电影虽然讲述的故事发生在40多年前,但是对于当世而言依然有很强烈的警示作用。「如果纽约时报和我们(华盛顿邮报)输了,那么自由新闻才是真的输了。」可以很自然的想到,如果当初华盛顿邮报在于政府的禁令面前败下阵来,那么1年后的水门事件也将同样的被压下来。而曝光水门事件促使尼克松下台的,正是华盛顿邮报。
看完电影真的非常感动,感动于那些新闻工作者为了国家,为了社会,为了人民在努力追求真相,拼死把真相曝光。可恨在于我们的当下,没有质量、没有深入调查、没有核实来源的假新闻、谣言却铺天盖地。不管是前段时间的「汤兰兰事件」,还是「慈溪被害女生事件」等等新闻,都是为了增加曝光量,却没有考虑到当事人、当事方的感受的新闻。一味追求标题党,撒手把事情甩锅给其他人,让当事人当事方得花十倍百倍的力气去辟谣。这种新闻是可恨的。而那些追求真理,曝光真相的新闻,比如曝光疫苗案、比如毒奶粉案等等的调查记者们,却因为触动了某些人的利益,触动了它们脆弱的神经,遭到掩盖、封杀、甚至人身威胁。演变成现在,我们很多的新闻、很多的细节不得不通过微信公众号、微博截图等等才能看到第一手的材料。因为一旦晚了,就是「该内容已被发布者删除」「该内容因违规无法查看」。这里面也是鱼龙混杂,有的人为了坚持正义,发出的文章无奈被封,而自己的账号也被封禁;有的人为了蹭热点不惜一切代价做出煽情的文章,而变相输出一些谣言。然而人们在这里面获取到信息后通常容易出现广泛传播。当局的做法通常是不论真假一并封杀。
《中华人民共和国宪法》第二章第三十五条规定:中华人民共和国公民有言论、出版、集会、结社、游行、示威的自由。
然而今天的真相是我们的「言论自由」是有代价的,通常只要触犯到某些人的利益就会遭到全面的封杀。我不知道它们看过《华盛顿邮报》之后会怎么想,恐怕会很害怕吧。「宜疏不宜堵」「水能载舟亦能覆舟」的道理,小学生都知道。不知道那些口口声声说着「为人民服务」的人,为什么不懂呢。好电影,值得一看。
第十二人——————————————Den 12. mann
Hi,各位好久不见!好久没给大家推荐战争类型的电影了。本次给大家推荐的是一部来自挪威的电影,讲述了一个12人的小队,最终只有第12个人生还的故事。这部电影最震撼的就是片头说的「这个故事里,最令人难以置信的是,确有其事。」。
跟敦刻尔克一样,这部电影讲述的不是消灭了多少德军,而是讲述了如何生还(或者「逃跑」)的故事。这个故事讲述的虽然是「一个人」,但是实际上讲的是一群人的故事。「我不是英雄,那些帮助了我的人才是英雄」。这部电影伟大之处在于给予一路上帮助主角的人足够多的镜头和描写。为了帮助主角逃生,很多人甚至会为此付出生命的代价。
这部取景很「冷」的电影,在冲破国境线的那一刻却格外地热血沸腾。好电影,值得一看。
我不是药神——————————————我不是药神
Hi,各位好久不见!本周给大家推荐的是刚上映的大热门《我不是药神》。其实光看名字和海报的时候,我以为只是徐峥的一部常规喜剧电影。然而自从点映以来就有不少朋友给我推荐。于是今天也去电影院看了,发现确实值得上豆瓣9.0的分数。「我们也拍出了韩国那样的电影」。这是我看完感慨最深的一点。
在审查严苛、国情如此的情况下我们还能拿出一部直击社会问题,反映社会现实和矛盾,并让不少人由衷落泪的电影,真的非常不容易。其实从前年的《湄公河行动》、去年的《战狼2》、今年的《红海行动》之后,我很害怕我们国家以后的「好」电影都只能是这类主旋律的动作片了。我们有《心迷宫》、《暴裂无声》等质量上乘的悬疑电影,也有逗男女老少开心一笑的《泰囧》、《夏洛特烦恼》等优质喜剧片等等。但是我们缺的是直击社会问题,挖掘人性的剧情电影。我们少了多少《辩护人》、少了多少未见的《熔炉》、少了多少难得的《Taxi Driver》。今天一部《我不是药神》让我看到了中国电影的未来还是有希望的。这部基于真实事件改编的电影,从影片一开始就会让你有种深深代入感——因为你也是千千万万中国人之一,这就会是发生在你身边的事。
不管是配乐、剪辑还是情节的把控,导演和主演们都让我们感到了深深的负责和认真。该给的镜头一个不少,该有的细节一个不缺,该哭的泪点一个不落。你知道这将是中国电影的一个里程碑式的电影么,这么棒的电影真的值得你去一看。
寻梦环游记——————————————Coco
Hi,各位好久不见。本周给大家推荐的是来自皮克斯的动画电影《寻梦环游记》。这部电影在当时上映的时候获得了很高的评价。不过我直到最近才看了它。不得不说确实是一部很赞的电影。关于这部电影有个有趣的段子,引进的时候,由于把审片的人都看哭了,所以本来是不能上映的亡灵题材的电影也过审了。暂且不考虑这个说法的真实性,从这个段子里你也能看出这部电影的硬实力确实很强。
这部是一部关于音乐,梦想和爱的电影。故事的构思很巧妙,背景设置在墨西哥也是别有一番风味。电影给我们营造了一个及其绚丽的亡灵世界。在这里大部分的生活是快乐的,不过也有令人揪心的问题——如果在人间没有人还记得你的话那么你将会在亡灵世界里消逝。这点真的非常赞,完美诠释了那句话:「人会死三次,第一次是在他停止呼吸的时候 ,从生物学上说他死了,他失去了思考的能力;第二次是在他下葬的时候,人们来参加他的葬礼,怀念他的过往和人生,然后在社会上他死了,活着的世界里不再会有他的位置;第三次是世界上最后一个记得他的人把他忘记的时候,那时候他才能算是真正的死了,永远的死了。」这个设定也带来了剧情的里的矛盾点和发展点。后续的情节铺开张弛有度,有情理之中也有意料之外。感动人心,好电影值得一看~
燃烧——————————————버닝
Hi,各位好久不见。本周给大家推荐的是今年韩国一部大热的电影《燃烧》。这部电影并不是一部容易读懂的电影。改编自村上春树的《烧仓房》,不过导演也为这部电影注入了很多自己的思想。(建议可以看看《烧仓房》,是部短篇小说)这部电影的节奏比较缓慢,很多细节是慢慢地又完整地展现在你面前。而电影里最让人困惑,或者最烧脑,或者说加入了作者最深入的思考的部分,却又是那些可有可无的「线索」。它们有些到最后都没有得到导演给出的解释。开放式的结局,甚至开放式的剧情都是这部电影非常让人难以缓过神来的地方。而平淡地讲述故事的同时,也有着「最美之舞」的唯美画面。
聚焦着当下韩国年轻人的痛点,在讲述一场可疑的案件的同时,又让你不得不思考,自己活着是一个「little hunger」还是一个「greate hunger」。而为何有些人,莫名其妙地,年纪轻轻就成了「盖茨比」。不平等的阶层注定不平等的追求。好电影,值得一看。
国际市场——————————————국제시장
Hi,各位好久不见。本周给大家推荐的是来自韩国的《国际市场》。初看这部电影的名字,并不能看出什么名堂。不过这部电影类似于《阿甘正传》一样,描述了一部韩国的现代史。从6·25事件(可以认为是朝鲜战争全面爆发)开始,一直延续到如今。
这是目前韩国影史票房第二的电影。能获得如此巨大的成功,我想除了过硬的演员素质(黄晸玟、金允珍、吴达洙等实力派演员),编剧和导演对于情节穿插的到位把控,还有就是唤起了很多韩国人对于朝鲜战争过后,韩国从无到有,从落后朝鲜到超过朝鲜的那段历史的回忆。这部电影以主角德秀的成长作为主线,对父亲的承诺,对妹妹的愧疚,对姑姑的感恩等等。并不断加入一些暗藏的线索或者说「彩蛋」,在整体氛围是催人泪下的情况下,还能掺杂着不少喜剧的成分,非常具有这几年韩国高分电影的风格。
影片末尾的那句,「但是爸,我真的好累啊」让我不禁落泪。这部电影能直击你内心最容易被触动的角落,好电影,值得一看。
暴烈无声——————————————Wrath of Silence
Hi,各位好久不见!本周给大家推荐的是来自《心迷宫》导演的第二部佳作《暴烈无声》。好吧依然是一部从名字里看不出说啥的电影。有了《心迷宫》的成功铺垫,《暴烈无声》在经费上也算是大大地改善了。而且宋洋、姜武等实力派演员的出演也是给这部电影添色不少。
不过相比心迷宫的烧脑,这部电影打出的宣传语是:「烧脑,更烧心」。是的,这部电影虽然是一部悬疑片,但是不仅仅是一部悬疑片。导演其实是想借着这部电影,隐喻现实中的三类人。一类是在社会底层摸爬滚打,有苦说不出,有难没处诉的人;一类是生活过得还可以,努力工作努力养家糊口的中产阶级;一类是生活在社会顶层,物质生活富裕,但是依仗钱、权背地里做些违法乱纪之事的人。
我们常听「邪不压正,正义或许会迟到,但绝不会缺席」。不过看完这部电影,你会发现其实还是有不少的事,正义的缺席带给你我的,是多么无助,多么的无力。暴烈无声,拳头能换来的,是说不出的绝望。好电影,值得一看。
至暗时刻——————————————Darkest Hour
Hi,各位好久不见。这段时间确实比较忙,一直拖更我表示非常愧疚。本周给大家带来的是一部来自英国的电影《至暗时刻》。别看名字好像阴森恐怖,但是实际上它并不是一部恐怖电影。它是一部讲述英国著名首相丘吉尔的电影。
丘吉尔在面对重重困难,做出了一系列后世看来非常正确的决策。但是鲜有人知这些决策背后的故事。如果你知道敦刻尔克大撤退,那么你未必知道为了让敦刻尔克的30万英法联军撤回英国,为了多争取时间,牺牲了临近的一个英国旅(4000人);你未必知道当时的情况下,征用民船已经是无奈之举,而且几乎是千钧一发之际才正好赶上撤军;你也许知道丘吉尔爱抽雪茄,但是你未必知道丘吉尔还嗜酒,个性分明……
这部电影里,加里·奥德曼把丘吉尔演绎得惟妙惟肖,十分令人印象深刻。也因此大家都在说他要因为出演丘吉尔这个角色拿到奥斯卡小金人了。好电影,值得一看!
1987:黎明到来的那一天——————————————일구팔칠
Hi,各位好久不见!本周给大家推荐的一部电影是国内豆瓣都「没有办法出现」的韩国电影《1987:黎明到来的那一天》。因为某些原因这部电影在国内被封杀,所以我也不好说得太多。去看看吧,好电影值得一看。
头号玩家——————————————Ready Player One
Hi,各位好久不见!本周给大家推荐的是最近影院的大热门《头号玩家》。这部电影的豆瓣评分有点「虚高」,不过不可否认确实是一部非常棒的科幻电影。这是斯皮尔伯格送给年轻人、玩家、动漫迷们一份最好的礼物。
和过往的科幻电影有所不同的是,故事发生在不远的未来,不过科技并没有发展到「变态」的程度。所以电影里的很多东西,包括VR都是在现有的基础上进行的升华。而营造出来的虚拟世界无疑是最吸引眼球的。有人说这部电影是一部彩蛋里插播电影的电影。确实这部电影里彩蛋特别多,但是不用担心,没有人会真的了解所有的彩蛋,所以哪怕你并不关心游戏、动漫、电影,你也能在电影院感受两个半小时的视听盛宴。
大概最感动的地方就是遇到你所认识、你所熟知的角色、游戏在电影中的一闪而过。你会想起当年在家里打红白机、小霸王的那个年代,你会想起当年守在电视机前只为等待一部好看的动画片的自己。这部电影想要告诉你的也是一样——那些在你脑海中挥之不去的,那些回忆,那些童年才是最真实的。现实世界终究是追求自由追求真实的,在虚拟世界里再如何成功也不过是过眼烟云,现实中的伙伴,生活才是你最应该珍惜的。好电影,值得去一看。
启示录——————————————Apocalypto
Hi,各位好久不见!这部电影是来自公众号的粉丝推荐的一部好电影。很开心来一起分享它!这部电影的背景是玛雅文明衰落时期的故事,但是实际上有些情节又与当时鼎盛的阿兹特克文明有所重合。不过抛去历史背景,这个发生在热带雨林里的故事,却是惊心动魄,让人叹为观止。
影片的节奏松紧有度。开篇的情节把主要的人物性格、特点都印刻在观众的脑海中。而到了中途,就开始了震撼的追逐。如果从片名里直译的《启示录》里你看不到大致的情节的话,台译版的《阿波卡猎逃》可能就会让你的肾上腺素有所提升。不过如果你读过或者知道圣经里的《启示录》的话,那么这个题目真是太恰当不过了。一场文明的毁灭与新的文明的重生。这是一部让你无法忘却的电影,「文明」世界带去的文明,无非也是野蛮的征服。好电影,值得一看。
红海行动——————————————红海行动
Hi,各位好久不见!前不久我刚去看了最近大热的《红海行动》。记得当初也给大家推荐过《湄公河行动》和《战狼2》。不得不说这两年来我们自己拍出来的战争、动作片的水准是越来越高了。本片的导演也是《湄公河行动》的导演林超贤。可以说自《湄公河行动》后的两年,真的是卷土重来。并且带来了质量更好,水平更高,更加真实而震撼的场面。
本片根据真实事件改编,还原度相当高。不仅影片出现的枪械、装备、坦克等都非常写实,而且一些镜头例如汽车炸弹、精密狙击、迫击炮狂轰滥炸等等都有很强的视觉冲击。而最震撼人心的,还有出现的很多「血腥」的场景——以往在国产电影里被剪掉无法搬上荧幕的战争的一些遗体、残骸。而整体剧情也非常紧凑,从头到尾都无尿点啊。而海陆空全面的镜头也让人大呼过瘾。同时,反战的主题也深入人心啊。好电影,值得一看!
弱点——————————————The Blind Side
Hi,各位好久不见,新年快乐呀!趁新年还未完全过去,赶紧来给大家推荐每周一部的好电影,拖更了好久哈哈。本周给大家推荐的是一部来自美国的温情电影《弱点》。不过我一直觉得翻译有问题,翻译成「盲点」应该更好点。
这部电影是根据原著《The Blind Side: Evolution of the Game》改编的电影,而原著的原型也是来自于真实的故事。所以说这部电影的真实感让人非常感动——片中的人大多都超出你的想象的好。与大部分的电影不同的是,它的矛盾点、冲突点特别少。虽然在一些细节的处理上有些过快,不过能够在两个小时里塞进一个橄榄球传奇球员从默默无闻青年时代到最后脱颖而出的选秀状元,可以说是真的很不容易了。我在看的时候一直在惯性思考着等着导演「耍把戏」,不过从头到尾都非常地温馨,非常的动人。
它是一部能够打动你的泪腺的电影,一部好电影,献给新年的第一部推荐。
抓住那个家伙——————————————몽타주
Hi,各位好久不见!本周依然给大家推荐一部有悬疑色彩的犯罪电影。这部来自韩国的电影从一开始就让我们感到一丝伤感。导演很擅长用暗色调渲染这种压抑而忧伤的气氛。所以从一开始我们就逐渐掉进这个陷阱中了。
整体上电影是双线并进,两条时间线互相交错,以至于到最后汇合的时候碰撞出的火花恐怕要让你拍案叫绝。两起案件,两个真相。和里面的警察一样,我们大多数人都会被眼前的“证据”蒙蔽,相信一些说不过去的“真相”。而很多事情是需要推敲,需要冷静的。
受害者也是加害者,这样的双重身份在电影里互相交织,让人性这个词又得以从电影剧本里脱颖而出。事实上,这部电影也是一部关于人性的思考,关于悔过的思考。好电影,值得一看~
目击者追凶——————————————目擊者
Hi,各位好久不见!本周给大家推荐的是一部来自台湾的悬疑电影《目击者追凶》。我记得去年我曾推荐过一部大热的西班牙悬疑电影《看不见的客人》,精妙的剧本,峰会路转的剧情让很多人拍案叫绝。而这部来自宝岛台湾的电影也不逊色。
明线、暗线的堆叠,从一开始就埋下的伏笔。导演在一些细节的处理,比如一些闪回的镜头上做的是真的不错的。一次次小规模的反转铸就了最后令人吃惊而毛骨悚然的结局。这部电影里可以说基本没有“好人”。恐怕唯一的好人就是可怜的阿吉了。而所有其他出现在镜头里的主要角色,都有着外表和内在不同的反差。这也是这部电影可圈可点的地方。
而一部好的悬疑电影自然是到最后一刻才会完美收官。本片也不例外,片末的“鬼故事”,实在是画龙点睛之笔。好电影,值得一看。
无问西东——————————————Forever Young
Hi,各位好久不见!本次给大家推荐的是一部时隔5年重见天日的电影——《无问西东》。这部电影本来是打算献礼给清华大学建校100周年的,因为某些不可知的原因,一再推迟到如今才能上映。剧中的主演也都在这5年中结婚生子,变化之大也令人感慨。
既然是献礼给清华大学的电影,剧中自然少不了清华大学的身影。电影分成了4段故事来讲述。每段时间内发生的故事都有鲜明的时代特色。而4个故事之间的关联,也在影片“不经意”之间透露出来。而这样的安排也引起了一堆的不认可。然而我却觉得这样的安排非常棒。和云图的前世今生相比,这样的安排不仅更加贴近真实而且更加动人。
而每个故事里的主人公的演技我认为在当时,哪怕放到现在也都是非常不错的。就像豆瓣里有人说的,“最棒的王力宏和非常好的黄晓明”。两个小时,4个故事,横跨100年。这样庞大的题材,虽然导演确实在某些细节上处理的有些牵强,不过依旧不改这部电影交出的高分答卷。应对人生的选择,人声的苦难,你该如何继续?如果你没有看过《南渡北归》,你无法明白当年西南联大有多么不容易,当年的那些大家能够给本科生上课是有多么珍贵。电影最后给出的一个个那些年顶尖的学者,真是满满的感动和自豪。无问西东,砥砺前行。好电影,值得一看。
三块广告牌——————————————Three Billboards Outside Ebbing, Missouri
Hi,各位好久不见!忙完考试之后终于有时间来写本周的电影推荐了。本周给大家推荐的是去年底在美国上映(中国将在18年3月上映)的电影《三块广告牌》。
在这部充满美式幽默和美式愤怒的电影里,看到了久违的不靠煽情而让你动容的一部电影。不过我认为,真正让这部电影拥有如此好评的原因在于两点:
这部是一部讲述愤怒与善良,爱与恨的电影。愤怒不能解决问题,但是爱可以。影片塑造的多个人物都具有两面性——这也是本部电影最棒的地方。没有一个人是可以用好或者不好来形容的。每个人都有自己的阳光和阴暗面——而一开始我们不免进入了导演给我们设置的俗套。而随着剧情的发展你才会发现这一切都不是那么简单。“坏”警长其实不坏——相反还非常受人敬仰,“烂”警察其实不烂——相反他还自损三千地只为抓犯人等等。
而在被套路或者反套路的同时,你也逐渐了解到美国社会的诸多矛盾以及人的诸多美好品质。愤怒不能解决问题,但是善良与爱是可以的。好电影,值得一看。
我能说——————————————아이 캔 스피크
Hi,各位新年快乐~转眼之间一周一部好电影已经来到了第5个年头。前四个年头通过一周一部好电影我已经推送了186部各种主题,各种类型,各种风格的电影,希望新的一年里能够有更多的好电影能够分享给各位~
本周给大家推荐的电影是一部去年韩国的电影《我能说》。一部能用喜剧的形式来讲述“慰安妇”这个沉痛主题的电影,真的无不佩服编剧的功力以及演员的水平。两个主演的表演真的太动人了。
这部电影能分成两部分。前半部分以喜剧为主,讲述一个“鬼怪奶奶”的各种“鬼怪”行径。而前半部分用尽努力“掩盖”的欢乐,在后半段会被导演“无情”地打碎,同时打碎的还有观众的泪腺。而同时引出的是这部电影的主题啊——那些被人们忽略的受害者们,那些无法说出,不愿说出,不想说出自己曾经经历的受害者们。她们真的需要更多我们的关怀和帮助。国内今年的《二十二》也是同样的主题。不同的角度,不过都是同样的出发点和同样的愿望——日本政府的一句道歉。好电影,值得一看。
]]>赶巧前不久也有一个开发者chyingp的开源项目破了1000star,也有着类似的文章,祝贺!
我以前写博客的时候,由于一开始用的是七牛的图床,所以遇到要在markdown里贴图的时候就必须登录七牛,然后手动上传图片,再找到按钮来复制链接,然后复制到markdown里。要在markdown里显示一张图片我得经过上述4个步骤。
我自己的笔记本用的是mac,所以我就接触到了一款叫做iPic的图床神器。在用它的时候我也知道了微博图床。iPic的功能和体验真的特别好。不过如果需要使用七牛等其他图床的时候,我就需要付费了。其实如果iPic支持windows的话,我可能就真的去付费了。(因为实验室的电脑是windows,所以我平时在实验室里得用windows来写东西)。仅为mac一个平台付费我有点不能接受。
于是我就在想能否我自己写一个工具来简化我的上传图片的流程呢,这个应用可以实现拖拽图片就上传,然后上传完自动复制链接到剪贴板里,我就只要粘贴到markdown里就好了。一开始想用swift写mac的应用,用C#写windows的应用。后来发现工作量不仅大,而且学习成本也很高。于是最后还是选择投入electron的怀抱。
一开始不选择electron主要是因为我的印象中electron的应用体积都挺大的(100MB以上),所以感觉体积可能有点不友好。不过后来我在用了electron-vue
打包出来后发现体积是可以接受的范围(mac端大概50M,windows端大概38M),于是就决定用它来写了。用electron-vue
的主要原因是我写vue比较多,想要学习成本低一些,这样开发只要学electron的部分就好了。
说干就干,在去年年底(11月下旬)我开始写这个应用。
文末会给出electron-vue开发的系列经验教程
经过20多天的间断性地开发,我在12月12号发布了1.0.0版本。由于一开始是在mac上开发的,所以1.0.0版本也只支持了macOS。一开始支持的图床也不多,只支持了微博和七牛两个图床。
1.0.0版本的截图如下:
基本实现了我预期的功能,类似iPic能够通过拖拽到顶部栏图标上传。并且为了今后支持windows平台(windows平台的任务栏图标不支持拖拽事件),我就做了一个主窗口,在主窗口里也有拖拽上传的区域。因为有了主窗口,我就顺便把图床的配置也放到了主窗口里。
应用做出来了,我也想让更多的人用到。于是我在北邮人论坛、cnode、v2ex还有掘金都发了文章。不过一开始看到的人寥寥无几,发了文章也没多少人看到和使用。后来我在少数派上发了同样的文章,意外地被推荐到了首页。
这次的契机让PicGo意外地有了些用户和star数。在跟使用者交流的过程中我也开始逐步往PicGo里加功能和修复bug。在1月10日的时候,PicGo更新v1.3.1版本支持了windows系统。
因为开始有用户了,PicGo早期确实存在着不少功能的缺失,比如快捷键上传
,其他常用图床的缺失等等。所以那时候是PicGo迭代最快的一段时间。通过大家在issue里的反馈,我也在不断打磨PicGo。可以看到截止6月14日,已经有61个被关闭的issue了。
用户体验这个东西真的并不是开发者在开发的时候能够立马就想到的。这点在开发PicGo上我体会很深。
比如增加快捷键上传
这个功能,我一开始觉得自定义快捷键写起来比较麻烦,干脆我定一个大家基本用不到的快捷键吧。于是我默认给了一个【command/ctrl+shift+p】的快捷键。我自己用的时候没有什么问题。结果有人给我反馈说,快捷键跟某某某软件冲突了,能否给一个快捷键自定义的功能。这是我无法回避的一个问题。于是我就开始去学习如何加入自定义快捷键。并在不久之后实现了个这个功能。
比如自定义链接格式的问题。我一开始给了大家4种复制链接的格式,分别是markdown
、HTML
、URL
、UBB
。本来以为这4种格式就足够大家平时使用了。后来有人提了一个issue,问PicGo能否自定义链接格式,因为他想基于HTML增加一些属性,比如大小居中等。我觉得这个使用场景确实是有的,于是我便在后来的某个提交里实现了这个功能。
当然并不是大家有这个需求我就一定要做。还有一些需求我觉得并不符合我对于PicGo的定位的,那么我就会给予回绝。比如后期能否支持上传视频文件?,由于PicGo的开发初衷只针对图片,所以在流程上(图片->base64)就不允许上传视频文件。于是我拒绝了这个需求。
还有一个对我以及PicGo这个项目影响深远的issue,ZetaoYang提出了一个想法:
这个建议改变了我对PicGo开发的后续想法。我思考了好久,发现确实一步步增加默认的图床支持是不长远的。一个是重复性劳动太多(图床上传除了协议和加密方式不同之外,接收文件,转成base64和最后上传成功后存到本地的流程是一样的),一个是无止尽的图床支持其实也不应该。相比之下,把PicGo做成一个Core+Plugin模式的应用会更好。其中Core的部分可以单独只做图片接收和转码,并预留一些生命周期,供上传过程中不同的需求来调用。Core的部分可以单独发布成一个npm包。Plugin可以实现接入Core的生命周期,可以实现自己的上传逻辑,可以实现图片压缩、加水印等等其他功能。而PicGo只是在Core+Plugin的基础上套了一层electron的皮方便普通用户使用,而Core和Plugin可以独立拆出方便开发者使用和开发。这个也是PicGo的2.0版本将要做的事。
在开发PicGo的过程中我也深刻了解到,写一个DEMO不难,给这个DEMO注入你自己的思想和灵魂是难的。PicGo从一个一开始只是我想简化上传图片流程的玩具应用,发展到现在已经是不少用户的效率工具而言,其实一路走来也并不容易。现在大家对用户体验的要求越来越高,如果只沉醉在自己的DEMO里无法自拔,只会被更好的产品所淘汰。
开发PicGo也是一件很开心的事。大家给予我的赞赏和感谢,都是给我继续开发的动力。而我也发现越来越多的文章里,都提到了PicGo。如下:
我想,得到你们的认可,把它写进你们的文章,这是对我最大的肯定,这个比star数更令我感到开心。
我在开发PicGo的过程中,也写了一个系列文章Electron-vue开发实战。如果你也想学习electron或者electron-vue的开发的话,希望我的文章能够给你带来帮助。如果你之前没有听说过PicGo,那么不妨试试;如果你觉得它挺好用的,不妨点个star~
]]>VSCodeVim
这个插件严重拖慢了我的开发效率。本来用Vim
模式难道不应该是提高效率么?问题是在Normal
模式下,光标的移动会有肉眼可见的长延时。比如我按着j
,等我松开j
后,光标还在移动,而且还移动了一会儿。预期的效果应该是按下移动,松开停止。为此我查了一下相关issue,发现跟我一样的情况的人还不少。(不过也有不少人没有这个问题,貌似跟显卡有关系?我的mac是集显的)。卸载了VSCodeVim
之后,光标移动的速度又恢复了正常,不过没有Vim
模式的话非常别扭。所以我就开始看看VSCode还有没有其他Vim
模式的插件。于是我又试了另外两个插件:vimStyle和amVim。最终我选择了后者。不仅是支持的Vim命令更多,还有就是开发者的维护一直在继续。而且很关键的一点,amVim
的光标移动体验就是 如丝般顺滑 !
不过它有个让我很不习惯的地方:不支持:
号调起VSCode的Command Line
窗口,实现诸如:w
保存,:wq
退出等常见功能。这些功能在VSCodeVim
里是支持的。于是我就在想有没有办法「移植」一下VSCodeVim
的功能到amVim
来,既能保持光标移动体验顺滑,又能用上Command Line
的一些常用命令。所以开启了魔改模式,并在跟开发者的一系列交流后最终我提交的PR被merge了。
本文记录一下我第一次对VSCode插件(修改)开发的过程。
VSCode的插件通常是用TypeScript
来写的。如果你需要开发或者修改它,先要拥有TypeScript
的开发环境。
1 | npm install -g typescript |
通常TypeScript
的项目都会用上tslint
。所以你也最好全局安装它:
1 | npm install -g tslint |
然后打开VSCode,安装一下tslint
这个插件,它将通过我们上面安装在系统里的tslint
给我们的项目提供代码检查。
修改别人的插件,可以先fork
一份别人的代码。也为了之后方便提PR做准备。
然后就可以把插件clone
到本地了。比如本文的amVim-for-VSCode。
用VSCode打开这个项目,点击左侧的debug
可以看到一个launch extension
的配置:
运行它,你会得到另外一个窗口,这个就是可以调试插件功能的窗口了:
我的改进源码在这里:https://github.com/Molunerfinn/amVim-for-VSCode 作者合并之后做了一些修改,本文是以我的版本为主。
为了实现VSCodeVim
通过:
调起VSCode的inputBox
效果,我需要翻阅一下VSCodeVim
的源代码。
大致效果如下:
在查看了amVim
和VSCodeVim
在实现命令上的部分源码后,发现二者的实现上差距还是不小的。不过相比VSCodeVim
代码的庞大(甚至还有neoVim的支持),amVim
在实现上就比较精巧了。
在我的PR未被merge之前,amVim
插件提供了一个功能,按:
打开一个GoToLine
的inputBox
:
不过只能用于输入数字并跳转到相应行数。好在查看release更新日志,追溯这个commit,我们可以很容易找到它是如何实现的。
代码不多,就几行:
1 | // src/Modes/Normal.ts |
具体实现代码如下:
1 | // src/Actions/Command.ts |
所以是通过vscode
的commands
来打开的gotoLine
的inputBox
窗口。
再来看看VSCodeVim
是如何打开inputBox
的:
1 | // src/cmd_line/commandLine.ts |
可以看到关键的部分是通过vscode.window.showInputBox
打开的inputBox
。所以我也根据这个关键的入口来一步步实现我想要的功能。
参考VSCodeVim
的实现,在amVim
里可以大概分四个部分:
src/Modes/Normal.ts
作为入口文件,当用户输入:
键时触发后续功能。【已有】src/Actions/CommandLine/CommandLine.ts
作为打开inputBox
的入口函数,打开inputBox
,然后负责把用户输入的内容传给下一级的parser
,用于解析并执行相应命令。src/Actions/CommandLine/Parser.ts
,负责接收上一级传进来的命令,然后找到命令对应的函数,并执行该函数。如果找不到相应则返回。src/Actions/CommandLine/Commands/*
,存放各个命令的实现函数。其中src/Actions/CommandLine/CommandLine.ts
的逻辑跟VSCodeVim
的src/cmd_line/commandLine.ts
非常类似。
1 | import * as vscode from 'vscode'; |
1 | import { CommandBase } from './Commands/Base'; |
由于命令很多,我就举三个例子。一个是w
,一个是q
,和一个wq
。VSCode自己的一些功能比如关闭当前文件、保存文件等都是有自己的command的。在实现Vim模式的时候,实际上最后也是去调用VSCode自带的功能而已。
1 | import * as vscode from 'vscode'; |
1 | import * as vscode from 'vscode'; |
1 | import { CommandBase } from './Base'; |
这一步就很有意思了,因为我们之前实现了Write
和Quit
的功能,所以可以在这里调用它们。看到这里你可能会有问题,虽然我知道VSCode有这些功能,但是你是怎么知道这些功能是怎么写的呢?
如果只是我这篇文章的话,我在实现Vim模式的这些命令的时候,大部分是参考了VSCodeVim
的一些写法。它主要的命令实现在src/cmd_line/commands/*
里。但是只这样显然还是不够的。因此我给出几个比较有用的地方供大家开发插件的时候参考:
说实话VSCode的文档写得不是特别好。我要实现一个功能,查找文档查了半天。其实其中很大一部分操作,你可以在上面的第3点、第4点里通过快捷键的提供的Command id
去实现:
比如你要实现一个剪切的功能,有了Command id
,你就可以通过vscode.commands.executeCommand('editor.action.clipboardCutAction')
来实现。因此我推荐,如果你要实现的功能有些可以用已有快捷键实现的,那么就能在这个列表里找到对应的Command id
来手动实现了。
至于其他的一些非快捷键提供的功能,就还需要阅读第2点的api文档做出更深层次的修改了。
在改进完这个插件之后,我向作者提交了PR。在和作者交流后做出了一些修改,并最终被作者接受并合并。为开源项目贡献代码的感觉是真的很不错。
]]>注: 要开发微信二维码支付,你必须要有相应的商户号的权限,否则你是无法开发的。若无相应权限,本文不推荐阅读。
打开微信支付的文档,我们可以看到两种支付模式:模式一和模式二。这二者的流程图微信的文档里都给出了(不过说实话画得真的有点丑)。
文档里指出了二者的区别:
模式一开发前,商户必须在公众平台后台设置支付回调URL。URL实现的功能:接收用户扫码后微信支付系统回调的productid和openid。
模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。注意:code_url有效期为2小时,过期后扫码不能再发起支付。
模式一是我们平时在网购的时候比较常见的,会弹出一个专门的页面用于扫码支付,然后支付成功后这个页面会再次跳转回回调页面,通知你支付成功。第二种的话想对少一些,不过第二种开发起来相对简单点。本文主要介绍模式二的开发。
快速搭建Koa2的开发环境我推荐可以使用koa-generator。脚手架能帮我们省去Koa项目一开始的一些基本中间件的书写步骤。(如果你想学习Koa最好自己搭建一个。如果你已经会Koa了就可以使用一些快速脚手架了。)
首先全局安装koa-generator
:
1 | npm install -g koa-generator |
然后找一个目录用来存放Koa项目,我们打算给这个项目取个名字叫做koa-wechatpay
,然后就可以输入koa2 koa-wechatpay
。然后脚手架会自动创建相应文件夹koa-wechatpay
,并生成基本骨架。进入这个文件夹,安装相应的插件。输入:
1 | npm install |
接着你可以输入npm start
或者 yarn start
来运行项目(默认监听在3000端口)。
如果不出意外,你的项目跑起来了,然后我们用postman测试一下:
这条路由是在
routes/index.js
里。
如果你看到了
1 | { |
就说明没问题。(如果有问题,检查一下是不是端口被占用了等等。)
接下来在routes
文件夹里我们新建一个wechatpay.js
的文件用来书写我们的流程。
跟微信的服务器交流很关键的一环是签名必须正确,如果签名不正确,那么一切都白搭。
首先我们需要去公众号的后台获取我们所需要的如下相应的id或者key的信息。其中notify_url
和server_ip
是用于当我们支付成功后,微信会主动往这个urlpost
支付成功的信息。
签名算法如下:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
为了签名正确,我们需要安装一下md5
。
1 | npm install md5 --save |
1 | const md5 = require('md5') |
然后开始写签名函数:
1 | const signString = (fee, ip, nonce) => { |
其中fee
是要充值的费用,以分为单位。比如要充值1块钱,fee
就是100。ip是个比较随意的选项,只要符合规则的ip经过测试都是可以的,下文里我用的是server_ip
。nonce
就是微信要求的不重复的32位以内的字符串,通常可以使用订单号等唯一标识的字符串。
由于跟微信的服务器交流都是用xml来交流,所以现在我们要手动组装一下post请求的xml
:
1 | const xmlBody = (fee, nonce_str) => { |
如果你怕自己的签名的
xml
串有问题,可以提前在微信提供的签名校验工具里先校验一遍,看看是否能通过。
因为需要跟微信服务端发请求,所以我选择了axios
这个在浏览器端和node端都能发起ajax请求的库。
安装过程不再赘述。继续在wechatpay.js
写发请求的逻辑。
由于微信给我们返回的也将是一个xml格式的字符串。所以我们需要预先写好解析函数,将xml解析成js对象。为此你可以安装一个xml2js。安装过程跟上面的类似,不再赘述。
微信会给我们返回一个诸如下面格式的xml
字符串:
1 | <xml><return_code><![CDATA[SUCCESS]]></return_code> |
我们的目标是转为如下的js对象,好让我们用js来操作数据:
1 | { |
于是我们写一个函数,调用xml2js
来解析xml:
1 | // 将XML转为JS对象 |
上面的代码返回了一个Promise
对象,因为xml2js
的操作是在回调函数里返回的结果,所以为了配合Koa2的async
、await
,我们可以将其封装成一个Promise
对象,将解析完的结果通过resolve
返回回去。这样就能用await
来取数据了:
1 | const axios = require('axios') |
然后我们要将这个router挂载到根目录的app.js
里去。
找到之前默认的两个路由,一个index
,一个user
:
1 | const index = require('./routes/index') |
然后到页面底下挂载这个路由:
1 | // routes |
于是你就可以通过发送/api/pay
来请求二维码数据啦。(如果有跨域需要自己考虑解决跨域方案,可以跟Koa放在同域里,也可以开一层proxy来转发,也可以开CORS头等等)
注意, 本例里是用前端来生成二维码,其实也可以通过后端生成二维码,然后再返回给前端。不过为了简易演示,本例采用前端通过获取code_url
后,在前端生成二维码。
前端我用的是Vue
,当然你可以选择你喜欢的前端框架。这里关注点在于通过拿到刚才后端传过来的code_url
来生成二维码。
在前端,我使用的是@xkeshi/vue-qrcode这个库来生成二维码。它调用特别简单:
1 | import VueQrcode from '@xkeshi/vue-qrcode' |
然后就可以在前端里用<vue-qrcode>
的组件来生成二维码了:
1 | <vue-qrcode :value="codeUrl" :options="{ size: 200 }"> |
放到Dialog里就是这样的效果:
文本是我自己添加的
有两种将支付成功写入数据库的办法。
一种是在打开了扫码对话框后,不停向微信服务端轮询支付结果,如果支付成功,那么就向后端发起请求,告诉后端支付成功,让后端写入数据库。
一种是后端一直开着接口,等微信主动给后端的notify_url
发起post请求,告诉后端支付结果,让后端写入数据库。然后此时前端向后端轮询的时候应该是去数据库取轮询该订单的支付结果,如果支付成功就关闭Dialog。
第一种比较简单但是不安全:试想万一用户支付成功的同时关闭了页面,或者用户支付成功了,但是网络有问题导致前端没法往后端发支付成功的结果,那么后端就一直没办法写入支付成功的数据。
第二种虽然麻烦,但是保证了安全。所有的支付结果都必须等微信主动向后端通知,后端存完数据库后再返回给前端消息。这样哪怕用户支付成功的同时关闭了页面,下次再打开的时候,由于数据库已经写入了,所以拿到的也是支付成功的结果。
所以付款成功自动刷新页面
这个部分我们分为两个部分来说:
Vue的data部分
1 | data: { |
在methods里写一个查询订单信息的方法:
1 |
|
在打开二维码Dialog的时候,这个方法就启用了。然后就开始轮询。我订了一个时间,200s后如果还是没有付款信息也自动刷新页面。实际上你可以自己根据项目的需要来定义这个时间。
前端到后端只有一个接口,但是后端有两个接口。一个是用来接收微信的推送,一个是用来接收前端的查询请求。
先来写最关键的微信的推送请求处理。由于我们接收微信的请求是在Koa的路由里,并且是以流的形式传输的。需要让Koa支持解析xml格式的body,所以需要安装一个rawbody来获取xml格式的body。
1 | // 处理微信支付回传notify |
这里的坑就是Koa处理微信回传的xml。如果不知道是以raw-body
的形式回传的,会调试半天。。
接下来这个就是比较简单的给前端回传的了。
1 | const checkBill = async (ctx) => { |
至此,一整个基于Koa2的微信二维码支付流程就简单演示完了,由于不是公开的项目,所以没有实际的GitHub仓库。不过基本上关键的代码我都已经注释出来啦。我参考了不少人的实现,曾考虑过用一些比如wechatpay
的npm库,不过最终还是自己解决了。这里面感谢很多前人的分享,也希望我这篇文章能给你一些帮助。
微信支付文章
https://www.itbaby.me/blog/59e21af45d21b31fcd4e02c6
https://juejin.im/post/5a8e84faf265da4e7e10c92f
返回接口
XML流处理
]]>有人说这两个模式其实是一个模式。我想这句话的对错对半分吧。它们有类似的地方,不过也不能说完全一致。先来一张图,这张图解释了观察者模式
和发布订阅模式
在流程上的一些区别:
左边是观察者模式,右边是订阅发布模式。
简单阐述二者的模型:
观察者模式里,观察者(Observer)直接订阅(subscribe)主题(Subject),而当主题被激活的时候,会触发(fire)观察者里的事件。
订阅发布模式里,订阅者(Subscriber)通过监听(on)事件总线(Event Bus)里的事件,当事件总线里的事件被触发(emit)的时候,订阅者将会执行相应的操作。而这里需要注意的是,事件总线里的事件是通过发布者(Publisher)进行发布(publish)和 通知事件总线 触发 的。
注:事件总线也有说法叫为调度中心。本质上是一样的。不过因为写Vue时候习惯用Event Bus来说了,所以本文的调度中心皆以事件总线称呼。
所以事件总线本身不独自发布和触发事件,它会借由发布者来操作。这是跟观察者模式有着比较大的区别的地方。
当然只看这两张图和上面的解释,应该还是无法很好的理解。下面这张图能把流程讲得更清楚点。
这个例子可以理解为这样:左边是微信里的微商-顾客
之间的关系。右边是商家-淘宝-顾客
之间的关系。
观察者模式:顾客关注了微商的商品,微商会记住顾客关注的商品,一旦上新就直接 私聊 通知所有关注这个商品的顾客。这里的顾客就相当于观察者,这里的微商就相当于主题。
订阅发布模式:顾客通过淘宝(APP或者网站)关注了商家的商品,商家一旦上新就通过淘宝(APP或者网站)向关注了它的顾客 群发 消息。这里的顾客就是订阅者,这里的淘宝就是事件总线,这里的商家就是发布者。
所以可以看出,观察者模式的模型跟发布订阅模型里,差距就差在有没有一个中央的事件总线。如果有这个事件总线,我们就可以认为是个发布订阅模型。如果没有,那么就可以认为是个观察者模型。因为其实它们都实现了一个关键的功能:发布事件-订阅事件并触发事件。
下面用代码简单解释一下。
由于最近在学习TypeScript,所以下面的代码也会用TypeScript来书写。
我们先写一个定义观察者和主题的文件。
1 | // observer-pattern.ts |
于是在调用的时候,是这样调用的:
1 | import * as op from './observer-pattern' |
经过上述调用,subjects触发观察者订阅的click事件,observer.subject
的值将会变为Done
(原先为click
)。
接下来我们来实现一些订阅发布模式。订阅发布模式最关键的地方就在于中间的Event Bus
部分。它接管着事件总线的订阅和发布。
1 | // pubsub.ts |
可以看出在这里的EventBus
和观察者模式里的Subject
几乎一致对吧。但是需要注意的是,最后一行里,我们export default new EventBus()
,所以我们在项目里不同的地方import
它,都会指向同一个Event Bus
实例,这样的话就可以起到一个事件总线的作用了。它不在乎谁来监听,谁来发布。只要有人监听了,就把它放进监听队列中。只要有人发布了事件,就从相应的监听队列中触发回调。不过所有相关的事件都必须经过Event Bus
这个实例,而不能越过它直接由发布者通知监听者。
再次祭出这张图
所以在订阅发布模型里,发布者或者订阅者的身份已经被弱化。发布者可以在任何时候发布事件,而订阅者可能只是一个回调函数。而最关键的事件总线部分,则是发布订阅模型的核心。
如果你用过Vue的Event Bus
,相信不会陌生。接下来我们来用用我们刚才写的简单的Event Bus
。
1 | import bus from './pubsub.ts' |
所以你可以看到,这个事件总线是可以单独抽离出来的。如果要把我们这个文件丢到一个现有的项目里也是完全没问题的。
其实在写Vue组件通信的时候,你如果用到了Event Bus
的话,也是一样的。在全局声明一个new Vue()
做Event Bus
总线,然后在不同的组件里只要引入了这个事件总线,就能订阅或者发布不同的消息。这个就是一个非常典型的订阅发布模型。
而如果只是Vue的父子组件通信,子组件用的是this.$emit
来触发事件,父组件用的是this.$on
这样的方式去订阅事件,那么你可以认为这个就是一个简单的观察者模型。因为它们之间的联系是紧密耦合的。
不管是观察者模式也好,订阅发布模式也好,关键在于实现了在某个特定时间触发某个特定事件,从而触发监听这个特定事件的组件进行相应操作的功能。这个设计模式在很多时候非常有用。平时只是用到了它,但是没有深入去看看如何实现,这次借由这个机会把二者的关系和区别记录下来,也算是给自己加深了印象。
本文的代码你可以在我的学习仓库FE-Learning找到。如有错误欢迎指出!
https://www.zcfy.cc/article/observer-vs-pub-sub-pattern-hacker-noon
http://blog.zxbing0066.com/design-patterns/2016/09/12/observer-pattern.html
https://juejin.im/post/5af05d406fb9a07a9e4d2799
https://www.cnblogs.com/weebly/p/5279952.html
]]>一个进程好比是一个程序,它是 资源分配的最小单位 。同一时刻执行的进程数不会超过核心数。不过如果问单核CPU能否运行多进程?答案又是肯定的。单核CPU也可以运行多进程,只不过不是同时的,而是极快地在进程间来回切换实现的多进程。举个简单的例子,就算是十年前的单核CPU的电脑,也可以聊QQ的同时看视频。
电脑中有许多进程需要处于「同时」开启的状态,而利用CPU在进程间的快速切换,可以实现「同时」运行多个程序。而进程切换则意味着需要保留进程切换前的状态,以备切换回去的时候能够继续接着工作。所以进程拥有自己的地址空间,全局变量,文件描述符,各种硬件等等资源。操作系统通过调度CPU去执行进程的记录、回复、切换等等。
如果说进程和进程之间相当于程序与程序之间的关系,那么线程与线程之间就相当于程序内的任务和任务之间的关系。所以线程是依赖于进程的,也称为 「微进程」 。它是 程序执行过程中的最小单元 。
一个程序内包含了多种任务。打个比方,用播放器看视频的时候,视频输出的画面和声音可以认为是两种任务。当你拖动进度条的时候又触发了另外一种任务。拖动进度条会导致画面和声音都发生变化,如果进程里没有线程的话,那么可能发生的情况就是:
拖动进度条->画面更新->声音更新。你会明显感到画面和声音和进度条不同步。
但是加上了线程之后,线程能够共享进程的大部分资源,并参与CPU的调度。意味着它能够在进程间进行切换,实现「并发」,从而反馈到使用上就是拖动进度条的同时,画面和声音都同步了。所以我们经常能听到的一个词是「多线程」,就是把一个程序分成多个任务去跑,让任务更快处理。不过线程和线程之间由于某些资源是独占的,会导致锁的问题。例如Python的GIL多线程锁。
协程在线程中实现调度。你可以理解为它是 「微线程」 。它的调度不来自于CPU,而是完全来自于用户控制(可以理解为用代码控制流程)。协程的执行效率非常高,它的切换不是线程切换,没有线程切换的开销。而且只要线程越多,协程的性能优势就越明显。协程不需要多线程的锁机制,只需要判断状态即可。不过协程本身无法利用多核CPU,因为它基于线程,而线程又依赖于进程。
在JS里,常见的协程就是ES6的yield Generator
或者ES7的async await
。我们知道JS引擎是单线程的。所以在处理异步任务队列的时候,以往我们会陷入「回调金字塔」或者「回调地狱」。而有了协程之后我们可以在代码层面上来控制我们的程序。
比如我们有这么一个需求,等两个请求都返回之后,用它们的返回值共同做些事。(此处不用Promise.all()
来实现,不是说不行,而是为了更好地说明主题)
ES6 + co 的写法:
1 | const axios = require('axios') |
ES7 的写法:
1 | const axios = require('axios') |
上述用「同步」的方式写的代码实际上依然是异步执行的。不过因为了有协程,在单线程的JS里也能够让我们在代码层面上实现了任务调度。
可以说三者虽然是不同的东西,但是有着很密切的关系和类似的特性。它们的关系是从大到小,从上而下的。没有进程也就没有线程也就没有协程。总的来说,在多核处理器的情况下,多进程+多协程可以发挥最优的性能。
早先PicGo所支持的图床基本上都是属于国内的服务商提供的图床(如七牛、腾讯云COS等),这次更新加入了GitHub图床的支持。用GitHub做图床其实是不少写博客的朋友的做法。免费、原生支持HTTPS、GitHub仓库易于管理、和issue等功能无缝衔接都是它的优点。如果能接受GitHub在国内的访问速度不是特别快的缺点的话,用它来做你的图床是个不错的选择。来看看在PicGo里如何配置它:
1. 首先你得有一个GitHub账号。注册GitHub就不用我多言。
2. 新建一个仓库
记下你取的仓库名。
3. 生成一个token用于PicGo操作你的仓库:
访问:https://github.com/settings/tokens
然后点击Generate new token
。
把repo的勾打上即可。然后翻到页面最底部,点击Generate token
的绿色按钮生成token。
注意:这个token生成后只会显示一次!你要把这个token复制一下存到其他地方以备以后要用。
4. 配置PicGo
注意:仓库名的格式是用户名/仓库
,比如我创建了一个叫做test
的仓库,在PicGo里我要设定的仓库名就是Molunerfinn/test
。一般我们选择master
分支即可。然后记得点击确定以生效,然后可以点击设为默认图床
来确保上传的图床是GitHub。
至此配置完毕,已经可以使用了。当你上传的时候,你会发现你的仓库里也会增加新的图片了:
在支持腾讯云COS的路上,我可谓是费了一番心血。首先是官方提供的node-sdk对我来说基本属于瘫痪状态,只能上传具体文件而不能上传base64编码后的文件。而且居然还有v4和v5两个版本的COS,甚至两个版本的认证签名、上传url等等都完!全!不!同!。由于之前我只有v4版本的COS权限,只能开发和测试出v4版本的上传。而近来发现很多朋友用的都已经是v5版本的了,所以我提交了一个工单向腾讯云申请了v5版本的权限,没想到很快就给我派发权限了。于是就有了v5版本的面世。目前市面上能同时支持v4、v5版本COS的估计也只有PicGo了!
如果你是v5用户,但是之前下载了PicGo却不能用的话,别担心,v1.5版本的配置跟之前的配置几乎一致,而且可以一键切换v4\v5版本。
1. 获取你的APPID、SecretId和SecretKey
访问:https://console.cloud.tencent.com/cam/capi
2. 获取bucket名以及存储区域代号
访问:https://console.cloud.tencent.com/cos5/bucket
创建一个存储桶。然后找到你的存储桶名和存储区域代号:
v5版本的存储桶名称格式是bucket-appId
,类似于xxxx-12312313
。存储区域代码和v4版本的也有所区别,v5版本的如我的是ap-beijing
,别复制错了。
3. 选择v5版本并点击确定
然后记得点击设为默认图床
,这样上传才会默认走的是腾讯云COS。
有些时候可能上传的图片的url事后需要更改,比如修改http到https,比如加上一些操作后缀(例:七牛图床支持的?imgslim
)等等。PicGo本次的更新也让你能够更方便地管理你的图片库。
PicGo总共有三种上传模式:
其中前两种都是可以明确获得文件名,而第三种无法获取文件名(因为剪贴板里有些图片比如截图根本就不存在文件名),所以PicGo此前采取的规则是使用时间戳来命名剪贴板里的图片。这也导致了无法自定义文件名的问题。本次更新你可以选择开启「上传前重命名」这个选项:
之后你在上传的时候就会弹出一个小窗口让你重命名文件。如果你不想重命名,点击确定、取消或者直接关闭这个窗口都是可以的。如果你想要重命名就在输入框里输入想要更改的名字,然后点击确定即可。另外这个特性也支持批量上传,如下:
在主窗口的上传区,你可以直观地看到当前默认上传的图床,再也不用到处找当前的默认图床是哪个啦。
很多时候你并不会使用上PicGo给你提供的全部的图床。所以为了精简显示你可以只选择你想要的图床来显示,这样侧边栏也就不会出现滚动条了。不过需要注意的是,这个仅仅是显示/隐藏而并不是剔除相应的功能。假如你隐藏了七牛云,你依然是可以通过七牛云来上传图片的。
如果你觉得每次开机要主动开启PicGo是一件麻烦事,不妨试试让它开机自启吧~
v1.5不光更新了上述功能,也修复了不少问题。其中一个尤为重要的是从v1.4.1开始的一个bug——macOS的menubar无法拖拽上传。该bug也在这个版本被修复。
PicGo第一个稳定版本是在少数派上发布的,详见PicGo:基于 Electron 的图片上传工具。支持macOS和windows双平台,开源免费,界面美观,也得到了很多朋友的认可。本次更新也是充分聆听了大家的意见。如果你对它有什么意见或者建议,也欢迎在issues里指出。如果你喜欢它,不妨给它点个star或者请我喝杯咖啡(PicGo的GitHub首页有赞助的二维码)?
Windows用户请下载
.exe
文件,macOS用户请下载.dmg
文件。
Happy uploading!
]]>前段时间,我用electron-vue开发了一款跨平台(目前支持Mac和Windows)的免费开源的图床上传应用——PicGo,在开发过程中踩了不少的坑,不仅来自应用的业务逻辑本身,也来自electron本身。在开发这个应用过程中,我学了不少的东西。因为我也是从0开始学习electron,所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历,用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。
预计将会从几篇系列文章或方面来展开:
PicGo
是采用electron-vue
开发的,所以如果你会vue
,那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如react
、angular
,那么纯按照本教程虽然在render端(可以理解为页面)的构建可能学习到的东西不多,不过在main端(electron的主进程)应该还是能学习到相应的知识的。
如果之前的文章没阅读的朋友可以先从之前的文章跟着看。
经过前面几篇文章的实战,我相信大家已经对于构建一个基本的electron应用没有太多的问题了。本文主要阐述一下如何让我们的应用通过CI系统来自动帮我们构建应用,然后发布给用户使用。以及之后如果有更新,要如何通知用户更新。
当然,在此之前,我们还需要做一件事:给你应用加上好看的LOGO。LOGO的设计和制作不在本文的设计范围内。为了我们的应用能够跨平台地使用,不同平台上应用的LOGO尺寸和格式也不尽相同。三个平台所需的图片格式如下:
准备一张1024*1024以下,256*256以上(长宽一致)的png图片,(推荐512 * 512)然后我们可以用一些工具来实现从png到其他两种格式。搜索png转ico或者png转icns的话有很多在线转换的网站,可以去上面在线转换。在mac上我推荐用的是image2icon这个工具。
然后我们将所得的三个图片文件,放到electron-vue项目根目录的build/icons/
目录下。
本文我们主要采用electron-vue已经配置好的基于electron-builder的构建脚本来进行我们的应用构建。构建脚本会读取package.json
里的build
字段里的配置来进行构建。electron-vue默认的配置如下:
1 | "build": { |
简单讲述一下build配置里的一些字段的含义。
首先productName
是你的应用的名字。appId
的作用是用于Windows平台区分应用的标识。(注意该配置必须配置,而且稍后会有使用该配置的地方。如果不配置不使用的话,构建出来的Windows平台的应用将无法发送eletron的桌面通知)dmg
这个配置里描述了macOS平台里,打开dmg
安装包后显示的界面里的信息。如下图:
表示了有两个标识,一个是应用文件,坐标是(130, 150)
, 一个是应用文件夹的快捷方式,坐标是(410, 150)
。
directories
的output
字段是你应用打包完生成的文件放置的目录。
files
指明了要打包的目录。
而mac
,win
,linux
是针对三个平台的不同的配置了。可以看出默认的配置里对它们的配置都是指向了不同的icon图标(也就是上一节所说的LOGO)。
PicGo在实际开发中,针对一些情况对默认的build
配置项做出了一些增改:
1 | "build": { |
由于PicGo在macOS上主要是一个顶部栏应用,所以在底部docker栏我并不想拥有一个占位的图标,所以在mac
字段里加入了:
1 | "extendInfo": { |
这个属性。参考相关issue。
在Windows平台上,默认打包出来的安装包并没有办法选择安装的路径,只会默认装到C盘的用户目录。这个并不是我们想要的。我们想要的是让用户自己选择安装的路径。
所以需要修改windows
的一些配置以及加上一个nsis
的配置来实现:
1 | "win": { |
由于目前我还没有打包过Linux平台的应用,所以Linux相关的配置暂时先不做修改。
还记得前面说到的一个配置:appId
么,这个配置需要我们在主进程index.js
里也要使用。否则打包后的应用将失去Windows平台的应用通知功能。这个appId
是可以任意取的,只要保证不和其他应用重复即可。对于PicGo而言,appId
是com.molunerfinn.picgo
。
打开你的main/index.js
,在Windows平台的时候加上这个appId
:
1 | // ... |
这样就解决了通知的那个问题。
发布应用其实是一个比较繁琐的活,往往跟你的版本控制绑在一块,所以通常在项目开始的阶段就要有所布局。我说说我的做法吧,不一定很科学,不过简单易行。
其中简单的更新版本的脚本我是在package.json
里写了简单的scripts
:
1 | "scripts": { |
里面用到了npm的一个命令,npm version [options]
,具体可以参考version的文档。简单来说,它能够自动帮你升级版本,修改package.json
里的version,并打上相应的git tag,很方便。
举个例子,一个符合语义的版本号通常由如下三个部分组成:major.minor.patch
,比如1.5.3
。如果我运行了npm run patch
,那么将会将小版本更新:1.5.4
,同时修改package.json
里的version
字段为1.5.4
并自动打上一个git tag 1.5.4
,并将这个修改和tag推送到远端。
不过需要注意的是,一开始我是通过electron-vue自带的npm run build
这个脚本让CI去执行构建,但是发现无法自动上传到GitHub的release里。所以通过查阅相关资料后,发现最简单的就是把对应的npm scripts命名为release
。于是我把原本的npm run build
的脚本复制了一遍,起了一个新名release
:
1 | "scripts": { |
说到这里都还没说到CI系统。什么是CI?可以参考阮一峰老师给出的解释《持续集成是什么?》。我们如果每次发布应用都需要我们在本地构建,然后手动上传到GitHub(或者其他地方)去,然后让别人能下载的话,未免太累了。而且通常我们开发electron应用就是为了能够跨平台,但是要构建不同的平台的应用意味着我们要在不同的平台分别构建。这也是不能忍受的。
于是网上有一些第三方的CI系统,它们能够帮我们,在某些分支(比如master)发生了某些更新(比如更新了tag)的时候帮我们执行某些脚本(比如构建、测试)。这样就省却了我们在本地、多平台构建的烦心事,而且让一些都变得「自动化」了起来。
有了CI之后,我的electron应用的发布就变成这样的流程了:
这样,我们只需要Push代码足矣。
针对Linux或者macOS的构建,我们可以使用Travis-CI,针对Windows平台的构建,我们可以使用AppVeyor。一个好消息是,它们对于在GitHub上的开源项目都是可以免费构建的,并且和GitHub的仓库结合地特别好,配置也比较简单,可以说的非常良心了。
在使用它们之前,我们需要给予它们一定的权限让它们能够访问我们的GitHub仓库。所以需要:
.travis.yml
和AppVeyor的配置文件模板appveyor.yml
。所以我们基本上只需要在它们的基础上小修改即可。注册并登录Travis-CI后,找到你要构建的仓库,然后打开,点击设置进入如下页面:
配置一下环境变量,名为GH_TOKEN
,token的值就是上一步我们在GitHub生成的token。等会会有用。
PicGo经过修改后的.travis.yml
如下:
1 | # Commented sections below can be used to run tests on the CI server |
抛去很多前置依赖(比如C++编译库之类的)和构建环境(是什么系统,是什么语言),那些都是electron-vue给我们预置好的。我们需要注意的仅仅是几个部分:
script
是当系统和环境和依赖都准备好之后,你要CI运行的命令。在这里我运行了两个命令,一个是npm run release
,这个就是打包构建应用啦,并且执行了这个命令之后,electron-builder
会自动将生成好的安装包推送到我们GitHub仓库的draft release里。另一个是构建PicGo主页的命令yarn run build:docs
。
branches
声明了你要在哪些分支在GitHub接收到了代码更新之后就构建,这里我们自然选择的是master。
after_script
是当你执行完script里的脚本之后要做的事。可以为空。对于我而言主要在这个部分将上一步构建好的PicGo主页推送到GitHub的gh-pages
分支。当然如果你的应用有使用说明、文档之类的网站,也可以在这里进行构建和推送。
注意到,在after_script
命令的最后一行,有个${GH_TOKEN}
,这个就是我们之前在Travis-CI配置里配置的环境变量GH_TOKEN
。用环境变量的好处是不会暴露你的TOKEN,只有构建系统知道。
有了之前的经验,AppVeyor就更简单了。注册登录后,我们在主页添加一个PROJECT,选中你要构建的仓库。然后找到SETTING设置:
然后在左侧的Genral
一栏的内容区中,找到构建的分支为master,以及设置我们仅在tag
更新的时候构建:
当然这个都是根据项目实际来的配置,我只是说PicGo的项目是这样配置的。
然后在左侧的Environment
区,找到环境变量配置,我们依然写入GH_TOKEN
:
修改完配置都别忘了拉到底部去保存!
这样就算配置完了网页端的。而现在我们来看看appveyor.yml
这个配置文件:
1 | # Commented sections below can be used to run tests on the CI server |
依然是只需要关注我们所关心的配置即可。一个是branches
,一个是build_script
。有了Travis-CI
的.travis.yml
的经验,我相信你也能很快理解它。
经过上述配置之后,你已经实现了一个简单的前端工程的自动化构建推送流程了。而今你只需要关注代码提交,应用的构建都将会由CI系统自动帮你完成。当然CI系统也不仅仅是拿来构建electron应用的,正如你所见的,你能想到的其他项目的构建、测试其实它都能帮你通过预定义好的脚本完成。
当CI构建玩应用,会将其推送到你的GitHub的release页面成为一个draf
(草稿),你可以编辑这个草稿,加上标题和更新说明,就可以点击publish
发布你的新版本的应用啦。
electron应用的自动更新其实社区有很好的解决方案electron-updater。而electron-vue也在主进程的main/index.js
里预先帮我们写好了一段注释的代码:
1 | // import { autoUpdater } from 'electron-updater' |
只要引入autoUpdater就能自动帮我们检查更新和自动下载安装更新。不过,凡事都有不过。这个方式虽然很简单,但是它需要的条件比较严格,需要你拥有证书用于应用签名。而macOS平台下的证书需要你申请开发者,一年99$的费用让我望而却步。
于是我只能退而求其次,能不能通过查询GitHub的release版本号,来比对当前版本,是否需要更新,并提醒用户呢?经过尝试,发现可行。我的实现方法如下:
我首先写了一个updateChecker
的助手:
1 | import { dialog, shell } from 'electron' |
然后在main/index.js
里,我在app准备启动的时候,调用这个更新助手:
1 | // ... |
这样就能在启动应用的时候弹出更新提示:
本文简要地讲述了electron应用用上CI系统帮我们自动化构建和推送,以及在没有申请开发者,没有证书用于应用的代码签名的情况下如何告知用户进行应用更新。要做一个健壮的应用就应该考虑到应用的版本发布、版本更新和对用户的更新通知。
本文很多都是我在开发PicGo
的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的electron-vue
开发带来一些启发。文中相关的代码,你都可以在PicGo的项目仓库里找到,欢迎star~如果本文能够给你带来帮助,那么将是我最开心的地方。如果喜欢,欢迎关注我的博客以及本系列文章的后续进展。
]]>注:文中的图片除未特地说明之外均属于我个人作品,需要转载请私信
/
的页面呢这个问题是很多初学者会问的问题,于是结合我自己的学习经历也来简单的讲解一下这二者的区别与联系,希望能对你们有所帮助。
老手可以绕道,去看些更有用的文章吧~
理解Web路由这篇文章讲得特别好了。
在Web开发过程中,经常会遇到『路由』的概念。那么,到底什么是路由?简单来说,路由就是URL到函数的映射。
访问的URL会映射到相应的函数里(这个函数是广义的,可以是前端的函数也可以是后端的函数),然后由相应的函数来决定返回给这个URL什么东西。路由就是在做一个匹配的工作。
在web开发早期的「刀耕火种」年代里,一直是后端路由占据主导地位。不管是php,还是jsp、asp,用户能通过URL访问到的页面,大多是通过后端路由匹配之后再返回给浏览器的。经典面试题,「你从浏览器地址栏里输入www.baidu.com
到你看到网页这个过程中经历了什么」其实讲的也是这个道理。
在web后端,不管是什么语言的后端框架,都会有一个专门开辟出来的路由模块或者路由区域,用来匹配用户给出的URL地址,以及一些表单提交、ajax请求的地址。通常遇到无法匹配的路由,后端将会返回一个404
状态码。这也是我们常说的404 NOT FOUND
的由来。
如果你关注RESTful API,那么将会很熟悉下面四种发起请求的类型:GET
,POST
,PUT
,DELETE
。
它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。 ——来自阮一峰《理解RESTful架构》
虽然上面说的是RESTful API,但是实际上我们在地址栏输入一个URL,并回车的时候,是以GET
请求发出去的。这也体现了,URL地址和请求的method也应该是一一对应。下面给出一个例子:
1 | router.post('/user/:id', addUser) |
假如我的后端路由配置里只有这一句路由。那么我通过浏览器里访问:http://xxx.com/user/123
的话是无法访问到的,也会返回一个404。因为后端只配了一个post
方法的路由。如果要接受这个请求,那么必须有如下的路由:
1 | router.get('/user/:id', getUser) // 配置get路由 |
前面说了,「刀耕火种」的年代里,网页通常是通过后端路由直出给客户端浏览器的。也就是网页的html一般是在后端服务器里通过模板引擎渲染好再交给前端的。至于一些其他的效果,是通过预先写在页面里的jQuery、Bootstrap等常见的前端框架去负责的。
如果你说有些网站已经是通过ajax去实现的页面,比如gmail,比如qq邮箱。那么你要注意到哪怕是这些页面,它们页面的「龙骨」也并非是全部通过ajax去实现的,依然还是后端直出——这也就是我们现在又老生常谈的服务端渲染。
服务端渲染的好处有很多,比如对于SEO友好,一些对安全性要求高的页面采用服务端渲染是更保险的。而在当时还没有node.js的年代,为了良好地构建前端页面,都是通过服务端语言对应的模板引擎来实现动态网页、页面结构的组织、组件的复用。比如Laravel的blade,用在Django上的jinja2,用在Struts的jsp等等。实际上到如今,一门后端语言想要能实现自己的web功能,都需要有自己对应的模板引擎。
node.js诞生之后,前端拥有自己的后端渲染的模板引擎也成为了现实。常见的比如pug、ejs、nunjucks等。这些模板引擎搭配Express、Koa等后端框架也在一开始风靡一时。
不过在这个过程中,随着web应用的开发越来越复杂,单纯服务端渲染的问题开始慢慢的暴露出来了——耦合性太强了,jQuery时代的页面不好维护,页面切换白屏严重等等。耦合性问题虽然能通过良好的代码结构、规范来解决,不过jQuery时代的页面不好维护这是有目共睹的,全局变量满天飞,代码入侵性太高。后续的维护通常是在给前面的代码打补丁。而页面切换的白屏问题虽然可以通过ajax、或者iframe等来解决,但是在实现上就麻烦了——进一步增加了可维护的难度。
于是,我们开始进入了前端路由的时代。
前端路由——顾名思义,页面跳转的URL规则匹配由前端来控制。而前端路由主要是有两种显示方式:
#
号不好看#
号,好看。缺点是既需要浏览器支持也需要后端服务器支持前端路由应用最广泛的例子就是当今的SPA的web项目。不管是Vue、React还是Angular的页面工程,都离不开相应配套的router工具。前端路由带来的最明显的好处就是,地址栏URL的跳转不会白屏了——这也得益于前端渲染带来的好处。
讲前端路由就不能不说前端渲染。我以Vue项目为例。如果你是用官方的vue-cli
搭配webpack模板构建的项目,你有没有想过你的浏览器拿到的html是什么样的?是你页面长的那样有button
有form
的样子么?我想不是的。在生产模式下,你看看构建出来的index.html
长什么样:
1 |
|
通常长上面这个样子。可以看到,这个其实就是你的浏览器从服务端拿到的html。这里面空荡荡的只有一个<div id="app"></div>
这个入口的div以及下面配套的一系列js文件。所以你看到的页面其实是通过那些js渲染出来的。这也是我们常说的前端渲染。
前端渲染把渲染的任务交给了浏览器,通过客户端的算力来解决页面的构建,这个很大程度上缓解了服务端的压力。而且配合前端路由,无缝的页面切换体验自然是对用户友好的。不过带来的坏处就是对SEO不友好,毕竟搜索引擎的爬虫只能爬到上面那样的html,对浏览器的版本也会有相应的要求。
需要明确的是,只要在浏览器地址栏输入URL再回车,是一定会去后端服务器请求一次的。而如果是在页面里通过点击按钮等操作,利用router库的api来进行的URL更新是不会去后端服务器请求的。
hash模式利用的是浏览器不会对#
号后面的路径对服务端发起路由请求。也即在浏览器里输入如下这两个地址:http://localhost/#/user/1
和http://localhost/
其实到服务端都是去请求http://localhost
这个页面的内容。
而前端的router库通过捕捉#
号后面的参数、地址,来告诉前端库(比如Vue)渲染对应的页面。这样,不管是我们在浏览器的地址栏输入,或者是页面里通过router的api进行的跳转,都是一样的跳转逻辑。所以这个模式是不需要后端配置其他逻辑的,只要给前端返回http://localhost
对应的html,剩下具体是哪个页面,就由前端路由去判断便可。
不带#
号的路由,也就是我们通常能见到的URL形式。router库要实现这个功能一般都是通过HTML5提供的history这个api。比如history.pushState()
可以向浏览器地址栏push一个URL,而这个URL是不会向后端发起请求的!通过这个特性,便能很方便地实现漂亮的URL。不过需要注意的是,这个api对于IE9及其以下版本浏览器是不支持的,IE10开始支持,所以对于浏览器版本是有要求的。vue-router会检测浏览器版本,当无法启用history模式的时候会自动降级为hash模式。
上面说了,你在页面里的跳转,通常是通过router的api去进行的跳转,router的api调用的通常是history.pushState()
这个api,所以跟后端没什么关系。但是一旦你从浏览器地址栏里输入一个地址,比如http://localhost/user/1
,这个URL是会向后端发起一个get请求的。后端路由表里如果没有配置相应的路由,那么自然就会返回一个404了!这也就是很多朋友在生产模式遇到404页面的原因。
那么很多人会问了,那为什么我在开发模式下没问题呢?那是因为vue-cli
在开发模式下帮你启动的那个express
开发服务器帮你做了这方面的配置。理论上在开发模式下本来也是需要配置服务端的,只不过vue-cli
都帮你配置好了,所以你就不用手动配置了。
那么该如何配置呢?其实在生产模式下配置也很简单,参考vue-router给出的配置例子。一个原则就是,在所有后端路由规则的最后,配置一个规则,如果前面其他路由规则都不匹配的情况下,就执行这个规则——把构建好的那个index.html
返回给前端。这样就解决了后端路由抛出的404的问题了,因为只要你输入了http://localhost/user/1
这地址,那么由于后端其他路由都不匹配,那么就会返回给浏览器index.html
。
浏览器拿到这个html之后,router库就开始工作,开始获取地址栏的URL信息,然后再告诉前端库(比如Vue)渲染对应的页面。到这一步就跟hash模式是类似的了。
当然,由于后端无法抛出404的页面错误,404的URL规则自然是交给前端路由来决定了。你可以自己在前端路由里决定什么URL都不匹配的404页面应该显示什么。
虽然前端渲染有诸多好处,不过SEO的问题,还是比较突出的。所以react、vue等框架在后来也在服务端渲染上做着自己的努力。基于前端库的服务端渲染跟以前基于后端语言的服务端渲染又有所不同。前端框架的服务端渲染大多依然采用的是前端路由,并且由于引入了状态统一、vnode等等概念,它们的服务端渲染对服务器的性能要求比php等语言基于的字符串填充的模板引擎渲染对于服务器的性能要求高得多。所以在这方面不仅是框架本身在不断改进算法、优化,服务端的性能也必须要有所提升。当初掘金换成SSR的时候也遇到了对应的性能问题,就是这个原因。
当然在二者之间,也出现了预渲染的概念。也即先在服务端构建出一部分静态的html文件,用于直出浏览器。然后剩下的页面再通过常用的前端渲染来实现。通常我们可以把首页采用预渲染的方式。这个的好处是明显的,兼顾了SEO和服务器的性能要求。不过它无法做到全站SEO,生产构建阶段耗时也会有所提高,这也是遗憾所在。
关于预渲染,可以考虑使用prerender-spa-plugin这个webapck的插件,它的3.x版本开始使用puppeteer来构建html文件了。
得益于前端路由和现代前端框架的完整的前后端渲染能力,跟页面渲染、组织、组件相关的东西,后端终于可以不用再参与了。
前后端分离的开发模式也逐渐开始普及。前端开始更加注重页面开发的工程化、自动化,而后端则更专注于api的提供和数据库的保障。代码层面上耦合度也进一步降低,分工也更加明确。我们也摆脱了当初「刀耕火种」的web开发年代。撒花~
希望通过此文能够让你对于前后端路由和前后端渲染有所了解。在实际开发的过程中,也不应该仅仅关注于自己所在的领域,相关的领域也要有所涉猎,这样才能面对问题游刃有余。
]]>注:文中的图我使用OmniGraffle制作。转载请注明作者!
前段时间,我用electron-vue开发了一款跨平台(目前支持Mac和Windows)的免费开源的图床上传应用——PicGo,在开发过程中踩了不少的坑,不仅来自应用的业务逻辑本身,也来自electron本身。在开发这个应用过程中,我学了不少的东西。因为我也是从0开始学习electron,所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历,用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。
预计将会从几篇系列文章或方面来展开:
PicGo
是采用electron-vue
开发的,所以如果你会vue
,那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如react
、angular
,那么纯按照本教程虽然在render端(可以理解为页面)的构建可能学习到的东西不多,不过在main端(electron的主进程)应该还是能学习到相应的知识的。
如果之前的文章没阅读的朋友可以先从之前的文章跟着看。
虽然electron在大多数情况下的跨平台措施已经帮我们做得很好了。不过需要注意的是,不同平台必然存在细节上的差异。我们在书写跨平台应用的时候,如果只在自己书写平台下测试通过的话是不足以说明我们的应用是健壮的。(当然如果你只想提供给某个平台那另当别论)所以针对不同的发布平台,就需要做一些兼容性措施。
就我自己的感受而言,macOS平台支持的特性相对比较多,而这里面又很多是独有的,所以很多能在macOS上实现的功能却不一定能在windows上实现。所以对于windows用户而言,在保证整体应用的可用性的情况下,就有可能要相应地做一些妥协和牺牲。不过在windows上的一些操作习惯也可以反过来服务于macOS平台。这点我会在下面给出一个例子详细说明。
在开发electron应用的时候,很多时候我们只注意去查找api名,却容易忽视这个api能够使用的平台。在官方文档里,对于一些独占的api,大多都会有标识标出:
不过需要注意的是一些未有平台标识的api里的配置项,也有可能是某个平台的独占:
平时开发的过程中,用到文档的地方还是需要细细留心,避免后续不必要的麻烦。
上面讲了这么多,该到实例的时候了。在electron应用中,通常来说renderer
进程的东西不需要做太多的跨平台措施——毕竟不管是哪个平台,都是跑在Chrome里的页面。所以大多数情况下,这个方面的工作会放在main
进程里。不过也有例外:
下面是PicGo的windows版:
下面是PicGo的macOS版:
可以发现除了颜色有些区别之外,顶部的title-bar
操作栏也有些区别。macOS的程序窗口习惯将窗口的缩放、关闭按钮放在窗口的左上角。而windows程序则相反,它们喜欢放在窗口的右上角。所以为了迎合用户的操作习惯,我们在开发electron程序的时候也应该注意到这一点。
当然,如果是通过普通的BrowserWindow
创建的窗口,那么将会自动拥有常见的macOS、windows的顶部栏,以及默认的样式。
我在这里想说的是如果想要更加美观的界面,通常我们喜欢「沉浸式」的顶部栏。对于macOS而言,沉浸式的顶部栏就是将顶部栏的三个操作按钮直接「嵌入」窗口主题的左上角。而对于windows而言,只能删去顶部的三个操作按钮,自己用前端的方式来实现了。所以这个地方两个平台的差异性就出来了。
在main
进程里创建该窗口的时候,主要代码如下:
1 | const createSettingWindow = () => { |
主要的工具是通过process.platform
来判断不同的平台。当前可能的值有:
在这里我们基本上只需要关心darwin
(macOS)、win32
(windows)、linux
(Linux)这三个平台即可。注意,由于electron的对于renderer
进程的加持,在renderer
进程里也能直接使用process.platform
来判断当前的操作系统。这是一个很方便的特性。
针对windows平台,由于采用了frameless-window,所以我们需要手动「绘制」顶部的缩放和关闭按钮,并配上相应的事件来模拟真实的按钮。
1 | <div class="fake-title-bar"> |
相应的事件如下:
1 | minimizeWindow () { |
简单来说就是调用了BrowserWindow
的方法来获取当前激活的窗口,然后再对这个窗口进行缩小或关闭的操作。其实也不难对吧!
针对不同的平台,我对PicGo的任务栏图标交互也有所区别。对于macOS而言,点击顶部菜单栏的时候会弹出一个小窗口:
由于macOS的顶部栏图标可以接受拖拽事件,所以就针对macOS的顶部栏制作了顶部栏图标对应的小窗口。让大部分操作不经过主窗口也能实现。而对于windows而言,没有顶部栏,取而代之的是位于底部栏的右侧的任务栏,通常点击任务栏里的图标就会把应用的主窗口调出来。所以为了迎合不同平台的操作习惯,我对于这个地方也做了相应的兼容性适配:
1 | tray.on('click', () => { // 不管是顶部栏的图标还是任务栏的图标都是Tray组件生成的 |
在windows平台上,通常我们把应用的窗口都关了之后也就默认把这个应用给退出了。而如果在macOS系统上却不是这样。我们把应用的窗口关闭了,但是并非完全退出这个应用。所以为了实现这个操作习惯,我们也可以增加一个情况判断:
1 | app.on('window-all-closed', () => { // 当窗口都被关闭了 |
本文简要地讲述了electron应用在跨平台开发的时候的一些注意事项。可能很多人会觉得奇怪我为啥把这个章节单独拎出来讲。很多时候我们只关注于应用的开发过程,把应用的功能实现是很多情况下的「终极」目标。然而真实情况是,应用的功能实现只是「基本」目标。一个应用要给用户使用的话必然不仅要考虑到应用的功能,还必须考虑用户的使用习惯。要站在用户的角度来做应用。而不是做自嗨型的应用。所以这篇文章也希望能够帮助想要开发electron应用的你。
本文很多都是我在开发PicGo
的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的electron-vue
开发带来一些启发。文中相关的代码,你都可以在PicGo的项目仓库里找到,欢迎star~如果本文能够给你带来帮助,那么将是我最开心的地方。如果喜欢,欢迎关注我的博客以及本系列文章的后续进展。
]]>注:文中的图片除未特地说明之外均属于我个人作品,需要转载请私信
Supports iframe & slides. You can use a layout called slides
to enabled the slides layout.
Also you can add a iframe
front-matter with the slides
layout in your md
file to enable the iframe page.
===
1 | hexo new page slides |
===
1 | vim index.md |
Add a type called slides
:
1 | title: slides |
===
Add slides default config:
1 | slide: |
See reveal.js config
===
In _posts
folder, add a md
file.
For example:
1 | title: hexo-theme-melody v1.5 supports iframe & slides |
Then you will get a post of slides type.
===
If you want to add a website whatever you like within an iframe, try this:
In _posts
folder, add a md
file.
1 | title: hexo-theme-melody v1.5 supports iframe & slides |
Then you will get a post of iframe.
===
The slides config in meldoy.yml
can change whole slides page.
But if you set the config in the md file, it will effect the single page.
==
For example:
1 | title: hexo-theme-melody v1.5 supports iframe & slides |
===