CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

关于本书

许可

本书使用署名-非商业性使用-相同方式共享4.0许可。你不需要为本书付费。你可以自由的拷贝、发布、修改或展示本书。但是,我要求本书必须用我本人(Karl Seguin)的署名,同时不能作为经济用途。

你可在以下链接中查看到该许可的所有内容:

http://creativecommons.org/licenses/by-nc-sa/4.0/

最新版本

本书最新源码的放在:http://github.com/karlseguin/the-little-go-book

中文译本最新源码的放在:http://github.com/enderjo/the-little-go-book

为什么

自己用Hexo来做博客而用到了nodejs和npm。但npm生成的node-modules层次过深,时常打开文件夹时会很慢。

后来了解到了yarn,在yarn发布之前,所有Nodejs开发者用的都是npm包管理工具,而npm工具存在挺多难以忍受的诟病,包括安装速度慢、每次都要在线重新安装等问题,而yarn也是为了解决npm当前所存在的问题而出现的。

为了方便自己使用,整理了下安装和基本操作。

安装

  • 如果原先有npm工具的话,安装yarn很简单,只需要一行命令即可:
1
npm install -g yarn

更换安装源

安装yarn之后默认的包安装源是https://registry.yarnpkg.com,可用查看命令

1
yarn config get registry 

若想提高yarn安装的速度,可将包安装源修改为cnpm的安装源,执行以下命令即可

1
2
3
4

yarn config set registry https://registry.npm.taobao.org --global
yarn config set disturl https://npm.taobao.org/dist --global

操作及对比

操作 npm yarn
初始化项目 npm init yarn init
安装依赖操作 npm install/link yarn install/link
安装某个依赖,并且默认保存到package npm install xxx —save yarn add xxx
移除某个依赖项目 npm uninstall xxx —save yarn remove xxx
安装某个开发时依赖项目 npm install –save -dev xxx yarn add xxx —dev
更新某个依赖项目 npm update –save xxx yarn upgrade xxx
安装某个全局依赖项目 npm install -g xxx yarn global add xxx
发布/登录/登出,一系列NPM Registry操作 npm publish/login/logout yarn publish/login/logout
运行某个命令 npm run/test yarn run/test
查看帮助 npm -h yarn -h

注意事项

yarn global

yarn 的全局安装并不是加 -g 或者 –global 这样的参数,它使用 yarn global 命令。用 yarn global –help 可以看到子命令列表

这些子命令的用法和非 global 的同名命令用法差不多。

指定bin目录

对于一些带 CLI 的模块,通过 yarn global add 可能会出问题,yarn global bin查看目录,并把该目录添加到Path中。

指定registry但没有效果

  1. yarn.lock中包含的registry信息与设置的registry不同,删除yarn.lock重新生成即可。

参考

yarn —— Nodejs包新管理工具

使用 yarn global 代替 npm -g

“yarn config set registry” is not work · Issue #4862 · yarnpkg/yarn (github.com)

背景

最近刚好在做小程序的开发,需要用到本地调试,刚开始时使用Natapp的免费遂道,勉强能开发(域免),后面买了付费遂道,域名不变开发顺畅起来。但要真机调试时还是不行,微信不认三级域名,需要购买他的二级域名服务。转念一想还不如自己搭建一个呢,成本可控,也只要折腾一次就好,反正也能学东西。于是也就有了本文。

