airyland

airyland 查看完整档案

广州编辑武汉大学  |  机械 编辑  |  填写所在公司/组织 vux.li 编辑
编辑

vux 任何问题请到 Github 提问,请不要发私信。

个人动态

airyland 收藏了文章 · 2020-01-13

腾讯跨端框架 Hippy 常用调试方法和问题案例详解

导读 |近日,腾讯开源跨端框架 Hippy,一周即吸引3000+star。在腾讯内部,Hippy已运行3年之久,跨 BG 共有 18 款线上业务正在使用 Hippy,日均 PV 过亿,且已建立一套完整生态。

相较于其他跨端框架,Hippy 对前端开发者更友好:紧贴 W3C 标准,遵从网页开发各项规则,使用 JavaScript 为开发语言,同时支持 React 和 Vue 两种前端主流框架。本文将为大家介绍Hippy 常用调试方法和常见问题案例,希望能够帮助开发者快速上手。

一、常见调试方法

1.调试服务

前端调试在官网 [1] 已经有专门章节进行描述,就不多说,这里具体说一下调试常见问题、案例和一些基本原理。

Hippy 已经在 hippy-debug-server[2]中集成了一套基于 Chrome DevTools Protocol[3]的调试服务器,启动后在终端进入本地调试界面,便可以进入远程调试模式。

目前 iOS 和 Android 都已经支持了真机调试,Android 通过 adb reverse 命令直接实现了本地调试端口的转发。就是指在手机上访问 localhost:38989 的调试端口时,访问的实际是开发机上的 38989 端口。

但是 iOS 需要终端和前端的双方面配合修改端口才可以做到真机调试,所以建议先通过 iOS 模拟器进行调试工作。

启动调试服务、进入终端的本地调试环境后,JavaScript 代码将会通过调试服务加载到真机中运行。如果代码没问题应该能正常运行,但有时候会碰到启动就 Crash 的情况,可以参考常见案例最后一条“iOS 版本低于 9 时模拟器报告 SyntaxError”。

同样的,iOS 上某些特性有的能用 Polyfill 解决,但有的不行(例如 Proxy、正则表达式的 Sticky Flag 等就需要 iOS 10 以上才可以使用,而且无法 Polyfill),所以如果要兼容低版本 iOS,要注意不能使用到太新的 JS 特性。

2.整合到终端内的前端 jsbundle 包调试


有的 App 调试模式下运行很正常,但是打完包集成进去以后发现不行,这时候我们需要用到整合后的 jsbundle 包调试大法了。值得注意的是:该方案暂时只适用于 iOS。

其实非常简单,Hippy 在 iOS 中是通过自带的 JavaScriptCore 运行的,所以可以通过自带的 Safar 进行调试,在 Safari 的设置 -> 高级打开开发者菜单后 ,启动 Hippy 就能看到多出了一个模拟器设备。

Safari 调试菜单位置

然后就可以用 Safari 开始调试了。唯一要注意的是,断点需要在启动后才生效,启动时是断不下来的,启动问题可以在关键点加上日志,日志能够正常输出。如果是其它启动后问题,可以直接打断点,跟 Chrome 调试服务的使用方法基本一致。

整合后包打断点

3.内存占用情况

============

前端开发普遍对内存占用缺乏概念,直到终端同学过来说 JS 内存占用太多把 App 搞崩溃了才回过神来。

JavaScript 目前主要以标记清除算法[4] 的方案来进行内存回收,它的核心是定期从全局对象中遍历所有对象,并且对不可到达的对象进行标记,并进而清除。

在绝大多数情况下作为前端开发确实不需要关心内存占用,但是 Hippy 中不太一样,Hippy 是前端的开发方式去开发终端 App,有几个类在组件卸载时一定要记得销毁。

包含了 React 中负责事件监听的 EventEmitter 实例、Animation/AnimationSet 动画组件,Vue 中的 $app.on() 终端事件监听等等,不释放掉它们,它们就会一直占用着内存,随着界面越来越多,App 最终将会崩溃。

其实调试方法也非常简单,直接在调试器的 Memory 观察内存占用情况,打快照看一下当时各类对象对内存的占用情况,它是 Hippy 在浏览器里运行的容器,可以代表 App 的整体内存占用情况。

内存调试方案

当然,这部分内容 Google 官方也有文档 [5],感兴趣的同学也可以去检索查看。

二、常见案例

1. 数据已更新,但界面内容或样式不变


这是经常碰到的,最直接的方式是对 React 和 Vue 进行界面绘画的模块 - UIManagerModule的三个方法 :createNode、updateNode、deleteNode 打断点。

其实不管 MVVM 怎么做,最终都会通过这三个方法把界面通知终端画上去,这其实也带来了无限的扩展性,任何框架只要对接了这三个方法就可以进行 Hippy 绘制。

如果掌握了 UIManagerModule 的语法,甚至不需要 React 或者 Vue 也可以直接通过它画界面。

但从一定程度上来讲,Hippy 画界面的方式其实跟浏览器是不一样的,它是异步的。

MVVM 组件创建完毕,componentDidMount 或者 mounted 后,其实并不意味着界面真的画上去了(但是这个耗时极少,mounted 后基本可以认为真的画上去了)。

如果要对界面进行操作,需要确定终端确实画上去了才行,这可以通过 onLayout 事件获得。其次可以看到画界面和普通的 Native Module 调用没有本质区别,最终都要通过 JSBridge 进行通讯。-- 这部分正在通过 C++ 方式重写。

通过观察它,我们可以了解到最终通过 React、Vue 解析后的组件是什么样的,可以观察到为什么界面没有更新,或者样式不如预期。

Hippy 的前端框架在开发初期就考虑到了调试的便利性,调试模式下会将前端框架与终端之间的通讯都打印到 Console 里,当觉得自己的业务 App 或者框架显示存在问题时,直接观察它就能很方便获得所有信息。

以 Hippy-Vue 为例:

Hippy-Vue 的终端通讯日志

Hippy-Vue 要关闭该功能只要将入口文件中的 Vue.config.silent 改为 true 即可;Hippy-React 要关闭该功能需要在启动参数里增加一个 silent: true。不过一般不建议关闭,它在打包后会自动停止输出。

2. ScrollView或 ListView无法滚动


在 Hippy 中只有这两种 View 是可以滚动的,剩下的都不可以滚动,但是要让它们能滚起来也不是那么简单,需要有样式进行配合,简单说就是:

  • ScrollView(Vue 的 div + overflow-x/y:scroll) 以上所有父节点都必须有一个固定的高度,ScrollView 中只能嵌套一个内容子节点,它可以随意变高。
  • ListView (Vue 的 ul/li)以上所有父节点都必须有一个固定的高度,里面所有的 renderRow 出来的 ListItemView(Vue 中的 li)可以随意变高。

这里的固定高度可以是直接指定高度,也可以是通过 flex 进行界面动态分割的高度。但是一定要是固定的,因为滚动实际是终端去实现的,它需要能够区分可以滚动和不可以滚动的区域,如果容器高度和内容高度一样,那就变成不可以滚动了。

另外 Vue 里的 ul 默认已经加上了 flex: 1 样式会把整个 View 撑满屏幕,一般情况下不用做特别处理,但是 div + overflow-x/y: scroll 依然需要手工指定高度。

当滚动出现异常的时候,可以通过 XCode 调试一下终端代码,它有个 Debug View Hierarchy 功能,可以非常直观地看到界面层级和尺寸,对调试样式问题有很大帮助。

XCode 的界面层级调试

3. ListView性能很差、卡顿、闪烁


这里需要提到前端三点非常需要注意的地方:

(1)界面发生异常闪烁

首先需要通过第一个小章节里的UIManagerModule 观察法,看一下那三个方法是否有异常的执行。例如 updateNode 执行过于频繁,或者 deleteNode/createNode 异常执行,通常是由于数据有变化导致界面重绘,可以通过调用栈看一下是哪里的数据更新导致界面重绘,并针对性地进行前端优化。

(2)处理 key 值

ListView 决定界面是否重绘,有个很关键的参数是 key(React 官文 [6]、Vue 官文[7]),Hippy-React 也通过 getRowKey() 的方法实现了 key 在 ListView 中的应用。key 其实是数据的唯一标示符,数据不发生改变,key 就不应该发生改变,而 key 一旦发生改变 ListView 就会重绘。

目前很多业务在开发时 key 不指定,或者把 index 作为 key,前者会导致 ListView 每次有数据更新都做一次完整的 Array diff,开销非常大;后者会导致删除中间一个节点时将后面所有的节点全部删除再重新插入一次,开销也非常大。

出于性能考虑,key 是必须要加的,一般跟数据的主键保持一致即可。但是:如果 ListView 中的数据需要进行排序,那就不要指定 key 了。

目前 Hippy 的 moveNode 功能,已经计划但仍未完成,指定 key 后在重新排序时会因为对应索引的 key 值不同,先删除全部节点内容,再全部重建,可能会造成轻度闪烁。如果此时不指定 key,就只有一个更新节点的请求,两次请求合并为一次,终端层会对数据进行对比并更新节点内容。

(3)终端渲染

如果到这一步终端渲染依然很慢、帧率低,我们就要提到另外一个参数 type 了,对应到 Hippy-React 里是 getRowType() 方法,它是用来表示组件样式的,样式不变,type 就不变。

