Giter VIP home page Giter VIP logo

fe-problem-collection's People

Contributors

hjzheng avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fe-problem-collection's Issues

为什么我获取不到这个css样式?js原生获取css样式总结

还是自己遇到的一个坑的总结吧!与其说是坑不如说自己学艺不精,让我先哭一会!!

需求

简单就是获取一个css的height

(好吧 就是一个这么简单的需求)

实践

好吧 长时间的JQ 我已经对原生无能了 让我默哀3秒!!!

document.querySelector('.className').style.height;

这个果然不生效 好吧 看来我真的倒退不少!让我再哭一会!!(哭你妹 快去总结)

在学习中发现 其实js原生获取css的方法很多,上面写的就是其中一种,只不过情景不对啊!

getComputedStyle

  • 说明 一个可以获取当前元素所有最终使用的CSS属性值。返回的是一个CSS样式声明对象([object CSSStyleDeclaration])

简单来说 读出你八辈祖宗的一个方法。

  • 语法 window.getComputedStyle(dom, '伪元素')

看到伪元素、我就懵逼了 我只知道伪类啊 这有神马区别?

伪元素 其实就是 ::before :: after :: first-line ::first-letter ::content 等等
伪类 :hover :link :first-child :active 等等

  • 用法
var oImg = document.getElementById('photo');

window.getComputedStyle(oImg, null).height;

dom.style

  • 说明 获取元素style属性中的CSS样式, 简单说就是 能读能写 只能读你的外表
  • 语法 dom.style.样式名称
  • 用法
var oImg = document.getElementById('photo');

oImg.style.height;  // 只能获取css 样式表的

currentStyle

  • 说明 返回的是元素当前应用的最终CSS属性值(包括外链CSS文件,页面中嵌入的<style>属性等)与getComputedStyle那个读你八辈祖宗的很像,但是不能获取伪元素。且他们获取的属性也有很大差异。
  • 语法 element.currentStyle.样式
  • 用法
var oImg = document.getElementById('photo');

oImg.currentStyle.height;  // 只能获取css 样式表的

getPropertyValue和getAttribute

  • 说明 可以获取CSS样式申明对象上的属性值(直接属性名称),
    getPropertyValue可以不用驼峰,直接用其属性名
    getAttribute主要是为了兼容IE获取属性值,需要驼峰的写法
  • 语法
    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}  结果集
            ... ...
     ] 
}
  • 删除,更改和添加后响应格式:
    • 成功: 返回操作实体的简单信息,方便前端通知用户
    • 失败: 用 HTTP 状态码 表示, 返回错误 message 和 errorCode 参见 500 相应格式
{
       "id": {number}, 
       "name": {string}
}
  • 403响应
  • 500响应
{
    "message": {string}, // 友好的错误信息,可选,如不提供前端应当使用默认的提示信息
    "errorCode": {number} // 可选择性提供 errorCode 便于后续问题排查,可选
}

后端可以针对不同的响应格式创建不同Java类,其他同学只需继承对应的class,而前端同学针对500和403可以使用AngularJS拦截器统一处理。

最后再强调一点,接口如果一旦定义清楚后,如果需要改动,一定要及时通知到相关同学,所以建议将修改接口权限控制在专门人手里。

关于接口的设计,可以参考
https://github.com/ecomfe/ub-ria/wiki

推荐一个接口定义工具 https://github.com/thx/RAP

关于angular-ui-router使用

angular-ui-router使用

github源码地址:https://github.com/angular-ui/ui-router

api地址 http://angular-ui.github.io/ui-router/site

安装

npm install --save angular-ui-router

使用angular-ui-router

备注: 以下所有示例代码来源于个人所写的练习.

地址为:https://github.com/silence717/angular-webpack-demo

导入angular-ui-router
import uiRouter from 'angular-ui-router';
在angular.module中注入angular-ui-router
    angular.module('app',[uiRouter]);
为了方便使用,将$state与$stateParams全部挂载到$rootScope,在angular.module中初始化
function runBlock($rootScope, $state, $stateParams) {
    'ngInject';

    $rootScope.$state = $state;
    $rootScope.$stateParams = $stateParams;
}
定义路由规则,在angular.module中配置路由规则
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);
        }
    });
}
基于以上两部分的操作,完整的app初始化代码为
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'
    }
};
业务入口主文件导入,并且在angular.module中配置
import {HomeRouter} from './Routers';

function config($stateProvider) {
    'ngInject';
    $stateProvider.state(HomeRouter.state, HomeRouter.config);
}

export default angular
    .module('app.home', [])
    .config(config)
    .name;
页面的html如何书写
<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>

在这里做一个简单常用的API的解释

directive

ui-sref:A directive that binds a link ( tag) to a state.
ui-sref-active: A directive working alongside ui-sref to add classes to an element when the related ui-sref directive's state is active, and removing them when it is inactive.
ui-view: The ui-view directive tells $state where to place your templates.

ui-view使用有三种,分别为:

  1. as element:
<ui-view></ui-view>
  1. as attribute:
<ANY ui-view ></ANY>
  1. as class:
<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>

$state使用

Methods

get 获取当前state的配置信息
    <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"}...
go 跳转到一个新的state
    <a href="javascript:;" ng-click="vm.goUserCenter();">这是一个跳转链接...</a>
    // 不带参数跳转
    this.state.go('user.baseInfo');
    // 带参数跳转
    this.state.go('album.detail', {id: 1});
href 获取到当前state的href值
    console.log(this.state.href('album.detail', {id: 0}));
    // => #/album/0
includes 获取当前state是否包含某些state
    console.log(this.state.includes('album'));
    // => false
is Similar to $state.includes, but only checks for the full state name
    console.log(this.state.is('home'));   //=> true

