- 环境搭建
- 路由
- Controller间数据共享
- [ngIf / ngShow /ngHide区别](#ngIf / ngShow /ngHide区别)
- 定时器
- XHR与跨域访问
- 指令
- webpack
- 开发中的实际问题
- angular的学习文章
环境搭建
- 基于node环境,需要npm工具,以及基于node环境的其他工具,如bower,webpack,grunt,gulp等
- 使用angular1.x开发SPA单页面程序
- 使用webpack去管理依赖,打包资源;html代码、css代码、js代码、图片等其他资源统一使用webpack打包,压缩,混淆。
- 使用WebStorm IDE进行开发,webstorm是一款非常优秀的前端开发IDE,在快捷键和代码提示上给予很大的帮助,特别是支持angular语法,es6语法,JSX语法等。
路由
目前SDK2.0用的是ngRoute ,论坛系统用的是ui.router。ui.router提供了比ngRoute 更强大的功能,比如多视图,嵌套视图。ui.router 是基于 state (状态)的, ngRoute 是基于 url 的,ui.router模块具有更强大的功能,主要体现在视图的嵌套方面。
在sdk2.0中页面需要加载国际化资源,需要加载JSBridgeLIB资源,这个时候在ui.router中就可以方便的使用路由配置中的resolve属性,或者使用抽象状态去等待资源的加载。
在论坛系统中会有tab页,每个tab页对应着数据的详细信息。而tab页在页面分享的时候是需要正确的定位的,故需要将tab页放置在url中,这个场景下用ui.router的父子试图也是比较合适的。
Controller间数据共享
- 通过parent scope作用域去共享数据,如: $rootScope ,兄弟controller可以通过父controller共享数据
- 通过事件广播去共享数据,$emit、$on、$broadcast
- 通过service去共享数据,由于service是单例的,所以可以用service去创建单实例的model对象来共享数据
- 通过directive指定属性进行数据绑定
ngIf / ngShow /ngHide区别
第一点区别是,ng-if 在后面表达式为 true 的时候才创建这个 dom 节点,ng-show 是初始时就创建了,用 display:block 和display:none 来控制显示和不显示。
第二点区别是,ng-if 会(隐式地)产生新作用域,ng-switch 、 ng-include 等会动态创建一块界面的也是如此。
这样会导致,在 ng-if 中用基本变量绑定 ng-model,并在外层 div 中把此 model 绑定给另一个显示区域,内层改变时,外层不会同步改变,因为此时已经是两个变量了。
在论坛系统中,遇到这个问题比较多。主要是在多tab页时,刚开始是通过ngif控制tab页的显示问题,就导致了tab页中的指令都会重新渲染,之前的状态就全没了。
定时器
定时器的使用$timeout $interval:由于页面路由跳转的时候可能定时器没有执行完成,故如果不主动清除定时器会造成系统的bug,需要我们主动清除定时器。
在路由变化事件中处理
$rootScope.$on('$locationChangeStart', function (event, url) {
if (window.$timer) {
try {
$interval.cancel(window.$timer);
} catch (err) {
console.info(['路由变化时清除timer出错,请忽略', err]);
}
}
});
在controller的destory方法中处理
$scope.$on("$destroy", function() {
if (window.$timer) {
try {
$interval.cancel(window.$timer);
} catch (err) {
console.info(['路由变化时清除timer出错,请忽略', err]);
}
}
});
XHR与跨域访问
跨域解决方式:JSONP 或 CORS
在CORS中需要一些配置
/* 设置cors post不发送options请求 */
$httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
/* 设置cors时发送浏览器cookie到服务器 需要服务器返回Header中加入Access-Control-Allow-Credentials: true */
$httpProvider.defaults.withCredentials = true;
在angular中可以通过拦截器去设置一些通用的东西,例
1.通过拦截器设置超时
/* 定义拦截器 */
.factory('timeoutHttpInterceptor', function () {
return {
request: function (config) {
config.timeout = 1000 * 30;
return config;
}
}
})
/* 设置超时 */
$httpProvider.interceptors.push('timeoutHttpInterceptor');
2.通过拦截器设置统一的异常处理
/* 定义拦截器 */
.factory('httpErrorInterceptor', ['$q', 'toast', function ($q, toast) {
return {
responseError: function (response) {
switch (response.status) {
case 200:
break;
case -1:
toast('网络异常');
break;
case 404:
toast('请求地址不存在(404)');
break;
case 500:
toast('服务器开小差了,请稍后再试(500)');
break;
case 502:
toast('Bad GateWay(502)');
break;
default:
toast('网络异常(' + response.status + ')')
}
return $q.reject(response);
}
}
}])
/* 设置网络异常 */
$httpProvider.interceptors.push('httpErrorInterceptor');
3.通过拦截器设置那些XHR需要添加遮罩层
/* 定义拦截器 */
.factory('ajaxLoadingInterceptor', ["$q", "$rootScope", 'loading', function ($q, $rootScope, loading) {
return {
request: function (config) {
config.mask === true && loading.show();
return config || $q.when(config)
},
response: function (response) {
response.config.mask === true && loading.hide();
return response || $q.when(response);
},
responseError: function (response) {
response.config.mask === true && loading.hide();
return $q.reject(response);
}
};
}])
/* 设置XHR遮罩 */
$httpProvider.interceptors.push('ajaxLoadingInterceptor');
4.通过拦截器设置服务器返回未登录错误码是弹出登录框
/* 定义拦截器 */
.factory('loginInterceptor', function ($q, session, loginDialog) {
return {
response: function (response) {
if (response.data && response.data.status == '1' && response.data.reset == 'b499') { //如果错误码是未登录
loginDialog.show(); //弹出登录框
session.removeAttribute('user');
return $q.reject(response);
} else {
return response;
}
}
}
})
/* 设置弹出登陆框 */
$httpProvider.interceptors.push('loginInterceptor');
指令
angular中的指令是个重要点,也是难点。指令绑定策略中的@(or @attr) =(or =attr) &(or &attr)的作用如下:
@(or @attr):使用@符号将本地作用域同DOM属性值进行绑定
=(or =attr):通过=可以将本地作用域同父级作用域上的属性进行双向数据绑定
&(or &attr):通过&符号可以对父级作用域进行绑定,以便在其中运行函数
下面以一个加载更多指令为例,展示@、=和&的用法
/* 声明指令 */
.directive('ourpalmLoadMore', function ($timeout, $rootScope) {
return {
restrict: 'E',
replace: true,
scope: {
text: '@', //绑定属性-单向绑定 绑定文字:点击加载更多 | 加载中... | 没有更多了
text2: '=', //绑定属性-双向绑定 绑定文字:点击加载更多 | 加载中... | 没有更多了
loadMore: '&' //绑定事件
},
template: '<div class="arrow_down_on" ng-disabled="isDisabled"><div ng-click="onClick();"><a><em></em>{{text || "点击加载更多"}}</a></div></div>',
link: function (scope, elem, attrs) {
var method = scope.loadMore; //获取绑定的事件
scope.isDisabled = false;
scope.onClick = function () {
if (scope.isDisabled) {
return;
}
scope.isDisabled = true;
scope.$eval(method); //调用绑定的事件
};
}
}
})
/* 使用指令 注意loadText的两种使用方式,对应上面的@和=号 */
<ourpalm-load-more load-more="loadMore();" text="{{loadText}}" text2="loadText"></ourpalm-load-more>
webpack
webpack的功能十分的强大,基本上可以替代grunt和gulp。这里不讲解webpack的用法,主要说下我们用到的webpack的功能。
1.打包js
2.打包css、svg、ttf、woff、eot
3.打包html
4.打包图片
5.处理css兼容浏览器前缀,autoprefixer
6.处理angular打包压缩后导致的依赖注入不可用问题 ng-annotate-webpack-plugin
7.转译es6语法为es5语法,babel
8.打包压缩混淆
9.其他可根据需求添加
开发中的实际问题
1.移动端键盘遮盖输入框的问题
这个问题的解决方式是弹出一个置顶的输入框,在置顶的输入框中输入,在focusout事件中将数据关联到实际的输入框中。
angular指令<ourpalm-focus></ourpalm-focus>的部分实现为:
directive('ourpalmFocusText', function ($timeout) {
return {
replace: true,
restrict: 'A',
require: '?ngModel', //通过ngModel引用目标输入框
link: function ($scope, $ele, $attrs, ngModel) {
$ele.attr('readonly', true);
$ele.on('click', function () {
$scope.$emit('ourpalm-focus:open', 'text', $ele, function (newVal) {
if (ngModel) {
$timeout(function () {
ngModel.$setViewValue(newVal); //在置顶输入框失去焦点时更新目标输入框
});
}
$($ele).val(newVal);
})
})
}
}
})
2.history.back()或者history.go(-1);在ios7设备webview中不可用的问题。
问题描述:angular单页面程序,为了控制用户流量,启用h5 appcache,页面使用hash路由,在appcache启用的情况下,ios7 webview在history.back()上有bug。
解决思路:a.可以通过特殊机型判断,ios7不使用appcache,其他机型使用appcache;b.既然是单页面程序,自己可以实现一个基于内存的history api,不用浏览器默认的history api。
目前sdk2.0使用的是自己实现的基于内存的history api,实现原理:监听window.onhashchange事件。
3.移动端横屏显示问题
通过一个动画层提示用户切换到竖屏查看,不让用户查看。
在实现细节上有一个要注意的是,本质上是一个比较简单的问题,css3媒体查询+css3动画就可以搞定了。但是css3媒体查询在android手机多数浏览器的支持上有bug,注意是bug,不是不支持css3媒体查询。表现是在键盘弹出的时候,android媒体查询会把竖屏当做横屏处理。
下面是通过js实现代替媒体查询实现:
.directive('ourpalmLandscape', function ($timeout) {
return {
restrict: 'AE',
replace: true,
template: `
<div class="ourpalm-landscape" ng-show="landscape">
<img src="" alt="">
<p>
为了更好的体验,请使用竖屏浏览
</p>
</div>
`,
link: function (scope, ele, attrs) {
refresh();
window.addEventListener('orientationchange', function (event) { //通过orientationchange事件代替css3的媒体查询
refresh();
});
function refresh() {
if (window.orientation == 180 || window.orientation == 0) {
$timeout(function () {
scope.landscape = false;
});
}
if (window.orientation == 90 || window.orientation == -90) {
$timeout(function () {
scope.landscape = true;
});
}
}
}
}
})
4.webpack --watch监听文件变化自动打包不好使,修改webstrom的配合 取消 "safe write"
5.angular页面列表页与详情页切换导致数据需要重新加载和滚动条无法回位问题
通过上面Controller间数据共享的介绍我们已经知道了怎么样去保存数据,这里数据的保存通过Service单例去保存,原因是论坛系统中这样的页面太多了,如果通过ui.router的嵌套模板去定义父子关系的话,论坛系统中就会有很多额外的父子关系,以及父子试图。所以这里采用单例的方式去保存数据来达到页面切换时数据不丢失的目的。
另外的一个问题就是除了保存数据在页面返回的时候滚动条需要滚到历史的位置,这里我们通过一个自定义的模块来实现,如下:
(function (angular) {
angular
.module('ourpalm-util-scrolltop', [])
.run(function ($rootScope, $state) {
/* 监听ui.router状态改变事件,记录滚动条的位置 */
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams, options) {
if (fromState.scrollTop === true && window.localStorage) {
var scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
window.localStorage.setItem(fromState.name, scrollTop);
}
});
/* 在页面加载完成的时候将滚动条滚动到上次的位置 */
$rootScope.$on('$viewContentLoaded', function (event) {
if ($state.current.scrollTop === true && window.localStorage) {
var scrollTop = window.localStorage.getItem($state.current.name);
if (scrollTop) {
window.setTimeout(function () {
window.scrollTo(0, scrollTop);
}, 10)
}
}
});
})
})(angular);
6.angular中定位到指定的tab页与数据保存
这个场景涉及到以下问题:
- 页面加载了很多页,返回时需要保存之前的数据
- 返回的时候需要定位到正确的tab页
- 返回的时候滚动条需要滚动到正确的位置
- 浏览器地址分享出去后,其他用户打开网址可以定位到正确的tab页
解决思路:
数据保存和滚动条回位的问题可以通过上面的方式解决。浏览器地址的问题,可以将tab页对应的id放到浏览器hash路由中,这里通过ui.router的父子模板来实现,定义如下:
.state('base.board', { //父state -- tab页 标题
url: '/board/:sectionid',
template: require('../ourpalm-view/board_main.html')
})
.state('base.board.tab', {//子state -- tab页 内容
url: '/:subSectionId',
template: require('../ourpalm-view/board_main_tab.html')
})
7.android关闭硬件加速会出现兼容性问题,如部分低端机造成css3属性不支持
android:layerType="software" 存在兼容性问题
android:layerType="hardware" 正常
8.android和ios的webview js bridge开源实现
android的:https://github.com/lzyzsd/JsBridge
ios的:https://github.com/marcuswestin/WebViewJavascriptBridge
9.手机端点击事件会有300ms的延迟问题
fastclick about the 300ms delay; 详见 https://docs.angularjs.org/api/ngTouch
10.fiddle使用
a.设置断点
在命令行中输入 bpafter sdk2.appcache 回车
b.清除断点
在命令行中输入 bpafter 回车
angular的学习文章
- https://toddmotto.com/directive-to-directive-communication-with-require/
- https://courses.toddmotto.com/?utm_source=toddmotto.com/one-way-data-binding-in-angular-1-5/
- https://thinkster.io/angularjs-es6-tutorial
- https://thinkster.io/topics/angular
- https://github.com/angular-ui/ui-router
- https://www.ng-book.com/media/ng2/ng-book-2-table-of-contents.pdf
- https://github.com/toddmotto/angular-styleguide#components
- https://docs.angularjs.org/guide/component
- https://ui-router.github.io/guide/ng1/route-to-component
- https://angular.cn/
- https://angular.cn/resources/
在angular中指令是一个非常强大和非常难的知识点,关于angular1.x和angular2.x中的指令和组件可以参考一个通用的ui组件库源码来学习。