
uni-app 中如何打开外部应用,如:浏览器、淘宝、AppStore、QQ等
我们在开发 App 应用中,经常会遇到打开第三方程序的场景,比如打开手机淘宝、通过第三方浏览器打开一个 url 等等。
App不像网页可以使用http超链接互相跳转,但手机os设计了scheme机制,可以通过特殊的链接互相调起。
比如手机淘宝,其安装后会在手机os中会注册一个scheme协议,taobao://
。
这种协议还支持参数,比如taobao://s.taobao.com/search?q=uni-app
启动淘宝并打开搜索页面搜索uni-app。
在uni-app/5+App中,可以通过scheme呼起其他App,也支持给自己的App设置scheme参数。
这个功能小程序并不支持,属于App端的扩展API。
打开外部scheme的API是plus.runtime.openURL()
。查看文档:http://www.html5plus.org/doc/zh_cn/runtime.html
打开第三方程序
打开第三方程序,我们需要使用 runtime 模块,下面我罗列两个相关的方法。其他操作请详读文档。
-
调用外部浏览器打开指定的URL
plus.runtime.openURL( url, errorCB, identity );
- url: ( String ) 必选 要打开的URL地址
字符串类型,各平台支持的地址类型存在差异,参考平台URL支持表。 - errorCB: ( OpenErrorCallback ) 可选 打开URL地址失败的回调
打开指定URL地址失败时回调,并返回失败信息。 - identity: ( String ) 可选 指定打开URL地址的程序名称
在iOS平台此参数被忽略,在Android平台为程序包名,如果指定的包名不存在,则打开URL地址失败。
<template> <view> <button class="button" type="primary" @click="open(0)">使用第三方程序打开指定URL</button> </view> </template>
<script> export default { data() { return { url: 'https://uniapp.dcloud.io/' }; }, onLoad(op) {}, methods: { open(types) { plus.runtime.openURL(this.url, function(res) { console.log(res); }); } } }; </script>
- url: ( String ) 必选 要打开的URL地址
-
调用第三方程序
plus.runtime.launchApplication( appInf, errorCB );
- appInf: ( ApplicationInf ) 必选 要启动第三方程序的描述信息
- errorCB: ( LaunchErrorCallback ) 必选 启动第三方程序操作失败的回调函数
启动第三方程序失败时回调,并返回失败信息。
<template> <view> <button class="button" type="primary" @click="launchApp">打开淘宝</button> </view> </template>
<script> export default { data() { return { url: 'https://uniapp.dcloud.io/' }; }, onLoad(op) {}, methods: { launchApp() { let _this = this; // 判断平台 if (plus.os.name == 'Android') { plus.runtime.launchApplication( { pname: 'com.taobao.taobao' }, function(e) { console.log('Open system default browser failed: ' + e.message); } ); } else if (plus.os.name == 'iOS') { plus.runtime.launchApplication({ action: 'taobao://' }, function(e) { console.log('Open system default browser failed: ' + e.message); }); } } } }; </script>
常用URLscheme
[
// 只在 ios 中生效
{
name: 'App Store',
scheme: 'itms-apps://'
},
{
name: '支付宝',
pname: 'com.eg.android.AlipayGphone',
scheme: 'alipay://'
},
{
name: '淘宝',
pname: 'com.taobao.taobao',
scheme: 'taobao://'
},
{
name: 'QQ',
pname: 'com.tencent.mobileqq',
scheme: 'mqq://'
},
{
name: '微信',
pname: 'com.tencent.mm',
scheme: 'weixin://'
},
{
name: '京东',
pname: 'com.jingdong.app.mall',
scheme: 'openApp.jdMobile://'
},
{
name: '新浪微博',
pname: 'com.sina.weibo',
scheme: 'sinaweibo://'
},
{
name: '优酷',
pname: 'com.youku.phone',
scheme: 'youku://'
}
]
更多实用例子
除了简单的打开App,我们更多的时候想要直达。这里汇总了很多有用的直达案例:
- 使用应用商店打开指定App,可用于引导评分
- 强制使用应用宝打开指定App
- 打开淘宝搜索页面。需要你要做淘宝客,需要向淘宝申请自己的scheme参数并传入。
- 打开地图并指定地点
- 打开qq并到指定聊天界面,可用于客服
具体代码见下:
<template>
<view>
<page-head title="通过scheme打开三方app示例"></page-head>
<button class="button" @click="openBrowser('https://uniapp.dcloud.io/h5')">使用浏览器打开指定URL</button>
<button class="button" @click="openMarket()">使用应用商店打开指定App</button>
<button class="button" @click="openMarket('com.tencent.android.qqdownloader')">强制使用应用宝打开指定App</button>
<button class="button" @click="openTaobao('taobao://s.taobao.com/search?q=uni-app')">打开淘宝搜索页面</button>
<button class="button" @click="openMap()">打开地图并指定地点</button>
<view class="uni-divider">
<view class="uni-divider__content">打开QQ</view>
<view class="uni-divider__line"></view>
</view>
<view class="uni-padding-wrap">
<form @submit="openQQ">
<view>
<view class="uni-title">请输入聊天对象QQ号:</view>
<view class="uni-list">
<view class="uni-list-cell">
<input class="uni-input" name="qqNum" type="number"/>
</view>
</view>
</view>
<view>
<view class="uni-title">请选择QQ号类型:</view>
<radio-group class="uni-flex" name="qqNumType">
<label>
<radio value="wpa" checked=""/>普通QQ号</label>
<label>
<radio value="crm" />营销QQ号(无需加好友直接聊天)</label>
</radio-group>
</view>
<view class="uni-btn-v uni-common-mt">
<button class="button" formType="submit">打开qq并到指定聊天界面</button>
</view>
</form>
</view>
</view>
</template>
<script>
export default {
data() {
return {
};
},
methods: {
openBrowser(url){
plus.runtime.openURL(url)
},
openMarket(marketPackageName) {
var appurl;
if (plus.os.name=="Android") {
appurl = "market://details?id=io.dcloud.HelloH5"; //由于hello uni-app没有上Android应用商店,所以此处打开了另一个示例应用
}
else{
appurl = "itms-apps://itunes.apple.com/cn/app/hello-uni-app/id1417078253?mt=8";
}
if (typeof(marketPackageName)=="undefined") {
plus.runtime.openURL(appurl, function(res) {
console.log(res);
});
} else{//强制指定某个Android应用市场的包名,通过这个包名启动指定app
if (plus.os.name=="Android") {
plus.runtime.openURL(appurl, function(res) {
plus.nativeUI.alert("本机没有安装应用宝");
},marketPackageName);
} else{
plus.nativeUI.alert("仅Android手机才支持应用宝");
}
}
},
openTaobao(url){
plus.runtime.openURL(url, function(res) {
uni.showModal({
content:"本机未检测到淘宝客户端,是否打开浏览器访问淘宝?",
success:function(res){
if (res.confirm) {
plus.runtime.openURL("https://s.taobao.com/search?q=uni-app")
}
}
})
});
},
openMap(){
var url = "";
if (plus.os.name=="Android") {
var hasBaiduMap = plus.runtime.isApplicationExist({pname:'com.baidu.BaiduMap',action:'baidumap://'});
var hasAmap = plus.runtime.isApplicationExist({pname:'com.autonavi.minimap',action:'androidamap://'});
var urlBaiduMap = "baidumap://map/marker?location=39.968789,116.347247&title=DCloud&src=Hello%20uni-app";
var urlAmap = "androidamap://viewMap?sourceApplication=Hello%20uni-app&poiname=DCloud&lat=39.9631018208&lon=116.3406135236&dev=0"
if (hasAmap && hasBaiduMap) {
plus.nativeUI.actionSheet({title:"选择地图应用",cancel:"取消",buttons:[{title:"百度地图"},{title:"高德地图"}]}, function(e){
switch (e.index){
case 1:
plus.runtime.openURL(urlBaiduMap);
break;
case 2:
plus.runtime.openURL(urlAmap);
break;
}
})
}
else if (hasAmap) {
plus.runtime.openURL(urlAmap);
}
else if (hasBaiduMap) {
plus.runtime.openURL(urlBaiduMap);
}
else{
url = "geo:39.96310,116.340698?q=%e6%95%b0%e5%ad%97%e5%a4%a9%e5%a0%82";
plus.runtime.openURL(url); //如果是国外应用,应该优先使用这个,会启动google地图。这个接口不能统一坐标系,进入百度地图时会有偏差
}
} else{
// iOS上获取本机是否安装了百度高德地图,需要在manifest里配置,在manifest.json文件app-plus->distribute->apple->urlschemewhitelist节点下添加(如urlschemewhitelist:["iosamap","baidumap"])
plus.nativeUI.actionSheet({title:"选择地图应用",cancel:"取消",buttons:[{title:"Apple地图"},{title:"百度地图"},{title:"高德地图"}]}, function(e){
console.log("e.index: " + e.index);
switch (e.index){
case 1:
url = "http://maps.apple.com/?q=%e6%95%b0%e5%ad%97%e5%a4%a9%e5%a0%82&ll=39.96310,116.340698&spn=0.008766,0.019441";
break;
case 2:
url = "baidumap://map/marker?location=39.968789,116.347247&title=DCloud&src=Hello%20uni-app";
break;
case 3:
url = "iosamap://viewMap?sourceApplication=Hello%20uni-app&poiname=DCloud&lat=39.9631018208&lon=116.3406135236&dev=0";
break;
default:
break;
}
if (url!="") {
plus.runtime.openURL( url, function( e ) {
plus.nativeUI.alert("本机未安装指定的地图应用");
});
}
})
}
},
openQQ: function (e) {
// console.log("e.detail.value: " + JSON.stringify(e.detail.value));
// 没有校验qq号是否为空或合法数字,如果不是可用的qq号,启动qq后会停留在qq主界面
plus.runtime.openURL('mqqwpa://im/chat?chat_type=' + e.detail.value.qqNumType + '&uin=' + e.detail.value.qqNum,function (res) {
plus.nativeUI.alert("本机没有安装QQ,无法启动");
});
}
}
};
</script>
<style>
.button {
margin: 30upx;
color: #007AFF;
}
</style>
给自己的App设置scheme
可在manifest中可配置。
Android配置方法
iOS配置方法
我们在开发 App 应用中,经常会遇到打开第三方程序的场景,比如打开手机淘宝、通过第三方浏览器打开一个 url 等等。
App不像网页可以使用http超链接互相跳转,但手机os设计了scheme机制,可以通过特殊的链接互相调起。
比如手机淘宝,其安装后会在手机os中会注册一个scheme协议,taobao://
。
这种协议还支持参数,比如taobao://s.taobao.com/search?q=uni-app
启动淘宝并打开搜索页面搜索uni-app。
在uni-app/5+App中,可以通过scheme呼起其他App,也支持给自己的App设置scheme参数。
这个功能小程序并不支持,属于App端的扩展API。
打开外部scheme的API是plus.runtime.openURL()
。查看文档:http://www.html5plus.org/doc/zh_cn/runtime.html
打开第三方程序
打开第三方程序,我们需要使用 runtime 模块,下面我罗列两个相关的方法。其他操作请详读文档。
-
调用外部浏览器打开指定的URL
plus.runtime.openURL( url, errorCB, identity );
- url: ( String ) 必选 要打开的URL地址
字符串类型,各平台支持的地址类型存在差异,参考平台URL支持表。 - errorCB: ( OpenErrorCallback ) 可选 打开URL地址失败的回调
打开指定URL地址失败时回调,并返回失败信息。 - identity: ( String ) 可选 指定打开URL地址的程序名称
在iOS平台此参数被忽略,在Android平台为程序包名,如果指定的包名不存在,则打开URL地址失败。
<template> <view> <button class="button" type="primary" @click="open(0)">使用第三方程序打开指定URL</button> </view> </template>
<script> export default { data() { return { url: 'https://uniapp.dcloud.io/' }; }, onLoad(op) {}, methods: { open(types) { plus.runtime.openURL(this.url, function(res) { console.log(res); }); } } }; </script>
- url: ( String ) 必选 要打开的URL地址
-
调用第三方程序
plus.runtime.launchApplication( appInf, errorCB );
- appInf: ( ApplicationInf ) 必选 要启动第三方程序的描述信息
- errorCB: ( LaunchErrorCallback ) 必选 启动第三方程序操作失败的回调函数
启动第三方程序失败时回调,并返回失败信息。
<template> <view> <button class="button" type="primary" @click="launchApp">打开淘宝</button> </view> </template>
<script> export default { data() { return { url: 'https://uniapp.dcloud.io/' }; }, onLoad(op) {}, methods: { launchApp() { let _this = this; // 判断平台 if (plus.os.name == 'Android') { plus.runtime.launchApplication( { pname: 'com.taobao.taobao' }, function(e) { console.log('Open system default browser failed: ' + e.message); } ); } else if (plus.os.name == 'iOS') { plus.runtime.launchApplication({ action: 'taobao://' }, function(e) { console.log('Open system default browser failed: ' + e.message); }); } } } }; </script>
常用URLscheme
[
// 只在 ios 中生效
{
name: 'App Store',
scheme: 'itms-apps://'
},
{
name: '支付宝',
pname: 'com.eg.android.AlipayGphone',
scheme: 'alipay://'
},
{
name: '淘宝',
pname: 'com.taobao.taobao',
scheme: 'taobao://'
},
{
name: 'QQ',
pname: 'com.tencent.mobileqq',
scheme: 'mqq://'
},
{
name: '微信',
pname: 'com.tencent.mm',
scheme: 'weixin://'
},
{
name: '京东',
pname: 'com.jingdong.app.mall',
scheme: 'openApp.jdMobile://'
},
{
name: '新浪微博',
pname: 'com.sina.weibo',
scheme: 'sinaweibo://'
},
{
name: '优酷',
pname: 'com.youku.phone',
scheme: 'youku://'
}
]
更多实用例子
除了简单的打开App,我们更多的时候想要直达。这里汇总了很多有用的直达案例:
- 使用应用商店打开指定App,可用于引导评分
- 强制使用应用宝打开指定App
- 打开淘宝搜索页面。需要你要做淘宝客,需要向淘宝申请自己的scheme参数并传入。
- 打开地图并指定地点
- 打开qq并到指定聊天界面,可用于客服
具体代码见下:
<template>
<view>
<page-head title="通过scheme打开三方app示例"></page-head>
<button class="button" @click="openBrowser('https://uniapp.dcloud.io/h5')">使用浏览器打开指定URL</button>
<button class="button" @click="openMarket()">使用应用商店打开指定App</button>
<button class="button" @click="openMarket('com.tencent.android.qqdownloader')">强制使用应用宝打开指定App</button>
<button class="button" @click="openTaobao('taobao://s.taobao.com/search?q=uni-app')">打开淘宝搜索页面</button>
<button class="button" @click="openMap()">打开地图并指定地点</button>
<view class="uni-divider">
<view class="uni-divider__content">打开QQ</view>
<view class="uni-divider__line"></view>
</view>
<view class="uni-padding-wrap">
<form @submit="openQQ">
<view>
<view class="uni-title">请输入聊天对象QQ号:</view>
<view class="uni-list">
<view class="uni-list-cell">
<input class="uni-input" name="qqNum" type="number"/>
</view>
</view>
</view>
<view>
<view class="uni-title">请选择QQ号类型:</view>
<radio-group class="uni-flex" name="qqNumType">
<label>
<radio value="wpa" checked=""/>普通QQ号</label>
<label>
<radio value="crm" />营销QQ号(无需加好友直接聊天)</label>
</radio-group>
</view>
<view class="uni-btn-v uni-common-mt">
<button class="button" formType="submit">打开qq并到指定聊天界面</button>
</view>
</form>
</view>
</view>
</template>
<script>
export default {
data() {
return {
};
},
methods: {
openBrowser(url){
plus.runtime.openURL(url)
},
openMarket(marketPackageName) {
var appurl;
if (plus.os.name=="Android") {
appurl = "market://details?id=io.dcloud.HelloH5"; //由于hello uni-app没有上Android应用商店,所以此处打开了另一个示例应用
}
else{
appurl = "itms-apps://itunes.apple.com/cn/app/hello-uni-app/id1417078253?mt=8";
}
if (typeof(marketPackageName)=="undefined") {
plus.runtime.openURL(appurl, function(res) {
console.log(res);
});
} else{//强制指定某个Android应用市场的包名,通过这个包名启动指定app
if (plus.os.name=="Android") {
plus.runtime.openURL(appurl, function(res) {
plus.nativeUI.alert("本机没有安装应用宝");
},marketPackageName);
} else{
plus.nativeUI.alert("仅Android手机才支持应用宝");
}
}
},
openTaobao(url){
plus.runtime.openURL(url, function(res) {
uni.showModal({
content:"本机未检测到淘宝客户端,是否打开浏览器访问淘宝?",
success:function(res){
if (res.confirm) {
plus.runtime.openURL("https://s.taobao.com/search?q=uni-app")
}
}
})
});
},
openMap(){
var url = "";
if (plus.os.name=="Android") {
var hasBaiduMap = plus.runtime.isApplicationExist({pname:'com.baidu.BaiduMap',action:'baidumap://'});
var hasAmap = plus.runtime.isApplicationExist({pname:'com.autonavi.minimap',action:'androidamap://'});
var urlBaiduMap = "baidumap://map/marker?location=39.968789,116.347247&title=DCloud&src=Hello%20uni-app";
var urlAmap = "androidamap://viewMap?sourceApplication=Hello%20uni-app&poiname=DCloud&lat=39.9631018208&lon=116.3406135236&dev=0"
if (hasAmap && hasBaiduMap) {
plus.nativeUI.actionSheet({title:"选择地图应用",cancel:"取消",buttons:[{title:"百度地图"},{title:"高德地图"}]}, function(e){
switch (e.index){
case 1:
plus.runtime.openURL(urlBaiduMap);
break;
case 2:
plus.runtime.openURL(urlAmap);
break;
}
})
}
else if (hasAmap) {
plus.runtime.openURL(urlAmap);
}
else if (hasBaiduMap) {
plus.runtime.openURL(urlBaiduMap);
}
else{
url = "geo:39.96310,116.340698?q=%e6%95%b0%e5%ad%97%e5%a4%a9%e5%a0%82";
plus.runtime.openURL(url); //如果是国外应用,应该优先使用这个,会启动google地图。这个接口不能统一坐标系,进入百度地图时会有偏差
}
} else{
// iOS上获取本机是否安装了百度高德地图,需要在manifest里配置,在manifest.json文件app-plus->distribute->apple->urlschemewhitelist节点下添加(如urlschemewhitelist:["iosamap","baidumap"])
plus.nativeUI.actionSheet({title:"选择地图应用",cancel:"取消",buttons:[{title:"Apple地图"},{title:"百度地图"},{title:"高德地图"}]}, function(e){
console.log("e.index: " + e.index);
switch (e.index){
case 1:
url = "http://maps.apple.com/?q=%e6%95%b0%e5%ad%97%e5%a4%a9%e5%a0%82&ll=39.96310,116.340698&spn=0.008766,0.019441";
break;
case 2:
url = "baidumap://map/marker?location=39.968789,116.347247&title=DCloud&src=Hello%20uni-app";
break;
case 3:
url = "iosamap://viewMap?sourceApplication=Hello%20uni-app&poiname=DCloud&lat=39.9631018208&lon=116.3406135236&dev=0";
break;
default:
break;
}
if (url!="") {
plus.runtime.openURL( url, function( e ) {
plus.nativeUI.alert("本机未安装指定的地图应用");
});
}
})
}
},
openQQ: function (e) {
// console.log("e.detail.value: " + JSON.stringify(e.detail.value));
// 没有校验qq号是否为空或合法数字,如果不是可用的qq号,启动qq后会停留在qq主界面
plus.runtime.openURL('mqqwpa://im/chat?chat_type=' + e.detail.value.qqNumType + '&uin=' + e.detail.value.qqNum,function (res) {
plus.nativeUI.alert("本机没有安装QQ,无法启动");
});
}
}
};
</script>
<style>
.button {
margin: 30upx;
color: #007AFF;
}
</style>
给自己的App设置scheme
可在manifest中可配置。
Android配置方法
iOS配置方法

个推基于Consul的配置管理
作者:个推应用平台基础架构高级研发工程师 阿飞
在微服务架构体系中,由于微服务众多,服务之间又有互相调用关系,因此,一个通用的分布式配置管理是必不可少的。一般来说,配置管理需要解决配置集中管理、在系统运行期间可实现动态配置、配置修改后支持自动刷新等问题。
在大多数微服务体系中,都会有一个名为配置文件的功能模块来提供统一的分布式配置管理。构建配置中心,统一对应用中各个微服务进行管理,对微服务体系的意义重大。
Consul为什么适合做配置管理
Consul作为轻量级的分布式K/V存储系统,搭建方便,可用性高,并且支持多数据中心,提供Web UI进行K/V管理。此外Consul还可以结合Consul-Template或者在代码中引入Consul Client的相关依赖创建Watcher来实时Watch K/V的变化,是配置管理的不二之选。

下图为个推微服务体系基于Consul配置管理的整体设计。其中,CCenter就是在Consul的基础上进行二次开发的配置中心。
微服务体系下配置的分类和组织形式
在实践中,不同产品线的配置会放置在Consul的不同路径下,实现不同产品线配置之间的隔离。
按照配置的用途,可将同一产品线下的配置分为三类:
1.API网关相关配置;
2.服务注册与发现相关配置;
3.应用相关配置。
其中,每类配置会对应Consul上的不同目录。
按照配置的变化特性,可将配置分为两类:
1.环境相关的全局配置
如MySQL等外部依赖相关的配置和其他与环境相关的配置,这类配置在开发测试生产环境中存在差异,需要为不同环境配置不同的值。
2.应用本身的配置
一般为不经常性发生变化、可动态调整、开关的配置。这类配置比较稳定,在初始化后,只有在需要时才会改动,通常会设置默认值。这两类配置在Consul上会放在不同的子目录下。这样QA、运维只需要关注环境差异部分即可。
基于以上对配置的分类,最终Consul上的Key的格式如下:
/ProductLine_Prefix/Usage_Prefix/Environmental_Correlation_Prefix/Config_Item_Path
其中,
ProductLine_Prefix:用来隔离不同产品线的配置;
Usage_Prefix:用来区分配置的用途;
Environmental_Correlation_Prefix:用来分隔与环境相关的配置;
Config_Item_Path:具体的配置项。
配置在Consul上的组织形式有以下两种:
1.以配置文件的形式组织,Consul上的一个K/V,对应一个配置文件,如nginx的配置文件。
2.以配置项的形式组织,将配置文件模板化,拆成一个个的配置项,每个配置项对应Consul上的一个K/V,多个配置项对应一个配置文件。大部分配置文件本身都是以K/V的形式组织的,均适合模板化,模板化后即可以按照配置项的特性,在Consul上分成不同的类别进行管理。
如何实现配置更新
Consul上的K/V,要如何生成可加载的应用,或可使用的配置呢?
1.用Node和Lua实现的微服务的配置更新,使用Consul-Template来实现;
2.用Java实现的微服务的配置更新,通过Consul-Template工具(需要重启应用)和在代码中引入Consul Client的依赖创建Watcher(热更新)这两种方式来实现。
Consul-Template如何使用?
Consul-Template是一个后台进程,它可以根据Watch Consul上K/V的变化,更新任意数量的模板,同时生成对应的文件,之后还可以运行任意的命令。要使用Consul-Template一般需要定义两个文件:
1.模板文件
模板文件一般按照Go Template的格式进行编写,示例如下:
config-tree.ctmpl:
{{ tree /consul/path/to/configFiles | explode | toJSONPretty }}
该模板在/consul/path/to/configFiles路径下的配置发生变化时,会渲染出一个Json格式的字符串,其中包含了/consul/path/to/configFiles下所有的K/V.
config-kv.ctmpl:
return {
host='{{ printf "%s/mysql/host" (env "CONSUL_CONFIG_PREFIX") | key }}',
port={{ keyOrDefault (printf "%s/mysql/port" (env "CON-SUL_CONFIG_PREFIX")) "3306" }},
user='{{ printf "%s/mysql/user" (env "CONSUL_CONFIG_PREFIX") | key }}',
password='{{ printf "%s/mysql/password" (env "CON-SUL_CONFIG_PREFIX") | key }}'
}
该模板是按照配置项来渲染的,在该模板中使用了Consul-Template定义两个方法key和keyOrDefault。其中,key会在Consul上对应的K/V创建后,再进行渲染模板;keyOrDefault则会在Consul上没有对应的K/V时,使用默认值代替。
模板中还使用了 " CONSUL_CONFIG_PREFIX " 这个环境变量,这样,不同的产品线便可以使用同一个模板文件,只需要修改" CONSUL_CONFIG_PREFIX "这个环境变量的值即可。
2.配置文件
配置文件是按照HashiCorp Configuration Language (HCL)编写的,示例如下:
template {
source = "config-tree.ctmpl",
destination = "config-tree.json",
command = "sh updateAndReload.sh config-tree.json”
}
template {
source = "config-kv.ctmpl",
destination = "config-kv.lua",
command = "sh updateAndReload.sh config-kv.lua”
}
该配置文件的作用是使用" source"指定的两个模板文件进行渲染,将渲染的结果分别保存在" destination"指定的文件中,保存成功后,分别运行" command"指定的命令来更新并加载配置文件。
配置的更新方式
在个推的微服务体系中,配置的更新方式有两种:
1.替换配置文件,reload服务
2.调用服务接口直接更新内存中的配置
而在Java实现的微服务中,热更新配置通常是在代码中引入Consul Client的依赖,在应用启动时,会初始化一个Watcher来监听Consul上对应目录下K/V的变化,相关的K/V发生变化时,Watcher会负责将其拉取下来,然后调用相关的代码进行配置的更新。
基于Consul的二次开发-CCenter
配置中心CCenter在Consul上提供了更友好的WEB UI,并且增加版本控制,每次配置的更新都会生成一个版本,在应用版本后,配置才真正生效,可以更加方便地进行配置版本间的差异比较,应用任意版本的配置。
总结
以上就是个推在微服务实践中,基于Consul实现的一套配置管理的方案,作为轻量级的分布式K/V存储系统, Consul非常适合用于配置管理,可以帮助开发者们方便、快速地搭建配置中心,结合Consul-Template则可以方便地实现配置的实时更新,在Consul的基础上进行二次开发,实现了配置版本的有效控制,对微服务的配置管理起到了良好的辅助作用。
作者:个推应用平台基础架构高级研发工程师 阿飞
在微服务架构体系中,由于微服务众多,服务之间又有互相调用关系,因此,一个通用的分布式配置管理是必不可少的。一般来说,配置管理需要解决配置集中管理、在系统运行期间可实现动态配置、配置修改后支持自动刷新等问题。
在大多数微服务体系中,都会有一个名为配置文件的功能模块来提供统一的分布式配置管理。构建配置中心,统一对应用中各个微服务进行管理,对微服务体系的意义重大。
Consul为什么适合做配置管理
Consul作为轻量级的分布式K/V存储系统,搭建方便,可用性高,并且支持多数据中心,提供Web UI进行K/V管理。此外Consul还可以结合Consul-Template或者在代码中引入Consul Client的相关依赖创建Watcher来实时Watch K/V的变化,是配置管理的不二之选。
下图为个推微服务体系基于Consul配置管理的整体设计。其中,CCenter就是在Consul的基础上进行二次开发的配置中心。
微服务体系下配置的分类和组织形式
在实践中,不同产品线的配置会放置在Consul的不同路径下,实现不同产品线配置之间的隔离。
按照配置的用途,可将同一产品线下的配置分为三类:
1.API网关相关配置;
2.服务注册与发现相关配置;
3.应用相关配置。
其中,每类配置会对应Consul上的不同目录。
按照配置的变化特性,可将配置分为两类:
1.环境相关的全局配置
如MySQL等外部依赖相关的配置和其他与环境相关的配置,这类配置在开发测试生产环境中存在差异,需要为不同环境配置不同的值。
2.应用本身的配置
一般为不经常性发生变化、可动态调整、开关的配置。这类配置比较稳定,在初始化后,只有在需要时才会改动,通常会设置默认值。这两类配置在Consul上会放在不同的子目录下。这样QA、运维只需要关注环境差异部分即可。
基于以上对配置的分类,最终Consul上的Key的格式如下:
/ProductLine_Prefix/Usage_Prefix/Environmental_Correlation_Prefix/Config_Item_Path
其中,
ProductLine_Prefix:用来隔离不同产品线的配置;
Usage_Prefix:用来区分配置的用途;
Environmental_Correlation_Prefix:用来分隔与环境相关的配置;
Config_Item_Path:具体的配置项。
配置在Consul上的组织形式有以下两种:
1.以配置文件的形式组织,Consul上的一个K/V,对应一个配置文件,如nginx的配置文件。
2.以配置项的形式组织,将配置文件模板化,拆成一个个的配置项,每个配置项对应Consul上的一个K/V,多个配置项对应一个配置文件。大部分配置文件本身都是以K/V的形式组织的,均适合模板化,模板化后即可以按照配置项的特性,在Consul上分成不同的类别进行管理。
如何实现配置更新
Consul上的K/V,要如何生成可加载的应用,或可使用的配置呢?
1.用Node和Lua实现的微服务的配置更新,使用Consul-Template来实现;
2.用Java实现的微服务的配置更新,通过Consul-Template工具(需要重启应用)和在代码中引入Consul Client的依赖创建Watcher(热更新)这两种方式来实现。
Consul-Template如何使用?
Consul-Template是一个后台进程,它可以根据Watch Consul上K/V的变化,更新任意数量的模板,同时生成对应的文件,之后还可以运行任意的命令。要使用Consul-Template一般需要定义两个文件:
1.模板文件
模板文件一般按照Go Template的格式进行编写,示例如下:
config-tree.ctmpl:
{{ tree /consul/path/to/configFiles | explode | toJSONPretty }}
该模板在/consul/path/to/configFiles路径下的配置发生变化时,会渲染出一个Json格式的字符串,其中包含了/consul/path/to/configFiles下所有的K/V.
config-kv.ctmpl:
return {
host='{{ printf "%s/mysql/host" (env "CONSUL_CONFIG_PREFIX") | key }}',
port={{ keyOrDefault (printf "%s/mysql/port" (env "CON-SUL_CONFIG_PREFIX")) "3306" }},
user='{{ printf "%s/mysql/user" (env "CONSUL_CONFIG_PREFIX") | key }}',
password='{{ printf "%s/mysql/password" (env "CON-SUL_CONFIG_PREFIX") | key }}'
}
该模板是按照配置项来渲染的,在该模板中使用了Consul-Template定义两个方法key和keyOrDefault。其中,key会在Consul上对应的K/V创建后,再进行渲染模板;keyOrDefault则会在Consul上没有对应的K/V时,使用默认值代替。
模板中还使用了 " CONSUL_CONFIG_PREFIX " 这个环境变量,这样,不同的产品线便可以使用同一个模板文件,只需要修改" CONSUL_CONFIG_PREFIX "这个环境变量的值即可。
2.配置文件
配置文件是按照HashiCorp Configuration Language (HCL)编写的,示例如下:
template {
source = "config-tree.ctmpl",
destination = "config-tree.json",
command = "sh updateAndReload.sh config-tree.json”
}
template {
source = "config-kv.ctmpl",
destination = "config-kv.lua",
command = "sh updateAndReload.sh config-kv.lua”
}
该配置文件的作用是使用" source"指定的两个模板文件进行渲染,将渲染的结果分别保存在" destination"指定的文件中,保存成功后,分别运行" command"指定的命令来更新并加载配置文件。
配置的更新方式
在个推的微服务体系中,配置的更新方式有两种:
1.替换配置文件,reload服务
2.调用服务接口直接更新内存中的配置
而在Java实现的微服务中,热更新配置通常是在代码中引入Consul Client的依赖,在应用启动时,会初始化一个Watcher来监听Consul上对应目录下K/V的变化,相关的K/V发生变化时,Watcher会负责将其拉取下来,然后调用相关的代码进行配置的更新。
基于Consul的二次开发-CCenter
配置中心CCenter在Consul上提供了更友好的WEB UI,并且增加版本控制,每次配置的更新都会生成一个版本,在应用版本后,配置才真正生效,可以更加方便地进行配置版本间的差异比较,应用任意版本的配置。
总结
以上就是个推在微服务实践中,基于Consul实现的一套配置管理的方案,作为轻量级的分布式K/V存储系统, Consul非常适合用于配置管理,可以帮助开发者们方便、快速地搭建配置中心,结合Consul-Template则可以方便地实现配置的实时更新,在Consul的基础上进行二次开发,实现了配置版本的有效控制,对微服务的配置管理起到了良好的辅助作用。
收起阅读 »
hello uniapp 原生选项卡和非原生选项卡差别真是大啊(速度和性能)
我加载远程数据,原生顶端选项卡,基本上秒加载,同样的数据源,非原生选项卡,要耗费三倍以上时间,才能加载渲染完成。
性能和加载速度不是一个量级的。
左右滑动非原生选项卡,多划几次就卡死。官方的也这样。性能堪忧!
weex的性能确实好。但是不能横屏。好郁闷!
我加载远程数据,原生顶端选项卡,基本上秒加载,同样的数据源,非原生选项卡,要耗费三倍以上时间,才能加载渲染完成。
性能和加载速度不是一个量级的。
左右滑动非原生选项卡,多划几次就卡死。官方的也这样。性能堪忧!
weex的性能确实好。但是不能横屏。好郁闷!

app调用摄像头拍照+相册选择并且转base64上传到服务器(含上传ajax)
引用的CSS:
<link rel="stylesheet" type="text/css" href="../css/mui.min.css" />
引用的JS:
<script src="../js/mui.min.js"></script>
html部分如下:
<ul class="mui-table-view mui-table-view-chevron">
<li class="mui-table-view-cell" id="headImage">
<a class="mui-navigate-right" style="padding-right: 38px;">
<img class="mui-media-object mui-pull-right" style="width: 42px;height:42px;" id="mainImage">
<div style="line-height: 42px;color:#8f8f94 ;">上传单据图片</div>
</a>
</li>
</ul>
JS部分如下----首先定义两个数组:
var imagereturn=[];
var endBaseimg =[];
document.getElementById('headImage').addEventListener('click', function() {
if (mui.os.plus) {
var a = [{
title: "拍照"
}, {
title: "从手机相册选择"
}];
plus.nativeUI.actionSheet({
title: "上传单据图片",
cancel: "取消",
buttons: a
}, function(b) { /actionSheet 按钮点击事件/
switch (b.index) {
case 0:
break;
case 1:
getImage(); /拍照/
break;
case 2:
galleryImg();/打开相册/
break;
default:
break;
}
})
}
}, false);
// }
//拍照
function getImage() {
var c = plus.camera.getCamera();
c.captureImage(function(e) {
plus.io.resolveLocalFileSystemURL(e, function(entry) {
var s = entry.toLocalURL() + "?version=" + new Date().getTime();
uploadHead(s); /上传图片/
}, function(e) {
console.log("读取拍照文件错误:" + e.message);
});
}, function(s) {
console.log("error" + s);
}, {
filename: "_doc/head.png"
});
}
//本地相册选择
function galleryImg() {
plus.gallery.pick(function(a) {
plus.io.resolveLocalFileSystemURL(a, function(entry) {
plus.io.resolveLocalFileSystemURL("_doc/", function(root) {
root.getFile("head.png", {}, function(file) {
//文件已存在
file.remove(function() {
entry.copyTo(root, 'head.png', function(e) {
var e = e.fullPath + "?version=" + new Date().getTime();
uploadHead(e); /上传图片/
//变更大图预览的src
//目前仅有一张图片,暂时如此处理,后续需要通过标准组件实现
},
function(e) {
console.log('copy image fail:' + e.message);
});
}, function() {
console.log("delete image fail:" + e.message);
});
}, function() {
//文件不存在
entry.copyTo(root, 'head.png', function(e) {
var path = e.fullPath + "?version=" + new Date().getTime();
uploadHead(path); /上传图片/
},
function(e) {
console.log('copy image fail:' + e.message);
});
});
}, function(e) {
console.log("get _www folder fail");
})
}, function(e) {
console.log("读取拍照文件错误:" + e.message);
});
}, function(a) {}, {
filter: "image"
})
}
function uploadHead(imgPath) {
endBaseimg=[]
mainImage.src = imgPath;
var image = new Image();
image.src = imgPath;
image.onload = function() {
var imgData = getBase64Image(image);
endBaseimg.push(imgData);
// console.log(imgData)
/在这里调用上传接口/
var url = serverurl+'index/shangchuan';
mui.ajax(url, {
data:{
token:storaToken,
tu_cont:endBaseimg,
tu_kuozhan:'png'
},
type: 'post',
success: function(res) {
// console.log(JSON.stringify(res))
if(res.code == 1){
mui.alert('上传图片到服务器成功', '系统提示', function() { });
imagereturn = res.data.img_path
}
},
error: function(res) {
// alert(JSON.stringify(res))
mui.toast("请求服务器失败", {
duration: 'long',
type: 'div'
});
}
});
}
}
//将图片压缩转成base64
function getBase64Image(img) {
var canvas = document.createElement("canvas");
var width = img.width;
var height = img.height;
// calculate the width and height, constraining the proportions
if (width > height) {
if (width > 800) {
height = Math.round(height = 800 / width);
width = 800;
}
} else {
if (height > 800) {
width = Math.round(width = 800 / height);
height = 800;
}
}
canvas.width = width; /设置新的图片的宽度/
canvas.height = height; /设置新的图片的长度/
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height); /绘图/
var dataURL = canvas.toDataURL("image/png", 0.8);
// return dataURL.replace("data:image/png;base64,", "");
return dataURL;
}
引用的CSS:
<link rel="stylesheet" type="text/css" href="../css/mui.min.css" />
引用的JS:
<script src="../js/mui.min.js"></script>
html部分如下:
<ul class="mui-table-view mui-table-view-chevron">
<li class="mui-table-view-cell" id="headImage">
<a class="mui-navigate-right" style="padding-right: 38px;">
<img class="mui-media-object mui-pull-right" style="width: 42px;height:42px;" id="mainImage">
<div style="line-height: 42px;color:#8f8f94 ;">上传单据图片</div>
</a>
</li>
</ul>
JS部分如下----首先定义两个数组:
var imagereturn=[];
var endBaseimg =[];
document.getElementById('headImage').addEventListener('click', function() {
if (mui.os.plus) {
var a = [{
title: "拍照"
}, {
title: "从手机相册选择"
}];
plus.nativeUI.actionSheet({
title: "上传单据图片",
cancel: "取消",
buttons: a
}, function(b) { /actionSheet 按钮点击事件/
switch (b.index) {
case 0:
break;
case 1:
getImage(); /拍照/
break;
case 2:
galleryImg();/打开相册/
break;
default:
break;
}
})
}
}, false);
// }
//拍照
function getImage() {
var c = plus.camera.getCamera();
c.captureImage(function(e) {
plus.io.resolveLocalFileSystemURL(e, function(entry) {
var s = entry.toLocalURL() + "?version=" + new Date().getTime();
uploadHead(s); /上传图片/
}, function(e) {
console.log("读取拍照文件错误:" + e.message);
});
}, function(s) {
console.log("error" + s);
}, {
filename: "_doc/head.png"
});
}
//本地相册选择
function galleryImg() {
plus.gallery.pick(function(a) {
plus.io.resolveLocalFileSystemURL(a, function(entry) {
plus.io.resolveLocalFileSystemURL("_doc/", function(root) {
root.getFile("head.png", {}, function(file) {
//文件已存在
file.remove(function() {
entry.copyTo(root, 'head.png', function(e) {
var e = e.fullPath + "?version=" + new Date().getTime();
uploadHead(e); /上传图片/
//变更大图预览的src
//目前仅有一张图片,暂时如此处理,后续需要通过标准组件实现
},
function(e) {
console.log('copy image fail:' + e.message);
});
}, function() {
console.log("delete image fail:" + e.message);
});
}, function() {
//文件不存在
entry.copyTo(root, 'head.png', function(e) {
var path = e.fullPath + "?version=" + new Date().getTime();
uploadHead(path); /上传图片/
},
function(e) {
console.log('copy image fail:' + e.message);
});
});
}, function(e) {
console.log("get _www folder fail");
})
}, function(e) {
console.log("读取拍照文件错误:" + e.message);
});
}, function(a) {}, {
filter: "image"
})
}
function uploadHead(imgPath) {
endBaseimg=[]
mainImage.src = imgPath;
var image = new Image();
image.src = imgPath;
image.onload = function() {
var imgData = getBase64Image(image);
endBaseimg.push(imgData);
// console.log(imgData)
/在这里调用上传接口/
var url = serverurl+'index/shangchuan';
mui.ajax(url, {
data:{
token:storaToken,
tu_cont:endBaseimg,
tu_kuozhan:'png'
},
type: 'post',
success: function(res) {
// console.log(JSON.stringify(res))
if(res.code == 1){
mui.alert('上传图片到服务器成功', '系统提示', function() { });
imagereturn = res.data.img_path
}
},
error: function(res) {
// alert(JSON.stringify(res))
mui.toast("请求服务器失败", {
duration: 'long',
type: 'div'
});
}
});
}
}
//将图片压缩转成base64
function getBase64Image(img) {
var canvas = document.createElement("canvas");
var width = img.width;
var height = img.height;
// calculate the width and height, constraining the proportions
if (width > height) {
if (width > 800) {
height = Math.round(height = 800 / width);
width = 800;
}
} else {
if (height > 800) {
width = Math.round(width = 800 / height);
height = 800;
}
}
canvas.width = width; /设置新的图片的宽度/
canvas.height = height; /设置新的图片的长度/
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height); /绘图/
var dataURL = canvas.toDataURL("image/png", 0.8);
// return dataURL.replace("data:image/png;base64,", "");
return dataURL;
}

传智播客:年薪60万招聘uni-app培训讲师!
招聘岗位:前端与移动开发课程研究员,uni-app培训讲师
薪资范围:40k-60k
公司介绍:
传智播客是致力于高素质软件开发人才培养的新三板挂牌公司(代码:839976)。公司网址:http://www.itcast.cn
福利待遇:
五险一金、补充医疗险、意外险
交通补助、餐补
带薪年假
员工体检
其他福利:生日福利、节日福利、员工旅游、传智父母节等。
岗位职责:
针对前端流行技术进行相关预研工作
基于主流技术方案进行项目研发
负责设计前端项目课程
关注前端技术变革,能够快速进行课程转化
任职要求:
本科以上学历,5年以上前端领域开发经验,具备良好的表达能力
具备扎实的计算机理论基础,较强的学习能力和解决问题能力(重要)
熟悉前端页面局部,具备扎实的原生JavaScript基本功,熟悉Less/Sass等CSS预处理器,理解CSS模块化
熟悉Node.js
深入理解前端工程化思想,熟练使用webpack、gulp等工具
在实际项目中使用过Angular、React、Vue其中之一并有实践经验
熟悉前端echarts、d3、three.js等可视化库
有过微信小程序、服务号或Hybrid开发经验。
能够针对不同的业务场景进行合理的技术选型和项目架构
具备Java或者其他后台开发经验优先
熟悉uni-app开发
加分项:
有自己开源的前端项目
有课程设计或者授课经历
有自己的技术博客
工作地点:
北京
地址1:北京市昌平区建材城西路金燕龙办公楼一层(总部)
地址2:北京市昌平区北七家镇七北路42号TBD云集中心2号楼3单元3层
地址3:北京市顺义区京顺路99号黑马程序员(教学楼A栋)
上海
地址:上海市浦东新区航头镇航都路18号万香创新港
深圳
地址1:深圳市宝安区留仙二路中粮商务公园3栋17层
广州
地址:广州市天河区珠吉路58号津安创意园
武汉
地址1:湖北省武汉市东湖高新区高新二路22号中国光谷云计算海外高新企业孵化中心1号楼3层5层
地址2:武汉市东湖新技术开发区汤逊湖北路8号 湖北国知专利创业孵化园(华师一附中南门正对面)6号楼(知乐楼)
郑州
地址:郑州市高新区长椿路11号国家大学科技园8号楼三层
西安
地址:西安经开区草滩六路1369号绘锦园A栋3-4层
联系人:
丁老师:15321987531(微信与电话同步 )
招聘岗位:前端与移动开发课程研究员,uni-app培训讲师
薪资范围:40k-60k
公司介绍:
传智播客是致力于高素质软件开发人才培养的新三板挂牌公司(代码:839976)。公司网址:http://www.itcast.cn
福利待遇:
五险一金、补充医疗险、意外险
交通补助、餐补
带薪年假
员工体检
其他福利:生日福利、节日福利、员工旅游、传智父母节等。
岗位职责:
针对前端流行技术进行相关预研工作
基于主流技术方案进行项目研发
负责设计前端项目课程
关注前端技术变革,能够快速进行课程转化
任职要求:
本科以上学历,5年以上前端领域开发经验,具备良好的表达能力
具备扎实的计算机理论基础,较强的学习能力和解决问题能力(重要)
熟悉前端页面局部,具备扎实的原生JavaScript基本功,熟悉Less/Sass等CSS预处理器,理解CSS模块化
熟悉Node.js
深入理解前端工程化思想,熟练使用webpack、gulp等工具
在实际项目中使用过Angular、React、Vue其中之一并有实践经验
熟悉前端echarts、d3、three.js等可视化库
有过微信小程序、服务号或Hybrid开发经验。
能够针对不同的业务场景进行合理的技术选型和项目架构
具备Java或者其他后台开发经验优先
熟悉uni-app开发
加分项:
有自己开源的前端项目
有课程设计或者授课经历
有自己的技术博客
工作地点:
北京
地址1:北京市昌平区建材城西路金燕龙办公楼一层(总部)
地址2:北京市昌平区北七家镇七北路42号TBD云集中心2号楼3单元3层
地址3:北京市顺义区京顺路99号黑马程序员(教学楼A栋)
上海
地址:上海市浦东新区航头镇航都路18号万香创新港
深圳
地址1:深圳市宝安区留仙二路中粮商务公园3栋17层
广州
地址:广州市天河区珠吉路58号津安创意园
武汉
地址1:湖北省武汉市东湖高新区高新二路22号中国光谷云计算海外高新企业孵化中心1号楼3层5层
地址2:武汉市东湖新技术开发区汤逊湖北路8号 湖北国知专利创业孵化园(华师一附中南门正对面)6号楼(知乐楼)
郑州
地址:郑州市高新区长椿路11号国家大学科技园8号楼三层
西安
地址:西安经开区草滩六路1369号绘锦园A栋3-4层
联系人:
丁老师:15321987531(微信与电话同步 )
收起阅读 »
验证码登录
<template>
<view class="content">
<view class="login-info">
<view class="login">
<text class="login-title">输入验证码</text>
<text class="login-phone">已发到:+86 {{phone}}</text>
</view>
<form @submit="formSubmit">
<view class="uni-common-mt">
<view class="uni-flex uni-row" style="justify-content: center">
<view class="validate" @tap="tapStop" v-for="(item,index) in Length" :key="index">
<input class="uni-input v-code" :value="Value.length>=index+1?Value[index]:''" disabled maxlength="1"/>
</view>
</view>
<input name="password" class='input-code' type="number" :maxlength="Length" :focus="isFocus" @input="monitorInput" @blur="blurFocus"/>
<view class="">
<input class="uni-input" hidden type="text" name="phone" :value="phone" />
<button type="primary" formType="submit" id="go-login" class="go-login" :disabled="disabled" :class="[!prohibitSubmit ? 'prohibit-submit' : '']">{{reset_time}}</button>
</view>
</view>
</form>
</view>
</view>
</template>
<script>
export default {
data() {
return {
Length:4,
Value:"",
isFocus:true,
prohibitSubmit: false,
disabled: true,
phone:'',
reset_time:'重新获取',
setTime: null,
}
},
onLoad(e){
if(e.phone){
this.phone = e.phone;
}else{
uni.navigateBack({
delta: 1
});
}
},
created: function(e) {
this.setInterValFunc(this);
},
methods: {
blurFocus:function(e){
console.log('失去焦点');
this.isFocus=false;
},
monitorInput:function(event){
console.log(event.target.value);
var inputValue = event.target.value;
this.Value=inputValue;
if(inputValue.length == 4){
this.isFocus=false;
uni.request({
url: 'https://www.',
method:'POST',
data: {validate_code:event.target.value,phone:this.phone},
success: function(res) {
uni.hideLoading();
var data=res.data;
if(data.code>0){
uni.setStorage({
key: 'utoken',
data: data.data,
fail: () => {
uni.showModal({
title: '登录失败-写入数据',
showCancel:false
})
return false;
}
});
uni.switchTab({
url: '/pages/index/index'
});
}else{
uni.showModal({
title: "系统提示",
content: data.msg,
showCancel: false,
confirmText: "确定"
})
}
}
});
}
},
tapStop:function(event){
console.log(event);
this.isFocus=true;
},
setInterValFunc: function(obj) {
obj.prohibitSubmit = false;
obj.disabled = true;
obj.isFocus = true;
var reset_time = 60;
obj.setTime = setInterval(function() {
obj.reset_time= "重新获取 ("+ --reset_time +")";
if(reset_time <=0 ){
clearInterval(obj.setTime);
obj.reset_time= "重新获取";
obj.prohibitSubmit = true;
obj.disabled = false;
}
}, 1000);
},
formSubmit: function(e) {
if (e.detail.value.phone.length != 11) {
uni.showModal({
title: "系统提示",
content: "请输入正确的手机号",
showCancel: false,
confirmText: "确定"
})
return false;
};
uni.showLoading({
title: '加载中'
});
var obj=this;
uni.request({
url: 'https://www.',
method:'POST',
data: JSON.stringify(e.detail.value),
success: function(res) {
uni.hideLoading();
var data=res.data;
if(data.code>0){
obj.setInterValFunc(obj);
}else{
uni.showModal({
title: "系统提示",
content: data.msg,
showCancel: false,
confirmText: "确定"
})
}
}
});
}
}
}
</script>
<style>
page {
display: flex;
}
.content{
width: 750upx;
}
.login-info{
margin: 0 5%;
}
.login{
margin-top: 20upx;
}
.login-title{
margin-left: 20upx;
font-size:36upx;
font-weight: bold;
}
.login-phone{
margin-left: 20upx;
margin-top: 8upx;
display: block;
font-size:24upx;
color: #353535;
}
.uni-row{
margin-top: 60upx;
}
.validate{
width: 90upx;
height: 90upx;
margin: 0 20upx;
}
.v-code{
font-size: 45upx;
text-align: center;
border:1rpx solid #ddd;
border-radius: 20rpx;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.go-login{
height: 100upx;
line-height: 100upx;
border-radius: 50upx;
background-color: #f85314!important;
border-color: #f85314!important;
font-size: 30upx;
color: #FFFFFF;
margin-top: 10upx;
}
.input-code{
width: 0;
height: 0;
}
.uni-icon-clear{
color: #8f8f94;
}
.prohibit-submit{
background-color: #cec6c6!important;
border-color: #cec6c6!important;
}
.user-login{
font-size: 22upx;
color: #8f8f94;
margin-left: 30upx;
}
</style>
<template>
<view class="content">
<view class="login-info">
<view class="login">
<text class="login-title">输入验证码</text>
<text class="login-phone">已发到:+86 {{phone}}</text>
</view>
<form @submit="formSubmit">
<view class="uni-common-mt">
<view class="uni-flex uni-row" style="justify-content: center">
<view class="validate" @tap="tapStop" v-for="(item,index) in Length" :key="index">
<input class="uni-input v-code" :value="Value.length>=index+1?Value[index]:''" disabled maxlength="1"/>
</view>
</view>
<input name="password" class='input-code' type="number" :maxlength="Length" :focus="isFocus" @input="monitorInput" @blur="blurFocus"/>
<view class="">
<input class="uni-input" hidden type="text" name="phone" :value="phone" />
<button type="primary" formType="submit" id="go-login" class="go-login" :disabled="disabled" :class="[!prohibitSubmit ? 'prohibit-submit' : '']">{{reset_time}}</button>
</view>
</view>
</form>
</view>
</view>
</template>
<script>
export default {
data() {
return {
Length:4,
Value:"",
isFocus:true,
prohibitSubmit: false,
disabled: true,
phone:'',
reset_time:'重新获取',
setTime: null,
}
},
onLoad(e){
if(e.phone){
this.phone = e.phone;
}else{
uni.navigateBack({
delta: 1
});
}
},
created: function(e) {
this.setInterValFunc(this);
},
methods: {
blurFocus:function(e){
console.log('失去焦点');
this.isFocus=false;
},
monitorInput:function(event){
console.log(event.target.value);
var inputValue = event.target.value;
this.Value=inputValue;
if(inputValue.length == 4){
this.isFocus=false;
uni.request({
url: 'https://www.',
method:'POST',
data: {validate_code:event.target.value,phone:this.phone},
success: function(res) {
uni.hideLoading();
var data=res.data;
if(data.code>0){
uni.setStorage({
key: 'utoken',
data: data.data,
fail: () => {
uni.showModal({
title: '登录失败-写入数据',
showCancel:false
})
return false;
}
});
uni.switchTab({
url: '/pages/index/index'
});
}else{
uni.showModal({
title: "系统提示",
content: data.msg,
showCancel: false,
confirmText: "确定"
})
}
}
});
}
},
tapStop:function(event){
console.log(event);
this.isFocus=true;
},
setInterValFunc: function(obj) {
obj.prohibitSubmit = false;
obj.disabled = true;
obj.isFocus = true;
var reset_time = 60;
obj.setTime = setInterval(function() {
obj.reset_time= "重新获取 ("+ --reset_time +")";
if(reset_time <=0 ){
clearInterval(obj.setTime);
obj.reset_time= "重新获取";
obj.prohibitSubmit = true;
obj.disabled = false;
}
}, 1000);
},
formSubmit: function(e) {
if (e.detail.value.phone.length != 11) {
uni.showModal({
title: "系统提示",
content: "请输入正确的手机号",
showCancel: false,
confirmText: "确定"
})
return false;
};
uni.showLoading({
title: '加载中'
});
var obj=this;
uni.request({
url: 'https://www.',
method:'POST',
data: JSON.stringify(e.detail.value),
success: function(res) {
uni.hideLoading();
var data=res.data;
if(data.code>0){
obj.setInterValFunc(obj);
}else{
uni.showModal({
title: "系统提示",
content: data.msg,
showCancel: false,
confirmText: "确定"
})
}
}
});
}
}
}
</script>
<style>
page {
display: flex;
}
.content{
width: 750upx;
}
.login-info{
margin: 0 5%;
}
.login{
margin-top: 20upx;
}
.login-title{
margin-left: 20upx;
font-size:36upx;
font-weight: bold;
}
.login-phone{
margin-left: 20upx;
margin-top: 8upx;
display: block;
font-size:24upx;
color: #353535;
}
.uni-row{
margin-top: 60upx;
}
.validate{
width: 90upx;
height: 90upx;
margin: 0 20upx;
}
.v-code{
font-size: 45upx;
text-align: center;
border:1rpx solid #ddd;
border-radius: 20rpx;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.go-login{
height: 100upx;
line-height: 100upx;
border-radius: 50upx;
background-color: #f85314!important;
border-color: #f85314!important;
font-size: 30upx;
color: #FFFFFF;
margin-top: 10upx;
}
.input-code{
width: 0;
height: 0;
}
.uni-icon-clear{
color: #8f8f94;
}
.prohibit-submit{
background-color: #cec6c6!important;
border-color: #cec6c6!important;
}
.user-login{
font-size: 22upx;
color: #8f8f94;
margin-left: 30upx;
}
</style>

调研分享 -- 基于H5+SDK的跨平台集成方案
这是我做的关于公司内部方案的分享,主要讲解了
H5+历史发展和实现原理。
基于JsBridge跨平台开发的演进和Uni-app框架体系介绍。
从0集成到安卓原生工程的实践。
欢迎拍砖,点赞。
这是我做的关于公司内部方案的分享,主要讲解了
H5+历史发展和实现原理。
基于JsBridge跨平台开发的演进和Uni-app框架体系介绍。
从0集成到安卓原生工程的实践。
欢迎拍砖,点赞。

个推Node.js 微服务实践:基于容器的一站式命令行工具链
作者:个推Node.js 开发工程师 之诺
背景与摘要
由于工程数量的快速增长,个推在实践基于 Node.js 的微服务开发的过程中,遇到了如下问题:
-
每次新建项目都需要安装一次依赖,这些依赖之间基本相似却又有微妙的区别;
-
每次新建项目都要配置一遍相似的配置(比如 tsconfig、lint 规则等);
-
本地 Mac 环境与线上 Docker 内的 Linux 环境不一致(尤其是有 C++ 依赖的情况)。
为了解决上述问题,个推内部开发了一个命令行小工具来标准化项目初始化流程、简化配置甚至是零配置,提供基于 Docker 的一致构建、运行环境。
CLI: init, build, test & pack
新建一个 Node.js 项目的时候,我们一般会:
-
安装许多开发依赖:TypeScript、Jest、TSLint、benchmark、typedoc 等;
-
配置 tsconfig、lint 规则、.prettierrc 等;
-
安装众多项目依赖:koa、lodash、sequelize、ioredis、zipkin、node-fetch 等;
-
初始化目录结构;
-
配置CI 脚本。
通常,我们会选择复制一个现成的项目进行修改,导致出现众多看似相似却又不完全相同的项目,比如十个项目可能会对应十种配置组合。对于同时跨多个工程的开发人员来说,众多配置组合会增加他们的工作难度。而且,当安全审计发现某些 npm package 出现安全隐患时,开发人员则需要对每个引用这些包的项目逐一检查和修正。
在确定的开发场景下,几乎所有项目的开发依赖都差不多,开发配置也非常相似,因此我们基于 commander.js 写了一个 init 工具,它会开个命令行的向导,自动安装依赖、初始化项目目录结构和配置。从而创建项目,并按照场景将所有配置收缩为特定几种模板,进行统一处理。
随后,我们有了 build、test、pack 命令,托管了 tsconfig、jest 配置、打包配置,自动调用 tsc 编译,构建测试环境,然后调用 Jest 进行测试,进行标准化打包, CI 脚本基本可以简化为几行标准脚本。
CLI: Docker Build
在介绍这个命令前需要先简单了解一下个推的镜像体系:
前面提到我们将大部分依赖封装到了一个 npm 包,这一层封装也反映在个推的 Docker 镜像体系内,可以简单表述为下面的 Dockerfile:
# 公共依赖层的 Dockerfile
FROM node:10
RUN mkdir -p /usr/local/lib/webnode/node_modules \
&& cd /usr/local/lib/webnode \
&& npm install webnode
ENV NODE_PATH /usr/local/lib/webnode/node_modules
# 项目的 Dockerfile
FROM getui/webnode:1.2.3
COPY package*.json ./
RUN npm install
COPY . .
当把这层依赖直接做进 Docker 镜像时,虽然每个镜像的 SIZE 还是 1G 多,但是每个镜像的 UNIQUE SIZE 都是极小的,仅有数M的差分层。
一个简单的对比,比如有 800M 公共系统依赖 + 每个服务平均 200M 的 npm 依赖 + 1M 的服务代码,那么由于原先每个服务都会 npm install 大量重复依赖,20 个服务,就会有 800M + 200M 20 + 1M 20 = 4.82G 的总 UNIQUE SIZE。而采用依赖分层共享,则仅有 800M + 200M + 1M * 20 = 1.02G 的总 UNIQUE SIZE。在考虑应用的多版本之后,依赖分层共享带来在存储上的优势会更加明显。
我们以一定的依赖锁定周期和控制为代价,换取了:
- 减少依赖组合、依赖版本组合的可能性,开发者选择包的简化、初始化项目的简化;审计简化、安全更新简化 。
- CI 显著提速,节省等待时间。
- 传输和存储的压力减少许多。
- 公共依赖被多个项目使用,得到了更加充分的测试。
- webnode docker build 命令可以帮助简化 Docker image 的构建过程,它内置了一个 Dockerfile 和dockerignore,该命令运行时,会基于这两个文件和当前的 Context,自动构建docker 镜像。其中 Dockerfile 内含一些优化和我们的最佳实践,开发人员只需要专注 Node.js 的项目的开发,这个命令则可以负责配置文件权限等操作以及生成标准化的、优化的 Docker 镜像。
其设计目标是:
- 快:合理的依赖分层,最大程度应用 Docker 缓存机制,通过 .dockerignore 裁剪不必要的 Context,因此可以实现飞快的构建速度 。
- 小:依据变更频度做 Docker 分层设计、应用 multi-stage build,尽最大可能缩小一个镜像的 UNIQUE SIZE 。
- 可重现:同样的内容总是构建出相同的结果。
以 node_modules 依赖优化为例,下面两种 Dockerfile 其实会有很大的区别:
FROM getui/webnode:1.2.3
COPY . .
RUN npm install
FROM getui/webnode:1.2.3
COPY package*.json ./
RUN npm install
COPY . .
前者,每次 docker build 时,只要项目内任何代码变了,npm install 的缓存都会失效,需要重新安装,而后者仅当 package*.json 发生改变之时才会触发重新 npm install。另外,我们还会对 package.json 进行预编译,仅保留依赖相关的字段,避免出现修改 package.json 的版本号就重新 npm install的情况。
webnode docker build 不仅可以帮助开发者进行统一化的镜像构建、统一实践最佳优化,节约资源,还能避免所有开发人员都需要接触优化细节,省时省力。
CLI: Webnode Docker Start
在本地调试开发的过程中,我们遇到了一些环境差异引起的问题:
- 生产环境与本地开发环境 Node.js 版本不一致。
- 一些含有 C++ 代码的 npm 依赖运行的跨平台问题 。
- 文件权限配置、系统目录结构与线上运行环境不完全一致 。
- 启动初始化流程不一致(比如配置预拉取)。
- 开发本地常常缺少一些二进制工具或版本不一致(比如 consul-template、nc 等)。
- 与本地直接启动 Node.js 程序有所不同,这个命令会优先基于当前项目利用上面的 webnode docker build 命令构建 Docker 镜像,然后启动镜像。
Docker 可以帮助消解环境差异:
- 便捷地携带与生产环境一致的Node.js 版本以及其他二进制依赖。
- 一致的初始化流程。
- 轻松运行含有 C++ 的 npm 依赖。
- 文件权限、目录结构与线上运行环境一致。
容器化的Node.js调试方法有些许变化,需要暴露Node.js的Inspector端口,然后配一下Visual Studio Code的localRoot和remoteRoot:
WEBNODE_HOST=${WEBNODE_HOST:-127.0.0.1}
WEBNODE_PORT=${WEBNODE_PORT:-3000}
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS \
-it \
--rm \
--network=\"getui-dev\"
-p $WEBNODE_HOST:$WEBNODE_PORT:3000 \
-p 127.0.0.1:9229:9229 \
-e NODE_FLAGS=--inspect=0.0.0.0:9229 \
--name $CONTAINER"
docker run \
$DOCKER_RUN_OPTIONS \
$DOCKER_IMAGE_TAG
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach Local WebNode",
"address": "127.0.0.1",
"port": 9229,
"restart": true,
"protocol": "inspector",
"localRoot": "${workspaceFolder}",
"remoteRoot": "YOUR_REMOTE_ROOT",
"sourceMaps": true
},
]
}
基于容器开发 CLI 工具
基于容器的开发可以带来诸多好处。一是便于分发,基于 Docker 的 Tag,开发者可以很方便地做基于小版本、大版本、分支的分发,可以像 nvm 一样去切换版本。
二是CLI 脚本不用处处考虑跨平台兼容的问题,比如:
sed 在 Linux 和 Mac 下工作行为不一致的问题之类的。
有的环境有 Python 3 有的环境只有 Python 2
所有的依赖通过容器带进来,简洁而高效。
在基于 Docker 的工具开发的过程中,我们也遇到一些问题:
一是容器内外 UID/GID 不一致,如果是以非 ROOT 用户运行 docker run,会导致容器内程序在挂载的目录产生的文件权限与当前用户不一致。
Docker for Mac对于文件权限有一些特别的行为,具体可以参见:https://docs.docker.com/docker-for-mac/osxfs/#ownership
对于 Host 是 Linux 的情况,尤其在 CI 时,需要考虑 UID/GID 的问题。对于这种情况,我们选择覆盖掉了 entrypoint ,然后用 gosu 去做降权来处理。
CLI_EXEC_UID=${CLI_EXEC_UID:-0}
CLI_EXEC_GID=${CLI_EXEC_GID:-0}
exec gosu $CLI_EXEC_UID:$CLI_EXEC_GID env "$@"
其实RedHat 旗下用于设计container runtime 的daemonless (例如 podman),就很适合做CLI工具,可以 rootless 运行,又尊重系统的权限配置。然而其目前尚未成熟,业界采用率也不高,仍需要继续观望。
二是有时候 docker run 速度较慢,个推的解决方案是在首次启动时启动一个 docker run --detach,然后后续的 CLI 执行完全通过 docker exec 来进行,这样避免掉了每次执行命令时启动的开销,速度提升明显。
小结
以上便是个推 Node.js 微服务开发实践中关于 CLI 工具的实践,个推试图标准化、优化项目结构以及镜像构建,减少组合的可能性,有效降低了存储、传输、构建的成本,让开发人员更加省时省力。
后续我们还会继续为大家介绍个推的 Docker 镜像体系设计以及Node.js 微服务开发框架,敬请期待。
参考
https://docs.docker.com/docker-for-mac/osxfs/#ownership
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#entrypoint
https://www.projectatomic.io/blog/2018/02/reintroduction-podman/
https://www.slideshare.net/AkihiroSuda/the-state-of-rootless-containers
https://www.debian.org/doc/manuals/debian-faq/ch-pkg_basics.en.html#s-virtual
作者:个推Node.js 开发工程师 之诺
背景与摘要
由于工程数量的快速增长,个推在实践基于 Node.js 的微服务开发的过程中,遇到了如下问题:
-
每次新建项目都需要安装一次依赖,这些依赖之间基本相似却又有微妙的区别;
-
每次新建项目都要配置一遍相似的配置(比如 tsconfig、lint 规则等);
-
本地 Mac 环境与线上 Docker 内的 Linux 环境不一致(尤其是有 C++ 依赖的情况)。
为了解决上述问题,个推内部开发了一个命令行小工具来标准化项目初始化流程、简化配置甚至是零配置,提供基于 Docker 的一致构建、运行环境。
CLI: init, build, test & pack
新建一个 Node.js 项目的时候,我们一般会:
-
安装许多开发依赖:TypeScript、Jest、TSLint、benchmark、typedoc 等;
-
配置 tsconfig、lint 规则、.prettierrc 等;
-
安装众多项目依赖:koa、lodash、sequelize、ioredis、zipkin、node-fetch 等;
-
初始化目录结构;
-
配置CI 脚本。
通常,我们会选择复制一个现成的项目进行修改,导致出现众多看似相似却又不完全相同的项目,比如十个项目可能会对应十种配置组合。对于同时跨多个工程的开发人员来说,众多配置组合会增加他们的工作难度。而且,当安全审计发现某些 npm package 出现安全隐患时,开发人员则需要对每个引用这些包的项目逐一检查和修正。
在确定的开发场景下,几乎所有项目的开发依赖都差不多,开发配置也非常相似,因此我们基于 commander.js 写了一个 init 工具,它会开个命令行的向导,自动安装依赖、初始化项目目录结构和配置。从而创建项目,并按照场景将所有配置收缩为特定几种模板,进行统一处理。
随后,我们有了 build、test、pack 命令,托管了 tsconfig、jest 配置、打包配置,自动调用 tsc 编译,构建测试环境,然后调用 Jest 进行测试,进行标准化打包, CI 脚本基本可以简化为几行标准脚本。
CLI: Docker Build
在介绍这个命令前需要先简单了解一下个推的镜像体系:
前面提到我们将大部分依赖封装到了一个 npm 包,这一层封装也反映在个推的 Docker 镜像体系内,可以简单表述为下面的 Dockerfile:
# 公共依赖层的 Dockerfile
FROM node:10
RUN mkdir -p /usr/local/lib/webnode/node_modules \
&& cd /usr/local/lib/webnode \
&& npm install webnode
ENV NODE_PATH /usr/local/lib/webnode/node_modules
# 项目的 Dockerfile
FROM getui/webnode:1.2.3
COPY package*.json ./
RUN npm install
COPY . .
当把这层依赖直接做进 Docker 镜像时,虽然每个镜像的 SIZE 还是 1G 多,但是每个镜像的 UNIQUE SIZE 都是极小的,仅有数M的差分层。
一个简单的对比,比如有 800M 公共系统依赖 + 每个服务平均 200M 的 npm 依赖 + 1M 的服务代码,那么由于原先每个服务都会 npm install 大量重复依赖,20 个服务,就会有 800M + 200M 20 + 1M 20 = 4.82G 的总 UNIQUE SIZE。而采用依赖分层共享,则仅有 800M + 200M + 1M * 20 = 1.02G 的总 UNIQUE SIZE。在考虑应用的多版本之后,依赖分层共享带来在存储上的优势会更加明显。
我们以一定的依赖锁定周期和控制为代价,换取了:
- 减少依赖组合、依赖版本组合的可能性,开发者选择包的简化、初始化项目的简化;审计简化、安全更新简化 。
- CI 显著提速,节省等待时间。
- 传输和存储的压力减少许多。
- 公共依赖被多个项目使用,得到了更加充分的测试。
- webnode docker build 命令可以帮助简化 Docker image 的构建过程,它内置了一个 Dockerfile 和dockerignore,该命令运行时,会基于这两个文件和当前的 Context,自动构建docker 镜像。其中 Dockerfile 内含一些优化和我们的最佳实践,开发人员只需要专注 Node.js 的项目的开发,这个命令则可以负责配置文件权限等操作以及生成标准化的、优化的 Docker 镜像。
其设计目标是:
- 快:合理的依赖分层,最大程度应用 Docker 缓存机制,通过 .dockerignore 裁剪不必要的 Context,因此可以实现飞快的构建速度 。
- 小:依据变更频度做 Docker 分层设计、应用 multi-stage build,尽最大可能缩小一个镜像的 UNIQUE SIZE 。
- 可重现:同样的内容总是构建出相同的结果。
以 node_modules 依赖优化为例,下面两种 Dockerfile 其实会有很大的区别:
FROM getui/webnode:1.2.3
COPY . .
RUN npm install
FROM getui/webnode:1.2.3
COPY package*.json ./
RUN npm install
COPY . .
前者,每次 docker build 时,只要项目内任何代码变了,npm install 的缓存都会失效,需要重新安装,而后者仅当 package*.json 发生改变之时才会触发重新 npm install。另外,我们还会对 package.json 进行预编译,仅保留依赖相关的字段,避免出现修改 package.json 的版本号就重新 npm install的情况。
webnode docker build 不仅可以帮助开发者进行统一化的镜像构建、统一实践最佳优化,节约资源,还能避免所有开发人员都需要接触优化细节,省时省力。
CLI: Webnode Docker Start
在本地调试开发的过程中,我们遇到了一些环境差异引起的问题:
- 生产环境与本地开发环境 Node.js 版本不一致。
- 一些含有 C++ 代码的 npm 依赖运行的跨平台问题 。
- 文件权限配置、系统目录结构与线上运行环境不完全一致 。
- 启动初始化流程不一致(比如配置预拉取)。
- 开发本地常常缺少一些二进制工具或版本不一致(比如 consul-template、nc 等)。
- 与本地直接启动 Node.js 程序有所不同,这个命令会优先基于当前项目利用上面的 webnode docker build 命令构建 Docker 镜像,然后启动镜像。
Docker 可以帮助消解环境差异:
- 便捷地携带与生产环境一致的Node.js 版本以及其他二进制依赖。
- 一致的初始化流程。
- 轻松运行含有 C++ 的 npm 依赖。
- 文件权限、目录结构与线上运行环境一致。
容器化的Node.js调试方法有些许变化,需要暴露Node.js的Inspector端口,然后配一下Visual Studio Code的localRoot和remoteRoot:
WEBNODE_HOST=${WEBNODE_HOST:-127.0.0.1}
WEBNODE_PORT=${WEBNODE_PORT:-3000}
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS \
-it \
--rm \
--network=\"getui-dev\"
-p $WEBNODE_HOST:$WEBNODE_PORT:3000 \
-p 127.0.0.1:9229:9229 \
-e NODE_FLAGS=--inspect=0.0.0.0:9229 \
--name $CONTAINER"
docker run \
$DOCKER_RUN_OPTIONS \
$DOCKER_IMAGE_TAG
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach Local WebNode",
"address": "127.0.0.1",
"port": 9229,
"restart": true,
"protocol": "inspector",
"localRoot": "${workspaceFolder}",
"remoteRoot": "YOUR_REMOTE_ROOT",
"sourceMaps": true
},
]
}
基于容器开发 CLI 工具
基于容器的开发可以带来诸多好处。一是便于分发,基于 Docker 的 Tag,开发者可以很方便地做基于小版本、大版本、分支的分发,可以像 nvm 一样去切换版本。
二是CLI 脚本不用处处考虑跨平台兼容的问题,比如:
sed 在 Linux 和 Mac 下工作行为不一致的问题之类的。
有的环境有 Python 3 有的环境只有 Python 2
所有的依赖通过容器带进来,简洁而高效。
在基于 Docker 的工具开发的过程中,我们也遇到一些问题:
一是容器内外 UID/GID 不一致,如果是以非 ROOT 用户运行 docker run,会导致容器内程序在挂载的目录产生的文件权限与当前用户不一致。
Docker for Mac对于文件权限有一些特别的行为,具体可以参见:https://docs.docker.com/docker-for-mac/osxfs/#ownership
对于 Host 是 Linux 的情况,尤其在 CI 时,需要考虑 UID/GID 的问题。对于这种情况,我们选择覆盖掉了 entrypoint ,然后用 gosu 去做降权来处理。
CLI_EXEC_UID=${CLI_EXEC_UID:-0}
CLI_EXEC_GID=${CLI_EXEC_GID:-0}
exec gosu $CLI_EXEC_UID:$CLI_EXEC_GID env "$@"
其实RedHat 旗下用于设计container runtime 的daemonless (例如 podman),就很适合做CLI工具,可以 rootless 运行,又尊重系统的权限配置。然而其目前尚未成熟,业界采用率也不高,仍需要继续观望。
二是有时候 docker run 速度较慢,个推的解决方案是在首次启动时启动一个 docker run --detach,然后后续的 CLI 执行完全通过 docker exec 来进行,这样避免掉了每次执行命令时启动的开销,速度提升明显。
小结
以上便是个推 Node.js 微服务开发实践中关于 CLI 工具的实践,个推试图标准化、优化项目结构以及镜像构建,减少组合的可能性,有效降低了存储、传输、构建的成本,让开发人员更加省时省力。
后续我们还会继续为大家介绍个推的 Docker 镜像体系设计以及Node.js 微服务开发框架,敬请期待。
参考
https://docs.docker.com/docker-for-mac/osxfs/#ownership
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#entrypoint
https://www.projectatomic.io/blog/2018/02/reintroduction-podman/
https://www.slideshare.net/AkihiroSuda/the-state-of-rootless-containers
https://www.debian.org/doc/manuals/debian-faq/ch-pkg_basics.en.html#s-virtual