这里需要先说一下 Hippy ListView 的复用机制,当不指定 type 时,每次有新的ListItemView被渲染(HippyReact 里 renderRow() 将返回ListItemView、Hippy-Vue 里的 li),终端都会重新构建所有终端组件节点。

加了 type 之后,会将将之前渲染过的终端组件节点放到缓存池中,下次碰到相同 type 类型的 ListItemView,就不会重新渲染,而是从缓存池中把缓存的节点拿出来做次拷贝并更新数据,再上屏,即使只有一个样式的 ListItemView,通过 type 也能做到性能优化。

经过上面三步,基本可以解决 90% 的 ListView 性能问题。同样的,Hippy-Vue 官方范例中也对这三个参数加了注释。

4. iOS 上 ListView 不渲染,但 Android 没问题


首先需要检查 numberOfRows参数是否真的是 ListView 中 ListItemView 的数量,这个除了在业务代码中打断点查看数据数量是否和 numberOfRows 一致以外,也可以通过第一个 UIManagerModule 的调试方法查出来。

这个问题牵扯到 iOS 上一个 ListView 的上屏性能优化,iOS 上并不是发一个 ListItemView 就上屏一个的。而是需要先改变 ListView 的 numberOfRows 再去创建节点,当节点数量与 numberOfRows 一致时再上屏。

目前碰到的所有不渲染的问题都是因为这个原因造成的。

另外在 Hippy-Vue 中,对于静态的 li(就是终端的 ListItemView),可以不需要手工指定 numberOfRows,Hippy-Vue 会在 DOM 层计算子节点数量。

但是对于动态获取的数据,也必须要加上该参数,因为 Hippy-Vue 位于 Vue 的渲染层,跟业务还隔了一个 Vue,无法知道业务到底有多少数据准备要渲染。

5. iPhone 中红屏报告 ModuleNotRegist


这里需要提到 Hippy App 的启动方式:当终端 JS 引擎加载完 JavaScript 后,会从 GLOBAL.appRegister对象里去寻找终端指定的 moduleName,而 __GLOBAL__.appRegister 是在 Hippy 启动时通过 HippyRegister.regist() 方法注册上的。

在 Hippy-React 入口文件或者 Hippy-Vue 入口文件定义的 appName 最终都会执行到 regist() 方法上进行  __GLOBAL__.appRegister 的注册,所以,首先我们要检查终端的 moduleName 是否和 appName 一致。

如果一致依然出错的话,很大几率是之前 JS 执行失败,也不排除 SDK 更新后存在 bug,也有可能其它问题,会导致 __GLOBAL__.appRegister 未注册成功。

但我们可以在该错误抛出时二次确认一下终端所寻找到 moduleName 是否和前端定义的 appName 一致,只要在那一行打上日志,然后使用上文的 Release 包调试方案检查终端过来查的到底是什么 appName,问题就可以得到解决。

6. iOS 版本低于 9 时模拟器报告 SyntaxError


这是因为 Hippy 自带的 Webpack 默认调试模式配置文件,最低仅开启了 iOS 9 的输出,因为输出到 iOS 8 会多出很多 polyfill,语法上也会转换,导致体积大很多。

Hippy 本身最低支持的 iOS 8,我们建议在高版本的 iOS 上进行调试,然后打包后在低版本 iOS 走一遍测试流程,没什么问题即可。

如果非要在低版本的 iOS 上进行调试,修改一下 webpack 配置文件 iOS 将 preset-env 中的 ios 版本改成更低即可,但目前经过测试 core-js 对 iOS 8 那样对低版本可能存在问题,这就需要你自己手工调整了。

参考资料:

[1] Hippy 前端调试详解:https://tencent.github.io/Hip...

[2] 关于 hippy-debug-server :https://www.npmjs.com/package...

[3]Chrome DevTools Protocol 简介:https://developer.chrome.com/...

[4] 标记清除算法:https://developer.mozilla.org..._Management#Mark-and-sweep_algorithm

[5] 解决内存问题的Google官方文档:https://developers.google.com...

[6] React 官方文档:https://reactjs.org/docs/list...

[7] Vue 官方文档:https://vuejs.org/v2/guide/li...

欢迎关注「云加社区」,Hippy 的实战和原理解析系列文章将会陆续上线。

查看原文

airyland 赞了文章 · 2019-08-08

网页应该如何录屏呢?

摘要: 网页应该如何录屏呢?

Fundebug经授权转载,版权归原作者所有。

关键点

  • 首先,每一次会话都有一个唯一的session ID,这是串联起所有行为的纽带。
  • 其次,用户行为又分成两个部分,其一是用户的操作,比如鼠标滑动,点击,页面滚动等,其二是页面的变化。这两者我们都统称为用户行为,记录在同一个队列中。
  • 一开始的时候,系统会记录下初始的页面作为第一帧,这是唯一的一次完整页面记录。
  • 针对用户操作,我们会记录事件的类型,鼠标位置等关键信息,保存到队列中。
  • 针对页面变动,我们会起一个mutationObserve侦听页面的改动,每次只记录改动的部分,保存到队列中。
  • 无论是事件还是页面改动,都是对等的一帧,每一帧都会有当前时间,与上一帧间隔时间等基本信息用户还原
  • 一旦出错,SDK就把队列发送到监控系统,并清空当前队列。
  • 还原端根据记录的行为队列,根据时间逐一播放出来。最终形成一个类似于视频的效果。

初步思路

方式一:

  • 前端收集信息,首先,初始化的时候记录一个页面的初始状态,然后利用 MutationObserver 监听dom的改变事件,然后监听所有的鼠标事件、滚动事件等等所有的页面变化。
  • 在合理的时机把这些信息队列上传到服务器,如页面出错时等。
  • 后台分析前端收集到的信息,转为图片,然后形成"视频",或者用户行为栈。提供对应的调用 api。
  • 前端需要查找问题时,根据用户id等信息找到对应的出错栈。

方式二:

  • 前端根据 html 转为对应的图片(可以转为 base64 格式)
  • 将图片发送给后台
  • 后台将图片按序组成"视频"

现有SDK

Fundebug

录屏(截图)

  • html2canvas
  • puppeteer
  • rrweb+rrweb-player+rrweb-snapshot

html2canvas介绍

html2canvas 是通过分析页面中已加载好的 DOM 元素,然后 canvas 将生成的 DOM 节点绘制在画布上,最后转换为图片。它不是真正的截屏,只是根据页面元素信息还原出图片,所以并不是 100% 和页面相同的。

局限性

  • 页面中的图片不能跨域
  • 不是所有的 css 特性都支持,如不支持 box-shadow、filter 等
  • 不支持截取插件内容,如 Flash
  • 不支持 iframe 内容

浏览器支持

  • Firefox 3.5+
  • Google Chrome
  • Opera 12+
  • IE9+
  • Edge
  • Safari 6+

puppeteer介绍

Puppeteer 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具,它是一个 Node 库,提供了一个高级的 API 来控制 DevTools协议上的无头版 Chrome 。

局限性

  • Puppeteer 需要 Chromium。其主要应用在自动化测试上。

功能

  • 生成页面的截图和PDF。
  • 抓取SPA并生成预先呈现的内容(即"SSR")。
  • 从网站抓取你需要的内容。
  • 自动表单提交,UI测试,键盘输入等
  • 创建一个最新的自动化测试环境。使用最新的JavaScript和浏览器功能,直接在最新版本的Chrome中运行测试。
  • 捕获您的网站的时间线跟踪,以帮助诊断性能问题。

结论html2canvas 更适合于 C 端的用户行为截图跟踪,而 Puppeteer 适用于自动化测试。

rrweb介绍

rrweb 主要由 3 部分组成:

  • rrweb-snapshot,包含 snapshot 和 rebuild 两个功能。snapshot 用于将 DOM 及其状态转化为可序列化的数据结构并添加唯一标识;rebuild 则是将 snapshot 记录的数据结构重建为对应的 DOM。
  • rrweb,包含 record 和 replay 两个功能。record 用于记录 DOM 中的所有变更(mutation);replay 则是将记录的变更按照对应的时间一一重放。
  • rrweb-player,为 rrweb 提供一套 UI 控件,提供基于 GUI 的暂停、快进、拖拽至任意时间点播放等功能。

rrweb适用场景:

  • 用户行为分析;
  • 远程debug;
  • 录制操作;
  • 实时协作;

局限性

  • 社区资源较少
  • 部分代码用较旧的模式写的,有未知坑

最终结论

综合来看,结合思路一,基于 rrweb 来开发是最可行最快捷的。

Demo

目前,我基于 rrweb 已经做了个 demo 出来。以下是初步成果:demo代码

补充资料

rrweb的一些思路原理

rrweb:打开 web 页面录制与回放的黑盒子

MutationObserver介绍

Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。

特点

  • 它等待所有脚本任务完成后,才会运行(即异步触发方式)。
  • 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。
  • 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。

example

Select the node that will be observed for mutations var targetNode = document.getElementById('some-id');

// Options for the observer (which mutations to observe)
var config = { attributes: true, childList: true, subtree: true };

// Callback function to execute when mutations are observed
var callback = function(mutationsList, observer) {
    for (var mutation of mutationsList) {
        if (mutation.type == "childList") {
            console.log("A child node has been added or removed.");
        } else if (mutation.type == "attributes") {
            console.log(
                "The " + mutation.attributeName + " attribute was modified."
            );
        }
    }
};

// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);

// Start observing the target node for configured mutations
observer.observe(targetNode, config);

// Later, you can stop observing
observer.disconnect();

observe方法接受两个参数,第一个是所要观察的DOM元素是article,第二个是所要观察的变动类型(子节点变动和属性变动),方法调用时必须指定一种或多种变动类型,否则报错,变动类型如下:

boolean childList = false;
boolean attributes;
boolean characterData;
boolean subtree = false; //表示是否将该观察器应用于该节点的所有后代节点。
boolean attributeOldValue; //表示观察attributes变动时,是否需要记录变动前的属性值。
boolean characterDataOldValue; //表示观察characterData变动时,是否需要记录变动前的值。
sequence<DOMString> attributeFilter;//数组,表示需要观察的特定属性(比如['class','src'])

disconnect方法用来停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器。 takeRecords方法用来清除变动记录,即不再处理未处理的变动。该方法返回变动记录的数组。

MutationRecord对象

DOM 每次发生变化,就会生成一条变动记录(MutationRecord 实例)。该实例包含了与变动相关的所有信息。Mutation Observer 处理的就是一个个MutationRecord实例所组成的数组。 MutationRecord对象包含了DOM的相关信息,有如下属性:

type:观察的变动类型(attribute、characterData或者childList)。
target:发生变动的DOM节点。
addedNodes:新增的DOM节点。
removedNodes:删除的DOM节点。
previousSibling:前一个同级节点,如果没有则返回null。
nextSibling:下一个同级节点,如果没有则返回null。
attributeName:发生变动的属性。如果设置了attributeFilter,则只返回预先指定的属性。
oldValue:变动前的值。这个属性只对attribute和characterData变动有效,如果发生childList变动,则返回null。

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了20亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用!

查看原文

赞 23 收藏 12 评论 0

airyland 赞了文章 · 2019-08-08

带你理解 Git 中的 Merge 和 Rebase

概念

Rebase 和 merge 都被设计用来将变更从一个分支整合到另一个分支,但是它们的实现方式却不同。

下面假如我们有如下提交,merge 会将两个分支的代码合并,而 rebase 会将 feature 分支上所有的变更在 master 分支上重新应用一遍:

1-three.png

  • 当你将 feature 分支 rebase 到 master 时,实际上是将 feature 的 base 移动到了 master 分支的终点,所以 rebase 中文叫变基。(想象上图平移了两条线段)
  • merge 则是拿 feature 分支中的结果,合并到 master 分支,这个过程中只有 master 分支改变了,feature 分支保持不变
  • merge 的时候会产生一个新的 commit

Merge 的优与劣

优点

  • 简单易用,易于理解
  • 保留原始提交记录和源分支
  • 源分支上的提交与其他分支分离,这会方便你浏览并且合并到其他分支
  • 保留你的提交历史,保证提交历史在语义上的准确性

缺点

  • 提交历史 可能会变得很乱,尤其是很多人同时开发与合并分支时
  • 使用 git bisect 调试将变得困难

Rebase 的优与劣

优点

  • 代码历史简洁,线性,可读性强
  • 相比众多功能分支来说,只有一个分支,管理起来更加方便
  • 简洁的 提交记录 让调试和排查更容易

缺点

  • feature 分支变成了一些 commit,不利于体现开发时的场景
  • Rebase 不适合与 pull requests 同时工作,因为你看不出来哪里是别人做的变更。重写了历史记录也不利于团队协作

    你在使用 rebase 时也应该更加小心
  • 在处理 冲突 时需要花费更多的精力,使用 rebase 来合并功能分支,同一个冲突可能需要合并多次。

总结

  1. 如果有几个开发者同时在 feature 分支上开发,就不推荐使用 rebase,因为 rebase 会掩盖真实的提交场景。相对而言,个人开发更适合使用 rebase。
  2. 如果你想保留完整的历史记录,就应该使用 merge。记住,Merge 保留历史记录,而 Rebase 改写历史记录

Rebase 可以用来精简一个复杂的历史记录,通过交互式 rebase,你可以去掉不想要的 commit,合并多个 commit 甚至修改 commit 信息。

需要注意的是,由于 rebase 是将 commit 一个一个应用到目标分支,所以在产生冲突时,需要针对 commit 一个一个去解决,而 merge 是将 commit 的最终结果合并到目标分支,所以冲突只需要解决一次即可。而如果有很多冲突的话,撤销一个 rebase 也将会非常困难。

参考文章

查看原文

赞 10 收藏 7 评论 0

airyland 赞了文章 · 2019-05-09

旗帜鲜明地抵制 CSDN 下载(盗版)站!

SegmentFault 上线付费课程以来,对于内容质量一直严格把关,讲师认真备课,课后为学员答疑,广受好评。然而近期有多位 SegmentFault 讲师反馈在 CSDN 下载频道出现了大量他的盗版课程。

clipboard.png

不查不知道,一查我们发现——我们讲师辛辛苦苦花了上百个小时录制的付费课程,在 CSDN 下载频道竟有满满一屏幕的盗版存在(相关证据我们已经找律师团队取证),同时根据他们的关键词推荐我们发现在其博客频道也有大量的盗版内容,防不胜防,让人不吐不快。

clipboard.png

昨天我在朋友圈公开这个事情后,收到了大量业内同行的反馈,我们发现不仅仅是 SegmentFault,几乎我认识的所有同行以及我们熟悉的讲师的付费内容(包括不限于课程、图书、专栏)都有被 CSDN 下载频道侵权的经历,昨天,我们用了同行的一些关键词在 CSDN 下载频道进行检索,同样发现存在有大量的盗版内容存在。

clipboard.png

同时我们也收到一些用户的反馈在 CSDN 下载频道有大量 IT 相关技术书籍的扫描版文件,毫无疑问都是盗版资源。上传时期从 2016 年(或者更早)开始到至今一直存在。

我们通过关键词在 Google 检索后,发现最早控诉 CSDN 下载频道的内容《CSDN 首页鼓励盗版图书下载》是在 2005 年发布,在知乎等社区也有大量的讨论,如《如何看待 CSDN 利用用户上传的盗版资料卖积分赚钱?》《为什么 CSDN 能做到让用户花钱买积分下载自己网站的盗版资源?》……可见已在业内引起公愤。

clipboard.png

CSDN 做为中国最早的技术社区之一,我们认可其对开发者之间线上交流做出的贡献,但是其下载频道的存在大大助长了大量盗版侵权内容的产生。并非个例,长期存在,越发泛滥,从未被解决——这代表其产品存在根本上的机制问题。

CSDN 官方对此不知情吗?

不过是睁一只眼闭一只眼罢了——CSDN 的下载频道占了总社区超过 30% 的流量。靠着盗版和侵权他人优质内容,获取平台流量,再依靠平台流量进行广告和其他形式的变现。这不是非法牟利是什么?

我们不禁质疑号称中国最大技术社区的 CSDN 究竟拥有怎样的价值观?

纵容盗版,非法牟利;

无视用户隐私——曾经明文存储用户名密码导致用户数据泄露;

甚至欺骗客户——夸大网站流量,广告数据造假。还记得去年程序员广告代码刷量的乌龙事件吗?“博客详情页PC增加广告系统刷量代码”这句话写在了代码注释里面上线了。原来客户的广告数据都是刷出来的?(心疼投放广告的客户)

其实技术圈子非常小,很多同行可能碍于面子或者各种各样的原因,没有公开地去声讨 CSDN,一些声讨可能也并没有解决问题,这更助长了其纵容盗版的气焰。 SegmentFault 作为技术社区的一员,我们深知社区发展的不易,我们有责任帮助我们的讲师维护其付费内容的版权不被侵犯。

我们已经聘请专业的律师团队取证,同时我们也呼吁被 CSDN 下载站侵犯过内容版权的同行,讲师,书籍作者,广大开发者一起发声,在评论区留下你的声音和被侵权经历,通过曝光侵权甚至违法行为,共同净化行业环境。

一起举报

到中央网信办(www.12377.cn)

国家新闻出版广电总局官网(www.sapprft.gov.cn)

举报 CSDN下载站的盗版侵权行为。

图片描述

SegmentFault CEO:高阳Sunny 2019年5月9日凌晨于北京

查看原文

赞 145 收藏 1 评论 59

airyland 收藏了文章 · 2018-10-07

用 Electron 打造 Win/Mac 应用,从「代码」到可下载的「安装包」,可能比你想得麻烦一点

首发于酷家乐前端博客,作者@摘星(segmentfault @StinsonZhao)

我们能从很多地方学习到怎么起一个 Electron 项目,有些还会介绍怎么打包或构建你的代码,但距离「真正地发行一款 Electron 产品」这一目标,还有很多工作需要做...

这是 Electron 系列文章的第二篇,这一篇文章将和大家分享我是怎么去构建自动化的 Electron 开发构建工程的,说白了,就是怎么把敲的代码变成一个用户可以下载安装的包,当然随着之后应用复杂度的提升和技术再选型,工程体系可能随时会重构或演进,但至少可以给大家一些参考,欢迎留言交流。