events

$stateChangeStart 路由发生改变
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
        });
}
$stateChangeError 路由转变出错,参数基本与$stateChangeStart一致,多一个error参数
$stateChangeSuccess 路由转向成功,参数与$stateChangeStart完全一致
$stateNotFound 未找到路由,demo copy于参照官网
// 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 可以配置进入路由to do something
onEnter: function() {
    console.log('enter user.footprint state');
}
onExit 退出路由做什么
onExit: function() {
    // 用于初始化一些数据什么的,清空表单...
    console.log('exit user.footprint state');
}

部分知识点单列:

一、问: 多个页面可能使用相同的html模板,我们是要同样的代码写N遍吗? 答案肯定不是.那么问题来了,一个页面有多个模板文件,肿么办?

  1. 在html中给ui-view添加名字
<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>
  1. 在路由配置中添加配置信息
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

二、 $stateParams使用,^^最后一个点

先看代码:

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.

三、这次真的是最后一个点了 most important: $urlRouterProvider

  1. when() for redirection
    app.config(function($urlRouterProvider){
        $urlRouterProvider.when('', '/index');
    });
  1. otherwise() for invalid routes
app.config(function($urlRouterProvider){
    $urlRouterProvider.otherwise('/home');
});
it's over! 所有以上仅代表个人理解,如有不对,请指出,虚心接受改正错误!

AngularJS 1.x 中的 scope

关于 AngularJS 1.x 中的 scope, 官方 guide 已经讲的很清楚了,大家自己阅读就行。

官方 guide (中文): http://docs.ngnice.com/guide/scope
官方 guide (英文): https://docs.angularjs.org/guide/scope

我在这里只强调几个 scope 技巧:

  • scope 树状结构,类似于 dom 树, 在console中执行下面的代码, 看看是不是一颗树。
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();
  • 父 scope 和 子 scope 存在继承关系(这里实际上利用的是 JS 的原型继承, 请翻看 AngularJS 源码 关于scope中的 $new 方法),所以在父子 scope 中定义同名的变量,此时,你需要使用父元素上定义的变量,需要额外注意,防止被子 scope 的同名变量覆盖掉, 当然,现在都使用 controllerAs 的方式。
  • 额外关注一些指令,因为它们会产生子的 scope, 例如 ngController, ngInclude, ngRepeat 等,相应的说明都可以参看官方 API, 因为上面有详细的说明
  • scope 提供了$watch, $watchGroup, $watchCollection, 注意三者的区别, 并灵活运用
  • scope 提供事件机制.
    事件优化 请参考 https://segmentfault.com/a/1190000000502981#articleHeader12
    • $emit
    • $broadcast
    • $on

技巧2:慎用事件传播,先用其他方式,使用双向数据绑定或共享service等方法来代替。
$on 和 $broadcast 的源码解读
$on 将注册的函数放入 scope 对象的 $$listeners 中。
$broadcast 对 scope 树进行的遍历(深度优先遍历),然后在 $$listeners 上找到注册的方法后,执
行,效率不高

  • scope 的 $apply 和 $digest
    $apply 会使 ng 进入 $digest cycle, 并从 $rootScope 开始遍历(深度优先)检查数据变更。
    $digest 仅会检查该 scope 和它的子 scope,当你确定当前操作仅影响它们时,用 $digest 可以稍微提升性能。
  • scope 与 指令 https://github.com/hjzheng/angular-directive-learning#lesson-3

通过设置 scope 的值 ,false 使用父 scope, true 产生一个子 scope 继承自父scope, {} 隔离scope

AngularJS 如何实现复制到剪切板的功能?

AngularJS如何实现复制到剪切板的功能?

实践方案

上网查资料时发现有很多实现这个功能的例子,我尝试了其中最简单的一种方法,就是利用原生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操作,因此不是很理想的方案

其他方案:

  1. 参考ng-clip源码,其中部分代码看不懂,求大神指点 参考链接
  2. 安装依赖库angular-clipboard 参考链接

如何使用 Express 去 mock 后端请求

Express 是什么 ?

中文官网的回答: Express 是一个高度包容、快速而极简的 Node.js Web 框架。

如何使用

  • 安装
npm install express --save
  • 写一个 http server (可以作为模板)
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');
});

mock 后端请求

  • Mock JSON 数据
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;
  • Mock 各种请求

注意几个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
image

// 使用路由, 单独文件
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

使用 Strong Loop 自动生成 RESTful API

json-server

除了Strong Loop外,我们还有另一大杀器, 就是 json-server,只需提供简单的json数据,就会生成标准 RESTful API,很方便,很实用。
更多内容,请参考 json-server 的 README

通过配置生成后端 mock 请求, 受 wiremock 启发

代码在 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'
            }
        }
    });
}

ng中的事件订阅与发布

ng中的事件机制

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》

如果不是全局的,异步的,关于状态变迁的通知,还是应该谨慎使用,通常可以依赖双向数据绑定,便捷的解决问题。

通过事件携带数据的目的

  1. 事件的注册与触发拥有明显的前后条件逻辑
  2. 在不同的作用域之间传递信息。

考虑到ng中服务全局可见,并且能够被依赖注入,所以,我们可以考虑利用服务来指定目标来传递数据,避免性能上的浪费。
服务传递数据很容易实现,但是如何给服务增加注册与触发的先后条件逻辑呢?我们先来看看事件的先后条件逻辑是如何形成的。

JS实现事件机制