准备

  • 一台云主机,可在购买阿里云,开放对应端口。
  • 一个已经备案的域名(备案过程真是个痛苦的过程,各种资料,各种步骤,还可能审不过),并且做了二级域名泛解析。(本文假设你已经会域名解析配置)
  • Go http tunnel,可在这里下载(https://github.com/mmatczuk/go-http-tunnel/releases)
  • Let’s Encrypt工具。

配置

https证书

可参看这里这里
此处唯一要注意的就是有个域名解析的交互过程,这个在上述资料中也有说明。

记录下证书的生成位置如上文是/etc/letsencrypt/archive/newyingyong.cn,自动的生成路径可能不现。

http tunnel服务器

  • 服务器需要开放80,443,5223端口。
  • 根据自己的服务器系统下载对应版本。
  • 解压就可以了,进入目录。
  • 执行如下命令,以https证书参考资料证书地址为例。
    1
    tunneld -tlsCrt /etc/letsencrypt/archive/goldsyear.com/fullchain.pem -tlsKey /etc/letsencrypt/archive/goldsyear.com/privkey.pem

http tunnel客户端

  • 根据自己的服务器系统下载对应版本
  • 解压就可以了,进入目录。
  • 生成客户端证书openssl req -x509 -nodes -newkey rsa:2048 -sha256 -keyout client.key -out client.crt,并将证书放到解压目录
  • 创建tunnel.yml,默认读当前路径配置。
  • 参考样例:
    1
    2
    3
    4
    5
    6
    7
    server_addr: SERVER_IP:5223
    tunnels:
    webui:
    proto: http
    addr: localhost:8080
    auth: user:password
    host: goldsyear.com
    其中,server_addr为服务器地址,tunnels为开启的遂道列表,webui为实际的遂道名,正常可去掉auth配置,host中的域名与遂道名无直接关系。更多配置信息参看官网
  • 启动tunnel start-all

验证

在客户端启动最简单http服务,使用客户端配置的域名进行访问,如果正常访问,恭喜你。如果还有问题,请核查服务器或客户端配置。

推广

如果你觉得以上配置过于复杂。有以下两种方式供你选择,当然你可能有更好的选择。

  1. 可以使用Natapp,他提供免费的HTTPS的遂道,但提供的免费域名会一直变化。最好购买遂道和域名服务,付费时可以使用我的推广优惠码ADBE2C5C
  2. 如果你恰好刚起步,希望用于本地调试,可以加我微信号,也可提供相关服务支持。申请时请备注https tunnel技术支持微信

OLAP基本概念

联机分析处理(On-Line Analytical Processing,OLAP)

联机分析处理的概念最早是由关系数据库之父爱德华·库德(E·F·Codd)博士于1993年提出的,是一种用于组织大型商务数据库和支持商务智能的技术。OLAP 数据库分为一个或多个多维数据集,每个多维数据集都由多维数据集管理员组织和设计以适应用户检索和分析数据的方式,从而更易于创建和使用所需的数据透视表和数据透视图。

维(Dimension)

是人们观察数据的特定角度,是考虑问题时的一类属性,属性集合构成一个维(时间维、地理维等)。

维的层次(Level)

人们观察数据的某个特定角度(即某个维)还可以存在细节程度不同的各个描述方面(时间维:日期、月份、季度、年)。

维的成员(Member)

维的一个取值,是数据项在某维中位置的描述。(“某年某月某日”是在时间维上位置的描述)。

度量(Measure)

多维数组的取值。(2000年1月,上海,笔记本电脑,$100000)。

指标(Quota),补充

可度量的属性。一般为某种值,如费用,入院人次。

典型操作

OLAP的基本多维分析操作有钻取(Drill-up和Drill-down)、切片(Slice)和切块(Dice)、以及旋转(Pivot)等。

钻取

是改变维的层次,变换分析的粒度。它包括向下钻取(Drill-down)和向上钻取(Drill-up)/上卷(Roll-up)。Drill- up是在某一维上将低层次的细节数据概括到高层次的汇总数据,或者减少维数;而Drill-down则相反,它从汇总数据深入到细节数据进行观察或增加新维。

切片和切块

是在一部分维上选定值后,关心度量数据在剩余维上的分布。如果剩余的维只有两个,则是切片;如果有三个或以上,则是切块。

旋转

是变换维的方向,即在表格中重新安排维的放置(例如行列互换)。

ElasticSearch

Apache Lucene™可能是目前存在的,不论开源还是私有的,拥有最先进,高性能和全功能搜索引擎功能的库。但Lucene是很复杂的。 Elasticsearch是一个使用Java编写的开源的搜索引擎,它的内部使用 Lucene 做索引与搜索,但是它的目标是使全文检索变得简单, 通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。

然而,Elasticsearch不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容:

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持PB级别的结构化或者非结构化数据

Elasticsearch将所有的功能打包成一个单独的服务,这样你可以通过程序去访问它提供的简单的RESTful API服务, 不论你是使用自己喜欢的编程语言还是直接使用命令行。

Elasticsearch是一个实时的分布式搜索分析引擎, 它能让你以一个之前从未有过的速度和规模,去探索你的数据。 它被用作全文检索、结构化搜索、分析以及这三个功能的组合:

  • Wikipedia 使用 Elasticsearch 提供带有高亮片段的全文搜索,还有 search-as-you-type 和 did-you-mean 的建议。
  • 卫报 使用 Elasticsearch 将网络社交数据结合到访客日志中,实时的给它的编辑们提供公众对于新文章的反馈。
  • Stack Overflow 将地理位置查询融入全文检索中去,并且使用 more-like-this 接口去查找相关的问题与答案。
  • GitHub 使用 Elasticsearch 对1300亿行代码进行查询。

ES文档和多维数据集

ES的存储结构

ES文档通过JSON格式来表示。文档是可以被索引的基本单元,文档需要索引需要指定类型。ES能支持索引的数据类型有,其中的数组类型,对象类型和内嵌类型的支持,使得ES能存储更复杂的文档。

多维联机分析处理(MOLAP)

MOLAP将OLAP分析所用到的多维数据物理上存储为多维数组的形式,形成“立方体”的结构。维的属性值被映射成多维数组的下标值或下标的范围,而总结数据作为多维数组的值存储在数组的单元中。由于MOLAP采用了新的存储结构,从物理层实现起,因此又称为物理OLAP(PhysicalOLAP);而 ROLAP主要通过一些软件工具或中间软件实现,物理层仍采用关系数据库的存储结构,因此称为虚拟OLAP(VirtualOLAP)。

我们可以看到基于复杂数据类型(Complex datatypes)构建的ES文档与MOLAP的概念是非常接近的,利用ElasticSearch的聚合的能力,我们能相对简单的实现OLAP框架。

方案

要实现整个OLAP框架主要需要处理以下几个环节

  1. 数据建模
  2. 数据ETL
  3. 数据分析
  4. 数据展示

大致思路如下:
OLAP

数据建模

数据建模就是建立多维数据集的过程,同时为了能更有效的利用ES的索引,不建议过深的JSON层次,所以对应到维度建模时采用的是星形模式。

正常情况,是先进行数据建模,之后才能进行数据ETL,首先需要知道抽取的源数据的结构,即数据元数据之后,才能进一步分析数据模型。对于ES来说,实际上就是建立Index/Type的过程。

维度管理

元数据管理

元数据管理维护了原始数据的元数据结构,简单来说就是原始数据有哪些库,哪些表,表之间有什么关系。

映射管理

映射管理是指定了需要抽取的数据范围及方法和确定了多维数据集的模型。

数据ETL

正常情况,是先进行数据建模,之后才能进行数据ETL,而实际过程中,是一般需要知道源数据的结构,即数据元数据之后,才能进一步分析数据模型。

数据ETL,实质就是从各个数据源提取数据,对数据进行转换,并最终加载填充数据到数据仓库维度建模后的表中。只有当多维数据集被填充好,ETL工作才算完成。

数据加载主要分为两种,一种是首次加载,一种是刷新加载(增量加载)。当前产品中主要涉及两种数据库,Mysql和Hbase。

首次加载

通过Quartz Job或MapRedurce Job将数据转换为Kafka消息,由数据加载组件来进行数据处理。

刷新加载

通过扩展mysql-binlog-connector-java和HBase Side-Effect Processor,可以实现数据的实时增量更新。本方案中通过Kafka消息中间件来统一转发不同类型的数据更新,此处需要定义好Kafka的消息格式,以便数据加载组件能较简单的处理数据。

数据转换

转换步骤主要是指对提取好了的数据的结构进行转换,以满足目标数据仓库模型的过程。此外,转换过程也负责数据质量工作,这部分也被称为数据清洗(data cleaning)。转换过程可以通过逐步实现转换算法,动态上线的方式进行。

统计分析

统计分析其实就是从事实表中统计任意组合维度的指标,也就是过滤、分组、聚合,其中,聚合除了一般的SUM、COUNT、AVG、MAX、MIN等,还有一个重要的COUNT(DISTINCT)。ElasticSearch本来就是做实时搜索的,过滤自然不是问题,现在也支持各种聚合以及Pipeline aggregations(相当于SQL子查询的功能)。

统计分析是根据模板配置和页面操作时的条件生成ES语法,执行语法,返回结果数据的过程。从表面看,该过程不难,然而为了生成的ES语法的准确性,ES语法的生成引擎是非常重要的一个环节,通过在模板管理来增加一些配置来辅助ES的语法生成,如行维度,列维度,维的层次等等。

数据展示

通过模板的动态配置,利用Echart和Vue可以实现,动态布局和各种仪表盘的展示,这部分内容也可参看Kibana。

小结

ES的文档结构符合MOLAP的概念,它提供的聚合功能可以实现OLAP的各种操作,基于Lucene和分布式架构在性能上也能得到保障。通过定义一定的规则和开发一系列的辅助功能,可比较容易的实现一套通用的实时OLAP的框架。本文主要讲解的是基于ES构建OLAP框架的思路,而非完整的构建方案。

参考连接

起因

之前通过GithubPages和Hexo搭建了个人博客,在这里可看到搭建的方法。现在呢,希望用另一个帐号,管理一个不同内容的博客。有了之前的经验,整个搭建的过程还是比较顺利,主要碰到的问题就是代码发布。之前github帐号,是通过Windows的凭证管理来实现的,按理说Github配置了多个帐号也是可以管理的。如图:

crdential.png

但是两套博客在发布代码的时候都出现了问题。

初步方案

将用户名和密码配置在Hexo的_config.yml的配置项中,形如

1
https://enderjo:xyz@github.com/enderjo/enderjo.github.io

虽然这样配置是解决了发布问题,但密码安全问题就暴露出来了,想想之前有了解到Github有ssh功能,但也没用过,初步试了下一样发现两个帐号配置的问题,github不支持同一个公钥用于不同的帐号,会提示公钥已经被使用了,请教了下前同事事了解到ssh有个config的配置。

最终方案

基于同事的提示刚开始创建的config如下:

1
2
3
4
5
Host github:enderjo
Hostname github.com
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/github-enderjo/id_rsa

Window中在用户目录%USERPROFILE%下。之后使用ssh测试也是通的。
如下:

1
2
$ ssh -T github.com:enderjo
Hi enderjo! You've successfully authenticated, but GitHub does not provide shell access.

但测试Git Clone的时候提示无法

1
2
3
4
5
6
7
$ git clone git@github.com:enderjo/enderjo.github.io.git
Cloning into 'enderjo.github.io'...
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

参考Error: Permission denied (publickey)通过ssh-add证书的方式,能正常git clone。此时已可初步判断,git clone没有正常取到config的配置。
接着同事发来这篇文章
https://gitlab.com/gitlab-org/gitlab-ce/issues/45593
其中提到,可通过如下命令查看git clone的实际执行过程:

1
GIT_TRACE=1 GIT_SSH_COMMAND="ssh -vvv" git clone git@gitlab.example.com:my-group/my-project.git

测试后的部分代码如下:

1
2
3
4
5
$ GIT_TRACE=1 GIT_SSH_COMMAND="ssh -vvv" git clone git@github.com:enderjo/enderjo.github.io.git
14:03:51.164492 exec-cmd.c:236 trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
14:03:51.165492 git.c:415 trace: built-in: git clone git@github.com:enderjo/enderjo.github.io.git
Cloning into 'enderjo.github.io'...
14:03:51.225463 run-command.c:637 trace: run_command: unset GIT_DIR; 'ssh -vvv' git@github.com 'git-upload-pack '\''enderjo/enderjo.github.io.git'\'''

此时我们可以看到,实际执行的地址和测试的地址是不一样的。

1
2
$ ssh -T git@github.com:enderjo
$ ssh -vvv git@github.com

ssh命令是根据config文件中Host记录与ssh命令时所输入的主机名来进行匹配的,而git clone因为使用:分隔用户名。

之后查到这篇文章。config文件中的Host就是一个别名。

调整config

1
2
3
4
5
Host gh-enderjo
Hostname github.com
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/github-enderjo/id_rsa

修改git clone命令

1
2
3
4
5
6
7
$ git clone gh-enderjo:enderjo/enderjo.github.io.git
Cloning into 'enderjo.github.io'...
remote: Counting objects: 4822, done.
remote: Compressing objects: 100% (386/386), done.
remote: Total 4822 (delta 873), reused 2292 (delta 846), pack-reused 2496
Receiving objects: 100% (4822/4822), 5.20 MiB | 215.00 KiB/s, done.
Resolving deltas: 100% (1686/1686), done.

测试正常,修改Hexo的_config.yml也能正常发布代码了。

增加Mac的配置

因为之前配置过了Windows,这里我需要把Windows中的ssh配置先拷贝到Mac上(~/.ssh)。
测试,此时会提示。

1
2
3
4
5
6
7
8
Permissions 0755 for '/Users/airhead/.ssh/github-enderjo/id_rsa' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/Users/airhead/.ssh/github-enderjo/id_rsa": bad permissions
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights

需要修改权限为400(600也可以),注意修改路径

1
chmod 400 /Users/airhead/.ssh/github-enderjo/id_rsa

小结

利用ssh的config功能可以很好的管理Git的多帐号问题,但需要注意修改Git仓库的获取地址。

虽然一直有使用Git,但对于Git和ssh还是有很多不了解的地方。

参考链接

Error: Permission denied (publickey)
https://gitlab.com/gitlab-org/gitlab-ce/issues/45593
ssh-config配置
SSH Config 那些你所知道和不知道的事

因本人能力有限,理解不到位,翻译内容可能存在偏差。如果可能,请尽量读原文

We have recently completed a milestone where we were able to drop jQuery as a dependency of the frontend code for GitHub.com. This marks the end of a gradual, years-long transition of increasingly decoupling from jQuery until we were able to completely remove the library. In this post, we will explain a bit of history of how we started depending on jQuery in the first place, how we realized when it was no longer needed, and point out that—instead of replacing it with another library or framework—we were able to achieve everything that we needed using standard browser APIs.

我们最近完成了一个里程碑,就是将jQuery从我们Github.com的前端依赖库中移了。直到完全移除了jQuery库才意味着一个逐步的、长达数年的去jQuery化的结束。在这篇文章中,我们会解释为一些历史,为什么我们一开始会引入jQuery,我们是何时意识到已经不再需要jQuery了,以及找出可替代它的类库或者框架,我需要使用浏览器的标准API来达到目的。

为什么jQuery之前是有用的(Why jQuery made sense early on)

GitHub.com pulled in jQuery 1.2.1 as a dependency in late 2007. For a bit of context, that was a year before Google released the first version of their Chrome browser. There was no standard way to query DOM elements by a CSS selector, no standard way to animate visual styles of an element, and the 【XMLHttpRequest interface](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) pioneered by Internet Explorer was, like many other APIs, inconsistent between browsers.