这是一篇很长的文章(手册),写得比较「唐僧」(知我者可以说我写得比较用心),至少会花你一天时间(没开玩笑),适用场景是「用 Electron 打造 Windows 或 Mac 应用」,是的,你没看错,同时会讲清楚兼容 Win 和 Mac 两个系统的流程。文中提及的技术方案绝对不是最佳的(我保证),因为几乎每隔几天我都会发现某个环节可以做得更好,但要明白要唱多大的戏,就先搭多大的台,够用就好,不要为了搭台耽误演出时间

工程自动化,应该是所有开发者的一种基础追求,当你搭建建好工程体系,以后你将专注于产品功能的开发,而不会花大量不必要的时间去手动构建。作为前端,可能我们已经熟悉了 web 应用的构建和部署,但是客户端程序有其本身的特点,相比较 web 应用最大也是我认为最根本的一点区别在于「你的应用是被用户下载过去安装在用户本地再跑起来的」。

这一区别对工程的影响在于,你不可能把你的代码部署到「用户的电脑」,你需要构建安装包,你需要针对不同的用户系统构建不同的安装包,你需要让你的应用被系统认为是安全的...

本文目的

本文需要做的是,把客户端的打包构建发行这一流程做到像「把大象放进冰箱」一样的简单:打开命令行,敲一个 npm run xxx,喝一口咖啡,咪哩嘛哩哄,安装包出现(一开始打造这个流程时,剧本可能是「喝一口咖啡,啪,Error 了,又 Error 了」,take it easy,生活需要慢慢品味 —— 来自一位25岁的仙风道骨白胡子程序员)。

本文将分以下小节和大家分享「从本地的代码到云端可下载的安装包」这一路的风景,你会有漫步月球般的感觉(因为月球全是坑啊,还没氧气):

  • 第一节是关于目录结构的讨论,合适的目录结构会是一个良好的开端
  • 第二节是之后几个小节的概述,阐述了怎么把这一整个过程分成多个环节,每个环节又大致要做什么事
  • 第三到七节分别详细描述了「配置」、「打包」、「代码签名」、「构建安装包」、「发行安装包」这几个环节要做哪些事,有什么讲究
  • 第八节是简述一些可进一步研究或优化的点
  • 附:这样设计的 gulpfile 文件结构

下面一一展开进行阐述,再次强调,文中很多依赖的技术或包,你都可以尝试替换成自己相中的,不必在意是选「翠花」还是「桂花」,多处处就知道了。

一、目录结构

以下目录结构供参考,没有很详细地展开,因为每个应用可能不同,最想表达的是这是一个「双 package.json 结构」,你可以看到根目录下有一个package.jsonapp目录下还有一个package.json

/       // 项目根目录
├── app/                // 应用源码目录,打包就针对app目录进行打包
│   ├── assets/                     // 应用需要的图片、icon等资源
│   ├── config/                     // 配置文件存放
│   ├── consts/                     // 应用运行需要的常量,如ipcChanel
│   ├── lib/                        // 引入的库文件,如jquery
│   ├── plugins/                    // 应用运行需要的插件,如flash
│   ├── utils/                      // 常用的工具方法
│   ├── view/                       // 视图,html、css和js
│   ├── app_config.js               // 整个app的配置,引入config文件夹下文件
│   ├── main.js                     // 应用入口
│   ├── package.json                // 内部的package,定义应用的版本、运行依赖等
│   └── yarn.lock                   
├── build_resource/     // 构建需要的一些工具、资源或者脚本的目录
├── config/             // 环境配置文件目录,会选择一个写入到app/config
├── deploy/             // 部署脚本,用户部署文件到cdn或上传文件到OSS
├── reserve/            // 保留目录,存放一些文件用于写入到app内
├── dist/               // 打包和构建的目标目录
├── release/            // 发行的目标目录
├── .gitignore
├── gulpfile.js         // gulp配置
├── package.json        // 外部package.json,用于定义开发依赖和脚本
└── yarn.lock

这是因为,我们的应用在运行时需要一些第三方依赖,这些依赖我们需要打包到应用内,也就是说/app/node_modules目录内的内容是要被打包到应用内的,用户使用的时候才不会缺失「运行时依赖」,而如果我们只有一个package.json,那么所有的依赖都被下载和安装到同一个node_modules文件夹下,我们没法把我们需要打包进去的依赖树提取出来。所以这样双 package.json的结构最清晰明了和简单易用,dependenciesdevDependencies有了明确划分。

再大致解释下其他目录的作用:

  • app目录:是我们应用的源码目录,我们所说的打包针对的就是这个目录,其他目录和文件不会被打包进去,而app目录内的子目录和文件就见仁见智了,在不同的复杂度下有不同的设置,这里还有一些东西是需要从外面复制进来的,因为不同的平台下你可能需要打包进去的东西是不同的。

    • config:配置文件目录,可能因为你想打包的应用所处的阶段(开发、内测、众测、正式发行)和平台(Windows、Mac),那么可能需要不同的配置,比如一些资源的名称和路径等,这里你可以把不同情况下都一样的配置写到一个配置文件,而根据情况不一样的配置文件是从外部脚本写进来的,这就是为什么你会在app目录外面看到一个config文件夹的原因
    • plugins:是插件文件夹,你可能需要给自己的应用加一些插件,比如 flash,而一个 flash 插件有 40M 左右,Win(32bit)、Win(64bit) 和 Mac 需要的 flash 插件文件都是不一样的,所以如果全部打包进你的应用,再用「if - else」去选显然是不科学的,Mac 下的应用肯定是用不到 Win 版本的插件的,所以这里的文件也是从外面脚本写进来的
    • view:是视图文件夹,也可以说是渲染进程对应的代码文件夹
  • build_resource:构建资源或工具文件夹,这个文件夹下放打包到发行这一流程中需要用到的资源和工具,比如程序主图标、构建安装包的配置脚本(win)、代码签名工具等
  • deploy:存放部署脚本的文件夹,这里的脚本负责把你的应用安装包上传到云存储(OSS),我们会在 gulp 中的发行环节引入这里写的脚本进行自动上传安装包
  • distrelease:前者是打包和构建安装包这两步的 output 目录,后者是最终我们会上传到云端的安装包目录,构建和发行环节的差别我们后面会讲到

二、把整个流程拆分成段

这个部分没法正向推导,我是从一个乱七八糟的 windows 开发流程开始的,然后修改成一个合适的 windows 开发流程,再因为要兼容 Mac 的开发,再改成现在这样的流程设计的,所以我没法从一开始就说因为什么所以要考虑什么,然后慢慢构建出一个合适的工作流,这是上帝视角,这个偏实践经验的过程一定是实践越多,感受越多的。

所以我会先说我的做法,再说这么做的好处,所用的工具是 gulp(如果不熟悉,可以去 gulp 官网看一下,很容易上手),利用 gulp 的 task 串起整条流程,我把工程中的一个阶段称为一个环节,是为了和应用本身的阶段(开发、内测、众测或正式发行)做一个区分,不然都不知道说的阶段是指啥

流程概览

  • 配置环节:设定需要打包构建针对的系统、位数(Mac 版不考虑 32 bit)和这个版本所处的阶段(开发、内存、众测或正式发行)这些变量,然后把相关配置写入配置文件模板,再导入 app 文件夹内相应位置,把其他相应的文件也写入 app 文件夹内相应位置,如此 app 文件夹就 Ready 了。
  • 打包环节:根据不同的平台打出不同的可执行程序,这一步输出的是可运行的程序
  • 代码签名环节:客户端特殊的一步,你的应用需要被系统所信任,那就需要代码签名,获取对应平台下的代码签名 CA 然后进行应用签名,这样你的应用才能被系统信任
  • 构建安装包环节:根据不同的系统利用不同的技术和依赖构建安装包,Windows 下的 .exe 和 Mac下的 .dmg,并且对这两个安装包也需要代码签名,这一步后你的应用可以被分发安装啦
  • 发行环节:对构建的安装包进行最后一步修饰,比如修改合适的文件名,然后上传到云存储服务器,获取到可下载的链接,如此,你的应用已经可以经获取到的 url 访问进行下载安装了

以上每一步,Mac 版和 Windows 版的开发都需要经历,只是所用的方法不同,这样做的好处,一是统一了 Mac 和 Win 下开发工作流的生命周期,二是简单和直观,每一环节目的是什么,输出是什么很明确。

如此,我在package.json中的script就可以这么写:

... ...
"start": "cross-env NODE_ENV=dev gulp dev",
"packDev": "cross-env NODE_ENV=dev gulp pack",
"buildDev": "cross-env NODE_ENV=dev gulp build",
"releaseDev": "cross-env NODE_ENV=dev gulp release",
... ...

当然这里的NODE_ENV你也可以写成命令行参数(我只是习惯了用这个),利用这个参数去指定需要针对的应用阶段,像以上这样就配好了「dev」阶段的相关脚本,可以用npm run packDev -- --platform="xxxx" --arch="xxxx" --sign这样形式的命令行去执行不同的 gulp 任务,后面的参数,是需要我们在 gulpfile 文件中解析的,以上3个参数分别表示「系统平台」、「系统位数」、「是否需要代码签名」,我们可以在 gulpfile 文件中给这些参数合适的默认值,使操作更人性化。

三、配置环节

目的:一是为之后的环节初始化工作流参数,二是准备好应用文件夹内容(即要打包的目标文件夹 —— app)