事件携带数据很形象的模拟了一组交互过程,通过事件的注册,事件的触发,来完成所需要的逻辑。基于此,我们可以在js中来模拟事件传播机制。
事件的过程主要分为三个步骤:

  1. 基于一个事件名,注册事件处理函数,在相应的事件发生时,该函数便会执行。

  2. 在需要触发事件的地方,调用该事件的所有已注册函数。

  3. 最重要的一点,我们如何能够将同一个事件的注册函数与事件发生对应起来,达到触发的效果呢。基于事件名与事件处理函数一对多的映射关系,我们使用js中的对象来储存这种映射关系,我们称之为事件队列。

    综上,我们的事件对象应该至少拥有

    1. 注册事件处理函数的方法。
    2. 触发事件的方法。
    3. 用于储存映射关系的事件队列。
                // 事件对象的构造函数,每个实例拥有一个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的事件传播,具有简单明确的优点,精简了我们的逻辑。

web前端emoji表情

随着互联网的发展,传统的文字已经不能满足人们生活交流的需要,有时一张图片,一个表情,更能传递想要表达的内容。(具体可参考微信斗图-_-! )如今比较有名的 emoji 已经可以被大部分设备所兼容,那么问题来了,如何在网页上显示一个emoji表情呢?

通常作为前端,我们接收到的表情不会是一张图片,而是类似 :smile : 、[微笑]、/微笑这样的字符串,那么如何将字符串转换为对应的表情就是问题的关键。毋庸置疑,通过正则表达式对相应的字符串作匹配替换,从而将对应的emoji表情显示出来是比较好的一个方法。

首先,本地要有一个emoji表情库,类似
1111
或者一个雪碧图
22
本地表情文件的不同,对应的处理方法也会不一样。
有了这样的一个表情库文件,接下来就是对相应的表情字符串进行匹配。

其次:匹配,就免不了要用到正则表达式,针对不同的表情字符串,需要写不同的匹配规则。如需匹配[微笑]或者/微笑格式的表情,对应的正则如下

//对于元字符 如 [] {} + ? 等,需要转义
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表格

HTML表格

最近,项目中很多地方涉及到了表格,但是发现很多同学并没有使用表格,或者部分使用表格去实现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;
}

image

2.border-collapse 控制 cell 之间是否有空隙

table {
  border-collapse: collapse;
}

td, th {
  border: 1px solid #999;
  padding: 0.5rem;
  text-align: left;
}

image

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>

image

4.table元素的特点与块级元素的特征类似,独占一行,但是它的宽度和高度取决于内容大小,如果它本身没有设置宽度,高度或者宽度,高度大于内容大小的话,也就是说它的最小宽度取决于内容宽度,最小高度取决于内容高度。

让其他元素具备 table 元素的行为(主要是通过 display 属性模拟)

那么为什么要这样做的,那是因为 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;
}

image

一些重要样式对 table 的影响

vertical-align 控制 cell 中的元素垂直对齐位置
white-space 控制 cell 中文本的显示方式
border-collapse 在table上设置
border-spacing 在table上设置
width 固定列的宽度,该列上设置宽度
border td或th 需要设置边框

实战,自己找一个页面进行练习

qq20160715-1 2x

参考资料:
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获取浏览器中各种宽、高、偏移值

本文中提及的都是一些简单易懂但平时容易忽略的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:浏览器相对于屏幕左上角的纵向偏移值

边线: 滚动条和边框

10 个你可以使用 ES6 替换的 Lodash 特性(翻译)

Lodash 是目前最被依赖的 npm 包,但是如果你正在使用 ES6,实际上可以不需要它。围绕许多流行的使用场景,我们打算考虑使用可以帮助节约成本的原生集合方法与箭头函数以及其它新的 ES6 特性。

1. Map, Filter, Reduce

这些集合方法处理数据转换非常容易并且普遍都支持, 我们可以使用箭头函数配合它们,写出简洁的代码去代替 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。

2. Head & Tail

解构语法 (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();

3. Rest & Spread

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"

4. Curry

不像一些高级别的语言例如 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);

5. Partial

和柯里化一样,我们可以使用箭头函数使偏函数应用 (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"

6. Operators

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);

7. Paths

许多 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);

8. Pick

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 };

9. Constant, Identity, Noop

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

10. Chaining & Flow

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

web 性能优化 - HTTP 缓存

随着互联网的发展,前端页面变的越来越复杂,越来越多的事情,被拿到前端来处理。这就导致 HTML 页面会被加载大量的资源 包括 JS,CSS 以及图片等。

本文从 HTTP 缓存的角度,来谈谈如何优化页面资源加载性能。

HTTP协议

在谈 HTTP 缓存之前,让我们先来聊聊 HTTP 协议,HTTP 作为一个非常重要的协议,它的发展却驻足不前,翻开的它的历史,它竟然只有三个版本:

  • HTTP/0.9,在HTTP 1.0 之前,HTTP 标准并未正式建立,所以统称 HTTP/0.9。
  • HTTP/1.0, 1996年正式公布的版本,被记载在 RFC1945。
  • HTTP/1.1, 1997年1月公布的,是目前的主要版本,它也是我们将要讨论的主要版本。

然而 HTTP 协议只是 TCP/IP 协议族的一小部分,为了了解客户端和服务器是如何通信的,就必须了解 TCP/IP 协议族.

TCP/IP 协议族进行了分层,从上到下依次是,应用层,传输层,网络层,数据链路层。 HTTP 协议属于应用层,TCP 协议属于传输层。

HTTP 协议是建立在 TCP 协议基础上,让我们来简单描述一下,浏览器与服务器如何使用 HTTP 协议进行通信的:

  • 浏览器解析出 url 中的主机名
  • 浏览器查询这个主机名的 IP 地址 (233.44.78.5)(DNS 服务)
  • 浏览器获得端口号(80)
  • 浏览器发起到 233.44.78.5 端口为 80 的连接(三次握手后,建立起一条从浏览器到服务器的 TCP 连接)
  • 连接建立后,浏览器发送一条 HTTP GET 请求报文
  • 浏览器从服务器获得 HTTP 响应报文
  • 浏览器关闭连接

