这个时候,玄酱是不是应该说点什么...
新年到了,在这里小编祝大家新年快乐,鼠年大吉!春节,是我国最重要的传统节日,一提到春节,大家想到的就是,喜庆、欢乐和一年的丰收。接下来小编就带大家一起回顾一下金融行业热点事件。 2019即将到站!金融热点事件盘点: 阿里巴巴香港正式上市 11月26日,阿里巴巴集团阔别七年正式重返港股,股价也一路不断创新高。截至2019年12月24日收盘,阿里巴巴股价涨至210.40港元,市值4.5万亿港元,成功超越长期稳居港交所市值第一的腾讯控股,成为新任“市值第一”。 蚂蚁金服获香港金管局颁发的“虚拟银行”牌照 香港金管局于去年5月底发布了《虚拟银行的认可》指引,并提出了申请人需要满足“具备足够的财务、科技及其他相关资源;业务计划可信和可行,能提供新客户体验,并能促进普及金融和金融科技发展;有能力发展合适的信息科技平台;准备完备可迅速投入运营”等四项要求。从结果来看,蚂蚁金服凭借科技实力,从众多申请公司中脱颖而出。 科创板横空出世 2019年A股的头等大事是什么,毫无疑问是科创板的上市。从2018年11月5日到2019年7月22日,从宣布设立科创板并试点注册制到科创板正式开市,仅仅只用了259天,这个新兴市场带着支持科技创新产业的使命进入投资者眼中。 印度支付公司Paytm获10亿美元融资,蚂蚁金服、软银领投 2019年11月25日,印度数字支付巨头Paytm母公司One97 Communications宣布,公司已在新一轮融资中筹集了10亿美元资金,由软银集团和蚂蚁金服领投。本轮融资对One97的估值为160亿美元,使其成为亚洲估值最高的创业公司之一。 人民币汇率破“7” 2019年8月5日,人民币在岸、离岸汇率双双破“7”。央行盘中发声,称人民币资产的估值仍然偏低,稳定性相应更强,中国有望成为全球资金的“洼地”。人民银行有经验、有信心、有能力保持人民币汇率在合理均衡水平上基本稳定。 回顾完行业事件,金融行业是怎么上云的? 看金融行业是如何携手云计算变革大局,跟着小编往下看: 来看看金融行业企业的上云故事 【招商银行】招商银行信用卡中心智能外呼系统上云案例 我们在外呼及客户智能交互中使用阿里云产品。语音识别产品准确率达到业界领先水平,能很好的处理普通话和带口音普通话;语音合成产品拟人度非常高,音库也非常丰富。 【红岭创投】红岭创投应用及数据迁移上云案例 ”阿里云为红岭创投提供7*24不间断的护航保障,快速响应,助力业务系统和数据平滑上云,让业务系统平稳度过业务流量高峰。“ 【鹏元征信】鹏元征信混合云架构平滑上云案例 我们有大量需要和客户联系的业务,如何在保证和客户沟通质量的前提下,降低运营成本;如何采用智能化的手段加快人员培训,是卡中心要解决的业务难题。 【帝国金融】帝国金融平台安全迁移案例 作为一个金融机构,为客户提供稳定服务是我们的首要任务。阿里云的企业保障服务帮助我们顺畅的完成平台搬迁,每时每刻都在为我们的服务提供保障。 【众安保险】阿里云助力众安保险“双11”大促案例 阿里云护航小组对众安保险MaxCompute、ECS、RDS、安全等资源情况进行了整体评估,保障众安业务系统双十一平稳运行。 【盒子科技】盒子科技支付业务平滑迁移上云案例 历时1个月,盒子科技支付业务平滑迁移到阿里云,解决IT基础设施限制业务发展的难题。依托阿里云优质的BGP网络,盒子科技的商户支付体验大幅提升。 更多案例,请访问 云栖号-案例库 怎么上云?不同阶段的最佳实践指导你一步步上云! 初级阶段 Terraform应用 使用Terraform工具进行云上业务架构的应用实践和代码示例 服务器迁移 使用阿里云提供的迁移工具将物理服务器、虚拟机以及其他云平台云主机一站式地迁移到阿里云ECS用于企业上云,提高服务器迁移时的系统还原度,降低操作难度,提高迁移速度。 发展阶段 弹性裸金属自建ORACLE数据库单机版 使用弹性裸金属在云上自建ORACLE数据库,并对ORACLE宕机重新快速恢复进行了介绍,解决如何利用云上强劲资源,如神龙服务器、ESSD存储,支撑数据库高效稳健运行,如何快速备份和恢复数据库数据,保证云上数据的安全性。 自建K8S集群迁移ACK弹性裸金属集群 从线下IDC自建K8S零改造迁移云上ACK神龙集群,在微服务化改造之后,企业在享受K8S带来应用管理的便利的同时,存在硬件性能不足,本地扩展性差,容器容灾难,K8S管理复杂等问题。 创新阶段 GPU AI模型训练 使用阿里云基础设施搭建AI训练的容器环境,利用Perseus加速框架进行AI模型训练加速,适用于AI图片训练场景,使用CPFS/NAS作为共享存储,利用容器服务Kubernetes版管理GPU云服务器集群进行图片AI训练。 抢占式ECS搭建离线大数据分析集群 使用阿里云抢占式ECS实例以及OSS等云上产品构建低成本弹性大数据分析系统 更多实践,请访问 云栖号-最佳实践库 涉及到云产品不了解?小编带你云产品快速入门! 快速了解 ECS 资源计费规则,最省钱的使用ECS! 介绍云服务器 ECS 的计费资源类型、各种资源类型的计费方式对比和购买资源时的支付方式,便于您更合理地使用ECS。 SLB 一站式学习 负载均衡 SLB(Server Load Balancer)是将访问流量根据转发策略分发到后端多台云服务器(ECS 实例)的流量分发控制服务。 大数据计算服务 · MaxCompute MaxCompute(原ODPS)是一项大数据计算服务,它能提供快速、完全托管的PB级数据仓库解决方案,使您可以经济并高效的分析处理海量数据。 云数据库 RDS MySQL 版 MySQL 是全球最受欢迎的开源数据库之一,作为开源软件组合 LAMP(Linux + Apache + MySQL + Perl/PHP/Python) 中的重要一环,广泛应用于各类应用场景 对象存储 OSS 比传统存储成本下降25%~75%的强安全企业级存储 更多云产品入门,请访问 云栖号-快速入门 <完结> 还想了解更多不同行业,不同产品的云资讯,云产品入门,上云案例和实践,请访问 云栖号
前言 3 个月前,微信小程序推出了 web-view 组件引发了一波小高潮,笔者所在的大前端团队写过一篇浅析,详情可见:浅谈微信小程序前端生态。 我们曾大胆猜想,这一功能,可能直接导致小程序数量增长迎来一波高峰。 毕竟磨刀霍霍却一直资源不足的团队应该不少,现在可以把已有 H5 应用嵌入到小程序 web-view 容器中,以最低的开发成本坐蹭微信流量红利,何乐而不为呢? 我们也曾畅想也许“小程序页面+ web 页”混合开发(甚至 web 更重)会成为以后的新趋势。 2M 代码限制(如今已更新至 4M)使得像“转转官方”这样功能繁复的小程序必须考虑引入 web 内容,再有就是小程序审核发布机制使得它终究不能像 web 一样迭代迅速。 正好笔者所在的业务线,存在已有的 H5 应用却无对应小程序的情况。我们在开发对应小程序时也算收获了不少经验(踩了不少坑),分享给有小程序需求的朋友们~ 最大的坑:不支持服务通知 是的,web-view 不支持推送服务通知(或称模板消息)。 如图所示,类似订阅号在对话列表的模式 为什么能称为最大的坑?我们先了解一下服务通知,以下引用全部来自微信官方小程序文档。 基于微信的通知渠道,我们为开发者提供了可以高效触达用户的模板消息能力,以便实现服务闭环并提供更佳的体验。 看起来很厉害,如果咱们的小程序没这个功能会怎样? “用完即走”是小程序的口号,没有服务通知代表失去了高效触达(召回)用户的能力,然后用户就再也回不来了,促活和留存怎么办? 很多功能不是像订阅号里看篇文章一样,几分钟就能搞定的,比如绝大部分电商的行为:从搜索、浏览比价、跟卖家交流,到加入购物车仅仅是走完了不到一半的生命周期;然后才是下单支付评价,还不包括推荐复购取消退款等等,没个15-30分钟哪里够。然而,没有用户会一直开着某个小程序,别人还要切出去聊天刷朋友圈呢。没有了化同步为异步的能力,绝大部分产品逻辑如何实现服务闭环? 一篇教你突破小程序模板消息推送限制的文章中,也总结了服务通知的「多、快、好、省」等特点。这些先不展开,我们还能看到: 该小程序近 30 天访问来源数据显示,有 20% 左右的用户通过模板消息进入小打卡,在各种来源中排名第 3 位(如果分母去掉新用户的来源,比率和排名会更高); 况且,用户基本都不会关闭微信的消息推送,相较 App 的推送和短信推送来说,小程序的推送触达率会高很多。 so,没有哪个(正经的)小程序会不支持服务通知(流氓些的比如拼多多,看了个商品能给你连着推 N 条)。试想一下没有推送通知的 APP,你的产品、运营和老板们会同意么? 为什么不支持 然而,为什么 web-view 不支持服务通知?哪里坑了?还请继续看微信官方文档里的定义。 下发条件:用户本人在微信体系内与页面有交互行为后触发 总结起来就是,支付3条、提交表单(该表单需声明为要发模板消息)1条,7天有效。 首先,这里区别了支付和提交表单两种行为,要分不同的情况上报,开始了看到没… 然后,web-view 不支持支付能力(其 JSAPI 能力不包含微信支付),这个在微信的文档里没有显式的声明,不过能在微信的 web-view 问题汇总中看到,这个也挺坑的… 其实,支付行为对小程序本身而言只是极少数的交互,大多数小程序甚至不含支付。所以我们基本还得靠表单,可问题就出在这:小程序的 web-view 和表单(form 组件) 不兼容!!! PS:我们先区分下 form 组件,它跟 web-view 内网页的表单(form 标签)没有任何关系。 PS:RN 和 Weex 也没有 form 组件。为什么笔者一看到 form 就想到如下的图? 1999年12月发布的 HTML 4.01 Specification 就支持了 form,自 AJAX 在2005年风靡世界后,跨域、文件上传都有了 form 之外的解决方案,谁没事还用这玩意? 先不吐槽微信文档里 form 组件的定义是有多简陋,再看其 web-view 组件的定义~ web-view 组件是一个可以用来承载网页的容器,会自动铺满整个小程序页面。 何止铺满,尝试把 web-view 放在 form 组件内,form 组件都铺没了。so,自动铺满 = 页面独占 = 所有其他元素都被直接覆盖…好吧,别人在文档最下方的 Bug & Tip 里写了行小字~ 综上,web-view 跟服务通知已绝缘。so,小程序里网页的交互行为不算在微信体系内!!? 我不禁回想起 Google 之前推出的 PWA(Progressive Web App),在这又有那么一丝神似。 两者同是基于 Web 的技术,开发出(或许)可替代 APP 的伪原生应用; PWA 的推送通知因其 API 太超前和一些不知名的服务被和谐用不了(你懂的); 小程序的服务通知嘛,你要想用 web-view 做壳就发布上线也用不了。 扯远了点,但大家都说:PWA 是引领下一代潮流的 Web 技术超集,而小程序是对 Web 技术进行修(阉割)补(Hack)的(黑)魔法… 不做展开,欢迎移步:如何客观地评价「小程序」的体验? Web 在继续离我们远去 那怎么办? 由于笔者团队的业务对服务通知与支付有大量依赖,那么我们就要彻底放弃 web-view,把之前的 H5 应用全部重写至原生小程序了么?显然不是。 正如前文所说,支付仅是电商诸多环节中的一环,主要在商品 or 订单详情页(这些必须重写)。关于服务通知,通过几个重写后的原生小程序页,也能收集到足够的 form。 具体如何重写,可参考我们之前的像 Vue 一样写微信小程序-深入研究 wepy 框架。虽然 wepy 框架尝试从语法层面尽可能做到与 vue 技术栈的 web 项目同构,但是两端特性 API 兼容依旧是个棘手问题,而且毕竟两者的语法糖和生命周期函数都不一样。这里还有不少人工成本及学习与适应的过程,贴一个例子: 基于 wepy,模板部分就是个替换+适配的活,JS 麻烦些。离同构差距不小,美团您的 mpVue 呢? 具体如何收集,可参考教你突破小程序模板消息的推送限制这篇文章的做法。也如文章所说,一般大家都会在小程序页中,**把所有能点击的地方都换成 form。**如果觉得不够简单粗暴,也能在 form 中多层嵌套 form,然后让点击事件冒泡的方式来做!(谁让此 form 非彼 form 呢…够魔法么…) 剩下的业务,理论上都可以用 web-view 来实现!!!运营活动页就不说了,开放 web-view 能力最大的优势就是方便了这类需求。小程序首页,甚至配置了 tabBar 的小程序页都可以。因为我们还发现一个神奇的 feature… 大概是用了原生的 UITabBar,web-view 和 tabBar 能共存 总结 亏了 web-view 组件的及时推出,我们只需重写部分详情页和其依赖的组件,最后复盘一下。 定位:小程序的 web-view 就像是 Hybrid 客户端嵌 H5 页一样,需要一些基础能力的时候,比如支付、服务通知(IM 和召回等场景)等等,最好使用原生小程序; 兼容性:这个无须过多担心,最新的基础库统计数据,1.6.4+ 的覆盖率已达 95% 以上; 数据通信:小程序 => web-view 可以在 url 中用 search、hash 的方式,web-view => 小程序可用 bindmessage,一般用来解决分享信息传递的问题; 登录:a. web-view 内走微信授权,b. 小程序登录后再进入 web-view,并把相关 cookie 通过 url 传递给 web-view。 其它 feature(欢迎讨论和补充): web-view 跟小程序是独立的两个环境,数据完全不通,包括 cookie、session、localStorage 等等; 但小程序内嵌 web-view 跟微信内置浏览器是一套环境,也就是说你在 web-view 里面留下的以上痕迹,到微信里内置浏览器打开也有; 在两种环境下,不太容易区分到底是什么环境,小程序官方给的判断方法是 window.__wxjs_environment === 'miniprogram',但是在 web-view 进入第二页时候,安卓机下这个变量就 undefined 了。 其它的坑(常见错误): 打开的域名没有在小程序管理后台设置业务域名(注意是业务域名,不是服务器域名); 打开的页面 302 过去的地址也必须设置过业务域名; 页面可以包含 iframe,但是 iframe 的地址必须为业务域名; 打开的页面必须为 https 服务; 开发者自己检查自己的 https 服务是否正常,测试方法:普通浏览器打开对应的地址; 等等,详情请移步 web-view 问题汇总(https://developers.weixin.qq.com/blogdetail?action=get_post_info&lang=zh_CN&token=585555149&docid=ebfd9e5ec9986b4f23c41f8d8bbf2730)查阅,或在该帖子里留言。 本文作者:大转转FE 本文发布时间:2018年03月02日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
简要介绍:React中的render存在着不必要的渲染,可以通过Puer Render(PureComponent)来减少渲染的次数,但是Puer Render只是进行浅比较,无法实现深层对象上的性能优化。Pure Render结合Immutable可以减少渲染次数。 1 . React中的render 仅通过React中的render,存在着不必要的渲染,这种不必要的渲染分为两大类。 (1)自身的state没有改变 在React的render中,只要发生setState行为,就会去重新渲染,即使setState的属性前后并没有发生变化,比如: class TestComponent extends React.Component{ constructor(props){ super(props); this.state={ count:0 } } componentDidMount(){ let self=this; setTimeout(function(){ self.setState({ count:0 }) },1000) } componentDidUpdate(){ console.log('组件更新了'); } render(){ return <div>1</div> } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 在这个组件中,我们setState的值在前后并没有发生改变,但是调用此组件会输出: //组件更新了 1 说明只要setState发生了,即使值在前后没有发生变化,组件也会重新render。 (2)父组件传递的props会引起所有子组件render 父组件中的值传递给子组件,即使某些子组件中不需要重新渲染,也会重新render。举例来说,父组件为Father: class Father extends React.Component{ constructor(props){ super(props); this.state={ obj:{ title:'test immutable', childZero:{ name:'childZero', age:0 }, childOne:{ name:'childOne', age:1 }, childTwo:{ name:'childTwo', age:2 } } } } componentDidMount(){ let self=this; let {obj}=this.state; setTimeout(function(){ self.setState({ obj:obj }) },1000) } render(){ const {obj}=this.state; return <div> <ChildZero obj={obj}/> <ChildOne obj={obj}/> <ChildTwo obj={obj}/> </div> } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 父组件有3个子组件: class ChildZero extends React.Component{ constructor(props){ super(props); } componentDidUpdate(){ console.log('childZero触发了更新') } render(){ return <div>3</div> } } class ChildOne extends React.Component{ constructor(props){ super(props); } componentDidUpdate(){ console.log('childOne触发了更新') } render(){ return <div>3</div> } } class ChildTwo extends React.Component{ constructor(props){ super(props); } componentDidUpdate(){ console.log('childTwo触发了更新') } render(){ return <div>3</div> } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 我们在父组件的componentDidMout方法中,setState然后观察子组件的更新情况,发现所有的子组件都会更新,具体输出为: //childZero触发了更新 //childOne触发了更新 //childTwo触发了更新 1 2 3 2 . Pure Render可以减少浅层对象上不必要的更新 通过定义组件为Pure Render可以通过浅层比较,减少不必要的更新。我们通过使用PureComponent。同样的我们以1(1)中的为例: class TestComponent extends React.PureComponent{ constructor(props){ super(props); this.state={ count:0 } } componentDidMount(){ let self=this; setTimeout(function(){ self.setState({ count:0 }) },1000) } componentDidUpdate(){ console.log('组件更新了'); } render(){ return <div>1</div> } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 通过PureComponent来代替Component,此时如果仅仅是浅层对象属性,当setState前后属性不变时,那么就不会有不必要的渲染。但是对于深层对象而言,pure Render无法实现。 3 .通过Immutable实现深层对象的性能优化 Immutable实现了js中的不可变数据结构,immutable具有不可变性,持久性等特点,通过数据共享的方式,修改相应的属性实现深度克隆的过程只会影响父类属性。 通过immutablejs可以方便进行深层对象的“相等”判断。在React的性能优化中,在生命周期函数shouldComponetUpdate中判断在是否需要更新,进行前后props和前后state的深层比较。 shouldComponentUpdate(nextProps,nextState){ //进行深层判断使用immutable const thisProps=this.props; if(Object.keys(thisProps).length!==Object.keys(nextProps).length){ return true; } for(const key in nextProps){ console.log(is(thisProps[key],nextProps[key])); if(!is(thisProps[key],nextProps[key])){ return true; } } return false; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 如果返回true,那么会进行render,如果返回false,就不会render,从而可以控制深层对象是否需要重新render,实现了性能的优化。 这里使用immutable,主要是因为其拥有如下特点: I)快,在深层对比对象(Map)或者数组(List)是否相同,比深层克隆式的比较快 II)安全,指的是对所有immutable的增删改查,都是增量,不会使得原始数据丢失 3.immutable的缺点 使用immutalbe也存在了一些缺点: (1)immutablejs源文件较大 (2)具有很强的侵入性 本文作者:小小小小小亮 本文发布时间:2018年04月20日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
使用xpath实现document.querySelector样式选择器进行html解析(一):将html转成xml 使用xpath实现document.querySelector样式选择器进行html解析(二):扩展一下xpath以便支持正则 使用xpath实现document.querySelector样式选择器进行html解析(三):实现样式选择器 使用xpath实现document.querySelector样式选择器进行html解析(四):将选择结果封装进行输出 ----------------------------------------------------------------- 文盲做采集工作也做了有些年头了,一直以来,对采集到的内容都是用正则进行数据提取的,但是使用的时间越长,越觉得使用正则很麻烦。 第一,了解正则的人在行业内真的是少数,而且复杂的业务逻辑写出来的正则,隔段时间,自己都看不懂了。。。 第二,正则对文档的格式还是有一定要求的,比如说如何提取一个完整的闭合html标签,这个正则就很复杂,用到层深计算了,如果一旦html内出现了非法内容,那就是一场灾难,正则会整个卡死。。。。。 所以,文盲老顾一直想找一个htmlparser类型的东西来代替正则,恩,比如说Winista.HtmlParser啦、HtmlAgilityPack啦 但是,这里要说一个但是,这些第三方的东西并不符合咱们的日常使用习惯,什么是日常使用习惯呢?当然是css选择器啦!不管是按id找啦,按样式找啦,还是按标签找啦,这些方式我相信大部分开发人员都能很快上手。 于是,按照这个目的触发,那么文盲老顾找到的第三方工具都需要帕斯掉了,因为他们不支持,或仅支持部分需求,恩。。。。hmmmmmmmm,也许是文盲老顾没弄明白这些东西到底怎么来实现这个css选择器方式的内容查找,总之,文盲决定自己搞一个htmlparser了 废话说到这里,下边开始编写文盲版的htmlparser ----------------------------------------------------------------- 在开始编写之前整理一下思路 首先,html是一个格式很随意的文本文档,不能强求它一定符合xhtml规范 第二,在xml中,可以通过xpath来实现诸如id、样式、文字包含等css1.0、2.0、3.0各种规范的选择器(虽然可能比较复杂,但文盲老顾在2014年的确已经实现了很多内容,css伪类没做实现,有需要的话,各位同学可以在本文后留言共同讨论) 第三,html无法直接转成xml,所以我们需要对html进行一些处理,使其能正常的转换到xml格式 最后,定义一个通用方法,来实现css选择器方式选取节点并得到想提取的信息 根据这个思路,第一步应该是先把html转成xml,好了,开始做第一步工作 ----------------------------------------------------------------- 首先先定义一个类,用以加载html内容 public class HtmlObject { private string _html = string.Empty; private List<string> _tags = new List<string>(); private List<string> _self = new List<string>(); private XmlDocument _xml = null; public string Html { get { return _html; } } public XmlDocument Xml { get { return _xml; } } public HtmlObject() { InitDefine(); } public HtmlObject(string html) { _html = html; InitDefine(); InitHtml(); } public void Load(string file) { LoadHtml(FileHelper.FileToString(file)); } public void LoadHtml(string html) { _html = html; InitHtml(); } public void LoadUrl(string url) { Ajax ajax = new Ajax(); ajax.AppendCss = false; ajax.AddFullPath = true; ajax.AutoSave = false; ajax.AutoUpdate = true; LoadHtml(ajax.Http(url)); } private void InitDefine() { // 声明自闭合标签 _self.AddRange(new string[] { "img", "br", "hr", "base", "meta", "link", "area" }); } private void InitHtml() { _tags = new List<string>(); XmlDocument xml = new XmlDocument(); xml.LoadXml("<r />"); MatchCollection mc = Regex.Matches(_html, @"<!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?>|<([%\?])[\s\S]*?\2>|<!--[\s\S]*?-->|<(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?>|<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>|[^<]+(?=<|$)", RegexOptions.IgnoreCase); XmlNode node = xml.DocumentElement; for (int i = 0; i < mc.Count; i++) { ParseNode(ref node, mc[i].Value); } _xml = xml; } private void ParseNode(ref XmlNode node, string value) { // 如果是标签 if (Regex.IsMatch(value, @"^<")) { XmlNode xn = null; string name = string.Empty; //如果是样式或脚本 if (Regex.IsMatch(value, @"^<(script|style)(?!\w)", RegexOptions.IgnoreCase)) { xn = XMLExpand.AppendNode(node, Regex.Match(value, @"(?<=^<)(style|script)", RegexOptions.IgnoreCase).Value.ToLower()); xn.AppendChild(xn.OwnerDocument.CreateCDataSection(Regex.Match(value, @"(?<=^<(style|script)[^<>]*?>)[\s\S]*?(?=</\1[^<>]*?>$)", RegexOptions.IgnoreCase).Value)); } // 注释或其他程序语言标签 if (Regex.IsMatch(value, @"^<[!%\?]")) { node.AppendChild(node.OwnerDocument.CreateCDataSection(value)); //XMLExpand.AppendNode(node, "REM").InnerText = value; } // 正常标签 if (Regex.IsMatch(value, @"^<(?!(script|style))\w+")) { name = Regex.Match(value, @"(?<=^<)\w+", RegexOptions.IgnoreCase).Value.ToLower(); // 如果不是自闭合标签则将当前增加的标签放入到待闭合标签中 if (!Regex.IsMatch(value, @"/>$") && !_self.Contains(name)) { _tags.Add(name); } xn = XMLExpand.AppendNode(node, name); node = xn; } // 正常标签结束 if (Regex.IsMatch(value, @"^</")) { name = Regex.Match(value, @"(?<=^</)\w+", RegexOptions.IgnoreCase).Value.ToLower(); if (node.Name == name) { _tags.RemoveAt(_tags.Count - 1); node = node.ParentNode; } else { // 如果待闭合标签中包含对应标签则关闭对应标签,否则忽视 if (_tags.Contains(name)) { for (int i = _tags.Count; i > 0; i--) { if (_tags[i - 1] == name) { _tags.RemoveRange(i - 1, _tags.Count - i + 1); break; } } while (node.Name != name) { node = node.ParentNode; } } } } if (Regex.IsMatch(value, @"^<(?![/!%\?])") && xn != null) { Match m = Regex.Match(value, @"^<[^<>]*?>", RegexOptions.IgnoreCase); ParseAttribute(xn, m); } // 如果是自闭合标签 if (xn != null && xn == node && !string.IsNullOrEmpty(name) && (Regex.IsMatch(value, @"/>$") || _self.Contains(name))) { node = node.ParentNode; } } else { // 纯文本,将文本内容作为节点文本内容 node.AppendChild(node.OwnerDocument.CreateCDataSection(value)); //XMLExpand.AppendNode(node, "TEXT").InnerText = value; } } private void ParseAttribute(XmlNode node, Match match) { string html = match.Value; MatchCollection mc = Regex.Matches(html, @"(?<=[\r\n\s\t])(\w+)[\r\n\s\t]*=[\r\n\s\t]*((['""])([^'""]*)\3|[^\s\r\t\n>]+)", RegexOptions.IgnoreCase); for (int i = 0; i < mc.Count; i++) { XMLExpand.SetAttribute(node, mc[i].Groups[1].Value.ToLower(), string.IsNullOrEmpty(mc[i].Groups[4].Value) ? (Regex.IsMatch(mc[i].Groups[2].Value, @"^(['""])\1$") ? "" : mc[i].Groups[2].Value) : mc[i].Groups[4].Value); } } } 恩。。。。。。反正就是这么个代码,呵呵 构造函数有两个,一个是带html文本的,一个是不带的 加载文档则有三个方法,一个是直接加载html文本的LoadHtml方法,一个是加载本地文件的Load方法,一个是加载网址获得文档LoadUrl,Hmmmmmmmm,LoadUrl就忽略好了,Load方法也忽略好了。。。。我的代码中用到的类可以自己去实现后替换,反正意思一样。。。。 在这个类中,我声明了两个私有数组,_tags和_self,_tags是用来存储解析过程中,未闭合的标签,而_self则保存无需闭合的标签枚举 然后,就是InitHtml这个核心方法了。。。。。 对html文档,我使用正则将其切分成一个数组,这个正则大家也可以帮我看看有没有需要调整的地方 <!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?> | <([%\?])[\s\S]*?\2> | <!--[\s\S]*?--> | <(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?> | <(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?> | [^<]+(?=<|$) 我是这么想的,html中显示的文本是在标签之外的,恩,用最后一个正则片段实现,也就是[^<]+(?=<|$)部分 然后是正常的标签部分,不管是结束标签还是闭合标签还是其他什么html不识别的标签,只要是标签格式,我都拿出来当标签处理,恩,用倒数第二个正则片段实现,也就是<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>部分 但是,在实际使用过程中,有些标签中会包含一些特定文本,比如样式、比如脚本,那么把样式和脚本作为特定标签处理,于是产生了倒数第三个正则片段。。。恩,主要是为了在脚本片段中允许出现小于号,还有</script>这样的常量,所以这个正则稍微麻烦了些 再然后,发现还有注释内容也很蛋疼。。。。例如<!--这里是包含标签的注释内容<a href="">链接</a>-->。。。。没办法,继续加特例。。。。于是倒数第四个正则片段也出现了。。。。。 哦,写到这里,发现还会可能出现其他脚本语言片段。。。例如<% %>啦,例如<? ?>啦。。。得,再来搞个正则用来把它也摘出来 最后。。。。还有html声明。。。。也就是<!doctype html>这样的html代码片段也得特殊声明下。。。。。。好了,第一步我们完成了。。。。。把html用正则拆开了。。。。 MatchCollection mc = Regex.Matches(_html, @"<!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?>|<([%\?])[\s\S]*?\2>|<!--[\s\S]*?-->|<(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?>|<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>|[^<]+(?=<|$)", RegexOptions.IgnoreCase); 说真的,如果这个正则还有其他文盲没有考虑到的情况,请在本文后留言,文盲会尽快测试,或者,同学们要是发现使用这个正则拆分html的时候出现内容丢失或者拆分结果不符合预期的时候,也请留言,并将html片段贴出来 恩。。。。。。第一步完成了,就继续下一步,解析节点。。。,也就是ParseNode方法了 解析节点的思路也比较简单,如果是文本,则扔个CDataSection节点到xml里,如果是标签,则按照标签格式扔不同的节点到xml里,如果是非闭合标签,则当前标签修正为新增标签,如果是闭合标签,则当前标签修正为对应的开始标签的父级,如果新增了标签,顺便把新增标签的属性也解析一下,恩,也就是ParseAttribute 不知道会不会有其他异常,也请大家帮忙测试 好了,第一阶段完成,可以把Html转成xml了,实现选择的的内容,我们下次再说 本文作者:文盲老顾 本文发布时间:2018年06月29日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
使用xpath实现document.querySelector样式选择器进行html解析(一):将html转成xml 使用xpath实现document.querySelector样式选择器进行html解析(二):扩展一下xpath以便支持正则 使用xpath实现document.querySelector样式选择器进行html解析(三):实现样式选择器 使用xpath实现document.querySelector样式选择器进行html解析(四):将选择结果封装进行输出 ----------------------------------------------------------------- 好了,我们继续下一步,准备实现querySelector。。。。。呃。。。。问问同学们,对样式选择器有多大了解,比如 “#main,div .category,div>span.active ~ *”,这个内容都选择了哪些东西?嗯,扔个html片段上来,然后不要着急向后看,先自己看看能得到什么结果,再和文盲老顾的答案对照一下,看看你的基础知识是否掌握的很牢固,嘿嘿 <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="Generator" content="EditPlus®"> <meta name="Author" content=""> <meta name="Keywords" content=""> <meta name="Description" content=""> <title>Document</title> </head> <body> <div class="header"> <div class="category"> <span>页面顶部分类列表</span> <span>页面顶部分类列表</span> <span>页面顶部分类列表</span> <span class="active">页面顶部分类列表</span> <span>页面顶部分类列表</span> <span>页面顶部分类列表</span> <span>页面顶部分类列表</span> <span>页面顶部分类列表</span> </div> </div> <div class="nav category"> <ul id="main"> <span class="active">导航1</span> <span>导航2</span> <span>导航3</span> <span>导航4</span> <span>导航5</span> </ul> </div> </body> </html> ----------------------------------------------------------------- <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="Generator" content="EditPlus®"> <meta name="Author" content=""> <meta name="Keywords" content=""> <meta name="Description" content=""> <title>Document</title> </head> <body> <div class="header"> <div class="category" title="div .category 选中我啦"> <span>页面顶部分类列表</span> <span>页面顶部分类列表</span> <span>页面顶部分类列表</span> <span class="active">页面顶部分类列表</span> <span title="div>span.active ~ * 选中我啦">页面顶部分类列表</span> <span title="div>span.active ~ * 选中我啦">页面顶部分类列表</span> <span title="div>span.active ~ * 选中我啦">页面顶部分类列表</span> <span title="div>span.active ~ * 选中我啦">页面顶部分类列表</span> </div> </div> <div class="nav category"> <ul id="main" title="#main 选中我啦"> <span class="active">导航1</span> <span>导航2</span> <span>导航3</span> <span>导航4</span> <span>导航5</span> </ul> </div> </body> </html> 不玩了,看看上边的选择器到底选中了什么,首先,需要把逗号作为分隔符,也就是说,上边的选择器被分成了三个单独的选择器,他们的关系是或的关系,最后的结果是并集 #main div .category div>span.active ~ * 嗯。。。。然后。。。说不清楚,先说处理方式。。。。以空白符、大于号、加号、波浪线作为分隔符,将样式进行再次拆分。。。。这个分别是继承选择器、子选择器、相邻选择器和通用选择器。。。。这个里边继承选择器和子选择器很容易实现,分别是//*[name()='div']//*[regex:ismatch('@class','(?<!\w)category(?!\w)')]和//*[name()='div']/*[name()='span'],至于相邻选择器和通用选择器。。。。。稍后再说,这个比较复杂 (╯‵□′)╯︵┻━┻ 还有一个需要注意的地方,上边的选择器里,span.active中间没有空格哦,这个也是在解析时需要处理的地方 经过两次切分了,现在的选择器有哪些了呢 #main div.category div span.active * 嗯,span.active也需要拆,拆成两个样式,但他们的关系是与的关系,好在这个选择器可以写到同一个xpath的条件里 然后,还需要实现诸如[att=value]啦,:first-child伪类啦,:nth-of-type伪类啦,当然这个看自己需要,反正文盲是没去实现伪类,呵呵,原因很直接:采集数据提取一般用不到伪类 ----------------------------------------------------------------- 前边都是废话,Hahahha,开始正式贴代码了,嗯,这个代码就不再进行解释了,如果使用过程中出现了问题,就直接在本文后留言吧,文盲老顾会努力维护这个小程序的 首先,建立一个QuerySelector方法 public void QuerySelector(string selection) { _result = new List<XmlNode>(); _count = 0; string xpath = CssParser.ParseCSS(selection); try { XmlNodeList xnl = _xml.SelectNodes(xpath, XMLExpand.XPathExpand); if (xnl != null) { _count = xnl.Count; for (int i = 0; i < xnl.Count; i++) { _result.Add(xnl[i]); } } } catch (Exception ex) { throw ex; } } 其中的核心代码只有两句,第一句是将样式转成xpath,即CssParser.ParseCSS,第二句是使用带有我们xpath扩展的方式选取节点,即_xml.SelectNodes(xpath,XMLExpand.XPathExpand) 为什么使用xpath来实现样式选择呢,因为xml是一个序列化的文档,且xpath中具有多种定位方式,比如节点定位、属性定位、轴定位等等,而css选择器定位呢,基本上就只使用了节点定位,属性定位(类选择器和ID选择器都是属性定位),伪类里才会用到轴定位,比如:first-child,那么,用xpath来实现样式选择器就变得可行了 现在继续,把css转xpath的过程按照上边我们分析的过程一步一步实现,首先是按逗号切分 public static string ParseCSS(string selection) { string result = string.Empty; // 切分逗号,每个逗号为一个单独的选择器,多个选择之间为或的关系 string[] csses = selection.Split(new string[] { "," }, StringSplitOptions.None); for (int i = 0; i < csses.Length; i++) { result += (string.IsNullOrEmpty(result) ? "" : "|") + ParseCssLevel(csses[i].Trim()); } return result; } 再然后,按照继承(层级)方式切分 private static string ParseCssLevel(string css) { string result = string.Empty; #region // 切分样式,用来分辨选择器类型:继承,子,相邻,通用 // 空格为继承,可跳跃节点 // > 为子,不可跳跃节点 // + 为相邻,为同级节点的下一个兄弟节点 // ~ 为通用,为同级节点的所有后边的兄弟节点 #endregion MatchCollection mc = Regex.Matches(css, @"([~>\+\s]*)([^\s~>\+]+)", RegexOptions.IgnoreCase); for (int i = 0; i < mc.Count; i++) { string tp = mc[i].Groups[1].Value.Trim(); string cssparser = ParseCssClass(mc[i].Groups[2].Value.Trim()); //string pre = new Regex(@"(?<=/)([^/]+$)", RegexOptions.IgnoreCase).Match(xpath).Value; switch (tp) { case "": // 继承 CSS1.0 result += (string.IsNullOrEmpty(result) ? "" : "//") + cssparser; break; case "~": // 通用 CSS3.0 // "preceding-sibling::[$pre]" result = Regex.Replace(result, @"(?<!/)[/]+([^/]+)$", (Regex.IsMatch(cssparser, @"[\]]$") ? Regex.Replace(cssparser, @"[\]]$", " and preceding-sibling::$1]", RegexOptions.IgnoreCase) : cssparser + "[preceding-sibling::$1]"), RegexOptions.IgnoreCase); break; case ">": // 子 CSS2.0 result += (string.IsNullOrEmpty(result) ? "" : "/") + cssparser; break; case "+": // 相邻 CSS2.0 // "count(preceding-sibling::[$pre]/preceding-sibling::*)+1=count(self::node()/preceding-sibling::*)" result = Regex.Replace(result, @"(?<!/)[/]+([^/]+)$", (Regex.IsMatch(cssparser, @"[\]]$") ? Regex.Replace(cssparser, @"(?=[\]]$)", " and count(preceding-sibling::$1/preceding-sibling::*)+1=count(self::node()/preceding-sibling::*)") : "[count(preceding-sibling::$1/preceding-sibling::*)+1=count(self::node()/preceding-sibling::*)]"), RegexOptions.IgnoreCase); break; default: throw new Exception("未知的选择器类型"); } } return result; } 最后,实现样式选择器最终定位 private static string ParseCssClass(string css) { // 伪类除contains外不进行解析,基本用不到 // 对多值进行匹配,推荐使用*=运算,可直接指定为正则表达式 if (css == "*") { return "//*"; } string result = "//*["; // 切分独立样式选择器 MatchCollection mc = Regex.Matches(css, @"([\.#])?([\w\*]+)((\[[^\[\]]+\]|[^\.#])*)", RegexOptions.IgnoreCase); for (int i = 0; i < mc.Count; i++) { if (i > 0) { result += " and "; } string c = mc[i].Groups[1].Value.Trim(); string n = mc[i].Groups[2].Value; string p = mc[i].Groups[3].Value; if (n != "*") { switch (c) { case "": result += "name()='" + n + "'"; break; case ".": result += "regex:ismatch('@class','(?<!\\w)" + n + "(?!\\w)')";//contains(@class,'" + n + "') and break; case "#": result += "@id='" + n + "'"; break; } } if (!string.IsNullOrEmpty(p)) { if (n != "*") { result += " and "; } MatchCollection condition = Regex.Matches(p, @"(?<=\[)[^\[\]]+(?=\])|(?<=(:)([^:\(]+)\()[^\(\)]+(?=\))", RegexOptions.IgnoreCase); for (int j = 0; j < condition.Count; j++) { if (j > 0) { result += " and "; } if (condition[j].Groups[1].Value == ":") { // 此处实现伪类选择器 string cl = condition[j].Groups[2].Value; switch (cl) { case "contains": result += "regex:ismatch('.','" + condition[j].Value + "')"; break; default: throw new Exception("不支持的伪类选择器"); } } else { Match m = Regex.Match(condition[j].Value, @"([^=!~\^\$\*\|]+)(?:([=!~\^\$\*\|]+)(['""]?)([^\[\]]*)\3)?", RegexOptions.IgnoreCase); if (m.Success) { string att = m.Groups[1].Value; string opr = m.Groups[2].Value; string val = m.Groups[4].Value; switch (opr) { case "": // 包含指定属性 result += "@" + att; break; case "=": // 指定属性完全等于指定值 result += "@" + att + "='" + val + "'"; break; case "!=": // 指定属性不完全等于指定值 result += "@" + att + "!='" + val + "'"; break; case "^=": // 指定属性以指定字符开头(指定字符后可跟任意字符) result += "regex:ismatch('@" + att + "','^" + val + "')"; break; case "$=": // 指定属性以指定字符结尾(指定字符前可跟任意字符) result += "regex:ismatch('@" + att + "','" + val + "$')"; break; case "*=": // 指定属性中任意位置包含指定的字符串 result += "regex:ismatch('@" + att + "','" + val + "')"; break; case "~=": // 指定属性中任意单一值等于指定值 result += "regex:ismatch('@" + att + "','(?<!\\w)" + val + "(?!\\w)')"; break; case "|=": // 指定属性中,以指定值开头,后边可跟其他值或- result += "regex:ismatch('@" + att + "','^" + val + "(?=[\\s-]|$)')"; break; } } } } } } result += "]"; return result; } Hmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm,真累 那么现在我们再定位元素的时候就非常简单了,直接使用QuerySelector指令即可,因为html文档已经在类里加载进来了 如果有疑问,还请各位多多留言,共同进步 本文作者:文盲老顾 本文发布时间:2018年06月30日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
使用xpath实现document.querySelector样式选择器进行html解析(一):将html转成xml 使用xpath实现document.querySelector样式选择器进行html解析(二):扩展一下xpath以便支持正则 使用xpath实现document.querySelector样式选择器进行html解析(三):实现样式选择器 使用xpath实现document.querySelector样式选择器进行html解析(四):将选择结果封装进行输出 ----------------------------------------------------------------- 继续我们的工作,在进行下一步之前,先考虑一下,为了支持css选择器,我们需要使用xpath完成哪些东西 标签选择器。。。。这个很简单嘛,//*[name()='tagName'],完全就是标签选择器嘛,根本都不需要再加工了 id选择器。。。。这个好像也很容易,//*[@id='ID'],貌似也挺容易 再看看类选择器。。。。好像有点问题,//*[contains(@class,'className')] 到是能把符合条件的节点选择出来,但是结果貌似比我们预期的要多了?他连 class="classNameA"、class="PickclassName"之类的也给匹配上了?! Hmmmmmmmmmmm,好吧,在类选择器上,看来是必须扩展一下xpath的方法了,不管是扩展一个正则支持,还是扩展一个其他自定义函数支持,就看个人爱好了,文盲个人是倾向用正则来搞一下,毕竟除了以上三个基本选择器,后边还有属性选择器等着我们实现类似*=啦、^=啦、$=啦,嗯。。。。。为了再htmlParser中不使用正则,结果编写的实现代码中,正则还是不少啊。。。 好了,我们开始去实现一下xpath的扩展吧,这个东西网上搜一搜还是挺多了,基本上就是XsltContext、IXsltContextFunction来对xpath进行扩展 public class XpathContext: XsltContext { private XsltArgumentList _args; public XpathContext() { } public XpathContext(NameTable nt) : base(nt) { } public XpathContext(NameTable nt,XsltArgumentList args) : base(nt) { _args = args; } public XsltArgumentList ArgList { get { return _args; } } public override IXsltContextFunction ResolveFunction(string prefix, string name, XPathResultType[] ArgTypes) { XPathExtensionFunction fun = null; switch (prefix) { case "regex": // 这里是前缀名 switch (name) { case "ismatch": // 这里是函数名,下边的委托中,第一个参数是委托调用的函数名?应该可以这么说吧。。。。 fun = new XPathExtensionFunction("RegexIsMatch", 1, 2, new XPathResultType[] { XPathResultType.NodeSet, XPathResultType.String }, XPathResultType.Boolean); break; } break; } return fun; } public override IXsltContextVariable ResolveVariable(string prefix, string name) { XPathExtensionVariable result = new XPathExtensionVariable(name); return result; } public override int CompareDocument(string baseUri, string nextbaseUri) { return 0; } public override bool PreserveWhitespace(XPathNavigator node) { return true; } public override bool Whitespace { get { return true; } } } public class XPathExtensionFunction : IXsltContextFunction { private XPathResultType[] _xprts; private XPathResultType _xprt; private string _fn; private int _min; private int _max; public int Minargs { get { return _min; } } public int Maxargs { get { return _max; } } public XPathResultType[] ArgTypes { get { return _xprts; } } public XPathResultType ReturnType { get { return _xprt; } } public XPathExtensionFunction(string fn, int min, int max, XPathResultType[] argTypes, XPathResultType returnType) { _fn = fn; _min = min; _max = max; _xprts = argTypes; _xprt = returnType; } public object Invoke(XsltContext xls,object[] args,XPathNavigator doc) { switch (_fn) // 根据函数名,进行具体实现。Hmmmmmmm,应该可以叫做函数名吧。^v^ { case "RegexIsMatch": // 具体实现稍后再说 return false; } return null; } } public class XPathExtensionVariable : IXsltContextVariable { private string _fn = string.Empty; public XPathExtensionVariable(string fn) { _fn = fn; } public object Evaluate(XsltContext xsl) { XsltArgumentList vars = ((XpathContext)xsl).ArgList; return vars.GetParam(_fn, null); } public bool IsLocal { get { return false; } } public bool IsParam { get { return false; } } public XPathResultType VariableType { get { return XPathResultType.Any; } } } 呵呵,别看上边这些代码一大片,其实。。。。都是网上抄的,嗯,真的,文盲同学抄完了之后,都没弄明白各个方法之间传递的都是什么玩意,结果一不小心掉到坑里了,先不要关正则的实现,看看我们的类选择器应该怎么实现 前边已经说了,//*[contains(@class,'className')]不合适,那么用正则来进行选择就好了,//*[regex:ismatch(@class,'(?<!\w)className(?!\w)')],嗯,这个正则很标准嘛,肯定不会选择出多余的东西。。。。好吧,我说的早了,被打脸了 问题出在什么地方?仔细调试后发现在具体实现的地方,也就是Invoke方法里,我所设置的@class传递进来的是个什么玩意?怎么看都没有发现和class这个属性有关系。。。。 然后再想想,正则除了需要和属性计算之外,还可以和节点的正文计算,或者下一级指定节点的正文进行计算,嗯。。。xpath有这个功能,比如//div[.='标题']、//div[a=链接],好吧,我们先吧选择器调整调整//*[regex:ismatch('@class','(?<!\w)className(?!\w)')],嗯,这次Invoke传递进来的参数args的所有元素我都可以看懂了,进来了两个字符串,嘿嘿 具体实现正则其实就很简单了。。。 public object Invoke(XsltContext xls,object[] args,XPathNavigator doc) { XmlElement xe = doc.UnderlyingObject as XmlElement; switch (_fn) { case "RegexIsMatch": string att = args[0].ToString(); string reg = args[1].ToString(); // 按属性匹配 if (att.Substring(0, 1) == "@") { if (xe.Attributes.GetNamedItem(att.Substring(1)) == null) { return false; } else { // 考虑到css选择器是区分大小写的,所以这里的正则就不忽视大小写了 return Regex.IsMatch(xe.Attributes.GetNamedItem(att.Substring(1)).Value, reg); } } // 实现其他正则需要实现的匹配 return false; } return null; } 哦了,关于xpath的扩展我们也就写好了,使用这个扩展的方式也很简单,直接 xml.SelectNodes("//div",new XpathContext())即可,嗯,我是将这个扔到一个静态类里,这样只需要实例化一次就可以了 补充两个方法,XmlExpand的 public static XmlNode addNode(XmlNode node, string name, string namespaceURI) { if (node == null) { return null; } XmlNode n = node.OwnerDocument.CreateNode(XmlNodeType.Element, name, namespaceURI); node.AppendChild(n); return n; } public static XmlNode addNode(XmlNode node, string name) { return addNode(node, name, ""); } public static void setAttribute(XmlNode node, string name, string attrib, string namespaceURI) { if (node.Name == "#text") { return; } if (node.Attributes[name] != null) { node.Attributes.GetNamedItem(name).Value = attrib; } else { XmlNode att = node.OwnerDocument.CreateNode(XmlNodeType.Attribute, name, namespaceURI); att.Value = attrib; node.Attributes.SetNamedItem(att); } } public static void setAttribute(XmlNode node, string name, string attrib) { setAttribute(node,name,attrib,""); } 本文作者:文盲老顾 本文发布时间:2018年06月30日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
vue-router是Vue.js官方提供的一套专用的路由工具库 安装命令如下 npm i vue-router -D vue-router 实例是一个Vue插件,我们需要在Vue全局引用中通过Vue.use() 将它接入到Vue实例中。 在我们的工程中,,main.js是默认的程序入口文件,所有的全局配置都会在这个文件中进行。 我们在main.js中加入如下引用 import VueRouter from 'vue-router' Vue.use(VueRouter) 这样就完成了 vue-router最基本的安装工作了。 接下来我们要实现的功能描述如下 在首页上有两个链接分别是:购物车和个人中心 点击不同的链接显示不同的内容 首先我们在 src 目录下建立两个组件文件: Cart.vue Me.vue 新建的两个组件文件的内容暂时都是同样的结构 <template> <!-- 这个div里面的内容可设置不同以区分 --> <div>购物车</div> </template> <script> export default {} </script> <style lang="scss"></style> 接下来就是在main.js文件中定义路由与这些组件之间的匹配规则了。 VueRouter的定义非常简单:创建一个VueRouter实例,将路由path指定到一个组件类型上 如下代码所示(main.js) import Vue from 'vue' import App from './App.vue' import VueRouter from 'vue-router' //引入创建的两个组件 import Cart from './Cart.vue' import Me from './Me.vue' //使用路由实例插件 Vue.use(VueRouter) const router = new VueRouter({ mode:'history', base: '__dirname', routes:[ //将页面组件与path指令的路由关联 {path:'/cart',component:Cart}, {path:'/me',component:Me} ] }) new Vue({ el: '#app', //将路由实例添加到Vue实例中去 router, render: h => h(App) }) 我们可以将上面的路由有关的代码提取出来放在另外的一个routes.js文件中去,防止main.js文件的内容越来越长。 新建一个 config 文件夹,然后将routes.js文件加入进去。 则routes.js代码如下 import Vue from 'vue' import VueRouter from 'vue-router' //引入创建的两个组件 import Cart from '../Cart.vue' import Me from '../Me.vue' //使用路由实例插件 Vue.use(VueRouter) const router = new VueRouter({ mode:'history', base: '__dirname', routes:[ //将页面组件与path指令的路由关联 {path:'/cart',component:Cart}, {path:'/me',component:Me} ] }) export default router; 然后main.js文件代码就减小到如下: import Vue from 'vue' import App from './App.vue' import router from './config/routes' new Vue({ el: '#app', //将路由实例添加到Vue实例中去 router, render: h => h(App) }) vue-router 提供了两个指令标签 <router-view> : 渲染路径匹配到的视图组件 <router-link> : 支持用户在具有路由功能的应用中导航 在有了上面的两个指令标签,我们就可以在程序入口 App.vue编写相应的代码了: <template> <div id="app"> <div class="tabs"> <ul> <li> <router-link to ="/cart"> <div>购物车</div> </router-link> </li> <li> <router-link to ="/me"> <div>个人中心</div> </router-link> </li> </ul> </div> <div class="content"> <!-- 使用 router-view 渲染视图 --> <router-view></router-view> </div> </div> </template> <script> export default { name: "app" }; </script> <style lang="scss"></style> 到此上面的代码已经实现了预期的功能了。 然后我们看to ="/cart"这个里面的路径其实已经在{path:'/cart',component:Cart}定义过了,如果需要修改,就得需要这两个地方同时修改(如果有其他地方用的就改动的更多) 那么直接将{path:'/cart',component:Cart}中的路径取出来岂不是很好。 这个时候我们的 vue-router提供了一种隐式的路由引用方式,称之为 —— 命名路由 简单来说就是通过路由的名称引用来取代Url 于是VueRouter的配置代码改为如下: const router = new VueRouter({ mode:'history', base: '__dirname', routes:[ //将页面组件与path指令的路由关联 {name:'cart',path:'/cart',component:Cart}, {name:'me',path:'/me',component:Me} ] }) 这样我们在 <router-link >的to属性使用v-bind绑定到Vue实例中,然后通过名称直接得到Url了 于是App.vue中的链接部分的代码改为如下 <li> <router-link :to ="{name:'cart'}"> <div>购物车</div> </router-link> </li> <li> <router-link :to ="{name:'me'}"> <div>个人中心</div> </router-link> </li> 至此,使用vue-router完成了简单导航功能 说明 <router-link>默认渲染成带有正确链接的<a>标签 ,也可以通过配置 tag 属性生成别的标签 比如 <li> <router-link :to ="{name:'cart'}" tag="span"> <div>购物车</div> </router-link> </li> 本文作者:zjq_1314520 本文发布时间:2018年03月09日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
在上一节中,学习了怎么利用SVG的stroke-dasharray和stroke-dashoffset来制作进度条。记得在文章末尾留了一个悬念,说这一节中,要聊聊怎么用Vue来把这个SVG的进度封装成组件。 咱们先不聊Vue怎么把这个封装成组件(我搜索了一下,有现在所这方面组件,而且做得蛮好的,接下来先学习一下)。今天接着聊上一节中的进度条怎么来实现。不过略有不同。不同点来自于网上一位朋友向我提的一个问题。问题是这样的: 用SVG做一个环形的进度条一样的东西,这个很好做(用的 stroke-dasharray),但是,我需要再做一个小圆,随着环形慢慢变成一个圆时一直和头部在一起移动(这个不懂)。这个能稍微指点一下我吗? 说实话,我也很好奇!但我想在以前的基础上添加一个<circle>,让这个新添加的circle跟着内圈做位移(旋转),应该是可以的。也正因为出于好奇,给他找了两个Demo(Demo1、Demo2) 。原本应该能满足其需求。但并不是这样,他希望不借用第三方的库。后来回来自己写了一个 Demo。简单的记录一下这个过程。 简单的分析一下 不管是水平的还是圆形的,其都具有三层,比如下图所示: 灰色表示进度条的底层(总共的长度) 红色表示进度条的进度 (已完成长度) 绿色表示起点 (跟随圆点) 在SVG中的话,对应的就是: 水平进度条,灰色和红色是由<line>元素构建,绿色的由<circle>构建 圆形进度条由三个<circle>构建 知道其中的原委,那就好办的得多了。咱们可以先绘制一个静态的图出来: [xml] view plain copy <svg width="200" height="200" viewBox="0 0 200 200"> <!-- 水平进度条 --> <line x1="10" y1="10" x2="180" y2="10"fill="none" stroke-width="12" stroke="#666" stroke-linecap="round" /> <line x1="10" y1="10" x2="180" y2="10" fill="none" stroke-width="12" stroke="#FC4D04" stroke-dasharray="170" stroke-dashoffset="90" stroke-linecap="round" id="lineInner" /> <circle cx="10" cy="10" r="3" fill="none" stroke="green" stroke-width="6" /> <!-- 圆形进度条 --> <circle cx="100" cy="120" r="74" fill="none" stroke="#666" stroke-width="12" /> <circle cx="100" cy="120" r="74"fill="none" stroke="#FC4D04" stroke-width="12" stroke-dasharray="465" stroke-dashoffset="400" stroke-linecap="round" /> <circle cx="100" cy="120" r="74" fill="none" stroke="green" stroke-width="12" stroke-dashoffset="464.5" stroke-dasharray="465" stroke-linecap="round" /></svg> 这个时候看到的效果如下: 从效果中,可以看出来,现在绿色的圆点都在默认的起点。并没有跟随进度条红色的部分。在SVG中并没有直接的方法或者API来让绿色点保持在红色点的终点。这个时候咱们需要借助CSS的特性来完成。这也离不开一些数学的计算。具体怎么来计算呢?先来看水平进度条。 通过前面的学习,知道使用.getTotalLength()可以知道其长度。在这个示例中,通过: document.querySelectorAll('line')[0].getTotalLength() // => 170 可以知道整个水平进度条的总长度是170,而红色的进度是通过stroke-dashoffset值来控制的,在这个示例中是90。有了这两个值,就很好的控制绿色的值了。使用CSS的translateX()来做对应的位移,其位移的值是170 - 90,即80。 [xml] view plain copy <circle cx="10" cy="10" r="3" fill="none" stroke="green" stroke-width="6" id="circle" style="transform: translateX(80px);"/> 在行内添加了transform:translateX(80px),当然你也可以在CSS样式中写。这个时候你看到的效果如下: 上面完成了水平进度条的效果,接下来看圆形进度条。对于圆形的进度条,我们同要要知道其长度,同样可以能过.getTotalLength()来获取: document.querySelectorAll('circle')[2].getTotalLength(); // =>464.2044677734375 建议上向上取整,此时我们的周长是465。除此之外,还可以通过2π * r来计算。 Math.PI * 2 * 74; // => 464.9557127312894 因为圆形进度条是一个圆,那么通过transltate()是无法实现的,这个时候,需要使用的是transform中的rotate()来旋转。既然需要旋转就需要一个角度值。那又得数学公式了。简单的回忆一下: 弧度 = 角度 * Math.PI / 180 => rad = (π / 180) * deg 角度 = 弧度 * 180 / Math.PI => deg = (rad * 180) / π 其实就是角度(deg)和弧度(rad)之间的转换。一个完整的圆的弧度是2π,所以2π rad = 360°,1 π rad = 180°,1°=π/180 rad,1 rad = 180°/π(约57.29577951°)。以度数表示的角度,把数字乘以π/180便转换成弧度;以弧度表示的角度,乘以180/π便转换成度数。 角所对的弧长是半径的几倍,那么角的大小就是几弧度 平时我们常看到的各种弧度如下: 通过JavaScript可以来这样进行角度和弧度之间的换算: rad = (Math.PI * deg) / 180deg = (rad * 180) / Math.PI 下图展示了常见的角度和弧度之间的换算: 回到我们的示例当中来。通过.getTotalLength()可以获取圆角的长度约为465,对应红色圆的stroke-dashoffset值为400。那么就能获取到红色进度的弧长为65。前面说过,角所对的弧长是半径的几倍,那么角的大小就是几弧度: 65 / 74 = .87rad 另外也可以将.87rad换成deg。根据前面的计算公式,可以计算出来: 65 / 74 * 180 / Math.PI = 50.32737389662636 大约50deg。那么在circle元素中添加: transform:rotate(.8783783783783784rad); transform-origin: 100px 120px; 这个时候看到的效果如下: 特别声明:SVG的坐标系统和Web中的坐标系统是不一样的,所以需要对元素做坐标变换,也就有了transform-origin: 100px 120px的设置。 添加动画效果 SVG中改变stroke-dashoffset的值,可以实现线条自画的效果。这并不是什么新东西,也不是复杂的东西。在我们今天的示例当中的关键点是怎么当绿色的点跟着移动。 通过前面的了解,在水平进度条中,需要给translateX( )一个值,而这个值是: [sql] view plain copy .getTotalLength() - stroke-dashoffset 对于圆形的稍为复杂一点,其要给rotate()传一个值: (.getTotalLength() - stroke-dashoffset) / r; // => 弧度值 或者: [sql] view plain copy (.getTotalLength() - stroke-dashoffset) / r * 180 / Math.PI; =>角度值 既然知道其中原理,我们就来换成Vue的环境。把红色的stroke-dashoffset绑定一个数据,然后使用input[type="range"]来动态修改stroke-dashoffset的值。并且动态修改绿色圆点的位置或者旋转值。比如: [xml] view plain copy <div id="app"> <svg width="200" height="200" viewBox="0 0 200 200"> <line x1="10" y1="10" x2="180" y2="10" fill="none" stroke-width="12" stroke="#666" stroke-linecap="round" /> <line x1="10" y1="10" x2="180" y2="10" fill="none" stroke-width="12" stroke="#FC4D04" stroke-dasharray="170" :stroke-dashoffset="dashOffsetLine" stroke-linecap="round" id="lineInner" /> <circle cx="10" cy="10" r="3" fill="none" stroke="green" stroke-width="6" id="circle" style="transform: translateX(80px);" id="circle"/> <circle cx="100" cy="120" r="74" fill="none" stroke="#666" stroke-width="12" /> <circle cx="100" cy="120" r="74" fill="none" stroke="#FC4D04" stroke-width="12" stroke-dasharray="465" :stroke-dashoffset="dashOffsetCircle" stroke-linecap="round" /> <circle cx="100" cy="120"r="74" fill="none" stroke="green" stroke-width="12" stroke-dashoffset="464.5" stroke-dasharray="465" stroke-linecap="round" id="circleInner" style="transform:rotate(.8783783783783784rad); transform-origin: 100px 120px;"/> </svg> <div class="action"> <div class="line"> <label for="">line: stroke-dashoffset = "{{dashOffsetLine}}"</label> <input type="range" name="points" min="0" value="{{dashOffsetLine}}" v-model="dashOffsetLine" max="170" step="1" @input="moveLine"> </div> <div class="circle"> <label>circle:stroke-dashoffset = "{{dashOffsetCircle}}"</label> <input type="range" name="points" min="0" value="{{dashOffsetCircle}}" v-model="dashOffsetCircle" max="465" step="1" @input="moveCircle"> </div> </div></div> 对应的Vue代码: [javascript] view plain copy let app = new Vue({ el: '#app', data () { return { dashOffsetCircle: 400, dashOffsetLine: 90, } }, methods: { moveLine: function (e){ let circleLine = document.getElementById('circle') let lineInnerLen = document.getElementById('lineInner').getTotalLength() circleLine.style.transform="translateX(" + (lineInnerLen - e.target.value) + "px)" }, moveCircle: function(e) { let circleInner = document.getElementById('circleInner') let circleInnerLen = Math.ceil(document.getElementById('circleInner').getTotalLength()) let arcLen = circleInnerLen - this.dashOffsetCircle let rad = arcLen / 74 circleInner.style.transform="rotate(" + rad + "rad)" } } }) Vue代码写得比较拙逼,路过的大婶请多多指点。这个时候,你拖动input,改变其值时,就可以看到对应的效果。 事实上除了使用line元素和circle元素之外,还可以使用path元素来实现类似的效果。感兴趣的同学,不仿使用path来做一个。如果做了,记得在下面的评论中与一起分享您的成果。 总结 这篇文章主要在上一节的基础上添加了一个网友想要的效果。就是在进度条上有一个圆点,能跟随进度条一起移动。其实现原理并不复杂。对于水平进度条,通过translateX()来改变其位移的位置,对于圆形进度条,则通过旋转来改变其位置。不管是哪一种,都需要动态的计算出他们的值,然后修改对应元素的样式。很多时候CSS和SVG的结合能让我们做出一些意想不到的效果。如果你感兴趣的话,不仿一试。 文章涉及到图片和代码,如果展示不全给您带来不好的阅读体验,欢迎点击文章底部的 阅读全文。如果您觉得小站的内容对您的工作或学习有所帮助,欢迎关注此公众号。 本文作者:W3cplus_ 本文发布时间:2018年01月27日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
1、前言 如今h5新特性、新标签、新规范等有很多,而且正在不断完善中,各大浏览器商对它们的支持,也是相当给力。作为前端程序员,我觉得我们还是有必要积极关注并勇敢地加以实践。接下来我将和各位分享一个特别好用的h5新特性(目前也不是特别新),轻松监听任何App自带的返回键,包括安卓机里的物理返回键,从而实现项目开发中进一步的需求。 2、起因 大概半年前接到pm一需求,用纯h5实现多audio的播放、暂停、续播,页面放至驾考宝典App中,与客户端没有任何的交互,所以与客户端相关的js不需要引用。看上去这需求挺简单的嘛,虽然之前也没做过类似的需求。不管三七二十一,挽起袖子就是干。开始了学习之旅。 3、我这里着重介绍下我具体是怎么监听任何App自带的返回键,以及安卓机里的物理返回键。 那为什么我要去监听呢,这里我有必要强调强调再强调。苹果手机不管是微信、QQ、App,还是浏览器里,涉及到audio、video,返回上一页系统会自动暂停当前的播放的,但不是所有安卓机都可以。所以我们自己必须自定义监听。很多朋友可能第一想法就是百度,然后出来的答案无非是这样 pushHistory(); window.addEventListener("popstate", function(e) { alert("我监听到了浏览器的返回按钮事件啦");//根据自己的需求实现自己的功能 }, false); function pushHistory() { var state = { title: "title", url: "#" }; window.history.pushState(state, "title", "#"); } 是不是很眼熟?然而关键需求不能完美实现,要这段代码有何用,当时我也是绞尽脑汁。直到经过大神好友指导,复制了这段代码 var hiddenProperty = 'hidden' in document ? 'hidden' : 'webkitHidden' in document ? 'webkitHidden' : 'mozHidden' in document ? 'mozHidden' : null; var visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange'); var onVisibilityChange = function(){ if (!document[hiddenProperty]) { console.log('页面非激活'); }else{ console.log('页面激活') } } document.addEventListener(visibilityChangeEvent, onVisibilityChange); 所有问题迎刃而解。 这段代码的原理我个人理解就是通过判断用户浏览的是否为当前页,从而进行相关操作。 这是 MDN相关链接:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/hidden 并不是说真的可以通过JS监听到App里的自带返回键,甚至安卓的物理返回键,而是通过转变思路,快速实现需求。希望这个特性能帮到各位。 END 本文作者:JavaScript_w 本文发布时间:2018年03月14日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
使用css制作简单的点击切换效果,参考了以下教程:css实现的轮播和点击切换(无js版) 首先先制作一个容器,用来容纳所显示的内容: HTML代码: <html></html> <head> <meta charset="utf-8"> <link href="css/test.css" rel="stylesheet" type="text/css" media="all"> </head> <body> <div class="contain"> <ul> <li></li> <li></li> <li>/li> </ul> </div> </body> </html> CSS代码: .contain{ position: relative; margin:auto; width: 600px; height: 200px; text-align: center; font-family: Arial; color: #FFF; } 接下来,根据需要设置ul的长度,这里先制作三个切换窗口,因此将ul的宽度设置为容器宽度的300%,li即为每次切换时显示的子元素,宽度设置为显示容器的100%; HTML代码: <div class="contain"> <ul> <li class="sildeInput-1">one-点击切换</li> <li class="sildeInput-2">two-点击切换</li> <li class="sildeInput-3">three-点击切换</li> </ul> </div> CSS代码: .contain ul{ margin:10px 0; padding:0; width: 1800px; } .contain li{ float: left; width: 600px; height: 200px; list-style: none; line-height: 200px; font-size: 36px; } .sildeInput-1{ background: #9fa8ef; } .sildeInput-2{ background: #ef9fb1; } .sildeInput-3{ background: #9fefc3; } 效果如下: 可以看到,多出来的部分也被显示出来了,此时就要给显示窗口设置overflow:hidden;属性,将多出来的部分隐藏起来 CSS代码: .contain{overflow: hidden;} 效果如下: 可以看到,多出来的部分全部被隐藏起来了,这样,我们就可以通过修改ul的margin-left属性值实现简单的切换效果。 接下来写三个单选框用于切换,因为需要实现点击之后才进行切换的效果,此时将lable指向相应的input标签的id并使用伪类:checked来实现选择切换的效果。 HTML代码: <div class="contain"> <input type="radio" name="sildeInput" value="0" id="Input1" hidden> <label class="label1" for="Input1">1</label> <input type="radio" name="sildeInput" value="1" id="Input2" hidden> <label class="label2" for="Input2">2</label> <input type="radio" name="sildeInput" value="1" id="Input3" hidden> <label class="label3" for="Input3">3</label> <ul> <li class="sildeInput-1">one-点击切换</li> <li class="sildeInput-2">two-点击切换</li> <li class="sildeInput-3">three-点击切换</li> </ul> </div> CSS代码 .label1{ position: absolute; bottom: 10px; left: 0px; width: 20px; height: 20px; margin: 0 10px; line-height: 20px; color: #FFF; background: #000; cursor: pointer; }/*用于调整单选框的属性以及位置*/ .label2{ position: absolute; bottom: 10px; left: 30px; width: 20px; height: 20px; margin: 0 10px; line-height: 20px; color: #FFF; background: #000; cursor: pointer; }/*用于调整单选框的属性以及位置*/ .label3{ position: absolute; bottom: 10px; left: 60px; width: 20px; height: 20px; margin: 0 10px; line-height: 20px; color: #FFF; background: #000; cursor: pointer; }/*用于调整单选框的属性以及位置*/ #Input1:checked~ul{ margin-left: 0;}/*第一张点击切换*/ #Input1:checked~.label1{ background: #535353;}/*点击后改变单选框颜色*/ #Input2:checked~ul{ margin-left: -600px;}/*第二张点击切换*/ #Input2:checked~.label2{ background: #535353;}/*点击后改变单选框颜色*/ #Input3:checked~ul{ margin-left: -1200px;}/*第三张点击切换*/ #Input3:checked~.label3{ background: #535353;}/*点击后改变单选框颜色*/ 效果如下: 最后,再给ul标签添加transition:all 0.5s;属性,设置0.5秒的平滑过渡 css代码: .contain ul{transition:all 0.5s;} 这样,就可以实现页面的平滑切换了。 本文作者:qq_41061352 本文发布时间:2018年02月02日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
一、前言 vue2.0的到来,凭借这其简单易学、完善的API中文文档、丰富的生态系统,成为国内目前十分受欢迎的前端MVVM框架, element-ui是基于 vue2.0的 ui框架,由饿了么团队开发维护的,目前是vue的ui库中最受欢迎的一个框架 element-ui官网 vue专题网站 在vue专题中可以看到各个ui框架的受欢迎程度 二、代码操作 使用vue-cli + element-ui有两种方式 方案一: ①先使用vue-cli 搭建好项目, ②再使用npm install对应的模块、插件, ③再去webpack.conf.js中进行配置各种文件的加载器, ④再去.babelrc 中配置插件 如果项目还没开始编写,就不要使用方案一了,如果项目已经启动了一阵子了,那就得按以上要点每一步都得处理好,稍有不慎,就报错了,这里介绍一下方案二,就是使用 element-ui 提供的一个模板 方案二: ① 使用 git 命令git clone https://github.com/ElementUI/element-starter.git下载官方提供的模板, ② 使用cnpm intsall下载依赖的模块(没有淘宝镜像 cnpm 可以使用npm install) ③ 使用npm run dev跑项目 模板的基本架构如下——和使用vue-cli 脚手架搭建的项目基本没有区别,十分干净 项目跑起来后可以看到一个简单的页面——就比使用vue-cli搭建的项目的页面多了一个element-ui按钮组件 接下来就可以使用element-ui库了~ 本文作者:larger5 本文发布时间:2018年01月31日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
前言 在移动端的web开发中,一提起输入框,程序猿(媛)肯定有很多可以吐槽的点。 在输入框的运用中,小编也是很心累呀~ 不过,经过我 潜(cai)心(keng)研(jiao)究(xun),也算是了解了它的脾性... ... 特别鸣谢:周涵,家兴等 正文这里开始 ? — — — — — — — — 问题探究 1. ios中,输入框获得焦点时,页面输入框被遮盖,定位的元素位置错乱: 当页input存在于吸顶或者吸底元素中时,用户点击输入框,输入法弹出后,fiexd失效,页面中定位好的元素随屏幕滚动。 针对这个问题,我们一起来看下以下几种方案: 方案一: Web API 接口 :scrollIntoView 的应用,将input输入框显示在可视区域。 // 输入框获得焦点时,元素移动到可视区域 inputOnFocus(e) { setTimeout(function(){ e.target.scrollIntoView(true); // true:元素的顶端将和其所在滚动区的可视区域的顶端对齐; false:底端对齐。 },200); // 延时 == 键盘弹起需要时间 } 一行代码,轻松搞定,输入框就乖乖的出现在你眼前了。 不过有点小缺陷:页面过长时,由于fixed失效,输入框依然也会跟着页面滑走。 这时,我们需要一个固定的输入框...... 方案二:在输入框获得焦点时,将页面滑动到最底部,避免fixed导致的页面乱飞,并且保证input在最底部。 var timer; // 输入框获得焦点时,将元素设置为position:static,设置timer inputOnFocus(e) { e.target.style.className = 'input input-static'; timer = setInterval( function() { document.body.scrollTop = document.body.scrollHeight }, 100) }; // 输入框失去焦点时,将元素设置为 position:fixed,清除timer inputOnbulr(e) { e.target.parentNode.className = 'input input-fixed'; clearInterval(timer) }; 效果如下图 当获得焦点弹出虚拟键盘后,input输入框会一直紧贴键盘顶部。如果,你的页面弹出输入法后不需要滑动查看其他内容,那么你对这种方案应该很中意。 But,可能你做的是一个类似聊天的页面,需要在回复时,查看历史消息,那么,请你继续往下看 方案三:将页面进行拆分: 页面(main) = 内容(sectionA) + 输入框(sectionB)+ 其他(sectionOther) 原理 : main.height = window.screen.height ; sectionA 绝对定位,进行内部滚动 overflow-y:scroll ; sectionB 可保证在页面最底部。 .main { position: relative; height: 100%; } .sectionA { box-sizing: border-box; padding-bottom: 60px; height: 100%; overflow-y: scroll; -webkit-overflow-scrolling: touch //为了使滚动流畅,sectionA 添加属性 } .sectionB { position: absolute; height: 60px; overflow: hidden; left: 0; right: 0; bottom: 0; } 纯css3打造,可以滚动,可以固定位置,基本满足大部分布局需要。 2. IOS 中单行输入框输入内容长被遮盖,不能显示全部,且不能左右滑动。 这个是IOS的一个bug,可以考虑用 textarea 替换 input,设置一行的高,进行上下滚动查看。(其他方案可以参看下面 第 6 点) 3. 获得焦点时,光标消失或错位: -webkit-user-select:none 导致 input 框在 iOS 中无法输入,光标不出现,设置如下 user-select:text; -webkit-user-select:text; 利用scrollIntoView 使当前元素出现到指定位置,避免光标错位,设置如下: e.target.scrollIntoView(true); e.target.scrollIntoViewIfNeeded(); 4. 进入页面如何自动获取焦点,弹出软键盘? 添加 autofocus 属性 支持自动获得焦点 触发 focus() 事件 5.随文字输入,输入框宽度自适应。 onkeyPress(e) { const testLength = e.target.value.length; e.target.style.width = `${testLength*8+10}px` } 这种方案基本满足自动获取效果。 testLength * 8 英文字符,testLength * 16中文字符, +10为后边光标预留位置。 这种方案显然不适用于对精确度有很高要求的需求。 6. 介绍一个属性:contenteditable,模拟输入时动态获取宽高 (1)div设置contentditable=true 可以将此元素变成可输入状态。 <div class="inputContent" contenteditable="true" ></div> (2)想要变成input输入框,利用css模拟输入框的样式 .inputContent{ color:#444; border:#999 solid 1px; border-radius: 3px; padding: 5px 10px; box-sizing: border-box; min-width:50px; max-width: 300px; background: #ffffff; } 这里配合min-width,max-width 效果更真实。 (3)点击div可以弹出软键盘,但是无法输入内容,需要设置属性,如下 .inputContent{ user-select:text; -webkit-user-select:text; } 这样就完成一个可以根据获取输入内容来动态来调节宽高。 (这里是一个gif图) 还可以利用js模拟placeholder等,这里就不展开了 7.其他问题及解决 输入框获得焦点可弹出软键盘,却没有光标闪烁,也无法正常输入。 -webkit-user-select:none 导致的,可以这样解决 *:not(input,textarea) { -webkit-touch-callout: none; -webkit-user-select: none; } input 自定义样式 // 使用伪类 input::-webkit-input-placeholder, input::-moz-placeholder, input::-ms-input-placeholder { ...style text-align: center; } 好了,就写到这了,希望看过后对你能有帮助。 本文作者:大转转FE 本文发布时间:2018年01月26日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
有关于移动端的适配布局一直以来都是众说纷纭,对应的解决方案也是有很多种。在《使用Flexible实现手淘H5页面的终端适配》提出了Flexible的布局方案,随着viewport单位越来越受到众多浏览器的支持,因此在《再聊移动端页面的适配》一文中提出了vw来做移动端的适配问题。到目前为止不管是哪一种方案,都还存在一定的缺陷。言外之意,还没有哪一个方案是完美的。 事实上真的不完美?其实不然。最近为了新项目中能更完美的使用vw来做移动端的适配。探讨出一种能解决不兼容viewport单位的方案。今天整理一下,与大家一起分享。如果方案中存在一定的缺陷,欢迎大家一起拍正。 准备工作 对于Flexible或者说vw的布局,其原理不在这篇文章进行阐述。如果你想追踪其中的原委,强烈建议你阅读早前整理的文章《使用Flexible实现手淘H5页面的终端适配》和《再聊移动端页面的适配》。 说句题外话,由于Flexible的出现,也造成很多同学对rem的误解。正如当年大家对div的误解一样。也因此,大家都觉得rem是万能的,他能直接解决移动端的适配问题。事实并不是如此,至于为什么,我想大家应该去阅读flexible.js源码,我相信你会明白其中的原委。 回到我们今天要聊的主题,怎么实现vw的兼容问题。为了解决这个兼容问题,我将借助Vue官网提供的构建工程以及一些PostCSS插件来完成。在继续后面的内容之前,需要准备一些东西: NodeJs NPM Webpack Vue-cli postcss-import postcss-url postcss-aspect-ratio-mini postcss-cssnext autoprefixer postcss-px-to-viewport postcss-write-svg cssnano postcss-viewport-units Viewport Units Buggyfill 对于这些起什么作用,先不阐述,后续我们会聊到上述的一些东西。 使用Vue-cli来构建项目 对于NodeJs、NPM和Webpack相关介绍,大家可以查阅其对应的官网。这里默认你的系统环境已经安装好Nodejs、NPM和Webpack。我的系统目前使用的Node版本是v9.4.0;NPM的版本是v5.6.0。事实上,这些都并不重要。 使用Vue-cli构建项目 为了不花态多的时间去深入的了解Webpack(Webpack对我而言,太蛋疼了),所以我直接使用Vue-cli来构建自己的项目,因为我一般使用Vue来做项目。如果你想深入的了解Webpack,建议你阅读下面的文章: Webpack文档 Awesome Webpack Webpack 教程资源收集 Vue+Webpack开发可复用的单页面富应用教程 接下来的内容,直接使用Vue官方提供的Vue-cli的构建工具来构建Vue项目。首先需要安装Vue-cli: $ npm install -g vue-cli 全局先安装Vue-cli,假设你安装好了Vue-cli。这样就可以使用它来构建项目: vue init webpack vw-layout 根据命令提示做相应的操作: 进入到刚创建的vw-layout: cd vw-layout 然后执行: npm run dev 在浏览器执行http://localhost:8080,就可以看以默认的页面效果: 以前的版本需要先执行npm i安装项目需要的依赖关系。现在新版本的可以免了。 这时,可以看到的项目结构如下: 安装PostCSS插件 通过Vue-cli构建的项目,在项目的根目录下有一个.postcssrc.js,默认情况下已经有了: 对应我们开头列的的PostCSS插件清单,现在已经具备了: postcss-import postcss-url autoprefixer 简单的说一下这几个插件。 postcss-import postcss-import相关配置可以点击这里。目前使用的是默认配置。只在.postcssrc.js文件中引入了该插件。 postcss-import主要功有是解决@import引入路径问题。使用这个插件,可以让你很轻易的使用本地文件、node_modules或者web_modules的文件。这个插件配合postcss-url让你引入文件变得更轻松。 postcss-url postcss-url相关配置可以点击这里。该插件主要用来处理文件,比如图片文件、字体文件等引用路径的处理。 在Vue项目中,vue-loader已具有类似的功能,只需要配置中将vue-loader配置进去。 autoprefixer autoprefixer插件是用来自动处理浏览器前缀的一个插件。如果你配置了postcss-cssnext,其中就已具备了autoprefixer的功能。在配置的时候,未显示的配置相关参数的话,表示使用的是Browserslist指定的列表参数,你也可以像这样来指定last 2 versions 或者 > 5%。 如此一来,你在编码时不再需要考虑任何浏览器前缀的问题,可以专心撸码。这也是PostCSS最常用的一个插件之一。 其他插件 Vue-cli默认配置了上述三个PostCSS插件,但我们要完成vw的布局兼容方案,或者说让我们能更专心的撸码,还需要配置下面的几个PostCSS插件: postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-cssnext cssnano postcss-viewport-units 要使用这几个插件,先要进行安装: npm i postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-cssnext postcss-viewport-units cssnano --S 安装成功之后,在项目根目录下的package.json文件中,可以看到新安装的依赖包: "dependencies": { "cssnano": "^3.10.0", "postcss-aspect-ratio-mini": "0.0.2", "postcss-cssnext": "^3.1.0", "postcss-px-to-viewport": "0.0.3", "postcss-viewport-units": "^0.1.3", "postcss-write-svg": "^3.0.1", "vue": "^2.5.2", "vue-router": "^3.0.1" }, 接下来在.postcssrc.js文件对新安装的PostCSS插件进行配置: module.exports = { "plugins": { "postcss-import": {}, "postcss-url": {}, "postcss-aspect-ratio-mini": {}, "postcss-write-svg": { utf8: false }, "postcss-cssnext": {}, "postcss-px-to-viewport": { viewportWidth: 750, // (Number) The width of the viewport. viewportHeight: 1334, // (Number) The height of the viewport. unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to. viewportUnit: 'vw', // (String) Expected units. selectorBlackList: ['.ignore', '.hairlines'], // (Array) The selectors to ignore and leave as px. minPixelValue: 1, // (Number) Set the minimum pixel value to replace. mediaQuery: false // (Boolean) Allow px to be converted in media queries. }, "postcss-viewport-units":{}, "cssnano": { preset: "advanced", autoprefixer: false, "postcss-zindex": false } } } 特别声明:由于cssnext和cssnano都具有autoprefixer,事实上只需要一个,所以把默认的autoprefixer删除掉,然后把cssnano中的autoprefixer设置为false。对于其他的插件使用,稍后会简单的介绍。 由于配置文件修改了,所以重新跑一下npm run dev。项目就可以正常看到了。接下来简单的介绍一下后面安装的几个插件的作用。 postcss-cssnext postcss-cssnext其实就是cssnext。该插件可以让我们使用CSS未来的特性,其会对这些特性做相关的兼容性处理。其包含的特性主要有: 有关于cssnext的每个特性的操作文档,可以点击这里浏览。 cssnano cssnano主要用来压缩和清理CSS代码。在Webpack中,cssnano和css-loader捆绑在一起,所以不需要自己加载它。不过你也可以使用postcss-loader显式的使用cssnano。有关于cssnano的详细文档,可以点击这里获取。 在cssnano的配置中,使用了preset: "advanced",所以我们需要另外安装: npm i cssnano-preset-advanced --save-dev cssnano集成了一些其他的PostCSS插件,如果你想禁用cssnano中的某个插件的时候,可以像下面这样操作: [css] view plain copy "cssnano": { autoprefixer: false, "postcss-zindex": false } 上面的代码把autoprefixer和postcss-zindex禁掉了。前者是有重复调用,后者是一个讨厌的东东。只要启用了这个插件,z-index的值就会重置为1。这是一个天坑,千万记得将postcss-zindex设置为false。 postcss-px-to-viewport postcss-px-to-viewport插件主要用来把px单位转换为vw、vh、vmin或者vmax这样的视窗单位,也是vw适配方案的核心插件之一。 在配置中需要配置相关的几个关键参数: "postcss-px-to-viewport": { viewportWidth: 750, // 视窗的宽度,对应的是我们设计稿的宽度,一般是750 viewportHeight: 1334, // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置 unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除) viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw selectorBlackList: ['.ignore', '.hairlines'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名 minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值 mediaQuery: false // 允许在媒体查询中转换`px`} 目前出视觉设计稿,我们都是使用750px宽度的,那么100vw = 750px,即1vw = 7.5px。那么我们可以根据设计图上的px值直接转换成对应的vw值。在实际撸码过程,不需要进行任何的计算,直接在代码中写px,比如: .test { border: .5px solid black; border-bottom-width: 4px; font-size: 14px; line-height: 20px; position: relative;}[w-188-246] { width: 188px;} 编译出来的CSS: [css] view plain copy .test { border: .5px solid #000; border-bottom-width: .533vw; font-size: 1.867vw; line-height: 2.667vw; position: relative;}[w-188-246] { width: 25.067vw;} 在不想要把px转换为vw的时候,首先在对应的元素(html)中添加配置中指定的类名.ignore或.hairlines(.hairlines一般用于设置border-width:0.5px的元素中): [xml] view plain copy <div class="box ignore"></div> 写CSS的时候: .ignore { margin: 10px; background-color: red;}.box { width: 180px; height: 300px;}.hairlines { border-bottom: 0.5px solid red;} 编译出来的CSS: [css] view plain copy .box { width: 24vw; height: 40vw;}.ignore { margin: 10px; /*.box元素中带有.ignore类名,在这个类名写的`px`不会被转换*/ background-color: red;}.hairlines { border-bottom: 0.5px solid red;} 上面解决了px到vw的转换计算。那么在哪些地方可以使用vw来适配我们的页面。根据相关的测试: 容器适配,可以使用vw 文本的适配,可以使用vw 大于1px的边框、圆角、阴影都可以使用vw 内距和外距,可以使用vw postcss-aspect-ratio-mini postcss-aspect-ratio-mini主要用来处理元素容器宽高比。在实际使用的时候,具有一个默认的结构 [xml] view plain copy <div aspectratio> <div aspectratio-content></div></div> 在实际使用的时候,你可以把自定义属性aspectratio和aspectratio-content换成相应的类名,比如: [xml] view plain copy <div class="aspectratio"> <div class="aspectratio-content"></div></div> 我个人比较喜欢用自定义属性,它和类名所起的作用是同等的。结构定义之后,需要在你的样式文件中添加一个统一的宽度比默认属性: [aspectratio] { position: relative;}[aspectratio]::before { content: ''; display: block; width: 1px; margin-left: -1px; height: 0;}[aspectratio-content] { position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%;} 如果我们想要做一个188:246(188是容器宽度,246是容器高度)这样的比例容器,只需要这样使用: [css] view plain copy [w-188-246] { aspect-ratio: '188:246';} 有一点需要特别注意:aspect-ratio属性不能和其他属性写在一起,否则编译出来的属性只会留下aspect-ratio的值,比如: [xml] view plain copy <div aspectratio w-188-246 class="color"></div> 编译前的CSS如下: [w-188-246] { width: 188px; background-color: red; aspect-ratio: '188:246';} 编译之后: [css] view plain copy [w-188-246]:before { padding-top: 130.85106382978725%;} 主要是因为在插件中做了相应的处理,不在每次调用aspect-ratio时,生成前面指定的默认样式代码,这样代码没那么冗余。所以在使用的时候,需要把width和background-color分开来写: [w-188-246] { width: 188px; background-color: red;}[w-188-246] { aspect-ratio: '188:246';} 这个时候,编译出来的CSS就正常了: [css] view plain copy [w-188-246] { width: 25.067vw; background-color: red;}[w-188-246]:before { padding-top: 130.85106382978725%;} 有关于宽高比相关的详细介绍,如果大家感兴趣的话,可以阅读下面相关的文章: CSS实现长宽比的几种方案 容器长宽比 Web中如何实现纵横比 实现精准的流体排版原理 目前采用PostCSS插件只是一个过渡阶段,在将来我们可以直接在CSS中使用aspect-ratio属性来实现长宽比。 postcss-write-svg postcss-write-svg插件主要用来处理移动端1px的解决方案。该插件主要使用的是border-image和background来做1px的相关处理。比如: @svg 1px-border { height: 2px; @rect { fill: var(--color, black); width: 100%; height: 50%; } }.example { border: 1px solid transparent; border-image: svg(1px-border param(--color #00b1ff)) 2 2 stretch;} 编译出来的CSS: .example { border: 1px solid transparent; border-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='2px'%3E%3Crect fill='%2300b1ff' width='100%25' height='50%25'/%3E%3C/svg%3E") 2 2 stretch; } 上面演示的是使用border-image方式,除此之外还可以使用background-image来实现。比如: @svg square { @rect { fill: var(--color, black); width: 100%; height: 100%; } }#example { background: white svg(square param(--color #00b1ff));} 编译出来就是: #example { background: white url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2300b1ff' width='100%25' height='100%25'/%3E%3C/svg%3E"); } 解决1px的方案除了这个插件之外,还有其他的方法。可以阅读前期整理的《再谈Retina下1px的解决方案》一文。 特别声明:由于有一些低端机对border-image支持度不够友好,个人建议你使用background-image的这个方案。 CSS Modules Vue中的vue-loader已经集成了CSS Modules的功能,个人建议在项目中开始使用CSS Modules。特别是在Vue和React的项目中,CSS Modules具有很强的优势和灵活性。建议看看CSS In JS相关的资料。在Vue中,使用CSS Modules的相关文档可以阅读Vue官方提供的文档《CSS Modules》。 postcss-viewport-units postcss-viewport-units插件主要是给CSS的属性添加content的属性,配合viewport-units-buggyfill库给vw、vh、vmin和vmax做适配的操作。 这是实现vw布局必不可少的一个插件,因为少了这个插件,这将是一件痛苦的事情。后面你就清楚。 到此为止,有关于所需要的PostCSS已配置完。并且简单的介绍了各个插件的作用,至于详细的文档和使用,可以参阅对应插件的官方文档。 vw兼容方案 在《再聊移动端页面的适配》一文中,详细介绍了,怎么使用vw来实现移动端的适配布局。这里不做详细的介绍。建议你花点时间阅读这篇文章。 先把未做兼容处理的示例二维码贴一个: 你可以使用手淘App、优酷APP、各终端自带的浏览器、UC浏览器、QQ浏览器、Safari浏览器和Chrome浏览器扫描上面的二维码,您看到相应的效果: 但还有不支持的,比如下表中的No,表示的就是不支持 正因如此,很多同学都不敢尝这个螃蟹。害怕去处理兼容性的处理。不过不要紧,今天我把最终的解决方案告诉你。 最终的解决方案,就是使用viewport的polyfill:Viewport Units Buggyfill。使用viewport-units-buggyfill主要分以下几步走: 引入JavaScript文件 viewport-units-buggyfill主要有两个JavaScript文件:viewport-units-buggyfill.js和viewport-units-buggyfill.hacks.js。你只需要在你的HTML文件中引入这两个文件。比如在Vue项目中的index.html引入它们: [xml] view plain copy <script src="//g.alicdn.com/fdilab/lib3rd/viewport-units-buggyfill/0.6.2/??viewport-units-buggyfill.hacks.min.js,viewport-units-buggyfill.min.js"></script> 你也可以使用其他的在线CDN地址,也可将这两个文件合并压缩成一个.js文件。这主要看你自己的兴趣了。 第二步,在HTML文件中调用viewport-units-buggyfill,比如: [xml] view plain copy <script> window.onload = function () { window.viewportUnitsBuggyfill.init({ hacks: window.viewportUnitsBuggyfillHacks }); }</script> 为了你Demo的时候能获取对应机型相关的参数,我在示例中添加了一段额外的代码,估计会让你有点烦: 具体的使用。在你的CSS中,只要使用到了viewport的单位(vw、vh、vmin或vmax )地方,需要在样式中添加content: .my-viewport-units-using-thingie { width: 50vmin; height: 50vmax; top: calc(50vh - 100px); left: calc(50vw - 100px); /* hack to engage viewport-units-buggyfill */ content: 'viewport-units-buggyfill; width: 50vmin; height: 50vmax; top: calc(50vh - 100px); left: calc(50vw - 100px);';} 这可能会令你感到恶心,而且我们不可能每次写vw都去人肉的计算。特别是在我们的这个场景中,咱们使用了postcss-px-to-viewport这个插件来转换vw,更无法让我们人肉的去添加content内容。 这个时候就需要前面提到的postcss-viewport-units插件。这个插件将让你无需关注content的内容,插件会自动帮你处理。比如插件处理后的代码: Viewport Units Buggyfill还提供了其他的功能。详细的这里不阐述了。但是content也会引起一定的副作用。比如img和伪元素::before(:before)或::after(:after)。在img中content会引起部分浏览器下,图片不会显示。这个时候需要全局添加: [css] view plain copy img { content: normal !important;} 而对于::after之类的,就算是里面使用了vw单位,Viewport Units Buggyfill对其并不会起作用。比如: [sql] view plain copy // 编译前 .after { content: 'after content'; display: block; width: 100px; height: 20px; background: green; } // 编译后 .after[data-v-469af010] { content: "after content"; display: block; width: 13.333vw; height: 2.667vw; background: green; } 这个时候我们需要通过添加额外的标签来替代伪元素(这个情景我没有测试到,后面自己亲测一下)。 到了这个时候,你就不需要再担心兼容问题了。 请用你的手机,不管什么APP扫一扫,你就可以看到效果。(小心弹框哟),如果你发现了还是有问题,请把弹出来的信息截图发给我。 如查你想看看别的机型效果,可以点击这里、这里、这里、还有这里。整个示例的源码,可以点击这里下载。 如果你下载了示你源码,先要确认你的系统环境能跑Vue的项目,然后下载下来之后,解压缩,接着运行npm i,再运行npm run dev,你就可以看到效果了。 总结 如果你看到这里了,希望这篇文章对你有所帮助。能帮助你解决项目中的实际问题,让你不再担心移动端的适配问题。当然更希望的是你在实际的项目中用起这个方案,把碰到的问题及时反馈给偶。如果你有更好的方案,欢迎在下面的评论中与我们一起分享。 文章涉及到图片和代码,如果展示不全给您带来不好的阅读体验,欢迎点击文章底部的 阅读全文。如果您觉得小站的内容对您的工作或学习有所帮助,欢迎关注此公众号。 本文作者:W3cplus_ 本文发布时间:2018年01月25日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
英文:Mark Brouch 译文:王下邀月熊 segmentfault.com/a/1190000007553885 在Walmart Labs的产品开发中,我们进行了大量的Code Review工作,这也保证了我有机会从很多优秀的工程师的代码中学习他们的代码风格与样式。在这篇博文里我会分享出我最欣赏的五种组件模式与代码片。不过我首先还是要谈谈为什么我们需要执着于提高代码的阅读体验。就好像你有很多种方式去装扮一只猫,如果你把你的爱猫装扮成了如下这样子: 你或许可以认为萝卜青菜各有所爱,但是代码本身是应当保证其可读性,特别是在一个团队中,你的代码是注定要被其他人阅读的。电脑是不会在意这些的,不管你朝它们扔过去什么,它们都会老老实实的解释,但是你的队友们可不会这样,他们会把丑陋的代码扔回到你的脸上。而所谓的Pretty Components,应该包含如下的特性: 即使没有任何注释的情况下也易于理解 比乱麻般的代码有更好的性能表现 更易于进行Bug追溯 简洁明了,一句顶一万句 SFC:Stateless Functional Component 我觉得我们在开发中经常忽略掉的一个模式就是所谓的Stateless Functional Component,不过这是我个人最爱的React组件优化模式,没有之一。我喜爱这种模式不仅仅因为它们能够减少大量的模板代码,而且因为它们能够有效地提高组件的性能表现。总而言之,SFC能够让你的应用跑的更快,长的更帅。 直观来看,SFC就是指那些仅有一个渲染函数的组件,不过这简单的改变就可以避免很多的无意义的检测与内存分配。下面我们来看一个实践的例子来看下SFC的具体作用,譬如: 如果我们用正统的React组件的写法,可以得出如下代码: export default class RelatedSearch extends React.Component { constructor(props) { super(props); this._handleClick = this._handleClick.bind(this); } _handleClick(suggestedUrl, event) { event.preventDefault(); this.props.onClick(suggestedUrl); } render() { return ( <section className="related-search-container"> <h1 className="related-search-title">Related Searches:</h1> <Layout x-small={2} small={3} medium={4} padded={true}> {this.props.relatedQueries.map((query, index) => <Link className="related-search-link" onClick={(event) => this._handleClick(query.searchQuery, event)} key={index}> {query.searchText} </Link> )} </Layout> </section> ); } } 而使用SFC模式的话,大概可以省下29%的代码: const _handleClick(suggestedUrl, onClick, event) => { event.preventDefault(); onClick(suggestedUrl); }; const RelatedSearch = ({ relatedQueries, onClick }) => <section className="related-search-container"> <h1 className="related-search-title">Related Searches:</h1> <Layout x-small={2} small={3} medium={4} padded={true}> {relatedQueries.map((query, index) => <Link className="related-search-link" onClick={(event) => _handleClick(query.searchQuery, onClick, event)} key={index}> {query.searchText} </Link> )} </Layout> </section> export default RelatedSearch; 代码量的减少主要来源两个方面: 没有构造函数(5行) 以Arrow Function的方式替代Render语句(4行) 实际上,SFC最迷人的地方不仅仅是其代码量的减少,还有就是对于可读性的提高。SFC模式本身就是所谓纯组件的一种最佳实践范式,而移除了构造函数并且将_handleClick()这个点击事件回调函数提取出组件外,可以使JSX代码变得更加纯粹。另一个不错的地方就是SFC以Arrow Function的方式来定义了输入的Props变量,即以Object Destructring语法来声明组件所依赖的Props: const RelatedSearch = ({ relatedQueries, onClick }) => 这样不仅能够使组件的Props更加清晰明确,还能够避免冗余的this.props表达式,从而使代码的可读性更好。 最后,我还想要强调下虽然我很推崇SFC,不过也不能滥用它。最合适使用SFC的地方就是之前你用纯组件的地方。在Walmart Labs中,我们使用Redux来管理应用的状态,也就意味着我们绝大部分的组件都是纯组件,也就给了SFC广阔的应用空间。一般来说,有以下特征的组件式绝对不适合使用SFC的: 需要自定义整个组件的生命周期管理 需要使用到refs Conditional Components JSX本身不支持if表达式,不过我们可以使用逻辑表达式的方式来避免将代码切分到不同的子模块中,大概是如下样子: render() { <div class="search-results-container"> {this.props.isGrid ? <SearchResultsGrid /> : <SearchResultsList />} </div> } 这种表达式在二选一渲染的时候很有效果,不过对于选择性渲染一个的情况很不友好,譬如如下的情况: render() { <div class="search-results-list"> {this.props.isSoftSort ? <SoftSortBanner /> : null } </div> } 这样子确实能起作用,不过看上去感觉怪怪的。我们可以选用另一种更加语义化与友好的方式来实现这个功能,即使用逻辑与表达式然后返回组件: render() { <div class="search-results-list"> {!!this.props.isSoftSort && <SoftSortBanner />} </div> } 不过这一点也是见仁见智,每个人按照自己的喜好来就行了。 Arrow Syntax In React And Redux ES2015里包含了不少可口的语法糖,我最爱的就是那个Arrow Notation。这个特性在编写组件时很有作用: const SoftSort = ({ hardSortUrl, sortByName, onClick }) => { return ( <div className="SearchInfoMessage"> Showing results sorted by both Relevance and {sortByName}. <Link href={`?${hardSortUrl}`} onClick={(ev) => onClick(ev, hardSortUrl)}> Sort results by {sortByName} only </Link> </div> ); }; 该函数的功能就是返回JSX对象,我们也可以忽略return语句: const SoftSort = ({ hardSortUrl, sortByName, onClick }) => <div className="SearchInfoMessage"> Showing results sorted by both Relevance and {sortByName}. <Link href={`?${hardSortUrl}`} onClick={(ev) => onClick(ev, hardSortUrl)}> Sort results by {sortByName} only </Link> </div> 代码行数又少了不少咯! 另一块我觉得非常适用Arrow Function的地方就是Redux的mapStateToProps函数: const mapStateToProps = ({isLoading}) => { return ({ loading: isLoading, }); }; 需要注意的是,如果你返回的是Object,你需要包裹在大括号内: const mapStateToProps = ({isLoading}) => ({ loading: isLoading }); 使用Arrow Function优化的核心点在于其能够通过专注于函数的重要部分而提升代码的整体可读性,并且避免过多的模板代码带来的噪音。 合理使用Object Destructing与Spread Attributes 大的组件往往受困于this.props过长的窘境,典型的如下所示: render() { return ( <ProductPrice hidePriceFulfillmentDisplay= {this.props.hidePriceFulfillmentDisplay} primaryOffer={this.props.primaryOffer} productType={this.props.productType} productPageUrl={this.props.productPageUrl} inventory={this.props.inventory} submapType={this.props.submapType} ppu={this.props.ppu} isLoggedIn={this.props.isLoggedIn} gridView={this.props.isGridView} /> ); } 这么多的Props估计看着都头疼,如果我们要将这些Props继续传入下一层,大概就要变成下面这个样子了: render() { const { hidePriceFulfillmentDisplay, primaryOffer, productType, productPageUrl, inventory, submapType, ppu, isLoggedIn, gridView } = this.props; return ( <ProductPrice hidePriceFulfillmentDisplay={hidePriceFulfillmentDisplay} primaryOffer={primaryOffer} productType={productType} productPageUrl={productPageUrl} inventory={inventory} submapType={submapType} ppu={ppu} isLoggedIn={isLoggedIn} gridView={isGridView} /> ); } 暂时不考虑unKnown Props,我们可以使用解构赋值来实现这个功能: render() { const props = this.props; return <ProductPrice {...props} /> } Method Definition Shorthand 最后这个方法不一定多有用,不过还是能让你的代码变得更加漂亮。如果你希望在Object中添加函数,你可以使用ES2015 Method Definition Shorthand来代替传统的ES5的表达式,譬如: 如果你想设置一个默认的空方法,也可以利用这种方式: ProductRating.defaultProps = { onStarsClick() {} }; 本文作者:前端大全 本文发布时间:2018年03月14日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
在提笔写下这篇文章之前,我查阅了很多的平台的文章,看下大家都怎样把一个话题写好,也学习到不少东西,非常感谢有这样一个平台可以和大家互相交流。 我尽可能写得简单清晰,让大家看完可以马上从零开始,记得要自己动手丰衣足食。 写这篇文章原因是因为刚好年底做项目总结,把公司产品做了规划,把组件做了整合整理,我们用 Vue2+,于是造了轮子,也借此机会给感兴趣的朋友分享下经验。 前端组件化是当今热议的话题之一,也是我们在项目开发中经常会碰到的问题,目前各个大厂开源了自己的 UI 库,有入 iView、Element 等,但是现实中存在一些问题。 比如每个公司业务组件不尽相同,没有办法完全满足需求,又或者各位 Geek 想通过学习框架,来打造属于自己的一套组件库,那么该如何去做呢? 本话题分为以下 6 部分内容: 环境配置; 代码组织结构; 开始第一个组件; 思考全局组件; 编写 API 文档; 打包发布。 可能需要你会一点 webpack、ES6 和 Vue 的知识,以下代码为了直观,大部分会以截图方式展示,只有特殊地方会以 code 方式出现。 具体代码可以查看线上版本 XMUI:https://github.com/monw3c/xmui。 1. 环境配置 工欲善其事必先利其器,IDE 我推荐用 VSC,必装插件有 Vetur、Eslint、 VSC 里用户设置添加: "eslint.autoFixOnSave": true, "eslint.validate": [ "javascript",{ "language": "vue", "autoFix": true },"html", "vue" ], "files.associations": { "*.vue ": "vue" } 使 VSC 可以高亮 .vue 文件,Eslint 可以规范代码。用 Mac 的童鞋,Command Line 建议安装 iTerm2,接下来使用官方的 vue-cli 生成项目结构作为我们的库的结构。 全局安装 vue-cli: cnpm install -g vue-cli 创建一个基于 webpack 模板的新项目并安装依赖: vue init webpack projectName cd projectName npm install npm run dev 成功的话会显示 Vue 官方的首页。 第一步(请跟着动手),在 src 目录下建 comps(名字随你喜欢),放置组件库全部文件。在根目录 main.js 文件里加入: import xmui from ‘./comps/index’ Vue.use(xmui) router 目录的 index.js 修改如下: import Vue from ‘vue’ import Router from ‘vue-router’ Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'home', component: (resolve) => { require(['@/views/home'], resolve) } } ] }) 新建 views 目录,新建 home.vue 作为展示页模板。 接着需要对 webpack 配置文件稍作一下调整,在 build 目录新增三个文件: 在根目录 package.json 里添加三个 scripts: “scripts”: { … “package:dev”: “webpack —config build/package.dev.config.js”, “package:prod”: “webpack —config build/package.prod.config.js”, “package”: “npm run package:prod && npm publish && npm run build” } 需要修改几处地方。 (1)package.config.js 修改组件库的 index.js 为入口文件,这里的 package 不是组件目录,是 npm run package:prod 最终生成的压缩目录。 (2)package.json 的 main 配置是 当从 npm 安装包后,告诉 node_modules 查找引用的路径。 (3)调整 dist 目录生成到 docs 文档内,这样的做法是放到 github pages 上可以同时查看文档和示例: 2. 代码组织结构 先看目录截图: 总体分为 3 块,把各个组件都放在 components 目录内,公共样式放在 styles,index.js 作为入口文件。 首先 components 是所有的组件集合,以 button 组件目录为例: 一般就是 .vue、.scss、.js 三种类型文件组成,具体的实现在第 3 点开始写第一个组件详细介绍。 再看 styles 目录,应该从名字童鞋们可以看懂都是表示什么意思,样式组织我用的是 BEM 规范,分为 normalize(reset 样式)、varibles(各种变量定义,例如字体、背景颜色、按钮颜色等)、icon(图标类),mixin(方法函数)、index 为入口文件,样式写法建议直接看线上 styles 库:https://goo.gl/Tss1Ry,最终会是这样: 这里有个更好的方法,可以通过方法自动生成组件样式的 import,免去每次增加一个组件都需要添加一遍,具体留给童鞋们去实现。 最后看 index.js 入口文件(既 Vue 组件注册文件): 上面这段代码是官方的写法,意思就是把上面的所以组件注入到 Vue 组件对象里,这样可以让组件支持全局引入和按需引入。 到这里基本的框架结构就说完了,准备写第一个组件吧! 3. 开始第一个组件 还是以 button 为例,在上一节提到过,有三种类型文件,在这个组件里,考虑在项目使用中有两种情况:button 和 button group(按钮组),分别建立四个文件 button.vue、button-group.vue、button.scss 和 index.js。 先说 button.vue 的设计,<template></template>里写入 html 代码: <button class="xm__btn" @click="handleClick" :style="{backgroundColor: bgColor, color: color, 'border-color':borderColor}" :class="[ 'xm__btn--'+type, {'is-plain': plain, 'is-round': round, 'is-long': long, 'xm__btn--block': block, 'no-radius': noRadius}, iconClass ]" :disabled="disabled" > <i :class="icon" v-if="icon"></i> <i class="xm__icon--loading" v-if="loading && !icon"></i> <slot></slot> </button> 既然是组件,就要考虑到可复用,可扩展,通过 props 传入不同参数和类型,来显示不同的按钮,<script></script>里写入: export default { name: 'xm-button', // 组件名,例如这样用 <xm-button> 按钮 </xm-button> props: { // 父组件传入的值 type: { type: String, default: 'default' }, long: Boolean, loading: { type: Boolean, default: false }, noRadius: { type: Boolean, default: false }, bgColor: { type: String, default: '' }, borderColor: { type: String, default: '' }, icon: { type: String, default: '' }, color: { type: String, default: '' }, block: Boolean, disabled: Boolean, plain: Boolean, round: Boolean }, methods: { // 绑定的方法 handleClick (event) { if (this.disabled) return this.$emit('click', event) // 传播方法名为 click,你也可以自定义其他名字 } }, computed: { // 计算属性 iconClass () { if (this.icon !== '') return 'xm__hasIconBtn' } } } 计算属性,可以进行缓存,只有在它的相关依赖发生改变时才会重新求值,可以减少性能开销。 组件调用是这样的: 最终样子: 再来看 button-group.vue 的设计: <template> <div class="xm__btn--group"><slot></slot></div> </template> <script> export default { name: 'xm-button-group' } </script> 提供 slot 来按需插入,组件调用是这样的: <xm-button-group class="btn__group"> <xm-button type="warning"> 警告 </xm-button> <xm-button type="primary" @click="btnClick" icon="xm__icon--checked" > 主要 </xm-button> <xm-button type="success" icon="xm__icon--loading"> 成功 </xm-button> </xm-button-group> 最终样子: 样式我用的是 .scss(你也可以用 .less),这里以.xm__btn--default为例: 命名我用 BEM 的规范,如果不了解可以看这里 BEM:https://goo.gl/UnYbkQ。 最后是 index.js 文件,export 模块: 是不是很简单?!一个组件就写完了。 本文作者:gitchat 本文发布时间:2018年02月14日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
记得当那天使用 CakePHP 开发的时候,我很喜欢它简易入门的特性。其文档不仅结构严密,详尽,而且对用户友好。多年以后,我在 Vue.js 上找到了同样的感觉。然而,与 Cake 相比,Vue 文档还有一个缺点:(缺乏)真实的项目教程。 不管框架的文档有多好,对与所有人来说都是不够的。阅读有关的概念并不是总能帮助你了解更多东西,也不能帮助你理解如何使用它们来实际做出某些事情。如果你和我一样,你会在实践过程中学到更多,在你编码的时候参考文档,因为你需要它们。 在本教程中,我们将构建一个星级评分系统组件。我们将在需要时介绍几个 Vue.js 概念,并介绍为什么要使用它们。 TL;DR: 这篇文章详细的介绍了如何使用 vue.js 和为什么使用 vue.js 。它旨在帮助掌握 Vue.js 的一些核心概念,并教你如何为未来的项目做出设计决策。如果你想了解整个思维过程,请继续阅读。否则,你可以直接查看 CodeSandbox 上的最终代码。 凉凉_ 翻译于 6个月前 2人顶 顶 翻译得不错哦! 入门指南 Vue.js(正确地)以一个简单的脚本引入足以开始运行,但是当你想使用single-file components,情况会有所不同。 现在,你不必这样构建组件。 你可以很容易地用 Vue.component 定义一个全局组件。 问题在于,这样做需要权衡使用字符串模板,没有 CSS 支持,也没有构建步骤(所以没有预处理器)。 然而,我们想要更深入地学习如何构建一个真正的在项目中使用的实际组件。出于这些原因,我们将使用由 Webpack 提供支持的实际设置。 为了保持简单并减少配置时间,我们将使用 vue-cli 和简单的 webpack-simple Vue.js模板。 首先,你需要全局安装 vue-cli。启动你的终端并键入以下内容: npm install -g vue-cli 你现在可以通过几个按键生成随时可用的 Vue.js 样板。然后继续输入: vue init webpack-simple path/to/my-project 你会碰到几个问题。 选择除“使用sass”之外的所有默认值,你应该回答 yes(y)。然后,vue-cli 将初始化项目并创建 package.json 文件。完成后,可以导航到项目目录,安装依赖关系,然后运行项目: cd path/to/my-project npm install npm run dev 就这么简单!Webpack 将开始在端口 8080(如果可用)上为你的项目提供服务并在浏览器中启动它。如果一切顺利,你应该看到这样的欢迎页面。 凉凉_ 翻译于 6个月前 0人顶 顶 翻译得不错哦! 我们做到了吗? 可以说我们做到了!为了正确调试你的Vue.js组件,你需要正确的工具。 继续并安装Vue.js devtools浏览器扩展(Firefox/Chrome/Safari)。 你的第一个组件 Vue.js最好的功能之一是single-file components(SFC)。 它们允许您在一个文件中定义组件的结构,样式和行为,而不存在混合HTML,CSS和JavaScript的常见缺陷。 SFC以.vue扩展名结尾,并具有以下结构: <template> <!-- Your HTML goes here --> </template> <script> /* Your JS goes here */ </script> <style> /* Your CSS goes here */ </style> 让我们开始创建我们的第一个组件:在/src/components中创建一个Rating.vue文件,然后复制/粘贴上面的代码片段。然后,打开/src/main.js并调整现有的代码: import Vue from 'vue' import Rating from './components/Rating' new Vue({ el: '#app', template: '<Rating/>', components: { Rating } }) 最后,添加一些HTML代码到你的Rating.vue文件: <template> <ul> <li>One</li> <li>Two</li> <li>Three</li> </ul> </template> 现在看看你的浏览器中的页面,你应该看到列表。Vue.js会将您的<Rating>组件附加到index.html中的#app元素。如果检查HTML,则应该看不到#app元素的符号:Vue.js将其替换为组件。 旁注:你有没有注意到你甚至不需要重新加载页面?这是因为Webpack的vue-loader带有一个热加载功能。与实时重新加载或浏览器同步相反,每次更改文件时,热重新加载都不会刷新页面。而是监视组件更改,只刷新它们,保持状态不变。 现在,我们已经花了一些时间来设置,是时候真正写出有意义的代码了。 凉凉_ 翻译于 6个月前 0人顶 顶 翻译得不错哦! 模板 我们将使用 vue-awesome,一个用 Font Awesome icons 构建的 Vue.js 的 SVG 图标组件。我们可以只加载我们需要的图标,使用 npm(或 Yarn)进行安装: npm install vue-awesome 然后编辑你的组件,如下所示: <template> <div> <ul> <li><icon name="star"/></li> <li><icon name="star"/></li> <li><icon name="star"/></li> <li><icon name="star-o"/></li> <li><icon name="star-o"/></li> </ul> <span>3 of 5</span> </div> </template> <script> import 'vue-awesome/icons/star' import 'vue-awesome/icons/star-o' import Icon from 'vue-awesome/components/Icon' export default { components: { Icon } } </script> 好吧,让我们慢一点,解释一下。 Vue.js 使用原生 ES6 模块来处理依赖和导出组件。<script>块中的前两行分别导入图标,所以最终捆绑包中不需要图标。第三个图标是从 vue-awesome 导入的 Icon 组件,所以你可以在你的项目中使用它。 图标是一个 Vue.js SFC,就像我们正在构建的这一个。如果你打开这个文件,你会发现它和我们的结构完全一样。 export default 模块将对象文字导出为我们组件的视图模型。我们在组件属性中注册了 Icon 组件,所以我们可以在本地使用它。 最后,我们在 HTML <template> 中使用了 Icon,并传递了一个 name 属性来定义我们想要的图标。通过将组件转换为 kebab-case(例如:MyComponent 变成 <my-component>),可以将组件用作自定义 HTML 标记。我们不需要在组件内嵌入任何东西,所以我们使用了一个自闭合标签。 旁注:你有没有注意到我们在 HTML 中添加了一个<div>标签?这是因为我们还在根级别的<span>中添加了一个计数器,Vue.js 中的组件模板只接受一个根元素。如果你不遵守,会得到一个编译错误。 凉凉_ 翻译于 6个月前 0人顶 顶 翻译得不错哦! 样式 如果你已经使用过 CSS,你应该知道一个主要的挑战就是要处理它的全局性。嵌套一直被认为是解决这个问题的方法。但现在我们知道它很快就会导致特殊性问题,使得样式难以覆盖,不能被重用,并且这将是一个难以衡量的噩梦。 于是发明了像 BEM 这样的方法来绕过这个问题,并且通过命名空间类来保持低的特异性。有一段时间,这是编写干净和可扩展的 CSS 的理想方法。然后,像 Vue.js 或 React 这样的框架和库就出现了,并将 scoped styling 引入表中。 React 具有样式化的组件,Vue.js 具有 scoped styling CSS。它可以让你编写特定组件的 CSS,而不必拿出一些技巧来保持它的包含结构。您使用“普通”类名编写常规 CSS,Vue.js 通过将数据属性分配给 HTML 元素并将其附加到编译样式来处理范围限定。 让我们在组件上添加一些简单的类: <template> <div class="rating"> <ul class="list"> <li class="star active"><icon name="star"/></li> <li class="star active"><icon name="star"/></li> <li class="star active"><icon name="star"/></li> <li class="star"><icon name="star-o"/></li> <li class="star"><icon name="star-o"/></li> </ul> <span>3 of 5</span> </div> </template> 和 css 样式: <style scoped> .rating { font-family: 'Avenir', Helvetica, Arial, sans-serif; font-size: 14px; color: #a7a8a8; } .list { margin: 0 0 5px 0; padding: 0; list-style-type: none; } .list:hover .star { color: #f3d23e; } .star { display: inline-block; cursor: pointer; } .star:hover ~ .star:not(.active) { color: inherit; } .active { color: #f3d23e; } </style> 看到那个scoped属性了吗? 这是告诉 Vue.js 去范围化样式,所以他们作用范围不会涵盖到其他地方。 如果您在 index.html 中正确地复制/粘贴 HTML 代码,您将注意到您的样式不适用:这是因为它们的作用域是组件。 那么预处理器呢? Vue.js 使得从简单的 CSS 切换到您最喜欢的预处理器变得轻而易举。你所需要的只是适当的 Webpack 加载器和<style>块上的简单属性。我们在生成项目时对“使用sass”选择“是”,所以 vue-cli 已经为我们安装并配置了 sass-loader。现在,我们需要做的就是将 lang="scss" 添加到开始的<style>标签中。 现在我们可以使用 Sass 编写组件级样式,导入变量,颜色定义或混合等部分。如果您更喜欢缩进语法(或“sass”符号),只需在 lang 属性中将 scss 切换 sass 即可。 凉凉_ 翻译于 6个月前 0人顶 顶 翻译得不错哦! 行为 现在我们的组件看起来不错,现在是时候让它开始工作了。目前,我们有一个硬编码的模板。让我们设置一些初始的模拟状态,并调整模板,使其显示出来: <script> ... export default { components: { Icon }, data() { return { stars: 3, maxStars: 5 } } } </script> /* ... */ <template> <div class="rating"> <ul class="list"> <li v-for="star in maxStars" :class="{ 'active': star <= stars }" class="star"> <icon :name="star <= stars ? 'star' : 'star-o'"/> </li> </ul> <span>3 of 5</span> </div> </template> 我们在这里所做的是使用 Vue 的数据来设置组件状态。你在 data 中定义的每个属性都是有响应性的:如果它发生变化,它将反映在视图中。 我们正在创建一个可重用的组件,因此 data 需要成为工厂函数而不是对象文字。这样我们就得到了一个新的对象,而不是一个可以跨几个组件共享的现有对象。 我们的 data 工厂返回两个属性:stars,当前“活动”的 star 数和 maxStars,还有一个就是组件中 star 的总数。因为我们会适配我们的模板规则,所以它反映了组件的实际状态。Vue.js 带有一堆指令,可以让您将演示逻辑添加到模板中,而无需将其与纯 JavaScript 代码混合。v-fordirective 遍历任何可迭代的对象(数组,对象文字,映射等)。它也可以把一个数字作为一个范围重复 x 次、这就是我们用 v-for="star in maxStars" 所做的,所以我们对组件中的每个星星都有一个<li>。 您可能已经注意到一些属性以冒号为前缀,这是 v-bind 指令的缩写,它将属性动态绑定到表达式。我们可以把它写成长的形式,v-bind:class。 当 star 处于活动状态时,我们需要在 <li> 元素上添加 active 类。在我们的项目下,这意味着每个 <li> 的索引小于 stars 应该有 active 类。我们在 :class 指令中使用了一个表达式,当当前 star 小于总 star 数时,才会追加 active。同样条件下我们使用三元运算符来定义 Icon 组件使用的什么样的图标:star 或 star-o。 凉凉_ 翻译于 6个月前 0人顶 顶 翻译得不错哦! 那计数器呢? 现在我们的 star 列表是绑定到实际的数据,现在我们是时候对计数器也执行相同的操作。最简单的方法是使用带有 mustache 语法的文本插值: <span>{{ stars }} of {{ maxStars }}</span> 很简单,不是吗? 现在在这种况下,这是诀窍。 但是,如果我们需要一个更复杂的 JavaScript 表达式,最好将其抽象到一个计算属性中。 export default { ... computed: { counter() { return `${this.stars} of ${this.maxStars}` } } } /* ... */ <span>{{ counter }}</span> 在这里,这是矫枉过正。 我们可以避开模板内表达式,并保持可读性。然而,当你不得不处理更复杂的逻辑时,记住计算的属性。 另一件我们需要做的是提供一种方法来隐藏计数器,如果我们不需要它的时候。 最简单的方法是使用带有布尔值的 v-if 指令。 <span v-if="hasCounter">{{ stars }} of {{ maxStars }}</span> /* ... */ export default { ... data() { return { stars: 3, maxStars: 5, hasCounter: true } } } 凉凉_ 翻译于 6个月前 0人顶 顶 翻译得不错哦! 交互 我们差不多完成了,但是我们仍然需要实现组件中最有趣的部分:响应性。我们将使用 v-on,这是处理事件和方法的 Vue.js 指令,可以附加所有方法的 Vue.js 属性。 <template> ... <li @click="rate(star)" ...> ... </template> /* ... */ export default { ... methods: { rate(star) { // do stuff } } } 我们在 <li> 上添加了 @click 属性,这是 v-on:click 的简写。该指令包含对我们在组件的 methods 属性中定义的 rate 方法的调用。 “等一下...这看起来非常像熟悉的 HTML 的 onclick 属性。在 HTML 中使用内联 JavaScript 不是一个过时和不好的做法吗?“ 确实如此,但是即使语法看起来很像 onclick,但比较两者是一个错误。当你构建一个 Vue.js 组件时,你不应该把它看作是分离的 HTML/CSS/JS,而应该是一个使用多种语言的组件。当项目在浏览器中开启服务或编译生产时,所有的 HTML 和指令都被编译成普通的 JavaScript。如果您检查已渲染的 HTML,您将看不到您的指令的任何标志,也没有任何 onclick 属性。Vue.js 会编译好你的组件并创建合适的绑定。 这也是为什么您可以从模板访问组件的上下文的原因:因为指令绑定到视图模型。与具有单独 HTML 的传统项目相反,模板是组件的组成部分。 回到我们的 rate 方法。我们需要将 stars 变为 clicked 元素的索引,所以我们通过 @click 指令的索引,可以做到以下几点: export default { ... methods: { rate(star) { this.stars = star } } } 去查看您的浏览器页面,并尝试点击 star:它运行成功了! 凉凉_ 翻译于 6个月前 0人顶 顶 翻译得不错哦! 如果你打开浏览器开发者工具栏中的 Vue 面板并选择 <Rating> 组件,当你点击 star 时,你会看到数据的变化。这表明你的 star 属性是响应性的:当你改变它的时候,它会把它的改变指派给视图。 这个概念被称为数据绑定,如果您使用过 Backbone.js 或 Knockout 之类的框架,您应该熟悉这个概念。 不同之处在于,Vue.js 和 React 一样,只能在一个方向上进行:这就是所谓的单向数据绑定。不过这个话题值得写一篇单独的文章。 在这一点上,我们可以认为已完成 —— 但我们可以做更多的工作来改善用户体验。 现在,我们实际上不能给出 0 的等级,因为点击一个 star 会将它的比率设置为它的索引。更好的方案是重新点击同一颗 star,并切换至其当前状态,而不是保持 active 状态。 export default { ... methods: { rate(star) { this.stars = this.stars === star ? star - 1 : star } } } 现在,如果点击的 star 的索引等于 star 当前值,我们就减少它的值。 否则,我们给它分配 star 值。 如果我们想要彻底解决,我们还应该添加一个控制层,以确保 star 从来没有被赋予一个没有意义的值。我们需要确保 star 永远不会小于 0,也绝不会比 maxStars 更大,而且它是一个合适的数字。 export default { ... methods: { rate(star) { if (typeof star === 'number' && star <= this.maxStars && star >= 0) { this.stars = this.stars === star ? star - 1 : star } } } } 凉凉_ 翻译于 6个月前 0人顶 顶 翻译得不错哦! 传递 props 属性 现在,组件的数据在数据属性中被硬编码。如果我们希望我们的组件实际上是可用的,我们需要能够从其实例传递自定义数据。在 Vue.js 中,我们用 props 做到这一点。 export default { props: ['grade', 'maxStars', 'hasCounter'], data() { return { stars: this.grade } }, ... } 和在 main.js 文件里: new Vue({ el: '#app', template: '<Rating :grade="3" :maxStars="5" :hasCounter="true"/>', components: { Rating } }) 这里有三件事要注意: 首先,我们使用 v-bind 简写从组件实例传递 props 属性:这就是 Vue.js 所谓的动态语法。当你想要传递一个字符串值时,你不需要知道它的具体值,为此,字面值语法(没有 v-bind 的普通属性)将起作用。但对我们而言,由于我们正在传递数字和布尔值,所以这很重要。 props 和数据属性在编译时被合并,所以我们不需要改变在视图模型或模板中调用属性的方式。出于同样的原因,我们不能在 props 数据属性中使用相同的名称。 最后,我们定义了一个级别属性,并将其作为 star 数值属性中的值传递给它。我们之所以这样做,不是直接使用级别属性,而是因为级别改变,值会发生变化。在 Vue.js 中,props 从父级传递给子级,而不是反过来传递,所以你不会改变父级的状态。这将违背 单向数据流 的原则,使事情难以调试。这就是为什么你不应该试图改变子组件内的 prop。相反,定义一个使用 props 的初始值作为自声的本地数据属性。 最后的润色 在这一天马上过去之前,我们应该了解 Vue.js 最后一个惊奇的地方:prop 的验证。 Vue.js 允许你在传递给组件之前控制 prop。您可以执行四个主要的事情:检查类型,要求定义一个 prop 属性,设置默认值,并执行自定义验证。 export default { props: { grade: { type: Number, required: true }, maxStars: { type: Number, default: 5 }, hasCounter: { type: Boolean, default: true } }, ... } 我们使用类型检查来确保将正确类型的数据传递给组件。这将对我们忘记使用动态语法来传递非字符串值的错误特别有用。我们也确保通过要求它填写 grade 属性。对于其他 props 属性,我们定义了默认值,所以即使没有传递自定义数据,组件也能正常工作。 现在我们可以简单地通过执行以下操作来实例化组件: <Rating :grade="3"/> 就是这样!您刚刚创建了第一个 Vue.js 组件,并探索了许多概念 本文作者:凉凉_ 本文发布时间:2018年01月29日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
数字安全领域的全球领导者金雅拓(Euronext NL0000400653 GTO)日前宣布采用单模LTE Cat.1,通过LTE网络为工业应用提供高品质的语音功能,掀起了一场物联网(IoT)连接的革命。在圣克拉拉物联网世界前期活动上,金雅拓展示了其全球首个Cinterion LTE语音 (VoLTE) Category 1 (Cat.1) 模块,利用LTE将同步语音和数据功能相结合。该模块适用于升级到LTE并且想保留灵活且经济高效的语音功能的解决方案,包括安全和报警系统、移动医疗监测和智能家居及大楼应用。 该模块是当今市场所使用的最完整的机器类通信标准,是金雅拓日益庞大的LTE Cat. 1模块系列的新成员,能够提供4G的可靠性、长使用寿命和功率效率。该解决方案可简化最终用户的控制和语音功能,与传统VoIP业务相比,能够实现更大的成本效率并提高连接速度。 Strategy Analytics企业与物联网研究执行主管Andrew Brown 表示:“将语音集成到工业物联网应用不失为一种多功能的方式,可以优化物联网解决方案的人类交互和管控。金雅拓Cinterion VoLTE Cat.1解决方案拥有得天独厚的优势,能够为开发人员、设备制造商和终端用户提供比传统交互方式(如数据输入和触摸屏)更加灵活和更具成本优势的体验。” 金雅拓M2M产品组合和战略副总裁Axel Hansmann表示:“随着2G和3G网络日益被淘汰,业界快速向LTE迁移,以确保物联网技术投资的长效性。因此,预计VoLTE将成为实现节能物联网语音功能的必选标准。” 关于金雅拓 金雅拓(泛欧证券交易所 NL0000400653 GTO)是数字安全领域全球领导者,2015年的年营业额达31亿欧元,客户遍及180多个国家。金雅拓为这个日益互联的世界带来更多信任。 我们为企业和政府提供技术和服务用以验证身份及保护数据安全,并支持在个人终端、联网设备、云端及这些载体之间的服务。 金雅拓的解决方案定位于现代生活的核心领域,从支付到企业安全和物联网。我们对身份、交易以及设备进行验证,保护数据并为软件创造价值——助力我们的客户为数十亿人和设备提供安全的数字服务。 我们拥有1.4万名员工,分布在全球49个国家的118个办事处、45所个人化和数据中心,以及27个研究与软件开发中心。 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。
关键词:Promise,resolve,reject,Prepending,Resolve,Reject,then,catch,all,race 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。 Promise对象有以下两个特点。 (1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 (2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。 要理解Promise要知道没有Promise的回调地狱: 一般我们要在一个函数执行完之后执行另一个函数我们称之为callback‘回调’,简单的写一下: setTimeout(function(){ left(function(){ setTimeout(function(){ left(function(){ setTimeout(function(){ left(); },2000); }); }, 2000); }); }, 2000); 以上代码就是传说中的回调地狱,如果有多层业务逻辑嵌套的话,不仅会使代码阅读困难,而且后面维护起来也是难点。 之后在ES6,Promise就应运而生。 Promise语法与then的用法: var promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } }); resolve(value)是在Promise在已经异步完成成功(Resolved)之后执行的 reject(value)是在Promise在异步失败之后(Rejected)执行。 当然,也可以用then来指定:then(resolve,reject) 或者:then(resolve),catch(reject) promise.then(function(value) { // success }, function(error) { // failure }); //等价于: promise.then(function(){ //success }).catch(function(){ //failure }) 范例展示:写一个img图片加载示例点击 Tips 连续调用回调: 以刚开始的回调地狱为例子: setTimeout(function(){ left(function(){ setTimeout(function(){ left(function(){ setTimeout(function(){ left(); },2000); }); }, 2000); }); }, 2000); //我们给left函数内容换成console.log(11); var p = new Promise((resolve,reject)=>{ setTimeout( resolve , 2000 ) }) .then( ()=>setTimeout( null, 2000 ) ) .then( ()=>setTimeout(function(){ console.log(11) },2000) ) //这样在6秒钟之后会打出11 范例点击 总结: 可以采用连续的then链式操作来写回调(这是因为返回值一直是新的Promise实例)。 以上例子可以看出来只要在第一个promise回调中添加resolve,之后的连续then就会默认执行。 可以在then中return出数据,并且这个数据会以参数的形式传入下一个then。 var p = new Promise(function(resolve,reject){ var a=1 resolve(a); }).then(function(data){ console.log(data) return ++data; }).then( function(data){ console.log(data) } ) //打印出来的结果依次是: 1 2 。 接下来介绍一下catch() catch是用于指定发生错误时的回调函数。(建议不要在then的第二个参数写rejected状态,总是使用catch) catch()使回调报错时不会卡死js而是会继续往下执行。点击范例 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。 如: getJSON('/post/1.json').then(function(post) { return getJSON(post.commentURL); }).then(function(comments) { // some code }).catch(function(error) { // 处理前面三个Promise产生的错误 }); 用一段小代码来理解catch() var p = new Promise((resolve,reject)=> { n } ).then(()=>console.log('运行成功')) .catch( ()=>{a;console.log('报错');} )//这里我们没有定义a的值会报错 .catch( ()=> console.log('报错2') ) .then( ()=>console.log('报错后的回调') ) //运行结果是:'报错2' '报错后的回调' 首先n没有定义,所以第一层出错。下一个then的‘运行成功’不会被打出来。而是会被下一个catch捕获,第一个catch没有定义a,所以报错,console.log('报错')没办法打出来,又被下一个catch捕获: 第二个catch没有问题:打出‘报错2’。运行成功传给下一个then,打出'报错后的回调'。 这里要注意,不管是then或者catch返回的都是一个新的Promise实例!而每个Primise实例都有最原始的Pending(进行中)到Resolve(已完成),或者Pending(进行中)到Reject(已失败)的过程。 Promise.all() Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。 如: var p = Promise.all([p1, p2, p3]); all()接受数组作为参数。p1,p2,p3都是Promise的实例对象,p要变成Resolved状态需要p1,p2,p3状态都是Resolved,如果p1,p2,p3至少有一个状态是Rejected,p的状态就变成Rejected(个人感觉很想&&符号链接) Promise.race(); var p = new Promise( [p1,p2,p3] ) 上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。(感觉就是||符号操作~~~) Promise resolve(): 有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。 Promise.resolve等价于下面的写法。 Promise.resolve('foo') // 等价于 new Promise(resolve => resolve('foo')) Promise reject() Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。 Promise.reject('foo') // 等价于 new Promise(reject => reject('foo')) 注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。 本文作者:ShiYadong_ 本文发布时间:2018年06月29日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
爬过得那些坑 前言:在整个Vue的过程中,遇到了不少坑。查找不同的资料,把这些坑给填了,记录下这些坑,以及解决办法。 一、Http请求的那些坑 1.不支持http请求 表现为:程序启动正常,点击按妞不跳转,后台无响应,浏览器调试出现 Uncaught TypeError: Cannot read property 'post' of undefined 解决办法:添加vue-resource支持,在main.js添加 import VueResource from 'vue-resource' Vue.use(VueResource); 2.post请求,后台接收参数为null 表现为:后台响应但是参数为null,正确的登陆失效,调试时,参数为from object 解决办法:http请求中,添加 {emulateJSON:true} 全部的Http请求部分代码为 _this.$http.post('http://localhost:8080/person/login', { username: _this.username, password: _this.password } ,{emulateJSON:true} ) .then(function (response) { var errorcode = response.data.code; if (errorcode == "200") { _this.$router.push( { path: '/HelloWorld', query: { user: response.data.data, } }); } else { _this.$router.push({ path: '/Fail' }); } }) .catch(function (error) { console.log(error); }); 3、正确处理后,跳转到空页面 原因:路由的url配置有问题,注意组件的引用的对应关系以及path的路径问题 4.Request请求变成Options 解决办法:设置头格式 http: { headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'} }, 二、Vue视图之间的跳转实现 1.引用router组件 2.在router.js添加对应的view,以及相对应的path的配置 3.this.$router.push({path:'url'}) 4.可参照上文的Http请求部分代码 三、Vue跳转传递参数 采用编程式的实现方式 传递参数 _this.$router.push( { path: '/HelloWorld',//跳转的路径 query: {//query 代表传递参数 user: response.data.data,//参数名key,以及对应的value } }); 接收参数 this.$route.query.user user代表的参数名,不但可以传递单个参数,也可以传递对应,解析的方式user.属性 四、实例,登陆页面的Vue代码 <template> <div class="login"> {{ message }}<br/> <input v-model="username" placeholder="用户名"><br/> <input v-model="password" placeholder="密码"><br/> <button v-on:click="login">登陆 </button> </div> </template> <script> export default { name: "login", data() { return { message: 'Vue 登陆页面', username: '', password: '' } }, http: { headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'} }, methods: { login: function () { var _this = this; console.log(_this.username+_this.password); _this.$http.post('http://localhost:8080/person/login', { username: _this.username, password: _this.password } ,{emulateJSON:true} ) .then(function (response) { var errorcode = response.data.code; if (errorcode == "200") { _this.$router.push( { path: '/HelloWorld', query: { user: response.data.data, } }); } else { _this.$router.push({ path: '/Fail' }); } }) .catch(function (error) { console.log(error); }); } } } </script> <style scoped> </style> 本文作者:shenbug 本文发布时间:2018年03月13日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
前后端的结合 前言:前后端分离趋势越来越明显,分离的好处在于前后端可独立进行开发进行,前端写前端的代码,后端写后端的代码,后端提供相应的数据接口提供给前端。本文采用的是Vue+springboot的结合,做了一个登陆的demo,主要是理解前后端如何结合在一起,这里演示的为前后端在各自的服务器上运行,可参考前后端分离之Vue(一)Vue环境搭建,建立Vue项目 一、后端服务器的开发 后端采用的是SSM的框架结构进行改造,将前端部分交由Vue看来完成,只负责请求处理。这里只列举变化的部分,不变的部分springboot搭建的SSM结构即可,具体后端代码可参看https://github.com/dgyuanjun/SpringBoot-Vue.git 1.跨域的处理 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; /** * @author Administrator * @create 2018/3/12-15:17 * @DESCRIPTION 跨域系统配置 */ @Configuration public class CorsConfig { /** 允许任何域名使用 允许任何头 允许任何方法(post、get等) */ private CorsConfiguration buildConfig() { CorsConfiguration corsConfiguration = new CorsConfiguration(); // // addAllowedOrigin 不能设置为* 因为与 allowCredential 冲突,需要设置为具体前端开发地址 corsConfiguration.addAllowedOrigin("http://localhost:8000");//前端的开发地址 corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); // allowCredential 需设置为true corsConfiguration.setAllowCredentials(true); return corsConfiguration; } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", buildConfig()); return new CorsFilter(source); } } 2.统一API响应结果封装 import com.alibaba.fastjson.JSON; /** * @author Administrator * @create 2018/3/12-14:31 * @DESCRIPTION 统一API响应结果封装 */ public class RestResult { private int code;//状态码 private String message;//消息 private Object data;//数据 get.set方法 } 3.响应码的枚举 /** * @author Administrator * @create 2018/3/12-14:33 * @DESCRIPTION 响应码枚举,参考HTTP状态码的语义 */ public enum ResultCode { SUCCESS(200),//成功 FAIL(400),//失败 UNAUTHORIZED(401),//未认证(签名错误) NOT_FOUND(404),//接口不存在 INTERNAL_SERVER_ERROR(500);//服务器内部错误 private final int code; ResultCode(int code) { this.code = code; } public int code() { return code; } } 4.接口响应信息生成 import org.springframework.stereotype.Component; /** * 工厂模式 * 接口信息生成工具 * 。@Component 添加到Spring组件中 * Created by bekey on 2017/12/10. */ @Component public class ResultGenerator { private static final String SUCCESS = "success"; //成功 public RestResult getSuccessResult() { return new RestResult() .setCode(ResultCode.SUCCESS) .setMessage(SUCCESS); } //成功,附带额外数据 public RestResult getSuccessResult(Object data) { return new RestResult() .setCode(ResultCode.SUCCESS) .setMessage(SUCCESS) .setData(data); } //成功,自定义消息及数据 public RestResult getSuccessResult(String message,Object data) { return new RestResult() .setCode(ResultCode.SUCCESS) .setMessage(message) .setData(data); } //失败,附带消息 public RestResult getFailResult(String message) { return new RestResult() .setCode(ResultCode.FAIL) .setMessage(message); } //失败,自定义消息及数据 public RestResult getFailResult(String message, Object data) { return new RestResult() .setCode(ResultCode.FAIL) .setMessage(message) .setData(data); } //自定义创建 public RestResult getFreeResult(ResultCode code, String message, Object data) { return new RestResult() .setCode(code) .setMessage(message) .setData(data); } } 具体代码可参考:https://github.com/dgyuanjun/SpringBoot-Vue.git 二、Vue前端的开发 1.新建登陆页面,在components里,新建Login.vue <template> <div class="login"> {{ message }} <input v-model="username" placeholder="用户名"> <input v-model="password" placeholder="密码"> <button v-on:click="login">登陆 </button> </div> </template> <script> export default { name: "login", data() { return { message: 'Hello Vue!', username: '', password: '' } }, http: { headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'} }, methods: { login: function () { var _this = this; console.log(_this.username+_this.password); _this.$http.post('http://localhost:8080/person/login', { username: _this.username, password: _this.password },{emulateJSON:true} ) .then(function (response) { var errorcode = response.data.code; console.log(response.data.data) if (errorcode == "200") { _this.$router.push( { path: '/HelloWorld', query: { user: response.data.data, } }); } else { _this.$router.push({ path: '/Test' }); } }) .catch(function (error) { console.log(error); }); } } } </script> <style scoped> </style> 2.新建登陆失败的提示页面Fail.vue,成功的页面可采用原有的HelloWorld.vue <template> <div class="hello"> <h2>{{ msg }}</h2> </div> </template> <script> export default { name: 'HelloWorld', data () { return { msg: '登陆失败' } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style> 3.将组件添加到路由表中,在router下的index.js文件 import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld'//组件的位置 import Login from '@/components/Login' import Fail from '@/components/Fail' Vue.use(Router) export default new Router({ routes: [ { path: '/',//系统的首页面url name: 'Login', component: Login//对应上文的import },{ path: '/HelloWorld', name: 'HelloWorld', component: HelloWorld },{ path: '/Fail', name: 'Fail', component: Fail } ] }) 4.main.js文件添加vue-resource,支持http请求,如果未安装依赖包,先npm安装依赖 $ npm install vue-resource // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' import VueResource from 'vue-resource' Vue.use(VueResource); Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' }) 三、测试效果 1.登陆页面 2.成功后显示后台数据信息 3.登陆失败 本文作者:shenbug 本文发布时间:2018年03月13日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
一、前言 1、作为一个专注后台方向上的学习者,肯定不只是简单地处理业务逻辑而已 2、自己独立完成一个作品,一般都要涉及到几个界面的编写: ①登录界面 ②CRUD界面 ③主页 ④… 3、UI方面的简单的框架有很多,使用的过程也是深有感触: ①之前先是使用 EasyUI ,后来逐渐觉得其写出来的界面如明日黄花,简陋不已,写出来的界面都不好意思说是自己写的。 ②于是接着选择了最新最热门的基于 Vue2.0 的 ElementUI,为此还特地深入学习了 Vue,后来觉得有点笨重,并且其是侧重于前端工程化的,最好每次都使用 webpack 去开启,作为一个后台的,写好 CRUD、Controller 就已经花费了不少时间了,还把简单的界面处理得那么复杂,总共花费的时间太多成本太高了。 ③最后还是回到了 BootStrap,其实很早就接触它了,不过觉得这是新手的玩意,不愿意使用,但是后来觉得开发成本真的极低,且界面淳朴无邪~ ④当然中间还有使用了很多 UI 框架,如国内的贤心返璞归真的 LayUI,梁先生的基于 vue 的 iView ;特效框架如基于 CSS3 的 Animate 等等,感觉还行。 二、代码与效果 后面有心情的话,再把其他的界面补上 1、登录界面 ① 代码 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>larger5</title> </head> <link rel="stylesheet" href="bootstrap/css/bootstrap.css" /> <link rel="stylesheet" href="bootstrap/css/bootstrap-responsive.css" /> <script type="text/javascript" src="bootstrap/js/jQuery.js"></script> <script type="text/javascript" src="bootstrap/js/bootstrap.js"></script> <body> <div class="container"> <div class="row"> <div class=""> <img src="images/logo.png" /> <br /> <br /> <div> <form> <!--表单控件--> <input type="text" name="username" id="username" placeholder="用户名" class="form-control " /> <br /> <input type="password" name="password" id="password" placeholder="密码" class="form-control" /> <br /> <label class="radio-inline"> <input type="radio" name="inlineRadioOptions" id="inlineRadio1" value="option1" class="radio-inline"> boss </label> <label class="radio-inline"> <input type="radio" name="inlineRadioOptions" id="inlineRadio2" value="option2" class="radio-inline" > son </label> <br /> <br /> <input type="submit" value="登录" class="btn btn-success" /> </form> </div> </div> </div> </div> </body> </html> ② 效果 2、前端主页 <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"></meta> <title>larger5</title> <link rel="stylesheet" href="bootstrap/css/bootstrap.css" /> <link rel="stylesheet" href="bootstrap/css/bootstrap-responsive.css" /> <script type="text/javascript" src="bootstrap/js/jQuery.js"></script> <script type="text/javascript" src="bootstrap/js/bootstrap.js"></script> </head> <style type="text/css"> body { padding-top: 60px; padding-bottom: 40px; } .data_list { border: 1px solid #E5E5E5; padding: 10px; background-color: #FDFDFD; margin-top: 15px; } .data_list .data_list_title { font-size: 15px; font-weight: bold; border-bottom: 1px solid #E5E5E5; padding-bottom: 10px; padding-top: 5px; } .data_list .data_list_title img { vertical-align: top; } </style> <body> <!--导航条浮动,固定在上方,不会随着滚动条滑下而消失--> <div class="navbar navbar-fixed-top"> <div class="navbar-inner"> <div class="container"> <!--标题,字体稍微大点--> <a class="brand" href="#">larger5</a> <!--导航条具有响应式功能,主要是在屏幕较小的设备也能用--> <div class="nav-collapse collapse"> <!--导航标签,H5就有了--> <ul class="nav"> <li > <a href="#"><i class="icon-home"></i>&nbsp;主页</a> </li> <li > <a href="#"><i class="icon-pencil"></i>&nbsp;写博文</a> </li> <li> <a href="#"><i class="icon-book"></i>&nbsp;分类管理</a> </li> <li > <a href="#"><i class="icon-user"></i>&nbsp;个人中心</a> </li> </ul> </div> <!--导航条内的表单,居右--> <form name="myForm" class="navbar-form pull-right" method="post" action=""> <input class="span2" id="s_title" name="s_title" type="text" style="margin-top:5px;height:30px;" placeholder="文章标题"> <button type="submit" class="btn" onkeydown="if(event.keyCode==13) myForm.submit()"><i class="icon-search"></i>&nbsp;搜索</button> </form> </div> </div> </div> <div class="container"> <!--流式,按百分比分列--> <div class="row-fluid"> <div class="span9"> <!--这里的内容经常会变化--> <div class="data_list"> <div class="data_list_title"> <img src="images/list_icon.png" /> 最新资讯 </div> </div> </div> <div class="span3"> <div class="data_list"> <div class="data_list_title"> <img src="images/user_icon.png" /> 个人中心 </div> </div> <div class="data_list"> <div class="data_list_title"> <img src="images/byType_icon.png" /> 按类别 </div> </div> <div class="data_list"> <div class="data_list_title"> <img src="images/byDate_icon.png" /> 按日期 </div> </div> </div> </div> </div> </body> </html> 3、后台 CRUD 界面 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>后台管理</title> <link rel="stylesheet" href="bootstrap3/css/bootstrap-theme.min.css" /> <link rel="stylesheet" href="bootstrap3/css/bootstrap.min.css" /> <script type="text/javascript" src="bootstrap3/js/jquery-1.11.2.min.js"></script> <script type="text/javascript" src="bootstrap3/js/bootstrap.min.js"></script> </head> <style type="text/css"> body { padding: 20px; } .headLeft { float: left; } .headRight { padding-top: 40px; padding-left: 340px; } .search { margin-bottom: 10px; } .search .toolbar {} </style> <!--必须使用较新版本的BootStrap才有如下效果--> <body> <div class="container"> <div class="row"> <div class="col-md-12"> <nav class="navbar navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand " href="#">后台管理系统</a> </div> <div id="navbar" class="navbar-right"> <a class="navbar-brand" href="#">当前用户:xx</a> </div> </div> </nav> </div> </div> <div class="row" style="padding-top: 45px"> <div class="col-md-3"> <div class="list-group"> <!--激活,作为标题--> <a href="#" class="list-group-item active "> 系统菜单 </a> <a href="#" class="list-group-item">管理一</a> <a href="#" class="list-group-item">管理二</a> <a href="#" class="list-group-item">管理三</a> <a href="#" class="list-group-item">管理四</a> <a href="#" class="list-group-item">退出</a> </div> </div> <div class="col-md-9"> <div> <ol class="breadcrumb"> <li><span class="glyphicon glyphicon-home"></span>&nbsp; <a href="#">主页</a> </li> <li class="active"></li> </ol> </div> <div align="center" style="padding-top: 50px;"> <h1>后台管理系统</h1> </div> </div> </div> <div class="row"> <div class="col-md-12"> <div align="center" style="padding-top: 200px"> Copyright © 2017-2018 larger5 </div> </div> </div> </div> </body> </html> 效果: 4、… (有心情再写~) 三、小结 1、做后台做烦了,可以试着去写点前端的东西,前端专注与视觉的建设,能让你带愉悦的心情重返后台的工作~ 2、BootStrap3 官网 本文作者:larger5 本文发布时间:2018年02月27日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
本章节主要讲计算后样式和测量的相关知识点。 我们可以通过dom提供的可靠的api,去获取样式计算后最终的值。 计算后样式 获取计算后的样式的方法有以下几种 window.getComputedStyle("对象").getPropertyValue("属性");//window可以忽略 getComputedStyle("对象")["属性"]; 对象.currentStyle.属性;/对象.currentStyle[属性] 我们来分析一下三者的使用区别 -----getComputedStyle("对象").getPropertyValue("属性") 该方法无法兼容到IE6 7 8 ,后面的属性同css样式一样的写法 -----getComputedStyle("对象")["属性"]; 该方法同样也无法兼容到IE6 7 8,后面的属性可以用css名字,也可以用到驼峰式。驼峰式如background-color要写成backgroundColor ------对象.currentStyle.属性/对象.currentStyle[属性] 对象.currentStyle.属性/对象.currentStyle[属性] 该方法用于IE6 7 8,后面的属性也需要驼峰式。 ----------------------------------------------------------------- 那么有人会问,如果去兼容到IE6 7 8要怎么做?这时候我们就需要进行能力检测。 能力检测 if(window.getComputedStyle){ 大于IE8的获取计算后样式的方法 }else{ 小于IE6 7 8 获取计算后样式的方法 } 以上就是获取计算后样式兼容性问题的解决方案,if里面的条件就是能力检测,去检测当前浏览器是否有getComputedStyle方法,没有也就是意味着IE版本小于等于8,则使用小于IE6 7 8的方法。下面我们来写一个封装一个能力检测获取计算后样式的方法。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> div { padding: 10px 20px 30px 40px; } </style> </head> <body> <div></div> <script type="text/javascript"> var div = document.getElementsByTagName("div")[0]; /** obj元素,property属性:可能是css样式的,可能是驼峰式的需要做处理 **/ function isComputedStyle(obj,property){ if (window.getComputedStyle) {//能力检测 return getComputedStyle(obj)[property];//因为css样式或者是驼峰式都可以 }else{ if (property.indexOf("-")) {//非驼峰式需要转成驼峰式 property = property.replace(/\-([a-z])/g,function(match,$1){ return $1.toUpperCase(); }) } return obj.currentStyle[property]; } } console.log(isComputedStyle(div,"padding-left")); </script> </body> </html> 透明度 透明度在css样式中有两种设置方法,和js中设置的方法 opacity:0.5; ---->元素.style.opacity = 0.5; fliter:alpha(opacity=50); ---->元素.style.fliter = "alpha(opactity="+50+");"; 所以在通过计算后样式的时候,可能会有顾虑,其实高版本就要高版本的获取方式去获取opacity,低版本就用低版本的获取方法去获取opacity就可以算兼容,前提是opacity的值于fliter的值相等即可 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> div { width: 300px; height: 300px; background: #000; opacity: 0.2; filter: alpha(opacity=20); /*为了兼容IE*/ } </style> </head> <body> <div> </div> <script type="text/javascript"> // 获取div的透明度 var oDiv = document.getElementsByTagName("div")[0]; alert(getComputedStyle(oDiv)["opacity"]); //0.2 高版本浏览器 // alert(oDiv.currentStyle.opacity); // IE6 7 8 .2 </script> </body> </html> 就可以做到兼容。 offsetLeft/offsetTop 因为offsetTop同offsetLeft一样,故在此我们就讲解offsetLeft offsetLeft表示这个元素的左边距的外面到offsetParent的左边距的内侧的距离。 什么是offsetParent?offsetParent表示参照物,那么offsetParent参照的依据是什么呢?这里就是要为不同的浏览器的版本进行分析。 IE9以及chrome等高级浏览器 自己祖先元素中,离自己最近的且设置了定位的元素,若都没有则以body为offsetparent。 IE8 同高级浏览器一样,但是需要多算上一条border的宽度(父亲的边框) IE6 7 第一种情况,自己有定位的,则offsetParent就是离自己最近的且有定位的元素 第二种情况,自己没有定位的,则offsetParent就是离自己最近的且有width/height的元素 想要去兼容这三种情况要怎么去做? 自己定位,父无边宽,且父设置了定位。 offsetWidth/offsetHeight 表示自己当前元素的宽高。(都兼容) offsetWidth = 左右border+width+左右padding offsetHeight = 上下border+height+上下padding 假如没有设置宽度和高度,则width和height有内容撑开。 clientWidth/clientHeight 也是表示自己当前元素的宽高。(都兼容 IE6有点小问题) clientWidth = width+左右padding clientHeight = height+上下padding IE6 假如没有设置宽高的话,clientHeight的高度为0,其他浏览器都是正常的 定时器 1.setInterval(function(){},time) 2.setTimeout(function(){},time) setInterval 表示每个一段时间去调用方法。对应的清除方法是如下 var timer = setInterval(function() { }, 50); clearInterval(timer) setTimeout 表示延迟多少时间去调用方法,方法只调用一次。 var timer = setTimeout(function(){ console.log("setTimeout"); }, 1000); clearTimeout(timer) 那么我们就可以通过定时器,去给修改元素的值,从而变了动画。下面我们来看看demo <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> div { width: 100px; height: 100px; background-color: pink; position: absolute; left: 60px; top: 40px; } </style> </head> <body> <div class="box" id="box"></div> <script type="text/javascript"> // 页面一打开,div运动到702px自动停止 var box = document.getElementById("box"); var num = 60; var timer = setInterval(function() { num+=8; if(num>=702) { // 拉终停表 num = 702; clearInterval(timer); } box.style.left = num + "px"; }, 30); </script> </body> </html> 以上demo就是用过定时器去不断的改变left,当left到了的值大于702的时候,就要去解绑定时器。 可以通过定时器去写一个自动无缝的轮播图,还有点击暂停再点击播放的功能。效果图如下。 有几个注意点,从最后一张到第一张图片的过渡,每一张图片的无缝。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style type="text/css"> *{ margin: 0; padding: 0; } .banner{ width: 600px; height: 130px; overflow:hidden; border: 1px solid black; margin: 0 auto; position: relative; } .banner .content { width: 5400px; height: 130px; position: absolute; } ul{ list-style: none; } .banner .content li{ float: left; } .banner .content li img{ width: 600px; height: 130px; } </style> </head> <body> <div class="banner" id="banner"> <div class="content" id="content"> <ul> <li><a href=""><img src="img/0.png"></a></li> <li><a href=""><img src="img/1.png"></a></li> <li><a href=""><img src="img/2.png"></a></li> <li><a href=""><img src="img/3.png"></a></li> <li><a href=""><img src="img/4.png"></a></li> <li><a href=""><img src="img/5.png"></a></li> <li><a href=""><img src="img/6.png"></a></li> <li><a href=""><img src="img/7.png"></a></li> <li><a href=""><img src="img/0.png"></a></li> </ul> </div> </div> <script type="text/javascript"> var banner = document.getElementById("banner"); var content = document.getElementById("content"); var img_list = content.getElementsByTagName("li"); var move_num = 0; var timer; move(); for(var i = 0;i<img_list.length;i++){ img_list[i].onclick=function(){ if (timer!=undefined) { clearInterval(timer); timer = undefined; }else{ move(); } } } function move (){ timer = setInterval(function(){ move_num -=5; if (move_num<=-600*(img_list.length-1)) { move_num = 0; // clearInterval(timer); } content.style.left = move_num+"px"; },10); } </script> </body> </html> Json 与后台的大部分数据交互都是json格式。 var person = { "name":"heihei", "age":18, "sex":"m", "wife": { "name": "Angelababy", "age":16, "shengao":168 } }; 其中name就是key,heihei就是value,key必须是字符串带着双引号的。 如何去获取key对应的value?如获取name对应的值 person.name / person["name"]这两种方式都可以去获取。 如何去给person添加新的值呢? person.hobby=["haha","heihei"];直接添加即可 如何去删除某个属性 delete person.age 如何循环的获取person的k,value? for(var k in person) { alert(k);// 代表某个属性 alert(person[k]); // 属性值 } 同样json对象还能存放方法 person.myfun=function(){ console.log("hahaha")};直接添加即可 js的dom对象的基本知识已讲完,接下来会来讲自己封装一个动画运动框架,如有表达错的请谅解,并请提出指出,且修改错误,望能共同进步。 本文作者:gaoyouhuang 本文发布时间:2018年06月23日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
什么是跨域? 跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。 广义的跨域: 1.) 资源跳转: A链接、重定向、表单提交 2.) 资源嵌入: <link>、<script>、<img>、 <frame>等dom标签, 还有样式中background:url()、 @font-face()等文件外链 3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等 其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。 什么是同源策略? 同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。 同源策略限制以下几种行为: 1.) Cookie、LocalStorage 和 IndexDB 无法读取 2.) DOM 和 Js对象无法获得 3.) AJAX 请求不能发送 常见跨域场景 跨域解决方案 1、 通过jsonp跨域 2、 document.domain + iframe跨域 3、 location.hash + iframe 4、 window.name + iframe跨域 5、 postMessage跨域 6、 跨域资源共享(CORS) 7、 nginx代理跨域 8、 nodejs中间件代理跨域 9、 WebSocket协议跨域 一、 通过jsonp跨域 通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。 1.)原生实现: <script> var script = document.createElement('script'); script.type = 'text/javascript'; // 传参并指定回调执行函数为onBack script.src = 'http://www.domain2.com:8080/ login?user=admin&callback=onBack'; document.head.appendChild(script); // 回调执行函数 function onBack(res) { alert(JSON.stringify(res)); } </script> 服务端返回如下(返回时即执行全局函数): onBack({"status": true, "user": "admin"}) 2.)jquery ajax: $.ajax({ url: 'http://www.domain2.com:8080/login', type: 'get', dataType: 'jsonp', // 请求方式为jsonp jsonpCallback: "onBack", // 自定义回调函数名 data: {} }); 3.)vue.js: this.$http.jsonp(' http://www.domain2.com:8080/login', { params: {}, jsonp: 'onBack'}).then((res) => { console.log(res); }) 后端node.js代码示例: var querystring = require('querystring'); var http = require('http'); var server = http.createServer(); server.on('request', function(req, res) { var params = qs.parse(req.url.split('?')[1]); var fn = params.callback; // jsonp返回设置 res.writeHead(200, { 'Content-Type': 'text/javascript' }); res.write(fn + '(' + JSON.stringify(params) + ')'); res.end(); }); server.listen('8080'); console.log('Server is running at port 8080...'); jsonp缺点:只能实现get一种请求。 二、 document.domain + iframe跨域 此方案仅限主域相同,子域不同的跨域应用场景。 实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。 1.)父窗口:(http://www.domain.com/a.html) <iframe id="iframe" src="http://child.domain.com/b.html"> </iframe><script> document.domain = 'domain.com'; var user = 'admin';</script> 2.)子窗口:(http://child.domain.com/b.html) <script> document.domain = 'domain.com'; // 获取父窗口中变量 alert('get js data from parent ---> ' + window.parent.user); </script> 三、 location.hash + iframe跨域 实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。 具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。 1.)a.html:(http://www.domain1.com/a.html) <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"> </iframe> <script> var iframe = document.getElementById('iframe'); // 向b.html传hash值 setTimeout(function() { iframe.src = iframe.src + '#user=admin'; }, 1000); // 开放给同域c.html的回调方法 function onCallback(res) { alert('data from c.html ---> ' + res); } </script> 2.)b.html:(http://www.domain2.com/b.html) <iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"> </iframe> <script> var iframe = document.getElementById('iframe'); // 监听a.html传来的hash值,再传给c.html window.onhashchange = function () { iframe.src = iframe.src + location.hash; }; </script> 3.)c.html:(http://www.domain1.com/c.html) <script> // 监听b.html传来的hash值 window.onhashchange = function () { // 再通过操作同域a.html的js回调, 将结果传回 window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', '')); }; </script> 四、 window.name + iframe跨域 window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。 1.)a.html:(http://www.domain1.com/a.html) var proxy = function(url, callback) { var state = 0; var iframe = document.createElement('iframe'); // 加载跨域页面 iframe.src = url; // onload事件会触发2次, 第1次加载跨域页, 并留存数据于window.name iframe.onload = function() { if (state === 1) { // 第2次onload(同域proxy页)成功后, 读取同域window.name中数据 callback(iframe.contentWindow.name); destoryFrame(); } else if (state === 0) { // 第1次onload(跨域页)成功后, 切换到同域代理页面 iframe.contentWindow.location = 'http://www.domain1.com/proxy.html'; state = 1; } }; document.body.appendChild(iframe); // 获取数据以后销毁这个iframe, 释放内存; 这也保证了安全(不被其他域frame js访问) function destoryFrame() { iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } }; // 请求跨域b页面数据proxy ('http://www.domain2.com/b.html', function(data) { alert(data); }); 2.)proxy.html:(http://www.domain1.com/proxy.... 中间代理页,与a.html同域,内容为空即可。 3.)b.html:(http://www.domain2.com/b.html) <script> window.name = 'This is domain2 data!'; </script> 总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。 五、 postMessage跨域 postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题: a.) 页面和其打开的新窗口的数据传递 b.) 多窗口之间消息传递 c.) 页面与嵌套的iframe消息传递 d.) 上面三个场景的跨域数据传递 用法:postMessage(data,origin)方法接受两个参数 data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。 origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。 1.)a.html:(http://www.domain1.com/a.html) <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"> </iframe> <script> var iframe = document.getElementById('iframe'); iframe.onload = function() { var data = { name: 'aym' }; // 向domain2传送跨域数据 iframe.contentWindow.postMessage (JSON.stringify(data), 'http://www.domain2.com'); }; // 接受domain2返回数据 window.addEventListener ('message', function(e) { alert('data from domain2 ---> ' + e.data); }, false); </script> 2.)b.html:(http://www.domain2.com/b.html) <script> // 接收domain1的数据 window.addEventListener ('message', function(e) { alert('data from domain1 ---> ' + e.data); var data = JSON.parse(e.data); if (data) { data.number = 16; // 处理后再发回domain1 window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com'); } }, false);</script> 六、 跨域资源共享(CORS) 普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。 需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:七、nginx反向代理中设置proxy_cookie_domain 和 八、NodeJs中间件代理中cookieDomainRewrite参数的设置。 目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。 1、 前端设置: 1.)原生ajax // 前端设置是否带cookiexhr.withCredentials = true; 示例代码: var xhr = new XMLHttpRequest(); // IE8/9需用 window.XDomainRequest兼容 // 前端设置是否带 cookiexhr.withCredentials = true; xhr.open('post', 'http://www.domain2.com:8080/login', true); xhr.setRequestHeader ('Content-Type', ' application/x-www-form-urlencoded'); xhr.send('user=admin'); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { alert(xhr.responseText); } }; 2.)jQuery ajax $.ajax({ ... xhrFields: { withCredentials: true // 前端设置是否带cookie }, crossDomain: true, // 会让请求头中包含跨域的额外信息, 但不会含cookie ... }); 3.)vue框架 在vue-resource封装的ajax组件中加入以下代码: Vue.http.options.credentials = true 2、 服务端设置: 若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。 1.)Java后台: /* * 导入包: import javax.servlet.http.HttpServletResponse; * 接口参数中定义: HttpServletResponse response */response.setHeader ("Access-Control-Allow-Origin", "http://www.domain1.com"); // 若有端口需写全(协议+域名+端口) response.setHeader ("Access-Control-Allow-Credentials", "true"); 2.)Nodejs后台示例: var http = require('http');var server = http.createServer();var qs = require('querystring'); server.on('request', function(req, res) { var postData = ''; // 数据块接收中 req.addListener('data', function(chunk) { postData += chunk; }); // 数据接收完毕 req.addListener('end', function() { postData = qs.parse(postData); // 跨域后台设置 res.writeHead(200, { 'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie 'Access-Control-Allow-Origin': 'http://www.domain1.com', // 允许访问的域(协议+域名+端口) 'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com; HttpOnly' // HttpOnly:脚本无法读取cookie }); res.write(JSON.stringify(postData)); res.end(); }); }); server.listen('8080');console.log('Server is running at port 8080...'); 七、 nginx代理跨域 1、 nginx配置解决iconfont跨域 浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。 location / { add_header Access-Control-Allow-Origin *; } 2、 nginx反向代理接口跨域 跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。 实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。 nginx具体配置: #proxy服务器server { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 index index.html index.htm; # 当用webpack-dev-server等中间件代理接口访问nignx时, 此时无浏览器参与, 故没有同源限制, 下面的跨域配置可不启用 add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时, 可为* add_header Access-Control-Allow-Credentials true; } } 1.) 前端代码示例: var xhr = new XMLHttpRequest(); // 前端开关: 浏览器是否读写cookiexhr.withCredentials = true; // 访问nginx中的代理服务器 xhr.open('get', 'http://www.domain1.com:81/?user=admin', true); xhr.send(); 2.) Nodejs后台示例: var http = require('http'); var server = http.createServer(); var qs = require('querystring'); server.on('request', function(req, res) { var params = qs.parse(req.url.substring(2)); // 向前台写cookie res.writeHead(200, { 'Set-Cookie': 'l=a123456; Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取 }); res.write(JSON.stringify(params)); res.end(); }); server.listen('8080'); console.log('Server is running at port 8080...'); 八、 Nodejs中间件代理跨域 node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。 1、 非vue框架的跨域(2次跨域) 利用node + express + http-proxy-middleware搭建一个proxy服务器。 1.)前端代码示例: var xhr = new XMLHttpRequest(); // 前端开关: 浏览器是否读写cookiexhr.withCredentials = true; // 访问http-proxy-middleware代理服务器 xhr.open('get', ' http://www.domain1.com:3000/login? user=admin', true); xhr.send(); 2.)中间件服务器: var express = require('express'); var proxy = require('http-proxy-middleware'); var app = express(); app.use('/', proxy({ // 代理跨域目标接口 target: 'http://www.domain2.com:8080', changeOrigin: true, // 修改响应头信息,实现跨域并允许带cookie onProxyRes: function(proxyRes, req, res) { res.header('Access-Control-Allow-Origin', 'http://www.domain1.com'); res.header('Access-Control-Allow-Credentials', 'true'); }, // 修改响应信息中的cookie域名 cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改})); app.listen(3000);console.log(' Proxy server is listen at port 3000...'); 3.)Nodejs后台同(六:nginx) 2、 vue框架的跨域(1次跨域) 利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。 webpack.config.js部分配置: module.exports = { entry: {}, module: {}, ... devServer: { historyApiFallback: true, proxy: [{ context: '/login', target: 'http://www.domain2.com:8080', // 代理跨域目标接口 changeOrigin: true, secure: false, // 当代理某些https服务报错时用 cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改 }], noInfo: true } } 九、 WebSocket协议跨域 WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。 原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。 1.)前端代码: <div>user input: <input type="text"> </div> <script src="./socket.io.js"> </script> <script>var socket = io(' http://www.domain2.com:8080'); // 连接成功处理 socket.on('connect', function() { // 监听服务端消息 socket.on('message', function(msg) { console.log('data from server: ---> ' + msg); }); // 监听服务端关闭 socket.on('disconnect', function() { console.log('Server socket has closed.'); }); }); document.getElementsByTagName('input') [0].onblur = function() { socket.send(this.value); }; </script> 2.)Nodejs socket后台: var http = require('http'); var socket = require('socket.io'); // 启http服务 var server = http.createServer(function(req, res) { res.writeHead(200, { 'Content-type': 'text/html' }); res.end(); }); server.listen('8080'); console.log('Server is running at port 8080...'); // 监听socket连接 socket.listen(server).on('connection', function(client) { // 接收信息 client.on('message', function(msg) { client.send('hello:' + msg); console.log('data from client: ---> ' + msg); }); // 断开处理 client.on('disconnect', function() { console.log('Client socket has closed.'); }); }); 完~ 本文作者:web前端开发V 本文发布时间:2018年03月07日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
开篇 有30个CSS选择器你必须烂熟于心,它们适应于当今各大主流浏览器。 1.* * { margin: 0; padding: 0; } 1 2 3 4 *选择器选择的是每一个单一元素。很多程序员用上面的CSS将所有元素的margin和padding清零。虽然这是有效的,但最好还是别这么做,这会使得浏览器的负担很重。 *选择器也可以用在孩子选择器中。 #container * { border: 1px solid black; } 1 2 3 这会使#container所有孩子都有border,但还是那句话,如果不是必须得这么做,还是别用星选择器。 view demo Compatibility IE6+ Firefox Chrome Safari Opera 2.#x #container { width: 960px; margin: auto; } 1 2 3 4 id选择器的优先级很高,因此在用之前问问自己:我仅仅是为了找到这个元素而去给他加id的吗? view demo Compatibility IE6+ Firefox Chrome Safari Opera 3..x .error { color: red; } 1 2 3 class选择器和id选择器不同,首先优先级没有id高,再者id选择器只能选择一个,而class选择器则可以筛选出来一组。 view demo Compatibility IE6+ Firefox Chrome Safari Opera 4.x y li a { text-decoration: none; } 1 2 3 当不是选择所有后代时,后代选择器将会非常有用,但有一点得注意: 如果你的选择器是x y z a b.error, 那你就用错了。你得问问自己是否需要应用每一层? view demo Compatibility IE6+ Firefox Chrome Safari Opera 5.x a { color: red; } ul { margin-left: 0; } 1 2 如果想选择同一类元素,那就不要用id或class了,直接用元素选择器。 view demo Compatibility IE6+ Firefox Chrome Safari Opera 6.x:visted and x:link a:link { color: red; } a:visted { color: purple; } 1 2 我们常常用伪类:link筛选a标签是还未被点击;而用:visited去筛选哪些别点击过了。 view demo Compatibility IE7+ Firefox Chrome Safari Opera 7.x + y ul + p { color: red; } 1 2 3 相邻选择器会选择第一个相邻的元素,如上面的例子会让ul后第一个段落的字体变为红色(而ul与p之间是不能有其他元素的)。 view demo Compatibility IE7+ Firefox Chrome Safari Opera 8.x > y div#container > ul { border: 1px solid black; } 1 2 3 这也是一种后代选择器,但它与x y这种后代选择器不同,它只能选择直系后代。如: <div id="container"> <ul> <li> List Item <ul> <li> Child </li> </ul> </li> <li> List Item </li> <li> List Item </li> <li> List Item </li> </ul> </div> 1 2 3 4 5 6 7 8 9 10 11 12 在这个例子中,#cotainer > ul只会选择第一个ul,而不会search到ul里面那个包含li的ul。 view demo Compatibility IE7+ Firefox Chrome Safari Opera 9.x ~ y ul ~ p { color: red; } 1 2 3 这种兄弟选择器与x + y类似,但不同的是,后者只能筛选第一个p,而这种却可以满足ul下所有的直系p标签。 view demo Compatibility IE7+ Firefox Chrome Safari Opera 10.x[title] a[title] { color: green; } 1 2 3 属性选择器。这将选择所有具有title属性的a标签。 view demo Compatibility IE7+ Firefox Chrome Safari Opera 11.x[href="foo"] a[href="http://net.tutsplus.com"] { color: #1f6053; /* nettuts green */ } 1 2 3 这个选择器可以选择链接为href="http://net.tutsplus.com"的a标签,可如果这个里这个链接变了呢?,这未免有些严格,可以适当的用正则表达式去匹配。 view demo Compatibility IE7+ Firefox Chrome Safari Opera 12.x[href*="nettuts"] a[href*="tuts"] { color: #1f6053; /* nettuts green */ } 1 2 3 ‘*’号将匹配href中含有nuttuts字符,如果想更加严格,还可以用^和$表示开始和结束。 view demo Compatibility IE7+ Firefox Chrome Safari Opera 13.x[href^="http"] a[href^="http"] { background: url(path/to/external/icon.png) no-repeat; padding-left: 10px; } 1 2 3 4 这样去筛选具有有效href的a将匹配http://和https://. view demo Compatibility IE7+ Firefox Chrome Safari Opera 14.x[href$=".jpg"] a[href$=".jpg"] { color: red; } 1 2 3 这将会选择链接为jpg格式的图片链接,可是如果图片类型为png或gif等怎么办? view demo Compatibility IE7+ Firefox Chrome Safari Opera 15.x[data-*="foo"] a[data-filetype="image"] { color: red; } 1 2 3 按照规则14.我们可能得: a[href$=".jpg"], a[href$=".jpeg"], a[href$=".png"], a[href$=".gif"] { color: red; } 1 2 3 4 5 6 可这也太。。。 我们可以加一个属性用以标示。 <a href="path/to/image.jpg" data-filetype="image"> Image Link </a> 1 a[data-filetype="image"] { color: red; } 1 2 3 view demo Compatibility IE7+ Firefox Chrome Safari Opera 16.x[foo~="bar"] a[data-info~="external"] { color: red; } a[data-info~="image"] { border: 1px solid black; } 1 2 3 4 5 6 7 ~将会让我们匹配到属性值被空格分隔符分开的值,如: "<a href="path/to/image.jpg" data-info="external image"> Click Me, Fool </a> 1 view demo Compatibility IE7+ Firefox Chrome Safari Opera 17.x:checked input[type=radio]:checked { border: 1px solid black; } 1 2 3 这个常常对checkbox非常有用。 view demo Compatibility IE9+ Firefox Chrome Safari Opera 18.x:after 伪类before和after已经有了一些新的用法,比如最常见的: .clearfix:after { content: ""; display: block; clear: both; visibility: hidden; font-size: 0; height: 0; } .clearfix { *display: inline-block; _height: 1%; } 1 2 3 4 5 6 7 8 9 10 11 12 13 没错,这就是默认标准clearfix的实现原理。 Compatibility IE8+ Firefox Chrome Safari Opera 19.x:hover div:hover { background: #e3e3e3; } 1 2 3 但是得注意,:hover在早期IE中并不适用。 Compatibility IE6+(In IE6, :hover must be applied to an anchor element) Firefox Chrome Safari Opera 20.x:not(selector) div:not(#container) { color: blue; } 1 2 3 反选选择器。 view demo Compatibility IE9+ Firefox Chrome Safari Opera 21.x::pseudoElement p::first-line { font-weight: bold; font-size: 1.2em; } p::first-letter { float: left; font-size: 2em; font-weight: bold; font-family: cursive; padding-right: 2px; } 1 2 3 4 5 6 7 8 9 10 11 12 伪元素选择器,注意尽量还是按标准来,多使用::而不是:。 view demo Compatibility IE6+ Firefox Chrome Safari Opera 22.x:nth-child(n) li:nth-child(3) { color: red; } 1 2 3 选择一组中特定的孩子。n表示第几个,也可以是表达式,如2n+1,2n. view demo Compatibility IE9+ Firefox 3.5+ Chrome Safari Opera 23.x:nth-last-child(n) li:nth-last-child(2) { color: red; } 1 2 3 如果li有400个,而你需要target到第397个,那用这个咱合适不过了。 view demo Compatibility IE9+ Firefox 3.5+ Chrome Safari Opera 24.x:nth-of-type(n) ul:nth-of-type(3) { border: 1px solid black; } 1 2 3 如果ul没有id,而你又要找第三个ul,那个这种方式是不错。 view demo Compatibility IE9+ Firefox 3.5+ Chrome Safari Opera 25.x:nth-last-of-type(n) ul:nth-last-of-type(3) { border: 1px solid black; } 1 2 3 与ul:nth-of-type刚好相反。 Compatibility IE9+ Firefox 3.5+ Chrome Safari Opera 26.x:first-child ul li:first-child { border-top: none; } 1 2 3 view demo Compatibility IE7+ Firefox Chrome Safari Opera 27.x:last-child Example <ul> <li> List Item </li> <li> List Item </li> <li> List Item </li> </ul> 1 2 3 4 5 ul { width: 200px; background: #292929; color: white; list-style: none; padding-left: 0; } li { padding: 10px; border-bottom: 1px solid black; border-top: 1px solid #3c3c3c; } 1 2 3 4 5 6 7 8 9 10 11 12 13 view demo 但是我不想要第一个的上边框和最后一个的下边框,可以这么做: li:first-child { border-top: none; } li:last-child { border-bottom: none; } 1 2 3 4 5 6 7 view demo Compatibility IE9+ Firefox Chrome Safari Opera 28.X:only-child div p:only-child { color: red; } 1 2 3 这将匹配div里只有一个p的元素。如: <div><p> My paragraph here. </p></div> <div> <p> Two paragraphs total. </p> <p> Two paragraphs total. </p> </div> 1 2 3 4 5 6 view demo Compatibility IE9+ Firefox Chrome Safari Opera 29.X:only-of-type li:only-of-type { font-weight: bold; } 1 2 3 这将匹配元素内只有一个li的元素,有时这个做法很简便。比如要寻找只有一个列表的ul,可以: ul > li:only-of-type { font-weight: bold; } 1 2 3 view demo Compatibility IE9+ Firefox 3.5+ Chrome Safari Opera 30.x:first-of-type Example <div> <p> My paragraph here. </p> <ul> <li> List Item 1 </li> <li> List Item 2 </li> </ul> <ul> <li> List Item 3 </li> <li> List Item 4 </li> </ul> </div> 1 2 3 4 5 6 7 8 9 10 11 12 如何寻找上面的 “List Item 2”呢? 办法1 ul:first-of-type > li:nth-child(2) { font-weight: bold; } 1 2 3 办法2 p + ul li:last-child { font-weight: bold; } 1 2 3 办法3 ul:first-of-type li:nth-last-child(1) { font-weight: bold; } 1 2 3 view demo Compatibility IE9+ Firefox 3.5+ Chrome Safari Opera 总结 上述选择器在IE6中的使用要尤其小心,但是别因为这而影响你阅读这篇文章。你可以参考一下浏览器兼容表,或者你可以用 Dean Edward’s excellent IE9.js script 为旧版浏览器提供这些选择器的支持。 另外,当你在使用一些Javascript库时,如jQuery,请尽量的使用这些原生的CSS3选择器,因为浏览器选择器引擎将会按照浏览器的原生方式去解析,这将使得你的代码更加高效。 本文作者:Jason_Jiagen 本文发布时间:2018年06月14日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
记录以下自己将web app打包成移动端app的步骤及问题 事先准备,开发完成的web app项目(也可以利用vue-cli脚手架构建vue模板项目),npm run dev可以正常预览的项目 1,将项目目录下config文件内index.js中assetsPublicPath修改为 assetsPublicPath: './' 2,执行npm run build之后生成dist文件夹 3,打开HBuilder,文件->打开目录,如下图 选择刚才生成的dist目录,输入项目名称,点击完成 附HBuilder下载地址:http://www.dcloud.io/ 3,此时会看到HBuilder项目下多了一个W标识(表示web项目)的myApp项目, 右键菜单选择‘转换成移动‘转换成移动App’,然后‘myApp’前面的标识就变成了‘A’,至此就已经转换成移动app了, 随后就可以利用HBuilder连接真机运行 或者发行成为原生app 注意:如果真机运行或模拟器运行报如下错误 Uncaught Error: [vuex] vuex requires a Promise polyfill in this browser 可参考:http://www.jianshu.com/p/3e3b171179f8 以下为以android apk为例的发行为原生app的步骤 1,点击发行,这里发布测试apk选择使用DCloud公用证书,点击‘打包’ 2,正在制作安装包,制作完成,手动下载 3,将下载的apk安装到android的手机看效果,以下是放到模拟器中的效果 本文作者:逍遥596607010 本文发布时间:2018年06月29日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
CSS相信大家不会陌生,在百度百科中它的解释是一种用来表现HTML(标准通用标记语言的一个应用)或XML(标准通用标记语言的一个子集)等文件样式的计算机语言。那么,它仅仅只是一种用来表示样式的语言吗?当然不是!其实早在几年前,CSS就已被安全研究人员运用于渗透测试当中。这里有一篇文章就为我们详细介绍了一种,使用属性选择器和iFrame,并通过CSS注入来窃取敏感数据的方法。但由于该方法需要iFrame,而大多数主流站点都不允许该操作,因此这种攻击方法并不实用。 这里我将为大家详细介绍一种不需要iframe且只需10秒,就能为我们有效地窃取CSRF token的方法 一旦用户的CSRF token被窃取,由于受害者已经在攻击者的网站上,因此攻击者可以继续攻击并完成对用户的CSRF攻击操作。 背景 正如原文所描述的那样,CSS属性选择器开发者可以根据属性标签的值匹配子字符串来选择元素。 这些属性值选择器可以做以下操作: 如果字符串以子字符串开头,则匹配 如果字符串以子字符串结尾,则匹配 如果字符串在任何地方包含子字符串,则匹配 属性选择器能让开发人员查询单个属性的页面HTML标记,并且匹配它们的值。一个实际的用例是将以“https://example.com”开头的所有href属性变为某种特定的颜色。 而在实际环境中,一些敏感信息会被存放在HTML标签内。在大多数情况下CSRF token都是以这种方式被存储的:即隐藏表单的属性值中。 这使得我们可以将CSS选择器与表单中的属性进行匹配,并根据表单是否与起始字符串匹配,加载一个外部资源,例如背景图片,来尝试猜测属性的起始字母。 通过这种方式,攻击者可以进行逐字猜解并最终获取到完整的敏感数值。 想要解决这个问题受害者可以在其服务器实施内容安全策略(CSP),防止攻击者从外部加载CSS代码。 无iFrames 要做到无iFrame,我将使用一种类似于之前我讨论过的方法:我将创建一个弹窗,然后在设置计时器后更改弹出窗口的位置。 使用这种方法,我仍然可以加载受害者的CSS,但我不再依赖于受害者是否允许iFrame。因为最初的弹出是通过用户事件触发的,所以我并没有被浏览器阻止。 为了强制重载,我在CSS注入间弹出一个虚拟窗口,如下: var win2 = window.open('https://security.love/anything', 'f', "top=100000,left=100000,menubar=1,resizable=1,width=1,height=1") var win2 = window.open(`https://security.love/cssInjection/victim.html?injection=${css}`, 'f', "top=100000,left=100000,menubar=1,resizable=1,width=1,height=1") 没有后端服务器 在CureSec的文章中描述了将数据传输到后端服务器,但由于CSRF是针对客户端的攻击,因此如果我们能想出一种不需要服务器的方法,那么就可以为我们节省大量的开销和简化我们的操作。 为了接收受害者客户端加载资源,我们可以利用Service Workers来拦截和读取请求数据。Service Workers目前只适用于同源请求,在我的演示中受害者和攻击者页面已处于同一源上。 不过不久后,chrome很可能会合并这个实验性的功能,允许Service Workers拦截跨域请求。 这样,就可以确保我们在客户端的攻击100%的执行,并强制用户在10秒内点击链接执行CSRF攻击,演示如下: Demo 如上所述,因为我并不想运行一个web服务器,所以我使用service workers拦截和模拟服务器端组件。目前,该演示只适用于Chrome浏览器。 首先,我创建了一个易受攻击的目标,它存在一个基于DOM的CSS注入漏洞,并在页面放置了一个敏感token。我还对脚本标签添加了一些保护措施,对左尖括号和右尖括号进行了编码。 <form action="https://security.love" id="sensitiveForm"> <input type="hidden" id="secret" name="secret" value="dJ7cwON4BMyQi3Nrq26i"> </form> <script src="mockingTheBackend.js"></script> <script> var fragment = decodeURIComponent(window.location.href.split("?injection=")[1]); var htmlEncode = fragment.replace(/</g,"&lt;").replace(/>/g,"&gt;"); document.write("<style>" + htmlEncode + "</style>"); </script> 接下来,我们将强制加载受害者的CSS,并且使用上述方法,可一次窃取(猜解)一个敏感字符。 在接收端,我已经定义了一个拦截请求的service worker,并通过post-message将它们发送回域,然后我们将token存储在本地存储中以供后续使用。你也可以想象一个后端Web服务器,通过Web套接字或轮询将CSRF token回发给攻击者域。 目前该测试仅支持CHROME: demo 如果你的浏览器支持的话,只需点击打开页面任意位置,你将看到CSRF token将逐一被猜解出来。 结语 有趣的是,反射型CSS注入实际上比存储型CSS注入更致命,因为存储型CSS注入需要一个服务器在受害者渲染之前来更新CSS。 一段时间以来,CSS注入在严重程度上来回变化。过去IE浏览器是允许用户在CSS中执行Javascript代码的。这个演示也从某种程度上表明了CSS注入,以及在你的域上渲染不受信任的CSS仍会导致严重的安全问题。 本文作者:secist 本文发布时间:2018-02-20 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
最近,我从 Grover网站上发现以一个好玩儿的悬停动画,也有了些自己的灵感。这个动画是将鼠标移动到订阅按钮上移动光标会显示相应的彩色渐变。这个想法很简单,但是它能使这个按钮脱颖而出,人们一下子就注意到它了,增加了点击的概率。 怎样才能达到这个效果,使我们的网站脱颖而出呢?其实,它并不像你想象的那么难! 追踪位置 我们要做的第一件事就是获取到鼠标的位置。 document.querySelector('.button').onmousemove = (e) => { const x = e.pageX - e.target.offsetLeft const y = e.pageY - e.target.offsetTop e.target.style.setProperty('--x', `${ x }px`) e.target.style.setProperty('--y', `${ y }px`) } 选择元素,等待,直到用户将鼠标移过它; 计算相对于元素的位置; 将坐标存在CSS的变量中。 是的,仅仅9行代码就让你能获知用户放置鼠标的位置,通过这个信息你能达到意想不到的效果,但是我们还是先来完成CSS部分的代码。 动画渐变 我们先将坐标存储在CSS变量中,以便能够随时使用它们。 .button { position: relative; appearance: none; background: #f72359; padding: 1em 2em; border: none; color: white; font-size: 1.2em; cursor: pointer; outline: none; overflow: hidden; border-radius: 100px; span { position: relative; } &::before { --size: 0; content: ''; position: absolute; left: var(--x); top: var(--y); width: var(--size); height: var(--size); background: radial-gradient(circle closest-side, #4405f7, transparent); transform: translate(-50%, -50%); transition: width .2s ease, height .2s ease; } &:hover::before { --size: 400px; } } 用span包裹文本,以避免显示在按钮的上方。 将 width和height初始化为0px,当用户悬停在按钮上时,将其改为400px。不要忘了设置这种转换以使其像风一样 本文作者:程序员观察 本文发布时间:2018年05月11日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
有两周没写简书了,有一丢丢懒惰了,还是要坚持下来啊 废话不多说,进入主题 需求 现在有一个类似聚合阅读的APP里面的文章都是以WebView来显示内容 用户点击文章中的图片会进入到查看图片的页面 问题 根据Android Hybrid的想法,需要在HTML页面中提供让Android调用的js方法,但是在的HTML中并没有提供让Android调用的方法的时候该怎么实现功能。 思路 使用WebView的js注入,为HTML加入js函数监听 在Android中添加当触发HTMl中js方法的接口 实现 布局就一个webview,这里就不贴出了 MainActivity.java public class MainActivity extends AppCompatActivity{ private static final String URL = "http://www.jianshu.com/p/c51174efd824"; private WebView webView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); webView = (WebView) findViewById(R.id.wv_test); //获得webview的设置,并设置webview支持js webView.getSettings().setJavaScriptEnabled(true); webView.setWebViewClient(new MyWebViewClient()); webView.addJavascriptInterface(new scriptInterface(MainActivity.this), "imagelistner"); webView.loadUrl(URL); } // WebViewClient监听 private class MyWebViewClient extends WebViewClient { @Override public void onPageFinished(WebView view, String url) { // html加载完成之后,添加监听图片的点击js函数 addImageClickListner(); } } /** * WebView与JS交互 **/ // 注入js函数监听 @android.webkit.JavascriptInterface private void addImageClickListner() { // 这段js函数的功能就是,遍历所有的img标签,并添加onclick函数,在还是执行的时候调用本地接口传递url过去 webView.loadUrl("javascript:(function(){" + "var objs = document.getElementsByTagName(\"img\"); " + "for(var i=0;i<objs.length;i++) " + "{" + " " + "objs[i].onclick=function() " + " " + "{ " + " window.imagelistner.openImage(this.src); " + " " + " } " + "}" + "})()"); } // js通信接口 class scriptInterface { private Context context; public scriptInterface(Context context) { this.context = context; } @android.webkit.JavascriptInterface public void openImage(String img) { //模拟点击跳转 Toast.makeText(MainActivity.this, "点击了图片" + img, Toast.LENGTH_SHORT).show(); } }} 本文作者:code小生_ 本文发布时间:2018年05月23日 本文来自云栖社区合作伙伴CSDN,了解相关信息可以关注csdn.net网站。
一、什么是跨域 由于浏览器对安全访问因素的考虑,是不允许js跨域调用其他页面的,这里的域我们把它想象成域名,如,一个域名为https://www.oschina.net,另外一个域名为https://www.zhihu.com,这两者属于不同的域名,它们之间的页面也是不能相互调用的,它属于同源策略所定义限制中的一种。同源策略具体分为以下几类: 不同域名 相同域名不同端口号,如https://www.oschina.net:8000和https://www.oschina.net:8001 同一个域名不同协议,如http://www.oschina.net和https://www.oschina.net 域名和域名对应的的IP,如http://b.qq.com和 http://10.198.7.85 主域和子域,如http://www.oschina.net和https://test.oschina.net 子域和子域,如https://test1.oschina.net和https://test2.oschina.net 以上情况只要出现了,那么就会产生跨域问题。那么如果解决跨域问题呢,下面的小节会总结一些解决跨域常用的方法。 二、跨域解决方案 1、JSONP 对于JSONP,有个通俗易懂的解释-JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。 由于同源策略,如上所述。但是(中国人讲话是很有文化的),HTML的 <script>元素是一个例外,它并不遵循同源策略,利用 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器运行而不是用JSON解析器解析。来来来,我来举个栗子吧 前端浏览器页面 <script> function jsonpCallBack (res, req) { console.log(res, req); } </script> <script type="text/JavaScript" src="http://localhost/test/jsonp.php?callback=jsonpCallBack&data=getJsonpData"></script> 另一个域名服务器请求接口 <?php /*后端获取请求字段数据,并生成返回内容*/ $data = $_GET["data"]; $callback = $_GET["callback"]; echo $callback."('success', '".$data."')"; ?> 测试结果如下 这种方案需要注意的是他支持GET这一种HTTP请求类型,还有尤为重要的就是其他域要有一定可靠性,不然你的网站会GG的。当然有时我们还可以通过一个方法来动态生成需要的JSONP。 2、跨域资源共享(CORS-Cross Origin Resource Sharing) CORS,它是JSONP模式的现代升级版,与JSONP不同的是,CORS除了GET要求方法以外也支持其他的 HTTP要求。浏览器CORS请求分成两种 a、简单请求 b、协商模型/预检请求(Preflighted Request),即非简单请求 如何区分请求具体属于哪一种呢,下面我总结了几点: 1) 请求方式 GET HEAD POST 2)HTTP的头信息子段 Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain,其中'text/plain'默认支持,其他两种则需要预检请求和服务器协商。 满足以上两大点的即为简单请求,否则为非简单请求。具体请求处理的不同,大家可以去查阅下MDN https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS ,那里有详细的解析及用法。 3、document.domain+iframe(适用于主域名相同的情况) 从上面的同源策略我们可以知道,浏览器这边是认为主域和子域、子域和子域,它们属于不同的域,那么我们如果需要让主域和子域之间可以进行通信,需要做的就是通过修改document.domain,把它们改成相同的domain 在域名为server.example.com中的a.html document.domain = 'example.com'; var $iframe = document.createElement('iframe'); $iframe.src = 'server.child.example.com/b.html'; $iframe.style.display = 'none'; document.body.appendChild($iframe); $iframe.onload = function(){ var doc = $iframe.contentDocument || $iframe.contentWindow.document; //在这里操作doc,也就是操作b.html $iframe.onload = null; }; 在域名为server.child.example.com中的b.html document.domain = 'example.com' 这种形式方便归方便,但也有其方便带来的隐患 安全性,当一个站点被攻击后,另一个站点会引起安全漏洞。 若页面中引入多个iframe,要想操作所有iframe,domain需要全部设置成一样的。 4、window.name + iframe window 对象的name属性是一个很特别的属性,它可以在不同页面甚至不同域名加载后依旧存在。使用步骤如下: step1 - 首先在页面A中利用iframe加载其他域中的页面B step2 - 在页面B中将需要传递的数据赋给window.name step3 - iframe加载完成后,页面A中修改iframe地址,将其变成同一个域下的地址,然后获取iframe中页面B的window.name 属性 示例代码如下: 首先我们在域名为http://127.0.0.1下建立好B页面,在B页面的<script>标签中将需要传递的数据赋给window.name window.name = '页面B中传递给页面A的数据'; 然后我们域名为http://127.0.0.1:9000的A页面,这里我们需要做的一件事就是利用iframe加载页面B,并将其域名进行修改,变成和页面A一样的域名。 function proxy (url, callback) { var flag = true, $iframe = document.createElement('iframe'), loadCallBack = function () { if (flag) { // 这里我们还得在域名为 http://127.0.0.1:9000 建立一个tmp.html文件当做缓存界面 $iframe.contentWindow.location = 'http://127.0.0.1:9000/tmp.html'; flag = false; } // 修改localtion后,每次触发onload事件会重置src,相当于重新载入页面,然后继续触发onload。 // 这里是针对该问题做的处理 else { callback($iframe.contentWindow.name); $iframe.contentWindow.close(); document.body.removeChild($iframe); $iframe.src = ''; $iframe = null; } }; $iframe.src = url; $iframe.style.display = 'none'; // 事件绑定兼容简单处理 // IE 支持iframe的onload事件,不过是隐形的,需要通过attachEvent来注册 if ($iframe.attachEvent) { $iframe.attachEvent('onload', loadCallBack); } else { $iframe.onload = loadCallBack; } document.body.appendChild($iframe); } proxy('http://127.0.0.1/bop/test.html', function(data){ console.log(data); }); 测试结果如图 5、HTML5中的postMessage(适用于两个iframe或两个页面之间) postMessage隶属于html5,但是它支持IE8+和其他浏览器,可以实现同域传递,也能实现跨域传递。它包括发送消息postMessage和接收消息message功能。 postMessage调用语法如下 otherWindow.postMessage(message, targetOrigin, [transfer]); otherWindow : 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。 message : 将要发送到其他 window的数据,类型为string或者object。 targetOrigin : 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。 transfer (可选) : 一串和message 同时传递的 Transferable 对象。 接收消息message 的属性有: data :从其他 window 中传递过来的数据。 origin :调用 postMessage 时消息发送方窗口的 origin 。 source :对发送消息的窗口对象的引用。 示例如下:域名http://127.0.0.1:9000页面A通过iframe嵌入了http://127.0.0.1页面B,接下来页面A将通过postMessage对页面B进行数据传递,页面B将通过message属性接收页面A的数据 页面A发送消息代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面A</title> </head> <body> <h1>hello jsonp</h1> <iframe src="http://127.0.0.1/b.html" id="iframe"></iframe> </body> </html> <script> window.onload = function() { var $iframe = document.getElementById('iframe'); var targetOrigin = "http://127.0.0.1"; $iframe.contentWindow.postMessage('postMessage发送消息', targetOrigin); }; </script> 页面B接收消息代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面B</title> </head> <body> <h1>hello jsonp</h1> </body> </html> <script> var onmessage = function (event) { var data = event.data; //消息 var origin = event.origin; //消息来源地址 var source = event.source; //源Window对象 if(origin === "http://127.0.0.1:9000"){ console.log(data, origin, source); } }; // 事件兼容简单处理 if (window.addEventListener) { window.addEventListener('message', onmessage, false); } else if (window.attachEvent) { window.attachEvent('onmessage', onmessage); } else { window.onmessage = onmessage; } </script> 运行结果如下 6、location.hash + iframe(适用于两个iframe之间) 对于location.hash,我们先看一张图先 相信看完图,大家也大概清楚了location.hash到底是用来干啥子的。没错,它可以用来获取或设置页面的标签值 如http://127.0.0.1:9000/#hello ,它的location.hash值则为'#hello'。它一般用于浏览器锚点定位,HTTP请求过程中却不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录,这对我们进行跨域通信给予了帮助。我们可以通过修改URL的hash部分来进行双向通信。 示例如下:域名http://127.0.0.1:9000页面A通过iframe嵌入了http://127.0.0.1页面B,接下来页面A和页面B将通过location.hash进行双向通信。 页面A代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面A</title> </head> <body> <iframe src="http://127.0.0.1/bop/test.html#locationHash" id="ifr"></iframe> </body> </html> 页面B代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面B</title> </head> <body> <h1>hello localtionHash</h2> </body> </html> <script> try { parent.location.hash = 'data'; } catch (e) { // ie、chrome的安全机制无法修改parent.location.hash,所以要借助于父窗口域名下的一个代理iframe var $ifrproxy = document.createElement('iframe'); $ifrproxy.style.display = 'none'; // 注意proxy.html必须是域名为 http://127.0.0.1:9000 下的页面 $ifrproxy.src = "http://127.0.0.1:9000/proxy.html#locationHashChange"; document.body.appendChild($ifrproxy); } </script> 代理页面代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Proxy页面</title> </head> <body> </body> <script> // 因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值 parent.parent.location.hash = self.location.hash.substring(1); </script> </html> 运行结果如图 以上内容便是我本博客的所有内容了,希望多多少少可以帮助小伙伴们去更好的理解跨域,当然,不对的地方,还请小伙伴们轻喷哦 ^_^ 本文作者:qiangdada 本文发布时间:2017/03/08 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
一、需求: 后台系统中经常会使用到的功能,选择一个时间区间,根据这个时间区间去筛选一些信息,比如,某一时间段的注册用户。 二、最后效果 三、需要引入的文件(src/index.html) 注意:1、jQuery文件先引用,因为在初始化日期插件是,需要找到DOM中的对象,添加一些样式; 2、可以看到,我项目中并没有引用bootstrap.min.css这个文件,因为是在index.html全局引用的这个样式,对已经写好的样式有很大的影响,因此产生的影响就是样式是乱掉的,你看到的效果图(第一张图片)的样式就需要自己动手啦,写一个共用的样式,在每个组件中引用。 具体样式就不加了,相信如果你引用插件的这个坑淌过来了,这点儿小事儿,就想一颗奶油巧克力,带着成就感慢慢‘品尝’吧~ 四、代码部分 A、 To Date (html代码) <!--选择时间 datetimepicker 选择到天--> <div> <label class="date-label-width">时间(To Date):</label> <div class="input-group date form_datetime date-div-inline"> <input type="datetime" size="16" id="startTime" name="startTime" class="date-input-size date-minute-bgcolor" value="" readonly > <span class="input-group-addon date-div-inline"><span class="fa fa-calendar fa-lg"></span></span> </div> <label for="endTime" >-</label> <div class="input-group date form_datetime date-div-inline"> <input type="datetime" id="endTime" name="endTime" class="date-input-size date-minute-bgcolor" value="" readonly> <span class="input-group-addon date-div-inline"><span class="fa fa-calendar fa-lg"></span></span> </div> </div> JS代码 //初始化日期插件 -- 选择到天 $('#startTime').datetimepicker({ format: 'yyyy-mm-dd',//显示格式 todayHighlight: 1,//今天高亮 minView: "month",//设置只显示到月份 startView:2, forceParse: 0, showMeridian: 1, autoclose: true,//选择后自动关闭 language: 'zh-CN', weekStart: 1, // todayBtn: 1, // autoclose: 1, // todayHighlight: 1, // startView: 2, // minView: 2, // forceParse: 0, // pickerPosition:'bottom-right'//日期插件弹出的位置 }).on("changeDate", function () { $('#endTime').datetimepicker('setStartDate', $("#startTime").val()); console.log( $("#startTime").val()); $("#endTime").focus() }); $('#endTime').datetimepicker({ format: 'yyyy-mm-dd',//显示格式 todayHighlight: 1,//今天高亮 minView: "month",//设置只显示到月份 startView:2, forceParse: 0, showMeridian: 1, autoclose: true,//选择后自动关闭 language: 'zh-CN', weekStart: 1, // todayBtn: 1, // autoclose: 1, // todayHighlight: 1, // startView: 2, // minView: 2, // forceParse: 0, // pickerPosition:'bottom-right'//日期插件弹出的位置 }).on("changeDate", function () { $('#startTime').datetimepicker('setEndDate', $("#endTime").val()); console.log( $("#endTime").val()); }); format这个参数可以设置日期的格式,yyyy-mm-dd,yyyy/mm/dd B、To Minute (html代码) <!--选择时间 datetimepicker 选择到分钟--> <div> <label for="dtp_input1" class="date-label-width">时间(To Minute):</label> <div class="input-group date form_datetime date-div-inline" data-date="" data-date-format="dd MM yyyy - HH:ii p" data-link-field="dtp_input1"> <input class="date-input-size " id="startTimeMinute" size="16" type="text" value="" readonly> <span class="input-group-addon date-div-inline"><span class="fa fa-calendar fa-lg"></span></span> <!--<span class="input-group-addon"><span class="glyphicon glyphicon-th"></span></span>--> </div> <input type="hidden" id="dtp_input1" value="" /> <label for="dtp_input2">-</label> <div class="input-group date form_datetime date-div-inline" data-date="" data-date-format="dd MM yyyy - HH:ii p" data-link-field="dtp_input1"> <input class="date-input-size " id="endTimeMinute" size="16" type="text" value="" readonly> <span class="input-group-addon date-div-inline"><span class="fa fa-calendar fa-lg"></span></span> <!--<span class="input-group-addon"><span class="glyphicon glyphicon-th"></span></span>--> </div> <input type="hidden" id="dtp_input2" value="" /> </div> (JS 代码) // //初始化日期插件 -- 选择到分钟 $('#startTimeMinute').datetimepicker({ //language: 'fr', format: 'yyyy-mm-dd hh:ii',//显示格式 weekStart: 1, todayBtn: 1, autoclose: 1, todayHighlight: 1, startView: 2, forceParse: 0, showMeridian: 1 }).on("changeDate", function () { $('#endTimeMinute').datetimepicker('setStartDate', $("#startTimeMinute").val()); console.log( $("#startTimeMinute").val()); $("#endTimeMinute").focus() }); $('#endTimeMinute').datetimepicker({ //language: 'fr', format: 'yyyy-mm-dd hh:ii',//显示格式 weekStart: 1, todayBtn: 1, autoclose: 1, todayHighlight: 1, startView: 2, forceParse: 0, showMeridian: 1 }).on("changeDate", function () { $('#startTimeMinute').datetimepicker('setEndDate', $("#endTimeMinute").val()); console.log( $("#endTimeMinute").val()); }); 注意:因为是一个时间区间,第一个input是开始时间,第二个是结束时间,开始时间必须在结束时间之前,因此,id必须加在input上,而不是div上。 至此,我已经如愿看到了理想的效果,准备洗洗睡了,你呢? 欢迎各位大神批评指正。 本文作者:qiangdada 本文发布时间:2018/03/14 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
一、项目介绍 vui : 一个私人的vue ui 组件库(移动端为主) 文档官网 已有组件 swiper scroller search message modal table picker select dropdown 二、安装下载 npm install x-vui -S 三、快速开始 3.1 构建项目(配合vue-cli) # 全局安装 vue-cli npm install --global vue-cli # 创建一个基于 webpack 模板的新项目 vue init webpack my-vui-project # 安装依赖,并下载x-vui cd my-vui-project npm install && npm install x-vui # 项目启动 默认端口localhost:8080 npm run dev 3.2 引入vui组件库 你可以引入整个 vui,或是根据需要仅引入部分组件。我们先介绍如何引入完整的 vui。 3.2.1 完整引入 在main.js中写入 import Vue from 'vue' import vui from 'x-vui' import 'x-vui/lib/vui-css/index.css'; Vue.use(vui) 3.2.2 按需部分引入 在main.js中写入(假如我只需要Scroller和Select组件) import Vue from 'vue' import { Scroller, Select // ... } from 'x-vui' import 'x-vui/lib/vui-css/scroller.css'; import 'x-vui/lib/vui-css/select.css'; Vue.component(Scroller.name, Scroller) Vue.component(Select.name, Select) 3.2.3 全局注册vui插件 注:完整引入了vui,则无需再注册插件 import Vue from 'vue'; import { $Toast, $Dialog // ... } from 'x-vui'; Vue.prototype.$toast = $Toast Vue.prototype.$dialog = $Dialog 四、组件用法 4.1 swiper 可以自己调配自己想要的swiper,不一定得是轮播图 4.1.1 Attributes 参数 说明 类型 可选值 默认值 type swiper类型 string swiper(正常)/thum(缩略) swiper auto 自动播放时长 number — 5000 items swiper展示的列表 array — [] showIndicators 是否展示swiper小圆点 boolean — true styles swiper样式控制 object — {} resetTitle 重置title内容 string — — 4.1.2 Events 事件名称 说明 回调参数 change swiper滑动回调 当前swiper item索引 4.1.3 用法 <template> <div class="swiper-page"> <p>正常swiper</p> <x-swiper type='swiper' :items='items' :styles="{height: '180px'}"></x-swiper> <p>缩略swiper</p> <x-swiper type='swiper' :items='items' :type="'thum'" :styles="{height: '208px'}"></x-swiper> </div> </template> <script> export default { data () { return { items: [ require('assets/beauty_1.png'), require('assets/beauty_2.png'), require('assets/beauty_3.png'), require('assets/beauty_4.png'), require('assets/beauty_5.png') ], } } } </script> <style lang="stylus" scoped> .swiper-page { height auto } </style> 4.2 scroller(下拉刷新上拉加载) 4.2.1 Attributes 参数 说明 类型 可选值 默认值 onRefresh 下拉回调 function — — onInfinite 上拉回调 function — — width scroller宽度 string — 100% height scroller高度 string — 100% isLoadMore 是否展示上拉加载 boolean — true refreshText 下拉文本内容 string — 下拉刷新 noDataText 无数据文本 string — 没有更多数据啦~ refreshLayerColor 下拉文本颜色 string — #AAA loadingLayerColor 上拉文本颜色 string — #AAA animating 是否有动画 boolean — true animationDuration 动画间隔 number — 250 bouncing 是否有反弹效果 string — true cssClass content css class string — — 4.2.2 用法 <style scoped> .scroller-page { height: 330px } ul { padding: 20px 0 } li { width: 100%; height: 35px; line-height: 35px; border-bottom: 1px solid #eee; text-align: center; } </style> <template> <div class="scroller-page"> <x-scroller :on-refresh="refresh" :on-infinite="infinite" :noDataText="noDataText" > <!-- content is here --> <ul> <li>数据1</li> <li>数据2</li> <li>数据3</li> <li>数据4</li> <li>数据5</li> <li>数据6</li> </ul> </x-scroller> </div> </template> <script> export default { data () { return { noDataText: '没有更多数据啦~' } }, methods: { refresh (done) { setTimeout(done, 1000) this.noDataText = '' console.log('refresh'); }, infinite (done) { setTimeout(done, 1000, true) this.noDataText = '没有更多数据啦~' console.log('infinite'); } } } </script> 4.3 search 4.3.1 Attributes 参数 说明 类型 可选值 默认值 async 是否进行节流 boolean — true timeout 搜索节流时长 number — 100 styles search样式 object — — placeholder placeholder string — '搜索' autofocus 是否自动聚焦(iOS端autofocus无效) boolean — — clear 进行搜索是否清空search框内容 boolean — false 4.3.2 Events 事件名称 说明 回调参数 search search搜索回调 搜索文本 enter enter时搜索回调 搜索文本 close 点击搜索关闭按钮回调 '' 4.3.3 用法 只有搜索框 <style lang="stylus"> .search-page { padding: 0 10px; margin-top: 10px; } </style> <template> <div> <x-search placeholder="请输入搜索关键字" @search="searchFn" @enter="searchEnter" @close="closeFn" ></x-search> </div> </template> <script> export default { methods: { searchFn (query) { console.log('search', query) }, searchEnter (query) { console.log('enter', query) }, closeFn (query) { console.log('close', query) } } } </script> 拥有默认的搜索结果列表 <style lang="stylus"> .search-page { padding: 0 10px; margin-top: 10px; } </style> <template> <div class="search-page" v-title data-title="search"> <x-search placeholder="请输入搜索关键字" :autofocus="true" :async="false" @search="searchFn" @enter="searchEnter" @close="closeFn" > <x-search-list :result="filterResult" @listSearch="listSearch" v-show="visible"></x-search-list> </x-search> </div> </template> <script> export default { data () { return { keyword: '', visible: false, // 点击列表,列表是否消失 defaultResult: [ 'Apple', 'Banana', 'Orange', 'Durian', 'Lemon', 'Peach', 'Cherry', 'Berry', 'Core', 'Fig', 'Haw', 'Melon', 'Plum', 'Pear', 'Peanut', 'Other' ] } }, watch: { keyword (val) { if (!val) { this.visible = false; } } }, methods: { searchFn (query) { this.keyword = query; this.visible = true; console.log('search', query) }, searchEnter (query) { this.keyword = query; console.log('enter', query) }, closeFn (query) { this.keyword = query; console.log('close', query) }, listSearch (index) { this.visible = false; console.log(index, this.defaultResult[index]) } }, computed: { filterResult() { return this.defaultResult.filter(item => new RegExp(this.keyword, 'i').test(item)); } } } </script> 定制化结果列表,关键字高亮匹配 <style lang="stylus"> .search-page { padding: 0 10px; margin-top: 10px; .search-result { position: relative; overflow: hidden; .l { width: 100%; margin-bottom: 5px; } .r { position: absolute; right: 0; top: 50%; margin-top: -10px; line-height: 20px; } .price { color: #ff6f5c; } .gray { font-size: 12px; } } } </style> <template> <div class="search-page" v-title data-title="search"> <x-search placeholder="请输入搜索关键字" :autofocus="true" :async="false" @search="searchFn" @enter="searchEnter" @close="closeFn" > <x-search-list :result="filterResult" @listSearch="listSearch" v-show="visible"> <div class="search-result" slot="list-item" slot-scope="props"> <p class="l" v-html="props.slotValue.name"></p> <p class="gray" v-show="props.slotValue.price">¥{{props.slotValue.price}}/斤</p> <div class="gray r" v-show="props.slotValue.amount">剩余{{props.slotValue.amount}}斤</div> </div> </x-search-list> </x-search> </div> </template> <script> export default { data () { return { keyword: '', visible: false, defaultResult: [ {name: 'Apple', price: 5, amount: 20}, {name: 'Banana', price: 5, amount: 30}, {name: 'Orange', price: 3, amount: 10}, {name: 'Durian', price: 10, amount: 25}, {name: 'Lemon', price: 4, amount: 30}, {name: 'Peach', price: 5, amount: 40}, {name: 'Cherry', price: 20, amount: 50}, {name: 'Berry', price: 15, amount: 60}, {name: 'Core', price: 10, amount: 21}, {name: 'Fig', price: 10, amount: 22}, {name: 'Haw', price: 10, amount: 23}, {name: 'Melon', price: 10, amount: 24}, {name: 'Plum', price: 10, amount: 25}, {name: 'Pear', price: 10, amount: 26}, {name: 'Peanut', price: 10, amount: 27}, {name: 'Other'} ], // 防止defaultResult值被污染 copy: [] } }, watch: { keyword (val) { if (!val) { this.visible = false; } } }, methods: { searchFn (query) { this.keyword = query; this.visible = true; console.log('search', query) }, searchEnter (query) { this.keyword = query; console.log('enter', query) }, closeFn (query) { this.keyword = query; console.log('close', query) }, listSearch (index) { this.visible = false; console.log(index, this.defaultResult[index].name) } }, computed: { filterResult() { // i 忽略大小写 let result = this.defaultResult.filter(item => new RegExp(this.keyword, 'i').test(item.name)); // 关键字高亮匹配 this.copy = JSON.parse(JSON.stringify(result)) this.copy.forEach((item, index) => { let name = item.name, word = this.keyword; name = name.toLowerCase(); word = word.toLowerCase(); if (word && name.indexOf(word) !== -1) { let arr = item.name.split('') let i = name.indexOf(word); let len = word.length; let active = '<span class="price">' + arr.splice(i, len).join('') + '</span>'; arr.splice(i, 0, active); item.name = arr.join(''); } }) return this.copy; } } } </script> 4.4 dialog 4.4.1 Attributes message 参数 说明 类型 可选值 默认值 msg msg文本内容 string — — timeout msg显示时长 number — 2000 callback 回调函数 function — — icon 特殊icon string — — modal 参数 说明 类型 可选值 默认值 show modal是否显示 boolean — — title modal标题 string — — content modal内容 string — — onOk 确定按钮回调 function — — onCancel 取消按钮回调 function — — okText 确定按钮内容 string — — cancelText 取消按钮内容 string — — showCloseIcon 是否显示关闭icon boolean — true 4.4.2 用法 msg this.$dialog.msg({msg: 'hello message components ~'}) modal(插件) this.$dialog.modal({ title: 'Demo Modal', cancelText: '取消', okText: '确定', content: '测试,测试,测试,测试,测试,测试,测试,测试,测试', onOk () { console.log('click ok btn to do someting'); }, onCancel () { console.log('click cancel btn to do someting'); } }) modal(组件) <style lang="stylus"> .dialog-page { .dialog-btn { width 100% position absolute top 50% left 0 transform translateY(-50%) > p { width 80% height 50px line-height 50px margin 40px auto 0 border 1px solid #CCC border-radius 10px font-size 16px font-weight bold letter-spacing 2px text-align center &:first-child { margin-top 0 } } } .modal-text { text-align: center; } } </style> <template> <div class="dialog-page"> <div class="dialog-btn"> <p @click="message">message dialog</p> <p @click="open">modal dialog</p> </div> <x-modal title="Demo Modal" cancelText="取消" :onCancel="close" :show="selectModel" okText="确认" :onOk="close"> <p class="modal-text">modal components test is awesome!!!</p> </x-modal> </div> </template> <script> export default { data () { return { selectModel: false } }, methods: { message () { return this.$dialog.msg({msg: 'this is a message dialog'}) }, open () { this.selectModel = true }, close () { this.selectModel = false } } } </script> 4.5 table 4.5.1 Attributes 参数 说明 类型 可选值 默认值 tableData table数据 array — — label thead标题(TableColum) — — prop 绑定的数据 string — — 4.5.2 用法 配合scroller进行展示(注:目前table较为简单,后期将进行完善,使得其可以应对不同场景) <template> <div class="table" v-title data-title="table"> <x-scroller :on-refresh="refresh" :on-infinite="infinite" :noDataText="noDataText" class="table-content" > <x-table :tableData="items"> <x-table-column prop="list_1" label="LIST ONE"></x-table-column> <x-table-column prop="list_2" label="LIST TWO"></x-table-column> <x-table-column prop="list_3" label="LIST THREE"></x-table-column> <x-table-column prop="list_4" label="LIST FOUR"></x-table-column> </x-table> </x-scroller> </div> </template> <script> export default { data () { return { items: [ { list_1: '2017.12.09', list_2: '路人1', list_3: '爱过', list_4: '有' }, { list_1: '2017.12.10', list_2: '路人2', list_3: '爱过', list_4: '有' }, { list_1: '2017.12.11', list_2: '路人3', list_3: '爱过', list_4: '没有' }, { list_1: '2017.12.12', list_2: '路人4', list_3: '爱过', list_4: '没有' } ], noDataText: '没有更多数据啦~' } }, methods: { refresh (done) { setTimeout(done, 1000) this.noDataText = '' console.log('refresh'); }, infinite (done) { setTimeout(done, 1000, true) this.noDataText = '没有更多数据啦~' console.log('infinite'); } } } </script> 4.6 picker 4.6.1 Attributes 参数 说明 类型 可选值 默认值 default picker默认选中的值 string/number — — type picker类型 string date/time/datetime/custom datetime title 选择器弹窗标题 string — — placeholder placeholder string — 请选择时间 timeStep 时间选择粒度(有分钟的选择器) number — 1 startYear 起始年份 number/string — 今年 endYear 结束年份 number/string — 10年的范围 startDate 起始日期 string — — endDate 结束日期 string — — startHour 起始时间 number/string — 0 endHour 结束时间 number/string — 23 startMinute 起始分钟 number/string — 0 endMinute 结束分钟 number/string — 59 yearFormat “年“的格式化 string — '{value}年' monthFormat “月“的格式化 string — '{value}月' dayFormat “日“的格式化 string — '{value}日' hourFormat “时“的格式化 string — '{value}时' minuteFormat “分“的格式化 string — '{value}分' 4.6.2 用法 <style lang="stylus"> .picker-page { .x-list { padding: 0 0.32rem; background: #fff; color: #333; font-size: 14px; > li { min-height: 60px; padding-top: 21px; border-bottom: 1px solid #f2f2f2; overflow: hidden; > label { float: left; } > div { float: right; } .x-list-arrow { min-width: 100px; margin-right: 10px; position: relative; > div { float: right; text-align: right; margin-right: 10px; } &:after { content: ''; position: absolute; top: 4px; right: -5px; width: 10px; height: 10px; border-top: 1px solid #ccc; border-right: 1px solid #ccc; transform: rotate(45deg); -webkit-transform: rotate(45deg); } } } } } </style> <template> <div class="picker-page" v-title data-title="picker"> <ul class='x-list'> <li> <label>日期选择</label> <div class="x-list-arrow"> <x-picker title="选择日期" startYear="2016" startDate="2015-01-01" endDate="2019-12-01" placeholder="请选择日期" v-model="now_date" type="date"></x-picker> </div> </li> <li> <label>时间选择</label> <div class="x-list-arrow"> <x-picker title="选择时间" placeholder="请选择时间" startMinute="2" endMinute="30" v-model="now_time" type="time"></x-picker> </div> </li> <li> <label>日期时间选择</label> <div class="x-list-arrow"> <x-picker title="选择日期时间" placeholder="请选择日期时间" v-model="now_datetime" :timeStep="20" type="datetime"></x-picker> </div> </li> <li> <label>性别选择</label> <div class="x-list-arrow"> <x-picker v-model="gender.value" placeholder="请选择性别" :default="gender.default" title="选择性别" type="custom"></x-picker> </div> </li> </ul> </div> </template> <script> export default { data() { return { gender: { default: -1, value: [ { name: "保密", value: 0 }, { name: "男", value: 1 }, { name: "女", value: 2 } ] }, now_date: null, now_time: null, now_datetime: null // new Date().getTime()/1000 }; } }; </script> 4.7 select 4.7.1 Attributes 参数 说明 类型 可选值 默认值 selectData 下拉数据 array — [] title 默认显示的标题 string — '' alwaysShowTitle 是否一直显示默认标题 boolean — false defaultValue 默认选中的值 number/string — 0 width select组件的宽度 string — '100%' ellipsisWidth select文字超过多出省略号的宽度 string — '120px' 4.7.2 Events 事件名称 说明 回调参数 search select 选择时的回调函数 参数1:索引,参数2:所中项的id值 4.7.3 用法 <template> <div class="select-page" v-title data-title="select"> <x-select @search="searchFn" :selectData="selectData" title="LIST ONE" :alwaysShowTitle="false" width="50%" defaultValue="0" ></x-select> <x-select @search="searchFn1" :selectData="selectData1" title="LIST TWO" width="50%" ellipsisWidth="65px" defaultValue="1" ></x-select> </div> </template> <script> export default { data() { return { selectData: [ { id: 1, name: "LIST ONE 1" }, { id: 2, name: "LIST ONE 2" }, { id: 3, name: "LIST ONE 3" }, { id: 4, name: "LIST ONE 4" }, { id: 5, name: "LIST ONE 5" } ], selectData1: [ { id: 1, name: "LIST TWO 1" }, { id: 2, name: "LIST TWO 2" }, { id: 3, name: "LIST TWO 3" }, { id: 4, name: "LIST TWO 4" }, { id: 5, name: "LIST TWO 5" } ] }; }, methods: { searchFn(index, id) { console.log(index, id); }, searchFn1(index, id) { console.log(index, id); } } }; </script> 4.8 dropdown 这个下拉菜单偏PC端的这里就不多做介绍了 <template> <div class="test"> <x-dropdown trigger="click" @command="commandHandle" :hide-on-click="true"> <span class="drop-down_link">下拉菜单</span> <x-dropdown-menu> <x-dropdown-list command="a">下拉列表1</x-dropdown-list> <x-dropdown-list command="b">下拉列表2</x-dropdown-list> <x-dropdown-list command="c"><h4>下拉列表3</h4></x-dropdown-list> </x-dropdown-menu> </x-dropdown> </div> </template> <script> export default { name: 'Dropdown', methods: { commandHandle (command, instance) { console.log(command, instance); } } } </script> 以上组件便是目前vui所有的组件了,后期会不断的进行维护并进行新组件的开发。 本文作者:qiangdada 本文发布时间:2017/12/14 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
前言 上一篇文章我们实现了一个属于自己的简易MVVM库,里面实现了一个mvvm库应有基本功能,里面对数据进行了数据劫持,但是仅仅只是对对象进行了数据劫持,并没有实现数组的一个监听。今天我将带着大家实现数组的observe。 一、整体思路 1、定义变量arrayProto接收Array的prototype 2、定义变量arrayMethods,通过Object.create()方法继承arrayProto 3、重新封装数组中push,pop等常用方法。(这里我们只封装我们需要监听的数组的方法,并不做JavaScript原生Array中原型方法的重写的这么一件暴力的事情) 4、更多的奇淫技巧探究 二、监听数组变化实现 这里我们首先需要确定的一件事情就是,我们只需要监听我们需要监听的数据数组的一个变更,而不是针对原生Array的一个重新封装。 其实代码实现起来会比较简短,这一部分代码我会直接带着注释贴出来 // 获取Array原型 const arrayProto = Array.prototype; const arrayMethods = Object.create(arrayProto); const newArrProto = []; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ].forEach(method => { // 原生Array的原型方法 let original = arrayMethods[method]; // 将push,pop等方法重新封装并定义在对象newArrProto的属性上 // 这里需要注意的是封装好的方法是定义在newArrProto的属性上而不是其原型属性 // newArrProto.__proto__ 没有改变 newArrProto[method] = function mutator() { console.log('监听到数组的变化啦!'); // 调用对应的原生方法并返回结果(新数组长度) return original.apply(this, arguments); } }) let list = [1, 2]; // 将我们要监听的数组的原型指针指向上面定义的空数组对象 // newArrProto的属性上定义了我们封装好的push,pop等方法 list.__proto__ = newArrProto; list.push(3); // 监听到数组的变化啦! 3 // 这里的list2没有被重新定义原型指针,所以这里会正常执行原生Array上的原型方法 let list2 = [1, 2]; list2.push(3); // 3 目前为止我们已经实现了数组的监听。从上面我们看出,当我们将需要监听的数组的原型指针指向newArrProto对象上的时候(newArrProto的属性上定义了我们封装好的push,pop等方法)。这样做的好处很明显,不会污染到原生Array上的原型方法。 三、更多的奇淫技巧 1、分析实现的机制 从上面我们看出,其实我们做了一件非常简单的事情,首先我们将需要监听的数组的原型指针指向newArrProto,然后它会执行原生Array中对应的原型方法,与此同时执行我们自己重新封装的方法。 那么问题来了,这种形式咋这么眼熟呢?这不就是我们见到的最多的继承问题么?子类(newArrProto)和父类(Array)做的事情相似,却又和父类做的事情不同。但是直接修改__proto__隐式原型指向总感觉心里怪怪的(因为我们可能看到的多的还是prototype),心里不(W)舒(T)服(F)。 那么接下来的事情就是尝试用继承(常见的prototype)来实现数组的变更监听。对于继承这一块可以参考我之前写过的一篇文章浅析JavaScript继承。 2、利用ES6的extends实现 首先这里我们会通过ES6的关键字extends实现继承完成Array原型方法的重写,咱总得先用另外一种方式来实现一下我们上面实现的功能,证明的确还有其他方法可以做到这件事。OK,废话不多说,直接看代码 class NewArray extends Array { constructor(...args) { // 调用父类Array的constructor() super(...args) } push (...args) { console.log('监听到数组的变化啦!'); // 调用父类原型push方法 return super.push(...args) } // ... } let list3 = [1, 2]; let arr = new NewArray(...list3); console.log(arr) // (2) [1, 2] arr.push(3); // 监听到数组的变化啦! console.log(arr) // (3) [1, 2, 3] 3、ES5及以下的方法能实现么? OK,终于要回到我们常见的带有prototype的继承了,看看它究竟能不能也实现这件事情呢。这里我们直接上最优雅的继承方式-寄生式组合继承,看看能不能搞定这件事情。代码如下 /** * 寄生式继承 继承原型 * 传递参数 subClass 子类 * 传递参数 superClass 父类 */ function inheritObject(o){ //声明一个过渡函数 function F(){} //过渡对象的原型继承父对象 F.prototype = o; return new F(); } function inheritPrototype(subClass,superClass){ //复制一份父类的原型副本保存在变量 var p = inheritObject(superClass.prototype); //修正因为重写子类原型导致子类的constructor指向父类 p.constructor = subClass; //设置子类的原型 subClass.prototype = p; } function ArrayOfMine (args) { Array.apply(this, args); } inheritPrototype(ArrayOfMine, Array); // 重写父类Array的push,pop等方法 ArrayOfMine.prototype.push = function () { console.log('监听到数组的变化啦!'); return Array.prototype.push.apply(this, arguments); } var list4 = [1, 2]; var newList = new ArrayOfMine(list4); console.log(newList, newList.length, newList instanceof Array, Array.isArray(newList)); newList.push(3); console.log(newList, newList.length, newList instanceof Array, Array.isArray(newList)); 目前我们这么看来,的的确确是利用寄生式组合继承完成了一个类的继承,那么console.log的结果又是如何的呢?是不是和我们预想的一样呢,直接看图说话吧 我擦嘞,这特么什么鬼,教练,我们说好的,不是这个结果。这是典型的买家秀和卖家秀吗? 那么我们来追溯一下为什么会是这种情况,我们预想中的情况应该是这样的 newList => [1, 2] newList.length => 2 Array.isArray(newList) => true push执行之后的理想结果 newList => [1, 2, 3] newList.length => 3 Array.isArray(newList) => true 我们先抛弃Array的apply之后的结果,我们先用同样的方式继承我们自定义的父类Father,代码如下 function inheritObject(o){ function F(){}; F.prototype = o; return new F(); } function inheritPrototype(subClass,superClass){ var p = inheritObject(superClass.prototype); p.constructor = subClass; subClass.prototype = p; } function Father() { // 这里我们暂且就先假定参数只有一个 this.args = arguments[0]; return this.args; } Father.prototype.push = function () { this.args.push(arguments); console.log('我是父类方法'); } function ArrayOfMine () { Father.apply(this, arguments); } inheritPrototype(ArrayOfMine, Father); // 重写父类Array的push,pop等方法 ArrayOfMine.prototype.push = function () { console.log('监听到数组的变化啦!'); return Father.prototype.push.apply(this, arguments); } var list4 = [1, 2]; var newList = new ArrayOfMine(list4, 3); console.log(newList, newList instanceof Father); newList.push(3); console.log(newList, newList instanceof Father); 结果如图 结果和我们之前预想的是一样的,我们自己定义的类的话,这种做法是可以行的通的,那么问题就来了,为什么将父类改成Array就行不通了呢? 为了搞清问题,查阅各种资料后。得出以下结论: 因为Array构造函数执行时不会对传进去的this做任何处理。不止Array,String,Number,Regexp,Object等等JS的内置类都不行。。这也是著名问题 ES5及以下的JS无法完美继承数组 的来源,不清楚的小伙伴可以Google查查这个问题。那么,为什么不能完美继承呢? 1、数组有个响应式的length,一方面它会跟进你填入的元素的下表进行一个增长,另一方面如果你将它改小的话,它会直接将中间的元素也删除掉 var arr1 = [1]; arr1[5] = 1; console.log(arr1.length === 6); // true // 以及 var arr2 = [1,2,3]; arr2.length = 1 console.log(arr2); // [1] 此时元素2,3被删除了 2、数组内部的[[class]] 属性,这个属性是我们用Array.isArray(someArray)和Object.prototype.String.call(someArray) 来判定someArray是否是数组的根源,而这又是内部引擎的实现,用任何JS方法都是无法改变的。而为啥要用这两种方法进行数组的判定,相信大家从前面的代码结果可以看出来,利用instanceof去判定是否为数组,结果是有问题的。 因为数组其响应式的length属性以及内部的[[class]]属性我们无法再JS层面实现,这就导致我们无法去用任何一个对象来“模仿”一个数组,而我们想要创建一个ArrayOfMine继承Array的话又必须直接用Array的构造函数,而上面我提到了Array构造函数执行时是不会对传进去的this做任何处理,也就是说这样你根本就不能继承他。而利用__proto__隐式原型的指针变更却能实现,因为他是一个非标准的属性(已在ES6语言规范中标准化),详请请点击链接__proto__。 所以要实现最上面我们实现的功能,我们还是需要用到__proto__属性。变更后代码如下 function inheritObject(o){ function F(){} F.prototype = o; return new F(); } function inheritPrototype(subClass,superClass){ var p = inheritObject(superClass.prototype); p.constructor = subClass; subClass.prototype = p; } function ArrayOfMine () { var args = arguments , len = args.length , i = 0 , args$1 = []; // 保存所有arguments for (; i < len; i++) { // 判断参数是否为数组,如果是则直接concat if (Array.isArray(args[i])) { args$1 = args$1.concat(args[i]); } // 如果不是数组,则直接push到 else { args$1.push(args[i]) } } // 接收Array.apply的返回值,刚接收的时候arr是一个Array var arr = Array.apply(null, args$1); // 将arr的__proto__属性指向 ArrayOfMine的 prototype arr.__proto__ = ArrayOfMine.prototype; return arr; } inheritPrototype(ArrayOfMine, Array); // 重写父类Array的push,pop等方法 ArrayOfMine.prototype.push = function () { console.log('监听到数组的变化啦!'); return Array.prototype.push.apply(this, arguments); } var list4 = [1, 2]; var newList = new ArrayOfMine(list4, 3); console.log(newList, newList.length, newList instanceof Array, Array.isArray(newList)); newList.push(4); console.log(newList, newList.length, newList instanceof Array, Array.isArray(newList)); 结果如图 自此,我所知道几种实现数组监听的方法便得于实现了。 总结 总结以上几点方案,基于上篇文章的基础,完整的数组监听代码如下 // Define Property function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, configurable: true, writable: true }) } // observe array let arrayProto = Array.prototype; let arrayMethods = Object.create(arrayProto); [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ].forEach(method => { // 原始数组操作方法 let original = arrayMethods[method]; def(arrayMethods, method, function () { let arguments$1 = arguments; let i = arguments.length; let args = new Array(i); while (i--) { args[i] = arguments$1[i] } // 执行数组方法 let result = original.apply(this, args); // 因 arrayMethods 是为了作为 Observer 中的 value 的原型或者直接作为属性,所以此处的 this 一般就是指向 Observer 中的 value // 当然,还需要修改 Observer,使得其中的 value 有一个指向 Observer 自身的属性,__ob__,以此将两者关联起来 let ob = this.__ob__; // 存放新增数组元素 let inserted; // 为add 进arry中的元素进行observe switch (method) { case 'push': inserted = args; break; case 'unshift': inserted = args; break; case 'splice': // 第三个参数开始才是新增元素 inserted = args.slice(2); break; } if (inserted) { ob.observeArray(inserted); } // 通知数组变化 ob.dep.notify(); // 返回新数组长度 return result; }) }) 喜欢的话走波star,star是我继续下去最大的动力了。 以上便是这篇文章的所有内容了,如果有哪里写的有问题,还请各位小伙伴拍砖指出,共同进步学习!最后祝各位小伙伴们端午节快乐! 本文作者:qiangdada 本文发布时间:2017/05/30 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
前言 本文所有代码都已经push到本人github个人仓库overwrite->my-mvvm 我们知道的,常见的数据绑定的实现方法 1、数据劫持(vue):通过Object.defineProperty() 去劫持数据每个属性对应的getter和setter 2、脏值检测(angular):通过特定事件比如input,change,xhr请求等进行脏值检测。 3、发布-订阅模式(backbone):通过发布消息,订阅消息进行数据和视图的绑定监听。具体代码实现可以参考我github个人仓库overwrite->my-observer 一言不合先上代码和效果图吧code <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>example</title> <script src="./mvvm.js" charset="utf-8"></script> </head> <body> <div id="mvvm"> <h2>{{b}}</h2> <input type="text" x-model="a"> <input type="text" name="" value="" x-model="a"> <p x-html="a">{{ a }}</p> <button type="button" name="button" x-on:click="testToggle">change b</button> </div> </body> <script> var vm = new MVVM({ el: '#mvvm', data: { a: 'test model', b: 'hello MVVM', flag: true }, methods: { testToggle: function () { this.flag = !this.flag; this.b = this.flag ? 'hello MVVM' : 'test success' } } }); </script> </html> 效果图 看完效果图之后,接下来我们直接搞事情吧 一、总体大纲 要实现一个我们自己的mvvm库,我们首先需要做的事情不是写代码,而是整理一下思路,捋清楚之后再动手绝对会让你事半功倍。先上流程图,我们对着流程图来捋思路 如上图所示,我们可以看到,整体实现分为四步 1、实现一个Observer,对数据进行劫持,通知数据的变化 2、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数 3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update 4、实现MVVM,整合以上三者,作为一个入口函数 二、动手时间 思路捋清楚了,接下来要做的事就是开始动手。能动手的我决不动口 1、实现Observer 这里我们需要做的事情就是实现数据劫持,并将数据变更给传递下去。那么这里将会用到的方法就是Object.defineProperty()来做这么一件事。先不管三七二十一,咱先用用Object.defineProperty()试试手感。 function observe (data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(key => { observeProperty(data, key, data[key]) }) } function observeProperty (obj, key, val) { observe(val); Object.defineProperty(obj, key, { enumerable: true, // 可枚举 configurable: true, // 可重新定义 get: function () { return val; }, set: function (newVal) { if (val === newVal || (newVal !== newVal && val !== val)) { return; } console.log('数据更新啦 ', val, '=>', newVal); val = newVal; } }); } 调用 var data = { a: 'hello' } observe(data); 效果如下 看完是不是发现JavaScript提供给我们的Object.defineProperty()方法功能巨强大巨好用呢。 其实到这,我们已经算是完成了数据劫持,完整的Observer则需要将数据的变更传递给Dep实例,然后接下来的事情就丢给Dep去通知下面完成接下来的事情了,完整代码如下所示 /** * @class 发布类 Observer that are attached to each observed * @param {[type]} value [vm参数] */ function observe(value, asRootData) { if (!value || typeof value !== 'object') { return; } return new Observer(value); } function Observer(value) { this.value = value; this.walk(value); } Observer.prototype = { walk: function (obj) { let self = this; Object.keys(obj).forEach(key => { self.observeProperty(obj, key, obj[key]); }); }, observeProperty: function (obj, key, val) { let dep = new Dep(); let childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { dep.depend(); } if (childOb) { childOb.dep.depend(); } return val; }, set: function(newVal) { if (val === newVal || (newVal !== newVal && val !== val)) { return; } val = newVal; // 监听子属性 childOb = observe(newVal); // 通知数据变更 dep.notify(); } }) } } /** * @class 依赖类 Dep */ let uid = 0; function Dep() { // dep id this.id = uid++; // array 存储Watcher this.subs = []; } Dep.target = null; Dep.prototype = { /** * [添加订阅者] * @param {[Watcher]} sub [订阅者] */ addSub: function (sub) { this.subs.push(sub); }, /** * [移除订阅者] * @param {[Watcher]} sub [订阅者] */ removeSub: function (sub) { let index = this.subs.indexOf(sub); if (index !== -1) { this.subs.splice(index ,1); } }, // 通知数据变更 notify: function () { this.subs.forEach(sub => { // 执行sub的update更新函数 sub.update(); }); }, // add Watcher depend: function () { Dep.target.addDep(this); } } // 结合Watcher /** * Watcher.prototype = { * get: function () { * Dep.target = this; * let value = this.getter.call(this.vm, this.vm); * Dep.target = null; * return value; * }, * addDep: function (dep) { * dep.addSub(this); * } * } */ 至此,我们已经实现了数据的劫持以及notify数据变化的功能了。 2、实现Compile 按理说我们应该紧接着实现Watcher,毕竟从上面代码看来,Observer和Watcher关联好多啊,但是,我们在捋思路的时候也应该知道了,Watcher和Compile也是有一腿的哦。所以咱先把Compile也给实现了,这样才能更好的让他们3P。 我不是老司机,我只是一个纯洁的开电动车的孩子 废话不多说,干实事。 Compile需要做的事情也很简单 a、解析指令,将指令模板中的变量替换成数据,对视图进行初始化操作 b、订阅数据的变化,绑定好更新函数 c、接收到数据变化,通知视图进行view update 咱先试着写一个简单的指令解析方法,实现解析指令初始化视图。 js部分 function Compile (el, value) { this.$val = value; this.$el = this.isElementNode(el) ? el : document.querySelector(el); if (this.$el) { this.compileElement(this.$el); } } Compile.prototype = { compileElement: function (el) { let self = this; let childNodes = el.childNodes; [].slice.call(childNodes).forEach(node => { let text = node.textContent; let reg = /\{\{((?:.|\n)+?)\}\}/; // 如果是element节点 if (self.isElementNode(node)) { self.compile(node); } // 如果是text节点 else if (self.isTextNode(node) && reg.test(text)) { // 匹配第一个选项 self.compileText(node, RegExp.$1.trim()); } // 解析子节点包含的指令 if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }) }, // 指令解析 compile: function (node) { let nodeAttrs = node.attributes; let self = this; [].slice.call(nodeAttrs).forEach(attr => { var attrName = attr.name; if (self.isDirective(attrName)) { var exp = attr.value; node.innerHTML = typeof this.$val[exp] === 'undefined' ? '' : this.$val[exp]; node.removeAttribute(attrName); } }); }, // {{ test }} 匹配变量 test compileText: function (node, exp) { node.textContent = typeof this.$val[exp] === 'undefined' ? '' : this.$val[exp]; }, // element节点 isElementNode: function (node) { return node.nodeType === 1; }, // text纯文本 isTextNode: function (node) { return node.nodeType === 3 }, // x-XXX指令判定 isDirective: function (attr) { return attr.indexOf('x-') === 0; } } html部分 <body> <div id="test"> <h2 x-html="a"></h2> <p>{{ a }}</p> </div> </body> <script> var data = { a: 'hello' } new Compile('#test', data) </script> 结果如图所示 按照步骤走的我已经实现了指令解析! 这里我们只是实现了指令的解析以及视图的初始化,并没有实现数据变化的订阅以及视图的更新。完整的Compile则实现了这些功能,详细代码如下 /** * @class 指令解析类 Compile * @param {[type]} el [element节点] * @param {[type]} vm [mvvm实例] */ function Compile(el, vm) { this.$vm = vm; this.$el = this.isElementNode(el) ? el : document.querySelector(el); if (this.$el) { this.$fragment = this.nodeFragment(this.$el); this.compileElement(this.$fragment); // 将文档碎片放回真实dom this.$el.appendChild(this.$fragment) } } Compile.prototype = { compileElement: function (el) { let self = this; let childNodes = el.childNodes; [].slice.call(childNodes).forEach(node => { let text = node.textContent; let reg = /\{\{((?:.|\n)+?)\}\}/; // 如果是element节点 if (self.isElementNode(node)) { self.compile(node); } // 如果是text节点 else if (self.isTextNode(node) && reg.test(text)) { // 匹配第一个选项 self.compileText(node, RegExp.$1); } // 解析子节点包含的指令 if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }); }, // 文档碎片,遍历过程中会有多次的dom操作,为提高性能我们会将el节点转化为fragment文档碎片进行解析操作 // 解析操作完成,将其添加回真实dom节点中 nodeFragment: function (el) { let fragment = document.createDocumentFragment(); let child; while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, // 指令解析 compile: function (node) { let nodeAttrs = node.attributes; let self = this; [].slice.call(nodeAttrs).forEach(attr => { var attrName = attr.name; if (self.isDirective(attrName)) { var exp = attr.value; var dir = attrName.substring(2); // 事件指令 if (self.isEventDirective(dir)) { compileUtil.eventHandler(node, self.$vm, exp, dir); } // 普通指令 else { compileUtil[dir] && compileUtil[dir](node, self.$vm, exp); } node.removeAttribute(attrName); } }); }, // {{ test }} 匹配变量 test compileText: function (node, exp) { compileUtil.text(node, this.$vm, exp); }, // element节点 isElementNode: function (node) { return node.nodeType === 1; }, // text纯文本 isTextNode: function (node) { return node.nodeType === 3 }, // x-XXX指令判定 isDirective: function (attr) { return attr.indexOf('x-') === 0; }, // 事件指令判定 isEventDirective: function (dir) { return dir.indexOf('on') === 0; } } // 定义$elm,缓存当前执行input事件的input dom对象 let $elm; let timer = null; // 指令处理集合 const compileUtil = { html: function (node, vm, exp) { this.bind(node, vm, exp, 'html'); }, text: function (node, vm, exp) { this.bind(node, vm, exp, 'text'); }, class: function (node, vm, exp) { this.bind(node, vm, exp, 'class'); }, model: function(node, vm, exp) { this.bind(node, vm, exp, 'model'); let self = this; let val = this._getVmVal(vm, exp); // 监听input事件 node.addEventListener('input', function (e) { let newVal = e.target.value; $elm = e.target; if (val === newVal) { return; } // 设置定时器 完成ui js的异步渲染 clearTimeout(timer); timer = setTimeout(function () { self._setVmVal(vm, exp, newVal); val = newVal; }) }); }, bind: function (node, vm, exp, dir) { let updaterFn = updater[dir + 'Updater']; updaterFn && updaterFn(node, this._getVmVal(vm, exp)); new Watcher(vm, exp, function(value, oldValue) { updaterFn && updaterFn(node, value, oldValue); }); }, // 事件处理 eventHandler: function(node, vm, exp, dir) { let eventType = dir.split(':')[1]; let fn = vm.$options.methods && vm.$options.methods[exp]; if (eventType && fn) { node.addEventListener(eventType, fn.bind(vm), false); } }, /** * [获取挂载在vm实例上的value] * @param {[type]} vm [mvvm实例] * @param {[type]} exp [expression] */ _getVmVal: function (vm, exp) { let val = vm; exp = exp.split('.'); exp.forEach(key => { key = key.trim(); val = val[key]; }); return val; }, /** * [设置挂载在vm实例上的value值] * @param {[type]} vm [mvvm实例] * @param {[type]} exp [expression] * @param {[type]} value [新值] */ _setVmVal: function (vm, exp, value) { let val = vm; exps = exp.split('.'); exps.forEach((key, index) => { key = key.trim(); if (index < exps.length - 1) { val = val[key]; } else { val[key] = value; } }); } } // 指令渲染集合 const updater = { htmlUpdater: function (node, value) { node.innerHTML = typeof value === 'undefined' ? '' : value; }, textUpdater: function (node, value) { node.textContent = typeof value === 'undefined' ? '' : value; }, classUpdater: function () {}, modelUpdater: function (node, value, oldValue) { // 不对当前操作input进行渲染操作 if ($elm === node) { return false; } $elm = undefined; node.value = typeof value === 'undefined' ? '' : value; } } 好了,到这里两个和Watcher相关的“菇凉”已经出场了 3、实现Watcher 作为一个和Observer和Compile都有关系的“蓝银”,他做的事情有以下几点 a、通过Dep接收数据变动的通知,实例化的时候将自己添加到dep中 b、属性变更时,接收dep的notify,调用自身update方法,触发Compile中绑定的更新函数,进而更新视图 这里的代码比较简短,所以我决定直接上代码 /** * @class 观察类 * @param {[type]} vm [vm对象] * @param {[type]} expOrFn [属性表达式] * @param {Function} cb [回调函数(一半用来做view动态更新)] */ function Watcher(vm, expOrFn, cb) { this.vm = vm; expOrFn = expOrFn.trim(); this.expOrFn = expOrFn; this.cb = cb; this.depIds = {}; if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = this.parseGetter(expOrFn); } this.value = this.get(); } Watcher.prototype = { update: function () { this.run(); }, run: function () { let newVal = this.get(); let oldVal = this.value; if (newVal === oldVal) { return; } this.value = newVal; // 将newVal, oldVal挂载到MVVM实例上 this.cb.call(this.vm, newVal, oldVal); }, get: function () { Dep.target = this; // 将当前订阅者指向自己 let value = this.getter.call(this.vm, this.vm); // 触发getter,将自身添加到dep中 Dep.target = null; // 添加完成 重置 return value; }, // 添加Watcher to Dep.subs[] addDep: function (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this); this.depIds[dep.id] = dep; } }, parseGetter: function (exp) { if (/[^\w.$]/.test(exp)) return; let exps = exp.split('.'); // 简易的循环依赖处理 return function(obj) { for (let i = 0, len = exps.length; i < len; i++) { if (!obj) return; obj = obj[exps[i]]; } return obj; } } } 没错就是Watcher这么一个简短的“蓝银”和Observer和Compile两位“菇凉”牵扯不清 4、实现MVVM 可以说MVVM是Observer,Compile以及Watcher的“boss”了,他才不会去管他们员工之间的关系,只要他们三能给干活,并且干好活就行。他需要安排给Observer,Compile以及Watche做的事情如下 a、Observer实现对MVVM自身model数据劫持,监听数据的属性变更,并在变动时进行notify b、Compile实现指令解析,初始化视图,并订阅数据变化,绑定好更新函数 c、Watcher一方面接收Observer通过dep传递过来的数据变化,一方面通知Compile进行view update 具体实现如下 /** * @class 双向绑定类 MVVM * @param {[type]} options [description] */ function MVVM (options) { this.$options = options || {}; let data = this._data = this.$options.data; let self = this; Object.keys(data).forEach(key => { self._proxyData(key); }); observe(data, this); new Compile(options.el || document.body, this); } MVVM.prototype = { /** * [属性代理] * @param {[type]} key [数据key] * @param {[type]} setter [属性set] * @param {[type]} getter [属性get] */ _proxyData: function (key, setter, getter) { let self = this; setter = setter || Object.defineProperty(self, key, { configurable: false, enumerable: true, get: function proxyGetter() { return self._data[key]; }, set: function proxySetter(newVal) { self._data[key] = newVal; } }) } } 至此,一个属于我们自己的mvvm库也算是完成了。由于本文的代码较多,又不太好分小部分抽离出来讲解,所以我将代码的解析都直接写到了代码中。文中一些不够严谨的思考和错误,还请各位小伙伴们拍砖指出,大家一起纠正一起学习。 三、源码链接 最后完整代码来源(再发一次) github-https://github.com/xuqiang521/overwrite 码云-https://git.oschina.net/qiangdada_129/overwrite 如果喜欢欢迎各位小伙伴们star,overwrite将不断更新哦 本文作者:qiangdada 本文发布时间:2017/05/21 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
之前我写过一篇有关vue组件开发的文章,这次将是对上次的一次拓展。其中也会有vue部分源码的解析,接下来直接上正文吧。 一、父子组件之间的通信 总所周知,如果进行组件开发的话,必定存在组件通信的问题,具体通信如何进行的呢,我借用一张vue官网的图 图中很明显可以看到,Parent组件通过props向下传递数据(props down),Child组件通过events向上传递消息(events up)。具体通信机制,请转链接 https://vuejs.org/v2/guide/components.html。那么如果不是父子组件关系,而是slot节点之间的关系,又该如何进行通信呢。下面的内容会带着大家一步一步解惑。 二、组件的开发 接下里的例子我将模拟element-ui中的dropdown下拉菜单组件,对组件开发进行详细的解剖。进行开发前我们先看一下element-ui中的dropdown组件实现了哪些功能(具体功能转链接http://element.eleme.io/#/zh-CN/component/dropdown),这里我们挑选一些不会涉及到调用其他element-ui组件的功能,接下来,希望小伙伴们跟着我一起慢慢实现一个属于自己的element-ui组件吧。 1、组件设计 如上图所示,整个dropdown组件分成了三个组件模块,最外层的dropdown,下拉菜单dropdown-menu,以及下拉列表dropdown-list。至于为何这样设计,主要是为了该组件的cover范围可以大,可以适用各种场景。 我们先看下实现功能后每个组件对应的template内容a、dropdown组件 <style media="screen"> .v-dropdown { display: inline-block; position: relative; color: #48576a; font-size: 14px; } </style> <template> <div class="v-dropdown" :trigger="trigger" :visible="visible" :hideOnClick="hideOnClick" v-clickoutside="hide" > <slot></slot> </div> </template> b、dropdown-menu组件 <style media="screen"> .v-dropdown-menu { margin: 5px 0; background-color: #fff; border: 1px solid #d1dbe5; box-shadow: 0 2px 4px rgba(0,0,0,.12), 0 0 6px rgba(0,0,0,.12); padding: 6px 0; z-index: 10; position: absolute; top: 20px; left: 0; min-width: 100px; } </style> <template> <ul class="v-dropdown-menu" v-show="visible"> <slot></slot> </ul> </template> c、dropdown-list组件 <style media="screen"> ul, li { list-style: none; } .v-dropdown-menu_list { cursor: pointer; } </style> <template> <li class="v-dropdown-menu_list" @click="handleClick" :command="command" > <slot></slot> </li> </template> 如上,大家也可以看出来,dropdown组件负责一个全局的控制,他通过向dropdown-menu组件传递visible属性控制着其消失与显示,对于dropdown-list组件点击事件的回调与否的控制则是通过$emit监听dropdown组件中是否存在自定义事件command。 2、组件功能的实现 a、dropdown-menu消失与显示 首先我们实现一个基本功能,通过hover或者click事件控制dropdown-menu组件的显示与否,这里我们需要给dropdown组件绑定两个属性,一个是visible,一个是trigger。 <template> <div class="v-dropdown" :trigger="trigger" :visible="visible" > <slot></slot> </div> </template> 这里我们需要先重写两个方法,一个是broadcast(向下传递),一个是dispatch(向上传递),后面的传递也基本基于这两种方法。具体的mixins方法emitter.js如下 /** * [broadcast 上下传递] * @param {[type]} componentName [组件别名] * @param {[type]} eventName [事件别名] * @param {[type]} params [事件回调参数] */ function broadcast(componentName, eventName, params) { // 遍历当前实例的children节点 this.$children.forEach(child => { var name = child.$options.componentName; // 如果子节点名称和组件别名相同,则当前子节点为目标节点 if (name === componentName) { // 找到目标节点后,触发事件别名对应的回调,并将参数传入 child.$emit.apply(child, [eventName].concat(params)); } // 如果子节点名称和组件别名不相同,继续遍历子节点的子节点,以此类推,直到找到目标节点 else { broadcast.apply(child, [componentName, eventName].concat([params])); } }); } /** * [dispatch 向上传递] * @param {[type]} componentName [组件别名] * @param {[type]} eventName [事件别名] * @param {[type]} params [事件回调参数] */ function dispatch(componentName, eventName, params) { var parent = this.$parent || this.$root; var name = parent.$options.name; // 向上找目标父节点,如果上一级父节点不符合,则继续往上查询 while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } // 找到目标父节点后,触发事件别名对应的回调,并将参数传入 if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } } export default { methods: { broadcast(componentName, eventName, params) { broadcast.apply(this, [componentName, eventName, params]); }, dispatch(componentName, eventName, params) { dispatch.apply(this, [componentName, eventName, params]); } } }; 好了对于事件传递的mixins方法我们已经写好了,接下来我们需要做的就是通过在dropdown-menu组件中注册好visible事件,代码如下 <template> <ul class="v-dropdown-menu" v-show="visible"> <slot></slot> </ul> </template> <script> export default { name: 'VDropdownMenu', componentName: 'VDropdownMenu', // 组件create的时候进行事件注册 created () { this.$on('visible', val => { this.visible = val; }); } }; </script> 对于dropdown组件,则是通过watch visible属性,如果visible属性发生改变则将visible属性的最新值传递给dropdown-menu组件并触发其回调。而对于visible属性的控制,具体如下 <template> <div class="v-dropdown" :trigger="trigger" :visible="visible" :hideOnClick="hideOnClick" v-clickoutside="hide" > <slot></slot> </div> </template> <script> // vue自带指令,点击节点以外地方,并触发回调 import Clickoutside from 'element-ui/src/utils/clickoutside'; import Emitter from 'element-ui/src/mixins/emitter'; export default { name: 'VDropdown', componentName: 'VDropdown', mixins: [Emitter], // 注册指令 directives: { Clickoutside }, props: { trigger: { type: String, default: 'hover' }, hideOnClick: { type: Boolean, default: true } }, data () { return { timeout: null, visible: false } }, methods: { // 显示 show () { let that = this; clearTimeout(this.timeout); this.timeout = setTimeout(function () { that.visible = true; }, 150); }, // 隐藏 hide () { let that = this; clearTimeout(this.timeout); this.timeout = setTimeout(function () { that.visible = false; }, 150); }, // click事件的处理 handleClick () { this.visible = !this.visible; }, initEvent () { let {trigger, show, hide, handleClick} = this; // 触发事件的elm节点 let triggerElm = this.$slots.default[0].elm; // hover事件处理 if (trigger === 'hover') { triggerElm.addEventListener('mouseenter', show); triggerElm.addEventListener('mouseleave', hide); } // click事件处理 else if (trigger === 'click') { triggerElm.addEventListener('click', handleClick); } } }, watch: { // 向下传递,即VDropdownMenu组件传递visible属性并触发其回调 visible (val) { this.broadcast('VDropdownMenu', 'visible', val); } }, mounted () { this.initEvent(); } }; </script> 写到这里dropdown-menu的消失与显示的功能则已实现。 b、dropdown-list点击事件command指令的实现 在这里,我们需要实现的则是对于dropdown-list组件的拓展功能的实现,试想,如果我需要在点击dropdown-list的时候做一些自定义的事件,该如何实现呢?那么接下来我们要做的就是给人提供一个对外的指令接口command,$emit监测到command指令的时候触发其自定义的事件回调。 首先我们看看dropdown-list进行的操作,具体如下 <template> <li class="v-dropdown-menu_list" @click="handleClick" :command="command" > <slot></slot> </li> </template> <script> import Emitter from 'element-ui/src/mixins/emitter'; export default { name: 'VDropdownList', mixins: [Emitter], props: { command: String }, methods: { // 点击dropdown-list时,向上传递,即监听VDropdown的 menu-list-click自定义事件并触发其回调 handleClick (e) { this.dispatch('VDropdown', 'menu-list-click', [this.command, this]); } } }; </script> 对于dropdown组件中,需要做的事情便是在组件渲染完成后通过$on注册 'menu-list-click'事件,如下 this.$on('menu-list-click', this.handleMenuListClick); 需要被触发的回调如下 handleMenuListClick (command, instance) { // 点击list后是否隐藏menu,属性通过hideOnClick控制 if (this.hideOnClick) { this.visible = false; } // 监听command指令,并触发其回调 this.$emit('command', command, instance); } 调用如下 <template> <v-dropdown trigger="click" @command="commandHandle" :hide-on-click="false"> <span class="drop-down_link">下拉菜单</span> <v-dropdown-menu> <v-dropdown-list command="a">下拉列表1</v-dropdown-list> <v-dropdown-list command="b">下拉列表2</v-dropdown-list> <v-dropdown-list command="c"><h4>下拉列表3</h4></v-dropdown-list> </v-dropdown-menu> </v-dropdown> </template> <script> export default { methods: { commandHandle(command) { console.log(command); } } } </script> 执行结果如下(点击每个列表) 到这里,点击dropdown-list触发的事件回调也就完成了。我们需要完成的属于自己的dropdown组件也算是完成了。 三、vue部分源码解析 我们看到上面的代码可以看出,对于组件之间的消息与事件的传递我们是通过$on,$emit完成的。当然我们看文档还知道,vue还提供了$once,$off的实例方法(API链接:https://vuejs.org/v2/api/#vm-on)。那么对于$on,$once,$off,$emit,vue的作者又是如何实现的呢。 其实从上面$on,$emit实现的功能来看,我们就能看出,对于$on,他就像一个发布者,只负责发布消息。而$emit则相当于订阅者,监听发布者发布的消息。而$once则只发布一次消息,$off则取消发布的消息。想要了解观察者模式(发布-订阅者模式)的小伙伴请先转链接http://www.sxrczx.com/docs/js/2355128.html。 下面我将直接将源码及我写好的注释放给大家,具体如下 var hookRE = /^hook:/; /** * [$on 事件注册] * @param {[type]} event [注册事件别名] * @param {Function} fn [注册事件对应回调] */ Vue.prototype.$on = function (event, fn) { var this$1 = this; var vm = this; // 遍历需要发布的消息是否是数组,如果是,则循环注册 if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { this$1.$on(event[i], fn); } // 如果不是则单次注册 } else { // 默认值 vm._events = Object.create(null); 通过数组的push()将注册事件回调保存在vm._events[event]中 (vm._events[event] || (vm._events[event] = [])).push(fn); if (hookRE.test(event)) { // 默认值vm._hasHookEvent = false vm._hasHookEvent = true; } } return vm }; /** * [$once 仅注册一次事件] * @param {[type]} event [注册事件别名] * @param {Function} fn [注册事件对应回调] */ Vue.prototype.$once = function (event, fn) { var vm = this; // 定义 on()函数进行事件监听并移除,同时作为$on() 函数的回调执行 function on () { // 移除事件 vm.$off(event, on); // 执行回调,进行事件监听 fn.apply(vm, arguments); } on.fn = fn; vm.$on(event, on); return vm }; /** * [$off 事件移除] * @param {[type]} event [注册事件别名] * @param {Function} fn [注册事件对应回调] */ Vue.prototype.$off = function (event, fn) { var this$1 = this; var vm = this; // 移除所有的事件监听器 if (!arguments.length) { vm._events = Object.create(null); return vm } // 如果事件别名是数组,则循环将数组中对应的所有事件别名对应的监听器移除 if (Array.isArray(event)) { for (var i$1 = 0, l = event.length; i$1 < l; i$1++) { this$1.$off(event[i$1], fn); } return vm } // specific event var cbs = vm._events[event]; if (!cbs) { return vm } // 如果只传了事件别名一个参数,则移除该事件对应的所有监听器 if (arguments.length === 1) { vm._events[event] = null; return vm } // 参数中既传了事件别名,还传了回调 var cb; var i = cbs.length; // 遍历事件对应的所有监听器,即 cbs = vm._events[event] while (i--) { cb = cbs[i]; // 如果找到目标监听器,则通过splice移除数组中的监听器,并通过break终止循环 if (cb === fn || cb.fn === fn) { cbs.splice(i, 1); break } } return vm }; /** * [$emit 触发事件] * @param {[type]} event [事件别名] */ Vue.prototype.$emit = function (event) { var vm = this; { var lowerCaseEvent = event.toLowerCase(); if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( "Event \"" + lowerCaseEvent + "\" is emitted in component " + (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " + "Note that HTML attributes are case-insensitive and you cannot use " + "v-on to listen to camelCase events when using in-DOM templates. " + "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"." ); } } // 定义cbs接收 vm._events[event] var cbs = vm._events[event]; if (cbs) { // 通过判断cbs缓存的监听器个数,确保cbs为数组,以便下面的循环执行 cbs = cbs.length > 1 ? toArray(cbs) : cbs; var args = toArray(arguments, 1); // 遍历数组cbs,循环执行数组cbs中的方法 for (var i = 0, l = cbs.length; i < l; i++) { cbs[i].apply(vm, args); } } return vm }; OK,到这里,这篇博客也该谢幕了,相信看到这里,小伙伴们应该也能写出属于自己的element-ui组件,并且理解了vue是如何进行事件的注册,及事件的回调触发的。对于博客中我实现的dropdown组件,后期我会做下整理并上传到github。小伙伴们关注走一波,后续动弹更精彩哦! 本文作者:qiangdada 本文发布时间:2017/04/30 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
前言 3D 场景中的面不只有水平面这一个,空间是由无数个面组成的,所以我们有可能会在任意一个面上放置物体,而空间中的面如何确定呢?我们知道,空间中的面可以由一个点和一条法线组成。这个 Demo 左侧为面板,从面板中拖动物体到右侧的 3D 场景中,当然,我鼠标拖动到的位置就是物体放置的点,但是这次我们的重点是如何在斜面上放置模型。 效果图 代码生成 创建场景 dm = new ht.DataModel();//数据模型(http://hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html) g3d = new ht.graph3d.Graph3dView(dm);//3D 场景组件(http://hightopo.com/guide/guide/core/3d/ht-3d-guide.html) palette = new ht.widget.Palette();//面板组件(http://hightopo.com/guide/guide/plugin/palette/ht-palette-guide.html) splitView = new ht.widget.SplitView(palette, g3d, 'h', 0.2);//分割组件,第三个参数为分割的方式 h 为左右分,v 为上下分;第四个参数为分割比例,大于 1 的值为绝对宽度,小于 1 则为比例 splitView.addToDOM();//将分割组件添加进 body 体中 关于这些组件的定义可以到对应的链接里面查看,至于将分割组件添加进 body 体中的 addToDOM 函数有必要解释一下(我每次都提,这个真的很重要!)。 HT 的组件一般都会嵌入 BorderPane、SplitView 和 TabView 等容器中使用,而最外层的 HT 组件则需要用户手工将 getView() 返回的底层 div 元素添加到页面的 DOM 元素中,这里需要注意的是,当父容器大小变化时,如果父容器是 BorderPane 和 SplitView 等这些HT预定义的容器组件,则 HT 的容器会自动递归调用孩子组件 invalidate 函数通知更新。但如果父容器是原生的 html 元素, 则 HT 组件无法获知需要更新,因此最外层的 HT 组件一般需要监听 window 的窗口大小变化事件,调用最外层组件 invalidate 函数进行更新。 为了最外层组件加载填充满窗口的方便性,HT 的所有组件都有 addToDOM 函数,其实现逻辑如下,其中 iv 是 invalidate 的简写: addToDOM = function(){ var self = this, view = self.getView(),//获取组件的底层 div style = view.style; document.body.appendChild(view);//将组件底层div添加进body中 style.left = '0';//ht 默认将所有的组件的position都设置为absolute绝对定位 style.right = '0'; style.top = '0'; style.bottom = '0'; window.addEventListener('resize', function () { self.iv(); }, false);//窗口大小改变事件,调用刷新函数 } 大家可能注意到了,场景中我添加的斜面实际上就是一个 ht.Node 节点,作为与地平面的参照,在这样的对比下立体感会更强一点。下面是这个节点的定义: node = new ht.Node(); node.s3(1000, 1, 1000);//设置节点的大小 node.r3(0, 0, Math.PI/4);//设置节点旋转 这个旋转的角度是有学问的,跟下面我们要设置的拖拽放置的位置有关系 node.s('3d.movable', false);//设置节点在3d上不可移动 因为这个节点只是一个参照物,建议是不允许移动 dm.add(node);//将节点添加进数据容器中 左侧内容构建 Palette 和 GraphView 类似,由 ht.DataModel 驱动,用 ht.Group 展示分组,ht.Node 展示按钮元素。我将加载 Palette 面板中的图元函数封装为 initPalette,定义如下: function initPalette() {//加载palette面板组件中的图元 var arrNode = ['displayDevice', 'cabinetRelative', 'deskChair', 'temperature', 'indoors', 'monitor','others']; var nameArr = ['展示设施', '机柜相关', '桌椅储物', '温度控制', '室内', '视频监控', '其他'];//arrNode中的index与nameArr中的一一对应 for (var i = 0; i < arrNode.length; i++) { var name = nameArr[i]; var vName = arrNode[i]; arrNode[i] = new ht.Group();//palette面板是将图元都分在“组”里面,然后向“组”中添加图元即可 palette.dm().add(arrNode[i]);//向palette面板组件中添加group图元 arrNode[i].setExpanded(true);//设置分组为打开的状态 arrNode[i].setName(name);//设置组的名字 显示在分组上 var imageArr = []; switch(i){//根据不同的分组设置每个分组中不同的图元 case 0: imageArr = ['models/机房/展示设施/大屏.png']; break; case 1: imageArr = ['models/机房/机柜相关/配电箱.png', 'models/机房/机柜相关/室外天线.png', 'models/机房/机柜相关/机柜1.png', 'models/机房/机柜相关/机柜2.png', 'models/机房/机柜相关/机柜3.png', 'models/机房/机柜相关/机柜4.png', 'models/机房/机柜相关/电池柜.png']; break; case 2: imageArr = ['models/机房/桌椅储物/储物柜.png', 'models/机房/桌椅储物/桌子.png', 'models/机房/桌椅储物/椅子.png']; break; case 3: imageArr = ['models/机房/温度控制/空调精简.png', 'models/机房/消防设施/消防设备.png']; break; case 4: imageArr = ['models/室内/办公桌简易.png', 'models/室内/书.png', 'models/室内/办公桌镜像.png', 'models/室内/办公椅.png']; break; case 5: imageArr = ['models/机房/视频监控/摄像头方.png', 'models/机房/视频监控/对讲维护摄像头.png', 'models/机房/视频监控/微型摄像头.png']; break; default: imageArr = ['models/其他/信号塔.png']; break; } setPalNode(imageArr, arrNode[i]);//创建palette上节点及设置名称、显示图片、父子关系 } } 我在 setPalNode 函数中做了一些名称的设置,主要是想要根据上面 initPalette 函数中我传入的路径名称来设置模型的名称以及在不同文件在不同的文件夹下的路径: function setPalNode(imageArr, arr) { for (var j = 0; j < imageArr.length; j++) { var imageName = imageArr[j]; var jsonUrl = imageName.slice(0, imageName.lastIndexOf('.')) + '.json';//shape3d中的 json 路径 var name = imageName.slice(imageName.lastIndexOf('/')+1, imageName.lastIndexOf('.')); //取最后一个/和.之间的字符串用来设置节点名称 var url = imageName.slice(imageName.indexOf('/')+1, imageName.lastIndexOf('.'));//取第一个/和最后一个.之间的字符串用来设置拖拽生成模型obj文件的路径 createNode(name, imageName, arr, url, jsonUrl);//创建节点,这个节点是显示在palette面板上 } } createNode 创建节点的函数比较简单: function createNode(name, image, parent, urlName, jsonUrl) {//创建palette面板组件上的节点 var node = new ht.Node(); palette.dm().add(node); node.setName(name);//设置节点名称 palette面板上显示的文字也是通过这个属性设置名称 node.setImage(image);//设置节点的图片 node.setParent(parent);//设置父亲节点 node.s({ 'draggable': true,//设置节点可拖拽 'image.stretch': 'centerUniform',//设置节点图片的绘制方式 'label': ''//设置节点的label为空,这样即使设置了name也不会显示在3d中的模型下方 }); node.a('urlName', urlName);//a设置用户自定义属性 node.a('jsonUrl', jsonUrl); return node; } 虽然简单,但是还是要提一下,draggable: true 为设置节点可拖拽,否则节点不可拖拽;还有 node.s 是 HT 默认封装好的样式设置方法,如果用户需要自己添加方法,则可通过 node.a 方法来添加,参数一为用户自定义名称,参数二为用户自定义值,不仅能传常量,也能传变量、对象,还能传函数!又是一个非常强大的功能。 拖拽功能 拖拽基本上就是响应 windows 自带的 dragover 以及 drop 事件,要在放开鼠标的时候创建模型,就要在事件触发时生成模型: function dragAndDrop() {//拖拽功能 g3d.getView().addEventListener("dragover", function(e) {//拖拽事件 e.dataTransfer.dropEffect = "copy"; handleOver(e); }); g3d.getView().addEventListener("drop", function(e) {//放开鼠标事件 handleDrop(e); }); } function handleOver(e) { e.preventDefault();//取消事件的默认动作。 } function handleDrop(e) {//鼠标放开时 e.preventDefault();//取消事件的默认动作。 var paletteNode = palette.dm().sm().ld();//获取palette面板中最后选中的节点 if (paletteNode) { loadObjFunc('assets/objs/' + paletteNode.a('urlName') + '.obj', 'assets/objs/' + paletteNode.a('urlName') + '.mtl', paletteNode.a('jsonUrl'), g3d.getHitPosition(e, [0, 0, 0], [-1, 1, 0]));//加载obj模型 } } 这里完全有必要说明一下,这个 Demo 的重点来了! loadObjFunc 函数中的最后一个参数为生成模型的 position3d 坐标,g3d.getHitPosition 这个方法总共有三个参数,第一个参数为事件类型,第二和第三个参数如果不设置,则默认为水平面的中心点也就是 [0, 0, 0] 以及法线为 y 轴,也就是 [0, 1, 0],一条法线和一个点就可以确定一个面,所以我们通过这个方法来设置这个节点所要放置的平面是在哪一个面上,我前面将 node 节点设置为绕 z 轴旋转 45° 角,所以这边的法线也就要好好想想如何设置了,这是数学上的问题,要自己思考了。 加载模型 HT 通过 ht.Default.loadObj 函数来加载模型,但是前提是要有一个节点,然后再在这个节点上加载模型: function loadObjFunc(objUrl, mtlUrl, jsonUrl, p3) {//加载obj模型 var node = new ht.Node(); var shape3d = jsonUrl.slice(jsonUrl.lastIndexOf('/')+1, jsonUrl.lastIndexOf('.')); ht.Default.loadObj(objUrl, mtlUrl, {//HT 通过 loadObj 函数来加载 obj 模型 cube: true,//是否将模型缩放到单位1的尺寸范围内,默认为false center: true,//模型是否居中,默认为false,设置为true则会移动模型位置使其内容居中 shape3d: shape3d,//如果指定了shape3d名称,则HT将自动将加载解析后的所有材质模型构建成数组的方式,以该名称进行注册 finishFunc: function(modelMap, array, rawS3) {//用于加载后的回调处理 if (modelMap) { node.s({//设置节点样式 'shape3d': jsonUrl,//jsonUrl 为 obj 模型的 json 文件路径 'label': ''//设置label为空,label的优先级高于name,所以即使设置了name,节点的下方也不会显示name名称 }); g3d.dm().add(node);//将节点添加进数据容器中 node.s3(rawS3);//设置节点大小 rawS3 模型的原始尺寸 node.p3(p3);//设置节点的三维坐标 node.setName(shape3d);//设置节点名称 node.setElevation(node.s3()[1]/2);//控制Node图元中心位置所在3D坐标系的y轴位置 g3d.sm().ss(node);//设置选中当前节点 g3d.setFocus(node);//将焦点设置在当前节点上 return node; } } }); } 代码结束! 总结 说实在的这个 Demo 真的是非常容易,难度可能在于空间思维能力了,先确认法线和点,然后根据法线和点找到那个面,这个面按照我的这种方式有个对照还比较能够理解,真幻想的话,可能容易串。这个 Demo 容易主要还是因为封装的 hitPosition 函数简单好用,这个真的是功不可没。 本文作者:qiangdada 本文发布时间:2018/03/13 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
webpack.config.js文件 const path = require('path'); let htmlwebpackplugin = require('html-webpack-plugin');//引入html-webpack-plugin插件 let export_html= { entry: { main:__dirname+"/app/js/main.js",//入口文件 main1:__dirname+"/app/js/main1.js",//入口文件 }, output: { path: __dirname+"/_build/", filename: "js/[name].js",//产出文件,name根据entry的入口文件键名定 }, module: { loaders: [ { test: /(\.jsx|\.js)$/, loader: 'babel-loader', query: { presets: ['es2015'] } }, ] } , plugins: [ //new一个模板出来,这一个不使用chunks new htmlwebpackplugin({ template: './app/home.html',//入口文件 filename: 'home1.html',//产出文件 }), //new一个模板出来 new htmlwebpackplugin({ template: './app/home.html',//入口文件 filename: 'home2.html',//产出文件 chunks : ['main'],//可以设置chunks按需引入JS文件,不设置就会引入所有产出的js chunksSortMode: 'manual',//将chunks按引入的顺序排序,不用这个的话,引入到html的JS可能是错乱排序的 }) ] }; module.exports=export_html; 看plugins这里 //new一个模板出来,这一个不使用chunks new htmlwebpackplugin({ template: './app/home.html', filename: 'home1.html',// 会生成home1.html }), //new一个模板出来 new htmlwebpackplugin({ template: './app/home.html', filename: 'home2.html',//会生成home2.html chunks : ['main'],//注意:chunks里面的值是对应entry入口的键名来的 chunksSortMode: 'manual', }) app目录下的home.html文件 _build目录下的home1.html文件 _build目录下的home2.html文件 可以看到,home1.html引入了两个js文件,而且main1.js排在main.js前面, 而home2.html,只引入了指定的main.js; 在home2.html的chunks加上:main1 //new一个模板出来 new htmlwebpackplugin({ template: './app/home.html',//入口文件 filename: 'home2.html',//产出文件 chunks : ['main',"main1"],//可以设置chunks按需引入JS文件,不设置就会引入所有产出的js chunksSortMode: 'manual',//将chunks按引入的顺序排序,不用这个的话,引入到html的JS可能是错乱排序的 }) 因为chunks里,main在main1之前,所以引入的文件也是按照这个顺序来的; 顺序的问题主要归功于:这一条属性 chunksSortMode: 'manual',//将chunks按引入的顺序排序,不用这个的话,引入到html的JS可能是错乱排序的 更进一步: 每次都这样new很麻烦,故而写个函数简化过程 let get_html = function(name,chunk){//封装 return { template: './app/ejs for html/'+ name + '.ejs', filename: name+ '.html', chunks : ['main',chunk||name],//可以设置chunks按需引入JS文件,不设置就会引入所有产出的js chunksSortMode: 'manual',//将chunks按引入的顺序排序 inject : true, hash : true, xhtml : true } }; 然后在plugin里面new一个测试一下; 此时,webpack.config.js: const path = require('path'); let htmlwebpackplugin = require('html-webpack-plugin');//引入html-webpack-plugin插件 let get_html = function(name,chunk){//封装 return { template: './app/'+ name + '.html', filename: name+ '.html', chunks : ['main',chunk||null],//这里引入公共文件main.js,另外一个文件按需引入,当然也可以把这个的值设为数组,传入function的第二个值用数组就行 chunksSortMode: 'manual',//将chunks按引入的顺序排序 inject : true,//所有JavaScript资源插入到body元素的底部 hash : true,//避免缓存 xhtml : true //规范html书写 } }; let export_html= { entry: { main:__dirname+"/app/js/main.js",//入口文件 main1:__dirname+"/app/js/main1.js",//入口文件 }, output: { path: __dirname+"/_build/", filename: "js/[name].js",//产出文件,name根据entry的入口文件键名定 }, module: { loaders: [ { test: /(\.jsx|\.js)$/, loader: 'babel-loader', query: { presets: ['es2015'] } }, ] } , plugins: [ //new一个模板出来测试一下 new htmlwebpackplugin(get_html("home","main1")) ] }; module.exports=export_html; 结果: 成功! 本文作者:qiangdada 本文发布时间:2018/03/22 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
项目相关 自我介绍:职业经历,项目经历 选一个你觉得印象最深的项目讲一讲,然后会从项目里面切入到 web 基础(html/css/js),这一块大概会聊 20-30 分钟,所以一定要提前选好一个自己做过的得意的项目,花一点时间捋一捋你觉得项目中出色的点,用到了比较 hack,比较酷炫的方法解决了哪些痛点。 JS 基础(ES5) 原型:这里可以谈很多,只要围绕 [[ prototype ]] 谈,都没啥问题 闭包:牵扯作用域,可以两者联系起来一起谈 作用域:词法作用域,动态作用域 this:不同情况的调用,this 指向分别如何。顺带可以提一下 es6 中箭头函数没有 this, arguments, super 等,这些只依赖包含箭头函数最接近的函数 call,apply,bind 三者用法和区别:参数、绑定规则(显示绑定和强绑定),运行效率(最终都会转换成一个一个的参数去运行)、运行情况(call,apply 立即执行,bind 是return 出一个 this “固定”的函数,这也是为什么 bind 是强绑定的一个原因)。 注:“固定”这个词的含义,它指的固定是指只要传进去了 context,则 bind 中 return 出来的函数 this 便一直指向 context,除非 context 是个变量 6. 变量声明提升:js 代码在运行前都会进行 AST 解析,函数申明默认会提到当前作用域最前面,变量申明也会进行提升。但赋值不会得到提升。关于 AST 解析,这里也可以说是形成词法作用域的主要原因 这里如果面试官问到2,3,4,5,6中的一点,你能够把2,3,4,5,6整理到一起,串联起来进行统一的回答效果极佳 具体参考 从指向看JavaScript JS 基础(ES6) let,const:let 产生块级作用域(通常配合 for 循环或者 {} 进行使用产生块级作用域),const 申明的变量是常量(内存地址不变) Promise:这里你谈 promise 的时候,除了将他解决的痛点以及常用的 API 之外,最好进行拓展把 eventloop 带进来好好讲一下,microtask、macrotask 的执行顺序,如果看过 promise 源码,最好可以谈一谈 原生 Promise 是如何实现的。Promise 的关键点在于callback 的两个参数,一个是 resovle,一个是 reject。还有就是 Promise 的链式调用(Promise.then(),每一个 then 都是一个责任人)。 详细参考 my-promise Generator:遍历器对象生成函数,最大的特点是可以交出函数的执行权 function 关键字与函数名之间有一个星号; 函数体内部使用 yield 表达式,定义不同的内部状态; next 指针移向下一个状态 这里你可以说说 Generator 的异步编程,以及它的语法糖 async 和 awiat,传统的异步编程。ES6 之前,异步编程大致如下 回调函数 事件监听 发布/订阅 传统异步编程方案之一:协程,多个线程互相协作,完成异步任务。 async、await:Generator 函数的语法糖。有更好的语义、更好的适用性、返回值是 Promise。 async => * await => yield 基本用法 async function timeout (ms) { await new Promise((resolve) => { setTimeout(resolve, ms) }) } async function asyncConsole (value, ms) { await timeout(ms) console.log(value) } asyncConsole('hello async and await', 1000) 注:最好把2,3,4 连到一起讲 AMD,CMD,CommonJs,ES6 Module:解决原始无模块化的痛点 AMD:requirejs 在推广过程中对模块定义的规范化产出,提前执行,推崇依赖前置 CMD:seajs 在推广过程中对模块定义的规范化产出,延迟执行,推崇依赖就近 CommonJs:模块输出的是一个值的 copy,运行时加载,加载的是一个对象(module.exports 属性),该对象只有在脚本运行完才会生成 ES6 Module:模块输出的是一个值的引用,编译时输出接口,ES6 模块不是对象,它对外接口只是一种静态定义,在代码静态解析阶段就会生成。 CSS相关 左边定宽,右边自适应方案:float + margin,float + calc /* 方案1 */ .left { width: 120px; float: left; } .right { margin-left: 120px; } /* 方案2 */ .left { width: 120px; float: left; } .right { width: calc(100% - 120px); float: left; } 左右两边定宽,中间自适应:float,float + calc, 圣杯布局(设置BFC,margin负值法),flex .wrap { width: 100%; height: 200px; } .wrap > div { height: 100%; } /* 方案1 */ .left { width: 120px; float: left; } .right { float: right; width: 120px; } .center { margin: 0 120px; } /* 方案2 */ .left { width: 120px; float: left; } .right { float: right; width: 120px; } .center { width: calc(100% - 240px); margin-left: 120px; } /* 方案3 */ .wrap { display: flex; } .left { width: 120px; } .right { width: 120px; } .center { flex: 1; } 左右居中 行内元素: text-align: center 定宽块状元素: 左右 margin 值为 auto 不定宽块状元素: table布局,position + transform /* 方案1 */ .wrap { text-align: center } .center { display: inline; /* or */ /* display: inline-block; */ } /* 方案2 */ .center { width: 100px; margin: 0 auto; } /* 方案2 */ .wrap { position: relative; } .center { position: absulote; left: 50%; transform: translateX(-50%); } 上下垂直居中: 定高:margin,position + margin(负值) 不定高:position + transform,flex,IFC + vertical-align:middle /* 定高方案1 */ .center { height: 100px; margin: 50px 0; } /* 定高方案2 */ .center { height: 100px; position: absolute; top: 50%; margin-top: -25px; } /* 不定高方案1 */ .center { position: absolute; top: 50%; transform: translateY(-50%); } /* 不定高方案2 */ .wrap { display: flex; align-items: center; } .center { width: 100%; } /* 不定高方案3 */ /* 设置 inline-block 则会在外层产生 IFC,高度设为 100% 撑开 wrap 的高度 */ .wrap::before { content: ''; height: 100%; display: inline-block; vertical-align: middle; } .wrap { text-align: center; } .center { display: inline-block; vertical-align: middle; } 盒模型:content(元素内容) + padding(内边距) + border(边框) + margin(外边距) 延伸: box-sizing content-box:默认值,总宽度 = margin + border + padding + width border-box:盒子宽度包含 padding 和 border,总宽度 = margin + width inherit:从父元素继承 box-sizing 属性 BFC、IFC、GFC、FFC:FC(Formatting Contexts),格式化上下文 BFC:块级格式化上下文,容器里面的子元素不会在布局上影响到外面的元素,反之也是如此(按照这个理念来想,只要脱离文档流,肯定就能产生 BFC)。产生 BFC 方式如下 float 的值不为 none。 overflow 的值不为 visible。 position 的值不为 relative 和 static。 display 的值为 table-cell, table-caption, inline-block中的任何一个。 用处?常见的多栏布局,结合块级别元素浮动,里面的元素则是在一个相对隔离的环境里运行。 IFC:内联格式化上下文,IFC 的 line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的 padding/margin 影响)。 IFC中的line box一般左右都贴紧整个 IFC,但是会因为 float 元素而扰乱。float 元素会位于 IFC 与 line box 之间,使得 line box 宽度缩短。 同个 ifc 下的多个 line box 高度会不同。 IFC 中时不可能有块级元素的,当插入块级元素时(如 p 中插入 div )会产生两个匿名块与 div 分隔开,即产生两个 IFC ,每个 IFC 对外表现为块级元素,与 div 垂直排列。 用处? 水平居中:当一个块要在环境中水平居中时,设置其为 inline-block 则会在外层产生IFC,通过 text-align 则可以使其水平居中。 垂直居中:创建一个 IFC,用其中一个元素撑开父元素的高度,然后设置其 vertical-align: middle,其他行内元素则可以在此父元素下垂直居中 GFC:网格布局格式化上下文(display: grid) FFC:自适应格式化上下文(display: flex) 详细参考 css3中的BFC,IFC,GFC和FFC 框架相关 数据双向绑定原理:常见数据绑定的方案 Object.defineProperty(vue):劫持数据的 getter 和 setter 脏值检测(angularjs):通过特定事件进行轮循 发布/订阅模式:通过消息发布并将消息进行订阅 详细细节参考 实现一个属于我们自己的简易MVVM库 扩充:如何监听数组变化 VDOM:三个 part, 虚拟节点类,将真实 DOM 节点用 js 对象的形式进行展示,并提供 render 方法,将虚拟节点渲染成真实 DOM 节点 diff 比较:对虚拟节点进行 js 层面的计算,并将不同的操作都记录到 patch 对象 re-render:解析 patch 对象,进行 re-render 详细请参考 实现Virtual Dom && Diff 补充1:VDOM 的必要性? 创建真实DOM的代价高:真实的 DOM 节点 node 实现的属性很多,而 vnode 仅仅实现一些必要的属性,相比起来,创建一个 vnode 的成本比较低。 触发多次浏览器重绘及回流:使用 vnode ,相当于加了一个缓冲,让一次数据变动所带来的所有 node 变化,先在 vnode 中进行修改,然后 diff 之后对所有产生差异的节点集中一次对 DOM tree 进行修改,以减少浏览器的重绘及回流。 补充2:vue 为什么采用 vdom? 引入 Virtual DOM 在性能方面的考量仅仅是一方面。 性能受场景的影响是非常大的,不同的场景可能造成不同实现方案之间成倍的性能差距,所以依赖细粒度绑定及 Virtual DOM 哪个的性能更好还真不是一个容易下定论的问题。 Vue 之所以引入了 Virtual DOM,更重要的原因是为了解耦 HTML 依赖,这带来两个非常重要的好处是: 不再依赖 HTML 解析器进行模版解析,可以进行更多的 AOT 工作提高运行时效率:通过模版 AOT 编译,Vue 的运行时体积可以进一步压缩,运行时效率可以进一步提升; 可以渲染到 DOM 以外的平台,实现 SSR、同构渲染这些高级特性,Weex 等框架应用的就是这一特性。 综上,Virtual DOM 在性能上的收益并不是最主要的,更重要的是它使得 Vue 具备了现代框架应有的高级特性。 vue 和 react 区别 相同点:都支持 ssr,都有 vdom,组件化开发,实现 webComponents 规范,数据驱动等 不同点:vue 是双向数据流(当然为了实现单数据流方便管理组件状态,vuex 便出现了),react 是单向数据流。vue 的 vdom 是追踪每个组件的依赖关系,不会渲染整个组件树,react 每当应该状态被改变时,全部子组件都会 re-render。 上面提到的每个点,具体细节还得看自己的理解 为什么用 vue :简洁、轻快、舒服、没了 网络基础类 跨域:很多种方法,但万变不离其宗,都是为了搞定同源策略。重用的有 jsonp、iframe、cors、img、HTML5 postMessage等等。其中用到 html 标签进行跨域的原理就是 html 不受同源策略影响。但只是接受 Get 的请求方式,这个得清楚。 详细内容传送门 延伸1:img iframe script 来发送跨域请求有什么优缺点? iframe 优点:跨域完毕之后DOM操作和互相之间的JavaScript调用都是没有问题的 缺点:1.若结果要以URL参数传递,这就意味着在结果数据量很大的时候需要分割传递,巨烦。2.还有一个是iframe本身带来的,母页面和iframe本身的交互本身就有安全性限制。 script 优点:可以直接返回json格式的数据,方便处理 缺点:只接受GET请求方式 图片ping 优点:可以访问任何url,一般用来进行点击追踪,做页面分析常用的方法 缺点:不能访问响应文本,只能监听是否响应 延伸2:配合 webpack 进行反向代理? webpack 在 devServer 选项里面提供了一个 proxy 的参数供开发人员进行反向代理 '/api': { target: 'http://www.example.com', // your target host changeOrigin: true, // needed for virtual hosted sites pathRewrite: { '^/api': '' // rewrite path } }, 然后再配合 http-proxy-middleware 插件对 api 请求地址进行代理 const express = require('express'); const proxy = require('http-proxy-middleware'); // proxy api requests const exampleProxy = proxy(options); // 这里的 options 就是 webpack 里面的 proxy 选项对应的每个选项 // mount `exampleProxy` in web server const app = express(); app.use('/api', exampleProxy); app.listen(3000); 然后再用 nginx 把允许跨域的源地址添加到报头里面即可 说到 nginx ,可以再谈谈 CORS 配置,大致如下 location / { if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Headers' 'DNT, X-Mx-ReqToken, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type'; add_header 'Access-Control-Max-Age' 86400; add_header 'Content-Type' 'text/plain charset=UTF-8'; add_header 'Content-Length' 0; return 200; } } http 无状态无连接 http 协议对于事务处理没有记忆能力 对同一个url请求没有上下文关系 每次的请求都是独立的,它的执行情况和结果与前面的请求和之后的请求是无直接关系的,它不会受前面的请求应答情况直接影响,也不会直接影响后面的请求应答情况 服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器 人生若只如初见,请求过的资源下一次会继续进行请求 http协议无状态中的 状态 到底指的是什么?! 【状态】的含义就是:客户端和服务器在某次会话中产生的数据 那么对应的【无状态】就意味着:这些数据不会被保留 but 通过增加cookie和session机制,现在的网络请求其实是有状态的 在没有状态的http协议下,服务器也一定会保留你每次网络请求对数据的修改,但这跟保留每次访问的数据是不一样的,保留的只是会话产生的结果,而没有保留会话 http-cache:就是 http 缓存咯 首先得明确 http 缓存的好处 减少了冗余的数据传输,减少网费 减少服务器端的压力 Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间 加快客户端加载网页的速度 常见 http 缓存的类型 私有缓存(一般为本地浏览器缓存) 代理缓存 然后谈谈本地缓存 本地缓存是指浏览器请求资源时命中了浏览器本地的缓存资源,浏览器并不会发送真正的请求给服务器了。它的执行过程是: 第一次浏览器发送请求给服务器时,此时浏览器还没有本地缓存副本,服务器返回资源给浏览器,响应码是200 OK,浏览器收到资源后,把资源和对应的响应头一起缓存下来。 第二次浏览器准备发送请求给服务器时候,浏览器会先检查上一次服务端返回的响应头信息中的Cache-Control,它的值是一个相对值,单位为秒,表示资源在客户端缓存的最大有效期,过期时间为第一次请求的时间减去Cache-Control的值,过期时间跟当前的请求时间比较,如果本地缓存资源没过期,那么命中缓存,不再请求服务器。 如果没有命中,浏览器就会把请求发送给服务器,进入缓存协商阶段。 与本地缓存相关的头有:Cache-Control、Expires,Cache-Control有多个可选值代表不同的意义,而Expires就是一个日期格式的绝对值。 Cache-Control Cache-Control是HTPP缓存策略中最重要的头,它是HTTP/1.1中出现的,它由如下几个值 no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。 no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。 public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。 private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。 max-age:从当前请求开始,允许获取的响应被重用的最长时间(秒)。 例如: Cache-Control: public, max-age=1000 表示资源可以被所有用户以及代理服务器缓存,最长时间为1000秒。 Expires Expires是HTTP/1.0出现的头信息,同样是用于决定本地缓存策略的头,它是一个绝对时间,时间格式是如Mon, 10 Jun 2015 21:31:12 GMT,只要发送请求时间是在Expires之前,那么本地缓存始终有效,否则就会去服务器发送请求获取新的资源。如果同时出现Cache-Control:max-age和Expires,那么max-age优先级更高。他们可以这样组合使用 Cache-Control: public Expires: Wed, Jan 10 2018 00:27:04 GMT 所谓的缓存协商 当第一次请求时服务器返回的响应头中存在以下情况时 没有Cache-Control和Expires Cache-Control和Expires过期了 Cache-Control的属性设置为no-cache时 那么浏览器第二次请求时就会与服务器进行协商,询问浏览器中的缓存资源是不是旧版本,需不需要更新,此时,服务器就会做出判断,如果缓存和服务端资源的最新版本是一致的,那么就无需再次下载该资源,服务端直接返回304 Not Modified 状态码,如果服务器发现浏览器中的缓存已经是旧版本了,那么服务器就会把最新资源的完整内容返回给浏览器,状态码就是200 Ok,那么服务端是根据什么来判断浏览器的缓存是不是最新的呢?其实是根据HTTP的另外两组头信息,分别是:Last-Modified/If-Modified-Since 与 ETag/If-None-Match。 Last-Modified 与 If-Modified-Since 浏览器第一次请求资源时,服务器会把资源的最新修改时间Last-Modified:Thu, 29 Dec 2011 18:23:55 GMT放在响应头中返回给浏览器 第二次请求时,浏览器就会把上一次服务器返回的修改时间放在请求头If-Modified-Since:Thu, 29 Dec 2011 18:23:55发送给服务器,服务器就会拿这个时间跟服务器上的资源的最新修改时间进行对比 如果两者相等或者大于服务器上的最新修改时间,那么表示浏览器的缓存是有效的,此时缓存会命中,服务器就不再返回内容给浏览器了,同时Last-Modified头也不会返回,因为资源没被修改,返回了也没什么意义。如果没命中缓存则最新修改的资源连同Last-Modified头一起返回。 第一次请求返回的响应头: Cache-Control:max-age=3600 Expires: Fri, Jan 12 2018 00:27:04 GMT Last-Modified: Wed, Jan 10 2018 00:27:04 GMT 第二次请求的请求头信息: If-Modified-Since: Wed, Jan 10 2018 00:27:04 GMT 这组头信息是基于资源的修改时间来判断资源有没有更新,另一种方式就是根据资源的内容来判断,就是接下来要讨论的ETag与If-None-Match ETag与If-None-Match ETag/If-None-Match与Last-Modified/If-Modified-Since的流程其实是类似的,唯一的区别是它基于资源的内容的摘要信息(比如MD5 hash)来判断 浏览器发送第二次请求时,会把第一次的响应头信息ETag的值放在If-None-Match的请求头中发送到服务器,与最新的资源的摘要信息对比,如果相等,取浏览器缓存,否则内容有更新,最新的资源连同最新的摘要信息返回。用ETag的好处是如果因为某种原因到时资源的修改时间没改变,那么用ETag就能区分资源是不是有被更新。 第一次请求返回的响应头: Cache-Control: public, max-age=31536000 ETag: "15f0fff99ed5aae4edffdd6496d7131f" 第二次请求的请求头信息: If-None-Match: "15f0fff99ed5aae4edffdd6496d7131f" cookie 和 session session: 是一个抽象概念,开发者为了实现中断和继续等操作,将 user agent 和 server 之间一对一的交互,抽象为“会话”,进而衍生出“会话状态”,也就是 session 的概念 cookie:它是一个世纪存在的东西,http 协议中定义在 header 中的字段,可以认为是 session 的一种后端无状态实现 现在我们常说的 “session”,是为了绕开 cookie 的各种限制,通常借助 cookie 本身和后端存储实现的,一种更高级的会话状态实现 session 的常见实现要借助cookie来发送 sessionID 安全问题,如 XSS 和 CSRF XSS:跨站脚本攻击,是一种网站应用程序的安全漏洞攻击,是代码注入的一种。常见方式是将恶意代码注入合法代码里隐藏起来,再诱发恶意代码,从而进行各种各样的非法活动。 防范:记住一点 “所有用户输入都是不可信的”,所以得做输入过滤和转义 详细点击 对于跨站脚本攻击(XSS攻击)的理解和总结 CSRF:跨站请求伪造,也称 XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。与 XSS 相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。 防范:用户操作验证(验证码),额外验证机制(token使用)等 本文作者:qiangdada 本文发布时间:2018/01/14 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
前言: 两个月前开始全身心投入到公司的一个移动端项目,框架选型是vue,这篇文章也是在花费两个月的时间,项目一期完成之后得空进行的一片总结性文章,其中包括通用的移动端开发的坑以及vue在移动端开发特有的一些坑,本博文目的也是为了让小伙伴们以后在开发移动端的时候可以尽量避免掉这些坑,从而提高自己的开发效率。 本博文总结顺序大概如下 移动端开发通用坑 vue移动开发特有坑以及小技巧分享 移动端开发性能优化 一、移动端开发通用坑 1、click300ms延迟? 讲道理,现在开发移动端基本是不会有这么一个问题的。但作为移动端以前的经典坑,我这里也拿出来说上一说吧。 移动设备上的web网页是有300ms延迟的,玩玩会造成按钮点击延迟甚至是点击失效。这是由于区分单击事件和双击屏幕缩放的历史原因造成的。但在2014年的Chrome 32版本已经把这个延迟去掉了,so you know。但如果你还是出现了300ms的延迟问题,也是有路子搞定的。 解决方案如下: fastclick可以解决在手机上点击事件的300ms延迟 zepto的touch模块,tap事件也是为了解决在click的延迟问题 触摸事件的响应顺序为 touchstart --> touchmove --> touchend --> click,也可以通过绑定ontouchstart事件,加快对事件的响应,解决300ms延迟问题 若移动设备兼容性正常的话(IE/Firefox/Safari(IOS 9.3)及以上),只需加上一个meta标签<meta name="viewport" content="width=device-width"> 即把viewport设置成设备的实际像素,那么就不会有这300ms的延迟。 2、移动端样式兼容处理 当今的手机端,各式各样的手机,屏幕分辨率也是各有不同,为了让页面可以可以兼容各大手机,解决方案如下 设置meta标签viewport属性,使其无视设备的真实分辨率,直接通过dpi,在物理尺寸和浏览器之间重设分辨率,从而达到能有统一的分辨率的效果。并且禁止掉用户缩放<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" /> 使用rem进行屏幕适配,设置好root元素的font-size大小,然后在开发的时候,所有与像素有关的布局统一换成rem单位。针对不同的手机,使用媒体查询对root元素font-size进行调整 3、阻止旋转屏幕时自动调整字体大小 移动端开发时,屏幕有竖屏和横屏模式,当屏幕进行旋转时,字体大小则有可能会发生变化,从而影响页面布局的整体样式,为避免此类情况发生,只需设置如下样式即可 * { -webkit-text-size-adjust: none; } 4、修改移动端难看的点击的高亮效果,iOS和安卓下都有效 * { -webkit-tap-highlight-color: rgba(0,0,0,0); } 不过这个方法在现在的安卓浏览器下,只能去掉那个橙色的背景色,点击产生的高亮边框还是没有去掉,有待解决! 一个CSS3的属性,加上后,所关联的元素的事件监听都会失效,等于让元素变得“看得见,点不着”。IE到11才开始支持,其他浏览器的当前版本基本都支持。详细介绍见这里:https://developer.mozilla.org/zh-CN/docs/Web/CSS/pointer-events pointer-events: none; 5、iOS下取消input在输入的时候英文首字母的默认大写 <input type="text" autocapitalize="none"> 6、禁止 iOS 识别长串数字为电话 <meta name="format-detection" content="telephone=no" /> 7、禁止 iOS 弹出各种操作窗口 -webkit-touch-callout: none; 8、禁止ios和android用户选中文字 -webkit-user-select: none; 9、calc的兼容处理 CSS3中的calc变量在iOS6浏览器中必须加-webkit-前缀,目前的FF浏览器已经无需-moz-前缀。 Android浏览器目前仍然不支持calc,所以要在之前增加一个保守尺寸: div { width: 95%; width: -webkit-calc(100% - 50px); width: calc(100% - 50px); } 10、fixed定位缺陷 iOS下fixed元素容易定位出错,软键盘弹出时,影响fixed元素定位,android下fixed表现要比iOS更好,软键盘弹出时,不会影响fixed元素定位 。iOS4下不支持position:fixed 解决方案: 可用iScroll插件解决这个问题 11、一些情况下对非可点击元素如(label,span)监听click事件,ios下不会触发 针对此种情况只需要对不触发click事件的那些元素添加一行css代码即可 cursor: pointer; 12、消除transition闪屏问题 /*设置内嵌的元素在 3D 空间如何呈现:保留 3D*/ -webkit-transform-style: preserve-3d; /*(设置进行转换的元素的背面在面对用户时是否可见:隐藏)*/ -webkit-backface-visibility: hidden; 13、CSS动画页面闪白,动画卡顿 解决方法: 1.尽可能地使用合成属性transform和opacity来设计CSS3动画,不使用position的left和top来定位 2.开启硬件加速 -webkit-transform: translate3d(0, 0, 0); -moz-transform: translate3d(0, 0, 0); -ms-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); 14、iOS系统中文输入法输入英文时,字母之间可能会出现一个六分之一的空格 解决方法:通过正则去除 this.value = this.value.replace(/\u2006/g, ''); 15、input的placeholder会出现文本位置偏上的情况 input 的placeholder会出现文本位置偏上的情况:PC端设置line-height等于height能够对齐,而移动端仍然是偏上,解决方案时是设置css line-height:normal; 16、浮动子元素撑开父元素盒子高度 解决方法如下: 父元素设置为 overflow: hidden; 父元素设置为 display: inline-block; 等 这里两种方法都是通过设置css属性将浮动元素的父元素变成间接变成BFC元素,然后使得子元素高度可以撑开父元素。这里需要注意的时,最好使用方法1, 因为inline-block元素本身会自带一些宽高度撑开其本身。 17、往返缓存问题 点击浏览器的回退,有时候不会自动执行js,特别是在mobilesafari中。这与往返缓存(bfcache)有关系。 解决方法 : window.onunload = function () {}; 18、overflow-x: auto在iOS有兼容问题 解决方法: -webkit-overflow-scrolling: touch; 二、vue移动开发特有坑以及小技巧分享 1、iOS原始输入法问题 iOS原始输入法,中文输入时,无法触发keyup事件,且keyup.enter事件无论中英文,都无法触发 解决方法: 改用input事件进行监听 将keyup监听替换成值的watch 让使用者安装三方输入法,比如搜狗输入法(不太现实) 2、input元素失焦问题 业务场景重现: 项目中需要写一个搜索组件,相关代码如下 <template> <div class="y-search" :style="styles" :clear="clear"> <form action="#" onsubmit="return false;"> <input type="search" class="y-search-input" ref="search" v-model='model' :placeholder="placeholder" @input="searchInputFn" @keyup.enter="searchEnterFn" @foucs="searchFocusFn" @blur="searchBlurFn" /> <y-icons class="search-icon" name="search" size="14"></y-icons> </form> <div v-if="showClose" @click="closeFn"> <y-icons class="close-icon" name='close' size='12'></y-icons> </div> </div> </template> 其中我需要在enter的时候进行对应的搜索操作并实现失焦,解决方法其实很简单,在enter时进行DOM操作即可 searchEnterFn (e) { document.getElementsByClassName('y-search-input')[0].blur() // dosomething yourself } 对了,这里还有一个坑,就是在移动端使用input类型为search的时候,必须使用form标签包裹起来,这样在移动端呼出键盘的enter才会是搜索按钮,否则只是默认的enter按钮。 3、vue组件开发 这个点不能算坑,只能算是小技巧分享吧。 业务场景重现:很多时候,在开发项目的时候是需要抽离公共组件和业务组件的。而有些公共组件在全局注册的同时可能还需要拓展成vue的实例方法,通过把它们添加到 Vue.prototype 上实现,方便直接使用js全局调用。拿一个Message组件做例子吧,代码比较简单,就直接上代码了。 1.首先开发好Message.vue文件 <template> <div class='y-mask-white-dialog' v-show='show'> <div class='y-message animated zoomIn' > <span v-html="msg"></span> </div> </div> </template> <script> export default { name: 'yMessage', props: { msg: String, timeout: Number, callback: Function, icon: String, }, data() { return { show: true, }; } }; </script> <style lang="stylus" scoped> .y-mask-white-dialog { background-color: rgba(0, 0, 0, .4); position: fixed; z-index: 999; bottom: 0; right: 0; left: 0; top: 0; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; -webkit-box-align: center; -webkit-align-items: center; -ms-flex-align: center; align-items: center; } .y-message { min-width: 2.9rem; max-width: 5.5rem; width:100%; padding: 0.32rem; font-size: 14px; text-align: center; border-radius: 4px; background :rgba(0,0,0,0.8); color: #fff; animation: zoomIn .15s ease forwards; } </style> 2.构建Message的Constructor import Vue from 'vue'; const MsgConstructor = Vue.extend(require('./Message.vue')); const instance = new MsgConstructor({ // el: document.createElement('div'), }).$mount(document.createElement('div')); MsgConstructor.prototype.closeMsg = function () { const el = instance.$el; el.parentNode && el.parentNode.removeChild(el); typeof this.callback === 'function' && this.callback(); }; const Message = (options = {}) => { instance.msg = options.msg; instance.timeout = options.timeout || 2000; instance.icon = options.icon; instance.callback = options.callback; document.body.appendChild(instance.$el); const timer = setTimeout(() => { clearTimeout(timer); instance.closeMsg(); }, instance.timeout + 100); }; export default Message; 3.在main.js里面进行组件注册 import Message from './components/Message'; Vue.component(Message.name, Message) Vue.prototype.$message = Message 然后你就可以尽情使用Message组件了. // <y-message msg="test message"><y-message> // or this.$message({ msg: 'test message' // ... }) 4、巧用flex布局让图片等比缩放 这也是一个小技巧!项目中需要开发swiper轮播图,那么你懂的,图片肯定是需要保证等比缩放展示。 <div class="parent"> <img :src="imgSrc" alt=""> </div> <style lang="stylus" scoped> .parent { width: 100px; height: 100px; display: flex; align-items: center; img { width :100%; height: auto; } } </style> 是不是贼简单,是的,贼简单这个样式同时适应手机全屏预览竖屏的情况,当手机横屏的时候,加一个媒体查询即可搞定 @media (orientation: landscape) { img { width auto height 100% margin auto } } 这里我就不上轮播图的代码了,有点小多。有需要的小伙伴可以私聊我,我后期直接传到github上去,代码可以自行查阅。效果图如下 5、枚举值过滤处理 业务重现:考虑到项目后期会做国际化,前端需要对项目中几乎所有的枚举值进行过滤处理,从而进行展示 接下来就直接讲讲这块吧。既然要过滤,那么首选肯定是vue提供的filter指令。这里我举一个支付方式的枚举值处理的例子。首先配置代码如下 // 配置文件 export default { env: (process.env.NODE_ENV === 'development' ? require('./env/dev') : require('./env/pro')), headShow: false, lng: 'zh', }; 枚举代码如下 // 获取语言环境 import config from '../config/index'; const {lng} = config; // 账户类型 const type = { zh: { 1: '银行', 2: '支付宝', 3: '微信支付', }, en: { 1: 'bank_type', 2: 'alipay_type', 3: 'wxpay_type', } } export default type[lng]; 枚举注册代码分别如下 import accountType from './accountType'; // 账户类型 const factory = { accountType(value) { if (value === -1) { return '请选择账户类型'; } return accountType[value] ? accountType[value] : '请选择账户类型'; } } const filter = [ { name: 'formatEnum', // 过滤类型 filter: function(value, type, status) { return factory[type](value, status); } } ]; export default { filter }; // filter import baseFilter from './filter/index'; const filters = [ ...baseFilter.filter ]; filters.map(f => { Vue.filter(f.name, f.filter); return ''; }); 接下来就可以轻松使用啦 <li> <label支付类型</label> <span> {{info.account_type | formatEnum('accountType')}} </span> </li> 6、时间过滤处理 这点还是属于过滤处理的一个part,但是手机端有个兼容问题,如果是时间戳转的话,那么可以转化成任意我们想要的形式,但是String类型的时间转化的话,他只能兼容 "yyyy/MM/dd" 形式的时间值,因为我们DateTime组件默认的形式是"yyyy-MM-dd",那么只需要在DateTime组件正则替换一下即可,代码如下 currentValue = _this.currentValue = _this.value ? _this.value.replace(/-/g, '/') : ''; 时间过滤代码如下 const filter = [ { name: 'formatEnum', // 过滤类型 filter: function(value, type, status) { return factory[type](value, status); } }, { name: 'formatDate', // 日期 filter: function(value, format = 'yyyy-MM-dd') { if (!value || value === '') return ''; let v = value; if (!isNaN(value)) { v = +value * 1000; } const newDate = new Date(v); return newDate.Format(format); }, } ]; 7、路由权限判定 业务重现:由于不同的用户,可能拥有不同权限,而目前我们的项目是基于微信公众号进行开发的,页面权限这边也是交给了我们前端处理。既然要前端配置权限,那么我们能想到的比较好的方式就是通过配置路由文件,完成权限判定。下面我会列举一小部分代码(以我们的工单列表)进行演示,路由配置代码如下 const getWorkOrder = pageName => resolve => require(['../pages/WorkOrder'], pages => resolve(pages[pageName])) let routers = []; routers = [ { path: '/workorder', name: 'workorder', component: room, children: [ { path: 'list', // 管家端工单列表 name: 'list', rule: 3, component: getWorkOrder('WorkOrderList') //WorkOrder, }, { path: 'managerList', // 店长端工单列表 name: 'managerList', rule: 6, component: getWorkOrder('ManagerWorkOrder') // WorkOrder, }, ] } ] 然后进行路由统一过滤处理 import Vue from 'vue'; import Store from 'store'; import Router from 'vue-router'; import routers from './router.config'; Vue.use(Router); // 遍历路由名称以及权限 let arr = {}; const routeName = function (router) { if (!router) return false; router.forEach((v) => { arr[v.name] = v.rule; routeName(v.children); }) } routeName(routers); const RouterModel = new Router({ mode: 'history', routes: routers, }); // 路由钩子,进入路由之前判断 RouterModel.beforeEach((to, from, next) => { // 处理query 参数,传入到 jumpUrl,便于登录后跳转到对应页面 let qu = Object.entries(to.query); if (qu.length > 0) { qu = qu.map((item, index) => { return item.join('='); }) } if (arr[to.name]) { // 如果有权限需要 const userInfo = Store.get('yu_info'); const cookie = Vue.prototype.$util.getCookie('X-Auth-Token'); const userId = Vue.prototype.$util.getCookie('userid'); if (userInfo && cookie && +userId > 0) { next(); } else { // 未登录,跳转登录 let param = `jumpUrl=${to.path}`; if (qu.length > 0) { param += `&${qu.join("&")}`; } if (arr[to.name] && !to.query.rule) { param += `&rule=${arr[to.name]}`; } window.location.href = `/login?${param}`; } } else { // 如果不需要权限放行 next(); } }) export default RouterModel; 然后在登陆界面定位到微信授权 getCode() { // 定位到微信授权,若是不需要授权可以在此处处理 let query = this.$route.query; let param = `jumpUrl=${query.jumpUrl || '/home'}`; let path = window.location.origin; // 登录角色处理 let cfg = api[query.rule ? query.rule : 1]; if (query.rule) { param += `&rule=${query.rule}`; } let redirect_url = encodeURIComponent(`${path}/login?${param}`); let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${cfg.tempApp}&redirect_uri=${redirect_url}&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect`; if (query.rule === 3) { url += '&agentid=1000004'; } location.replace = url; } 如果不是微信端,访问到到rule规则的界面时,则会如下 而当微信授权通过的时候,rule权限不足的情况则会如下 8、使用vue-cli proxyTable进行反向代理,解决跨域问题 开发项目,在前后端联调的时候肯定是会遇上跨域的问题的。很简单,做个反向代理呗,对于想了解正向代理和反向代理的,请点击这里 vue-cli脚手架搭建的工程中,在config/index.js文件中可以利用预留的proxyTable一项,设置地址映射表,如下 proxyTable: { '/api': { target: 'http://www.example.com', // your target host changeOrigin: true, // needed for virtual hosted sites pathRewrite: { '^/api': '' // rewrite path } } } 然后使用http-proxy-middleware插件对api请求地址进行代理 // proxy api requests Object.keys(proxyTable).forEach(function (context) { var options = proxyTable[context] if (typeof options === 'string') { options = { target: options } } app.use(proxyMiddleware(options.filter || context, options)) }) http-proxy-middleware地址 三、移动端开发性能优化 对于这点,有一篇文章建议大家可以先看看《移动前端H5性能优化指南》。接下来我会结合实际项目抽几个点配合代码进行较为详细的讲解。 1、首屏渲染优化 决定用户体验最重要的一个点之一,这个点的重要性,相信不用我说了。下面直接谈实战。 减少资源请求次数 加载时使用过渡样式,防止用户网络太差影响对首页的体验 图片使用懒加载,这一part,我们目前项目中使用的vue的三方插件vue-lazyload,大致使用方法如下 // 全局注册 import VueLazyload from 'vue-lazyload'; Vue.use(VueLazyload, { error: require('./assets/close.svg'), loading: require('./assets/loading.svg'), attempt: 1, }); // 使用 <img v-lazy="room.img" :alt="room.community_name" width="100%"> HTML使用Viewport,Viewport可以加速页面的渲染。 <meta name=”viewport” content=”width=device-width, initial-scale=1″> 除此之外,还有很多点可做优化,进而提升首屏加载速度。 2、雪碧图 这个老生常谈了,为了减少图片请求次数,加快页面加载,当然会考虑使用雪碧图。大家这个应该没啥疑问吧。来个小例子,一天,设计小姐姐给了我一张设计稿,稿子如图 然后给了我6张图片,一看,每张图都有6K左右的大小。好嘛,只能自己在线合张雪碧图,不然,太影响页面加载,合完雪碧图顺带在线压缩优化下,然后总大小只有6K。还有一个点,就是尽量使用::before或::after伪类,Sprites中的图片排版可以更紧 ,图片体积更小, HTML更简洁。部分代码如下 <style lang="stylus" scoped> .chose-house { height: 86px; width: 64px; margin 0 auto position relative &::before { content: '\20'; height: 100%; width: 100%; position: absolute; left: 0; top: 0; background: url('../../assets/sprite-min.png') 0px 0px no-repeat; } } </style> 3、路由懒加载 关于路由懒加载这一部分,尤大在vue-router文档中也有所提及,链接点击这里。 其实在vue项目中使用路由懒加载非常简单,我们要做的就是把路由对应的组件定义成异步组件,代码如下 //在router/index.js中引入组件时可使用如下异步方式引入 const Foo = resolve => { // require.ensure 是 Webpack 的特殊语法,用来设置 code-split point // (代码分块) require.ensure(['./Foo.vue'], () => { resolve(require('./Foo.vue')) }) } // or const Foo = resolve => require(['./Foo.vue'], resolve) 再将组件按组分块,如 const Foo = r => require.ensure([], () => r(require('./Foo.vue')), 'group-foo') 实际项目中的代码则如同我在章节《路由权限判定》提及到的一样 const getWorkOrder = pageName => resolve => require(['../pages/WorkOrder'], pages => resolve(pages[pageName])) let routers = []; routers = [ { path: '/workorder', name: 'workorder', component: room, children: [ { path: 'list', // 管家端工单列表 name: 'list', rule: 3, component: getWorkOrder('WorkOrderList') //WorkOrder, }, { path: 'managerList', // 店长端工单列表 name: 'managerList', rule: 6, component: getWorkOrder('ManagerWorkOrder') // WorkOrder, }, ] } ] 如上将组件通过传递pageName参数分别打包到了各个chunk中,这样每个组件加载时都只会加载自己对应的代码,从而加快渲染速度! 4、全局组件按需注册 当时我们为了优化首屏渲染速度,也是考虑到这一点,项目的src/main.js文件主要负责注册全局组件,插件,路由,以及实例化Vue等。在webpack的配置里面也是当成entry入口进行了配置,如果我在main.js里面讲每个组件都进行import的话,那么它将会全部一起注册打包,页面加载也会将每个组件文件都加载下来,这样对渲染速度还是有一定影响的。 解决方法就是:按需注册,这样在打包的时候,会按需加载首页(其他界面也同样适用)使用到的全局组件。基本步骤如下: 将需要注册的组件写进components/base.js文件中,然后exports出来 exports.Foo = require('./Foo.vue'); exports.Bar = require('./Bar.vue'); exports.Baz = require('./Baz.vue'); 在main.js中进行注册 const components = [ require('./components/base').Foo, require('./components/base').Bar, require('./components/base').Baz, ]; components.map(component => { Vue.component(component.name, component); }); OK,大功告成! 以上便是我在最近的移动端项目实战中的一些经验总结,希望对各位小伙伴或多或少有些帮助吧!如果有帮助,别吝惜你手上的赞哦~ 本文作者:qiangdada 本文发布时间:2017/10/24 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
一、什么是跨域 由于浏览器对安全访问因素的考虑,是不允许js跨域调用其他页面的,这里的域我们把它想象成域名,如,一个域名为https://www.oschina.net,另外一个域名为https://www.zhihu.com,这两者属于不同的域名,它们之间的页面也是不能相互调用的,它属于同源策略所定义限制中的一种。同源策略具体分为以下几类: 不同域名 相同域名不同端口号,如https://www.oschina.net:8000和https://www.oschina.net:8001 同一个域名不同协议,如http://www.oschina.net和https://www.oschina.net 域名和域名对应的的IP,如http://b.qq.com和 http://10.198.7.85 主域和子域,如http://www.oschina.net和https://test.oschina.net 子域和子域,如https://test1.oschina.net和https://test2.oschina.net 以上情况只要出现了,那么就会产生跨域问题。那么如果解决跨域问题呢,下面的小节会总结一些解决跨域常用的方法。 二、跨域解决方案 1、JSONP 对于JSONP,有个通俗易懂的解释-JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。 由于同源策略,如上所述。但是(中国人讲话是很有文化的),HTML的 <script>元素是一个例外,它并不遵循同源策略,利用 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器运行而不是用JSON解析器解析。来来来,我来举个栗子吧 前端浏览器页面 <script> function jsonpCallBack (res, req) { console.log(res, req); } </script> <script type="text/JavaScript" src="http://localhost/test/jsonp.php?callback=jsonpCallBack&data=getJsonpData"></script> 另一个域名服务器请求接口 <?php /*后端获取请求字段数据,并生成返回内容*/ $data = $_GET["data"]; $callback = $_GET["callback"]; echo $callback."('success', '".$data."')"; ?> 测试结果如下 这种方案需要注意的是他支持GET这一种HTTP请求类型,还有尤为重要的就是其他域要有一定可靠性,不然你的网站会GG的。当然有时我们还可以通过一个方法来动态生成需要的JSONP。 2、跨域资源共享(CORS-Cross Origin Resource Sharing) CORS,它是JSONP模式的现代升级版,与JSONP不同的是,CORS除了GET要求方法以外也支持其他的 HTTP要求。浏览器CORS请求分成两种 a、简单请求 b、协商模型/预检请求(Preflighted Request),即非简单请求 如何区分请求具体属于哪一种呢,下面我总结了几点: 1) 请求方式 GET HEAD POST 2)HTTP的头信息子段 Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain,其中'text/plain'默认支持,其他两种则需要预检请求和服务器协商。 满足以上两大点的即为简单请求,否则为非简单请求。具体请求处理的不同,大家可以去查阅下MDN https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS ,那里有详细的解析及用法。 3、document.domain+iframe(适用于主域名相同的情况) 从上面的同源策略我们可以知道,浏览器这边是认为主域和子域、子域和子域,它们属于不同的域,那么我们如果需要让主域和子域之间可以进行通信,需要做的就是通过修改document.domain,把它们改成相同的domain 在域名为server.example.com中的a.html document.domain = 'example.com'; var $iframe = document.createElement('iframe'); $iframe.src = 'server.child.example.com/b.html'; $iframe.style.display = 'none'; document.body.appendChild($iframe); $iframe.onload = function(){ var doc = $iframe.contentDocument || $iframe.contentWindow.document; //在这里操作doc,也就是操作b.html $iframe.onload = null; }; 在域名为server.child.example.com中的b.html document.domain = 'example.com' 这种形式方便归方便,但也有其方便带来的隐患 安全性,当一个站点被攻击后,另一个站点会引起安全漏洞。 若页面中引入多个iframe,要想操作所有iframe,domain需要全部设置成一样的。 4、window.name + iframe window 对象的name属性是一个很特别的属性,它可以在不同页面甚至不同域名加载后依旧存在。使用步骤如下: step1 - 首先在页面A中利用iframe加载其他域中的页面B step2 - 在页面B中将需要传递的数据赋给window.name step3 - iframe加载完成后,页面A中修改iframe地址,将其变成同一个域下的地址,然后获取iframe中页面B的window.name 属性 示例代码如下: 首先我们在域名为http://127.0.0.1下建立好B页面,在B页面的<script>标签中将需要传递的数据赋给window.name window.name = '页面B中传递给页面A的数据'; 然后我们域名为http://127.0.0.1:9000的A页面,这里我们需要做的一件事就是利用iframe加载页面B,并将其域名进行修改,变成和页面A一样的域名。 function proxy (url, callback) { var flag = true, $iframe = document.createElement('iframe'), loadCallBack = function () { if (flag) { // 这里我们还得在域名为 http://127.0.0.1:9000 建立一个tmp.html文件当做缓存界面 $iframe.contentWindow.location = 'http://127.0.0.1:9000/tmp.html'; flag = false; } // 修改localtion后,每次触发onload事件会重置src,相当于重新载入页面,然后继续触发onload。 // 这里是针对该问题做的处理 else { callback($iframe.contentWindow.name); $iframe.contentWindow.close(); document.body.removeChild($iframe); $iframe.src = ''; $iframe = null; } }; $iframe.src = url; $iframe.style.display = 'none'; // 事件绑定兼容简单处理 // IE 支持iframe的onload事件,不过是隐形的,需要通过attachEvent来注册 if ($iframe.attachEvent) { $iframe.attachEvent('onload', loadCallBack); } else { $iframe.onload = loadCallBack; } document.body.appendChild($iframe); } proxy('http://127.0.0.1/bop/test.html', function(data){ console.log(data); }); 测试结果如图 5、HTML5中的postMessage(适用于两个iframe或两个页面之间) postMessage隶属于html5,但是它支持IE8+和其他浏览器,可以实现同域传递,也能实现跨域传递。它包括发送消息postMessage和接收消息message功能。 postMessage调用语法如下 otherWindow.postMessage(message, targetOrigin, [transfer]); otherWindow : 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。 message : 将要发送到其他 window的数据,类型为string或者object。 targetOrigin : 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。 transfer (可选) : 一串和message 同时传递的 Transferable 对象。 接收消息message 的属性有: data :从其他 window 中传递过来的数据。 origin :调用 postMessage 时消息发送方窗口的 origin 。 source :对发送消息的窗口对象的引用。 示例如下:域名http://127.0.0.1:9000页面A通过iframe嵌入了http://127.0.0.1页面B,接下来页面A将通过postMessage对页面B进行数据传递,页面B将通过message属性接收页面A的数据 页面A发送消息代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面A</title> </head> <body> <h1>hello jsonp</h1> <iframe src="http://127.0.0.1/b.html" id="iframe"></iframe> </body> </html> <script> window.onload = function() { var $iframe = document.getElementById('iframe'); var targetOrigin = "http://127.0.0.1"; $iframe.contentWindow.postMessage('postMessage发送消息', targetOrigin); }; </script> 页面B接收消息代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面B</title> </head> <body> <h1>hello jsonp</h1> </body> </html> <script> var onmessage = function (event) { var data = event.data; //消息 var origin = event.origin; //消息来源地址 var source = event.source; //源Window对象 if(origin === "http://127.0.0.1:9000"){ console.log(data, origin, source); } }; // 事件兼容简单处理 if (window.addEventListener) { window.addEventListener('message', onmessage, false); } else if (window.attachEvent) { window.attachEvent('onmessage', onmessage); } else { window.onmessage = onmessage; } </script> 运行结果如下 6、location.hash + iframe(适用于两个iframe之间) 对于location.hash,我们先看一张图先 相信看完图,大家也大概清楚了location.hash到底是用来干啥子的。没错,它可以用来获取或设置页面的标签值 如http://127.0.0.1:9000/#hello ,它的location.hash值则为'#hello'。它一般用于浏览器锚点定位,HTTP请求过程中却不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录,这对我们进行跨域通信给予了帮助。我们可以通过修改URL的hash部分来进行双向通信。 示例如下:域名http://127.0.0.1:9000页面A通过iframe嵌入了http://127.0.0.1页面B,接下来页面A和页面B将通过location.hash进行双向通信。 页面A代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面A</title> </head> <body> <iframe src="http://127.0.0.1/bop/test.html#locationHash" id="ifr"></iframe> </body> </html> 页面B代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面B</title> </head> <body> <h1>hello localtionHash</h2> </body> </html> <script> try { parent.location.hash = 'data'; } catch (e) { // ie、chrome的安全机制无法修改parent.location.hash,所以要借助于父窗口域名下的一个代理iframe var $ifrproxy = document.createElement('iframe'); $ifrproxy.style.display = 'none'; // 注意proxy.html必须是域名为 http://127.0.0.1:9000 下的页面 $ifrproxy.src = "http://127.0.0.1:9000/proxy.html#locationHashChange"; document.body.appendChild($ifrproxy); } </script> 代理页面代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Proxy页面</title> </head> <body> </body> <script> // 因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值 parent.parent.location.hash = self.location.hash.substring(1); </script> </html> 运行结果如图 以上内容便是我本博客的所有内容了,希望多多少少可以帮助小伙伴们去更好的理解跨域,当然,不对的地方,还请小伙伴们轻喷哦 ^_^ 本文作者:qiangdada 本文发布时间:2017/03/08 本文来自云栖社区合作伙伴开源中国,了解相关信息可以关注oschina.net网站。
美的集团要约收购库卡集团又有关键进展。公司今日公告,截至北京时间7月16日,要约收购库卡集团的要约期已经结束。在要约期内,接受本次要约收购的库卡集团股份数量合计为2871.00万股,占库卡集团已发行股本的比例为72.18%。目前,本次要约收购已经满足最低30%的比例要求以及其他非政府审批的交割条件。 本次要约收购前,美的集团已持有库卡集团13.51%股权,按截至目前接受要约收购的库卡集团股份数量计算,美的集团持有的库卡集团股份比例已达到85.69%。 不过,收购还未完全结束。根据本次要约收购的进程安排,在上述要约接受结果公布后两周内(即“额外要约期”),要约期内未接受要约的库卡集团股东还可以继续决定是否接受要约。额外要约期将于2016年7月21日0时(德国法兰克福当地时间)开始,并将于2016年8月3日24时(德国法兰克福当地时间)结束。 美的集团董事长兼总裁方洪波对媒体表示:“今天是美的与库卡深化合作关系的重要里程碑,我们的合作将进一步提升两家公司的价值。美的此前与库卡所签订的投资协议主导着我们的合作关系。我们将继续支持库卡的业务并促进其市场发展,尤其是在中国市场的发展。” 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。
提供Availability for the Always-On Enterprise(企业级业务永续的可用性)解决方案的创新公司Veeam Software(卫盟软件)今天宣布,其2016第二季度总预订收入相比去年同期增长38%,显示出全球企业对24.7.365(全年365天、每周7天、每天24小时)全天候可用性的需求日益增加。 Veeam总预订收入季度同比增长更达到40%,进一步显示出Veeam在2016上半年成功地用创新方法提供可用性,以实现服务等级协议(service level agreements,简称SLA)所有应用及数据的恢复时间和恢复点目标(RTPO)少于15分钟。Veeam的企业总预订收入相比去年同期增长40%,目前70%的财富500强企业及50%的全球2000企业都是Veeam的客户。Veeam同时也迎来了第200,000位付费用户。 就包括中国、日本、澳大利亚、新西兰及其他亚洲国家在内的亚太区而言,总预订收入年同比增长33%;客户和合作伙伴数量稳定上升;云相关业务增长强劲,年同比增长达49%;Veeam云和服务供应商(VCSP)数量增加,目前已有1,230名授权服务提供商。 Veeam首席营销官Peter Ruchatz表示:“2016上半年对于Veeam来说极具意义。随着企业希望满足用户采用革新性技术(如云、流动及社交媒体平台)的需求,我们在企业市场的渗透率也持续迅速上升。此前我们宣布将于今年推出Veeam Availability Suite 9.5,并在Gartner 6月发布的数据中心备份与恢复软件魔力象限(2016 Magic Quadrant for Data Center Backup and Recovery Software)中被列为领导者。我们扩大并强化了管理团队,继续向成为市值10亿美元企业的目标前进,同时也实现了一个重要的里程碑—拥有了第200,000位用户。总的来说,Veeam在2016年拥有一个非常好的开始。” Veeam在推出首个产品Veeam FastSCP for VMware ESX Server的10年后,宣布其客户数量在本季度正式突破200,000位,进一步彰显了Veeam的快速增长。第200,000位客户为拉斯维加斯Hard Rock酒店(Hard Rock Hotel&Casino Las Vegas)。Hard Rock酒店的客户要求持续在线、24.7.365的服务,是企业级业务永续的典型案例。该酒店面积超过10万平方米,并拥有超过1,500间客房、可定制的会议空间、多个知名的娱乐场所以及无数著名的餐馆和酒吧。 拉斯维加斯Hard Rock酒店IT总监Kevin Ragsdale表示:“我们非常荣幸与Veeam合作,以满足我们度假村对可用性的需求。Veeam为市场带来了前所未有的软件技术。该平台提供的功能将确保高可用性、支持全天候的业务运营,帮助我们满足客户的需求。” Veeam刚刚任命的总裁兼首席运营官Peter Mckay表示:“传统备份技术不能满足现代企业的需求已是众所周知的事实,最让Veeam振奋的是客户采用我们的创新方法以实现企业可用性的程度。我们最新的业绩明确显示行业已发生巨大转变,而拉斯维加斯Hard Rock酒店作为我们的第200,000位客户,更有力地证明了 Veeam对于现代企业的重要性。企业无论规模大小都开始舍弃传统的思维,并理解24.7.365可用性的意义。我们的未来策略非常清晰,将继续为一系列解决方案增加更丰富的功能,而且这些功能不仅能满足今天的用户需求,更能为未来打下基础。” IDC保护、可用性及恢复研究总监Phil Goodwin表示:“在市场不明朗的环境下,Veeam仍能保持强劲的收入增长。以如今美元汇率计算,整个数据保护和恢复软件市场于2015年的增长率仅为0.9%,但Veeam却能摆脱强势的美元因素获得21.5%的增长。为了实现2019年年收入达到10亿美元的目标,其年增长需要达到30%。最新的业绩正在以所需的速度增长甚至更快。” 在2016年第二季度,Veeam也取得以下成绩: Veeam宣布任命前VMware美洲区高级副总裁兼总经理Peter C. McKay为新任总裁兼首席运营官,并晋升现执行副总裁William H. Largent为新首席执行官。 Veeam继续在云端业务保持令人惊艳的增长,尤其当市场对灾难恢复即服务(DRaaS)的需求日益增加。Veeam云和服务供应商(VCSP)项目,一直为服务供应商提供获得新客户和增加收入的现成机会,其交易量比去年同期增加了76%。VCSP项目在全球已拥有12,400个服务和云端提供商,其中超过1,900家授权为Veeam Cloud Connect提供服务。 全球客户数量达205,000个,其中12,200个来自亚太区:Veeam在2016年第二季度新增了近12,500家付费客户,超过每月增加约3,500新客户的历史平均水准。 Veeam可用性解决方案现为1,180万台虚拟机提供保护。 全球ProPartners合作伙伴拓展至41,000家,其中4,250家来自亚太区:Veeam对渠道的承诺持续吸引新的分销商和方案商关注可用性解决方案。 Microsoft Hyper-V新增授权预订数比去年同期增长49%。Veeam还入围微软2016年度合作伙伴奖的两项最终提名:应用程序开发及Microsoft Azure Certified ISV 解决方案。该奖项被视为业界所公认的卓越标准,Veeam对于成为业界精英的一分子深感荣幸。 关于Veeam Veeam了解全球各地各种规模的企业在实现24.7.365(全年365天、每周7天、每天24小时)不间断运行、始终在线业务(Always-On Business)的过程中面临着种种新的挑战。为了解决这一问题,Veeam率先提出Availability for the Always-On Enterprise的概念并借此开拓出新的市场。“传统备份”解决方案只能实现数小时乃至数天的恢复时间目标(RTO)和恢复点目标(RPO),而Veeam可以帮助企业实现15分钟内恢复所有应用程序和数据的恢复时间和恢复点目标(RTPO)。如此卓越的表现得益于Veeam推出的全新整体式解决方案,具备高速恢复、避免数据丢失、验证保护、有效利用数据和全面透视等功能。包括Veeam Backup&Replication在内的Veeam Availability Suite利用虚拟化、存储和云技术打造现代数据中心,帮助企业节省时间、降低风险并大幅减少资本支出和运营成本。 Veeam成立于2006年,目前在全球范围内拥有41,000家合作伙伴和205,000多家客户,并在世界各地设有多个办事处。 Gartner免责声明: Gartner 并不认可其研究发布上描述的任何厂商、产品或服务,也不建议技术用户只选择最高评级或其他指定供应商。Gartner的研究发布由Gartner的研究机构提供意见,不应被理解为对事实的陈述。Gartner对于这项研究不作任何保证、明示或暗示,也不担保任何销售性或针对特定用途的保证。 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。
[2016年5月16日] ]手机无故死机、流量费用居高不下…….查清"真凶"是这些从应用商店下载的APP的时候,却常常一脸的茫然:明明我只在正规应用商店内下载APP,怎么会染毒呢?近期,央视就公布了10款恶意APP,其中相当一部分都来源于正规应用商店,这些APP不仅会导致隐私被窃、手机变慢,还会窃取手机用户的话费。趋势科技提醒用户在下载APP的时候要更关注APP试图获取的系统权限以及行为,并使用可靠的手机安全软件对APP进行安全扫描。 【央视公布的10款恶意APP】 在央视报道的这十款恶意APP中,集中存在着窃取用户信息、破坏用户数据、擅自调用付费业务、影响移动终端运行、恶意推送广告等安全问题,不仅会影响手机的应用体验,还会导致话费账单金额居高不下。那么他们是怎么"混入"应用商店的呢?趋势科技(中国区)资深产品市场经理徐江明分析称,现在很多恶意APP已经不再简单的被视为木马或病毒,而是在正常的APP功能基础上加入广告软件或其他有目的性代码,和正规APP很难区分。再加上很多第三方应用商店在审核机制上并不严格,导致诸多恶意APP找到可乘之机。 即使是在安全审核机制上更加严格的应用商店内,也很难完全阻止恶意APP的侵入。近期,安全研究人员就在Google Play上发现了有190款应用感染了Android.Click.95恶意程序,该恶意程序会在用户的浏览器中强行加载一个URL,其中包含了恐吓软件类的信息,以手机出现问题为幌子,欺骗用户安装推广APP。 【Google Play也会被恶意APP"入侵"】 徐江明指出:"手机用户需要特别关注的是,除了这些'天生'的恶意APP之外,很多网络不法分子还将正规的APP进行重新封装,以'修改版'、'增强版'等名义上传到应用商店。这类的APP还可能通过刷榜的方式挤到排行榜的前几位,以吸引用户下载,这让第三方应用商店变得危险重重。" 要防范这些APP,首先要记住不要迷信第三方应用商店的安全审核机制,而是应该在下载前进行自身甄别。那些在第三方应用商店中下载量变化幅度大、用户评分低的应用程序有更高的风险,在安装这些应用程序时,应该特别留意用户评价以及其试图获取的系统权限,一旦有可疑行为应该立即取消安装。 此外,用户最好能安装趋势科技移动安全个人版等手机防毒软件。趋势科技移动安全个人版软件提供了超强的云防护功能,能在云端就主动侦测通过APP、网页、短信等传送的恶意程序,并进行有效拦截,给用户的数字生活提供一个安全的移动网络环境。 关于趋势科技(Trend Micro) 趋势科技是全球虚拟化及云计算安全的领导厂商,致力于保障企业及消费者交换数字信息环境的安全。趋势科技始终秉持技术革新的理念,基于业内领先的云安全智能防护网络(Smart Protection Network)核心技术架构,为全世界各地用户提供领先的整合式信息安全威胁管理技术能防御恶意软件、垃圾邮件、数据外泄以及最新的Web信息安全,保障信息与财产的安全。同时,遍布全球各地的1,500余名趋势科技安全专家可为各国家和地区的企业级个人用户提供7×24的全天候响应及技术支持服务。 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。
作为一家服务于全球不同类型客户的IT专业公司,ThoughtWorks一直致力于用卓越的技术解决不同的问题。ThoughtWorks技术雷达是ThoughtWorks服务于各个行业客户的技术准绳,中科软总裁曾做出对此做出这样的评价:“我认为技术雷达是我见过的最好的偏重技术软件发展的技术报告,是软件相关行业和行业用户CTO的首选参考资料,有很好的发展潜力。” 技术对于我们这个行业和社会意味着什么? 在5月7日ThoughtWorks第一届技术雷达峰会上,ThoughtWorks中国区总经理张松提出了这样的问题,并进行了深入解读。经过过去三四十年的发展及改变,直至今天,随着智能硬件、IoT、云计算等等新技术的兴起,所有的产品都嵌入了芯片传感器,产品本身与IT技术变得密不可分,商业的创新也完全由技术驱动,我们看到新兴的互联网金融和零售模式,其背后并不是纯粹的商业模式和商业概念的变化,而是由于技术进步所引发的巨大变化。 是什么东西驱动了这样的变化? 这是张松提出的第二个问题。首先是技术的演进,大型企业及投资所带来的技术变革,以及许多开源技术和创业团队,他们带来的技术快速地融入到企业和消费者的开发中,这带来了非常巨大的变化;第二是消费行为快速地演进;最后一点,目前,监管机构看得见的手,似乎放松了些。如此这般的大环境造就了技术风起云涌的变化,造成新的商业此起彼伏。新的技术正在快速地演进,新的技术不断出现,原来的技术不断消退,变化之多、变化之大、变化之快,由此可见一斑。在这样的环境之下,对企业来讲,想要跟踪判断技术潮流、把握技术方向,变得难上加难。但正是这样的困难,才让ThoughtWorks得天独厚的优势得到了体现,“ThoughtWorks有遍布全球的开发团队,这些开发团队工作的环境做的产品遍布不同的行业,包括企业与消费者的产品,我们一线的开发人员和技术专家形成了对技术发展趋势的观点”。这正是技术雷达的由来。 技术雷达的“前世今生” ThoughtWorks中国区CTO徐昊在会上就技术的雷达的“前世今生”进行了详尽的介绍,并在其中就技术雷达中包含的趋势进行了说明。第一个趋势是Open Souce,Open Souce已经从软件代码组织变成文化和运动;第二是PARSING THE PAAS PUZZLE;第三是Docker,“Docker带来的不仅仅是工具链的变化更多是部署结构的提升”;第四是Over Reactive,“当你对一个技术感到狂热的时候是不是有一个优秀的点让你很狂热还是仅仅听到一个名字就很好”,对此,徐昊如此说。 趋势之下 把握趋势,围绕该趋势,ThoughtWorks的高级咨询师刘尚奇以《JavaScrip技术爆炸下的项目选型何去何从》为题,首席咨询师王健以《技术雷达之微服务架构》为题,高级咨询师孙建康以《云环境下的技术站的管理》为题,ThoughtWorks高级分析师钟健鑫以《Docker打造App-Centric交付》为题,ThoughtWorks首席咨询师韩锴以《技术雷达之构筑软件安全DNA》为题分别进行了分享。同时,还有海航生态科技集团的龙旭东先生以《海航集团的数字化转型》为题,ThoughtWorks顾问咨询师袁英杰以《实践演进式设计》为题,丝芙兰的首席架构师杨波以《传统企业的微服务架构转型》为题,ThoughtWorks的首席咨询师刘宇峰以《通过CI/CD来保障数字化转型》为题,Ruff CTO郑晔以《Ruff的前世今生》为主题分别进行了分享。在分享中,Ruff CTO郑晔说道:“理想的企业内部开源和社交化编程,一个品牌,三大服务,四大核心理念,开源云化社交化服务化,还有一个大数据化,这个大数据化在这几块完成之后自然而然会产生。”ThoughtWorks首席咨询师刘宇峰在分享的最后总结道:“我们提到企业数字化转型的挑战,首先是我们要从中心化IT支撑模式转型为去中心化IT开发模式,不同的业务线需要开发符合自己目标和特点的软件,第二是软件需要符合企业的业务特点和三项目标,第三是确保企业的信息和数据资源是能够整合的。我们也提到应对方法,一是利用工程自动化降低管理和协作成本,如通过持续交付这样一套工具链和对应的方法帮助降低管理成本提高协作。二是改变企业内部的IT运作方式,实现康威定律,提升业务和技术高度结合的开发能力。做到这三个方面,并且同时做到,我们的企业就真正能做到数字化转型。在企业数字化转型这三个方面,不管是工程自动化技术还是CI/CD持续交付以及更极端的通过PaaS做到工程自动化,或者是改变IT运作方式,如何做精益企业转型,或者是结合设计业务测试到最后运维的端到端的交付,ThoughtWorks都有非常多的经验和总结,希望以后和大家形成合作,进一步的进行沟通。” 正如刘宇峰最后说的一样,ThoughtWorks有非常多的经验和总结,希望达成合作、进行沟通。在这样“一个新的时代,一个技术的时代”,ThoughtWorks技术雷达,将发挥“干货”怎样的光和热,值得我们共同期待。 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。
小心!看到陌生的Word文档,你可别急着打开,因为你的重要文件可能因此被非法加密。近日,亚信安全就截获了一个通过Word文档传播的Locky勒索软件变种,这个名叫"W2KM_LOCKY.B"的勒索软件会随着Word文档的打开而自动运行,加密用户电脑中的重要文件,进而勒索巨额赎金。对此,亚信安全建议用户需要格外关注不明邮件、链接、文件,利用安全软件策略封锁勒索软件入侵,或者更新亚信安全产品,有效防范此类勒索软件及其变种攻击。 亚信安全研究人员发现,此次截获的勒索软件变种"W2KM_LOCKY.B"传播方式与之前相似,都是通过伪装成邮件附件进行传播。不同的是,变种病毒的载体不同,之前的勒索软件载体多数为JS文件,而此次截获的病毒载体是插入恶意宏代码的Word文档。一旦Word文档被运行,其会链接特定C&C服务器接收和发送信息,并下载勒索软件病毒,继而加密计算机中的Word、Excle、Ppt等重要办公文件,还会在桌面上显示勒索信息。 【勒索软件在受害者电脑桌面上显示的勒索信息】 为了感染更多的用户,不法分子很有可能通过社交工程攻击的方式,有针对性地假借邮递服务、电信、公共事业和政府机构通知的名义来制作诱饵,诱使受害者点击并打开邮件中的附件,以获取更多的赎金。 【勒索软件通过邮件来传播】 亚信安全技术总经理蔡昇钦指出:"勒索软件在今年第一季度已经成为用户的头号威胁因素,可能导致用户数据无法恢复乃至钱财被敲诈。而且勒索软件还产生了大量小范围传播的变种,防护难度很大。特别是对于中小企业来说,由于其安全防护能力相对薄弱、支付赎金的意愿较高,因此更容易成为勒索软件的攻击目标。" 要防范此类勒索软件的攻击,亚信安全建议用户执行以下策略: 1.不要打开来自未知或无法验证发件人的电子邮件,当打开邮件附件时,更要注意查看附件扩展名。 2.不要点击电子邮件中的不明链接,在访问之前可以先检查网站信誉度。 3.由于许多勒索软件加密的文件暂时无法通过第三方还原,请注意备份重要文档。备份的最佳做法是采取3-2-1规则,即至少做三个副本,用两种不同格式保存,并将副本放在异地存储。 4.亚信安全最新发布的中国区病毒码版本12.516.60已包含截止5月10日收到的所有变种,请及时更新病毒码。 5.使用亚信安全防毒墙网络版(OfficeScan 11 SP1)和 Worry-Free 9.0 SP2,开启针对勒索软件(Ransomware)的行为阻止策略。 关于亚信安全 亚信安全是亚信集团"领航产业互联网"版图中的重要业务板块,于2015年由亚信科技对全球最大的独立网络安全软件提供商趋势科技中国区业务进行收购重组,专注于产业互联网安全服务领域,是中国领先的云与大数据安全技术、产品、方案和服务供应商。亚信安全在中国北京和南京设有独立研发中心,拥有超过2000人的专业安全团队,以"护航产业互联网"为使命,以"云与大数据的安全技术领导者"为战略愿景,亚信安全坚持"产品、服务、运营三位一体"的经营模式 ,助力客户构建"立体化主动防御体系",为国家提供网络安全与云产业安全保障,推动实施自主可控战略。 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。
伴随着商业社会的发展,尤其是互联网和各种新兴信息技术对传统商业模式的冲击,IT行业已经逐渐发展至价值链整合,持续创新发展阶段。IT技术开始慢慢外化,并直接面对终端用户,在一定程度上,简化企业管理! 经过长时间的客观调查和反复的话题推敲,清邈商务咨询将于2016年5月27日隆重推出"2016年信息技术卓越峰会"。 这次峰会,专为具有互联网创新精神的企业技术管理者打造,在短短的一天时间里,将围绕:"新一代信息技术主题,从宏观趋势到微观细节展开深入讨论,梳理信息技术发展与大数据,工业4.0,传统行业与互联网的结合,数据分析的新思维,新思维,新做法。 中国独家体验式it盛会: 此次峰会话题主要涉及到大制造和零售快消两大类,大制造行业包含整车、零配件、机械、制药、化工等;零售行业包括卖场、连锁餐饮、美妆、时尚、服装、快消品等。 演讲嘉宾: 迅达管理(上海)有限公司首席信息官 中国医药集团总公司 信息化专家小组组长&信息部主任 阳光信保 数据开发部总经理 欧莱雅中国 大中华区IT 总监 强生消费品(中国)有限公司 高级信息技术总监 戴尔 安全事业部大华区总经理 招商银行 海外数据中心运维主管 欧瑞莲化妆品(中国)有限公司中国区IT 经理 长安汽车有限公司 管理与创新中心副部长 长安福特 IT总监 博士汽车部件(苏州)有限公司 资深IT经理 百安居(中国) 信息技术总监 朗浩集团 信息技术负责人 佐敦(上海)投资管理有限公司 东北亚区信息技术经理 长安铃木 信息系统课课长 持续确认中…. 专家顾问委员会: 马瑞利(中国)有限公司中国区信息技术经理 东芝电子(中国)有限公司信息系统统括部副总监 博士伦 亚太区IT 总监 马克华菲IT 总监 海拉(中国) IT总监 菲亚特克莱斯勒亚太投资有限公司亚太区信息技术经理 持续确认中…. 热点话题: "搭建大数据征信生态圈的机遇与挑战 "医药及互联网创新 "微信时代的来临,你准备好了吗? "数字化在迅达 "工业4.0到底有多远? "共建互联网生态圈 "精益生产对智能制造有多重要? "利用数据作为业务拓展的新能源 "招银金融云助力企业信息化升级 "来自戴尔安全的精彩分享 "Express Route 实施及常见问题 "从O2O到O plus的全渠道整合看数字媒体时代下的精准营销 ……… 最接地气的案例分享,最真实的经验分享,崭新的思路和理念,等您来收获!真正的零距离接触,就在2016信息技术卓越峰会! 火热报名中……. 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。
据外媒17日报道,高盛分析师在报告中称,自2010年以来,其首次对中国最大一些互联网公司的增长和利润有了信心。 高盛谈到7个主要利润来源包括游戏、广告、电子商务、旅游、本地服务、金融和云服务。预计到2020年非金融产品的市场规模将增长至10万亿美元;预计到2020年潜在利润达700亿美元。 高盛认为阿里巴巴、百度、腾讯是主要受益者,拥有社交网络资产的公司似乎最容易受益。 本文出处:畅享网 本文来自云栖社区合作伙伴畅享网,了解相关信息可以关注vsharing.com网站。
-------------------------
最简单的就是按计算器,手机和电脑都有,
首先讲一下“权重”的概念,数字中某位的权重:2的(该位所在的位数(从右至左)-1)次方,比如:100的权重为:2^(1-1)=1 1的权重为:2^(2-1)=2,二进制转十进制:数字中所有位*本位的权重然后求和。
比如将10101转化为十进制:10101=1*2^4+0*2^3+1*2^2+0*2^1+1*2^0=21
十进制如何转二进制:将该数字不断除以2直到商为零,然后将余数由下至上依次写出,即可得到该数字的二进制表示,以将数字21转化为二进制为例。
2.当商为零时,将余数由下至上依次写出,即为21的二进制表示。
拓展资料:
十进制数转换为二进制数时,由于整数和小数的转换方法不同,所以先将十进制数的整数部分和小数部分分别转换后,再加以合并。
而由二进制数转换成十进制数是把二进制数首先写成加权系数展开式,然后按十进制加法规则求和,这种做法称为“按权相加”法。