做的事:解析命令行参数,初始化工作参数,填充配置文件,把配置文件和相关依赖文件导入到app文件夹内合适的地方

1. 初始化工作参数

所用工具:yargs

yargs 是一款优秀的命令行参数解析工具,我们要初始化的工作参数包括以下 3 个:「系统平台」、「系统位数」、「需不需要签名」,你也可以把应用的所处阶段(开发、内测、众测、正式)设计成参数。

// 以下 3 个变量在 gulpfile 内全局声明

// 这里的 detectPlatform() 需要自己写,利用 node 的 os 模块去检测开发机环境从而给出
// 为了理解上直观一些,把 32 位的 win 写成 win32,64 位的 win 写成 win64
// node os.platform() 没有 win64 的返回的,只有在返回 win32 基础上,你再使用 os.arch() 去确定是否是win64
// 可能的合法值:darwin、win64、win32
platform = yargs.argv.platform || detectPlatform() || 'win32';

// 系统位数,如果是 Mac OS X,不考虑 32位
// 可能的合法值:x64、ia32
arch = platform === 'darwin' ? 'x64' : (yargs.argv.arch || 'ia32');

// 布尔值,指定是否需要代码签名
needSign = yargs.argv.sign || process.env.NODE_ENV === 'prod' || platform === 'darwin';

看到上面的参数初始化,可能会有疑问,既然已经在platform中区分了 win32(32bit) 和 win64(64bit),而且darwin下不考虑 32bit(因为 OS X 10.6 之后就全是 64 位的),arch参数是否多余?这是可以认为是多余的,但是有的话更完整,而且如果你以后又想兼容 linux 了呢?

2. 填充并导入配置文件

所用工具:gulp API、gulp-replace、gulp-rename

首先我会在根目录下的 config 文件夹下放几个不同的配置文件模板,分别对应应用不同的阶段的配置(比如dev.js、alpha.js、beta.js、prod.js),然后利用gulp-replace去替换掉里面的一些占位字符串(也就是填充模板),最后利用gulp-rename重命名为比如env.js后,利用gulp.dest写入文件到 app/config 目录下,于是配置文件 Ready。

3. 二进制文件导入(以 flash 为例)

所用工具:gulp API、del

以 flash 插件为例,首先你要找到需要的插件文件,electron 官网所说的打开chrome://plugins已经没法用了,从 chrome 的某个版本开始,chrome://plugins Is Not Available。

所以用系统的搜索功能吧,记得先装下 chrome 浏览器,Mac 搜索「PepperFlashPlayer.plugin」,Windows 搜索「pepflashplayer」,Windows下如果搜到多个,记得选择和 chrome 目录有关的那个「.dll」文件,此外 win32bit 和 win64bit 所用的 flash 也是不同的,Mac 下的「PepperFlashPlayer.plugin」本质是一个文件夹,整个文件夹都需要。所有的3个插件放进根目录下 reserve 文件夹。

接下来需要做的就是,根据不同的平台读不同的 flash 插件( .dll 文件或 .plugin 文件夹)到 app/plugin 文件夹下。

这里有一个需要注意的是,每次你构建时,如果 app/plugin 下的 flash 不是你要的,那么你需要先删除那个旧的,否则你的 app/plugin 文件夹下会躺着一个你不会用的 flash 插件,但会被打包进去,你的文件大小突然多了 40M,我这里用的删除工具是 del。

经过配置环节,app文件夹已经准备就绪,所以以开发模式(不需要打包)运行应用也就没啥大问题,可以另写一个「dev」的 gulp task,利用 node 的child_process模块下的exec调用下electron app --debug就可以运行应用了,没啥可以多说的,我们继续进入下一步 —— 打包。

四、打包环节

目的:产出一个可执行程序,简单来说,就是能有一个应用,双击能运行起来

做的事:利用electron-packager打包,补充应用信息(only for win)

1. 利用electron-packager打包

利用electron-packager打包,只需要针对不同系统平台给出不同的配置,然后调用其 API 就可以了。

// Mac
const options = {
    dir: './app',
    name: '应用名字',
    platform: 'darwin',
    arch: arch, // 这就是工作参数 arch
    overwrite: true,
    appVersion: 'Copyright(C) 2017 Qunhe',
    asar: {
        unpackDir: 'plugins' // plugins 内的文件我们不希望打进 asar 格式包内
    },
    out: './dist',
    icon: './build_resource/logo.icns' // Mac 下 icon 格式是 .icns
};

// Win
const options = {
    dir: './app',
    platform: 'win32', // 不管是 32bit 还是 64bit 的 win,这里都是 win32
    arch: arch, // 这里依靠 x64 或 ia32 去区分位数
    overwrite: true,
    asar: {
        unpackDir: 'plugins'
    },
    out: './dist',
    icon: './build_resource/logo.ico' // Win 下 icon 格式是 .ico
};

Mac 下各处(Dock、任务栏、进程名等地)展示的应用名字只要指定了name选项,就是处处一样的,所以你可以用 name 指定一个中午名字,而且 Mac 下默认编码都是 UTF-8,问题不大。

而对于 Windows,首先其中文默认编码是 GBK 的,而所以如果指定中文名字可能会有奇怪的问题,所以 Windows 应用一般我不填name项,这样它会去找你 app 目录下的 package.json 文件中的productNamename字段值,这个字段一般设置是英文的,第二个不去设置中文的原因是,Windows 下应用的展示名字是 exe 主程序的FileDescription配置项决定的,如果不去设置,那么可能你的应用用任务管理器打开,显示的进程是「Electron」,而不是你的应用名字。

关于应用的实际名字和展示名字,Win 和 Mac 下都有自己的一套,这里不细展开。而基于目前的实践,我给的建议是,Mac 下的开发,你可以直接指定name为一个你要的中文应用名,而对于 Win,你最好像下面那样操作。

2. 补充应用信息(for win)

所用工具:rcedit

Command line tool to edit resources of exe file on Windows. 翻译过来就是一个用于编辑 exe 文件信息的windows 命令行工具,当然它已经有了 node 版本,叫 node-rcedit,也就是说你可以用 node 子进程的exec去执行,也可以调用 node 版本的 API。

可以这么用:

execSync(`
.\\node_modules\\rcedit\\bin\\rcedit  // 调用rcedit
./dist/xxxxxx.exe  // 目标文件(刚打包出来的主程序)
--set-version-string "LegalCopyright" "Copyright(C) 2017 Health" // 版权信息
--set-version-string "CompanyName" "仙风道骨养生俱乐部"  // 公司名字
--set-version-string "ProductName" "养生" // 产品名字
--set-version-string "FileDescription" "养生宝典" // 这个很重要,因为这个就是你打开任务管理器看到的进程名字
`);

大部分信息,你可以右键主程序(.exe)文件,「属性 —— 详细信息」中看到,这么做还有一个考虑是,这样你的应用看上去会更加规范。

这里肯定有人说,为什么不用electron-builder,因为我首先接触到的是electron-packager,我觉得够用(因为我有一台 win 和一台 mac,跨平台打包,不存在的),第二,electron-packager完成打包的事就够了,后面构建安装包等过程可以让我们有更多的选择,符合本文的工作流设定,每个环节做每个环节该做的事就好,当然你也可以选择electron-builder,能达到目的就好。

五、代码签名环节

目的:使应用被系统所认可,能正常安装

做的事:给应用进行代码签名

1. 为什么需要代码签名,没有会怎样

代码签名的目的就是为了安全,你的应用一旦经过了代码签名,如果发行过程中被篡改,你的用户会看到系统给出的警告提示,而对于发行方而言,代码签名后,应用才能被系统认可,很大概率不会被杀毒软件做掉,而且如果你要提交一些软件市场,一些软件市场要求应用需要有合法的代码签名。

而如果作为铁头娃的你铁定不签名,这应用就不能跑了么?不是的,还是可以跑的,只不过对你的用户来说很不友好。

1.1 Windows 下有和没有代码签名的差别

签名对比

Windows 下代码签名的限制没有 Mac 那么严,你选择「是」都是可以安装使用的,但是从你产品的用户角度,有一个代码签名会更可靠,此外,这样的没有签名的安装包在一些软件市场可能都提交不上去。

1.2 Mac 下有和没有代码签名的差别

Mac 下有和没有代码签名的差别就很大了,没有合法的代码签名,你的 .dmg 安装包根本没法打开。

如果没有代码签名,Mac 下的 .dmg 安装包打开,首先会提示你「该应用来自身份不明的开发者,是否确认打开」,然后你点「确认」,再根据你的安全设定(系统偏好设置 —— 安全和隐私 —— 允许从以下位置的应用下的设置)去决定,而绝大部分的 Mac 用户都是勾选「App Store 和 被认证的开发者」,于是就算你点了「打开」,直接会告诉你「打不开XXX,因为它来自身份不明的开发者」,这个时候只能去改变「系统偏好设置 —— 安全和隐私 —— 允许从以下位置的应用下的设置」才能打开。

典型的盗版软件安装方式啊,所以作为一款要发行的产品,我们一定是需要代码签名的。

2. Windows 下的代码签名

总体建议:个人的小项目就不用 Windows 代码签名了,因为很贵,2K+/年,而且 Windows 下代码签名没有问题不是非常大(和 Mac 相比),公司的产品,那就必须要的。