浏览器和服务器通信发送的数据块是 HTTP 报文。HTTP 报文由起始行,首部和主体三部分组成。
HTTP 报文一般分为请求报文和响应报文。请求报文的起始行由方法,URI 和 HTTP 版本组成。响应报文的起始行由状态码,原因短语组成。它们的首部都有首部字段和值组成。主体是需要发送和返回的数据。首部和主体之间用 CRLF 空行分割。

配置响应首部字段

如何利用好 HTTP 缓存的关键在于响应和请求的首部字段。

因为每个浏览器都实现了 HTTP 缓存,所以无需设置请求的首部字段,目前所要做的就是,确保每个服务器响应都提供正确的 HTTP 首部,以指导浏览器何时可以缓存资源以及可以缓存多久。

使用 ETag 验证缓存的资源是否被修改

ETag 为我们提供一种验证资源是否被修改的方法,举例,浏览器首次请求资源,服务器返回一个带 ETag 首部的响应,大约过了一小时后,浏览器再次发起同样的请求,此时浏览器自动将 ETag 的值作为 If-None-Match 首部的值,发送给服务器,当服务器使用该值,检查响应的资源是否被修改,如果未修改,则返回 304 Not Modified 响应。注意这里我们并未下载资源,节约时间和宽带资源。

使用 Cache-Control 指导浏览器如何缓存资源

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 秒

配置 Cache-Control 的策略
如下图
image

我们可以根据自己实际的业务和不同的资源制定不同的缓存策略:

  • 针对 HTML 页面,使用 no-cache, 例如 index.html Cache-Control: no-cache
  • 针对 JS 和 CSS 文件,可以进行长时间缓存, 如果 JS 中有一些敏感信息,可以使用 private 禁止中继缓存存储。
    • 例如 main.2da43e32.css Cache-Control: max-age=31536000
    • 例如 main.1a3dq2q6.js Cache-Control: private max-age=31536000
  • 针对 图片或 字体文件,可以设置一个月(31天),例如 logo.png Cache-Control: public max-age=2678400

总结

在定义缓存策略时, 有一些准则是可以参考的:

  1. 服务器需要提供 ETag。
  2. 明确中继缓存可以缓存哪些资源。
  3. 根据资源的特点设置缓存时间。
  4. 针对同一资源,将变动的资源和不变资源区分。例如 JS 文件在压缩合并时,将 lib(一些库和框架js) 和 app(业务js)分离。设置不同的缓存策略。

配置 nginx

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 中含义略有不同:
image

资料

常见服务器配置
nginx配置location总结及rewrite规则写法

angular-tree的简单实现

用法

在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

嗯,简单的用法就是这样,接下来说一下实现方法。

实现

在做东西之前,首先要清楚自己需要的是什么,那么现在需要一个可以用户自定义内容与交互的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在此期间提供的支持与帮助。

理解 node.js 中的 module.exports 与 exports

理解 node.js 中的 module.exports 与 exports

原文链接: 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

What is a Module?

一个模块就是将文件中相关的代码封装为一个代码块。
创建一个module,可以理解为将所有相关的方法挪到一个文件中。
我们使用一个node.js的应用程序来说明一下这个观点。
创建一个名叫 greetings.js 的文件,其中包含下面两个方法:

// greetings.js
sayHelloInEnglish = function() {
  return "Hello";
};

sayHelloInSpanish = function() {
  return "Hola";
};

Exporting a Module

为了 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";
  }
};

Importing a Module

我们在 main.js 中 require greetings.js 的公开接口。这个过程有以下三个步骤:

1)关键词 require 在 node.js 中用于导入模块,即所获取模块的 exports 对象。
我们可以想到它是这么定义的:

var require = function(path) {

  // ...

  return module.exports;
};
  1. 在 main.js 中 require greetings.js
// main.js
var greetings = require("./greetings.js");

上面的代码等同于:

// main.js
var greetings = {
  sayHelloInEnglish: function() {
    return "HELLO";
  },

  sayHelloInSpanish: function() {
    return "Hola";
  }
};
  1. 现在我们可以在 main.js 中使用greetings访问 greetings.js 中公开的方法就像获取它的属性一样。
// main.js
var greetings = require("./greetings.js");

// "Hello"
greetings.sayHelloInEnglish();

// "Hola"  
greetings.sayHelloInSpanish();

Salient Points 重点

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里面的方法。

Conclusion 总结

importing 和 exporting 模块在 node.js 中是一个随处可见的任务。
我希望 exports 和 module.exports之间的差异更加清晰。
此外,如果将来你遇到调用公共方法错误的时候,我希望你可以对这些
错误的原因有一个更好地理解。

PS: 第一次尝试翻译文章,有的地方确实感觉很生硬,不合适的地方欢迎吐槽,继续改进!

CSS text-overflow: ellipsis 如何使用 & 文本溢出造成水平不对其的问题

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/

JS event loop 与 AngularJS 的问题?

今天 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 肯定不是我们想要的。

可以参看此图:
qq20160707-1 2x

那么怎么办呢,大家都知道 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/

Angular中的安全性验证

在开发的过程中,遇到身份验证的问题,如果在不登录的情况下直接访问项目的地址,怎么对用户的身份进行验证?常见的处理是:如果没有读取到用户的登录信息,则页面自动转向登录页面,那么作为开发人员,怎么实现此需求?目前据我所知的解决方案有两种。第一种是用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实现即可,这里不做赘述。

React JSX 中的条件判断

在书写 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行要省略该如何解决呢?