GitHub.com在2007年底时引入了jQuery 1.2.1。因为这样的一些背景,当时距Google发布第一个版本还有一年时间。通过CSS选择器来查询DOM元素没有标准的方式,将元素视觉动画化也没有标准的样式,IE提供XMLHttpRequest接口,和其他API一样,在不同的浏览器之间也是不同。

jQuery made it simple to manipulate the DOM, define animations, and make “AJAX” requests— basically, it enabled web developers to create more modern, dynamic experiences that stood out from the rest. Most importantly of all, the JavaScript features built in one browser with jQuery would generally work in other browsers, too. In those early days of GitHub when most of its features were still getting fleshed out, this allowed the small development team to prototype rapidly and get new features out the door without having to adjust code specifically for each web browser.

jQuery使得操作DOM,定义动画,发起“AJAX”请求变得简单——基本上,它使Web开发人员能够创建出更现代、更动态的体验,这些体验在其他方面都非常突出。更为重要的是,使用了jQuery的JavaScript特性在不同的浏览器都可正常工作。在GitHub早期,它的大部分功能还在不断充实,这使得小型开发团队能够快速地进行原型,而无需为每个Web浏览器专门调整代码来实现新的特性。

The simple interface of jQuery also served as a blueprint to craft extension libraries that would later serve as building blocks for the rest of GitHub.com frontend: pjax and facebox.

