
【5+】Webview页面之间的数据交流
一个App,其中大部分是要对页面之间的数据进行交互。
碧如:A打开B页面,B页面执行一些代码,再通知回A页面。
这可能是h5+er们遇到最常见的一个场景了。
ok,我们将问题实例化:
A页面有个选择地区的按钮,需要打开B页面选择一个地区,然后获取到选取结果返回给A页面并展示。
我们看看用mui.fire怎么来实现这个功能
A页面
<header class="mui-bar mui-bar-nav">
<h1 class="mui-title">A</h1>
</header>
<div class="mui-content">
<input type="text" readonly placeholder="未选择">
<button type="button" class="mui-btn mui-btn-blue">选取地区</button>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init();
// 自定义监听select事件
document.addEventListener('select', function(e){
var text = e.detail.text;
document.querySelector("input").value = text;
});
// 按钮点击事件
document.querySelector(".mui-btn-blue").addEventListener('tap', function(){
// 打开B页面,选取一个结果
mui.openWindow('B.html');
});
</script>
B页面
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">B</h1>
</header>
<div class="mui-content">
<ul class="mui-table-view">
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
上海
</a>
</li>
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
深圳
</a>
</li>
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
北京
</a>
</li>
</ul>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init();
mui('ul').on('tap', 'li', function() {
// 获取当前选择的内容
var text = this.innerText;
// 通知上个页面
var w = plus.webview.currentWebview();
var opener = w.opener();
mui.fire(opener, "select",{
text: text
});
// 关闭本页面
w.close();
});
真机调试一下,o98k。
但是!我个人还是建议脱离mui.js来实现这个功能
可以借用咱们之前文章里面的讲过的,利用webview对象的evalJS方法
【5+】跨webview多页面 触发事件(一)
【5+】跨webview多页面 触发事件(一)
感觉用Broadcast.js有点小题大做
那咱们就写一个类似Android中的onActivityResult和setResult方法
新建一个app.js,作为一个自己的插件,里面实现两个方法 onActivityResult 和 setResult
(function(app){
/**
* 打开一个页面
* @param {String} url 页面路径
* @param {String} id 页面id
* @param {Object} ex 参数
* @param {Function} callback
*/
app.onActivityResult = function(url, id, ex, callback){
};
/**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data){
};
}(window.app || (window.app = {})));
我们一步步来,先看看setResult如何触发上个页面的函数
/**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data){
// 获取当前webview
var indexW = plus.webview.currentWebview();
// 获取创建者的webview
var opener = indexW.opener();
// 执行js字符串
opener.evalJS();// ??????
};
卧槽,那么,问题来了,evalJS该执行什么呢?
如果我在A页面的window对象下定一个函数
window.test = function(data){
alert(JSON.stringify(data));
}
那么,我们在evalJS里面就该这么写
// 执行js字符串
var jsstr = "window.test && window.test(" + JSON.stringify(data) + ")";
opener.evalJS(jsstr);
好吧,考虑到一个页面可能通过这个方式打开多个页面,那么我们这个test函数就得改一个不重复唯一的名称,并且定义放到onActivityResult方法里面
var _id = 0,
_tempName = '',
ow,cw;
/**
* 打开一个页面
* @param {String} url 页面路径
* @param {String} id 页面id
* @param {Object} ex 参数
* @param {Function} callback
*/
app.onActivityResult = function(url, id, ex, callback) {
// 生成唯一回调函数名称
_tempName = 'APP_RESULT_FUN_' + _id++;
// 定义函数
window[_tempName] = function(data){
// 执行自定义回调
callback(data);
};
// 传递函数名称到目标页面
ex.callbackName = _tempName;
// 显示菊花
cw = plus.nativeUI.showWaiting();
// 创建目标页面
ow = plus.webview.create(url, id, {
render: "always"
}, ex);
// title更新时显示 页面
ow.addEventListener('titleUpdate', function(){
// 关闭菊花
cw && (cw.close(),cw = null);
// 显示页面
ow.show('pop-in');
});
// 页面关闭时,注销window下此次事件
ow.addEventListener('close', function(){
setTimeout(function(){
window[_tempName] = null;
});
});
};
生成特殊一个函数,并把函数名通过extras的方式传参到目标页面,
相应的,setResult方法也需要少许更改
/**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data) {
// 获取当前webview
var indexW = plus.webview.currentWebview();
// js字符串
var jsstr = "";
// 如果存在自定义回调函数名
if(indexW.callbackName){
// 拼接js字符串
jsstr = "window." + indexW.callbackName;
jsstr = jsstr + "&&" + jsstr + "(" + JSON.stringify(data) + ")";
// 执行
indexW.opener().evalJS(jsstr);
}
// 返回当前页面
return indexW;
};
试试引用后在AB页面测试一下
// A页面 按钮点击事件
document.querySelector(".mui-btn-blue").addEventListener('tap', function(){
// 打开B页面,选取一个结果
app.onActivityResult('B.html', 'B', {}, function(data){
// 修改内容
document.querySelector("input").value = data.text;
});
});
// B页面 选项点击事件
mui('ul').on('tap', 'li', function() {
// 获取当前选择的内容
var text = this.innerText;
// 通知上个页面 并关闭本页面
app.setResult({
text: text
}).close();
});
卧槽666。
class Man{
constructor(){
this.name = 'newsning'
}
say(){
console.log('天行健, 君子以自强不息. ')
}
}
一个App,其中大部分是要对页面之间的数据进行交互。
碧如:A打开B页面,B页面执行一些代码,再通知回A页面。
这可能是h5+er们遇到最常见的一个场景了。
ok,我们将问题实例化:
A页面有个选择地区的按钮,需要打开B页面选择一个地区,然后获取到选取结果返回给A页面并展示。
我们看看用mui.fire怎么来实现这个功能
A页面
<header class="mui-bar mui-bar-nav">
<h1 class="mui-title">A</h1>
</header>
<div class="mui-content">
<input type="text" readonly placeholder="未选择">
<button type="button" class="mui-btn mui-btn-blue">选取地区</button>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init();
// 自定义监听select事件
document.addEventListener('select', function(e){
var text = e.detail.text;
document.querySelector("input").value = text;
});
// 按钮点击事件
document.querySelector(".mui-btn-blue").addEventListener('tap', function(){
// 打开B页面,选取一个结果
mui.openWindow('B.html');
});
</script>
B页面
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">B</h1>
</header>
<div class="mui-content">
<ul class="mui-table-view">
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
上海
</a>
</li>
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
深圳
</a>
</li>
<li class="mui-table-view-cell">
<a class="mui-navigate-right">
北京
</a>
</li>
</ul>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init();
mui('ul').on('tap', 'li', function() {
// 获取当前选择的内容
var text = this.innerText;
// 通知上个页面
var w = plus.webview.currentWebview();
var opener = w.opener();
mui.fire(opener, "select",{
text: text
});
// 关闭本页面
w.close();
});
真机调试一下,o98k。
但是!我个人还是建议脱离mui.js来实现这个功能
可以借用咱们之前文章里面的讲过的,利用webview对象的evalJS方法
【5+】跨webview多页面 触发事件(一)
【5+】跨webview多页面 触发事件(一)
感觉用Broadcast.js有点小题大做
那咱们就写一个类似Android中的onActivityResult和setResult方法
新建一个app.js,作为一个自己的插件,里面实现两个方法 onActivityResult 和 setResult
(function(app){
/**
* 打开一个页面
* @param {String} url 页面路径
* @param {String} id 页面id
* @param {Object} ex 参数
* @param {Function} callback
*/
app.onActivityResult = function(url, id, ex, callback){
};
/**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data){
};
}(window.app || (window.app = {})));
我们一步步来,先看看setResult如何触发上个页面的函数
/**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data){
// 获取当前webview
var indexW = plus.webview.currentWebview();
// 获取创建者的webview
var opener = indexW.opener();
// 执行js字符串
opener.evalJS();// ??????
};
卧槽,那么,问题来了,evalJS该执行什么呢?
如果我在A页面的window对象下定一个函数
window.test = function(data){
alert(JSON.stringify(data));
}
那么,我们在evalJS里面就该这么写
// 执行js字符串
var jsstr = "window.test && window.test(" + JSON.stringify(data) + ")";
opener.evalJS(jsstr);
好吧,考虑到一个页面可能通过这个方式打开多个页面,那么我们这个test函数就得改一个不重复唯一的名称,并且定义放到onActivityResult方法里面
var _id = 0,
_tempName = '',
ow,cw;
/**
* 打开一个页面
* @param {String} url 页面路径
* @param {String} id 页面id
* @param {Object} ex 参数
* @param {Function} callback
*/
app.onActivityResult = function(url, id, ex, callback) {
// 生成唯一回调函数名称
_tempName = 'APP_RESULT_FUN_' + _id++;
// 定义函数
window[_tempName] = function(data){
// 执行自定义回调
callback(data);
};
// 传递函数名称到目标页面
ex.callbackName = _tempName;
// 显示菊花
cw = plus.nativeUI.showWaiting();
// 创建目标页面
ow = plus.webview.create(url, id, {
render: "always"
}, ex);
// title更新时显示 页面
ow.addEventListener('titleUpdate', function(){
// 关闭菊花
cw && (cw.close(),cw = null);
// 显示页面
ow.show('pop-in');
});
// 页面关闭时,注销window下此次事件
ow.addEventListener('close', function(){
setTimeout(function(){
window[_tempName] = null;
});
});
};
生成特殊一个函数,并把函数名通过extras的方式传参到目标页面,
相应的,setResult方法也需要少许更改
/**
* 返回创建者页面数据
* @param {Object} data 需要返回的数据
* @return {Webview} w 当前webview
*/
app.setResult = function(data) {
// 获取当前webview
var indexW = plus.webview.currentWebview();
// js字符串
var jsstr = "";
// 如果存在自定义回调函数名
if(indexW.callbackName){
// 拼接js字符串
jsstr = "window." + indexW.callbackName;
jsstr = jsstr + "&&" + jsstr + "(" + JSON.stringify(data) + ")";
// 执行
indexW.opener().evalJS(jsstr);
}
// 返回当前页面
return indexW;
};
试试引用后在AB页面测试一下
// A页面 按钮点击事件
document.querySelector(".mui-btn-blue").addEventListener('tap', function(){
// 打开B页面,选取一个结果
app.onActivityResult('B.html', 'B', {}, function(data){
// 修改内容
document.querySelector("input").value = data.text;
});
});
// B页面 选项点击事件
mui('ul').on('tap', 'li', function() {
// 获取当前选择的内容
var text = this.innerText;
// 通知上个页面 并关闭本页面
app.setResult({
text: text
}).close();
});
卧槽666。
class Man{
constructor(){
this.name = 'newsning'
}
say(){
console.log('天行健, 君子以自强不息. ')
}
}
收起阅读 »

点击li跳转时总跳转至第一个li的页面。而alert显示的是正确的innertext
var obj_lis = document.getElementById("my_list_movie").getElementsByTagName("li");
for(i = 0; i < obj_lis.length; i++) {
obj_lis[i].onclick = function() {
if(this.innerHTML.indexOf("电影票订单")) {
window.location.href = "ticket_order.html";
alert(this.innerHTML);
} else if(this.innerHTML.indexOf("代金优惠卷")) {
window.location.href = "coupons.html";
alert(this.innerHTML);
} else if(this.innerHTML.indexOf("收藏的电影")) {
window.location.href = "favorite_movie.html";
alert(this.innerHTML);
}
}
}
var obj_lis = document.getElementById("my_list_movie").getElementsByTagName("li");
for(i = 0; i < obj_lis.length; i++) {
obj_lis[i].onclick = function() {
if(this.innerHTML.indexOf("电影票订单")) {
window.location.href = "ticket_order.html";
alert(this.innerHTML);
} else if(this.innerHTML.indexOf("代金优惠卷")) {
window.location.href = "coupons.html";
alert(this.innerHTML);
} else if(this.innerHTML.indexOf("收藏的电影")) {
window.location.href = "favorite_movie.html";
alert(this.innerHTML);
}
}
}
收起阅读 »

vue2.x引入mui的开源项目源码+案例公开,没有过多的业务逻辑,刚写的项目,希望有好的commit提交过来整理好项目
项目地址:https://github.com/wuhou123/mui-vue2
下载app可以查看我的-我的足迹用到h5+和原生定位功能
也有关于mui常见问题文档
如果有问题提issues吧
项目地址:https://github.com/wuhou123/mui-vue2
下载app可以查看我的-我的足迹用到h5+和原生定位功能
也有关于mui常见问题文档
如果有问题提issues吧

css常用知识点
解决img标签与p标签之间有空隙的问题
1.将img设置为block: img{display:block}
- 设置img的竖直对其方式:vertical-align: bottom;
- 定义容器里的字体大小为0。
div{
width:110px;
border:1px solid #000000;
font-size:0px;
}
css实现图片元素水平垂直居中
样式//style .outer{ width: 500px; height: 200px; line-height: 200px; text-align: center; border: green solid 5px; } .inner{ display: inline-block; vertical-algin: middle; }
html
<div class='outer'> <img src="https://www.baidu.com/img/baidu_jgylogo3.gif"> </div>
.one {
position:absolute;
width200px;
height:200px;
top:50%;
left:50%;
margin-top:-100px;
margin-left:-100px;
background:red;
}
.two{
position:fixed;
width:180px;
height:180px;
top:50%;
left:50%;
margin-top:-90px;
margin-left:-90px;
background:orange;
}
.three{
position:fixed; width:160px; height:160px; top:0; right:0; bottom:0; left:0; margin:auto; background:pink;
}
.four{ position:absolute; width:140px; height:140px; top:0; right:0; bottom:0; left:0; margin:auto; background:black;}
css实现单行、多行文本溢出显示省略号
单行文本的移除显示省略号用text-overflow:ellipsis属性,还需要加宽度width属性来兼容部分浏览器。
实现方法:
overflow:hidden;
white-spacen:nowrap;
text-overflow:ellipsis;
1.使用overflow:hidden把超出的内容进行隐藏;
- 然后使用white-space:nowrap设置内容不换行;
- 最后使用text-overflow:ellipsis设置超出内容为省略号
但是这个属性只支持单行文本的溢出显示省略号。
实现多行文本溢出显示省略号,如下。
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
overflow:hidden;
因为使用了webkit的css扩展属性,该方法适用于webkit浏览器及移动端;
注:
1.-webkit-line-clamp用来限制在一个块元素显示的文本的行数。为了实现该效果,它需要组合其他的webkit属性。常见结合属性:
- display:-webkit-box;必须结合的属性,将对象作为弹性伸缩盒子模型显示。
- -webkit-box-orient必须结合的属性,设置或检索伸缩盒对象的子元素的排列方式。
实现方法:p{position:relative;line-height:20px;max-height:40px;overflow:hidden;} p::after{content:"...";position:absolute;bottom:0;right:0;padding-left:40px; background: line-gradient(to right,transparent,#fff 55%); background: -webkit-linear-gradient(left, transparent, #fff 55%); background: -o-linear-gradient(right, transparent, #fff 55%); background: -moz-linear-gradient(right, transparent, #fff 55%); }
该方法使用范围广,但文字未超出行的情况下也会出现省略号,可结合js优化该方法。
注:
1.将height设置为line-height的整倍数,防止超出的文字露出。 - 给p::after添加背景可避免文字只显示一半。
3.由于ie6-7不显示content内容,所以要添加标签兼容ie6-7(如:<span>…<span/>);兼容ie8需要将::after替换成:after
css3中的transform对行内元素(inline)无效,查资料后发现,transform适用于:所有块级元素及某些内联元素。
相关链接:http://www.css88.com/book/css/properties/transform/transform.htm
解决img标签与p标签之间有空隙的问题
1.将img设置为block: img{display:block}
- 设置img的竖直对其方式:vertical-align: bottom;
- 定义容器里的字体大小为0。
div{
width:110px;
border:1px solid #000000;
font-size:0px;
}
css实现图片元素水平垂直居中
样式//style .outer{ width: 500px; height: 200px; line-height: 200px; text-align: center; border: green solid 5px; } .inner{ display: inline-block; vertical-algin: middle; }
html
<div class='outer'> <img src="https://www.baidu.com/img/baidu_jgylogo3.gif"> </div>
.one {
position:absolute;
width200px;
height:200px;
top:50%;
left:50%;
margin-top:-100px;
margin-left:-100px;
background:red;
}
.two{
position:fixed;
width:180px;
height:180px;
top:50%;
left:50%;
margin-top:-90px;
margin-left:-90px;
background:orange;
}
.three{
position:fixed; width:160px; height:160px; top:0; right:0; bottom:0; left:0; margin:auto; background:pink;
}
.four{ position:absolute; width:140px; height:140px; top:0; right:0; bottom:0; left:0; margin:auto; background:black;}
css实现单行、多行文本溢出显示省略号
单行文本的移除显示省略号用text-overflow:ellipsis属性,还需要加宽度width属性来兼容部分浏览器。
实现方法:
overflow:hidden;
white-spacen:nowrap;
text-overflow:ellipsis;
1.使用overflow:hidden把超出的内容进行隐藏;
- 然后使用white-space:nowrap设置内容不换行;
- 最后使用text-overflow:ellipsis设置超出内容为省略号
但是这个属性只支持单行文本的溢出显示省略号。
实现多行文本溢出显示省略号,如下。
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
overflow:hidden;
因为使用了webkit的css扩展属性,该方法适用于webkit浏览器及移动端;
注:
1.-webkit-line-clamp用来限制在一个块元素显示的文本的行数。为了实现该效果,它需要组合其他的webkit属性。常见结合属性:
- display:-webkit-box;必须结合的属性,将对象作为弹性伸缩盒子模型显示。
- -webkit-box-orient必须结合的属性,设置或检索伸缩盒对象的子元素的排列方式。
实现方法:p{position:relative;line-height:20px;max-height:40px;overflow:hidden;} p::after{content:"...";position:absolute;bottom:0;right:0;padding-left:40px; background: line-gradient(to right,transparent,#fff 55%); background: -webkit-linear-gradient(left, transparent, #fff 55%); background: -o-linear-gradient(right, transparent, #fff 55%); background: -moz-linear-gradient(right, transparent, #fff 55%); }
该方法使用范围广,但文字未超出行的情况下也会出现省略号,可结合js优化该方法。
注:
1.将height设置为line-height的整倍数,防止超出的文字露出。 - 给p::after添加背景可避免文字只显示一半。
3.由于ie6-7不显示content内容,所以要添加标签兼容ie6-7(如:<span>…<span/>);兼容ie8需要将::after替换成:after
css3中的transform对行内元素(inline)无效,查资料后发现,transform适用于:所有块级元素及某些内联元素。
相关链接:http://www.css88.com/book/css/properties/transform/transform.htm

cookie和session有什么区别?
1.由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识别具体的用户,这个机制就是session。典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的session,用于标识这个用户,并且跟踪用户,这样才知道购物车里有几本书这个session是保存在服务端的,有一个唯一标识。在服务端保存session的方法很多,内存、数据库、文件都有。集群的时候也要考虑session的转移,在大型的网站,一般会有专门的session服务器集群,用来保存用户会话,这个时候session信息都是放在内存的,使用一些缓存服务比如Mecached之类的来放session。
- 服务端是如何识别特定的客户的?这个时候cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用Cookie来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在Cookie里面记录一个Seeion ID,以后每次请求都会把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了Cookie怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被加上一个诸如sid=XXX这样的参数,服务端据此来识别用户。
- cookie信息可以写到cookie里面,访问网站的时候,网站页面的脚本客户已读取这个信息,就自动帮你把用户名填了,方便用户
session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存咋集群、数据库、文件中;
cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是是实现session的一种方式。
不要混淆session和session实现。
本来session是一个抽象的概念,开发者为了实现中断和继续等操作,将user agent和server之间一对一的交互,抽象为“会话”,进而衍生出“会话状态”,也就是session的概念。
而cookie是一个实际存在的东西,http协议中定义在header中的字段。可以认为是session的一种后端无状态实现。
而我们今天常说的“session”,是为了绕开cookie的各种限制,通常借助cookie本身和后端存储实现的,一种更高级的会话状态实现。
所以cookie和session,你可以认为是同一层次的概念,也可以认为是不同层次的概念。具体到实现,session因为session id的存在,通常要借助cookie实现,但这并非必要,只能说是通用性较好的一种实现方案。
1.session在服务端,cookie在客户端(浏览器)
- session默认被存在服务器的一个文件里(不是内存)
- session的运行依赖session id,而session id是存在cookie中的,也就是说如果浏览器禁用了cookie,同时session也会失效(可以通过其它方式实现,如在url中传递session_id)
- session可以放在文件、数据库、或内存中的都可以。
- 用户验证这种场合一般都会用session
因此,维持一个会话的核心就是客户端的唯一标识,即session_id
1.由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识别具体的用户,这个机制就是session。典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的session,用于标识这个用户,并且跟踪用户,这样才知道购物车里有几本书这个session是保存在服务端的,有一个唯一标识。在服务端保存session的方法很多,内存、数据库、文件都有。集群的时候也要考虑session的转移,在大型的网站,一般会有专门的session服务器集群,用来保存用户会话,这个时候session信息都是放在内存的,使用一些缓存服务比如Mecached之类的来放session。
- 服务端是如何识别特定的客户的?这个时候cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用Cookie来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在Cookie里面记录一个Seeion ID,以后每次请求都会把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了Cookie怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被加上一个诸如sid=XXX这样的参数,服务端据此来识别用户。
- cookie信息可以写到cookie里面,访问网站的时候,网站页面的脚本客户已读取这个信息,就自动帮你把用户名填了,方便用户
session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存咋集群、数据库、文件中;
cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是是实现session的一种方式。
不要混淆session和session实现。
本来session是一个抽象的概念,开发者为了实现中断和继续等操作,将user agent和server之间一对一的交互,抽象为“会话”,进而衍生出“会话状态”,也就是session的概念。
而cookie是一个实际存在的东西,http协议中定义在header中的字段。可以认为是session的一种后端无状态实现。
而我们今天常说的“session”,是为了绕开cookie的各种限制,通常借助cookie本身和后端存储实现的,一种更高级的会话状态实现。
所以cookie和session,你可以认为是同一层次的概念,也可以认为是不同层次的概念。具体到实现,session因为session id的存在,通常要借助cookie实现,但这并非必要,只能说是通用性较好的一种实现方案。
1.session在服务端,cookie在客户端(浏览器)
- session默认被存在服务器的一个文件里(不是内存)
- session的运行依赖session id,而session id是存在cookie中的,也就是说如果浏览器禁用了cookie,同时session也会失效(可以通过其它方式实现,如在url中传递session_id)
- session可以放在文件、数据库、或内存中的都可以。
- 用户验证这种场合一般都会用session
因此,维持一个会话的核心就是客户端的唯一标识,即session_id

关于打包正式版APP
一定要用最新版。否则图标弄死都是Hbuilder那个图标。
坑了我一个月,也是醉了。都不强制更新下app。找资料也找不到。
一定要用最新版。否则图标弄死都是Hbuilder那个图标。
坑了我一个月,也是醉了。都不强制更新下app。找资料也找不到。

上传图片和压缩
分享自己写的一个上传图片并压缩的方法
element是点击的元素
defaultPic默认图片的名称
defaultPath默认图片的路径
category是我写的一个分类
overwrite是否覆盖压缩 默认覆盖压缩
quality压缩的大小 默认百分之二十
overWriteName 图片压缩后的名称
<div class="uploadPhotoItem">
<img class="pictureUpload pictures" src="../../../images/pic_morentu.jpg">
<span>照片</span>
</div>
//获取上传图片元素
var pictureUploads = document.getElementsByClassName('pictureUpload');
for (var i = 0; i < pictureUploads.length; i++) {
utilsJs.uploadPictures({
element: pictureUploads[i],
defaultPic: 'pic_morentu',
defaultPath: '../../images/pic_morentu.jpg',
category: 'order',
onSuccess: function () {
},
onFailed: function () {
ApiConfig.staticShowToast('图片上传失败');
}
})
}
Utils.prototype.uploadPictures = function (options) {
var _this = this;
options = options || {};
var element = options.element;
var quality = options.quality;
var category = options.category;
var overwrite = options.overwrite;
var defaultPic = options.defaultPic;
var defaultPath = options.defaultPath;
var overWriteName = options.overWriteName;
if (element) {
element.addEventListener('tap', function () {
_this.selectUploadWay({
element: element,
quality: quality,
category: category,
overwrite: overwrite,
defaultPic: defaultPic,
defaultPath: defaultPath,
overWriteName: overWriteName,
onSuccess: function (responseJson) {
options.onSuccess && options.onSuccess(responseJson);
},
onFailed: function (errorJson) {
options.onFailed && options.onFailed(errorJson);
},
onCompleted: function () {
options.onCompleted && options.onCompleted();
}
});
})
}
}
Utils.prototype.selectUploadWay = function (options) {
options = options || {};
var _this = this;
var element = options.element;
var defaultPic = options.defaultPic;
var defaultPath = options.defaultPath;
var overWriteName = options.overWriteName;
var bts = [{
title: '相册'
}, {
title: '拍照'
}, {
title: '删除'
}];
plus.nativeUI.actionSheet({
cancel: "取消",
buttons: bts
},
function (e) {
var i = e.index;
switch (i) {
case 1:
// 拍照添加文件
plus.gallery.pick(function (path) {
_this.uploadPictureHandle({
src: path,
dst: options.dst,
quality: options.quality,
element: options.element,
category: options.category,
overwrite: options.overwrite,
overWriteName: overWriteName,
onSuccess: function (responseJson) {
options.onSuccess && options.onSuccess(responseJson);
},
onFailed: function (errorJson) {
options.onFailed && options.onFailed(errorJson);
},
onCompleted: function () {
options.onCompleted && options.onCompleted();
}
})
});
break;
case 2:
// 从相册添加文件
plus.camera.getCamera().captureImage(function (path) {
_this.uploadPictureHandle({
src: path,
dst: options.dst,
quality: options.quality,
element: options.element,
category: options.category,
overwrite: options.overwrite,
overWriteName: overWriteName,
onSuccess: function (responseJson) {
options.onSuccess && options.onSuccess(responseJson);
},
onFailed: function (errorJson) {
options.onFailed && options.onFailed(errorJson);
},
onCompleted: function () {
options.onCompleted && options.onCompleted();
}
})
});
break;
case 3:
if (element.src.indexOf(defaultPic) >= 0) {
ApiConfig.staticShowToast('当前未上传图片,无法删除');
} else {
element.src = defaultPath;
}
break;
default:
break;
}
}
);
}
Utils.prototype.uploadPictureHandle = function (options) {
options = options || {};
var element = options.element;
var overWriteName = options.overWriteName;
if (!options.overwrite) {
overWriteName = '_doc/default.jpg';
}
plus.zip.compressImage({
src: options.src,
dst: overWriteName,
quality: options.quality || 20,
overwrite: options.overwrite || true
},
function (event) {
dpHttp.upLoadPictureHttp({
files: event.target,
category: options.category,
onSuccess: function (responseJson) {
element.src = responseJson.url;
options.onSuccess && options.onSuccess(responseJson);
},
onFailed: function (errorJson) {
options.onFailed && options.onFailed(errorJson);
},
onCompleted: function () {
options.onCompleted && options.onCompleted();
}
});
},
function (error) {
options.onFailed && options.onFailed(error);
console.log(JSON.stringify(error));
}
);
}
HttpUtils.prototype.upLoadFile = function (options) {
var wt = plus.nativeUI.showWaiting();
options = options || {};
var responseJson = null;
var postParams = options.postParams;
var postFilesMap = options.postFilesMap;
ApiConfig.staticIsDebug('upLoadFile', options.url);
var task = plus.uploader.createUpload(options.url, {
method: "POST",
priority: 100
}, function (t, status) {
if (status == 200) {
try {
wt.close();
responseJson = JSON.parse(t.responseText);
if (responseJson.rt) {
ApiConfig.staticIsDebug('responseJson', responseJson, 1);
options.onSuccess && options.onSuccess(responseJson);
} else {
ApiConfig.staticIsDebug('errorJson', responseJson, 1);
options.onFailed && options.onFailed(responseJson);
}
} catch (e) {
wt.close();
ApiConfig.staticIsDebug('error', e);
ApiConfig.staticIsDebug('errorJson', xhr.responseText);
options.onFailed && options.onFailed(responseJson);
}
} else {
wt.close();
ApiConfig.staticIsDebug('errorJson', responseJson, 1);
options.onFailed && options.onFailed(responseJson);
}
});
//task.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var keys = postFilesMap.keys();
task.addFile(postFilesMap.get(keys[0]), {
key: keys[0]
});
if (postParams) {
var paramsKeys = postParams.keys();
if (paramsKeys.length > 1) {
paramsKeys.forEach(function (key) {
task.addData(key, postParams.get(key));
})
} else {
task.addData(paramsKeys[0], postParams.get(paramsKeys[0]));
}
}
task.start();
}
分享自己写的一个上传图片并压缩的方法
element是点击的元素
defaultPic默认图片的名称
defaultPath默认图片的路径
category是我写的一个分类
overwrite是否覆盖压缩 默认覆盖压缩
quality压缩的大小 默认百分之二十
overWriteName 图片压缩后的名称
<div class="uploadPhotoItem">
<img class="pictureUpload pictures" src="../../../images/pic_morentu.jpg">
<span>照片</span>
</div>
//获取上传图片元素
var pictureUploads = document.getElementsByClassName('pictureUpload');
for (var i = 0; i < pictureUploads.length; i++) {
utilsJs.uploadPictures({
element: pictureUploads[i],
defaultPic: 'pic_morentu',
defaultPath: '../../images/pic_morentu.jpg',
category: 'order',
onSuccess: function () {
},
onFailed: function () {
ApiConfig.staticShowToast('图片上传失败');
}
})
}
Utils.prototype.uploadPictures = function (options) {
var _this = this;
options = options || {};
var element = options.element;
var quality = options.quality;
var category = options.category;
var overwrite = options.overwrite;
var defaultPic = options.defaultPic;
var defaultPath = options.defaultPath;
var overWriteName = options.overWriteName;
if (element) {
element.addEventListener('tap', function () {
_this.selectUploadWay({
element: element,
quality: quality,
category: category,
overwrite: overwrite,
defaultPic: defaultPic,
defaultPath: defaultPath,
overWriteName: overWriteName,
onSuccess: function (responseJson) {
options.onSuccess && options.onSuccess(responseJson);
},
onFailed: function (errorJson) {
options.onFailed && options.onFailed(errorJson);
},
onCompleted: function () {
options.onCompleted && options.onCompleted();
}
});
})
}
}
Utils.prototype.selectUploadWay = function (options) {
options = options || {};
var _this = this;
var element = options.element;
var defaultPic = options.defaultPic;
var defaultPath = options.defaultPath;
var overWriteName = options.overWriteName;
var bts = [{
title: '相册'
}, {
title: '拍照'
}, {
title: '删除'
}];
plus.nativeUI.actionSheet({
cancel: "取消",
buttons: bts
},
function (e) {
var i = e.index;
switch (i) {
case 1:
// 拍照添加文件
plus.gallery.pick(function (path) {
_this.uploadPictureHandle({
src: path,
dst: options.dst,
quality: options.quality,
element: options.element,
category: options.category,
overwrite: options.overwrite,
overWriteName: overWriteName,
onSuccess: function (responseJson) {
options.onSuccess && options.onSuccess(responseJson);
},
onFailed: function (errorJson) {
options.onFailed && options.onFailed(errorJson);
},
onCompleted: function () {
options.onCompleted && options.onCompleted();
}
})
});
break;
case 2:
// 从相册添加文件
plus.camera.getCamera().captureImage(function (path) {
_this.uploadPictureHandle({
src: path,
dst: options.dst,
quality: options.quality,
element: options.element,
category: options.category,
overwrite: options.overwrite,
overWriteName: overWriteName,
onSuccess: function (responseJson) {
options.onSuccess && options.onSuccess(responseJson);
},
onFailed: function (errorJson) {
options.onFailed && options.onFailed(errorJson);
},
onCompleted: function () {
options.onCompleted && options.onCompleted();
}
})
});
break;
case 3:
if (element.src.indexOf(defaultPic) >= 0) {
ApiConfig.staticShowToast('当前未上传图片,无法删除');
} else {
element.src = defaultPath;
}
break;
default:
break;
}
}
);
}
Utils.prototype.uploadPictureHandle = function (options) {
options = options || {};
var element = options.element;
var overWriteName = options.overWriteName;
if (!options.overwrite) {
overWriteName = '_doc/default.jpg';
}
plus.zip.compressImage({
src: options.src,
dst: overWriteName,
quality: options.quality || 20,
overwrite: options.overwrite || true
},
function (event) {
dpHttp.upLoadPictureHttp({
files: event.target,
category: options.category,
onSuccess: function (responseJson) {
element.src = responseJson.url;
options.onSuccess && options.onSuccess(responseJson);
},
onFailed: function (errorJson) {
options.onFailed && options.onFailed(errorJson);
},
onCompleted: function () {
options.onCompleted && options.onCompleted();
}
});
},
function (error) {
options.onFailed && options.onFailed(error);
console.log(JSON.stringify(error));
}
);
}
HttpUtils.prototype.upLoadFile = function (options) {
var wt = plus.nativeUI.showWaiting();
options = options || {};
var responseJson = null;
var postParams = options.postParams;
var postFilesMap = options.postFilesMap;
ApiConfig.staticIsDebug('upLoadFile', options.url);
var task = plus.uploader.createUpload(options.url, {
method: "POST",
priority: 100
}, function (t, status) {
if (status == 200) {
try {
wt.close();
responseJson = JSON.parse(t.responseText);
if (responseJson.rt) {
ApiConfig.staticIsDebug('responseJson', responseJson, 1);
options.onSuccess && options.onSuccess(responseJson);
} else {
ApiConfig.staticIsDebug('errorJson', responseJson, 1);
options.onFailed && options.onFailed(responseJson);
}
} catch (e) {
wt.close();
ApiConfig.staticIsDebug('error', e);
ApiConfig.staticIsDebug('errorJson', xhr.responseText);
options.onFailed && options.onFailed(responseJson);
}
} else {
wt.close();
ApiConfig.staticIsDebug('errorJson', responseJson, 1);
options.onFailed && options.onFailed(responseJson);
}
});
//task.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var keys = postFilesMap.keys();
task.addFile(postFilesMap.get(keys[0]), {
key: keys[0]
});
if (postParams) {
var paramsKeys = postParams.keys();
if (paramsKeys.length > 1) {
paramsKeys.forEach(function (key) {
task.addData(key, postParams.get(key));
})
} else {
task.addData(paramsKeys[0], postParams.get(paramsKeys[0]));
}
}
task.start();
}
收起阅读 »

总结下dcloud中遇到的问题
1、微信分享返回 code:-100,innerCode:-6解决方案
解决办法: 确认申请时填写的应用签名是否小写,是否去除冒号,签名正确,以上验证过了,确保打包验证,不要debug方式验证
2、code: -2,message: 用户取消,
解决办法:确认申请时填写的包名和打包时是否一致
1、微信分享返回 code:-100,innerCode:-6解决方案
解决办法: 确认申请时填写的应用签名是否小写,是否去除冒号,签名正确,以上验证过了,确保打包验证,不要debug方式验证
2、code: -2,message: 用户取消,
解决办法:确认申请时填写的包名和打包时是否一致

mui整合vue2.x的案例,没有添加业务逻辑,只是基本页面
说说自己的开源项目,用mui+vue2.x写的,参照官方的例子,适合vue组件化,也整理了一个mui常见问题文档,有需要的就看看吧
项目地址:https://gitee.com/wuhou123/mui-vue2
说说自己的开源项目,用mui+vue2.x写的,参照官方的例子,适合vue组件化,也整理了一个mui常见问题文档,有需要的就看看吧
项目地址:https://gitee.com/wuhou123/mui-vue2

【经验分享】用HBuilder开发的基于MUI和H5+的APP开发及上架经历
一、写在前面
2017年,个人最大的收获,是第一次完成了这一款APP的开发并顺利上架,同时获得了还算可观的收益。
这是我前公司的项目,公司的主营业务是旅游,并不是什么科技公司,我之前一直在公司任职技术中心负责人,负责开发企业相关的应用系统和网站开发。
二、拿到项目外包,签订协议
早在2016年底,老板就找我谈,准备搞个旅游类的APP,主打重庆地区的旅游周边服务,我当时与他讨论了很久,基本上定下来要做这个项目,但我并不会iOS和Android开发,我一直苦口婆心的劝他只搞微信版,但老板的意思还是要一个能占领用户桌面的APP图标,也就意味着非搞APP不可。如此一来,我肯定没这个能力搞APP,再加上老板并未完全作好准备,这个项目搁置了大半年,直到2017年6月,老板再次主动打电话找我谈,并且非常坚定的要我负责这个项目的开发,即便是我不直接参与开发,也得为他物色开发团队。
好吧,我思前想后,虽然我不会开发APP,但是后端工作还得我来搞,APP前端就找朋友来做,最起码我还是捞得到酬劳吧!
我找到了一个开发APP的朋友,对方给我报价iOS+Andriod一共是5万块,我觉得价格不算高。我又给老板预算了一下费用,所有工作算下来,我向老板报的是17万,老板大概自己也私下询价了一些科技公司,双平台的APP这个价格肯定是白菜价。在签合同之际,老板突然让我赠送一个微信版(这是生意人的套路),整个费用一共17万,称兄道弟的说就当让我帮个忙。
就这样,我拿到了预付款7万元,签了技术合作协议,约定周期是3个月完工,所有前期手续办妥。
三、不靠谱的技术伙伴
整个项目,除了我和老板清楚需求之外,没有人知道具体的需求,我既是产品经理,又是项目的技术负责人。
我一直从事WEB方面的工作,在整个WEB项目实施层面,我样样都会一些,当然UI设计我也会一点。我参照网上大神们的经验,结合APP的界面规范要求,花了10多天,用PS画好了40多页的APP界面图。
我把做好的APP界面和功能需求,连同我自己制定的用色、字体规范文档、图标字体库,发给了负责APP前端的技术伙伴儿。
这是我亲自画的简要流程图
这是我亲手画的UI界面图
这是我制定的用色规范
这是我制定的字体规范
我这个伙伴儿,也是通过另外的朋友认识的,一枚92年的程序猿,会Android和iOS开发,他给我报的费用预算是5万,2个月完工(这是他的一口价,我并未讨价还价)。这费用在业内算超级白菜价,但这对于这样一个年轻小伙儿来讲,数目并不算太低,至少比他现在上班拿的工资高几倍。
提交给他相关开发材料后,我满以为这个伙伴儿会尽心尽力的按照我的要求按部就班的进行开发,可是,我错了。
我发送给他资料的时候,感觉他并不是特别留意我的材料,收到文件,也从不提问和询问细节,往往只回复一个“好的”、“行”、“OK”。我心里始终有点忐忑。但凭我跟他见面聊的,他又自称是比较有能力的程序员,总是信誓旦旦的表示技术方面没问题,我还是愿意相信他。
过了大概一个多星期,对方并没有主动找过我一次,我在想,难道是我准备的资料太过充分详尽,以至于对方都可以看懂和轻松实施?我天真的这么想。
又过了一个多星期,我开始催他给我看看做出来的样品,对方表示再等两天。再等了两天,对方发来了Android版的样品,我看了看,感觉非常失望,并不完全是按照我的设计图做的,界面很粗糙,字体也不对。我向他表示要很精细的成品,但对方表示,这些可以后期调整的,我还是同意了。
可是,时间过得真快,转眼一个月过去了,对方还没有给出比较像样的样板,我开始慌了,我了解后发现,他居然应聘了一份工作,开始上起了班,这样一来,难怪没时间搞我的项目。我整晚睡不着觉,我一个人单枪匹马接了17万的项目,如果进度上再不作一点改变,很可能要泡汤了。于是我思前想后,作出决定:我必须要放弃这个朋友的合作!
四、进度受阻,及时改变技术策略
我在合适的时候,找了个理由,向这个技术伙伴儿表明了意思,APP开发要中止了,否则再这样下去,我们都会违约,什么好处都捞不到不说,我还会产生违约赔偿。
对方一开始还是不太情愿,虽然5万块钱对于APP开发来讲,费用不算多,但对于他来讲,也算是一笔不小的收益。但最终我还是说服了他,之前付给他的1万定金,我只要求他退我7000,3000作为对他工作的补偿。
与技术伙伴儿的合作是中止了,但项目还得继续,该怎么办?如果我重新找人做,我觉得不太现实,也在网上找了一些人,都不是太理想,沟通成本也很高。于是我胆怯的决定了用HBuilder来搞混合APP开发,用MUI和H5+来做混合型APP!
但我之前一直没做过这样的工作,并且听说iOS上架审核非常严苛,被拒绝的案例大有人在,我还是很害怕出现这样的结果。我在MUI的QQ裙里也询问了一下群友,并未得到明确的上架方面的答案,也没吃到定心丸。我一直在想,这样开发出来的APP,上架应该很难。
但是我没有别的选择,我毅然决然选择用MUI和H5+来做,管他呢,听天由命,博一把吧!
五、潜心开发,往原生靠拢
我用HBuilder开始马不停蹄的开发APP,第一次开发APP,踩了不少坑,也从头了解MUI和H5+。我尽量把自己的项目往原生效果靠拢,这样我才有更多上架的胜算机会,我往往为了一个不起眼的效果而折腾几天。
改变技术策略之后,我深知我的任务量很重,我加了不少班,通宵达旦的写代码。刚好我的儿子在我最忙的时候出生了,我把家庭上所有的事情交给了媳妇儿打理,还把我母亲和丈母娘叫来帮我带儿子,我几乎是全身心投入到项目开发中,想到成功以后可以拿到不多不少的17万,想到辛苦这三个月就可以带着全家人出去度假,我一刻都没懈怠,并且治好了以前顽固的拖延症。
大概前后搞了3个多月,我用HBuilder开发的APP成品基本上出来了,我一个人完成了所有的工作(包含各种第三方平台支付API申请、地图API申请、打包证书创建)!整个项目APP前端一共有34个JS文件,14个CSS文件,24个HTML页面。后端则有几十个PHP类文件,10几个API,58个Mysql数据表。基本上APP能够跑起来了,开始进行测试。
六、内部测试,一波三折
APP能在真实手机环境下跑起来了,这是我比较欣慰的。安卓、苹果手机都进行了安装,看起来是个像模像样的APP了。于是我找到老板,进行了一次员工测试前的动员会议。
公司虽然有几十号人,一直以来,却没人知道我跟老板在搞什么名堂,只知道我们有一款APP在开发。当我们开会讲到这款APP的时候,我拿出了演示。各部门体验了一番,他们的负责人开始了他们的“演讲”,各种思路和脑洞,充斥着整个项目的讨论中,有些员工提出一些很不合实际的建议,但对于不懂技术和互联网的老板看来,这些建议好像也很有道理,于是很多言论左右着老板对这个APP成品的看法。
果然,老板要求按照他们的建议作一些改动和功能添加(尽管这并不是合同上载明的),都到这地步了,我只能默默的同意了,尽量争取到让大家满意,后面才好验收。
我按照大家的建议作了APP的修改,让各部门进行测试,又给了两周时间来测试,心想,测试完毕,就该让公司签验收报告了。
公司的员工并不积级,再三催促和老板施压下,大家勉强进行了一轮测试,主要集中在交易环节,我让大家从下单->付款->退款等环节进行了完整测试,保障所有资金走向正确无误,这样才能代表APP的交易功能形成闭环。
接下来我针对APP使用又组织了一次集中培训,大家听我讲了各种操作,我估计认真听的也没几个人,但我还是很认真的培训完了,也算对公司有个交待。
整个项目至此花了4个多月时间,按照约定我是已经超出了一个月,虽然老板催促过我,但我毕竟是公司的前技术员工,同时以前也是老员工,独立完成了公司的内部系统,老板比较信任我,公司并没有在合同方面上纲上线。
七、APP验收及尾款
到这个环节,应该是要进行验收了,于是我向公司请求进行验收,我内心其实一直认为验收环节会存在一定的问题,老板应该不会那么爽快,毕竟项目开发费用也不是一点小钱。另外,之前我的一位同事用他的经历告诉我,老板并不是那么爽快的。
但老板在资源方采购了很多资源,这些资源就等着APP上线,多等一天,老板就白花一天的钱。于是老板也敦促各部门快速完成验收,不得拖拖拉拉,只要符合要求,就立即签验收报告!这一点,我比较感恩老板,他比有的老板诚恳爽快多了。
验收方面,我不知道那几天到底发生了什么,最开始几个部门都不愿签验收报告,怕担责任,我感觉到非常难搞,但突然有一天,与我对接的小伙子打电话让我去拿验收报告,说所有部门都签字了!我感到一阵意外和欣喜。
既然验收报告签了,也就意味着项目成功交货了(此时APP还未上架,我心想交货了再慢慢搞上架的事儿,一直到上架成功为止),也就是说应该拿尾款了。我并未期望一次性能拿到尾款,10万块对于这样的公司也不算是太小的数目,不管从老板个人还是从公司财务而言,都不会轻易打给我吧。
可是,事情的顺利程度超出我的想象,当我拿到验收报告时,刚好碰见老板,他第一句话就是叫我去财务拿钱,说已经跟财务打好招呼了。
就在第二天,我成功收到财务打款的10万元,当看到短信提示收款100000元的时候,我感到之前所有的辛苦终于有了回报。
我当时开车收到了10万尾款到账短信
八、APP一次性顺利上架成功,一切变得很美好
收到公司的钱就完事儿了吗?还没有。钱是拿到手了,但APP还没上架呢!我最担心的事情就是iOS上架,不过,一切都要按照计划来。
我用HBuilder在线打好了原生包,然后向公司申请了一台Apple笔记本电脑,用于APP上架(公司在这方面还是很大方的,我说要什么,他们尽量满足)。
其实非要用苹果笔记本电脑,主要是为了使用Application Loader将ipa包上传到iTunes Connect,用于APP提交。其它的环节倒不一定需要苹果电脑(包括证书生成,都不一定要苹果电脑,Windows下也可以完成),XCode什么的更是没用到。
第一步:上传ipa到iTunes Connect,在这个软件的自动检测环节我遇到问题了,原因是APP的contact权限有问题,后来按网上的资料改好了,将APP安装包顺利上传到了iTunes Connect。这个环节成功了,我心里又踏实了一些。
第二步:准备提交,填写APP资料。按照页面上的提示和要求,乱七八糟的编好文案,上传几张APP预览图。
第三步:提交APP。我提交APP的时候刚好是圣诞节期间,苹果APP提交后台刚好把【提交审核】按钮置为灰色了,我以为是我的资料填写有问题导致按钮不可用,折腾了好久。过了圣诞节发现提交按钮可用了,原因是前两天苹果公司过年放假,不接受APP提交(这种可能性都能碰到,日了狗了),于是我点了提交审核,页面上呈现“正在等待审核”状态,我便开始了内心七上八下、茶不思饭不想的等待……只希望出奇迹,苹果审核人员让APP通过审核,我几乎是过几分钟就刷新一次页面,看看状态有没有改变。我一整晚睡不好觉,梦里全是APP上架审核相关的内容,有时候梦见我的APP被拒绝了。(我其实是有心理准备的,被拒绝几次应该是很正常,毕竟我的APP涉及到很多第三方API,包括支付功能,审核应该更加严格)
我是12月28日正式提交审核的,12月29日刷了一整天的上架后台,一直是“正在等待审核”,我明明知道没这么快,可还是忍不住不停的去刷新页面,期待审核状态的更新。
12月30日上午我一醒来就刷页面,但到9:40几的时候我刷上架后台,发现状态变成了“正在审核”,咦,苹果公司在审核我的APP了,这个时间点,应该是中国团队在审核!我的心都提到嗓子眼了,噗通跳个不停,很紧张很紧张,那感觉难以形容!
当天10:36,我万分惊喜的发现,我的APP审核通过了,状态变成了“等待开发人员发布”(这个状态是我因为在提交时选择了“手工发布”)!当时那种激动,可能超过了向心爱的女孩表白成功的感觉。
我把APP进行了发布,状态就变成了“可供销售”,也就是正式上架成功了,我第一时间在公司群里将这个好消息通知了所有人!
我完全没想到第一次开发的APP上架如此顺利,苹果公司审核如此之快!(整个上架审核过程仅花了不到3天时间)
这是第一次上架审核成功,直接用的1.0.2的版本号
九、好事多磨,第一个版本有BUG,更新后提交了一次加急审核
由于打包上架的仓促和紧张,我画蛇添足的在manifest配置中勾选了“根据重力自动横竖屏”的第二个选项“upside down”,导致APP安装后,画面是倒置的,不得不重新打包,更新一下版本。
苹果的更新审核可能并不那么快,我提交了两三天,一点动静没有,我便申请了一次加急审核,理由是APP有重大BUG。不得不说,加急真是非常高效,申请加急的时间当晚9点左右,通过审核是次日0:30左右,是由苹果美国公司审核的。
上图是1月3日晚9点提交的加急审核通道
十、十全十美,再接再励
这个APP的开发和上架过程,是我从业以来最为激动人心的一次经历,虽然很辛苦,但结果很美好。我顺利拿到了17万的酬劳,更重要的是收获了难能可贵的经历和成就感!
感谢DCloud让我在这段经历当中获得了宝贵的经验,HBuilder很棒!MUI很棒,H5+非常棒!也期望DCloud将免费进行到底。
写到这里,我也是想通过自己的经验告诉各位初次开发APP的朋友们,跨平台混合开发出来的APP是可以被认可和接受的,HTML5用好了也是可以做出来让人满意的APP的。不要惧怕前面的困难,也不要道听途说,导致你的畏首畏尾,请大胆去尝试吧,愿命运眷顾愿意辛勤和努力付出的我们!
一、写在前面
2017年,个人最大的收获,是第一次完成了这一款APP的开发并顺利上架,同时获得了还算可观的收益。
这是我前公司的项目,公司的主营业务是旅游,并不是什么科技公司,我之前一直在公司任职技术中心负责人,负责开发企业相关的应用系统和网站开发。
二、拿到项目外包,签订协议
早在2016年底,老板就找我谈,准备搞个旅游类的APP,主打重庆地区的旅游周边服务,我当时与他讨论了很久,基本上定下来要做这个项目,但我并不会iOS和Android开发,我一直苦口婆心的劝他只搞微信版,但老板的意思还是要一个能占领用户桌面的APP图标,也就意味着非搞APP不可。如此一来,我肯定没这个能力搞APP,再加上老板并未完全作好准备,这个项目搁置了大半年,直到2017年6月,老板再次主动打电话找我谈,并且非常坚定的要我负责这个项目的开发,即便是我不直接参与开发,也得为他物色开发团队。
好吧,我思前想后,虽然我不会开发APP,但是后端工作还得我来搞,APP前端就找朋友来做,最起码我还是捞得到酬劳吧!
我找到了一个开发APP的朋友,对方给我报价iOS+Andriod一共是5万块,我觉得价格不算高。我又给老板预算了一下费用,所有工作算下来,我向老板报的是17万,老板大概自己也私下询价了一些科技公司,双平台的APP这个价格肯定是白菜价。在签合同之际,老板突然让我赠送一个微信版(这是生意人的套路),整个费用一共17万,称兄道弟的说就当让我帮个忙。
就这样,我拿到了预付款7万元,签了技术合作协议,约定周期是3个月完工,所有前期手续办妥。
三、不靠谱的技术伙伴
整个项目,除了我和老板清楚需求之外,没有人知道具体的需求,我既是产品经理,又是项目的技术负责人。
我一直从事WEB方面的工作,在整个WEB项目实施层面,我样样都会一些,当然UI设计我也会一点。我参照网上大神们的经验,结合APP的界面规范要求,花了10多天,用PS画好了40多页的APP界面图。
我把做好的APP界面和功能需求,连同我自己制定的用色、字体规范文档、图标字体库,发给了负责APP前端的技术伙伴儿。
这是我亲自画的简要流程图
这是我亲手画的UI界面图
这是我制定的用色规范
这是我制定的字体规范
我这个伙伴儿,也是通过另外的朋友认识的,一枚92年的程序猿,会Android和iOS开发,他给我报的费用预算是5万,2个月完工(这是他的一口价,我并未讨价还价)。这费用在业内算超级白菜价,但这对于这样一个年轻小伙儿来讲,数目并不算太低,至少比他现在上班拿的工资高几倍。
提交给他相关开发材料后,我满以为这个伙伴儿会尽心尽力的按照我的要求按部就班的进行开发,可是,我错了。
我发送给他资料的时候,感觉他并不是特别留意我的材料,收到文件,也从不提问和询问细节,往往只回复一个“好的”、“行”、“OK”。我心里始终有点忐忑。但凭我跟他见面聊的,他又自称是比较有能力的程序员,总是信誓旦旦的表示技术方面没问题,我还是愿意相信他。
过了大概一个多星期,对方并没有主动找过我一次,我在想,难道是我准备的资料太过充分详尽,以至于对方都可以看懂和轻松实施?我天真的这么想。
又过了一个多星期,我开始催他给我看看做出来的样品,对方表示再等两天。再等了两天,对方发来了Android版的样品,我看了看,感觉非常失望,并不完全是按照我的设计图做的,界面很粗糙,字体也不对。我向他表示要很精细的成品,但对方表示,这些可以后期调整的,我还是同意了。
可是,时间过得真快,转眼一个月过去了,对方还没有给出比较像样的样板,我开始慌了,我了解后发现,他居然应聘了一份工作,开始上起了班,这样一来,难怪没时间搞我的项目。我整晚睡不着觉,我一个人单枪匹马接了17万的项目,如果进度上再不作一点改变,很可能要泡汤了。于是我思前想后,作出决定:我必须要放弃这个朋友的合作!
四、进度受阻,及时改变技术策略
我在合适的时候,找了个理由,向这个技术伙伴儿表明了意思,APP开发要中止了,否则再这样下去,我们都会违约,什么好处都捞不到不说,我还会产生违约赔偿。
对方一开始还是不太情愿,虽然5万块钱对于APP开发来讲,费用不算多,但对于他来讲,也算是一笔不小的收益。但最终我还是说服了他,之前付给他的1万定金,我只要求他退我7000,3000作为对他工作的补偿。
与技术伙伴儿的合作是中止了,但项目还得继续,该怎么办?如果我重新找人做,我觉得不太现实,也在网上找了一些人,都不是太理想,沟通成本也很高。于是我胆怯的决定了用HBuilder来搞混合APP开发,用MUI和H5+来做混合型APP!
但我之前一直没做过这样的工作,并且听说iOS上架审核非常严苛,被拒绝的案例大有人在,我还是很害怕出现这样的结果。我在MUI的QQ裙里也询问了一下群友,并未得到明确的上架方面的答案,也没吃到定心丸。我一直在想,这样开发出来的APP,上架应该很难。
但是我没有别的选择,我毅然决然选择用MUI和H5+来做,管他呢,听天由命,博一把吧!
五、潜心开发,往原生靠拢
我用HBuilder开始马不停蹄的开发APP,第一次开发APP,踩了不少坑,也从头了解MUI和H5+。我尽量把自己的项目往原生效果靠拢,这样我才有更多上架的胜算机会,我往往为了一个不起眼的效果而折腾几天。
改变技术策略之后,我深知我的任务量很重,我加了不少班,通宵达旦的写代码。刚好我的儿子在我最忙的时候出生了,我把家庭上所有的事情交给了媳妇儿打理,还把我母亲和丈母娘叫来帮我带儿子,我几乎是全身心投入到项目开发中,想到成功以后可以拿到不多不少的17万,想到辛苦这三个月就可以带着全家人出去度假,我一刻都没懈怠,并且治好了以前顽固的拖延症。
大概前后搞了3个多月,我用HBuilder开发的APP成品基本上出来了,我一个人完成了所有的工作(包含各种第三方平台支付API申请、地图API申请、打包证书创建)!整个项目APP前端一共有34个JS文件,14个CSS文件,24个HTML页面。后端则有几十个PHP类文件,10几个API,58个Mysql数据表。基本上APP能够跑起来了,开始进行测试。
六、内部测试,一波三折
APP能在真实手机环境下跑起来了,这是我比较欣慰的。安卓、苹果手机都进行了安装,看起来是个像模像样的APP了。于是我找到老板,进行了一次员工测试前的动员会议。
公司虽然有几十号人,一直以来,却没人知道我跟老板在搞什么名堂,只知道我们有一款APP在开发。当我们开会讲到这款APP的时候,我拿出了演示。各部门体验了一番,他们的负责人开始了他们的“演讲”,各种思路和脑洞,充斥着整个项目的讨论中,有些员工提出一些很不合实际的建议,但对于不懂技术和互联网的老板看来,这些建议好像也很有道理,于是很多言论左右着老板对这个APP成品的看法。
果然,老板要求按照他们的建议作一些改动和功能添加(尽管这并不是合同上载明的),都到这地步了,我只能默默的同意了,尽量争取到让大家满意,后面才好验收。
我按照大家的建议作了APP的修改,让各部门进行测试,又给了两周时间来测试,心想,测试完毕,就该让公司签验收报告了。
公司的员工并不积级,再三催促和老板施压下,大家勉强进行了一轮测试,主要集中在交易环节,我让大家从下单->付款->退款等环节进行了完整测试,保障所有资金走向正确无误,这样才能代表APP的交易功能形成闭环。
接下来我针对APP使用又组织了一次集中培训,大家听我讲了各种操作,我估计认真听的也没几个人,但我还是很认真的培训完了,也算对公司有个交待。
整个项目至此花了4个多月时间,按照约定我是已经超出了一个月,虽然老板催促过我,但我毕竟是公司的前技术员工,同时以前也是老员工,独立完成了公司的内部系统,老板比较信任我,公司并没有在合同方面上纲上线。
七、APP验收及尾款
到这个环节,应该是要进行验收了,于是我向公司请求进行验收,我内心其实一直认为验收环节会存在一定的问题,老板应该不会那么爽快,毕竟项目开发费用也不是一点小钱。另外,之前我的一位同事用他的经历告诉我,老板并不是那么爽快的。
但老板在资源方采购了很多资源,这些资源就等着APP上线,多等一天,老板就白花一天的钱。于是老板也敦促各部门快速完成验收,不得拖拖拉拉,只要符合要求,就立即签验收报告!这一点,我比较感恩老板,他比有的老板诚恳爽快多了。
验收方面,我不知道那几天到底发生了什么,最开始几个部门都不愿签验收报告,怕担责任,我感觉到非常难搞,但突然有一天,与我对接的小伙子打电话让我去拿验收报告,说所有部门都签字了!我感到一阵意外和欣喜。
既然验收报告签了,也就意味着项目成功交货了(此时APP还未上架,我心想交货了再慢慢搞上架的事儿,一直到上架成功为止),也就是说应该拿尾款了。我并未期望一次性能拿到尾款,10万块对于这样的公司也不算是太小的数目,不管从老板个人还是从公司财务而言,都不会轻易打给我吧。
可是,事情的顺利程度超出我的想象,当我拿到验收报告时,刚好碰见老板,他第一句话就是叫我去财务拿钱,说已经跟财务打好招呼了。
就在第二天,我成功收到财务打款的10万元,当看到短信提示收款100000元的时候,我感到之前所有的辛苦终于有了回报。
我当时开车收到了10万尾款到账短信
八、APP一次性顺利上架成功,一切变得很美好
收到公司的钱就完事儿了吗?还没有。钱是拿到手了,但APP还没上架呢!我最担心的事情就是iOS上架,不过,一切都要按照计划来。
我用HBuilder在线打好了原生包,然后向公司申请了一台Apple笔记本电脑,用于APP上架(公司在这方面还是很大方的,我说要什么,他们尽量满足)。
其实非要用苹果笔记本电脑,主要是为了使用Application Loader将ipa包上传到iTunes Connect,用于APP提交。其它的环节倒不一定需要苹果电脑(包括证书生成,都不一定要苹果电脑,Windows下也可以完成),XCode什么的更是没用到。
第一步:上传ipa到iTunes Connect,在这个软件的自动检测环节我遇到问题了,原因是APP的contact权限有问题,后来按网上的资料改好了,将APP安装包顺利上传到了iTunes Connect。这个环节成功了,我心里又踏实了一些。
第二步:准备提交,填写APP资料。按照页面上的提示和要求,乱七八糟的编好文案,上传几张APP预览图。
第三步:提交APP。我提交APP的时候刚好是圣诞节期间,苹果APP提交后台刚好把【提交审核】按钮置为灰色了,我以为是我的资料填写有问题导致按钮不可用,折腾了好久。过了圣诞节发现提交按钮可用了,原因是前两天苹果公司过年放假,不接受APP提交(这种可能性都能碰到,日了狗了),于是我点了提交审核,页面上呈现“正在等待审核”状态,我便开始了内心七上八下、茶不思饭不想的等待……只希望出奇迹,苹果审核人员让APP通过审核,我几乎是过几分钟就刷新一次页面,看看状态有没有改变。我一整晚睡不好觉,梦里全是APP上架审核相关的内容,有时候梦见我的APP被拒绝了。(我其实是有心理准备的,被拒绝几次应该是很正常,毕竟我的APP涉及到很多第三方API,包括支付功能,审核应该更加严格)
我是12月28日正式提交审核的,12月29日刷了一整天的上架后台,一直是“正在等待审核”,我明明知道没这么快,可还是忍不住不停的去刷新页面,期待审核状态的更新。
12月30日上午我一醒来就刷页面,但到9:40几的时候我刷上架后台,发现状态变成了“正在审核”,咦,苹果公司在审核我的APP了,这个时间点,应该是中国团队在审核!我的心都提到嗓子眼了,噗通跳个不停,很紧张很紧张,那感觉难以形容!
当天10:36,我万分惊喜的发现,我的APP审核通过了,状态变成了“等待开发人员发布”(这个状态是我因为在提交时选择了“手工发布”)!当时那种激动,可能超过了向心爱的女孩表白成功的感觉。
我把APP进行了发布,状态就变成了“可供销售”,也就是正式上架成功了,我第一时间在公司群里将这个好消息通知了所有人!
我完全没想到第一次开发的APP上架如此顺利,苹果公司审核如此之快!(整个上架审核过程仅花了不到3天时间)
这是第一次上架审核成功,直接用的1.0.2的版本号
九、好事多磨,第一个版本有BUG,更新后提交了一次加急审核
由于打包上架的仓促和紧张,我画蛇添足的在manifest配置中勾选了“根据重力自动横竖屏”的第二个选项“upside down”,导致APP安装后,画面是倒置的,不得不重新打包,更新一下版本。
苹果的更新审核可能并不那么快,我提交了两三天,一点动静没有,我便申请了一次加急审核,理由是APP有重大BUG。不得不说,加急真是非常高效,申请加急的时间当晚9点左右,通过审核是次日0:30左右,是由苹果美国公司审核的。
上图是1月3日晚9点提交的加急审核通道
十、十全十美,再接再励
这个APP的开发和上架过程,是我从业以来最为激动人心的一次经历,虽然很辛苦,但结果很美好。我顺利拿到了17万的酬劳,更重要的是收获了难能可贵的经历和成就感!
感谢DCloud让我在这段经历当中获得了宝贵的经验,HBuilder很棒!MUI很棒,H5+非常棒!也期望DCloud将免费进行到底。
写到这里,我也是想通过自己的经验告诉各位初次开发APP的朋友们,跨平台混合开发出来的APP是可以被认可和接受的,HTML5用好了也是可以做出来让人满意的APP的。不要惧怕前面的困难,也不要道听途说,导致你的畏首畏尾,请大胆去尝试吧,愿命运眷顾愿意辛勤和努力付出的我们!