解决

  • 解决方案1 css 的不规则属性解决

-webkit-line-clamp 是一个不规则属性,不出现在css 草案中。用来实现块级元素显示几行,它需要与其他元素组合使用

display:-webkit-box;
-webkit-box-orient: vertical; // 从上向下垂直排列子元素。设置或检索伸缩盒对象的子元素的排列方式 。
text-overflow: ellipsis; //文本溢出 用省略号显示
-webkit-line-clamp: 2;
overflow: hidden;

当然也需要设置高度来配合显示

  • 解决方案2 css 的伪类实现

对要显示的文字进行相对定位
对省略号进行绝对定位

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/

  • 解决方案3:js解决方法

Clamp.js 这个就不具体展开了

可以参考demo http://codepen.io/feiwen8772/pen/AckqK

参考

document 和 $document 的区别?

区别

$document 等于 angular.element(window.document) 实际上它是 jqLite 对 window.document的包装
$window.document 等于 window.document 它们都是 DOM element document.

$window.document === $document[0] //true

联想

window 和 $window 的区别?

$window 其实是 AngularJS 将全局 window 对象包装成的服务

settimeout 和 $timeout 的区别?

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 性能的一个优化点。

setInterval 和 $setInterval 的区别?

区别同上, 可以参考官方API https://docs.angularjs.org/api/ng/service/$interval

关于ESlint校验 angular/on-watch 如何fix问题

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);
    }
})();

AngularJS 实现 checkbox 全选、反选的思考

需求

1 还有个总的 checkbox 负责全选和反选
2 当每一项开头都选中 checkbox 的时候,上面的全选自动选上
3 当全选后,取消其中一项的 checkbox ,全选取消

实践

我表示刚开始我是不会的!!!

思路1 上网查找 demo

然后开始查资料

发现个不错的网上案例 like this > demo

感觉非常符合我的需求,但是看到 demo。

缺点: 没玩几下就发现无法符合需求2、3。但是貌似1 是可以实现的。

思路2 ng-checked

开始查了下 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);
    }
}]);

缺点 参考 not-binding-to-ng-checked-for-checkboxes

大概意思说 如果你用了 ng-checked 就是默认它最初定义就是 true, 因此就没有必要使用 ng-model 了。简单说来就是 ng-model 和 ng-checked 不需要同时用。

看来是能用 ng-click 或者 ng-change 了

思路3 给数组里面每一个 list 绑定 checked 的属性

这个想法也是联想到公司之前的一个大牛的写的一个关于 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);
    }
}]);

推荐第三种方法!以上

参考:

modules and dependency injection

modules

module构成了我们的web应用。

从面向对象的角度来看,我们期望module拥有:

  • 包含自身信息的属性,如name,requires(依赖)以及others;
  • 供外界用于向module内部填充内容的方法,如constant,service,factory,directive以及controller等。

1. 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);

2. module对象的管理

为了方便对模块的管理,我们还需要一个变量来存储模块,一个简便的方法来新建或获取模块如:

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;

dependency injection

当使用依赖注入机制时,我们的期望是:当我们在特定的上下文中,需要使用别的值或者对象,甚至是类时,我们只需给出它们名称,然后就可以在接下来的上下文中直接使用它们。

1. 我们的期望

在angular中,我们很熟悉这样的代码:

function myController($scope, $window, myService) {
    // 直接使用$scope,$window, myService。
}

我们希望的是直接在该函数内部使用$scope之类的对象。

2. 如何实现

在如何实现我们的期望之前,我们首先来看看我们最熟悉的函数 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的依赖注入更为全面和复杂,不过,一切都是从这简单的逻辑开始的。

如何使用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

参考文档:

  1. $http
  2. $resource

angular开发过程中遇到问题 --- $apply

angular开发过程中 $apply 问题

接到一个类似于dropdown这样的需求,点击按钮下拉选择展示,而它的关闭有3中场景。

  1. 目前处于展开状态,再次点击按钮,下拉隐藏。
  2. 点击里面的任一条件,下拉隐藏。
  3. 点击空白处,下拉隐藏。

相信这样的使用场景一定不陌生,因为他经常出现。

一开始的时候想象了一下jquery多么美好,实现起来多么简单,其实angular也很容易。

思路解析:

外层设置一个状态值,通过添加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/

decorator Method in ng

前提 依赖注入与$injector

  1. AngularJS的世界里,依赖注入的实现使我们面向对象编码过程中,能够更清晰的管理类之间的耦合关系,将构造所需实例的逻辑使用统一的方法实现,在ng中,这个方法就是$injector。
  2. 当我们将依赖的实现交给$injector后,自身内部不再出现关于依赖的实现逻辑,但是,$injector帮我们实现的依赖有时并不能完全满足当前对象的逻辑,这时,我们需要在依赖的实现逻辑中加入我们自身的逻辑。
  3. 那么,问题来了,该部分逻辑应该放在哪里?首先,这部分逻辑不应该出现在自身内部,因为按照依赖注入的设计,当我们声明依赖时,只需关注自身如何使用依赖,并不关心依赖的实现过程;其次,不应该出现依赖自身的实现上,自定义的逻辑应该只适用于自身的依赖,不能改变该依赖本身的实现方式。

思考,能否在$injector的逻辑中,存在修改已注册依赖的入口,让我们针对具体对象能够配置对应的依赖,并且没有破坏原有依赖自身的实现逻辑?

依赖注入关注各个对象之间的关系,ng中将可以被依赖注入的对象,称为服务,以下将对依赖的修改统称为对服务的修改。

什么是decorator

首先来看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.

