xianfeschool / fe-problem-collection Goto Github PK
View Code? Open in Web Editor NEW前端问题收集和知识经验总结
Home Page: https://github.com/ShuyunXIANFESchool/FE-problem-collection/issues
前端问题收集和知识经验总结
Home Page: https://github.com/ShuyunXIANFESchool/FE-problem-collection/issues
还是自己遇到的一个坑的总结吧!与其说是坑不如说自己学艺不精,让我先哭一会!!
简单就是获取一个css的height
(好吧 就是一个这么简单的需求)
好吧 长时间的JQ 我已经对原生无能了 让我默哀3秒!!!
document.querySelector('.className').style.height;
这个果然不生效 好吧 看来我真的倒退不少!让我再哭一会!!(哭你妹 快去总结)
在学习中发现 其实js原生获取css的方法很多,上面写的就是其中一种,只不过情景不对啊!
简单来说 读出你八辈祖宗的一个方法。
看到伪元素、我就懵逼了 我只知道伪类啊 这有神马区别?
伪元素 其实就是 ::before :: after :: first-line ::first-letter ::content 等等
伪类 :hover :link :first-child :active 等等
var oImg = document.getElementById('photo');
window.getComputedStyle(oImg, null).height;
var oImg = document.getElementById('photo');
oImg.style.height; // 只能获取css 样式表的
<style>
属性等)与getComputedStyle
那个读你八辈祖宗的很像,但是不能获取伪元素。且他们获取的属性也有很大差异。var oImg = document.getElementById('photo');
oImg.currentStyle.height; // 只能获取css 样式表的
getPropertyValue("background-color")
getAttribute("backgroundColor")
var oImg = document.getElementById('photo');
var oStyle = oImg.currentStyle? oImg.currentStyle : window.getComputedStyle(oImg, null);
oStyle.getPropertyValue("background-color")
oStyle.getAttribute("backgroundColor")
如果我想获取某个属性值可以 比如高度 ?
(dom.currentStyle? dom.currentStyle : window.getComputedStyle(dom, null)).height;
如果是复合的某个属性获取?
(oImg.currentStyle? oImg.currentStyle : window.getComputedStyle(oImg, null)).getPropertyValue("background-color")
如果我想给这个属性重新设置这个高度?
可以先用上面方法取到,然后用
dom.style.height = XX + 'px';
如果是复合的属性值,请注意是驼峰的写法!
其实在看了这些以后,我用了上面的这个方法我依然没有获取到这个图片的高度?为什么啊 ?
因为图片存在一个加载的问题,你刚进来获取当然无法获取到? 其实可以用onload事件来解决,具体还有其他更多的方法。
最近在项目开发过程中,作为前端leader,发现前后端联调的时候,花费的时间特别多,按理说在接口定义清楚的前提下,联调是非常快速的。
然而事实上,当我仔细阅读接口文档后,发现很多东西,并未定义清楚,或者压根没有。
那么一个完善的接口文档应该是什么样的?
1.首先接口的设计,需要遵循REST原则,如果有同学不懂REST,可以看看这篇文章 #4
2.接口的名称及作用描述。
3.接口的URL和请求时使用的HTTP Method。
4.接口的请求格式及参数包括参数类型,参数默认值,参数描述。
5.接口可能返回的状态码,及每个状态码下的响应类型和格式。
以下是一个接口文档的示例:
ticket列表
用于获取ticket列表,带分页功能
接口:
GET /tickets
请求参数:
名称 类型 定义 必需 默认值(default) 说明 name string 查询name "" 作用于name pageNum number 页码 1 pageSize number 每页大小 10 status number ticket状态 -1 参考ticket状态枚举说明 orderBy string 排序字段名称 "id" 可以使用"id"或"name" order string 排序方式 "asc" 可以为"asc"或"desc" 其他说明
ticket状态枚举 {-1: '全部',0: '关闭',1: '开启'}响应:
成功:200
响应格式:JSON
{ "pageNum":{number} {default: 从请求获得},// 页码 "pageSize": {number} {default: 从请求获得},// 每页大小 "totalCount": {number} {default: 0}, // 总数 "results": [ // {number} 结果集 { "id": {number}, // 必填 "name": {string} {default: null}, // ticket name "status": {number} {default: null}, // ticket状态 参考ticket枚举说明 "createTime": {number} {default: null}, // 使用timestamp,方便前端转换 }, ... ] }登录超时 / 未登录: 403
参考标准403响应
服务器内部错误: 500
参考标准500响应
另外我们还需要提供一些标准响应格式约定:
{
"pageNum": {number} {default: 从请求获得}, // 页码, 不分页列表 为 null
"pageSize": {number} {default: 从请求获得}, // 每页大小,不分页列表 为 null
"totalCount": {number} {default: 0}, // 总数, 不分页列表 为 null
"results": [ // {number} 结果集
... ...
]
}
{
"id": {number},
"name": {string}
}
{
"message": {string}, // 友好的错误信息,可选,如不提供前端应当使用默认的提示信息
"errorCode": {number} // 可选择性提供 errorCode 便于后续问题排查,可选
}
后端可以针对不同的响应格式创建不同Java类,其他同学只需继承对应的class,而前端同学针对500和403可以使用AngularJS拦截器统一处理。
最后再强调一点,接口如果一旦定义清楚后,如果需要改动,一定要及时通知到相关同学,所以建议将修改接口权限控制在专门人手里。
关于接口的设计,可以参考
https://github.com/ecomfe/ub-ria/wiki
推荐一个接口定义工具 https://github.com/thx/RAP
npm install --save angular-ui-router
备注: 以下所有示例代码来源于个人所写的练习.
地址为:https://github.com/silence717/angular-webpack-demo
import uiRouter from 'angular-ui-router';
angular.module('app',[uiRouter]);
function runBlock($rootScope, $state, $stateParams) {
'ngInject';
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
}
export function routerConfig($urlRouterProvider) {
'ngInject';
// 默认路由设置
$urlRouterProvider.otherwise('/home');
// 无视浏览器中 url末尾的"/"
// 设置时 url, 末尾不要添加 "/"
$urlRouterProvider.rule(($injector, $location) => {
const path = $location.path();
const hashTrailingSlash = path[path.length -1] === '/';
if (hashTrailingSlash) {
return path.slice(0, path.length -1);
}
});
}
angular
.module('app', [uiRouter])
.config(routerConfig)
.run(runBlock);
export const HomeRouter = {
state: 'home',
config: {
url: '/home',
views: {
'@': {
template: homeTpl,
controller: 'HomeController',
controllerAs: 'vm'
}
},
title: '好玩的app'
}
};
import {HomeRouter} from './Routers';
function config($stateProvider) {
'ngInject';
$stateProvider.state(HomeRouter.state, HomeRouter.config);
}
export default angular
.module('app.home', [])
.config(config)
.name;
<ul class="nav navbar-nav">
<li ui-sref-active="active"><a ui-sref="home">首页</a></li>
<li ui-sref-active="active"><a ui-sref="album">相册</a></li>
<li ui-sref-active="active"><a ui-sref="user.baseInfo">个人中心</a></li>
</ul>
<div ui-view=""></div>
ui-view使用有三种,分别为:
<ui-view></ui-view>
<ANY ui-view ></ANY>
<ANY class="ui-view"></ANY>
具体里面的参数不做介绍,自己查阅官方文档
上面的html代码会被compile为:
<ul class="nav navbar-nav">
<li ui-sref-active="active" ui-sref-active-eq="" class="active"><a ui-sref="home" href="#/home">首页</a></li>
<li ui-sref-active="active" class=""><a ui-sref="album" href="#/album">相册</a></li>
<li ui-sref-active="active" class=""><a ui-sref="user.baseInfo" href="#/user/baseInfo">个人中心</a></li>
</ul>
<a href="javascript:;" ng-click="vm.getConfig();">获取state配置</a>
let config = this.state.get('user');
console.log(config);
// => Object {url: "/user", views: Object, title: "个人中心", name: "user"}...
<a href="javascript:;" ng-click="vm.goUserCenter();">这是一个跳转链接...</a>
// 不带参数跳转
this.state.go('user.baseInfo');
// 带参数跳转
this.state.go('album.detail', {id: 1});
console.log(this.state.href('album.detail', {id: 0}));
// => #/album/0
console.log(this.state.includes('album'));
// => false
console.log(this.state.is('home')); //=> true
function stateChangeStart($rootScope) {
$rootScope.$on('$stateChangeStart',
(event, toState, toParams, fromState, fromParams) => {
// event.preventDefault();
console.log('开始改变=====');
console.log(toState);
console.log(toParams);
console.log(fromState);
console.log(fromParams);
// 开始改变=====
// app.js:48Object {url: "/home", views: Object, title: "好玩的app", name: "home"}
// app.js:49Object {}
// app.js:50Object {name: "", url: "^", views: null, abstract: true}
// app.js:51Object {}
// client.js:55 [HMR] connected
});
}
// somewhere, assume lazy.state has not been defined
$state.go("lazy.state", {a:1, b:2}, {inherit:false});
// somewhere else
$scope.$on('$stateNotFound',
function(event, unfoundState, fromState, fromParams) {
console.log(unfoundState.to); // "lazy.state"
console.log(unfoundState.toParams); // {a:1, b:2}
console.log(unfoundState.options); // {inherit:false} + default options
})
onEnter: function() {
console.log('enter user.footprint state');
}
onExit: function() {
// 用于初始化一些数据什么的,清空表单...
console.log('exit user.footprint state');
}
<div ui-view="content"></div>
<div ng-show="vm.isShowThumb" class="module-content" ui-view="thumbList"></div>
<div ng-show="vm.isShowDetail" ui-view="detailList"></div>
export const UserFootprintRouter = {
state: 'user.footprint',
config: {
url: '/footprint',
views: {
'content@user': {
template: footPrintTpl,
controller: 'FootprintController',
controllerAs: 'vm'
},
'[email protected]': {
template: thumbListTpl
},
'[email protected]': {
template: detailListTpl
}
},
title: '我的足迹'
}
};
个人理解就是: viewname@statename去设置template
先看代码:
export const AlbumDetailRouter = {
state: 'album.detail',
config: {
url: '/:id',
views: {
'@': {
template: albumDetailTpl,
controller: 'PhotoController',
controllerAs: 'vm'
}
},
title: '单张照片show'
}
};
问: 我们经常会需要用到路由去传参,例如编辑一条信息,获取单个信息,应该如何去做呢?
答: angular-ui-router提供了一个$stateParams的service,可直接获取.在controller中的使用示例
export default class PhotoController {
constructor(photoResource, $stateParams) {
let vm = this;
photoResource.success(data => {
vm.detail = data[$stateParams.id];
})
}
}
有人肯定会疑问,$stateParams从何而来,在上面我们给angular.module中已经将其初始化,挂在到$rootScope.
app.config(function($urlRouterProvider){
$urlRouterProvider.when('', '/index');
});
app.config(function($urlRouterProvider){
$urlRouterProvider.otherwise('/home');
});
关于 AngularJS 1.x 中的 scope, 官方 guide 已经讲的很清楚了,大家自己阅读就行。
官方 guide (中文): http://docs.ngnice.com/guide/scope
官方 guide (英文): https://docs.angularjs.org/guide/scope
我在这里只强调几个 scope 技巧:
document.querySelectorAll('.ng-scope').forEach(function(node) {
node.style.border = '1px solid red';
});
技巧1: 查看 dom 元素 上的 scope, 更多技巧,请参看 hjzheng/CUF_meeting_knowledge_share#40
angular.element($0).scope();
技巧2:慎用事件传播,先用其他方式,使用双向数据绑定或共享service等方法来代替。
$on 和 $broadcast 的源码解读
$on 将注册的函数放入 scope 对象的 $ $listeners 中。
$broadcast 对 scope 树进行的遍历(深度优先遍历),然后在 $ $listeners 上找到注册的方法后,执
行,效率不高
通过设置 scope 的值 ,false 使用父 scope, true 产生一个子 scope 继承自父scope, {} 隔离scope
上网查资料时发现有很多实现这个功能的例子,我尝试了其中最简单的一种方法,就是利用原生JS
中的document.execCommand('copy')方法实现: 详见参考链接
js部分
toClipboard(content) {
const copyElement = angular.element(`<span id="ngClipboardCopyId">${content}</span>`);
const body = injector.get('$document').find('body').eq(0);
body.append(injector.get('$compile')(copyElement)(injector.get('$rootScope')));
const ngClipboardElement = angular.element(document.getElementById('ngClipboardCopyId'));
const range = document.createRange();
range.selectNode(ngClipboardElement[0]);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand('copy');
window.getSelection().removeAllRanges();
copyElement.remove();
}
html部分
<input type="text" class="code-input" ng-model="vm.arrangeCode">
<button class="btn-function-normal" ng-click="vm.toClipboard(vm.arrangeCode)">复制</button>
用这种方法虽然实现了功能,但有太多的DOM操作,因此不是很理想的方案
中文官网的回答: Express 是一个高度包容、快速而极简的 Node.js Web 框架。
npm install express --save
var express = require('express');
var bodyParser = require('body-parser'); // 引入 body-parser 中间件, 记得 npm install
var router = express.Router();
var app = express();
var userRouter = require('./userRouter'); // 引入自己的路由
// 添加中间件
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: false
}));
app.use(express.static('public')); // 设置静态资源路径
app.use(userRouter); // 添加自己的路由
// 默认 路由到目录下的index.html
app.get('/', function(req, res) {
res.sendFile(__dirname + '/index.html');
});
// 设置地址和端口
app.listen(3000, 'localhost', function(err) {
if (err) {
console.log(err);
return;
}
console.log('Listening at http://localhost:3000');
});
var users = [
{id: 1, name: 'hjzheng', grade: 88},
{id: 2, name: 'King', grade: 48},
{id: 3, name: 'Sandy', grade: 98},
{id: 4, name: 'Andy', grade: 78},
{id: 5, name: 'Yun', grade: 92}
];
module.exports = users;
注意几个req参数方法
req.params http://expressjs.com/zh-cn/4x/api.html#req.params
req.query http://expressjs.com/zh-cn/4x/api.html#req.query
req.body http://expressjs.com/zh-cn/4x/api.html#req.body
// 使用路由, 单独文件
var express = require('express');
var _ = require('lodash'); // 引入 lodash 处理集合, 记得 npm install
var router = express.Router();
var users = require('./users');
router
.get('/users/:id', function(req, res, next) {
if (req.params.id) {
res.json(_.find(users, { 'id': parseInt(req.params.id, 10)}));
}
})
.get('/users', function(req, res, next) {
res.json(users);
})
.put('/users/:id', function(req, res, next) {
var user = _.find(users, { 'id': parseInt(req.params.id, 10)});
_.merge(user, req.body);
res.json({success: true});
})
.post('/users', function(req, res, next) {
req.body.id = users[users.length - 1].id + 1;
users.push(req.body);
res.json({success: true});
})
.delete('/users/:id', function(req, res, next) {
_.remove(users, function(u) {
return u.id === parseInt(req.params.id, 10);
});
res.json({success: true});
});
module.exports = router;
res.status(403).end();
res.status(400).send('Bad Request');
res.status(404).sendFile('/absolute/path/to/404.png');
使用 Strong Loop 自动生成 RESTful API
除了Strong Loop外,我们还有另一大杀器, 就是 json-server,只需提供简单的json数据,就会生成标准 RESTful API,很方便,很实用。
更多内容,请参考 json-server 的 README
代码在 newcomer 中
你只需要在conf中写一个简单的配置文件即可, 例如
module.exports = function(configurations) {
// 传入一个对象
configurations.add({
request: {
method: 'GET',
urlPattern: '/update'
},
response: {
status: 200,
body: function(req) {
if (req.query.id) {
return {success: true};
} else {
return {success: false};
}
},
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=-1'
}
}
});
}
Scopes can propagate(传送) events in similar fashion(类似的方式) to DOM events. The event can be broadcasted(向下广播) to the scope children or emitted(向上发布) to scope parents.
ng中的作用域拥有发送事件,携带信息的能力,类似于DOM的事件类型,ng中的Scope拥有$emit向上发布到$rootScope,$broadcast向下广播到所有的子节点,当我们想在不同作用域之间做数据交互的时候,不免会考虑到使用事件机制携带数据,在指定的作用域上使用$on接受即可。
但我们在实际中使用事件机制来携带数据时,可能需要接受事件来获取数据的作用域只有一两处,而ng的事件模型却帮我们遍历了所有的子作用域或者父作用域,换句话说,我们使用了一种大范围的广播携带数据,却只在某一处有价值的使用了数据,这点,就造成了性能上的浪费。
参考《精通AngularJS》
如果不是全局的,异步的,关于状态变迁的通知,还是应该谨慎使用,通常可以依赖双向数据绑定,便捷的解决问题。
通过事件携带数据的目的
考虑到ng中服务全局可见,并且能够被依赖注入,所以,我们可以考虑利用服务来指定目标来传递数据,避免性能上的浪费。
服务传递数据很容易实现,但是如何给服务增加注册与触发的先后条件逻辑呢?我们先来看看事件的先后条件逻辑是如何形成的。
事件携带数据很形象的模拟了一组交互过程,通过事件的注册,事件的触发,来完成所需要的逻辑。基于此,我们可以在js中来模拟事件传播机制。
事件的过程主要分为三个步骤:
基于一个事件名,注册事件处理函数,在相应的事件发生时,该函数便会执行。
在需要触发事件的地方,调用该事件的所有已注册函数。
最重要的一点,我们如何能够将同一个事件的注册函数与事件发生对应起来,达到触发的效果呢。基于事件名与事件处理函数一对多的映射关系,我们使用js中的对象来储存这种映射关系,我们称之为事件队列。
综上,我们的事件对象应该至少拥有
// 事件对象的构造函数,每个实例拥有一个subscribers 来储存自己的事件队列。
var EventBus = function() {
this.subscribers = [];
};
// 原型中拥有事件的注册方法和触发方法,为了方便管理,我们扩展除了删除事件队列的方法
EventBus.prototype = {
constructor: EventBus,
// 注册方法,返回接收event标识符
sub: function(evt, fn) {
this.subscribers[evt] ? this.subscribers[evt].push(fn) : (this.subscribers[evt] = []) && this.subscribers[evt].push(fn);
return '{"evt":"' + evt + '","fn":"' + (this.subscribers[evt].length - 1) + '"}';
},
// 触发方法,成功后返回自身
pub: function() {
var evt = arguments[0];
var args = [].slice.call(arguments, 1);
if (this.subscribers[evt]) {
for (var i in this.subscribers[evt]) {
if (angular.isFunction(this.subscribers[evt][i])) {
this.subscribers[evt][i].apply(null, args);
}
};
return this;
}
return false;
},
// 解除注册,需传入接收event标识符
unsub: function(subId) {
try {
var id = angular.fromJson(subId);
this.subscribers[id.evt][id.fn] = null;
delete this.subscribers[id.evt][id.fn];
} catch (err) {
console.log(err);
}
}
};
通过以上逻辑,我们就可以实现自定义的事件机制。
那么,如何在不同作用域中使用我们的事件机制来携带数据呢,很简单,将EventBus 改写成可以被全局注入的服务即可。
巧妙的是,我们利用服务保存的不是需要传递的数据,而是事件队列,数据通过队列中方法的参数来传递。这样我们的事件机制与ng中的服务便完美的结合在一起了。
由于采用构造函数的写法,我们可以很方便的使用service方法来实现。
这样,我们自己的事件机制就可以使用了,由于我们的事件服务需要手动注入到需要使用的作用域内,不需要作用域的遍历,这样,相比使用scope的事件传播,具有简单明确的优点,精简了我们的逻辑。
随着互联网的发展,传统的文字已经不能满足人们生活交流的需要,有时一张图片,一个表情,更能传递想要表达的内容。(具体可参考微信斗图-_-! )如今比较有名的 emoji 已经可以被大部分设备所兼容,那么问题来了,如何在网页上显示一个emoji表情呢?
通常作为前端,我们接收到的表情不会是一张图片,而是类似 :smile : 、[微笑]、/微笑这样的字符串,那么如何将字符串转换为对应的表情就是问题的关键。毋庸置疑,通过正则表达式对相应的字符串作匹配替换,从而将对应的emoji表情显示出来是比较好的一个方法。
首先,本地要有一个emoji表情库,类似
或者一个雪碧图
本地表情文件的不同,对应的处理方法也会不一样。
有了这样的一个表情库文件,接下来就是对相应的表情字符串进行匹配。
其次:匹配,就免不了要用到正则表达式,针对不同的表情字符串,需要写不同的匹配规则。如需匹配[微笑]或者/微笑格式的表情,对应的正则如下
//对于元字符 如 [] {} + ? 等,需要转义
var reg = new RegExp("\\[" + emoji.name + "\\]|\\/" + emoji.name, "gm");
写好了匹配规则,接下来要对需要转换的字符串进行替换。
replace()很好的帮我们解决了这个问题。
var result = input.replace(reg, '<img class="emoji" src="' + emoji.src + '"/>');
那么问题来了,在这两行code中,用到了emoji.name,emoji.src,这个emoji对象到底是个什么样的东西。其实这个emoji对象相当于一个emoji配置,他的code长这个样子:
var emojis = [{
name: '微笑',
src: 'emoji/default/0.png'
}, {
name: '撇嘴',
src: 'emoji/default/1.png'
}, {
name: '发呆',
src: 'emoji/default/3.png'
}]
我们用到的emoji则是其中的一个个对象。最后,我们需要循环这个数组,将每个对应的字符串匹配成对应的表情。完整代码如下:
function filterEmoji(input, emoji) {
var regexp = new RegExp('\\[' + emoji.name + '\\]|\\/' + emoji.name, 'gm');
var result = input.replace(regexp, '<img class="emoji" src="' + emoji.src + '"/>');
return result;
}
function emoji($sce) {
return function(input) {
// 循环emojis配置表,匹配对应表情
emojis.forEach(function(emoji) {
input = filterEmoji(input, emoji);
});
//这里由于angular的安全限制,使用$sce将匹配到的表情转换为可识别的标签。
return $sce.trustAsHtml(input);
};
}
最后,在需要转换表情的地方,加上这个emoji过滤器就Ok了。
当然,这种方法有一个弊端就是需要加载很多的emoji表情图片,可以优化为用一张雪碧图去搞定。
在写法上也要有一些变化。首先肯定不能再通过标签去显示每一张表情图片,而是通过css添加background来实现。我们需要一个css的配置文件去匹配每个表情对应的雪碧图的位置。
.emoji {
display: inline-block;
background: url(emoji/default/emoji.png) no-repeat;
width: 21px;
height: 21px
}
.emoji.emoji_dai {
background-position: 0 0
}
.emoji.emoji_smile {
background-position: -21px 0
}
emojis也需要做一些修改
var emojis = [{
name: '微笑',
css: 'emoji_smile'
}, {
name: '发呆',
css: 'emoji_dai'
}]
匹配函数修改为:
function filterEmoji(input, emoji) {
var regexp = new RegExp('\\[' + emoji.name + '\\]|\\/' + emoji.name, 'gm');
var result = input.replace(regexp,"<i class='emoji" + b + "></i>");
}
最后,一个完整的emoji表情匹配到底结束。
最近,项目中很多地方涉及到了表格,但是发现很多同学并没有使用表格,或者部分使用表格去实现html页面,其实这样是不符合语义化的,仔细询问后,发现大家对 table 的特性还不是很了解,其实我自己也不了解,但是当我再翻看 taobao和京东的订单页面的时候,发现它们都使用的 table。
我们来看一个例子:
<table>
<thead>
<tr>
<th>Name</th>
<th>ID</th>
<th>Favorite Color</th>
</tr>
</thead
<tfoot>
<tr>
<th>Name</th>
<th>ID</th>
<th>Favorite Color</th>
</tr>
</tfoot>>
<tbody>
<tr>
<td>Jim</td>
<td>00001</td>
<td>Blue</td>
</tr>
<tr>
<td>Sue</td>
<td>00002</td>
<td>Red</td>
</tr>
<tr>
<td>Barb</td>
<td>00003</td>
<td>Green</td>
</tr>
</tbody>
</table>
我们有 table thead tr th tbody td 和 tfoot 注意 tfoot 和 thead元素
table {
display: table;
border-collapse: separate; /* 控制 cell 之间的是否有空隙 */
border-spacing: 2px; /* 控制 cell 之间的空隙 */
border-color: gray
}
thead {
display: table-header-group;
vertical-align: middle;
border-color: inherit
}
tbody {
display: table-row-group;
vertical-align: middle;
border-color: inherit
}
tfoot {
display: table-footer-group;
vertical-align: middle;
border-color: inherit
}
table > tr {
vertical-align: middle;
}
col {
display: table-column
}
colgroup {
display: table-column-group
}
tr {
display: table-row;
vertical-align: inherit;
border-color: inherit
}
td, th {
display: table-cell;
vertical-align: inherit
}
th {
font-weight: bold
}
caption {
display: table-caption;
text-align: -webkit-center
}
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
1.border-spacing 样式 控制 cell 之间的空隙大小
table {
border-spacing: 0.5rem;
}
td, th {
border: 1px solid #999;
padding: 0.5rem;
}
2.border-collapse 控制 cell 之间是否有空隙
table {
border-collapse: collapse;
}
td, th {
border: 1px solid #999;
padding: 0.5rem;
text-align: left;
}
3.colspan 和 rowspan 合并 cell (关于这一块,我记有同学问过我,怎样使用ng-repeat处理这种结构的表格,使用ng-repeat-start 和 ng-repeat-end)
<table>
<thead>
<tr>
<th colspan="2">Name</th>
<th>Team</th>
</tr>
</thead>
<tbody>
<tr>
<td>HaoJu</td>
<td>Zheng</td>
<td rowspan="2">Customer Service</td>
</tr>
<tr>
<td>Fang</td>
<td>Yang</td>
</tr>
<tr>
<td>Bao</td>
<td>Xiao</td>
<td>Other</td>
</tr>
</tbody>
</table>
4.table元素的特点与块级元素的特征类似,独占一行,但是它的宽度和高度取决于内容大小,如果它本身没有设置宽度,高度或者宽度,高度大于内容大小的话,也就是说它的最小宽度取决于内容宽度,最小高度取决于内容高度。
那么为什么要这样做的,那是因为 table-cell 有这样的特点,里面的元素可以同行等高显示,宽度自动调节的特点,可以通过设置 vertical-align 去控制同行元素的高度,也可以设置 padding 但是设置margin 没有任何效果。
<div class="table">
<header class="row">
<div class="td">Name</div>
<div class="td">Age</div>
<div class="td">Team</div>
</header>
<div class="row">
<div class="td">HaoJu Zheng</div>
<div class="td">29</div>
<div class="td">Customer Service</div>
</div>
</div>
.table {
display: table;
border-collapse: collapse;
}
.td {
border: 1px solid #999;
padding: 0.5rem;
display: table-cell;
}
.row {
display: table-row;
}
vertical-align 控制 cell 中的元素垂直对齐位置
white-space 控制 cell 中文本的显示方式
border-collapse 在table上设置
border-spacing 在table上设置
width 固定列的宽度,该列上设置宽度
border td或th 需要设置边框
参考资料:
https://css-tricks.com/complete-guide-table-element/#article-header-id-15
http://www.cnblogs.com/StormSpirit/archive/2012/10/24/2736453.html
https://www.sitepoint.com/solving-layout-problems-css-table-property/
本文中提及的都是一些简单易懂但平时容易忽略的JS属性,我已全部验证。现在整理出来供大家查阅,如有错误,欢迎指正。
width:
clientWidth:对象实际内容的宽度,不包含边线,最大不超过视口宽度
offsetWidth:对象实际宽度,包含边线,最大不超过视口宽度
scrollWidth:对象实际宽度,不包含边线,可超过视口宽度
height:(都可超过视口高度)
clientHeight:对象实际内容的高度,不包含边线
offsetHeight:对象实际高度,包含边线
scrollHeight:对象实际高度,不包含边线。如果对象为body,大于视口时为实际高度,小于视口时为视口高度
分辨率宽高:
window.screen.width:屏幕分辨率宽度
window.screen.height:屏幕分辨率高度
window.screen.availWidth:屏幕工作区宽度
window.screen.availHeight:屏幕工作区高度(不包含windows底边栏)
滚动距离:
scrollTop:对象被卷去的高度
scrollLeft:对象被卷去的宽度
偏移值:
event.clientX:相对文档的横向偏移值
event.clientY:相对文档的纵向偏移值
event.offsetX:相对当前对象的横向偏移值
event.offsetY:相对当前对象的纵向偏移值
offsetLeft:相对父元素的横向偏移值
offsetTop:相对父元素的纵向偏移值
window.screenTop:浏览器相对于屏幕左上角的横向偏移值
window.screenLeft:浏览器相对于屏幕左上角的纵向偏移值
边线: 滚动条和边框
Lodash 是目前最被依赖的 npm 包,但是如果你正在使用 ES6,实际上可以不需要它。围绕许多流行的使用场景,我们打算考虑使用可以帮助节约成本的原生集合方法与箭头函数以及其它新的 ES6 特性。
这些集合方法处理数据转换非常容易并且普遍都支持, 我们可以使用箭头函数配合它们,写出简洁的代码去代替 Lodash 提供的实现。
_.map([1, 2, 3], function(n) { return n * 3; });
// [3, 6, 9]
_.reduce([1, 2, 3], function(total, n) { return total + n; }, 0);
// 6
_.filter([1, 2, 3], function(n) { return n <= 2; });
// [1, 2]
// becomes
[1, 2, 3].map(n => n * 3);
[1, 2, 3].reduce((total, n) => total + n);
[1, 2, 3].filter(n => n <= 2);
不应该只有这些,如果使用 ES6 polyfill,我们也可以使用 find, some, every 和 reduceRight。
解构语法 (Destructuring syntax) 允许我们可以在不使用 utility 函数取得一个列表的 head 和 tail。
_.head([1, 2, 3]);
// 1
_.tail([1, 2, 3]);
// [2, 3]
// becomes
const [head, ...tail] = [1, 2, 3];
也可以使用相似的方式获取 initial 的元素和 last 元素。
_.initial([1, 2, 3]);
// -> [1, 2]
_.last([1, 2, 3]);
// 3
// becomes
const [last, ...initial] = [1, 2, 3].reverse();
如果你因为 reverse 方法改变数据结构而烦恼,可以在调用 reverse 之前使用展开操作克隆数组。
const xs = [1, 2, 3];
const [last, ...initial] = [...xs].reverse();
rest 和 spread 方法允许我们定义和调用接收可变参数的函数。ES6 为这些操作引入 dedicated syntaxes。
var say = _.rest(function(what, names) {
var last = _.last(names);
var initial = _.initial(names);
var finalSeparator = (_.size(names) > 1 ? ', & ' : '');
return what + ' ' + initial.join(', ') +
finalSeparator + _.last(names);
});
say('hello', 'fred', 'barney', 'pebbles');
// "hello fred, barney, & pebbles"
// becomes
const say = (what, ...names) => {
const [last, ...initial] = names.reverse();
const finalSeparator = (names.length > 1 ? ', &' : '');
return `${what} ${initial.join(', ')} ${finalSeparator} ${last}`;
};
say('hello', 'fred', 'barney', 'pebbles');
// "hello fred, barney, & pebbles"
不像一些高级别的语言例如 TypeScript 或 Flow,JS 无法提供 function 的类型信息,这使得柯里化相当困难。 当我们拿到一个柯里化的函数,很难知道有多少参数已经被提供和下次我们需要提供那些参数。使用箭头函数我们可以明确定义柯里化函数,使它们易于被其他程序员理解。
function add(a, b) {
return a + b;
}
var curriedAdd = _.curry(add);
var add2 = curriedAdd(2);
add2(1);
// 3
// becomes
const add = a => b => a + b;
const add2 = add(2); // 与上面比很明显 a 是 2
add2(1); //与上面比很明显 b 是 1
// 3
使用箭头函数定义的柯里化函数特别适合调试。
var lodashAdd = _.curry(function(a, b) {
return a + b;
});
var add3 = lodashAdd(3);
console.log(add3.length)
// 0
console.log(add3);
//function wrapper() {
// var length = arguments.length,
// args = Array(length),
// index = length;
//
// while (index--) {
// args[index] = arguments[index];
// }…
// becomes
const es6Add = a => b => a + b;
const add3 = es6Add(3);
console.log(add3.length);
// 1
console.log(add3);
// function b => a + b
如果我们正在使用函数式编程库,像 lodash/fp 或 ramda,我们也可以使用箭头函数去除必要的自动柯里化风格。
_.map(_.prop('name'))(people);
// becomes
people.map(person => person.name);
和柯里化一样,我们可以使用箭头函数使偏函数应用 (partial application) 变得容易和明确。
var greet = function(greeting, name) {
return greeting + ' ' + name;
};
var sayHelloTo = _.partial(greet, 'hello');
sayHelloTo('fred');
// "hello fred"
// becomes
const sayHelloTo = name => greet('hello', name);
sayHelloTo('fred');
// "hello fred"
它也可能通过展开操作将剩余参数用于可变参数函数。
const sayHelloTo = (name, ...args) => greet('hello', name, ...args);
sayHelloTo('fred', 1, 2, 3);
// "hello fred"
Lodash 配备了大量被重新实现的语法操作的函数,因此它们可以被传入到集合方法中。
在大部分情况下,箭头函数使它们精简到我们可以将它们定义成单行。
_.eq(3, 3);
// true
_.add(10, 1);
// 11
_.map([1, 2, 3], function(n) {
return _.multiply(n, 10);
});
// [10, 20, 30]
_.reduce([1, 2, 3], _.add);
// 6
// becomes
3 === 3
10 + 1
[1, 2, 3].map(n => n * 10);
[1, 2, 3].reduce((total, n) => total + n);
许多 lodash 的方法,将 path 看做字符串或者数组。我们可以使用箭头函数创建更可复用 path。
var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };
_.at(object, ['a[0].b.c', 'a[1]']);
// [3, 4]
_.at(['a', 'b', 'c'], 0, 2);
// ['a', 'c']
// becomes
[
obj => obj.a[0].b.c,
obj => obj.a[1]
].map(path => path(object));
[
arr => arr[0],
arr => arr[2]
].map(path => path(['a', 'b', 'c']));
由于这些 path “恰恰类似” 函数,我们也可以把它们组装成如下形式:
const getFirstPerson = people => people[0];
const getPostCode = person => person.address.postcode;
const getFirstPostCode = people => getPostCode(getFirstPerson(people));
甚至可以做出高优先级且带参数的 path。
const getFirstNPeople = n => people => people.slice(0, n);
const getFirst5People = getFirstNPeople(5);
const getFirst5PostCodes = people => getFirst5People(people).map(getPostCode);
pick 允许我们从一个目标对象中选择我们想要的属性。我们可以使用解构(destructuring)和简写对象字面量(shorthand object literals)达到相同的结果。
var object = { 'a': 1, 'b': '2', 'c': 3 };
return _.pick(object, ['a', 'c']);
// { a: 1, c: 3 }
// becomes
const { a, c } = { a: 1, b: 2, c: 3 };
return { a, c };
Lodash 提供一些用于创建特殊行为的简单函数的工具。
_.constant({ 'a': 1 })();
// { a: 1 }
_.identity({ user: 'fred' });
// { user: 'fred' }
_.noop();
// undefined
We can define all of these functions inline using arrows.
const constant = x => () => x;
const identity = x => x;
const noop = () => undefined;
Or we could rewrite the example above as:
(() => ({ a: 1 }))();
// { a: 1 }
(x => x)({ user: 'fred' });
// { user: 'fred' }
(() => undefined)();
// undefined
Lodash 提供一些函数帮助我们写出链式语句。在多数情况中,原生的集合方法返回一个可以直接被链式操作的数组实例,但是在一些情况中,方法改变了集合,链式操作就不太可能了。(but in some cases where the method mutates the collection, this isn’t possible. 这句不好翻译,求帮助?)
然而,我们可以用箭头函数数组定义相同的变换。
_([1, 2, 3])
.tap(function(array) {
// Mutate input array.
array.pop();
})
.reverse()
.value();
// [2, 1]
// becomes
const pipeline = [
array => { array.pop(); return array; },
array => array.reverse()
];
pipeline.reduce((xs, f) => f(xs), [1, 2, 3]);
使用这种方式,我们甚至都不需要考虑 tap 和 thru 的不同。在函数中封装 reduce 操作会是一个很棒的通用工具。
const pipe = functions => data => {
return functions.reduce(
(value, func) => func(value),
data
);
};
const pipeline = pipe([
x => x * 2,
x => x / 3,
x => x > 5,
b => !b
]);
pipeline(5);
// true
pipeline(20);
// false
Lodash 仍然是一个伟大的库,本文只是提供一个全新视角,新版本的 JavaScript 如何让我们解决一些在之前依赖 utility 模块解决的问题。
原文地址:
https://www.sitepoint.com/lodash-features-replace-es6/?utm_source=javascriptweekly&utm_medium=email
随着互联网的发展,前端页面变的越来越复杂,越来越多的事情,被拿到前端来处理。这就导致 HTML 页面会被加载大量的资源 包括 JS,CSS 以及图片等。
本文从 HTTP 缓存的角度,来谈谈如何优化页面资源加载性能。
在谈 HTTP 缓存之前,让我们先来聊聊 HTTP 协议,HTTP 作为一个非常重要的协议,它的发展却驻足不前,翻开的它的历史,它竟然只有三个版本:
然而 HTTP 协议只是 TCP/IP 协议族的一小部分,为了了解客户端和服务器是如何通信的,就必须了解 TCP/IP 协议族.
TCP/IP 协议族进行了分层,从上到下依次是,应用层,传输层,网络层,数据链路层。 HTTP 协议属于应用层,TCP 协议属于传输层。
HTTP 协议是建立在 TCP 协议基础上,让我们来简单描述一下,浏览器与服务器如何使用 HTTP 协议进行通信的:
浏览器和服务器通信发送的数据块是 HTTP 报文。HTTP 报文由起始行,首部和主体三部分组成。
HTTP 报文一般分为请求报文和响应报文。请求报文的起始行由方法,URI 和 HTTP 版本组成。响应报文的起始行由状态码,原因短语组成。它们的首部都有首部字段和值组成。主体是需要发送和返回的数据。首部和主体之间用 CRLF 空行分割。
如何利用好 HTTP 缓存的关键在于响应和请求的首部字段。
因为每个浏览器都实现了 HTTP 缓存,所以无需设置请求的首部字段,目前所要做的就是,确保每个服务器响应都提供正确的 HTTP 首部,以指导浏览器何时可以缓存资源以及可以缓存多久。
ETag 为我们提供一种验证资源是否被修改的方法,举例,浏览器首次请求资源,服务器返回一个带 ETag 首部的响应,大约过了一小时后,浏览器再次发起同样的请求,此时浏览器自动将 ETag 的值作为 If-None-Match 首部的值,发送给服务器,当服务器使用该值,检查响应的资源是否被修改,如果未修改,则返回 304 Not Modified 响应。注意这里我们并未下载资源,节约时间和宽带资源。
Cache-Control 可以设置的值有: no-cache 或 no-store, public 或 private, max-age。
no-cache 或 no-store
no-cache 表示必须先与服务器确认返回的资源是否被修改,然后才能确认是否使用缓存资源来满足后续相同的请求。因此,如果存在 ETag,no-cache 会发起往返通信来验证缓存的资源,如果资源未被更改,可以避免下载。
no-store 禁止浏览器和所有中继缓存(例如代理缓存服务器)存储返回的响应资源。
public 或 private
public 表示允许浏览器和任何中继缓存缓存响应资源
private 只允许浏览器缓存响应资源
max-age
max-age 指定从当前请求开始,允许获取的响应资源被重用的最长时间 例如:max-age=120 表示响应可以再缓存和重用 120 秒
我们可以根据自己实际的业务和不同的资源制定不同的缓存策略:
在定义缓存策略时, 有一些准则是可以参考的:
nginx 服务器中配置 ETag 和 Cache-Control
server {
listen 80;
server_name get-set.cn;
root /data/hjzheng/build/;
charset utf-8;
etag on; # 启用 ETag
try_files $uri =404;
location ~* (.+)\.html {
add_header Cache-Control no-cache;
}
location ~* (.+)\.js {
add_header Cache-Control private;
expires 1y; # 设置 max-age
}
location ~* (.+)\.css {
add_header Cache-Control public;
expires 1y;
}
location ~* (.+)\.(jpg|png|gif|eot|svg|ttf|woff) {
add_header Cache-Control public;
expires 31d;
}
}
注意, 本文主要讲述 Cache-Control 中值在 response 中设置的作用,在 Request 中含义略有不同:
document.execCommand('copy')这行命令足以满足你的需求(注:货号内‘copy’是参数)
猛戳此处查看例子
document.execCommand当然还有很多用法啦,这里简单做一下介绍,详细可参见链接https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand
cut参数,剪切当前选中内容于剪贴板
paste 参数,粘贴剪贴板内容于选中区域
delete参数,删除当前选中内容
在html页面,使用tree标签以及data和item-template属性。
<tree data="vm.tree" item-template="vm.itemTemplate"></tree>
其中,data是我们的tree所需要展示的数据,item-template是tree要显示的内容。(用户自定义)
data:
vm.tree = [{
name: 'PC游戏',
id: 1,
children: [
{
name: '角色扮演',
id: 2,
children: [
{
name: '月影传说',
id: 3
}
]
},
]
}];
item-template:用户可以在这里自定义所需要显示的内容,并且增加增删改查方法。
vm.itemTemplate = `<div ng-class="{'cur-select' : vm.id === item.id}" ng-click="vm.add(item)" ng-dblclick="vm.edit(item)" ng-show="!item.isEdit">{{item.name}}</div>
<input ng-dblclick="vm.edit(item)" type="text" ng-model="item.name" ng-show="item.isEdit">
<button ng-click="vm.del(item)" ng-show="vm.id === item.id">删除</button>`;
嗯,简单的用法就是这样,接下来说一下实现方法。
在做东西之前,首先要清楚自己需要的是什么,那么现在需要一个可以用户自定义内容与交互的tree组件,所以此angular-tree组件就应运而生了。
最开始的想法是,直接在使用tree指令的时候,在tree的标签里传入用户自定义的内容,通过transclude去实现,类似于下面这样
<tree data="vm.tree">
<div ng-transclude>{{item.name}}</div>
</tree>
然而事实上这样并不行,(当然不是在标签里直接写内容不行,而是通过transclude这种方式实现不可行),于是只能改用通过属性传值的形式,在js中写好我们需要的模板,传到指令中去,在做编译。
在我们的html结构中,通常一个tree的结构都是<ul>
和<li>
的互相嵌套去完成,类似这样:
<ul>
<li>
一级标题
<ul>
<li>二级标题</li>
</ul>
</li>
<li>
一级标题
<ul>
<li>二级标题</li>
</ul>
</li>
</ul>
所以我们需要两个html模板去配合我们做这样的处理。第一个模板负责我们一级标题的内容展示,然后通过repeat循环我们的第二个模板达到显示所有二级标题的效果。
template1
<tree-item ng-repeat="treeItem in $ctrl.data" item="treeItem" item-template="$ctrl.itemTemplate"></tree-item>
template2
<ul>
<li>
{{item.name}}
<i ng-click="$ctrl.showChild()" ng-if="!$ctrl.isLeaf(item)" class="fa" ng-class="{'fa-caret-right': !$ctrl.childShow, 'fa-caret-down': $ctrl.childShow}"></i>
<i ng-if="!$ctrl.isLeaf(item)" class="fa" ng-class="{'fa-folder': !$ctrl.childShow, 'fa-folder-open': $ctrl.childShow}"></i>
</li>
<li ng-show="$ctrl.childShow" ng-if="!$ctrl.isLeaf(item)">
<tree-item ng-repeat="treeItem in item.children" item="treeItem" item-template="itemTemplate" ></tree-item>
</li>
</ul>
通过这种模板套模板的方式,可以达到循环显示每一个级别的内容以及深层次的嵌套。
在template2中,我们需要做一些处理,就是增加展开收缩逻辑以及显示子集的逻辑。这两个功能是tree的基本功能,并不需要暴露给用户,所以我们在directive2的内部去实现即可。
我们给每个子节点增加了 childShow属性,并且通过父节点的箭头(showChild()
)点击来展示和收缩子节点的内容。至于判断是否是末尾节点,则更简单,只需要判断它的children属性是否为空或者不存在即可。
vm.showChild = function () {
vm.childShow = !vm.childShow;
};
vm.isLeaf = function(item) {
return !item.children || item.children.length === 0;
}
至此,一个具备展开收缩功能的tree,就基本完成了。但是用的时候就会发现,我们在item-template中定义的一些方法属性等,完全没有生效,这是为什么呢?
其实原因很简单,是在指令中使用了隔离作用域导致的,指令内部由于切断了与外部scope的联系,在指令外面定义的方法,指令内部并不认识。所以那些方法并没有生效。为了能让指令内部与指令外部建立联系,需要将外部的scope传到指令中,这样在指令外面定义的方法,指令里面也可以使用生效了。
首先在第一层指令的controller中,拿到当前scope的父级scope,也就是scope.$parent
。并将这个scope传到第二层指令当中,且挂载到第二层指令中scope的原型上,即可达到scope
的继承。Object.setPrototypeOf(scope, scope.baseScope);
最后,为了能让指令内部认识我们传进去的模板,需要在指令内部去重新编译。
// 在li中插入用户指令的模板
element.find('ul').find('li').append(scope.itemTemplate);
$compile(element.find('ul').find('li').contents())(scope);
第二行代码很重要,为什么不直接编译element.contents()呢?这样做其实是为了防止重复编译,导致我们在指令内部定义的方法会被执行两遍。
最后,再修改一下样式,一个简单的tree就完成了。(源码戳这里)用户可以在自己的controller中定义自己想要的东西、交互、效果等,而不需要交给指令内部去实现,比较灵活。当然,不爽的地方就是需要在js里写html的代码。那么有没有办法直接在html标签中去写我们需要的内容呢?当然,不过这种实现方法比较麻烦,在此不做赘述,有兴趣的话可以看这个github。
他的实现方式是通过js去生成dom元素,并且在每个item上scope.$new()
去创建scope并继承到用户的scope上。一个很6的实现方法。
在写tree的过程中,遇到了一些问题,最后也都得到了解决,并且在这个过程中,加深了对指令的理解。平时多写一些这样的小指令,对技能的掌握与巩固还是很有帮助的。最后,感谢@hjzheng ,@fnjoe在此期间提供的支持与帮助。
原文链接: https://www.sitepoint.com/understanding-module-exports-exports-node-js/
作为一个开发者,我们经常会遇到需要使用不熟悉的代码的情况。
在这个过程中遇到一个问题:我需要花费多少时间去理解这些代码,明白如何使用?
一个典型的回答就是:先让我可以开始coding,等到时间允许再去做深入研究。
接下来我们将对 module.exports 和 exports 在 node.js中的使用有一个更好地了解。
Note: 这篇文章包括了 node 中 module 的使用。如果你想了解浏览器内部 modules 的使用,可以参考这面这篇文章:
Understanding JavaScript Modules: Bundling & Transpiling
一个模块就是将文件中相关的代码封装为一个代码块。
创建一个module,可以理解为将所有相关的方法挪到一个文件中。
我们使用一个node.js的应用程序来说明一下这个观点。
创建一个名叫 greetings.js 的文件,其中包含下面两个方法:
// greetings.js
sayHelloInEnglish = function() {
return "Hello";
};
sayHelloInSpanish = function() {
return "Hola";
};
为了 greetings.js 公共逻辑增加的时候,其封装的代码可以在其他文件中使用。所以我们
重构一下 greetings.js 来达到这个目的。为了更好地理解这个过程,我们分为3步:
1) 想象一下有这么一行代码在 greetings.js 的第一行:
// greetings.js
var exports = module.exports = {};
2) 把greetings.js中的方法赋值给exports对象在其他文件中使用:
// greetings.js
// var exports = module.exports = {};
exports.sayHelloInEnglish = function() {
return "HELLO";
};
exports.sayHelloInSpanish = function() {
return "Hola";
};
在上面的代码中,我们可以使用 module.exports 替换 exports达到相同的结果。
这看起来似乎有些困惑,请记住:exports 和 module.exports引用的是同一对象。
3) 此时 module.exports 是这样的:
module.exports = {
sayHelloInEnglish: function() {
return "HELLO";
},
sayHelloInSpanish: function() {
return "Hola";
}
};
我们在 main.js 中 require greetings.js 的公开接口。这个过程有以下三个步骤:
1)关键词 require 在 node.js 中用于导入模块,即所获取模块的 exports 对象。
我们可以想到它是这么定义的:
var require = function(path) {
// ...
return module.exports;
};
// main.js
var greetings = require("./greetings.js");
上面的代码等同于:
// main.js
var greetings = {
sayHelloInEnglish: function() {
return "HELLO";
},
sayHelloInSpanish: function() {
return "Hola";
}
};
// main.js
var greetings = require("./greetings.js");
// "Hello"
greetings.sayHelloInEnglish();
// "Hola"
greetings.sayHelloInSpanish();
require 返回一个 object ,该对象引用了 module.exports 的值。
如果开发者无意或有意的将 module.exports 赋值给另外一个对象,
或者赋予不同的数据结构,这样会导致原来的 module.exports 对象
所包含的属性失效。
看一个复杂的示例去说明这个观点。
// greetings.js
// var exports = module.exports = {};
exports.sayHelloInEnglish = function() {
return "HELLO";
};
exports.sayHelloInSpanish = function() {
return "Hola";
};
/*
* this line of code re-assigns
* module.exports
*/
module.exports = "Bonjour";
在 main.js 中require greetings.js
// main.js
var greetings = require("./greetings.js");
此时,和之前并没有任何变化。我们将greetings.js中公开的方法
赋值给greetings变量。
当我们试图调用sayHelloInEnglish和sayHelloInSpanish结果显示为
module.exports 重新赋值给一个新的不同于默认值的数据格式。
// main.js
// var greetings = require("./greetings.js");
/*
* TypeError: object Bonjour has no
* method 'sayHelloInEnglish'
*/
greetings.sayHelloInEnglish();
/*
* TypeError: object Bonjour has no
* method 'sayHelloInSpanish'
*/
greetings.sayHelloInSpanish();
为了清楚地知道这个错误原因,我们将greetings的结果打印出来:
// "Bonjour"
console.log(greetings);
在这个点上,我们试着在 module.exports 抛出来的字符串"Bonjour" 去调用 sayHelloInEnglish 和 sayHelloInSpanish
方法,换句话说,我们永远也不会引用到 module.exports 默认输出object里面的方法。
importing 和 exporting 模块在 node.js 中是一个随处可见的任务。
我希望 exports 和 module.exports之间的差异更加清晰。
此外,如果将来你遇到调用公共方法错误的时候,我希望你可以对这些
错误的原因有一个更好地理解。
PS: 第一次尝试翻译文章,有的地方确实感觉很生硬,不合适的地方欢迎吐槽,继续改进!
css 写法
.truncate {
width: 250px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
当然 JS 也可以实现 (如果是 AngularJS的话 可以写一个 filter)
function shorten(text, maxLength) {
var ret = text;
if (ret.length > maxLength) {
ret = ret.substr(0,maxLength-3) + "...";
}
return ret;
}
参考地址:
https://css-tricks.com/snippets/css/truncate-string-with-ellipsis/
https://software.intel.com/en-us/html5/hub/blogs/ellipse-my-text/
今天 Fix bug 遇到一个典型的问题, 先简单描述一下该问题:
<div ng-click="vm.toggle()"></div>
<div ng-show="vm.show">这是测试内容</div>
<div id="test">这是测试内容</div>
vm.toggle = function() {
vm.show = !vm.show
console.log($document[0].getElementById('test').offsetTop);
};
参看上面的例子,其实我们是想得到中间 div 隐藏后,最后一个 div 的 offsetTop, 实际上得到的是中间 div 隐藏前的 offsetTop。
为什么呢,因为实际 toggle 中变量的变化,还没有引起 dom 变化,也就是脏检查并没有结束,Dom 也没有重新渲染。此时拿到的 offsetTop 肯定不是我们想要的。
那么怎么办呢,大家都知道 js 是单线程的,存在一个 event loop 来处理异步任务,这些异步任务存放在一个队列里, 所以我们只需要将获取 offsetTop 操作放入这个队列里就 OK。
当 ng-click 中的函数,AngularJS的脏检查等如上图所示 在队列中执行完毕后,就轮到我们的获取 offsetTop 的异步任务了。
很简单
vm.toggle = function() {
vm.show = !vm.show
$timeout(function() {
console.log($document[0].getElementById('test').offsetTop);
}, 0, false);
};
参考资料:
https://docs.angularjs.org/guide/scope
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
http://notes.iissnan.com/2014/waiting-for-dom-to-finish-rendering/
因为,未设置display为inline-block
stackoverflow上的回答
http://stackoverflow.com/questions/30812808/how-can-i-rotate-a-font-icon-45-degrees
在开发的过程中,遇到身份验证的问题,如果在不登录的情况下直接访问项目的地址,怎么对用户的身份进行验证?常见的处理是:如果没有读取到用户的登录信息,则页面自动转向登录页面,那么作为开发人员,怎么实现此需求?目前据我所知的解决方案有两种。第一种是用ui-router提供的$stateChangeStart对于发生路由转换时进行判断,第二种则是用angular自带的拦截器进行处理。
先说第一种:$stateChangeStart
由于ui-router关心的是状态,所以我们可以在每次模板引擎被解析前触发此方法
// 路由改变时判断是否登录,未登录则跳转至登录页面
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
// 这里用event.preventDefault()可以阻止模板解析
if (toState.name !== 'login' || fromState.name === 'login') {
// 这里做身份验证的判断
// 如判断cookie,session等。
// 若判断不通过,则跳转至login页面
}
});
其中toState和fromState参数是获取路由的前一个状态和下一个状态,当发生路由状态改变时,如果访问的不是登陆页面或者不是从登陆页面跳转时,执行身份判断。
但是这种方法是有缺陷的,1.用到了$rootScope. 2.基于ui-router.3.不稳定(项目中测试发现).由于问题的存在,所以可以采用第二种拦截器的方法解决这些问题。
拦截器 Angular Interceptor
在与后台交互的过程中,有时会希望俘获一些请求,在其发送到服务端之前进行操作,或者在服务器完成响应执行调用前处理,拦截器就是为此应运而生的一种方法。
$httpProvider 中有一个 interceptors 数组,而所谓拦截器只是一个简单的注册到了该数组中的常规服务工厂。拦截器提供了四个方法:request , requestError ,response 和responseError 来对请求和响应进行处理。具体介绍看这里:
要实现身份验证,可以在每次$http请求到后台之前进行验证
function requestInterceptor($cookies,) {
var requestConfig = {
request: function(config) {
// 这里做身份验证的判断
// 如判断cookie,session等。
// 若判断不通过,则跳转至login页面
return config;
}
};
return requestConfig;
}
该方法接收请求配置对象作为参数,然后必须返回配置对象或者 promise 。如果返回无效的配置对象或者 promise 则会被拒绝,导致 $http 调用失败。然后只需将创建的拦截器注册到$httpProvider的interceptors数组中即可。
module.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('myInterceptor');
}]);
在每次发送请求前,拦截器都会执行我们事先写好的判断代码,去做身份的验证。比stateChangeStart好的是,它在每次请求前都进行验证,而不只是对于路由切换时才验证。
拦截器可以做的事情还有很多,如请求恢复、loading等,搭配request , requestError ,response 和responseError实现即可,这里不做赘述。
团队内部成员 @baukh789 的一篇博文
@baukh789 是一位非常优秀且有着丰富经验的前端工程师,请大家关注他的 GridManager
博文地址: http://www.lovejavascript.com/#!zone/blog/content.html?id=24
在书写 JSX 时,经常会有一类需求,就是在特定条件下 render 一些组件。
例如:
{this.showMessage && <Message type={'warning'} />}
{this.loading ? <Loading /> : <Table />}
这里,我使用 JavaScript 表达式进行处理,这样做,从代码的可读性和可维护性上看起来都还不错。
继续增加复杂度
{ show && (isAdmin || hasPermission) && <Dialog /> }
继续增加复杂度
{ isShow && configDataIsDone && (isAdmin || hasPermission) && <Dialog /> }
显而易见,随着复杂度的增加,代码的可读性变的很差,可维护性也变的不好。
此时,最简单的处理方式,创建一个 helper 函数
canShowDialog () {
const { isShow, configDataIsDone, isAdmin, hasPermission } = this.props;
return isShow && configDataIsDone && (isAdmin || hasPermission);
}
... ...
{ this.canShowDialog() && <Dialog /> }
或者创建一个 getter
get canShowDialog () {
const { isShow, configDataIsDone, isAdmin, hasPermission } = this.props;
return isShow && configDataIsDone && (isAdmin || hasPermission);
}
... ...
{ this.canShowDialog && <Dialog /> }
当然,有些时候,也可以依赖一些第三方库
例如 render-if
const { isShow, configDataIsDone, isAdmin, hasPermission } = this.props;
const canShowDialog = renderIf(isShow && configDataIsDone && (isAdmin || hasPermission));
... ...
{ this.canShowDialog(<Dialog />) }
还可以使用高阶组件
react-only-if
const { isShow, configDataIsDone, isAdmin, hasPermission } = this.props;
const DialogOnlyIf = onlyIf(({isShow, configDataIsDone, isAdmin, hasPermission}) => {
return isShow && configDataIsDone && (isAdmin || hasPermission);
})(<Dialog />);
... ...
{ <DialogOnlyIf isShow={...} configDataIsDone={...} isAdmin={...} hasPermission={...} /> }
即使这样,可读性上还是稍差点,不可否认通用编程语言在功能上,远大于模板,毕竟模板只是 DSL,针对特定领域的语言,在某些点上的能力毕竟有限,因为它是针对特定领域的,所以在特定领域的可读性和易用上要好于通用编程语言。
想象一下, 我们可不可以增加一些条件组件,参考一下 AngularJS1.x:
例如:
<div ng-if=“true”>要显示内容</div>
将组件设计成这样:
<If condition={isShow && configDataIsDone && (isAdmin || hasPermission)}>
<Dialog />
</If>
其他组件 参考 ng-switch (else 和 else If 可以用 switch 代替)
<If condition={...}>
{'if语句'}
</If>
<Switch>
<When condition={...}>...</When>
<When condition={...}>...</When>
<Default>....</Default>
</Switch>
庆幸的是,有第三方的库可以进行支持,目前我所知道的有两个库:
react-if
jsx-control-statements
以 jsx-control-statements 为例:
{/* if语句 */}
<If condition={ test }>
<span>Truth</span>
</If>
{/* switch 语句 */}
<Choose>
<When condition={ test1 }>
<span>IfBlock</span>
</When>
<When condition={ test2 }>
<span>ElseIfBlock</span>
<span>Another ElseIfBlock</span>
<span>...</span>
</When>
<Otherwise>
<span>ElseBlock</span>
</Otherwise>
</Choose>
{/* for 循环 */}
<For each="item" index="idx" of={ [1,2,3] }>
<span key={ idx }>{ item }</span>
<span key={ idx + '_2' }>Static Text</span>
</For>
最近在项目的制作过程中遇到超出需要折行的问题
如果是一行超出
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
当然还需要设置宽度的
还可以参考 之前的解决方案 #13
如果是两行显示,第一行折行,第二行需要超出省略该如何实现呢?
或者第N行要省略该如何解决呢?
-webkit-line-clamp
是一个不规则属性,不出现在css 草案中。用来实现块级元素显示几行,它需要与其他元素组合使用
display:-webkit-box;
-webkit-box-orient: vertical; // 从上向下垂直排列子元素。设置或检索伸缩盒对象的子元素的排列方式 。
text-overflow: ellipsis; //文本溢出 用省略号显示
-webkit-line-clamp: 2;
overflow: hidden;
当然也需要设置高度来配合显示
对要显示的文字进行相对定位
对省略号进行绝对定位
p {
position: relative;
line-height: 1.4em;
height: 4.2em // 1.4 *3 显示三行的 所有高度是行高的三倍
overflow: hidden;
}
p::after {
content: '...',
position: absolute,
bottom: 0;
right: 0;
padding: 5px;
}
demo https://jsfiddle.net/mayufo/us8tcuyz/
Clamp.js 这个就不具体展开了
可以参考demo http://codepen.io/feiwen8772/pen/AckqK
$document 等于 angular.element(window.document) 实际上它是 jqLite 对 window.document的包装
$window.document 等于 window.document 它们都是 DOM element document.
$window.document === $document[0] //true
$window 其实是 AngularJS 将全局 window 对象包装成的服务
Angular's wrapper for window.setTimeout. The fn function is wrapped into a try/catch block and delegates any exceptions to $exceptionHandler service.
$timeout([fn], [delay], [invokeApply], [Pass]);
注意 $timeout 第三个参数,是否调用 apply 方法, 这是 AngularJS 性能的一个优化点。
区别同上, 可以参考官方API https://docs.angularjs.org/api/ng/service/$interval
7:3 error The "$watch" call should be assigned to a variable, in order to be destroyed during the $destroy event angular/on-watch
(function() {
angular.module('app').run(run);
run.$inject = ['$rootScope', '$location', '$http'];
function run($rootScope, $location, $http) {
var callback = $rootScope.$watch(function() {
return $location.search();
}, function(newValue) {
if (newValue.token) {
$http.defaults.headers.common.token = newValue.token;
}
});
$rootScope.$on('$destroy', callback);
}
})();
1 还有个总的 checkbox 负责全选和反选
2 当每一项开头都选中 checkbox 的时候,上面的全选自动选上
3 当全选后,取消其中一项的 checkbox ,全选取消
我表示刚开始我是不会的!!!
然后开始查资料
发现个不错的网上案例 like this > demo
感觉非常符合我的需求,但是看到 demo。
开始查了下 checkbox 在 AngularJS 里面的用法和 ng-checked ,发现如果用了 ng-checked 目测可以实现
html
<div ng-controller="myController">
<label for="flag">全选
<input id="flag" type="checkbox" ng-model="select_all" ng-click="selectAll()">
</label>
<ul>
<li ng-repeat="i in list">
<input type="checkbox" ng-model="m[i.id]" ng-checked = "select_one" ng-click="selectOne(m[i.id])">
<span>{{i.id}}</span>
</li>
</ul>
</div>
js
var app = angular.module('myApp',[]);
app.controller('myController', ['$scope', function ($scope) {
$scope.list = [
{'id': 101},
{'id': 102},
{'id': 103},
{'id': 104},
{'id': 105},
{'id': 106},
{'id': 107}
];
$scope.m = [];
$scope.checked = [];
$scope.selectAll = function () {
if($scope.select_all) {
$scope.select_one = true;
$scope.checked = [];
angular.forEach($scope.list, function (i, index) {
$scope.checked.push(i.id);
$scope.m[i.id] = true;
})
}else {
$scope.select_one = false;
$scope.checked = [];
$scope.m = [];
}
console.log($scope.checked);
};
$scope.selectOne = function (select) {
angular.forEach($scope.m , function (i, id) {
var index = $scope.checked.indexOf(id);
if(i && index === -1) {
$scope.checked.push(id);
} else if (!i && index !== -1){
$scope.checked.splice(index, 1);
};
});
if ($scope.list.length === $scope.checked.length) {
$scope.select_all = true;
} else {
$scope.select_all = false;
}
console.log($scope.checked);
}
}]);
大概意思说 如果你用了 ng-checked 就是默认它最初定义就是 true, 因此就没有必要使用 ng-model 了。简单说来就是 ng-model 和 ng-checked 不需要同时用。
看来是能用 ng-click 或者 ng-change 了
这个想法也是联想到公司之前的一个大牛的写的一个关于 checkbox 的指令。
html
<div ng-controller="myController">
<label for="flag">全选
<input id="flag" type="checkbox" ng-model="select_all" ng-change="selectAll()">
</label>
<ul>
<li ng-repeat="i in list">
<input type="checkbox" ng-model="i.checked" ng-change="selectOne()">
<span>{{id}}</span>
</li>
</ul>
</div>
js
var app = angular.module('myApp',[]);
app.controller('myController', ['$scope', function ($scope) {
$scope.list = [
{'id': 101},
{'id': 102},
{'id': 103},
{'id': 104},
{'id': 105},
{'id': 106},
{'id': 107}
];
$scope.m = [];
$scope.checked = [];
$scope.selectAll = function () {
if($scope.select_all) {
$scope.checked = [];
angular.forEach($scope.list, function (i) {
i.checked = true;
$scope.checked.push(i.id);
})
}else {
angular.forEach($scope.list, function (i) {
i.checked = false;
$scope.checked = [];
})
}
console.log($scope.checked);
};
$scope.selectOne = function () {
angular.forEach($scope.list , function (i) {
var index = $scope.checked.indexOf(i.id);
if(i.checked && index === -1) {
$scope.checked.push(i.id);
} else if (!i.checked && index !== -1){
$scope.checked.splice(index, 1);
};
})
if ($scope.list.length === $scope.checked.length) {
$scope.select_all = true;
} else {
$scope.select_all = false;
}
console.log($scope.checked);
}
}]);
推荐第三种方法!以上
参考:
module构成了我们的web应用。
从面向对象的角度来看,我们期望module拥有:
// 实现module类
function Module(name,requires) {
this.name = name;
this.requires = requires;
}
Module.prototype.constant = function (value) {
// 使该module拥有该value;
};
Module.prototype.service = function () {
// 使该module拥有该service
};
// 创建一个user module;
var userModule = new Module('user', null);
为了方便对模块的管理,我们还需要一个变量来存储模块,一个简便的方法来新建或获取模块如:
var myAngular = {};
var modules = {};
// 新建或者获取模块,取决于是否传入requires参数
myAngular.module = function (name, requires) {
if (requires) {
return modules[name] = new Module(name, requires);
} else {
return modules[name];
}
};
angular内部对模块机制的实现大体是这个思路,只不过没有采用构造函数与原型的写法来新建模块,直接使用了对象的字面量写法。
var moduleInstance = {
name: name,// 模块名称
requires: requires,//模块依赖
constant: invokeLater('constant', 'unshift'),
provider: invokeLater('provider'),
_invokeQueue: invokeQueue
};
modules[name] = moduleInstance;
当使用依赖注入机制时,我们的期望是:当我们在特定的上下文中,需要使用别的值或者对象,甚至是类时,我们只需给出它们名称,然后就可以在接下来的上下文中直接使用它们。
在angular中,我们很熟悉这样的代码:
function myController($scope, $window, myService) {
// 直接使用$scope,$window, myService。
}
我们希望的是直接在该函数内部使用$scope之类的对象。
在如何实现我们的期望之前,我们首先来看看我们最熟悉的函数 function。
在JavaScript中,function拥有作用域,从function的使用中我们就可以理解依赖注入的概念。
var a = 1;
var b = [1,2,3];
var c = {x: 1, y: 2};
function useThem(a, b, c) {
c.z = b.push(a);
}
useThem(a, b, c);
上面本身就是JavaScript中function的写法,使用参数可以直接从外部作用域去获取需要的值。
如我们之前提到的,函数的参数传递本身就符合我们的希望——使用名称,也可以说是标志来获取对应的值。
唯一不同的是,函数的参数代表什么是由函数调用时我们向它内部传递什么决定的,而我们在controller中的期望是当我声明好对应的参数时,这个参数所代表的值已经确定了,也就是函数声明时的参数决定了它的调用中传入什么参数。
要实现我们的期望,我们只需要增加一层逻辑,具体来说,也就是说我们需要统一我们函数的调用入口;
// 我们的函数
function myController($scope, $window, myService) {
// 直接使用$scope,$window, myService。
}
// 函数的统一调用入口
function invoke(fn, self) {
var argues = getArgs(fn);
fn.apply(self,argues);
}
// 获取函数依赖的方法。
// 1. 从哪里去获取依赖的标志, 从数组式的写法,还是$inject的写法,还是使用正则匹配来进行字符串匹配
// 2. 获取到依赖的标志后,按照该标志从哪里去取对应的值。
function getArgs(fn) {
// return what you want
}
也许,你已经想到需要一个cache变量来存储你所声明的可以被依赖注入的值了。
接下来,只要我们在调用我们的函数时使用统一的调用入口 invoke 方法时,便可以在函数内部自动根据参数的名称或者其他声明(数组式的依赖以及$inject式的声明依赖)来自动注入我们需要的值了。
由于我们在angular的框架中编码时,其实都是进行module内容的配置,真正帮我们去调用这些内容的是angular,它在内部实现了如上所述的invoke,以及getArgs逻辑。
当然,angular的依赖注入更为全面和复杂,不过,一切都是从这简单的逻辑开始的。
1.首先添加html代码
<input type="file" file-up-load="vm.fileUpLoad" />
2.接下来需要监听该控件的change事件,第一时间想到的肯定是
ng-change
事件,但是实验会发现当选择要上传的文件后,是不会触发
该事件的,所以需要换一种方式解决这个问题,这里选择用指令的方式监听
onchange事件,从而触发对应的方法。
angular
.module('fileUpLoadModule',[])
.directive('fileUpLoad', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var onChangeHandler = scope.$eval(attrs.fileUpLoad);
element.bind('change', onChangeHandler);
}
};
});
3.在controller里写响应的处理函数,处理文件的上传逻辑
vm.fileUpLoad = function(event) {
var file = event.target.files[0];
var formData = new FormData();
formData.append('fileParam', file);
};
首先通过event获取file对象,然后将file添加到FormData中,以便于向后台传送
4.通过$resource请求将文件信息发送给后台,但是需要注意的是,因为传输的是文件,而$resource的post请求默认添加application/json作为Content-Type,所以需要改变其Content-Type值,通过参考angular官方文档,修改方式如下:
$resource('http://example.com', null, {
upLoadFile: {
method: 'POST',
headers: {'Content-Type': undefined}
// $http默认会为post请求的Content-type设置为application/json,
// 可以通过将它设置成undefined去删除默认设置的Content-type
}
});
5.可以使用express mock上传请求,可以使用中间件connect-multiparty
参考文档:
接到一个类似于dropdown这样的需求,点击按钮下拉选择展示,而它的关闭有3中场景。
相信这样的使用场景一定不陌生,因为他经常出现。
思路解析:
外层设置一个状态值,通过添加ngClass控制下拉是否显示。设想都是美好的,也通过测试这样没有问题。
实现过程:
1.html书写
<div class="content" ng-class="{'open': vm.open}">
<span class="show" ng-click="vm.toggle()">cilic me!</span>
<div class="list">
<ul>
<li class="item" ng-repeat="item in list" ng-click="vm.itemClick();">{{item.title}}</li>
</ul>
</div>
</div>
2.css代码控制
.content .list{
display: block;
}
.content.open .list{
display: block;
}
3.mock静态数据
vm.list = [
{title: '下拉选项1'},
{title: '下拉选项2'},
{title: '下拉选项3'},
{title: '下拉选项4'},
{title: '下拉选项5'}
];
4.点击按钮控制显示隐藏,我只需要控制open状态为true或false即可。
// 设置初始状态为不显示
vm.open = false;
// 显示,关闭浮层
vm.toggle = function() {
vm.open = !vm.open;
};
5.点击任一下拉选择,隐藏。
vm.itemClick = function() {
vm.ticketOpen = false;
};
6.点击空白处,隐藏。
$document.off('click').on('click', function() {
vm.open = false;
});
看到这样的代码,你觉得有问题吗?反正我当时觉得自己一定是对的,但调试结果就是不生效,下拉怎么都不会隐藏。
通过断点调试,页面输出open的值,发现js中的open确实已经发生改变,但是页面的值确没有改变,然后联想到双向数据绑定失效。
谁决定什么事件进入angular context,而哪些又不进入呢?$apply!
这里声明一点ng-click不需要单独去做处理是因为angular已经做了,因此点击带有ng-click的元素时,事件就会被封装到一个$apply调用。
所以上面的问题也显而易见,是因为没有调用$apply,事件没有进入angular context, $digest循环永远没有执行。
so将code修为:
$document.off('click').on('click', function() {
vm.open = false;
$scope.$apply();
});
这样一测,立马有用了。
$apply是$scope的一个函数,调用它会强制一次$digest循环.
然后看到网上有人说有种更好用的办法,尝试了一下确实有效:
$document.off('click').on('click', function() {
$scope.$apply(function () {
vm.open = false;
});
});
解释为:
What’s the difference?
The difference is that in the first version, we are updating the values outside the angular context so if that throws an error, Angular will never know.
Obviously in this tiny toy example it won’t make much difference,
but imagine that we have an alert box to show errors to our users and we have a 3rd party library that does a network call and it fails.
If we don’t wrap it inside an $apply, Angular will never know about the failure and the alert box won’t be there.参考文章地址: http://angular-tips.com/blog/2013/08/watch-how-the-apply-runs-a-digest/
思考,能否在$injector的逻辑中,存在修改已注册依赖的入口,让我们针对具体对象能够配置对应的依赖,并且没有破坏原有依赖自身的实现逻辑?
首先来看ng官方的定义:
The $provide service has a number of methods for registering components with the $injector. Many of these functions are also exposed on angular.Module.
ng对decorator方法的划分也是在一个$provide的服务上,这样我们可以通过注入$provide来调用。下面来看具体的使用方式。
decorator(name, decorator);
Register(注册) a decorator function with the $injector. A decorator function intercepts(拦截) the creation of a service, allowing it to override(覆盖) or modify(修改) the behavior of the service. The return value of the decorator function may be the original service, or a new service that replaces (or wraps and delegates(代表) to the original service.
简单来说:
decorator的本质是在$injector上注册了一个方法。
这个方法可以拦截一个服务的创建,并且可以覆盖或修改这个服务的行为。
该方法应该有一个返回值,这个值可以是原来的服务(即你没有做任何修改),也可以是一个替换或者修改并代表了原服务的一个全新的服务。
很顺利,ng给我们提供了一个入口来切入服务创建的过程,并且能够写入自身的逻辑。
就是本文的主角:decorator 方法。
结合我们的问题与ng给出的定义,很清楚的可以得到实现过程,首先来看,decorator方法在代码中使用:
decorator(name, decorator);
This function will be invoked(运行) when the service needs to be provided(需要被提供) and should return the decorated service instance(修改过的服务实例). The function is called using the injector.invoke method and is therefore fully injectable(可注入的).
Local injection arguments:
$delegate - The original service instance, which can be replaced, monkey patched, configured, decorated or delegated to.
要拿到原有服务,我们需要为这个方法注入$delegate,同样的,我们可以注入更多的服务,来丰富自定义逻辑。
剩下的就很简单了,下面是一个简单的代码示例。
angular.module('app').config(config); // 在模块启动时更改原有服务逻辑
config.$inject = ['$provide']; // 注入$provide以便调用decorator方法
function config($provide) {
// 要修改的目标OriginService
$provide.decorator('OriginService', OriginServiceDecorator);
// 注入$delegate来获得原有的服务实例,注入其他服务来实现逻辑
OriginServiceDecorator.$inject = ['$delegate', 'other'];
function OriginServiceDecorator($delegate, other) {
var firstMethod= $delegate.firstMethod;
function newFirstMethod() {
// new service function
other.someFunc();
return firstMethod.apply($delegate, arguments);
}
$delegate.firstMethod= newFirstMethod;
var secondMethod = $delegate.secondMethod ;
function newSecondMethod () {
other.someFunc();
return secondMethod.apply($delegate, arguments);
}
$delegate.secondMethod = newSecondMethod ;
// 通过以上的修改,原有的OriginService已经被我们新增加了自己的逻辑,
// 并且没有改变原有OriginService实现逻辑。仅在app模块以及其依赖模块上有效。
return $delegate;
}
}
当我使用iframe将一个另一个独立开发的web网页嵌入到ngApp中时,ng向我抛出了一个错误,它认为使用iframe来引入一个陌生的url所指引的web网页是不安全的,但开发者判断这是个安全的来源。我们需要告知ng。精确来说,我们需要告知ng中负责安全限制的 $sce。
$sce is a service that provides Strict Contextual Escaping services to AngularJS.
Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to result in a value that is marked as safe to use for that context.
所以,ng设计了$sce来管理可能出现不安全来源数据的场景,上文中提到的iframe的url便是其中之一。
当我们能明确判断数据安全性的时候,我们不需要ng对上述的情况作出转译以及限制,于是我们需要明确告知ng,这时我们就可以使用$sce来设置,甚至,我们可以在全局中关闭$sce功能,不过,这个并不被提倡。
具体的$sce提供的安全上下文及设置方法请参考 $sce doc
最近,在项目中遇到需要在 config 阶段中注入一些service的情况,然而 factory,service 还有 value 是不能在 config 中注入的,先看一个清单:
服务/阶段 | provider | factory | service | value | constant |
---|---|---|---|---|---|
config阶段 | Yes | No | No | No | Yes |
run 阶段 | Yes | Yes | Yes | Yes | Yes |
注意,provider 在config阶段,注入的时候需要加上 provider 后缀,可以调用非 $get 返回的方法
在 run 阶段注入的时候,无需加 provider 后缀,只能调用 $get 返回的方法
关于服务之间的关系: 大家可以参考官方文档给出的说明
provider(name, provider) - registers a service provider with the $injector
constant(name, obj) - registers a value/object that can be accessed by providers and services.
value(name, obj) - registers a value/object that can only be accessed by services, not providers.
factory(name, fn) - registers a service factory function that will be wrapped in a service provider object, whose $get property will contain the given factory function.
service(name, Fn) - registers a constructor function that will be wrapped in a service provider object, whose $get property will instantiate a new object using the given constructor function.
decorator(name, decorFn) - registers a decorator function that will be able to modify or replace the implementation of another service.
我们来举例说明 provider 的强大:
(function() {
angular.module('app').provider('message', message);
function message() {
var level = 'log';
// 该方法可以在 config 阶段使用
this.setLevel = function(level) {
level = level;
};
// 该方法返回的对象方法可以在 run 阶段使用
this.$get = message;
function message() {
return {
log: function() {
level === 'log' ? console.log(arguments) : angular.noop() ;
},
error: function() {
level === 'error' ? console.error(arguments) : angular.noop() ;
},
warn: function() {
level === 'warn' ? console.warn(arguments) : angular.noop() ;
}
};
};
}
})();
// 注意注入的时候,加上Provider后缀
angular.module('app').config(function(messageProvider) {
messageProvider.setLevel('error');
});
//注入的时候,无需加后缀
angular.module('app').controller('MainCtrl', function(message) {
message.error('just a test');
});
参考资料: AngularJS Guide
在做项目的过程中,遇到一个业务需求,需要用到柯里化。
接着回过头看看什么是柯里化。抄一段百度百科的解释:
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
对于我们的关注点来说,重要的不是它是什么,而是它能为我们做什么。柯里化有三个常见的作用:1.参数复用,2.提前返回,3.延迟计算/运行。这里我们要用到它的第三个作用,延迟计算。
这里看一个例子:
//Curry
var curry = function(fn) {
var args = [].slice.call(arguments, 1); //等同于arguments.slice(1);
return function() {
// 将新参数和之前的参数连接起来作为curry函数中所传入函数的参数。
var newArgs = args.concat([].slice.call(arguments));
return fn.apply(null, newArgs);
};
}
var add = function(x,y) {
return x + y;
}
// 调用curry函数,传入一个add方法和一个参数5,此时这里的5等于上面的args。
var result = curry(add, 5)
// 当调用result函数并传入参数 10 时,
// 将10和之前的参数5结合起来作为之前curry函数中add方法的两个参数,
// 并运行执行,最终返回10。
console.log(result(10)); //15
在这里,当我们调用curry函数时,将5作为curry函数中add方法的第一个参数,并保存起来,(这里用到了闭包的知识,所以能保存,闭包以后再谈,因为现在不会-_-!)然后在下面调用result方法并传入参数10,此时参数 10 作为add方法的第二个参数传入,并执行。最终返回结果:15。
当我们调用result方法时,不需要再显示的传入第一个参数,只需要传入第二个参数即可完成方法调用。
接下来,我们人工debug一下这段代码。在curry函数中,至少需要接受一个参数 fn,然后进入 var args = [].slice.call(arguments, 1);
这里将在调用curry函数中,可能传入的后续参数保存为数组args。在curry函数return的function 中,这里的function就是result。在return的函数中,将新传入的参数与原来的args数组合并为新的参数数组newArgs,并最终传给fn,作为fn 的参数。此时就完成了参数的合并。
接着跑一下这段代码,var result = curry(add, 5);
这里传入的参数5,被放到了args里面保存起来等待与新的参数合并。 最后执行 result(10),10 作为curry函数中,renturn function
里面的新传入的参数,与之前的args里的5合并成为[5,10],然后fn.apply(null, newArgs)
,新参数传给fn,也就是这个add函数,调用add,最终返回结果 15.
写的有些乱,有看不懂的地方可以提出来再修改下。当然,curry还有另一种写法curryRight.
function curryRight(fn) {
var args = [].slice.call(arguments, arguments.length - 1);
return function() {
var newArgs = [].concat([].slice.call(arguments), args);
return fn.apply(null, newArgs);
};
}
大同小异,自己体会。
how Angular interacts with the browser's event loop?.
Integration with the browser event loop
The browser's event-loop waits for an event to arrive. An event is a user interaction, timer event, or network event (response from a server).
上图的左侧上部分,浏览器的事件机制等待事件的触发来响应操作。
这里的事件包含用户的交互操作(click,mousemove等)、js内部通过浏览器的定时操作(setInterval,setTimeout)以及与服务器的交互(ajax);
The event's callback gets executed. This enters the JavaScript context. The callback can modify the DOM structure.
此时浏览器的事件机制被触发,注册在对应事件上的listener被执行,此时进入js的上下文执行环境,执行我们预设的操作。在angular的项目中,会在此时通过调用$apply(fn)方法进入自身的数据检查模式$digest,就是图中的右侧部分。
这里的fn通常是一些会引起模型变化的动作,如:DOM事件,XHR响应回调,浏览器location变化,计时器的回调等,这也就是angular为何要与浏览器的event loop相互作用的原因。
$apply方法会先执行fn,然后进入$rootScope的脏检查来保证数据的一致性。
Only operations which are applied in the Angular execution context will benefit(受益) from Angular data-binding(数据绑定), exception handling, property watching, etc...
通常ng已经将上述浏览器的动作加入了$apply中来实现数据绑定,这就是我们并没有明确的在代码中调用$apply却实现了数据绑定的原因,当然,你也可以手动调用$apply来明确的引起一轮angular的数据检查。
现在我们进入了$digest loop,如上图所示,这里面又内涵了两个小的loop。
The $evalAsync queue is used to schedule work(安排任务) which needs to occur outside of current stack frame, but before the browser's view render. This is usually done with setTimeout(0), but the setTimeout(0) approach suffers from slowness and may cause view flickering since the browser renders the view after each event.
目前只理解到这个队列中的任务会在$digest期间确保执行,下文提到的$evalAsync方法正是这个队列的入口。
当我们进行了一些操作,并引起了数据的变化,需要借助$digest来同步数据,如果此时已经存在一个$digest,我们便可以使用$evalAsync方法来该操作添加到已经存在的循环中的evalAsyncQueue,达到同步数据的目的。
The $watch list is a set of expressions which may have changed since last iteration. If a change is detected then the $watch function is called which typically updates the DOM with the new value.
$watch list 是一系列表达式的集合,ng中将模型展示在view中的方式便是对模型的值添加一些watch,如ng-model、{{}}插值表达式,以及指令的赋值等,也包括在js中在scope上调用$watch方法来实现数据的监听,这些所有便构成了$watch list。
The $digest loop keeps iterating until the model stabilizes(模型稳定), which means that the $evalAsync queue is empty and the $watch list does not detect any changes.
$digest loop终止的条件便是$evalAsync queue为空和$watch不再变化,至此我们便可以重新渲染DOM。
至此,数据的改变便驱动了view层的变化。
表达式的解析与执行,函数的执行必须与作用域联系起来,并且该动作可能会引起数据的变化。
Executes the expression on the current scope and returns the result.
在当前作用域中执行表达式并返回结果。
考虑到执行的动作可能会改变数据,所以需要配合别的动作来进入digest循环,来确保数据的一致性。
Executes the expression on the current scope at a later point in time.
“稍后但很及时的”在当前作用域中执行表达式,ng在这个动作中整合了保证数据一致性的动作。
$rootScope.$digest()
来确保数据的一致性。$rootScope.$digest()
;$rootScope.$digest()
。$rootScope.$apply(flushApplyAsync)
,并给applyAsyncId赋值来注册$apply的状态。关于$applyAsync方法以及applyAsyncQueue队列的用意,目前没有很好的理解,$digest循环中也会进行applyAsyncId的判断以及applyAsyncQueue队列的flush(冲刷);
MVVM模式的核心在于数据驱动,所以,一切对应用状态的操作的出发点应该是操作数据,再通过框架对数据的处理,最后映射到应用的状态,就是页面中的DOM状态。而这个处理数据的核心操作便是$digest循环。
在项目中使用angular作为框架时,能整理出驱动整个应用状态流转的数据变化,那么,与angular的合作便很顺利了。
说到 AngularJS 的性能优化,一个很重要的点就是从 数据的脏检查 开始:
做一个试验,看看什么情况下,变量会被加入到 $$watchers 中
http://plnkr.co/edit/hDHmkdNjQQkdwscjouzE?p=preview
再看看什么情况会触发脏检查
$watch(watchExpression, listener, [objectEquality])
不要使用第三个参数参考资料:
https://docs.google.com/presentation/d/15XgHRI8Ng2MXKZqglzP3PugWFZmIDKOnlAXDGZW2Djg/edit#slide=id.g2a0ec7d53_00
https://www.ng-book.com/p/The-Digest-Loop-and-apply/
这是Todd大神写的一篇博客,关于AngularJS1.5 lifecycle hooks.
lifecycle hooks 最早是在 Angular2 alpha 时候引入的, 或多或少的受到 Custom Elements lifecycle callbacks 的启发, 很庆幸,AngularJS 1.5 引入了这个新特性,让大家在学习 Angular2 lifecycle hooks 之前尝尝鲜。
原文地址: https://toddmotto.com/angular-1-5-lifecycle-hooks
中文翻译: Jocs/jocs.github.io#3 (感谢公司罗冉同学)
function asyncGreet(name) {
// perform some asynchronous operation, resolve or reject the promise when appropriate.
return $q(function(resolve, reject) {
setTimeout(function() {
if (okToGreet(name)) {
resolve('Hello, ' + name + '!');
} else {
reject('Greeting ' + name + ' is not allowed.');
}
}, 1000);
});
}
var promise = asyncGreet('Robin Hood');
promise.then(function(greeting) {
alert('Success: ' + greeting);
}, function(reason) {
alert('Failed: ' + reason);
});
function asyncGreet(name) {
var deferred = $q.defer();
setTimeout(function() {
deferred.notify('About to greet ' + name + '.');
if (okToGreet(name)) {
deferred.resolve('Hello, ' + name + '!');
} else {
deferred.reject('Greeting ' + name + ' is not allowed.');
}
}, 1000);
return deferred.promise;
}
var promise = asyncGreet('Robin Hood');
promise.then(function(greeting) {
alert('Success: ' + greeting);
}, function(reason) {
alert('Failed: ' + reason);
}, function(update) {
alert('Got notification: ' + update);
});
$resource 是基于 $http 基础之上的一个 AngularJS 的服务,特别擅长与 RESTful 进行交互。
RESTful API的开发和使用,无非是客户端向服务器发请求(request),以及服务器对客户端请求的响应(response)。本真RESTful架构风格具有统一接口的特点,即,使用不同的http方法表达不同的行为:
- GET(SELECT):从服务器取出资源(一项或多项)
- POST(CREATE):在服务器新建一个资源
- PUT(UPDATE):在服务器更新资源(客户端提供完整资源数据)
- PATCH(UPDATE):在服务器更新资源(客户端提供需要修改的资源数据)
- DELETE(DELETE):从服务器删除资源
客户端会基于GET方法向服务器发送获取数据的请求,基于PUT或PATCH方法向服务器发送更新数据的请求等,服务端在设计API时,也要按照相应规范来处理对应的请求,这点现在应该已经成为所有RESTful API的开发者的共识了,而且各web框架的request类和response类都很强大,具有合理的默认设置和灵活的定制性,Gevin在这里仅准备强调一下响应这些request时,常用的Response要包含的数据和状态码(status code),不完善的内容,欢迎大家补充:
- 当GET, PUT和PATCH请求成功时,要返回对应的数据,及状态码200,即SUCCESS
- 当POST创建数据成功时,要返回创建的数据,及状态码201,即CREATED
- 当DELETE删除数据成功时,不返回数据,状态码要返回204,即NO CONTENT
- 当GET 不到数据时,状态码要返回404,即NOT FOUND
- 任何时候,如果请求有问题,如校验请求数据时发现错误,要返回状态码 400,即BAD REQUEST
- 当API 请求需要用户认证时,如果request中的认证信息不正确,要返回状态码 401,即NOT AUTHORIZED
- 当API 请求需要验证用户权限时,如果当前用户无相应权限,要返回状态码 403,即FORBIDDEN
最后,关于Request 和 Response,不要忽略了http header中的Content-Type。以json为例,如果API要求客户端发送request时要传入json数据,则服务器端仅做好json数据的获取和解析即可,但如果服务端支持多种类型数据的传入,如同时支持json和form-data,则要根据客户端发送请求时header中的Content-Type,对不同类型是数据分别实现获取和解析;如果API响应客户端请求后,需要返回json数据,需要在header中添加Content-Type=application/json。
angular.module('app', ['ngResource']);
angular.module('app').service('UserResource', function($resource) {
return $resource('/users/:id', {id: '@_id'}, {
update: {
method: 'PUT'
}
});
});
{
'get': {method:'GET'},
'save': {method:'POST'},
'query': {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'}
};
HTTP GET "class" actions: Resource.action([parameters], [success], [error])
non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
non-GET instance actions: instance.$action([parameters], [success], [error])
// 第一个参数,用法如下
// Given a template /path/:verb and parameter {verb:'greet', salutation:'Hello'} results
// in URL /path/greet?salutation=Hello.
// 第二个参数是 success 回调函数 拿到请求结果
// 第三个参数是 error 回调函数,这里省略掉了
UserResource.get({id: 1}, function(data) {
console.log(data);
});
// 第一个参数,添加到 request body
// 第二个参数是 success 回调函数 拿到请求结果
// 第三个参数是 error 回调函数,这里省略掉了
UserResource.save({name: 'haha', grade: 99}, function(data) {
console.log(data);
});
// 第一个参数,对象,查询条件
// 不传第一个参数,查询所有结果
// 其他参数与 get 相同
UserResource.query(function(data) {
console.log(data);
});
//第一个参数 和 get 一样
// 第二个参数是 success 回调函数 拿到请求结果
// 第三个参数是 error 回调函数,这里省略掉了
UserResource.delete({id: 1}, function(data) {
console.log(data);
});
// 这里例子同样也省略了 error 回调函数
var user3 = UserResource.get({id: 3}, function(data) {
user3 = data;
});
user3.name = 'Test';
user3.$save(function(data) {
console.log(data);
});
user3.$delete({id: user3.id}, function(data) {
console.log(data);
});
angular.module('app').service('UserResource', function($resource) {
return $resource('/users/:id', {id: '@_id'}, {
update: {
method: 'PUT'
}
});
});
//第二参数 为 request body 中的内容
UserResource.update({id: 3}, {name: 'hurry', grade: 100}, function(data) {
console.log(data);
});
user3.$update({id: 3}, function(data) {
console.log(data);
});
angular.module('app').service('UserResource', function($resource) {
var User = $resource('/users/:id', {id: '@_id'}, {
update: {
method: 'PUT'
}
});
User.prototype.isPass = function() {
return this.grade >= 60;
};
return User;
});
angular.module('app').service('BookResource', function($resource) {
var Book = $resource('/book?id=:id', {id: '@_id'}, {
list: {
method: 'GET',
url: '/book/list',
isArray: true
},
save: {
method: 'POST',
url: '/book/save'
},
delete: {
method: 'GET',
url: '/book/delete'
}
});
return Book;
});
本例中所有代码包括后端的 mock 请参考 $resource Example
参考资料:
https://www.sitepoint.com/creating-crud-app-minutes-angulars-resource/
// 当前日期减3个月
var current = new Date();
current.setMonth(current.getMonth() - 3);
// 参数 年,月(0-11)
function getDaysInMonth(year, month) {
var days_in_months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (month === 1) {
return ((year % 4 === 0) && ((year % 100) !== 0)) || (year % 400 === 0) ? 29 : 28;
} else {
return days_in_months[month];
}
}
var EndTime = new Date('2014/12/20 00:00:00');
var NowTime = new Date();
var t = EndTime.getTime() - NowTime.getTime();
var d = Math.floor(t/1000/60/60/24); // 天
var h = Math.floor(t/1000/60/60%24); // 小时
var m = Math.floor(t/1000/60%60); // 分钟
var s = Math.floor(t/1000%60); //秒
其他相关资料
lishengzxc/bblog/issues/5
Calculating Dates and Times (JavaScript)
├── LICENSE
├── README.md
├── demos
├── dist
├── docs
├── mock
├── package.json
├── scripts 工程化脚本
│ ├── build-prepare-production.sh
│ ├── build-prepare-test.sh
│ ├── build.sh
│ ├── publish-docs.sh
│ ├── publish-package.sh
│ ├── release-production.sh
│ ├── release-test.sh
│ └── release.sh
├── server.js express 启动文件
├── src
│ ├── assets css,图片等静态资源
│ ├── common 公共 utils 配置
│ ├── components 组件
│ └── index.js
├── test
├── webpack-build.config.js
├── webpack-common-loaders.js
├── webpack-dev.config.js
└── yarn.lock
环境 | 机器 | 账号 | 密码 | 配置信息 |
---|---|---|---|---|
dev | 172.17.33.222 | root | ***** | 操作系统,服务, nginx 配置,代码部署位置等信息 |
qa | 172.17.33.223 | root | ***** | 如上 |
stage | 172.19.33.222 | root | ***** | 如上 |
关于 ES 装饰器(decorator) 这个特性,就不在这里详细的介绍了: 更多内容大家可以参考javascript-decorators
简单的总结一下:
ES 的装饰器可以装饰类和类的方法(也可以装饰对象的方法):
1.装饰类的方法
function readonly(target, key, descriptor) {
// 注意这三个参数:
// target 类的 prototype
// key 方法名称
// descriptor descriptor 对象
descriptor.writable = false
return descriptor
}
class User {
@readonly
say () {
return '你好!';
}
}
2.装饰类
function test(target) {
// target 指类本身
target.test= true
}
@test
class User {
say () {
return '你好!';
}
}
关于装饰器如何传参,参考上面提到的资料。
因为现有产品需要切换成 ES6(当然这里不单指 ES6 的特性)
在公司 AngularJS1.x 与 ES6 的编码风格中,对 controller 的使用,已经全面使用 class 去实现,这为使用装饰器创造了条件。
1.声明依赖注入
AngularJS 依赖注入显示声明, 可以很好的利用装饰器。请看实现:
const Inject = (...dependencies) => (target) => {
target.$inject = dependencies;
};
@Inject('$scope', '$q', '$resource')
class MainCtrl {
constructor($scope, $q, $resource) {
}
}
当然还有更好的实现,这个大家可以参看 angular-es-utils 中的 inject 实现,非常的巧妙。
如果考虑到继承的情况,angular-es-utils 中的 inject 就不合适了。 另外该 Inject 返回了新的 class 这样会导致一块使用的装饰器,无法获取原构造函数的信息。
const toString = Object.prototype.toString;
export const Inject = (...dependencies) => (target) => {
// 获取当前 class 的父类
const parentClass = Object.getPrototypeOf(target);
const parentDependencies = parentClass.$inject;
if (parentDependencies && toString.call(parentDependencies) === '[object Array]') {
dependencies = [...dependencies, ...parentDependencies];
}
target.$inject = dependencies;
};
/**
* 考虑继承父类的依赖注入
*
* @Inject('$q', '$scope')
* class P {
* constructor(...dependencies) {
*
* }
* }
*
* @Inject('$http')
* class C extends P {
* constructor($http, ...parentDependencies) {
* super(...parentDependencies);
* }
* }
* */
最终方案,使用 Proxy 修改 constructor,自动将注入的服务挂载到 controller prototype 上
const toString = Object.prototype.toString;
export const Inject = (...dependencies) => (originTarget) => {
// 获取当前 class 的父类
const parentClass = Object.getPrototypeOf(originTarget);
const parentDependencies = parentClass.$inject;
if (parentDependencies && toString.call(parentDependencies) === '[object Array]') {
dependencies = [...dependencies, ...parentDependencies];
}
originTarget.$inject = dependencies;
// 使用 Proxy 修改构造函数
const handler = {
construct(target, argumentsList) {
dependencies.forEach((dependence, index) => {
target.prototype[`_${dependence}`] = argumentsList[index];
});
return Reflect.construct(target, argumentsList);
}
};
const newTarget = new Proxy(originTarget.prototype.constructor, handler);
return newTarget;
};
2.$apply
该实现依赖 angular-es-utils
import injector from 'angular-es-utils/injector';
import angular from 'angular';
const $rootScope = injector.get('$rootScope');
export const $apply = (target, key, descriptor) => {
const fn = descriptor.value;
if (!angular.isFunction(fn)) {
throw new SyntaxError('Only functions can be @$apply');
}
return {
...descriptor,
value(...args) {
if (!$rootScope.$$phase) {
$rootScope.$digest(() => {
fn.apply(this, args);
});
}
}
};
};
class MainCtrl {
@$apply
test(){
}
}
3.$timeout
import injector from 'angular-es-utils/injector';
import angular from 'angular';
const $timeout = injector.get('$timeout');
export const $timeout = (delay = 0, invokeApply = true) => (target, key, descriptor) => {
const fn = descriptor.value;
if (!angular.isFunction(fn)) {
throw new SyntaxError('Only functions can be @timeout');
}
return {
...descriptor,
value(...args) {
$timeout(() => {
fn.apply(this, args);
}, delay, invokeApply);
}
};
};
class MainCtrl {
@$timeout(0, false)
test(){
}
}
4.路由配置
使用 UI-Router 去实现应用中的路由,使用装饰器将路由配置与 controller class 进行绑定,当 Angular 声明 module 时,读取对应的路由配置进行路由设置。
@Router('example', {
url: '/example',
templateUrl: ExampleTplUrl,
controller: 'ExampleCtrl',
controllerAs: 'vm'
})
export default class ExampleCtrl {
constructor() {
this.init();
}
init() {
}
}
将配置存入一个公共对象中,以 class 名称作为 key(也可以使用 Reflect.defineProperty 看你的浏览支持情况)
import map from '../utils/map';
import traverse from '../utils/traverse';
export const Router = (state, config) => (target) => {
// use target replace controller name
traverse(config, 'controller', target);
const routers = map.get('uiRoutersConf') || {};
const className = target.name;
routers[className] = {
state,
config
};
map.set('uiRoutersConf', routers);
};
封装 AngularJS module 方法,当初始化 module 时,设置路由, 根据 AngularJS + ES6 风格指南,顺便不对外提供 factory 和 filter 方法
import angular from 'angular';
import map from './map';
class DecoratedModule {
constructor(name, modules = false) {
this.routers = map.get('uiRoutersConf') || {};
this.name = name;
if (modules) {
this.ngModule = angular.module(name, modules);
} else {
this.ngModule = angular.module(name);
}
}
router(className) {
const routers = this.routers;
configRouter.$inject = ['$stateProvider'];
function configRouter($stateProvider) {
if (className) {
$stateProvider.state(routers[className].state, routers[className].config);
} else {
Object.keys(routers).forEach((key) => {
$stateProvider.state(routers[key].state, routers[key].config);
});
}
}
this.ngModule.config(configRouter);
return this;
}
routerAll() {
return this.router();
}
config(configFunc) {
this.ngModule.config(configFunc);
return this;
}
run(runFunc) {
this.ngModule.run(runFunc);
return this;
}
controller(...params) {
this.ngModule.controller(...params);
return this;
}
}
function Module(...params) {
const module = new DecoratedModule(...params);
module.routerAll();
return module;
}
export default Module;
5.Mixin
除了使用继承外, 为了简化 controller, 将其它功能通过 Mixin 的方式混入 controller class 中。
// 该实现是对[core-decorators.js] (https://github.com/jayphelps/core-decorators.js)的 mixin 实现的简化
const { defineProperty, getOwnPropertyNames, getOwnPropertyDescriptor } = Object;
function getOwnPropertyDescriptors(obj) {
const descs = {};
getOwnPropertyNames(obj).forEach((key) => {
descs[key] = getOwnPropertyDescriptor(obj, key);
});
return descs;
}
export const Mixin = (...mixins) => (target) => {
if (!mixins.length) {
throw new SyntaxError(`@mixin() class ${target.name} 至少需要一个参数.`);
}
for (let i = 0; i < mixins.length; i++) {
const descs = getOwnPropertyDescriptors(mixins[i]);
const keys = getOwnPropertyNames(descs);
for (let j = 0, k = keys.length; j < k; j++) {
const key = keys[j];
if (!(key in target.prototype)) {
defineProperty(target.prototype, key, descs[key]);
}
}
}
};
const obj = {
myMethod(){
}
}
@Mixin(obj)
class MainCtrl {
constructor() {
this.myMethod();
}
}
6.Before/After
在 AngularJS1.x 结合 ES6 规范中已经弃用了 filter/service/factory 具体原因参考规范中No Service/Filter !!。 $provide.decorator 已经没有应用场景了。此时需要扩展一个 util 类或对象的方法,除了继承外,也可以使用装饰器进行扩展。
import angular from 'angular';
export const Before = (beforeFn) => (target, key, descriptor) => {
const fn = descriptor.value;
if (!angular.isFunction(fn)) {
throw new SyntaxError('Only functions can be @Before');
}
if (!angular.isFunction(beforeFn)) {
throw new SyntaxError('Only function can be pass to @Before');
}
return {
...descriptor,
value(...args) {
args = beforeFn.apply(this, args) || args;
return fn.apply(this, args);
}
};
};
export const After = (afterFn) => (target, key, descriptor) => {
const fn = descriptor.value;
if (!angular.isFunction(fn)) {
throw new SyntaxError('Only functions can be @After');
}
if (!angular.isFunction(afterFn)) {
throw new SyntaxError('Only function can be pass to @After');
}
return {
...descriptor,
value(...args) {
const result = fun.apply(this, args);
return fn.apply(this, args.unshift(result)) || result;
}
};
};
7.其他功能
类似 Debounce Bind 等功能,非常有用。这些都可以参考 core-decorators.js
以上装饰器的实现,请参考 https://github.com/hjzheng/angular-utils
装饰器特性不仅可以在不改变原有类或方法的前提下,增加新的功能和特性,另外还可以简化代码的写法,对于编码效率提升非常有用。
select {
-webkit-appearance: none;
border-radius: 0px;
}
上边的方法,会一并删除右边的图标
select:not([multiple]){
-webkit-appearance:none;
-moz-appearance:none;
background-position:right 50%;
background-repeat:no-repeat;
background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAMCAYAAABSgIzaAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDZFNDEwNjlGNzFEMTFFMkJEQ0VDRTM1N0RCMzMyMkIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDZFNDEwNkFGNzFEMTFFMkJEQ0VDRTM1N0RCMzMyMkIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NkU0MTA2N0Y3MUQxMUUyQkRDRUNFMzU3REIzMzIyQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0NkU0MTA2OEY3MUQxMUUyQkRDRUNFMzU3REIzMzIyQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGsgwQAAAA5SURBVHjaYvz//z8DOYCJgUxAf42MQIzTk0D/M+KzkRGPoQSdykiKJrBGpOhgJFYTWNEIiEeAAAMAzNENEOH+do8AAAAASUVORK5CYII=);
padding: .5em;
padding-right:1.5em;
border-radius:0;
}
stackoverflow上的回答
http://stackoverflow.com/questions/5780109/removing-rounded-corners-from-a-select-element-in-chrome-webkit
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.