2.1 购买微软代码签名证书

可以向权威的 CA 机构购买代码签名证书,这里就我了解的做一个建议:建议向赛门铁克购买签名普通软件(非驱动)的微软代码签名证书,大概几百刀一年。

背景说明:目前我们用的是沃通的代码签名证书,赛门铁克的只是咨询过,没用过。

就以上的建议做一个解释,为什么我这么建议:

  • 我们需要代码签名,进一步,需要把 Windows 代码签名这一环节也做到自动化流程中,这是我们的需求
  • 沃通的代码签名证书是封死在 U 盘里,所以可认为这是物理证书,更安全,但很不方便,不可能导出来进行签名的
  • 了解到的,赛门铁克颁发的如果是针对普通软件(非驱动的),那么是可以给颁发文件格式的真·电子证书的
  • 意味着沃通的证书我们要签名,需要依靠一个物理U盘
  • 最坑爹的:沃通的代码签名时,要手输密码,如果一个 Windows 应用我们选择 SHA1 + SHA256 的签名方式,那么应用和安装包,我们需要输4次密码,气到拉闸,他们官方说有自己的命令行,实际是命令行唤起他们的 GUI 图形界面来签名,还不是需要人工操作
  • 所以,显然这和我们的「自动化」目标相去甚远,我建议普通的应用,没有涉及到高度安全的,不要选择购买封死在 U 盘中的 Windows 代码签名证书。

2.2 签名

当你购买了证书后,就可以利用signtool 命令行进行签名了,命令怎么写,这些都在你购买证书的 CA 网站上找到或者 google 一下,这里要说的就两点:

  • Windows 代码签名我们目前选择 SHA1 签名后再追加 SHA2(SHA256) 签名,这样的组合方式,安全和兼容性最好
  • 代码签名可以在 gulpfile 文件中封装成一个方法(参数是需要签名的文件路径),因为我们会多次调用

2.3 查看签名信息

查看 Windows 代码签名信息很简单,右键你签名的文件,签名后的文件,属性打开会有一个「数字签名」的 tab,点击切换到「数字签名」可以看到代码签名信息。

3. Mac 下的代码签名

总体建议:Mac 下应用要代码签名,因为很方便,也不是很贵,个人开发者 99 USD 一年,如果公司有 Apple Develop Team,你可以直接加入,关键是 Mac 下如果你不进行可供分发的代码签名,你的应用很难被他人安装啊。

3.1 利用 Xcode 申请证书,各个证书间差别

证书是可以在 Xcode 下申请的,Xcode —— Preference —— Account 下,选择一个Team(之前要先加入),如果是独立开发者,就选自己 Apple ID 的那个,点击「Manage Certificates」,弹出的弹窗中左下角点加号,可以选择需要的证书。

我看到之后的第一反应是:尼玛,哪些是我要的啊。下面简单说明下(摘自Mac App 发布的最后 1km):

  • Developer Certificate

    • Mac Development :这个只用来开发,Debug,不是正式发布的版本
  • Production Certificate

    • Mac App Store

      • Mac App Distribution :这个用于 Xcode 自己把 .app 文件上传到 Mac App Store
      • Mac Installer Distribution :这个没用过,但可以肯定的,也是上传 Mac App Store 用的
    • Developer ID

      • Developer ID Application:这个用于开发者使用开发者帐号签名,导出一个线下发布版本的 .app 文件,脱离了苹果的 Mac App Store。
      • Developer ID Installer:用于开发者打包,同时加上开发者帐号签名,打包工具在下面介绍。

我们主要需要的就是「Developer ID Application」这个类型的证书,「Mac Development」只是用于开发的,而前者可以供分发,也就是签名后,别人下载安装,就是来自「被认证的开发者」的应用啦。

如果是在一个 Team 中,不是个人独立开发者,那么这个「Developer ID Application」证书的申请你是没有权限的,就算你们 Team 的 Agent 设置你为 admin(管理员),你还是没有权限的,因为一个「Developer ID Application」只有一个 Team 的 agent(owner) 才能申请,你需要做的是利用你 Mac 上的钥匙串工具(具体怎么做,google 下就可以了),生成「CertificateSigningRequest」(简称 CSR),然后发给你的 team agent,让他帮你生成证书,发回给你,你再安装到自己机子上,搞定。

你可以在终端调用security find-identity -p codesigning -v来看一下你可用的代码签名证书,其中那个Developer ID Application开头的就是我们要的。

3.2 签名

所用工具:electron-osx-sign

Mac 下的签名简直是红红火火开开心心嘿嘿哈哈啊,你可以从electron-osx-sign 指导这里获得完全的指导,你在这个页面右边可以根据你的项目进行填写,页面最后会根据你的配置,给你一段你都可以直接复制的签名代码,完美。

而且签名还能集成到打包阶段,不过我建议还是拿出来好,比较清真。

3.3 查看签名信息

Mac 下查看文件签名信息,你可以终端运行codesign --display --verbose=4 "文件路径"

六、构建安装包环节

目的:使你的应用可以被安装(如果没有这一步,你能怎么办,压缩整个应用文件夹,然后分发这个压缩包,呃,你能接受也可以啊)

做的事:把经历了打包和签名环节后的应用程序文件夹(Mac 下的.app其实也是文件夹)打成一个安装包文件

为什么要构建安装包,这有很多的原因,可能你也会想到很多,其中值得强调的两点,一是构建安装包会直接便利于应用的自动更新,具体我们下一篇文章里再说,二是 Win 下安装包的体积相比原先的文件夹,体积明显小很多,在硬盘容积很大的时代,下载体积才是最影响用户体验的,而安装后的体积不是最需要考虑的体积。

安装包这个事和代码签名类似,两个不同的系统(Win 和 Mac)实现完全不同,Windows 下我们习惯.exe.msi这样的安装包格式,习惯点下一步到完成或一键安装,而 Mac 下除了 Store 下载安装的,我们习惯的.dmg格式的,挂载后打开,将里面的应用拖入到Application文件夹就完成了安装。

这里我们实现的就是经典的 Windows exe 安装和 Mac dmg 安装,相比较而言,Windows 下的繁琐得多得多。

1. Windows 下利用 inno setup 进行安装包构建

1.1 为什么用这个 inno setup

最终说服我使用 inno setup 来构建应用安装包的理由是,VS Code 也是这么做的。因为按照程序这个领域离一个小前端已经很遥远了,对于跨度大的未知东西,一般都会做充足的调研,最后发现 VS Code 也是这么做的,好,干!

而使用了一段时间后,我可以说几点不后悔的理由(当然我没使用过其他的安装包构建工具,所以仅一些偏见):

  • inno setup 应该是 windows 下构建安装程序的老牌工具了,你可以去进他们的官网,一股「老牌可靠」的风格扑面而来,可靠
  • 它有 GUI 和 命令行工具,有 unicode 版本(意味着完全支持中文),gulp 有别人写好的现成的插件(对于中文应用需要修改)
  • 基本使用的话,学习成本不大,基本去找一些案例配置文件去学一下就可以了
  • 进阶使用,需要写 pascal 脚本,但是功能是真的强大
  • 还有一点我感受很好的是,这个工具的支持很好,stackoverflow 上有足够的问答资源,如果还是没有你满意的,官网有一个看上去很很很简陋的论坛,但是很有用啊,我问过 2 个问题,睡一觉起来都有回应了

1.2 怎么学习 inno setup

先可以自己去搜一下 inno setup,进入官网逛一逛,下载安装一下(记得安装 unicode 版本,即括号里有 u 的版本),浏览后有几个基本认知需要具备:

  • inno setup 是完全根据配置文件(.iss)来构建安装程序的,你用 GUI 其实也是去编写 .iss 文件,然后利用这个配置构建的
  • inno setup 可以用 pascal 脚本控制安装向导的行为,这是进阶的使用方式,足够你安装自己的设想优化安装程序了
  • inno setup 构建出来的安装包运行时可以添加参数,使安装有不同的表现,比如完全静默的后台安装(Amazing,这里的参数对于自动更新很有用)

有了上面的几点认知,可以给出「学习和使用 inno setup 路径」的建议:

  1. 下载安装后,找几篇 inno setup GUI 使用教程,尝试构建一个安装包(要可以安装的)
  2. 找一些 inno setup 配置文件的案例,对于 inno setup 配置方式有一个印象,分多个[section],每个[section]有很多配置项,每个配置项可能有多个字段
  3. 可以把 inno setup 官方文档 浏览一遍,跳过「pascal scripting」部分
  4. 到这里,你应该能看得懂他人的 .iss 文件里除了 [code] 这个 section 外的配置了
  5. 把安装向导的语言换成中文(先要导入中文语言包,再改配置,具体做法也有一些文章说到了,不多说,这一步对于你之后步骤也是有用的)
  6. 可以尝试正式结合到你的 gulp 工作流了

1.3 怎么结合到 gulp 工作流中

所用工具:修改后的 gulp-inno

如果按照之前的步骤花了个把小时大概学习了下 inno setup 的话,那么到这里你应该可以尝试把 inno setup 构建安装包做到你的 gulp 工作流中了,如果还不熟悉 inno setup 配置文件,没关系,你可以从仿照开始,不要怂,就是干,都到这一步了,谁怂谁尴尬。