简单来说:

  1. decorator的本质是在$injector上注册了一个方法。

  2. 这个方法可以拦截一个服务的创建,并且可以覆盖或修改这个服务的行为。

  3. 该方法应该有一个返回值,这个值可以是原来的服务(即你没有做任何修改),也可以是一个替换或者修改并代表了原服务的一个全新的服务。

    很顺利,ng给我们提供了一个入口来切入服务创建的过程,并且能够写入自身的逻辑。
    就是本文的主角:decorator 方法

怎样使用decorator

结合我们的问题与ng给出的定义,很清楚的可以得到实现过程,首先来看,decorator方法在代码中使用:

decorator(name, decorator);

  1. name正是我们需要去添加自定义逻辑的已经注册的服务的名称。
  2. decorator是一个方法,这个方法的返回值正是我们需要的新的服务的实现方式。
  3. name很好理解,我们重点来关注第二个参数的实现方式,这里需要一个函数,在这个函数中我们希望能够获得原有的服务,这样我们可以在其基础上添加逻辑,并且返回新的服务来覆盖或修改原有服务。

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;
        }
    }

$sce in ng

场景

当我使用iframe将一个另一个独立开发的web网页嵌入到ngApp中时,ng向我抛出了一个错误,它认为使用iframe来引入一个陌生的url所指引的web网页是不安全的,但开发者判断这是个安全的来源。我们需要告知ng。精确来说,我们需要告知ng中负责安全限制的 $sce。

$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.

  1. 首先,$sce是一个全局可见的服务。
  2. 其次,它用来执行“严格上下文控制”这个用来确保数据绑定安全的模式,保证整个页面上下文安全。
  3. 在ng以数据为驱动的设计模式中,我们需要确保数据的来源是安全的,相对于JavaScript代码直接暴露在客户端的特定来说,增加对于数据来源的验证是十分有必要的。

所以,ng设计了$sce来管理可能出现不安全来源数据的场景,上文中提到的iframe的url便是其中之一。

$sce怎样工作

  1. 总体来说,需要引入数据的地方都是需要确保数据的来源是安全的,不过,很多情况下,数据类型与来源本身就是安全的,所以我们不需要进行安全性控制,自然,也就没有$sce的用武之地了。
  2. 当我们的数据类型存在安全性方面的风险,ng在不被特意通知的情况下便会启动$sce来处理数据,对应的场景包括
  • Directive 中 attribute 的绑定,如ng-bind,{{}} 插值表达式等会转译html代码。
  • 载入html模板时,如ng-include,Directive中的 templateUrl;默认下,ng只从当前域名与端口下加载模板,这些都是基于js的同源策略实施的。
  • 对于iframe与object等url的限制。

$sce 支持的安全上下文

当我们能明确判断数据安全性的时候,我们不需要ng对上述的情况作出转译以及限制,于是我们需要明确告知ng,这时我们就可以使用$sce来设置,甚至,我们可以在全局中关闭$sce功能,不过,这个并不被提倡。

具体的$sce提供的安全上下文及设置方法请参考 $sce doc

ng中的provider写法

最近,在项目中遇到需要在 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);
    };
}

大同小异,自己体会。

angular的数据驱动与浏览器的 event loop

一、开始

how Angular interacts with the browser's event loop?.

  • Angular 如何与浏览器的 event loop 相互作用?
  • 再说,为什么要与浏览器的 event loop 相互作用?

image
Integration with the browser event loop

1.wait(等待触发 )

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);

2.emits(事件触发)&& callback executes(执行回调)

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。

$evalAsyncQueue 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,达到同步数据的目的。

$watchList loop

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。

$digest done && before DOM render(脏检查结束,DOM渲染之前)

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。

3.browser re-rendering(DOM重新渲染)

至此,数据的改变便驱动了view层的变化。

二、深入

1.ng如何根据作用域解析并执行表达式,或执行函数

表达式的解析与执行,函数的执行必须与作用域联系起来,并且该动作可能会引起数据的变化。

$eval

Executes the expression on the current scope and returns the result.

在当前作用域中执行表达式并返回结果。
考虑到执行的动作可能会改变数据,所以需要配合别的动作来进入digest循环,来确保数据的一致性。

$evalAsync

Executes the expression on the current scope at a later point in time.
“稍后但很及时的”在当前作用域中执行表达式,ng在这个动作中整合了保证数据一致性的动作。

  1. 判断当前是否处在一个脏检查中,如果存在,就会将该表达式添加到evalAsyncQueue这个对列中,所以,这个动作便会整合进入已经存在的脏检查,被执行并保证了数据的一致。
  2. 如果不存在,ng会该表达式添加到evalAsyncQueue这个对列中,并主动发起$rootScope.$digest()来确保数据的一致性。

2.ng中如何检查数据。

$digest是数据绑定的核心,可分为

  1. 声明当前处于一个digest状态中。
  2. 当$rootScope.$digest()时,判断applyAsyncId是否存在,如果存在,取消对应的$apply动作,并且执行flushApplyAsync。这个步骤的用意是,当多个$apply的请求存在时,ng会在这个时间点来取消注册了的$apply,flushApplyAsync会执行所有的applyAsyncQueue队列动作,然后统一开始$digest循环。
  3. 进行evalAsyncQueue队列的遍历并执行,执行完后重置evalAsyncQueue。这个步骤的用意是,当回引起数据变化的动作存在时,依次执行完所有动作,再统一去遍历数据。
  4. 进行当前作用域中$watch list的遍历,比较新旧值,判断是否执行$watch list队列中的watcher。并且这个遍历是深度优先遍历。
  5. 根据遍历结果(dirty)与evalAsyncQueue是否为空来确定是否进入下一次循环。
  6. 最后,清除当前digest状态的声明。
  7. 执行postDigestQueue队列中的内容。

3.与浏览器eventLoop的结合。

