hybrid-cli v1.10.3
一、Hybrid-Cli简介
1、概述
Hybrid-Cli是一套前端使用的打包脚本,可以用来将node项目打包成Android apk。本脚本必须配合指定的混合版Android项目一起使用。
2、引入
使用npm下载即可,暂时不支持yarn。
npm install -g hybrid-cli
3、命令
Hybrid-Cli包含一套命令,帮助前端项目打包使用。
命令 | 含义 |
---|---|
hybrid run build | 打包Android apk(release版和debug版都会打出) |
hybrid run device | 打包Android apk并在打包之后,运行到手机上 |
hybrid run release | 打包Android apk(release版和debug版都会打出) |
hybrid platform add | 重新下载Android项目 |
hybrid platform rm | 删除Android项目 |
hybrid gen resource | 生成资源文件夹 |
hybrid gen config | 生成配置文件 |
hybrid gen mtijs | 生成mti.js插件脚本 |
4、配置
前端项目需要引入hybrid.json配置文件,并进行适当的配置。
{
"AndroidGitUrl": "", // Android项目的Git地址
"WebUrl": "backup/index.html", // 前端首页访问地址
"WebTitle": true, // Android项目是否显示ActionBar
"WebTitleText": "MTI", // Android项目的ActionBar的文字内容
"SDK": "", // SDK路径
"Gradle": "", // Gradle路径
"WebProjectDist": "dist", // 前端项目打包后,前端包相对于前端项目根目录的相对路径
"KeyStore": "", // KeyStore文件
"KeyAlias": "", // KeyStore别名
"KeyPassword": "", // Key密码
"StorePassword": "", // Store密码
"AppId": "", // 打包apk的应用ID
"AppName": "" // 打包apk的应用名称
}
二、插件
1、插件使用指南
1.1、拍照
调用示例
mti.takePhoto().then(data => {
console.log(data);
}).catch(e => {
console.log(e);
})
调用参数
无
返回内容
// 成功示例:
{
compressUrl:"/storage/emulated/0/mti/compress/20190618-080235.jpg", // 压缩图地址
isUpload:false, // 是否已经上传
localUrl:"/storage/emulated/0/mti/picture/20190618-080224.jpg" // 原图地址
}
// 失败示例:
"canceled"
1.2、录音
调用示例
mti.takeAudio().then(data => {
console.log(data);
}).catch(e => {
console.log(e);
})
调用参数
无
返回内容
// 成功示例:
{
duration:1, // 音频时长
isUpload:false, // 是否已经上传
localUrl:"/storage/emulated/0/mti/audio/20190618-081044.amr" // 音频地址
}
// 失败示例:
"canceled"
1.3、视频
调用示例
mti.takeVideo().then(data => {
console.log(data);
}).catch(e => {
console.log(e);
})
调用参数
无
返回内容
// 成功示例:
{
compressUrl:"/storage/emulated/0/mti/compress/20190618-081932.jpg", // 视频首帧压缩图
isUpload:false, // 是否已经上传
localUrl:"/storage/emulated/0/mti/video/20190618-081926.mp4" // 视频地址
}
// 失败示例:
"canceled"
1.4、语音转文字
调用示例
mti.speechToWord().then(data => {
console.log(data);
}).catch(e => {
console.log(e);
})
调用参数
无
返回内容
// 成功示例:
"今晚去哪吃饭?"
// 失败示例:
"canceled"
1.5、定位
调用示例
mti.lastLocation().then(data => {
console.log(data);
}).catch(e => {
console.log(e);
})
调用参数
无
返回内容
// 成功示例:
"121.03965,29.29768" // 格式:经度在前、纬度在后、中间用英文逗号分隔
// 失败示例:
"no location"
1.6、文件上传
调用示例
mti.fileUpload({
uploadFile: '/storage/emulated/0/mti/download/node-v10.15.3-x64.msi',
uploadPath: 'http://10.168.6.246:8080/Web/rest/file',
uploadData: '1'
}).then(data => {
console.log(data);
}).catch(e => {
console.log(e);
})
调用参数
// 参数示例:
{
uploadFile: "/storage/emulated/0/mti/download/node-v10.15.3-x64.msi", // 本地路径
uploadPath: "http://10.168.6.246:8080/Web/rest/file", // 上传路径
uploadData: "1" // 额外携带数据,字符串类型
}
注意:上述参数会转换为Multipart的格式上传到指定后台。文件Key值为:file,数据Key值为:data。
返回内容
// 成功示例:
Object // 服务端的响应体对象
// 失败示例:
"404" // http请求响应的错误消息
1.7、文件下载
调用示例
mti.fileDownload({
downloadPath: 'http://cdn.npm.taobao.org/dist/node/v10.15.3/node-v10.15.3-x64.msi'
}).then(data => {
console.log(data);
}).catch(e => {
console.log(e);
})
调用参数
// 参数示例:
{
downloadPath: 'http://cdn.npm.taobao.org/node-v10.15.3-x64.msi' // 请求地址
}
返回内容
// 成功示例:
"/storage/emulated/0/mti/download/node-v10.15.3-x64.msi" // 下载完成后,文件的本地路径
// 失败示例:
"404" // http请求响应的错误消息
1.8、文件选择功能
html5支持通过input标签来选择文件,在移动端也对该功能增加了支持。
调用示例
<!-- 选择本地图片并在img标签中显示 -->
<input type="file" accept="image/*" capture="camera" onchange="image(this.files)">
<img id="file-image" style="width: 100px; height: 100px;">
<script type="text/javascript">
function image(files) {
var fileImage = document.getElementById('file-image')
fileImage.src = URL.createObjectURL(files[0]);
}
</script>
<!-- 选择本地视频并在video标签中显示 -->
<input type="file" accept="video/*" capture="camcorder" onchange="video(this.files)">
<video id="file-video" style="width: 100px; height: 100px;"></video>
<script type="text/javascript">
function video(files) {
var fileVideo = document.getElementById('file-video')
fileVideo.src = URL.createObjectURL(files[0]);
}
</script>
<!-- 选择本地音频并在audio标签中显示 -->
<input type="file" accept="audio/*" capture="microphone" onchange="audio(this.files)">
<audio id="file-audio" controls>/audio>
<script type="text/javascript">
function audio(files) {
var fileAudio = document.getElementById('file-audio')
fileAudio.src = URL.createObjectURL(files[0]);
}
</script>
1.9、注册Websocket监听
通过本插件可以向Android项目中的长连接注册消息。
调用示例
mti.registerWebsocket('a', 'handler', (data) => {
console.log(data);
})
调用参数
// 参数示例:
messageType // 注册的消息类型名称
callbackName // Js回调函数的名称
callback // 接收消息的Js回调函数
返回内容
// 成功示例:
{ "messageType": "a", "messageData": "" }
2、资源调用指南
在上述插件中,涉及到拍照、录音、视频的插件,返回到前端的内容是本地路径,类似如下:
{
compressUrl:"/storage/emulated/0/mti/compress/20190618-081932.jpg", // 视频首帧压缩图
localUrl:"/storage/emulated/0/mti/video/20190618-081926.mp4" // 视频地址
}
有时,前端可能有这样的需求:拍照之后,将本地图片在标签中展示。这个需求有两种实现可能:
- 源站点是file://头协议的站点(使用hybrid-cli打包属于file://头协议的源站点)。在绝对路径前增加file://协议头即可访问。
<img src="file:///storage/emulated/0/mti/compress/20190618-081932.jpg">
<img src="http://mti-media/emulated/0/mti/compress/20190618-081932.jpg">
其他文件类型(视频、音频)也是上述的实现方式。但是需要注意,如果使用、、等标签显示文件,需要确定这些标签是否支持相应的文件类型。
3、插件开发指南
本节前端开发者不需要关心,Android端开发者需要注意,开发新的插件的步骤如下:
- 第一步,自定义插件继承BridgePlugin类。重写构造方法,为插件的BridgeContext上下文赋值。重写handler方法,实现插件功能。
public class TakePhotoPlugin extends BridgePlugin {
public TakePhotoPlugin(BridgeContext context) {
this.context = context;
}
@Override
public void handler(String data, CallBackFunction function) {
// 在此处实现插件的功能
}
}
- 第二步,在BridgePluginMap类中,配置插件。
public class BridgePluginMap {
// 插件名,js端调用时使用的就是这个插件名
public final static String TAKE_PHOTO = "takePhoto";
private Map<String, Class> pluginMap;
// 存储所有的插件的类对象
private BridgePluginMap() {
pluginMap = new ArrayMap<>();
pluginMap.put(BridgePluginMap.TAKE_PHOTO, TakePhotoPlugin.class);
}
}
- 第三部,修改mti.js文件中调用插件的方法。
// 调用android中的拍照插件
Plugins.prototype.takePhoto = function () {
return new Promise(function (resolve, reject) {
WebViewJavascriptBridge.callHandler('takePhoto', '',
function (responseData) {
console.log(responseData);
let result = JSON.parse(responseData);
if (result.ret == 200) {
resolve(result.data);
} else {
reject(result.message);
}
}
);
})
}
- 第四步,如果插件中需要处理Android权限问题,可以使用@RequestPermission注解请求权限,然后实现PermissionInterface接口,获取请求权限的结果。注意,在没有获得权限之前,注解方法之后的逻辑都不会执行。
public class TakePhotoPlugin extends BridgePlugin implements PermissionInterface {
public CallBackFunction mCallBackFunction;
public TakePhotoPlugin(BridgeContext context) {
this.context = context;
}
@Override
public void handler(String data, CallBackFunction function) {
this.mCallBackFunction = function;
takePhoto();
}
// 调用系统相机拍照
@RequestPermission(permission = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE})
public void takePhoto() {}
// 插件权限请求结果
@Override
public void requestResult(boolean result) {
if (!result) {
Log.e("tag", "拒绝存储权限,拍照功能不可用!");
return;
}
takePhoto();
}
}
- 第五步,如果插件中使用了startActivityForResult方法,所使用的请求码建议统一到AppRequestCode类中定义,并遵循如下规则。
/**
* 项目中用到的所有RequestCode都在此处定义
* 这样统一定义的好处是,方便对来自不同上下文环境的请求进行分流
*/
public class AppRequestCode {
/**
* Activity请求使用到的请求码(规则是:100<code<1000)
*/
public final static int BOUND_OF_ACTIVITY_REQUEST = 100;
//文件选择
public final static int ACTIVITY_REQUEST_FILE_CHOOSER = 101;
/**
* 插件请求使用到的请求码(规则是:1000<code<10000)
*/
public final static int BOUND_OF_PLUGIN_REQUEST = 1000;
//插件的拍照请求
public final static int PLUGIN_REQUEST_TAKE_PHOTO = 1001;
//插件的录音请求
public final static int PLUGIN_REQUEST_TAKE_AUDIO = 1002;
//插件的视频请求
public final static int PLUGIN_REQUEST_TAKE_VIDEO = 1003;
/**
* 权限请求使用到的请求码(规则是:code>10000)
*/
public final static int BOUND_OF_PERMISSION_REQUEST = 10000;
//Activity中请求权限
public final static int PERMISSION_REQUEST_FOR_ACTIVITY = 10001;
//Plugin中请求权限
public final static int PERMISSION_REQUEST_FOR_PLUGIN = 10002;
}
4、注意事项
在使用Vue等类似框架时,请不要在created、mounted等生命周期方法中,调用插件方法。原因是:在执行created、mounted等生命周期方法时,可能插件依赖的JsBridge还没有初始化完成,导致插件调用不成功。所以,现在提供window上的全局事件,通过监听这个事件,并在这个事件触发时,执行调用插件的逻辑。
window.addEventListener('nativeReady', event => {
mti.registerWebsocket('a', 'handler', (data) => {
console.log(data);
});
})
三、热更新
1、概述
本项目的Android部分提供了三种更新方式:
- Apk更新
- Tinker补丁热更新
- 前端资源包单独更新
有关热更新的配置同样位于hybrid.json的配置文件中,所有配置如下:
配置 | 含义 |
---|---|
AppVersionCode | 应用版本号 |
AppVersionName | 应用版本名称 |
TinkerVersionCode | 补丁版本号 |
WebVersionCode | 前端版本号 |
VersionControlUrl | 更新接口地址 |
TinkerEnable | 是否开启Tinker编译 |
TinkerBaseApk | Tinker编译补丁的基准包绝对路径 |
热更新依赖VersionControlUrl配置更新接口的地址,这是在Android项目中已经写好的HTTP请求,请求类型是GET,接收的返回值类型是application/json。这个地址代表的可以是某个服务端的json文件或者接口,这个可以根据各自项目来决定,但是返回的参数类型是已经被定义好的,也就是说这个接口必须返回如下类型的参数:
{
"patchVersion": 3,
"patchPath": "http://localhost:8080/test/patch_signed_7zip.apk",
"webVersion": 3,
"webPath": "http://localhost:8080/test/dist.zip",
"apkVersion": 2,
"apkPath": "http://localhost:8080/test/update-apk.apk"
}
2、APK更新
当客户端请求到上述的更新响应值时,App会判断apkVersion,如果大于App自身的VersionCode,则会提醒用户进行版本更新。当用户同意版本更新,则会通过apkPath下载最新的apk并安装。
3、补丁更新
当客户端请求到上述的更新响应值时,App同样会判断patchVersion,如果大于App当前的补丁版本号,那么App会自动通过patchPath下载并自动热更新。
打包方式
补丁的生成和编译依赖tinker的命令。首先,需要将TinkerEnable配置为true(在平时开发过程中,最好不要开启这个配置,因为会生成大量的编译缓存apk)。然后,配置TinkerBaseApk为基准包的绝对路径(基准包是指当前的补丁是针对哪个版本的APK,比如:基准包为A.apk,Android项目修改之后,基于A.apk生成补丁,这个补丁会运行在A.apk对应版本的App上)。接下来,在命令行中输入如下命令:
hybrid run tinker
补丁生成完成后,生成结果路径为platform/baseframework/release/tinkerPatch/hybrid/release/patch_signed_7zip.apk,将这个apk补丁放到服务器相应的目录等待用户下载即可。该文件名暂不支持修改,否则客户端无法读取到补丁。
适用场景
补丁式热更新支持Android项目的热更新,同时也支持前端资源的热更新。更新的过程也比较方便,但是缺点就是在打包时需要提供基准包。
4、前端更新
当客户端请求到上述的更新响应值时,App同样会判断webVersion,如果大于App当前的前端版本号,那么App会自动通过webPath下载并自动热更新。
打包方式
将前端资源打包压缩成zip,放到服务器相应目录即可。但注意压缩包中不可嵌套多余目录,并且解压后的目录名称和结构要和WebUrl保持一致。
适用场景
前端热更新只支持前端资源的热更新,如果遇到只需要更新资源的场景,可以使用这种方式。但是这个方式的缺点是,热更新的资源缓存在本地,有可能通过删除数据的方式被用户误删,所以持久性比较差,推荐调试或小版本更新使用。
四、长连接
1、使用步骤
本项目的Android部分为前端提供了Websocket长连接。使用步骤如下:
- 首先,在hybrid.json中配置Websocket的地址。
"WebsocketUrl": "ws://192.168.1.5:8081/ws"
- 然后,调用插件注册Websocket消息的监听,参考第二章的1.9小节。注册之后即可监听Websocket消息。
2、服务端消息格式
服务端在推送消息时,需要遵守如下的消息格式:
{ "messageType": "a", "messageData": "" }
Android客户端在收到上述消息时,会根据messageType来判断消息类型,从而分发到指定的Js监听回调函数中。
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago