
云栖直播
小单大比例返现,用价格告诉你什么是真正的超值精选两大会场,超高比例返现爆款,现在推广最合适! 新用户限时优惠,全场3折起,云上爆款 轻量应用服务器 基础版1核1G3Mbps 40G起 SSD 500G起 免费流量¥1197.00/3年起返佣35% ECS计算型c52核4G 1~10M 可选带宽 40G SSD/高效云盘¥1282.20/年起返佣33% ECS计算网络增强型sn1ne2核4G 1~10M 可选带宽 40G 高效云盘¥1390.20/年起返佣30% 轻量应用服务器 通用版1核2G5Mbps 40G起 SSD 500G起 免费流量¥609.00/年起返佣36% 云数据库MySQL1核1G 基础版¥624.60/年起返佣35% 限时3年3折,企业爆款 通用网络增强 2核8G40G高效云盘(系统盘)3930.12/3年返佣32%
为数不多的小单超值返利政策,你还在等什么?小单大比例返现,用价格告诉你什么是真正的超值 点击查询最新推广规则 马上前往个人中心,生产并分享专属推广链接得最高40%奖励 还没加入阿里云推广奖励计划? 立即加入!推广赚取高额现金奖励 高比例返现爆款,现在推广最合适! 爆款产品清单,请访问:云上爆款 企业爆款
小编一直混迹于多个云大使的钉钉群,每天解答各位大使的提问: 现在怎样推广怎么才能高效有用? 云大使可以推广那些返利产品? 云大使获得的奖励金额多久结算? 自己是云大使怎么搞优惠券? 等等一系列问题,然而小编只有几人,大使却有成千上万人,解答效率并不是很高。 终于在大家的呼唤中,云大使答疑机器人于近日在云大使新手钉群上线了,便于解答各位大使遇到的各种问题。当然,因为刚刚上线,问题还不够完善,需要各位一起协助让机器人的内容更加丰富。 大家可以在钉群中@云大使答疑助手,调戏和训练机器人!! 点击立即入群 也可扫码加入我们:
“干垃圾,还是湿垃圾?你是什么垃圾?” 相信魔都的小伙伴已经要被垃圾分类逼疯了,还要面临垃圾桶前,志愿者们的灵魂一问:“你是什么垃圾?” 更糟糕的是,垃圾分类,还要“自学成才”,没人可问。 想人之所想,急人之所急,阿里AI懂你。 经过48小时紧急开发,阿里志愿者紧急开发了AI智能识别垃圾功能(测试版)在手机淘宝上线,这款功能通过阿里云搭建。 直接进入拍立淘,点击“戳我识别垃圾分类”的按钮,即可进入垃圾识别功能。 或者,打开手机淘宝搜索“你是什么垃圾”,系统可以通过阿里云异构计算能力进行快速智能识别,直接进入垃圾识别功能。 (淘宝已上线AI识别垃圾分类功能) 此外,支付宝、天猫精灵、盒马等也推出了不同的垃圾识别功能。 垃圾分类的同时现在更有官网新用户大额优惠活动在等着你! 阿里云官网新注册会员用户,优惠活动已于2019年4月25日开始,数量有限,先到先得! 注册成功,立即领券,领取新人注册礼包,领取成功后,直接发放到你的账户! 活动规则 一、活动对象 阿里云官网新注册会员用户 二、活动时间 2019年4月25日起,数量有限,先到先得。 三、活动规则 1、活动期间,阿里云用户在购买活动页面指定产品时,可享受相应的优惠折扣(以具体页面展示为准)。 注册成功,立即领券,点击了解详情
直播介绍: 本次阿里云618大促有什么重磅的促销活动呢? 本次618的拼团活动,满返活动 怎么玩呢? 都有哪些产品参与活动呢?我改如何选择呢? 有没有爆款产品推荐呢? 尽在618直播!买之前必看!带你玩转阿里云618! 活动策略解读 a.爆款限量抢b.首购用户拼团活动 c.满返活动介绍d.分享云上故事集赞活动 直播时间: 2019-06-17 16:00 至 17:00 观看直播: 扫码即可观看本次直播
不知不觉,云栖社区已经陪伴广大开发者走过了三年,不知您是否遇到过这些尴尬的问题? 编辑博客文章却找不到在哪里插入图片 想申请直播,却一直找不到入口而最后放弃了直播 想成为专家,却不知道社区专家有什么具体要求 ...... 今天,社区重磅推出《玩转云栖36计》系列课程,详细讲解了社区各版块的使用方法,方便大家更好的在社区分享内容。 以下是各版块的操作视频,如需要请自行点击观看↓↓↓ 注册: 玩转云栖36计——注册云栖社区 专家频道: 玩转云栖36计——专家认证 下载频道: 玩转云栖36计——下载频道 博客频道: 玩转云栖36计——博客频道玩转云栖36计——博客专辑 直播频道: 玩转云栖36计——直播报名玩转云栖36计——直播创建玩转云栖36计——直播OBS操作玩转云栖36计——直播发送奖品玩转云栖36计——直播回放 云栖号频道: 玩转云栖36计——云栖号频道 问答频道: 玩转云栖36计——问答 聚能聊频道: 玩转云栖36计——聚能聊 PS:课程还在继续,总计36节。如果有其他建议或需求,欢迎在下方评论区留言。
2009年,阿里云正式成立;在过去的10年里,阿里云引领了国内云计算市场的蓬勃发展;今天,我们从阿里云到阿里巴巴的云,从云到云智能。3月21日举办的2019阿里云峰会·北京,将首次公开解读阿里云智能新的战略思考和发展方向。阿里云愿与众多合作伙伴及客户携手继续前进,十年再出发!以下是各专场PPT资料合集,请自行领取哦~ 观看大会回顾视频请点击这里 【主会场】 2019阿里云峰会·北京 | 十年再出发https://yq.aliyun.com/articles/695149 【开发者专场】 开发者专场-三红 kingsum Dragonwell云栖最终版https://yq.aliyun.com/download/3429 开发者专场-瓦力 探索实时计算新架构-Apache Flink 的云原生部署架构和实践最终版https://yq.aliyun.com/download/3430 开发者专场-刘力华 代码智能化的探索与实践https://yq.aliyun.com/download/3431 开发者专场-张磊2019阿里云峰会北京最终版https://yq.aliyun.com/download/3432 开发者专场-云原生赋能AI:基于云原生的AI开发普惠https://yq.aliyun.com/download/3433 开发者专场-杨皓然2019阿里云峰会北京_函数计算https://yq.aliyun.com/download/3434 开发者专场-张暘 深度解密基于 Serverless下 构建的工具家族https://yq.aliyun.com/download/3435 【互联网出海专场】 互联网出海专场—马全治2019再出发,阿里云的互联网海外布局https://yq.aliyun.com/download/3417 互联网出海专场-【小影-合作伙伴】2019阿里云峰会北京- 终稿-视觉优化https://yq.aliyun.com/download/3418 互联网出海专场--MinTech 2019阿里云峰会北京https://yq.aliyun.com/download/3419 互联网出海专场-—李刚科技创新助力业务出海https://yq.aliyun.com/download/3420 互联网出海专场-大数据助力海外精准营销https://yq.aliyun.com/download/3421 互联网出海专场-项碧波技术创新改变教育未来https://yq.aliyun.com/download/3422 互联网出海专场-企业级基础设施-重磅解读2019云计算发展趋势https://yq.aliyun.com/download/3423 【金融专场】 金融专场-金融行业如何实现IT到DT 的转型https://yq.aliyun.com/download/3424 金融专场-科技驱动数字金融https://yq.aliyun.com/download/3425 金融专场-金融零售数字化运营探索https://yq.aliyun.com/download/3426 金融专场-新一代移动研发平台mPaaS智能化实践https://yq.aliyun.com/download/3427 金融专场-DevOps驱动金融行业研发效能升级https://yq.aliyun.com/download/3428 【企业级互联网架构专场】 企业级互联网架构专场-2019阿里云中间件新功能重磅发布https://yq.aliyun.com/download/3436 企业级互联网架构专场-Aliware支撑商越采购中台 帮助企业采购向数字化转型https://yq.aliyun.com/download/3437 【数字政府专场】 数字政府专场-华夏电通与阿里云智能云上法院解决方案发布https://yq.aliyun.com/download/3447 数字政府专场-2019阿里云峰会北京_一云两端 智税惠民https://yq.aliyun.com/download/3448 数字政府专场-政务行业云全新发布-陈峥https://yq.aliyun.com/download/3449 【物联网专场】 物联网专场-龙一民 阿里云智能IoT高级技术专家https://yq.aliyun.com/download/3450 物联网专场-陈明波 阿里云智能IoT高级产品专家https://yq.aliyun.com/download/3451 物联网专场-佘尚锋 美的IoT总经理https://yq.aliyun.com/download/3452 物联网专场-赵刚 阿里云智能IoT高级产品专家https://yq.aliyun.com/download/3453 物联网专场-Steen Graham 英特尔集团IoT生态及渠道部门总经理https://yq.aliyun.com/download/3454 物联网专场-《智能人居设备应用规范》发布https://yq.aliyun.com/download/3455 物联网专场-《智能人居设备应用规范发布》背景PPThttps://yq.aliyun.com/download/3456 物联网专场-远程养老看护方案https://yq.aliyun.com/download/3457 物联网专场-基于物联网技术赋能框架传媒行业变革https://yq.aliyun.com/download/3458 物联网专场-助力“生产力云化”重塑产业竞争力https://yq.aliyun.com/download/3474 物联网专场-ICA 物模型https://yq.aliyun.com/download/3475 物联网专场-IOT城市-“1+N”产品体系 助力智能城市建设https://yq.aliyun.com/download/3476 物联网专场-阿里云智能IOT赋能威胜信息实现转型之路https://yq.aliyun.com/download/3477 物联网专场-阿里云工业互联网平台 助力智造升级https://yq.aliyun.com/download/3478 【云迁移与云容灾】 云迁移与云容灾-数字时代企业IT系统云化之路https://yq.aliyun.com/download/3459 云迁移与云容灾-构建混合驱动的云平台-引领体验至上的数字化转型https://yq.aliyun.com/download/3460 云迁移与云容灾-基于混合云架构的迁移和灾备https://yq.aliyun.com/download/3461 云迁移与云容灾-【驻云科技】2019阿里云峰会北京https://yq.aliyun.com/download/3462 云迁移与云容灾-Serverless架构企业数据备份和迁移https://yq.aliyun.com/download/3463 云迁移与云容灾-基于弹性裸金属(神龙)服务器K8S容器迁云最佳实践https://yq.aliyun.com/download/3464 云迁移与云容灾-云上数据库迁移与云原生容灾https://yq.aliyun.com/download/3465 云迁移与云容灾-企业迁移公共云的网络之道https://yq.aliyun.com/download/3466 【SaaS企业加速器专场】 SaaS企业加速器专场-SaaS加速器https://yq.aliyun.com/download/3397 SaaS企业加速器专场-云拐点:中国SaaS创新探索https://yq.aliyun.com/download/3398 SaaS企业加速器专场-【蓝凌】2019阿里云峰会北京https://yq.aliyun.com/download/3399 SaaS企业加速器专场-SaaS加速器_商业中心https://yq.aliyun.com/download/3400 SaaS企业加速器专场 畅捷通-阿里云分会场https://yq.aliyun.com/download/3401 SaaS企业加速器专场-SaaS加速器_能力中心https://yq.aliyun.com/download/3402 【workshop专场】 workshop专场--容器、消息&IoT专场-开发者动手实践营-容器、消息和IoT-Java诊断利器Arthas排查问题实践https://yq.aliyun.com/download/3403 workshop专场--容器、消息&IoT专场-开发者动手实践营-容器、消息和IoT-PouchContainer + Kubernetes 云原生https://yq.aliyun.com/download/3404 workshop专场--容器、消息&IoT专场-开发者动手实践营-容器、消息和IoT--RocketMQhttps://yq.aliyun.com/download/3405 workshop专场--容器、消息&IoT专场-开发者动手实践营-容器、消息和IoT--物联网端到端全链路开发https://yq.aliyun.com/download/3406 workshop专场--容器、消息&IoT专场-开发者动手实践营-容器、消息和IoT--云原生镜像分发利器之Dragonfly实践https://yq.aliyun.com/download/3407 workshop专场-微服务专场-开发者动手实践营-微服务-Dubbo2.7和Dubbo Admin新特性https://yq.aliyun.com/download/3408 workshop专场-微服务专场-开发者动手实践营-Fescar 在微服务架构下分布式一致性的实践https://yq.aliyun.com/download/3409 workshop专场-微服务专场-开发者动手实践营-微服务-Spring Cloud Alibaba 微服务全家桶体验https://yq.aliyun.com/download/3410 workshop专场-微服务专场-开发者动手实践营-微服务-使用Nacos进行服务的动态发现和流量调度https://yq.aliyun.com/download/3411 workshop专场-微服务专场-开发者动手实践营-微服务-使用Sentinel进行微服务流量控制https://yq.aliyun.com/download/3412 【企业级基础设施专场】 企业级基础设施专场-飞天基础设施智能运维_何诚https://yq.aliyun.com/download/3438 企业级基础设施专场-重磅解读2019云计算发展趋势https://yq.aliyun.com/download/3439 企业级基础设施专场-基于ECS构建稳定、高业务弹性、低成本的企业应用实践解析https://yq.aliyun.com/download/3440 企业级基础设施专场-普惠数据智能经济 下一代云上存储创新https://yq.aliyun.com/download/3441 企业级基础设施专场-DT时代的企业混合云存储架构探讨 https://yq.aliyun.com/download/3442 企业级基础设施专场-构建安全智能开放的下一代企业级网络https://yq.aliyun.com/download/3443 企业级基础设施专场-异构计算为企业上云提供核动力https://yq.aliyun.com/download/3444 企业级基础设施专场-实现规模化、自动化的云上IT管理https://yq.aliyun.com/download/3445 企业级基础设施专场-云上护航服务-保障云上的尖峰时刻https://yq.aliyun.com/download/3446 【边缘计算专场】 边缘计算专场_边缘节点服务(ENS),离用户更近的计算https://yq.aliyun.com/download/3470 边缘计算专场-基于阿里云ENS为互动直播带来的变革https://yq.aliyun.com/download/3471 边缘计算专场-基于英特尔平台技术的创新应用https://yq.aliyun.com/download/3472 边缘计算专场-Link IoT Edge让物联智能更进一步https://yq.aliyun.com/download/3473 【德勤专场】 德勤专场-阿里云智能数字化中台联合解决方案https://yq.aliyun.com/download/3413 德勤专场-阿里云智能智慧出行联合解决方案https://yq.aliyun.com/download/3414 德勤专场-阿里云智能核心系统上云联合解决方案https://yq.aliyun.com/download/3415 德勤专场-数字化转型创新资产-陆晓松https://yq.aliyun.com/download/3416
一、点击进入社区直播首页,直播创建入口 二、按照直播创建快速手册开始创建直播: 三、按照OBS操作手册,进行直播测试 请务必至少提前一天测试,并尽量保证测试时所处的场景、直播设备等,和正式直播无差别 四、其他可能遇到的问题
一、报名与登录:1、用户观看直播必须登录吗?直播编辑页面,这里不打开,用户可以不登录直接观看,登录的弹窗跳出,点叉掉即可(有涨粉需求的同学建议开启) 2、用户观看直播必须报名吗?直播未开始前,是要报名的,直播生成回放后,可以直接观看。若要强制用户报名,可以开启直播管理页面左下方的强制用户报名 3、直播支持手机端吗?支持的 二、回放问题: 1、直播之后有回放吗?回放会自动生成,回放视频和直播链接是同一个 2、回放视频多久可以生成?根据直播时间的长短,一般30分钟内可以生成,若时间很长的直播,请耐心等待 3、回放视频需要处理,可以直接下载吗? 若是旧版本通过目睹上传的回放,可以直接下载 若是新版本自动生成的回放,则不能直接下载,请联系百见/安平,获取视频源地址下载 三:其他问题: 1、测试需要建新的直播间吗? 不需要,设置的正式直播时间之前的所有操作都算测试,观众不会看到。正式时间到了会自动跳到观众页面直播管理页面下拉,前台测试播放效果,就是测试的观看地址 2、可以直接上传视频吗?可以的,(1)成为专家,即可一键发起直播,专家申请入口 (2)如果你是技术爱好者,还在成长为专家的道路上,愿意分享开发者相关内容,可以将你的直播主题、简介、社区昵称发送至邮箱 baijian@alibaba-inc.com进行内测申请,我们会在1个工作日内给到回复步骤如下: 3、可以对用户发奖吗可以的4、如何给用户发奖:步骤如下: 5、可以录播吗?可以的步骤如下:目睹登录地址:http://mudu.tv/console/?c=index&a=index登录账号请钉钉联系安平1)、将视频传到媒体库2)、进入首页,选择本场直播 3)、点击进入,选择列表直播,添加视频 注意:上传的视频大小不能超过3个G 4)、选好当场直播的视频,到设置的直播开始时间点击开始直播即可,根据视频时间长短,视频结束,可能需要手动点击结束 6、直播结束时间设置: 直播到了设置的结束时间,如果用户一直在观看页面,没有退出,那么可以继续观看,如果是重新进入直播间的观众,那么将无法看到直播。所以为了避免出现这种情况,请将设置的结束时间延长30-60分钟
云栖社区直播频道改版啦!新版直播OBS操作速速Get! 一、普通桌面直播(仅展现ppt与主播声音) 1、安装OBS之后点击页面右下方设置: 2、选择流——选择自定义流媒体服务器 3、将下面地址分别对应填入以下位置(创建直播间的人可以看到) 若主持人和讲师是同一个人 4、新建场景,选择窗口捕获,打开PPT,双击选择捕捉PPT的窗口 若想全屏显示PPT,调节红框大小到PPT的大小,可以当做PPT是全屏如图:拖动红框到 若主持人和讲师不是同一个人 5、添加显示器捕获请注意:显示器捕获,电脑上的所有操作都会直播出去! 6、若桌面没有显示完全,拖动红框位置,调节一下即可 二、普通桌面直播,并显示讲师画面1、若需要拍摄到讲师,在同一个场景下,添加一个视频捕获设备点击确定即可 三、切换已录制好的视频若需要切换广告片等视频,新建一个场景,添加媒体源,在本地文件里选择需要的视频 需要捕捉声音的场景,请单独添加音频输入捕获设备 注意:场景请用自己分得清的名字命名,以防切换错误 四、开始直播 1、点击开始推流,开始直播 2、直播结束点结束推流,回放视频将在直播结束后自动生成 五、其他情况 1、线下沙龙转播如果是供应商推流,将以下推流地址中间加"/"提供给供应商即可:例如:rtmp://live-push.aliyun.com/yq和4a5528b603f08231cf7369ef3890cefb?auth_key=1539684550-0-1831129997670534-958092413cf82872afd2ce677c252a7f和一起就是:rtmp://live-push.aliyun.com/yq/4a5528b603f08231cf7369ef3890cefb?auth_key=1539684550-0-1831129997670534-958092413cf82872afd2ce677c252a7f
2018杭州云栖大会展览吸引了来自16个国家与地区超过400家企业参展,总面积超过30000平方米,精心打造涵盖人工智能、大数据、自动驾驶、智慧家居、智能制造、城市大脑、医疗与生命科学、FinTech及物流等领域的前沿科技盛宴。 2018杭州·云栖大会完整展商名录如下: 展区 展位号 展商名字 公司中文名 英文(名字露出) 公司英文名 C C-101 阿里云(数据中心) 阿里云 数据中心 Alibaba Cloud demo center ALIBABA CLOUD DEMO CENTER C C-102 智能办公 阿里巴巴人工智能实验室 Alibaba A.I.Labs ALIBABA A.I.LABS C C-103 菜鸟 菜鸟 CAINIAO CAINIAO C C-104 车+路板块 AliOS 斑马 高德 AliOS banma AMAP ALIOS BANMA AMAP C C-105 达摩院 阿里巴巴达摩院 ALIBABA DAMO ACADEMY ALIBABA DAMO ACADEMY C C-106 优酷 优酷 YOUKU YOUKU C C-107 星空之镜 上海科技馆 Shanghai Science & Technology Museum SHANGHAI SCIENCE & TECHNOLOGY MUSEUM C C-108 盒马 盒马 FRESHIPPO FRESHIPPO C C-109 全家 全家便利店 Familymart FamilyMart C C-110 亲橙邦 亲橙邦 Alitopia ALITOPIA C C-201 DXC DXC.technology China DXC.technology China DXC.TECHNOLOGY CHINA C C-202 Bespin Global 北京贝斯平云科技有限公司 Bespin Global China BESPIN GLOBAL CHINA C C-203 Intel 英特尔(中国)公司 Intel China Ltd INTEL CHINA LTD C C-204 千寻位置 千寻位置网络有限公司 Qianxun Spatial Intelligence Inc. QIANXUN SPATIAL INTELLIGENCE INC. C C-205 浪潮科技 浪潮电子信息产业股份有限公司 Inspur Electronic Information Industry Co., Ltd INSPUR ELECTRONIC INFORMATION INDUSTRY CO., LTD C C-206 友盟 友盟 Umeng UMENG C C-207 G7 G7 G7 G7 C C-208 哈曼 浙江哈曼视听科技有限公司 ZHEJIANG HARMAN AV TECHNOLOGY CO.,LTD. ZHEJIANG HARMAN AV TECHNOLOGY CO.,LTD. C C-209 CMI 中国移动国际有限公司 China Mobile International Limited CHINA MOBILE INTERNATIONAL LIMITED C C-301 Red Hat 红帽软件(北京)有限公司 Red Hat Pte Ltd RED HAT PTE LTD C C-302 CNCF 云原生计算基金会(CNCF) Cloud Native Computing Foundation CLOUD NATIVE COMPUTING FOUNDATION C C-303 SAP 思爱普(中国)有限公司 SAP SAP C C-304 Accenture Accenture C C-305 银杏谷资本 银杏谷资本 Yinxinggu Capital YINXINGGU CAPITAL C C-306 心中有数 北京心中有数科技有限公司 KuWeather Science & Technology Co., Ltd KUWEATHER SCIENCE & TECHNOLOGY CO., LTD C C-307 SUSE 北京络威尔软件有限公司 SUSE SUSE C C-308 PCCW PCCW Global PCCW GLOBAL C C-309 HGC 环球全域电讯有限公司 HGC Global Communications Ltd. HGC GLOBAL COMMUNICATIONS LTD. C C-401 Toshiba 东芝电子(中国)有限公司 Toshiba Electronics (China) co., Ltd. TOSHIBA ELECTRONICS (CHINA) CO., LTD. C C-402 Hitachi 日立(中国)有限公司 Hitachi (China) Ltd. HITACHI (CHINA) LTD. C C-403 云徙科技 杭州云徙科技有限公司 Hangzhou Yunxi Technology Co., Ltd. HANGZHOU YUNXI TECHNOLOGY CO., LTD. C C-405 浩鲸科技 浩鲸云计算科技股份有限公司 Whale Cloud Technology Co., Ltd WHALE CLOUD TECHNOLOGY CO., LTD C C-406 袋鼠云 袋鼠云 DTSTACK.COM DTSTACK.COM C C-407 Commvault 慷孚沃德软件技术(北京)有限公司 Commvault Software Technology (Beijing) Co., Ltd. COMMVAULT SOFTWARE TECHNOLOGY (BEIJING) CO., LTD. C C-408 HashiCorp 哈希公司 HashiCorp HASHICORP C C-409 CenturyLink CenturyLink CENTURYLINK C C-410 E-Glober TradeFive TRADEFIVE C C-501 Equinix Equinix EQUINIX C C-502 Fortinet 防特网信息科技(北京)有限公司 Fortinet FORTINET C C-503 NetApp 凌拓(上海)商贸有限公司 NetApp (Shanghai) Commercial Co., Ltd. NETAPP (SHANGHAI) COMMERCIAL CO., LTD. C C-504 企加云 企加云 iTrigger ITRIGGER C C-505 驻云科技 上海驻云信息科技有限公司 Shanghai Zhuyun Information Technology Co., Ltd SHANGHAI ZHUYUN INFORMATION TECHNOLOGY CO., LTD C C-601 上海数据港 上海数据港股份有限公司 Shanghai At Hub Co., Ltd SHANGHAI AT HUB CO., LTD C C-602 SBC SB Cloud Corp SB CLOUD CORP C C-603 NTT NTT通信系统(中国)有限公司 NTT Communications China Co., Ltd. NTT COMMUNICATIONS CHINA CO., LTD. C C-604 HKSTP Hong Kong Science and Technology Parks Corporation 香港科技园公司 香港科技园公司 C C-605 Invest Hong Kong 香港特别行政区政府投资推广署 Invest Hong Kong, The Government of the Hong Kong Special Administrative Region INVEST HONG KONG, THE GOVERNMENT OF THE HONG KONG SPECIAL ADMINISTRATIVE REGION C C-606 七牛云 上海七牛信息技术有限公司 Shanghai Qiniu Information Technologies Co., Ltd. SHANGHAI QINIU INFORMATION TECHNOLOGIES CO., LTD. C C-607 ASL 自动系统集团有限公司 Automated Systems Holdings Limited AUTOMATED SYSTEMS HOLDINGS LIMITED C C-608 JetBrains JetBrains JETBRAINS C C-609 Docker 道科技术(北京)有限公司 DOCKER DOCKER D1 D1-101 数美时代 北京数美时代科技有限公司 Beijing Shumei Times Technology Co., Ltd BEIJING SHUMEI TIMES TECHNOLOGY CO., LTD D1 D1-102 深圳数位传媒科技有限公司 深圳数位传媒科技有限公司 Shenzhen SHUWEI Media Technology Co., Ltd SHENZHEN SHUWEI MEDIA TECHNOLOGY CO., LTD D1 D1-103 杭州比智科技有限公司 杭州比智科技有限公司 HangZhou BitMind Technology Co., Ltd HANGZHOU BITMIND TECHNOLOGY CO., LTD D1 D1-104 上海百胜软件股份有限公司 上海百胜软件股份有限公司 ShangHai Baison Software Co., Ltd SHANGHAI BAISON SOFTWARE CO., LTD D1 D1-105 上海伯俊软件科技有限公司 上海伯俊软件科技有限公司 burgeon BURGEON D1 D1-106 心怡科技股份有限公司 心怡科技股份有限公司 ALOG Technology Co., Ltd. ALOG TECHNOLOGY CO., LTD. D1 D1-107 中通快递股份有限公司 中通快递 ZTO Express ZTO EXPRESS D1 D1-108 圆通信息科技(西安)有限公司 圆通信息科技(西安)有限公司 YTO Express Co., Ltd. YTO EXPRESS CO., LTD. D1 D1-109 神龙汽车有限公司 东风雪铁龙 Dongfeng Citroën DONGFENG CITROËN D1 D1-110 北京天源迪科 北京天源迪科信息技术有限公司 BEIJING TIANYUAN DIC INFORMATION TECHNOLOGY CO., LTD. BEIJING TIANYUAN DIC INFORMATION TECHNOLOGY CO., LTD. D1 D1-111 深圳美云智数科技有限公司 深圳美云智数科技有限公司 Shenzhen Meicloud Technology Co., Ltd SHENZHEN MEICLOUD TECHNOLOGY CO., LTD D1 D1-112 苏州思必驰信息科技有限公司 苏州思必驰信息科技有限公司 AI Speech Co., Ltd. AI SPEECH CO., LTD. D1 D1-113 杭州海康威视系统技术有限公司 杭州海康威视系统技术有限公司 Hangzhou Hikvision System Technology Co., Ltd. HANGZHOU HIKVISION SYSTEM TECHNOLOGY CO., LTD. D1 D1-114 杭州云栖云数据有限公司 杭州云栖云数据有限公司 Hangzhou cloud data limited company HANGZHOU CLOUD DATA LIMITED COMPANY D1 D1-115 杭州半云科技有限公司 杭州半云科技有限公司 Hangzhou Bywin Co., Ltd. HANGZHOU BYWIN CO., LTD. D1 D1-116 杭州数空科技有限公司 杭州数空科技有限公司 bigdata-x BIGDATA-X D1 D1-117 飞界 浙江飞界科技有限公司 ZHEJIANG FLYING WORLD TECHNOLOGY CO., LTD ZHEJIANG FLYING WORLD TECHNOLOGY CO., LTD D1 D1-118 智慧城市 第五届互联网之光博览会推介 introduction of the fifth world internet conference INTRODUCTION OF THE FIFTH WORLD INTERNET CONFERENCE D1 D1-119 舒福德智能科技(杭州)有限公司 舒福德智能科技(杭州)有限公司 Softide SOFTIDE D1 D1-120 乐偶 乐偶(杭州)网络有限公司 Leou (Hangzhou) network co., ltd LEOU (HANGZHOU) NETWORK CO., LTD D1 D1-201 深圳汇茂科技股份有限公司 深圳汇茂科技股份有限公司 Sinocan International Technologies Co.,Ltd SINOCAN INTERNATIONAL TECHNOLOGIES CO.,LTD D1 D1-202 杭州首展科技有限公司 杭州首展科技有限公司 Hangzhou First Shows Technology co., LTD HANGZHOU FIRST SHOWS TECHNOLOGY CO., LTD D1 D1-203 神州数码集团股份有限公司 神州数码集团股份有限公司 Digital China DIGITAL CHINA D1 D1-204 上海利奥信息科技有限公司 上海利奥信息科技有限公司 LVX China Limited LVX CHINA LIMITED D1 D1-205 上海商米科技有限公司 上海商米科技有限公司 Shanghai Sunmi Technology Co., Ltd. SHANGHAI SUNMI TECHNOLOGY CO., LTD. D1 D1-206 上海管易云计算软件有限公司 上海管易云计算软件有限公司 Shanghai tube Yi Yun calculation software co., Ltd SHANGHAI TUBE YI YUN CALCULATION SOFTWARE CO., LTD D1 D1-207 上海简米网络科技有限公司 上海简米网络科技有限公司 Ping++ PING++ D1 D1-208 广州青莲网络科技有限公司 广州青莲网络科技有限公司 Guangzhou clotus network technology co., LTD GUANGZHOU CLOTUS NETWORK TECHNOLOGY CO., LTD D1 D1-209 北京三一智农数据技术有限公司 北京三一智农数据技术有限公司 Beijing 31 Intelligent Agriculture Data Technology Co., Ltd BEIJING 31 INTELLIGENT AGRICULTURE DATA TECHNOLOGY CO., LTD D1 D1-210 上海发网供应链管理有限公司 上海发网供应链管理有限公司 FineEx FineEx D1 D1-211 上海科箭软件科技有限公司 上海科箭软件科技有限公司 Quantum Asia Solutions Limited QUANTUM ASIA SOLUTIONS LIMITED D1 D1-212 中储南京智慧物流科技有限公司 中储南京智慧物流科技有限公司 CMST CMST D1 D1-213 唯智信息技术(上海)股份有限公司 唯智信息技术(上海)股份有限公司 vTradEx Information Technology (Shanghai) Co., Ltd vTradEx INFORMATION TECHNOLOGY (SHANGHAI) CO., LTD D1 D1-214 北京海天瑞声科技股份有限公司 北京海天瑞声科技股份有限公司 Speechocean SPEECHOCEAN D1 D1-215 北京合力亿捷科技股份有限公司 北京合力亿捷科技股份有限公司 HOLLYCRM Co., Ltd. HOLLYCRM CO., LTD. D1 D1-216 北京格灵深瞳信息技术有限公司 北京格灵深瞳信息技术有限公司 D1 D1-217 Solarflare Communications, Inc. / Solarflare Communications, Inc. SOLARFLARE COMMUNICATIONS, INC. D1 D1-218 北京旷视科技有限公司 北京旷视科技有限公司 Beijing Kuangshi Technology Co., Ltd BEIJING KUANGSHI TECHNOLOGY CO., LTD D1 D1-219 用友网络科技股份有限公司 用友网络科技股份有限公司 Yonyou YONYOU D1 D1-220 上海泛微网络科技股份有限公司 上海泛微网络科技股份有限公司 Shanghai Weaver Network SHANGHAI WEAVER NETWORK D1 D1-221 民生科技有限公司 民生科技有限公司 MINSHENG FINTECH CORP. LTD. MINSHENG FINTECH CORP. LTD. D1 D1-222 北京航天宏图信息技术股份有限公司 北京航天宏图信息技术股份有限公司 Beijing PIESAT Information Technology Co., Ltd. BEIJING PIESAT INFORMATION TECHNOLOGY CO., LTD. D1 D1-223 上海宝信软件股份有限公司 上海宝信软件股份有限公司 Shanghai Baosight Software Co, Ltd. SHANGHAI BAOSIGHT SOFTWARE CO, LTD. D1 D1-224 北京群智合信息科技股份有限公司 北京群智合信息科技股份有限公司 Wisenergy (Beijing) Information Technologies Co., Ltd WISENERGY (BEIJING) INFORMATION TECHNOLOGIES CO., LTD D1 D1-225 锐捷网络股份有限公司 锐捷网络股份有限公司 Ruijie Networks Co., Ltd. RUIJIE NETWORKS CO., LTD. D1 D1-226 软通智慧科技有限公司 软通智慧科技有限公司 iSoftStone Smart Technology Co., LTD (issTech) iSoftStone SMART TECHNOLOGY CO., LTD (issTech) D1 D1-227 杭州网银互联科技股份有限公司 杭州网银互联科技股份有限公司 Hangzhou Netbank Interlink Technologies co., ltd HANGZHOU NETBANK INTERLINK TECHNOLOGIES CO., LTD D1 D1-228 杭州云嘉云计算有限公司 杭州云嘉云计算有限公司 Hangzhou Yunjia Cloud Computing Co., Ltd HANGZHOU YUNJIA CLOUD COMPUTING CO., LTD D1 D1-301 杭州融都科技股份有限公司 杭州融都科技股份有限公司 D1 D1-302 秦皇岛商之翼网络科技有限公司 秦皇岛商之翼网络科技有限公司 Qinhuangdao BUSINESS WINGS NETWORK TECHNOLOGY CO., lTD. QINHUANGDAO BUSINESS WINGS NETWORK TECHNOLOGY CO., LTD. D1 D1-303 杭州有云科技有限公司 杭州有云科技有限公司 Hangzhou youyun technology co., LTD HANGZHOU YOUYUN TECHNOLOGY CO., LTD D1 D1-304 百望股份有限公司 百望股份有限公司 Best Wonder Co., Ltd BEST WONDER CO., LTD D1 D1-305 杭州鸿元汇锋科技有限公司 杭州鸿元汇锋科技有限公司 HangZhou HongYuanHuiFeng Technology Ltd. HANGZHOU HONGYUANHUIFENG TECHNOLOGY LTD. D1 D1-306 中军凯旋汽车租赁有限公司 中军凯旋汽车租赁有限公司 CHINA MILITARY AUTO RENTAL CO., LTD. CHINA MILITARY AUTO RENTAL CO., LTD. D1 D1-307 洛可可 D1 D1-308 鲸腾 浙江鲸腾网络科技有限公司 Zhejiang Jingteng Network Technology Co., Ltd. ZHEJIANG JINGTENG NETWORK TECHNOLOGY CO., LTD. D1 D1-309 上海重盟信息技术有限公司 上海重盟信息技术有限公司 Shanghai ZMT Information Technology Co., Ltd. SHANGHAI ZMT INFORMATION TECHNOLOGY CO., LTD. D1 D1-310 保歌(上海)金融信息服务有限公司 保歌(上海)金融信息服务有限公司 D1 D1-311 浙江华通云数据科技有限公司 浙江华通云数据科技有限公司 WATONE CLOUD DATA. WATONE CLOUD DATA. D1 D1-312 杭州雷数科技有限公司 杭州雷数科技有限公司 Hangzhou Thunders Data Technology Co., Ltd HANGZHOU THUNDERS DATA TECHNOLOGY CO., LTD D1 D1-313 杭州优云软件有限公司 北京广通信达软件股份有限公司 Beijing Guangtong Xinda Software Inc BEIJING GUANGTONG XINDA SOFTWARE INC D1 D1-314 睿码控股集团 睿码控股集团 Remark Holdings REMARK HOLDINGS D1 D1-315 上海恬胜信息科技有限公司 上海恬胜信息科技有限公司 Teambition TEAMBITION D1 D1-316 广州探迹科技有限公司 广州探迹科技有限公司 Tungee TUNGEE D2 D2-101 VerisignInc Verisign Inc VERISIGN INC D2 D2-102 杭州天谷 杭州天谷信息科技有限公司 Tsign TSIGN D2 D2-103 杭州安恒信息技术股份有限公司 杭州安恒信息技术股份有限公司 Dbappsecurity Co., Ltd DBAPPSECURITY CO., LTD D2 D2-104 江苏凌通通信技术有限公司 江苏星网时频技术有限公司 Jiangsu Starlink Time-frequency Technology Co., Ltd. JIANGSU STARLINK TIME-FREQUENCY TECHNOLOGY CO., LTD. D2 D2-105 北京零重空间技术有限公司 北京零重空间技术有限公司 Beijing ZeroG Space Technology Co., LTD BEIJING ZEROG SPACE TECHNOLOGY CO., LTD D2 D2-106 优客工场(北京)创业投资有限公司 优客工场(北京)创业投资有限公司 Ucommune (Beijing) Venture Investment Co., Ltd UCOMMUNE (BEIJING) VENTURE INVESTMENT CO., LTD D2 D2-107 上海云轴信息科技有限公司 上海云轴信息科技有限公司 Zstack Zstack D2 D2-201 杭州持码网络科技有限公司 杭州持码网络科技有限公司 Clothesmake CLOTHESMAKE D2 D2-202 北京悦游信息技术有限公司 犀思云(苏州)云计算有限公司 Syscloud(SuZhou) technology Limited SYSCLOUD(SUZHOU) TECHNOLOGY LIMITED D2 D2-203 ForeScout 锋哨软件技术(上海)有限公司 ForeScout Technologies FORESCOUT TECHNOLOGIES D2 D2-204 北京山石网科信息技术有限公司 北京山石网科信息技术有限公司 Hillstone Networks HILLSTONE NETWORKS D2 D2-205 CheckPoint 以色列捷邦安全软件科技有限公司 Check Point Software Technologies Ltd CHECK POINT SOFTWARE TECHNOLOGIES LTD D2 D2-206 CyberArkSoftwareLtd. 赛普拉斯半导体技术 CyberArk Software Limited CyberArk SOFTWARE LIMITED D2 D2-207 北京易华录信息技术股份有限公司 北京易华录信息技术股份有限公司 Beijing E-Hualu Information Technology Co., Ltd BEIJING E-HUALU INFORMATION TECHNOLOGY CO., LTD D2 D2-208 鹏博士电信传媒集团 鹏博士集团 DR.PENG GROUP DR.PENG GROUP D2 D2-209 政采云有限公司 政采云有限公司 GP-Cloud GP-CLOUD D2 D2-210 荣之联 北京荣之联科技股份有限公司 D2 D2-211 AT&T AT&T AT&T D2 D2-212 好球 北京河马能量体育科技有限公司(Hao球国际) Beijing Hippo Energy Sports Technology Co., Ltd. (Haoqiu) BEIJING HIPPO ENERGY SPORTS TECHNOLOGY CO., LTD. (HAOQIU) D2 D2-213 北京微吼时代科技有限公司 北京微吼时代科技有限公司 D2 D2-214 北京元鼎时代科技股份有限公司 北京元鼎时代科技股份有限公司 D2 D2-215 上海安畅网络科技股份有限公司 上海安畅网络科技股份有限公司 Shanghai Anchnet Network Technology Co., Ltd SHANGHAI ANCHNET NETWORK TECHNOLOGY CO., LTD D2 D2-216 CSDN CSDN CSDN CSDN D2 D2-301 医疗点 医疗点 First Aid FIRST AID D2 D2-302 九州云腾 北京九州云腾科技有限公司 Beijing JiuZhouYunTeng Technology Co., Ltd. BEIJING JIUZHOUYUNTENG TECHNOLOGY CO., LTD. D2 D2-303 杭州星犀科技有限公司 杭州星犀科技有限公司 Hangzhou Xingxi Technology Co., Ltd HANGZHOU XINGXI TECHNOLOGY CO., LTD D2 D2-304 北京众签科技有限公司 北京众签科技有限公司 Beijing CrowdSign Technology Co., Ltd., BEIJING CROWDSIGN TECHNOLOGY CO., LTD., D2 D2-305 青岛青软实训教育科技服份有限公司 青软实训教育科技股份有限公司 QST Educational Technology Co., Ltd. QST EDUCATIONAL TECHNOLOGY CO., LTD. D2 D2-306 北京明智墨思科技有限公司 北京明智墨思科技有限公司 Beijing MMX Technology Co., Ltd. BEIJING MMX TECHNOLOGY CO., LTD. D3 D3-101 上海东软载波微电子有限公司 上海东软载波微电子有限公司 Shanghai Eastsoft Microelectronics Co., Ltd. SHANGHAI EASTSOFT MICROELECTRONICS CO., LTD. D3 D3-102 博通集成电路(上海股份有限公司) 博通集成电路(上海)股份有限公司 Beken Corporation BEKEN CORPORATION D3 D3-103 深圳慧联无限科技有限公司 深圳慧联无限科技有限公司 SHENZHEN EASYLINKIN TECHNOLOGY CO., LTD SHENZHEN EASYLINKIN TECHNOLOGY CO., LTD D3 D3-104 1.泰凌微电子(上海)有限公司 2.上海诺行信息技术有限公司 3.北京门思科技有限公司 泰凌微电子(上海)有限公司 上海诺行信息技术有限公司 Telink Semiconductor (Shanghai) Co., Ltd. Shanghai Notion Information Technology Co., Ltd TELINK SEMICONDUCTOR (SHANGHAI) CO., LTD. SHANGHAI NOTION INFORMATION TECHNOLOGY CO., LTD D3 D3-105 紫光展锐科技有限公司 紫光展锐科技有限公司 Unisoc Technologies Co., Ltd. UNISOC TECHNOLOGIES CO., LTD. D3 D3-106 中移物联,兆易创新 中移物联网有限公司 北京兆易创新科技股份有限公司 China Mobile IoT Company Limited GigaDevice Semiconductor (Beijing) Inc. CHINA MOBILE IOT COMPANY LIMITED GIGADEVICE SEMICONDUCTOR (BEIJING) INC. D3 D3-107 意法半导体(中国)投资有限公司 意法半导体 STMicroelectronics STMICROELECTRONICS D3 D3-108 Cypress 赛普拉斯半导体 Cypress Semiconductor CYPRESS SEMICONDUCTOR D3 D3-109 亿可能源科技(上海)有限公司 亿可能源科技(上海)有限公司 EQuota Energy Technology (Shanghai) Co., Ltd. EQUOTA ENERGY TECHNOLOGY (SHANGHAI) CO., LTD. D3 D3-110 上海展湾信息科技有限公司 上海展湾信息科技有限公司 D3 D3-111 上海纽酷信息科技有限公司 上海纽酷信息科技有限公司 NEW CORE Technology Co., Ltd NEW CORE TECHNOLOGY CO., LTD D3 D3-112 重庆伏特猫科技有限公司 重庆伏特猫科技有限公司 Chongqing Voltmao Technology Co., Ltd. CHONGQING VOLTMAO TECHNOLOGY CO., LTD. D3 D3-113 杭州雅观科技有限公司 杭州雅观科技有限公司 Hangzhou Yaguan Technology Co., Ltd. HANGZHOU YAGUAN TECHNOLOGY CO., LTD. D3 D3-114 浙江辉驿网络科技有限公司 浙江辉驿网络科技有限公司 Zhejiang Huiyi Technology Co., Ltd ZHEJIANG HUIYI TECHNOLOGY CO., LTD D3 D3-115 杭州鸿雁电器有限公司 杭州鸿雁电器有限公司 HANGZHOU HONYAR ELECTRICAL CO., LTD HANGZHOU HONYAR ELECTRICAL CO., LTD D3 D3-116 杭州筑家易网络科技股份有限公司 杭州筑家易电子商务有限公司 D3 D3-117 上海通信服务 中国通信服务上海公司 CHINA COMSERVICE SHANGHAI CORPORATION CHINA COMSERVICE SHANGHAI CORPORATION D3 D3-118 LifeSmart云起 云起LifeSmart LifeSmart LifeSmart D3 D3-119 杭州赛客网络科技有限公司 杭州赛客网络科技有限公司 Hangzhou SIG Network Technology Co., Ltd HANGZHOU SIG NETWORK TECHNOLOGY CO., LTD D3 D3-120 福建格通电子信息科技有限公司 福建格通电子信息科技有限公司 GRIDLINK GRIDLINK D3 D3-201 深圳市有方科技股份有限公司 深圳市有方科技股份有限公司 Neoway Technology Co., Ltd. NEOWAY TECHNOLOGY CO., LTD. D3 D3-202 联发科技股份有限公司 联发科技股份有限公司 MEDIATEK INC. MEDIATEK INC. D3 D3-203 澜起、汉风、深圳市唯传 深圳市唯传科技有限公司 上海汉枫电子科技有限公司 Shanghai High-Flying Electronics Technology Co., Ltd SHANGHAI HIGH-FLYING ELECTRONICS TECHNOLOGY CO., LTD D3 D3-204 南方硅谷、深圳市瑞科慧联、上海域格信息、瑞兴恒方 深圳市瑞科慧联科技有限公司 Shenzhen RAKwireless Technology Co., Ltd SHENZHEN RAKWIRELESS TECHNOLOGY CO., LTD D3 D3-205 Realtek 瑞昱半导体股份有限公司 Realtek Semiconductor Corp. REALTEK SEMICONDUCTOR CORP. D3 D3-206 上海移远通信技术股份有限公司 上海移远通信技术股份有限公司 Quectel Wireless Solutions Co., Ltd. QUECTEL WIRELESS SOLUTIONS CO., LTD. D3 D3-207 恩智浦半导体 恩智浦(中国)管理有限公司 NXP (CHINA) MANAGEMENT LTD. NXP (CHINA) MANAGEMENT LTD. D3 D3-208 英飞凌 英飞凌 Infineon Technologies INFINEON TECHNOLOGIES D3 D3-209 杭州万科产城发展有限公司 杭州万科产城发展有限公司 HangZhou Vanke Industry And Urban Development Co. Ltd HANGZHOU VANKE INDUSTRY AND URBAN DEVELOPMENT CO. LTD D3 D3-210 浙江安达系统工程有限公司 浙江安达系统工程有限公司 ZHEJIANG ANDA SYSTEM ENGINEERING CO., LTD. ZHEJIANG ANDA SYSTEM ENGINEERING CO., LTD. D3 D3-211 上海瑞眼科技有限公司 上海瑞眼科技有限公司 RAYEYE RAYEYE D3 D3-212 杭州基础创新科技有限公司 杭州基础创新科技有限公司 Hangzhou Basic Innovation Technology Co., Ltd. HANGZHOU BASIC INNOVATION TECHNOLOGY CO., LTD. D3 D3-213 蘑菇物联、泰华智慧、深圳同洲 深圳市同洲电子股份有限公司 蘑菇物联技术(深圳)有限公司 泰华智慧产业集团股份有限公司 SHENZHEN COSHIP ELECTRONICS CO., LTD Mogulinker Technology (Shenzhen) Co., Ltd SHENZHEN COSHIP ELECTRONICS CO., LTD MOGULINKER TECHNOLOGY (SHENZHEN) CO., LTD D3 D3-214 移柯通信、艾矽科技、厦门四信 上海移柯通信技术股份有限公司 厦门四信通信科技有限公司 深圳市艾矽科技有限公司 SHANGHAI MOBILETEK COMMINICATION LTD Xiamen Four-Faith Communication Technology Co., Ltd SHANGHAI MOBILETEK COMMINICATION LTD XIAMEN FOUR-FAITH COMMUNICATION TECHNOLOGY CO., LTD D3 D3-215 上海庆科信息技术有限公司 上海庆科信息技术有限公司 Shanghai MXCHIP Information Technology Co., Ltd. SHANGHAI MXCHIP INFORMATION TECHNOLOGY CO., LTD. D3 D3-216 上海艾拉比智能科技有限公司 上海艾拉比智能科技有限公司 Abup Technology Co., Ltd ABUP TECHNOLOGY CO., LTD D3 D3-217 上海绿立方农业发展有限公司 上海绿立方农业发展有限公司 Shanghai Leafa Agriculture Development Co., Ltd. SHANGHAI LEAFA AGRICULTURE DEVELOPMENT CO., LTD. D3 D3-218 固安京蓝云科技 京蓝云智物联网技术有限公司 Kingland Cloud Technology Co., Ltd. KINGLAND CLOUD TECHNOLOGY CO., LTD. D3 D3-219 知晓(北京)通信科技有限公司 知晓(北京)通信科技有限公司 GOVGO(Beijing) Communication Technology Co., Ltd GOVGO(BEIJING) COMMUNICATION TECHNOLOGY CO., LTD D3 D3-220 江苏深农智能科技有限公司 江苏深农智能科技有限公司 Jiangsu Deep Agriculture AI. Co., Ltd. JIANGSU DEEP AGRICULTURE AI. CO., LTD. D3 D3-221 青岛罗博数码科技有限公司 青岛罗博数码科技有限公司 ROBOTPEN ROBOTPEN D3 D3-222 上海磐度信息科技有限公司 上海磐度信息科技有限公司 Pendo Technology China Corporation PENDO TECHNOLOGY CHINA CORPORATION D3 D3-223 武汉天喻教育科技有限公司 武汉天喻教育科技有限公司 WuHan TianYu Education Tech Co., Ltd. WUHAN TIANYU EDUCATION TECH CO., LTD. D3 D3-224 广东天波教育科技有限公司 广东天波教育科技有限公司 Telpo Education Co.,ltd. TELPO EDUCATION CO.,LTD. D3 D3-301 小舞台 小舞台 Stage STAGE D3 D3-302 开发者生态 开发者生态 Developer Ecosystem DEVELOPER ECOSYSTEM D3 D3-303 厦门大洋通信有限公司 厦门大洋通信有限公司 Netvox Technology Co., Ltd(Xiamen) NETVOX TECHNOLOGY CO., LTD(XIAMEN) D3 D3-304 杭州罗万信息科技有限公司 杭州罗万信息科技有限公司 Hangzhou Lowan Information Technology Co., Ltd HANGZHOU LOWAN INFORMATION TECHNOLOGY CO., LTD D3 D3-305 浙江利尔达物联网技术有限公司 利尔达科技集团股份有限公司 Lierda Science & Technology Group Co., Ltd. LIERDA SCIENCE & TECHNOLOGY GROUP CO., LTD. D3 D3-306 杭州东信实业有限公司 杭州东信实业有限公司 Hangzhou Eastcom Industrial Co., Ltd. HANGZHOU EASTCOM INDUSTRIAL CO., LTD. D3 D3-307 北京深瞐科技有限公司 北京深瞐科技有限公司 Beijing Seemmo Technology Co., Ltd BEIJING SEEMMO TECHNOLOGY CO., LTD D3 D3-308 江苏智慧新吴信息科技有限公司 江苏智慧新吴信息科技有限公司 Jiangsu Ixinwu infotech CO., LTD JIANGSU IXINWU INFOTECH CO., LTD D3 D3-309 智能城市 智能城市 Smart City SMART CITY D3 D3-310 拓普索尔、枫昱能源科技、圣普信息科技、展为只能 上海展为智能技术股份有限公司 Shanghai Zhanway Intelligence Technology Inc SHANGHAI ZHANWAY INTELLIGENCE TECHNOLOGY INC D3 D3-311 Intel Intel Intel INTEL D3 D3-312 中卡智能、魔点科技、品元智造、提点信息 湖北品元智造电子科技有限公司 上海中卡智能卡有限公司 杭州魔点科技有限公司 苏州提点信息科技有限公司 HUBEI PLITZ INTELLIGENT MANUFACTURING CO., LTD HUBEI PLITZ INTELLIGENT MANUFACTURING CO., LTD 户外 X1 深圳市鲸仓科技有限公司 深圳市鲸仓科技有限公司 Shenzhen Whalehouse Technology Company Limited SHENZHEN WHALEHOUSE TECHNOLOGY COMPANY LIMITED 户外 X2 松下家电(中国)有限公司 松下家电(中国)有限公司 Panasonic Appliance Corporation of China PANASONIC APPLIANCE CORPORATION OF CHINA 户外 X3 佳明 上海佳明航电企业管理有限公司 Garmin GARMIN 户外 X5 小鹏汽车 广州小鹏汽车科技有限公司 Guangzhou Xiaopeng Motors Technology Co., Ltd. GUANGZHOU XIAOPENG MOTORS TECHNOLOGY CO., LTD. 户外 X6 奕乾科技 上海奕乾文化传播有限公司 SHANGHAI YIQIAN COMMUNICATIONS CO., LTD. SHANGHAI YIQIAN COMMUNICATIONS CO., LTD. 户外 X7 冲浪区 云·冲浪 Cloud·Surfing CLOUD·SURFING 户外 X8 优锘 北京优锘科技有限公司 uinnova UINNOVA 户外 X9 万国数据 万国数据服务有限公司 GDS Services Ltd. GDS SERVICES LTD. 户外 X10 NVIDIA NVIDIA技术服务(北京)有限公司 NVIDIA Technical Service (Beijing) Co., Ltd. NVIDIA TECHNICAL SERVICE (BEIJING) CO., LTD. 户外 X11 专有云,阿里云 阿里云专有云 Alibaba Cloud Apsara Stack ALIBABA CLOUD APSARA STACK
MaxCompute 技术公开课第二季已经结束,共进行了5次大数据技术直播,有近6000名用户、大数据专家、技术牛人、大数据爱好者参与其中。我们为大家整理了一下直播的PPT和视频内容,方便大家随时学习。以下是直播干货: 主题:MaxCompute客户端 - odpscmd操作使用: 分享嘉宾: 曲宁 阿里巴巴计算平台 产品专家 PPT下载地址:https://yq.aliyun.com/download/2943 视频回顾:https://yq.aliyun.com/video/play/1568 主题:MaxCompute多租户数据安全体系介绍及实践 分享嘉宾:四相 阿里巴巴计算平台 产品专家 PPT下载地址:https://yq.aliyun.com/download/2899 视频回顾:https://yq.aliyun.com/video/play/1569 主题:MaxCompute Logview参数详解和问题排查 分享嘉宾:云花 阿里巴巴计算平台 产品专家 PPT下载地址:https://yq.aliyun.com/download/2944 视频回顾:https://yq.aliyun.com/video/play/1570 主题:DataWorks调度配置最佳实践 分享嘉宾:祎休 阿里巴巴计算平台 产品专家 PPT下载地址:https://yq.aliyun.com/download/2942 视频回顾:https://yq.aliyun.com/video/play/1571 主题:自建Hadoop数据如何托管到MaxCompute 分享嘉宾: 结网 阿里云数据技术专家 PPT下载地址:https://yq.aliyun.com/download/2952 视频回顾:https://yq.aliyun.com/video/play/1572
8月17日,HBaseCon亚洲2018峰会在北京歌华开元大酒店盛大开幕。作为Apache基金会旗下HBase社区的顶级用户峰会,HBaseCon大会是Apache HBase™官方从2012年开始发起和延续至今的技术会议,先后在美国加州、日本东京和中国深圳等地举办,得到了Google、Facebook、雅虎和阿里巴巴等众多全球顶级互联网公司大力支持。 此次大会期间,云栖社区为独家直播合作方,以下是本次大会的直播回顾与PPT下载地址! HBaseConAsia2018 was successfully held on Aug. 17th at Gehua New Century Hotel, Beijing, China. It's the 2nd HBaseCon held in Asia, officially authorized by The Apache Software Foundation, supported by HBase open source community, and hosted by Alibaba. PPT slides and video recording of all talks could be found and downloaded following below links. Please check and enjoy! Keynote-1 标题 (Title):HBase Current State and Future : Community View 演讲者 (Speakers):Michael Stack,Duo Zhang and Yu Li 嘉宾简介 (Speaker Introduction): Michael Stack, HBase资深PMC, HBase项目发起者之一, Cloudera HBase负责人 Michael Stack (Cloudera), Lead of HBase project, HBase PMC member Duo Zhang(张铎), HBase PMC, 小米HBase团队负责人 Duo Zhang (Xiaomi), HBase PMC member Yu Li(李钰/绝顶), HBase PMC, 阿里巴巴高级技术专家 Yu Li (Alibaba), HBase PMC member PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2916 视频回顾 (Video):https://yq.aliyun.com/video/play/1544 Keynote-2 标题 (Title):Recent Development of HBase in Alibaba and Cloud 演讲者 (Speakers):Chunhui Shen and Long Cao 嘉宾简介 (Speaker Introduction): Chunhui Shen(沈春辉/天梧), HBase PMC, 阿里巴巴资深技术专家 Chunhui Shen (Alibaba), HBase PMC member Long Cao(曹龙/封神), 阿里巴巴高级技术专家,阿里云hbase技术负责人 Long Cao (Alibaba), Lead of Alicloud HBase team PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2917 视频回顾 (Video):https://yq.aliyun.com/video/play/1546 Track1-1 标题 (Title):Use CCSMap to improve HBase YGC time 演讲者 (Speakers):Chance Li and Lijin Bin 嘉宾简介 (Speaker Introduction): Chance Li, 李强(强思), 阿里巴巴技术专家 Chance Li (Alibaba), Technical Expert Lijin Bin, 宾利金(天照), 阿里巴巴技术专家 Lijin Bin (Alibaba), Technical Expert PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2918 视频地址 (Video):https://yq.aliyun.com/video/play/1547 Track1-2 标题 (Title):WALLess HBase with persistent memory devices 演讲者 (Speakers):Ramkrishna Vasudevan and Anoop Sam John 嘉宾简介 (Speaker Introduction): Anoop Sam John, HBase资深PMC, Intel HBase资深专家 Anoop Sam John (Intel), HBase PMC member Ramkrishna Vasudevan, HBase PMC, Intel HBase资深专家 Ramkrishna Vasudevan (Intel), HBase PMC member PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2919 视频地址 (Video):https://yq.aliyun.com/video/play/1548 Track1-3 标题 (Title):HBase at Xiaomi 演讲者 (Speakers):GuangHao Zhang 嘉宾简介 (Speaker Introduction): GuangHao Zhang, 张洸豪,HBase PMC, 小米人工智能与云平台研发工程师 GuangHao Zhang (Xiaomi), HBase PMC member PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2920 视频地址 (Video):https://yq.aliyun.com/video/play/1549 Track1-4 标题 (Title):HBase at DiDi 演讲者 (Speakers):JingYi Yao 嘉宾简介 (Speaker Introduction): JingYi Yao, 姚婧怡, 滴滴HBase负责人 JingYi Yao (DiDi), Lead of Didi HBase Team PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2921 视频地址 (Video):https://yq.aliyun.com/video/play/1550 Track1-5 标题 (Title):Improving HBase reliability at Pinterest with geo-‐replication and efficient backup 演讲者 (Speakers):Chenji Pan and Lianghong Xu 嘉宾简介 (Speaker Introduction): Chenji Pan, Software Engineer, Storage&Caching, Pinterest Lianghong Xu, Software Engineer, Storage&Caching, Pinterest PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2922 视频地址 (Video):https://yq.aliyun.com/video/play/1551 Track1-6 标题 (Title):Separating hot-cold data into heterogeneous storage based on layered compaction 演讲者 (Speakers):WenLong Yang 嘉宾简介 (Speaker Introduction): WenLong Yang, 杨文龙(正研), HBase committer, 阿里巴巴技术专家 Allan Yang (Alibaba), HBase committer PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2923 视频地址 (Video):https://yq.aliyun.com/video/play/1552 Track1-7 标题 (Title):HDFS optimization for HBase at XiaoMi 演讲者 (Speakers):Gang Xie, Yingchao Zhou and Chen Zhang 嘉宾简介 (Speaker Introduction): Gang Xie, 小米人工智能与云平台研发工程师 Gang Xie (Xiaomi), Storage Expert Yingchao Zhou and Chen Zhang,Solution & Ecology PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2924 视频地址 (Video):https://yq.aliyun.com/video/play/1553 Track2-1 标题 (Title):Kerberos—based Big Data Security Solution and Practice in Alibaba Cloud HBase 演讲者 (Speakers):Jiajia Li and Chao Guo 嘉宾简介 (Speaker Introduction): Jiajia Li, 李佳佳, Apache Kerby/Directory PMC, Intel大数据研发工程师 Jiajia Li (Intel), Apache Kerby/Directory PMC member Chao Guo, 郭超(玄陵),阿里云高级开发工程师 Chao Guo (Alibaba), Advanced Developer at Alicloud PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2925 视频地址 (Video):https://yq.aliyun.com/video/play/1554 Track2-2 标题 (Title):Apache Kylin on HBase: extreme OLAP for big data 演讲者 (Speakers):ShaoFeng Shi 嘉宾简介 (Speaker Introduction): Shaofeng Shi, 史少锋,Apache Kylin committer & PMC, Kyligence 高级架构师 Shaofeng Shi (Kyligence), Apache Kylin committer & PMC, Principle Architect PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2926 视频地址 (Video):https://yq.aliyun.com/video/play/1555 Track2-3 标题 (Title):Bringing MySQL Compatibility to HBase using Database Virtualization 演讲者 (Speakers):Water Guo 嘉宾简介 (Speaker Introduction): Water Guo, Founder of AntsDB, Founder and CTO of BDI Systems Inc. PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2927 视频地址 (Video):https://yq.aliyun.com/video/play/1556 Track2-4 标题 (Title):HTAP DB—System : ApsaraDB HBase Phoenix and Spark 演讲者 (Speakers):Yun Zhang and Wei Li 嘉宾简介 (Speaker Introduction): Wei Li, 李伟(沐远), 阿里云技术专家 Wei Li (Alibaba), Technical Expert at Alicloud Yun Zhang, 张赟(瑾谦), 阿里云高级开发工程师 Yun Zhang (Alibaba), Advanced Developer at Alicloud PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2928 视频地址 (Video):https://yq.aliyun.com/video/play/1557 Track2-5 标题 (Title):JanusGraph—Distributed graph database with HBase 演讲者 (Speakers):XueMin Zhang 嘉宾简介 (Speaker Introduction): Xuemin Zhang, 张学敏, TalkingData数据中心数据工程团队负责人 Xuemin Zhang (TalkingData), Lead of TalkingData Data Engineering Team PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2929 视频地址 (Video):https://yq.aliyun.com/video/play/1558 Track2-6 标题 (Title):Scaling 30 TB's of Data lake with Apache HBase and Scala DSL at Production 演讲者 (Speakers):Chetankumar Jyestaram Khatri 嘉宾简介 (Speaker Introduction): Chetankumar Jyestaram Khatri, Lead Data Engineer, Accionlabs India PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2930 视频地址 (Video):https://yq.aliyun.com/video/play/1559 Track2-7 标题 (Title):A real—time cold backup solution for HBase with zero HBase modification,low latency and heterogeneous storage 演讲者 (Speakers):QingYi Meng 嘉宾简介 (Speaker Introduction): QingYi Meng, 孟庆义(天引), 阿里巴巴技术专家 QingYi Meng (Alibaba), Technical Expert PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2931 视频地址 (Video):https://yq.aliyun.com/video/play/1560 Track3-1 标题 (Title):Serving billions of queries in millisecond latency 演讲者 (Speakers):Biju Nair 嘉宾简介 (Speaker Introduction): Biju Nair (BloomBerg), Experienced Software Engineer PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2932 视频地址 (Video):https://yq.aliyun.com/video/play/1561 Track3-2 标题 (Title):HBase at China Telecom 演讲者 (Speakers):Chen Ze 嘉宾简介 (Speaker Introduction): Chen Ze, 中国电信 Chen Ze (China Telecom) PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2933 视频地址 (Video):https://yq.aliyun.com/video/play/1562 Track3-3 标题 (Title):HBase Practice In China Life Insurance 演讲者 (Speakers):Zheng Fan 嘉宾简介 (Speaker Introduction): Zheng Fan,中国人寿 Zheng Fan (China Life Insurance) PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2934 视频地址 (Video):https://yq.aliyun.com/video/play/1563 Track3-4 标题 (Title):HBase and OpenTSDB practice at Huawei 演讲者 (Speakers):Pankaj Kumar, Wei Zhi and Chaoqiang Zhong 嘉宾简介 (Speaker Introduction): Pankaj Kumar, 华为 Pankaj Kuma (Huawei), Technical Project Leader Wei Zhi, 智伟, 华为FusionInsight客户运维技术负责人 Wei Zhi (Huawei), Lead of FusionInsight SRE Team YiJun Guo, 郭益君, 华为云表格存储服务CloudTable架构师 YiJun Guo (Huawei), Architect of Huawei CloudTable PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2935 视频地址 (Video):https://yq.aliyun.com/video/play/1564 Track3-5 标题 (Title):HBase Practice at Lianjia 演讲者 (Speakers):GuoXian Zhao 嘉宾简介 (Speaker Introduction): GuoXian Zhao, 赵国贤,贝壳大数据架构团队负责人 GuoXian Zhao (Ke.com), Lead of BigData Architect Team PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2936 视频地址 (Video):https://yq.aliyun.com/video/play/1565 Track3-6 标题 (Title):HBase at Meituan 演讲者 (Speakers):Chen Xu 嘉宾简介 (Speaker Introduction): Chen Xu,陈旭,美团存储开发专家 Chen Xu (MeiTuan), Technical Expert on Storage PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2937 视频地址 (Video):https://yq.aliyun.com/video/play/1566 Track3-7 标题 (Title):The Application of Hbase in New Energy Vehicle Monitoring System 演讲者 (Speakers):Yan Yu 嘉宾简介 (Speaker Introduction): Yan Yu, 重庆博尼施科技有限公司CTO Yan Yu (Brunish), CTO of Chongqing Brunish Corporation PPT下载地址 (PPT slides):https://yq.aliyun.com/download/2938 视频地址 (Video):https://yq.aliyun.com/video/play/1567
万亿车联网产业正在爆发,传统IDC架构面临淘汰,然而云端部署还有哪些坑?如何从零开始搭建高并发、高可用、高容量的车联网云上架构?提高研发效率、降低成本又应该怎么做? “双11”千亿并发背后的男人——阿里云技术总监杨旭,将联合资深解决方案工程师周克伟,全面拆解车联网上云最佳实践。 课程完整视频请戳这里 课程PPT免费下载请戳这里 课程课纲: 一、车联网行业特性讲解 二、传统IDC架构介绍及技术详解 1、业务架构 2、应用架构 三、云上对标架构及技术详解 1、云上对标架构介绍 2、数据迁移策略 四、云上部署实践详解 1、数据库准备和配置 2、基础服务准备和配置 3、应用部署和配置 4、负载均衡配置 5、测试验证 6、流量割接 7、安全加固 五、云上关键业务测试及性能调优 1、负载均衡选型及性能指标 2、ECS选型及性能测试 3、数据库RDS测试及调优 4、Elasticsearch性能测试及调优 5、云数据库 HBase性能测试及调优 6、HiTSDB性能测试及调优 六、运维管控&DevOps 1、自动扩容/缩容 2、自动发布 3、监控报警 4、日志服务 5、数据大屏 6、企业运维管理 七、车联网云市场推荐 1、基础软件市场 2、物联网市场 3、API市场 八、使用阿里云带来的价值 1、节约成本 2、弹性扩展 3、安全稳定 4、快速运维 5、解决瓶颈和痛点 6、站在巨人的肩膀上起跳 欢迎加入阿里云技术总监系列课钉钉交流群,与大牛零距离互动!
近日,阿里中间件(Aliware)联合阿里巴巴技术协会,在深圳举办了Apache RocketMQ毕业后的第二次线下Meetup。当天现场的700名和线上三个直播平台的开源技术爱好者一起,与活动现场的Committer及Contributor就RocketMQ的CI/CD、最终一致性事务、金融领域的最佳实践、流计算生态、以及开源社区生态建设等话题展开深入探讨。本次活动不仅促进了开源社区的发展壮大,也推进了消息技术及规范的进一步发展。据统计,报名活动页面浏览量58000次,报名人数达2034人,线上3大平台直播,总PV超过17000,UV接近7500。 以下是直播PPT,欢迎点击收藏。 1.Dubbo开源现状与2.7规划 分享嘉宾:阿里巴巴中间件技术专家陈志轩 PPT下载地址:https://yq.aliyun.com/download/2900 2. Secret of Eventual Consistency in Apache RocketMQ, with No Budget 分享嘉宾:Apache RocketMQ技术专家杜恒 PPT下载地址:https://yq.aliyun.com/download/2901 3. Apache RocketMQ 持续集成 分享嘉宾:Apache RocketMQ Contributor李维军 PPT下载地址:https://yq.aliyun.com/download/2902 4.Nacos 启航,发布第一个版本, 云原生时代助力用户微服务平台建设 分享嘉宾:中间件高级技术专家慕义 PPT下载地址:https://yq.aliyun.com/download/2903 5.Stream Processing with Apache RocketMQ 分享嘉宾: Apache Stream PMC Member/Committer王鑫 PPT下载地址:https://yq.aliyun.com/download/2904 6.Sentinel分布式系统下的流量防卫兵 分享嘉宾:中间件高级技术专家子矜 PPT下载地址:https://yq.aliyun.com/download/2905 7.乐信的微服务之路 分享嘉宾:乐信集团架构中心总监康彬 PPT下载地址:https://yq.aliyun.com/download/2906 8.Building a Messaging Service with Apache RocketMQ 分享嘉宾: Apache RocketMQ Contributor 陈广胜 PPT下载地址:https://yq.aliyun.com/download/2907
追求多端效率和native性能的团队,看过来! 无论是初创团队还是已有规模的公司,移动端的开发效率都是技术团队的关注重点。 免费、开源的Flutter框架不仅能够大幅提升跨栈开发的效率,其构筑的页面性能也接近native,受到了开发者的广泛喜爱。 本周四(8月2日)晚7点,云栖君邀请到阿里高级无线开发专家、闲鱼客户端团队负责人于佳(花名宗心),针对客户端团队面临的业务和技术挑战,向社区粉丝们分享自己的思考和经验,特别是Flutter框架的应用实践,干货满满! 本场讲座将在云栖社区进行视频直播。 直播间地址:https://yq.aliyun.com/webinar/play/482 同时,直播期间,参与直播间互动将有机会获得30元阿里云无门槛代金券(限量5张),还可以在社群和专家零距离互动,马上报名吧! 演讲嘉宾 阿里高级无线开发专家 12年毕业加入阿里巴巴,现任闲鱼客户端团队负责人,多年来见证了集团业务无线化的高速发展,并参与和推动了集团移动基础设施的建设。目前专注flutter在闲鱼端侧的应用,探索下一代无线架构的可能性。 直播内容 本次分享的主要内容是在闲鱼客户端团队面临的业务和技术挑战下的思考和整个落地过程,通过困境/思考/落地/展望,四个环节,分享使用flutter的新架构下带来的优势和挑战,供更多对flutter感兴趣的同学进行参考和交流,期间也会分享一些对团队和个人成长的思考。
Elasticsearch Meetup系列第二期活动中,7位来自阿里巴巴、eBay、沪江、携程等知名企业的技术大咖现场分享了集群管理、架构、定制开发、业务支撑的使用经验。以下是此次活动的直播整理。 《利用Elastic Stack快速搭建SIEM系统》 分享嘉宾:Elastic架构师 Monash大学计算机硕士 吴斌 视频链接:https://yq.aliyun.com/video/play/1526 PDF下载:https://yq.aliyun.com/download/2884 《Elasticsearch diagnose and index lifecycle management services》 分享嘉宾:eBay 软件工程师 丁旻奕、王佩 视频链接:https://yq.aliyun.com/video/play/1527 PDF下载:https://yq.aliyun.com/download/2886 《EYou—阿里云Elasticsearch智能优化运维工具分享》 分享嘉宾:阿里巴巴Elasticsearch工程师张家杰 视频链接:https://yq.aliyun.com/video/play/1528 PDF下载:https://yq.aliyun.com/download/2887 《ES Cross Cluster Search生产实践》 分享嘉宾:携程旅行网 技术保障部系统研发专家刘佳 视频链接:https://yq.aliyun.com/video/play/1529 PDF下载:https://yq.aliyun.com/download/2888 《B站日志系统的演进之路》 分享嘉宾:Bilibili 资深运维研发工程师王翔宇 视频链接:https://yq.aliyun.com/video/play/1530 PDF下载:https://yq.aliyun.com/download/2889 《基于 Elasticsearch 电商搜索》 分享嘉宾:沪江教育科技有限公司 资深架构师曹林华 视频链接:https://yq.aliyun.com/video/play/1531 PDF下载:https://yq.aliyun.com/download/2890 《Elasticsearch 在企业协作服务中的应用实践》 分享嘉宾:Teambition,高级后端工程师宓文捷 视频链接:https://yq.aliyun.com/video/play/1532 PDF下载:https://yq.aliyun.com/download/2891 2017年9月,阿里云基于开源Elasticsearch及商业版X-Pack插件,提供云上ELK服务,同时阿里云ES技术人员会分享解决云上业务痛点的案例实践,敬请期待!了解产品更多详情 https://data.aliyun.com/product/elasticsearch
什么是Mock Mock顾名思义是一种模拟。通常利用相同的接口来模拟出一个对象以代替真实对象,这样能有效隔离外部依赖,便于测试。对于前端开发,Mock作为重要一环,能带来很多好处: 前后端并行开发 模拟各种响应值,便于测试 可及早发现一些极端响应值下的页面布局问题等 背景 前端开发可简单分为三个阶段:并行开发阶段、联调阶段和测试阶段。现在的前端项目大多为前后端分离,在开发、联调阶段不可避免要面对数据源的问题。 在联调阶段,各个环境已有真实数据,方便本地调试,我们一般会将接口指向真实数据源。如果有跨域限制的话,可利用Charles、Fiddler等调试代理工具来解决,也可以起一个本地Server: const express = require('express'); const proxy = require('http-proxy-middleware'); const app = express(); app.use('/api', proxy({ target: 'your-api-url', changeOrigin: true })); app.listen(3000); 如果还处在并行开发阶段,那我们就需要Mock数据,一般有以下几种常用方式: 1、拦截Ajax、Fetch请求 缺点:前端混入脏代码;无法有效模拟网络情况。 2、本地Mock Server 缺点:接口众多,创建和修改成本高。 3、YApi、Easy Mock的接口管理平台 缺点:灵活性不够。比如一些配置信息分散在各个接口,没法集中管理,修改成本高。 本文以笔者接触较多的Swagger为例,从一个侧面改善本地Mock Server需要不断创建接口的缺点。打开后端提供的Swagger UI地址的Network,发现有个api-docs文件。 这个JSON文件包含接口、请求方法、响应格式等信息。可以想见解析这个文件并不难,唯一比较麻烦的可能就是响应值的解析和类型转换。如果能适时同步数据到本地Mock Server,能省去不少乏味的体力活。 Talk is cheap 1、目标 接口路径和Mock目录相对应,便于查找、修改 以请求方法为文件名,一个方法对应一个文件,减少多人编辑冲突 使用Mock.js包装响应值,易于模拟一些极端状况 2、解析JSON文件 前面我们提到解析JSON文件的难点主要在响应值类型的转换,这边我们利用Easy Mock的一个解析模块来做这件事情。 const swaggerParserMock = require('swagger-parser-mock'); const synchronizeSwagger = { init({ url, blacklist, outputPath }) { this.url = url; this.blacklist = blacklist; this.outputPath = outputPath; this.parse(); }, async parse() { const { paths } = await swaggerParserMock(this.url); this.generate(paths); console.log(paths); } } synchronizeSwagger.init({ // Swagger api-docs地址 "url": "your-api-docs-url", // 输出目录 "outputPath": "./routes", // 黑名单,跳过一些不需要同步的api "blacklist": [] }); 打印paths信息,格式大致如下: "/path/foo": { "get": { "summary": "bar", "responses": { "200": { "example": "'@string'" // 模块为我们做的类型转化和Mock.js包装。 } } }, "post": { "summary": "baz", "responses": { "200": { "example": "'@string'" } } } } 3、遍历接口。我们可以加入黑名单,过滤掉一些对前端没有用处的接口。减少干扰,提高可维护性。 const fs = require('fs'); const { join } = require('path'); const { promisify } = require('util'); const mkdirp = require('mkdirp'); const writeFile = promisify(fs.writeFile); const mkdir = promisify(mkdirp); const synchronizeSwagger = { // 遍历api path信息 traverse(paths) { for (let path in paths) { if (this.blacklist.includes(path)) { continue; } for (let method in paths[path]) { const pathInfo = paths[path][method]; if (!pathInfo['responses']['200']) { continue; } this.generate(path, method, pathInfo); } } } } 4、生成Mock文件,可以添加注释等信息。 const synchronizeSwagger = { // 生成mock文件 async generate(path, method, pathInfo) { const outputPath = join(__dirname, this.outputPath, path); const { summary, responses: { 200: responseOK }, } = pathInfo; try { // 生成目录 await mkdir(outputPath); const example = responseOK['example']; // 生成文件内容 const template = this.generateTemplate({ summary, example, method, path, }); // 生成文件, 已存在的跳过,避免覆盖本地以及编辑的文件 const fPath = join(outputPath, `${method}.js`); await writeFile(fPath, template, { flag: 'wx' }); console.log(`增加Mock文件:${fPath}`); } catch (error) { /* eslint-disable no-empty */ } }, generateTemplate({ summary, example, method, path }) { // prettier-ignore // api path中的{petId}形式改为:petId return `/** ${summary} **/ const Mock = require("mockjs"); module.exports = function (app) { app.${method}('/api${path.replace(/\{([^}]*)\}/g, ":$1")}', (req, res) => { res.json(Mock.mock(${example})); }); };`; }, } 5、启动Mock Server 以express为例,利用require动态特征我们来创建路由,映射到刚才创建的接口文件。 const fs = require('fs'); const join = require('path').join; const express = require('express'); const app = express(); const port = process.env.PORT || 3000; app.listen(port, function() { console.log(`server is listening ${port}`); }); function scan(path, app) { const files = fs.readdirSync(path); for (let i = 0; i < files.length; i++) { const fpath = join(path, files[i]); const stats = fs.statSync(fpath); if (stats.isDirectory()) { scan(fpath, app); } if (stats.isFile()) { require(fpath)(app); } } } scan(join(__dirname, './routes'), app); 写在最后 至此我们就利用Swagger UI同步Mock数据,如果再加上cors、body-parser等Middleware,一个本地Mock Server基本成形。方便同步,我们将它加入npm scripts。 "scripts": { "ss": "node ./synchronizeSwagger.js" }, 执行npm run ss,就能生成相应的Mock数据和访问接口了。 原文发布时间为:2018年06月22日原文作者:朱子彦小朋友 本文来源: 掘金 如需转载请联系原作者
更多相关内容见博客 github.com/zhuanyongxi… 函数式编程中的pointfree的意思就是“无参”或“无值”,pointfree style是一种编程范式,也作tacit programming,就是“无参编程”的意思了。什么是“无参编程”? // 这就是有参的,因为有word var snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_'); // 这是pointfree var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase); 从另一个角度看,有参的函数的目的是得到一个数据,而pointfree的函数的目的是得到另一个函数。 所以,如下的方程,虽然也有参,也可以认为是pointfree的。 const titlesForYear = year => pipe( filter(publishedInYear(year)), map(book => book.title) ) 那这pointfree有什么用? 它可以让我们把注意力集中在函数上,参数命名的麻烦肯定是省了,代码也更简洁优雅。 需要注意的是,一个pointfree的函数可能是由众多非pointfree的函数组成的,也就是说底层的基础函数大都是有参的,pointfree体现在用基础函数组合而成的高级函数上。如果我们使用函数式编程的工具,如ramda,这些基础函数大都已经被写好了,这样我们去写pointfree的代码就很容易了。 什么是声明式编程?它区别于命令式编程 // 命令式 var words = []; for (i = 0; i < otherWords.length; i++) { words.push(otherWords[i].word); } // 声明式 var words = otherWords.map(function(ele){ return ele.word; }); 容易看出,命令式的代码,我们不但要去遍历,还要关注如何遍历。而声明式的就容易很多,可以节省我们的注意力,代码也更加简洁。 其他的命令式的写法有:使用ifelse进行的条件判断,使用算数运算符进行的算数运算,使用比较运算符进行的比较运算和使用逻辑运算符进行的逻辑运算。 至于那些说“虽然如此,但使用命令式循环速度要快很多”的人,我建议你们先去学学 JIT 优化代码的相关知识。这里有一个非常棒的视频,可能会对你有帮助。 需要注意的是,要实现这种声明式的编程,首先我们要有这个map方法,这一点与pointfree相同,都是需要我们先对常用的操作做一次封装,而这些常用的操作本身还是命令式的。 pointfree的声明式代码是函数式编程应该有的样子。 最后用一个来自Scott Sauyet的文章《Favoring Curry》中的例子,使用的函数式工具是ramda。下面的代码不需要一句一句的看,大概体会一下就可以了。 一组JSON数据 var data = { result: "SUCCESS", interfaceVersion: "1.0.3", requested: "10/17/2013 15:31:20", lastUpdated: "10/16/2013 10:52:39", tasks: [ {id: 104, complete: false, priority: "high", dueDate: "2013-11-29", username: "Scott", title: "Do something", created: "9/22/2013"}, {id: 105, complete: false, priority: "medium", dueDate: "2013-11-22", username: "Lena", title: "Do something else", created: "9/22/2013"}, {id: 107, complete: true, priority: "high", dueDate: "2013-11-22", username: "Mike", title: "Fix the foo", created: "9/22/2013"}, {id: 108, complete: false, priority: "low", dueDate: "2013-11-15", username: "Punam", title: "Adjust the bar", created: "9/25/2013"}, {id: 110, complete: false, priority: "medium", dueDate: "2013-11-15", username: "Scott", title: "Rename everything", created: "10/2/2013"}, {id: 112, complete: true, priority: "high", dueDate: "2013-11-27", username: "Lena", title: "Alter all quuxes", created: "10/5/2013"} // , ... ] }; 需求是找到Scott所有未完成的任务,并按照到期日期升序排列。 正确的结果是 [ {id: 110, title: "Rename everything", dueDate: "2013-11-15", priority: "medium"}, {id: 104, title: "Do something", dueDate: "2013-11-29", priority: "high"} ] 命令式的代码如下 getIncompleteTaskSummaries = function(membername) { return fetchData() .then(function(data) { return data.tasks; }) .then(function(tasks) { var results = []; for (var i = 0, len = tasks.length; i < len; i++) { if (tasks[i].username == membername) { results.push(tasks[i]); } } return results; }) .then(function(tasks) { var results = []; for (var i = 0, len = tasks.length; i < len; i++) { if (!tasks[i].complete) { results.push(tasks[i]); } } return results; }) .then(function(tasks) { var results = [], task; for (var i = 0, len = tasks.length; i < len; i++) { task = tasks[i]; results.push({ id: task.id, dueDate: task.dueDate, title: task.title, priority: task.priority }) } return results; }) .then(function(tasks) { tasks.sort(function(first, second) { var a = first.dueDate, b = second.dueDate; return a < b ? -1 : a > b ? 1 : 0; }); return tasks; }); }; pointfree的代码 var getIncompleteTaskSummaries = function(membername) { return fetchData() .then(R.prop('tasks')) .then(R.filter(R.propEq('username', membername))) .then(R.reject(R.propEq('complete', true))) .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority']))) .then(R.sortBy(R.prop('dueDate'))); }; pointfree的声明式的代码 // 提取 tasks 属性 var SelectTasks = R.prop('tasks'); // 过滤出指定的用户 var filterMember = member => R.filter( R.propEq('username', member) ); // 排除已经完成的任务 var excludeCompletedTasks = R.reject(R.propEq('complete', true)); // 选取指定属性 var selectFields = R.map( R.pick(['id', 'dueDate', 'title', 'priority']) ); // 按照到期日期排序 var sortByDueDate = R.sortBy(R.prop('dueDate')); // 合成函数 var getIncompleteTaskSummaries = function(membername) { return fetchData().then( R.pipe( SelectTasks, filterMember(membername), excludeCompletedTasks, selectFields, sortByDueDate, ) ); }; 参考文章 Pointfree编程风格指南 Favoring Curry JS函数式编程指南 Tacit programming Thinking in Ramda: Pointfree Style Thinking in Ramda: Declarative Programming 原文发布时间为:2018年06月17日 原文作者:砖用冰西瓜 本文来源: 掘金 如需转载请联系原作者
上一篇文章介绍了Redux的数据中心,并分别讲解了数据中心为开发者提供的各种接口,了解到要触发状态的更新就需要调用dispatch方法来分发action。然而store提供的dispatch方法只能够用来分发特定格式的action。 如果我们想要更强大的功能怎么办?如果我们想要打印状态变化前后的日志?或者说想自定义某类方法来作为dispatch的参数?我们当然可以重写原来的dispatch方法,然而这并不优雅,维护成本相对较高。这篇文章想详细讲解一下,在函数式编程的背景下如何以中间件的方式优雅地扩展我们的dispatch方法。 1. 中间件 中间件这个概念存在于许多流行的Web框架中,可以把它想象成是请求/响应分发的中间层,用于对请求/响应做进一步的处理,而无需改变原有的代码逻辑。在node.js社区的KOA轻量级框架很出色地体现了这一点(当然它肯定不是第一个这样干的人)。koa本身只提供了最基础的请求/响应功能,如果想要更强大的功能(比如说日志,时间记录等功能)则需要自己添加相应的中间件。 Redux继承了这一理念,它把中间件应用到了dispatch方法的扩展中,让我们可以优雅地扩展dispatch方法,而不需要重写原有的dispatch方法,接下来我们好好体会一下它的精妙之处。 2. 中间件在Redux中的应用 在分析源码之前先来看看在Redux里面如何使用中间件,最关键的是applyMiddleware这个方法 import { createStore, applyMiddleware } from 'redux' // Add thunk import thunk from 'redux-thunk' import logger from 'redux-logger' const reducer = (state) => state let newCreateStore = applyMiddleware( logger, thunk )(createStore) // 创建store,数据中心 let store = newCreateStore(reducer) 其中thunk跟logger就是我们提到的中间件,依次把它们传入applyMiddleware函数中,就会返回一个新的函数,然后再用这个函数处理原始的createStore方法就会返回一个增强过的createStore方法。 另外,还记得createStore函数可以接收enhancer这个参数不?其实applyMiddleware这个方法经过调用后所得到的就是一个增强器。为此我们还可以这样调用createStore,并生成store。 .... let enhancer = applyMiddleware( logger, thunk ) let store = createStore(reducer, enhancer) 这种做法跟前面的扩展效果是一样的。 3. 源码分析 1) 中间件原理 在源码分析之前,先举个例子来看看一个简单的中间件内部应该是什么样子的,我分别定义middleware1,middleware2两个中间件(他们本质是高阶函数),并用来扩展originDispatch函数 let originDispatch = (...args) => { console.log(...args) } const middleware1 = (dispatch) => { return (...args) => { console.log('middleware1 before dispatch') dispatch(...args) console.log('middleware1 after dispatch') } } const middleware2 = (dispatch) => { return (...args) => { console.log('middleware2 before dispatch') dispatch(...args) console.log('middleware2 before dispatch') } } originDispatch = middleware2(middleware1(originDispatch)) originDispatch('ruby', 'cool', 'language') 结果如下 middleware2 before dispatch middleware1 before dispatch ruby cool language middleware1 after dispatch middleware2 before dispatch 是不是运行过程是不是有点像洋葱?我们可以使用中间件来对原有的方法进行增强,并返回一个增强了的方法,然后再用另一个中间件来对这个已经增强过的方法再进一步增强,模型示意图如下 2) compose--方法封链辅助函数 从上面的洋葱模型可以看出我们如果要增强一个方法,它的步骤如下 newFunc = f1(f2(func)) 可以简单地把f1,f2理解成我们需要各自定义的中间件函数,然而如果我们每次都要手动调用这些方法的话似乎并不太优雅,这个时候可以使用compose函数来完成这种事情。 compose在中文里面是组合的意思,Redux所定义的compose函数可以把函数的参数列表构造成依次调用的形式,并返回一个新的函数。它的源码如下 export default function compose(...funcs) { ... // 以上都是判断 return funcs.reduce((a, b) => (...args) => a(b(...args))) } 文字解释可能还不如流程图来得直观,下面简单地分析一下compose(f1, f2, f3, f4)的调用过程 a: f1, b: f2, return: (...args) => f1(f2(...args)) a: (...args) => f1(f2(...args)), b: f3, return: (...args) => f1(f2(f3(...args))) a: (...args) => f1(f2(f3(...args))), b: f4, return: (...args) => f1(f2(f3(f4(...args)))) 把这个方法应用在最初的例子中 > newfunc = compose(middleware2, middleware1)(originDispatch) [Function] > newfunc('node', 'good', 'languate') middleware2 before dispatch middleware1 before dispatch node good languate middleware1 after dispatch middleware2 before dispatch 结果是一样的。而且从这个例子还可以看出在compose函数的参数列表中越靠后的函数,在构造完成之后,距离原始函数就越近。 3) applyMiddleware--收集中间件,扩展createStore applyMiddleware.js这个文件里面就包含着它的源码 export default function applyMiddleware(...middlewares) { return createStore => (...args) => { const store = createStore(...args) let dispatch = () => { throw new Error( `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ) } const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } // #1 中间件应该接收store const chain = middlewares.map(middleware => middleware(middlewareAPI)) // #2 返回的函数用于处理dispatch函数 dispatch = compose(...chain)(store.dispatch) // #3 替换dispatch return { ...store, dispatch } } } 代码片段#2中我们传入compose函数里的所有函数都是用于扩展dispatch的,这些函数会被定义为这种形式(dispatch) => { return function(...args) { // do something before dispatch(...args) // do something after } } 这些函数会接收一个dispatch方法为参数,并返回一个增强的dispatch方法。然而我们需要编写的中间件却不仅如此,接下来再看看代码片段#1,以及相关的上下文逻辑 export default function applyMiddleware(...middlewares) { .... const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } // #1 中间件应该接收store const chain = middlewares.map(middleware => middleware(middlewareAPI)) // #2 返回的函数用于处理dispatch函数 dispatch = compose(...chain)(store.dispatch) ... } 我们通过map方法来处理applyMiddleware所接收的所有中间件,让他们分别以middlewareAPI这个对象作为参数调用过后会返回一个新的函数列表,而这个函数列表才是真正用来增强dispatch的。 middlewareAPI是仅仅包含了getState与dispatch这两个字段的对象,可以把它看成是一个精简版的store。因此我们需要编写的中间件应该是以store作为参数,并且返回一个用于增强dispatch方法的函数,而这个store我们只能够使用getState,dispatch这两个接口。听起来有点拗口,下面我们自行编写一个用于打印状态日志的中间件。 const Logger = (store) => (dispatch) => { return function(...args) { const wrappedDispatch = store.dispatch const getState = store.getState console.log('before dispatch', getState()) dispatch(...args) console.log('after dispatch', getState()) console.info(dispatch) console.info(wrappedDispatch) } } 其中dispatch与wrappedDispatch所指代的分发方法是不一样的。 dispatch是从参数中传入,如果当前中间件是第一个对dispatch方法进行增强的中间件,则当前的dispatch所指向的就是Redux原生定义的dispatch方法。如果当前中间件前面已经有若干中间件的调用,则当前dispatch所指代的是经过前面中间件加强过的新的dispatch方法。我们可以来验证一下 let enhancer = applyMiddleware( Logger, // 我们自己编写的Logger thunk ) dispatch的打印结果如下 ƒ (action) { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); } 可见,这是一个经过thunk中间件处理后返回的方法。 wrappedDispatch因为匿名函数(...args) => dispatch(...args)的关系,在applyMiddleware函数运行完成并返回之后,匿名函数内部的dispatch会始终指向经过我们增强的dispatch方法。也就是说在中间件里面执行store.dispatch就会始终运行最外层的被增强过的dispatch方法。模型如下 wrappedDispatch打印结果虽然看不出什么,但我也顺手贴一下吧 ƒ dispatch() { return _dispatch.apply(undefined, arguments); } 接下来,我们看applyMiddleware的返回值,它会返回一个新的函数,该函数会以createStore作为参数,处理过后返回一个新的createStore方法,它的模式大概是这样子 (createStore) => (...args) => { // createStore方法用来创建store return { ... getState: ... dispatch: ... } } 而在applyMiddleware中实际上我们只需要增强dispatch方法,为此我们只需要用新的dispatch方法来替换原来的便可。代码片段#3就是用新的dispatch方法取代原来store中的dispatch方法。.... return { ...store, dispatch } .... 4. 尾声 本章着重介绍了Redux中的中间件的原理,我们可以通过洋葱模型来增强dispatch函数。还可以通过compose方法构造调用链,使得我们的调用逻辑更加优雅。分析的过程中我还顺手编写了一个Logger中间件,用于打印action分发前后的状态,我们也可以根据自己的需求来编写属于自己的中间件。 Happy Coding and Writing!!! 原文发布时间为:2018年06月22日 原文作者:lanzhiheng 本文来源: 掘金 如需转载请联系原作者
记得4月新出了webpack4,这个月刚好没什么事情,用webpack4又重新去搭了一遍自己的项目。在搭项目的途中,忽然对webpack模块化之后的代码起了兴趣,于是想搞清楚我们引入的文件到底是怎么运行的。 1、基本版——单入口引入一个js文件 所谓的基本版,就是我只引入了一个test.js,代码只有一行var a = 1。打包之后,发现生成的文件main.js并没有多少代码,只有90行不到。 截取出真正执行的代码就更加少了,只有下面4行。我们接下去就从这几行代码中看下打包出来的文件的执行流程是怎么样的。 (function(modules) { //新建一个对象,记录导入了哪些模块 var installedModules = {}; // The require function 核心执行方法 function __webpack_require__(moduleId){/*内容暂时省略*/} // expose the modules object (__webpack_modules__) 记录传入的modules作为私有属性 __webpack_require__.m = modules; // expose the module cache 缓存对象,记录了导入哪些模块 __webpack_require__.c = installedModules; // Load entry module and return exports 默认将传入的数组第一个元素作为参数传入,这个s应该是start的意思了 return __webpack_require__(__webpack_require__.s = 0); })([(function(module, exports, __webpack_require__) { /* 0 */ var a = 1; /***/ }) /******/ ]) 首先很明显,整个文件是个自执行函数。传入了一个数组参数modules。 这个自执行函数内部一开始新建了一个对象installedModules,用来记录打包了哪些模块。 然后新建了函数__webpack_require__,可以说整个自执行函数最核心的就是__webpack_require__。__webpack_require__有许多私有属性,其中就有刚刚新建的installedModules。 最后自执行函数return了__webpack_require__,并传入了一个参数0。因为__webpack_require__的传参变量名称叫做moduleId,那么传参传进来的也就是*模块id**。所以我大胆猜测这个0可能是某个模块的id。 这时候我瞄到下面有一行注释/* 0 */。可以发现webpack会在每一个模块导入的时候,会在打包模块的顶部写上一个id的注释。那么刚才那个0就能解释了,就是我们引入的那个模块,由于是第一个模块,所以它的id是0。 那么当传入了moduleId之后,__webpack_require__内部发生了什么? __webpack_require__解析 function __webpack_require__(moduleId) { // Check if module is in cache // 检查缓存对象中是否有这个id,判断是否首次引入 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) 添加到.c缓存里面 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function 执行通过moduleId获取到的函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded // 表示module对象里面的模块加载了 module.l = true; // Return the exports of the module return module.exports; } 首先通过moduleId判断这个模块是否引入过。如果已经引入过的话,则直接返回。否则installedModules去记录下这次引入。这样子如果别的文件也要引入这个模块的话,避免去重复执行相同的代码。 然后通过modules[moduleId].call去执行了引入的JS文件。 看完这个函数之后,大家可以发现其实webpack打包之后的文件并没有什么很复杂的内容嘛。当然这很大一部分原因是因为我们的场景太简单了,那么接下来就增加一点复杂性。 2、升级版——单入口引入多个文件 接下来我修改一下webpack入口,单个入口同时下引入三个个文件 entry: [path.resolve(__dirname, '../src/test.js'),path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')], 三个文件的内容分别为var a = 1,var b = 2,var c = 3。接下来我们可以看看打包之后的代码 打包之后的文件main.js核心内容并没有发生变化,和上面一模一样。但是这个自执行函数传入的参数却发生了变化。 (function(modules) { /*这部分内容省略,和前面一模一样*/ })([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { __webpack_require__(1); __webpack_require__(2); module.exports = __webpack_require__(3); /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { var a = 1; /***/ }), /* 2 */ /***/ (function(module, exports, __webpack_require__) { var b = 2; /***/ }) /* 3 */ /***/ (function(module, exports, __webpack_require__) { var c = 3; /***/ }) /******/ ]); 前面说过,自执行函数默认将传入的参数数组的第一个元素传入__webpack_require__执行代码。 我们可以看一下传入第一个参数的内容,在上一章中是我们引入的文件内容var a = 1,但是这里却不是了。而是按模块引入顺序执行函数__webpack_require__(1),__webpack_require__(2),__webpack_require__(3),通过__webpack_require__函数去执行了我们引入的代码。 大家可以先想一下这里的1,2,3是怎么来的,为什么可以函数调用的时候,直接传参1,2,3 不过到这里还不明白,module.exports到底起了什么作用,如果起作用,为什么又只取最后一个呢? 3.升级版——多入口,多文件引入方式 因为好奇如果多入口多文件是怎么样的,接下去我又将入口改了一下,变成了下面这样 entry: { index1: [path.resolve(__dirname, '../src/test1.js')], index2: [path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')], }, 打包生成了index1.js和index2.js。发现index1.js和第一章讲的一样,index2.js和第二个文件一样。并没有什么让我很意外的东西。 4、进阶版——引入公共模块 在前面的打包文件中,我们发现每个模块id似乎是和引入顺序有关的。而在我们日常开发环境中,必然会引入各种公共文件,那么webpack会怎么处理这些id呢 于是我们在配置文件中新增了webpack.optimize.SplitChunksPlugin插件。 在webpack2和3版本中是webpack.optimize.CommonsChunkPlugin插件。但是在webpack4进行了一次优化改进,想要了解的可以看一下这篇文章webpack4:代码分割CommonChunkPlugin的寿终正寝。所以这里的代码将是使用webpack4打包出来的。 然后修改一下配置文件中的入口,我们开了两个入口,并且两个入口都引入了test3.js这个文件 entry: { index1: [path.resolve(__dirname, '../src/test.js'),path.resolve(__dirname, '../src/test3.js')], index2: [path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')], }, 可以看到,打包后生成了3个文件。 <script type="text/javascript" src="scripts/bundle.4474bdd2169853ce33a7.js"></script> <script type="text/javascript" src="scripts/index1.4474bdd2169853ce33a7.js"></script> <script type="text/javascript" src="scripts/index2.4474bdd2169853ce33a7.js"></script> 首先bundle.js(文件名自己定义的)很明显是一个公共文件,里面应该有我们提取test3.js出来的内容。打开文件后,发现里面的代码并不多,只有下面几行。 (window["webpackJsonp"] = window["webpackJsonp"] || []).push([[2],{ /***/ 2: /***/ (function(module, exports, __webpack_require__) { var c = 1; /***/ }) }]); 单纯看文件内容,我们大概能推测出几点: window全局环境下有一个名为webpackJsonp的数组 数组的第一个元素仍然是数组,记录了数字2,应该是这个模块的id 数组第二个元素是一个记录了形式为{模块id:模块内容}的对象。 对象中的模块内容就是我们test3.js,被一个匿名函数包裹 在webpack2中,采用的是{文件路径:模块内容}的对象形式。不过在升级到webpack3中优化采用了数字形式,为了方便提取公共模块。 注意到一点,这个文件中的2并不像之前一样作为注释的形式存在了,而是作为属性名。但是它为什么直接就将这个模块id命名为2呢,目前来看,应该是这个模块是第二个引入的。带着这个想法,我接下去看了打包出来的index1.js文件 截取出了真正执行并且有用的代码出来。 // index1.js (function(modules) { // webpackBootstrap // install a JSONP callback for chunk loading function webpackJsonpCallback(){ /*暂时省略内容*/ return checkDeferredModules } function checkDeferredModules(){/*暂时省略内容*/} // The module cache var installedModules = {}; // object to store loaded and loading chunks // undefined = chunk not loaded, null = chunk preloaded/prefetched // Promise = chunk loading, 0 = chunk loaded var installedChunks = { 0: 0 }; var deferredModules = []; // var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); jsonpArray.push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); for(var i = 0; i < jsonpArray.length; i++){ webpackJsonpCallback(jsonpArray[i]); } var parentJsonpFunction = oldJsonpFunction; // add entry module to deferred list deferredModules.push([0,2]); // run deferred modules when ready return checkDeferredModules(); })([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { __webpack_require__(1); module.exports = __webpack_require__(2); /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { var a = 1; /***/ }) /******/ ]); 在引入webpack.optimize.SplitChunksPlugin之后,核心代码在原来基础上新增了两个函数webpackJsonpCallback和checkDeferredModules。然后在原来的installedModules基础上,多了一个installedModules,用来记录了模块的运行状态;一个deferredModules,暂时不知道干嘛,看名字像是存储待执行的模块,等到后面用到时再看。 此外,还有这个自执行函数最后一行代码调用形式不再像之前一样。之前是通过调用__webpack_require__(0),现在则变成了checkDeferredModules。那么我们便顺着它现在的调用顺序再去分析一下现在的代码。 在分析了不同之后,接下来就按照运行顺序来查看代码,首先能看到一个熟悉的变量名字webpackJsonp。没错,就是刚才bundle.js中暴露到全局的那个数组。由于在html中先引入了bundle.js文件,所以我们可以直接从全局变量中获取到这个数组。 前面已经简单分析过window["webpackJsonp"]了,就不细究了。接下来这个数组进行了一次for循环,将数组中的每一个元素传参给了方法webpackJsonpCallback。而在这里的演示中,传入就是我们bundle.js中一个包含模块信息的数组[[2],{2:fn}}]。 接下来就看webpackJsonpCallback如何处理传进来的参数了 webpackJsonpCallback简析 /******/ function webpackJsonpCallback(data) { /******/ var chunkIds = data[0]; // 模块id /******/ var moreModules = data[1]; // 提取出来的公共模块,也就是文件内容 /******/ var executeModules = data[2]; // 需要执行的模块,但演示中没有 /******/ // add "moreModules" to the modules object, /******/ // then flag all "chunkIds" as loaded and fire callback /******/ var moduleId, chunkId, i = 0, resolves = []; /******/ /******/ for(;i < chunkIds.length; i++) { /******/ chunkId = chunkIds[i]; /******/ if(installedChunks[chunkId]) { /******/ resolves.push(installedChunks[chunkId][0]); /******/ } /******/ installedChunks[chunkId] = 0; /******/ } /******/ for(moduleId in moreModules) { /******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { /******/ modules[moduleId] = moreModules[moduleId]; /******/ } /******/ } /******/ if(parentJsonpFunction) parentJsonpFunction(data); /******/ /******/ while(resolves.length) { /******/ resolves.shift()(); /******/ } /******/ /******/ // add entry modules from loaded chunk to deferred list /******/ deferredModules.push.apply(deferredModules, executeModules || []); /******/ /******/ // run deferred modules when all chunks ready /******/ return checkDeferredModules(); /******/ }; 这个函数中主要干了两件事情,分别是在那两个for循环中。 一是在installedChunks对象记录引入的公共模块id,并且将这个模块标为已经导入的状态0。 installedChunks[chunkId] = 0;然后在另一个for循环中,设置传参数组modules的数据。我们公共模块的id是2,那么便设置modules数组中索引为2的位置为引入的公共模块函数。 modules[moduleId] = moreModules[moduleId]; //这段代码在我们的例子中等同于 modules[2] = (function(){/*test3.js公共模块中的代码*/}) 其实当看到这段代码时,心里就有个疑问了。因为index1.js中设置modulesp[2]这个操作并不是一个push操作,如果说数组索引为2的位置已经有内容了呢?暂时保留着心中的疑问,继续走下去。心中隐隐感觉到这个打包后的代码其实并不是一个独立的产物了。 我们知道modules是传进来的一个数组参数,在第二个章节中可以看到,我们会在最后执行函数__webpack_require__(0),然后依顺序去执行所有引入模块。 不过这次却和以前不一样了,可以看到webpackJsonpCallback最后返回的代码是checkDeferredModules。前面也说了整个自执行函数最后返回的函数也是checkDeferredModules,可以说它替代了__webpack_require__(0)。接下去就去看看checkDeferredModules发生了什么 checkDeferredModules简析 /******/ function checkDeferredModules() { /******/ var result; /******/ for(var i = 0; i < deferredModules.length; i++) { /******/ var deferredModule = deferredModules[i]; /******/ var fulfilled = true; /******/ for(var j = 1; j < deferredModule.length; j++) { /******/ var depId = deferredModule[j]; /******/ if(installedChunks[depId] !== 0) fulfilled = false; /******/ } /******/ if(fulfilled) { /******/ deferredModules.splice(i--, 1); /******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]); /******/ } /******/ } /******/ return result; /******/ } 这个函数关键点似乎是在deferredModules,但是我们刚才webpackJsonpCallback唯一涉及到这个的只有这么一句,并且executeModules其实是没有内容的,所以可以说是空数组。 deferredModules.push.apply(deferredModules, executeModules || []); 既然没有内容,那么webpackJsonpCallback就只能结束函数了。回到主线程,发现下面马上是两句代码,得,又绕回来了。 // add entry module to deferred list deferredModules.push([0,2]); // run deferred modules when ready return checkDeferredModules(); 不过现在就有deferredModules这个数组终于有内容了,一次for循环下来,最后去执行我们模块的代码仍然是这一句 result = __webpack_require__(__webpack_require__.s = deferredModule[0]); 很熟悉,有木有,最后还是回到了__webpack_require__,然后就是熟悉的流程了 __webpack_require__(1); module.exports = __webpack_require__(2); 但是当我看到这个内容竟然有这行代码时__webpack_require__(2);还是有点崩溃的。为什么?因为它代码明确直接执行了__webpack_require__(2)。但是2这个模块id是通过在全局属性webpackJsonp获得的,代码不应该明确知道的啊。 我原来以为的运行过程是,每个js文件通过全局变量webpackJsonp获得到公共模块id,然后push到自执行函数传参数组modules。那么等到真正执行的时候,会按照for循环依次执行数组内的每个函数。它不会知道有1,2这种明确的id的。 为什么我会这么想呢?因为我一开始认为每个js文件都是独立的,想交互只能通过全局变量来。既然是独立的,我自然不知道公共模块id是2事实上,webpackJsonp的确是验证了我的想法。 可惜结果跟我想象的完全不一样,在index1.js直接指定执行哪些模块。这只能说明一个事情,其实webpack内部已经将所有的代码顺序都确定好了,而不是在js文件中通过代码来确定的。事实上,当我去查看index2.js文件时,更加确定了我的想法。 /******/ (function(modules) {/*内容和index1.js一样*/}) /************************************************************************/ /******/ ([ /* 0 */, /* 1 */, /* 2 */, /* 3 */ /***/ (function(module, exports, __webpack_require__) { __webpack_require__(4); module.exports = __webpack_require__(2); /***/ }), /* 4 */ /***/ (function(module, exports, __webpack_require__) { var b = 2; /***/ }) /******/ ]); //# sourceMappingURL=index2.19eeab4e90ee99ee1ce4.js.map 仔细查看自执行函数的传参数组,发现它的第0,1,2位都是undefined。我们知道这几个数字其实就是每个模块本身的Id。而这几个id恰恰就是index1.js和bundle.js中的模块。理论上来说在浏览器下运行,index2.js应该无法得知的,但是事实却完全相反。 走到这一步,我对webpack打包后的代码也没有特别大的欲望了,webpack内部实现才是更重要的了。好了,不说了,我先去看网上webpack的源码解析了,等我搞明白了,再回来写续集。 原文发布时间为:2018年06月24日 原文作者:linzx本文来源: 掘金 如需转载请联系原作者
引言 现代前端应用通常都会使用ES6进行开发,ReactNative项目同样也会使用ES6进行开发,对于现代前端项目开发来说,掌握ES6成为一件十分必要的事情。对于ES6的学习,通常都会阅读阮一峰的《ECMAScript 6 入门》,以下这本书中开发ReactNative必备的知识点。 ECMAScript 6简介 ECMAScript6(以下简称ES6)是JavaScript语言的下一代标准,它的目标是让JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。 let和const命令 ES6新增了let命令,用来声明变量,它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。const声明一个只读的常量,一旦声明,常量的值就不能变化。 变量的解构赋值 ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。 函数的扩展 ES6能直接为函数的参数指定默认值 参数默认值可以与解构赋值的默认值,结合起来使用。 通常情况下,定义了默认值的参数,应该是函数的尾参数 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。 利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。 ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。 函数的name属性,返回该函数的函数名。 ES6 允许使用“箭头”(=>)定义函数。 箭头函数内部,还可以再使用箭头函数。 尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。 数组的扩展 扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。 由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。 数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。 扩展运算符提供了数组合并的新写法。 扩展运算符可以与解构赋值结合起来,用于生成数组。 扩展运算符还可以将字符串转为真正的数组。 fill方法使用给定值,填充一个数组。 ES6 提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象 对象的扩展 ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。 JavaScript 定义对象的属性,有两种方法。 函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。 ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。 Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。 Object.assign方法有很多用处。 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。 ES6 一共有 5 种方法可以遍历对象的属性。 我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。 Promise对象 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大 ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。 Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。 Iterator 和 for...of 循环 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环 有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法),除了下文会介绍的for...of循环,还有几个别的场合 字符串是一个类似数组的对象,也原生具有 Iterator 接口。 Symbol.iterator方法的最简单实现,还是使用下一章要介绍的 Generator 函数。 遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。 ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。 Class的基本语法 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。 constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。 生成类的实例对象的写法,与 ES5 完全一样,也是使用new命令。前面说过,如果忘记加上new,像函数那样调用Class,将会报错。 与函数一样,类也可以使用表达式的形式定义。 类不存在变量提升(hoist),这一点与 ES5 完全不同。 类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。 Class的继承 Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。 Object.getPrototypeOf方法可以用来从子类上获取父类。 super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。 extends关键字后面可以跟多种类型的值。 Module的语法 ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。 ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。 模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。 使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。 除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。 编程风格 ES6 提出了两个新的声明变量的命令:let和const。其中,let完全可以取代var,因为两者语义相同,而且let没有副作用。 在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。 使用数组成员对变量赋值时,优先使用解构赋值。 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。 使用扩展运算符(...)拷贝数组。 立即执行函数可以写成箭头函数的形式。 注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。 总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。 首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require。 ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。 原文发布时间为:2018年06月21日 原文作者:芒言本文来源: 掘金 如需转载请联系原作者
初衷 webpack4出了两个月,发现大家包括我对splitChunk的使用都还是在摸索阶段。我也看了挺多别人的配置demo,都觉得不太满意或者没得到太好的解惑,issue 下面的问题也没什么人回复,只能自己操作了,顺便记录下来,如果大家有更好的,欢迎评论区留下地址。 常用参数 minSize(默认是30000):形成一个新代码块最小的体积 minChunks(默认是1):在分割之前,这个代码块最小应该被引用的次数(译注:保证代码块复用性,默认配置的策略是不需要多次引用也可以被分割) maxInitialRequests(默认是3):一个入口最大的并行请求数 maxAsyncRequests(默认是5):按需加载时候最大的并行请求数。 chunks (默认是async) :initial、async和all test: 用于控制哪些模块被这个缓存组匹配到。原封不动传递出去的话,它默认会选择所有的模块。可以传递的值类型:RegExp、String和Function name(打包的chunks的名字):字符串或者函数(函数可以根据条件自定义名字) priority :缓存组打包的先后优先级。 如果你对这些配置还是不熟悉的话,一拉到底,看看文档 正文 先总览一下所有配置,后续会根据demo跑一遍常见的需求。 optimization: { splitChunks: { chunks: "async", // 必须三选一: "initial" | "all"(推荐) | "async" (默认就是async) minSize: 30000, // 最小尺寸,30000 minChunks: 1, // 最小 chunk ,默认1 maxAsyncRequests: 5, // 最大异步请求数, 默认5 maxInitialRequests : 3, // 最大初始化请求书,默认3 automaticNameDelimiter: '~',// 打包分隔符 name: function(){}, // 打包后的名称,此选项可接收 function cacheGroups:{ // 这里开始设置缓存的 chunks priority: 0, // 缓存组优先级 vendor: { // key 为entry中定义的 入口名称 chunks: "initial", // 必须三选一: "initial" | "all" | "async"(默认就是async) test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk name: "vendor", // 要缓存的 分隔出来的 chunk 名称 minSize: 30000, minChunks: 1, enforce: true, maxAsyncRequests: 5, // 最大异步请求数, 默认1 maxInitialRequests : 3, // 最大初始化请求书,默认1 reuseExistingChunk: true // 可设置是否重用该chunk } } } }, 接下来看看第一个例子 entry: { pageA: "./pageA", // 引用utility1.js utility2.js pageB: "./pageB", // 引用utility2.js utility3.js pageC: "./pageC" // 引用utility2.js utility3.js }, optimization: { splitChunks: { cacheGroups: { commons: { chunks: "initial", minChunks: 2, maxInitialRequests: 5, // The default limit is too small to showcase the effect minSize: 0 // This is example is too small to create commons chunks } } } }, 结果如图,一切都很正常 commons~pageA~pageB~pageC.js 文件就是utility2.js commons~pageB~pageC.js,根据上述代码,这里的utility2被引用了三次,首先就被抽离了commons~pageA~pageB~pageC.js,然后utility3被引用了两次就放到了commons~pageB~pageC.js,最后只剩下被引用一次的utility1.js,就直接放到了pageA.js里面,如果这里的utility1.js的也是两次,他还是会新建一个chunk放进去,而不是合并到commons~pageB~pageC.js,除非同入口引用才会合并。 mpageA.js pageB.js pageC.js 这里有个地方是需要优化一下的,就是pageA.js pageB.js pageC.js的代码不多,但是打出来的包很大,肯定是一些webpack的运行文件,直接加上 runtimeChunk runtimeChunk: "single" // 等价于 runtimeChunk: { name: "manifest" } 现在就好了 引用第三方模块 pageA引用vue.js pageB引用react react-dom vendor: { test: /node_modules/, chunks: "initial", name: "vendor", priority: 10, enforce: true } 但是,这样子的话,会把pageA pageB pageC所有的库都打包到一起vendor.js 假如我想拆分这个vendor.js为pageA-vendor.js pageB-vendor.js怎么办,我试了很久,试出一个最简单的办法,去掉手动的vendor,让插件自动处理。 splitChunks: { chunks: "all", cacheGroups: { commons: { chunks: "initial", minChunks: 2, maxInitialRequests: 5, // The default limit is too small to showcase the effect minSize: 0 // This is example is too small to create commons chunks } } }, 后来,我把webpack mode改成production后,发现不管用了,同样的配置,在生产模式下,打包出来的东西有点匪夷所思,vendor-pageB.js被合并到了pageB.js里面了。 后来我折腾了好久也分析不出来为什么,自己折腾出来一种方式,还是老子手动来吧,自动化一边去 commons: { chunks: "initial", minChunks: 2, maxInitialRequests: 5, // The default limit is too small to showcase the effect minSize: 0 // This is example is too small to create commons chunks }, 'vendor-pageA': { test: /vue/, // 直接使用 test 来做路径匹配 chunks: "initial", name: "vendor-pageA", enforce: true, }, 'vendor-pageB': { test: /react/, // 直接使用 test 来做路径匹配 chunks: "initial", name: "vendor-pageB", enforce: true, }, 成功打包出来了自己想要的东西。 动态引入 动态引入大家应该都不陌生,就是大家所说的懒加载,直接在pageA和pageB页面里动态引入common-async.js,在这里我先说说,splitChunk应该是可以自动化处理类似commonChunk里的async,child等情况的。 import(/* webpackChunkName: "common-async.js" */"./common-async").then(common => { console.log(common); }) 还不错,成功打包出来了 f.js成功的被抽离出来了,其他文件也没有被重复打包,挺好的。 注意的地方 cacheGroups 会继承和覆盖splitChunks的配置项,但是test、priorty和reuseExistingChunk只能用于配置缓存组。。 cacheGroups 里的每一项最好都要加上chunks参数,不然可能打包不出来你想要的东西。 minSize 默认是30KB(注意这个体积是压缩之前的)在小于30kb的情况下一定要设置一个值,否则也可能打包不出来你想要的东西,而且这东西要加在cacheGroups里面。 priority 在某些情况下,还是挺有用的,可以设置打包chunks的优先级。 上面的例子里面配置了一个commons,这里的name可以自己设置,也可以不设置,我是没设置的,你可以试试设置了是什么样子的,然后你就会明白这个name其实在某些情况下还是不设置的比较好。 commons: { chunks: "initial", minChunks: 2, maxInitialRequests: 5, // The default limit is too small to showcase the effect minSize: 0 , name: 总结 可见,splitChunk在懒加载方面,自动化处理的挺不错的,但在多页面的配置(根据不同的页面抽离不同的vendor)里,他因为有自己的一套优化策略,往往会得到不是我们想要的输出。这篇文章只是我初步的一些常识,我还没有深入去看源码,后续有空可能会补上splitChunk源码分析,到时候就更清晰了 源码 代码分割 代码懒加载 参考文章 splitChunk中文版 splitChunk英文版 webpack文档 官方demo 原文发布时间为:2018年05月26日 原文作者:乘风gg 本文来源: 掘金 如需转载请联系原作者
基本用法 GraphQL概述 GraphQL基本语法特性 GraphQL类型系统 GraphQL类型系统内置基础类型 GraphQL类型系统内置修饰符 GraphQL工作原理 GraphQL执行过程 Vue工程接入GraphQL 基本用法(如何去用) package.json "dependencies": { "apollo-server-koa": "^1.3.6", "graphql": "^0.13.2", "graphql-import": "^0.6.0", "graphql-tools": "^3.0.2", "koa": "^2.5.1", "koa-bodyparser": "^4.2.1", "koa-router": "^7.4.0", "koa-websocket": "^5.0.1" }, "devDependencies": { "babel-cli": "^6.26.0", "babel-preset-env": "^1.7.0" } server.js import koa from 'koa' import koaRouter from 'koa-router' import koaBody from 'koa-bodyparser' import websocketify from 'koa-websocket' import { graphqlKoa, graphiqlKoa } from 'apollo-server-koa' import { makeExecutableSchema } from 'graphql-tools' const app = websocketify(new koa()) const router = new koaRouter() const PORT = 3000 // fake data const moments = [ { user: { id: 1000, name: '锐雯', avatar: 'http://imgsrc.baidu.com/imgad/pic/item/42a98226cffc1e17d31927154090f603738de974.jpg' }, main: { content: '这是一条朋友圈', pics: [ 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1529219875063&di=bc0bcc78ae800c1c21c198f52697f515&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fimgad%2Fpic%2Fitem%2F4a36acaf2edda3ccd53548ea0be93901203f9223.jpg', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1529219893624&di=8d9e418df27e1fdb6afb1d993801a980&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fimgad%2Fpic%2Fitem%2F3801213fb80e7beca9004ec5252eb9389b506b38.jpg' ] }, comments: [ { user: { id: 233122, name: '亚索' }, reply: '面对疾风吧' } ] } ] const typeDefs = ` type Query { moments: [Moment] } type Mutation { addComment( entity: Add_Comment ) : Comment } type Moment { user: User main: Main comments: [Comment] } type User { id: Int name: String avatar: String } type Comment { user: User reply: String } type Main { content: String pics: [String] } input Add_User { id: Int name: String } input Add_Comment { user: Add_User reply: String } # 定义graphqlf服务哪个是RootQuery以及RootMutation schema { query: Query mutation: Mutation } ` const resolvers = { Query: { moments () { return moments } }, Mutation: { addComment (_, { entity }, unknown, context) { console.log(entity) moments[0].comments.push(entity) return entity } } } const schema = makeExecutableSchema({ typeDefs, resolvers }) // koaBody is needed just for POST. router.post('/graphql', koaBody(), graphqlKoa({ schema: schema })) // router.get('/graphql', graphqlKoa({ schema: schema })) router.get('/graphiql', graphiqlKoa({ endpointURL: '/graphql' })) async function responseMiddleware(ctx, next) { ctx.set('Access-Control-Allow-Origin', 'http://localhost:8080') ctx.set('Access-Control-Allow-Methods', 'POST,OPTIONS') ctx.set('Access-Control-Allow-Headers', 'authorization,content-type') // ctx.set('Access-Control-Allow-Credentials', 'true') await next() } app.use(responseMiddleware) app.use(router.routes()) app.use(router.allowedMethods()) app.ws.use(responseMiddleware) app.ws.use(router.routes()) app.ws.use(router.allowedMethods()) app.listen(PORT) GraphQL概述 GraphQL基本语法特性 包括有fields,alias,arguments,fragments,variables,directives,inline fragments field GraphQL类型系统 主要由RootQuery + RootMutation两种入口类型(操作)加上RootValue(resolvers)构成GraphQL Schema。(此处用graphql-tools是为了将所有的类型定义在一个字符串中,后续会移到一个.graphql文件中,然后用graphql-import导入) GraphQL类型系统内置基础类型 标量类型(Scalar Types) Int: 有符号的32位整数 Float: 有符号双精度浮点值 String: UTF-8字符序列 Boolean: true or false ID:ID 标量类型表示一个唯一标识符(类似一种UUID),通常用以重新获取对象或者作为缓存中的键。ID 类型使用和 String 一样的方式序列化。 枚举类型(Enumeration Types) 是一种特殊的标量类型 enum Episode { NEWHOPE EMPIRE JEDI } 数组类型(Array Types) 用方括号[]标记列表 接口类型(Interface Types) 是一种抽象类型,与java的interface机制类似。 联合类型(Union Types) { search(text: "an") { ... on Human { name height } ... on Droid { name primaryFunction } ... on Starship { name length } } } 输入类型(Input Types) 与之前提到的所有Types对立,这是一种也是唯一一种输入类型,其主要用于mutations时传递整个对象的case,它没有参数。 内置修饰符 !: 表示非空。如下 query DroidById($id: ID!) { droid(id: $id) { name } } GraphQL工作原理 GraphQL中每个查询字段是返回子类型的父类型函数。每个类型的字段对应由一个resolver函数支持,当字段被执行时,响应的resolver被调用并return结果。 如果字段产生结果为标量类型值,比如字符串或数字,则执行完成。否则递归执行对应解析器直至结果为标量类型值。 GraphQL基本数据流 每个GraphQL服务端应用的顶层必定会有一个入口点,通常为Root或者Query类型,接着执行该字段预设的解析器(同步或异步),而每个字段被解析的结果被放置在键值映射中,字段名(或别名)作为键,解析器的值作为值,这个过程从查询字段的底部叶子节点开始返回,直到Query类型的起始节点,最后生成镜像查询结果返回给客户端 Vue工程接入GraphQL 安装vue-cli3.x npm i -g @vue/cli 初始化工程 vue create [project-name] 引入apollo插件cd [project-name] vue add apollo FriendCircle.vue<template> <div> <div v-for="(item, index) in moments" :key="index"> {{item}} </div> <input type="text" v-model="comment.reply" placeholder="请输入要回复的内容"> <button @click="addComment">回复</button> </div> </template> <script> import gql from 'graphql-tag' const QUERY_LIST = gql` query { moments { user { id name avatar } main { content pics } comments { user { id name } reply } } } ` export default { data () { return { moments: [], comment: { user: { id: (Math.random() * 10000).toFixed(0), name: '费德提克' }, reply: '' } } }, apollo: { moments: { query: QUERY_LIST } }, methods: { addComment () { this.$apollo.mutate({ mutation: gql` mutation addComment($comment: Add_Comment) { addComment(entity: $comment) { user { id name } reply } } `, variables: { comment: this.comment }, update: (store, { data: { addComment } }) => { // Read the data from our cache for this query. const data = store.readQuery({ query: QUERY_LIST }) // set first moment's comment data.moments[0].comments.push(addComment) // Write our data back to the cache. store.writeQuery({ query: QUERY_LIST, data }) } }) } } } </script> 涉及的知识点有Root_Query,Root_Mutation,variables以及store cache cache 其核心机制包括以下两点 对所有(包括嵌套的)非标量类型递归进行缓存,往往通过类型id或_id以及__typename唯一组合标识,然后在一个扁平的数据结构中存储 可以设置不同缓存策略:cache-and-network,no-cache,network-only update回调 this.$apollo.mutate(options) options中有一个update回调,在成功响应数据后触发,并且可以直接读取并操作由apollo-cache-inmemory生成的store。上述例子中使用此回调同步更新缓存以及UI 注:所有绑定的变量均不可直接修改,内部使用Object.freeze将对象冻结,无法直接增删。 文发布时间为:2018年06月17日 原文作者:清风0o0 本文来源: 掘金 如需转载请联系原作者
记录一下,自己以后可能用的到。 原文:blog.jscrambler.com/12-extremel… 网上看到的。 1) 使用!!将变量转换成布尔类型 有时,我们需要检查一些变量是否存在,或者它是否具有有效值,从而将它们的值视为true。对于做这样的检查,你可以使用!!(双重否定运算符),它能自动将任何类型的数据转换为布尔值,只有这些变量才会返回false:0,null,"",undefined或NaN,其他的都返回true。我们来看看这个简单的例子: function Account(cash) { this.cash = cash; this.hasMoney = !!cash; } var account = new Account(100.50); console.log(account.cash); // 100.50 console.log(account.hasMoney); // true var emptyAccount = new Account(0); console.log(emptyAccount.cash); // 0 console.log(emptyAccount.hasMoney); // false 在这个例子中,如果account.cash的值大于零,则account.hasMoney的值就是true。 2) 使用+将变量转换成数字 这个转换超级简单,但它只适用于数字字符串,不然就会返回NaN(不是数字)。看看这个例子: function toNumber(strNumber) { return +strNumber; } console.log(toNumber("1234")); // 1234 console.log(toNumber("ACB")); // NaN 这个转换操作也可以作用于Date,在这种情况下,它将返回时间戳: console.log(+new Date()) // 1461288164385 3) 短路条件如果你看到过这种类似的代码: if (conected) { login(); } 那么你可以在这两个变量之间使用&&(AND运算符)来缩短代码。例如,前面的代码可以缩减到一行: conected && login(); 你也可以用这种方法来检查对象中是否存在某些属性或函数。类似于以下代码: user && user.login(); 4) 使用||设置默认值在ES6中有默认参数这个功能。为了在旧版浏览器中模拟此功能,你可以使用||(OR运算符),并把默认值作为它的第二个参数。如果第一个参数返回false,那么第二个参数将会被作为默认值返回。看下这个例子: function User(name, age) { this.name = name || "Oliver Queen"; this.age = age || 27; } var user1 = new User(); console.log(user1.name); // Oliver Queen console.log(user1.age); // 27 var user2 = new User("Barry Allen", 25); console.log(user2.name); // Barry Allen console.log(user2.age); // 25 5) 在循环中缓存array.length这个技巧非常简单,并且在循环处理大数组时能够避免对性能造成巨大的影响。基本上几乎每个人都是这样使用for来循环遍历一个数组的: for (var i = 0; i < array.length; i++) { console.log(array); } 如果你使用较小的数组,那还好,但是如果处理大数组,则此代码将在每个循环里重复计算数组的大小,这会产生一定的延迟。为了避免这种情况,你可以在变量中缓存array.length,以便在循环中每次都使用缓存来代替array.length: var length = array.length; for (var i = 0; i < length; i++) { console.log(array); } 为了更简洁,可以这么写: for (var i = 0, length = array.length; i < length; i++) { console.log(array); } 6) 检测对象中的属性 当你需要检查某些属性是否存在,避免运行未定义的函数或属性时,这个技巧非常有用。如果你打算编写跨浏览器代码,你也可能会用到这个技术。例如,我们假设你需要编写与旧版Internet Explorer 6兼容的代码,并且想要使用document.querySelector()来通过ID获取某些元素。 但是,在现代浏览器中,这个函数不存在。所以,要检查这个函数是否存在,你可以使用in运算符。看下这个例子: if ('querySelector' in document) { document.querySelector("#id"); } else { document.getElementById("id"); } 在这种情况下,如果在document中没有querySelector函数,它就会使用document.getElementById()作为代替。 7) 获取数组的最后一个元素Array.prototype.slice(begin,end)可以用来裁剪数组。但是如果没有设置结束参数end的值的话,该函数会自动将end设置为数组长度值。我认为很少有人知道这个函数可以接受负值,如果你将begin设置一个负数的话,你就能从数组中获取到倒数的元素: var array = [1, 2, 3, 4, 5, 6]; console.log(array.slice(-1)); // [6] console.log(array.slice(-2)); // [5,6] console.log(array.slice(-3)); // [4,5,6] 8) 数组截断这个技术可以锁定数组的大小,这对于要删除数组中固定数量的元素是非常有用的。例如,如果你有一个包含10个元素的数组,但是你只想获得前五个元素,则可以通过设置array.length = 5来阶段数组。看下这个例子: var array = [1, 2, 3, 4, 5, 6]; console.log(array.length); // 6 array.length = 3; console.log(array.length); // 3 console.log(array); // [1,2,3] 9) 全部替换String.replace()函数允许使用String和Regex来替换字符串,这个函数本身只能替换第一个匹配的串。但是你可以在正则表达式末尾添加/g来模拟replaceAll()函数: var string = "john john"; console.log(string.replace(/hn/, "ana")); // "joana john" console.log(string.replace(/hn/g, "ana")); // "joana joana" 10) 合并数组如果你需要合并两个数组,你可以使用Array.concat()函数: var array1 = [1, 2, 3]; var array2 = [4, 5, 6]; console.log(array1.concat(array2)); // [1,2,3,4,5,6]; 但是,这个函数对于大数组来说不并合适,因为它将会创建一个新的数组并消耗大量的内存。在这种情况下,你可以使用Array.push.apply(arr1,arr2),它不会创建一个新数组,而是将第二个数组合并到第一个数组中,以减少内存使用: var array1 = [1, 2, 3]; var array2 = [4, 5, 6]; console.log(array1.push.apply(array1, array2)); // [1,2,3,4,5,6]; 11) 把NodeList转换成数组 如果运行document.querySelectorAll("p")函数,它会返回一个DOM元素数组,即NodeList对象。但是这个对象并没有一些属于数组的函数,例如:sort(),reduce(),map(),filter()。为了启用这些函数,以及数组的其他的原生函数,你需要将NodeList转换为数组。要进行转换,只需使用这个函数:[] .slice.call(elements): var elements = document.querySelectorAll("p"); // NodeList var arrayElements = [].slice.call(elements); // 现在已经转换成数组了 var arrayElements = Array.from(elements); // 把NodeList转换成数组的另外一个方法 12) 对数组元素进行洗牌如果要像外部库Lodash那样对数据元素重新洗牌,只需使用这个技巧: var list = [1, 2, 3]; console.log(list.sort(function() { return Math.random() - 0.5 })); // [2,1,3] 原文发布时间为:2018年05月11日 原文作者:PY2706 本文来源: 掘金 如需转载请联系原作者
最近的两次Bug: 做A需求的时候,看到以前的一段代码写的很难看,很不好维护,我忍不了了。所以麻(shou)利(jian)的修改了,然后我就狗带了~ 做B需求的时候,要修改以前的代码,我在已有的函数上加了一个参数。然后我又狗带了~ 发bug就像无孔不入的虫子,你以为你堵住了这个洞,但是它有在另外一个洞生长飞进来,防不胜防! 总结:导致发bug的原因往往不在新的功能和新的需求,而在你修改了以前的代码!!这些牵涉到的修改点,测试并不会去很仔细的测啊!!!! 以下是我们高工严厉指出并给我的建议并加上我的个人总结,与大家分享,定期踩坑更新(我希望再也不要更新了!!!)。 1. 修改前与原作者沟通 如果原作者还在,可以和原作者聊一下,最好他能讲一下代码结构,可以减少很多看代码的时间 2. 仔细阅读修改段落的上下文,并Get以下重点: 包括参数命名模式 注释风格等代码风格(分号啊,空行啊,空格啊.....) 参数定义位置(一般都在顶部) 函数定义方式(函数声明方式or 函数表达式方式) 代码结构(MVC分块) get之后就按照已有的风格进行修改,也许你是一个很有风格的码农,你可以改变其他人一起用这个风格~ 3. 开始修改了,以下雷请扫: 定义一个新的变量(函数): 记得检查变量作用域并全局搜索~ 不要自信的觉得你和别人定义的不一样。 修改已有的函数参数: 不要修改参数顺序!!!!这样就算有些调用的地方你没有看到,也会降低很多的bug率。 4. review一遍代码,这里分为自己review和邀请其他人review。 自己review: 看是否有啥异常没有处理。前端经常犯错就是容错性不够好,毕竟从发起后台请求到你拿到数据,过程是多么艰辛。 修改了哪些点,尽可能记下来会影响哪些模块!! 他人review: 对于新人来很有用啊~ 5. 提交测试 把自己会影响的模块列出来发给测试,这样他才知道重点啊~尤其是在写新需求夹带修改旧代码的时候!! 这个很重要!!! 如果有接入前端错误监控的话,关注一下报错内容。我们项目是有接入Badjs错误监控的。 6. 发布之后,你的产品有论坛或者群吗? 有的话直接加上去,观察用户反馈。往往这些地方是bug反馈最及时的~~~~~ 总结完了,忐忑的去吃鸡然后默默等待明天批评。 原文发布时间为:2018年05月27日 原文作者:MirroZhou本文来源: 掘金 如需转载请联系原作者
前言 其实一开始对栈、堆的概念特别模糊,只知道好像跟内存有关,又好像事件循环也沾一点边。面试薄荷的时候,面试官正好也问到了这个问题,当时只能大方的承认不会。痛定思痛,回去好好的研究一番。 我们将从JS的内存机制以及事件机制和大量的(例子)来了解栈、堆究竟是个什么玩意。概念比较多,不用死读,所有的心里想一遍,浏览器console看一遍就很清楚了。 let's go JS内存机制 因为JavaScript具有自动垃圾回收机制,所以对于前端开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。特别是很多不专业的朋友在进入到前端之后,会对内存空间的认知比较模糊。 在JS中,每一个数据都需要一个内存空间。内存空间又被分为两种,栈内存(stack)与堆内存(heap)。 栈内存一般储存基础数据类型 Number String Null Undefined Boolean (es6新引入了一种数据类型,Symbol) 最简单的 var a = 1 我们定义一个变量a,系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问。 数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。 堆内存一般储存引用数据类型 堆内存的 var b = { xi : 20 } 与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。看一下下面的图,加深理解。 比较  var a1 = 0; // 栈 var a2 = 'this is string'; // 栈 var a3 = null; // 栈 var b = { m: 20 }; // 变量b存在于栈中,{m: 20} 作为对象存在于堆内存中 var c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3] 作为对象存在于堆内存中 因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从栈中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。 测试 var m = { a: 10, b: 20 } var n = m; n.a = 15; console.log(m.a) var a = 20; var b = a; b = 30; console.log(a) 同学们自己在console里打一遍,再结合下面的图例,就很好理解了 内存机制我们了解了,又引出一个新的问题,栈里只能存基础数据类型吗,我们经常用的function存在哪里呢? 浏览器的事件机制 一个经常被搬上面试题的 console.log(1) let promise = new Promise(function(resolve,reject){ console.log(3) resolve(100) }).then(function(data){ console.log(100) }) setTimeout(function(){ console.log(4); }) console.log(2) 上面这个demo的结果值是 1 3 2 100 4 对象放在heap(堆)里,常见的基础类型和函数放在stack(栈)里,函数执行的时候在栈里执行。栈里函数执行的时候可能会调一些Dom操作,ajax操作和setTimeout定时器,这时候要等stack(栈)里面的所有程序先走**(注意:栈里的代码是先进后出)**,走完后再走WebAPIs,WebAPIs执行后的结果放在callback queue(回调的队列里,注意:队列里的代码先放进去的先执行),也就是当栈里面的程序走完之后,再从任务队列中读取事件,将队列中的事件放到执行栈中依次执行,这个过程是循环不断的。 1.所有同步任务都在主线程上执行,形成一个执行栈 2.主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。 3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行 4.主线程从任务队列中读取事件,这个过程是循环不断的 概念又臭又长,没关系,我们先粗略的扫一眼,接着往下看。 举一个说明栈的执行方式 var a = "aa"; function one(){ let a = 1; two(); function two(){ let b = 2; three(); function three(){ console.log(b) } } } console.log(a); one(); demo的结果是 aa 2 图解 执行栈里面最先放的是全局作用域(代码执行有一个全局文本的环境),然后再放one, one执行再把two放进来,two执行再把three放进来,一层叠一层。 最先走的肯定是three,因为two要是先销毁了,那three的代码b就拿不到了,所以是先进后出(先进的后出),所以,three最先出,然后是two出,再是one出。 那队列又是怎么一回事呢? 再举一个 console.log(1); console.log(2); setTimeout(function(){ console.log(3); }) setTimeout(function(){ console.log(4); }) console.log(5); 首先执行了栈里的代码,1 2 5。 前面说到的settimeout会被放在队列里,当栈执行完了之后,从队列里添加到栈里执行(此时是依次执行),得到 3 4 再再举一个 console.log(1); console.log(2); setTimeout(function(){ console.log(3); setTimeout(function(){ console.log(6); }) }) setTimeout(function(){ console.log(4); setTimeout(function(){ console.log(7); }) }) console.log(5) 同样,先执行栈里的同步代码 1 2 5. 再同样,最外层的settimeout会放在队列里,当栈里面执行完成以后,放在栈中执行,3 4。 而嵌套的2个settimeout,会放在一个新的队列中,去执行 6 7. 再再再看一个 console.log(1); console.log(2); setTimeout(function(){ console.log(3); setTimeout(function(){ console.log(6); }) },400) setTimeout(function(){ console.log(4); setTimeout(function(){ console.log(7); }) },100) console.log(5) 如上:这里的顺序是1,2,5,4,7,3,6。也就是只要两个set时间不一样的时候 ,就set时间短的先走完,包括set里面的回调函数,再走set时间慢的。(因为只有当时间到了的时候,才会把set放到队列里面去) setTimeout(function(){ console.log('setTimeout') },0) for(var i = 0;i<10;i++){ console.log(i) } 这个demo的结果是 0 1 2 3 4 5 6 7 8 9 setTimeout 所以,得出结论,永远都是栈里的代码先行执行,再从队列中依次读事件,加入栈中执行 stack(栈)里面都走完之后,就会依次读取任务队列,将队列中的事件放到执行栈中依次执行,这个时候栈中又出现了事件,这个事件又去调用了WebAPIs里的异步方法,那这些异步方法会在再被调用的时候放在队列里,然后这个主线程(也就是stack)执行完后又将从任务队列中依次读取事件,这个过程是循环不断的。 再回到我们的第一个 console.log(1) let promise = new Promise(function(resolve,reject){ console.log(3) resolve(100) }).then(function(data){ console.log(100) }) setTimeout(function(){ console.log(4); }) console.log(2) 上面这个demo的结果值是 1 3 2 100 4 为什么setTimeout要在Promise.then之后执行呢? 为什么new Promise又在console.log(2)之前执行呢? setTimeout是宏任务,而Promise.then是微任务 这里的new Promise()是同步的,所以是立即执行的。 这就要引入一个新的话题宏任务和微任务(面试也会经常提及到) 宏任务和微任务 参考 Tasks, microtasks, queues and schedules(https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly) 概念:微任务和宏任务都是属于队列,而不是放在栈中 一个新的 console.log('1'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('2'); 1 2 promise1 promise2 setTimeout 宏任务(task) 浏览器为了能够使得JS内部宏任务与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->…) 鼠标点击会触发一个事件回调,需要执行一个宏任务,然后解析HTMl。但是,setTimeout不一样,setTimeout的作用是等待给定的时间后为它的回调产生一个新的宏任务。这就是为什么打印‘setTimeout’在‘promise1 , promise2’之后。因为打印‘promise1 , promise2’是第一个宏任务里面的事情,而‘setTimeout’是另一个新的独立的任务里面打印的。 微任务 (Microtasks) 微任务通常来说就是需要在当前 task 执行结束后立即执行的任务 比如对一系列动作做出反馈,或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。只要执行栈中没有其他的js代码正在执行且每个宏任务执行完,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。微任务包括了mutation observe的回调还有接下来的例子promise的回调。 一旦一个pormise有了结果,或者早已有了结果(有了结果是指这个promise到了fulfilled或rejected状态),他就会为它的回调产生一个微任务,这就保证了回调异步的执行即使这个promise早已有了结果。所以对一个已经有了结果的**promise调用.then()**会立即产生一个微任务。这就是为什么‘promise1’,'promise2’会打印在‘script end’之后,因为所有微任务执行的时候,当前执行栈的代码必须已经执行完毕。‘promise1’,'promise2’会打印在‘setTimeout’之前是因为所有微任务总会在下一个宏任务之前全部执行完毕。 还是 <div class="outer"> <div class="inner"></div> </div> // elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); //监听element属性变化 new MutationObserver(function() { console.log('mutate'); }).observe(outer, { attributes: true }); // click listener… function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); click promise mutate click promise mutate (2) timeout 很好的解释了,setTimeout会在微任务(Promise.then、MutationObserver.observe)执行完成之后,加入一个新的宏任务中 多看一些 console.log(1); setTimeout(function(){ console.log(2); Promise.resolve(1).then(function(){ console.log('promise1') }) }) setTimeout(function(){ console.log(3) Promise.resolve(1).then(function(){ console.log('promise2') }) }) setTimeout(function(){ console.log(4) Promise.resolve(1).then(function(){ console.log('promise3') }) }) 1 2 promise1 3 promise2 4 promise3 console.log(1); setTimeout(function(){ console.log(2); Promise.resolve(1).then(function(){ console.log('promise1') setTimeout(function(){ console.log(3) Promise.resolve(1).then(function(){ console.log('promise2') }) }) }) }) 1 2 promise1 3 promise2 总结回顾 栈: 存储基础数据类型 按值访问 存储的值大小固定 由系统自动分配内存空间 空间小,运行效率高 先进后出,后进先出 栈中的DOM,ajax,setTimeout会依次进入到队列中,当栈中代码执行完毕后,再将队列中的事件放到执行栈中依次执行。 微任务和宏任务 堆: 存储引用数据类型 按引用访问 存储的值大小不定,可动态调整 主要用来存放对象 空间大,但是运行效率相对较低 无序存储,可根据引用直接获取 广而告之 本文发布于薄荷前端周刊,欢迎Watch & Star ,转载请注明出处。 欢迎讨论,点个赞再走吧 。◕‿◕。 ~ 原文发布时间为:2018年06月11日 原文作者:薄荷前端 本文来源: 掘金 如需转载请联系原作者
什么是栈(Stack) 栈是一种遵从后进先出(LIFO)原则的有序集合。 新添加的或待删除的元素都保存在栈的末尾,称为栈顶,另一端叫栈底。 在栈里,新元素都靠近栈顶,旧元素都接近栈底 现实中的例子 在生活中也能发现很多栈的例子。例如,厨房里堆放的盘子,总是叠在上方的先被使用;输入框内容进行删除时,总是最后输入的先删除;弹夹中的子弹,越后装入的,越先发射...... 手动实现一个栈 首先,创建一个类来表示栈 function Stack () { } 我们需要选择一种数据结构来保存栈里的元素,可以选择数组 function Stack(){ var items = []; //用来保存栈里的元素 } 接下来,为栈添加一些方法 push(element(s)); //添加新元素到栈顶 pop(); //移除栈顶的元素,同时返回被移除的元素 peek(); //返回栈顶的元素,不对栈做任何修改 isEmpty(); //如果栈里没有任何元素就返回true,否则false clear(); //移除栈里的所有元素 size(); //返回栈里的元素个数,类似于数组的length属性 我们需要实现的第一个方法时push。用来往栈里添加新元素,有一点很重要:该方法只添加到栈顶,也就是栈的末尾。所以,可以这样写: this.push = function (element) { items.push(element); } 利用数组的push方法,就可以实现在栈顶末尾添加新的元素了。 接着,来实现pop方法,用来实现移除栈里的元素。栈遵从LIFO(后进先出)原则。移出去的是最后添加进去的元素。因此,可以使用数组的pop方法。 this.pop = function () { return items.pop(); } 这样一来,这个栈自然就遵从了LIFO原则 现在,再来为这个栈添额外的辅助方法。 如果想知道栈里最后添加的元素是什么,可以用peek方法。这个方法将返回栈顶的元素 this.peek = function () { return items[items.length-1]; } 因为类内部是用数组保存元素的,所以这里访问数组最后一个元素用length-1 下一个要实现的方法是isEmpty,如果栈为空的话,就返回true,否则返回false: this.isEmpty = function () { return items.length == 0; } 使用isEmpty方法,就能简单地判断栈内部是否为空。 类似于数组地length属性,我们也可以实现栈地length。 this.size = function () { return items.length; } 因为栈地内部使用数组保存元素,所以数组地length就是栈的长度。 实现clear方法,clear方法用来清空栈中所有的元素。最简单的实现方法是: this.clear = function () { items = []; } 其实多次调用pop方法也可以,但是没有这个方法来的简单快捷。 最后,为了检查栈里的内容,还需要实现一个辅助方法:print。它会把栈里的元素都输出到控制台: this.print = function () { console.log(items.toString()); } 至此,我们就完整地创建了一个栈! 栈的完整代码 function Stack(){ var items = []; //用来保存栈里的元素 this.push = function (element) { items.push(element); } this.pop = function () { return items.pop(); } this.peek = function () { return items[items.length-1]; } this.isEmpty = function () { return items.length == 0; } this.size = function () { return items.length; } this.clear = function () { items = []; } this.print = function () { console.log(items.toString()); } } 使用Stack类 栈已经创建好了,我们来测试一下 首先,来初始化Stack类。然后,验证一下栈是否为空 var stack = new Stack(); console.log(stack.isEmpty()); //控制台输出true 接下来,往栈里面添加一下元素: stack.push(5); stack.push(8); 如果调用peek方法,很显然将会输出8,因为它是栈顶的元素: console.log(stack.peek()); //控制台输出8 再添加一个元素: stack.push(11); console.log(stack.size()); //控制台输出3 我们往栈里又添加了11。如果调用size方法,输出为3,因为栈里有三个元素(5,8和11)。如果这时候调用isEmpty方法,会看到输出了false(因为此时栈不为空)。最后,再来往里面添加一个元素: stack.push(15); 然后,调用两次pop方法从栈里移除两个元素: stack.pop(); stack.pop(); console.log(stack.size()); //控制台输出2 stack.print(); //控制台输出[5,8] 到这里,整个栈的功能测试完成。 用栈来解决问题 使用栈来完成进制转换。 现实生活中,我们主要用10进制,但在计算科学中,二进制非常重要,因为计算机里所有的内容都是用二进制数字0和1来表示的。大学的计算机课都会先教进制转换。以二进制为例: function divideBy2 (decNumber) { var remStack = new Stack(), rem, binaryString = ''; while (decNumber>0) { //{1} rem = Math.floor(decNumber % 2); //{2} remStack.push(rem); //{3} decNumber = Math.floor(decNumber / 2); //{4} } while (!remStack.isEmpty()) { //{5} binaryString += remStack.pop().toString(); } return binaryString; } 这段代码里,当结果满足和2做整除的条件时,(行{1}),我们会获得当前结果和2的余数,放到栈里(行{2}、{3})。然后让结果和2做整除(行{4}) 注:JavaScript有数字类型,但是它不会区分时整数还是浮点数。因此,要使用Math.floor函数让除法的操作仅返回整数部分。 最后,用pop方法把栈中的元素都移除,把出栈的元素连接成字符串(行{5})。 测试一下: console.log(divideBy2(520)); //输出1000001000 console.log(divideBy2(10)); //输出1010 console.log(divideBy2(1000)); //输出1111101000 接下来,可以很容易的修改上面的算法,使它能够把十进制转化为任何进制。除了让十进制数字和2整除转成二进制数,还可以传入其他任意进制的基数作为参数,就像下面的算法这样: function baseConverter (decNumber, base) { var remStack = new Stack(), rem, baseString = ''; digits = '0123456789ABCDEF'; //{6} while (decNumber>0) { rem = Math.floor(decNumber % base); remStack.push(rem); //{3} decNumber = Math.floor(decNumber / base); } while (!remStack.isEmpty()) { baseString += digits[remStack.pop()]; //{7} } return baseString; } 在将十进制转成二进制时,余数是0或1;在将十进制转成八进制时,余数时0-8之间的数;但是将十进制转成十六进制时,余数时0-9之间的数字加上A、B、C、D、E、F(对应10、11、12、13、14和15)。因此,需要对栈中的数字做个转化才可以(行{6}、{7})。 来测试一下输出结果: console.log(baseConverter(1231,2)); //输出10011001111 console.log(baseConverter(1231,8)); //输出2317 console.log(baseConverter(1231,16)); //输出4CF 显然是正确的。 小结 我们用js代码自己实现了栈。并且通过进制转换的例子来实际应用了它。栈的应用实例还有很多,比如平衡圆括号和汉诺塔。感兴趣可以自行百度去了解 原文链接:行无忌的成长小屋:如何用JavaScript手动实现一个栈 原文发布时间为:2018年05月26日 原文作者:行无忌 本文来源: 掘金 如需转载请联系原作者
谈谈学习 我做前端已经有五年的时间了,从大学刚毕业的时候,我是一个完全什么都不懂的小白。虽然我大学里学的是软件工程专业,但是因为在大学里荒废学业,每天只知道打游戏,基本上到大学毕业之前我是什么都不会的,而且大学里是没有教前端相关的东西的。 我是在大学毕业之前,两三个月,自学了一点点前端的皮毛,也就是会写一点点的html和css。那个时候找工作也不容易,终于有一个公司收留了我,虽然说工资什么的我们暂时按下不提。但是至少他们要我,我就非常地开心了。 从那个时候开始,因为有了项目要做,所以我逼迫自己,每天去学习新的内容。那个时候我从jQuery开始做,因为公司的项目比较特殊,就是单页应用,总共加起来大概就五六个页面,但是要一次性全部加载完,后续的所有数据都是根据接口去获取。那个时候没有框架,没有React,ngular也刚刚出来,还并不流行。所以那个时候我们就用jQuery。 然后我就想办法去做,第一版做的非常的烂,就一个js文件我写了3000多行代码。所以在那个时候我逼迫自己想办法去解决各种问题。我就针对自己当时的认知,比如说js文件太大了,应该怎么办? 然后我去网上搜,我发现了sea.js这个东西。我学习了它异步加载JavaScript的方法,后来才把它引用到项目里。 当我发现代码量多了,很难去维护这个逻辑,还有页面的切换。于是我就去网上搜资料学习,发现有backbone.js这个东西,它可以帮我解决这个问题。 在后来这段时间里,我积累了非常多的东西,也是我进步最快的一个时期。 当我再去接触到这些Vue、React这些框架的时候,我先去学习了一遍它们主要是解决了什么问题,它们主要的功能有哪些。然后我再尝试用到项目里面去。那个时候,我刚开始学的是Angular,当时也没有了解到有构建工具这个概念。那时候最多只知道有Grunt,帮你去压缩一下js。 当时,我还没有模块化的概念。 后来又做了一些项目,发现了很多的问题,我再去想办法去解决。这个时候才会知道有Webpack这些模块化组织代码的工具。我就是在这种环境下一步一步成长起来的。我所有学的内容,对于前端的了解,基本上都是基于我想要去解决一个问题,然后再去网上搜相关的内容,然后去想办法把这个问题解决掉。 所以对于我来说,学习这个东西,你要知道你要解决什么问题。然后你学的这个东西,它解决了什么问题。它适不适合用在你遇到的这些问题上,以及你在学习这个内容之前,你必须要对它的整体有一个了解。不然的话,你就是瞎用。你用的方法可能根本就不是这个框架要解决的问题。 比如Vue,大家对于vue整体有一个概念之后,再去用到一个项目里面去。而项目一开始我们做的时候并不要求做的特别的复杂。只要你基础扎实,你肯定有办法去解决这些在项目里碰到的问题。而在解决问题的过程当中,才是你掌握了你学习到的东西,巩固了你学习到的这些内容的地方。 所以这就是我要谈的学习。 谈谈前端 现在的前端跟五年前的前端是天壤之别。 五年前的前端,框架之流刚刚兴起,也就是Angular带的头,那个时候刚出来没多久。那个时候流行什么呢?就是写页面模板。 我们写好html、css,以及这个页面上简单的动画效果,比如说点击一下有什么事件之类的。然后这个东西写好之后干嘛呢?丢给后端人员他们用后端模板,比如说最多的用Java,Java有jsp模板语言,接着把数据填进去,就OK了。 这个时候我们一个网站有很多页面怎么办呢?比如说我们写了很多的html页面,写好之后交给后端人员,然后他们整合在模板,再把数据动态插进去。这样的话,每个http的请求发送到后端那边,后端根据这个http请求,返回不同的模板,并且把数据渲染进去,最终返回给用户。 这就是以前的前端。那么现在的前端呢? 我们把渲染模板的操作从后端挪到了前端来做。因为把模板渲染这部分拿到前端来做,所以很多业务逻辑的判断、渲染、数据获取等都要在前端来做。这个时候就大大加深了前端的业务逻辑。它包含的内容就比以前上升了好几个量级。 在这种情况下,我们需要一个框架去整理代码。而这个时候就会出现各种帮助我们前端开发者去处理这些内容的东西,比如说我们有了前端路由的概念,我们有了store状态管理的概念。以前MVC的时候,还有过其他的处理数据的方式,反正各种各样的工具帮我们去处理这个问题。 伴随着前端业务代码越来越复杂,我们用以前的方式去写JavaScript代码,效率非常的低。因为我们写的JavaScript代码很多都受浏览器兼容的限制,或者是它的低版本的很多功能不支持,导致我们写起来非常的蛋疼。 彼时前端界不缺人才,他们发明了各种各样的工具,比如用babel让我们能提前享受到最新的JavaScript语法带来的很多很好用的功能。当我们有了React这类框架之后,希望前端的应用以js作为入口,而不是以html作为入口。我们html里显示的内容都是从js里面渲染出来的。 由于整个应用基本上以js作为入口,我们就需要用js来处理css、图片等其他静态资源。这个时候我们发现,js本身不是用来处理这些东西的,而我们又迫切需要一些工具来处理这个问题,因此又出现了webpack。 所以前端的发展速度在整个编程界来说,是相对特别快的。主要的原因就是因为以前的前端它的标准实在是太低了。现在补充的很多js 的标准、前端的一些标准都是以前发展缓慢,落下来的。所以说,现在只能说是慢慢地变成了一个正常的状态,而不是说已经超前非常多了。 前端现在要入门的童鞋,对你们来说,其实不算是一个特别好的时机。 如果你之前是有基础的,那么还可以。如果你之前一点基础都没有,你会发现前端进来之后,你要学的东西特别的多,各种各样的东西。在这种鱼龙混杂的时候,你必须要保持一颗好奇心,保持一个吸收新知识的能力。你要经常去看一些文章,或者是GitHub,或者是一些厉害的程序员的博客。他们有发现新的东西,好玩的东西,都会分享出来。然后你要立马去学习,因为它有可能就是将来的主流。 所以现在去学习前端,你要时刻保持一种学习的心态。 谈谈面试 现在的前端状态有点混乱,发展节奏有点快,你仅仅是想把vue学好是不可能的。你要学vue就必须要学webpack。当然你可以直接用vue-cli去生成一个项目。但是对于好奇的你,肯定会想,vue-cli里面到底做了哪些东西。 然后当你以后出去面试的时候,你跟面试官说,你做过vue的项目。然后面试官肯定会问你一些webpack相关的一些东西。这个时候你又说不出个所以然来。他会问你,那你的项目是怎么生成的,你会说用vue-cli脚手架生成的。他会问你,那你有没有看过vue-cli里面生成的一些代码,然后这个时候你又说没有。那么他对你的期望值就会变得非常的低。 当然有的面试官还会不厌其烦地问你,为什么我们用webpack生成的文件名后面要带上一串哈希码?这个问题至少我问过很多初中级的童鞋,他们都是不知道的,不太清楚,或者只是清楚一些有概念叫做缓存。但是至于这个文件名后面加一堆哈希码,到底是做什么事情,他们不太清楚。浏览器如何去缓存,他们也不太清楚。 所以webpack是一个逃不开的话题。然后还有vue-router、vuex这些已经算是vue项目开发的三驾马车。因此当你想学好vue的时候,意味着你还要学好webpack、vue-router、vuex。 当然,服务端渲染也是值得讨论的话题。这些东西要去全部学习好,对于基础比较差的童鞋来讲,确实有点难,他一下子接收的东西实在太多了。一旦涉及到webpack,而webpack又是跑在node端的,所以你还要学习用node.js如何去写脚本。 当然,如果对于追求上进的你来说,还想学习下SSR服务端渲染,首先你要去学习用node.js如何去写一个server,然后还有服务端的接口实现,虽然这是一个纯后端的实现内容。 当你以为这就结束了的话,你就错了。如果你面试的是BATJ等互联网大厂,面试官可能还会问你http相关的内容。比如说,面试官会问你,像http的get、post、put、delete这些request methods它们又有什么用,有什么区别?从语义上来讲,http的code又有什么用。当这些问题抛向你的时候,如果你平时没有注意积累这方面的技术知识,此时的你是否依然会处于一个懵圈的状态? 这些东西,对于一个纯做前端,纯做页面的童鞋来说的话,你确实不用特别的了解。但是你一旦升级为一个工程师,你要去跟后端交流一些接口,你要去思考统筹整个项目的时候,你必须要了解。不然的话,你会踩非常多的坑,犯非常多的错误。导致你的整个项目到后期维护的时候,有很多的内容到后期是没办法去升级的。 讲了这么多关于学习、前端、面试等话题,终归学习是要讲效率的,如果你学了非常多的时间,发现自己还是不怎么会,你就会特别没有成就感。当时就不会想继续学下去了。 最后,向内认知,向外行走。 谨以此文与君共勉。 原文发布时间为:2018年05月14日原文作者:闰土大叔本文来源: 掘金 如需转载请联系原作者
简介 由于小程序开发工具的封闭,我们无法通过安装 chrome 插件来方便地使用 vue-devtools 调试我们的 mpvue 项目。vuetron 是一个 vue.js 的项目调试工具, 同时支持对 vuex 及 vue-router 的调试。这里我们主要使用其对 vuex 的调试功能。 安装 npm 模块 npm install vuetron weapp.socket.io --save-dev 客户端应用 windows 平台提供的是压缩包,下载解压后,双击 Vuetron.exe 即可运行,另外两个平台提供的应该都是安装包。 windows mac linux 使用 配置 webpack 的 alias 需使用 weapp.socket.io 替代 socket.io-client resolve: { extensions: ['.js', '.vue', '.json'], alias: { '@': resolve('src'), 'socket.io-client': 'weapp.socket.io', 'vue': 'mpvue', }, }, 作为 vuex 的插件引入 import Vue from 'vue'; import Vuex from 'vuex'; import vuetron from 'vuetron'; Vue.use(Vuex); const store = new Vuex.Store({ // ... plugins: [vuetron.VuetronVuex()], }); 功能 查看并与管理项目当前的数据 在不同时刻的数据之间进行切换,或者立即恢复一组完整的项目数据 订阅特定数据来进行调试 vue-devtools 事实上 vue-devtools 也提供了一个远程调试工具 vue-remote-devtools,开发微信公众号时直接引用一个脚本即可,但是小程序中目前还无法直接使用,需要我们对官方的脚本做些修改,后面有空完善后会再做详细介绍。 注 截图来自 vuetron 的官方文档。 原文发布时间为:2018年06月24日 原文作者:Float 本文来源: 掘金 如需转载请联系原作者
说明 下面代码演示基于window系统chrome浏览器环境,版本号为63.0.3239.132,32位!相关结果可能会有一点出入,请也实际为准! 相关代码调试的过程中查看结果的步骤: 打开浏览器控制台,切换到sources板块,并选择相应的源文件; 在对应的源文件代码左边的行号上打上断点; 然后刷新浏览器,浏览器会在对应打断点的代码出停止执行,此时我们根据需要按f11键一步一步的运行代码,并同时查看代码的调用栈,作用域等情况,主要查看source板块下最右边子板块的Call Stack和Scope项。 相关概念梳理 其实从我自身出发,我觉的如果需要更好地理解变量对象的话,那么需要对以下概念有一个比较基本的理解会更方便些! 函数调用栈 为了理解函数调用栈,我们先写一段代码: function fn1(){ console.log('fn1'); fn2(); }; function fn2(){ console.log('fn2'); }; fn1(); 我们在fn2函数调用的地方打上断点,然后刷新浏览器,查看Call Stack选项,会看到下面的结果: fn1 (anonymous) 此时再按一次f11键,此时Call Stack显示结果如下: fn2 fn1 (anonymous) 这里说明一下,anonymous指代全局匿名调用函数环境,而fn1和fn2分别指代fn1函数作用域和fn2作用域环境。 我们梳理一下浏览器的调用过程:1 js进入全局匿名函数环境,把这个所谓的匿名函数推入调用栈;2 发现此时又调用了fn1,于是把fn1函数推入调用栈,此时fn1在anonymous的上面; 3 紧接着,发现调用了fn2,于是把fn2推入调用栈,于是得到了上面的结果。 如果后续我们继续按f11键调试,会发现Call Stack会依次出现先面的结果: fn2 fn1 (anonymous) fn1 (anonymous) (anonymous) 也就是说最后只剩下了全局匿名函数环境,这里强调一下,anonymous环境将会伴随着程序运行一直存在,除非你关闭了浏览器。 于是我们总结得到这样的结果:存在这样一个调用栈,默认推入一个全局匿名函数在栈底,当此时再调用其它全局函数的时候,会把该函数推入栈,并在anonymous上面,如果该函数内部继续调用了其它函数,那么同样道理,会把其它函数推入栈,放在该函数上面,最后当函数在调用完成的过程中,会依次退出该调用栈,退出的过程中会把权限交给上一层函数,最后又回归了只剩下全局匿名函数环境。于是我们说了这么多,其实这就是函数调用栈! 函数调用栈你可以理解为函数调用前后包含关系:先进后出,后进先出!它描述了代码执行的先后顺序以及当前代码执行控制权限的拥有者关系等。 函数作用域 我们都知道javascript是没有块级作用域的,只有函数作用域,怎么理解?我们看下面的代码: 代码1 for(var k = 0;k<10;k++){ //... }; console.log(k);//输出10 我们发现for循环代码块执行完了之后,依然能得到k的值。再看下面的代码: 代码2 function fn3(){ var a = 'a'; }; fn3(); console.log(a);//报错 a is not defined 我们发现在函数里面定义的变量a,在函数外面是拿不到的,这就是函数作用域能做到的。 函数作用域能让我们定义一些函数内部使用的与外部环境同名的变量而不会跟外部环境冲突,我们用的较多的地方就是即时函数,如下: var a = 'outer'; (function(){ var a = 'inner'; conosle.log(a);//输出inner })(); console.log(a);//输出outer 当然了在es6及后续版本javascript中,我们可以使用let和const标志符来定义块级变量,这里不作讨论! 这里再做一下扩展,作用域中涉及到最多的一个概念就是作用域链,这个是什么意思呢!看下面的代码: let a = 'a'; function fn4(){ let b = 'b'; return a+b; }; let c = fn4(); console.log(c);//输出'ab' 作用域链描述的是一种函数在执行的过程中查找变量的方式,具体来说:函数执行,如果遇到某变量,会首先在自身作用域环境查找改变量,如果不存在,会向上一层作用域查找变量,也就是函数调用栈中当前执行函数的下一层函数环境查找变量,依次类推,直到到全局环境中查找,如果在全局中都没有找到变量的话,那么就会报错! 其实原型链也是一种描述实例属性查找的过程,跟作用域链类似! 闭包 恐怕接触过javascript的开发者,听到最多与其有关的概念就是闭包了。而且网上有很多文章和书籍都对闭包进行了说明,其实我觉的理解闭包并不难哈!我们来看段代码吧: function fn5(){ let a = 0; return function(){ a++; console.log(a); }; }; let fn = fn5(); fn(); 我们在fn调用的地方打一个断点,随后按一次f11键进入fn执行环境,看下Scope项结果,会发现Scope下有一项子项: Closure(fn5) |_ a 我可以明确的告诉你,此时的fn5对于fn来说就是一个闭包(你可以理解是一个环境概念,该环境维护了一些内部返回的匿名函数用到的一些外部变量)! 梳理一下,闭包指得是某个函数调用之后,自身执行环境已不复存在,但返回了一个函数,由于该返回函数内部用到了外部函数里面的一些变量,并又该内部函数又赋值给了其它变量,导致虽然外部函数不存在了,但是它引用的那些外部变量却不能回收的一个环境(闭包)。 闭包用好了,可以保证好多变更的作用域周期得以提升,减少变量命名冲突,但是过多的使用闭包,也会存在内存泄漏的问题,因为你的很多变量都没有被垃圾回收器回收。 进入正题,变量对象 为了让你对变量对象整体有个最初的概念。在详细介绍之前,我对变量对象概念作如此表述:变量对象指函数执行过程中,函数自身用到数据从哪里来的,函数怎么管理这些数据的等等,其实变量对象里面保存了函数在执行过程中所有用到的数据以及某个时刻值等! 时间仓促,后续待更新…… 原文发布时间为:2018年06月21日 原文作者:掘金 本文来源: 掘金 如需转载请联系原作者
前面若干篇文章(链接见文末)已经介绍了重构组件的各种姿势,让我们从“写出一个组件”到“写好组件”有了理论基础。 本文尝试将相关的概念做一个总结,列出一张可用、实用的方法论清单,让我们每次新建组件、修改组件时有章可循,真诚是让一切变好的基础,但实用的套路也是必不可少的。 主要概念 重构:在不改变外部行为的前提下,有条不紊地改善代码 依赖:A 组件的变化会影响 B 组件,就是 B 依赖于 A 耦合:耦合度就是组件之间的依赖性,要尽可能追求松耦合 副作用:除了返回值,还会修改全局变量或参数 纯函数:没有副作用,并针对相同的输入有相同的输出 Q: 为什么要优化、重构?A: 时过、境迁、物是、人非,代码必然变得难以理解 Q: 什么时候需要重构?A: 不光是事后修改自己或他人的代码,从代码新鲜出炉后就应开始 checklist 是否符合单一职责原则 只保留一项最主要的职责 让该职责的输入,依靠 props 获得 把该职责的输出,用 props 中的回调处理 在 propTypes 中写清所有 props 的 类型/结构 及是否必选 用 defaultProps 列出默认值 把另一项相关的职责,用 HOC 提取成组件,并满足上一项职责的输入输出 重复以上步骤,直至完成所有职责 是否和其他组件松耦合 不能将实例引用或 refs 等传给外部,改为提供 props 回调 外部不能调用本组件生命周期或 setState() 等方法,改为提供 props 回调 是否有内部数组、对象等在运行中可能被扩展,改为 props 回调 参考以上几步,反向检查是否直接 依赖/调用 了其他类的实例、方法等 是否直接调用了其他 组件/类 的静态方法,改为 props 注入 在 propTypes 中写清所有 props 的 类型/结构 及是否必选 用 defaultProps 列出默认值 是否可以重用 相同/相似 的逻辑 重复的纯 逻辑/计算 可提取成工具方法,并用可选参数实现通用 涉及界面的重复可封装成通用组件,并用可选 props 实现通用 相似的其他组件,可将差异部分提取为 prop 传入的子组件,实现通用 在 propTypes 中写清所有 props 的 类型/结构 及是否必选 用 defaultProps 列出默认值 组件能否提纯 将全局变量、随机数、new Date / Date.now() 等提取为 props 检查对相同输入是否保证相同输出,重复以上步骤 将网络请求等异步操作提取为 props 回调 检查组件是否有其他副作用,提取为 props 包含回调的生命周期方法是否可以用 HOC 分离出去 在 propTypes 中写清所有 props 的 类型/结构 及是否必选 用 defaultProps 列出默认值 组件命名是否清晰规范 用驼峰拼写法,首字母也大写 用尽可能通俗规范的英文,不用自定义的缩写 写清楚含义,不单纯追求短命名 应用同样的意义不用多种命名 代码含义是否清晰 不使用含糊无意义的变量名等 直接写在代码中的数字要提取成命名清晰的常量 重复以上两步,尽可能少甚至不用注释 确实无法用代码本身解释的业务需求等,用注释解释 修正无意义的或语焉不详的注释 全局性的约定、共识、注释也无法说清的功能,总结到文档中 编写测试 针对重构后的组件,可以轻易编写单元测试了 若编写测试仍遇到问题,重复检查以上所有步骤 重构案例:秒杀商品详情弹窗 用一个小的例子来实践这份清单,虽然不可能每次重构都把上面的 checkbox 画满 √,但基本的流程是相同的。 这是一个既有的组件,在秒杀活动的商品列表中点击某一项时,会在原页面弹出这个组件: //<PROJECT_PATH>/components/spike/PopupItem.jsx import Weui from 'weui'; import * as _ from 'underscore'; import Immutable from 'seamless-immutable'; import React,{Component} from 'react'; import {List, BasePopup} from '../AppLib'; import CountDown from '../CountDown'; import SpikeInfo from './SpikeInfo'; import {i18n, updateSpiked, updateGradeCard} from 'utils/product/util'; export default class PopupItem extends BasePopup { constructor(props) { ... } onClose() { ... } handleVal(e){ console.log(e) } spikeSubmit(e) { ... } render() { ... } componentDidUpdate(prevProps, prevState) { ... } // componentDidMount(){ // let heights=this.refs.innbox, // mEle = heights.firstElementChild; // console.log(heights,mEle) // _.delay(()=>{ // try { // console.log(heights.querySelector('section').offsetHeight) // // console.log(window.getComputedStyle('section').style.height) // // mEle.style.height = 'auto'; // // mEle.style.position = 'relative'; // // rEle.style.overflowY = 'auto'; // // rEle.style.overflowX = 'hidden'; // // mEle.style.height = Math.max(mEle.clientHeight, wh) + 'px'; // let style={ // "height": heights.querySelector('section').offsetHeight+8+'px' // }; // console.log(style) // this.setState({ // style: style // }); // } catch (ex) { // console.log(ex.stack); // } // }, 0); // } componentWillReceiveProps(nextProps) { ... } } step1: 初步清理 重新缩进,手动格式化 删除没有被调用的方法 handleVal(e) 和整段注释掉的过时逻辑 修正不统一的 import 格式等 一处枚举值不为 0 的判断由 _d.type && ... 改为 _d.type != 0 && ... 多处硬编码的中文 “库存:{_d.standard[idx].onhand}”,提取到语言包中 //组件中: {i18n('spike.onhand', _d.standard[idx].onhand)} //语言包 spike: { onhand: '库存:{0}', ... } step2: 理清逻辑 阅读源码->结合文档->通读代码->在浏览器中跑通->询问原作者,理出大致逻辑 在关键的地方先补充上必要的注释 step3: 厘清职责 代码现状分析: componentDidUpdate() 和 this.state.show 控制是否显示整个弹窗组件 onClose() 只用来供外部调用关闭整个弹窗组件 spikeSubmit(e) 只和 render() 中被 2 次渲染的 CountDown 组件关联 除了以上问题,一些弹窗要求的特有样式也混杂在具体组件中 CountDown 所在区域为 <header> 中一块较繁杂的代码,根据条件有两种不同的渲染 根据 gradeRules 和 desc 渲染出了 2 个结构一样的代码段 根据“单一职责”和“重用”的原则,规划新的组件结构如下: 本组件( <PopupItem> )应该只负责组合渲染大致框架 “是否显示” 和 “外部关闭” 等逻辑和特殊样式等“Popup通用组件”相关的逻辑用 HOC 提取,业务组件不用关心 CountDown 所在的头部两种样式的渲染部分及相关逻辑收敛成 <PopupItemHeader> 组件 将原来底部两处重复的渲染提取成 <PopupItemRuleList> 组件 根据以上步骤,分别动手;修改后的代码是这样的: //<PROJECT_PATH>/components/spike/PopupItem.jsx import Weui from 'weui'; import Immutable,{asMutable} from 'seamless-immutable'; import React,{Component} from 'react'; import PropTypes from 'prop-types'; import {assign} from 'underscore'; import {List, BasePopup, makePopup} from '../AppLib'; import PopupItemHeader from './PopupItemHeader'; import PopupItemRuleList from './PopupItemRuleList'; import {i18n} from 'utils/product/util'; export class PopupItemCore extends BasePopup { constructor(props) { super(props); this.state = { data: Immutable(this.props.data) }; } render() { const _d = this.state.data; return <div className="spikeDetail" id="product_detail"> <div className="spikeInner"> {this.getCloseButton()} <PopupItemHeader itemData={_d} /> {_d.isVirtualCard && <span className="vir_msg"> {i18n('spike.virtualCardWarn')}</span> } <PopupItemRuleList styleName="gradeRules" listData={ _d.gradeRules ? asMutable(_d.gradeRules) : null } /> <PopupItemRuleList styleName="desc" listData={ _d.describes ? asMutable(_d.describes) : null } /> </div> </div>; } componentWillReceiveProps(nextProps) { if (this.state.data === nextProps.data) return; this.setState({ data: nextProps.data, idx: nextProps.idx }); } } PopupItemCore.propTypes = { data: PropTypes.instanceOf(Immutable).isRequired }; export default makePopup(PopupItemCore); //<PROJECT_PATH>/components/AppLib.jsx ... /** * 让普通组件具有 onClose 方法 * @private * @description 搭配 PopupOpener 使用 */ function closeHandlerHOC(WrappedComponent) { return class CloseHandlerRefsHOC extends WrappedComponent { constructor(props) { super(props); } onClose() { let _d = this.state.data; this.props.onClose(_d); } }; } /** * 让普通组件具有 打开关闭 逻辑 * @private * @description 搭配 PopupOpener 使用 */ function showLogicHOC(WrappedComponent) { return class ShowLogicRefsHOC extends WrappedComponent { constructor(props) { super(props); this.state = { data: Immutable(this.props.data), show: false }; } componentDidUpdate(prevProps, prevState) { if (this.state.show === prevState.show) { return; } if (this.state.show) { $(this.refs.p_root).popup(); } } }; } /** * 让普通组件符合 PopupOpener 要求的样式 * @private * @description 搭配 PopupOpener 使用 */ function PopupItemHOC(WrappedComponent) { return class PopupItemHOC extends WrappedComponent { render() { return <div className="weui-popup-container" ref="p_root"> <div className="weui-popup-modal"> { super.render() } </div> </div>; } }; } /** * 让普通组件符合 PopupOpener 要求 * @private * @description 搭配 PopupOpener 使用 */ export function makePopup(WrappedComponent) { return showLogicHOC(closeHandlerHOC(PopupItemHOC(WrappedComponent))); } //<PROJECT_PATH>/components/spike/PopupItemRuleList.jsx import React, {Component} from 'react'; import PropTypes from 'prop-types'; const PopupItemRuleList = ({listData, styleName})=>{ return listData ? <div>{listData.map( (item,idx)=>( <div key={idx} className={styleName}> <h4>{item.key}</h4> <div className="cont" dangerouslySetInnerHTML={{__html: item.value}}> </div> </div> ))}</div> : null; }; PopupItemRuleList.propTypes = { listData: PropTypes.arrayOf(PropTypes.shape({ key: PropTypes.string.isRequired, value: PropTypes.string.isRequired })).isRequired, styleName: PropTypes.string.isRequired }; export default PopupItemRuleList; //<PROJECT_PATH>/components/spike/PopupItemHeader.jsx import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {noop} from 'underscore'; import Immutable from 'seamless-immutable'; import CountDown from '../CountDown'; import SpikeInfo from './SpikeInfo'; import {i18n, updateSpiked, updateGradeCard} from 'utils/product/util'; export const PopupItemHeaderCore = ({itemData, countDownCallback})=>{ const _d = itemData; const idx = 0; return <header className={ (_d.isVirtual ? ('coupon_'+ _d.couponType) : '') + ' ' + (_d.pic ? 'nobefore' : '') }> { // <CountDown> 所在的逻辑代码块 } <SpikeInfo ... /> </header> }; PopupItemHeaderCore.propTypes = { itemData: PropTypes.instanceOf(Immutable).isRequired, countDownCallback: PropTypes.func }; PopupItemHeaderCore.defaultProps = { countDownCallback: noop }; const HOC = (WrappedComponent)=>{ class Header extends Component { constructor(props) { super(props); } spikeSubmit(e) { e.stopPropagation(); const newK = this.props.itemData.setIn(['standard', 0, 'count'], 1); const arr = [newK]; if(newK.isGradeCard) { if(newK.isCanBuy != 1) { let warnMsg = i18n('spike.buyTip', newK.productGradeName); if(newK.isCanBuy == 3) { warnMsg = i18n('spike.buyTipNo'); } $.modal({ title: '', text: '<ul id="stockout_dlg_list">'+ warnMsg +'</ul>', buttons: [{ text: i18n('spike.tipBtn'), onClick: noop }] }); return; } updateGradeCard(arr, false); _appFacade.go('product_submit'); } updateSpiked(arr, true); _appFacade.go('product_submit'); }; render() { return <WrappedComponent itemData={this.props.itemData} countDownCallback={this.spikeSubmit.bind(this)} />; } }; Header.propTypes = { itemData: PropTypes.instanceOf(Immutable).isRequired }; return Header; }; export default HOC(PopupItemHeaderCore); 至此,原本的一个文件被按职责隔离拆分开来,也用 PropTypes 等明确了所需的属性和回调等;虽然 PopupItemHeader.jsx 等还有进一步拆分细化的空间,此处按下不表,按此思路照猫画虎即可。 step4: 排除干扰因素 浏览拆分后的代码,虽然结构清晰了许多,但仔细观察会发现,诸如 i18n()、 updateSpiked()、_appFacade.go() 、$.modal() 等外部或全局的方法,不时地混杂其中,分别用以格式化语言字符串、升级本地存储、全局路由跳转或调用自定义弹窗等。 正如在“提纯”的相关文章中所介绍的,这些外部依赖一方面会在测试时造成多余的负担,甚至难以模仿;另一方面也使得组件对于相同输入产生的输出变得不确定。 _appFacade 或 $ 等全局对象从外部注入相对简单,而 updateSpiked、updateGradeCard 这样在模块上下文中引入的部分最难将息;在 React 组件中,可以选择的方法之一是用 props 注入可选值。 此处就以这两个操作本地存储的外部方法为例,完善 PopupItemHeader 中的 HOC 部分: //<PROJECT_PATH>/components/spike/PopupItemHeader.jsx import {noop} from 'underscore'; import {i18n, updateSpiked, updateGradeCard} from 'utils/product/util'; ... const HOC = (WrappedComponent)=>{ class Header extends Component { constructor(props) { super(props); } spikeSubmit(e) { e.stopPropagation(); const newK = this.props.itemData.setIn(['standard', 0, 'count'], 1); const arr = [newK]; if(newK.isGradeCard) { if(newK.isCanBuy != 1) { let warnMsg = this.props.word('spike.buyTip', newK.productGradeName); if(newK.isCanBuy == 3) { warnMsg = this.props.word('spike.buyTipNo'); } $.modal({ title: '', text: '<ul id="stockout_dlg_list">'+ warnMsg +'</ul>', buttons: [{ text: this.props.word('spike.tipBtn'), onClick: noop }] }); return; } this.props.localUpdateGradeCard(arr, false); _appFacade.go('product_submit'); } this.props.localUpdateSpiked(arr, true); _appFacade.go('product_submit'); }; render() { return <WrappedComponent itemData={this.props.itemData} countDownCallback={this.spikeSubmit.bind(this)} />; } }; Header.propTypes = { itemData: PropTypes.instanceOf(Immutable).isRequired, word: PropTypes.func, localUpdateSpiked: PropTypes.func, localUpdateGradeCard: PropTypes.func }; Header.defaultProps = { word: i18n, localUpdateGradeCard: updateGradeCard, localUpdateSpiked: updateSpiked }; return Header; }; export default HOC(PopupItemHeaderCore); step5: 让代码自己说话 基本的结构梳理清楚些了,再看代码好像还是一下子读不懂;仍然以上面的 HOC 为例,首先组件本身在调试工具中的名称也让人摸不清头脑;其次,newK 是什么意思?if(newK.isCanBuy != 1) 在判断个啥?这些如果不去搜索相关的前后端代码,根本无从可知。 根据清单中的命名和注释规则,对其进一步优化: //<PROJECT_PATH>/utils/product/constants.js ... export const BUY_STATUS = { AVAILABLE: 1, //可以购买 HIGH_LEVEL: 2, //等级高于购买的等级 NOT_IN_QUEUE: 3 //没有在升降级规则队列里 }; //<PROJECT_PATH>/components/spike/PopupItemHeader.jsx import {noop} from 'underscore'; import {i18n, updateSpiked, updateGradeCard} from 'utils/product/util'; import {BUY_STATUS} from 'utils/product/constants'; ... const PopupItemHeaderHOC = (WrappedComponent)=>{ class PopupItemHeader extends Component { constructor(props) { super(props); } spikeSubmit(e) { e.stopPropagation(); const firstZeroCountItemData = this.props.itemData.setIn(['standard', 0, 'count'], 1); const itemDataAsList = [firstZeroCountItemData]; if(firstZeroCountItemData.isGradeCard) { if(firstZeroCountItemData.isCanBuy != BUY_STATUS.AVAILABLE) { const WARN = firstZeroCountItemData.isCanBuy == BUY_STATUS.NOT_IN_QUEUE ? this.props.word('spike.buyTipNo') : this.props.word('spike.buyTip', firstZeroCountItemData.productGradeName); $.modal({ title: '', text: '<ul id="stockout_dlg_list">'+ WARN +'</ul>', buttons: [{ text: this.props.word('spike.tipBtn'), onClick: noop }] }); return; } this.props.localUpdateGradeCard(itemDataAsList, false); _appFacade.go('product_submit'); } this.props.localUpdateSpiked(itemDataAsList, true); _appFacade.go('product_submit'); }; render() { return <WrappedComponent itemData={this.props.itemData} countDownCallback={this.spikeSubmit.bind(this)} />; } }; PopupItemHeader.propTypes = { itemData: PropTypes.instanceOf(Immutable).isRequired, word: PropTypes.func, localUpdateSpiked: PropTypes.func, localUpdateGradeCard: PropTypes.func }; PopupItemHeader.defaultProps = { word: i18n, localUpdateGradeCard: updateGradeCard, localUpdateSpiked: updateSpiked }; return PopupItemHeader; }; export default PopupItemHeaderHOC(PopupItemHeaderCore); step6: 编写测试验证更改 现在,这段代码已经改观了很多,虽然过程和结果还称不上是优雅完美的,但无论是可重用性还是可阅读性都得到了改善;在此基础上无论是扩展功能或是复用逻辑都更加有把握了。 心里觉得没问题,浏览器也看过了;可一来手动验证难免百密一疏,对 mock 数据的要求也较高,二来之后再做哪怕一点小改动,理论上也要把之前这些成果再检查一遍。此时要做的就是对新划分好的关键组件,比如 PopupItemHeader、 PopupItemRuleList ,做出单元测试;并将之纳入打包发布工作流中,比如每次 build 或 commit 之前自动检查一遍,就能避免上述的担心。 总结 对于 UI 组件,无论是作为一种特殊的 OOP 实现,或是采纳函数式的组合提纯,都需要尽量减少对外部的依赖、排除改变参数或全局变量的副作用,并尽可能拥有唯一的职责。 总之,重构并非锦上添花,而是软件开发过程中必不可少的工作。 相关文章 抽丝剥茧 - 实例简析重构代码的三板斧 mp.weixin.qq.com/s/dvuMmBnAM… 对 React 组件进行单元测试 mp.weixin.qq.com/s/oE944uljX… 深入 React 高阶组件 mp.weixin.qq.com/s/dtlrOGTjo… 用 SOLID 原则保驾 React 组件开发 mp.weixin.qq.com/s/jxdMzD3sm… 不用祖传秘方 - 写好代码的几个小技巧 mp.weixin.qq.com/s/w2NFvGeV9… 更可靠的 React 组件:单一职责原则 mp.weixin.qq.com/s/M2Liy7NRV… 更可靠的 React 组件:组合及可重用性 mp.weixin.qq.com/s/A3L-q6p_K… 更可靠的 React 组件:提纯 mp.weixin.qq.com/s/Q9Yhdced6… 更可靠的 React 组件:从可测试的到测试通过的 mp.weixin.qq.com/s/9wLF5bmj_… 更可靠的 React 组件:清楚易懂的可表达性 mp.weixin.qq.com/s/dpmab2l-d… (end) 原文发布时间为:2018年06月28日 原文作者:掘金 本文来源: 掘金 如需转载请联系原作者
的确写Proxy文章很多,那么今天我也不凑字数了,炒两个栗子吧。 一、虚拟属性 const person = { name: 'xiaoyun', province: '江苏省', city: '南京市' } 对于上述对象,我们可能需要地址信息(由省市拼接而成),在此之前我们可能会采取下列方式: 直接在person对象上声明一个address属性; 当用到address信息时,再通过person拼接。 第一个方法的主要弊端是污染了原有的对象,而第二种方法就很不灵活。现在我们可以通过Proxy实现比较好的效果: const enhancePerson = new Proxy(person, { get (target, name) { switch (name) { case 'address': return `${target['province']}-${target['city']}` default: return target[name] } } }) enhancePerson.address // 江苏省-南京市 通过这种方式我们就可以实现虚拟属性了,因为它不会被遍历出来: Object.keys(enhancePerson) // [ 'name', 'province', 'city' ] 这里还有一个我觉得比较容易忽略的点: person === enhancePerson // false enhancePerson.city = '苏州市' enhancePerson.city === person.city // true 显然这两个并不是同一个对象,但是我通过改变enhancePerson的city属性却影响到了person的city属性,这就是我们通常会忽略掉的,如果你不设置Proxy的set方法,它会保持默认行为: set (target, propKey, value) { target[propKey] = value } 可能有些同学会想不就是不让它遍历出来吗?看这招: const person = { name: 'xiaoyun', province: '江苏省', city: '南京市', get address () { return `${this.province}-${this.city}` } } const enhancePerson = new Proxy(person, { ownKeys (target) { return Object.keys(target).filter(item => item !== 'address') } }) enhancePerson.address // 江苏省-南京市 Object.keys(enhancePerson) // [ 'name', 'province', 'city' ] 虽然是实现了上述的功能,但是Proxy中的ownKeys拦截的方法太多,所以我们拦截ownKeys之后,会导致某些方法无法使用,并且拦截ownKeys返回的结果也有严格的要求: 返回的结果必须是一个数组 并且数组的元素必须是String或者Symbol类型 结果必须包含对象的所有不可配置、自身拥有的属性 如果对象不能扩展,则结果只能包含自身拥有的属性,不能含有其他属性 所以在拦截方法注意点很多,不然很容易出现问题。 Tip: 未通过defineProperty定义的属性的描述器属性默认为true,否则默认为false。 二、扩展基本操作 当我第一次接触Python时,比较有印象的就是它的List的一个语法: arr[1:3],以前只有羡慕的份,现在完全可以自己撸一个: const arr = [1, 2, 3, 4, 5, 6, 7, 8] const list = new Proxy(arr, { get (target, name) { if (name.includes(':')) { const indexs = name.split(':') return target.slice(indexs[0], indexs[1]) } return target[name] } }) list['2:6'] // [ 3, 4, 5, 6 ] 是不是,对于对象,我们同样可以采用类似的方法: const obj = { a: { b: { c: 'xiaoyun' } } } const obj1 = new Proxy(obj, { get (target, name) { const keys = name.split('.') return keys.reduce((pre, next) => { if (pre !== null && pre !== undefined) { pre = pre[next] } return pre }, target) } }) obj1['a.b.c'] // xiaoyun 原文发布时间为:2018年05月26日 原文作者:云起时 本文来源: 掘金 如需转载请联系原作者
前言 Promise Race 方法是我们在使用 Promise 的时候比较容易使用的一个方法。按照 MDN 对 Promise Race 的定义如下, The Promise.race(iterable) method returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise. 按照其字面意思理解,Promise.race 方法会返回一个 Promise, 这个 Promise 是一个已经被 resolved 的。 其被 resolved 值为最快被 resolved 的 Promise 的值或者被 rejected 的值。 换句话说, 就是给予 Promise.race 一个 Promise 数组作为输入, 其中最快被 resolved 的值作为返回值 Promise 的 resolve 或者 rejected 值。 在 MDN 中所贴出来的代码例子如下: var promise1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, 'one'); }); var promise2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, 'two'); }); Promise.race([promise1, promise2]).then(function(value) { console.log(value); // Both resolve, but promise2 is faster }); // expected output: "two" 容易造成的误解 在上面的代码中,有一句注释,“Both resolve, but promise2 is faster”, 所以期望的结果是 "two"。这里会给我们造成一种错觉,就是哪个promise快,就一定返回其 resolve 值。其实在这里是有一些前提条件的。 Promise.race 一定要尽可能在所定义的 Promise 们 之后调用。 在某些情况下,promise2 就算更快,也不一定返回其值。 下面详细讲一下上面所说的两种容易造成 Promise.race 错误的情况。 Promise.race 一定要尽可能在所定义的 Promise 们 之后调用。 我们稍稍把 MDN 的代码做一些改动,让 Promise.race 不立即执行,而是在下一个执行周期去执行。 var promise1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, 'one'); }); var promise2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, 'two'); }); // 在这里,我使用了一个定时器回调 Promise.race 方法。 // 这个定时器的时间正好为两个 promise 所要等待时间的最长时间,即500ms。 // 这时, console.log(value)的值只和第一个 promise 相关联, // 就算 promise2 比 promise1 快,返回的结果还是 “one” setTimeout(()=> { Promise.race([promise1, promise2]).then(function(value) { console.log(value); }); }, 500) 在某些情况下,promise2 就算更快,也不一定返回其值。 我们再来对 MDN 的代码做一些调整,如下: var promise1 = new Promise(function(resolve, reject) { setTimeout(resolve, 1, 'one'); }); var promise2 = new Promise(function(resolve, reject) { setTimeout(resolve, 0, 'two'); }); Promise.race([promise1, promise2]).then(function(value) { console.log(value); // Both resolve, but promise2 is faster }); 上面的代码比较极端,但是也能反应一些事情。 promise2 依然更快,但是返回的结果确是 “one”。(这个地方很有可能和setTimeout的机制有关,我把 promise1 设置为大于等于2时,返回结果为“two”。希望有知道的大神补充说明一下。我在以后会继续研究setTimeout的相关运行机制。) 原罪 为什么会造成上面的错误结果呢?我们可以先来看看 Promise.race 的实现源代码。 cdn.jsdelivr.net/npm/es6-pro… function race(entries) { /*jshint validthis:true */ var Constructor = this; // this 是调用 race 的 Promise 构造器函数。 if (!isArray(entries)) { return new Constructor(function (_, reject) { return reject(new TypeError('You must pass an array to race.')); }); } else { return new Constructor(function (resolve, reject) { var length = entries.length; for (var i = 0; i < length; i++) { Constructor.resolve(entries[i]).then(resolve, reject); } }); } } 所以 race 的实现原理就是循环遍历 [promise1, promise2, ...], 并按照顺序去 resolve 各个 promise. 注意:这里是按照顺序遍历,所以 race 不是严格意义的公平 race, 也就是说总有人先抢跑。在这里 promise1 首先执行其 executor, 然后在调用 race 的时候,又首先被 Promise.race 遍历。 因此,首先定义的 promise 和放在数组前面的 promise 总是最先具有机会被 resolve。 因此,在这里,如果我们没有考虑到 race 的这种顺序遍历 promise 实例的特性,就有可能得到意外的结果,正如在上面我所列出的反例所得到的结果。 第一个列子中,promise1 理论上在500毫秒后 resolve 结果,promise2 理论上在100毫秒后 resolve 结果。我给 Promise.race 设置了一个500毫秒的timer. 这个500毫秒的 timer 给了 promise1 充分的时间去 resolve 结果,所以就算 promise2 resolve 更快,但是得到的结果还是 promise1 的结果。 而在第二个例子中,我的理解是,当调用 Promise.race 时,根据上面 race 的源代码我们可以知道,race 会通过给 then 传递 resolve 的方式,来把最先完成的 Promise 值给 resolve。 而 then 这种方法是一个异步方法,意思即为调用 then 以后,不管是 resolve,还是 reject 都是在下一个周期去执行,所以这就给了一些短期能够结束的 Promise 机会。这样,如果 Promise 中的 setTimeout 的时间足够短的话,那么在第一次调用 then 时, 前面的 Promise 首先 resolve 掉的话,就算数组后面的 Promise 的 setTimeout 时间更短,那么也只会 resolve 最先 resolved 的值。 结论 为了在使用 Promise 不造成非预期结果,我们需要尽量在定义完 Promise 们后,立即调用 Promise.race。其实,这一条建议也不是完全能保证 Promise.race 能够公平地返回最快 resolve 的值,比如: let promises = []; for (let i = 30000; i > -1; i-- ) { promises.push(new Promise(resolve => { setTimeout(resolve, i, i); })) } Promise.race(promises).then(function(value) { console.log(value); // Both resolve, but promise2 is faster }); 虽然 Promise.race 在定义完所有 promise 后立即调用,但是由于 Promise 巨大的数量,超过一定临界值的话,这时,resolve 出来的值就和遍历顺序以及执行速度有关了。 总之,Promise.race 是顺序遍历,而且通过 then 方法,又把回调函数放入了 event queue 里, 这样, 回调函数又要经历一遍顺序调用,如果 event queue 里的 then 的回调方法都还没有执行完毕的话,那么 Promise.race 则会返回最快的 resolve 值,但是一旦某些执行较快的异步操作在所有 then 回调函数遍历完毕之前就得到了返回结果的话,就有可能造成,异步返回速度虽然快,但是由于在 event queue 中排在较慢的异步操作之后,得到一个意外的返回结果的情况。 原文发布时间为:2018年06月24日 原文作者:Jayden.李 本文来源: 掘金 如需转载请联系原作者
flex布局具有便捷、灵活的特点,熟练的运用flex布局能解决大部分布局问题,这里对一些常用布局场景做一些总结。 web页面布局(topbar + main + footbar) 示例代码 要实现的效果如下: html代码: <div class="container"> <header>header...</header> <main>内容</main> <footer>footer...</footer> </div> css代码: header{ height:100px; background:#ccc; } footer{ height:100px; background:#ccc; } .container{ display:flex; flex-direction:column; height:100vh; } main{ flex-grow:1; } 应用的flex属性 本例中主要应用了以下flex属性: flex-direction:column 使整体布局从上到下排列 flex-grow:1, 应用于main,使得main自动填充剩余空间 本例中应用以较少的css代码实现了传统的上中下页面布局,其中的关键通过flex-grow的使用避免了当main内容过少时footer部分会被提升到页面上方的问题(传统方式上可能需要靠绝对定位来解决了~) 每行的项目数固定并自动换行的列表项 要实现的效果如下: html代码: 示例代码 css代码: ul{ display:flex; flex-wrap:wrap; } li{ list-style:none; flex:0 0 25%; background:#ddd; height:100px; border:1px solid red; } 应用的flex属性 本例中主要应用了以下flex属性: flex:0 0 25%,相当于flex-basis:25%,使得每一个列表项的宽度占外层容器(本例中的ul元素)的25%,因此每行最多能够排开四个列表项。 flex-wrap:wrap,使得每行填满时会自动换行 实现自动划分多余空间的列表项 本例的效果和上例中的图2很相似,只是每行为3个固定宽度的列表项,并且各列表项之间留有相同宽度的空隙 传统方式上实现这种效果,不可避免的要用到负margin,那么现在来看了用flex实现的方式吧 示例代码 css代码: ul{ display:flex; flex-wrap:wrap; justify-content:space-between; border:1px solid black; } li{ list-style:none; width:120px; background:#ddd; height:100px; border:1px solid red; } 应用的flex属性 本例中主要应用了以下flex属性: justify-content:space-between;使主轴方向的多余空间平均分配在两两item之间 平均分配空间的栅格布局 各大UI里栅格布局基本是必备的布局之一,平均分配布局又是栅格布局里最常用的布局,利用flex实现平均分配的栅格布局,关键之处就是利用它的自动收缩空间。 示例 html如下: <div class="row"> <div class="column">column1</div> <div class="column">colum22</div> <div class="column">colum322</div> </div> css如下: .row{ display:flex; flex-wrap:wrap; border:1px solid black; } .column{ list-style:none; background:#ddd; flex:1; height:100px; border:1px solid red; } 应用的flex属性 本例中主要应用了以下flex属性: flex:1 这里针对item应用了flex:1,相当于flex:1 1 0%,而之所以不管各个column元素内容的宽度为多大,都能均分到相等的空间,正式因为相当于在设置了flex-grow:1使得剩余空间按相等比例自动分配的同时又设置了flex-basis:0%,才使得整个空间都平均分配了(更详细的关于[flex]缩写属性,请戳重新认识flex缩写属性—[flex])。 圣杯布局 传统的圣杯布局需要涉及绝对定位、浮动、负margin等几大最头疼属性,有了flex布局以后发现,原来这么简单的配方,也能实现这么复杂的效果哈~ 示例代码 html代码: <div class="container"> <main>main</main> <aside>aside</aside> <nav>nav</nav> </div> css代码: .container{ display:flex; height:100vh; } aside{ width:50px; background:#ccc; } main{ flex-grow:1; background:#def; } nav{ width:80px; background:#ccc; order:-1; } 应用的flex属性 对main用flex-grow:1,使得中间主元素空间自动扩充 对nav应用order:-1,使得order处于最左侧(html中main写在了最前,以利于优先加载主内容区) 元素水平垂直居中 如何让一个元素同时水平垂直居中?答案很多样也很复杂,但是在这么多样化得答案中flex绝对是最简单的一种了~ 示例代码 html代码: <div class="container"> <div class="inner">我是中间的内容</div> </div> css代码: .container{ height:300px; width:300px; border:1px solid red; display:flex; justify-content:center; align-items:center; } .inner{ border:1px solid black; } 应用的flex属性 justify-content:center;使item元素水平居中 align-items:center;使item元素垂直居中 这么多场景都难不倒flex有木有,果然名不虚传~( 想更详细的了解flex相关属性的话,请戳flex入门—了解这些flex属性~) 原文发布时间为:2018年05月29日 原文作者:LT_bear 本文来源: 掘金 如需转载请联系原作者
在上篇文章中我们提到了闭包、对象、以及栈外的其它东西。我们学习的大部分内容都是与特定编程语言无关的元素,但是,我主要还是专注于 JavaScript,以及一些 C。让我们以一个简单的 C 程序开始,它的功能是读取一首歌曲和乐队名字,然后将它们输出给用户: #include <stdio.h> #include <string.h> char *read() { char data[64]; fgets(data, 64, stdin); return data; } int main(int argc, char *argv[]) { char *song, *band; puts("Enter song, then band:"); song = read(); band = read(); printf("\n%sby %s", song, band); return 0; } stackFolly.c 下载 如果你运行这个程序,你会得到什么?(=> 表示程序输出): ./stackFolly => Enter song, then band: The Past is a Grotesque Animal of Montreal => ?ǿontreal => by ?ǿontreal (曾经的 C 新手说)发生了错误? 事实证明,函数的栈变量的内容仅在栈帧活动期间才是可用的,也就是说,仅在函数返回之前。在上面的返回中,被栈帧使用过的内存 被认为是可用的,并且在下一个函数调用中可以被覆写。 下面的图展示了这种情况下究竟发生了什么。这个图现在有一个图片映射(LCTT 译注:译文中无法包含此映射,上下两个矩形区域分别链接至输出的 #47 行和 #70 行),因此,你可以点击一个数据片断去看一下相关的 GDB 输出(GDB 命令在 这里)。只要 read() 读取了歌曲的名字,栈将是这个样子: 在这个时候,这个 song 变量立即指向到歌曲的名字。不幸的是,存储字符串的内存位置准备被下次调用的任意函数的栈帧重用。在这种情况下,read() 再次被调用,而且使用的是同一个位置的栈帧,因此,结果变成下图的样子(LCTT 译注:上下两个矩形映射分别链接至 #76 行和 #79行): 乐队名字被读入到相同的内存位置,并且覆盖了前面存储的歌曲名字。band 和 song 最终都准确指向到相同点。最后,我们甚至都不能得到 “of Montreal”(LCTT 译注:一个欧美乐队的名字) 的正确输出。你能猜到是为什么吗? 因此,即使栈很有用,但也有很重要的限制。它不能被一个函数用于去存储比该函数的运行周期还要长的数据。你必须将它交给 堆,然后与热点缓存、明确的瞬时操作、以及频繁计算的偏移等内容道别。有利的一面是,它是工作 的: 这个代价是你必须记得去 free() 内存,或者由一个垃圾回收机制花费一些性能来随机回收,垃圾回收将去找到未使用的堆对象,然后去回收它们。那就是栈和堆之间在本质上的权衡:性能 vs. 灵活性。 大多数编程语言的虚拟机都有一个中间层用来做一个 C 程序员该做的一些事情。栈被用于值类型,比如,整数、浮点数、以及布尔型。这些都按特定值(像上面的 argc )的字节顺序被直接保存在本地变量和对象字段中。相比之下,堆被用于引用类型,比如,字符串和 对象。 变量和字段包含一个引用到这个对象的内存地址,像上面的 song 和 band。 参考这个 JavaScript 函数: function fn() { var a = 10; var b = { name: 'foo', n: 10 }; } 它可能的结果如下(LCTT 译注:图片内“object”、“string”和“a”的映射分别链接至 #1671行、 #8656 行和 #1264 行): 我之所以说“可能”的原因是,特定的行为高度依赖于实现。这篇文章使用的许多流程图形是以一个 V8 为中心的方法,这些图形都链接到相关的源代码。在 V8 中,仅 小整数 是 以值的方式保存。因此,从现在开始,我将在对象中直接以字符串去展示,以避免引起混乱,但是,请记住,正如上图所示的那样,它们在堆中是分开保存的。 现在,我们来看一下闭包,它其实很简单,但是由于我们将它宣传的过于夸张,以致于有点神化了。先看一个简单的 JS 函数: function add(a, b) { var c = a + b; return c; } 这个函数定义了一个词法域lexical scope,它是一个快乐的小王国,在这里它的名字 a、b、c是有明确意义的。它有两个参数和由函数声明的一个本地变量。程序也可以在别的地方使用相同的名字,但是在 add 内部它们所引用的内容是明确的。尽管词法域是一个很好的术语,它符合我们直观上的理解:毕竟,我们从字面意义上看,我们可以像词法分析器一样,把它看作在源代码中的一个文本块。 在看到栈帧的操作之后,很容易想像出这个名称的具体实现。在 add 内部,这些名字引用到函数的每个运行实例中私有的栈的位置。这种情况在一个虚拟机中经常发生。 现在,我们来嵌套两个词法域: function makeGreeter() { return function hi(name){ console.log('hi, ' + name); } } var hi = makeGreeter(); hi('dear reader'); // prints "hi, dear reader" 那样更有趣。函数 hi 在函数 makeGreeter 运行的时候被构建在它内部。它有它自己的词法域,name 在这个地方是一个栈上的参数,但是,它似乎也可以访问父级的词法域,它可以那样做。我们来看一下那样做的好处: function makeGreeter(greeting) { return function greet(name){ console.log(greeting + ', ' + name); } } var heya = makeGreeter('HEYA'); heya('dear reader'); // prints "HEYA, dear reader" 虽然有点不习惯,但是很酷。即便这样违背了我们的直觉:greeting 确实看起来像一个栈变量,这种类型应该在 makeGreeter() 返回后消失。可是因为 greet() 一直保持工作,出现了一些奇怪的事情。进入闭包(LCTT 译注:“Context” 和 “JSFunction” 映射分别链接至 #188 行和 #7245 行): 虚拟机分配一个对象去保存被里面的 greet() 使用的父级变量。它就好像是 makeGreeter 的词法作用域在那个时刻被关闭closed over了,一旦需要时被具体化到一个堆对象(在这个案例中,是指返回的函数的生命周期)。因此叫做闭包closure,当你这样去想它的时候,它的名字就有意义了。如果使用(或者捕获)了更多的父级变量,对象内容将有更多的属性,每个捕获的变量有一个。当然,发送到 greet() 的代码知道从对象内容中去读取问候语,而不是从栈上。 这是完整的示例: function makeGreeter(greetings) { var count = 0; var greeter = {}; for (var i = 0; i < greetings.length; i++) { var greeting = greetings[i]; greeter[greeting] = function(name){ count++; console.log(greeting + ', ' + name); } } greeter.count = function(){return count;} return greeter; } var greeter = makeGreeter(["hi", "hello","howdy"]) greeter.hi('poppet');//prints "howdy, poppet" greeter.hello('darling');// prints "howdy, darling" greeter.count(); // returns 2 是的,count() 在工作,但是我们的 greeter 是在 howdy 中的栈上。你能告诉我为什么吗?我们使用 count 是一条线索:尽管词法域进入一个堆对象中被关闭,但是变量(或者对象属性)带的值仍然可能被改变。下图是我们拥有的内容(LCTT 译注:映射从左到右“Object”、“JSFunction”和“Context”分别链接至 #1671 行、#7245 行和 #188 行): 这是一个被所有函数共享的公共内容。那就是为什么 count 工作的原因。但是,greeting 也是被共享的,并且它被设置为迭代结束后的最后一个值,在这个案例中是 “howdy”。这是一个很常见的一般错误,避免它的简单方法是,引用一个函数调用,以闭包变量作为一个参数。在 CoffeeScript 中, do 命令提供了一个实现这种目的的简单方式。下面是对我们的 greeter 的一个简单的解决方案: function makeGreeter(greetings) { var count = 0; var greeter = {}; greetings.forEach(function(greeting){ greeter[greeting] = function(name){ count++; console.log(greeting + ', ' + name); } }); greeter.count = function(){return count;} return greeter; } var greeter = makeGreeter(["hi", "hello", "howdy"]) greeter.hi('poppet'); // prints "hi, poppet" greeter.hello('darling'); // prints "hello, darling" greeter.count(); // returns 2 它现在是工作的,并且结果将变成下图所示(LCTT 译注:映射从左到右“Object”、“JSFunction”和“Context”分别链接至 #1671 行、#7245 行和 #188 行): 这里有许多箭头!在这里我们感兴趣的特性是:在我们的代码中,我们闭包了两个嵌套的词法内容,并且完全可以确保我们得到了两个链接到堆上的对象内容。你可以嵌套并且闭包任何词法内容,像“俄罗斯套娃”似的,并且最终从本质上说你使用的是所有那些 Context 对象的一个链表。 当然,就像受信鸽携带信息启发实现了 TCP 一样,去实现这些编程语言的特性也有很多种方法。例如,ES6 规范定义了 词法环境 作为 环境记录( 大致相当于在一个块内的本地标识)的组成部分,加上一个链接到外部环境的记录,这样就允许我们看到的嵌套。逻辑规则是由规范(一个希望)所确定的,但是其实现取决于将它们变成比特和字节的转换。 你也可以检查具体案例中由 V8 产生的汇编代码。Vyacheslav Egorov 有一篇很好的文章,它使用 V8 的 闭包内部构件 详细解释了这一过程。我刚开始学习 V8,因此,欢迎指教。如果你熟悉 C#,检查闭包产生的中间代码将会很受启发 —— 你将看到显式定义的 V8 内容和实例化的模拟。 闭包是个强大的“家伙”。它在被一组函数共享期间,提供了一个简单的方式去隐藏来自调用者的信息。我喜欢它们真正地隐藏你的数据:不像对象字段,调用者并不能访问或者甚至是看到闭包变量。保持接口清晰而安全。 但是,它们并不是“银弹”(LCTT 译注:意指极为有效的解决方案,或者寄予厚望的新技术)。有时候一个对象的拥护者和一个闭包的狂热者会无休止地争论它们的优点。就像大多数的技术讨论一样,他们通常更关注的是自尊而不是真正的权衡。不管怎样,Anton van Straaten 的这篇 史诗级的公案 解决了这个问题: 德高望重的老师 Qc Na 和它的学生 Anton 一起散步。Anton 希望将老师引入到一个讨论中,Anton 说:“老师,我听说对象是一个非常好的东西,是这样的吗?Qc Na 同情地看了一眼,责备它的学生说:“可怜的孩子 —— 对象不过是穷人的闭包而已。” Anton 待它的老师走了之后,回到他的房间,专心学习闭包。他认真地阅读了完整的 “Lambda:The Ultimate…" 系列文章和它的相关资料,并使用一个基于闭包的对象系统实现了一个小的架构解释器。他学到了很多的东西,并期待告诉老师他的进步。在又一次和 Qc Na 散步时,Anton 尝试给老师留下一个好的印象,说“老师,我仔细研究了这个问题,并且,现在理解了对象真的是穷人的闭包。”Qc Na 用它的手杖打了一下 Anton 说:“你什么时候才能明白?闭包是穷人的对象。”在那个时候,Anton 顿悟了。Anton van Straaten 说:“原来架构这么酷啊?” 探秘“栈”系列文章到此结束了。后面我将计划去写一些其它的编程语言实现的主题,像对象绑定和虚表。但是,内核调用是很强大的,因此,明天将发布一篇操作系统的文章。我邀请你 订阅 并 关注我。 原文发布时间为:2018年06月20日 原文作者:掘金 本文来源: 掘金 如需转载请联系原作者
mpvue-calendar 基于vue-calendar的适配mpvue平台的的微信小程序日历组件 预览 安装 npm i mpvue-calendar 使用 import Calendar from 'mpvue-calendar' 引入组件 components中注册组件Calendar template中使用组件<Calendar /> 参数及方法 参数or方法 类型 说明 months Array 自定义月份,不传默认为中文一到十二月 weeks Array 自定义星期,不传默认为中文日到六 value Array 默认选中日期 begin Array 限制开始日期,不传则不限制 end Array 限制结束日期,不传则不限制 disabled Array 禁用日期 events Object 自定义备注 lunar Boolean 是否显示农历,默认为false clean Boolean 是否为简洁模式,简洁模式下自定义备注会显示为圆点,默认为false range Boolean 是否为范围模式,默认为false multi Boolean 是否为多选模式,默认为false select(val, val2) function 日期选中事件,在range模式下val为开始日期、val2为结束日期,其他val为选中日期 setToday() function 返回今日 selectYear(val) function 选择年份事件,val为选中的年份 prev(val) function 选择上一月事件,val为月份 next(val) function 选择下一月事件,val为月份 arrowLeft String 自定义左箭头图片,填写图片路径,不填则使用默认图片 arrowRight String 自定义右箭头图片,填写图片路径,不填则使用默认图片 value 参数 在普通模式下value为一维数组如2018年6月21为[2018,6,21] 在range和multi模式下value为二维数组,如range模式选中2018年6月21到6月28为[[2018,6,21], [2018,6,28]] events 参数 events为自定义备注,例如备注2018年6月21日为{'2018-6-21': '今日备注', '2018-6-22':'明日备注'},在clean模式下备注为圆点,lunar农历模式下不显示备注 disabled 参数 disabled为禁用日期,如禁用2018-6-21日为['2018-6-21'] 示例 <template> <div> <Calendar :months="months" :value="value" @next="next" @prev="prev" :events="events" lunar clean @select="select" ref="calendar" @selectMonth="selectMonth" @selectYear="selectYear" :arrowLeft="arrowLeft" /> <button @click="setToday">返回今日</button> </div> </template> <script> import Calendar from 'mpvue-calendar' import arrowLeft from '../assets/arrowLeft.png' export default { data () { return { months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], disabledarr: ['2018-6-27','2018-6-25'], value: [2018,6,7], begin:[2016,1,1], end:[2020,1,1], events: {'2018-6-7':'今日备注', '2018-6-8':'一条很长的明日备注'}, arrowLeft: arrowLeft } }, components: { Calendar }, methods: { selectMonth(month,year){ console.log(year,month) }, prev(month){ console.log(month) }, next(month){ console.log(month) }, selectYear(year){ console.log(year) }, setToday(val,val1,val2) { this.$refs.calendar.setToday(); }, select(val, val2) { console.log(val) console.log(val2) } } } </script> 原文发布时间为:2018年06月25日 原文作者:飞翔荷兰人 本文来源: 掘金 如需转载请联系原作者
1 引言 本期精读的文章是:JS 引擎基础之 Shapes and Inline Caches 一起了解下 JS 引擎是如何运作的吧! JS 的运作机制可以分为 AST 分析、引擎执行两个步骤: JS 源码通过 parser(分析器)转化为 AST(抽象语法树),再经过 interperter(解释器)解析为 bytecode(字节码)。 为了提高运行效率,optimizing compiler(优化编辑器)负责生成 optimized code(优化后的机器码)。 本文主要从 AST 之后说起。 2 概述 JS 的解释器、优化器 JS 代码可能在字节码或者优化后的机器码状态下执行,而生成字节码速度很快,而生成机器码就要慢一些了。 V8 也类似,V8 将 interpreter 称为 Ignition(点火器),将 optimizing compiler 成为 TurboFan(涡轮风扇发动机)。 可以理解为将代码先点火启动后,逐渐进入涡轮发动机提速。 代码先快速解析成可执行的字节码,在执行过程中,利用执行中获取的数据(比如执行频率),将一些频率高的方法,通过优化编译器生成机器码以提速。 火狐使用的 Mozilla 引擎有一点点不同,使用了两个优化编译器,先将字节码优化为部分机器码,再根据这个部分优化后的代码运行时拿到的数据进行最终优化,生成高度优化的机器码,如果优化失败将会回退到部分优化的机器码。 笔者:不同前端引擎对 JS 优化方式大同小异,后面会继续列举不同前端引擎在解析器、编译器部分优化的方式。 微软的 Edge 浏览器,使用的 Chakra 引擎,优化方式与 Mozilla 很像,区别是第二个最终优化的编译器同时接收字节码和部分优化的机器码产生的数据,并且在优化失败后回退到第一步字节码而不是第二步。 Safari、React Native 使用的 JSC 引擎则更为极端,使用了三个优化编译器,其优化是一步步渐进的,优化失败后都会回退到第一步部分优化的机器码。 为什么不同前端引擎会使用不同的优化策略呢?这是由于 JS 要么使用解释器快速执行(生成字节码),或者优化成机器码后再执行,但优化消耗时间的并不总是小于字节码低效运行损耗的时间,所以有些引擎选择了多个优化编译器,逐层优化,尽可能在解析时间与执行效率中找到一个平衡点。 JS 的对象模型 JS 是基于面向对象的,那么 JS 引擎是如何实现 JS 对象模型的呢?他们用了哪些技巧加速访问 JS 对象的属性? 和解析器、优化器一样,大部分主流 JS 引擎在对象模型实现上也很类似。 ECMAScript 规范确定了对象模型就是一个以字符串为 key 的字典,除了其值以外,还定义了 Writeable Enumerable Configurable 这些配置,表示这个 key 能否被重写、遍历访问、配置。 虽然规范定义了 [[]] 双括号的写法,那这不会暴露给用户,暴露给用户的是 Object.getOwnPropertyDescriptor 这个 API,可以拿到某个属性的配置。 在 JS 中,数组是对象的特殊场景,相比对象,数组拥有特定的下标,根据 ECMAScript 规范规定,数组下标的长度最大为 2³²−1。同时数组拥有 length 属性: length 只是一个不可枚举、不可配置的属性,并且在数组赋值时,会自动更新数值: 所以数组是特殊的对象,结构完全一致。 属性访问效率优化 属性访问是最常见的,所以 JS 引擎必须对属性访问做优化。 Shapes JS 编程中,给不同对象相同的 key 名很常见,访问不同对象的同一个 propertyKey 也很常见: const object1 = { x: 1, y: 2 }; const object2 = { x: 3, y: 4 }; function logX(object) { console.log(object.x); // ^^^^^^^^ } logX(object1); logX(object2);这时 object1 与 object2 拥有一个相同的 shape。拿拥有 x、y 属性的对象来看:如果访问 object.y,JS 引擎会先找到 key y,再查找 [[value]]。 如果将属性值也存储在 JSObject 中,像 object1 object2 就会出现许多冗余数据,因此引擎单独存储 Shape,与真实对象隔离: 这样具有相同结构的对象可以共享 Shape。所有 JS 引擎都是用这种方式优化对象,但并不都称为 Shape,这里就不详细罗列了,可以去原文查看在各引擎中 Shape 的别名。 Transition chains 和 Transition trees 如果给一个对象增加了 key,JS 引擎如何生成新的 Shape 呢? 这种 Shape 链式创建的过程,称为 Transition chains: 开始创建空对象时,JSObject 和 Shape 都是空,当为 x 赋值 5 时,在 JSObject 下标 0 的位置添加了 5,并且 Shape 指向了拥有字段 x 的 Shape(x),当赋值 y 为 6 时,在 JSObject 下标 1 的位置添加了 6,并将 Shape 指向了拥有字段 x 和 y 的 Shape(x, y)。 而且可以再优化,Shape(x, y) 由于被 Shape(x) 指向,所以可以省略 x 这个属性: 笔者:当然这里说的主要是优化技巧,我们可以看出来,JS 引擎在做架构设计时没有考虑优化问题,而在架构设计完后,再回过头对时间和空间进行优化,这是架构设计的通用思路。 如果没有连续的父 Shape,比如分别创建两个对象: const object1 = {}; object1.x = 5; const object2 = {}; object2.y = 6; 这时要通过 Transition trees 来优化: 可以看到,两个 Shape(x) Shape(y) 别分继承 Shape(empty)。当然也不是任何时候都会创建空 Shape,比如下面的情况: const object1 = {}; object1.x = 5; const object2 = { x: 6 }; 生成的 Shape 如下图所示: 可以看到,由于 object2 并不是从空对象开始的,所以并不会从 Shape(empty) 开始继承。 Inline Caches 大概可以翻译为“局部缓存”,JS 引擎为了提高对象查找效率,需要在局部做高效缓存。 比如有一个函数 getX,从 o.x 获取值: function getX(o) { return o.x; } JSC 引擎 生成的字节码结构是这样的: get_by_id 指令是获取 arg1 参数指向的对象 x,并存储在 loc0,第二步则返回 loc0。 当执行函数 getX({ x: 'a' }) 时,引擎会在 get_by_id 指令中缓存这个对象的 Shape: 这个对象的 Shape 记录了自己拥有的字段 x 以及其对应的下标 offset: 执行 get_by_id 时,引擎从 Shape 查找下标,找到 x,这就是 o.x 的查找过程。但一旦找到,引擎就会将 Shape 保存的 offset 缓存起来,下次开始直接跳过 Shape 这一步: 以后访问 o.x 时,只要 Shape 相同,引擎直接从 get_by_id 指令中缓存的下标中可以直接命中要查找的值,而这个缓存在指令中的下标就是 Inline Cache. 数组存储优化 和对象一样,数组的存储也可以被优化,而由于数组的特殊性,不需要为每一项数据做完整的配置。 比如这个数组: const array = ["#jsconfeu"]; JS 引擎同样通过 Shape 与数据分离的方式存储: JS 引擎将数组的值单独存储在 Elements 结构中,而且它们通常都是可读可配置可枚举的,所以并不会像对象一样,为每个元素做配置。 但如果是这种例子: // 永远不要这么做 const array = Object.defineProperty([], "0", { value: "Oh noes!!1", writable: false, enumerable: false, configurable: false }); JS 引擎会存储一个 Dictionary Elements 类型,为每个数组元素做配置: 这样数组的优化就没有用了,后续的赋值都会基于这种比较浪费空间的 Dictionary Elements 结构。所以永远不要用 Object.defineProperty 操作数组。 通过对 JS 引擎原理的认识,作者总结了下面两点代码中的注意事项: 尽量以相同方式初始化对象,因为这样会生成较少的 Shapes。 不要混淆对象的 propertyKey 与数组的下标,虽然都是用类似的结构存储,但 JS 引擎对数组下标做了额外优化。 3 精读 这次原理系列解读是针对 JS 引擎执行优化这个点的,而网页渲染流程大致如下: 可以看到 Script 在整个网页解析链路中位置是比较靠前的,JS 解析效率会直接影响网页的渲染,所以 JS 引擎通过解释器(parser)和优化器(optimizing compiler)尽可能 对 JS 代码提效。 Shapes 需要特别说明的是,Shapes 并不是 原型链,原型链是面向开发者的概念,而 Shapes 是面向 JS 引擎的概念。 比如如下代码: const a = {}; const b = {}; const c = {}; 显然对象 a b c 之间是没有关联的,但共享一个 Shapes。 另外理解引擎的概念有助于我们站在语法层面对立面的角度思考问题:在 JS 学习阶段,我们会执着于思考如下几种创建对象方式的异同: const a = {}; const b = new Object(); const c = new f1(); const d = Object.create(null); 比如上面四种情况,我们要理解在什么情况下,用何种方式创建对象性能最优。 但站在 JS 引擎优化角度去考虑,JS 引擎更希望我们都通过 const a = {} 这种看似最没有难度的方式创建对象,因为可以共享 Shape。而与其他方式混合使用,可能在逻辑上做到了优化,但阻碍了 JS 引擎做自动优化,可能会得不偿失。 Inline Caches 对象级别的优化已经很极致了,工程代码中也没有机会帮助 JS 引擎做得更好,值得注意的是不要对数组使用 Object 对象下的方法,尤其是 defineProperty,因为这会让 JS 引擎在存储数组元素时,使用 Dictionary Elements 结构替代 Elements,而 Elements 结构是共享 PropertyDescriptor 的。 但也有难以避免的情况,比如使用 Object.defineProperty 监听数组变化时,就不得不破坏 JS 引擎渲染了。 笔者写 dob 的时候,使用 proxy 监听数组变化,这并不会改变 Elements 的结构,所以这也从另一个侧面证明了使用 proxy 监听对象变化比 Object.defineProperty 更优,因为 Object.defineProperty 会破坏 JS 引擎对数组做的优化。 4 总结 本文主要介绍了 JS 引擎两个概念: Shapes 与 Inline Caches,通过认识 JS 引擎的优化方式,在编程中需要注意以下两件事: 尽量以相同方式初始化对象,因为这样会生成较少的 Shapes。 不要混淆对象的 propertyKey 与数组的下标,虽然都是用类似的结构存储,但 JS 引擎对数组下标做了额外优化。 原文发布时间为:2018年06月25日 原文作者:黄子毅本文来源: 掘金 如需转载请联系原作者
为什么要做 今年4月份,开始自己的第二份工作,习惯了老东家如丝般的发布体验,一下子感觉来到了“原始社会”。 首先这是一篇长文,主要介绍自己在做自动发布这个过程的一些思考。 引用玉伯的Web研发模式演变来说,现在我们处于 - Web1.0时代: 前后端代码耦合 java环境对前端过于复杂 缺乏工具和规范,代码难维护 内嵌代码:html内嵌js,jsp内嵌java逻辑 页面级代码,代码叠加:单文件js随意2000行以上 人工手动发布,变更麻烦 遇到这种情况,首先会想到的肯定是前后端分离。但考虑到目前的人员、技术储备情况,直接过渡到基于NodeJS的全栈式开发,阻力大,周期长,很可能会难产。 而我们首要要解决的问题是: 前后端职责清晰 提升开发效率、体验 自动化发布 所以我们暂时先做到前后端物理分离,大致如 - Web2.0时代 代码仓库分离,分开维护 发布部署分离 模板由前端维护,在浏览器渲染,后端只提供基础页面容器(视情况而定) 交互性、非SEO页面:后端负责接口,前端负责展现,通过接口读取数据在浏览器端渲染 需要SEO的页面:相关模板还是放在后端,但是会减少业务逻辑 目标 我们先从开发、发布流程来看我们最终希望的结果是什么,然后再分析如何完成这一目标 开发流程 项目遵循流程:需求评审 -> 视觉评审 -> 接口约定 -> 需求评估 -> TC评审 -> 并行独立开发 -> 联调 -> 测试 -> 发布 开发过程前后端明确任务,独立并行开发 发布流程 发布要严格遵守流程,测试审核通过才能上线 整个流程只需简单发布指令,所有的编译构建、同步服务器的事情交给任务去做(后面我们会提到发布任务需要做哪些事情) 分离需要做什么 代码分离 使用git来做代码版本管理,申请新应用维护前端代码 使用webpack,做模块管理 代码分离后,我们可以使用目前前端主流的框架、工具,搭建友好的开发环境、流程 分离之后,请求后端接口,联调、debug,都需要设置代理 自动化发布 服务器配置 考虑如何部署前端代码 自动化发布 首先聊一下一般发布的流程: 代码提交 打包构建 备份服务器当前文件 - 回滚使用 将构建结果同步到服务器目录 合并代码到Master - 保证后续的代码都是最新的 这是一些纯体力活,要保证步骤顺序和正确性,容易出问题,而且没有记录和日志,所以一般做权限控制,发布个普通需求还要找对应的同学发布,变更麻烦 所以发布必须自动化,网上搜前端自动化发布,大多数的结果都是Jenkins + githook ( Jenkins+github 前端自动化部署) 其核心原理主要是通过 提交代码触发webhook push event Jenkins监听到webhook post请求,执行编写好的脚本构建、同步服务器(主要依赖于脚本) 但是如果我想要查看发布记录、回滚、控制发布流程,看起来Jenkins就帮不上忙了(可能有对应的插件,没深究) 同样的发布脚本,用node也能执行,所以我们打算使用node来写一个发布集成服务来代替Jenkins,它可以做更细致的控制: 提交代码不代表发布,可能只是代码备份,发布指令才代表发布 可以生成发布记录,在发布平台展示,方便查看和回滚 实时反馈发布流程信息 控制发布流程,加入审核、CodeReview,让发布更安全 所以我们的发布自动化主要做三个东西 CLI:让熟悉命令行的同学,git push后马上就可以敲命令发布(创建新发布、发布) 发布平台:查看发布记录,发布,审核,查看日志,回滚 集成发布服务:执行发布脚本,同步服务器,备份近期发布文件(快速回滚),反馈发布信息,发布控制 CLI 该CLI是一套标准的前端开发生命周期命令,通过几个子命令去完成前端开发流程的各个任务,包含了: init:初始化项目结构,类似于vue-cli init,不过比较入门简单(因为暂时业务的体量并不需要频繁创建新项目) dev:启动开发调试服务,主要是npm run dev,也不是重点 publish:发布项目代码,执行publish后将执行项目仓库中对应开发分支下的代码发布任务。在云端构建后的代码最终会发布到对应的环境(SIT、UAT、生产)。 关于CLI的开发参考 - 如何用Node开发CLI 主要是:commander + inquirer 从此发布就变成了一个命令的事 发布平台 发布平台提供了比CLI更多的功能: 查看发布记录 查看日志 回滚 发布管理、控制 集成发布服务 到了重头戏,这里就介绍一些概念 发布流程 为什么在云端构建发布 首先,最终代码部署到服务器肯定都是通过scp等命令来同步文件到服务器,因为权限问题,通过云端统一管控是比较靠谱的。 然后,每个人的机器环境都不一样,有可能在A这构建成功,在B那却构建失败(比如A添加了一个依赖,但没有保存dependencies),所以以统一的环境来编译构建,可以避免因为环境问题引起的构建问题。 最后,需要一个地方去统一管理发布记录,避免发布冲突,记录发布日志,方便回滚操作等。 分支管理 每个人都基于Master拉取自定义分支开发,最终通过发布自动同步到Master分支,保证开发时都是基于最新的线上代码,同时发布时做冲突检查,保证发布安全。 发布过程的分支变化如下: 发布管理 在整个发布过程,我们的代码要通过日常、预发测试才能最终上线,这个过程是需要占用对应服务器并保持稳定,需要避免出现其他同学发布覆盖的情况,所以我们使用MongoDB来维护发布记录,实现发布控制和流程控制 发布控制 当指定发布环境有一个项目发布时,该环境被占用,其他项目发布会提示有其他项目发布,联系对应的发布同学,双方根据重要性来决定是否退出发布让后来的项目先上 流程控制 为了保证最终上线的代码是正确运行的,整个过程需要测试和Code Review,必须通过测试、审核才能进入下一个环节 发布反馈 发布脚本需要执行上面提到一系列的过程,这需要一个等待的过程,我们需要实时给发布同学提供发布反馈(发布流程、成功与否),并将相关信息保存到日志。所以发布过程通过soket.io建立socket链接,集成发布服务有任何流程变化都会反馈 同步服务器 同步服务器可以使用 scp 或 rsync 命令,关于它们的介绍和不同看这个 这两个命令通过ssh同步,都需要在执行命令后输入密码,所以需要配合expect来实现自动同步 原文发布时间为:2018年06月16日 原文作者:okbeng03本文来源: 掘金 如需转载请联系原作者
我收听了一个很棒的语法FM播客,它总结了有用的JavaScript 数组和对象方法。这些方法可以帮助开发人员编写干净而且可读性高的代码。许多这样的方法可以减少像Lodash这样的实用程序库的依赖。 本文中的所有方法都是可链式调用的,这意味着它们可以相互结合使用,而且它们也不会发生数据突变,这在使用React时尤其重要。 使用所有这些数组和对象的方法,你将发现再也不需要再进行 for 或 while 循环来达到目的了。 .filter() 查看MDN文档 根据数组元素是否通过特定条件测试来创建新数组。 示例 创建一个符合法定饮酒年龄的学生年龄数组。 const studentsAge = [17, 16, 18, 19, 21, 17]; const ableToDrink = studentsAge.filter( age => age > 18 ); // ableToDrink 将等于 [19, 21] .map() 查看MDN文档 通过操作一个数组中的值来创建一个新数组。 这个方法非常适合数据操作,它经常用于React,因为它是一种不可变的方法。 示例 创建一个数组,在每个数字的开头添加一个$。 const numbers = [2, 3, 4, 5]; const dollars = numbers.map( number => '$' + number); // dollars 将等于 ['$2', '$3', '$4', '$5'] .reduce() 查看MDN文档 这是一个经常被忽视的方法,使用一个累加器将数组中的所有元素项减少为单个值。非常适合计算总数。返回的值可以是任何类型(即对象,数组,字符串,整数)。 示例 把数组中的整数加起来。 const numbers = [5, 10, 15]; const total = numbers.reduce( (accumulator, currentValue) => accumulator + currentValue); // total 将等于 30 在MDN文档中,列出的 .reduce() 一些非常酷的用例,比如如何执行平铺数组,通过属性对对象分组,以及删除数组中的重复项等操作示例。 .forEach() 查看MDN文档 在数组中的每个元素项上应用一个函数。 示例 将每个数组元素项记录到控制台 const emotions = ['happy', 'sad', 'angry']; emotions.forEach( emotion => console.log(emotion) ); // 控制台将打印: // 'happy' // 'sad' // 'angry' .some() 查看MDN文档 检查数组中的任何元素是否通过该条件。一个好的用例是检查用户权限。它也可以类似于.forEach(),您可以在每个数组项上执行操作,并在返回真值(truthy)后跳出循环。 示例 检查数组中是否至少有一个’admin’。 const userPrivileges = ['user', 'user', 'user', 'admin']; const containsAdmin = userPrivileges.some( element => element === 'admin'); // containsAdmin 将等于 true .every() 查看MDN文档 与 .some() 类似,但检查数组中的所有项是否通过条件。 示例 检查所有评级是否等于或大于3星。 const ratings = [3, 5, 4, 3, 5]; const goodOverallRating = ratings.every( rating => rating >= 3 ); // goodOverallRating 将等于 true .includes() 查看MDN文档 检查数组是否包含某个值。它类似于 .some() ,但是它不是寻找要传递的条件,而是查看数组是否包含一个特定的值。 示例 检查数组是否包含字符串 ‘waldo’ 元素项。 const names = ['sophie', 'george', 'waldo', 'stephen', 'henry']; const includesWaldo = names.includes('waldo'); // includesWaldo 将等于 true Array.from() 查看MDN文档 这是一种基于另一个数组或字符串创建数组的静态方法。 您还可以将 map回调函数作为参数,新数组中的每个元素会执行该回调函数。 老实说,我不太确定为什么有人会通过 .map() 方法使用它。 示例 从一个字符串创建一个数组。 const newArray = Array.from('hello'); // newArray 将等于 ['h', 'e', 'l', 'l', 'o'] 创建一个数组,该数组的值是另一个数组中每个值的两倍。 const doubledValues = Array.from([2, 4, 6], number => number * 2); // doubleValues 将等于 [4, 8, 12] Object.values() 查看MDN文档 返回一个对象属性值的数组。 示例 const icecreamColors = { chocolate: 'brown', vanilla: 'white', strawberry: 'red', } const colors = Object.values(icecreamColors); // colors 将等于 ["brown", "white", "red"] Object.keys() 查看MDN文档 返回一个对象属性名的数组。 示例 const icecreamColors = { chocolate: 'brown', vanilla: 'white', strawberry: 'red', } const types = Object.keys(icecreamColors); // types 将等于 ["chocolate", "vanilla", "strawberry"] Object.entries() 查看MDN文档 创建一个数组,其中包含一个对象的键/值对数组。 示例 const weather = { rain: 0, temperature: 24, humidity: 33, } const entries = Object.entries(weather); // entries 将等于 // [['rain', 0], ['temperature', 24], ['humidity', 33]] Array spread (数组展开) 查看MDN文档 使用展开运算符( ... )展开数组,允许你展开数组中的元素。 将一堆数组连接在一起时非常有用。 当从数组中删除某些元素时,避免使用 splice() 方法也是一种好方法,因为它可以与 slice() 方法结合使用,以防止数组的直接突变。 示例 组合两个数组。 const spreadableOne = [1, 2, 3, 4]; const spreadableTwo = [5, 6, 7, 8]; const combined = [...spreadableOne, ...spreadableTwo]; // combined 将等于 [1, 2, 3, 4, 5, 6, 7, 8] 删除一个数组元素而不改变原始数组。 const animals = ['squirrel', 'bear', 'deer', 'salmon', 'rat']; const mammals = [...animals.slice(0,3), ...animals.slice(4)]; // mammals 将等于 ['squirrel', 'bear', 'deer', 'rat'] Object spread (对象展开) 查看MDN文档 展开一个对象,允许向一个对象添加新的属性和值,而不突变(比如创建一个新对象),也可以将多个对象组合在一起。应该注意的是,展开对象不会进行嵌套复制。 示例 添加一个新的对象属性和值,而不会改变原始对象。 const spreadableObject = { name: 'Robert', phone: 'iPhone' }; const newObject = { ...spreadableObject, carModel: 'Volkswagen' } // newObject 将等于 // { carModel: 'Volkswagen', name: 'Robert', phone: 'iPhone' } Function Rest (剩余参数) 查看MDN文档 函数可以使用 rest(剩余)参数语法接受任意数量的参数作为数组。 示例 显示传递参数的数组。 function displayArgumentsArray(...theArguments) { console.log(theArguments); } displayArgumentsArray('hi', 'there', 'bud'); // 将打印 ['hi', 'there', 'bud'] Object.freeze() 查看MDN文档 防止你修改现有的对象属性或向对象添加新的属性和值。通常人们认为 const 会这样做,但是 const 允许你修改对象。 示例 冻结对象以防止更改 name 属性。 const frozenObject = { name: 'Robert' } Object.freeze(frozenObject); frozenObject.name = 'Henry'; // frozenObject 将等于 { name: 'Robert' } Object.seal() 查看MDN文档 拒绝将任何新属性添加到对象,但允许更改现有属性。 示例 封闭一个对象,以防止添加 wearsWatch 属性。 const sealedObject = { name: 'Robert' } Object.seal(sealedObject); sealedObject.name = 'Bob'; sealedObject.wearsWatch = true; // sealedObject 将等于 { name: 'Bob' } Object.assign() 查看MDN文档 允许对象组合在一起。不再需要这个方法,因为您可以使用上面说的对象展开语法。与对象展开操作符一样,Object.assign() 也不会执行深层克隆。当谈到深度克隆对象时,Lodash 是你最好的朋友。 示例 将两个对象组合成一个。 const firstObject = { firstName: 'Robert' } const secondObject = { lastName: 'Cooper' } const combinedObject = Object.assign(firstObject, secondObject); // combinedObject 将等于 { firstName: 'Robert', lastName: 'Cooper' } 原文发布时间为:2018年06月20日 原文作者:掘金 本文来源: 掘金 如需转载请联系原作者
一、前言 当我们的应用遇到多个组件共享状态时,会需要多个组件依赖于同一状态。传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。在搭建下面页面时,你可能会对 vue 组件之间的通信感到崩溃 ,特别是非父子组件之间通信。此时就应该使用vuex,轻松可以搞定组件间通信问题。 二、什么是Vuex Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。这里的关键在于集中式存储管理。这意味着本来需要共享状态的更新是需要组件之间通讯的,而现在有了vuex,就组件就都和store通讯了。 三、什么时候使用Vuex 虽然 Vuex 可以帮助我们管理共享状态,但也附带了更多的概念和框架。这需要对短期和长期效益进行权衡。 如果您的应用够简单,您最好不要使用 Vuex,因为使用 Vuex 可能是繁琐冗余的。一个简单的 global event bus 就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。 四、Vuex安装(限定开发环境为 vue-cli) 首先要安装vue-cli脚手架,对于大陆用户,建议将npm的注册表源设置为国内的镜像(淘宝镜像),可以大幅提升安装速度。 npm config set registry https://[registry.npm.taobao.org](http://registry.npm.taobao.org/) npm config get registry//配置后可通过下面方式来验证是否成功 npm install -g cnpm --registry=[https://registry](https://registry/).npm.taobao.org //cnpm安装脚手架 cnpm install -g vue-cli vue init webpack my-vue cd my-vue cnpm install cnpm run dev 脚手架安装好后,再安装vuex cnpm install vuex --save 五、如何使用Vuex 1.如何通过Vuex来实现如下效果? ①创建一个store.js文件 import Vue from "vue" import Vuex from "vuex" Vue.use(Vuex) const store = new Vuex.Store({ state: { //这里的state必须是JSON,是一个对象 count: 1 //这是初始值 }, mutations: {//突变,罗列所有可能改变state的方法 add(state) { state.count++; //直接改变了state中的值,而并不是返回了一个新的state }, reduce(state){ state.count--; } } }); export default store;//用export default 封装代码,让外部可以引用 ②在main.js文件引入store.js文件 import store from "./vuex/store" new Vue({ router, store, el: '#app', render: h => h(App) }) ③新建一个模板count.vue <template> <div> <h2>{{msg}}</h2><hr/> <h2>{{$store.state.count}}-{{count}}</h2>//这两种写法都可以 <button @click="addNumber">+</button> <button @click="reduceNumber">-</button> </div> </template> <script> import {mapState} from 'vuex' export default { data() { return { msg: "Hello Vuex" }; }, methods: { addNumber() { return this.$store.commit("add"); }, reduceNumber() { return this.$store.commit("reduce"); } }, computed: mapState(['count'])// 映射 this.count 到 this.$store.state.count mapState 函数可以接受一个对象,也可以接收一个数组 }; </script> 由于 store 中的状态是响应式的,当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。改变store 中的状态的唯一途径就是显式地提交 (commit) mutations。 这样,我们就可以实现自增1或是自减1,那假如我们想要点击一次实现自增2,这该如何实现,也就是如何向Mutations传值? 2.如何在Mutations里传递参数 先store.js文件里给add方法加上一个参数n mutations: { add(state,n) { state.count+=n; }, reduce(state){ state.count--; } } 然后在Count.vue里修改按钮的commit( )方法传递的参数 addNumber() { return this.$store.commit("add",2); }, reduceNumber() { return this.$store.commit("reduce"); } 3.getters如何实现计算过滤操作 getters从表面是获得的意思,可以把他看作在获取数据之前进行的一种再编辑,相当于对数据的一个过滤和加工。你可以把它看作store.js的计算属性。 例如:要对store.js文件中的count进行操作,在它输出前,给它加上100。 首先要在store.js里Vuex.Store()里引入getters getters:{ count:state=>state.count+=100 } 然后在Count.vue中对computed进行配置,在vue 的构造器里边只能有一个computed属性,如果你写多个,只有最后一个computed属性可用,所以要用展开运算符”…”对上节写的computed属性进行一个改造。 computed: { ...mapState(["count"]), count() { return this.$store.getters.count; } } 需要注意的是,此时如果点击'+',就会增加102,如果点击'-',就会增加99,最后的结果是mutations和getters共同作用的。 4.actions如何实现异步修改状态 actions和上面的Mutations功能基本一样,不同点是,actions是异步的改变state状态,而Mutations是同步改变状态。 ①在store.js中声明actions actions: { addAction(context) { context.commit('add', 2);//一开始执行add,并传递参数2 setTimeout(() => { context.commit('reduce') }, 2000);//两秒后会执行reduce console.log('我比reduce提前执行'); }, reduceAction({ commit }) { commit('reduce') } } actions是可以调用Mutations里的方法的,调用add和reduce两个方法。在addAction里使用setTimeOut进行延迟执行。在actions里写了两个方法addAction和reduceAction,两个方法传递的参数也不一样。 context:上下文对象,这里你可以理解称store本身。 {commit}:直接把commit对象传递过来,可以让方法体逻辑和代码更清晰明了 ②模板中的使用 <p> <button @click="addNumber">+</button> <button @click="reduceNumber">-</button> </p> <p> <button @click="addAction">+</button>//新增 <button @click="reduceAction">-</button>//新增 </p> import { mapState, mapGetters, mapActions } from "vuex"; methods:{ ...mapMutations([ 'add','reduce' ]), ...mapActions(['addAction','reduceAction']) } 最后得到如下效果:点击addAction按钮事件时,先累加2,两秒后再减去1,而addNumber事件则不能实现异步效果。 ps:如果想访问源代码,请猛戳git地址 如果觉得本文对您有用,请给本文的github加个star,万分感谢 参考文章 vuex官方文档 Vuex 2.0 源码分析 技术胖的vuex视频教程 原文发布时间为:2018年06月16日 原文作者:浪里行舟 本文来源: 掘金 如需转载请联系原作者
实现思路 在页面放置img标签 给img图片加上alt, width, height 和 data-src 通过js判断页面是否滚动到某张图片需要显示的位置,这时将src赋值为data-src offsetTop方式 图片出现在视窗内的情况: offsetTop < clientHeight + scrollTop getBoundingClientRect方式 图片出现在视窗内的情况: element.getBoundingClientRect().top < clientHeight h5的IntersectionObserver方式 intersectionRatio:目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为 1 ,完全不可见时小于等于 0 具体用法如下: function getTag(tag) { return Array.from(document.getElementsByTagName(tag)); } var observer = new IntersectionObserver( (changes) => { changes.forEach((change) => { if (change.intersectionRatio > 0) { var img = change.target; img.src = img.dataset.src; observer.unobserve(img); } }) } ) getTag('img').forEach((item) => { observer.observe(item); }) 优化 通过以下css可以提高性能 # 之所以使用visibility而不是display是因为 # visibility不会触发重绘(repaint)和重排(reflow) img { visibility: hidden; } img[src] { visibility: visible; } 因为scroll事件的触发频率很高,频繁操作dom结点会造成很大的性能问题,所以需要做节流和防抖设计,减少scroll事件的触发频率 原文发布时间为:2018年06月26日 原文作者:linlixy 本文来源: 掘金 如需转载请联系原作者
Weex 上一篇文章讲到了混合应用简单的发展史,本文以Weex为例分析一下混合应用,本文并非是介绍Weex是怎么使用的,如果想要了解怎么使用,不如了解一下 Eros 的解决方案,主要想剖析一下Weex的原理,了解Weex的运行机制。 为什么要选择 Weex 首先想聊一聊我们为什么选择Weex。上一篇文章结尾对Weex和ReactNative进行了简要的分析,在我们做技术选型时大环境下RN不管从哪方面来说都是一个更好的方案,更多的对比可以去 weex&ReactNative对比 看看,在做技术选型的时候也在不断的问,为什么?最后大概从下面几个方面得到了一个相对好的选择。 Weex 的优缺点 首先肯定需要看看优缺点,优点用来判断自己的场景适不适合做这个技术,缺点来看自己的场景会不会被限制住,有没有办法解决和绕开。 优点: js 能写业务,跨平台,热更新 Weex 能用 Vue 的 framework,贴近我们的技术栈 Weex 比 RN 更轻量,可以分包,每个页面一个实例性能更好 Weex 解决了 RN 已经存在的一些问题,在 RN 的基础上进行开发 有良好的扩展性,比较好扩展新的 Component 和 Module 缺点: 文档不全,资料少,社区几乎等于没有,issue 堆积,后台 issue 的方式改到了 JIRA 上,很多开发者都不了解 bug 多,不稳定,遇到多次断崖式更新 Component 和 Module 不足以覆盖功能 其实总结起来就是起步晚的国产货,优点就不赘述了。主要看缺点会不会限制住业务场景,有没有对应的解决方案。 相关资料比较少,好在能看到源码,有了源码多花点时间琢磨,肯定是能继续下去的,顺着源码看过去,文档不全的问题也解决了,主要是发现了Weex提供了非常多文档上没有写的好属性和方法。 项目起步比较晚,bug比较多,更新也是断崖式的,我们最后采用源码集成的方法,发现有bug就修源码,并给官方提PR,我们团队提的很多PR也被官方采纳,主要还是每次版本更新比较浪费时间,一方面要看更新日志,还要对源码进行diff,如果官方已经修复了就删除我们自己的补丁。这块确实是会浪费时间一点,但是RN想要自己扩展也是需要经历这个阵痛的。 提供的Component和Module不足以完成业务需求,当然官方也提供了扩展对应插件化的方式,尝试扩展了几个插件具备原生知识扩展起来也比较快,并且我们一开始就决定尽量少用官方的Module,尽量Module都由我们的客户端自己扩展,一方面不会受到官方的Module bug或者不向下兼容时的影响,另一方面在扩展原生Module的同时能了解其机制,还能让扩展的Module都配合我们的业务。 接入成本与学习成本 我们主要的技术栈是围绕着Vue建立的,自己做了统一的脚手架,已经适配了后台系统、微信公众号、小程序、自助机等多端的项目,就差APP的解决方案了,如果能用Vue的基础去接入,就完善了整个前端技术链,配合脚手架和Vue的语法基础项目间的切换成本就会很低,开发效率会很高。 基于Vue的技术栈,让我们写业务的同学能很快适应,拆分组件,widget插件化,mixins这些相关的使用都能直接用上,剩下需要学习的就是Weex的Component和Module的使用及css的支持性,我们脚手架接入之后也直接支持sass/less/styule,整个过程让新同学上手,半天的时候见能搭建出一个完整的demo页面,上手开发很快。总体来说,成本对于我们来说是一个大的优势 开发体验与用户体验 上图是我们通过改进最后给出的 Eros 开发的方案,以脚手架为核心的开发模式。 开发体验基于Vue的方式,各种语法都已经在脚手架那层抹平了,开发起来和之前的开发模式基本一致,开发调试的方式Weex提供了独立的模块支持,了解原理之后,我们很快做了保存即刷新的功能,加上本身Weex debug提供的debug页面,js也能进行调试,客户端也支持了日志输出,开发体验整体来看还比较流畅,确实是不如web开发那么自然,但是我们通过对脚手架的改造,对客户端支持热刷新功能,及原生提供的一些工具,大大的改善了开发体验。 用户体验方面整体性能对比RN有了提高,站在RN的肩膀上,确实解决了很多性能的问题,首次的白屏时间,我们采用的是内置包,并且配合我们的热更新机制,是能保证客户端打开的时候,一定是有对应的内容的,不需要额外去加载资源,白屏时间也有了保证。页面切换的时候我们采用多页面的方式去实现Weex,配合我们自己扩展的路由机制每个页面是一个单独的Weex实例,所以每个页面单独渲染的性能和效率要更好,并且我们也一直在做预加载的方案,虽然说对于性能改善的效果不是很明显,但是每一小步都是可以减少页面间切换的白屏时间的。 性能监控和容灾处理 Weex自己本身就做了很多性能监控,只需要对性能数据接入我们的监控系统,就能展示出对应的性能数据,目前从监控效果上来看确实实现了Weex对性能的承诺。 容灾处理用于处理jsBundle访问失败的情况,Weex自己具备容灾处理的方案,需要开发者自己做改造进行降级处理,展示页⾯面时,客户端会加载对应如果客户端加载js bundle失败可以启用webView访问,展示HTML端,但是体验会非常不好,我们采用内置包 + 热更新的机制,保证我们不会出现包解析失败或者访问不到的问题,如果发布的包有问题,可以紧急再发布,用户立马会接收到更新,并且根据配置告知用户是否立马更新,想要做的更好,可以保存一个稳定版本的包在用户手机中,遇到解析错误崩溃的问题,立即启用稳定版本的内置包,但是这样会导致包比较大,如果需要稳定的容灾处理可以考虑这样去实现。 在完成了方案调研和简单的demo测试,我们就开始落地,围绕的Weex也做了非常多的周边环境的建设,比如现有脚手架的改造以支持Weex的开发、热更新机制如何构建、客户端底层需要哪些支持、如何做扩展能与源码进行解耦等等。 还是说回正题,接下来介绍一下Weex整体的架构。 Weex 整体架构 从上面这个图可以看出Weex整体的运行原理,这里对流程做一个大概的介绍,后面每一步都会有详细的介绍。 Weex提供不同的framework解析,可以用.we和.vue文件写业务,然后通过webpack进行打包编译生成js bundle,编译过程中主要是用了weex相关的loader,Eros 对打包好的js bundle生成了zip包,还会生成差分包的逻辑。不管生成的是什么文件,最后都是将js bundle部署到服务器或者CDN节点上。 客户端启动时发现引入了Weex sdk,首先会初始化环境及一些监控,接着会运行本地的main.js即js framework,js framework会初始化一些环境,当js framework和客户端都准备好之后,就开始等待客户端什么时候展示页面。 当需要展示页面时,客户端会初始化Weex实例,就是WXSDKInstance,Weex实例会加载对应的js bundle文件,将整个js bundle文件当成一个字符串传给js framework,还会传递一些环境参数。js framework开始在JavaScript Core中执行js bundle,将js bundle执行翻译成virtual DOM,准备好数据双绑,同时将vDOM进行深度遍历解析成vNode,对应成一个个的渲染指令通过js Core传递给客户端。 js framework调用Weex SDK初始化时准备好的callNative、addElement 等方法,将指令传递给 native,找到指令对应的Weex Component执行渲染绘制,每渲染一个组件展示一个,Weex性能瓶颈就是来自于逐个传递组件的过程,调用module要稍微复杂一些,后面会详解,事件绑定后面也会详解。至此一个页面就展示出来了。 Weex SDK 上面我们分析了大概的Weex架构,也简单介绍了一下运行起来的流程,接下来我们基于 Eros 的源码来详细看一下每一步是如何进行的,Eros 是基于Weex的二次封装,客户端运行的第一个部分就是初始化Weex的sdk。 初始化Weex sdk主要完成下面四个事情: 关键节点记录监控信息 初始化 SDK 环境,加载并运行 js framework 注册 Components、Modules、Handlers 如果是在开发环境初始化模拟器尝试连接本地 server Eros 在Weex的基础上做了很多扩展,Weex的主要流程就是上面一些,Eros 主要的代码流程就是下面这样的。 + (void)configDefaultData { /* 启动网络变化监控 */ AFNetworkReachabilityManager *reachability = [AFNetworkReachabilityManager sharedManager]; [reachability startMonitoring]; /** 初始化Weex */ [BMConfigManager initWeexSDK]; BMPlatformModel *platformInfo = TK_PlatformInfo(); /** 设置sdimage减小内存占用 */ [[SDImageCache sharedImageCache] setShouldDecompressImages:NO]; [[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO]; [[SDImageCache sharedImageCache] setShouldCacheImagesInMemory:NO]; /** 设置统一请求url */ [[YTKNetworkConfig sharedConfig] setBaseUrl:platformInfo.url.request]; [[YTKNetworkConfig sharedConfig] setCdnUrl:platformInfo.url.image]; /** 应用最新js资源文件 */ [[BMResourceManager sharedInstance] compareVersion]; /** 初始化数据库 */ [[BMDB DB] configDB]; /** 设置 HUD */ [BMConfigManager configProgressHUD]; /* 监听截屏事件 */ // [[BMScreenshotEventManager shareInstance] monitorScreenshotEvent]; } 初始化监控记录 Weex其中一个优点就是自带监控,自己会记录一下简单的性能指标,比如初始化SDK时间,请求成功和失败,js报错这些信息,都会自动记录到WXMonitor中。 Weex将错误分成两类,一类是global,一类是instance。在iOS中WXSDKInstance初始化之前,所有的全局的global操作都会放在WXMonitor的globalPerformanceDict中。当WXSDKInstance初始化之后,即 WXPerformanceTag中instance以下的所有操作都会放在instance.performanceDict`中。 global的监控 SDKINITTIME:SDK 初始化监控 SDKINITINVOKETIME:SDK 初始化 invoke 监控 JSLIBINITTIME:js 资源初始化监控 instance监控 NETWORKTIME:网络请求监控 COMMUNICATETIME:交互事件监控 FIRSETSCREENJSFEXECUTETIME:首屏 js 加载监控 SCREENRENDERTIME:首屏渲染时间监控 TOTALTIME:渲染总时间 JSTEMPLATESIZE:js 模板大小 如果想要接入自己的监控系统,阅读一下WXMonitor相关的代码,可以采用一些AOP的模式将错误记录到自己的监控中,这部分代码不是运行重点有兴趣的同学就自己研究吧。 初始化 SDK 环境 这是最主要的一部初始化工作,通过 [BMConfigManager initWeexSDK];Eros 也是在这个时机注入扩展。我们将我们的扩展放在registerBmComponents、registerBmModules、registerBmHandlers这三个方法中,然后统一注入,避免与Weex本身的代码耦合太深。 + (void)initWeexSDK { [WXSDKEngine initSDKEnvironment]; [BMConfigManager registerBmHandlers]; [BMConfigManager registerBmComponents]; [BMConfigManager registerBmModules]; #ifdef DEBUG [WXDebugTool setDebug:YES]; [WXLog setLogLevel:WeexLogLevelLog]; [[BMDebugManager shareInstance] show]; // [[ATManager shareInstance] show]; #else [WXDebugTool setDebug:NO]; [WXLog setLogLevel:WeexLogLevelError]; #endif } 下面是我们部分的扩展,详细的扩展可以看看我们的源码,为了与官方的源码集成扩展解耦我们将我们的注入时机放在了Weex initSDKEnvironment之后。 // 扩展 Component + (void)registerBmComponents { NSDictionary *components = @{ @"bmmask": NSStringFromClass([BMMaskComponent class]), @"bmpop": NSStringFromClass([BMPopupComponent class]) ... }; for (NSString *componentName in components) { [WXSDKEngine registerComponent:componentName withClass:NSClassFromString([components valueForKey:componentName])]; } } // 扩展 Moudles + (void)registerBmModules { NSDictionary *modules = @{ @"bmRouter" : NSStringFromClass([BMRouterModule class]), @"bmAxios": NSStringFromClass([BMAxiosNetworkModule class]) ... }; for (NSString *moduleName in modules.allKeys) { [WXSDKEngine registerModule:moduleName withClass:NSClassFromString([modules valueForKey:moduleName])]; } } // 扩展 Handlers + (void)registerBmHandlers { [WXSDKEngine registerHandler:[WXImgLoaderDefaultImpl new] withProtocol:@protocol(WXImgLoaderProtocol)]; [WXSDKEngine registerHandler:[WXBMNetworkDefaultlpml new] withProtocol:@protocol(WXResourceRequestHandler)]; ... } 初始化SDK就是执行WXSDKEngine这个文件的内容,最主要注册当前的Components、Modules、handlers。 + (void)registerDefaults { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self _registerDefaultComponents]; [self _registerDefaultModules]; [self _registerDefaultHandlers]; }); } Components 注册 小白同学可能会比较疑惑为什么Weex只支持一些特定的标签,不是HTML里的所有标签都支持,首先标签的解析肯定需要与原生有一个对应关系,这些对应关系的标签才能支持。这个对应关系从哪儿来,就是首先 Weex 会初始化一些Components,首先要告诉Weex SDK我支持哪些标签,这其中就包括Weex提供的一些标签,和我们通过Weex Component的扩展方法扩展出来的标签。 我们来看看Components是怎么注册的,就是上面方法中的_registerDefaultComponents,下面是这些方法的部分代码 // WXSDKEngine.m + (void)_registerDefaultComponents { [self registerComponent:@"container" withClass:NSClassFromString(@"WXDivComponent") withProperties:nil]; [self registerComponent:@"cell-slot" withClass:NSClassFromString(@"WXCellSlotComponent") withProperties: @{@"append":@"tree", @"isTemplate":@YES}]; ... } 上面方法中两者有一些差别,withProperties参数不同,如果是带有@{@"append":@"tree"},先渲染子节点;isTemplate是个boolean值,如果为true,就会将该标签下的所有子模板全部传递过去。后面也会详细分析这两个参数的作用 在初始化WeexSDK的时候,Weex会调用_registerDefaultComponents方法将Weex官方扩展好的组件进行注册;继续看一下registerComponent:withClass:withProperties:方法 + (void)registerComponent:(NSString *)name withClass:(Class)clazz withProperties:(NSDictionary *)properties { if (!name || !clazz) { return; } WXAssert(name && clazz, @"Fail to register the component, please check if the parameters are correct !"); // 注册组件的方法 [WXComponentFactory registerComponent:name withClass:clazz withPros:properties]; // 遍历出组件的异步方法 NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name]; dict[@"type"] = name; // 将组件放到 bridge 中,准备注册到 js framework 中。 if (properties) { NSMutableDictionary *props = [properties mutableCopy]; if ([dict[@"methods"] count]) { [props addEntriesFromDictionary:dict]; } [[WXSDKManager bridgeMgr] registerComponents:@[props]]; } else { [[WXSDKManager bridgeMgr] registerComponents:@[dict]]; } } 首先看一下参数,name为注册在jsfm中Component的名字(即标签的名字),clazz为Component对应的类,properties为一些扩展属性; 在这个方法中又调用了WXComponentFactory的方法registerComponent:name withClass:clazz withPros:properties来注册Component,WXComponentFactory是一个单例,负责解析Component的方法,并保存所有注册的Component对应的方法;继续到 WXComponentFactory 中看一下 registerComponent:name withClass:clazz withPros:properties方法的实现: // 类 - (void)registerComponent:(NSString *)name withClass:(Class)clazz withPros:(NSDictionary *)pros { WXAssert(name && clazz, @"name or clazz must not be nil for registering component."); WXComponentConfig *config = nil; [_configLock lock]; config = [_componentConfigs objectForKey:name]; if(config){ WXLogInfo(@"Overrider component name:%@ class:%@, to name:%@ class:%@", config.name, config.class, name, clazz); } // 实例 WXComponentConfig 并保存到 _componentConfigs 中 config = [[WXComponentConfig alloc] initWithName:name class:NSStringFromClass(clazz) pros:pros]; [_componentConfigs setValue:config forKey:name]; [config registerMethods]; [_configLock unlock]; } 该方法中会实例化一个WXComponentConfig对象config,每个Component都会有一个与之绑定的WXComponentConfig实例,然后将config实例作为value,key为Component的name保存到 _componentConfigs中(_componentConfigs 是一个字典),config中保存了Component的所有暴露给js的方法,继续看一下WXComponentConfig的registerMethods方法: - (void)registerMethods { // 获取类 Class currentClass = NSClassFromString(_clazz); if (!currentClass) { WXLogWarning(@"The module class [%@] doesn't exit!", _clazz); return; } while (currentClass != [NSObject class]) { unsigned int methodCount = 0; // 获取方法列表 Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount); // 遍历方法列表 for (unsigned int i = 0; i < methodCount; i++) { // 获取方法名称 NSString *selStr = [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding]; BOOL isSyncMethod = NO; // 同步方法 if ([selStr hasPrefix:@"wx_export_method_sync_"]) { isSyncMethod = YES; // 异步方法 } else if ([selStr hasPrefix:@"wx_export_method_"]) { isSyncMethod = NO; // 其他未暴露方法 } else { continue; } NSString *name = nil, *method = nil; SEL selector = NSSelectorFromString(selStr); // 获取方法实现 if ([currentClass respondsToSelector:selector]) { method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector); } if (method.length <= 0) { WXLogWarning(@"The module class [%@] doesn't has any method!", _clazz); continue; } NSRange range = [method rangeOfString:@":"]; if (range.location != NSNotFound) { name = [method substringToIndex:range.location]; } else { name = method; } // 将方法保持到对应的字典中 NSMutableDictionary *methods = isSyncMethod ? _syncMethods : _asyncMethods; [methods setObject:method forKey:name]; } free(methodList); currentClass = class_getSuperclass(currentClass); } } WXComponentConfig中有两个字典_asyncMethods与_syncMethods,分别保存异步方法和同步方法;registerMethods方法中就是通过遍历Component类获取所有暴露给jsfm的方法;然后让我们在回到WXSDKEngine的registerComponent:withClass:withProperties:方法中。 + (void)registerComponent:(NSString *)name withClass:(Class)clazz withProperties:(NSDictionary *)properties { if (!name || !clazz) { return; } WXAssert(name && clazz, @"Fail to register the component, please check if the parameters are correct !"); [WXComponentFactory registerComponent:name withClass:clazz withPros:properties]; // ↑ 到这里 Component 的方法已经解析完毕,并保持到了 WXComponentFactory 中 // 获取 Component 的异步方法 NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name]; dict[@"type"] = name; // 最后将 Component 注册到 jsfm 中 if (properties) { NSMutableDictionary *props = [properties mutableCopy]; if ([dict[@"methods"] count]) { [props addEntriesFromDictionary:dict]; } [[WXSDKManager bridgeMgr] registerComponents:@[props]]; } else { [[WXSDKManager bridgeMgr] registerComponents:@[dict]]; } } Component解析完毕后,会调用WXSDKManager中的bridgeMgr的registerComponents:方法;WXSDKManager持有一个WXBridgeManager,这个WXBridgeManager又有一个的属性是WXBridgeContext,WXBridgeContext又持有一个js Bridge的引用,这个就是我们常说的Bridge。下面是相关的主要代码和bridge之间的关系。(现在WXDebugLoggerBridge已经不存在了) // WXSDKManager @interface WXSDKManager () @property (nonatomic, strong) WXBridgeManager *bridgeMgr; @property (nonatomic, strong) WXThreadSafeMutableDictionary *instanceDict; @end // WXBridgeManager @interface WXBridgeManager () @property (nonatomic, strong) WXBridgeContext *bridgeCtx; @property (nonatomic, assign) BOOL stopRunning; @property (nonatomic, strong) NSMutableArray *instanceIdStack; @end // WXBridgeContext @interface WXBridgeContext () @property (nonatomic, strong) id<WXBridgeProtocol> jsBridge; @property (nonatomic, strong) id<WXBridgeProtocol> devToolSocketBridge; @property (nonatomic, assign) BOOL debugJS; //store the methods which will be executed from native to js @property (nonatomic, strong) NSMutableDictionary *sendQueue; //the instance stack @property (nonatomic, strong) WXThreadSafeMutableArray *insStack; //identify if the JSFramework has been loaded @property (nonatomic) BOOL frameworkLoadFinished; //store some methods temporarily before JSFramework is loaded @property (nonatomic, strong) NSMutableArray *methodQueue; // store service @property (nonatomic, strong) NSMutableArray *jsServiceQueue; @end 上面大致介绍了一下三个类的属性,从属性看也可以看出大致的作用,各自间的调用关系也比较明确了,通过调用WXBridgeManager调用registerComponents方法,然后再调用WXBridgeContext的registerComponents方法,进行组件的注册。 // WXBridgeManager - (void)registerComponents:(NSArray *)components { if (!components) return; __weak typeof(self) weakSelf = self; WXPerformBlockOnBridgeThread(^(){ [weakSelf.bridgeCtx registerComponents:components]; }); } // WXBridgeContext - (void)registerComponents:(NSArray *)components { WXAssertBridgeThread(); if(!components) return; [self callJSMethod:@"registerComponents" args:@[components]]; } WXPerformBlockOnBridgeThread这个线程是一个jsThread,这是一个全局唯一线程,但是此时如果直接调用callJSMethod,肯定会失败,因为这个时候js framework可能还没有执行完毕。 如果此时js framework还没有执行完成,就会把要注册的方法都放到_methodQueue缓存起来,js framework加载完成之后会再次遍历这个_methodQueue,执行所有缓存的方法。 - (void)callJSMethod:(NSString *)method args:(NSArray *)args { // 如果 js frameworkLoadFinished 就立即注入 Component if (self.frameworkLoadFinished) { [self.jsBridge callJSMethod:method args:args]; } else { // 如果没有执行完,就将方法放到 _methodQueue 队列中 [_methodQueue addObject:@{@"method":method, @"args":args}]; } } - (void)callJSMethod:(NSString *)method args:(NSArray *)args onContext:(JSContext*)context completion:(void (^)(JSValue * value))complection { NSMutableArray *newArg = nil; if (!context) { if ([self.jsBridge isKindOfClass:[WXJSCoreBridge class]]) { context = [(NSObject*)_jsBridge valueForKey:@"jsContext"]; } } if (self.frameworkLoadFinished) { newArg = [args mutableCopy]; if ([newArg containsObject:complection]) { [newArg removeObject:complection]; } WXLogDebug(@"Calling JS... method:%@, args:%@", method, args); JSValue *value = [[context globalObject] invokeMethod:method withArguments:args]; if (complection) { complection(value); } } else { newArg = [args mutableCopy]; if (complection) { [newArg addObject:complection]; } [_methodQueue addObject:@{@"method":method, @"args":[newArg copy]}]; } } // 当 js framework 执行完毕之后会回来调用 WXJSCoreBridge 这个方法 - (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args { WXLogDebug(@"Calling JS... method:%@, args:%@", method, args); return [[_jsContext globalObject] invokeMethod:method withArguments:args]; } 接下来就是调用js framework的registerComponents注册所有相关的Components,下面会详细分析这部分内容,按照执行顺序接着会执行Modules的注册。 Modules 注册 入口还是WXSDKEngine,调用_registerDefaultModules,读所有的Modules进行注册,注册调用registerModule方法,同样的会注册模块,拿到WXModuleFactory的实例,然后同样遍历所有的同步和异步方法,最后调用WXBridgeManager,将模块注册到WXBridgeManager中。 + (void)_registerDefaultModules { [self registerModule:@"dom" withClass:NSClassFromString(@"WXDomModule")]; [self registerModule:@"locale" withClass:NSClassFromString(@"WXLocaleModule")]; ... } + (void)registerModule:(NSString *)name withClass:(Class)clazz { WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !"); if (!clazz || !name) { return; } NSString *moduleName = [WXModuleFactory registerModule:name withClass:clazz]; NSDictionary *dict = [WXModuleFactory moduleMethodMapsWithName:moduleName]; [[WXSDKManager bridgeMgr] registerModules:dict]; } 注册模块也是通过WXModuleFactory,将所有的module通过_registerModule生成ModuleMap。注册模块不允许同名模块。将name为key,value为WXModuleConfig存入_moduleMap字典中,WXModuleConfig存了该Module相关的属性,如果重名,注册的时候后注册的会覆盖先注册的。 @interface WXModuleFactory () @property (nonatomic, strong) NSMutableDictionary *moduleMap; @property (nonatomic, strong) NSLock *moduleLock; @end - (NSString *)_registerModule:(NSString *)name withClass:(Class)clazz { WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !"); [_moduleLock lock]; //allow to register module with the same name; WXModuleConfig *config = [[WXModuleConfig alloc] init]; config.name = name; config.clazz = NSStringFromClass(clazz); [config registerMethods]; [_moduleMap setValue:config forKey:name]; [_moduleLock unlock]; return name; } 当把所有的Module实例化之后,遍历所有的方法,包括同步和异步方法,下面的方法可以看到,在遍历方法之前,就已经有一些方法在_defaultModuleMethod对象中了,这里至少有两个方法addEventListener和removeAllEventListeners,所以这里返回出来的方法都具备上面两个方法。 - (NSMutableDictionary *)_moduleMethodMapsWithName:(NSString *)name { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSMutableArray *methods = [self _defaultModuleMethod]; [_moduleLock lock]; [dict setValue:methods forKey:name]; WXModuleConfig *config = _moduleMap[name]; void (^mBlock)(id, id, BOOL *) = ^(id mKey, id mObj, BOOL * mStop) { [methods addObject:mKey]; }; [config.syncMethods enumerateKeysAndObjectsUsingBlock:mBlock]; [config.asyncMethods enumerateKeysAndObjectsUsingBlock:mBlock]; [_moduleLock unlock]; return dict; } - (NSMutableArray*)_defaultModuleMethod { return [NSMutableArray arrayWithObjects:@"addEventListener",@"removeAllEventListeners", nil]; } 接下来就是调用js framework注入方法了,和registerComponent差不多,也会涉及到线程的问题,也会通过上面WXSDKManager -> WXBridgeManager -> WXBridgeContext。最后调用到下面这个方法。最后调用registerModules将所有的客户端Module注入到js framework中,js framework还会有一些包装,业务中会使用weex.registerModule来调用对应的方法。 - (void)registerModules:(NSDictionary *)modules { WXAssertBridgeThread(); if(!modules) return; [self callJSMethod:@"registerModules" args:@[modules]]; } handler 注入 Component和Module大家经常使用还比较能理解,但是handler是什么呢? Weex规定了一些协议方法,在特定的时机会调用协议中的方法,可以实现一个类遵循这些协议,并实现协议中的方法,然后通过handler的方式注册给weex,那么在需要调用这些协议方法的时候就会调用到你实现的那个类中。比如说 WXResourceRequestHandler: @protocol WXResourceRequestHandler <NSObject> // Send a resource request with a delegate - (void)sendRequest:(WXResourceRequest *)request withDelegate:(id<WXResourceRequestDelegate>)delegate; @optional // Cancel the ongoing request - (void)cancelRequest:(WXResourceRequest *)request; @end WXResourceRequestHandler中规定了两个方法,一个是加载资源的请求方法,一个是需要请求的方法,然后看一下WXResourceRequestHandlerDefaultImpl类: // // WXResourceRequestHandlerDefaultImpl.m // #pragma mark - WXResourceRequestHandler - (void)sendRequest:(WXResourceRequest *)request withDelegate:(id<WXResourceRequestDelegate>)delegate { if (!_session) { NSURLSessionConfiguration *urlSessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; if ([WXAppConfiguration customizeProtocolClasses].count > 0) { NSArray *defaultProtocols = urlSessionConfig.protocolClasses; urlSessionConfig.protocolClasses = [[WXAppConfiguration customizeProtocolClasses] arrayByAddingObjectsFromArray:defaultProtocols]; } _session = [NSURLSession sessionWithConfiguration:urlSessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]]; _delegates = [WXThreadSafeMutableDictionary new]; } NSURLSessionDataTask *task = [_session dataTaskWithRequest:request]; request.taskIdentifier = task; [_delegates setObject:delegate forKey:task]; [task resume]; } - (void)cancelRequest:(WXResourceRequest *)request { if ([request.taskIdentifier isKindOfClass:[NSURLSessionTask class]]) { NSURLSessionTask *task = (NSURLSessionTask *)request.taskIdentifier; [task cancel]; [_delegates removeObjectForKey:task]; } } WXResourceRequestHandlerDefaultImpl遵循了WXResourceRequestHandler协议,并实现了协议方法,然后注册了Handler,如果有资源请求发出来,就会走到WXResourceRequestHandlerDefaultImpl的实现中。 客户端初始化SDK就完成了注册相关的方法,上面一直都在提到最后注册是注册到js 环境中,将方法传递给js framework进行调用,但是js framework一直都还没有调用,下面就是加载这个文件了。 加载并运行 js framework 在官方GitHub中 runtime 目录下放着一堆js,这堆js最后会被打包成一个叫native-bundle-main.js的文件,我们暂且称之为main.js,这段js就是我们常说的js framework,在SDK初始化时,会将整段代码当成字符串传递给WXSDKManager并放到JavaScript Core中去执行。我们先看看这个runtime下的文件都有哪些 |-- api:冻结原型链,提供给原生调用的方法,比如 registerModules |-- bridge:和客户端相关的接口调用,调用客户端的时候有一个任务调度 |-- entries:客户端执行 js framework 的入口文件,WXSDKEngine 调用的方法 |-- frameworks:核心文件,初始化 js bundle 实例,对实例进行管理,dom 调度转换等 |-- services:js service 存放,broadcast 调度转换等 |-- shared:polyfill 和 console 这些差异性的方法 |-- vdom:将 VDOM 转化成客户端能渲染的指令 看起来和我们上一篇文章提到的js bridge的功能很相似,但是为什么Weex的这一层有这么多功能呢,首先Weex是要兼容三端的,所以iOS、android、web的差异性必定是需要去抹平的,他们接受指令的方式和方法都有可能不同,比如:客户端设计的是createBody和addElement,而web是createElement、appendChild等。 除了指令的差异,还有上层业务语言的不同,比如Weex支持Vue和Rax,甚至可能支持React,只要是符合js framework的实现,就可以通过不同的接口渲染在不同的宿主环境下。我们可以称这一层为DSL,我们也看看js framework这层的主要代码 |-- index.js:入口文件 |-- legacy:关于 VM 相关的主要方法 | |-- api:相关 vm 定义的接口 | |-- app:管理页面间页面实例的方法 | |-- core:实现数据监听的方法 | |-- static:静态方法 | |-- util:工具类函数 | |-- vm:解析指令相关 |-- vanilla:与客户端交互的一些方法 运行 framework 首先注册完上面所提到的三个模块之后,WXSDKEngine继续往下执行,还是先会调用到WXBridgeManager中的executeJsFramework,再调用到WXBridgeContext的executeJsFramework,然后在子线程中执行js framework。 // WXSDKEngine [[WXSDKManager bridgeMgr] executeJsFramework:script]; // WXBridgeManager - (void)executeJsFramework:(NSString *)script { if (!script) return; __weak typeof(self) weakSelf = self; WXPerformBlockOnBridgeThread(^(){ [weakSelf.bridgeCtx executeJsFramework:script]; }); } // WXBridgeContext - (void)executeJsFramework:(NSString *)script { WXAssertBridgeThread(); WXAssertParam(script); WX_MONITOR_PERF_START(WXPTFrameworkExecute); // 真正的执行 js framework [self.jsBridge executeJSFramework:script]; WX_MONITOR_PERF_END(WXPTFrameworkExecute); if ([self.jsBridge exception]) { NSString *exception = [[self.jsBridge exception] toString]; NSMutableString *errMsg = [NSMutableString stringWithFormat:@"[WX_KEY_EXCEPTION_SDK_INIT_JSFM_INIT_FAILED] %@",exception]; [WXExceptionUtils commitCriticalExceptionRT:@"WX_KEY_EXCEPTION_SDK_INIT" errCode:[NSString stringWithFormat:@"%d", WX_KEY_EXCEPTION_SDK_INIT] function:@"" exception:errMsg extParams:nil]; WX_MONITOR_FAIL(WXMTJSFramework, WX_ERR_JSFRAMEWORK_EXECUTE, errMsg); } else { WX_MONITOR_SUCCESS(WXMTJSFramework); //the JSFramework has been load successfully. // 执行完 js self.frameworkLoadFinished = YES; // 执行缓存在 _jsServiceQueue 中的方法 [self executeAllJsService]; // 获取 js framework 版本号 JSValue *frameworkVersion = [self.jsBridge callJSMethod:@"getJSFMVersion" args:nil]; if (frameworkVersion && [frameworkVersion isString]) { [WXAppConfiguration setJSFrameworkVersion:[frameworkVersion toString]]; } // 计算 js framework 的字节大小 if (script) { [WXAppConfiguration setJSFrameworkLibSize:[script lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; } //execute methods which has been stored in methodQueue temporarily. // 开始执行之前缓存在队列缓存在 _methodQueue 的方法 for (NSDictionary *method in _methodQueue) { [self callJSMethod:method[@"method"] args:method[@"args"]]; } [_methodQueue removeAllObjects]; WX_MONITOR_PERF_END(WXPTInitalize); }; } 上面执行过程中比较核心的是如何执行js framework的,其实就是加载native-bundle-main.js文件,执行完了之后也不需要有返回值,或者持有对js framework的引用,只是放在内存中,随时准备被调用。在执行前后也会有日志记录 // WXBridgeContext - (void)executeJSFramework:(NSString *)frameworkScript { WXAssertParam(frameworkScript); if (WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) { [_jsContext evaluateScript:frameworkScript withSourceURL:[NSURL URLWithString:@"native-bundle-main.js"]]; }else{ [_jsContext evaluateScript:frameworkScript]; } } 我们先抛开js framework本身的执行,先看看执行完成之后,客户端接着会完成什么工作,要开始加载之前缓存在_jsServiceQueue和_methodQueue中的方法了。 // WXBridgeContext - (void)executeAllJsService { for(NSDictionary *service in _jsServiceQueue) { NSString *script = [service valueForKey:@"script"]; NSString *name = [service valueForKey:@"name"]; [self executeJsService:script withName:name]; } [_jsServiceQueue removeAllObjects]; } for (NSDictionary *method in _methodQueue) { [self callJSMethod:method[@"method"] args:method[@"args"]]; } [_methodQueue removeAllObjects]; _methodQueue比较好理解,前面哪些原生注册方法都是缓存在_methodQueue中的,_jsServiceQueue是从哪儿来的呢?js service下面还会详细说明,broadcastChannel就是Weex提供的一种js service,官方用例也 提供了扩展js service的方式,由此可以看出js service只会加载一次,js service只是一堆字符串,所以直接执行就行。 // WXSDKEngine NSDictionary *jsSerices = [WXDebugTool jsServiceCache]; for(NSString *serviceName in jsSerices) { NSDictionary *service = [jsSerices objectForKey:serviceName]; NSString *serviceName = [service objectForKey:@"name"]; NSString *serviceScript = [service objectForKey:@"script"]; NSDictionary *serviceOptions = [service objectForKey:@"options"]; [WXSDKEngine registerService:serviceName withScript:serviceScript withOptions:serviceOptions]; } // WXBridgeContext - (void)executeJsService:(NSString *)script withName:(NSString *)name { if(self.frameworkLoadFinished) { WXAssert(script, @"param script required!"); [self.jsBridge executeJavascript:script]; if ([self.jsBridge exception]) { NSString *exception = [[self.jsBridge exception] toString]; NSMutableString *errMsg = [NSMutableString stringWithFormat:@"[WX_KEY_EXCEPTION_INVOKE_JSSERVICE_EXECUTE] %@",exception]; [WXExceptionUtils commitCriticalExceptionRT:@"WX_KEY_EXCEPTION_INVOKE" errCode:[NSString stringWithFormat:@"%d", WX_KEY_EXCEPTION_INVOKE] function:@"" exception:errMsg extParams:nil]; WX_MONITOR_FAIL(WXMTJSService, WX_ERR_JSFRAMEWORK_EXECUTE, errMsg); } else { // success } }else { [_jsServiceQueue addObject:@{ @"name": name, @"script": script }]; } } _methodQueue队列的执行是调用callJSMethod,往下会调用WXJSCoreBridge的invokeMethod,这个就是就是调用对应的js framework提供的方法,同时会发现一个WXJSCoreBridge文件,这里就是Weex的bridge,_jsContext就是提供的全部客户端和js framework真正交互的所有方法了,这些方法都是提供给js framework来调用的,主要的方法后面都会详细讲到。 js framework 执行过程 js framework执行的入口文件/runtime/entries/index.js,会调用/runtime/entries/setup.js,这里的js模块化粒度很细,我们就不一一展示代码了,可以去Weex项目的里看源码。 /** * Setup frameworks with runtime. * You can package more frameworks by * passing them as arguments. */ export default function (frameworks) { const { init, config } = runtime config.frameworks = frameworks const { native, transformer } = subversion for (const serviceName in services) { runtime.service.register(serviceName, services[serviceName]) } runtime.freezePrototype() // register framework meta info global.frameworkVersion = native global.transformerVersion = transformer // init frameworks const globalMethods = init(config) // set global methods for (const methodName in globalMethods) { global[methodName] = (...args) => { const ret = globalMethods[methodName](...args) if (ret instanceof Error) { console.error(ret.toString()) } return ret } } } 我们主要看,js framework的执行完成了哪些功能,主要是下面三个功能: 挂载全局属性方法及 VM 原型链方法 创建于客户端通信桥 弥补环境差异 挂载全局属性方法及 VM 原型链方法 刚才已经讲了DSL是什么,js framework中非常重要的功能就是做好不同宿主环境和语言中的兼容。主要是通过一些接口来与客户端进行交互,适配前端框架实际上是为了适配iOS、android和浏览器。这里主要讲一讲和客户端进行适配的接口。 getRoot:获取页面节点 receiveTasks:监听客户端任务 registerComponents:注册 Component registerMoudles:注册 Module init: 页面内部生命周期初始化 createInstance: 页面内部生命周期创建 refreshInstance: 页面内部生命周期刷新 destroyInstance: 页面内部生命周期销毁 ... 这些接口都可以在WXBridgeContext里看到,都是js framework提供给客户端调用的。其中Weex SDK初始化的时候,提到的registerComponents和registerMoudles也是调用的这个方法。 registerComponents js framework中registerComponents的实现可以看出,前端只是做了一个map缓存起来,等待解析vDOM的时候进行映射,然后交给原生组件进行渲染。 // /runtime/frameworks/legacy/static/register.js export function registerComponents (components) { if (Array.isArray(components)) { components.forEach(function register (name) { /* istanbul ignore if */ if (!name) { return } if (typeof name === 'string') { nativeComponentMap[name] = true } /* istanbul ignore else */ else if (typeof name === 'object' && typeof name.type === 'string') { nativeComponentMap[name.type] = name } }) } } registerMoudles registerMoudles时也差不多,放在了nativeModules这个对象上缓存起来,但是使用的时候要复杂一些,后面也会讲到。 // /runtime/frameworks/legacy/static/register.js export function registerModules (modules) { /* istanbul ignore else */ if (typeof modules === 'object') { initModules(modules) } } // /runtime/frameworks/legacy/app/register.js export function initModules (modules, ifReplace) { for (const moduleName in modules) { // init `modules[moduleName][]` let methods = nativeModules[moduleName] if (!methods) { methods = {} nativeModules[moduleName] = methods } // push each non-existed new method modules[moduleName].forEach(function (method) { if (typeof method === 'string') { method = { name: method } } if (!methods[method.name] || ifReplace) { methods[method.name] = method } }) } } 创建于客户端通信桥 js framework是客户端和前端业务代码沟通的桥梁,所以更重要的也是bridge,基本的桥的设计上一篇也讲了,Weex选择的是直接提供方法供js调用,也直接调用js的方法。 客户端调用js直接使用callJs,callJs是js提供的方法,放在当前线程中,供客户端调用,包括DOM事件派发、module调用时的时间回调,都是通过这个接口通知js framework,然后再调用缓存在js framework中的方法。 js调用客户端使用callNative,客户端也会提供很多方法给js framework供,framework调用,这些方法都可以在WXBridgeContext中看到,callNative只是其中的一个方法,实际代码中还有很多方法,比如addElement、updateAttrs等等 弥补环境差异 除了用于完成功能的主要方法,客户端还提供一些方法来弥补上层框架在js中调用时没有的方法,就是环境的差异,弥补兼容性的差异,setTimeout、nativeLog等,客户端提供了对应的方法,js framework也无法像在浏览器中调用这些方法一样去调用这些方法,所以需要双方采用兼容性的方式去支持。 还有一些ployfill的方法,比如Promise,Object.assign,这些ployfill能保证一部分环境和浏览器一样,降低我们写代码的成本。 执行完毕 执行js framework其他的过程就不一一展开了,主要是一些前端代码之间的互相调用,这部分也承接了很多Weex历史遗留的一些兼容问题,有时候发现一些神奇的写法,可能是当时为了解决一些神奇的bug吧,以及各种istanbul ignore的注释。 执行完js framework之后客户端frameworkLoadFinished会被置位 YES,之前遗留的任务也都会在js framework执行完毕之后执行,以完成整个初始化的流程。 客户端会先执行js-service,因为js-service只是需要在JavaScript Core中执行字符串,所以直接执行executeAllJsService就行了,并不需要调用js framework的方法,只是让当前内存环境中有js service的变量对象。 然后将_methodQueue中的任务拿出来遍历执行。这里就是执行缓存队列中的registerComponents、registerModules、registerMethods。上面也提到了具体两者是怎么调用的,详细的代码都是在这里。 执行完毕之后,按理说这个js Thread应该关闭,然后被回收,但是我们还需要让这个js framework一直运行在js Core中,所以这个就需要给js Thread开启了一个runloop,让这个js Thread一直处于执行状态 Weex 实例初始化 前面铺垫了非常多的初始化流程,就是为了在将一个页面是如何展示的过程中能清晰一点,前面相当于在做准备工作,这个时候我们来看Weex实例的初始化。Eros 通过配置文件将首页的 URL 配置在配置文件中,客户端能直接拿到首页直接进行初始化。 客户端通过 _renderWithURL去加载首页的URL,这个URL不管是放在本地还是服务器上,其实就是一个js bundle文件,就是一个经过特殊loader打包的js文件,加载到这个文件之后,将这个调用到js framework中的 createInstance。 /* id:Weex 实例的 id code:js bundle 的代码 config:配置参数 data:参数 */ function createInstance (id, code, config, data) { // 判断当前实例是否已经创建过了 if (instanceTypeMap[id]) { return new Error(`The instance id "${id}" has already been used!`) } // 获取当前 bundle 是那种框架 const bundleType = getBundleType(code) instanceTypeMap[id] = bundleType // 初始化 instance 的 config config = JSON.parse(JSON.stringify(config || {})) config.env = JSON.parse(JSON.stringify(global.WXEnvironment || {})) config.bundleType = bundleType // 获取当前的 DSL const framework = runtimeConfig.frameworks[bundleType] if (!framework) { return new Error(`[JS Framework] Invalid bundle type "${bundleType}".`) } if (bundleType === 'Weex') { console.error(`[JS Framework] COMPATIBILITY WARNING: ` + `Weex DSL 1.0 (.we) framework is no longer supported! ` + `It will be removed in the next version of WeexSDK, ` + `your page would be crash if you still using the ".we" framework. ` + `Please upgrade it to Vue.js or Rax.`) } // 获得对应的 WeexInstance 实例,提供 Weex.xx 相关的方法 const instanceContext = createInstanceContext(id, config, data) if (typeof framework.createInstance === 'function') { // Temporary compatible with some legacy APIs in Rax, // some Rax page is using the legacy ".we" framework. if (bundleType === 'Rax' || bundleType === 'Weex') { const raxInstanceContext = Object.assign({ config, created: Date.now(), framework: bundleType }, instanceContext) // Rax 或者 Weex DSL 调用初始化的地方 return framework.createInstance(id, code, config, data, raxInstanceContext) } // Rax 或者 Weex DSL 调用初始化的地方 return framework.createInstance(id, code, config, data, instanceContext) } // 当前 DSL 没有提供 createInstance 支持 runInContext(code, instanceContext) } 上面就是调用的第一步,不同的DSL已经在这儿就开始区分,生成不同的Weex实例。下一步就是调用各自DSL的createInstance,并把对应需要的参数都传递过去 // /runtime/frameworks/legacy/static/create.js export function createInstance (id, code, options, data, info) { const { services } = info || {} resetTarget() let instance = instanceMap[id] /* istanbul ignore else */ options = options || {} let result /* istanbul ignore else */ if (!instance) { // 创建 APP 实例,并将实例放到 instanceMap 上 instance = new App(id, options) instanceMap[id] = instance result = initApp(instance, code, data, services) } else { result = new Error(`invalid instance id "${id}"`) } return (result instanceof Error) ? result : instance } // /runtime/frameworks/legacy/app/instance.js export default function App (id, options) { this.id = id this.options = options || {} this.vm = null this.customComponentMap = {} this.commonModules = {} // document this.doc = new renderer.Document( id, this.options.bundleUrl, null, renderer.Listener ) this.differ = new Differ(id) } 主要的还是initAPP这个方法,这个方法中做了很多补全原型链的方法,比如bundleDefine、bundleBootstrap等等,这些都挺重要的,大家可以看看 init 方法,就完成了上述的操作。 最主要的还是下面这个方法,这里会是最终执行js bundle的地方。执行完成之后将 Weex的单个页面的实例放在instanceMap,new Function是最核心的方法,这里就是将整个JS bundle由代码到执行生成VDOM,然后转换成一个个VNode发送到原生模块进行渲染。 if (!callFunctionNative(globalObjects, functionBody)) { // If failed to compile functionBody on native side, // fallback to callFunction. callFunction(globalObjects, functionBody) } // 真正执行 js bundle 的方法 function callFunction (globalObjects, body) { const globalKeys = [] const globalValues = [] for (const key in globalObjects) { globalKeys.push(key) globalValues.push(globalObjects[key]) } globalKeys.push(body) // 所有的方法都是通过 new Function() 的方式被执行的 const result = new Function(...globalKeys) return result(...globalValues) } js Bundle 的执行 js bundle就是写的业务代码了,大家可以写一个简单的代码保存一下看看,由于使用了Weex相关的loader,具体的代码肯定和常规的js代码不一样,经过转换主要还是<template>和<style>部分,这两部分会被转换成两个JSON,放在两个闭包中。上面已经说到了最后是执行了new Function,具体的执行步骤在init,由于代码太长,我们主要看核心的部分。 const globalObjects = Object.assign({ define: bundleDefine, require: bundleRequire, bootstrap: bundleBootstrap, register: bundleRegister, render: bundleRender, __weex_define__: bundleDefine, // alias for define __weex_bootstrap__: bundleBootstrap, // alias for bootstrap __weex_document__: bundleDocument, __weex_require__: bundleRequireModule, __weex_viewmodel__: bundleVm, weex: weexGlobalObject }, timerAPIs, services) 上述这些代码是被执行的核心部分, bundleDefine 部分,这里是解析组件的部分,分析哪些是和Weex对应的Component,哪些是用户自定义的Component,这里就是一个递归遍历的过程。 bundleRequire和bundleBootstrap,这里调用到了 bootstrap和 Vm,这里有一步我不是很明白。bootstrap主要的功能是校验参数和环境信息,这部分大家可以看一下源码。 Vm是根据Component新建对应的ViewModel,这部分做的事情就非常多了,基本上是解析整个VM的核心。主要完成了初始化生命周期、数据双绑、构建模板、UI绘制。 // bind events and lifecycles initEvents(this, externalEvents) console.debug(`[JS Framework] "init" lifecycle in Vm(${this._type})`) this.$emit('hook:init') this._inited = true // proxy data and methods // observe data and add this to vms this._data = typeof data === 'function' ? data() : data if (mergedData) { extend(this._data, mergedData) } initState(this) console.debug(`[JS Framework] "created" lifecycle in Vm(${this._type})`) this.$emit('hook:created') this._created = true // backward old ready entry if (options.methods && options.methods.ready) { console.warn('"exports.methods.ready" is deprecated, ' + 'please use "exports.created" instead') options.methods.ready.call(this) } if (!this._app.doc) { return } // if no parentElement then specify the documentElement this._parentEl = parentEl || this._app.doc.documentElement build(this) 初始化生命周期 代码实现;这个过程中初始化了4个生命周期的钩子,init、created、ready、destroyed。除了生命周期,这里还绑定了vm的事件机制,组件间互相通信的方式。 数据双绑 代码实现;Vue DSL数据双绑可以参考一下Vue的数据双绑实现原理,Rax也是大同小异,将数据进行代理,然后添加数据监听,初始化计算属性,挂载_method方法,创建getter/setter,重写数组的方法,递归绑定...这部分主要是Vue的内容,之前也有博客详细说明了Vue的数据双绑机制。 模板解析 代码实现;这里也是Vue的模板解析机制之一,大部分是对Vue模板语法的解析,比如v-for、:class解析语法的过程是一个深度遍历的过程,这个过程完成之后js bundle就变成了VDOM,这个VDOM更像是符合某种约定格式的JSON数据,因为客户端和js framework可共用的数据类型不多,JSON是最好的方式,所以最终将模板转换成JSON的描述方式传递给客户端。 绘制 Native UI 代码实现;通过differ.flush调用,会触发VDOM 的对比,对比的过程是一个同级对比的过程,将节点也就是VNode逐一diff传递给客户端。先对比外层组件,如果有子节点再递归子节点,对比不同的部分都传递给客户端,首次渲染全是新增,后面更新UI的时候会有用到remove、update等API。 最终绘制调用 appendChild,这里封装了所有和native有交互的方法。DOM操作大致就是addElement、removeElement等方法,调用taskCenter.send,这里是一个任务调度,最终所有的方法都是通过这里调用客户端提供的对应的接口。 send (type, params, args, options) { const { action, component, ref, module, method } = params // normalize args and options args = args.map(arg => this.normalize(arg)) if (typof(options) === 'Object') { options = this.normalize(options, true) } switch (type) { case 'dom': return this[action](this.instanceId, args) case 'component': return this.componentHandler(this.instanceId, ref, method, args, Object.assign({ component }, options)) default: return this.moduleHandler(this.instanceId, module, method, args, options) } } 调用客户端之后,回顾之前Weex SDK初始化的时候,addElement是已经在客户端注入的方法,然后将对应的Component映射到对应的解析原生方法中。原生再找到对应Component进行渲染。 由于Weex渲染完成父级之后才会渲染子,所以传递的顺序是先传父,再传子,父渲染完成之后,任务调度给一个渲染完成的回调,然后再进行递归,渲染子节点的指令,这样可能会比较慢,上面提到注册Component的时候会有两个参数append=tree和istemplate=true,这两种方式都是优化性能的方案,上面提到在Components注册的时候有这两个参数。 append=tree BOOL appendTree = !appendingInTree && [component.attributes[@"append"] isEqualToString:@"tree"]; // if ancestor is appending tree, child should not be laid out again even it is appending tree. for(NSDictionary *subcomponentData in subcomponentsData){ [self _recursivelyAddComponent:subcomponentData toSupercomponent:component atIndex:-1 appendingInTree:appendTree || appendingInTree]; } [component _didInserted]; if (appendTree) { // If appending tree,force layout in case of too much tasks piling up in syncQueue [self _layoutAndSyncUI]; } Weex的渲染方式有两种一种是node,一种是tree,node是先渲染父节点,再渲染子节点,而tree是先渲染子节点,最后一次性layout渲染父节点。渲染性能上讲,刚开始的绘制时间,append="node"比较快,但是从总的时间来说,append="tree"用的时间更少。 如果当前Component有{@"append":@"tree"}属性并且它的父Component没有这个属性将会强制对页面进行重新布局。可以看到这样做是为了防止UI绘制任务太多堆积在一起影响同步队列任务的执行。 istemplate=true WXComponentConfig *config = [WXComponentFactory configWithComponentName:type]; BOOL isTemplate = [config.properties[@"isTemplate"] boolValue] || (supercomponent && supercomponent->_isTemplate); if (isTemplate) { bindingProps = [self _extractBindingProps:&attributes]; bindingStyles = [self _extractBindings:&styles]; bindingAttibutes = [self _extractBindings:&attributes]; bindingEvents = [self _extractBindingEvents:&events]; } 那么客户端在渲染的时候,会将整个Component子节点获取过来,然后通过DataBinding转换成表达式,存在bindingMap中,相关的解析都在WXJSASTParser.m文件中,涉及到比较复杂的模板解析,表达式解析和转换,绑定数据与原生UI的关系。 渲染过程中客户端和js framework还有事件的沟通,通过桥传递createFinished和renderFinished事件,js framework会去执行Weex实例对应的生命周期方法。 至此页面就已经渲染出来了,页面渲染完成之后,那么点击事件是怎么做的呢? 事件传递 全局事件 在了解事件如何发生传递之前,我们先看看事件有几种类型,Eros 封装了路由的事件,将这些事件封装在组件上,在Vue模板上提供一个 Eros 对象,在Weex创建实例的时候绑定这些方法注入回调等待客户端回调,客户端在发生对应的事件的手通过全局事件来通知到js framework执行weex实例上的回调方法。 // app 前后台相关 start appActive() { console.log('appActive'); }, appDeactive() { console.log('appDeactive'); }, // app 前后台相关 end // 页面周期相关 start beforeAppear (params, options) { console.log('beforeAppear'); }, beforeBackAppear (params, options) { console.log('beforeBackAppear'); }, appeared (params, options) { console.log('appeared'); }, backAppeared (params, options) { console.log('backAppeared'); }, beforeDisappear (options) { console.log('beforeDisappear'); }, disappeared (options) { console.log('disappeared'); }, // 页面周期相关 end 全局事件 Eros 是通过类似node js的处理,在js core中放一个全局对象,也是类似使用Module的方式去使用,通过封装类似js的事件机制的方式去触发。 交互事件 我们主要分析的是页面交互的事件,比如点击事件;客户端在发生事件的时候,怎么能执行我们在Vue实例上定义的方法呢?这个过程首先点击事件需要注册,也就是说是在初始化的时候,js framework就已经告诉客户端哪些组件是有事件绑定回调的,如果客户端不管接受到什么事件都抛给js,性能肯定会很差。 事件创建 js framework在解析模板的时候发现有事件标签@xxx="callback",就会在创建组件的时候通过callAddEvent将event传递给native,但是不会传递事件的回调方法,因为客户端根本就不识别事件回调的方法,客户端发现有事件属性之后,就会对原生的事件进行事件绑定,在渲染组件的时候,每个组件都会生成一个组件ID,就是ref,type就是事件类型比如:click、longpress等。 // https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/vm/compiler.js if (!vm._rootEl) { vm._rootEl = element // bind event earlier because of lifecycle issues const binding = vm._externalBinding || {} const target = binding.template const parentVm = binding.parent if (target && target.events && parentVm && element) { for (const type in target.events) { const handler = parentVm[target.events[type]] if (handler) { element.addEvent(type, bind(handler, parentVm)) } } } } // https://github.com/apache/incubator-weex/blob/master/runtime/vdom/Element.js addEvent (type, handler, params) { if (!this.event) { this.event = {} } if (!this.event[type]) { this.event[type] = { handler, params } const taskCenter = getTaskCenter(this.docId) if (taskCenter) { taskCenter.send( 'dom', { action: 'addEvent' }, [this.ref, type] ) } } } 上面可以看出只传递了一个ref过去,绑定完毕至所有组件渲染完成之后,当视图发生对应的事件之后,客户端捕获到了事件之后通过fireEvent将对应的事件,传递四个参数,ref、type、event、domChanges,通过bridge将这些参数传递给js framework的bridge,但是到底层的时候还会携带一个Weex实例的ID,因为此时可能存在多个weex实例,通过Weex ID找到对应的weex`实例。 如果事件绑定有多个ref,还需要遍历递归一下,也是一个深度遍历的过程,然后找到对应的事件,触发对应的事件,事件里可能有对双绑数据的改变,进而改变DOM,所以事件触发之后再次进行differ.flush。对比生成新的VDOM,然后渲染新的页面样式。 事件触发 // https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/ctrl/misc.js export function fireEvent (app, ref, type, e, domChanges) { console.debug(`[JS Framework] Fire a "${type}" event on an element(${ref}) in instance(${app.id})`) if (Array.isArray(ref)) { ref.some((ref) => { return fireEvent(app, ref, type, e) !== false }) return } const el = app.doc.getRef(ref) if (el) { const result = app.doc.fireEvent(el, type, e, domChanges) app.differ.flush() app.doc.taskCenter.send('dom', { action: 'updateFinish' }, []) return result } return new Error(`invalid element reference "${ref}"`) } app.doc.fireEvent(el, type, e, domChanges)主要来看看这个方法,首先是获取到当时的事件回调,然后执行事件回调,原生的组件不会有事件冒泡,但是js是有事件冒泡机制的,所以下面模拟了一个事件冒泡机制,继续触发了父级的fireEvent,逐个冒泡到父级,这部分是在js framework中完成的。 // https://github.com/apache/incubator-weex/blob/master/runtime/vdom/Element.js fireEvent (type, event, isBubble, options) { let result = null let isStopPropagation = false const eventDesc = this.event[type] if (eventDesc && event) { const handler = eventDesc.handler event.stopPropagation = () => { isStopPropagation = true } if (options && options.params) { result = handler.call(this, ...options.params, event) } else { result = handler.call(this, event) } } if (!isStopPropagation && isBubble && (BUBBLE_EVENTS.indexOf(type) !== -1) && this.parentNode && this.parentNode.fireEvent) { event.currentTarget = this.parentNode this.parentNode.fireEvent(type, event, isBubble) // no options } return result } 上述就完成了一次完整的事件触发,如果是简单的事件,类似click这样的一次传递完成一次事件回调,不会有太大的问题,但是如果是滚动这样的事件传递难免会有性能问题,所以客户端在处理滚动事件的时候,肯定会有一个最小时间间隔,肯定不是无时无刻的触发。 更好的处理是Weex也引入了expression binding,将js的事件回调处理成表达式,在绑定的时候一并传给客户端,由于是表达式,所以客户端也可以识别表达式,客户端在监听原生事件触发的时候,就直接执行表达式。这样就省去了传递的过程。Weex的bingdingX也是可以用来处理类似频繁触发的js和客户端之间的交互的,比如动画。 module 的使用 上面已经讲了module的注册,最终调用js framework的registerModules注入所有module方法,并将方法存储在nativeModules对象上,注册的过程就算完成了。 // https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/static/register.js export function registerModules (modules) { /* istanbul ignore else */ if (typeof modules === 'object') { initModules(modules) } } // https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/register.js export function initModules (modules, ifReplace) { for (const moduleName in modules) { // init `modules[moduleName][]` let methods = nativeModules[moduleName] if (!methods) { methods = {} nativeModules[moduleName] = methods } // push each non-existed new method modules[moduleName].forEach(function (method) { if (typeof method === 'string') { method = { name: method } } if (!methods[method.name] || ifReplace) { methods[method.name] = method } }) } } requireModule 我们通过weex.requireModule('xxx')来获取module,首先我们需要了解一下weex这个全局变量是哪儿来的,上面在渲染的过程中的时候会生成一个weex实例,这个信息会被保存在一个全局变量中weexGlobalObject,在callFunction的时候,这个对象会被绑定在js bundle执行时的weex对象上,具体如下。 const globalObjects = Object.assign({ ... weex: weexGlobalObject }, timerAPIs, services) weex这个对象上还有会很多方法和属性,其中就有能调用到module的方法就是requireModule,这个方法和上面客户端注入Module时的方法是放在同一个模块中的,也就是同一个闭包中的,所以可以共享nativeModules这个对象。 //https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/index.js App.prototype.requireModule = function (name) { return requireModule(this, name) } // https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/register.js export function requireModule (app, name) { const methods = nativeModules[name] const target = {} for (const methodName in methods) { Object.defineProperty(target, methodName, { configurable: true, enumerable: true, get: function moduleGetter () { return (...args) => app.callTasks({ module: name, method: methodName, args: args }) }, set: function moduleSetter (value) { if (typeof value === 'function') { return app.callTasks({ module: name, method: methodName, args: [value] }) } } }) } return target } 上面为什么没有使用简单的call或者apply方法呢?而是在返回的时候对这个对象所有方法进行了类似双绑的操作。首先肯定是为了避免对象被污染,这个nativeModules是所有weex实例共用的对象,如果一旦可以直接获取,前端对象都是引用,就有可能被重写,这样的肯定是不好的。 这里还用了一个callTasks,这个前面初始化的时候都已经说明过了,其实就是调用对应native的方法,taskCenter.send就会去查找客户端对应的方法,上面有taskCenter相关的代码,最后通过callNativeModule调用到客户端的代码。 // https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/ctrl/misc.js export function callTasks (app, tasks) { let result /* istanbul ignore next */ if (typof(tasks) !== 'array') { tasks = [tasks] } tasks.forEach(task => { result = app.doc.taskCenter.send( 'module', { module: task.module, method: task.method }, task.args ) }) return result } 完成调用之后就等待客户端处理,客户端处理完成之后进行返回。这里虽然是一个forEach的遍历,但是返回的result都是同步的最后一个result。这里不是很严谨,但是我们看上层结构又不会有问题,tasks传过来一般是一个一个的任务,不会传array过来,并且大部分的客户端调用方法都是异步的,很少有同步回调,所以只能说不严谨。 总结 通过上面的梳理,我们可以看到Weex运行原理的细节,整体流程也梳理清楚了,我们通过一年的实践,不管是纯Weex应用还是现有APP接入都有实践,支撑了我们上百个页面的业务,同时开发效率得到了非常大的提升,也完善了我们基于Vue的前端技术栈。 现在Weex本身也在不断的更新,至少我们的业务上线之后让我们相信Weex是可行的,虽然各种缺点不断的被诟病,但是哪个优秀的技术的没有经历这样的发展呢。摘掉我们前端技术的鄙视链眼镜,让技术更好的为业务服务。 最后我们在通过业务实践和积累之后,也归纳总结出了基于Weex的技术解决方案 Eros并开源出来,解决了被大家所诟病的环境问题,提供更多丰富的Component和Module解决实际的业务问题。目前已有上千开发者有过开发体验,在不断吐槽中改进我们的方案,稳定了底层方案,构建了新的插件化方式,目前已经有开发者贡献了一些插件,也收集到开发者已上线的40+ APP的案例,还有非常多的APP在开发过程中。希望我们的方案能帮助到APP开发中的你。 下面是一些通过 Eros 上线的APP案例 原文发布时间为:2018年06月08日 原文作者:还是怕麻烦 本文来源: 掘金 如需转载请联系原作者
前言 在项目中经常会遇到下载或导出服务端资源的需求,一般分为2种做法 获取文件流,编码后下载 获取文件的url,直接下载 本文主要探讨第二种方法,在最后会提及文件流的方法。 浏览器的安全策略 在介绍方法之前,我们需要知道浏览器的一些安全机制,防止恶意代码对用户的破坏。 现代浏览器(ie8除外)检测到非用户直接操作产生的新窗口,一般会阻止,比如在ajax的回调中打开新的窗口,因为这些操作并不是在用户点击的线程中,所以会拦截。 预开新标签页 做法 在异步操作之前,先打开一个新标签页 请求后端资源的地址 获取url后去修改空白页的url const downloadTab = window.open('about:blank'); ajax.get('xxx').then(url => { // 使用后端资源的url进行下载 downloadTab.location.href = href; }).catch(err => { // 处理异常 downloadTab.close(); }) 缺点 不管请求成功还是失败都会有新页面的闪烁出现 打开的新页面在什么时候关闭是个问题,因为如果请求时间过长,用户可能自己关闭新页面,更不好处理的情况是页面什么时候触发了下载,因为如果只是更改url就关闭窗口可能还没有开始下载的操作。 生成iframe 做法 为了避免页面闪烁与关闭时机的问题,可以在当前页面使用动态创建iframe的方式,直接下载 ajax.get('xxx').then(href => { if (!href) { return; } if (!this.downIframe) { this.downIframe = document.createElement('iframe'); // 动态创建iframe this.downIframe.src = href; // iframe 中加载的页面 this.downIframe.id = 'downloadIframe'; // iframe 中加载的页面 this.downIframe.style.width = '1px'; this.downIframe.style.height = '1px'; this.downIframe.style.position = 'absolute'; this.downIframe.style.left = '-100px'; document.body.appendChild(this.downIframe); // 添加到当前窗体 } else { this.downIframe.src = href; // iframe 中加载的页面 } }).catch(err => { // 处理异常 }) 缺点 虽然可以优雅的下载文件了,可是如果需要定制下载文件的名称就会令人头疼了,我们需要时机去改变下载文件的名称,否则就会使服务端的文件名称。 生成标签 利用download属性设置文件名, ajax.get('xxx').then(href => { if (!href) { return; } var a = document.createElement('a'); var url = href; var filename = 'test.zip'; a.href = url; a.download = filename; // 在没有download属性的情况,target="_blank",仍会阻止打开 a.click(); }).catch(err => { // 处理异常 }) 注意: 有些浏览器需要将a标签嵌入页面才可执行。 处理文件流 最后大概讲一下如果后端返回流怎么处理。 function funDownload(content, filename) { // 创建隐藏的可下载链接 var link = document.createElement('a'); link.download = filename; link.style.display = 'none'; // 字符内容转变成blob地址 var blob = new Blob([content]); //如果是excel格式 //var blob = new Blob([content], {type: 'application/vnd.ms-excel'}), link.href = URL.createObjectURL(blob); // 触发点击 document.body.appendChild(link); link.click(); // 然后移除 document.body.removeChild(link); }; URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。 注意: 在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。浏览器会在文档退出的时候自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。 原文发布时间为:2018年06月15日 原文作者:nanchenk 本文来源: 掘金 如需转载请联系原作者
vue-cli到多页应用 前言:我有一个cli创建的vue项目,但是我想做成多页应用,怎么办,废话不多说,直接开撸~ 约定:新增代码部分在//add和//end中间 删除(注释)代码部分在//del和//end中间,很多东西都写在注释里 第一步:cli一个vue项目 新建一个vue项目 官网 vue init webpack demo cli默认使用webpack的dev-server服务,这个服务是做不了单页的,需要手动建一个私服叫啥你随意 一般叫dev.server或者dev.client 第二步:添加两个方法处理出口入口文件(SPA默认写死的) 进入刚刚创建vue项目 cd demo 在目录下面找到build/utils.js文件 修改部分: utils.js 'use strict' const path = require('path') const config = require('../config') const ExtractTextPlugin = require('extract-text-webpack-plugin') const packageConfig = require('../package.json') //add const glob = require('glob'); const HtmlWebpackPlugin = require('html-webpack-plugin'); //功能:生成html文件及js文件并把js引入html const pagePath = path.resolve(__dirname, '../src/views/'); //页面的路径,比如这里我用的views,那么后面私服加入的文件监控器就会从src下面的views下面开始监控文件 //end exports.assetsPath = function (_path) { const assetsSubDirectory = process.env.NODE_ENV === 'production' ? config.build.assetsSubDirectory : config.dev.assetsSubDirectory return path.posix.join(assetsSubDirectory, _path) } exports.cssLoaders = function (options) { options = options || {} const cssLoader = { loader: 'css-loader', options: { sourceMap: options.sourceMap } } const postcssLoader = { loader: 'postcss-loader', options: { sourceMap: options.sourceMap } } // generate loader string to be used with extract text plugin function generateLoaders (loader, loaderOptions) { const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] if (loader) { loaders.push({ loader: loader + '-loader', options: Object.assign({}, loaderOptions, { sourceMap: options.sourceMap }) }) } // Extract CSS when that option is specified // (which is the case during production build) if (options.extract) { return ExtractTextPlugin.extract({ use: loaders, fallback: 'vue-style-loader' }) } else { return ['vue-style-loader'].concat(loaders) } } // https://vue-loader.vuejs.org/en/configurations/extract-css.html return { css: generateLoaders(), postcss: generateLoaders(), less: generateLoaders('less'), sass: generateLoaders('sass', { indentedSyntax: true }), scss: generateLoaders('sass'), stylus: generateLoaders('stylus'), styl: generateLoaders('stylus') } } // Generate loaders for standalone style files (outside of .vue) exports.styleLoaders = function (options) { const output = [] const loaders = exports.cssLoaders(options) for (const extension in loaders) { const loader = loaders[extension] output.push({ test: new RegExp('\\.' + extension + '$'), use: loader }) } return output } exports.createNotifierCallback = () => { const notifier = require('node-notifier') return (severity, errors) => { if (severity !== 'error') return const error = errors[0] const filename = error.file && error.file.split('!').pop() notifier.notify({ title: packageConfig.name, message: severity + ': ' + error.name, subtitle: filename || '', icon: path.join(__dirname, 'logo.png') }) } } //add 新增一个方法处理入口文件(单页应用的入口都是写死,到时候替换成这个方法) exports.createEntry = () => { let files = glob.sync(pagePath + '/**/*.js'); let entries = {}; let basename; let foldername; files.forEach(entry => { // Filter the router.js basename = path.basename(entry, path.extname(entry), 'router.js'); foldername = path.dirname(entry).split('/').splice(-1)[0]; // If foldername not equal basename, doing nothing // The folder maybe contain more js files, but only the same name is main if (basename === foldername) { entries[basename] = process.env.NODE_ENV === 'development' ? [ 'webpack-hot-middleware/client?noInfo=true&reload=true&path=/__webpack_hmr&timeout=20000', entry ]: [entry]; } }); return entries; }; //end //add 新增出口文件 exports.createHtmlWebpackPlugin = (publicModule) => { let files = glob.sync(pagePath + '/**/*.html', {matchBase: true}); let entries = exports.createEntry(); let plugins = []; let conf; let basename; let foldername; publicModule = publicModule || []; files.forEach(file => { basename = path.basename(file, path.extname(file)); foldername = path.dirname(file).split('/').splice(-1).join(''); if (basename === foldername) { conf = { template: file, filename: basename + '.html', inject: true, chunks: entries[basename] ? [basename] : [] }; if (process.env.NODE_ENV !== 'development') { conf.chunksSortMode = 'dependency'; conf.minify = { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true }; // 在构建生产环境时,需要指定共用模块 conf.chunks = [...publicModule, ...conf.chunks]; } plugins.push(new HtmlWebpackPlugin(conf)); } }); return plugins; }; //end 第三步:创建私服(不使用dev-server服务,自己建一个) 从express新建私服并配置(build文件夹下新建 我这里叫webpack.dev.client.js) webpack.dev.client.js /** * created by qbyu2 on 2018-05-30 * express 私服 * */ 'use strict'; const fs = require('fs'); const path = require('path'); const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); //文件监控(前面配置了从views下面监控) const webpackHotMiddleware = require('webpack-hot-middleware'); //热加载 const config = require('../config'); const devWebpackConfig = require('./webpack.dev.conf'); const proxyMiddleware = require('http-proxy-middleware'); //跨域 const proxyTable = config.dev.proxyTable; const PORT = config.dev.port; const HOST = config.dev.host; const assetsRoot = config.dev.assetsRoot; const app = express(); const router = express.Router(); const compiler = webpack(devWebpackConfig); let devMiddleware = webpackDevMiddleware(compiler, { publicPath: devWebpackConfig.output.publicPath, quiet: true, stats: { colors: true, chunks: false } }); let hotMiddleware = webpackHotMiddleware(compiler, { path: '/__webpack_hmr', heartbeat: 2000 }); app.use(hotMiddleware); app.use(devMiddleware); Object.keys(proxyTable).forEach(function (context) { let options = proxyTable[context]; if (typeof options === 'string') { options = { target: options }; } app.use(proxyMiddleware(context, options)); }); //双路由 私服一层控制私服路由 vue的路由控制该页面下的路由 app.use(router) app.use('/static', express.static(path.join(assetsRoot, 'static'))); let sendFile = (viewname, response, next) => { compiler.outputFileSystem.readFile(viewname, (err, result) => { if (err) { return (next(err)); } response.set('content-type', 'text/html'); response.send(result); response.end(); }); }; //拼接方法 function pathJoin(patz) { return path.join(assetsRoot, patz); } /** * 定义路由(私服路由 非vue路由) * */ // favicon router.get('/favicon.ico', (req, res, next) => { res.end(); }); // http://localhost:8080/ router.get('/', (req, res, next)=>{ sendFile(pathJoin('index.html'), res, next); }); // http://localhost:8080/home router.get('/:home', (req, res, next) => { sendFile(pathJoin(req.params.home + '.html'), res, next); }); // http://localhost:8080/index router.get('/:index', (req, res, next) => { sendFile(pathJoin(req.params.index + '.html'), res, next); }); module.exports = app.listen(PORT, err => { if (err){ return } console.log(`Listening at http://${HOST}:${PORT}\n`); }) 私服创建好了 安装下依赖 有坑。。。 webpack和热加载版本太高太低都不行 npm install webpack@3.10.0 –save-dev npm install webpack-dev-middleware –save-dev npm install webpack-hot-middleware@2.21.0 –save-dev npm install http-proxy-middleware –save-dev 第四步:修改配置 webpack.base.conf.js 'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js devServer: { clientLogLevel: 'warning', historyApiFallback: { rewrites: [ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, ], }, hot: true, contentBase: false, // since we use CopyWebpackPlugin. compress: true, host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, // necessary for FriendlyErrorsPlugin watchOptions: { poll: config.dev.poll, } }, plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin //del 注释掉spa固定的单页出口 末尾动态配上出口 // new HtmlWebpackPlugin({ // filename: 'index.html', // template: 'index.html', // inject: true // }), //end // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]) ] //add .concat(utils.createHtmlWebpackPlugin()) //end }) //del // module.exports = new Promise((resolve, reject) => { // portfinder.basePort = process.env.PORT || config.dev.port // portfinder.getPort((err, port) => { // if (err) { // reject(err) // } else { // // publish the new Port, necessary for e2e tests // process.env.PORT = port // // add port to devServer config // devWebpackConfig.devServer.port = port // // // Add FriendlyErrorsPlugin // devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ // compilationSuccessInfo: { // messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], // }, // onErrors: config.dev.notifyOnErrors // ? utils.createNotifierCallback() // : undefined // })) // // resolve(devWebpackConfig) // } // }) // }) //end webpack.dev.conf.js 'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') process.env.NODE_ENV = 'development'; const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js //del 注掉SPA的服务器 // devServer: { // clientLogLevel: 'warning', // historyApiFallback: { // rewrites: [ // { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, // ], // }, // hot: true, // contentBase: false, // since we use CopyWebpackPlugin. // compress: true, // host: HOST || config.dev.host, // port: PORT || config.dev.port, // open: config.dev.autoOpenBrowser, // overlay: config.dev.errorOverlay // ? { warnings: false, errors: true } // : false, // publicPath: config.dev.assetsPublicPath, // proxy: config.dev.proxyTable, // quiet: true, // necessary for FriendlyErrorsPlugin // watchOptions: { // poll: config.dev.poll, // } // }, //end plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin //del 注释掉spa固定的单页出口 末尾动态配上出口 // new HtmlWebpackPlugin({ // filename: 'index.html', // template: 'index.html', // inject: true // }), //end // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]) ] //add .concat(utils.createHtmlWebpackPlugin()) //end }) //del // module.exports = new Promise((resolve, reject) => { // portfinder.basePort = process.env.PORT || config.dev.port // portfinder.getPort((err, port) => { // if (err) { // reject(err) // } else { // // publish the new Port, necessary for e2e tests // process.env.PORT = port // // add port to devServer config // devWebpackConfig.devServer.port = port // // // Add FriendlyErrorsPlugin // devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ // compilationSuccessInfo: { // messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], // }, // onErrors: config.dev.notifyOnErrors // ? utils.createNotifierCallback() // : undefined // })) // // resolve(devWebpackConfig) // } // }) // }) //end module.exports = devWebpackConfig; webpack.prod.conf.js plugins最后加上.concat(utils.createHtmlWebpackPlugin([‘manifest’, ‘vendor’])) test环境一样 第五步:修改package.json 指令配置 scripts下面’dev’: 这样执行的时候就不会走默认的dev-server而走你的私服了 "scripts": { "dev": "node build/webpack.dev.client.js", "start": "npm run dev", "build": "node build/build.js" }, 第六步:创建测试文件 src目录下新建 views文件夹 (代码注释里有 当时配的目录跟这个一致就可以 随便你命名 遵循命名规范就行) views 文件夹下新建两个文件夹index和home 代表多页 每页单独一个文件夹 文件夹下建对应文件 打包改为相对路径config/index.js build下面 assetsPublicPath: '/', => assetsPublicPath: './', 最后,npm run dev 或者 npm run build 测试环境自己配 跟 生产环境差不多,就几个配置参数不一样 这个时候你会发现,特么的什么鬼文章 报错了啊 稍安勿躁~ 两个地方, 1.webpack.dev.client.js //双路由 私服一层控制私服路由 vue的路由控制该页面下的路由 app.use(router) app.use('/static', express.static(path.join(assetsRoot, 'static'))); 这个assetsRoot cli创建的时候是没有的 在config/index.js 下面找到dev加上 assetsRoot: path.resolve(__dirname, '../dist'), 2.还是版本问题 webpack-dev-middleware 默认是3.1.3版本但是会报错 具体哪个版本不报错我也不知道 context.compiler.hooks.invalid.tap('WebpackDevMiddleware', invalid); 找不到invalid 源码里面是有的卸载webpack-dev-middleware npm uninstall webpack-dev-middleware 使用dev-server自带的webpack-dev-middleware (cli单页应用是有热加载的)重新install dev-server npm install webpack-dev-server@2.10.0 --save-dev npm run dev 总结:核心点就在创建并配置私服和修改出口入口配置,坑就在版本不兼容 建议:cli一个vue的demo项目 从头撸一遍 再在实际项目里使用,而不是copy一下运行没问题搞定~ 建议而已,你怎么打人,呜呜呜~ 快过节了,觉得本文对你有用的话请随意打赏,让作者可以买个棒棒糖吃~ ——————————————-6.1更—————————————– 留了一个坑,一天了,有赞有收藏,没见人评论指出坑,心痛的无法呼吸~ build 后 没有引入共用模块 代码已更新~ build后可正常访问… 注:内容有不当或者错误处请指正~转载请注明出处~谢谢合作! 原文发布时间为:2018年06月15日 原文作者:掘金本文来源: 掘金 如需转载请联系原作者
Solution of management system front-end based on vue.js and ElementUI 介绍 D2Admin 是一个开源的管理系统前端集成方案 Github仓库 - 预览地址 中文文档 D2Admin 是完全开源免费的管理系统集成方案,由 FairyEver 在工作之余完全由兴趣驱动完成,如果你也一样喜欢前端开发,欢迎加入我们的讨论/学习群,群内可以提问答疑,分享学习资料或者随便扯淡 功能 首屏加载等待动画 避免首次加载白屏尴尬 简约主题 每个插件和组件都配有介绍文档 图片资源 sketch 源文件( 可以在这个文件内重新生成所有图片资源 ) 登陆和注销 根据路由自动生成菜单 可折叠侧边栏 方便的菜单设置 多国语言支持 富文本编辑器 Markdown 编辑器 全屏功能 Fontawesome 图标库 图标选择器(组件) 自动引入下载的 SVG 图标 前端假数据支持( mock ) 集成蚂蚁金服出品的 G2 图表 图表自适应可拖拽大小的卡片容器(示例) 简化剪贴板操作 简化Cookie操作 时间日期计算工具 导入 Excel ( xlsx 格式 + csv 格式 ) 数据导出 Excel ( xlsx 格式 + csv 格式 ) 数据导出文本 数字动画 可拖拽调整大小的切分布局 可拖拽调整大小和位置的网格布局 提供三种方便的页面容器组件(正常卡片,隐形容器,填满页面) 代码高亮显示 加载并解析(或者直接指定资源) markdown 文件 GitHub 样式的 markdown 显示组件 markdown 内代码高亮 为 markdown 扩展了百度云链接解析和优化显示 右键菜单组件 自定义滚动条和滚动控制 内置4种主题 公用样式抽离,方便的主题定制 支持百万级数据量的表格组件 打包后随意目录部署(已经做好兼容设置) TODO D2Admin 仍然处于开发中,这里有一些计划: 分离出简化版本 推出基于 ice 平台的版本 增加右上角通知中心 增加一些实例页面以提供业务页面布局建议 更换图表库 多 tab 页结构 面包屑导航 树型表格组件 1.1.0 完成 抽离项目里的文档,集中存放在文档站点 1.1.0 完成 对主界面进行一次完善,调整整体布局和颜色 1.1.0 完成 切换主题功能 欢迎你为 D2Admin 的开发作出贡献(代码编写/文档翻译)。 目录结构 ├─ build ├─ config ├─ docs // 文档 ├─ src │ ├─ assets // 资源 │ │ ├─ icons │ │ ├─ image │ │ ├─ library │ │ └─ style │ ├─ components // 组件 │ │ ├─ charts │ │ ├─ core │ │ └─ demo │ ├─ i18n // 多国语 │ ├─ menu // 菜单 │ ├─ mock // 模拟数据 │ ├─ pages // 页面 │ ├─ plugin // 插件 │ ├─ router // 路由 │ ├─ store // vuex │ ├─ utils │ ├─ App.vue │ └─ main.js ├─ static // 静态资源 ├─ .babelrc ├─ .editorconfig ├─ .eslintignore ├─ .eslintrc.js ├─ .gitattributes ├─ .gitignore ├─ .postcssrc.js ├─ LICENSE ├─ README.md ├─ deploy.sh ├─ design.sketch // 设计文件 ├─ index.html └─ package.json 原文发布时间为:2018年05月26日 原文作者:FaryEver本文来源: 掘金 如需转载请联系原作者
这是准备跑路了么?
写脚本,找到元数据表,循环truncate
TSQL在阿里集团内部业务中用于基础设施实时监控报警,查询秒级返回,性能满足业务的要求。当然,查询延迟和查询的复杂度和数据量相关,具体的情况要具体分析。