HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

uni-app 中如何打开外部应用,如:浏览器、淘宝、AppStore、QQ等

打开外部应用 Runtime plus uniapp scheme schema

我们在开发 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 模块,下面我罗列两个相关的方法。其他操作请详读文档。

  1. 调用外部浏览器打开指定的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>  
    
  2. 调用第三方程序

    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 模块,下面我罗列两个相关的方法。其他操作请详读文档。

  1. 调用外部浏览器打开指定的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>  
    
  2. 调用第三方程序

    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配置方法

收起阅读 »

fdsafdsa

fdsafdsa

fdsafdsa

个推基于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集成到安卓原生工程的实践。

欢迎拍砖,点赞。

一个签到、兑换、收集的组件,求赞赏。

h5 小程序 mpvue uni_app

是uni-app通用组件,支持mpvue、小程序、原生、H5等

是uni-app通用组件,支持mpvue、小程序、原生、H5等

个推Node.js 微服务实践:基于容器的一站式命令行工具链

node.js

作者:个推Node.js 开发工程师 之诺

背景与摘要

由于工程数量的快速增长,个推在实践基于 Node.js 的微服务开发的过程中,遇到了如下问题:

  1. 每次新建项目都需要安装一次依赖,这些依赖之间基本相似却又有微妙的区别;

  2. 每次新建项目都要配置一遍相似的配置(比如 tsconfig、lint 规则等);

  3. 本地 Mac 环境与线上 Docker 内的 Linux 环境不一致(尤其是有 C++ 依赖的情况)。

为了解决上述问题,个推内部开发了一个命令行小工具来标准化项目初始化流程、简化配置甚至是零配置,提供基于 Docker 的一致构建、运行环境。

CLI: init, build, test & pack

新建一个 Node.js 项目的时候,我们一般会:

  1. 安装许多开发依赖:TypeScript、Jest、TSLint、benchmark、typedoc 等;

  2. 配置 tsconfig、lint 规则、.prettierrc 等;

  3. 安装众多项目依赖:koa、lodash、sequelize、ioredis、zipkin、node-fetch 等;

  4. 初始化目录结构;

  5. 配置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 的微服务开发的过程中,遇到了如下问题:

  1. 每次新建项目都需要安装一次依赖,这些依赖之间基本相似却又有微妙的区别;

  2. 每次新建项目都要配置一遍相似的配置(比如 tsconfig、lint 规则等);

  3. 本地 Mac 环境与线上 Docker 内的 Linux 环境不一致(尤其是有 C++ 依赖的情况)。

为了解决上述问题,个推内部开发了一个命令行小工具来标准化项目初始化流程、简化配置甚至是零配置,提供基于 Docker 的一致构建、运行环境。

CLI: init, build, test & pack

新建一个 Node.js 项目的时候,我们一般会:

  1. 安装许多开发依赖:TypeScript、Jest、TSLint、benchmark、typedoc 等;

  2. 配置 tsconfig、lint 规则、.prettierrc 等;

  3. 安装众多项目依赖:koa、lodash、sequelize、ioredis、zipkin、node-fetch 等;

  4. 初始化目录结构;

  5. 配置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项目数字滚动

uniapp插件 uniapp

uni-app项目数字滚动

github地址,喜欢的可以star下哦

插件预览图

使用教程

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项目数字滚动

github地址,喜欢的可以star下哦

插件预览图

使用教程

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自定义验证码、密码输入框

Vue uniapp

Getting started

github地址,喜欢的可以star下哦

插件预览图

使用教程

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

github地址,喜欢的可以star下哦

插件预览图

使用教程

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>  
收起阅读 »