Quantcast
Channel: IT社区推荐资讯 - ITIndex.net
Viewing all 11848 articles
Browse latest View live

请警惕 ES 的三大坑

$
0
0

本文主要内容如下:

搜索引擎现在是用得越来越多了,比如 日志系统用到的 ELK 中的 E 就是 搜索引擎 Elasticsearch(简称 ES)。

那对于搜索这种技术来说,最看重的是搜索的结果的准确性和搜索的响应时间。ES 的准确性可以通过 倒排索引算法来保证,那响应时间就需要磁盘或缓存来支持了,那么磁盘和缓存会带来哪些坑呢? ( 其实不论是分布式的,还是单机模式下的搜索引擎都会遇到这个问题。 )

一、ES 慢查询之坑

Elasticsearch 是现如今用的最广泛的搜索引擎。它是一个分布式的开源搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据。

1.1 工作原理:

ES 的工作原理:往 ES 里写数据时,实际是写到磁盘文件,查询时,操作系统会将磁盘文件里的数据自动缓存 filesystem cache 里面。如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,则搜索的时候走内存,性能较好。

坑: 首先如果访问磁盘那一定很慢,而走缓存会快很多。但如果很多没用的字段数据都丢到缓存里面,则会浪费缓存的空间,所以很多数据还是存在磁盘里面的,那么大部分查询走的数据库,则会带来性能问题。

1.2 案例

ES 节点 3 台机器,每台机器 32 G 内存,总内存 96 G,给 ES JVM 堆内存是 16 G,那么剩下来给 cache 的是 16 G,总共 ES 集群的的 cache 占用内存 48 G ,如果所有的数据占据磁盘空间 600 G,那么每台机器的数据量是 200 G,而查询时,有 150 G 左右的的数据是走磁盘查询的,那么走 cache 的概率是 48 G/ 600 G = 8%,也就是说大量查询是走磁盘的。

1.3 避坑指南:

1.3.1 存储关键信息

将数据中索引字段存到 cache,比如 一行数据有 name、gender、age、city、job 字段,而检索这条数据只需要 name 和 gender 就可以查询出数据,那么 cache 就只需要存 id、name 和 gender 字段。别把所有字段都丢到 cache 里面,纯属浪费空间,资源是有限的。那剩下的字段怎么检索出来?可以把其他字段存到 mysql/hbase 里面。hbase 特点:适用于海量数据的在线存储。缺点是不能进行复杂的搜索。根据 name 和 gender 字段从 ES 中拿到 100 条数据 ( 包含 doc id ) ,然后根据 doc id 再去 hbase 中查询每个 doc id 对应的完整数据,将结果组装后返回给前端 ( 需要考虑分页的情况 ) 。

1.3.2 数据预热

将访问量高的数据或者即将访问量高的数据放到 filesystem cache 里面。每隔一段时间就从数据库访问下数据,然后同步到 filesystem cache 里面。

1.3.3 冷热分离

不常访问的数据和经常访问的数据进行隔离。比如 3 台机器存放冷数据的索引,另外 3 台存放热数据的索引。

1.3.4 避免使用关联查询

ES 中的关联查询是比较慢的,性能不佳,尽量避免使用。

二、ES 架构之坑

通常情况下,我们会使用 ES 的集群模式,在集群规模不大的情况下,性能还算可以,但如果集群规模变得很大,则会遇到集群瓶颈,也就是说集群扩大,性能提升甚微,甚至不增反降。

ES 的集群也是采用中心化的分布式架构,整个集群只有一个是 Master 节点。而它的职责非常重要:负责整个集群的元数据管理,元数据包含全局的配置信息、索引信息、节点信息,如果元数据发生改变,则需要 master 节点将变更信息发布到集群的其他节点。

另外因为 master 节点的任务处理是单线程的,所以每次处理任务时,需要等待全部节点接收到变更信息,并处理完变更的任务后,才算完成了变更任务。

那么这样的架构会带来什么问题:

响应时间问题。如果元数据发生了改变,但某节点假死,比如 JVM 的内存爆了,但是进程还活着,那么响应 master 节点的时间会非常长,今而影响单个同步信息任务的完成时间。任务恢复问题。有大量恢复任务的时候,任务需要排队,恢复时间变长。任务回调问题。任务执行完成后,需要回调大量 listener 处理元数据变更。如果分片的数据很大,则处理时间会到 10 秒级,严重影响了集群的恢复能力。

解决方案:采用 ES 的 tribe node 特性实现 ES 多集群。文中后面会介绍下 tribe node 的原理。

三、业务场景的坑

ES 的被广泛应用到多个场景,比如查询日志、查询商品资料、数据聚合等。而这些场景的需求又有非常大的差异,这也是一个坑。

**场景一:前端首页搜索功能。**比如搜索商品,数据实时写入的频率不高,但是读的频率很高。

**场景二:日志检索的功能。**日志系统中,我们一般都是 ELK 这种架构模式,对实时写入要求很高,而查询的次数其实不多,毕竟查询日志多工作还是开发和运维人员来做。

场景三:监控、分析的功能。ES 也会被运用到需要监控数据和分析数据的场景中,而这种场景又是对 ES 的内存要求比较高,因为这些分析功能是在内存中完成的,若内存出现太大的压力,则会造成系统的垃圾回收,可能出现短暂的服务抖动。

解决方案:按业务场景划分 ES 集群,同样采用 ES tribe node 功能。

四、ES Tribe Node 方案

ES tribe node 功能原理图如下所示:

有两个 ES 集群,每个集群都有多个 ES 节点。Logstash 负责日志搜集。Kibana 负责客户端查询,将查询命令传送到 ES Tribe Node。ES Tribe Node 还承担了集群管理的职责。

参考资料:https://www.infoq.cn/article/SbfS6uOcF_gW6FEpQlLKhttps://www.elastic.co/guide/en/elasticsearch/reference/2.0/modules-tribe.htmladvance-java


几款移动跨平台App开发框架比较 - song-z - 博客园

$
0
0

整理目前流行的跨平台WebApp开发技术的特点,仅供参考。

每个框架几乎都包含以下特性:

  • 使用 HTML5 + CSS + JavaScript 开发
  • 跨平台重用代码
  • 丰富的UI库
  • 提供访问设备原生API的 JavaScript API 包装器
  • 解决原生开发中机型适配的难题
  • 提供打包、部署的工具或服务
  • 都需要学习自身封装的 JavaScript API

筛选框架的要求:

  • 性能:运行速度快
  • UI:提供接近原生的UI体验
  • 插件多,文档丰富,开发效率高,容易扩展和维护
  • 满足业务需求

Cordova

Cordova 和 PhoneGap 的区别?
PhoneGap 是 Apache Cordova 的一个分发版,就像 Ubuntu 是 基于 Linux 的一个发行版,其代码库也基于 Cordova,只是 PhoneGap 关联了 Adobe 的一些额外的商业工具或服务,例如 PhoneGap Build 和 Adobe Shadow,来帮助开发者简化开发。
此外,两者提供的CLI工具、项目结构有差异,如:
Cordova 把 config.html 放在项目目录下,而 PhoneGap 把它放在www 目录下。

优点

  • 开源免费,社区生态成熟,插件丰富
  • 支持离线场景应用
  • 开发工具选择空间大

缺点:

  • 只提供基础访问设备的接口,需要自己搭配其他UI框架和JavaScript框架来搭配

UI框架

参考资料

Cordova中文文档
创建第一个App(英文)
利用 Cordova+Famous 创建高性能跨平台APP
使用 Cordova 和 Vue.js 创建移动应用

Ionic

官网地址: http://www.ionic.wang/(有案例)
Ionic = Cordova + AngularJS + 一套样式库。

技术要求

  • HTML + CSS + AngularJS

优点

  • 基于 Cordova
  • 漂亮的界面,追求性能,专注原生,免费开源
  • Angular JS MVVM 开发理念,数据双向绑定
  • 继承自 Cordova,可以使用 Cordova 的插件

缺点

  • Angular JS 学习路线陡峭
  • Ionic 框架相比于原生的 Cordova 有所差异,Cordova 某些官方插件可能不适用于Ionic

AppCan

通过AppCan IDE集成开发系统、云端打包器等,快速开发出Android、iOS、WP平台上的移动应用。

有两种方式创建项目:IDE 和云端,并且IDE可以同步到云端。
免费用户有100M空间、50个应用的限制。

优点

  • 提供一体化解决方案,方便环境搭建、开发、调试、发布
  • 框架自带UI包,包含常用控件样式
  • 框架对UI、动画渲染进行过优化,反应速度快
  • 支持本地打包、云端打包
  • 基于密钥的代码加密

缺点

  • 不开源,无法修改、优化底层代码
  • 分大众版和企业版,大众版免费,但功能有缺失,详细见附录
  • 暂不支持自行开发控件/,无法调取android原生功能
  • 框架自带功能过多,导致应用安装包偏大。
  • 文档偏少
  • 部分系统无法使用IDE进行调试
  • 只能在服务器端发布,无法在本地发布
  • IOS发布,需要将证书上传至服务器

参考:
Phonegap VS AppCan

使用案例
我爱我家App 等

附录
企业版和大众版主要有以下几点区别:

  • 开发环境:
    企业版走独立的开发环境与打包环境,企业版配备macmini打包服务器,可以实现本地环境下创建项目,调试,打包;
    大众版不管是创建项目还是打包都需要依托于官方的服务器,需要在联网的情况下进行,打包需要将源码上传到官方服务器进行打包;
  • 版本控制:企业版独立控制引擎插件的版本;
    大众版官方统一维护,官方换哪个版本开发者就需要使用哪个版本,没有选择;
  • 协同开发:企业版可通过macmini后台分配开发者或者应用管理员帐号,可实现协同开发。
    大众版不能满足协同开发
  • 企业版有推送API接口
    大众版没有
  • 售后服务:企业版有独立的售后团队
    大众版的入口是论坛

Dcloud

Dcloud组件

Dcloud和原生开发对比

特点:
云编译必须联网获取AppId

优点:

  • 国内厂商,中文文档
  • 对HTML5的性能、工具、能力都做了深入扩展,提供 IDE 、云服务等帮助节省时间
  • MUI 更贴近国内App使用习惯,提供模块的详细例子,如登录,个人中心

缺点:

  • 部分操作需要具备原生开发经验,如离线打包App
  • 新产品仍然有bug,还需改进

学习路线:

DCloud学习路线

APICloud

优点:

  • 不懂原生开发,不懂后台语言就可完成APP

缺点:

  • 更新速度快,版本不够稳定
  • 面向不懂App开发人群,不适合程序员和科技公司,过度依赖会降低技术水平
  • 涉嫌抄袭DCloud大量代码

React Native

能够在Javascript和React的基础上获得完全一致的开发体验,构建世界一流的原生APP。
仅需学习一次,编写任何平台。(Learn once, write anywhere)

缺点:

  • 初次学习成本高
  • 必须在不同平台下写两套代码,依赖暴露的接口

总结

经过一番对比,感觉 Cordova 和 DCloud 更适合本次项目。

原因:
Cordova

  • 生态成熟,有更多可搭配工具使用,开源代码可自由定制;
  • 前端框架: famous 或 Framework7

DCloud

  • 国产中的开源,免费,性能不错
  • 提供云服务帮助打包和部署、测试,降低一部分门槛,减少时间;
  • 前端框架:MUI

其他框架不适合的原因
APICloud

  • 生态不好,名声不好,面向群体不适合;

AppCan

  • 闭源,商业化产品,免费版限制太多;

Ionic

  • AngularJS 学习曲线陡峭,需要时间;

React Native

  • 学习成本高

基于RN+微应用打造多业务支撑的企业官方App_开发

$
0
0

一、用户体验差

大型企业里不同C端业务大都是由不同的团队开发,所使用的技术以及页面风格都不相同,有的使用原生开发,体验较好;有的使用h5,体验较差。很难做到统一。

二、碎片化严重

不同的业务建设相互独立的App,独立分发推广,浪费资源不说,还显得很杂乱。对于市场和需求的变更,很难形成合力。

三、需求响应缓慢

市场需求变化非常快,越来越多的业务都在手机端处理了,以保险业务为例,用户办理了寿险的业务同时引导用户办理财产险业务,这个时候希望可以直接办理而不是去下载一个产险的App再去办理。

四、用户留存率低

App业务的单一化导致用户需求量大大减少,用户大多是在业务员的推广中下载安装了App,但App只涉及到单一业务,很难吸引用户再次开启去查看。

综上,大型企业建设统一的官方App是一种必然趋势。当然,在建设的过程中也将面临诸多挑战,接下来给大家分享建设官方App所面临的 三大挑战:

挑战一:用户体验与技术门槛的抉择

随着移动技术的的发展,智能手机的普及,越来越多的App建设把用户体验放在了第一位,要求界面好看,操作要流畅,简单来说就是要像微信支付宝一样好用。需要完成这样的需求首要选择是使用原生开发。原生开发无疑体验是最好的,但同时也带来了新的问题,操作系统的多样性如何快速适配,业务如何快速上线,如何可以不经过Appstore的审核即可灵活控制上线流程……

挑战二:独立建设的App如何整合

很多企业在移动信息化的浪潮中建立了许多App,建设统一的官方App不可能从零开始,不然原有的投入浪费不说,大型企业的业务相对复杂,如此多的业务很难在短时间内开发完成。

挑战三:不同的团队如何协作开发

独立建设的App往往是有不同的团队开发,所采用的技术语言不同,引用的第三方sdk的版本不同。相同的功能模块选择了不同的实现,如OCR,有的团队使用的是前端解析,有的是后端。整合到一起后的App如何协同开发是第三大挑战。

2.基于RN+微应用打造多业务支撑的

企业官方App

为什么选择RN作为整合技术?

选择什么样的技术作为官方App的整合技术是关键,既要良好的用户体验,又需要快速开发、易于整合。我们来看下移动端技术的演进,大致分为如下 四个阶段

1、网页开发

相信早期做移动App还记得,Appstore刚推出来的时候,还是允许App做个壳,直接连的是后端的一个网站,目前这种App上不了Appstore,体验实在是太差。当然本地能力也是缺失了,比如调用摄像头。

2、原生开发

随着智能手机的普及,原生开发兴起。原生开发的体验好,但是成本相对来讲高,业务一致性相对比较低,业务上线的时候,Android和iOS都要上线才可以。当然,对于这种方式还有个硬伤,更新应用严重依赖与市场和用户是不是主动下载最新版本,推广的难度也比较高。原生热更方案难以落地,特别是上Appstore的应用,会直接被拒绝。

3、混合开发

是结合了网页开发的和原生开发的优点,其大致的思路是采用HTML(或者很多人说的H5)作为UI,通过嵌入或者系统的浏览器作为渲染(通常采用Webkit),当需要本地能力的时候,采用原生语言的方式编写,并提供接口给UI端调用。因其UI的渲染采用浏览器的方式,难免会影响到用户的体验。

4、驱动原生

对于驱动原生,这种方案的大致思路是,在运行态的时候,通过调用操作系统提供的接口,对UI进行渲染,而不是把渲染交给浏览器内核。无论从用户体验、跨平台、性能、以及热更方案,都得到了广泛的认可。

综上我们选择的RN作为整合官方App的主要开发语言。

选择RN的优势一:技术先进,用户体验好

RN技术的三大特性:体验好,热更新,原生能力。可以实现一个真正的Mobile Native App,降低了技术门槛的同时带来了良好的用户体验。

但RN有一个缺陷就是开发人员开发的所有业务代码最终都会打包成一个bundle文件:

1、随着业务的增加,bundle文件越来越大,应用启动和运行速度都会较慢,达不到原来预想的原生体验。

2、对于多个开发团队,开发的代码耦合性太高,必须打包一起才可以发布,开发维护成本非常高;对于需求的响应会变的缓慢。

选择RN的优势二:RN多bundle模式支撑多团队开发

针对原生的RN应用,我们团队将其拆分成了多bundle,有效支撑多团队并行开发:

1、将RN基础能力和公共的一些API打包成了单独bundle文件,随着apk和ipa一起发布到应用市场。

2、业务代码拆分打包成了多个bundle文件,每个bundle文件都可以独立发布。

3、应用启动的时候加载badebundle+所需要的业务bundle,做到按需加载,业务代码再多也不用担心首次启动过慢问题。

选择RN的优势三:底层原生,易于整合多应用

很多大型企业早期对于C端业务也建设了一些App,建设统一的官方App并不是从零开始,如何有效的整合原有的应用,保障业务的正常运转是官方App必须考虑的问题,原有的多个App都使用了三方SDK,如何处理呢?

第一步:需要梳理出公共的SDK,封装公共API

第二步:对于一些偏向业务的原生模块,封装成业务API

选择RN做为整合语言,因为RN底层是原生应用,易于整合现有的三方SDK和公共的API,可以很好的和其他微应用通信。

为什么选择微应用模式?

微应用模式区别于传统的App开发模式,具备以下 三个特点

第一:开发期项目独立,这是微应用模式的基础

开发的独立性,确保了多个团队能够并行开发且无需要相互依赖,其应用的功能又可以与官方App相互独立,确保其自身功能的自由性。当然开发期的独立性并不意味着没有相关的约束。为了能让官方App健康的发展,相同的约束是必须的。我们熟悉的微信,在开发公众号时,需要遵守微信的相关的API规范。总结来说,开发期项目的独立性,并不是随意性,而是从团队、时间、功能等角度的独立性。

第二:业务上隔离性是官方App能够正常运转的基础

这里需要考虑两个因素,业务的相关资源需要单独规划,避免业务之间相互干扰;同时需要避免新增代码导致整个官方App的不稳定性。

第三:运行态支持动态部署

开发完成的App既可以运行在官方App中,也可以打包成单独的App在手机上运行。开发人员不用关心开发完成的App是在微应用中运行,还是独立的App。

选择微应用模式的优势一:既支持独立开发,又能约束引用

微应用模式的好处就是独立开发,对于使用RN结合微应用模式,RN使用的公共接口我们在basebundle里约定,可以很好的控制App的安全性和稳定性(特别是三方SDK的引用,可以有效控制,避免冲突),会有专门的团队去维护basebundle。各业务功能的开发团队只需要根据API文档开发相应的业务功能即可,然后打包成微应用提供给官方App即可。

选择微应用模式的优势二:统一开发流程,易于整合现有业务

官方App建设不是说所有的功能都完美了再发布,而是一个快速迭代的过程。微应用模式的第二个优势:可以制定统一的开发流程,从RN微应用的开发调试、编译、测试、发布更新全生命周期的管理。保障了各模块新的业务功能能够独立的开发测试及发布上线,互不影响。

3.某保险公司官方App案例

某保险公司C端App现状

某保险公司有着千万级别的用户群体,包含产险、寿险、健康险、养老、保险箱等多项业务,而这些触点都是独立开发维护的,所使用技术也不一样,原生+RN+H5+混合开发,移动端的技术基本都使用了,如何有效的整合现有的App到官方App里,是非常大的难点。

基于RN+微应用聚合官方生态App

在该客户实施过程中,我们采用了RN+微应用的模式,整合了现有App共同打造了集团的官方App。对于原有的业务,依然由原有的开发团队使用微应用模式开发,通过统一的编译服务,最终整合成统一的官方App,保障了原有业务的正常使用。

统一的官方App

建设完成的统一的官方App,小伙伴们在首页就可以看到熟悉的业务App图标,点击立即到达。

建设完成统一的官方App:

一、提升用户体验

一站式APP内体验跨子公司的服务,统一入口,全面提升用户体验

二、提高用户覆盖数

全集团统一APP,实现各BG客户关键旅程的融合,提高用户覆盖量达分发数80%

三、提高用户活跃度

统一APP入口,提供多元化服务,满足客户多样化业务需求,提升整体用户活跃度

四、降低开发成本

基于同一套APP基座进行应用层面功能开发,统一管理,有效降低开发成本

4.总结

建设统一的官方App是一种必然趋势,通过本文主要和大家分享了采用RN+微应用模式建设统一的官方App,采用RN技术有效整合原有的开发资源,带来原生的用户体验;采用微应用模式,支撑了多团队快速开发业务需求并能整合到官方App中,降低了开发维护成本。建设完整的官方App打造了一站式服务体验,提供多元化服务,满足客户多样化业务需求,提升整体用户活跃度。

问1:微应用的原理是啥?

1)有没有侵入RN jsbundle的打包,id转化为name之类的

2)支不支持动态的删除和加载微应用(在不重启的情况下)

3)RN不同版本的适配问题

4)微应用动态加载过程中能够定位出现的问题吗?

5)微应用的开发调试

答:微应用是应用存在的另一种模式,支持动态加载

1)我们修改的RN的js编译流程,对rn的编译做了优化

2)对于所有的微应用支持在不重启的情况动态加载,刷新RN的缓存

3)对于RN的版本我们约定统一版本,所有接入微应用会统一升级

4)在开发期调试错误会正常显示,运行太实用框架收集错误日志

5)我们提供了微应用的调试服务和调试基座,支持动态调试

问2:rn和flutter,该怎么选呢?

答:RN 是Facebook2015年推出的驱动原生框架,Facebook自己也在使用,各大主流的互联网公司使用类似的技术开发App。开发期使用js,一次开发可以运行在Android和iOS两个平台,运行时接近原生的体验。google自己推出的框架在自己公司的产品里都没有使用,建议观望其发展而不要冒然跟进。

问3:请问微应用也是rn开发的吗?

答:是的,大多说的微应用是使用RN开发的,也有部分微应用采用的混合模式,后续会迁移使用RN开发。

问4:APP基座主要负责提供哪些能力给微应用?

答:基座负责整个App框架的搭建,所有接入的三方能力(如微信分享、支付、OCR、活体)等等。提供微应用运行能力,微应用之间业务流转能力等。

问5:原生渲染和h5相比,效率差很多吗?

答:原生渲染使用的操作系统底层的能力渲染,而h5使用的是webkit的壳渲染,效率相差很大。

问6:目前很多app,原生部分已经很少了,大部分是h5页面,那么替换成rn是否有必要呢?另外rn只是实现原来原生的部分,还是一些h5也要改成rn呢?

答:感兴趣的话您可以去关注下目前各大互联网公司的App,基本都是采用原生或运行时原生处理的。对于需要交互的界面为了用户体验大都采用原生,而浏览类的使用h5。h5有使用的场景,不是所有的都要替换,建议用户交互类的使用RN开发,单纯的浏览功能使用h5。

问7:还是不明白这几种不同方式开发的app是如何整合到统一官方的app,能否再详细点说说这个过程。

答:对于采用不同开发技术及语言开发的App整合到一起确实不是一件容易的事情,前期需要做大量的调研工作。

1、调研各App团队所使用的技术语言,开发框架

2、需要各团队整理出使用的三方组件

3、制定统一的开发规范和集成规范

4、整合开发集成

问8:airbnb早期宣布放弃rn,能分享下rn做业务开发遇到的坑吗,还建议使用吗?

答:任何的技术语言和开发框架都有利有弊,主要是合理利用其优势部分。RN 的优势在于快速开发、迭代,支持热更新,业务可以快速到达。

问9:上面说的案例,各个团队按微应用方式开发,是说按新框架进行原有应用的整个开发吧?

答:微应用模式开发,但不是推翻重新开发,而是按照统一的开发规范改造原有应用使其能够在统一的官方App正常运行。

关于作者:刘磊,普元移动产品资深研发工程师,诺亚财富,张家港银行、韵达快递、中信重工、联通集团等众多移动平台项目实施研发经验,精通移动平台架构及管控体系设计。


有道Kubernetes容器API监控系统设计和实践

$
0
0

【编者的话】本篇文章将分享有道容器服务API监控方案,这个方案同时具有轻量级和灵活性的特点,很好地体现了Kubernetes集群化管理的优势,解决了静态配置的监控不满足容器服务监控的需求。并做了易用性和误报消减、可视化面板等一系列优化,目前已经超过80%的容器服务已经接入了该监控系统。

背景

Kubernetes 已经成为事实上的编排平台的领导者、下一代分布式架构的代表,其在自动化部署、监控、扩展性、以及管理容器化的应用中已经体现出独特的优势。

在Kubernetes容器相关的监控上, 我们主要做了几块工作,分别是基于Prometheus的Node、Pod、Kubernetes资源对象监控,容器服务API监控以及基于Grafana的业务流量等指标监控。

在物理机时代,我们做了分级的接口功能监控——域名级别接口监控和机器级别监控,以便在某个机器出现问题时,我们就能快速发现问题。
1.png

上图中,左边是物理机时代对应的功能监控,包括域名级别接口监控和3台物理机器监控。右边是对应的Kubernetes环境,一个Service的流量会由Kubernetes负载均衡到pod1,pod2,pod3中,我们分别需要添加的是Service和各个Pod的监控。

由于Kubernetes中的一切资源都是动态的,随着服务版本升级,生成的都是全新的Pod,并且Pod的IP和原来是不一样的。

综上所述,传统的物理机API不能满足容器服务的监控需求,并且物理机功能监控需要手动运维管理, 为此我们期望设计一套适配容器的接口功能监控系统,并且能够高度自动化管理监控信息,实现Pod API自动监控。

技术选型

为了满足以上需求,我们初期考虑了以下几个方案。
  1. 手动维护各个Service和Pod监控到目前物理机使用的PodMonitor开源监控系统。
  2. 重新制定一个包含Kubernetes目录树结构的系统,在这个系统里面看到的所有信息都是最新的, 在这个系统里面,可以做我们的目录树中指定服务的发布、功能监控、测试演练等。
  3. 沿用PodMonitor框架,支持动态获取Kubernetes集群中最新的服务和Pod信息,并更新到监控系统中。


方案分析

针对方案一,考虑我们服务上线的频率比较高,并且Kubernetes设计思想便是可随时自动用新生成的Pod(环境)顶替原来不好用的Pod,手动维护Pod监控效率太低,该方案不可行。

第二个方案应该是比较系统的解决办法,但需要的工作量会比较大,这种思路基本全自己开发,不能很好的利用已有的功能监控系统,迁移成本大。

于是我们选择了方案三,既能兼容我们物理机的接口功能监控方案,又能动态生成和维护pod监控。

整体设计思路

Kubernetes监控包括以下几个部分: 其中API功能监控,是我们保证业务功能正确性的重要监控手段。
2.png

通常业务监控系统都会包含监控配置、数据存储、信息展示,告警这几个模块,我们的API功能监控系统也不例外。

我们沿用API Monitor框架功能,并结合了容器服务功能监控特点,和已有的告警体系,形成了我们容器API功能监控系统结构:
3.png

首先介绍下目前我们物理机使用的API Monitor监控:一个开源的框架 https://gitee.com/ecar_team/apimonitor

可以模拟探测http接口、http页面,通过请求耗时和响应结果来判断系统接口的可用性和正确性。支持单个API和多个API调用链的探测。

如下图所示,第一行监控里面监控的是图片翻译服务域名的地址,后边的是各台物理机的IP:端口。
4.png

点开每条监控 :
5.png

我们沿用API Monitor框架的大部分功能,其中主要的适配和优化包括:
  1. 监控配置和存储部分:一是制定容器服务Service级别监控命名规则:集群.项目.命名空间.服务;(和Kubernetes集群目录树保持一致,方便根据Service生成Pod监控),二是根据Service监控和Kubernetes集群信息动态生成Pod级别监控
  2. 监控执行调度器部分不用改动
  3. 信息展示部分,增加了趋势图和错误汇总图表
  4. 告警部分,和其它告警使用统一告警组。


具体实践操作

添加Service级别API监控告警

需要为待监控服务,配置一个固定的容Service级别监控。

Service级别监控命名规则:集群.项目.命名空间.服务。

以词典查词服务为例,我们配置一条Service级别的多API监控(也可以是单API监控)。
  • 单API:一个服务只需要加一条case用
  • 多API:如果一个服务需要加多条功能case


6.png

其中“所属系统”是服务所属的告警组,支持电话、短信、popo群、邮件等告警方式(和其它监控告警通用)。

任务名称:取名规则,Rancher中 Kubernetes集群名字.项目名字.命名空间名字.service名字(一共四段)。

告警消息的字段含义:
  • docker-dict:告警组,订阅后会收到告警消息
  • k8s-prod-th:集群
  • dict:项目
  • dict:命名空间
  • data-server:workload名字
  • data-server-5b7d996f94-sfjwn:Pod名字
  • {}:接口返回内容,即:response.content
  • http://dockermonitor.xxx.youda ... sfjwn:告警详细链接


自动生成Pod API监控

自动生成下面三行监控任务:(第一行监控是按上面方法配置的容器Service IP监控,后边三行是自动生成Pod监控任务 )
7.png

监控Service级别是单API,则自动生成的是单API,Service级别是多API,则自动生成的是多API监控。

自动生成的Pod级别监控,除了最后两行标红处(ip: port)和Service级别不一样,其他都一样。
8.png

实现Pod自动生成的方法:
  1. 给Pod Monitor(改框架是Java语言编写的),增加一个Java模块,用来同步Kubernetes信息到Pod Monitor中。考虑到修改Pod Monitor中数据这个行为,本身是可以独立于框架的,可以不修改框架任何一行代码就能实现数据动态更新。
  2. 对比Pod Monitor数据库和Kubernetes集群中的信息,不一致的数据,通过增删改查DB,增加Pod的监控。由于数据之间存在关联性,有些任务添加完没有例行运行,故采用了方法三。
  3. 对比Pod Monitor数据库和Kubernetes集群中的信息,不一致的数据,通过调用Pod Monitor内部接口添加/删除一项监控,然后调接口enable /disable job等。按照可操作性难易, 我们选择了方法三。


针对于Kubernetes集群中查到的一条Pod信息,总共有三种情况:
  1. 对于表中存在要插入Pod的监控信息记录,并且enable状态为1。则认为该Pod的监控不需要改变
  2. 对于表中存在要插入Pod的监控信息记录(删除操作并不会删除源数据信息),并且enable状态为0。则认为该Pod的监控已被删除或者被停止。调用删除操作, 清空QRTZ (例行任务插件)表中的响应内容, 调用delete db操作清出监控信息相关表中的内容(使得监控记录不至于一直在增长)
  3. 对于表中不存在Pod相关信息记录, 则需要新增加的一个Pod。调用post创建监控任务接口(根据Service监控配置), 并调用get请求设置接口为监控enabled状态。


另外对于已经在物理机Pod Monitor中添加了监控的服务,提供了一个小脚本,用于导出物理机Pod Monitor域名级别监控到Docker Monitor监控中。

难点和重点问题解决

误报消减

上线告警抑制

由于服务重启期间,会有removing状态和未ready状态的Pod,在Docker Monitor系统中存在记录,会引起误报。 我们的解决方法是提供一个通用脚本,根据Kubernetes服务的存活检查时间,计算容器服务的发布更新时间,确定再自动开启服务监控的时机。实现在服务重启时间段,停止该服务的接口功能告警;存活检查时间过了之后,自动开启监控。 如下如所示,即Health Check中的Liveness Check检查时间。
9.png

在我们上线发布平台上衔接了该告警抑制功能。

弹性扩缩容告警抑制

原来我们通过查询Rancher的API接口得到集群中全量信息,在我们服务越来越多之后, 查询一次全量信息需要的时间越来越长,基本需要5 min左右。在这个过程中,存在docker-monitor和Kubernetes集群中的信息不一致的情况。一开始试图通过按照业务分组,并行调用Rancher接口得到业务Kubernetes集群信息。时间从5 min缩短到1 min多钟。误报有一定的减少, 但从高峰期到低谷期时间段, 仍然会有若干Pod在Kubernetes集群中缩掉了, 但docker-monitor中仍有相应的告警。

在调研了一些方案之后, 我们通过Kubernetes增量事件(如Pod增加、删除)的机制,拿到集群中最新的信息,Pod的任何变更,3s钟之内就能拿到。

通过ES的查询接口,使用filebeat-system索引的日志, 把Pod带有关键字Releasing address using workloadID (更及时),或kube-system索引的日志:Deleted pod: xx delete 。

通过这个方案,已经基本没有误报。

策略优化

为了适配一些API允许一定的容错率,我们在API Monitor框架中增加了重试策略(单API和多API方式均增加该功能)。

为了适配各类不同业务,允许设置自定义超时时间。
10.png

易用性

增加复制等功能,打开一个已有的告警配置,修改后点击复制, 则可创建一个新的告警项 使用场景:在多套环境(预发、灰度和全量)监控,以及从一个相似API接口微调得到新API监控。
11.png

5业务适配

精品课对服务的容器化部署中使用了接口映射机制,使用自定义的监听端口来映射源端口,将Service的监听端口作为服务的入口port供外部访问,如下图所示。当Service的监听端口收到请求时,会将请求报文分发到Pod的源端口,因此对Pod级别的监控,需要找到Pod的源端口。
12.png

我们分析了Rancher提供的服务API文件后发现,在端口的配置信息中,port.containerPort为服务的监听端口,port.sourcePort为Pod的监听端口,port.name包含port.containerPo -rt和port.sourcePort的信息,由此找到了Pod的源端口与Service监听端口的关键联系,从而实现了对精品课服务接入本平台的支持。
13.png

上线效果

1、容器服务API监控统一,形成一定的规范,帮助快速发现和定位问题。

通过该容器API监控系统,拦截的典型线上问题有:
  • xx上线误操作
  • 依赖服务xxxlib版本库问题
  • DNS server解析问题
  • xxx服务OOM问题
  • xxx服务堆内存分配不足问题
  • xx线上压测问题
  • 多个业务服务日志写满磁盘问题
  • 各类功能不可用问题
  • ……


2、同时增加了API延时趋势图标方便评估服务性能:
14.png

错误统计表方便排查问题:
15.png

结合我们Kubernetes资源对象监控,和Grafana的业务流量等指标监控,线上故障率显著减少,几个业务的容器服务0故障。

总结与展望

总结

本期文章中我们介绍了基于静态API监控和Kubernetes集群化管理方案,设计了实时的自动容器API监控系统。

通过上述方案,我们能够在业务迁移容器后,很快地从物理机监控迁移到容器监控。统一的监控系统,使得我们线上服务问题暴露更及时、故障率也明显减少

展望

  1. 自动同步Kubernetes服务健康检查到docker-monitor系统,保证每一个服务都有监控。
  2. 集成到容器监控大盘中,可以利用大盘中Kubernetes资源目录树,更快查找指定服务,以及关联服务的Grafana指标等监控。
  3. 自动恢复服务,比如在上线指定时间内,发生API监控告警,则自动回滚到上一版本,我们希望监控不仅能发现问题,还能解决问题。


监只是手段,控才是目标。

结语

Docker技术将部署过程代码化和持续集成,能保持跨环境的一致性,在应用开发运维的世界中具有极大的吸引力。

而Kubernetes做了Docker的集群化管理技术,它从诞生时就自带的平台属性,以及良好的架构设计,使得基于Kubernetes容器可以构建起一整套可以解决上述问题的“云原生”技术体系,也降低了我们做持续集成测试、发布、监控、故障演练等统一规划和平台的难度。目前有道业务服务基本都上线到容器,后续我们将陆续迁移基础服务,实现整体的容器化。