jQuery的简单接口为设计扩展库提供了蓝图,GitHub.com扩展前端的基础模块: pjaxfacebox

We will always be thankful to John Resig and the jQuery contributors for creating and maintaining such a useful and, for the time, essential library.

我们将一直感激John Resig和jQuery的贡献者创建和维护了这样一个有用的,对于当时来说,必不可少的类库。

近年来的Web标准(Web standards in the later years)

Over the years, GitHub grew into a company with hundreds of engineers and a dedicated team gradually formed to take responsibility for the size and quality of JavaScript code that we serve to web browsers. One of the things that we’re constantly on the lookout for is technical debt, and sometimes technical debt grows around dependenices that once provided value, but whose value dropped over time.

这些年来,Github成长为一家拥有数百名工程师的公司,并逐渐形成了一只专门负责我们运行在Web浏览器上的JavaScript代码的大小和质量的团队。其中一件我们一直在做的事情就是找出技术债务,有时技术债务会随着曾经提供价值的附属物而增长,但其价值会随着时间的推移而下降。

When it came to jQuery, we compared it against the rapid evolution of supported web standard in modern browsers and realized:

The $(selector) pattern can easily be replaced with querySelectorAll();
CSS classname switching can now be achieved using Element.classList;
CSS now supports defining visual animations in stylesheets rather than in JavaScript;
$.ajax requests can be performed using the Fetch Standard;
The addEventListener() interface is stable enough for cross-platform use;
We could easily encapsulate the event delegation pattern with a lighweight library;
Some syntactic sugar that jQuery provides has become reduntant with the evolution of JavaScript language.
Furthermore, the chaining syntax didn’t satisfy how we wanted to write code going forward. For example:

$(‘.js-widget’)
.addClass(‘is-loading’)
.show()
This syntax is simple to write, but to our standards, doesn’t communicate intent really well. Did the author expect one or more js-widget elements on this page? Also, if we update our page markup and accidentally leave out the js-widget classname, will an exception in the browser inform us that something went wrong? By default, jQuery silently skips the whole expresion when nothing matched the initial selector; but to us, such behavior was a bug rather than a feature.

Finally, we wanted to start annotating types with Flow to perform static type checking at build time, and we concluded that the chaining syntax doesn’t lend itself well to static analysis, since almost every result of a jQuery method call is of the same type. We chose Flow over alternatives because, at the time, features such as @flow weak mode allowed us to progressively and efficiently start applying types to a codebase which was largely untyped.

All in all, decoupling from jQuery would mean that we could rely on web standards more, have MDN web docs be de-facto default documentation for our frontend developers, maintain more resilient code in the future, and eventually drop a 30 kB dependency from our packaged bundles, speeding up page load times and JavaScript execution times.

Incremental decoupling
Even with an end goal in sight, we knew that it wouldn’t be feasible to just allocate all resources we had to rewriting everything from jQuery to vanilla JS. If anything, such a rushed endeavor would likely lead to many regressions in site functionality that we would later have to weed out. Instead, we:

Set up metrics that tracked ratio of jQuery calls used per overall line of code and monitored that graph over time to make sure that it’s either staying constant or going down, not up.

Graph of jQuery usage going down over time.

We discouraged importing jQuery in any new code. To facilitate that using automation, we created eslint-plugin-jquery which would fail CI checks if anyone tried to use jQuery features, for example $.ajax.

There were now plenty of violations of eslint rules in old code, all of which we’ve annotated with specific eslint-disable rules in code comments. To the reader of that code, those comments would serve as a clear signal that this code doesn’t represent our current coding practices.

We created a pull request bot that would leave a review comment on a pull request pinging our team whenever somebody tried to add a new eslint-disable rule. This way we would get involved in code review early and suggest alternatives.