uni-app项目数字滚动
uni-app项目数字滚动
插件预览图
使用教程
1.插件代码拷贝
- 下载后把components目录下countUp.vue文件拷贝到自己项目目录下
2.插件全局配置
- 在项目里main.js中配置如下代码
import countUp from './components/countUp.vue'
Vue.component('countup',countUp)
3.插件使用
- vue页面使用
<template>
<view>
<countup :num="num" color="#ff9e50" width='13' height='23' fontSize='23'></countup>
<button @tap="add">Add</button>
<button @tap="reduce">Reduce</button>
</view>
</template>
<script>
export default {
data() {
return {
num:123.453
};
},
onLoad() {},
methods: {
add() {
this.num=++this.num;
},
reduce(){
this.num=--this.num;
}
}
};
</script>
兼容性
uni-app项目中使用都兼容
uni-app项目数字滚动
插件预览图
使用教程
1.插件代码拷贝
- 下载后把components目录下countUp.vue文件拷贝到自己项目目录下
2.插件全局配置
- 在项目里main.js中配置如下代码
import countUp from './components/countUp.vue'
Vue.component('countup',countUp)
3.插件使用
- vue页面使用
<template>
<view>
<countup :num="num" color="#ff9e50" width='13' height='23' fontSize='23'></countup>
<button @tap="add">Add</button>
<button @tap="reduce">Reduce</button>
</view>
</template>
<script>
export default {
data() {
return {
num:123.453
};
},
onLoad() {},
methods: {
add() {
this.num=++this.num;
},
reduce(){
this.num=--this.num;
}
}
};
</script>
兼容性
uni-app项目中使用都兼容
收起阅读 »
uni-app自定义验证码、密码输入框
Getting started
插件预览图
使用教程
1.插件代码拷贝
- 下载后把components目录下validCode.vue文件拷贝到自己项目目录下
2.插件全局配置
- 在项目里main.js中配置如下代码
import validCode from './components/validCode.vue'
Vue.component('validcode',validCode)
3.插件使用
- vue页面使用
<template>
<view class="content">
<view>验证码:</view>
<validcode :maxlength="4" :isPwd="false" @finish="getCode"></validcode>
<view>密码:</view>
<validcode :maxlength="6" :isPwd="true" @finish="getPwd"></validcode>
</view>
</template>
<script>
export default {
data() {
return {};
},
onLoad() {},
methods: {
getCode(val) {
console.log(val);
},
getPwd(val){
console.log(val);
}
}
};
</script>
<style>
.content {
height: 400upx;
}
</style>
Getting started
插件预览图
使用教程
1.插件代码拷贝
- 下载后把components目录下validCode.vue文件拷贝到自己项目目录下
2.插件全局配置
- 在项目里main.js中配置如下代码
import validCode from './components/validCode.vue'
Vue.component('validcode',validCode)
3.插件使用
- vue页面使用
<template>
<view class="content">
<view>验证码:</view>
<validcode :maxlength="4" :isPwd="false" @finish="getCode"></validcode>
<view>密码:</view>
<validcode :maxlength="6" :isPwd="true" @finish="getPwd"></validcode>
</view>
</template>
<script>
export default {
data() {
return {};
},
onLoad() {},
methods: {
getCode(val) {
console.log(val);
},
getPwd(val){
console.log(val);
}
}
};
</script>
<style>
.content {
height: 400upx;
}
</style>
收起阅读 »