$apply用于响应eventloop的事件,并进入$rootScope.$digest()

  1. 声明当前处于一个$apply的循环状态。
  2. 在当前作用域上解析并执行fn。
  3. 最终主动发起$rootScope.$digest()

$applyAsync用于处理多个$apply请求。

  1. 如果参数存在,将执行该参数的动作添加到applyAsyncQueue这队列中,此时并没有执行该动作。
  2. 然后判断,如果applyAsyncId不存在,异步的发起$rootScope.$apply(flushApplyAsync),并给applyAsyncId赋值来注册$apply的状态。

关于$applyAsync方法以及applyAsyncQueue队列的用意,目前没有很好的理解,$digest循环中也会进行applyAsyncId的判断以及applyAsyncQueue队列的flush(冲刷);

三、总结

MVVM模式的核心在于数据驱动,所以,一切对应用状态的操作的出发点应该是操作数据,再通过框架对数据的处理,最后映射到应用的状态,就是页面中的DOM状态。而这个处理数据的核心操作便是$digest循环。
在项目中使用angular作为框架时,能整理出驱动整个应用状态流转的数据变化,那么,与angular的合作便很顺利了。

AngularJS 性能优化

AngularJS 性能优化

说到 AngularJS 的性能优化,一个很重要的点就是从 数据的脏检查 开始:

理解脏检查机制 参考 #27

  • $digest loop
  • $watch list ($$watchers)
  • $evalAsync queue ($$asyncQueue)

做一个试验,看看什么情况下,变量会被加入到 $$watchers 中

http://plnkr.co/edit/hDHmkdNjQQkdwscjouzE?p=preview

  • {{}}
  • 某些指令: ng-repeat ng-model ng-bind
  • $watch

再看看什么情况会触发脏检查

  • ng封装的DOM事件 (例如ng-click) 可以查看源码 ngEventDirectives 的定义
  • XHR响应事件 ($http)
  • 浏览器Location变更事件 ($location)
  • Timer事件($timeout, $interval)
  • 主动执行$digest()或$apply()

减少 $watch list 数量,有以下处理手段:

  1. 使用 ng-if 替换 ng-show 或 ng-hide
  2. 使用 单次绑定 例如: {{::name}} 单次绑定不会加入到 $$watchers 中

降低 $watch list 中表达式的复杂度

  1. $scope.$watch(watchExpression, modelChangeCallback), watchExpression可以是String或Function。
  2. 避免watchExpression中执行耗时操作,因为它在调用一次 $digest 方法都会执行2次。(这个可以参考我渣译的Build Your Own AngularJS
  3. 避免watchExpression中操作dom,因为它很耗时
  4. 避免深度watch $watch(watchExpression, listener, [objectEquality]) 不要使用第三个参数
  5. 对于集合使用 $watchCollection

减少触发 $digest loop

  1. ngModelOptions 官方API
  2. $httpProvider.useApplyAsync(true); #27 参考@kuitos的回答
  3. 减少在页面使用filter,例如 {{1288323623006 | date:'medium'}}

缩小 $digest 的影响范围

  1. $apply会使ng进入$digest cycle, 并从$rootScope开始遍历(深度优先)检查数据变更。
  2. $digest仅会检查该scope和它的子scope,当你确定当前操作仅影响它们时,用$digest可以稍微提升性能。
  3. $digest 建议在隔离scope的指令中使用

其他方面:

  1. ng-repeat 使用 track by
  2. ng-bind 优于 {{}} https://ng-perf.com/2014/10/30/tip-4-ng-bind-is-faster-than-expression-bind-and-one-time-bind/
  3. Disabled debug Info $compileProvider.debugInfoEnabled(false); https://docs.angularjs.org/guide/production#disabling-debug-data

参考资料:
https://docs.google.com/presentation/d/15XgHRI8Ng2MXKZqglzP3PugWFZmIDKOnlAXDGZW2Djg/edit#slide=id.g2a0ec7d53_00
https://www.ng-book.com/p/The-Digest-Loop-and-apply/

关于Promise的用法(基础篇)

一. $q的构成

  • defer
    • promise
      • then(successCallback, [errorCallback], [notifyCallback]):successCallback为完成promise的回调方法,errorCallback为失败时的回调方法,notifyCallback为通知时的回调方法
      • catch(errorCallback):promise.then(null, errorCallback)方法的简写
      • finally(callback, notifyCallback):无论promise被resolve还是reject,都会调用callback方法,但是该方法是没有参数的,如果是链式调用的话,会继续使用该promise被resolve或者reject所传入的值向后传递
    • resolve(value):完成promise调用这个方法,value会被当做参数传入对应的方法
    • reject(reason):失败时调用这个方法,reason会被当做参数传入对应的方法
    • notify(value):通知的时候调用,可以调用零次或者多次,必须在调用resolve或者reject之前调用
  • reject(reason):返回一个已经调用过将reason作为参数的reject方法的promise
  • when(value, [successCallback], [errorCallback], [progressCallback]):该方法返回新的promise,执行该promise的resolve方法,以value作为参数,并将 successCallback, errorCallback, progressCallback作为参数调用then方法作为返回值
  • resolve(value, [successCallback], [errorCallback], [progressCallback]):when方法的别名,为了和ES6的命名标尺一致
  • all(promises):该方法通过传入promises这个promise数组或者对象的集合,当全部的promise完成时,才会调用resolve方法,并将该promises数组或者对象的reslove结果包装成对应的数组或者对象作为参数;只要数组或者对象的一个promise失败,就会调用该reject方法,并将该promise的reson作为参数
  • race(promises):该方法通过传入promises这个promise数组或者对象的集合,只要数组或者对象的一个promise完成或者失败,就会调用resolve或者reject,并传入对应的参数

二. 用法

1.像ES6标准的Promise的用法一样的构造器风格:

  • $q(resolver):resolver格式为function(function, function),返回值为Promise;resolver的第一个参数是一个解决(resolve)Promise的方法,第二个参数是一个拒绝(reject)Promise的方法;
  • 例子:
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);
});    
  • 注意 :
    1. 该用法不支持progress/notify回调用法
    2. 在resolver这个构造方法里抛出的异常,并不会自动调用该Promise的reject方法