A lot of old code had explicit coupling to external interfaces of pjax and facebox jQuery plugins, so we’ve kept their interfaces relatively the same while we’ve internally replaced their implementation with vanilla JS. Having static type checking helped us have greater confidence around those refactorings.

Plenty of old code interfaced with rails-behaviors, our adapter for the Ruby on Rails approach to “unobtrusive” JS, in a way that they would attach an AJAX lifecycle handler to certain forms:

// LEGACY APPROACH
$(document).on(‘ajaxSuccess’, ‘form.js-widget’, function(event, xhr, settings, data) {
// insert response data somewhere into the DOM
})
Instead of having to rewrite all of those call sites at once to the new approach, we’ve opted to trigger fake ajax* lifecycle events and keep these forms submitting their contents asynchronously as before; only this time fetch() was used internally.

We maintained a custom build of jQuery and whenever we’ve identified that we’re not using a certain module of jQuery anymore, we would remove it from the custom build and ship a slimmer version. For instance, after we have removed the final usage of jQuery-specific CSS pseudo-selectors such as :visible or :checkbox, we were able to remove the Sizzle module; and when the last $.ajax call was replaced with fetch(), we were able to remove the AJAX module. This served a dual purpose: speeding up JavaScript execution times while at the same time ensuring that no new code is created that would try using the removed functionality.

We kept dropping support for old Internet Explorer versions as soon as it would be feasible to, as informed by our site analytics. Whenever use of a certain IE version dropped below a certain treshold, we would stop serving JavaScript to it and focus on testing against and supporting more modern browsers. Dropping support for IE 8–9 early on allowed us to adopt many native browser features that would otherwise be hard to polyfill.

As part of our refined approach to building frontend features on GitHub.com, we focused on getting away with regular HTML foundation as much as we could, and only adding JavaScript behaviors as progressive enhancement. As a result, even those web forms and other UI elements that were enhanced using JS would usually also work with JavaScript disabled in the browser. In some cases, we were able to delete certain legacy behaviors altogether instead of having to rewrite them in vanilla JS.

With these and similar efforts combined over the years, we were able gradually reduce our dependence on jQuery until there was not a single line of code referencing it anymore.

Custom Elements
One technology that has been making waves in the recent years is Custom Elements: a component library native to the browser, which means that there are no additional bytes of a framework for the user to download, parse and compile.

We had created a few Custom Elements based on the v0 specification since 2014. However, as standards were still in flux back then, we did not invest as much. It was not until 2017 when the Web Components v1 spec was released and implemented in both Chrome and Safari that we began to adopt Custom Elements on a wider scale.

During the jQuery migration, we looked for patterns that would be suitable for extraction as custom elements. For example, we converted our facebox usage for displaying modal dialogs to the element.

Our general philosophy of striving for progressive enhancement extends to custom elements as well. This means that we keep as much of the content in markup as possible and only add behaviors on top of that. For example, shows the raw timestamp by default and gets upgraded to translate the time to the local timezone, while , when nested in the

element, is interactive even without JavaScript, but gets upgraded with accessibility enhancements.

Here is an example of how a custom element could be implemented:

// The local-time element displays time in the user’s current timezone
// and locale.
//
// Example:
// Sep 6, 2018
//
class LocalTimeElement extends HTMLElement {
static get observedAttributes() {
return [‘datetime’]
}

attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName === ‘datetime’) {
const date = new Date(newValue)
this.textContent = date.toLocaleString()
}
}
}

if (!window.customElements.get(‘local-time’)) {
window.LocalTimeElement = LocalTimeElement
window.customElements.define(‘local-time’, LocalTimeElement)
}

One aspect of Web Components that we’re looking forward to adopting is Shadow DOM. The powerful nature of Shadow DOM has the potential to unlock a lot of possibilities for the web, but that also makes it harder to polyfill. Because polyfilling it today incurs a performance penalty even for code that manipulates parts of the DOM unrelated to web components, it is unfeasible for us to start using it in production.

Polyfills
These are the polyfills that helped us transition to using standard browser features. We try to serve most of these polyfills only when absolutely necessary, i.e. to outdated browsers as part of a separate “compatibility” JavaScript bundle.

背景

公司IM项目采用个推进行消息推送,之前使用Hbuilder在线打包的方式集成。这次采用Hbuilder插件开发并离线打包的方式新增视频功能(视频使用WebRTC协议),其中的呼入消息需要使用到个推的推送(主要考量就是复用,减少开发量)。
通过个推官网的文档Android Studio快速集成(推荐),能很快的将个推SDK集成进来。

实现及问题