原文链接: https://mp.weixin.qq.com/s/K6UJnnpbhciHyvrACo1xAw

    搭建前端异常监控系统

    $
    0
    0

    搭建前端异常监控系统

    涉及技能

    • 收集前端错误(原生、React、Vue)
    • 编写错误上报逻辑
    • 利用Egg.js编写一个错误日志采集服务
    • 编写webpack插件自动上传sourcemap
    • 利用sourcemap还原压缩代码源码位置
    • 利用Jest进行单元测试

    工作流程

    1. 收集错误
    2. 上报错误
    3. 代码上线打包将sourcemap文件上传至错误监控服务器
    4. 发生错误时监控服务器接收错误并记录到日志中
    5. 根据sourcemap和错误日志内容进行错误分析

    异常收集

    首先先看看如何捕获异常。

    JS异常

    js异常的特点是,出现不会导致JS引擎崩溃 最多只会终止当前执行的任务。比如一个页面有两个按钮,如果点击按钮发生异常页面,这个时候页面不会崩溃,只是这个按钮的功能失效,其他按钮还会有效。

    setTimeout(() => {
      console.log('1->begin')
      error
      console.log('1->end')
    })
    setTimeout(() => {
      console.log('2->begin')
      console.log('2->end')
    })
    复制代码

    上面的例子我们用setTimeout分别启动了两个任务,虽然第一个任务执行了一个错误的方法。程序执行停止了。但是另外一个任务并没有收到影响。

    其实如果你不打开控制台都看不到发生了错误。好像是错误是在静默中发生的。

    下面我们来看看这样的错误该如何收集。

    try-catch

    我们首先想到的使用try-catch来收集。

    setTimeout(() => {
      try {
        console.log('1->begin')
        error
        console.log('1->end')
      } catch (e) {
        console.log('catch',e)
      }
    })
    复制代码

    如果在函数中错误没有被捕获,错误会上抛。

    function fun1() {
      console.log('1->begin')
      error
      console.log('1->end')
    }
    setTimeout(() => {
      try {
        fun1()
      } catch (e) {
        console.log('catch',e)
      }
    })
    复制代码

    控制台中打印出的分别是错误信息和错误堆栈。

    读到这里大家可能会想那就在最底层做一个错误try-catch不就好了吗。但是理想很丰满,现实很骨感。我们看看下一个例子。

    function fun1() {
      console.log('1->begin')
      error
      console.log('1->end')
    }
    
    try {
      setTimeout(() => {
        fun1()
    
      })
    } catch (e) {
      console.log('catch', e)
    }
    复制代码

    大家注意运行结果,异常并没有被捕获。

    这是因为JS的try-catch功能非常有限一遇到异步就不好用了。那总不能为了收集错误给所有的异步都加一个try-catch吧,太坑爹了。

    window.onerror

    window.onerror 最大的好处就是可以同步任务还是异步任务都可捕获。

    function fun1() {
      console.log('1->begin')
      error
      console.log('1->end')
    }
    window.onerror = (...args) => {
      console.log('onerror:',args)
    }
    
    setTimeout(() => {
      fun1()
    })
    复制代码
    • onerror返回值

      onerror还有一个问题大家要注意 如果返回返回true 就不会被上抛了。不然控制台中还会看到错误日志。

    监听error事件

    window.addEventListener('error',() => {})

    其实onerror固然好但是还是有一类异常无法捕获。这就是网络异常的错误。比如下面的例子。

    <img src="./xxxxx.png">
    复制代码

    试想一下我们如果页面上要显示的图片突然不显示了,而我们浑然不知那就是麻烦了。

    addEventListener就是

    window.addEventListener('error', args => {
        console.log('error event:', args
        );
        return true;
      }, 
      true // 利用捕获方式
    );
    复制代码

    Promise异常捕获

    Promise的出现主要是为了让我们解决回调地域问题。基本是我们程序开发的标配了。虽然我们提倡使用es7 async/await语法来写,但是不排除很多祖传代码还是存在Promise写法。

    new Promise((resolve, reject) => {
      abcxxx()
    });
    复制代码

    这种情况无论是onerror还是监听错误事件都是无法捕获的

    new Promise((resolve, reject) => {
      error()
    })
    // 增加异常捕获
      .catch((err) => {
      console.log('promise catch:',err)
    });
    复制代码

    除非每个Promise都添加一个catch方法。但是显然是不能这样做。

    window.addEventListener("unhandledrejection", e => {
      console.log('unhandledrejection',e)
    });
    复制代码

    我们可以考虑将unhandledrejection事件捕获错误抛出交由错误事件统一处理就可以了

    window.addEventListener("unhandledrejection", e => {
      throw e.reason
    });
    复制代码

    async/await异常捕获

    const asyncFunc = () => new Promise(resolve => {
      error
    })
    setTimeout(async() => {
      try {
        await asyncFun()
      } catch (e) {
        console.log('catch:',e)
      }
    })
    复制代码

    实际上async/await语法本质还是Promise语法。区别就是async方法可以被上层的try/catch捕获。

    如果不去捕获的话就会和Promise一样,需要用unhandledrejection事件捕获。这样的话我们只需要在全局增加unhandlerejection就好了。

    小结

    异常类型同步方法异步方法资源加载Promiseasync/await
    try/catch✔️✔️
    onerror✔️✔️
    error事件监听✔️✔️✔️
    unhandledrejection事件监听✔️✔️

    实际上我们可以将unhandledrejection事件抛出的异常再次抛出就可以统一通过error事件进行处理了。

    最终用代码表示如下:

    window.addEventListener("unhandledrejection", e => {
      throw e.reason
    });
    window.addEventListener('error', args => {
      console.log('error event:', args
      );
      return true;
    }, true);
    复制代码

    Webpack工程化

    现在是前端工程化的时代,工程化导出的代码一般都是被压缩混淆后的。

    比如:

    setTimeout(() => {
        xxx(1223)
    }, 1000)
    复制代码

    出错的代码指向被压缩后的JS文件。

    如果想将错误和原有的代码关联起来就需要sourcemap文件的帮忙了。

    sourceMap是什么

    简单说, sourceMap就是一个文件,里面储存着位置信息。

    仔细点说,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。

    那么如何利用sourceMap对还原异常代码发生的位置这个问题我们到异常分析这个章节再讲。

    Vue

    创建工程

    利用vue-cli工具直接创建一个项目。

    # 安装vue-cli
    npm install -g @vue/cli
    
    # 创建一个项目
    vue create vue-sample
    
    cd vue-sample
    npm i
    // 启动应用
    npm run serve
    
    复制代码

    为了测试的需要我们暂时关闭eslint 这里面还是建议大家全程打开eslint

    在vue.config.js进行配置

    module.exports = {   
      // 关闭eslint规则
      devServer: {
        overlay: {
          warnings: true,
          errors: true
        }
      },
      lintOnSave:false
    }
    复制代码

    我们故意在src/components/HelloWorld.vue

    <script>
    export default {
      name: "HelloWorld",
      props: {
        msg: String
      },
      mounted() {
        // 制造一个错误
        abc()
      }
    };
    script>
    ​```html
    
    然后在src/main.js中添加错误事件监听
    
    ​```js
    window.addEventListener('error', args => {
      console.log('error', error)
    })
    复制代码

    这个时候 错误会在控制台中被打印出来,但是错误事件并没有监听到。

    handleError

    为了对Vue发生的异常进行统一的上报,需要利用vue提供的handleError句柄。一旦Vue发生异常都会调用这个方法。

    我们在src/main.js

    Vue.config.errorHandler = function (err, vm, info) {
      console.log('errorHandle:', err)
    }
    复制代码

    React

    npx create-react-app react-sample
    cd react-sample
    yarn start
    复制代码

    我们利用useEffect hooks 制造一个错误

    import React ,{useEffect} from 'react';
    import logo from './logo.svg';
    import './App.css';
    
    function App() {
      useEffect(() => {
        // 发生异常
        error()
      });
    
      return (
        <div className="App">
          // ...略...
        div>
      );
    }
    
    export default App;
    复制代码

    并且在src/index.js中增加错误事件监听逻辑

    window.addEventListener('error', args => {
        console.log('error', error)
    })
    复制代码

    但是从运行结果看虽然输出了错误日志但是还是服务捕获。

    ErrorBoundary标签

    错误边界仅可以捕获其子组件的错误。错误边界无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。这也类似于 JavaScript 中 catch {} 的工作机制。

    创建ErrorBoundary组件

    import React from 'react'; 
    export default class ErrorBoundary extends React.Component {
        constructor(props) {
          super(props);
        }
        componentDidCatch(error, info) {
          // 发生异常时打印错误
          console.log('componentDidCatch',error)
        }
        render() {
          return this.props.children;
        }
      }
    复制代码

    在src/index.js中包裹App标签

    import ErrorBoundary from './ErrorBoundary'
    
    ReactDOM.render(
        <ErrorBoundary><App />
        ErrorBoundary>
        , document.getElementById('root'));
    复制代码

    上一篇我们主要谈到的JS错误如何收集。这篇我们说说异常如何上报和分析。

    异常上报

    选择通讯方式

    动态创建img标签

    其实上报就是要将捕获的异常信息发送到后端。最常用的方式首推动态创建标签方式。因为这种方式无需加载任何通讯库,而且页面是无需刷新的。基本上目前包括百度统计 Google统计都是基于这个原理做的埋点。

    new Image().src = 'http://localhost:7001/monitor/error'+ '?info=xxxxxx'
    复制代码

    通过动态创建一个img,浏览器就会向服务器发送get请求。可以把你需要上报的错误数据放在querystring字符串中,利用这种方式就可以将错误上报到服务器了。

    Ajax上报

    实际上我们也可以用ajax的方式上报错误,这和我们在业务程序中并没有什么区别。在这里就不赘述。

    上报哪些数据

    我们先看一下error事件参数:

    属性名称含义类型
    message错误信息string
    filename异常的资源urlstring
    lineno异常行号int
    colno异常列号int
    error错误对象object
    error.message错误信息string
    error.stack错误信息string

    其中核心的应该是错误栈,其实我们定位错误最主要的就是错误栈。

    错误堆栈中包含了绝大多数调试有关的信息。其中包括了异常位置(行号,列号),异常信息

    上报数据序列化

    由于通讯的时候只能以字符串方式传输,我们需要将对象进行序列化处理。

    大概分成以下三步:

    • 将异常数据从属性中解构出来存入一个JSON对象

    • 将JSON对象转换为字符串

    • 将字符串转换为Base64

    当然在后端也要做对应的反向操作 这个我们后面再说。

    window.addEventListener('error', args => {
      console.log('error event:', args
      );
      uploadError(args)
      return true;
    }, true);
    function uploadError({
        lineno,
        colno,
        error: {
          stack
        },
        timeStamp,
        message,
        filename
      }) {
        // 过滤
        const info = {
          lineno,
          colno,
          stack,
          timeStamp,
          message,
          filename
        }
        // const str = new Buffer(JSON.stringify(info)).toString("base64");
      const str = window.btoa(JSON.stringify(info))
        const host = 'http://localhost:7001/monitor/error'
        new Image().src = `${host}?info=${str}`
    }
    复制代码

    异常收集

    异常上报的数据一定是要有一个后端服务接收才可以。

    我们就以比较流行的开源框架eggjs为例来演示

    搭建eggjs工程

    # 全局安装egg-cli
    npm i egg-init -g 
    # 创建后端项目
    egg-init backend --type=simple
    cd backend
    npm i
    # 启动项目
    npm run dev
    复制代码

    编写error上传接口

    首先在app/router.js添加一个新的路由

    module.exports = app => {
      const { router, controller } = app;
      router.get('/', controller.home.index);
      // 创建一个新的路由
      router.get('/monitor/error', controller.monitor.index);
    };
    复制代码

    创建一个新的controller (app/controller/monitor)

    'use strict';
    
    const Controller = require('egg').Controller;
    const { getOriginSource } = require('../utils/sourcemap')
    const fs = require('fs')
    const path = require('path')
    
    class MonitorController extends Controller {
      async index() {
        const { ctx } = this;
        const { info } = ctx.query
        const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
        console.log('fronterror:', json)
        ctx.body = '';
      }
    }
    
    module.exports = MonitorController;
    
    复制代码

    记入日志文件

    下一步就是讲错误记入日志。实现的方法可以自己用fs写,也可以借助log4js这样成熟的日志库。

    当然在eggjs中是支持我们定制日志那么我么你就用这个功能定制一个前端错误日志好了。

    在/config/config.default.js中增加一个定制日志配置

    // 定义前端错误日志
    config.customLogger = {
      frontendLogger : {
        file: path.join(appInfo.root, 'logs/frontend.log')
      }
    }
    复制代码

    在/app/controller/monitor.js中添加日志记录

    async index() {
        const { ctx } = this;
        const { info } = ctx.query
        const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
        console.log('fronterror:', json)
        // 记入错误日志
        this.ctx.getLogger('frontendLogger').error(json)
        ctx.body = '';
      }
    复制代码

    异常分析

    谈到异常分析最重要的工作其实是将webpack混淆压缩的代码还原。

    Webpack插件实现SourceMap上传

    在webpack的打包时会产生sourcemap文件,这个文件需要上传到异常监控服务器。这个功能我们试用webpack插件完成。

    创建webpack插件

    /source-map/plugin

    const fs = require('fs')
    var http = require('http');
    
    class UploadSourceMapWebpackPlugin {
      constructor(options) {
        this.options = options
      }
    
      apply(compiler) {
        // 打包结束后执行
        compiler.hooks.done.tap("upload-sourcemap-plugin", status => {
          console.log('webpack runing')
        });
      }
    }
    
    module.exports = UploadSourceMapWebpackPlugin;
    复制代码

    加载webpack插件

    webpack.config.js

    // 自动上传Map
    UploadSourceMapWebpackPlugin = require('./plugin/uploadSourceMapWebPackPlugin')
    
    plugins: [
        // 添加自动上传插件
        new UploadSourceMapWebpackPlugin({
          uploadUrl:'http://localhost:7001/monitor/sourcemap',
          apiKey: 'xxx'
        })
      ],
    
    复制代码

    添加读取sourcemap读取逻辑

    在apply函数中增加读取sourcemap文件的逻辑

    /plugin/uploadSourceMapWebPlugin.js

    const glob = require('glob')
    const path = require('path')
    apply(compiler) {
      console.log('UploadSourceMapWebPackPlugin apply')
      // 定义在打包后执行
      compiler.hooks.done.tap('upload-sourecemap-plugin', async status => {
        // 读取sourcemap文件
        const list = glob.sync(path.join(status.compilation.outputOptions.path, `./**/*.{js.map,}`))
        for (let filename of list) {
          await this.upload(this.options.uploadUrl, filename)
        }
      })
    }
    复制代码

    实现http上传功能

    upload(url, file) {
      return new Promise(resolve => {
        console.log('uploadMap:', file)
        const req = http.request(
          `${url}?name=${path.basename(file)}`,
          {
            method: 'POST',
            headers: {'Content-Type': 'application/octet-stream',
              Connection: "keep-alive","Transfer-Encoding": "chunked"
            }
          }
        )
        fs.createReadStream(file)
          .on("data", chunk => {
          req.write(chunk);
        })
          .on("end", () => {
          req.end();
          resolve()
        });
      })
    }
    复制代码

    服务器端添加上传接口

    /backend/app/router.js

    module.exports = app => {
      const { router, controller } = app;
      router.get('/', controller.home.index);
      router.get('/monitor/error', controller.monitor.index);
      // 添加上传路由
     router.post('/monitor/sourcemap',controller.monitor.upload)
    };
    复制代码

    添加sourcemap上传接口

    /backend/app/controller/monitor.js

    async upload() {
        const { ctx } = this
        const stream = ctx.req
        const filename = ctx.query.name
        const dir = path.join(this.config.baseDir, 'uploads')
        // 判断upload目录是否存在
        if (!fs.existsSync(dir)) {
          fs.mkdirSync(dir)
        }
    
        const target = path.join(dir, filename)
        const writeStream = fs.createWriteStream(target)
        stream.pipe(writeStream)
    }
    复制代码

    执行webpack打包时调用插件sourcemap被上传至服务器。

    解析ErrorStack

    考虑到这个功能需要较多逻辑,我们准备把他开发成一个独立的函数并且用Jest来做单元测试

    先看一下我们的需求

    输入stack(错误栈)ReferenceError: xxx is not defined\n' + ' at http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js:1:1392'
    SourceMap
    输出源码错误栈{ source: 'webpack:///src/index.js', line: 24, column: 4, name: 'xxx' }

    搭建Jest框架

    首先创建一个/utils/stackparser.js文件

    module.exports = class StackPaser {
        constructor(sourceMapDir) {
            this.consumers = {}
            this.sourceMapDir = sourceMapDir
        }
    }
    
    复制代码

    在同级目录下创建测试文件stackparser.spec.js

    以上需求我们用Jest表示就是

    const StackParser = require('../stackparser')
    const { resolve } = require('path')
    const error = {
        stack: 'ReferenceError: xxx is not defined\n' +'    at http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js:1:1392',
        message: 'Uncaught ReferenceError: xxx is not defined',
        filename: 'http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js'
    }
    
    it('stackparser on-the-fly', async () => {
    
        const stackParser = new StackParser(__dirname)
    
        // 断言 
        expect(originStack[0]).toMatchObject(
            {
                source: 'webpack:///src/index.js',
                line: 24,
                column: 4,
                name: 'xxx'
            }
        )
    })
    
    复制代码

    下面我们运行Jest

    npx jest stackparser --watch
    复制代码

    显示运行失败,原因很简单因为我们还没有实现对吧。下面我们就实现一下这个方法。

    反序列Error对象

    首先创建一个新的Error对象 将错误栈设置到Error中,然后利用error-stack-parser这个npm库来转化为stackFrame

    const ErrorStackParser = require('error-stack-parser')
    /**
     * 错误堆栈反序列化
     * @param {*} stack 错误堆栈
     */
    parseStackTrack(stack, message) {
      const error = new Error(message)
      error.stack = stack
      const stackFrame = ErrorStackParser.parse(error)
      return stackFrame
    }
    复制代码

    解析ErrorStack

    下一步我们将错误栈中的代码位置转换为源码位置

    const { SourceMapConsumer } = require("source-map");
    async getOriginalErrorStack(stackFrame) {
            const origin = []
            for (let v of stackFrame) {
                origin.push(await this.getOriginPosition(v))
            }
    
            // 销毁所有consumers
            Object.keys(this.consumers).forEach(key => {
                console.log('key:',key)
                this.consumers[key].destroy()
            })
            return origin
        }
    
        async getOriginPosition(stackFrame) {
            let { columnNumber, lineNumber, fileName } = stackFrame
            fileName = path.basename(fileName)
            console.log('filebasename',fileName)
            // 判断是否存在
            let consumer = this.consumers[fileName]
    
            if (consumer === undefined) {
                // 读取sourcemap
                const sourceMapPath = path.resolve(this.sourceMapDir, fileName + '.map')
                // 判断目录是否存在
                if(!fs.existsSync(sourceMapPath)){
                    return stackFrame
                }
                const content = fs.readFileSync(sourceMapPath, 'utf8')
                consumer = await new SourceMapConsumer(content, null);
                this.consumers[fileName] = consumer
            }
            const parseData = consumer.originalPositionFor({ line:lineNumber, column:columnNumber })
            return parseData
        }
    复制代码

    我们用Jest测试一下

    it('stackparser on-the-fly', async () => {
    
        const stackParser = new StackParser(__dirname)
        console.log('Stack:',error.stack)
        const stackFrame = stackParser.parseStackTrack(error.stack, error.message)
        stackFrame.map(v => {
            console.log('stackFrame', v)
        })
        const originStack = await stackParser.getOriginalErrorStack(stackFrame)
    
        // 断言 
        expect(originStack[0]).toMatchObject(
            {
                source: 'webpack:///src/index.js',
                line: 24,
                column: 4,
                name: 'xxx'
            }
        )
    })
    复制代码

    将源码位置记入日志

    async index() {
        console.log
        const { ctx } = this;
        const { info } = ctx.query
        const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
        console.log('fronterror:', json)
        // 转换为源码位置
        const stackParser = new StackParser(path.join(this.config.baseDir, 'uploads'))
        const stackFrame = stackParser.parseStackTrack(json.stack, json.message)
        const originStack = await stackParser.getOriginalErrorStack(stackFrame)
        this.ctx.getLogger('frontendLogger').error(json,originStack)
        ctx.body = '';
      }
    复制代码

    开源框架

    Fundebug

    Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、荔枝FM、掌门1对1、核桃编程、微脉等众多品牌企业。欢迎免费试用!

    Sentry

    Sentry 是一个开源的实时错误追踪系统,可以帮助开发者实时监控并修复异常问题。它主要专注于持续集成、提高效率并且提升用户体验。Sentry 分为服务端和客户端 SDK,前者可以直接使用它家提供的在线服务,也可以本地自行搭建;后者提供了对多种主流语言和框架的支持,包括 React、Angular、Node、Django、RoR、PHP、Laravel、Android、.NET、JAVA 等。同时它可提供了和其他流行服务集成的方案,例如 GitHub、GitLab、bitbuck、heroku、slack、Trello 等。目前公司的项目也都在逐步应用上 Sentry 进行错误日志管理。

    总结

    截止到目前为止,我们把前端异常监控的基本功能算是形成了一个MVP(最小化可行产品)。后面需要升级的还有很多,对错误日志的分析和可视化方面可以使用ELK。发布和部署可以采用Docker。对eggjs的上传和上报最好要增加权限控制功能。

    参考代码位置: github.com/su37josephx…

    MySQL 数据库事务隔离性的实现

    $
    0
    0

    ​​​​摘要: 事实上在数据库引擎的实现中并不能实现完全的事务隔离,比如串行化。

    本文分享自华为云社区 《【数据库事务与锁机制】- 事务隔离的实现》,原文作者:技术火炬手 。

    事实上在数据库引擎的实现中并不能实现完全的事务隔离,比如串行化。这种事务隔离方式虽然是比较理想的隔离措施,但是会对并发性能产生比较大的影响,所以在 MySQL 中事务的默认隔离级别是 REPEATABLE READS(可重复读),下面我们展开讨论一下 MySQL 对数据库隔离性的实现。

    MySQL 事务隔离性的实现

    在 MySQL InnoDB (下称 MySQL)中实现事务的隔离性是通过锁实现的,大家知道在并发场景下我常用的隔离和一致性措施往往是通过锁实现,所以锁也是数据库系统常用的一致性措施。

    MySQL 锁的分类

    我们主要讨论 InnoDB 锁的实现,但是也有必要简单了解 MySQL 中其他数据库引擎对锁的实现。整体来说 MySQL 中可以分为三种锁的类型 表锁、行锁、页锁,其中使用表锁的是 MyISAM 引擎,支持行锁的是 InnoDB 引擎,同时 InnoDB 也支持表锁,BDB 支持页锁(不是太了解)。

    表锁 table-level locking

    表级别的锁顾名思义就是加锁的维度是表级别的,是给一个表上锁,这种锁的特点是开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,但是并发度也是最低的,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用。

    MySQL 表锁的使用

    在 MySQL 中使用表锁比较简单,可以通过 LOCK TABLE 语句对一张表进行加锁,如下:

    # 加锁
    LOCK TABLE T_XXXXXXXXX;
    # 解锁
    UNLOCK TABLES;
    复制代码

    加锁和解锁的语法

    LOCK TABLES
        tbl_name [[AS] alias] lock_type
        [, tbl_name [[AS] alias] lock_type] ...
    lock_type: {
        READ [LOCAL]
      | [LOW_PRIORITY] WRITE
    }
    UNLOCK TABLES
    复制代码

    需要注意的是 LOCK TABLE 是指当前会话的锁,也就是通过 LOCK TABLE 显示的为当前会话获取表锁,作用是防止其他会话在需要互斥访问时修改表的数据,会话只能为其自身获取或释放锁。一个会话无法获取另一会话的锁,也不能释放另一会话持有的锁。同时 LOCK TABLE 不单单可以获取一个表的锁,也可以是一个视图,对于视图锁定,LOCKTABLES 将视图中使用的所有基本表添加到要锁定的表集合中,并自动锁定它们。

    LOCK TABLES 在获取新锁之前,隐式释放当前会话持有的所有表锁

    UNLOCK TABLES 显式释放当前会话持有的所有表锁

    LOCKTABLE 语句有两个比较重要的参数 lock_type 它可以容许你指定加锁的模式,是读锁还是写锁,也就是 READLOCK 和 WRITE LOCK。

    • READ 锁

    读锁的特点是 持有锁的会话可以读取表但不能写入表,多个会话可以同时获取 READ 该表的锁

    • WRITE 锁

    持有锁的会话可以读取和写入表,只有持有锁的会话才能访问该表。在释放锁之前,没有其他会话可以访问它,保持锁定状态时,其他会话对表的锁定请求将阻塞

    WRITE 锁通常比 READ 锁具有更高的优先级,以确保尽快处理更新。这意味着,如果一个会话获取了一个 READ 锁,然后另一个会话请求了一个 WRITE 锁,则随后的 READ 锁请求将一直等待,直到请求该 WRITE 锁的会话已获取并释放了该锁

    通过上面对表锁的简单介绍我们引出两个比较重要的信息,就是读锁和写锁,那么答案就浮出水面,在表级别的锁中其实 MySQL 是通过 共享读锁,和排他写锁来实现隔离性的,下面我们减少共享读锁和排他写锁。

    共享读锁(Table ReadLock)

    共享锁又称为读锁,简称 S 锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改

    对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;也即当一个 session 给表加读锁,其他 session 也可以继续读取该表,但所有更新、删除和插入将会阻塞,直到将表解锁。MyISAM 引擎在执行 select 时会自动给相关表加读锁,在执行 update、delete 和 insert 时会自动给相关表加写锁

    独占写锁(Table WriteLock)

    排他锁又称为写锁,简称 X 锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改

    独占写锁也被称之为排他写锁,MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM 表的读操作与写操作之间,以及写操作之间是串行的。也即当一个 session 给表加写锁,其他 session 所有读取、更新、删除和插入将会阻塞,直到将表解锁

    共享锁和独占锁的兼容性

    ​行锁 Row -level locking

    在 MySQL 中 支持行锁的引擎是 InnoDB,所以我们这里我们指的行锁主要是说 InnoDB 的行锁。

    InnoDB 锁的实现和 Oracle 非常类似,提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。

    lock 与 latch

    Latch 一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在 InnoDB 中,latch 又可以分为 mutex(互斥量)和 rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。

    Lock 的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般 lock 的对象仅在事务 commit 或 rollback 后进行释放(不同事务隔离级别释放的时间可能不同)。

    lock 与 latch 的比较

    latch 可以通过命令 SHOWENGINE INNODB MUTEX 查看,Lock 可以通过命令 SHOW ENGINE INNODB STATUS 及 information_schema 架构下的表 INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS 来查看

    和上面表锁中讲的一样 MySQL 行锁也是通过 共享锁和独占锁(排他锁)实现的,所以关于这两种锁的概述就不过多简绍。

    InnoDB 还支持多粒度(granular)锁定,允许事务同时存在行级锁和表级锁,这种种额外的锁方式,称为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁

    如果对最下层(最细粒度)的对象上锁,那么首先需要对粗粒度的对象上锁,意向锁为表级锁,不会阻塞除全表扫描以外的任何请求。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。两种意向锁。

    • 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
    • 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁

    表级意向锁与行级锁的兼容性

    下面命令或表都可以查看当前锁的请求

    SHOW FULL PROCESSLIST;
    SHOW ENGINE INNODB STATUS;
    SELECT * FROM information_schema.INNODB_TRX;
    SELECT * FROM information_schema.INNODB_LOCKS;
    SELECT * FROM information_schema.INNODB_LOCK_WAITS;
    复制代码

    一致性非锁定读

    一致性的非锁定读(consistent nonlocking read)是指 InnoDB 通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行 DELETE 或 UPDATE 操作,这时不会去等待行上锁的释放。而是去读取行的一个快照数据(之前版本的数据)。

    一个行记录多个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。

    之所以称为非锁定读,因为不需要等待访问的行上 X 锁的释放。实现方式是通过 undo 段来完成。而 undo 用来在事务中回滚数据,快照数据本身没有额外的开销,也不需要上锁,因为没有事务会对历史数据进行修改操作。非锁定读机制极大地提高了数据库的并发性。在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也不相同。在事务隔离级别 READ COMMITTED 和 REPEATABLE READ 下,InnoDB 使用非锁定的一致性读。但对快照数据的定义不相同。在 READCOMMITTED 事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在 REPEATABLEREAD 事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

    自增长与锁

    自增长在数据库中是非常常见的一种属性,也是首选的主键方式。在 InnoDB 的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-incrementcounter)。

    插入操作会依据这个自增长的计数器值加 1 赋予自增长列。这个实现方式称做 AUTO-INC Locking,采用了一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的 SQL 语句后立即释放。

    因此 InnoDB 提供了一种轻量级互斥量的自增长实现机制,大大提高了自增长值插入的性能。同时提供了一个参数 innodb_autoinc_lock_mode 来控制自增长的模式,该参数的默认值为 1。了解其实现之前,先对自增长的插入进行分类,如下表:

    参数 innodb_autoinc_lock_mode 的说明

    InnoDB 中自增长的实现和 MyISAM 不同,MyISAM 存储引擎是表锁设计,自增长不用考虑并发插入的问题。如果主从分别使用 InnoDB 和 MyISAM 时,必须考虑这种情况。

    另外,在 InnoDB 存中,自增长值的列必须是索引,同时必须是索引的第一个列。如果不是第一个列会抛出异常,而 MyISAM 没有这个问题。

    外键和锁

    外键主要用于引用完整性的约束检查。InnoDB 对于一个外键列,如果没有显式地对这个列加索引,会自动对其加一个索引,可以避免表锁。而 Oracle 不会自动添加索引,需要手动添加,可能会产生死锁问题。

    对于外键值的插入或更新,首先需要查询(select)父表中的记录。但是 select 父表操作不是使用一致性非锁定读,因为这会导致数据不一致的问题,因此这时使用的是 SELECT…LOCK IN SHARE MODE 方式,即主动对父表加一个 S 锁。如果这时父表上已经加了 X 锁,子表上的操作会被阻塞。如下表:

    ​行锁的 3 种算法

    InnoDB 有如下 3 种行锁的算法

    • Record Lock:单个行记录上的锁。总去锁住索引记录,如果表没有设置任何索引,会使用隐式的主键来进行锁定
    • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
    • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。行的查询采用这种锁定算法

    例如一个索引有 10,11,13 和 20 这四个值,那么该索引可能被 Next-KeyLocking 的区间为

    采用 Next-Key Lock 的锁定技术称为 Next-Key Locking。其设计的目的是为了解决幻读问题(Phantom Problem)。Next-Key Lock 是谓词锁(predict lock)的一种改进。还有 previous-key locking 技术。同样上述的索引 10、11、13 和 20,若采用 previous-key locking 技术,那么锁定的区间为

    当查询的索引含有唯一属性时,会对 Next-Key Lock 进行优化。对聚集索引,将其降级为 Record Lock。对辅助索引,将对下一个键值加上 gap lock,即对下一个键值的范围为加锁

    Gap Lock 的作用是为了阻止多个事务将记录插入到同一范围内,而这会产生导致幻读问题,用户可以通过以下两种方式来显式地关闭 Gap Lock

    • 将事务的隔离级别设置为 READ COMMITTED
    • 将参数 innodb_locks_unsafe_for_binlog 设置为 1

    上述设置破坏了事务的隔离性,并且对于 replication,可能会导致主从数据的不一致。此外,从性能上来看,READCOMMITTED 也不会优于默认的事务隔离级别 READ REPEATABLE。

    解决幻读问题

    幻读问题是指在同一事务下,连续执行两次同样的范围查询操作,得到的结果可能不同

    Next-KeyLocking 的算法就是为了避免幻读问题。对于上述的 SQL 语句,其锁住的不是单个值,而是对(2,+∞)这个范围加了 X 锁。因此任何对于这个范围的插入不允许,从而避免了幻读问题。Next-Key Locking 机制在应用层还可以实现唯一性的检查。例如:

    select * from table_name where col = xxx LOCK IN SHARE MODE;
    复制代码

    如果用户通过索引查询一个值,并对该行加上一个 SLock,那么即使查询的值不在,其锁定的也是一个范围,因此若没有返回任何行,那么新插入的值一定是唯一的。如果此时有多个事务并发操作,那么这种唯一性检查机制也不会存在问题。因为这时会导致死锁,只有一个事务的插入操作会成功,而其余的事务会抛出死锁的错误。

    通过 Next-Key Locking 实现应用程序的唯一性检查:

    总结

    以上我们简单简绍了 MySQL 如何通过锁机制实现对事务的隔离,也简绍了一些实现这些所的算法,如果对细节比较感兴趣的同学可以参考 官方文档中对 InnoDB 的详细简绍。

    点击关注,第一时间了解华为云新鲜技术~

    Kubernetes 切换到 Containerd

    $
    0
    0

    一、环境准备

    • Ubuntu 20.04 x5
    • Etcd 3.4.16
    • Kubernetes 1.21.1
    • Containerd 1.3.3

    1.1、处理 IPVS

    由于 Kubernetes 新版本 Service 实现切换到 IPVS,所以需要确保内核加载了 IPVS modules;以下命令将设置系统启动自动加载 IPVS 相关模块,执行完成后需要重启。

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # Kernel modules       
    cat > /etc/modules-load.d/50-kubernetes.conf <<EOF
    # Load some kernel modules needed by kubernetes at boot
    nf_conntrack
    br_netfilter
    ip_vs
    ip_vs_lc
    ip_vs_wlc
    ip_vs_rr
    ip_vs_wrr
    ip_vs_lblc
    ip_vs_lblcr
    ip_vs_dh
    ip_vs_sh
    ip_vs_fo
    ip_vs_nq
    ip_vs_sed
    EOF

    # sysctl
    cat > /etc/sysctl.d/50-kubernetes.conf <<EOF
    net.ipv4.ip_forward=1
    net.bridge.bridge-nf-call-iptables=1
    net.bridge.bridge-nf-call-ip6tables=1
    fs.inotify.max_user_watches=525000
    EOF

    重启完成后务必检查相关 module 加载以及内核参数设置:

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    # check ipvs modules       
    ➜ ~ lsmod | grep ip_vs
    ip_vs_sed 16384 0
    ip_vs_nq 16384 0
    ip_vs_fo 16384 0
    ip_vs_sh 16384 0
    ip_vs_dh 16384 0
    ip_vs_lblcr 16384 0
    ip_vs_lblc 16384 0
    ip_vs_wrr 16384 0
    ip_vs_rr 16384 0
    ip_vs_wlc 16384 0
    ip_vs_lc 16384 0
    ip_vs 155648 22 ip_vs_wlc,ip_vs_rr,ip_vs_dh,ip_vs_lblcr,ip_vs_sh,ip_vs_fo,ip_vs_nq,ip_vs_lblc,ip_vs_wrr,ip_vs_lc,ip_vs_sed
    nf_conntrack 139264 1 ip_vs
    nf_defrag_ipv6 24576 2 nf_conntrack,ip_vs
    libcrc32c 16384 5 nf_conntrack,btrfs,xfs,raid456,ip_vs

    # check sysctl
    ➜ ~ sysctl -a | grep ip_forward
    net.ipv4.ip_forward = 1
    net.ipv4.ip_forward_update_priority = 1
    net.ipv4.ip_forward_use_pmtu = 0

    ➜ ~ sysctl -a | grep bridge-nf-call
    net.bridge.bridge-nf-call-arptables = 1
    net.bridge.bridge-nf-call-ip6tables = 1
    net.bridge.bridge-nf-call-iptables = 1

    1.2、安装 Containerd

    Containerd 在 Ubuntu 20 中已经在默认官方仓库中包含,所以只需要 apt 安装即可:

    1      
    2
    # 其他软件包后面可能会用到,所以顺手装了       
    apt install containerd bridge-utils nfs-common tree -y

    安装成功后可以通过执行 ctr images ls命令验证, 本章节不会对 Containerd 配置做说明,Containerd 配置文件将在 Kubernetes 安装时进行配置。

    二、安装 kubernetes

    2.1、安装 Etcd 集群

    Etcd 对于 Kubernetes 来说是核心中的核心,所以个人还是比较喜欢在宿主机安装;宿主机安装情况下为了方便我打包了一些 *-pack的工具包,用于快速处理:

    安装 CFSSL 和 ETCD

    1      
    2
    3
    4
    5
    6
    7
    8
    # 下载安装包       
    wget https://github.com/mritd/etcd-pack/releases/download/v3.4.16/etcd_v3.4.16.run
    wget https://github.com/mritd/cfssl-pack/releases/download/v1.5.0/cfssl_v1.5.0.run

    # 安装 cfssl 和 etcd
    chmod +x *.run
    ./etcd_v3.4.16.run install
    ./cfssl_v1.5.0.run install

    安装完成后, 自行调整 /etc/cfssl/etcd/etcd-csr.json相关 IP,然后执行同目录下 create.sh生成证书。

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    ➜ ~ cat /etc/cfssl/etcd/etcd-csr.json       
    {
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "names": [
    {
    "O": "etcd",
    "OU": "etcd Security",
    "L": "Beijing",
    "ST": "Beijing",
    "C": "CN"
    }
    ],
    "CN": "etcd",
    "hosts": [
    "127.0.0.1",
    "localhost",
    "*.etcd.node",
    "*.kubernetes.node",
    "10.0.0.11",
    "10.0.0.12",
    "10.0.0.13"
    ]
    }

    # 复制到 3 台 master
    ➜ ~ for ip in `seq 1 3`; do scp /etc/cfssl/etcd/*.pem root@10.0.0.1$ip:/etc/etcd/ssl; done

    证书生成完成后调整每台机器的 Etcd 配置文件,然后修复权限启动。

    1      
    2
    3
    4
    5
    6
    7
    8
    # 复制配置       
    for ip in `seq 1 3`; do scp /etc/etcd/etcd.cluster.yaml root@10.0.0.1$ip:/etc/etcd/etcd.yaml; done

    # 修复权限
    for ip in `seq 1 3`; do ssh root@10.0.0.1$ip chown -R etcd:etcd /etc/etcd; done

    # 每台机器启动
    systemctl start etcd

    启动完成后通过 etcdctl验证集群状态:

    1      
    2
    3
    4
    5
    # 稳妥点应该执行 etcdctl endpoint health       
    ➜ ~ etcdctl member list
    55fcbe0adaa45350, started, etcd3, https://10.0.0.13:2380, https://10.0.0.13:2379, false
    cebdf10928a06f3c, started, etcd1, https://10.0.0.11:2380, https://10.0.0.11:2379, false
    f7a9c20602b8532e, started, etcd2, https://10.0.0.12:2380, https://10.0.0.12:2379, false

    2.2、安装 kubeadm

    kubeadm 国内用户建议使用 aliyun 的安装源:

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # kubeadm       
    apt-get install -y apt-transport-https
    curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -
    cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
    deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
    EOF
    apt update

    # ebtables、ethtool kubelet 可能会用,具体忘了,反正从官方文档上看到的
    apt install kubelet kubeadm kubectl ebtables ethtool -y

    2.3、安装 kube-apiserver-proxy

    kube-apiserver-proxy 是我自己编译的一个仅开启四层代理的 Nginx,其主要负责监听 127.0.0.1:6443并负载到所有的 Api Server 地址( 0.0.0.0:5443):

    1      
    2
    3
    wget https://github.com/mritd/kube-apiserver-proxy-pack/releases/download/v1.20.0/kube-apiserver-proxy_v1.20.0.run       
    chmod +x *.run
    ./kube-apiserver-proxy_v1.20.0.run install

    安装完成后根据 IP 地址不同自行调整 Nginx 配置文件,然后启动:

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    ➜ ~ cat /etc/kubernetes/apiserver-proxy.conf       
    error_log syslog:server=unix:/dev/log notice;

    worker_processes auto;
    events {
    multi_accept on;
    use epoll;
    worker_connections 1024;
    }

    stream {
    upstream kube_apiserver {
    least_conn;
    server 10.0.0.11:5443;
    server 10.0.0.12:5443;
    server 10.0.0.13:5443;
    }

    server {
    listen 0.0.0.0:6443;
    proxy_pass kube_apiserver;
    proxy_timeout 10m;
    proxy_connect_timeout 1s;
    }
    }

    systemctl start kube-apiserver-proxy

    2.4、安装 kubeadm-config

    kubeadm-config 是一系列配置文件的组合以及 kubeadm 安装所需的必要镜像文件的打包,安装完成后将会自动配置 Containerd、ctrictl 等:

    1      
    2
    3
    4
    5
    wget https://github.com/mritd/kubeadm-config-pack/releases/download/v1.21.1/kubeadm-config_v1.21.1.run       
    chmod +x *.run

    # --load 选项用于将 kubeadm 所需镜像 load 到 containerd 中
    ./kubeadm-config_v1.21.1.run install --load

    2.4.1、containerd 配置

    Containerd 配置位于 /etc/containerd/config.toml,其配置如下:

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    version = 2       
    # 指定存储根目录
    root = "/data/containerd"
    state = "/run/containerd"
    # OOM 评分
    oom_score = -999

    [grpc]
    address = "/run/containerd/containerd.sock"

    [metrics]
    address = "127.0.0.1:1234"

    [plugins]
    [plugins."io.containerd.grpc.v1.cri"]
    # sandbox 镜像
    sandbox_image = "k8s.gcr.io/pause:3.4.1"
    [plugins."io.containerd.grpc.v1.cri".containerd]
    snapshotter = "overlayfs"
    default_runtime_name = "runc"
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
    runtime_type = "io.containerd.runc.v2"
    # 开启 systemd cgroup
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
    SystemdCgroup = true

    2.4.2、crictl 配置

    在切换到 Containerd 以后意味着以前的 docker命令将不再可用,containerd 默认自带了一个 ctr命令,同时 CRI 规范会自带一个 crictl命令; crictl命令配置文件存放在 /etc/crictl.yaml中:

    1      
    2
    3
    runtime-endpoint: unix:///run/containerd/containerd.sock       
    image-endpoint: unix:///run/containerd/containerd.sock
    pull-image-on-create: true

    2.4.3、kubeadm 配置

    kubeadm 配置目前分为 2 个,一个是用于首次引导启动的 init 配置,另一个是用于其他节点 join 到 master 的配置;其中比较重要的 init 配置如下:

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    # /etc/kubernetes/kubeadm.yaml       
    apiVersion: kubeadm.k8s.io/v1beta2
    kind: InitConfiguration
    # kubeadm token create
    bootstrapTokens:
    - token: "c2t0rj.cofbfnwwrb387890"
    nodeRegistration:
    # CRI 地址(Containerd)
    criSocket: unix:///run/containerd/containerd.sock
    kubeletExtraArgs:
    runtime-cgroups: "/system.slice/containerd.service"
    rotate-server-certificates: "true"
    localAPIEndpoint:
    advertiseAddress: "10.0.0.11"
    bindPort: 5443
    # kubeadm certs certificate-key
    certificateKey: 31f1e534733a1607e5ba67b2834edd3a7debba41babb1fac1bee47072a98d88b
    ---
    apiVersion: kubeadm.k8s.io/v1beta2
    kind: ClusterConfiguration
    clusterName: "kuberentes"
    kubernetesVersion: "v1.21.1"
    certificatesDir: "/etc/kubernetes/pki"
    # Other components of the current control plane only connect to the apiserver on the current host.
    # This is the expected behavior, see: https://github.com/kubernetes/kubeadm/issues/2271
    controlPlaneEndpoint: "127.0.0.1:6443"
    etcd:
    external:
    endpoints:
    - "https://10.0.0.11:2379"
    - "https://10.0.0.12:2379"
    - "https://10.0.0.13:2379"
    caFile: "/etc/etcd/ssl/etcd-ca.pem"
    certFile: "/etc/etcd/ssl/etcd.pem"
    keyFile: "/etc/etcd/ssl/etcd-key.pem"
    networking:
    serviceSubnet: "10.66.0.0/16"
    podSubnet: "10.88.0.1/16"
    dnsDomain: "cluster.local"
    apiServer:
    extraArgs:
    v: "4"
    alsologtostderr: "true"
    # audit-log-maxage: "21"
    # audit-log-maxbackup: "10"
    # audit-log-maxsize: "100"
    # audit-log-path: "/var/log/kube-audit/audit.log"
    # audit-policy-file: "/etc/kubernetes/audit-policy.yaml"
    authorization-mode: "Node,RBAC"
    event-ttl: "720h"
    runtime-config: "api/all=true"
    service-node-port-range: "30000-50000"
    service-cluster-ip-range: "10.66.0.0/16"
    # insecure-bind-address: "0.0.0.0"
    # insecure-port: "8080"
    # The fraction of requests that will be closed gracefully(GOAWAY) to prevent
    # HTTP/2 clients from getting stuck on a single apiserver.
    goaway-chance: "0.001"
    # extraVolumes:
    # - name: "audit-config"
    # hostPath: "/etc/kubernetes/audit-policy.yaml"
    # mountPath: "/etc/kubernetes/audit-policy.yaml"
    # readOnly: true
    # pathType: "File"
    # - name: "audit-log"
    # hostPath: "/var/log/kube-audit"
    # mountPath: "/var/log/kube-audit"
    # pathType: "DirectoryOrCreate"
    certSANs:
    - "*.kubernetes.node"
    - "10.0.0.11"
    - "10.0.0.12"
    - "10.0.0.13"
    timeoutForControlPlane: 1m
    controllerManager:
    extraArgs:
    v: "4"
    node-cidr-mask-size: "19"
    deployment-controller-sync-period: "10s"
    experimental-cluster-signing-duration: "8670h"
    node-monitor-grace-period: "20s"
    pod-eviction-timeout: "2m"
    terminated-pod-gc-threshold: "30"
    scheduler:
    extraArgs:
    v: "4"
    ---
    apiVersion: kubelet.config.k8s.io/v1beta1
    kind: KubeletConfiguration
    failSwapOn: false
    oomScoreAdj: -900
    cgroupDriver: "systemd"
    kubeletCgroups: "/system.slice/kubelet.service"
    nodeStatusUpdateFrequency: 5s
    rotateCertificates: true
    evictionSoft:
    "imagefs.available": "15%"
    "memory.available": "512Mi"
    "nodefs.available": "15%"
    "nodefs.inodesFree": "10%"
    evictionSoftGracePeriod:
    "imagefs.available": "3m"
    "memory.available": "1m"
    "nodefs.available": "3m"
    "nodefs.inodesFree": "1m"
    evictionHard:
    "imagefs.available": "10%"
    "memory.available": "256Mi"
    "nodefs.available": "10%"
    "nodefs.inodesFree": "5%"
    evictionMaxPodGracePeriod: 30
    imageGCLowThresholdPercent: 70
    imageGCHighThresholdPercent: 80
    kubeReserved:
    "cpu": "500m"
    "memory": "512Mi"
    "ephemeral-storage": "1Gi"
    ---
    apiVersion: kubeproxy.config.k8s.io/v1alpha1
    kind: KubeProxyConfiguration
    # kube-proxy specific options here
    clusterCIDR: "10.88.0.1/16"
    mode: "ipvs"
    oomScoreAdj: -900
    ipvs:
    minSyncPeriod: 5s
    syncPeriod: 5s
    scheduler: "wrr"

    init 配置具体含义请自行参考官方文档,相对于 init 配置,join 配置比较简单, 不过需要注意的是如果需要 join 为 master 则需要 controlPlane这部分,否则请注释掉 controlPlane

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # /etc/kubernetes/kubeadm-join.yaml       
    apiVersion: kubeadm.k8s.io/v1beta2
    kind: JoinConfiguration
    controlPlane:
    localAPIEndpoint:
    advertiseAddress: "10.0.0.12"
    bindPort: 5443
    certificateKey: 31f1e534733a1607e5ba67b2834edd3a7debba41babb1fac1bee47072a98d88b
    discovery:
    bootstrapToken:
    apiServerEndpoint: "127.0.0.1:6443"
    token: "c2t0rj.cofbfnwwrb387890"
    # Please replace with the "--discovery-token-ca-cert-hash" value printed
    # after the kubeadm init command is executed successfully
    caCertHashes:
    - "sha256:97590810ae34a82501717e33acfca76f16044f1a365c5ad9a1c66433c386c75c"
    nodeRegistration:
    criSocket: unix:///run/containerd/containerd.sock
    kubeletExtraArgs:
    runtime-cgroups: "/system.slice/containerd.service"
    rotate-server-certificates: "true"

    2.5、拉起 master

    在调整好配置后,拉起 master 节点只需要一条命令:

    1      
    kubeadm init --config /etc/kubernetes/kubeadm.yaml --upload-certs --ignore-preflight-errors=Swap       

    拉起完成后记得保存相关 Token 以便于后续使用。

    2.6、拉起其他 master

    在第一个 master 启动完成后,使用 join命令让其他 master 加入即可; 需要注意的是 kubeadm-join.yaml配置中需要替换 caCertHashes为第一个 master 拉起后的 discovery-token-ca-cert-hash的值。

    1      
    kubeadm join 127.0.0.1:6443 --config /etc/kubernetes/kubeadm-join.yaml --ignore-preflight-errors=Swap       

    2.7、拉起其他 node

    node 节点拉起与拉起其他 master 节点一样,唯一不同的是需要注释掉配置中的 controlPlane部分。

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # /etc/kubernetes/kubeadm-join.yaml       
    apiVersion: kubeadm.k8s.io/v1beta2
    kind: JoinConfiguration
    #controlPlane:
    # localAPIEndpoint:
    # advertiseAddress: "10.0.0.12"
    # bindPort: 5443
    # certificateKey: 31f1e534733a1607e5ba67b2834edd3a7debba41babb1fac1bee47072a98d88b
    discovery:
    bootstrapToken:
    apiServerEndpoint: "127.0.0.1:6443"
    token: "c2t0rj.cofbfnwwrb387890"
    # Please replace with the "--discovery-token-ca-cert-hash" value printed
    # after the kubeadm init command is executed successfully
    caCertHashes:
    - "sha256:97590810ae34a82501717e33acfca76f16044f1a365c5ad9a1c66433c386c75c"
    nodeRegistration:
    criSocket: unix:///run/containerd/containerd.sock
    kubeletExtraArgs:
    runtime-cgroups: "/system.slice/containerd.service"
    rotate-server-certificates: "true"
    1      
    kubeadm join 127.0.0.1:6443 --config /etc/kubernetes/kubeadm-join.yaml --ignore-preflight-errors=Swap       

    2.8、其他处理

    由于 kubelet 开启了证书轮转,所以新集群会有大量 csr 请求,批量允许即可:

    1      
    kubectl get csr | grep Pending | awk '{print $1}' | xargs kubectl certificate approve       

    同时为了 master 节点也能负载 pod,需要调整污点:

    1      
    kubectl taint nodes --all node-role.kubernetes.io/master-       

    后续 CNI 等不在本文内容范围内。

    三、Containerd 常用操作

    1      
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 列出镜像       
    ctr images ls

    # 列出 k8s 镜像
    ctr -n k8s.io images ls

    # 导入镜像
    ctr -n k8s.io images import xxxx.tar

    # 导出镜像
    ctr -n k8s.io images export kube-scheduler.tar k8s.gcr.io/kube-scheduler:v1.21.1

    四、资源仓库

    本文中所有 *-pack仓库地址如下:

    河南发布!十大5G建设应用案例 (干货)

    $
    0
    0

    日前,由中国通信学会主办的“2021世界电信和信息社会日大会”在郑州国际会展中心举办。“河南省十大 5G建设应用案例”在大会上正式发布。

    十大案例有——十大案例

    1. 中信重工5G+智慧工厂

    2. 多氟多“氟动中原”5G+智慧工厂

    3. 焦煤集团千业水泥5G+绿色无人矿山

    4. 郑大一附院5G智慧医疗案例

    5. 河南联通参与建设的中国一拖5G+AI工业视觉质检应用

    6. 郑州格力电器5G智慧工厂

    7. 郑州市郑东新区5G自动驾驶案例

    8. 河南电信参与建设的三门峡神通碳素5G+智慧工厂

    9. 许昌裕同5G+MEC数字化车间改造案例

    10. 河南广电参与建设的700MHz 5G专网在智能配电网中的应用案例。

    在对“ 中信重工5G+智慧工厂”的案例介绍中,提到

    作为大型、离散型、重型装备制造企业,如何解决“散”的问题,也是重工业生产需要重点突破的难点。

    2020年,是国家”一五”期间156项重点工程之一中信重工与河南移动联合创立5G+工业互联网联合创 新实验室,致力于解决当前企业痛点:设备多管理难、重型装备操作难度大、后台支撑不直观。尤其是生产设备互通率低、设备样机调测难、及复杂零部件装配困难诸如此类问题的存在。

    河南移动为离散型制造企业打造了5G+数据采集、5G+重装机械臂、5G+AR远程指导、5G+云化AGV、5G+数字孪生、5G+智慧仓储此9大应用场景,将车间12台大型机床数据5G联网分析,使机床有了头脑智能,解决了生产数据收集难的问题,设备利用率提升超20%。

    “5G+远程操控机械臂”实现用真实的物理遥控器来操控电脑里的数字样机,解决了重型设备操作安全的问题。

    “5G+智慧工厂”——

    “5G+智慧工厂”由河南联通与郑州格力电器共同打造。本项目采用混合专网专线实现方式,边缘计算下沉到格力厂区内部,实现数据不出厂区,降低了网络时延,也提升了数据传输可靠性和处理效率,保护私密的工业应用数 据。

    通过部署的5G应用场景,使物料运输效率提升30%,巡检效率提升20%,平均节省人力25%。实现可视化管理和精准决策。

    通过“5G+数据采集”让设备上的传感器模块借助 5G 工业网关上传生产信息,延伸管理触角,提高企业监 控、决策和指挥速度。

    通过“5G+ AGV小车”实现了物料配送节拍和配送路径的按需调度和实时优化,大幅度减少了人力资源消耗,生产布局变化时对应调整非常简单,达到了机器换人工的目标,可在生产时间进行,且不影响生产。

    通过“5G+机器视觉检测”,用工业相机现场抓取物料,传输到边缘计算平台进行AI分析,让算法上云、缺陷库共享,能实现速率高,ms级时延,而让结果指令回传至生产现场,实现了从人工抽检到AI智能全检的转变,检测准确率接近100%。同时具有调测扩展时间短、低空间占比,低能耗。

    而“5G+智能巡检”则扩大巡检覆盖面,减少巡 检的盲点 ,节约巡检的成本保障人员的安全。

    此外,“中国一拖5G+AI工业视觉质检应用”是通过端侧传感收集信息数据,边缘协同智能分析,云、边、端协同,实现机器视觉自动检测,提升企业生产效率。

    “多氟多5G+智慧工厂”解决的是监管手段不够健全、数据利用率不高、技能传承费时费力、产业链协同困难的问题。

    “三门峡神通碳素5G+智慧工厂”

    通过搭建5G+云平台,智慧工厂、基础平台,实现 智能人车管理、智能环保、智能消防、智能巡检、只能能耗管理五大场景的运行。

    其中,“5G+智慧环保”实现对PM2.5、PM10、二氧化硫及粉尘颗粒物的在线监测采集,累计减排各类污染物223吨,解决了各个工序扬尘污染问题,2020年通过环保B级企业认定,仅此一项每年可为企业节约成本300万元。通过5G改造,企业由原来每年亏损600万元,实现扭亏为盈,每年盈利1000万元,员工工资平均增长25%。


    会上,河南省通信管理局党组书记、局长陆建文重点就上述四个案例进行了详细解读。陆建文表示,5G是助力经济社会转型升级的战略性、基础性、先导性新型基础设施,5G融合应用是促进经济社会数字化、网络化、智能化转型的重要引擎。

    陆建文说,河南省全省信息通信业认真落实工业和信息化部、省委、省政府决策部署,聚焦构建“需求牵引供给、供给创造需求”的5G发展模式,加快推进“三个转变”:

    一、由通信运营商向5G应用运营商转变;

    二、由提供5G网络向提供5G产品转变;

    三、由普通网络建设向围绕客户需求开展建设转变。

    围绕推进产业数字化,省通信管理局组织省各基础电信运营企业推出了一系列5G应用产品,形成了一批可复制推广的5G应用项目。

    截止目前,省各电信企业深入推进的5G应用达127个,项目总投资17.8亿元,涉及工业互联网、智慧能源、智慧城市、智慧交通、智慧医疗、智慧农业、智慧电力、智慧文旅等8个领域。

    陆建文强调,发布的十大案例仅仅是全省5G建设应用实践的一个缩影,为加快推动全省5G规模化应用,赋能产业数字化转型。

    据悉,为加快推进5G应用推广,河南省通信管理局专门开办了“河南5G讲堂”,通过视频互动方式,让生产企业负责人远程介绍5G应用解决企业难点、痛点问题,视频互动、远程观摩、现场体验5G产品。

    ●对河南省各通信企业在智能制造、智慧矿山等8大领域100多个5G场景应用的远程接入进行集中呈现。

    ●为各级政府及有关部门领导、专家,特别是实体企业负责人提供了“足不出户”的5G应用观摩平台。

    “河南5G讲堂”、“企业5G实训基地”等深入挖掘了5G特色应用,对积极探索5G融合应用新产品、新业态、新模式,推动5G与实体经济深度融合,组织实体企业学习5G试点示范标杆,支持引导5G应用,对先行先试企业给予积极支持。

    “河南信息通信业借助主流媒体加大大众化5G应用宣传,加快构建5G产业新生态,积极赋能全省经济社会高质量发展。”陆建文如此总结。


    海康威视:形成软件开发方法论,社会经济到了由商业需求拉动的时代

    $
    0
    0

    近日,海康威视举行投资者问答会议。

    在AI市场机会上,海康认为AI的机遇已经得到行业普遍认同,未来AI成本会持续降低,AI技术在改善产品性能上作用很大,AI算法和大数据等,会带来许多之前想做但做不到的业务机会,也会开拓新玩法,未来会打开更多市场。

    在软件投入上,组件化的开发已经是海康确定的软件开发方法论。近几年海康的软件开发环境、管理环境、运维环境的使用都已经成熟,过程更规范、更有效率。未来PaaS、DaaS、SaaS层面都有更多的事情可做,海康各个层次的软件研发管理维护的资源团队都会再进一步加大投入。

    海康从可见光走向全光谱,发力多维感知,是作为一家做物联网、大数据的公司必然选择,既然是必然选择就先不考虑产出比。海康预期三年未来市场端变化将非常复杂,对海康靠技术创新的公司将有更多机会。

    未来10年,创新业务保持更快增长的确定性较高。

    未来几年,三个BG都将有不错发展,受不同因素影响会有阶段快慢。

    PBG会受限于地方政府的财政和疫情,但政府项目的数据规模大、应用复杂程度高,会一直是应用高地,也会有新的业务形式;

    EBG从长期看比PBG市场更大,社会的经济增长到了更多由商业需求拉动的时代;

    SMBG会围绕服务渠道伙伴、工程伙伴,连通线上线下,提高运转效率,改善生态,扩大海康的业务影响力。

    尽管未来有诸多不确定性,但海康的销售和研发的费用投入上,必须做两件事:

    一事平台的搭建与持续维护,与行业应用不断做新的融合;二是产品,基于创新业务和基础技术开发上的产品。

    做企业既要有风险管控能力,也需对保持对机会的追求。

    以下是调研全文重要内容,雷锋网作了不改变原意的整理与编辑:

    Q:目前AI的应用逐渐落地,公司觉得国内外市场机遇怎么判断?

    A:经过过去几年的发展,行业中的玩家已经更加认同,AI是个基础技术,在很多方面都会发挥作用。未来AI的成本还会持续的降低,AI技术在帮助产品性能的优化和改善上帮助是很大的,由此会打开越来越多的市场。

    AI的算法和大数据相关的应用,会带来许多以往我们想做但是做不到的业务机会,也开拓更多新技术平台上的新玩法。

    国内的应用走得快一些,但很难说用我们在国内锻炼出来的产品和方案直接复制海外市场。海外市场对AI应用的观点不同,AI应用的发展节奏也不同,不同国家和地区之间的发展路径也是不一样的。还是要根据每个国家每个地区的不同需求,来制定不同的产品策略与方案。

    Q:算法、数据对业务越来越重要,也看到海康为了适应对算法和数据的开发而在软件开发的方式上不断转型,用组件为基础开发软件已经看到很多成效,未来的软件投入主要是什么层次和方向上的?

    A:过去几年海康谈的比较多的是统一软件架构,组件化的开发已经是公司确定的软件开发方法论,也是用来整合各个团队开发资源的基础。

    这几年的软件开发环境、管理环境、运维环境的使用也都已经成熟,这些环境能保障软件开发和管理维护的过程更规范,更有效率。

    在这些基础能力上,我们也在针对数据和应用做很多工作,做PaaS和DaaS,把PaaS和DaaS4这两层搭起来用起来,现在已经有一些应用和模型基本上成熟了。

    SaaS部分海康和伙伴们分工协作,直接面对用户场景做定制开发。随着我们统一软件架构这个整体方法论的成熟,以及方法论之下各种技术工具不断被打磨,我们有了将应用和数据做深做厚的能力,也有了协调更多的软件研发资源,做更大规模开发的能力,未来PaaS、DaaS、SaaS层面都有更多的事情可做,我们的产品会更加丰富,公司各个层次的软件研发管理维护的资源团队会再进一步加大投入。

    Q:年报中判断未来三年是机遇期,也提到了海康正在从可见光走向全光谱,请问从可见光走向非可见光的部分,公司要怎么投入,能带来多少回报?

    A:非可见光在场景中最重要的作用在于,在某些场景下,某种感知方式可以发挥不可替代的作用,比可见光会更加有效,即使只在整体方案中占不多的比例,但作为补充会非常有价值。

    对于投入和回报,在初始阶段我们对产出不是特别关心,早期也不知道会做成怎么样。只是觉得对海康来说作为一家做物联网、大数据的公司,多维感知是我们必然的一个选择,既然是必然选择就先不考虑产出,效果交给时间。

    未来三年我们预期市场端的变化将是非常复杂的,包括逆全球化的趋势、上游供给侧的变革,会带来很多产业的结构发生深刻的变化。未来几年,尤其是像我们这样靠技术创新发展的公司来说,应该有更好的机遇。

    Q:如果未来三年是海康的一个大发展阶段的话,三个BG和创新业务的收入占比会是一个什么样的分布?

    A:创新业务整体来说占比还小,但是我们的创新业务很聚焦,可以和海康在技术能力、客户资源、产业链配合上有很强的协同效应,所以创新业务在下一个10年中保持更快的增长都是确定性比较高的。

    未来几年,三个BG都会有不错的发展,但是受到某些因素的影响,会出现阶段性的有快有慢,会不一样。

    PBG在过去2年多的时间里受限于地方政府的财政状况,今天疫情之下政府也在继续为企业降低负担,也在削减自身的财政收入,但是政府项目的数据规模大,应用复杂程度高,会一直是应用的高地,也会不断有新业务形式发展出来,带动公司的进化。

    从长期发展来看,EBG应该比PBG要大一些,社会也到了经济增长更多的由商业需求拉动的时代。SMBG方面我们会围绕服务渠道伙伴、工程伙伴做更多的工作,连通线上线下,让我们生态里的行业从业者更专业、更规范的开展工作,也用互联网后台的建设和管理帮他们提升运转效率,通过他们扩大海康的业务影响力。

    Q:海康的费用率从季度环比看是往下走的,在销售费用和研发费用的投入上接下来是什么节奏?

    A:未来还有很多不确定性,我们肯定会做很多事情。

    一是平台搭建,还需要不断的维护,更多的行业应用会起来,会有一些新的融合;

    二是产品,包括在创新业务、在基础技术开发上面,未来可能都会有大的变化。

    在当前国内大循环和国际双循环的大环境下,中国和美国在高科技上的投入态势会和过去20年有很大的差别,中国的科技产业的地缘分布结构可能会有很大的调整。

    我们相信在包括半导体、高科技、材料、基础算法等很多方面,中国企业在未来10年、15年会有很大的发展空间,这是我们看到的机遇,也要抓住这个机遇,保持相当的投入力度。费用率未来还是会继续往上走,我们做企业既要对风险有能力管控,也要对机会保持追求。雷锋网雷锋网雷锋网

    浅谈大型组织中前端管理架构

    $
    0
    0

    前端,现代前端分工变得越来越细致,页面制作、JavaScript框架设计、组件插件、交互设计、工程化脚手架等,项目中前端的占比也越来越高,继而出现了BFF (Back-end for Front-end 服务于前端的后端),这一切的助力离不开各大浏览器厂商的厮杀。

    周末来跟大家分享大型组织中(前端工程师的人数开始超过15人)前端管理架构,主要涉及的是团队协作,如何让团队运作更加高效规范。本文不讨论大公司中常见的管理问题或业务领域问题,而只关注前端的协作架构。

    如今,前端架构涉及的领域太多,一下是供参考的架构,后面将基于此架构进行展开介绍:

    image.png

    1、Visual Code

    从最简单的主题开始,这是前端开发最常用的代码编辑器,当然不排斥使用其他的,但还是建议最好统一代码编辑器。

    在同一家公司开发多个前端应用程序,个人觉得还应该具备一定的设计及品牌意识,希望团队成员开发出来的应用具备以下两点:

    • 品牌认知度
    • 相同的 UI/UX

    为此,需要制定一个设计规范,这里的设计规范主要是从VI的角度出发。此规范由设计团队提出,并在所有将来的产品设计中遵循这些设计准则。即使这是一个非常复杂的任务,需要设计团队、研发团队和产品之间进行大量讨论和协调。

    从前端的角度来看,可以将设计规范制作成脚手架,脚手架将设计规范的原则生成基础主题(样式、专用的Web资源、文档等),这样在项目实施过程中就可以共享此设计规范。

    2、代码结构

    接下来谈谈日常编码,确实实现了新功能、修复了bug,如果需要的话重构代码。需要关注代码库,试图让代码变得友好和容易理解。但是,当团队开始有不是1个、也不是2个,而是几十个大小项目时,会发生什么呢?

    常见的方式是以项目分组,并开始只与这组项目一起工作。由于人的本性和有限的时间,通常不能在一段时间内兼顾多于2-5个项目。尽管如此,项目开始之后会遇到越来越多的情况,跨团队协作需要检查彼此的代码和实现方案,甚至在其他应用程序中也要修复一些错误,或者在某个外部应用程序中添加新的紧急需求)。这种情况的避免就需要项目编码规范,统一代码结构、编码规范等,这些规范最好的方式是变成工具脚手架。

    • 项目中的文件夹结构

      开发人员第一次进入新项目时,与他开发过的项目中文件夹结构相同,对于理解代码、熟悉项目,快速进入研发进程有很大的帮助。

    • 配置或依赖文件的

      文件,如 package.json.gitignore.editorconfigwebpack.config等每一个项目应该总是在同一个地方。如果需要,将它们连接到测试配置文件或CI文件。

    • 文件类型的固定位置

      如果相同文件类型的位置始终遵循相同的结构,则有助于理解。例如,如果组件文件夹中始终有一个 style.scss文件:

    /Component
    --/Component.tsx
    --/style.scss
    --/index.ts
    复制代码
    • 组件内部结构:文件内部的结构应相同:导入、导出的顺序、公共功能的位置、类型等。在每种类型的文件中,都应该知道期望的内容。

    • 命名规范:这包括文件夹、文件、变量、函数、类、类型等的名称

    • 编码约定:总的来说,编码约定是一个非常宽泛的部分,最好团队成员能够达成一个一致的规范。

    在实践中,相同的代码结构和项目工具集非常紧密地结合在一起,有利于开发效率。这里所说的工具集是指 CLI工具(项目启动、检测、测试等)、IDE扩展等等。

    3、技术栈

    与上一节类似,团队在组织的各个项目中拥有统一的技术栈,有助于开发效率及质量的提升。

    在前端项目中,技术堆栈的组件可以是:构建该项目所基于的框架、主要语言、样式预处理器、数据层、状态管理、测试、代码整理、构建系统等。

    当然,所有规则中都有例外。有时某些技术非常适合某些特定项目,即使这些技术不属于团队熟悉的技术栈。但是,每当有脱离现有团队技术栈的想法时,都应该三思而后行,因为更换技术栈的成本非常高,需要衡量成本及带来的价值。

    这里提及一些通用技术堆栈,就目前可以适合大多数项目:

    在为公司定义技术栈并达成共识之后,还有其他非常重要的内容。

    首先,需要写下来的技术栈的文档。这些文档应该在工程师之间方便且容易地共享,因此他们始终可以相互链接并维护。

    其次,应该再次使用已定义的技术栈来写下并共享文档,以及如何启动和引导新项目的方式。

    4、工具

    现在,几乎在所有地方都使用了一些其他工具:规范、构建应用程序、CI、组件生成器等等。因此,这就是为什么能确定是否可以为项目选择正确的工具的原因至关重要。好的工具还是不好的工具(或者根本没有工具),就像自动化测试与手动测试之间的比较一样。

    在前面谈到了技术栈和代码结构,并提到需要编写大量文档来使项目成员关注维护它们。但是正确的工具集可以有机会按照团队规范进行自动化。

    例如,编码风格,则可以为项目提供 linting工具集,该工具集默认情况下遵循这些规则。如果具有定义的技术栈,那么良好的CLI工具将提供机会,使用技术栈中的特定技术来引导新项目。

    来看看工具可以覆盖前端体系结构的哪些部分:

    • 代码风格和结构:如之前所讨论的,可以通过工具轻松实现自动化

    • 项目自举:无需提出新的项目结构,手动安装所有需要的软件包等。

    • 组件生成:大多数情况下,应用程序中的某些组件甚至都不包含单个文件,因此文件创建、链接或者导入它们会花费一些时间,因此需要自动化。

    • 启动和构建:当然,最显而易见的要自动化的事情是如何构建或启动应用程序。

    • 测试:为测试构建应用程序并实际运行所有类型的测试(单元、集成等)的过程。

    • 依赖关系管理:现在大约80%的代码之间是有依赖关系。因此,需要让他们保持最新版本,并且要在大型公司中进行管理并非易事。

    • 跨项目的依赖关系:很可能项目不是孤立地工作,可能依赖于其他项目,,因此可能需要一些工具来简化链接它们的过程,并结合多个项目(例如 Bit等)等等。

    • CI:CI是日常工具集的重要组成部分,自动化和统一对团队协作是一项非常有益的工作。

    如果不想开发自己的新工具集,可以尝试 NX工具集。同样,Babel也提供了类似的解决方案。借助工具提高效率,是一个很好的起点。

    每个项目都是相同的,并由统一工具集维护和管理。每个项目都可以以相同的方式启动和构建。新的组件在相同的位置使用相同的命名准则生成。

    5、生产部署

    通常,在前端体系结构的这一部分中,前端小伙伴最不用担心。也许是因为它在大多数情况下与编码本身无关,可能并不那么令人兴奋,但同样重要。

    在生产中,通常需要注意以下事项:

    • Google Analytics(分析):各种不同的跟踪事件,例如Google Analytics(分析),Segment,HotJar等。

    • 状态监视:这包括诸如运行状况检查之类的内容,甚至可以在生产中运行测试,错误报告(例如 Sentry)等。

    *** 性能**:这与上一项相似,项目需要注重性能。包括测量响应时间、加载时间等。(可以使用 Lighthouse

    • A/B测试:各种A/B测试解决方案或功能标记。

    • 缓存:诸如 VarnishCloudflare之类的工具。

    所有这些都可以在公司的前端应用程序中统一,这将简化开发人员的工作。

    6、开发迭代

    CLI工具

    当接触前端CLI工具时,已有部分内容在“工具”部分讨论了开发经验。统一工具是开发人员日常工作的重要组成部分。

    API

    好的API设计是改善开发人员体验和开发速度的第二件事,关于API设计可以参阅《 9个REST API设计的基本准则》。通常,为前端工程师在本地提供API并不是一件容易的事:这可能包括他们不熟悉的安装工具或框架。配置各种服务器环境等需要花费大量的时间。在这种情况下,Docker是个不错的选择,作为前端开发人员也有必要掌握简单的使用。有兴趣的话可以参阅《 面向WEB开发人员的Docker

    CI

    CI是第三大部分。大部分公司已经有现成的一些CI工具作为前端工具 (例如Circle CI,Concourse CI或任何其他工具)。如果不是,则应统一。

    特定项目的CI配置应该是该项目团队的一部分。这给CI带来了稳定的机会,因为有些人对CI感兴趣,每天都要使用它,并且具有修复,配置和改进它的能力和技能。

    但是,并非所有工作都应由团队完成。对于前端应用程序,存在相当特定的一堆工作,如脚手架。

    演示环境

    最后是验证实现的功能。在开发人员完成所有工作并实施之后,几乎总是需要某种方式来检查其外观和功能,并将其与其他开发人员、设计师或测试人员共享演示环境。对于此类需求,它可以通过提供的URL在特定PR的应用程序的临时部署版本。

    演示环境加快了不同团队与人员之间的沟通,这是必须具备的。但是,临时部署的版本应尽可能接近生产环境,因为它也是检查某些表面错误或BUG的好工具。

    如果前端应用程序构建和部署流程是统一的,则可以轻松地将其添加到项目中并自动进行。同样,诸如 KubernetesHelm之类的工具或类似工具也可以在开发中提供很大帮助。

    7、模块化

    这个话题非常大,可能需要一篇单独的文章来讨论,这里简单介绍一下。

    在大型组织中,庞大的代码库并不罕见。与所有已知的问题一起出现,如缓慢的CI管道、协作工作问题、缓慢的测试等。因此,前端架构的一个重要部分是决定我们希望看到独立前端应用/模块的粒度。

    现在有三种主要的模式:

    • Monolith:一个大的存储库包含一个项目和所有的代码,所有的团队同时在这个存储库中工作。

    • Monorepo:很多项目,但仍然有一个很大的存储库(在wiki中是monorepo)。所有的团队仍然使用相同的存储库,但是使用的是不同的项目。我们已经有机会修复一些问题了,我们采用的是单一的方法,只针对特定的项目运行管道,项目有更小的测试套件等等。如果你选择了这种方法,像Lerna这样的工具可以让你的生活更简单。

    • Repo per project:每个项目都有自己的存储库和所有支持的东西,比如CI管道、部署等。

    在所有这些模型中,项目可能意味着独立的前端应用程序、页面、独立的前端模块等等。这取决于您希望如何划分前端应用程序的粒度。在大多数情况下,这种划分应该与所需的组织结构和人员管理同步。

    决定如何分割应用程序后的第二大主题是如何将这些部分连接在一起(如果你决定分割应用程序)。

    这里我们有以下方法:

    • Build-time composition:项目可以只是npm软件包,可以在构建期间安装和组成。
    • Server-side composition:通常包括服务器端渲染和服务器上发生的合成。像Hypernova这样的工具可以帮助更好地组织它。
    • Client-side composition:浏览器内部项目的组成。非常重要的是要提到 Module Federation,这是 Webpack 5中引入的一种新方法。
    • Route composition:超级简单——每个项目都有自己的URL,在 Nginx层级上决定把用户重定向到哪里。

    8、测试

    关于前端应用程序的测试,有很多可用的资源,这里不深入细节,而是更多地关注大型组织的问题以及如何解决它们。

    第一步——每个工程师对测试技术的理解是不同的,以及在什么情况下应用哪种技术,如何编写“好的”测试用例等等。所以非常有必要记录下公司所使用的测试标准的所有细微差别和指导方针,以及每个标准的指导方针。

    测试方案中可能需要制定的测试级别:

    • 单元测试
    • 整体测试
    • 端到端测试
    • 其他的

    此外,第二步,需要在公司的不同前端应用程序中统一它们,这样在参与其他项目时不会对如何以及如何进行测试有任何疑问。

    如果设法统一了测试级别和方法,就可以自动帮助解决第二个问题——测试基础设施设置。每个项目都需要在本地和CI上设置和配置一些测试基础设施。例如,使用 Cypress,它需要在docker镜像中运行。这需要一些时间在本地和CI上进行设置。如果把这个数字乘以我们所拥有的项目数量,那将是非常巨大的时间。因此,解决方案——再次统一并为项目提供一些工具。听起来很简单,但却需要大量的时间去实现。

    非开发时间测试

    再谈一谈在已实施和部署的应用之后需要做的测试,这类测试是为了更好的改善应用。

    在前面的部分中,已经提到了前端应用程序的错误和性能监视,正常运行时间监视以及来自不同位置的响应。

    在网站上运行Lighthouse测试是个不错的方法(可以包含在CI管道中)。通常可以发现性能瓶颈、可访问性问题并提高性能。

    最后,对最重要的业务流程进行生产测试,就需要模拟一个和生产环境接近的测试环境,这样有助于发现运行时的问题并快速进行改善。可以使用Docker,制作一个接近生产环境的镜像。

    数据库内核的并发控制

    $
    0
    0

    大部分程序员最先接触并发编程, 一般是从编程语言里的多线程和锁开始. 但是, 并发控制是一种广义的技术思想, 千万不可将眼光局限于编程语言所提供的锁. 将编程语言里的并发控制技术推广, 就能得到任何层面的并发控制技术.

    以操作一个文件为例, 如果不做并发控制, 就会遇到数据完整性问题. 例如, 我们写入的一项数据, 对应着现实对象, 如果不做并发控制, 那么可能读到的时两项数据的混合体, 或者只读到一项数据的部分.

    互斥锁

    互斥锁是最简单的并发控制技术, 无论读还是写, 通通进行排队, 一次只处理一个请求(读或者写). 这种技术实现起来简单, 不容易出 bug, 如果为了快速实现, 可以采用此技术. 但是, 因为涉及到排队串行化, 所以在很多实际场景中, 这种技术会非常低效.

    以操作一个文件为例, 给文件加一把互斥锁(任何层面的锁, 未必和编程语言或者操作系统所提供的锁对应). 当想要往文件中写入数据时, 先获取锁, 然后写入. 同样, 想要从文件中读取数据时, 先获取锁, 然后读.

    读写锁

    读写锁把请求进行分类, 然后按读和写两种类型进行排队, 不再按单个请求进行排队. 多个读请求可以同时进行, 但是, 写请求不能和读请求同时进行, 因为它们是不同的类型. 特别的, 写类型内部继续按单个请求为维度进行排队.

    一般读写锁不允许读操作插队到写操作的前面, 以避免写饥饿.

    回到文件操作的例子, 因为读操作可以并发进行, 这样, 整个系统的总体吞吐量就上去了. 不过, 类型之间依然有排队, 写和读之间有互斥, 效率还不是最高的.

    MVCC - 多版本并发控制

    可以从多个角度去理解 MVCC, 这些角度虽然不同, 但都是正确的.

    我们从锁粒度(分区, Sharding)的角度去理解 MVCC. 我们把一个文件人为地划分为多个分段, 每一个分段对应一把锁(互斥锁或者读写锁), 当针对其中一个分段加锁时, 并不影响其它分段的操作, 因此可以并发操作. 这种方法其实和使用多个文件是类似的, 原来我们全部的数据只写入同一个文件, 现在我们使用多个文件多把锁.

    如果数据有新旧顺序, 例如文件是 Append Only 的, 那么我们记录一个进度位置(check point), 在进度之前可以并发的读, 因为进度之前的数据已经确保不会发生变化. 进度条之后, 采用串行化写.

    正如我在 之前的一篇文章所提到的, MVCC 技术的核心是标记. 在快速的内存中保存着唯一的单点标记, 即使串行化访问内存标记也不会慢. 因为内存标记的存在, 同时慢速的硬盘支持并发读, 即使读到 Obsoleted 数据也无妨, 根据内存标记可以决定是否现场废弃.

    基于多版本的并发控制技术, 有可能产生无效数据, 所以需要有 垃圾回收机制(GC).

    实际例子

    Redolog 是很多数据库系统的必备基础组件, 即使某个数据库系统没有一个叫 Redolog 的模块, 那么它也有某个模块做的是 Redolog 一模一样的事情, 所以, 不要在意名字, 看实质.

    Redolog 模块的接口是这样的:

    type RedologManager interface {
        // 读取指定序号的日志
        func Get(seq Sequence) => Entry
        // 追加写一条日志
        func Append(ent Entry)
        // 将日志序列回滚到指定序号
        func Rollback(seq Sequence)
    }
    

    具体实现是这样的:

    type FileManger struct {
        var r_mux RWMutex
        var checkpoint Sequence
        var reader FileReader
        var w_mux Mutex
        var writer FileWriter
    }
    
    func (fm *FileManager)Get(seq Sequence) {
        // 可以并发读文件, 所以加读锁
        rm.r_mux.RLock()
        // 只返回该版本之前的数据
        if seq <= rm.checkpoint){
            rm.reader.Read(seq)
        }
        rm.r_mux.RUnlock()
    }
    
    func (fm *FileManager)set_checkpoint(seq Sequence) {
        rm.r_mux.Lock()
        rm.checkpoint = seq
        rm.r_mux.Unlock()
    }
    
    func (fm *FileManager)Append(ent Entry) {
        // 写操作不能并发, 所以加互斥锁
        rm.w_mux.Lock()
        {
            // 先追加并持久化文件
            rm.writer.Write(ent)
            rm.writer.Fsync()
            rm.reader.SetFileSize(rm.writer.FileSize())
    
            // 然后再增加进度
            rm.set_checkpoint(ent.Seq)
        }
        rm.w_mux.Unlock()    
    }
    
    func (fm *FileManager)Rollback(seq Sequence) {
        // 写操作不能并发, 所以加互斥锁
        rm.w_mux.Lock()
        {
            // 先减少进度
            rm.set_checkpoint(seq)
    
            // 然后再回滚持久化数据(收缩文件)
            rm.writer.Truncate(seq)
            rm.writer.Fsync()
            rm.reader.SetFileSize(rm.writer.FileSize())
        }
        rm.w_mux.Unlock()
    }
    
    // TODO: 如果关闭 auto fsync, 那么写操作不会立即影响持久化
    func (fm *FileManager)AutoFsync(enable bool) {
    }
    

    该模块支持并发读, 所以加的是读锁(r_mux). 而且, 在读的同时可以写, 所以, 读和写加的是不同的锁. 但是, 写操作不可以并发, 所以写操作之间加的是互斥锁(w_mux). 在写操作的过程中, 涉及到更新进度, 所以, 在更新进度的时候, 需要加进度锁的写锁.

    Related posts:

    1. Binlog, Redolog 在分布式数据库系统中的应用
    2. 接口与实现分离
    3. 小心递归次数限制
    4. 消除JavaScript闭包的一般方法
    5. 必须放在循环中的pthread_cond_wait

    论微前端在react项目中的应用 - 简书

    $
    0
    0

    本篇文章的大部分内容将用于解释如何通过 js 在运行时集成多个微应用。



    在这个例子中使用的是 React,你也可以使用其他的框架来实现

    容器

    我们先从开始,它的 package.json 内容如下

    {"name": "@micro-frontends-demo/container","description": "Entry point and container for a micro frontends demo","scripts": {"start": "PORT=3000 react-app-rewired start","build": "react-app-rewired build","test": "react-app-rewired test"
      },"dependencies": {"react": "^16.4.0","react-dom": "^16.4.0","react-router-dom": "^4.2.2","react-scripts": "^2.1.8"
      },"devDependencies": {"enzyme": "^3.3.0","enzyme-adapter-react-16": "^1.1.1","jest-enzyme": "^6.0.2","react-app-rewire-micro-frontends": "^0.0.1","react-app-rewired": "^2.1.1"
      },"config-overrides-path": "node_modules/react-app-rewire-micro-frontends"
    }
    复制代码

    从 package.json 的内容中我们可以知道这个一个使用 create-react-app 创建的 React 应用程序,在 package.json 中我们没有看到任何与微应用相关的配置,在上一篇文章中我们已经提到,构建时集成会导致发布周期的耦合。

    为了了解如何将微应用显示在界面上,让我们首先看看 App.js 中的内容,我们使用 React Router 将 URL 与预定义的路由列表相匹配,并呈现相应的组件

    <Switch><Route exact path="/" component={Browse} /><Route exact path="/restaurant/:id" component={Restaurant} /><Route exact path="/random" render={Random} /></Switch>
    复制代码

    Random 组件仅仅用于将页面重定向到一个随机的 restaurant 页面。Browse 和 Restaurant 组件像这个样子

    const Browse = ({ history }) => (<MicroFrontend history={history} name="Browse" host={browseHost} />
    );
    const Restaurant = ({ history }) => (<MicroFrontend history={history} name="Restaurant" host={restaurantHost} />
    );
    复制代码

    在这两种情况下,我们都在页面上渲染 MicroFrontend 组件。除了 history 对象之外,我们还将微应用名以及微应用的主机地址传递给 MicroFrontend 组件 。在本地运行时 host 的值像 http://localhost:3001这种形式,在生产环境中 host 的值像 https://browse.demo.microfrontends.com这样。

    在 App.js 中通过 React Router 能够匹配到要渲染的微应用,在 MicroFrontend.js 中渲染这个微应用,MicroFrontend.js 中的片段如下:

    // class MicroFrontend…
    class MicroFrontend extends React.Component {
      render() {
        return <main id={`${this.props.name}-container`} />;
      }
    }
    复制代码

    在渲染时,我们要做的就是在页面上放置一个容器元素,这个容器元素拥有一个以微应用名命名的 id,微应用将在这个位置渲染自己。我们在 React 的 componentDidMount 钩子中下载和安装微前端,代码如下:

    // class MicroFrontend…
    componentDidMount() {
        const { name, host } = this.props;
        const scriptId = `micro-frontend-script-${name}`;
    
        if (document.getElementById(scriptId)) {
          this.renderMicroFrontend();
          return;
        }
    
        fetch(`${host}/asset-manifest.json`)
          .then(res => res.json())
          .then(manifest => {
            const script = document.createElement('script');
            script.id = scriptId;
            script.src = `${host}${manifest['main.js']}`;
            script.onload = this.renderMicroFrontend;
            document.head.appendChild(script);
          });
      }
    复制代码

    首先我们先检查微应用相关的 script 是否被下载了,如果已经被下载就立即调用方法渲染页面,如果还有没有被下载,就从服务器请求 asset-manifest.json,从 asset-manifest.json中得到即将被渲染的微应用的 script 路径,等 script 下载完之后再渲染页面。this.renderMicroFrontend 的代码如下:

    // class MicroFrontend…
    renderMicroFrontend = () => {
        const { name, history } = this.props;
    
        window[`render${name}`](`${name}-container`, history);
        // E.g.: window.renderBrowse('Browse-container', history);
      };
    复制代码

    在上面的代码中我们调用了命名类似于 window.renderBrowse的全局方法,这个方法是在微应用的脚本中定义的,我们将 <main>的 id 和 history 对象传递给它。这个全局方法是容器应用与微应用之间建立联系的关键。我们应该使它简洁、易于维护,如果我们需要修改它,我们要认真的思考本次变更是否会带来代码库和通信上的耦合。

    在上面我们介绍了组件装载要做的事情,接下来介绍组件卸载要做的事情。当 MicroFrontend 组件被卸载的时候,我们希望与之相关的微应用也能够被卸载。每个微应用都会定义一个与卸载相关的全局方法,这个全局方法会在 MicroFrontend 组件的 componentWillUnmount 钩子函数中被调用,代码如下:

    componentWillUnmount() {
        const { name } = this.props;
    
        window[`unmount${name}`](`${name}-container`);
      }
    复制代码

    站点的头部和导航栏在所有页面中都是恒定的,所以他们直接位于容器应用中。我们要确保它们的 CSS 样式只作用于特定的地址,不会与微应用中的 CSS 发生冲突。

    在这个例子中容器应用的功能非常简陋,它只是给微应用提供了一个壳,在运行时动态下载我们的微应用,并将它们整合到一个页面上。这些微应用可以独立部署到生产环境中,而无需对任何其他微应用或容器本身进行任何更改。

    微应用

    从上一章节我们知道微应用暴露出的全局渲染方法是至关重要的。在微应用中对全局方法的定义如下:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import registerServiceWorker from './registerServiceWorker';
    
    window.renderBrowse = (containerId, history) => {
      ReactDOM.render(<App history={history} />, document.getElementById(containerId));
      registerServiceWorker();
    };
    
    window.unmountBrowse = containerId => {
      ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
    };
    复制代码

    在 React 应用中,通常会在顶层作用域调用 ReactDOM.render,这意味着只要脚本被加载就会立即渲染 DOM 元素。在这个例子中我们需要控制渲染 DOM 元素的位置和时间,所以我们将 ReactDOM.render 包裹在一个函数中,并且将 DOM 元素的 id 传递到函数中。然后将函数绑定在 window 对象上。

    虽然我们已经知道将微应用集成到整个容器应用程序时该函数是如何调用的,但我们还需要能够独立开发和运行微应用,所以每个微应用还应该有它们自己的 index.html,如下所示:

    <html lang="en"><head><title>Restaurant order</title></head><body><main id="container"></main><script type="text/javascript">
          window.onload = () => {
            window.renderRestaurant('container');
          };</script></body></html>
    复制代码

    这个 demo 中的微应用只是普通的 React 应用。 brows应用从服务器获取餐厅列表,提供一个 input 输入框去搜索特定的餐厅并且给每个餐厅提供可以跳转到餐厅详情的链接。 order应用显示餐厅详情



    在我们的微应用中使用 CSS-in-JS 方式来为组件定义样式,这保证了微应用中的样式不会影响到容器应用和其他的微应用

    通过路由进行跨应用程序通信

    在之前的文章中我们已经提到过,我们应该让跨应用通信尽可能简单。在本例中,我们唯一的需求是 browse 应用需要告诉 order 应用要加载哪个餐厅,在这里我们使用浏览器路由来解决这个问题。

    在这里相关的三个应用都使用 React Router 来声明路由,但是声明的方式有所不同。在容器应用程序中我们使用了一个 <BrowserRouter>,它将在内部实例化一个历史对象。这就是我们之前提到的 history 对象。我们使用这个对象来操作客户端历史记录,还可以使用它将多个 React 路由链接在一起。在微应用中,我们像下面这样初始化路由:

    <Router history={this.props.history}>
    复制代码

    在本例中,我们没有在微应用中用 React Router 实例化出一个新的 history 对象,而是将容器应用程序中的 history 对象传递到微应用中。所有的 <Router>实例都被连接在一起,因此在其中任何一个实例中触发的路由更改将反映在所有的 router 实例中。我们可以通过 URL 将参数从一个微应用传递到另一个微应用。例如在 browse应用中,我们有一个这样的链接:

    <Link to={`/restaurant/${restaurant.id}`}>
    复制代码

    当点击这个链接时,容器应用中的路径将会更新,容器将根据新的 URL 确定是否应该安装和呈现餐厅微应用。然后,餐厅微应用的路由逻辑将从 URL 中提取餐厅 ID,并呈现正确的信息。

    公共内容

    虽然我们希望我们的每个微应用尽可能独立,但有些内容应该是共同的。之前写过关于共享组件库如何帮助实现微应用的一致性,但是对于这里例子来说,一个组件库就太过了。因此,我们有一个公共内容的 小仓库,它里面包括图像、JSON数据和CSS,这些内容通过网络请求提供给所有的微应用。
    公共依赖可以在微应用中共享。正如我们将很快描述的那样,重复的依赖是微前端的一个常见缺点。尽管跨应用程序共享这些依赖有其自身的困难,但在这个 demo 中讨论一下如何实现它是值得的。

    第一步是确定要共享哪些依赖。通过对编译后的代码进行快速分析后发现大约 50% 的包是由 react 和 react-dom 提供的。这两个依赖是核心依赖,如果我们将它们从代码中提取出来可以显著的减少代码包的大小。

    为了提取 react 和 react-dom,我们需要修改 webpack 配置,代码如下:

    module.exports = (config, env) => {
      config.externals = {
        react: 'React','react-dom': 'ReactDOM'
      }
      return config;
    };
    复制代码

    然后,我们向每个 index.html 文件添加两个 script 标签,以便从共享内容服务器获取这两个库。

    <body><noscript>
        You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="%REACT_APP_CONTENT_HOST%/react.prod-16.8.6.min.js"></script><script src="%REACT_APP_CONTENT_HOST%/react-dom.prod-16.8.6.min.js"></script></body>
    复制代码

    跨团队共享代码总是一件棘手的事情。我们需要确保我们只共享我们真正想要共享的东西。我们只有谨慎对待我们共享和不共享的东西,我们才能真正的获益。

    缺点

    在上一篇文章中提到了微前端与任何架构一样,也需要权衡我们能够收获的好处和付出的代价,我们将在这里讨论微前端带来的弊端。

    重复的下载

    独立构建的 JavaScript 包会导致常见依赖的重复,增加我们必须通过网络发送给最终用户的字节数。例如,如果每个微应用都包含自己的 React 包,那么我们就迫使客户下载 n 次 React。要解决这个问题并不容易。我们希望让团队独立地编写他们的应用程序,以便他们能够自主地工作,但是我们又希望以一种能够共享共同依赖的方式构建我们的应用程序,这两者之间存在冲突。

    一种方法是将公共依赖提取为外部依赖,就像我们在上面演示的那样。但是只要这样做,我们都必须使用这些依赖的确切版本,这使得我们在微前端中重新引入了一些构建时耦合。

    重复的依赖是一个很棘手的问题,但是也不是全无好处。首先,如果我们对重复的依赖不做任何的处理,每个单独的页面仍然有可能比我们建立一个单体的前端更快地加载。这是因为通过单独的编译每一个页面,我们已经有效的进行的代码分割。在传统的巨石应用中,当应用程序中的任何页面被加载时,我们通常会同时下载其他页面的源代码和依赖项。通过独立构建,在这个 demo 中任何单个页面加载将只下载该页面的源代码和依赖项。这可能会导致初始页面加载速度变快了,但随后的导航跳转速度会变慢,因为用户被迫重新下载每个页面上相同的依赖项。

    在前面的段落中有许多“可能”和“可能”,这突出了这样一个事实,即每个应用程序总是具有自己独特的性能特征。如果您想确切地知道某个特定更改对性能的影响,那么没有什么可以替代实际的度量,最好是在生产环境中进行度量。因此,尽管考虑每个架构决策对性能的影响很重要,但一定要知道真正的瓶颈在哪里。

    环境差异

    我们应该能够独立开发一个单一的微应用,而不需要考虑其他团队正在开发的微应用。我们甚至可以在一个空白页面上以独立模式运行我们的微前端,而不是将其放入生产环境的容器应用程序中运行。但是,在与生产环境完全不同的环境中进行开发是存在风险的。我们可能会遇到在开发阶段运行微应用时微应用能够按照预期运行,但是在生成环境中运行却与预期不同的情况。我们需要特别关注容器或其他微应用可能带来的样式冲突。

    如果我们在本地开发微应用的环境与生产环境不一样,我们就需要确保我们平时集成和部署微前端的环境与生产环境一致。我们还需要在这些环境中做测试使得尽早的发现并解决集成问题,其实这还是不会完全解决问题,我们需要权衡:简化开发环境使得生产力提升,这值得我们冒集成问题的风险吗?答案将取决于项目。

    操作和治理复杂性

    作为一个分布式的体系结构,微前端架构将不可避免地导致有更多的东西需要管理,更多的仓库、更多的工具、更多的部署通道、更多的服务、更多的域名等。在采用微前端架构之前你需要认真思考下面的几个问题:

    1.您是否有足够的自动化流程来切实地提供和管理额外所需的基础设施?
    2.你的前端开发、测试和发布流程能扩展到多个应用程序吗?
    3.如果工具和开发过程中决策会变得更加分散和更不可控,你是否会感到不舒服?
    4.在多个独立的代码库中你怎么确保最高水平解耦?
    5.根据定义当你采用微前端架构就意味着你在创建很多小的组成部分而不是直接创建一个大的结果。你要认真的思考你是否有技术和成熟的组织去确保微前端架构不会给你带来混乱。

    总结

    近些年,前端代码库更得越来越复杂,我们对可扩展架构的需求日益增长。我们需要能够划清界限,在技术实体和领域实体之间建立正确的耦合和内聚级别。我们应该能够在独立、自主的团队之间衡量软件交付。

    可能是你见过最完善的微前端解决方案 - 知乎

    $
    0
    0
    Techniques, strategies and recipes for building a modern web appwith multiple teamsusing different JavaScript frameworks. — Micro Frontends

    前言

    TL;DR

    想跳过技术细节直接看怎么实践的同学可以拖到文章底部,直接看最后一节。

    目前社区有很多关于微前端架构的介绍,但大多停留在概念介绍的阶段。而本文会就某一个具体的类型场景,着重介绍微前端架构可以 带来什么价值以及 具体实践过程中需要关注的技术决策,并辅以具体代码,从而能真正意义上帮助你构建一个 生产可用的微前端架构系统。

    而对于微前端的概念感兴趣或不熟悉的同学,可以通过搜索引擎来获取更多信息,如 知乎上的相关内容, 本文不再做过多介绍。

    两个月前 Twitter 曾爆发过关于微前端的“热烈”讨论,参与大佬众多(Dan、Larkin 等),对“事件”本身我们今天不做过多评论(后面可能会写篇文章来回顾一下),有兴趣的同学可以通过这篇文章了解一二。

    微前端的价值

    微前端架构具备以下几个核心价值:

    • 技术栈无关 主框架不限制接入应用的技术栈,子应用具备完全自主权
    • 独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
    • 独立运行时 每个子应用之间状态隔离,运行时状态不共享

    微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用( Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

    针对中后台应用的解决方案

    中后台应用由于其应用生命周期长(动辄 3+ 年)等特点,最后演变成一个巨石应用的概率往往高于其他类型的 web 应用。而从技术实现角度,微前端架构解决方案大概分为两类场景:

    • 单实例:即同一时刻,只有一个子应用被展示,子应用具备一个完整的应用生命周期。通常基于 url 的变化来做子应用的切换。
    • 多实例:同一时刻可展示多个子应用。通常使用 Web Components 方案来做子应用封装,子应用更像是一个业务组件而不是应用。

    本文将着重介绍 单实例场景下的微前端架构实践方案(基于 single-spa),因为这个场景更贴近大部分中后台应用。

    行业现状

    传统的云控制台应用,几乎都会面临业务快速发展之后,单体应用进化成巨石应用的问题。为了解决产品研发之间各种耦合的问题,大部分企业也都会有自己的解决方案。笔者于17年底,针对国内外几个著名的云产品控制台,做过这样一个技术调研:

    MPA 方案的优点在于 部署简单、各应用之间硬隔离,天生具备技术栈无关、独立开发、独立部署的特性。缺点则也很明显,应用之间切换会造成浏览器重刷,由于产品域名之间相互跳转,流程体验上会存在断点。

    SPA 则天生具备体验上的优势,应用直接无刷新切换,能极大的保证多产品之间流程操作串联时的流程性。缺点则在于各应用技术栈之间是强耦合的。

    那我们有没有可能将 MPA 和 SPA 两者的优势结合起来,构建出一个相对完善的微前端架构方案呢?

    jsconf china 2016 大会上,ucloud 的同学分享了他们的基于 angularjs 的方案(单页应用“联邦制”实践),里面提到的 "联邦制" 概念很贴切,可以认为是早期的基于耦合技术栈的微前端架构实践。

    微前端架构实践中的问题

    可以发现,微前端架构的优势,正是 MPA 与 SPA 架构优势的合集。即保证应用具备独立开发权的同时,又有将它们整合到一起保证产品完整的流程体验的能力。

    这样一套模式下,应用的架构就会变成:



    Stitching layer 作为主框架的核心成员,充当调度者的角色,由它来决定在不同的条件下激活不同的子应用。因此主框架的定位则仅仅是: 导航路由 + 资源加载框架

    而具体要实现这样一套架构,我们需要解决以下几个技术问题:

    路由系统及 Future State

    我们在一个实现了微前端内核的产品中,正常访问一个子应用的页面时,可能会有这样一个链路:

    此时浏览器的地址可能是 https://app.alipay.com/subApp/123/detail,想象一下,此时我们手动刷新一下浏览器,会发生什么情况?

    由于我们的子应用都是 lazy load 的,当浏览器重新刷新时,主框架的资源会被重新加载,同时异步 load 子应用的静态资源,由于此时主应用的路由系统已经激活,但子应用的资源可能还没有完全加载完毕,从而导致路由注册表里发现没有能匹配子应用 /subApp/123/detail的规则,这时候就会导致跳 NotFound 页或者直接路由报错。

    这个问题在所有 lazy load 方式加载子应用的方案中都会碰到,早些年前 angularjs 社区把这个问题统一称之为 Future State

    解决的思路也很简单,我们需要设计这样一套路由机制:

    主框架配置子应用的路由为 subApp: { url: '/subApp/**', entry: './subApp.js' },则当浏览器的地址为 /subApp/abc时,框架需要先加载 entry 资源,待 entry 资源加载完毕,确保子应用的路由系统注册进主框架之后后,再去由子应用的路由系统接管 url change 事件。同时在子应用路由切出时,主框架需要触发相应的 destroy 事件,子应用在监听到该事件时,调用自己的卸载方法卸载应用,如 React 场景下 destroy = () => ReactDOM.unmountAtNode(container)

    要实现这样一套机制,我们可以自己去劫持 url change 事件从而实现自己的路由系统,也可以基于社区已有的 ui router library,尤其是 react-router 在 v4 之后实现了 Dynamic Routing能力,我们只需要复写一部分路由发现的逻辑即可。这里我们推荐直接选择社区比较完善的相关实践 single-spa

    App Entry

    解决了路由问题后,主框架与子应用集成的方式,也会成为一个需要重点关注的技术决策。

    构建时组合 VS 运行时组合

    微前端架构模式下,子应用打包的方式,基本分为两种:

    两者的优缺点也很明显:

    很显然,要实现真正的技术栈无关跟独立部署两个核心目标,大部分场景下我们需要使用运行时加载子应用这种方案。

    JS Entry vs HTML Entry

    在确定了运行时载入的方案后,另一个需要决策的点是,我们需要子应用提供什么形式的资源作为渲染入口?

    JS Entry 的方式通常是子应用将资源打成一个 entry script,比如 single-spa 的 example中的方式。但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。

    HTML Entry 则更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。想象一下这样一个场景:

    <!-- 子应用 index.html --><scriptsrc="//unpkg/antd.min.js"></script><body><mainid="root"></main></body>// 子应用入口
    ReactDOM.render(<App/>, document.getElementById('root'))

    如果是 JS Entry 方案,主框架需要在子应用加载之前构建好相应的容器节点(比如这里的 "#root" 节点),不然子应用加载时会因为找不到 container 报错。但问题在于,主应用并不能保证子应用使用的容器节点为某一特定标记元素。而 HTML Entry 的方案则天然能解决这一问题,保留子应用完整的环境上下文,从而确保子应用有良好的开发体验。

    HTML Entry 方案下,主框架注册子应用的方式则变成:

    framework.registerApp('subApp1',{entry:'//abc.alipay.com/index.html'})

    本质上这里 HTML 充当的是应用静态资源表的角色,在某些场景下,我们也可以将 HTML Entry 的方案优化成 Config Entry,从而减少一次请求,如:

    framework.registerApp('subApp1',{html:'',scripts:['//abc.alipay.com/index.js'],css:['//abc.alipay.com/index.css']})

    总结一下:

    模块导入

    微前端架构下,我们需要获取到子应用暴露出的一些钩子引用,如 bootstrap、mount、unmout 等(参考 single-spa),从而能对接入应用有一个完整的生命周期控制。而由于子应用通常又有集成部署、独立部署两种模式同时支持的需求,使得我们只能选择 umd 这种兼容性的模块格式打包我们的子应用。如何在浏览器运行时获取远程脚本中导出的模块引用也是一个需要解决的问题。

    通常我们第一反应的解法,也是最简单的解法就是与子应用与主框架之间约定好一个全局变量,把导出的钩子引用挂载到这个全局变量上,然后主应用从这里面取生命周期函数。

    这个方案很好用,但是最大的问题是,主应用与子应用之间存在一种强约定的打包协议。那我们是否能找出一种松耦合的解决方案呢?

    很简单,我们只需要走 umd 包格式中的 global export 方式获取子应用的导出即可,大体的思路是通过给 window 变量打标记,记住每次最后添加的全局变量,这个变量一般就是应用 export 后挂载到 global 上的变量。实现方式可以参考 systemjs global import,这里不再赘述。

    应用隔离

    微前端架构方案中有两个非常关键的问题,有没有解决这两个问题将直接标志你的方案是否真的生产可用。比较遗憾的是此前社区在这个问题上的处理都会不约而同选择”绕道“的方式,比如通过主子应用之间的一些默认约定去规避冲突。而今天我们会尝试从纯技术角度,更智能的解决应用之间可能冲突的问题。

    样式隔离

    由于微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。

    Shadow DOM?

    针对 "Isolated Styles" 这个问题,如果不考虑浏览器兼容性,通常第一个浮现到我们脑海里的方案会是 Web Components。基于 Web Components 的 Shadow DOM 能力,我们可以将每个子应用包裹到一个 Shadow DOM 中,保证其运行时的样式的绝对隔离。

    但 Shadow DOM 方案在工程实践中会碰到一个常见问题,比如我们这样去构建了一个在 Shadow DOM 里渲染的子应用:

    constshadow=document.querySelector('#hostElement').attachShadow({mode:'open'});shadow.innerHTML='<sub-app>Here is some new text</sub-app><link rel="stylesheet" href="//unpkg.com/antd/antd.min.css">';

    由于子应用的样式作用域仅在 shadow 元素下,那么一旦子应用中出现运行时越界跑到外面构建 DOM 的场景,必定会导致构建出来的 DOM 无法应用子应用的样式的情况。

    比如 sub-app 里调用了 antd modal 组件,由于 modal 是动态挂载到 document.body 的,而由于 Shadow DOM 的特性 antd 的样式只会在 shadow 这个作用域下生效,结果就是弹出框无法应用到 antd 的样式。解决的办法是把 antd 样式上浮一层,丢到主文档里,但这么做意味着子应用的样式直接泄露到主文档了。gg...

    CSS Module? BEM?

    社区通常的实践是通过约定 css 前缀的方式来避免样式冲突,即各个子应用使用特定的前缀来命名 class,或者直接基于 css module 方案写样式。对于一个全新的项目,这样当然是可行,但是通常微前端架构更多的目标是解决存量/遗产 应用的接入问题。很显然遗产应用通常是很难有动力做大幅改造的。

    最主要的是,约定的方式有一个无法解决的问题,假如子应用中使用了三方的组件库,三方库在写入了大量的全局样式的同时又不支持定制化前缀?比如 a 应用引入了 antd 2.x,而 b 应用引入了 antd 3.x,两个版本的 antd 都写入了全局的 .menu class,但又彼此不兼容怎么办?

    Dynamic Stylesheet !

    解决方案其实很简单,我们只需要在应用切出/卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样即能保证,在一个时间点里,只有一个应用的样式表是生效的。

    上文提到的 HTML Entry 方案则天生具备样式隔离的特性,因为应用卸载后会直接移除去 HTML 结构,从而自动移除了其样式表。

    比如 HTML Entry 模式下,子应用加载完成的后的 DOM 结构可能长这样:

    <html><body><mainid="subApp">// 子应用完整的 html 结构<linkrel="stylesheet"href="//alipay.com/subapp.css"><divid="root">....</div></main></body></html>

    当子应用被替换或卸载时, subApp节点的 innerHTML 也会被复写, //alipay.com/subapp.css也就自然被移除样式也随之卸载了。

    JS 隔离

    解决了样式隔离的问题后,有一个更关键的问题我们还没有解决:如何确保各个子应用之间的全局变量不会互相干扰,从而保证每个子应用之间的软隔离?

    这个问题比样式隔离的问题更棘手,社区的普遍玩法是给一些全局副作用加各种前缀从而避免冲突。但其实我们都明白,这种通过团队间的”口头“约定的方式往往低效且易碎,所有依赖人为约束的方案都很难避免由于人的疏忽导致的线上 bug。那么我们是否有可能打造出一个好用的且完全无约束的 JS 隔离方案呢?

    针对 JS 隔离的问题,我们独创了一个运行时的 JS 沙箱。简单画了个架构图:



    即在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap 开始之前的阶段,确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount 前的状态的,从而确保应用在 remount 时拥有跟第一次 mount 时一致的全局上下文。

    当然沙箱里做的事情还远不止这些,其他的还包括一些对全局事件监听的劫持等,以确保应用在切出之后,对全局事件的监听能得到完整的卸载,同时也会在 remount 时重新监听这些全局事件,从而模拟出与应用独立运行时一致的沙箱环境。

    蚂蚁的微前端落地实践

    自去年年底伊始,我们便尝试基于微前端架构模式,构建出一套全链路的面向中后台场景的产品接入平台,目的是解决不同产品之间集成困难、流程割裂的问题,希望接入平台后的应用,不论使用哪种技术栈,在运行时都可以通过自定义配置,实现不同应用之间页面级别的自由组合,从而生成一个千人千面的个性化控制台。

    目前这套平台已在蚂蚁生产环境运行半年多,同时接入了多个产品线的 40+ 应用、4+ 不同类型的技术栈。过程中针对大量微前端实践中的问题,我们总结出了一套完整的解决方案:



    在内部得到充分的技术验证和线上考验之后,我们决定将这套解决方案开源出来!

    qiankun - 一套完整的微前端解决方案

    https://github.com/umijs/qiankun

    取名 qiankun,意为统一。我们希望通过 qiankun 这种技术手段,让你能很方便的将一个巨石应用改造成一个基于微前端架构的系统,并且不再需要去关注各种过程中的技术细节,做到真正的开箱即用和生产可用。

    对于 umi 用户我们也提供了配套的 qiankun 插件 @umijs/plugin-qiankun,以便于 umi 应用能几乎零成本的接入 qiankun。

    最后欢迎大家点赞使用提出宝贵的意见。

    Probably the most complete micro-frontends solution you ever met .
    可能是你见过的最完善的微前端架构解决方案。



    【Weex】网易严选 App 感受 Weex 开发 · Issue #3 · zwwill/blog · GitHub

    $
    0
    0

    自打出生的那一天起,Weex 就免不了被拿来同 React Native「一决高下」的命运。React Native 宣称「Learn Once, Write Anywhere」,而 Weex 宣称「Write Once, Run Everywhere」。在我看来,并没有谁更好,只有谁更合适。下面我将围绕 Weex 入门进行讲解。
    (如果你尚不了解 React Native,并想简单入门,可以阅读 【整理】React Native 快速入门笔记

    网易严选 App 感受 Weex 开发

    什么都不说,先给你感受下 Weex 的效果。以下就是我使用 Weex,4*8h(不连续)做出来的 demo,其中还包括素材收集,踩坑总结等时间。

    demo 截图

    此处是 demo 源码:
    https://github.com/zwwill/yanxuan-weex-demo

    不得不说,使用 Weex 开发 app 对于我们纯前端人员来说,是件「 很爽」的事情,只要你熟悉了他的语法,基本可以做到一周上手写 app。极其适合交互要求不高,时间紧迫,人手不足的同构开发需求。

    但是,当然有但是,如果你想写出一个完美的 app,你就需要在性能优化上下很大的功夫,包括动画的优化,过场的优化,图片的优化,细节的打磨等等,再者,就是你需要掌握或者「能写」一些原生的代码,不然有些功能你是实现不了的,比如 status bar 的属性更改,开场动画的制作,内存的回收,webview 的监听等等。

    下面我们具体讲讲入门知识

    Write Once, Run Everywhere

    Weex 提供了多端一致的技术方案。

    • 首先,Weex 的开发和 web 开发体验可以说是几乎一样。包括语法设计和工程链路等。
    • 其次,Weex 的组件、模块设计都是 iOS、Android、Web 的开发者共同讨论出来的,有一定的通用性和普遍性。
    • Weex 开发同一份代码,可以在不同的端上分别执行,避免了多端的重复研发成本。

    在同构这条路上,Weex 比 React Native做得更彻底,他「几乎」做到了,「你来使用 vue 写一个webapp,我顺便给你编译成了 ios 和 android 的原生 app」

    至于为什么要造这个轮子,官方给了以下说法

    1、今天在技术社区有大量的 web 开发者,Weex 可以赋能更多的 web 开发者构建高性能和高体验的移动应用。
    2、Web 开发本身具有非常强的高效率和灵活性,这和 Weex 想解决的移动端动态性问题不谋而合。
    3、Web 标准和开发体验是很多顶尖而优秀的科技公司共同讨论和建设的结果,本身的设计和理念都有极高的品质保障
    4、同时 Weex 也希望可以借此机会努力为标准贡献一点自己的微薄之力。
    5、Web 是一种标准化的技术,标准本身就是一种力量,基于标准、尊重标准、贴近标准都意味着拥有更多的可能性。
    6、Web 今天的生态和社区是非常繁荣的,有很多成熟的工具、库、工程体系、最佳实践可以使用、引入和借鉴。

    在我看来,Weex 其实是 Alibaba 团队提高生产效率的产物,在淘宝这类要求多端统一迭代快速的部门,三端约定一种便于统一的规范,在加上时间的发酵,渐渐的就有了此类脚手架的雏形,同时在脸书 React Native 开源带来的极大轰动后,自己也坐不住了吧^_^

    好了,闲话就说到这,下面就来让我们解剖一下WEEX的优劣良莠。

    预科

    入门 Weex 前需要了解以下知识,这样能帮助你更快的掌握
    Node: 《Node.js 教程》
    Vue: 《Vue.js官方教程》
    ES6: 《ECMAScript 6 入门》
    再者就是 ios 和 android 开发语法的入门和编辑器的使用

    环境

    系统环境要求

    IOS : MacOS, 黑苹果
    Android : MacOS, Linux, Windows

    配置环境

    你可以参考官方文档安装必须的依赖环境 http://weex.apache.org/cn/guide/set-up-env.html
    也可以直接安装以下环境

    下载必须的插件:
    a) JDK1.8+
    b) Show Package Details
    c) Android SDK Build Tools
    d) Android Support Repository

    配置基础环境:
    a) ANDROID_HOME (如运行是遇到问题可参考此文 http://www.jianshu.com/p/a77396301b22
    b) JAVA_HOME

    Hello Weex

    官方文档上的入门 Hello world 是 web 端的,紧接着介绍了如何「 集成 Weex 到已有应用

    但是,身为一个 web 前端开发者,如果你不懂原生语音的话,介绍这些并不能起到很好的引导作用,因为web前端开发者都有「 一统前端界」的野心(Web+Android+IOS),「寄人篱下」只能是暂时的。

    快速创建并运行一个纯 Weex App 对于「纯」前端同学来说,才是有意思的事儿。
    但:

    为什么文档要这么设计也是跟Weex的定位有关的,读完下文后续你就慢慢懂了,后面我将做总结解释

    如果你在官方教程里没有找到创建工程的教程,可以阅读此文 《Weex 快速创建工程 Hello World》

    Vue Native

    Weex 在迭代的过程中选择了于 Vue 2.0 握手,因为该版本的 Vue 加入了 Virtual-DOM 和预编译器的设计,使得该框架在运行时能够脱离 HTML 和 CSS 解析,只依赖 JavaScript,如此,Vue 在和 Weex 合作后,便获得了使用 JS 预编译原生的组件 UI 的能力。

    同 React Native 一样,有人也将 Weex 叫做 Vue Native。

    如果你对 Vue 还不了解,可以先学习【预科】部分推荐的 《Vue.js 官方教程》

    那么接下来我们讲讲,Vue 在 Weex 中的不同

    Vue 在 Weex 中的不同

    虽说 Weex 使用 Vue 语言写的,但毕竟是需要在不同平台间运行的,虽然大部分语法都有支持,但是依然有部分语法是不同的

    语法差异

    1、“html标签”

    目前 Weex 支持了基本的容器 (div)、文本 (text)、图片 (image)、视频 (video) 等 组件,注意是组件,而不是标签,虽然使用起来跟 html 标签很像,至于其他标签基本可以使用以上组件组合而成。

    2、Weex 环境中没有 DOM

    因为 Weex 解析 vue 得到的并不是 dom,而是原生布局树

    3、支持有限的事件

    并不支持 Web 中所有的事件类型,详情请参考 《通用事件》

    4、没有 BOM 但可以调用原生 API

    在 Weex 中能够调用移动设备原生 API,使用方法是通过注册、调用模块来实现。其中有一些模块是 Weex 内置的,如 clipboard 、 navigator 、storage 等。
    《clipboard 剪切板》
    《navigator 导航控制》
    《storage 本地存储 》
    为了保持框架的通用性,Weex 内置的原生模块有限,不过 Weex 提供了横向扩展的能力,可以扩展原生模块,具体的扩展方法请参考 《iOS 扩展》 和 《Android 扩展》

    样式差异

    Weex 中的样式是由原生渲染器解析的,出于性能和功能复杂度的考虑,Weex 对 CSS 的特性做了一些取舍
    1、Weex 中只支持单个类名选择器,不支持关系选择器,也不支持属性选择器。
    2、组件级别的作用域,为了保持 web 和 Native 的一致性,需要 <style scoped>写法
    3、支持了基本的盒模型和 flexbox 布局,详情可参考 Weex 通用样式文档。但是需要注意的是,

    • 不支持 display: none;可用 opacity: 0;代替,(opacity<=0.01时,元素可点透)
    • 样式属性暂不支持简写(提高解析效率)
    • flex 布局需要注意 web 的兼容性
    • css 不支持 3D 变换

    Weex 开发&调试

    Vue 语法

    举个栗子,以下是严选App Demo首页的简化代码

    <template><divclass="wrapper"><textclass="iconfont"></text><home-header></home-header><scrollerclass="main-list"offset-accuracy="300px"><refresher></refresher><divclass="cell-button"@click="jumpWeb('https://m.you.163.com')"><yx-slider:imageList="YXBanners"></yx-slider></div><divclass="cell-button"><block-1:title="block1.title":items="block1.items"></block-1></div></scroller></div></template><stylescoped>.iconfont{font-family:iconfont;  }.main-list{position:fixed;top:168px;bottom:90px;left:0;right:0;  }</style><script>varnavigator=weex.requireModule('navigator');importutilfrom'../../src/assets/util';importHeaderfrom'../components/Header.vue';importrefresherfrom'../components/refresh.vue';importYXSliderfrom'../components/YXSlider.vue';importBlock1from'../components/Block1.vue';exportdefault{components:{'home-header':Header,'refresher':refresher,'yx-slider':YXSlider,'block-1':Block1},data() {return{YXBanners:[{ title:'', src:'http://doc.zwwill.com/yanxuan/imgs/banner-1.jpg'},{ title:'', src:'http://doc.zwwill.com/yanxuan/imgs/banner-2.jpg'},{ title:'', src:'http://doc.zwwill.com/yanxuan/imgs/banner-3.jpg'}]}},methods:{jumpWeb(_url) {consturl=this.$getConfig().bundleUrl;navigator.push({url:util.setBundleUrl(url,'page/web.js?weburl='+_url) ,animated:"true"});}}}</script>

    如果以上代码脱离工程单独出现,基本上是无法得知他是 Weex 工程。此处可切实感受到 Weex 的 web 开发体验

    名存实亡的<标签/>

    <template><div><textv-for="(v, i) in list"class="text">{{v}}</text><imagestyle=""src=""></image><videoclass="video":src="src"autoplaycontrols@start="onstart"@pause="onpause"@finish="onfinish"@fail="onfail"></video></div></template>

    Weex 工程中常用的标签有 <div /><text /><image /><video />(组件另算),由此四种标签基本可以满足绝大多数场景的需求,虽说此标签同 web 工程下的标签用法一致,但此处的标签已不再是我们前端口中常提的 html 标签,而且名存实亡的 Weex 标签,确切讲是 Weex 组件。

    通过 weex-loader、vue-loader、weex-vue-render的解析最终转换输出的便是实际的组件,有此设计只是为了完成「 web开发体验」的目标。但是我们身为上层的开发人员要清楚自己每天「把玩」的到底是个什么「鬼」。

    阉割版 CSS

    其实用阉割版来形容 Weex 的 css 支持度并不合适,但如果从「web开发体验」的角度来衡量,那么这个形容词也是可以理解的。(此处对 Weex 寄有厚望^_^)

    单位

    Weex 中的所有 css 属性值的单位均为 px,也可省略不写,系统会默认为 px单位。

    选择器

    Weex 中只支持单个类名选择器,不支持关系选择器,也不支持属性选择器。

    /* 支持单个类名选择器 */.one-class{font-size:36px;
    }/* 不支持关系选择器 */.parent>.child{padding-top:10px;
    }/* 不支持属性选择器,不支持 `v-cloak` 指令 */[v-cloak] {color:#FF6600;
    }

    这个只是对样式定义的限制,不影响样式类名的使用,在标签中可以添加多个样式类名,如:

    <template><div class="one two three"><div></template>

    盒模型

    weex支持css基本的盒模型结构,但需要注意的是

    • box-sizing属性值默认为 border-box
    • marginpaddingborder等属性暂不支持合并简写

    FlexBox

    Weex 中对 flexbox 布局支持度很高,但依然有部分属性并不支持,如 align-items:baseline;align-content:space-around;align-self:wrap_reverse;等。

    具体 Weex 对 flexbox 的支持和布局算法,可通过此文进行了解 由 FlexBox 算法强力驱动的 Weex 布局引擎,此处便不再赘述。

    显隐性

    在 Weex 的 ios 和 android 端,并不支持 display属性。

    因此,不能使用 display:none;来控制元素的显隐性,所以 vue 语法中的 v-show条件渲染是不生效的。

    我们可以使用 v-if代替,或者用 opacity:0;来模拟。

    需要注意的是,ios和android端并不能使用 opacity:0;来完全模拟 visibility: hidden;,因为,当
    opacity 的只小于等于 0.01 时,native 控件便会消失,占位空间还在,但用户无法进行交互操作,点击时会发生点透效果。

    CSS 3

    Weex 支持 css3 属性,虽然支持并不够,但相较 React Native 的「不能用」已经是强大很多了。

    以下几种属性我们在开发前需要知道她的支持度

    • transform:目前只支持 2D 转换
    • transition:v0.16.0+ 的 SDK 版本支持css过度动画,可根据情况配合内建组件 animation实现动画交互
    • linear-gradient:目前只支持双色渐变色
    • font-family:Weex 目前只支持 ttf 和 woff 字体格式的自定义字体

    第三方工具库

    由于使用了增强版的 webpak 打包工具 weexpack,支持第三方框架也是件自然而然的事情。

    常用的有 vuexvue-router等,可根据项目实际情况引入需要的第三方工具库

    npm 包管理

    npm 包管理是前端开发朋友们再熟悉不过的包管理方式了。这也是为什么 React Native 和 Weex 都选择这种管理方式的原因。

    以下是本工程的 package.json 文件,这里就不做讲解了,不熟悉的朋友点这里-> NPM 使用介绍

    {"name": "yanxuan-weex","version": "1.0.0","description": "a weex project","main": "index.js","scripts": {"build": "webpack","build_plugin": "webpack --config ./tools/webpack.config.plugin.js --color","dev": "weex-builder src dist -w","serve": "webpack-dev-server --config webpack.dev.js -p --open"
      },"keywords": ["weex"],"author": "zwwill","license": "MIT","dependencies": {"vue": "^2.4.2","vue-router": "^2.7.0","vuex": "^2.1.1","vuex-router-sync": "^4.3.0","weex-html5": "^0.4.1","weex-vue-render": "^0.11.2"
      },"devDependencies": {"babel-core": "^6.21.0","babel-loader": "^6.2.4","babel-plugin-add-module-exports": "^0.2.1","babel-plugin-transform-runtime": "^6.9.0","babel-preset-es2015": "^6.9.0","babel-runtime": "^6.9.2","css-loader": "^0.26.1","history": "^4.7.2","quick-local-ip": "^1.0.7","vue-loader": "^13.0.4","vue-template-compiler": "^2.4.2","webpack": "^2.7.0","webpack-dev-server": "^2.4.2","weex-builder": "^0.2.7","weex-loader": "^0.4.5","weex-router": "0.0.1"
      }
    }

    UI 尺寸适配

    Weex 容器默认的显示宽度 (viewport) 是 750px,页面中的所有组件都会以 750px 作为满屏宽度。

    这很像移动设备的逻辑像,比如 iPhone 6 的物理像素宽为 750,逻辑像素

    TypeiPhone 3GiPhone 4iPhone 6iPhone 6Plus
    物理像素320x480640x960750x11341080x1920
    逻辑像素320x480320x480375x667414x736
    像素比@1x@2x@2x@3x

    类比在 Weex 中,如果所有的显示宽度都是用默认值 750,那么显示出来的实际像素信息为

    TypeiPhone 3GiPhone 4iPhone 6iPhone 6Plus
    物理像素320x480640x960750x11341080x1920
    显示像素750x1125750x1125750x1134750x1333
    像素比@0.427x@0.85x@1x@1.44x

    所以我们在使用 Weex 做 UI 适配时就没有所谓的 @2x图和 @3x图,所有的尺寸都是Weex帮我们根据
    750 作为基数宽做的缩放。

    当然,Weex 提供了改变此显示宽度的 API, setViewport,通过此方法可以改变页面的显示宽度,可以实现每个页面根据自己的需求改变基数逻辑尺寸

    因此对于一些固定的 icon,不建议使用普通的静态图片或者雪碧图,这里建议使用矢量的字体图片,有以下优点:

    1. 适量图不会变糊
    2. 使用方便,通过 css 的字号控制大小,不用适配机型和屏幕尺寸
    3. 引用 ttf 文件,体积小,且容易更新

    本地调试

    Weex 的调试方式有多种,如果说RN的调试模式是解放了原生开发的调试,那么 Weex 的调试方式可以说是赋予了 web 模式调试原生应用的能力。

    方法一

    此方法多用于解决 bug,检测控件的布局问题

    # 调试单个页面
    $ weex debug your_weex.vue
    # 调试整个工程
    $weex debug your/path -e App.vue

    执行调试命令后,会将指定的文件打包成 JSBundle,并启动一个 weex Devtool 服务( http://localhost:8088可访问,如下图),同时将JSBundle 文件传递至该服务跟路径下的weex文件夹内( http://localhost:8088/weex/App.js,实际是下图右边二维码的的内容)。

    使用 Weex Playground App 扫下左二维码进入调试模,见下图

    再次扫码右方二维码,点击【inspector】即可进入调试模式。

    每一个控件都是相同的数据结构

    <view class="WXText" frame="{{0,0},{414,736}}" hidden="NO" alpha="1" opaque="YES"></view>
    • class:代表原声空间类型
    • frame:表示空间的坐标和大小
    • hidden:代表显隐性,css中visibility设置的值
    • alpha:不透明度,css中opacity设置的值
    • opaque:默认为YES,打开绘图系统性能优化的开关,即不去计算多透明块重合后的真正颜色,从而减小GPU的压力,weex中具体有没有地方可以设置这个开关暂时不清楚,有猎奇心的朋友可以研究下。

    方法二

    此方法多用于开发调试,试试观察结果

    $ weex your_weex.vue

    如果出现 access 权限报错,使用管理员指令

    $ sudo weex your_weex.vue

    此时本地同时启动一个watch的服务器用于检查代码变更,自动重新构建 JSBundle,视觉同步刷新。

    上图看到的效果即为H5页面的效果,我们一般在整个单页编写完成后在使用 Weex Playground App 扫码查看真机效果,或者你也可以在编写的同时使用真机观察代码的运行效果,每次重新构建包到重绘的速度还是很快的。

    但前提是你要保证,你的手机和电脑的连在同一个局域网下,并且使用IP访问。

    Weex 的原理

    虽然说,Weex 可以抹平三端开发的差异,但是知其然也应知其所以然使用起来才能游刃有余。

    打包

    熟悉 React Native 的人都知道, React Native 的发布实际上就是发布一个 JSBundle,Weex 也是这样,但不同的是,Weex 将工程进行分包,发布多个 JSBundle。因为 Weex 是单页独立开发的,每个页面都将通过 Weex 打包器将 vue/we 页面打包成一个单独的 JSBundle,这样的好处在于减少单个 bundle 包的大小,使其变的足够小巧轻量,提高增量更新的效率。

    # 仅打包
    $ npm run build
    # 打包+构建
    $ weex build ios
    # 打包+构建+安装执行
    $ weex run ios

    以上三种均会触发 Weex 对工程进行打包。
    在我们执行了以上打包命令后,所有的工程文件将被单独打成一个独立的 JSBundle,如下:

    打包后的 JSBundle 有两种格式

    #由.vue文件打包出来的包格式(简写),使用vue2.0语法编写// { "framework": "Vue"}/******/(function(modules){......./******/})
    #由.we文件打包出来的包格式(简写),使用weex语法编写// { "framework": "Weex" }/******/(function(modules){......./******/})

    不同的头部是要告诉使用什么语法解析此JSBundle。

    至此,我们准备「热更新的包」就已经准备完毕了,接下就是发包执行了。

    发包

    打包后的 JSBundle 一般发布到发包服务器上,客户端从服务器更新包后即可在下次启动执行新的版本,而无需重新下载 app,因为运行依赖的 WeexSDK 已经存在于客户端了,除非新包依赖于新的 SDK,这也是热更新的基本原理。

    【WeexSDK】包括

    • 【JS Framework】JSBundle 的执行环境
    • 【JS-Native Bridge】中间件或者叫通讯桥梁,也叫【Weex Runtime】
    • 【Native Render Engine】解析 js 端发出的指令做原生控件布局渲染

    执行

    Weex 的 iOS 和 Android 客户端的【JSFramework】中都会运行一个 JavaScript 引擎,来执行 JS bundle,同时向各端的渲染层发送规范化的指令,调度客户端的渲染和其它各种能力。iOS 下选择了 JavaScriptCore 内核,而在 Android 下选择了 UC 提供的 v8 内核(RN两端都是JavaScriptCore 内核)。

    JSBundle 被 push 到客户端后就会在 JSFramework 中执行,最终输出三端可读性的 VNode 节点,数据结构简化如下:

    {
      tag: 'div',
      data: {
        staticStyle: { justifyContent: 'center' }
      },
      children: [{
        tag: 'text',
        data: {
          staticClass: 'txt'
        },
        context: {
          $options: {
            style: {
              freestyle: {
                textAlign: 'center',
                fontSize: 200
              }
            }
          }
        },
        children: [{
          tag: '',
          text: '文字'
        }]
      }]
    }

    有了统一的 VNode 节点,各端即可根据自己的方法解析渲染原生UI了,之前的所有操作都是一致的,包括文件格式、打包编译过程、模板指令、组件的生命周期、数据绑定等。

    然而由于目标执行环境不同(浏览器和 Weex 容器),在渲染真实原生 UI 的时候调用的接口也不同。

    此过程发生在【Weex SDK】的【Weex Runtime】中。

    最总【Weex Runtime】发起渲染指令 callNative({...})有RenderEngine完成渲染

    总结一下

    • Weex 文件分包打包成单个 JSBundle 文件
    • 发布到发包服务器上,通过热更新 push 到用户的客户端,交由【Weex SDK】执行解析
    • SDK 中的【JS Framework】执行 Bundle 脚本生成 Virtual DOM
    • Virtual DOM 经由各端执行环境【Weex Runtime】解析翻译成执行指令
    • 【Native RenderEngine】接收到指令后执行渲染操作,作出渲染出完整的界面

    官方配图:

    扩充配图:

    Weex 的工作模式

    1. 全页模式

    目前支持单页使用或整个 App 使用 Weex 开发(还不完善,需要开发 Router 和生命周期管理)。

    本文先行的严选 demo 便是使用第二种全屏模式,使用 Weex 开发整个 App,期间触碰到 Weex 的在此模式下诸多不足,如 StatusBar 控制、Tab 切换、开场动画自定义、3DTouch、 Widget 等等原生的特色功能没有现成的 API,需要我们自己扩展,甚至扩展不了。因此并不能完全“灭掉”原生。

    所以,目前在阿里内部使用较多的是此模式中的单页模式,这也是为什么官方文档在介绍原理后就直接奔入 集成到原生应用的主题上去了。

    2. Native Component 模式

    把 Weex 当作一个 iOS/Android 组件来使用,类比 ImageView。这类需求遍布手淘主链路,如首页、主搜结果、交易组件化等,这类 Native 页面主体已经很稳定,但是局部动态化需求旺盛导致频繁发版,解决这类问题也是 Weex 的重点。

    3. H5 Component 模式

    在 H5 种使用 Weex,类比 WVC。一些较复杂或特殊的 H5 页面短期内无法完全转为 Weex 全页模式(或RN),比如互动类页面、一些复杂频道页等。这个痛点的解决办法是:在现有的H5页面上做微调,引入Native 解决长列表内存暴增、滚动不流畅、动画/手势体验差等问题。

    另外,WVC 将会融入到 Weex 中,成为 Weex 的 H5 Components 模式。

    严选 App Demo 实现过程中的感想

    Vue-Router & Tab

    由于 Weex 没有封装 Tab 的组件,因此笔者使用了很多方法来实现Tab切换的功能。

    1、vue-router:router 思想方便管理,但是每次切换都是新的实例,没有tab模式
    2、opacity、visablity:此处需要注意,Weex的渲染机制和web是有区别的,对夫层设置 opacity 或者visiablity隐藏是无法同时隐藏定位为 position:fixed;的子元素。
    3、position、transform:改变 tab 层的位置,此方法在定位为 position:fixed;的子元素上依然无效。

    image & iconfont

    Weex 中所有的静态资源基本都是网络资源,包括图片、字体图片等,所以使用 iconfont 图标是再合适不过的了。

    此 demo 中所有的 icon 均使用 的iconfont。

    此处强烈推荐一个站点 www.iconfont.cn

    在此平台你可以找到几乎所有你需要的 icon,你也可以上传自己的 icon 到自己创建的项目中。同时该系统还提供生成ttf、woff 资源,并且做了 cdn 加速和 gzip 压缩,是不是跟 Weex很配呢?

    不过也有风险,就是,如果哪天阿里不在维护并回收该平台的资源了,你的 app 可能就会变成这样,全是方框,或者 padding 掉你 H5 的页面

    当然,这种及情况出现的几率很小,如果你是一个大公司,你手上有更好的资源急速方案,那就自己保存吧。

    webview

    UIWebView是我们开发App常用的一个控件,不过Weex帮我们封装好的API明显时不够用的,目前只有 pagestartpagefinisherror,并没有封装像RN那样的 onShouldStartLoadWithRequest拦截地址请求的API,在我看来,这有些不合理,并不清楚轮子的制造者是什么意图。

    性能

    性能是一个大课题,在此就不做展开了,只稍微提及一些我们开发需要注意的几点

    • 性能影响点:UI更新>UI事件响应>后台运算
    • 合理优化过场&动画,过场和 console 容易引起 app crash 需要注意
    • 降低 js <-> native 的通信频率
    • 优化list结构,降低重排重绘压力
    • 把优先级低且耗时较长的工作推后处理

    Weex 的现状

    Weex 解决了的

    我的发布我做主(热更新)

    脚本语言天生自带“热更新”,Weex 针对 React Native 的热更新策略做了优化,将 WeexSDK 事先绑到了客户端上,并且对 JSBundle 进行分包增量更新,大大提高了热更新的效率。

    但优点也是缺点,如果新包依赖于心的 SDK,此情况下,我们需要发布还有新 SDK 的 app 到应用市场,用户也须从市场更新此 app。不够随着 WeexSDK 版本的稳定后,相信此策略的优势就会凸显出来。

    性能问题

    Weex 是一种轻量级、可扩展、高性能框架。集成也很方便,可以直接在 HTML5 页面嵌入,也可嵌在原生UI中。由于和 React Native 一样,都会调用 Native 端的原生控件,所以在性能上比 Hybrid 高出一个层次。

    统一三端

    虽说这是一个大胆的实践,但对于大前端社区的统一有着推动作用,显然阿里在这一方面已经迈出了第一步。基本解决了三端同等需求导致资源浪费的痛点。

    但后期可能会出现这种现象,开发一个三端的 App 会从原来的个人变成四个人,多出来的那一个人负责开发 Weex 单页。

    意思就是,三端统一的不够彻底,但就目前的环境下,这一句是最优方案了,却是提高了开发效率。大前端将来将如何一统三国我们且行且观望吧。

    做游戏

    对于一些交互视觉统一且没有很大的性能需求的游戏,Weex 还是可以胜任的。

    近期笔者将尝试发布一款纯Weex构建的益智小游戏,敬请期待。

    朋友们可以用这个demo体验下 Weex 版扫雷游戏开发

    Weex “暂时”放弃的

    虽然说大一统事件百利的事,但并非无一害。

    差异化

    对于一些有差异化完美体验追求的项目就只能收敛或者放弃了。

    独立的 bug 修复

    对于三端同时上线,一端存在 bug 的情况,Weex 并不能保证做到牵一发而不动全身。

    个性化功能

    比如安卓的波纹按钮、3DTouch、 Widget、iWatch版本等,目前这些功能还是没有的,不知道以后 Weex
    是否将其加入到官方文档中。

    声明

    以上均为个人见解,不代表官方。如有不当之处还望指正。

    参考

    [ 1 ] Weex官方文档- http://weex.apache.org/cn/references/
    [ 2 ] 场景研读- Native 性能稳定性极致优化 - https://yq.aliyun.com/articles/69005
    [ 3 ] 门柳- 详解 Weex JS Framework 的编译过程 - https://yq.aliyun.com/articles/59935?spm=5176.8067842.tagmain.66.1QA1fL
    [ 4 ] 阿里百川- 深度揭秘阿里移动端高性能动态化方案Weex - https://segmentfault.com/a/1190000005031818
    [ 5 ] 一缕殇流化隐半边冰霜- Weex 是如何在 iOS 客户端上跑起来的 - http://www.jianshu.com/p/41cde2c62b81

    The text was updated successfully, but these errors were encountered:

      适合前端Vue开发童鞋的跨平台Weex - SegmentFault 思否

      $
      0
      0

      基于 Vue 技术栈的你如果需要选用一种移动端跨平台框架,是 Weex?React-Native?还是Flutter? 无疑,相对于后两者,因为你现在已有比较熟练的 Vue 基础,如果在其他条件一致的情况,Weex 无疑是最佳选择;但是 Weex 真的适合在实际项目中进行移动端跨平台开发吗?Weex 的开发效率、Weex 的质量是否满足需求?

      一、开发环境

      在这个 Weex app 开发中,我的开发环境相关配置如下:

      工具名称版本号
      Node.js8.2.1
      Npm5.3.0
      Android Studio3.2
      Weex2.0.0-beta.17
      JDK1.8
      Weex-ui0.6.14

      二、Weex 介绍

      2.1、Weex 理念

      “Write once, run everywhere”, Weex 的定义就像是:写个 vue 前端,顺便帮你编译成性能还不错的 apk 和 ipa(当然,现实有时很骨感)。基于 Vue 设计模式,支持 web、android、ios 三端,原生端同样通过中间层转化,将控件和操作转化为原生逻辑来提高用户体验。 在 weex 中,主要包括三大部分:JS Bridge、Render、Dom,分别对应WXBridgeManager、WXRenderManager、WXDomManager,三部分通过 WXSDKManager 统一管理。其中 JS Bridge 和 Dom 都运行在独立的 HandlerThread 中,而 Render 运行在 UI 线程。 JS Bridge 主要用来和 JS 端实现进行双向通信,比如把 JS 端的 dom 结构传递给 Dom 线程。Dom 主要是用于负责 dom 的解析、映射、添加等等的操作,最后通知 UI 线程更新,而 Render 负责在 UI 线程中对 dom 实现渲染。
      Weex 所有的标签也不是真实控件,JS 代码中所生成存的 dom,最后都是由 Native 端解析,再得到对应的 Native控件渲染,如 Android 中标签对应 WXTextView 控件。 Weex 中文件默认为 .vue ,而 vue 文件是被无法直接运行的,所以 vue 会被编译成 .js 格式的文件,Weex SDK会负责加载渲染这个 js 文件。Weex 可以做到跨三端的原理在于:在开发过程中,代码模式、编译过程、模板组件、数据绑定、生命周期等上层语法是一致的。不同的是在 JS Framework 层的最后,web 平台和 Native 平台,对 Virtual DOM 执行的解析方法是有区别的。

      2.2 创建 Weex 项目

      Weex 提供了一个命令行工具 weex-toolkit 来帮助开发者使用 Weex,它可以用来快速创建一个空项目、初始化 iOS 和 Android 开发环境、调试、安装插件等操作。

      我们可以通过以下步骤创建一个基础的 Weex 项目:
      (1)安装 weex-toolkit 工具

      npm install weex-toolkit -g

      (2)创建新项目

      weex create weex_project

      (3)安装项目依赖

      cd weex_project
      npm install

      (4)启动项目

      npm start

      项目启动完毕,浏览器窗口会自动打开项目首页,如下图所示:
      在这里插入图片描述
      (5)添加 原生Android 平台

      weex platform add android

      (6)运行下面的命令,可以在模拟器或真实设备上启动 Android 应用:

      weex run android

      2.3、运行Weex项目

      2.3.1、启动服务端应用

      (1)进入目录 weex_project/backend/,安装服务端应用所需要的插件包:

      $ npm install

      (2)启动服务端应用

      $ npm run start

      2.3.2、启动 Weex 应用

      (1)如果你还没安装 weex 工具,可以运行以下命令进行安装:

      $ npm install -g weex-toolkit

      (2)安装项目需要的插件包:

      $ npm install

      (3)启动项目:

      $ npm run start

      三、Weex 常用的 VSCode 插件

      Weex为VSCode提供了一些常用的插件,可以提高开发效率:

      • weex-new-project - 用于在 VSCode 中创建Weex项目;
      • weex-lang - 用于在 VSCode 中对最新的 Weex 语法进行支持;
      • weex-doctor - 用于检查 iOS 和 Android 本地开发环境;
      • weex-debugger - 用于在 VSCode 中启动Weex调试工具;
      • weex-run - 用于在热更新模式下启动 Android 及 iOS 工程;

      3.1、weex-run

      可以使用截图的步骤来安装 weex-run插件,可以自行搜索如何安装VSCode插件。
      在这里插入图片描述
      (2)启动 Android 项目

      在这里插入图片描述
      启动成功控制台会输出一堆日志,如下图。
      在这里插入图片描述
      Weex自带热更新功能,接下来,我们查看 Android 项目的热更新。

      3.2、weex-debugger

      (1)安装 weex-debugger 插件,安装过程和安装weex-run插件类似。
      (2)ctrl + shift + p 弹出命令输入框,如下图所示输入:weex debug,然后网页会出现第 2 张截图的二维码:
      在这里插入图片描述
      在这里插入图片描述
      (3)用手机的 Weex Playground App 的二维码进行扫描,出现以下调试页面(一定一定要注意,手机连的 WiFi 和 你开发本地网络在同一局域网)。
      在这里插入图片描述
      (4)再用手机的 Weex Playground App 的二维码扫描 Weex 应用的二维码,调试页面就会变成对应的 Weex 应用的调试页面,如下图所示。
      在这里插入图片描述

      四、Weex 项目实战

      4.1、项目目录路径

      下面通过一个Weex 项目来说明Weex的一些基础,项目目录结构如下:
      在这里插入图片描述

      4.2、功能模块设计

      考虑到更好的体验 Weex 和 H5 在开发效率、功能性能、用户体验等方面的差异性,我们对功能模块进行精心设计,主要基于我们现有的实际项目的业务进行开发,并结合移动端特有的特性。
      相关的模块功能设计如下图所示,其中红色标注部分表示,受限于开发资源、Weex 生态方面原因,我们暂时还没完成全部功能的开发。
      在这里插入图片描述

      4.3、功能界面展示

      下面是Weex示例项目截取一些功能界面展示,如下图:
      在这里插入图片描述

      4.4、重要功能介绍

      除了一些常规的功能开发外,以下介绍的几个功能在 Weex 官网中并没有详细介绍或者根本没有介绍,我们在开发过程中踩了不少坑,因此将踩坑经验进行汇总,帮助大家避免踩坑:

      (1)登录 token 认证
      (2)图片选择/上传功能
      (3)websocket 功能实现
      (4)手机物理键返回上一级功能
      (5)Android 如何显示本地图片

      4.4.1、token 认证功能

      (1)token 认证介绍

      在 Web 领域基于 token 的身份验证随处可见。在大多数使用 Web API 的互联网公司中,tokens 是多用户下处理认证的最佳方式。token 具有以下特性:

      • 无状态、可扩展
      • 支持移动设备
      • 跨程序调用
      • 安全

      基于 token 的身份验证的过程如下:

      • 用户通过用户名和密码发送请求。
      • 服务端程序验证。
      • 程序返回一个签名的 token 给客户端。
      • 客户端储存 token,并且每次用于每次发送请求。
      • 服务端验证 token 并返回数据。

      (2)weex 和 express 之间实现 token 认证

      express 服务端主要使用 express-jwt 插件,express-jwt 是 nodejs 的一个中间件,内部对 jsonwebtoken 进行封装使用。express-jwt 会验证指定 http 请求的 jsonwebtoken 的有效性,如果有效就将 jsonwebtoken 的值设置到 req.user 里面,然后跳转到相应的 router。

      以下是服务端 express 的代码逻辑,代码如下:

      var expressJWT = require('express-jwt');
      // token 设置
      app.use(expressJWT({
        secret: CONSTANT.SECRET_KEY
      }).unless({
        // 除了以下配置的地址,其他的URL都需要验证
        path: ['/getToken', /^\/public\/.*/, /^\/user_disk\/.*/]
      }));
      
      // 登录时,需要进行用户密码认证,相应路由跳转到下面一步
      app.use('/getToken', tokenRouter);
      
      // 当用户密码正确时,我们进行 token 设置
      data: {
        token: jsonWebToken.sign({
          uid: obj.uid
        }, CONSTANT.SECRET_KEY, {
          expiresIn: 60 * 60 * 1
        }),
      }

      对应的Weex的代码如下:

      // Weex 登录逻辑
      login () {
        let param = {
          uid: this.uid,
          password: this.password
        };
        let options = {
          url: '/getToken',
          method: 'POST',
          body: JSON.stringify(param)
        };
        let vm = this;
        api.fetch(options, function (ret) {
          if (ret.ok && ret.data.code === 0) {
            // 前端可以获取到服务端返回的 token ,并将其作为全局变量  
            global.token = 'Bearer ' + ret.data.data.token;
            vm.$router.push('/tabIndex');
          } else {
            modal.toast({
              message: '用户认证失败!',
              duration: 1
            });
          }
        });
      }
      
      // Weex 的每次请求,头部都带上 token
      initOptions.headers['Authorization'] = global.token;

      经过以上代码逻辑处理后,我们查看 Weex 向服务端发送的请求头部,都携带了 token,如下图所示。这样服务端 express 处理这个请求时,就可以通过解析 token 获取到对应的用户 id ,从而允许其对服务端的数据访问。
      在这里插入图片描述

      4.4.2、图片选择/上传功能

      (1)存在问题

      很遗憾,Weex 竟然没有提供文件选择/上传的模块,对于前端开发者来说无疑晴天霹雳,那我不是要手动去写 Android 的 java 代码,经过反复查找,真的没有文件选择/上传模块,于是我们只能自己去写 Java 代码去实现 Android 端图片选择以及上传功能。当然,也可以使用一些第三方的插件。

      (2)实现 Android 原生的图片选择/上传功能

      在 weex_project/platforms/android/app/src/main/java/com/weexapp/extend 目录下新建 图片上传 模块的类 WXAlbumModule ,其继承 WXModule ,其主要两个方法为 choosePhoto 和 onActivityResult ,其中 choosePhoto 用于给 Weex 前端来调用,当 Weex 前端需要选择相册中的图片时,Weex 前端就调用 choosePhoto 方法;onActivityResult 是用户选择好相册中的图片后,会相应触发该事件,并将用户选择的相片以参数形式传入 onActivityResult ,从而我们可以在 onActivityResult 中进行图片的上传逻辑,图片上传完成后,Android 端会在回调事件中通知前端,图片放置在服务端的目录路径,前端可以对应进行图片显示等操作。关键代码逻辑如下,如果如果对 Java 完全一无所知的同学可以先不看,懂 java 代码的建议结合项目代码来看,会更清晰。

      例如,下面是Android端封装的

      @JSMethod(uiThread = true)
      // 给 Weex 前端调用,当用户点击时,调用该函数
      public void choosePhoto(String param, JSCallback callback) {
          if (ContextCompat.checkSelfPermission(mWXSDKInstance.getContext(),
                  Manifest.permission.WRITE_EXTERNAL_STORAGE)
                  != PackageManager.PERMISSION_GRANTED) {
              ActivityCompat.requestPermissions((WXPageActivity) mWXSDKInstance.getContext(),
                      new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                      CAMERA_REQUEST_CODE);
          } else {
              choosePhoto();
          }
          try{
              JSONObject jsonObject = new JSONObject(param);
              this.type = (String)jsonObject.get("type");
              this.path = (String)jsonObject.get("path");
              this.url = (String)jsonObject.get("url");
              this.token = (String)jsonObject.get("token");
          }catch (JSONException e){
              e.printStackTrace();
          }
          this.callback = callback;
      }

      选择完成后,系统会返回图片的信息,此时就可以进行上传操作,如下所示:

      @Override
      // 用户选择好相册中的图片后,会相应触发该事件,并将用户选择的相片以参数形式传入
      public void onActivityResult(int requestCode, int resultCode, Intent data) {
          if (resultCode == WXPageActivity.RESULT_OK) {
              switch (requestCode) {
                  case CAMERA_REQUEST_CODE: {
                      try {
                          Uri selectedImage = data.getData();
                          String[] filePathColumns = {MediaStore.Images.Media.DATA};
                          Cursor c = mWXSDKInstance.getContext().getContentResolver().query(selectedImage, filePathColumns, null, null, null);
                          c.moveToFirst();
                          int columnIndex = c.getColumnIndex(filePathColumns[0]);
                          String picturePath = c.getString(columnIndex);
                          c.close();
      
                          //上传的文件
                          File file = new File(picturePath);
                          // 普通参数
                          HashMap<String , String> params = new HashMap<>();
                          params.put("path", this.path);
                          uploadForm(params, "file", file, "", this.url);
      
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                      break;
                  }
              }
          }
          super.onActivityResult(requestCode, resultCode, data);
      }

      实现好以上选择图片和上传图片的代码逻辑后,我们需要在 weex_project/platforms/android/app/src/main/java/WXApplication.java 中进行模块的注册,代码逻辑如下:

      WXSDKEngine.registerModule("wxalbum", WXAlbumModule.class);

      然后,Weex 前端调用注册的原生模块即可,如下所示:

      const WXAlbum = weex.requireModule('wxalbum');
      
      upload () {
        let path = 'public/upload/';
        let vm = this;
        storage.getItem('token', event => {
          let param = {
            type: 'image/jpeg', // 选择的数据类型
            path: path,
            url: CONSTANT.SERVER_URL + '/users/upload',
            token: event.data
          };
          WXAlbum.choosePhoto(JSON.stringify(param), ret => {
            let obj = JSON.parse(ret);
            vm.imgPath = '/' + path + obj.file[0].originalFilename;
            modal.alert({
              message: vm.imgPath,
              okTitle: '确认'
            }, function () {
              console.log('alert callback')
            })
          });
        })
      },

      4.4.3、WebSocket 功能实现

      (1)存在问题

      Weex 官网的 webSocket 章节特意标注以下警告字眼:
      h5 提供 WebSockets 的 protocol 默认实现,iOS 和 Android 需要自定义实现,Android 可参考:

      • DefaultWebSocketAdapter.java
      • DefaultWebSocketAdapterFactory.java

      好吧,根本没有封装 WebSocket 功能,那我就按官网给的参考来实现吧,于是,我点击前面两个参考链接,链接打开的页面根本不存在,报 404(官网出现这种问题,实在不应该啊)。网上谷歌搜索一圈,没有发现类似的问题,还是主要查看了这个给的 url 以及结合阿里将 weex 贡献给 Apache 维护这个事情,猜测是不是 Weex 捐给 Apache 维护,github 的库目录更改,但是官网对应的 url 地址没有做修改。经过查找,确实是这个问题,在旧库中以下目录找到官网提的:DefaultWebSocketAdapter.java 和 DefaultWebSocketAdapterFactor.java :
      github.com/alibaba/wee…

      (2)手动实现 WebSocket 功能

      我们 在 weex_project/platforms/android/app/src/main/java/com/weex/appadapter 目录底下创建 Websocket 的实现类 DefaultWebSocketAdapter.java 和工厂创建类 DefaultWebSocketAdapterFactory.java ,关键逻辑代码如下:

      // 该类主要实现 Websocket 的连接、发送消息、接收消息、关闭等函数或事件
      public class DefaultWebSocketAdapter implements IWebSocketAdapter {
        @Override
        public void connect(){...}
        @Override
        public void send(String data) {...}
        @Override
        public void close(int code, String reason) {...}
        @Override
        public void destroy() {...}
        ...  
      }

      然后,为该类主要为创建 Websocket 对象的工厂类:

      // 该类主要为创建 Websocket 对象的工厂类
      public class DefaultWebSocketAdapterFactory implements IWebSocketAdapterFactory {
          @Override
          public IWebSocketAdapter createWebSocketAdapter() {
              return new DefaultWebSocketAdapter();
          }
      }

      接下来,在 weex_project/platforms/android/app/src/main/java/com/weexapp/WXApplication.java 中初始化 Websocket ,如下所示:

      WXSDKEngine.initialize(this,
              new InitConfig.Builder().setImgAdapter(new ImageAdapter()).                        setWebSocketAdapterFactory(new DefaultWebSocketAdapterFactory()).build()
      );

      然后,在 Weex 的前端中导入Websocket模块,就可以使用 Websocket,相关代码如下:

      const ws = weex.requireModule('webSocket');
      
      ws.WebSocket(CONSTANT.SOCKET_WS, '');
      // 需要注意 web 端的写法和 android 端的写法不一样
      // android 的 onxx 事件是一个方法,需要传入一个JSCallback的值,
      if (weex.config.env.platform === 'Web') {
        ws.onmessage = this.socketMessage;
      } else {
        ws.onmessage(this.socketMessage);
      }

      4.4.4、点击手机物理键返回上一级功能

      (1)存在问题

      我们开发的 Weex app,如果在 app 的哪个界面,点击手机的返回上一级物理键,都会导致 app 退出,好吧,Weex 也没有提供对应的事件处理,我们不得不自己再去写安卓的 java 代码去向 Weex 的 Web 端抛出这个事件。

      (2)重写手机物理键返回上一级的处理逻辑

      正常交互逻辑:当处于主界面时,返回上一级物理键会进行提示“再点击一次退出”,如果不是处于主界面时,会返回上一级页面。

      首先,我们在 weex_project/platforms/android/app/src/main/java/com/weexapp/WXPageActivity.java 中添加监听点击手机物理键的事件,如下所示:

      public void onBackPressed(){
          Map<String,Object> params=new HashMap<>();
          params.put("name","msg");
          mInstance.fireGlobalEventCallback("androidback",params);
        }

      在 Weex 的 vue 入口文件中,监听 androidback 事件,当接收到该事件时,进行相应的逻辑处理,代码如下所示:

      listenAndroidBack () {
        let vm = this;
        globalEvent.addEventListener('androidback', function (e) {
          if (vm.$route.name === 'tabIndex' || vm.$route.name === 'loginPage') {
            if (vm.exitFlag) {
              weex.requireModule('wxclose').closeApp();
            } else {
              modal.toast({
                message: '再点一次退出',
                duration: 1
              });
              vm.exitFlag = true;
              vm.clearExitFlag();
            }
          } else {
            vm.$router.go(-1);
          }
        });
      },

      4.4.5、Android 显示本地图片

      (1)存在问题

      Weex 官网中 image 图片组件显示项目目录下图片,src 地址直接写成相对路径,如下所示;但是这种写法存在问题,它只支持 web 端的显示,在 Android 端是无法显示的,找不到对应图片。

      <image ref="poster" src="path/to/image.png"></image>

      (2)Android/IOS 端显示本地图片

      Weex 没有在将 vue 编译成 Android 组件时,对应将图片放置到 Android 对应的目录下,所以我们只好自己将图片手动再放置一份,其中 Android 端需要额外将图片放在 /platforms/android/app/src/main/res/drawable-xxhdpi ,IOS 放入xcode 底下的 /Source/images/下 ,然后我们在代码逻辑中,根据环境判断现在是 Web 环境、Android 环境或者 IOS 环境,再对应的获取对应目录下的图片(暂时只能做到这种程度了...),如下代码所示:

      const ICON_URL = {
        Web: `${WEB_IMAGE_URL}`,
         android: `local:///${pureName}`,
         iOS: `local:///filePng/${pureName}${suffixName}`
      }
      return ICON_URL[CUR_RUN_PLATFORM];

      五、编译 Android apk

      Android apk 打包分 debug 版和 release 版,通常所说的打包指生成 release 版的 apk,release 版的 apk 会比debug 版的小,release 版的还会进行混淆和用自己的 keystore 签名,以防止别人反编译后重新打包替换你的应用。 下面我们主要介绍如何在 Android Studio 中对 weex 项目进行打包。

      5.1、Android 平台目录

      Android Studio 打开 Android 工程,目录为:weex 项目 /platforms/android 。

      5.2、常规的 AS 打包分为两种

      一种是没有 “.jks” 文件的打包
      一种是有 “.jks” 文件的打包
      注:.jks” 文件 类似 apk 身份证;

      5.3、没有 “ .jks ” 文件的打包

      (1)打包步骤如下截图:
      在这里插入图片描述
      在这里插入图片描述
      (2)我们点击选择 【Create new】创建jks
      在这里插入图片描述
      (3)填写 key 的相关信息
      在这里插入图片描述
      (4)点击 OK 之后,可以看到如下信息已被自动填充,并点击打包即可。
      在这里插入图片描述
      在这里插入图片描述
      (5)等待打包完成后,就可以查看打包好的 apk 文件
      在这里插入图片描述

      六、Weex 开发总结

      6.1、官网经常无法访问

      Weex 官网经常出现无法访问的情况,频率大概一周至少一次;这就很影响开发效率了,因为在开发过程中需要经常查看官网的写法、说明等,如果访问不了,则会造成一定程度的开发 block。

      6.2、官网文档粗糙

      Weex 官网的文档比较粗糙,如果没有比较好的前端和移动端原生开发知识储备的话,看官网的文档就很吃力了,官网很多讲解写的非常简单,都默认你同时熟练前端和移动端原生开发,而且同时有较好前端和移动端原生开发人员应该在业界还比较少吧。

      6.3、生态贫瘠

      Weex 生态是真的贫瘠,除了阿里自己出产的组件库 weex-ui 外,其它的相关插件几乎找不到,有也是少于100个 star 的,例如我在项目开始前设计的一些功能:拍照、图片选择上传、语音录入、通讯、定位、文件预览等等移动端的特有功能,都没有插件,都需要自己去写 Android 的原生代码,那这时就失去了利用框架提高开发效率的意义;生态跟 react-native 差的真不是一丁半点,而是根本不是一个量级。

      6.4、是否两个 Weex 版本

      结合上一点,坊间传闻:Weex 存在两个版本,一个版本是阿里内部使用的,一个是非阿里内部使用;这个传言无从验证,但是结合第2点说的 Weex 生态贫瘠,我却无意在浏览器搜索中,发现了一系列常见功能的插件封装:weex.apache.org/zh/biz-comp… ,截图如下,但是这些插件并没有提供出来使用,存在 Weex 官网中,但是却没有访问入口。如果这些插件功能能提供使用,无疑将很大程度丰富 Weex 的生态。
      在这里插入图片描述

      6.5、三端兼容性不好

      Weex 号称 “一次撰写,多端运行”,但是存在很多兼容性问题,比如我们在 Web 端调试开完后一个功能模块,但是在 Android 端一运行,就各种跑不通,各种兼容性问题;这种问题导致,我们后期根本不敢在 Web 环境开发,例如:我们这个项目是想开发个 Android 的 app,我们最终都直接在 Android 环境下开发,这种效率肯定就没有在 Web 环境开发效率高。

      6.6、Vue 支持度不够

      Weex 默认集成 Vue 框架,而且主打 Vue 受众,但是 Weex 对 Vue 的支持度还不够,除了官网上提到的那些 vue 特性不支持外,还有很多特性没有被列出,例如:vuex 等。

      参考:
      1,Weex实战项目链接
      2,《WEEX跨平台开发实战》出版啦
      3, Weex开发之地图篇
      4, Weex Eros快速入门
      5, WEEX环境搭建与入门
      6, Weex开发之WEEX-EROS开发踩坑
      7, 移动跨平台技术方案总结


      smart-doc 2.1.9 发布,Java 零注解 API 文档生成工具

      $
      0
      0

      smart-doc 是一款同时支持 java restful api 和 Apache Dubbo rpc 接口文档生成的工具,smart-doc 颠覆了传统类似 swagger 这种大量采用注解侵入来生成文档的实现方法。

      smart-doc 完全基于接口源码分析来生成接口文档,完全做到零注解侵入,你只需要按照 java 标准注释编写,smart-doc 就能帮你生成一个简易明了的 markdown 或是一个像 GitBook 样式的静态 html 文档。如果你已经厌倦了 swagger 等文档工具的无数注解和强侵入污染,那请拥抱 smart-doc 吧!

      功能特性

      • 支持接口 debug。
      • 零注解、零学习成本、只需要写标准 java 注释。
      • 基于源代码接口定义自动推导,强大的返回结构推导。
      • 支持 Spring MVC,Spring Boot,Spring Boot Web Flux(controller 书写方式)。
      • 支持 Callable,Future,CompletableFuture 等异步接口返回的推导。
      • 支持 JavaBean 上的 JSR303 参数校验规范,支持分组验证。
      • 对 json 请求参数的接口能够自动生成模拟 json 参数。
      • 对一些常用字段定义能够生成有效的模拟值。
      • 支持生成 json 返回值示例。
      • 支持从项目外部加载源代码来生成字段注释(包括标准规范发布的 jar 包)。
      • 支持生成多种格式文档:Markdown、HTML5、Asciidoctor、Postman collection、Open Api 3.0+。
      • 轻易实现在 Spring Boot 服务上在线查看静态 HTML5 api 文档。
      • 开放文档数据,可自由实现接入文档管理系统。
      • 一款代码注释检测工具,不写注释的小伙伴逃不过法眼了。
      • 插件式快速集成(支持 maven 和 gradle 插件)。
      • 支持 Apache Dubbo rpc 文档生成。

      Smart-doc 和其他工具的支持

      功能特性smart-docswagger
      代码侵入注解侵入性严重
      集成复杂度简单,只需插件偏复杂
      插件支持有gradle和maven插件无插件
      openapi规范支持支持openapi 3.0完全支持openapi的版本
      CI构建集成

      可在ci构建阶段使用

      maven或者gradle命令

      启动插件生成文档

       

      不支持
      集中化文档中心集成

      已经和torna企业级接口文档管理平台对接

      不支持
      维护持续性值得信赖,开源后用户基础多,一直持续维护全球用户多,开源维护值得信赖
      接口debug2.0.0版本开始已经支持debug,页面比swagger漂亮太多了。支持

      Smart-doc 从 2.0.0 后几乎实现了 swagger ui 的功能,并且比 swagger ui 更简洁大方,也更符合国内开发者的诉求。当然 smart-doc 的功能也已经

      超过了 swagger 为 java 开发者提供的功能。当然 smart-doc 本身是只支持扫描代码生成 openapi 3.0 的文档的,也可以将生成的 openapi 3.0 文档导入到其他ui中渲染展示。

      更新内容

      从 2.0.0 版本开始,smart-doc 完全支持生成 debug 调试页面。从2.1.0版本起,smart-doc 的对接了torna企业级的接口文档管理平台。本次发布内容如下:

       

        1. 修复inlineEnum为false时枚举展示在参数中的问题。
        2. 返回Spring文件下载对象支持自动识别为文件下载,减少手动标记@download tag。
        3. smart-doc使用的css cdn更换,默认使用国内cdn,提升国内的加载速度,切换英文环境使用google的cdn.
        4. 添加多层泛型嵌套的解析支持。gitee #I3T6UV .
        5. 修复父类是泛型时父类中LocalDateTime类型字段生成json样例错误。
        6. 添加将接口排序order推送到torna中。
        7. 修复类上的@ignore tag不生效bug.
        8. 优化字典码推送,空字典码不会像torna发起推送请求。
      

      debug 页面效果

      maven或gradle插件

      smart-doc 官方为了方便用户快速和无侵入的集成 smart-doc 的文档 api 生成能力,我们开发可相关的 maven 或者 gradle 插件。这里也推荐使用插件的方式来使用 smart-doc。

      https://gitee.com/smart-doc-team/smart-doc-maven-plugin

      官方推荐方案

      smart-doc +  Torna 组成行业领先的文档生成和管理解决方案,使用smart-doc无侵入完成Java源代码分析和提取注释生成API文档,自动将文档推送到Torna企业级接口文档管理平台。

      smart-doc+torna

      smart-doc+Torna文档自动化

      smart-doc在国内很多企业中被用来替换了swagger,甚至是在国内Top 3内的大厂都有smart-doc的二次开发版本。Torna未来的目标是追赶和超越Yapi。smart-doc针对java spring技术栈的解析能力目前为业内最强(不服就拿工具来跑smart-doc的解析demo)。所以 smart-doc+Torna的方案威力巨大,Torna目前处于高速迭代期,欢迎体验Torna,我们努力为社区提供高效好用的接口文档解决方案。

      升级建议

       smart-doc 目前最新版本已经支持将rest接口和dubbo rpc的接口文档都推送到Torna企业级接口文档管理系统中。

      DEMO

      使用demo轻松玩转接口文档生成,其他用户案例文档效果展示:https://api.doubans.com/

      知名用户

      • 科大讯飞
      • 一加
      • 小米

      鸣谢

      smart-doc也是利用一些开源技术构建起来的,我在这里对下列开源项目表示感谢。

      • Beetl 国内开源的JAVA模板引擎

      • QDOX 开源JAVA源代码解析库

      什么是 AMD,CommonJS 和 UMD? - 简书

      $
      0
      0

      (本文译自 What Is AMD, CommonJS, and UMD?

      介绍

      多年来,可供选择的JavaScript组件的生态系统不断地稳步增加。有很多的选择固然是很好的一件事,但是各个组件混合搭配使用的时候会带来不少的问题,开发者不会花很多时间就会发现所有组件使用起来总有这样那样的问题。

      为了解决这些问题,互为竞争对手的模块规范 AMD 和 CommonJS 出现了,它们可以让开发者在约定的沙箱以模块化的方式编写自己的代码,以免“污染生态系统”。

      AMD

      异步模块定义(英文简称AMD)已经引领了前端潮流,RequireJS已经是最流行的实现方式。

      下面的例子是 foo模块简单地依赖 jquery

      //    filename: foo.js
      define(['jquery'], function ($) {
          //    methods
          function myFunc(){};
      
          //    exposed public methods
          return myFunc;
      });

      下面的更复杂一点的例子就是多个依赖和多个暴露方法的用法。

      //    filename: foo.js
      define(['jquery', 'underscore'], function ($, _) {
          //    methods
          function a(){};    //    private because it's not returned (see below)
          function b(){};    //    public because it's returned
          function c(){};    //    public because it's returned
      
          //    exposed public methods
          return {
              b: b,
              c: c
          }
      });

      定义的第一部分是依赖的数组,而第二部分基本上是仅在第一部分声明好才能执行的回调函数。(像 RequireJS 这种脚本加载器才会关心这部分,包括找出依赖文件的位置)

      注意:定义中的依赖顺序很重要!(比如 jQuery---> $underscore---> _

      还要注意的是,我们可以映射依赖到我们想要的变量上。如果我们将上面代码中的 $改为 $$,那我们下面代码的函数块中引用到 jQuery时都得用 $$代替 $

      最重要的一点是:你绝对不能在上述代码外的函数中引用变量 $_,因为它对于外面来说就是一个不透明的沙箱。这就是那些规范想要达到的目标!

      CommonJS

      如果你用过 Node.js写过代码,那你会对 CommonJS感到熟悉(因为就是只有一些轻微的变动)。它已经变成使用 Browserify开发的前端开发者中的一种趋势。

      用跟上面一样的格式,下面就是采用 CommonJS规范的 foo模块写法。

      //    filename: foo.js
      
      //    dependencies
      var $ = require('jquery');
      
      //    methods
      function myFunc(){};
      
      //    exposed public method (single)
      module.exports = myFunc;

      下面是应用了多依赖和多个暴露方法的复杂例子:

      //    filename: foo.js
      var $ = require('jquery');
      var _ = require('underscore');
      
      //    methods
      function a(){};    //    private because it's omitted from module.exports (see below)
      function b(){};    //    public because it's defined in module.exports
      function c(){};    //    public because it's defined in module.exports
      
      //    exposed public methods
      module.exports = {
          b: b,
          c: c
      };

      UMD: 通用模块定义

      虽然 CommonJSAMD的风格同样大受欢迎,但是看起来似乎它们并没有达成共识。这样的局面也导致了一种能同时支持两种风格的需要出现,这带给了我们通用模块定义。

      下面这种模式诚然丑陋,但是能使 AMDCommonJS和谐相处,还支持老式的 global变量定义。

      (function (root, factory) {
          if (typeof define === 'function' && define.amd) {
              // AMD
              define(['jquery'], factory);
          } else if (typeof exports === 'object') {
              // Node, CommonJS-like
              module.exports = factory(require('jquery'));
          } else {
              // Browser globals (root is window)
              root.returnExports = factory(root.jQuery);
          }
      }(this, function ($) {
          //    methods
          function myFunc(){};
      
          //    exposed public method
          return myFunc;
      }));

      保持同样的模式实现更复杂的例子:

      (function (root, factory) {
          if (typeof define === 'function' && define.amd) {
              // AMD
              define(['jquery', 'underscore'], factory);
          } else if (typeof exports === 'object') {
              // Node, CommonJS-like
                  module.exports = factory(require('jquery'), require('underscore'));
          } else {
              // Browser globals (root is window)
              root.returnExports = factory(root.jQuery, root._);
          }
      }(this, function ($, _) {
          //    methods
          function a(){};    //    private because it's not returned (see below)
          function b(){};    //    public because it's returned
          function c(){};    //    public because it's returned
      
          //    exposed public methods
          return {
              b: b,
              c: c
          }
      }));

      微前端及Vue + qiankun 实现案例 - 简书

      $
      0
      0
      微前端架构

      微前端特征

      一、什么是微前端

      Techniques, strategies and recipes for building a modern web appwith multiple teamsthat can ship features independently. -- Micro Frontends

      微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

      微前端的概念出现于2016年末,其将微服务的概念引入前端世界。用以解决在需求、人员、技术栈等因素不断更迭下前端工程演变成巨石应用(Frontend Monolith)**而不可维护的问题。这类问题尤其常见于企业级Web项目中。

      微前端架构具备以下几个核心价值:

      • 技术栈无关
        主框架不限制接入应用的技术栈,微应用具备完全自主权

      • 独立开发、独立部署
        微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

      • 增量升级

        在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

      • 独立运行时
        每个微应用之间状态隔离,运行时状态不共享

      微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用( Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

      更多关于微前端的相关介绍,推荐大家可以去看这几篇文章:

      二、 qiankun

      qiankun 是蚂蚁金服开源的一套完整的微前端解决方案。具体描述可查看 文档Github
      链接: https://qiankun.umijs.org/zh/guide是qiankun的说明以及API教程 。

      2.1 qiankun 的核心设计理念

      🥄 简单

      由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。

      🍡 解耦/技术栈无关

      微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。

      2.2 Why Not Iframe

      为什么不用 iframe,这几乎是所有微前端方案第一个会被 challenge 的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的,并不是为了 "炫技" 或者刻意追求 "特立独行"。

      如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。

      iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

      其实这个问题之前 这篇也提到过,这里再单独拿出来回顾一下好了。

      1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
      2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
      3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
      4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

      其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。

      2.3 特性

      • 📦 基于 single-spa封装,提供了更加开箱即用的 API。
      • 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
      • 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
      • 🛡 样式隔离,确保微应用之间样式互相不干扰。
      • 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
      • ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
      • 🔌 umi 插件,提供了 @umijs/plugin-qiankun供 umi 应用一键切换成微前端架构系统。

      2.4 qiankun快速上手

      2.4.1 主应用

      2.4.1.1 安装 qiankun

      $ yarn add qiankun # 或者 npm i qiankun -S

      2.4.1.2 在主应用中注册微应用

      import  { registerMicroApps, start }  from  'qiankun';
      
      registerMicroApps([
      
        {
      
       name:  'react app',  // app name registered
      
       entry:  '//localhost:7100',
      
       container:  '#yourContainer',
      
       activeRule:  '/yourActiveRule',
      
        },
      
        {
      
       name:  'vue app',
      
       entry:  { scripts:  ['//localhost:7100/main.js']  },
      
       container:  '#yourContainer2',
      
       activeRule:  '/yourActiveRule2',
      
        },
      
      ]);
      
      start();

      当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

      如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:

      import  { loadMicroApp }  from  'qiankun';
      
      loadMicroApp(
      
        {  
      
       name:  'app',  
      
       entry:  '//localhost:7100',
      
       container:  '#yourContainer',  
      
        }
      
      );

      2.4.2 微应用

      微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。

      2.4.2.1 导出相应的生命周期钩子

      微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrapmountunmount三个生命周期钩子,以供主应用在适当的时机调用。

      /**
      
       * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
      
       * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
      
       */
      
      export  async  function  bootstrap()  {
      
       console.log('react app bootstraped');
      
      }
      
      /**
      
       * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
      
       */
      
      export  async  function  mount(props)  {
      
       ReactDOM.render(<App  />, props.container ? props.container.querySelector('#root')  : document.getElementById('root'));
      
      }
      
      /**
      
       * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
      
       */
      
      export  async  function  unmount(props)  {
      
       ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root')  : document.getElementById('root'));
      
      }
      
      /**
      
       * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
      
       */
      
      export  async  function  update(props)  {
      
       console.log('update props', props);
      
      }

      qiankun 基于 single-spa,所以你可以在 这里找到更多关于微应用生命周期相关的文档说明。

      无 webpack 等构建工具的应用接入方式请见 这里

      2.4.2.2 配置微应用的打包工具

      除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:

      webpack:
      const packageName =  require('./package.json').name;
      
      module.exports  =  {
      
       output:  {
      
       library:  `${packageName}-[name]`,
      
       libraryTarget:  'umd',
      
       jsonpFunction:  `webpackJsonp_${packageName}`,
      
        },
      
      };

      2.4.3 官方项目实践

      参考链接: https://qiankun.umijs.org/zh/guide/tutorial

      三、Vue+qiankun实现案例

      首先分别创建我们的 项目基座 和 子项目 在这里我分别创建了 qiankun-base qiankun-vue qiankun-react 三个基础的前端项目,直接用vue 和react的官方脚手架创建即可。
      vue create qiankun-base然后 install或者 add qiankun
      vue create qiankun-vue
      npx create-reacte-app qiankun-react

      为了美观 创建结束后 在基座应用中 引用一下 element-ui ,基座的app.vue这里就是简单的配置了一个项目路由的显示。

      <template><div><el-menu :router="true"
                   mode="horizontal"><!-- 基座内不可以放自己的路由 --><el-menu-item index="/">Home</el-menu-item><!-- 引用vue子路由 --><el-menu-item index="/vue">vue应用</el-menu-item><!-- 引用react子路由 --><el-menu-item index="/react">react应用</el-menu-item></el-menu><router-view></router-view><div id="vue"></div><div id="react"></div></div></template><style>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
      }
      #nav {
        padding: 30px;
      }
      #nav a {
        font-weight: bold;
        color: #2c3e50;
      }
      #nav a.router-link-exact-active {
        color: #42b983;
      }</style>

      然后再main.js中正式注册子项目 详细代码意义 的内容代码里做了注释。

      import Vue from 'vue'
      import App from './App.vue'
      import router from './router'
      import Elementui from 'element-ui'
      import 'element-ui/lib/theme-chalk/index.css'
      import { registerMicroApps, start } from 'qiankun'
      // Vue.config.productionTip = false
      const apps = [
        {
          name: 'vueApp', // 应用名
          entry: 'http://localhost:8081/', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)
          // fetch
          container: '#vue', // 容器
          activeRule: '/vue' // 激活路由
        },
        {
          name: 'reactApp',
          entry: 'http://localhost:8082/', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)
          // fetch
          container: '#react',
          activeRule: '/react'
        }
      ]
      registerMicroApps(apps, {
        // beforeMount()
        // beforeUnmount()
      }) //注册app +生命周期
      start({
        prefetch: false // 取消预加载
      }) // 启动
      Vue.use(Elementui)
      new Vue({
        router,
        render: h => h(App)
      }).$mount('#app')

      这里的registerMicroApps 和start 都是qiankun内部的注册方法 我就没有过多的注释详细的内容讲解可以 查看官方的API文档,其中也可以加一些app生命周期的操作,到这里其实基座的准备已经结束了 其实基座的作用就是承接一个子应用的一个挂载 至于内部的样式隔离 js 隔离qiankun在挂载的时候已经做了处理 在这里就不需要我们另外处理了。

      接下来我们修改我们的子应用。

      main.js。

      import Vue from 'vue'
      import App from './App.vue'
      import router from './router'
      // Vue.config.productionTip = false
      let instance = null
      function render (props) {
        instance = new Vue({
          router,
          //store:[],
          render: h => h(App)
          // props:{}
        }).$mount('#app') // 挂在到自己的HTML中 基座中会拿到这个挂载好的最终html 将其插入
      }
       
      if (window.__POWERED_BY_QIANKUN__) { // 判断是否为 qiankun挂载 不是的话自行启动挂载
        __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
      } else {
        render();
      }
      // export const mount = async () => render();
      // 子组件的渲染
      export async function bootstrap (props) { };
       
      export async function mount (props) {
        render(props)
      };
       
      export async function unmount (props) {
        instance.$destroy()
      };

      这里面有几个变量是qiankun的内置API变量和几个挂载方法,这里的render的判断条件 是为了区分独立运行和注入两种状态。

      然后配置我们的vue.config.js。

      module.exports = {
        devServer: {
          port: 8081,
          headers: {'Access-Control-Allow-Origin': '*'
          }
        },
        configureWebpack: {
          output: {
            library: 'vueApp',
            libraryTarget: 'umd'
          }
        }
      }

      设置允许所有人访问 和导出模式,这样我们的vue子应用就配置完成了。

      react 其实也是大同小异 但是为了改变react 默认的配置 我们需要安装一个插件 react-app-rewired

      然后改变package.json配置。

      "scripts": {"start": "react-app-rewired start","build": "react-app-rewired build","test": "react-app-rewired test","eject": "react-app-rewired eject"
        },

      创建 config-overrides.js。

      module.exports = {
        webpack: (config) => {
          config.output.library = 'reactApp'
          config.output.libraryTarget = 'umd'
          config.output.publicPath = '//localhost:8082'
          return config
        },
        devServer: (configFunction) => {
          return function (proxy, allowedHost) {
            const config = configFunction(proxy, allowedHost)
            config.headers = {"Access-Control-Allow-Origin": '*'
            }
            return config
          }
        }
      }

      内容跟vue 基本一致。

      修改index.js:

      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import App from './App';
      // import * as serviceWorker from './serviceWorker';
      
      function render () {
       ReactDOM.render(
         <React.StrictMode><App /></React.StrictMode>,
         document.getElementById('root')
       );
      }
      if (!window.__POWERED_BY_QIANKUN__) { // 判断是否为 qiankun挂载 不是的话自行启动挂载
       render();
      }
      // export const mount = async () => render();
      
      // 子组件的渲染
      export async function bootstrap (props) { };
      
      export async function mount (props) {
       render(props)
      };
      
      export async function unmount (props) {
       ReactDOM.unmountComponentAtNode(document.getElementById('root'))
      };
      
      // If you want your app to work offline and load faster, you can change
      // unregister() to register() below. Note this comes with some pitfalls.
      // Learn more about service workers: https://bit.ly/CRA-PWA
      // serviceWorker.unregister();

      新建一个.env文件配置一下自己的启动端口。

      PORT=8082
      WDS_SOCKET_PORT=8082

      最后app.js:

      import React from 'react';
      import logo from './logo.svg';
      import './App.css';
      import { BrowserRouter, Route, Link } from 'react-router-dom'
      function App () {
        return (<BrowserRouter basename="/react"><Link to="/">首页</Link><Link to="/about">关于</Link><Route path="/" exact render={() => (<div className="App"><header className="App-header"><img src={logo} className="App-logo" alt="logo" /><p>
                    Edit <code>src/App.js</code> and save to reload.</p><a
                    className="App-link"
                    href="https://reactjs.org"
                    target="_blank"
                    rel="noopener noreferrer">
                    Learn React</a></header></div>
            )}></Route><Route path="/about" exact render={() => (<div>about页面</div>
            )}></Route></BrowserRouter>
        );
      }
      export default App;

      这样我们的开发步骤基本就结束了。
      接下来看成果:

      万字长文带你彻底搞懂什么是 DevOps

      $
      0
      0

      DevOps 日渐成为研发人员耳熟能详的一个组合词,但什么是 DevOps,为什么 DevOps 对于互联网企业如此重要,真正将其思考透彻的人却不多,带着这些困惑,本文将带你一探 DevOps 的起源、原则和实践,让你搞清楚到底何为 DevOps。

      DevOps 的起源可以追溯到 2008 年,在一次敏捷大会的敏捷基础设施话题组被提及,从起源我们可以了解到 DevOps 的发展跟敏捷软件开发是密不可分的。

      DevOps 定义

      DevOps 经过这些年的发展,其定义也在不断变化,先来看三段 DevOps 的 wiki 定义。

      1. DevOps 2017 - 2020 年英文 wiki 定义(直译)

        DevOps是一种 软件工程文化和实践(Practices),旨在整合软件开发和软件运维。DevOps运动的主要特点是强烈倡导对构建软件的所有环节(从集成、测试、发布到部署和基础架构管理)进行全面的自动化和监控 DevOps 的目标是缩短开发周期,提高部署频率和更可靠地发布,与业务目标保持一致。

      2. DevOps 2021 年英文 wiki 定义(直译)

        DevOps 是一系列整合软件开发和软件运维活动的 实践(Practices)。目标是缩短软件开发生命周期并使用持续交付提供高质量的软件。

        另:

        DevOps 与敏捷软件开发是互补关系,DevOps 的许多方面来自于敏捷方法论。

      3. DevOps 中文 wiki 定义

        DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间 沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

      提取这三段的共同点,可以看到不论定义如何变化,DevOps 所要实现的目标都是一致的——缩短软件开发生命周期并使用 持续交付提供高质量的软件。由于持续交付活动中包含了构建、测试和发布等活动,我更倾向于用这个定义,可以更好地缩减定义长度。

      另外可以看到英文直接翻译过来的定义中都包含「 实践」 一词,而中文 wiki 经过一定的翻译或本地化后变成了「 文化、运动或惯例」,其还更强调开发运维之间 沟通合作这一点,因此将最新的英文 wiki 定义与中文 wiki 定义相结合,可以帮助我们更好地理解 DevOps,那么它的最终定义是什么就交由读者朋友自己去领会吧。

      DevOps 发展背景

      为什么 DevOps 会如此热门,时常被人所提及,这与其发展背景是分不开的,主要原因可以概括为以下几点:

      1. 敏态需求的增加,即探索性工作的增加;

        • 软件开发从传统的瀑布流方式到敏捷开发,再到现在对敏捷开发提出了更高的要求,近些年创新型的应用不断涌现,在这些应用的研发过程中多采用小步快跑、快速试错的方式,这些探索性工作要求运维能够具备一天发布多次的能力,需要企业完成由稳态到敏态的转变。
      2. 软件开发活动在企业经营活动中占比的不断增加;

        • 业务发展对软件的依赖由轻度依赖、中度依赖发展到目前的重度依赖。
      3. 企业存在对消除浪费的需求。

        • 软件开发活动在企业中的位置越来越重要,而像企业经营活动一样,软件开发活动中也存在着许多的浪费,企业管理上必然存在着 识别并消除浪费的需求。
        • 软件开发中的浪费包括不必要和必要的浪费,不必要的浪费有:无人使用的功能、软件bug、等待测试、等待审批等;必要的浪费包括:工作项移交、测试、项目管理等。

      以上主要从企业的角度说明了 DevOps 的发展,这是较为深层次的原因,表层的推动因素包括:容器化技术的发展、微服务架构的发展等等,这些技术上的创新为 DevOps 提供了良好的发展条件,以解决企业面临的这些问题。

      DevOps 原则与实践

      了解了什么是 DevOps 及其发展原因后,又该如何具体的进行 DevOps 实践,我们采用黄金圈法则来思考这一问题。

      golden_circle.png

      DevOps 原则是总体指导思想,实践是具体的执行方法,DevOps 是一个动态的过程,在进行相关实践的时候可以看看其应用了哪些原则,当违背原则的时候需要思考实践的合理性。

      DevOps 原则

      DevOps 包含以下三大原则:

      1. 流动原则: 加速从开发、运维到交付给客户的流程;
      2. 反馈原则:建设 安全可靠的工作体系;
      3. 持续学习与实验原则:采用科学的工作方式,将对组织的 改进和创新作为工作的一部分。

      流动原则

      1. 坚持少做

        • 产品开始开发时采用 MVP 原则。
        • 产品迭代时要适时做减法。
      2. 持续分解问题

        • 大的变更或需求拆解为一系列小的变更,快速解决。
      3. 工作可视化

        • 采用 Sprint 看板将工作可视化。
      4. 控制任务数量

        • 减少前置时间,降低测试人员的等待时间。
        • 任务越多,预估越不准确。
      5. 减少交接次数

        • 减少不必要的沟通和等待。
      6. 持续识别和改善约束点

        • 识别出影响流动的主要前置因素,比如搭建环境、需求文档。
        • QA、开发、运维、产品持续提升生产力。
        • 为非功能性需求预留20%的开发时间,减少技术债务。
      7. 消除价值流中的困境和浪费(导致交付延迟的主要因素)

        • 半成品——未完全完成的工作。
        • 额外工序——从不使用的文档、重复编写接口文档等。
        • 额外功能——用户实际不需要的功能。
        • 任务切换——将人员分配到多个项目或截然不同的工作任务中。
        • 等待、移动、缺陷、非标准化的手动操作。

      反馈原则

      1. 在复杂系统中安全地工作

        • 管理复杂的工作,识别出设计和操作的问题;
        • 群策群力解决问题,从而快速构建新知识;
        • 在整个组织中,将区域性的知识应用到全局范围;
        • 领导者要持续培养有以上才能的人。
      2. 及时发现问题

        • 快速、频繁和高质量的信息流——每个工序的操作都会被度量和监控。
        • 技术价值流的每个阶段(产品管理、开发、QA、安全、运维),建立快速的反馈和前馈回路(包括自动化构建、集成和测试过程)。
        • 全方位的遥测系统。
      3. 在源头保障质量

        • 过多的检查和审批流程,使得做决策的地方远离执行工作的地方,这导致流程有效性降低,减弱了因果关系之间反馈的强度。
        • 让开发人员也对系统质量负责,快速反馈,加速开发人员的学习。
      4. 为内部客户优化工作

        • 运维的非功能性需求(如架构、性能、稳定性、可测试性、可配置性和安全性)与用户功能同样重要。

      持续学习与实验原则

      1. 建立学习型组织和安全文化
      2. 将日常工作的改进制度化
      3. 把局部发现转化为全局优化
      4. 在日常工作中注入弹性模式
        • 缩短部署的前置时间、提高测试覆盖率、缩短测试执行时间,甚至在必要时解耦架构,都属于在系统中引入类似张力的做法。
      5. 领导层强化学习文化
        • 领导者帮助一线工作者在日常工作中发现并解决问题。

      DevOps 实践

      基于 DevOps 的相关原则,有与其对应的实践,包括:流动的技术实践、反馈的技术实践和持续学习与实验的技术实践。在应用这些实践之前还需认真设计组织结构,使其有利于实践的开展。

      设计组织结构

      • 利用康威定律设计团队结构。
        • 康威定律:软件的架构和软件团队的结构是一致的。
        • 软件的架构应该保证小团队能够独立运作,彼此充分解耦,从而避免过多不必要的沟通和协调。
      • 过度职能导向(成本优化)的危害。
        • 执行工作的人通常不理解自己的工作与价值流目标的关系(“我之所以要配置这台服务器,是因为别人要我这么做”)。
        • 如果运维部门的每个职能团队都要同时服务于多个价值流(即多个开发团队),那么问题更是雪上加霜,因为所有团队的时间都很宝贵。
      • 组建以市场为导向的团队。
        • 将工程师及其专业技能(例如运维、QA和信息安全)嵌入每个服务团队,或者向团队提供自助服务平台,其功能包括配置类生产环境、执行自动化测试或进行部署。
        • 这使每个服务团队能够独立地向客户交付价值,而不必提交工单给IT运维、QA或信息安全等其他部门。
      • 使职能导向有效。
        • 快速响应。
        • 高度信任的文化。
      • 将测试、运维和信息安全融入日常工作。
        • 保证质量、可用性和安全性不是某个部门的职责,而是所有人日常工作的一部分。
      • 使团队成员成为通才。
        • 培养全栈工程师。
        • 给工程师提供学习必要技能的机会,让他们有能力构建和运行所负责的系统。
      • 松耦合架构,提高生产力和安全性。
      • 保持小规模(“两个披萨原则”)。

      要使职能导向有效,需要由传统的集中式运维向提供运维服务的方向转变。

      oaas.png

      运维融入项目开发工作

      • 创建共享服务(类生产环境、部署流水线、自动化测试工具、生产环境监控台、运维服务平台等),提高开发生产力。
      • 运维工程师融入开发团队。
        • 使产品团队自给自足,可以完全负责服务的交付和支持。
        • 派遣工程师到项目开发团队(运维工程师的面试和聘用仍由集中式运维团队完成)。
      • 为每个项目团队分派运维联络人(派遣的运维工程师)。
        • 集中式运维团队管理所有环境,派遣的运维工程师需要理解:新产品的功能、开发原因、程序如何工作、可运维性、可扩展性、监控能力、架构模式、对基础设施的要求、产品特性的发布计划等。
      • 邀请运维联络人参加开发团队会议、每日站会、回顾会议。
      • 使用看板图展示运维工作。

      流动的技术实践

      该部分包含以下内容:

      • 运行部署流水线的基础。
      • 实现快速可靠的自动化测试。
      • 代码持续集成。
      • 自动化和低风险发布。
      • 降低发布风险的架构。

      运行部署流水线的基础

      • 自动化环境(开发、测试、正式)搭建。
        • 使用 Shell、IaC(Puppet、Ansible、Terraform)、Docker、K8S、OpenShift 等技术。
      • 所有内容做版本控制。
        • 应用程序代码版本控制;
        • 数据库代码版本控制;
        • 运维配置代码版本控制;
        • 自动化和手动测试的脚本;
        • 支持代码打包、部署、数据库迁移、应用配置的脚本;
        • 项目相关文件(需求文档、部署过程、发布说明等);
        • 防火墙配置、服务器配置等脚本。
      • 扩展完成的定义。
        • 在类生产环境中按照预期进行,开发工作才认为是完成的。

      实现快速可靠的自动化测试

      • 持续构建、测试和集成。
        • 代码分支持续集成到主干中,并确保通过单元测试、集成测试和验收测试。
        • 常用工具:Jenkins、TFS、TeamCity、GitLab CI。
        • 对持续集成的配合:自动化测试工具;一旦失败必须立即解决的文化;代码持续合入到主干,而不是持续在特性分支上工作。
      • 构建快速可靠的自动化测试套件。
        • 单元测试:JUnit、Mockito、PowerMock
        • 单元测试度量:测试覆盖率。
        • 验收测试:自动化API测试、自动化GUI测试。
        • 并行测试:安全测试、性能测试、单元测试、自动化测试。
        • 测试驱动开发:TDD、ATDD。
      • 让部署流水线始终保持绿色状态。
        • 部署流水线失败时,所有人立即解决问题或者立即回滚代码,后续的代码提交应该拒绝。

      代码持续集成

      • 持续集成代码。
        • 开发人员在自己的分支上独立工作的时间越长,就越难将变更合入主干。
      • 小批量开发。
      • 基于主干开发。
        • 频繁向主干提交(通过合并请求)代码。

      自动化和低风险发布

      • 自动化部署步骤:构建、测试、部署;相关流程包括:
        • 代码打包、构建;
        • 上传 Docker 镜像;
        • 创建预配置的 K8S 服务;
        • 自动化单元测试、冒烟测试;
        • 数据库迁移自动化;
        • 配置自动化。
      • 应用自动化的自助式部署
        • 开发人员专注于编写代码,点击部署按钮,通过监控指标看到代码在生产环境中正常运行,在代码出错时能获得错误信息快速修复。
        • 通过代码审查、自动化测试、自动化部署,控制部署风险,必要时使开发人员也可进行部署操作,测试人员和项目经理可在某些环境中进行部署。
      • 将部署和发布解耦
        • 部署指在特定环境中安装制定版本的软件。
        • 发布指将产品特性提供给所有客户或部分客户使用。
      • 基于环境的发布模式
        • 蓝绿部署
        • 灰度(金丝雀)发布
      • 基于应用的发布模式
        • 实现特性开关,好处:轻松地回滚、缓解性能压力、可以屏蔽服务依赖。
        • 实现黑启动:发布潜在风险的新特性时,隐式调用,仅记录测试结果。
      • 持续交付的实践
        • 持续交付是指,所有开发人员都在主干上进行小批量工作,或者在短时间存在的特性分支上工作,并且定期向主干合并,同时始终让主干保持可发布状态,并能做到在正常的工作时段里按需进行一键式发布。开发人员在引入任何回归错误时(包括缺陷、性能问题、安全问题、可用性问题等),都能快速得到反馈。一旦发现这类问题,就立即加以解决,从而保持主干始终处于可部署状态。
      • 持续部署的实践
        • 持续部署是指,在持续交付的基础上,由开发人员或运维人员自助式地定期向生产环境部署优质的构建版本,这通常意味着每天每人至少做一次生产环境部署,甚至每当开发人员提交代码变更时,就触发一次自动化部署。
      • 大多数团队采用持续交付实践。

      降低发布风险的架构

      • 松耦合架构
      • 面向服务的架构
      • 安全地演进企业架构
        • 绞杀者应用模式:API封装已有功能、按新架构实现新功能、API版本化。
      • 云原生架构

      反馈的技术实践

      这部分包含以下内容:

      • 建立遥测系统
      • 智能告警
      • 应用反馈实现安全部署
      • 应用A/B测试
      • 建立评审和协作流程

      建立遥测系统

      • 什么是遥测(Telemetry)?
        • 遥测包含监控,实现对网络实时、高速和更精细的监控技术。
        • 相比于传统的网络监控技术,遥测通过推模式,主动向采集器上推送数据信息,提供更实时更高速更精确的网络监控功能。
      • 遥测的三大维度
        • Tracing(跟踪),Metrics(指标) , Logging(日志)。
      • 可观察性
        • 系统可以由其外部输出(遥测的数据)推断其内部状态的程度。
        • 能发现、预测并解决问题。
      • 集中式监控系统(可使用:Prometheus、SkyWalking)
        • 在业务逻辑、应用程序和环境层收集数据。
        • 负责存储和转发事件和指标的事件路由器。
      • 应用程序日志遥测(ELK、审计日志、Metrics)
      • 重大应用事件清单:
        • 认证/授权的结果(包括退出);
        • 系统和数据的访问;
        • 系统和应用程序的变更(特别是特权变更);
        • 数据的变更,例如增加、修改或删除数据;
        • 无效输入(可能的恶意注入、威胁等);
        • 资源(内存、磁盘、中央处理器、带宽或其他任何具有硬/软限制的资源);
        • 健康度和可用性;
        • 启动和关闭;
        • 故障和错误;
        • 断路器跳闸;
        • 延迟;
        • 备份成功/失败。
      • 将建立生产遥测融入日常开发工作。
      • 使用遥测指导问题的解决。
      • 建立自助访问的可视化遥测信息系统(信息辐射器)
        • Grafana
        • SkyWalking
        • Kibana
      • 发现和填补遥测的盲区(建立充分而完整的遥测)
        • 业务级别:订单量、用户数、流失率、广告展示和点击等。
        • 应用程序级别:事务处理事件、应用程序故障等。
        • 基础架构级别:服务器吞吐量、CPU负载、磁盘使用率等。
        • 客户端软件级别:应用出错和崩溃、客户端的事务处理事件等。
        • 部署流水线级别:流水线状态、部署频率等。

      智能告警

      • 解决告警疲劳
        • 充分而完整的遥测会引入告警疲劳问题,需要更智能的报警。
      • 使用统计分析方法,而非静态阈值设置告警
        • 使用均值和标准差(适用于正态分布的数据):度量数据与均值存在较大标准差时告警。
      • 使用预防故障的告警,而不只是故障发生后的告警
        • 试着问有什么指标可以预测故障。
      • 异常检测技术
        • 平滑统计技术:使用移动平均数,利用每个点与滑动窗口中所有其他数据的平均值,来转换数据。
        • 支持高级异常检测的工具:Prometheus、Grafana。

      应用反馈实现安全部署

      • 通过遥测使部署更安全——部署后能立即发现问题。
      • 价值流中的所有人(开发人员、开发经理、架构师、运维团队等)共同承担运维事故的下游责任。
        • 共同承担值班工作、共同解决生产环境问题。
      • 让开发人员跟踪工作对运维人员的影响。
        • 使开发的应用易于部署,提升运维人员幸福感。
      • 让开发团队自行管理生产服务。
        • 首先由开发团队管理,然后才交由集中的运维团队管理。
        • 运维工程师由生产支持转变为顾问或加入团队,帮助做好部署准备,建立服务发布指南(包括:支持有效的监控、部署可靠、架构能支持快速频繁的部署等)。
        • 为团队分配SRE人员。SRE定位:SRE就是软件开发工程师负责了运维工作,SRE非常稀少,只能分配给最重要的团队。

      应用A/B测试

      • 在功能中集成A/B测试
        • 向用户随机展示一个页面的两个版本之一。
      • 在发布中集成A/B测试
        • 使用特性开关。
      • 在功能规划中集成A/B测试
        • 不仅要快速部署和发布软件,还要在实验方面不断提升,通过实验主动实现业务目标和客户满意度。

      建立评审和协作流程

      • 防止「过度控制变更」
        • 反事实思维容易认为事故是由于缺乏审批流程导致。
      • 建立同行评审,缩短审批流程
        • DevOps 中高绩效的组织更多地依赖同行评审,更少地依赖外部变更批准(层层审批)。
      • 代码评审
        • 每个人的代码提交到主干时,必须由同行进行评审;
        • 每个人应该持续关注其他成员的提交活动;
        • 定义高风险变更,从而决定是否需要请领域专家进行审查;
        • 将大的提交变更拆分成小批量变更。
      • 利用结对编程改进代码变更
        • 研究表明:结对的程序员比两个独立工作的程序员慢了15%,而‘无错误’代码量却从70%增加到了85%。
        • 测试和调试程序的成本通常比写初始代码的成本高出多倍。
      • 评估合并请求的有效性
        • 与在生产环境产生的结果无关。
        • 有效合并请求的基本要素:必须足够详细地说明变更的原因、如何做的变更,以及任何已识别的风险和应对措施。

      持续学习与实验的技术实践

      这部分包含以下内容:

      • 将学习融入日常工作
      • 将局部经验转化为全局改进
      • 预留组织学习和改进的时间

      将学习融入日常工作

      • 公正文化和学习文化
        • 人为错误往往不是问题的根本原因,可能是复杂系统中存在不可避免的设计问题而导致。
        • 不应该对造成故障的人进行「点名、责备和羞辱」,我们的目标是最大限度地抓住组织学习的机会。
        • 从学习的角度看待错误、报错、失误、过失等。
        • 相关实践1:在事后分析中,不指责,公正地进行评判,使工程师自己愿意对事情负责,并且热情地帮助其他人避免同样的错误发生;广泛地公开事后分析会议结果。
        • 相关实践2:在生产环境中引入受控的人为故障(捣乱猴),针对不可避免的问题进行演练。
      • 降低事故容忍度,寻找更弱的故障信号
        • 随着组织能力的提升,事故数量大幅降低,故障越不应该出现。
        • 在复杂的系统中,放大微弱的故障信号对于防范灾难性故障事关重要。
      • 重新定义失败
        • 高效能DevOps组织的变更频率是平均水平的30倍,即使失败率只有平均水平的一半,也显然意味着故障总数更多。
        • 鼓励创新并接受因此带来的风险。
      • 创建故障演练日
        • 帮助团队模拟和演练事故,使其具备实战能力。
        • 暴露系统的潜在缺陷。

      将局部经验转化为全局改进

      • [ChatOps] 使用聊天机器人、积累组织知识
        • 自动化工具集成到聊天中,比如(@bot depoy owl to production);
        • 操作结果由机器人发送回聊天室,每个人都能看到发生的一切;
        • 新来的工程师也可以看到团队的日常工作及执行方式;
        • 看到他人互相帮助时,人们也会倾向于寻求帮助;
        • 使用话题组,建立起组织学习,知识得到快速积累。
        • 加强了透明、协作的文化。
      • 将标准、流程和规范转化为便于执行的形式
        • [ArchOps] 使工程师成为构建者,而不是砌砖工;
        • 将手动操作流程转换为可自动化执行的代码;
        • 将合规性使用代码表达出来。
      • 运用自动化测试记录和传播知识
        • 自动化界面测试,令使用者知道系统如何使用;
        • 单元测试,令调用者知道方法API如何使用。
      • 项目开发中包含非功能性的运维需求
        • 对各种应用和环境进行充分的遥测;
        • 准确跟踪依赖关系的能力;
        • 具有弹性并能正常降级的服务;
        • 各版本之间具有向前和向后的兼容性;
        • 归档数据来管理生产数据集的能力;
        • 轻松搜索和理解各种服务日志信息的能力;
        • 通过多个服务跟踪用户请求的能力;
        • 使用功能开关或其他方法实现简便、集中式的运行时配置。
      • 把可重用的运维用户故事纳入开发
        • 将重复的运维工作通过编码进行实现。
      • 技术选型需要考虑运维因素
        • 不能减慢工作流;
        • 思考举例:TIDB VS MySQL 该如何选择。

      预留组织学习和改进的时间

      • 偿还技术债务制度化
        • 定时「大扫除」
        • 开发和运维针对非功能性需求进行优化,横跨整个价值流。
        • 价值:赋予一线工作人员不断识别和解决问题的能力。
      • 让所有人教学相长
        • 所有的工程师都越来越需要某些技能,而不只是开发人员如此。
        • 越来越多的技术价值流采用了DevOps的原则和模式。
        • [每周学习文化] 每周一次的学习时间,每个同伴既要自己学习,又要教别人。
      • 内部顾问和教练
        • 成立内部的教练和咨询组织,促进专业知识在组织内的传播。

      实践重点

      DevOps 的实践包含许多内容,提炼了以下重点方便查阅:

      • 流动原则的实践
        • 部署流水线的基础(所有内容做版本控制、在类生产环境按预期工作才算完成)
        • 实现快速可靠的自动化测试(自动化运行、始终保持流水线处于绿色状态)
        • 代码持续集成(小批量开发)
        • 自动化和低风险发布(自助式部署、部署和发布解耦、采用持续交付)
        • 降低发布风险的架构(云原生架构)
      • 反馈原则的实践
        • 建立遥测系统(Tracing、Metrics、Logging)
        • 智能告警(使用统计分析方法和预防故障的告警)
        • 应用反馈实现安全部署(部署后立即发现问题、共同承担责任)
        • 应用A/B测试(功能规划中集成A/B测试、使用特性开关)
        • 建立评审和协作流程(同行评审、减少审批流程、结对编程)
      • 持续学习与实验原则的实践
        • 将学习融入日常工作(从学习的角度看待事故、寻找更弱的故障信号)
        • 将局部经验转化为全局改进(ChatOps、让规范便于执行、非功能性的运维需求)
        • 预留组织学习和改进的时间(定时偿还技术债务、教学相长、内部教练)

      结语

      DevOps 的发展与技术的发展相辅相成,也为技术人员提供了更多的学习道路和发展方向,借用一句 DevOps 领袖的话来作为本文的结束语。

      对于所有热爱创新、热爱变革的专业技术人士来说,我们的前方是美好而充满活力的未来。


      本文整理自笔者分享的 ppt,原文及 ppt 地址: github.com/lcomplete/T…

      初中要如何提高语文成绩?

      $
      0
      0
      一吨妈妈的回答

      最行之有效的途径是拆解试卷。

      虽然各个地市中考语文试卷具体内容各异,但除了占比越来越少的字音字形基础知识题外(部分地市已经不考察字音字形),中考语文试卷可以拆分为三大板块:

      古诗鉴赏

      文言文

      现代文应用

      我们期末期中考试等语文试卷,基本也是由这三大板块构成。

      看到这里,肯定有人会疑惑:作文呢?分值最高的作文怎么不见了?

      不忙。看这里:现代文应用,分为现代文的阅读与现代文的写作(部分地市的语言应用题也属于现代文应用范畴),现代文写作,不就是我们的作文题嘛。

      为何我非要冒天下之大不韪而如此划分呢?

      并非我为了哗众取宠。而是,现代文阅读与写作本来就是不分家的,特别是写作,离不开阅读,任何扬言能够不需要读书便速成的写作技巧都是站不住脚的。

      在将试卷进行这三类划分后,同学们需要做的,就是找出你最近考试的两三次试卷,对照我的划分,看看哪一个部分失分特别多,这就是你的薄弱环间。然后针对薄弱环节进行加强即可。

      如果你的失分在文言文比较重,那恭喜你,文言文是这三部分中最容易的。

      你只需要认认真真,一板一眼地把从初一开始课本上所有文言文课本出现的实词虚词,文言句式等一个字挨一个字地整理一遍,该背的背,该整理的整理,你的文言文水平一定会有一个质变。然后辅之以几套文言文题目,刷刷找找感觉,分数就能很快上一个台阶。

      如果你的失分在诗词鉴赏比较重,它的提升难度略大。

      你首先要做的,是将从初一开始,课本上出现的,包括你语文老师补充的课外需要识记的诗词,该背的背过,该默写的默写,首先做到默写填空不丢分。然后,网络上有许多的诗词资料,包括主要的古典诗词意象,主要诗人的风格,不同题材的诗词的情感色彩答题术语等等,找一份出来,认认真真地学一下。然后,找几份近年来的中考诗词真题,做一做,特别是对照着答案看看差距在哪里,答案的得分点是怎么分布的,几套题做下来,你的古典诗词鉴赏题就能基本及格了。如果你要求更高,可以读一读古典诗词方面的鉴赏专著,比如我推荐给我学生过的《月迷津渡》,但是不建议古典诗词水平不到位的同学读,一定要在基础打牢固,并且自己对古典诗词也感兴趣的前提下去读。要不然,读此类书就是受罪。

      如果你的失分在现代文应用上比较多,(绝大多数同学都是在此失分最大,谁让它占比最重呢?),那我的建议是,掌握现代文答题的技巧固然是必须的,但是脱离了阅读与积累的情况下,仅靠答题技巧和作文套路,哪怕能在一两次考试中取得不错的分数,但必然也是昙花一现。

      对于初一初二的同学,大量阅读自然不需多言。即便是初三同学,我仍然建议,要读书,只是较之于初一初二同学可以大量泛读不同,要着重在精读细读上下功夫。

      其实,我们中考语文整体的难度并不是很大,在整体难度不大的情况下,它对于语感的要求却不低。特别是许多语言应用题,许多同学一定有这样的体验:我能够凭感觉做对,但我却说不上来为什么。

      这种说不上来的感觉,就是语感。语感的获得,唯有通过阅读。

      不管是初一初二还是初三的同学,阅读一定要注意有效性。

      这也是为何许多同学困惑:我读了不少书,语文成绩还是不行,是不是读书没有用?

      不是读书没有用,而是你的阅读有效性不够高。这种有效性,一方面依赖于阅读材料的质量,另一方面,也是发挥着决定性作用的,就是同学们的阅读,到底是浮于表面,还是真正地在进行阅读、理解与思考。

      一个真正有效果的,能够作用于提升语文成绩与作文水平的阅读,是不仅要动眼睛,更要动脑子,动笔头的。

      换句大白话,也就是,要写读书笔记。

      这不是什么秘诀,几乎每个语文老师都会要求同学们来写读书笔记。但是,据我了解,绝大多数的读书笔记都是“应付作业”。随便翻开一本书,挑一两段看起来还不错的段落,也不管它上下文到底在说什么,随随便便摘抄到本子上,就算是完成了任务。这属于“消极怠工”式读书笔记,没什么用处。

      还有一种读书笔记的误区,也不少见。有很多同学,会准备一些精美的小本子,然后从一些来路不明的“精美散文”或“网络美文”中摘抄一些或华丽或忧伤的小句子。我很欣赏这些同学的态度,非常认真,如果仅仅将这种笔记作为一种爱好,当然无可厚非。可是,如果我们对做读书笔记抱有帮助提升作文和语文成绩的期望,那此种笔记也达不到效果。

      综上两个例子,我要讲的意思就是:第一,我们做读书笔记的材料本身必须精挑细选;第二,读书笔记必须遵循一定的规范。

      第一点我先暂时不展开,因为每个同学的兴趣点和基础不一样,也不可能有一个统一的书单提供给所有人。我唯一强调的是,我们要有意识地对阅读材料进行分类。即区分哪些需要泛读,哪些需要精读,哪些需要一读再读。泛读的材料,可以不做笔记;精读的可以写读后感;值得一读再读的,便可以选择写读书笔记。

      我们今天重点来讲值得一读再读的材料的读书笔记应该怎么做。

      我以我一个初一学生最近做的萧红《回忆鲁迅先生》的读书笔记为例子来讲。

      稍微交代一下此读书笔记的背景:前不久我们共读《朝花夕拾》,我给这位同学提供了萧红的散文《回忆鲁迅先生》作为补充材料,目的是让同学们对于鲁迅有一个比较形象亲切的感知。我并没有要求对此材料做读书笔记。但这位同学在课后,主动做了《回忆鲁迅先生》的读书笔记。即如下:

      为了方便同学们阅读,我在此贴出来的是未批改版,所以内容上会有些许的误差和有待商榷之处。(图已打码,请勿盗图,盗图必究)

      这篇读书笔记主要分为了四个部分。第一部分是词汇积累;第二部分是语段分析;第三部分是作者生平;第四部分是阅读感悟。

      一个读书笔记,分为这四个板块,是很合理的。基本上需要同学们积累和总结的都涵盖在内了。

      我重点强调第二部分,也就是“语段分析”,这是一份读书笔记中的核心部分,也是最容易整理地不好的地方。

      一般来说,我们整理语段,可以从两方面入手。第一方面是语言方面,即写得特别优美动人的段落。第二方面即如本示例所整理的,能够在塑造人物或突显主旨方面有重要作用的,应该整理下来,并且进行分析。此文蓝色笔迹便是进行的语段分析。

      这个分析过程,其实就是一个进行文本细读的过程,一个做阅读理解题的过程,更是一个学习具体的写作技巧的过程。而这个过程,恰恰是大多数同学的读书笔记中所缺乏的。

      在这个分析过程中,同学们能够真正地对文本进行认知与思考,而我作为老师,通过对此分析的批阅,亦能够及时了解同学们理解不到位之处,需要进一步分析与讲解之处。

      比如,此文第一个语段的分析便不到位。当这种不到位通过读书笔记的形式暴露出来,我们便可以着重地进行详解。通过讲解前后的理解的对比,通过认识自己分析不到位的缘由,发现自己阅读理解的思考误区,从而能够更具操作性地掌握阅读理解的技巧和方法。

      以上便是在试卷拆解基础上,我们针对性地提升语文成绩的不同路径。

      我再多说一句:语文学习有方法有技巧,但真的没有捷径。

      部分比较聪明又善于总结答题技巧的初中同学,能够在不怎么读书的情况下,语文也考得不错,便宣扬“语文成绩可以与读书脱节”“只靠刷题一样能得高分”。

      小朋友们,此话真的不能当真,听听就好了。因为初中语文比较简单,当你进入高中后,语文难度一下子会拔高很多,而没有阅读底子的同学,很快便会在阅读速度,理解能力和写作能力上暴露出很难弥补的短板。为何说很难弥补?其实通过大量阅读,半年左右就可以补上。但是,高中生,真的没有这个时间。

      语文学习,不要想着偷懒,今天偷的懒,迟早要还的。

      ————————————

      公众号:一吨语文,更多语文学习干货!

      ——————————————

      ……二更…………

      二更来聊一下初中作文,这也是我教学的主业。但是,任谁也不可能写个几千字就能把作文给讲透了,同样也不存在看几篇文章就能把作文写好。如果作文这么简单,它就不可能值50分(有的地市60分)。所有的作文方法论,都需要一更中提到的大量的泛读与高质量的精读为底子。

      这次更新,我以20年上海中考两篇作文为例,来谈,我们初中同学如何找到和明确自己的写作方向。

      上海市20年中考作文题目为《有一种甜》。我们直接来看两篇考场作文:

      先来看例文1:


      再来看例文2:

      显然,例文1是最不出乎我们意料的一种写法。

      例文2,另辟蹊径,不落俗套。

      如果我问例文2好在哪里,相信所有人都能头头是道:立意新颖深刻,语言精致优美,诗词积累也不错,等等等等。

      例文2的好不需要分析,大家都一目了然。

      我要问的是,例文2,最大的不好在哪里?

      对于我们绝大多数同学而言,例文2最大的不好在于“不好模仿”。我们能够很好地欣赏它,却绞尽脑汁也很难模仿借鉴它。

      其实例文2的立意和结构很容易借鉴,其中涉及到的诗词其实也大多课内,但是,支撑整篇文章的语言,不容易学。这既涉及到细腻的文学感悟力,也涉及到遣词造句这样基础的文字把控能力。也就是,我们能够非常容易地掌握它的框架,但却填充不了内容进去。

      当然,对于少部分同学,则不存在这个问题。比如,我们完全可以运用例文2的写法,以《钢铁是怎样炼成的》为对象,运用于“难忘的一段经历”“特殊的一段友谊”此类的题目下。至于这部分同学是如何获得“让骨架长出血肉”的能力的,就要回归到一更中提到的阅读与积累。

      话说回来,我们现在暂时回到关于例文2对于大多数同学而言“不好模仿”的讨论上。

      我现在说例文2“不好模仿”好像没什么新奇,但在同学们实际的作文中,却有相当大一部分同学喜欢写例文2此类的文章。我们暂时将此类作文称为“好高骛远”的作文。

      这种“好高骛远”的作文,表现在卷面上就是:看不起写简单小事,不愿意踏踏实实把小事写好,醉心于在作文中填充大而无当的素材与华而不实的语句。

      此类作文往往会出现“形散神散”“主旨不清”“不知所云”此类毛病。

      比如,半命题作文:XX,我为你拍照。有的同学选择写的是《祖国,我为你拍照》,立意没有问题,但由于主题太过宏大而很难操控,最后沦为了几个素材的拼凑。

      对于作文有这种问题的同学,我总是说:不要走不稳当就想跑,踏踏实实写好身边一件小事就会是一篇很好的记叙文。

      虽然当面所有人都点头,但我知道,许多同学都对此不以为然:例文1这种作文太小儿科了,简直就像小学生作文!既不华美,也不深刻,也没有什么好词好句能被圈点出来。在高大上的例文2面前,它太素面朝天了。

      对的,例文1确实朴素,不像例文2让人一眼看过去就感慨:这位同学很厉害。

      但是,我们参加中考,追求的是别人夸你厉害吗?

      不是的,我们追求的是考个高分。

      既然一篇语言朴素的记叙文和一篇语言厉害的小散文(语言只是它们能够获得高分的一个方面,其他的要素我们暂且不论),都能够获得不错的分数,那干嘛不量力而行,选择适合自己模仿和借鉴的道路呢?

      一定有同学会反驳我:你说的道理我都懂,我也知道自己啥水平,但例文1一眼看过去不够惊艳,我写这种作文就怕被老师误以为我就是小学生水平。

      想要搞清楚这个问题,那我们就要明确,初中生记叙文,到底是要我们写什么。

      初中记叙文到底要让我们写什么呢?

      其实就是写你的经历和感受,也就是你的所见所闻在你心中产生了怎样的“所感”(不能违背基本法律和道德规范)。

      能够把所见所闻所感写明白,写生动,就会是一篇特别漂亮的文章。

      你无需用“是否对他人有价值”“是否站在很高的角度”这些标准去要求初中记叙文,这些误区所催生出的“扶老奶奶过马路系列”“马路边捡到钱系列”已经够吐槽二十年了。

      记叙文,本来就是记录一个独特的个体的独特的经历和体验。对你这个个体有触动,就够了,哪怕再微不足道,但我们的成长不就是这样一点一滴,一步一步地过来的吗?比如我们课本中的《散步》,《荷叶母亲》,《背影》,都是生活中小得不起眼的小事,但却因为它们对作者产生了触动,所以为作者所铭记,并用高妙的笔将这份经历和触动记录了下来。

      再比如我们今天提到的例文1,主要写的就是被选为剧社社长让自己特别高兴这样的一个经验。没什么特别深刻的意义。但是,这是一个真实独特的成长经历。字里行间,我们读者能充分地体会和分享到了作者对于戏剧的喜爱以及对于这份肯定的珍视。我们甚至能感受到,作者是如何脸带笑意地来回忆这份“甜”的。我们往大了说,这个“甜”的选材,也是具有现实意义的,它就是我们当代青少年阳光幸福生活的一种反映,是具有代表性的。当然,没必要非要上纲上线地往“大”了来提升它。它的核心价值,还是在于对作者个体的意义。

      在明确了选材方向后,接下来我们要做的就是要分析它,学习例文1是如何把这份“甜”写好的。我们要从结构上拆解,也要从语言的具体表达上分析。因为我们今天重点针对语言基本比较薄弱的同学来谈,所以,我们重点分析这篇看似朴素的记叙文,在语言表达上,我们可以学习什么。

      我们以矛盾冲突最激烈的这一段为例来说:

      “什么社长啊?”爸爸揉着眼睛,“看把你激动的!”“课本剧!课本剧!”“语文老师推荐我做分剧社的社长了!”我边叫边蹦,把昨晚工作到深夜的妈妈吵醒了。妈妈满脸怒气地说:“语文老师的课你都敢不上,还想当社长?快去上课!”“我,我当社长了啊!”“当社长就可以不好好上课吗?上课去!”

      这一段没有任何的“好词好句”,但把冲突场面再现地不错,特别是人物的对话,话顶话很紧凑,而且符合人物身份,尤其是“妈妈”的台词,几乎每一个中国老母亲们都会如此咆哮,这个安排特别真实,我们几乎要会心一笑:是啊,妈妈总是这样的,只关心上课和考试。

      再看“我”的台词:“课本剧!课本剧!”连着两个课本剧,而且都是感叹号,简短有力,把“我”激动,亢奋,迫不及待的状态就写出来了。被“妈妈”吼了第一句之后,“我”又怎么说的:“我,我当社长了啊!”虽然此处没有写“我”被当头泼了冷水后的小蒙圈,但通过话语里一点点的小磕巴,把“我”争辩的态度便写了出来。

      此外,我们还要重点注意到作者的动作描写,神态描写,但这两点略微逊色。

      经过如此分析,我们就学到了这位小作者的一点点小妙招。总结如下:第一,人物语言要合乎人物身份,在尽量真实的基础上,根据主旨表现的需要进行适度的文学修饰。千万不要动不动就写:妈妈语重心长地说“要为祖国四化而努力学习啊!”。这种道理虽然不差,但语言明显不够生活化,作文写出来就假。第二,表现人物的情绪,可以通过具体的语言和语气,比较委婉地传达给读者,不上一定要直白地“我兴奋地喊”“我委屈地说”来表现情绪变化。

      这两点很难吗?其实并不难。而且它对于语言的精美度要求不高,准确即可。

      同样写一件小事,优秀作文与普通作文的差距,就在于一点点的这些细节的不同。绝大多数人都能够写“父亲吃力地穿过火车道”,只有极少数能写出父亲具体的动作:“探身”“攀”“缩”“微倾”。我们也许无法一起步就能写得特别好,这也是不现实的,但经常对照考场优秀作文和课内外优秀作品来分析学习,每一篇都比上一篇有进步,相信我们可以写出优秀的记叙文。

      为避免引起歧义,我重申一下,我强调写好一件身边小事就能是一篇优秀的记叙文,并不意味着我对例文2此类小散文,以及考场议论文等这些写法的否定。对于学有余力的同学,我们当然愿意看到他们的创新。此前我对于《有一种甜》这个题目的写法,就已然提供了许多不同的角度:

      比如,善于观察生活,情感比较细腻的同学,可以写“奶奶包的粽子的那种甜”这种生活小事,写一篇细腻的情感文出来;
      比如,关注社会生活,喜欢针砭时弊的同学,可以从“奶茶的许多甜味都是勾兑的”,来写一篇小小的时评文;
      比如,对时政热心,比较擅长写主旋律的同学,可以从“抗击疫情中的温暖细节”等角度,来写一篇反应社会中人与人之间温情的小议论文;
      比如,对于喜欢哲思与辨证的同学,可以从“甜蜜的糖衣炮弹”这样的角度,来进行对“甜蜜舒适”的思辨;
      比如,对于喜欢文学的同学,可以将文学带给你的阅读体验这种“甜”来入手,写一篇入门级别的文学评论;



      我今日所做的分析,都旨在说明,踏踏实实,从写好一件小事开始,这是我们整个初中作文的起点,而且如果能够真的写好一件小事,完全也可以作为初中作文的终点(中考作文)。

      ——————————————

      第三更: 本次更新是针对性解决初中生记叙文“无事可写”的问题。这部分内容出自我的另一个回答。可惜那个问题流量太小了,我不忍心这个超级好用的方法称为“沧海遗珠”,便把它搬了过来。

      许多同学都存在记叙文“无事可写”的问题。我们老师经常说:“要观察生活,善于从生活中寻找写作素材”。

      道理是这么个道理,但生活应该怎么观察呢?谁来给我解开这个千古谜团。

      如果你也存在这个困惑,那就跟我一起去我们的语文课本中寻找答案。

      举个例子来说吧。现在我们要写一篇歌颂“母爱”的作文。

      抓耳挠腮,不知道该写什么。

      那我们来回忆一下,我们的课本中,比较经典的,关于“母爱”的课文。

      我首先想到了《秋天的怀念》。

      《秋天的怀念》写的是史铁生的母亲劝慰史铁生出门散心,最后自己却撒手人寰。

      我们当然没有史铁生残疾和痛失母亲的经历,那这篇课文对我们写“母爱”的作文有什么借鉴意义呢?

      如果你觉得没有意义,是因为,我还没有给你抽离出这篇课文的骨架。

      我把《秋天的怀念》写母爱的骨架抽离出来,其实就是:

      我遭受了挫折——自暴自弃——母亲鼓励我——我开始扭转心态

      看到没有,这就是《秋天的怀念》的骨架,也是它的行文思路。

      你只需要去思考一下,你的生活中,遭遇了怎样的挫折,你的心态发生了怎样的变化,你的母亲又是怎样来劝慰你的,对你产生了怎样的影响。把这些发生在你身上的事情组织一下,按照《秋天的怀念》的框架来安排,就会是一篇不错的作文。

      你可能会说,我身上没有经历史铁生那样的遭遇,怎么可能写得深刻?

      同学呀,你要追求的根本就不是对标史铁生的深刻,这种深刻本来就不是普通的十几岁的健健康康的中学生能体悟和抒写的。

      你要追求的,是学会史铁生是通过什么(即文章的骨架)来写母爱的。

      你往这个骨架里填充的应该是你生活中那些平凡而微不足道的小挫折小感动。比如考试考得不好,朋友闹了矛盾,自己被长辈误解等等。

      史铁生提供给你了角度,思路,框架,结构,一篇作文,绝大多数的问题,他都给你解决了。你只需要稍稍动动脑筋,想一个自己生活中的事情,然后用丰富的细节来填充史铁生给你的作文骨架,就可以了。

      作文,就变得如此的简单,so easy.

      退一万步,你的生活一帆风顺。你实在是没有经历过挫折,或者你的母亲没有在你遇到挫折时给予足够的关怀。

      那怎么办?

      我的建议是,放弃《秋天的怀念》这个思路,转投其他,而不是按照《秋天的怀念》去进行编造。

      为什么不鼓励编造呢?虽然我们有了骨架之后,编造变得易如反掌。

      因为,没有真实的经验,你的编造就只能泛泛而谈,无法有细节描写。而没有细节描写的记叙文,就是空中楼阁。

      连脚都站不住的作文,怎么可能打动人呢?

      在决定转投其他时,我们还是从课本下手,找一下其他的值得模仿和借鉴的佳作。

      我又想到了《散步》。

      《散步》我以前专门写过,为了省时间(偷懒),我还用这个例子吧。

      《散步》讲一家人去散步,奶奶想走大路,因为大路平坦,孙子想走小路,因为小路有趣。作者选择了大路,但奶奶为了孙子,让作者选择走小路。

      《散步》的骨架抽离出来是这样的:

      发生矛盾——内心活动——化解矛盾

      看到没有,这就是《散步》的骨架。

      这个骨架下,你总会有事可写了吧。

      我们的生活本来就是每天都充满了选择和矛盾,特别是我们中学生,与父母的矛盾简直不要太多。

      母亲逼迫我刷题,母亲逼迫我穿秋裤,母亲不同意我与同桌谈恋爱,母亲不同意我骑自行车上学,母亲不同意我只吃肉不吃菜。对吧,这种事情在我们生活中几乎人人都会遇到。

      这就是你与“母亲”之间的矛盾。

      这些矛盾发生时,你经历了怎样的心理过程,最终,这个矛盾是如何化解的,或者是因何无法化解的,把这个写出来,就会是非常出色的一篇作文。

      比如,你母亲逼迫你穿秋裤。你说你不冷,你不穿。你反抗她对你生活无微不至的关怀。你认为这种关怀几乎等同于专制,自己毫无自由。于是你感觉自己就像一个孤独的战士,为了反抗这种压迫,你就是没穿秋裤,毅然地摔门而出,在寒风中向学校走去。

      如果你在学校感觉冷了,你会不会突然醒悟自己“孤独感”的可笑,并察觉到母亲逼迫你穿秋裤的内涵是对你的爱。然后,以第二天穿上秋裤这种无言的行动作为自己的歉意。(看到么有,矛盾就这样化解了。重点写你态度转变的心理过程,以及你穿上秋裤时的心理活动。)

      如果你在学校一点也不冷,但经过一天的冷静,你察觉到母亲逼迫你穿秋裤的内涵是对你的爱,你回家就可以告诉你妈妈:我真的不冷,因为我长大了,我已经能够知冷知热,照料好自己的生活。(看到么有,矛盾也化解了。重点写你是如何克服羞赧与自尊向母亲“示弱”的,以及你母亲听到你这些话的反应。)

      如果你在学校一点也不冷,回家后,你仍然很生气,便冲你妈妈发火:我一点也不冷,你还非要我穿秋裤!什么都要管着我!然后你们爆发了一场激烈的冲突。最后不欢而散,你感觉自己受尽委屈,非常孤独。(矛盾虽然没有化解,甚至还加深了,但你仍然可以写啊,写为啥没化解,原因在哪里,这种专制的爱,对你而言到底意味着什么。把这个写出来啊,也会很好。但是,我唯一要提醒的,就是不要站在道德的制高点上随意指责任何人。反思,你要做的是,反思。反思矛盾为何没有化解,你的问题在哪里,对方的问题在哪里。这样的作文,才是正能量的,有价值的。)

      写了这么多,我想聪明的你,一定get到了我的意思。

      生活本来可写的内容就特别多。(你看,关于穿不穿秋裤,都可以写这么多话出来。这还只是个提纲而已。)

      既然凭一己之力无法取材和组织文本。那就充分地借助课本吧!

      关于“母爱”这个主题,我们课本中还有几篇特别好的课文。同学们可以试着像我一样,来整理这些课文的骨架,学习这些作者选材的角度,为自己所用。

      然后,这些骨架要推而广之。

      你会恍然大悟:原来,作文可写的能写的实在是太多了!

      ————————————

      公众号:一吨语文,更多语文学习干货!


      ——————————

      ——第四更——-

      本次更新,我想再分享一下关于课外阅读的看法。

      很多很多年以前,我读初一时,有两本书给我留下了至今无法磨灭的印象。一本是《子夜》,一本是《牛虻》。前者是因为篇幅过巨,后者则是因为我读不太懂。彼时的我,将读不懂的原因全部归结在自己身上,并且充分发扬了刨地精神,用最笨的方法开始读《牛虻》——抄书。

      “抄书”就是字面意思,一个字一个字地比着抄。抄的过程就不细表了,总之全部抄完,并且,真的通过抄看懂了这本书。

      这是一个非常笨的方法,年代特色明显,效率极其低下,完全不值得推崇。

      但是,通过这一件事情,我却发现,彼时的初一的我,已经开始了理解的自觉。(自吹自擂)

      换句话说,我在主动地想办法去理解我读的书,而不是限于翻一遍知道情节就满意了。

      对一本书产生属于自己的理解,是我们阅读中应该存在,而且极其有价值的一个环节。

      我为什么强调是“属于自己的理解”。因为这里有一个读懂的“标准”问题。有一种说法:作品属于读者,文学鉴赏本来就没有标准,自己想怎么理解就怎么理解。这样说可以,没问题,一千个人有一千个哈姆雷特嘛。但这个说法有个隐含的前提,那就是,我们不是在“学习”的情境中。

      如果我们将自己的身份设定为学生,并将阅读放置于“学习”的情景,就会发现,“文学理解”无法彻底放飞。必须有一定的准则。

      这个准则具体到考试的阅读理解中,就是“参考答案”。

      这个标准具体到课内文本的教学中,就是“段落大意”与“中心思想”。

      这个标准具体到课外文本的阅读中,它变得稍微自由了一些,同一个问题下可以诞生多个不同的答案,只要言之有理自圆其说即可。这就是属于你自己的理解。这个理解,未必非要与作者一个鼻孔出气,但却仍然必须建立在能够掌握情节、不脱离基本设定、与人物共命运的基础上。

      在这些基础上产生的属于自己的理解,对于我们的理解能力或者鉴赏水平的提升,才是比较有价值的。唯有这样才能称得上读懂。

      而读懂与读完,是两个完全不同的概念。

      这两者的区别到底在哪里?对阅读效果会有多大的影响?我们举个例子来说。

      最近,我的两个学生都在读《人生》。课上我与他们交流时(我的课堂是1对1课堂,分别交流,两位同学是互不知晓互不干扰的),我问了他们一个共同的问题:高加林最后为何会一无所有?你认为最重要的原因是什么?

      甲同学跟我说:“因为高加林太过于积极进取了,他急于想要成功,但积极过了头就是激进,所以栽了跟头。”

      乙同学跟我说:“因为高加林身上有落后文化的影响。这种落后文化就是文中农村人身上体现出的不识字、不讲卫生等恶习。”

      之所以问这个问题,绝非是要苛求同学们谈出多么深刻的认知,我们要尊重青少年的认知发展规律。我所期待的,就是如前文所言,形成自己对这个人物的理解。

      但这个理解,一定是要在学习的情境中,在符合人物基本设定,与人物共命运的基础上,提出的一点点看法。

      按照这个要求来看。如果你还对《人生》的情节有一定的印象。我想你大概率会认同我的判断:甲同学对《人生》已经有了一定的理解;乙同学则在理解上出现了比较大的偏差。

      这个例子,我想已经很形象地说明了“读懂”与“读完”两者的分野。

      如果我们的阅读止步于读完,我想,大概率要仰天长叹了:读书没有用啊!

      那问题来了。我们该如何判断自己是读懂还是读完了呢?怎样读才能尽可能地读懂呢?

      那就是一定要多问自己几个为什么。

      不同的文本,甚至具体到每一个篇目,我们的“为什么”都不一样。就《人生》此类书写普通人命运的小说,必然要问的一个“为什么”就是:主人公的命运为何会如此发展?(相似的变种问题:主人公为何落得如此下场?主人公为何做出这种选择?什么因素左右了主人公的命运?主人公的命运是由哪个重要情节决定了的?)

      多问几个“为什么”,并不仅仅是对作品情节的回顾,更重要的是,这是一种特别好的思维训练。它训练我们在阅读中带上自己的脑子,学会分析,从而形成自己的看法。

      这种“为什么”的追问,不一定非要形成书面的材料。完全可以在与同学、老师、家长的交流中完成。

      前一段时间,一位学生的家长,也是我的一位知乎的读者跟我交流:“我家孩子在读《骆驼祥子》时,说了这么一句话:祥子也不是个好人。”

      这一句话,让我一下子来了精神。我连忙追问:“他为什么这么说?我真的很想知道他的理由。”

      可惜,这位家长告诉我,她当时姑且一听,并没有与孩子就此深入地交流。

      我觉得特别遗憾。这么看似随口一说的话,其实,就是这位同学对《骆驼祥子》理解的火花。

      顺着这个火花,我们可以鼓励这位同学去思考:什么是好人呢?好人必须是完人吗?存在完人吗?如果不存在,那怎样才能称为好人?

      经过这一系列的思考,这位同学也许能够推翻自己的最初的判断,或者更加坚定自己的判断。这些都不重要,重要的 是,在这个过程中,他对于文本有了更深入的了解,对祥子有了更全面的把握,自己的思维能力得到了极大的锻炼。

      有的人可能会对此嗤之以鼻。这些问题有意义吗?考试又不考。只要在考到祥子的人物形象时,能够写出“祥子善良纯朴,热爱劳动,对生活具有骆驼一般的坚韧的精神,但他身上也有缺点,比如爱说谎话,好占便宜。”就行了。又不会考他到底是不是好人。

      可是,如果我们把课外阅读的目的,仅仅局限在那价值5分的名著题上,我只能遗憾地说,如此短视的学习很难令语文有长足的发展。

      我们对名著进行理解与思考本质其实是一种认知训练。这种认知训练,在初中阶段只是一个萌芽状态,在高中阶段,深层次的认知才成为语文考察的重点。但初中阶段不是考察重点,却并不意味着它不存在,不生长。我们应该有一个宏观的语文学习观,全局统揽整个义务教育阶段的学习。而绝不能头痛医头脚痛医脚。

      以上内容便是我讲的,在我们的阅读中,多问几个为什么,形成属于自己的理解的重要性的问题。

      接下来,我想再分享一下,这种理解与读书笔记之间有什么关系。

      很多情况下,我们并没有机会,或者说纯粹不想与他人分享我们对某本书的看法。此时,则可以选择通过读书笔记的方式,用文字来梳理自己的看法,哪怕是纯粹想吐槽一下主人公也好,完全可以在自己的本子上信马由缰一番。

      我虽然用了“信马由缰”这个词,但绝不是真正想怎么写就怎么写,我们要尽量有条理地去表达自己。在有条理地表达中,你的本来模模糊糊的思考就会逐渐明晰,甚至深化,甚至扭转,都有可能。

      借用王蒙的话说就是:“如果你不用语言来梳理你的思想,不用语言来生发你的思想,不用语言去梳理你的思想,那么你的思想是不可能成熟起来的。”

      稍微总结一下,本次更新的内容是,我们要区别“读懂”与“读完”,读懂作品,形成自己的理解才是比较有价值的。而这个途径可以通过追问来完成。这种追问可以在对话讨论的情境中实现,也可以通过文字的表达来完成。选择适合自己的即可。

      在此稍微回应一下评论区关于“读书笔记会不会破坏阅读趣味”的疑虑。答案是肯定的。毕竟没有哪个同学愿意写作业。

      我鼓励同学们获得阅读的乐趣,但阅读乐趣与写读书笔记并不是非此即彼、水火不容的状态。一方面,我在一更中已经强调,我们要对阅读材料进行有意识的分类,并不是每一本书都有写读书笔记的价值和必要。另一方面,对于需要我们理解的作品的阅读,本来就不能闲庭散步,当自己不给自己定下一定的目标和要求,学习效果肯定会大打折扣。

      希望同学们可以根据自己的情况进行阅读材料的分类,并且,尝试着用讨论或读书笔记的方法去梳理、表达自己的理解。相信一定会有所收获的。

      ——————————

      我的公众号:一吨语文,更多作文学习干货

      ————————————

      ----第五更------

      非常感谢大家对这个回答的认可。许多小学阶段的家长也“未雨绸缪”参与到了这个话题的讨论中。

      本次更新来自于我对另一个问题“如何让孩子爱上语文”的回答。主要讲的是如何通过“口语表达训练”帮助同学们学会理顺思路和清晰表达。对于小学阶段,以及初中低年级存在表达困难的同学都适用。现在正值寒假,家长与孩子有了更多的相处与交流的时间。不妨试一试我所提供的方法,说不定有可喜的收获。

      如下是经常在我课堂上出现的对话场景(对话对象为中学生,已做模糊化处理):

      我:“这位主人公为何会做出如下的举动?原因是什么?”
      同学:“责任,信念。”
      我:“很不错,方向是对的。请你更完整和详细地陈述一下自己的看法。”
      同学:“责任意识和信念。”
      我:“可不可以阐述地再具体一点呢?”
      同学在酝酿几秒钟后,会说出一两句完整的话,但意思却偏离了“责任”与“信念”。




      这不是发生在一个同学身上的个案。它具有一定的代表性。

      我常会思考,为什么,这些同学明明能够理解文章,能够用比较准确的关键词(比如“责任”“信念”)概括出自己对于文章的理解,但在要求详细地阐释时,却无法用完整的语段来表达自己呢?

      当我鼓励他们去向着明明正确的方向(即他们自己总结的关键词方向)去进行完整表达时,他们却无法用准确的话语来表达自己,好不容易表达出来的语句却并不符合自己的初衷,甚至完全走向了其他的方向。

      存在这个问题的同学,在书面表达与口头表达上,还有分化。有的会在书面表达时,能够比较清晰地表达自己。有的在书面表达时,仍然容易陷入混乱。后者,看他们的作文,总有一种特别遗憾的感觉:围着关键字眼一直在打转,极尽挣扎,就是说不到点子上。

      理解没有问题,表达成为了他们的掣肘。

      当我们一直以书面表达(主要为作文)作为评判一个人语文水平的唯一标准时,却疏忽了,书面表达的问题可能仅仅是冰山一角而已。而要解决书面表达的问题,追根溯源,我们发现,它可能不仅是简单的一句“孩子不会写作文”,而是“孩子不会表达自己”。

      如上的分析与这个问题的关系在于:我们许多人,包括部分老师和家长在内,将语文的理解太狭隘,将语文等同于“字词句段篇章”。“口语交际”环节,很难进入到大家的视野。而对于低年级的同学们而言,“口语交际”其实就是行之有效的表达训练。

      同样是语言学习,当我们拿起英语课本,背诵单词短语时,我们能够很明确地判断“我此时此刻在学英语”。但语文学习呢?几乎与我们的生活交织在一起,根本无法分开。

      举个例子。

      我曾经看到过这样一篇学生作文。这位同学做公交车,路过某个地方,等红灯时发现一个招牌写着四个字“阳光不锈”。这位同学面对如此意味深长的四个字感慨良多。当绿灯亮起,车往前开出去后,这位同学猛然发现,这个招牌完整的字眼是“阳光不锈钢”,顿时觉得俗不可耐。

      这是不是语文的学习呢?

      “阳光不锈”带给了她文学的审美,“阳光不锈钢”的大反转又像一个文字游戏让她体会到了语言的狡黠与多变。

      这就是语文的学习。

      可能有人会觉得,这样的机会是可遇不可求的。但在日常中,我们与同学们的每一次对话,只要留心,亦可以是非常好的语文学习机会。

      比如,我们可能会出现如下的对话场景:

      家长:今天运动会好玩吗?
      学生:好玩。
      家长:都有什么好玩的?
      学生:额,都挺好玩。


      “都挺好玩”,典型的概括。能不能举例子呢?我们引导着同学们去详细地描述一下让自己觉得好玩的事件。

      家长:“你给我详细说说呗。”
      学生思考一会儿后:“没啥好说的。就是挺好玩。”
      家长:“具体的每个人都干啥了呢?你把你几个好朋友都干啥了跟我说说。”
      学生:“小王调高没跳过去,摔了个大马趴。小李跟我一起跑接力赛了。小张跑长跑差点累死。对了,小王摔倒的时候,我还去扶他,他还不让我扶。”


      此处的思路略微有点跳跃太大。这时就需要我们帮同学们顺一顺。

      家长:“小王这件事很有意思。但你还没说完小王就去说小李小张,让我有点乱。咱们好好地先把小王这件事聊一聊。你再给我讲讲,他为啥摔倒,干嘛不让你扶?”

      我们为何要鼓励同学们详细尝试小王摔倒这件事呢?目的有二。一是训练有条理地表达自己。二是帮助同学们观察生活,观察生活其实就是在积累写作文的素材。

      划重点了。这就是在积累写作文的素材。

      说到这里,我岔开说一句题外话。我接触的不少同学,在小学阶段都进行过这样的一个工作:背素材。有的是背作文书上的,有的是背诵媒体上的,不一而足。

      当我们苦于千篇一律的“运动员表现了奋勇拼搏的精神”“我们班级展现了团结的风貌”这样的空话套话满天飞的作文后,在整天要求同学们写真情实感时,是不是要想一想,在要求孩子们“背素材”的时候,是不是已经将他们指向了一条并不明智的作文学习之路?

      我们不能停留在口头上要求同学们“真情实感”“我手写我心”。在他们还比较年幼,没有足够的观察能力去观察生活,观察自己的情绪情感时,我们作为老师或者家长,就应该肩负起帮助同学们去观察生活,去发掘自己的情绪情感的责任。

      这就是生活处处是语文。只看我们这些领路人,愿不愿意花心思,去进行这样的“教学”。

      没有任何一个人是喜欢完成任务的。最好的教育肯定是春风化雨。在不知不觉中,让同学们进行了一轮“综合性”的语文学习。

      语文是一门特殊的学科。它甚至超越了一门学科的属性,几乎成为我们作为人的一部分。我想,对于我们低年级同学的家长(也就是这篇文章的主要读者)来说,也许,我们可以先试着改变自己对于语文的观念,然后再潜移默化地进行语文能力的训练。(并不意味着对语文系统学习与阅读的否定,勿杠)也许,在我们的努力下,孩子自始至终都不会爱上这门学科,但至少不会妨碍他掌握必备的语文能力,并且考一个理想的分数。

      ————————————————————

      ——————第六更——————

      这次更新我们来交流一下现代文阅读的问题。我们主要来探究一下两个常见的学习问题产生的原因,一是课内语文教学与我们的现代文阅读考试到底是何关系;二是为何许多同学背诵了答题套路却仍然效果不明显。

      要做好现代文的阅读理解,特别是小说散文文体的阅读理解,一定是需要“两条腿走路”的。

      一条腿是:能够读懂理解文本。

      第二条腿是:能够掌握答题技巧和规范。

      这两条腿,代表了两种不同的能力,都需要掌握,不可偏废。

      读懂和理解文本的能力,主要的获得途径有二。

      第一种途径是课文的学习。也就是上课好好听讲。

      有同学可能会觉得学课文没有用,因为咱们考试时的阅读理解文章又不是课文。

      但阅读理解文章只是一个载体而已,并不是考察内容本身。阅读理解题目真正考察的,是以题目为载体所反映出来的对文章的理解能力。(当然也包括一定的审题和解题能力,此处暂且不表,放在后半部分。)

      这就是为啥,学霸不管考哪篇文章,都能考得不错。因为虽然文章变来变去,考察的能力却是不变的。

      而这些理解能力,主要通过咱们语文课本的学习获得。比如,比较常见的一种题型:

      19年绍兴中考阅读理解《父亲的露珠》
      请围绕“露珠”,用简洁的语言将内容补充完整。(3分)
        上苍分配露珠——(1)_____——(2)_____——(3)_____——追寻远去的露珠

      19年威海中考阅读理解《薄荷》
      阅读全文,请筛选出薄荷的特点。(3分)




      这是信息的提取、归纳与概括题。考察的就是咱们同学们能不能读懂文章、理清楚文章的主要脉络、提取出有效的信息。

      说起来有点玄乎。换句人话就是:这就是考察你是不是学会了老师上课讲的概括段落大意。

      你会概括段落大意了,那文章你基本就能分层,就能理顺作者的思路,也就能把这种信息提取概括题做得七七八八了。

      我们再来看另一个题型,赏析重点词汇题:

      一般都是这么问:加点词语有何妙处?试着赏析加点词语。或者说,加点词能不能用另外的一个词来替换?为什么?

      这考察的是咱们能否揣摩得出加点词在具体语境之下体现出来的微妙精巧的意义。

      为啥要强调具体语境呢?

      因为即便是最简单的词汇,在不同的语境之下,也会蕴含完全不同的意义。

      比如这两句话:

      你可真厉害,这么难的题目你竟然做对了!
      你可真厉害,这么简单的题目你竟然做不对。

      再看这两句:

      这个人一年只洗一次澡,是这条街上最脏的人。
      这个人偷鸡摸狗无恶不作,是这条街上最脏的人。

      “厉害”“脏”都是很简单的词汇,但在不同语境之下,表意千差万别。

      另外还有一些看起来比较相似的词汇,其实表意会相去甚远。比如:

      李雷和韩梅梅在一起做值日,李雷时不时地瞄韩梅梅几眼。
      李雷和韩梅梅在一起做值日,李雷时不时地看韩梅梅几眼。
      李雷和韩梅梅在一起做值日,李雷时不时地盯韩梅梅几眼。

      “瞄”“看”“盯”三个词表意看起来差不多。但放在具体的语境里,却相去甚远。李雷“瞄”韩梅梅,这个动作不是明目张胆的,而且是迅速地,装作若无其事的。这里的李雷,怕被韩梅梅发现自己在偷看,有点儿羞赧。

      李雷“看”韩梅梅,就大方多了,正大光明的,不害羞,大方磊落。

      “盯”,这样看女孩子的男孩,几乎有点儿呆了。

      这三个字,看起来是近义词,其实到了具体语境下,表意相去万里。

      这种对于词汇具体语境下表意的(包括隐含的作者的态度情感)的把握,其实就是咱们课内讲课文时老师的“咬文嚼字”。所以下次语文老师非要跟你讨论“捏着钢叉”为什么就是比“握着钢叉”好时,千万不要以为老师在“无聊”,在“做无用功”。

      看似无用的讲课文,其实真正地有大用。

      只是由于我们没有充分地察觉到或者说建立起“上课讲的内容”与“考试考的内容”之间的关联而已。

      以上就是第一条腿走路的主要方法。当然,认真听课只是基础,能保证你会走。想要走好走快、甚至想能跑会跳,则需要课外的大量泛读和有效精读。

      我们再来说审题技巧和答题技巧上可以做哪些工作。

      对标参考答案找差距、总结答题术语和规范、甚至包括背诵一定的答题模板,都是常规性的技巧。人人皆知。

      并非人人皆知的是,在做这些事情之前,其实我们还要去做一项工作。就是搞清楚在阅读理解题型中经常出现的一些学科概念。

      比如,我们以这个题型为例:

      请赏析一下划线句子:
      蘑菇屯村民们对我夹道欢迎,锣鼓喧天、鞭炮齐鸣,我的耳朵都要聋了。

      这类赏析句子题,一般就是从描写或者修辞的角度来答。如果是从修辞角度来回答,教辅资料总结的答题的话术一般为:这个句子用了……的修辞手法,心想生动地写出了……的特点,起到了……的效果,表达了作者……的感情。

      背诵这个答题话术其实一点也不难。但即便你背过了这个“公式”却仍然无法拿满分。

      因为,极有可能判断不出来这到底是一个什么修辞。

      如何才能判断正确?

      那这就涉及到,我们必须把常用的修辞手法(拟人 比喻 排比 设问 反复 夸张)到底是什么先搞清楚,不仅搞清楚,还得记清常见的修辞手法有哪一些。这样再做这种题时,如果不是那种简单到一眼就能判断出的修辞类型(排比、拟人、比喻相对来说非常容易识别),你完全可以如做选择题一般,拿着需要你赏析的句子,去你的脑子中挨着比对这几项主要的修辞手法。如同做选择题一般。这样判断出来的概率就非常高了。

      判断出修辞手法,这个题目还有可能会被扣分。扣分的点就在于“分析”。也就是分析这个句子到底是如何使用该修辞手法的。这是这个题目真正的难点所在,也是拉分的地方所在。也是语文真的好的同学与语文一般的同学的区分度所在。

      而想要写好这个修辞手法到底是如何使用的,而不是说来说去就是“形象生动”这四个字,这就主要依赖于第一条腿培养起来的感悟力,当然也必须辅之以对大量的参考答案的模仿与总结。

      以上说的是答题技巧上要做的工作,在审题技巧上,一样的。

      比如,我们来看如下的几个问题:

      请分析划线句子在文章中的作用。
      请赏析划线句子。
      请简要回答划线句子的表达效果。
      划线句子有什么作用?
      请理解一下划线句子的含义。



      这几个问题是不是都特别常见呢?

      头大了吗?晕了吗?

      如果你都分不清楚这些问题,被再多的答题话术有什么用?

      还不是照样对不上号?

      出题人稍微一变化提问方法,就觉得自己遇到了一个新题型,然后一发下试卷来,就拍着大腿悔恨:竟然是从这个角度答!我明明就会的!

      这就是为何,在各种教辅资料都已经把答题话术总觉得比较到位的情况下,语文阅读理解仍然做不到“开卷考试”,甚至总有一部分同学感觉自己做了很多题、背了很多套路却仍然“睁眼瞎”的原因。

      ——————————

      公众号:一吨语文

      更多语文学习干货

      本文将持续更新,有兴趣看到新增内容的知友请点亮追更吧。

      Viewing all 11848 articles
      Browse latest View live


      <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>