2. 传统的CommonJS-style用法:

  • 通过$q.defer()方法新建一个Defer对象,该对象带有promise属性,和resolve、reject和notify方法,然后在合适的时候调用这三个方法,就会调用promise的对应回调函数
  • 例子:
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);
});

注意 :

  • then、catch、finally方法不是必须在调用defer的resolve方法和reject方法之后才能调用(notifyCallback方法除外,该方法必须先绑定回调方法,才能通过notify方法调用,否则不会调用notifyCallback方法)
  • then、catch、finally方法一定是异步的,哪怕直接调用resolve方法或者reject方法
  • then、catch方法参数里的回调函数都只会被传入一个参数;finally方法的callback回调函数不会被传入参数,notifyCallback回调函数只会被传入一个参数
  • defer的resolve和reject方法一共只能被调用一次,之后的调用将不起作用
  • notify函数在调用resolve方法或者reject方法之前可以调用零次或者多次,之后的调用将不起作用

三.$q和Kris Kowal's Q的差异

  1. $q因为基于$rootScope.Scope作用域模型检查机制构建的,所以它可以更快的把解决或者拒绝的结果更新到你的models里,避免不必要的浏览器重绘(会导致UI闪烁)
  2. Q比$q拥有更多的功能特性,但带来的是代码字节数的增加。 $q很轻量级,但包含了一般异步任务所需的所有重要功能

关于$resource的使用

什么是 $resource ?

$resource 是基于 $http 基础之上的一个 AngularJS 的服务,特别擅长与 RESTful 进行交互。

什么是RESTful ?(摘自RESTful API编写指南)

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。

如何使用?(更多 https://docs.angularjs.org/api/ngResource)

  • 安装:
angular.module('app', ['ngResource']);
  • 一个 provide 和一个 serveice
    • $resourceProvider 用于配置 $resource 的默认行为, 例如支持的默认方法
    • $resource 用它来和 RESTful 进行交互
  • 使用
angular.module('app').service('UserResource', function($resource) {
     return $resource('/users/:id', {id: '@_id'}, {
         update: {
                method: 'PUT'
         }
      });
});
  • UserResource 会默认根据规律生成五个 RESTful 请求的方法在 UserResource 上
    qq20160621-0 2x
{ 
  '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);
});
  • 同样的 UserResource 的实例 也生成了以$开头的方法, $save 和 $delete
// 这里例子同样也省略了 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);
});
  • 再看这个例子中 $resource 的第三个参数, 自定义一个 update 方法
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);
});
  • 扩展 UserResource 的实例对象
 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;
});  
  • 当请求不符合 RESTful 时候, 通过第三个参数 进行重写
 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/

JavaScript中常见的日期计算

  • 加减年,月或日的计算
// 当前日期减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)

前端项目交接规范(draft)

前端项目交接规范

交接中关心的五大问题

  1. 如何进行开发?
  2. 如何发布?
  3. 有没有什么难点?
  4. 后端谁负责?
  5. 测试谁负责?

1.如何进行开发?

  • 技术栈部分 (框架,库,工具,项目结构,还有开发规范,注意事项等)
    • 框架和库
      • 例如 AngularJS 1,UI-Router 和 angular-utils 具体可以参考 package.json
    • 项目结构
      • 例如 目录结构列表,每个目录存放什么代码,如下方目录结构图所示,当然可以更详细,这里只是一个例子。
    • 开发规范
  • 业务部分(产品设计文档,UX 设计原型图,部分业务讲解)
    • 产品设计文档
      • 相关地址链接
    • UX 设计原型图
      • 相关地址链接
    • 部分业务讲解
      • 可以直接联系产品经理开会沟通
  • 工程化部分 (如何区分三种环境,本地如何mock,出了问题如何与QA或线上环境联调)
    • 打包
      • 例如请参考 package.json script
    • mock
      • 例如使用 json-server 进行 mock,mock方法或例子
    • 前后端联调
      • 例如通过 express 配置代理,如何配置
├── 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

2.如何发布?

  • 系统环境 (机器,系统信息,容器,nginx 配置等)
环境 机器 账号 密码 配置信息
dev 172.17.33.222 root ***** 操作系统,服务, nginx 配置,代码部署位置等信息
qa 172.17.33.223 root ***** 如上
stage 172.19.33.222 root ***** 如上
  • 发布 (QA,Dev,线上环境)
    • 发布方式: 例如如果是 jenkins,请告知方法,如果是其他方式,发布脚本或其他等信息交接。
    • 整个项目部署架构讲解,最好可以有架构图等相关信息。

3.有没有什么难点?

  • 业务中最难的部分
  • 技术上难点

4.后端谁负责?

  • 后端负责人
  • API DOC

5.测试谁负责?

  • 测试负责人
  • 测试用例和测试报告

ES 装饰器 (decorator) 在 AngularJS 1.x 中的使用

ES 装饰器在 AngularJS 1.x 中的使用

准备

关于 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

最后

装饰器特性不仅可以在不改变原有类或方法的前提下,增加新的功能和特性,另外还可以简化代码的写法,对于编码效率提升非常有用。

如何去掉chrome下select标签的圆角样式

  • 方法一:
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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.