业务逻辑

  1. 视频发起者,发起视频邀请。
  2. 服务器接到请求后,推送个推透传消息给视频接收者。
  3. 视频接收者,弹出的呼入页面,点击接收开始视频会话,或拒绝视频邀请。

    实现

    需要继承自com.igexin.sdk.GTIntentService的类,用于接收CID、透传消息。以下为对应的事件回调方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class DemoIntentService extends GTIntentService {
    @Override
    public void onReceiveMessageData(Context context, GTTransmitMessage msg) {
    //省略其他业务逻辑
    Intent intent = new Intent(getBaseContext(), XxxActivity.class);
    getApplication().startActivity(intent);
    }

    @Override
    public void onReceiveClientId(Context context, String clientid) {
    Log.e(TAG, "onReceiveClientId -> " + "clientid = " + clientid);
    //省略注册个推与用户信息
    }
    }

    问题

    正常情况,此时应该能弹出指定的XxxActivity页面,然而并未弹出。经过一段资料查找和测试验证。需要增加Intent.FLAG_ACTIVITY_NEW_TASK的设置,代码如下:
    1
    2
    3
    4
    5
    6
    7
    @Override
    public void onReceiveMessageData(Context context, GTTransmitMessage msg) {
    //省略其他业务逻辑
    Intent intent = new Intent(getBaseContext(), XxxActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    getApplication().startActivity(intent);
    }

    FLAG_ACTIVITY_NEW_TASK

    int FLAG_ACTIVITY_NEW_TASK
    If set, this activity will become the start of a new task on this history stack.

如果设置了FLAG_ACTIVITY_NEW_TASK,页面会被置顶。从文档来看主要还是对Android开发的基础知识不够了解。

小结

算是Android开发中的一个小坑吧,记录下来,方便日后查找。希望本文对采用类似方案的朋友有所帮助。

参考

个推-点击推送跳转至指定页面(透传)
Intent

准备

官方文档为兼容各个版本,看起来比较杂乱,不容易读懂。自己根据实际的开发情况,整理了本文。本文参考官方文档Android平台第三方插件开发指导, 对官方文档有理解不到位之处未能说明清楚,请参看官方文档。

下载最新版本5+SDK,下载地址, 将SDK解压到任意目录,目录结构如下,以下统称为5+SDK目录。

术语字典

JS Plugin Bridge

H5+ Plugin Bridge层JS部分API,插件调用者通过调用API,触发Native层扩展插件相应方法的调用。

Native Plugin Bridge

H5+ Plugin Bridge层Native部分API,插件开发者通过实现接口类方法,实现扩展插件业务逻辑。插件开发者调用API,实现Native扩展方法运行结果的返回。

Native层扩展插件

开发者使用原生语言实现的5+扩展插件,可被JS层通知调用。

插件类别名(插件别名)

读时需要按插件类|别名的方式来断句。JS层字符串,用来声明JS层和Native层插件的对应关系。

技术架构

HTML5+ 基座扩展采用三层结构,JS层、PluginBridge层和Native层。 三层功能分别是: 

  • JS层: 在Webview页面调用,触发Native层代码,获取执行结果。 
  • PluginBridge层: 将JS层请求进行处理,触发Native层扩展插件代码。 
  • Native层: 插件扩展的平台原生代码,负责执行业务逻辑并执行结果返回到请求页面。

插件开发者在开发扩展插件时需要为扩展插件编写对应的JS API,JS API将在HTML页面中触发调用对应Native扩展方法的请求,并负责接收运行结果。

插件调用者(也可能为插件调用者)通过调用Javascript Plugin Bridge的API用来完成对Native层代码的调用和运行结果的返回。

在实际应用中,插件开发者可以根据扩展方法的实际需求不同,提供同步或者异步JS API,插件调用者根据JS API将设置为同步执行或异步执行。

扩展插件工作流程

同步执行

同步执行的扩展方法会阻塞当前JS代码的执行,直到Native层插件扩展方法执行完毕。

异步执行

异步扩展方法不会阻塞当前JS代码的执行,插件调用者需要设置回调方法接收Native层返回的执行结果。插件开发者需要在插件中调用 Native plugin brigde的方法将执行结果返回到请求页面。

工程示例请参考SDK内包含的HBuilder-Integrate-AS工程,工程里已经整合了插件开发和集成方式的示例。

插件实现方式

创建插件类

创建一个继承自StandardFeature的类,实现第三方插件扩展。

创建插件类需要引入的包 

1
2
3
importio.dcloud.common.DHInterface.IWebview;
importio.dcloud.common.DHInterface.StandardFeature;
importio.dcloud.common.util.JSUtil;

实现扩展方法

插件初始化

重写onStart方法,需要设置dcloud_properties.xml的Service节点,该方法才会被调用。

1
public void onStart(Contextcontext,Bundlebundle,String[]strings)

扩展插件方法声明

Native层扩展插件的方法声明如下:

1
public void methodName(IWebview webView, JSONArray array)

参数说明

参数名 类型 说明
webView IWebview 发起请求的webview
array JSONArray JS请求传入的参数

只有符合以上函数声明的方法,才可被JS层调用。

决定执行方法

插件开发者对返回值的不同调用方式决定了JS API的执行方法。不同的执行方法对入参array(JSONArray)的要求也会有所有不同。

同步执行方法

插件开发者通过JSUtil.wrapJsVar返回时为同步执行。同步执行方法在返回结果时可以直接将结果以return的形式返回给js层。

方法声明
1
String wrapJsVar(String value);
参数说明
参数名 类型 说明
value String 要返回到JS层的值

查看io.dclod.util.JSUtil了解更多的返回值类型的处理(boolean,Number, String, JSONArray, JSONObject)。

示例代码
1
JSUtil.wrapJsVar("Html5 Plus Plugin Hello1!");

异步执行方法

插件开发者调用JSUtil.execCallback返回时为异步执行。通过CallbackId实现回调函数的关联处理,对于CallbackId的传入由插件开发者具体约定入参array(JSONArray)中的位置,通常使用第一个。

方法声明
1
String execCallback(IWebview pWebView, String pCallbackId, String pMessage, int pStatus, boolean isJson, boolean pKeepCallback);
参数说明
参数名 类型 说明
pWebView IWebview 扩展插件方法运行的窗口
pCallbackId String 回调函数的唯一标识
pMessage String 回调函数的参数,即返回结果
pStatus int 操作是否成功,成功则使用JSUtil.OK,否则使用错误代码
isJson boolean 回调函数参数是否为JSON数据
pKeepCallback boolean 是否可多次触发回调函数
示例代码
1
JSUtil.execCallback(pWebview, cbId, (which==AlertDialog.BUTTON_POSITIVE)?"ok":"cancel", JSUtil.OK, false, false);

完整代码示例

该示例只展示同步或异步方法,完整内容参考官网或者SDK中示例工程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.H5PlusPlugin;
import io.dcloud.common.DHInterface.IWebview;
import io.dcloud.common.DHInterface.StandardFeature;
import io.dcloud.common.util.JSUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class PGPlugintest extends StandardFeature
{
public void PluginTestFunction(IWebview pWebview, JSONArray array)
{
String CallBackID = array.optString(0);
JSONArray newArray = new JSONArray();
newArray.put(array.optString(1));
newArray.put(array.optString(2));
newArray.put(array.optString(3));
newArray.put(array.optString(4));
JSUtil.execCallback(pWebview, CallBackID, newArray, JSUtil.OK, false);
}
public String PluginTestFunctionSync(IWebview pWebview, JSONArray array)
{
String inValue1 = array.optString(0);
String inValue2 = array.optString(1);
String inValue3 = array.optString(2);
String inValue4 = array.optString(3);
String ReturnValue = inValue1 + "-" + inValue2 + "-" + inValue3 + "-" + inValue4;
return JSUtil.wrapJsVar(ReturnValue);
}
}

插件调用方法

设置插件类别名

插件调用者在实现JS API时首先要定义一个插件类别名,在Android工程的assets\data\dcloud_properties.xml文件中声明插件类别名和Native层扩展插件类的对应关系。feature节点下声明的插件将会在调用时创建相应的对象。

1
2
3
4
5
<properties>
<features>
<feature name="plugintest" value="com.example.H5PlusPlugin.PGPlugintest"></feature>
</features>
</properties>

如果开发的插件有应用启动时初始化的需求,需要同时配置service节点。

1
2
3
4
5
<properties>
<services>
<service name="plugintest" value="com.example.H5PlusPlugin.PGPlugintest"></service>
</services>
</properties>

调用方式实现

同步调用

插件调用者需要调用JS Plugin Bridge的window.plus.bridge.execSync()方法,该方法可同步获取Native插件返回的运行结果。

函数声明
1
void plus.bridge.execSync( String service, String action, Array<String> args );
参数说明
参数名 类型 说明
service String 插件类别名
action String 调用Native层插件方法名称
args Array 参数列表

异步调用

插件调用者需要调用JS Plugin Bridge的plus.bridge.exec()方法,该方法会通知Native层插件执行指定方法,运行结果会通过回调的方式通知JS层。

函数声明
1
void plus.bridge.exec( String service, String action, Array<String> args );
参数说明
参数名 类型 说明
service String 插件类别名
action String 调用Native层插件方法名称
args Array 参数列表
为了接收和处理结果,插件调用者需要调用window.plus.bridge.callbackId()生成callbackId,并通过args( Array )传入。对JS不是很了解,个人认为应该类似一个Map(回调函数表),提供回调函数注册。

实际使用及示例代码

根据业务的需要和使用的方便,可以在JS层进行插件调用的封装使用。

封装示例

该示例只展示同步或异步方法,完整内容参考官网或者SDK中示例工程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
document.addEventListener( "plusready",  function()
{
// 声明的JS“扩展插件别名”
var _BARCODE = 'plugintest',
B = window.plus.bridge;
var plugintest =
{
// 声明异步返回方法
PluginTestFunction : function (Argus1, Argus2, Argus3, Argus4, successCallback, errorCallback )
{
var success = typeof successCallback !== 'function' ? null : function(args)
{
successCallback(args);
},
fail = typeof errorCallback !== 'function' ? null : function(code)
{
errorCallback(code);
};
callbackID = B.callbackId(success, fail);
// 通知Native层plugintest扩展插件运行”PluginTestFunction”方法
return B.exec(_BARCODE, "PluginTestFunction", [callbackID, Argus1, Argus2, Argus3, Argus4]);
},
// 声明同步返回方法
PluginTestFunctionSync : function (Argus1, Argus2, Argus3, Argus4)
{
// 通知Native层plugintest扩展插件运行“PluginTestFunctionSync”方法并同步返回结果
return B.execSync(_BARCODE, "PluginTestFunctionSync", [Argus1, Argus2, Argus3, Argus4]);
}
};
window.plus.plugintest = plugintest;
}, true );

HTML使用示例

该示例只展示同步或异步方法,完整内容参考官网或者SDK中示例工程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<meta name="HandheldFriendly" content="true"/>
<meta name="MobileOptimized" content="320"/>
<title>H5Plugin</title>
<script type="text/javascript" src="./js/common.js"></script>
<script type="text/javascript" src="./js/test.js"></script>
<script type="text/javascript">
function pluginShow() {
plus.plugintest.PluginTestFunction("Html5","Plus","AsyncFunction","MultiArgument!", function( result ) {alert( result[0] + "_" + result[1] + "_" + result[2] + "_" + result[3] );},function(result){alert(result)});
}
function pluginGetString()
{
alert(plus.plugintest.PluginTestFunctionSync("Html5","Plus","SyncFunction","MultiArgument!"));
}
</script>
<link rel="stylesheet" href="./css/common.css" type="text/css" charset="utf-8"/>
</head>
<body>
<header>
<div class="nvbt" onclick="back();"><div class="iback"></div></div>
<div class="nvtt">PluginTest</div>
</header>
<div id="dcontent" class="dcontent">
<br/>
<div class="button" onclick="pluginShow()">PluginTestFunction()</div>
<div class="button" onclick="pluginGetString()">PluginTestFunctionSync()</div>
<br/>
</div>
</body>
</html>

参考链接

Android平台第三方插件开发指导
Android平台以WebView方式集成HTML5+SDK方法

本文来源Private Networks

私有网络(Private Networks)

Allows ipfs to only connect to other peers who have a shared secret key.

IPFS只有共享密钥的节点才可互相连接。

状态(State)

Experimental

实验性

In Version

master, 0.4.7

如何启用(How to enable)

Generate a pre-shared-key using ipfs-swarm-key-gen:

通过ipfs-swarm-key-gen生成一个共享密钥:

1
2
go get github.com/Kubuxu/go-ipfs-swarm-key-gen/ipfs-swarm-key-gen
ipfs-swarm-key-gen > ~/.ipfs/swarm.key

To join a given private network, get the key file from someone in the network and save it to ~/.ipfs/swarm.key (If you are using a custom $IPFS_PATH, put it in there instead).

加入私有网络,需要从网络中的其他人中获取密钥文件并把它保存为~/.ipfs/swarm.key(如果你设置了$IPFS_PATH,替换为设置的目录)。

When using this feature, you will not be able to connect to the default bootstrap nodes (Since we aren’t part of your private network) so you will need to set up your own bootstrap nodes.

当你使用这个功能的时候,你不可以使用默认的启动节点(因为不是私有网络的一部分),因此你需要设置你自己的启动节点。

First, to prevent your node from even trying to connect to the default bootstrap nodes, run:

首先为了防止你的节点尝试连接默认的启动节点,你需要运行:

1
ipfs bootstrap rm --all

注意,这里有个BUG,可能会导致当前节点也被移除,而启动错误,需要手动重新添加。

Then add your own bootstrap peers with:

然后添加你自己的启动节点

1
ipfs bootstrap add <multiaddr>

For example:

例如:

1
ipfs bootstrap add /ip4/104.236.76.40/tcp/4001/ipfs/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64

Bootstrap nodes are no different from all other nodes in the network apart from the function they serve.

除了提供启动功能外,启动节点和网络中的其他节点没有什么不同。

To be extra cautious, You can also set the LIBP2P_FORCE_PNET environment variable to 1 to force the usage of private networks. If no private network is configured, the daemon will fail to start.

为了更安全,你也可以设置LIBP2P_FORCE_PNET环境变量为1来强制使用私用网络。如果没有配置私有网络,ipfs守护进程就会无法启动。

参考链接

https://zhuanlan.zhihu.com/p/35141862

准备

本文主要参考使用新版本5+SDK创建最简Android原生工程(Android studio)Android离线打包。如有未说明清楚之处,参看官方文档。

下载最新版本5+SDK,下载地址, 将SDK解压到任意目录,目录结构如下,以下统称为5+SDK目录。

新建Android工程

从欢迎界面或者文件>新建工程,选择Android类型的项目,使用默认配置或者根据需要做相关调整。创建Anroid应用本身不是本文重点。如果有问题可以删了重来,建个一两次基本没问题了。
新建Android工程

配置Hbuilder

注意将项目树使用Project方式显示,以便说明内容不引起误解

删除原生文件

删除原生工程中java目录下系统默认创建的源代码

复制aar文件

复制5+SDK目录下的SDK->libs->lib.5plus.base-release.aar文件到原生工程工程的app->libs目录下

配置build.gradle

打开app下的build.gradle文件

添加aar文件引用到dependencies

添加aar文件引用到dependencies,如下代码。如过编译器有警告信息,根据提示信息修改成imlementation。

1
compile(name: 'lib.5plus.base-release', ext: 'aar')

添加aar文件搜索路径

添加aar文件搜索路径与dependencies同级, 代码如下

1
2
3
4
5
repositories {
flatDir {
dirs 'libs'
}
}

修改targetSdkVersion

targetSdkVersion修改为21

设置multiDexEnabled

multiDexEnabled设置成false

配置Androidmanifest.xml

打开工程的Androidmanifest.xml文件,用以下内容替换原有application节点的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<application
android:name="io.dcloud.application.DCloudApplication"
android:allowClearUserData="true"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:largeHeap="true"
>
<activity
android:name="io.dcloud.PandoraEntry"
android:configChanges="orientation|keyboardHidden|keyboard|navigation"
android:label="@string/app_name"
android:launchMode="singleTask"
android:hardwareAccelerated="true"
android:theme="@style/TranslucentTheme"
android:screenOrientation="user"
android:windowSoftInputMode="adjustResize" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

增加icon.png

在app->src->res->drawble目录下放应用的图标文件文件命名为icon.png,如果没有可从5+SDK目录下的HBuilder-Hello目录下找。

配置资源目录

创建assets目录(app->src->main->assets),将5+SDK目录下的SDK->assets->data目录复制到新创建的assets目录下

配置应用目录

创建apps目录(app->src->main->assets->apps),将5+SDK目录下的HBuilder-Hello下的Apps(HBuilder-Hello->app->src->main->assets->apps)目录下内容复制到新创建的apps目录下。 注意: 应用资源的路径为[appid]->www, appid为应用资源manifest.json文件中id节点的值。

配置dcloud_control.xml

修改应用的assets->data->dcloud_control.xml文件的apps->app节点的appid属性的值改为manifest.json文件id节点的值,资源目录(app->[appid])、manifest.json的id(id)和dcloud_control的appid,三者必须一致,否则应用无法正常启动。

编译运行。

打包

配置应用的权限

参考5+SDK目录下“Feature列表.xls”文档,确定应用中使用到的扩展API,在AndroidManifest.json文件中调整API的权限。

配置应用的包名及版本号

打开AndroidManifest.xml文件,在代码视图中修改根节点的package属性值,如下:

其中package为应用的包名,采用反向域名格式,为应用的标识;versionCode为应用的版本号(整数值),用于各应用市场的升级判断,建议与manifest.json中version -> code值一致;versionName为应用的版本名称(字符串),在系统应用管理程序中显示的版本号,建议与manifest.json中version -> name值一致。

配置应用名称

打开res -> values -> strings.xml文件,修改“app_name”字段值,该值为安装到手机上桌面显示的应用名称。

配置应用图标和启动界面

将应用的图标(文件名为icon.png)和启动图片按照对应的尺寸拷贝到工程的res -> drawable-XXX目录下。

应用图标

指定各种分辨率设备上使用的应用图标(png格式)

节点名 图标尺寸 说明
mdpi 48*48 普通屏程序图标
ldpi 48*48 大屏程序图标
hdpi 72*72 高分屏程序图标
xhdpi 96*96 20P高分屏程序图标
xxhdpi 144*144 1080P高分屏程序图标

启动图片

指定各种分辨率设备上使用的启动图片(png格式)

节点名 图标尺寸 说明
mdpi 240*282 普通屏启动图片
ldpi 320*442 大屏启动图片
hdpi 480*762 高分屏启动图片
xhdpi 720*1242 720高分屏幕启动图片
xxhdpi 1080*1882 1080p高分屏启动图片

更新应用资源

打开assets -> apps 目录,将下面“HelloH5”目录名称修改为应用manifest.json中的id名称(这步非常重要,否则会导致应用无法正常启动),并将所有应用资源拷贝到其下的www目录。

配置应用信息

打开assets -> data下的dcloud_control.xml文件:

其中appid值为apps目录下的[appid]目录,appid决定运行哪个应用;appver为应用的版本号,用于应用资源的升级,必须保持与manifest.json中的version -> name值完全一致;version值为应用基座版本号(plus.runtime.innerVersion返回的值),不要随意修改。

生成安装包

如果只是测试使用Build->build apks即可,如果用于正式环境,build->Generate Signed APK。按要求填写相关信息。