配置文件的详解不是这里的重点,所以不再展开,把 inno setup 整合进脚本中,因为它本身提供命令行工具,勤快和好学的你可以根据官方或其他渠道的指导自己封装一个 node 模块,而我就比较懒了,搜到一个已有的 gulp 插件 —— 「gulp-inno」,高兴地一匹。

然而,事情总不会那么顺利,该吃的shi躲不掉,该经历的坑绕不过,这才叫「历shi」。我利用「gulp-inno」根据其指导怎么都不能正确编译,大概提示是有不合法的字符的意思。

明白了,绝壁是「gulp-inno」里包的 inno setup 不是 unicode 版本,所以一旦有中文等字符,就出错了,我看到这个包里的 inno 文件夹完全就是和我的 inno setup 文件夹没差嘛,于是我把我本地安装的 inno setup 文件夹里内容复制替换到 gulp-inno 的 inno 的文件夹内,问题解决。

因为我之前导入过中文语言包,所以我复制过去的时候,中文语言包也复制过去了,可以愉快地配置安装向导界面为中文了。

一旦修改好「gulp-inno」包(替换成 unicode 版本 & 加入简体中文语言包),就可以怎么操作:


// 1. 准备 iss 文件:填充你的 iss 配置文件模板,并输出到 dist 目录下

const appInfo = require('./app/package.json'); // 所有和应用相关的信息从 package.json 读取
const bom = require('gulp-bom'); // 这是为了解析中文的
const outputName = 
`${appInfo.name}-${platform}-${appInfo.version}-${process.env.NODE_ENV}`;
const outputIssName = 
`${appInfo.name}-${platform}-${process.env.NODE_ENV}.iss`

gulp
    .src(`./build_resource/installer_win_config_${platform}.iss`)
    .pipe(bom())
    .pipe(replace('${version}', appInfo.version))
    .pipe(replace('${appExe}', `${appInfo.name}.exe`))
    .pipe(replace('${sourcePath}', `${appInfo.name}-${platform}`))
    .pipe(replace('${outputName}',outputName))
    .pipe(rename(outputIssName))
    .pipe(gulp.dest('./dist'))
    .on('end', () => {
        // .iss file is ready
    })

// 2. 交给 inno setup

const inno = require('my-gulp-inno'); // 修改后的 gulp-inno

gulp
    .src(`./dist/${outputIssName}`)
    .pipe(inno())
    .on('end', () => {
        // you have an installer now
    });

1.4 未来可以做什么

当时还有一个看中 inno setup 的理由是,它可以让我们定制我们的安装向导步骤和外观,也就是说你可以让你的应用也像其他一些优秀的产品一样,在安装的时候可以定制酷炫的外观,可以优化安装流程,支持一键安装,inno setup 还是可以玩出一些花样的,enjoy。

1.5 对安装包也进行代码签名

同样的,安装包也需要代码签名,利用之前封装的签名方法进行签名就行了。

2. Mac 下的构建 dmg 安装包

所用工具:appdmg

相比于 windows 的安装包构建,Mac 下的构建安装包又是美滋滋啊,你看我下面小标题都没有就知道了。

// 因为 appdmg 在 windows 下不能下载安装的,所以放在外部 package.json 的 optionalDependencies 下
// 在 gulp 脚本中需要做 try...catch 处理,否则当你回到 windows 下使用这份 gulp 时会出报错
let appdmg;
try {
    appdmg = require('appdmg');
} catch (err) {
    appdmg = null;
}

const dmg = appdmg({
    // 打出的目标 dmg
    target: `dist/balabala.dmg`,
    // 基准目录,以下的资源都基于这个目录
    basepath: __dirname,
    // 具体的选项
    specification: {
        // dmg 打开后的窗口名字
        // 注意不要给中文,给中文会导致下面的 background 无效,不明白, github 上也有人提了这个 issue
        title: `myapp`,
        // dmg 挂载后的图标,出现在桌面上
        icon: "xxx.icns",
        // 背景图,如果同时存在 bg.png 和 bg@2x.png,appdmg 会根据用户屏幕自己找合适的图
        background: "bg.png",
        // 里面所有icon的尺寸
        'icon-size': 96,
        // 窗口设置
        window: {
            size: {
                width: 550,
                height: 320
            }
        },
        // 里面的内容,x 是指这个 icon 中心距离窗口最左边的距离,y 是指这个 icon 中心距离窗口顶部的距离
        // 这里可以指定一个name项,不要给中文,会导致图标异常
        contents: [
            { "x": 400, "y": 128, "type": "link", "path": "/Applications" },
            { "x": 150, "y": 128, "type": "file", "path": "你的应用.app" }
        ],
        // 对 dmg 进行代码签名
        'code-sign': {
            'signing-identity': '你的代码签名证书'
        }
    }
});
dmg.on('finish', function () {
    // you have a dmg now
});
dmg.on('error', function (err) {
    // error
});

其余的配置和所以配置影响的内容可以参加 appdmg githug 主页,然后就是自己试试看了。

七、发行环节

目的:使应用可以被下载(上一步只是能被安装,但并不能被下载)

做的事:重命名应用安装包供发行,上传应用安装包到云存储服务器供下载

这一步根据每个人使用的云存储方式不同而需要利用卖方提供的 API 编写合适的脚本去上传你的安装包,因此具体的脚本不做展开,只是有几点最佳实践可以参考:

  • 上传前,把你的安装包文件重命名成符合一定规范的,可能是「应用名-版本-阶段-系统-尾数」,可能是「应用名-版本-系统-构建号」,可能是...这个就自己定,但一定要有一个合适的命名,这样一看到名字就知道这个是啥,不会弄错
  • 你的 OSS 服务器上要针对应用安装包的不同阶段建立不同的文件夹,一方面可以方面管理,另一方面也便于做权限管理

当你上传了你的安装包后,也就意味着这个安装包有了一个下载链接,你可以分发这个链接供用户下载啦,至此终于走完了「代码」到可下载「安装包」的过程,鼓掌。

八、路漫漫

这一路走来看上去已经很有成就感,但实际上还有许多事可以做得更好,不过工程化的东西,逻辑清晰、流程自动化、能满足需求就可以了,而搭好工程,我们需要开始专注于 Electron 应用的功能开发了,才刚刚要迈上红地毯,路还有很长,下期见。

附:gulp 文件和脚本看上去会是怎样的

对之前的工作流做一个小结(如果遇到有一些旧文件覆盖不了,可以自己加一个清理环节或方法,去清理旧文件)


/* gulpfile.js START */
// 此处省略一堆需要引入的依赖

// 工作参数
let platform = 'win32';
let arch = 'ia32';
let needSign = false;

// 配置环节
gulp.task('env', (cb) => {
    // ...
});

// 开发调试
gulp.task('dev', ['env'], (cb) => {
    exec('electron app --debug', (err) => {
        if (err) return cb(err);
        cb();
    });
});

// 打包环节
gulp.task('pack',['env'], (cb) => {
    if (platform === 'darwin') {
        // ...
    } else {
        // ...
    }
});

// 签名环节
gulp.task('sign-pack', ['pack'], (cb) => {
    if (needSign) {
        if (platform === 'win32' || platform === 'win64') {
            // ...
        } else if (platform === 'darwin') {
            // ...
        }
    } else {
        cb();
    }
});

// 构建环节
gulp.task('build', ['sign-pack'], (cb) => {
    if (platform === 'darwin') {
        // ...
    } else {
        // ...
    }
});

// 发行环节
gulp.task('release', ['build'], (cb) => {
    // ...
});

const codeSignForWin = (filePath) => {...};

const codeSignForMac = (filePath) => {...};

const detectPlatform = () => {...};
/* gulpfile.js END */


// package.json 中配脚本
 "scripts": {
    "yarnall": "yarn && (cd app && yarn)",
    "start": "cross-env NODE_ENV=dev gulp dev",
    "packDev": "cross-env NODE_ENV=dev gulp pack",
    "packAlpha": "cross-env NODE_ENV=alpha gulp pack",
    "packProd": "cross-env NODE_ENV=prod gulp pack",
    "buildDev": "cross-env NODE_ENV=dev gulp build",
    "buildAlpha": "cross-env NODE_ENV=alpha gulp build",
    "buildProd": "cross-env NODE_ENV=prod gulp build",
    "releaseDev": "cross-env NODE_ENV=dev gulp release",
    "releaseAlpha": "cross-env NODE_ENV=alpha gulp release",
    "releaseProd": "cross-env NODE_ENV=prod gulp release"
  }
// 可选命令行参数:
//      sign: 是否签名
//      platform: 系统平台
//      arch: 系统位数
查看原文

airyland 回答了问题 · 2018-04-29

小白请教各位大神:能不能在Weex中使用VUX库

不支持。因为要做很多适配,没有精力。

关注 5 回答 5

airyland 回答了问题 · 2018-04-25

解决vux 加载 drawer 空白

如文档所写
1.你不能直接在单页面里使用,要在 App.vue 使用,router-view 放在 default slot 里
2.drawer 里你也并没有使用 slot=drawer 定义菜单内容,也没有任何可以触发菜单显示的操作按钮,不显示任何东西也是正常的
3.参考 https://github.com/airyland/v...

关注 3 回答 3

airyland 收藏了文章 · 2018-02-11

每周一点canvas动画——《支付宝价格拖动选择》

效果源码

终于到年底了,再过两天我也要回家过年了,想想就激动呢!今天给大家带来一个基于移动端的canvas价格选择效果。

图片描述

主要功能就是拖动标尺变动价格。而且支付宝和京东金融的里也有这样的效果(果然天下设计都是你抄我我抄你啊?)。

效果演示地址

1.实现思路

整个效果的核心就是用canvas绘制的标尺。一共包括标尺主体,数字,和中间固定不变的标定轴,这几个部分都用canvas绘制。最上面的大号价格文字,因为其他地方会需要用它来计算相关的收益。所以,我们就用个DOM来呈现,这样比较方便获取。

标尺拖动的距离与价格之间有有一个映射关系,是整个效果最不好处理的部分,在具体处理到相关问题的时候我们再做分析。现在,我们先实现基础的标尺绘制。

2.标尺属性定义

我们先定义一个类叫Rule.js, 其具体属性如下。

图片描述

现在我们来了解一下每个属性的含义:

  1. x, y: 标尺的坐标位置
  2. vx: 标尺的移动速度
  3. ax: 标尺移动加速度
  4. color: 绘制标尺线条的颜色,与文字颜色
  5. scaleX, scaleY: 缩放比
  6. markShort, markLong: 标尺长短线的长度
  7. textHeight: 文字距离标尺主体的高度
  8. min, max: 要展示的最大值和最小值
  9. width: 标尺的像素宽度
  10. step: 步长
  11. seg: 段数
  12. pxStep: 在canvas上的实际步长(单位为px)
  13. minPxStep: 每个pxStep分10小段,每小段的实际像素宽度
  14. lineBottom: 底部横线参数
  15. lineRed: 标定轴参数

参数比较多,但真正需要传入的参数其实并不是很多。这里我讲解一下(8)~(15)这几个参数的思路。

min, max : 参数的作用是设置需要显示的最大金额和最小金额。这两个参数是外部传入的,比如设定用户最小能存100元,最大能存100000万元。那么min和max就分别对应100和100000。

width : 是整个标尺的实际屏幕长度,比如你只想标尺绘制1000px,那这里就传1000就好了。

step : 步长的含义就是每隔多少分一段,比如我们设定的最大金额为10000元, 那设置step为1000就意味着,每隔1000元表示一个小段,这也是canvas上标尺刻度需要绘制的数据。

seg : 段数等于总金额max除以step。

pxStep : 为真正映射到canvas上的像素步长。

miniPxStep : 每个pxStep分为10小段,每小段的像素距离。

lineBottom : 独立出来不和标尺刻度一起绘制,在绘制标尺的底部横线时,我是这样想的。底部横线的宽度其实就是canvas的宽度,没必要从标尺的初始画到标尺的结尾。而且为了用户体验,刻度的初始位置和结束位置都位于整个canvas的中心。所以,如果合在一起绘制,你需要先绘制一段没刻度的横线,然后再绘制刻度,到最后还要绘制一段没刻度的横线。这给无疑让绘制和后续的标尺移动变得相当麻烦。所以我把它抽出来,就是一条贯穿canvas的普通横线。

lineRed : 标定轴,始终在canvas的中间,也独立出来不和标尺刻度一起绘制。

属性都有了,下面添加一个draw方法,把我们的标尺绘制出来。

2.标尺绘制

a) 绘制标尺刻度部分
图片描述
这里有个截图错误,应该是i+=this.miniPxStep。这应该不难理解,就是每隔miniPxStep绘制一次线段,线段的类型根据n这个变量来确定。

b) 绘制标尺文字部分

图片描述

文字的绘制不能以真实的屏幕像素为准,必须映射到金额上,所以,这里绘制的数字是(n/10)* this.step。同时,还做了一个特殊的处理,就是初始值是1,不是0。因为,我们的金额不允许输入0元。如果你不需要这个,把这里注释掉就ok了。

c) 绘制底部横线

图片描述

d) 绘制标定轴

图片描述

这样整个标尺就完成了,rule.js文件在顶部的github中。现在我们调用一下这个文件,看看画出来的效果怎样。

图片描述

这里我们设置了最大额度为100000元,最小额度为500元。整个标尺的长度为5000px,步长step为1000元。效果图如下:

图片描述

让标尺偏移个200px, 比如设置: x: ruleX - 200, 效果如下:

图片描述

设置步长step为500,效果如下:

图片描述

ok,现在静态标尺就绘制完成,下一步就要完成交互功能,让标尺能够跟随鼠标滚动,并且展示当前拖动的金额。

3.拖动标尺

现在我们开始实现标尺的拖动。标尺的拖动原理很简单,就是让标尺的位置跟随鼠标移动。这里为了演示方便我换成了鼠标事件,到移动端换成touch事件即可。

首先引入我们的工具函数utils.js文件,然后定义几个变量。
图片描述

isMouseDown用来判断鼠标是否抬起, oldX用来记录上一次拖动的位置,mouse是使用captureMouse返回的对象,返回鼠标在canvas上的当前位置信息。

然后,监听canvas的鼠标事件mousedown, mouseup, mousemove。并改变rule的位置。

图片描述

当鼠标按下时,isMouseDown变为true, offsetX在上面忘记写了,它的作用是记录鼠标按下的位置与标尺位置之间的偏移量。然后在鼠标移动时标尺的位置rule.x = mouse.x - offsetX。如果不这样做,在点击canvas并拖动标尺的一瞬间,你会发现标尺的初始位置会瞬移到鼠标点击位置,这样体验很不好,我们需要不管点击哪,标尺都会在现有的位置跟随鼠标移动。如果,无法体会,动手试一试去掉回事什么效果。

oldX也很好理解,就是记录标尺上一次的位置,这里还没有用到它,后面可能会用到。
现在我们把标尺的绘制写进动画函数中

图片描述

看看动画效果如何。

图片描述

ok,现在我们已经实现了标尺跟随鼠标的拖动。下一步,我们就把拖动的金额显示出来。

4.金额显示

首先,增加一个input输入框,然后获取它。

图片描述

这里设置了输入框的最小值为标尺的最小额度,这里可以先不用管它。我们主要看onMouseMove函数

图片描述

注意money的计算值,它为(centerX - rule.x)*rule.ratioScale(centerX - rule.x)比较好理解,因为,我们的标尺是从canvas的中心点绘制的。但rule.ratioScale在最开始的构造函数中并没有定义。这里需要在构造函数中加上,它的含义是每像素代表多少钱,可以认为是图形比例尺。

this.ratioScale = Math.floor(this.max / this.width) //比列尺

那么自然,移动距离乘以比例尺就得出钱数了。我们看看效果。

图片描述

注意到上面的效果中金额显示出现了负数,所以我们需要对移动范围做限制。让其只能在限定的最大和最小金额之间移动。

5.移动范围限定

对一定范围的限定主要分为两部分。一、标尺范围的限定。二、金额显示的限定。这两部分我们放在一起做。

1)重设标尺的初始位置
假设我们设置的最小金额为500元,那么初始标定轴的位置应该就是500元的位置。所以初始化标尺的位置后,我们给它重置为最小金额的位置。这时候需要把金额换算一下。

rule.x = centerX - rule.min / rule.ratioScale;

就是把金额值得计算倒一下。

2)限定标尺的移动范围

图片描述

这里定义了一个检测边界值得函数,当金额小于最小投资金额时,标尺的位置为初始位置start(注意这个初始位置是已经被重置过的), 并且设置金额为最小额度。最大位置同理。
然后,在onMouseMove中调用。

图片描述

看看效果图。

图片描述

6.输入金额移动标尺

标尺的移动除了拖动以外,我们也希望通过金额输入框来达到。即输入金额,标尺便移动到目标金额的位置。

图片描述

同时我们也做了边界限定,当输入的金额小于或者大于设定值时会,设置标尺的位置和输入框的显示为边界值,看看效果。

图片描述

7.来个速度吧

现在拖动的还比较不自然,我们想要手指离开后标尺还会继续移动,直到速度慢慢减为0。为此,新建两个变量。

var speed = 0, fl = 0.95; //初始速度, 摩擦系数

新建一个move函数,在动画循环中调用。

图片描述

图片描述

至此,拖动输入的核心功能就开发完了。如果你要在项目中使用,另一个需要注意的事情是canvas在移动端的模糊问题,这个已经有了很多的解决方案,你只需要耐心调试就好。最后还是祝大家新年快乐,源码在头部地址哦。

查看原文

airyland 回答了问题 · 2017-12-14

微信公众平台中的,模板消息里,没有找到编号 OPENTM200605630 的模板,行业选的是 IT 科技 互联网|电子商务?

早期过于宽泛的模板会被微信逐步下线,已经在使用的用户不影响,但是其他用户无法再添加。

关注 1 回答 1

airyland 回答了问题 · 2017-11-27

使用vux 报_vm.$t is not a function 错误。还说vux-loader 不是最新版本!

哥,你发完 issue 再来这里发。文档已经写得很清楚了,你首次使用建议你用 vux2 模板初始化,你非要自己折腾也得参照文档配置 webpack 配置吧?

关注 3 回答 3

认证与成就

  • 获得 416 次点赞
  • 获得 83 枚徽章 获得 7 枚金徽章, 获得 38 枚银徽章, 获得 38 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2011-04-30
个人主页被 4.1k 人浏览