├── ._.DS_Store ├── ._Flutter零基础介绍.md ├── .gitignore ├── 1522049243596.png ├── 1522049541278.png ├── 2019前端性能优化.md ├── 2019面试.md ├── Flutter零基础介绍.md ├── Futter插件管理之Pigeon.md ├── Vue+TypeScript爬坑指南1.md ├── Vue+ts下的vw适配(第三方库css问题).md ├── WebComponents.md ├── acme.sh配置ssl https.md ├── deploy.sh ├── docs ├── .vuepress │ ├── config.js │ └── public │ │ └── dog.png ├── assets │ └── dog.png ├── flutter │ ├── Flutter FPS 监控.md │ ├── Flutter零基础介绍.md │ ├── Futter插件管理之Pigeon.md │ ├── 使用Nodejs和Puppeteer从HTML中导出PDF.md │ └── 本地编译FlutterEngine.md ├── img │ ├── 1522049243596.png │ ├── 1522049541278.png │ ├── flutter │ │ ├── 11111.gif │ │ ├── 1598105373710.jpg │ │ ├── 219ADF42-E06A-4EA5-BDC0-AE5A73CD369A.png │ │ ├── 22222.gif │ │ ├── 33333.gif │ │ ├── 392CF20F-2606-4966-92DE-6ECFC4065E2E.png │ │ ├── 3CB87568-7C57-4848-98E5-46E5CE62C2F8.png │ │ ├── 4274FBD2-1264-4B07-AFAE-F3125A1D3565.png │ │ ├── 44444.gif │ │ ├── 55555.gif │ │ ├── 66666.gif │ │ ├── 6F8E5930-4F87-4BBE-8F63-DA832C77E882.jpg │ │ ├── 77777.gif │ │ ├── 7914A8AF-5CC3-47B2-A2EF-CFFBC015A330.png │ │ ├── 91DB3A31-5812-478F-9133-14D67CA44BC3.jpg │ │ ├── 95058CE5-F44C-4839-89CD-CE1E85D3C619.jpg │ │ ├── AB694FB0-F007-48B0-A286-562E8B8FCBA2.jpg │ │ ├── CE226313-D816-45CF-B457-994B66283A44.png │ │ ├── E5C248B6-4945-44BC-A709-1DD82D49FAAF.png │ │ ├── F0B8566F-6D80-4B7A-939E-EC030527187C.jpg │ │ ├── F4BD2112-146E-4E74-B9B5-FCADFD3E2984.png │ │ ├── F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif │ │ ├── FC16404E-0CF9-4274-9032-691C5F99FBB2.jpg │ │ └── vue2.png │ ├── flutterPigeon │ │ ├── pigeon.jpg │ │ ├── pigeon1.png │ │ ├── pigeon2.png │ │ ├── pigeon3.png │ │ └── pigeon4.png │ ├── lifecycle.png │ ├── new-vue.png │ └── reactive.png ├── interview │ ├── 2019前端性能优化.md │ ├── 2019面试.md │ └── 手写js.md ├── js │ ├── js代码规范.md │ ├── useHooks.md │ └── 使用Nodejs和Puppeteer从HTML中导出PDF.md ├── other │ ├── WebComponents.md │ └── acme.sh配置ssl https.md ├── readme.md └── vue │ ├── Vue+TypeScript爬坑指南.md │ ├── Vue+ts下的vw适配(第三方库css问题).md │ ├── vue+半动化多页skeleton.md │ ├── 响应式原理.md │ ├── 数据驱动.md │ ├── 组件化.md │ └── 编译.md ├── event_loop ├── 1531538160931.png ├── 1531538317857.png ├── 1531541696514.png ├── 1531541718526.png ├── 1531541742410.png ├── 1531541758392.png ├── 1531541777200.png ├── 1531541780456.png ├── 1531541796985.png ├── 1531541834771.png ├── 1531541854818.png └── Eventloop学习.md ├── flutter ├── Flutter FPS 监控.md └── 本地编译FlutterEngine.md ├── img ├── ._.DS_Store ├── 1522049243596.png ├── 1522049541278.png ├── flutter │ ├── ._1598105373710.jpg │ ├── ._F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif │ ├── 11111.gif │ ├── 1598105373710.jpg │ ├── 219ADF42-E06A-4EA5-BDC0-AE5A73CD369A.png │ ├── 22222.gif │ ├── 33333.gif │ ├── 392CF20F-2606-4966-92DE-6ECFC4065E2E.png │ ├── 3CB87568-7C57-4848-98E5-46E5CE62C2F8.png │ ├── 4274FBD2-1264-4B07-AFAE-F3125A1D3565.png │ ├── 44444.gif │ ├── 55555.gif │ ├── 66666.gif │ ├── 6F8E5930-4F87-4BBE-8F63-DA832C77E882.jpg │ ├── 77777.gif │ ├── 7914A8AF-5CC3-47B2-A2EF-CFFBC015A330.png │ ├── 91DB3A31-5812-478F-9133-14D67CA44BC3.jpg │ ├── 95058CE5-F44C-4839-89CD-CE1E85D3C619.jpg │ ├── AB694FB0-F007-48B0-A286-562E8B8FCBA2.jpg │ ├── CE226313-D816-45CF-B457-994B66283A44.png │ ├── E5C248B6-4945-44BC-A709-1DD82D49FAAF.png │ ├── F0B8566F-6D80-4B7A-939E-EC030527187C.jpg │ ├── F4BD2112-146E-4E74-B9B5-FCADFD3E2984.png │ ├── F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif │ ├── FC16404E-0CF9-4274-9032-691C5F99FBB2.jpg │ └── vue2.png ├── flutterPigeon │ ├── pigeon.jpg │ ├── pigeon1.png │ ├── pigeon2.png │ ├── pigeon3.png │ └── pigeon4.png ├── lifecycle.png ├── new-vue.png └── reactive.png ├── js代码规范.md ├── package.json ├── readme.md ├── useHooks.md ├── vue+半动化多页skeleton.md ├── yarn.lock ├── 使用Nodejs和Puppeteer从HTML中导出PDF.md ├── 响应式原理.md ├── 手写js.md ├── 数据驱动.md ├── 组件化.md ├── 编译.md ├── 网页加载过程及优化.md └── 面向对象.md /._.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/._.DS_Store -------------------------------------------------------------------------------- /._Flutter零基础介绍.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/._Flutter零基础介绍.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /1522049243596.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/1522049243596.png -------------------------------------------------------------------------------- /1522049541278.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/1522049541278.png -------------------------------------------------------------------------------- /2019前端性能优化.md: -------------------------------------------------------------------------------- 1 | ### 2019前端性能优化 一 2 | --- 3 | - 起步:计划与指标 4 | 1.建立性能评估规范 5 | 2.目标:比你的对手快至少20%] 6 | 3.选择合适的指标 7 | 4.在目标用户的典型设备上收集数据 8 | 5.为测试设立“纯净”、“接近真实用户”的浏览器配置 9 | 6.与团队其他成员分享这份清单 10 | 11 | #### 1.建立性能评估规范 12 | 需要开发、设计与业务、市场团队的共同合作,从而确定页面速度会如何影响业务指标和大家多关心的KPI。 13 | 接着需要确定性能优化的指标,研究用户抱怨的常见问题,再看如何通过性能优化,接近这些问题。 14 | 15 | #### 2.目标:比你的对手快至少20% 16 | 如果你想比你的竞争对手快,至少要快20%。 17 | 你可以通过Chrome UX Report来了解对手的性能,并为具体的页面设置性能目标。 18 | 还有一点非常重要,那就是规划好加载的顺序和取舍。如果你预先规划好哪些部分更重要,并确定每部分的出现顺序,那么同事你也会知道哪些部分可以延迟加载。 19 | 20 | #### 3.选择合适的指标 21 | 这里有一些指标,首次绘制(First Paint)、首次有内容绘制(First Contentful Paint)、首次有意义绘制(First Meaning Paint)、视觉完备(Visual Complete)、首次可交互时间(Time To interactive)(ps:[区别](https://docs.google.com/presentation/d/1D4foHkE0VQdhcA5_hiesl8JhEGeTDRrQR4gipfJ8z7Y/present?slide=id.g21f3ab9dd6_0_33)) 22 | 通常情况被选用的,相对重要的指标有 23 | - 首次有效绘制 First Meaning Paint **FMP** 24 | - 首次可交互时间 Time to interactive **TII** 25 | - 首次输入延迟 First Input Delay **FID** 26 | - 速度指数 Speed Index 27 | - CPU耗时 28 | - 广告的影响 Ad Weight Impact 29 | - 偏离度指标 Deviation metrics 30 | - 自定义指标 Custom metrics 31 | 32 | #### 4.在目标用户的典型设备上收集数据 33 | 移动设备上的解析时间通常要比桌面设备长 36%。因此,一定要在一部平均水准的设备上进行测试 — 一部你的用户中最具代表性的设备。 34 | - 集成测试工具可以在预先规定了设备和网络配置的可复制环境中手机实验数据。例如 Lighthouse、WebPageTest 35 | - 真是用户监测(RUM)工具可以持续评估用户交互,收集实际数据。例如,SpeedCurve、New Relic,两者也都提供集成测试工具。 36 | 37 | ####5.为测试设立“纯净”、“接近真实用户”的浏览器配置(Profile) 38 | 使用被动测试监控工具时,可以关闭反病毒软件和CPU后台任务,关闭后台网络连接,使用没有安装任何插件的“干净的”浏览器配置,以避免结果失真。 39 | 当然了解你的用户会使用哪些插件也是个不错的主意。 40 | 41 | ####6.与团队其他成员共享这份清单 42 | 使每个人熟悉,从而根据性能预算和清单中定义的优先级来制定设计决策。 -------------------------------------------------------------------------------- /2019面试.md: -------------------------------------------------------------------------------- 1 | 金三银四的季节又来了,笔者一直在武汉工作,年前裸辞后,过完年一共面了接近两个星期,有阿里,腾讯,金山,万科,趣头条等公司,在武汉拿到了4个offer(金山、万科、航班管家、财人汇)后,又在深圳一站式面了腾讯,最终选择了腾讯,这里不完全记录一点面试中遇到的问题 2 | 3 | ### 面试不完全汇总 4 | 5 | float和flex的区别 6 | 为什么会有flex 7 | 了解CI CD吗 8 | React和Vue的差异 9 | 移动端高清问题 10 | 移动端点击穿透问题 11 | Nodejs的模块化和ES6的模块化区别 12 | http的优缺点 13 | restful和graphQL优缺点,对比 14 | 事件委托的优缺点 15 | 何时需要用到border-box 16 | csrf是什么,如何用预防 17 | fastclik的作用 18 | 前端性能优化 19 | CSS居中+等比宽高问题 20 | margin-top/margin-bottom百分数问题 21 | TCP和UDP区别 22 | 23 | 24 | #### float和flex的区别 25 | **float** 26 | 脱离文档流 27 | 布局的传统解决方案,基于盒状模型,依赖 display 属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。 28 | 缺点: 29 | - 容错性差,耦合度高,其中一个浮动元素出现问题,其他也会受影响 30 | - 必须要有固定的布局 31 | - 低版本IE不兼容 32 | 33 | **flex** 34 | W3C 提出了一种新的方案----Flex 布局,可以简便、完整、响应式地实现各种页面布局 35 | flex性能改进在于, 36 | - 相比传统margin,可伸缩性强(不算性能) 37 | - 相比float,关联性重绘好很多 38 | - 相比tranform,子元素和父元素样式控制操作更少 39 | 40 | RESTful优缺点 41 | 特点:充分利用 HTTP 协议本身语义。 42 | 无状态,这点非常重要。在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了复杂度。 43 | HTTP 本身提供了丰富的内容协商手段,无论是缓存,还是资源修改的乐观并发控制,都可以以业务无关的中间件来实现。 44 | 45 | #### React和Vue的差异 46 | 相似处 47 | Virtual DOM、组件化、Props 48 | 差异 49 | 1. 渲染优化,是否需要用shouldComponentUpdate,PureComponent 50 | 2. JSX 和 Template , CSS in JS 51 | 3. Vue-cli 和 create-react-app 52 | 4. 学习难度,学 React 前,你需要知道 JSX 和 ES2015 53 | 54 | #### 移动端高清问题 55 | - DPR (device pixel ratio)= device pixel(设备像素) \ CSS pixel(css像素) 56 | - 1px解决方案 57 | > - border-image 、 background-image 缺点:修改颜色麻烦,圆角需要特殊处理 58 | > - box-shadow 缺点:在Safari下不支持小数设置 59 | > - 伪类 + transform 60 | > - SVG 61 | > - viewport + rem 62 | 63 | #### 移动端点击穿透问题 64 | 场景:上层元素A绑定了tap事件,下层元素B绑定了click事件。当我们点击A时,A元素隐藏,此时也会触发下层B元素的click事件。 65 | 原因就是当上层A的tap事件发生后,其实是touchend结束后,会触发click事件,因为这几个事件的触发顺序是touchstart-touchmove-touchend-click。因此当click事件触发300ms后,上面的A元素已经消失,此时真正点击的就相当于下层的B元素,因此会发生点击穿透事件。 66 | **解决方案** 67 | - 都使用click或者tap 68 | - 在click事件触发前阻止,在touchend事件中使用e.preventDefault() 69 | - 使用fast-click。fastclick是一个专门解决300ms点击延迟的库。 70 | 71 | **FastClick实现原理** 72 | 在document.body绑定touchstart和touchend事件 73 | `touchstart` 74 | 用于记录当前点击的元素的targetElement 75 | `touchend` 76 | - 阻止默认事件(屏蔽之后的click事件) 77 | - 合成click事件,并添加可跟踪属性forwardedTouchEvent 78 | - 在targetElement上触发click事件 79 | - targetElement上绑定的事件立即执行,完成FastClick 80 | 81 | #### CommonJS的模块化和ES6的模块化区别 82 | CommonJS主要依赖module、exports、require、global这几个环境变量。 83 | CommonJS用同步方式加载模块。 84 | ES6 module的模块不是对象,`import`命令会被JS引擎动态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。(ps: 也正是因为这个,使得静态分析成为可能) 85 | **差异** 86 | 1. CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用 87 | - CommonJS模块输出值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。 88 | - ES6模块的运行机制与CommonJS不一样。JS引擎对脚本静态分析的时候,遇到模块加载命令`import`,就会生产一个只读引用。等到脚本真正执行时,再根据只读引用,到被加载的模块中取值。原始值变了,`import`加载的值也会跟着变。因此ES6模块是动态引用,并且不会缓存值,模块里的变量绑定其所在的模块。 89 | 2. CommonJS模块是运行时加载,ES6模块是编译时输出接口 90 | - 运行时加载:CommonJS模块就是对象,即在输入时先加载整个模块,生产一个对象,然后再从这个对象上面读取方法,这种加载成为”运行时加载“ 91 | - 编译时加载:ES6模块不是对象,而是通过`export`命令显示指定输出的代码,`import`时采用静态命令的形式。即在`import`时可以指定加载某个输出值,而不是加载整个模块,这种加载成为**编译时加载** 92 | CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 93 | 94 | #### RESTful优缺点 95 | **优点:** 96 | 1. 轻量,直接基于http,不需要任何别的消息协议。get/post/put/delete为CURD操作 97 | 2. 面向资源,一目了然,具有自解释性 98 | 3. 数据描述简单,一般是xml,json做数据交换 99 | 100 | **缺点:** 101 | 一个适用于简单操作的接口规范,复杂操作并不适用,适合CRUD并且只适合CRUD,有的浏览器可能不支持POST、GET以外的操作,要特殊处理。 102 | ps: 多个接口,更新订单收款状态、更新订单支付状态、更新订单结算状态。批量操作,get超出长度 103 | 104 | 1. 客户端-服务器:提供服务的服务器和使用服务的客户端分离解耦 105 | > - 提高客户端的便捷性(操作简单) 106 | > - 简化服务器提高可伸缩性(高性能、低成本) 107 | > - 允许客户端服务端分组优化不受影响 108 | 109 | 2. 无状态:来自客户的每一个请求必须包含服务器处理该请求所需的所有信息(请求信息唯一性) 110 | > - 提高了可见性(可以考虑每个请求) 111 | > - 提高了可靠性(更容易故障恢复) 112 | > - 提高了可扩展性(降低了服务器资源使用) 113 | 114 | 3. 可缓存:服务器必须让客户端知道请求是否可以被缓存?如果可以,客户端可以重用之前的请求信息发送请求 115 | > - 减少交互连接数 116 | > - 减少链接过程的网络延迟 117 | 118 | 4. 分层系统:允许服务器和客户端之前的中间层(代理,网关等)代替服务器对客户端的请求进行回应,而客户端不需要关心与它交互的组件之外的事情 119 | > - 提高了系统的可扩展性 120 | > - 简化了系统的复杂性 121 | 122 | 5. 统一接口:客户和服务器之前通信的方法必须是统一的。 123 | > 提高交互的可见性 124 | > 鼓励单独优化改善组件 125 | 126 | 6. 支持按需代码:服务器可以提供一些代码或者脚本并在客户的运行环境中执行 127 | > - 提高了可扩展性 128 | 129 | #### 事件委托的优缺点 130 | 优点: 131 | 1. 节省内存,减少事件注册。 132 | 2. 适合动态内容 133 | 134 | 确定: 135 | 1. 如果把所有事件都是用事件代理,可能会出现误判。即本不该被触发的事件被绑定上了事件。 136 | 2. 基于冒泡,对于不冒泡的事件不支持。 137 | 3. 层次过多时,可能会被某曾组织掉。 138 | 139 | #### CSRF 140 | 特点 141 | - CSRF通常发生在第三方域名 142 | - CSRF攻击者不能获取到Cookie等信息,只是使用。 143 | 144 | 防御策略 145 | - 阻止不明外域的访问 146 | - 同源监测 147 | - Samesite cookie 148 | 149 | - 提交时要求附加本域下才能获取的信息 150 | - CSRF TOken 151 | - 双重Cookie验证 152 | - 提交验证码 153 | 154 | #### 为什么用transform居中而不是marginLeft/Top 155 | 浏览器渲染过程 156 | > 解析DOM Tree,创建一个或多个渲染层(layer) 157 | > 将每个层独立的绘进位图中(计算样式->布局->栅格化) 158 | > 将层作为纹理(texture)上传至GPU 159 | > 复合(composite)多个层来生成最终的屏幕图像 160 | > 每个层的样式出现调整后,要重新计算样式->重新布局->重新栅格化->重新组合 161 | 162 | 使用top/left只会创建一个层,而是用transform会使浏览器将元素单独提取放在GPU单独的渲染层中,这样有**三点好处**: 163 | 1. 该元素任何合成属性(Composite Property)的变化将不会影响原有文档,不会导致文档被重新布局(重绘、回流) 164 | 2. 该层将由GPU负责渲染,从而节省CPU资源,不会阻塞主线程JS代码的执行 165 | 3. 动画更为平顺,这是因为使用transform可以小于像素的单位进行绘制 166 | 可能带来的负作用是额外的渲染层导致**更多的线程间通信**,如果过度使用,导致生成成百上千的渲染层,那反而会导致**组合各层图像的成本**迅速上升成为主要矛盾,且我们需要记住GPU也是有内存限制的。 167 | 168 | #### 使用css设置rem 169 | 使用calc(100vw / 屏幕宽度) 170 | 171 | ```javascript 172 | [] == ![] 这个要牵涉到 JavaScript 中不同类型 == 比较的规则, 具体是由相关标准定义的. ![] 的值是 false, 此时表达式变为 [] == false, 参照标准, 该比较变成了 [] == ToNumber(false), 即 [] == 0. 这个时候又变成了 ToPrimitive([]) == 0, 即 '' == 0, 接下来就是比较 ToNumber('') == 0, 也就是 0 == 0, 最终结果为 true. 173 | ``` 174 | 175 | 176 | 177 | ### CSS居中 + 宽高等比变化 178 | --- 179 | 1. font-size em-square 180 | 2. line-height 实际占地高度 181 | 3. 100px 100px -> 103px 182 | 4. vertical top middle bottom text-top text-bottom 183 | 5. 图片下面有空隙 184 | 1. vertical-align top 185 | 2. img{display: block} 186 | 3. font-size 0 傻逼才用 187 | 6. inline-block 元素对不齐 无解 用 flex或float 188 | 7. inline-block 有空隙 用 flex 或 float 189 | 190 | ##### css垂直居中对齐 191 | 192 |
193 | Center me, please! 194 |
195 | 1. display: flex + margin: auto 196 | ```css 197 | main{ 198 | width: 100%; 199 | min-height: 152px; 200 | display: flex; 201 | } 202 | main > span { 203 | margin: auto; 204 | } 205 | ``` 206 | 2. display: flex , grid 207 | ```css 208 | main{ 209 | width: 100%; 210 | min-height: 152px; 211 | display: grid; 212 | justify-content: center; 213 | align-items: center; 214 | } 215 | main > span { 216 | background: #b4a078; 217 | color: white; 218 | padding: .3em 1em .5em; 219 | border-radius: 3px; 220 | box-shadow: 0 0 .5em #b4a078; 221 | } 222 | ``` 223 | 3. position: absolute + calc() 224 | ```css 225 | main{ 226 | width: 100%; 227 | min-height: 152px; 228 | display: flex; 229 | } 230 | main > span { 231 | position: absolute; 232 | top: calc(50% - 16px); 233 | left: calc(50% - 72px); 234 | } 235 | ``` 236 | 4. position: absolute + translate 237 | ```css 238 | main{ 239 | width: 100%; 240 | min-height: 152px; 241 | display: flex; 242 | } 243 | main > span { 244 | position: absolute; 245 | top: 50%; left: 50%; 246 | transform: translate(-50%, -50%); 247 | } 248 | ``` 249 | 5. display: table / table-cell + vertical-align: middle 250 | ```css 251 | main { 252 | width: 100%; 253 | height: 152px; 254 | display: table; 255 | } 256 | main > div { 257 | display: table-cell; 258 | text-align: center; 259 | vertical-align: middle; 260 | } 261 | ``` 262 | 6. 伪元素 :after + vertical-align: middle 263 | ```css 264 | main { 265 | width: 100%; 266 | height: 152px; 267 | text-align: center; 268 | } 269 | main::after { 270 | content:''; 271 | display: inline-block; 272 | height: 100%; 273 | vertical-align: middle; 274 | } 275 | ``` 276 | 277 | #### 宽高等比用css如何实现 278 | 1. 使用隐藏图片 279 | 完美兼容PC端、移动端 280 | 如果div容器不给高度,它的高度会随容器内部元素的变化而撑大 281 | 在容器内添加一张等比宽高的图片,给图片设置宽度100% 高度auto 282 | 283 | ```javascript 284 | 296 |
297 |
298 | 299 |
300 |
301 | ``` 302 | 还可以用base64优化 303 | 304 | 2. 使用vmin 305 | 将父容器的宽度和高度定义为相同vmin,这样父容器宽高就是相同值,然后给子容器宽高设置百分比 306 | - vw 相对于视窗的宽度 307 | - vh 相对于视窗的高度 308 | - vmin 相对于视口的宽度或高度中较小的那个被均分为100单位的vmin 309 | - vmax 相对于视口的宽度或高度中较大的那个被均分为100单位的vmax 310 | 311 | 312 | ```css 313 | #container{ 314 | width: 100vmin; 315 | height: 100vmin; 316 | } 317 | 318 | .attr { 319 | width: 50%; 320 | height: 50%; 321 | background-color: orange; 322 | } 323 | ``` 324 | 325 | 3. 使用scale 326 | 327 | 328 | ```css 329 | .attr{ 330 | width:50%; 331 | height: calc(50%); 332 | } 333 | ``` 334 | 335 | 4. padding-top/padding-bottom 实现 336 | 因为padding-bottom的属性值百分比是按照父容器的宽度来计算。 337 | 338 | ```css 339 | 352 | 353 |
354 |
355 |
356 | ``` 357 | 358 | #### 为什么margin-top/margin-bottom的百分数也是相对于width而不是height呢? 359 | CSS权威指南中的解释: 360 | 361 | 我们认为,正常流中的大多数元素都会足够高以包含其后代元素(包括外边距),如果一个元素的上下外边距时父元素的height的百分数,就可能导致一个无限循环,父元素的height会增加,以适应后代元素上下外边距的增加,而相应的,上下外边距因为父元素height的增加也会增加,如果循环。 362 | 363 | 364 | 365 | ###UDP和TCP 366 | #### UDP 367 | 简介:UDP是面向无连接的,UDP只是数据报文的搬运工,不保证有序且不丢失的传递,UDP协议没有控制流量算法,相比TCP更加轻量 368 | 369 | #### 面向无连接 370 | UDP不需要和TCP一样在发送数据前进行三次握手建立连接。 371 | 并且只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接。 372 | 具体来说: 373 | - 在发送端,应用层的数据 -> 传输层UDP协议,UDP只会给数据增加一个UDP头标识 374 | - 在接收端,网络层将数据传给传输层,UDP也只去除IP报文头,就传给应用层 375 | 376 | #### 不可靠性 377 | 首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。 378 | 网络不佳的情况下,UDP因为没有拥塞控制,一直会以恒定的速度发送数据。**弊端**就是网络不好会丢包,**优点**就是在某些时效性要求高的场景适合。 379 | 380 | #### 高效 381 | UDP的头部开销小,只有八个字节(TCP至少二十字节) 382 | UDP头部包含以下几个数据: 383 | - 两个十六位端口号,分别为源端口和目标端口 384 | - 整个数据报文的长度 385 | - 整个数据报文的检验 386 | 387 | #### 传输方式 388 | UDP支持一对一,一对多,多对多,多对一,也就是单播,多播,广播 389 | 390 | #### 适用场景 391 | - 直播 392 | - 游戏 393 | 394 | ### TCP 395 | 建立和断开连接都要进行握手。在传输过程中,通过各种算法保证数据的可靠性。 396 | 相对UDP来说不那么的高效。 397 | 398 | #### 头部 399 | 400 | - Sequence number,这个序号保证了TCP传输的报文是有序的,对端可以通过序号顺序拼接报文 401 | - Acknowledgement number,这个序号表示数据接收端 期望接受的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到 402 | - Window Size,窗口大小,表示还能接受多少字节的数据,用于流量控制 403 | - 标识符 404 | 405 | #### 状态机 406 | 407 | - 建立连接三次握手 408 | 不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 是一个全双工的协议。 409 | 410 | ##### 第一次握手 411 | 客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。 412 | 413 | ##### 第二次握手 414 | 服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序列号,发送完成后便进入SYN-RECEIVED状态。 415 | 416 | ##### 第三次握手 417 | 当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。 418 | 客户端发完这个报文段便进入ESTABLISHED状态。 419 | 服务端发完这个应答后也会进入ESTABLISHED状态。 420 | 421 | ps: 为什么 TCP 建立连接需要三次握手,明明两次就可以建立起连接 422 | 因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。 423 | 424 | #### 断开连接四次握手 425 | - 第一次握手 426 | 若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。 427 | - 第二次握手 428 | B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。 429 | - 第三次握手 430 | B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。 431 | - 第四次握手 432 | A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。 433 | ps: **2MSL** 为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。 434 | 435 | #### ARQ协议 436 | ARQ协议就是超时重传机制。通过确认和超时机制保证数据的正确送达。 437 | ARQ协议包含**停止等待ARQ**和**连续ARQ**两种协议。 -------------------------------------------------------------------------------- /Flutter零基础介绍.md: -------------------------------------------------------------------------------- 1 | #### Flutter介绍 2 | --- 3 | 4 | > Flutter 框架是当下非常热门的跨端解决方案,能够帮助开发者通过一套代码库高效构建多平台精美应用,支持移动、Web、桌面等多端开发。但仍然有很多产品、设计、甚至开发同学并不了解 Flutter,所以本文将深入浅出和大家聊聊 Flutter 的设计背景、技术特点,以及与其他同类技术之间的对比,希望与大家一同交流。 5 | 6 | #### 跨平台背景 7 | 8 | **移动互联网的重要性** 9 | 10 | ![](./img/flutter/3CB87568-7C57-4848-98E5-46E5CE62C2F8.png) 11 | 12 | - 与2019年1月相比,全球使用互联网的人数已增加到45.4亿,增长了7%(2.98亿新用户)。 13 | - 到2020年1月,全球有38亿 社交媒体 用户,与去年同期相比,这个数字增长了9%以上(3.21亿新用户)。 14 | - 在全球范围内,现在有超过51.9亿人使用手机,在过去的一年中,用户数量增加了1.24亿(2.4%)。 15 | 16 | 现在,普通的互联网用户每天在线花费6个小时43分钟。这比去年同期减少了3分钟,但仍然相当于每个互联网用户每年连接时间超过100天。如果我们每天允许大约8个小时的睡眠,那意味着我们目前的醒来时间中,有40%以上是通过互联网度过的。 17 | 18 | ![](./img/flutter/F0B8566F-6D80-4B7A-939E-EC030527187C.jpg) 19 | 20 | 在移动互联网的浪潮下,开发效率和使用体验可以说是同等重要。但是,使用原生的方式来开发 App,就要求我们必须针对 **iOS** 和 **Android** 这两个平台分别开发。 21 | 因为这样的话,我们不仅需要在不同的项目间尝试用不同的语言去实现同样的功能,还要承担由此带来的维护任务。如果还要继续向其他平台(比如 Web、Mac 或 Windows)拓展的话,我们需要**付出的成本和时间将成倍增长**。而这,显然是难以接受的。于是,跨平台开发的概念顺势走进了我们的视野。 22 | 所以从本质上讲,`跨平台开发是为了增加业务代码的复用率,减少因为要适配多个平台带来的工作量,从而降低开发成本`。 23 | 24 | #### 跨平台开发方案的三个时代 25 | 26 | 根据实现方式的不同,业内常见的观点是将主流的跨平台方案划分为三个时代。 27 | - Web 容器时代:基于 Web 相关技术通过浏览器组件来实现界面及功能,典型的框架包括 Cordova(PhoneGap)、Ionic 和微信小程序。 28 | - 泛 Web 容器时代:采用类 Web 标准进行开发,但在运行时把绘制和渲染交由原生系统接管的技术,代表框架有 React Native、Weex 和快应用,广义的还包括天猫的 Virtual View 等。 29 | - 自绘引擎时代:自带渲染引擎,客户端仅提供一块画布即可获得从业务逻辑到功能呈现的多端高度一致的渲染体验。Flutter,是为数不多的代表。 30 | 31 | ##### web容器时代 32 | ![](./img/flutter/4274FBD2-1264-4B07-AFAE-F3125A1D3565.png) 33 | 34 | > Web 时代的方案,主要采用的是原生应用内嵌浏览器控件 WebView的方式进行 HTML5 页面渲染。 35 | > 由于采用了 Web 开发技术,社区和资源非常丰富,开发效率也很高。 36 | 37 | 但,**一个完整 HTML5 页面的展示要经历浏览器控件的加载、解析和渲染三大过程,性能消耗要比原生开发增加 N 个数量级**。 38 | 39 | ##### 泛 Web 容器时代 40 | 41 | ![](./img/flutter/AB694FB0-F007-48B0-A286-562E8B8FCBA2.jpg) 42 | 43 | > 泛 Web 容器时代的解决方案优化了 Web 容器时代的加载、解析和渲染这三大过程,把影响它们独立运行的 Web 标准进行了裁剪,以相对简单的方式支持了构建移动端页面必要的 Web 标准(如 Flexbox 等),也保证了便捷的前端开发体验;同时,采用原生自带的 UI 组件实现代替了核心的渲染引擎,仅保持必要的基本控件渲染能力,从而使得渲染过程更加简化,也保证了良好的渲染性能。 44 | 45 | ![](./img/flutter/95058CE5-F44C-4839-89CD-CE1E85D3C619.jpg) 46 | 47 | > 也就是说,在泛 Web 容器时代,我们仍然采用前端友好的 JavaScript 进行开发,整体加载、渲染机制大大简化,并且由原生接管绘制,即将原生系统作为渲染的后端,为依托于 JavaScript 虚拟机的 JavaScript 代码提供所需要的 UI 控件的实体。这,也是现在绝大部分跨平台框架的思路,而 React Native 和 Weex 就是其中的佼佼者。 48 | 49 | 50 | **总结起来其实就是利用 JS 来调用 Native 端的组件,从而实现相应的功能。** 51 | 52 | ##### 自绘引擎时代 53 | > 这一时期的代表 Flutter 则开辟了一种全新的思路,即从头到尾重写一套跨平台的 UI 框架,包括渲染逻辑,甚至是开发语言。 54 | > 1. 渲染引擎依靠跨平台的 `Skia 图形库`来实现,Skia 引擎会将使用 Dart 构建的抽象的视图结构数据加工成 GPU 数据,交由 OpenGL 最终提供给 GPU 渲染,至此完成渲染闭环,因此可以在最大程度上保证一款应用在不同平台、不同设备上的体验一致性。 55 | > 2. 而开发语言选用的是同时支持 JIT(Just-in-Time,即时编译)和 AOT(Ahead-of-Time,预编译)的 Dart,不仅保证了开发效率,更提升了执行效率(比使用 JavaScript 开发的泛 Web 容器方案要高得多) 56 | 57 | `Flutter 是什么?它出现的动机是什么,解决了哪些痛点?相比其他跨平台技术,Flutter 的优势在哪里?` 58 | 59 | --- 60 | 61 | ##### Flutter 出现的历史背景 62 | 63 | 为不同的操作系统开发拥有相同功能的应用程序,开发人员只有两个选择: 64 | 1. 使用原生开发语言(即 Java 和 Objective-C),针对不同平台分别进行开发。 65 | 2. 使用跨平台解决方案,对不同平台进行统一开发。 66 | 67 | 原生开发方式的体验最好,但研发效率和研发成本相对较高;而跨平台开发方式研发虽然效率高,但为了抹平多端平台差异,各类解决方案暴露的组件和 API 较原生开发相比少很多,因此研发体验和产品功能并不完美。 68 | 69 | 所以,最成功的跨平台开发方案其实是依托于浏览器控件的 Web。浏览器保证了 99% 的概率下 Web 的需求都是可以实现的,不需要业务将就“技术”。不过,Web 最大的问题在于它的性能和体验与原生开发存在肉眼可感知的差异,因此并不适用于对体验要求较高的场景。 70 | 71 | 对于用户体验更接近于原生的 React Native,对业务的支持能力却还不到浏览器的 5%,仅适用于中低复杂度的低交互类页面。面对稍微复杂一点儿的交互和动画需求,开发者都需要 case by case 地去 review,甚至还可能要通过原生代码去扩展才能实现。 72 | 73 | **带着这些问题,我们终于迎来了本次的主角——Flutter。** 74 | 75 | ![](./img/flutter/CE226313-D816-45CF-B457-994B66283A44.png) 76 | 77 | Flutter 是构建 Google 物联网操作系统 Fuchsia 的 SDK,主打`跨平台、高保真、高性能`。开发者可以通过 Dart 语言开发 App,一套代码可以同时运行在 iOS 和 Android 平台。 Flutter 使用 Native 引擎渲染视图,并提供了丰富的组件和接口,这无疑为开发者和用户都提供了良好的体验。 78 | 79 | 那么,**Flutter 是怎么完成组件渲染的呢**? 80 | 81 | 这需要从图像显示的基本原理说起。`在计算机系统中,图像的显示需要 CPU、GPU 和显示器一起配合完成:CPU 负责图像数据计算,GPU 负责图像数据渲染,而显示器则负责最终图像显示。` 82 | 83 | 随后视频控制器会以每秒 60 次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。 84 | 85 | ![](./img/flutter/219ADF42-E06A-4EA5-BDC0-AE5A73CD369A.png) 86 | 87 | 可以看到,Flutter 关注如何尽可能快地在两个硬件时钟的 VSync 信号之间计算并合成视图数据,然后通过 Skia 交给 GPU 渲染:UI 线程使用 Dart 来构建视图结构数据,这些数据会在 GPU 线程进行图层合成,随后交给 Skia 引擎加工成 GPU 数据,而这些数据会通过 OpenGL 最终提供给 GPU 渲染。 88 | 89 | ##### Skia 是什么? 90 | 91 | ![](./img/flutter/E5C248B6-4945-44BC-A709-1DD82D49FAAF.png) 92 | 93 | Skia是一个开源的 2D 图形库,提供各种常用的API,并可在多种软硬件平台上运行。谷歌Chrome浏览器、Chrome OS、安卓、Flutter、火狐浏览器、火狐操作系统以及其它许多产品都使用它作为图形引擎。 94 | 95 | Skia `在图形转换、文字渲染、位图渲染方面都表现卓越`,并提供了开发者友好的 API。 96 | 97 | 因此,架构于 Skia 之上的 Flutter,也因此拥有了彻底的跨平台渲染能力。通过与 Skia 的深度定制及优化,Flutter 可以最大限度地抹平平台差异,提高渲染效率与性能。 98 | 99 | 底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者再也不用操心平台相关的渲染特性了。`也就是说,Skia 保证了同一套代码调用在 Android 和 iOS 平台上的渲染效果是完全一致的。` 100 | 101 | 同样的在界面渲染、绘制的过程中,Flutter也做了很多优化处理,提升合成、渲染效率。 102 | 103 | ##### FLutte的优势 104 | 105 | 1. 在所有的平台下,都可以保持同样UI样式,同样的业务逻辑。 106 | 大多数跨平台框架中的UI呈现: 107 | 108 | ![](./img/flutter/7914A8AF-5CC3-47B2-A2EF-CFFBC015A330.png) 109 | 110 | 而Flutter直接通过画在画布上 111 | 112 | ![](./img/flutter/F4BD2112-146E-4E74-B9B5-FCADFD3E2984.png) 113 | 114 | 2. 减少开发所需的时间 115 | 116 | - Flutter的热重载可以高效快速的看到改变,甚至保留应用状态 117 | - 官方提供的各种现成的组件([Material](https://flutter.dev/docs/development/ui/widgets/material)和[Cupertino](https://flutter.dev/docs/development/ui/widgets/cupertino)) 118 | 119 | ![](./img/flutter/44444.gif) 120 | 121 | 3. 快速迭代上线 122 | 123 | - 不需要单独的是适配iOS、Android双端UI层面的 124 | 125 | 4. 更接近native的性能表现 126 | 127 | - Flutter不依赖任何中间代码,最终直接构建成机器码,提高了性能。 128 | 129 | 5. 自定义、复杂动画的 130 | 131 | - Flutter最大的优势之一就是可以定制你在屏幕上看到的任何东西,不管它有多复杂 132 | 133 | 6. 自己的渲染引擎 134 | 135 | - Flutter使用Skia将界面渲染到平台提供的画布上。(意味着不需调整,迁移到其他平台) 136 | 137 | 7. 更方便调用native api 138 | 139 | - 获取GPS坐标、蓝牙通信、收集传感器数据、权限处理等。(未支持的也可通过platform channel) 140 | 141 | 8. 更高的潜力 142 | 143 | - iOS、Android、Web、Desktop... 144 | 145 | --- 146 | 147 | ##### 接下来是相爱相杀时间,我们来对比一下Flutter和React Native(Hippy)。 148 | 149 | ##### UI方面 150 | 151 | ![](./img/flutter/392CF20F-2606-4966-92DE-6ECFC4065E2E.png) 152 | 153 | - 在新旧设备上也能保持一致 154 | 155 | android 5.1 vs android 8.1 156 | ![](./img/flutter/66666.gif)![](./img/flutter/77777.gif) 157 | 158 | 159 | 160 | - Flutter动画效果 161 | 162 | 163 | ![](./img/flutter/11111.gif) 164 | ![](./img/flutter/22222.gif) 165 | ![](./img/flutter/55555.gif) 166 | ![](./img/flutter/33333.gif) 167 | 168 | ##### 性能方面 169 | 170 | - 基于ListView的基准测试 171 | 172 | 在listView中中,我们有1000个元素,并且到达列表最后一个元素的滚动时间相同,这里使用到了一些第三方库。 173 | - ios Nuke 174 | - Android Glide 175 | - react native React-native-fast-image 176 | 177 | ![](./img/flutter/F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif) 178 | 179 | 结果 180 | 181 | ![](./img/flutter/6F8E5930-4F87-4BBE-8F63-DA832C77E882.jpg) 182 | ![](./img/flutter/91DB3A31-5812-478F-9133-14D67CA44BC3.jpg) 183 | 184 | ##### Flutter缺点 185 | 186 | 1. 开发者社区的规模和第三方库 187 | 2. 持续集成的能力 188 | 3. APK的大小 189 | 4. Dart语言学习成本 190 | 5. 动态更新能力 191 | 192 | 193 | 1. flutter是什么,原理和架构是怎样的 194 | 195 | - 拥有自绘引擎的跨平台开发框架 196 | 2. flutter和react-native(hippy)有什么区别,为什么用flutter,不用react-native(hippy) 197 | 198 | - 开发效率更高,减少客户端开发时间 199 | - 性能更优异 200 | - UI、动画支持更全面 201 | - 更接近native 202 | 203 | 3. flutter和react-native(hippy)在方案选型上的一些差异(比如自定义UI组件性能差异,接口怎么通信) 204 | 205 | - 自定义UI组件platformview性能有待提升 206 | - 接口通信,hippy与客户端通过jsbridge,flutter通过methodchannel 207 | 4. flutter混合栈的实现(flutter boost) 208 | 209 | ![](./img/flutter/1598105373710.jpg) 210 | 211 | 212 | 5. flutter为什么不支持动态更新(或者说我们为什么暂时不支持动态更新) 213 | 214 | - 初期考虑应用安全和苹果策略,所以不支持动态更新 215 | - 目前Android端可以通过整包方式实现动态更新, iOS 目前还不支持。 216 | - QQ团队MXFlutter 217 | 218 | 219 | 6. flutter代码多端复用(包括web,转web,转小程序) 220 | 221 | - flutter目前官方支持发布为web应用的能力 222 | - 京东flutter_mp,PCG flutter转手Q小程序 223 | 224 | 7. flutter的页面怎么请求后台(dartjce 类似wnsbuffer) 225 | 226 | - dartjce,在flutter端进行打解包 227 | 228 | 229 | 8. flutter动画能力(lottie? canvas? 序列帧?) 230 | 231 | - lottie - flutter_lottie、fluttie 232 | - canvas - CustomPainter(官方组件) 233 | - 序列帧动画 - (官方animation组件)https://verygood.ventures/blog/2020/2/25/a-deep-dive-into-the-flutter-animations-package -------------------------------------------------------------------------------- /Futter插件管理之Pigeon.md: -------------------------------------------------------------------------------- 1 | ![enter image description here](./img/flutterPigeon/pigeon.jpg) 2 | 3 | ### Flutter插件开发之Pigeon 4 | 5 | > 导语:跨端开发中,经常会遇到插件,接口管理上的问题。了解完本文,你将会Flutter是如何通过Pigeon去解决plugin中多端开发难以管理的问题。 6 | 7 | [demo源码地址](https://github.com/linpenghui958/flutterPigeonDemo) 8 | 9 | warning:目前Pigeon还是prerelease版本,所以可能会有breaking change。下文以0.1.7版本为例。 10 | 11 | [toc] 12 | 13 | #### 为何需要Pigeon 14 | 15 | 在hybird开发中,前端需要native能力,需要native双端开发提供接口。这种情况下就如何规范命名,参数等就成了一个问题,如果单独维护一份协议文件,三端依照协议文件进行开发,很容易出现协议更改后,没有及时同步,又或者在实际开发过程没有按照规范,可能导致各种意外情况。 16 | 在Flutter插件包的开发中,因为涉及到native双端代码实现能力,dart侧暴露统一的接口给使用者,也会出现同样的问题,这里Flutter官方推荐使用Pigeon进行插件管理。 17 | 18 | #### Pigeon的作用 19 | 20 | Flutter官方提供的Pigeon插件,通过dart入口,生成双端通用的模板代码,Native部分只需通过重写模板内的接口,无需关心methodChannel部分的具体实现,入参,出参也均通过生成的模板代码进行约束。 21 | 假设接口新增,或者参数修改,只需要在dart侧更新协议文件,生成双端模板,即可达到同步更新。 22 | 23 | 以Flutter官方plugin中的video_player为例,接入pigeon后最终效果如下 24 | 25 | ![demo](./img/flutterPigeon/pigeon1.png) 26 | 27 | 可以看到接入pigeon后整体代码简洁了不少,而且规范了类型定义。接下来我们看一下如何从零接入Pigeon。 28 | 29 | #### 接入Pigeon 30 | 31 | 先看一下pub.dev上Pigeon的[介绍](https://pub.dev/packages/pigeon),Pigeon只会生成Flutter与native平台通信所需的模板代码,没有其他运行时的要求,所以也不用担心Pigeon版本不同而导致的冲突。(这里的确不同版本使用起来差异较大,笔者这里接入的时候0.1.7与0.1.10,pigeon默认导出和使用都不相同) 32 | 33 | ##### 创建package 34 | 35 | ps:如果接入已有plugin库,可以跳过此部分,直接看接入部分。 36 | 37 | 执行生成插件包命令: 38 | 39 | ``` 40 | flutter create --org com.exmple --template plugin flutterPigeonDemo 41 | ``` 42 | 43 | 要创建插件包,使用`--template=plugin`参数执行`flutter create` 44 | 45 | - `lib/flutter_pigeon_demo.dart` 46 | - 插件包的dart api 47 | - `android/src/main/kotlin/com/example/flutter_pigeon_demo/FlutterPigeonPlugin.kt` 48 | - 插件包Android部分的实现 49 | - `ios/Classes/FlutterPigeonDemoPlugin.m` 50 | - 插件包ios部分的实现。 51 | - `example/` 52 | - 使用该插件的flutterdemo。 53 | 54 | 这里常规通过methodChannel实现plugin的部分省略,主要讲解一下如何接入pigeon插件。 55 | 56 | ##### 添加依赖 57 | 58 | 首先在`pubspec.yaml`中添加依赖 59 | 60 | ``` 61 | dev_dependencies: 62 | flutter_test: 63 | sdk: flutter 64 | pigeon: 65 | version: 0.1.7 66 | ``` 67 | 68 | 然后按照官方的要求添加一个pigeons目录,这里我们放dart侧的入口文件,内容为接口、参数、返回值的定义,后面通过pigeon的命令,生产native端代码。 69 | 70 | 这里以`pigeons/pigeonDemoMessage.dart`为例 71 | 72 | ``` 73 | import 'package:pigeon/pigeon.dart'; 74 | 75 | class DemoReply { 76 | String result; 77 | } 78 | 79 | class DemoRequest { 80 | String methodName; 81 | } 82 | 83 | // 需要实现的api 84 | @HostApi() 85 | abstract class PigeonDemoApi { 86 | DemoReply getMessage(DemoRequest params); 87 | } 88 | 89 | // 输出配置 90 | void configurePigeon(PigeonOptions opts) { 91 | opts.dartOut = './lib/PigeonDemoMessage.dart'; 92 | opts.objcHeaderOut = 'ios/Classes/PigeonDemoMessage.h'; 93 | opts.objcSourceOut = 'ios/Classes/PigeonDemoMessage.m'; 94 | opts.objcOptions.prefix = 'FLT'; 95 | opts.javaOut = 96 | 'android/src/main/kotlin/com/example/flutter_pigeon_demo/PigeonDemoMessage.java'; 97 | opts.javaOptions.package = 'package com.example.flutter_pigeon_demo'; 98 | } 99 | ``` 100 | 101 | `pigeonDemoMessage.dart`文件中定义了请求参数类型、返回值类型、通信的接口以及pigeon输出的配置。 102 | 103 | 这里`@HostApi()`标注了通信对象和接口的定义,后续需要在native侧注册该对象,在Dart侧通过该对象的实例来调用接口。 104 | 105 | `configurePigeon`为执行pigeon生产双端模板代码的输出配置。 106 | 107 | - `dartOut`为dart侧输出位置 108 | - `objcHeaderOut、objcSourceOut`为iOS侧输出位置 109 | - `prefix`为插件默认的前缀 110 | - `javaOut、javaOptions.package`为Android侧输出位置和包名 111 | 112 | 之后我们只需要执行如下命令,就可以生成对应的代码到指定目录中。 113 | 114 | ``` 115 | flutter pub run pigeon --input pigeons/pigeonDemoMessage.dart 116 | ``` 117 | 118 | - `--input`为我们的输入文件 119 | 120 | 生成模板代码后的项目目录如下 121 | 122 | ![catalogue](./img/flutterPigeon/pigeon2.png) 123 | 124 | 我们在Plugin库中只需要管理标红的dart文件,其余标绿的则为通过Pigeon自动生成的模板代码。 125 | 126 | 我们接下来看一下双端如何使用Pigeon生成的模板文件。 127 | 128 | ##### Android端接入 129 | 130 | 这里Pigeon生产的`PigeonDemoMessage.java`文件中,可以看到入参和出参的定义`DemoRequest、DemoReply`,而`PigeonDemoApi`接口,后面需要在plugin中继承PigeonDemoApi并实现对应的方法,其中setup函数用来注册对应方法所需的methodChannel。 131 | 132 | > ps: 这里生成的PigeonDemoApi部分,setup使用了接口中静态方法的默认实现,这里需要api level 24才能支持,这里需要注意一下。 133 | > 134 | > 考虑到兼容性问题,可以将setup的定义转移到plugin中。 135 | 136 | 首先需要在plugin文件中引入生成的PigeonDemoMessage中的接口和类。 137 | FlutterPigeonDemoPlugin先要继承PigeonDemoApi。 138 | 然后在onAttachedToEngine中进行PigeonDemoApi的setup注册。并在plugin中重写PigeonDemoApi中定义的getMessage方法 139 | 140 | 141 | 142 | 伪代码部分 143 | 144 | ``` 145 | // ... 省略其他引入 146 | import com.example.flutter_pigeon_demo.PigeonDemoMessage.* 147 | 148 | // 继承PigeonDemoApi 149 | public class FlutterPigeonDemoPlugin: FlutterPlugin, MethodCallHandler, PigeonDemoApi { 150 | 151 | //... 152 | override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 153 | channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_pigeon_demo") 154 | channel.setMethodCallHandler(this); 155 | // pigeon生成的api进行初始化 156 | PigeonDemoApi.setup(flutterPluginBinding.binaryMessenger, this); 157 | } 158 | 159 | // 重写PigeonDemoApi中的getMessage方法 160 | override fun getMessage(arg: DemoRequest): DemoReply { 161 | var reply = DemoReply(); 162 | reply.result = "pigeon demo result"; 163 | return reply; 164 | } 165 | } 166 | 167 | ``` 168 | 169 | 170 | 171 | ##### iOS接入 172 | 173 | ios相关目录下的`PigeonDemoMessage.m`也有`FLTDemoReply、FLTDemoRequest、FLTPigeonDemoApiSetup`的实现。 174 | 首先需要在plugin中引入头文件`PigeonDemoMessage.h`,需要在registerWithRegistrar中注册setup函数,并实现getMessage方法。 175 | 176 | ``` 177 | #import "FlutterPigeonDemoPlugin.h" 178 | #import "PigeonDemoMessage.h" 179 | 180 | @implementation FlutterPigeonDemoPlugin 181 | + (void)registerWithRegistrar:(NSObject*)registrar { 182 | FlutterPigeonDemoPlugin* instance = [[FlutterPigeonDemoPlugin alloc] init]; 183 | // 注册api 184 | FLTPigeonDemoApiSetup(registrar.messenger, instance); 185 | } 186 | 187 | // 重写getMessage方法 188 | - (FLTDemoReply*)getMessage:(FLTDemoRequest*)input error:(FlutterError**)error { 189 | FLTDemoReply* reply = [[FLTDemoReply alloc] init]; 190 | reply.result = @"pigeon demo result"; 191 | return reply; 192 | } 193 | 194 | @end 195 | 196 | ``` 197 | 198 | ##### Dart侧使用 199 | 200 | 最终在dart侧如何调用呢 201 | 首先看一下lib下Pigeon生成的dart文件`PigeonDemoMessage.dart` 202 | `DemoReply、DemoRequest`用来实例化入参和出参 203 | 然后通过`PigeonDemoApi`的实例去调用方法。 204 | 205 | ``` 206 | import 'dart:async'; 207 | 208 | import 'package:flutter/services.dart'; 209 | import 'PigeonDemoMessage.dart'; 210 | 211 | class FlutterPigeonDemo { 212 | static const MethodChannel _channel = 213 | const MethodChannel('flutter_pigeon_demo'); 214 | 215 | static Future get platformVersion async { 216 | final String version = await _channel.invokeMethod('getPlatformVersion'); 217 | return version; 218 | } 219 | 220 | static Future testPigeon() async { 221 | // 初始化请求参数 222 | DemoRequest requestParams = DemoRequest()..methodName = 'requestMessage'; 223 | // 通过PigeonDemoApi实例去调用方法 224 | PigeonDemoApi api = PigeonDemoApi(); 225 | DemoReply reply = await api.getMessage(requestParams); 226 | return reply; 227 | } 228 | 229 | } 230 | 231 | ``` 232 | 233 | 至此,Pigeon的接入就已经完成了。 234 | 235 | #### 接入Pigeon后的效果 236 | 237 | 本文demo代码较为简单,接入Pigeon前后的差异并不明显,我们可以看下一Flutter官方plugin中的video_player接入前后的对比。 238 | 239 | 左侧为接入Pigeon前,处理逻辑都在onMethodCall中,不同的方法通过传入的call.method来区分,代码复杂后很容易变成面条式代码,而且返回的参数也没有约定,有较多不确定因素。 240 | 241 | 右侧接入Pigeon后,只需要重写对应的方法,逻辑分离,直接通过函数名区分,只需要关心具体的业务逻辑即可。 242 | 243 | ![](./img/flutterPigeon/pigeon4.png) 244 | 245 | 而在dart的调用侧,接入前都是通过invokeMethod调用,传入的参数map内也是dynamic类型的值。接入后直接调用api的实例对象上的方法,并且通过Pigeon生成的模板代码,直接实例化参数对象。 246 | 247 | ![pigeon3](./img/flutterPigeon/pigeon3.png) 248 | 249 | 总结:通过Pigeon来管理Flutter的plugin库,只需要在dart侧维护一份协议即可,即使在多端协同开发的情况下,也能达到约束和规范的作用。 250 | 251 | 在实现原生插件时我们可以省去很多重复代码,并且不需要关心具体methodchannel的name,也避免了常规情况下,可能出现的面条式代码,只需通过重写pigeon暴露的方法就可以完成双端的通信。而dart侧也只需要通过模板暴露的实例对象来调用接口方法。 252 | 253 | -------------------------------------------------------------------------------- /Vue+TypeScript爬坑指南1.md: -------------------------------------------------------------------------------- 1 | ### Vue + TypeScript踩坑(初始化项目) 2 | --- 3 | - 前言,最近公司新项目需要进行重构,框架选择为Vue+TypeScript,记录初始化中不完全踩坑指南 4 |
5 | ##### 1.项目改造 6 | 安装所需的插件 7 | `npm install vue-property-decorator vuex-class --save` 8 | `npm install ts-loader typescript typescript-eslint-parser --save-dev` 9 |
10 | 这些库的作用,可以按需引入 11 | - [vue-property-decorator](https://github.com/kaorun343/vue-property-decorator "vue-property-decorator"):在vue-class-component的基础上优化{Emit, Inject, Model, Prop, Provide, Watch, Component} 12 | - [vuex-class](https://github.com/ktsn/vuex-class "vuex-class"):ts版的vuex插件 13 | - ts-loader: webpack插件 14 | - typescript: 必备插件 15 | - typescript-eslint-parser: eslint解析ts插件 16 | 17 |
18 | ##### 2.WebPack配置修改 19 | webpac.conf中添加ts的解析module 20 | extensions中添加ts结尾的文件 21 | 22 | entry: { 23 | app: './src/main.ts' 24 | }, 25 | // .... 26 | extensions: ['.js', '.vue', '.json', '.ts'], 27 | // .... 28 | { 29 | test: /\.ts$/, 30 | loader: 'ts-loader', 31 | exclude: /node_modules/, 32 | options: { 33 | appendTsSuffixTo: [/\.vue$/], 34 | } 35 | } 36 | 37 | ##### 3.添加tsconfig.json、d.ts文件,修改eslint 38 | 39 | 修改eslint解析规则为ts 40 | 41 | // ... 42 | parserOptions: { 43 | parser: 'typescript-eslint-parser' 44 | }, 45 | // ... 46 | plugins: [ 47 | 'vue', 48 | 'typescript' 49 | ], 50 | 51 | 在.eslintrc.js同级目录创建tsconfig.json文件 52 | 53 | { 54 | "include": [ 55 | "src/*", 56 | "src/**/*" 57 | ], 58 | "exclude": [ 59 | "node_modules" 60 | ], 61 | "compilerOptions": { 62 | // types option has been previously configured 63 | "types": [ 64 | // add node as an option 65 | "node" 66 | ], 67 | // typeRoots option has been previously configured 68 | "typeRoots": [ 69 | // add path to @types 70 | "node_modules/@types" 71 | ], 72 | // 以严格模式解析 73 | "strict": true, 74 | "strictPropertyInitialization": false, 75 | // 在.tsx文件里支持JSX 76 | "jsx": "preserve", 77 | // 使用的JSX工厂函数 78 | "jsxFactory": "h", 79 | // 允许从没有设置默认导出的模块中默认导入 80 | "allowSyntheticDefaultImports": true, 81 | // 启用装饰器 82 | "experimentalDecorators": true, 83 | "strictFunctionTypes": false, 84 | // 允许编译javascript文件 85 | "allowJs": true, 86 | // 采用的模块系统 87 | "module": "esnext", 88 | // 编译输出目标 ES 版本 89 | "target": "es5", 90 | // 如何处理模块 91 | "moduleResolution": "node", 92 | // 在表达式和声明上有隐含的any类型时报错 93 | "noImplicitAny": true, 94 | "lib": [ 95 | "dom", 96 | "es5", 97 | "es6", 98 | "es7", 99 | "es2015.promise" 100 | ], 101 | "sourceMap": true, 102 | "pretty": true 103 | } 104 | } 105 | 106 | 在src目录下添加/typings/vue-shims.d.ts,声明所有的.vue文件 107 | 108 | declare module "*.vue" { 109 | import Vue from "vue"; 110 | export default Vue; 111 | } 112 | 113 |
114 | 115 | ##### 4.对原有文件进行改造 116 | /src/router/index.ts 117 | 118 | import Vue, { AsyncComponent } from 'vue' 119 | import Router, { RouteConfig, Route, NavigationGuard } from 'vue-router' 120 | const Home: AsyncComponent = (): any => import('@/views/Home/index.vue') 121 | 122 | Vue.use(Router) 123 | 124 | const routes: RouteConfig[] = [ 125 | { 126 | path: '/', 127 | name: 'Home', 128 | component: Home 129 | } 130 | ] 131 | 132 | const router: Router = new Router({ 133 | mode: 'history', 134 | base: '/', 135 | routes 136 | }) 137 | 138 | export default router 139 | 140 | /src/store 141 | index.ts 142 | 143 | import Vue from 'vue' 144 | import Vuex, {ActionTree, MutationTree} from 'vuex' 145 | import actions from './actions' 146 | import mutations from './mutations'; 147 | import getters from './getters' 148 | 149 | Vue.use(Vuex) 150 | 151 | interface State { 152 | token: string, 153 | login: Boolean, 154 | } 155 | 156 | let state: State = { 157 | token: 'token', 158 | login: false 159 | } 160 | 161 | export default new Vuex.Store({ 162 | state, 163 | getters, 164 | mutations, 165 | actions 166 | }) 167 | 168 | mutations.ts 169 | 170 | import TYPES from './types' 171 | import { MutationTree } from 'vuex' 172 | 173 | const mutations: MutationTree = { 174 | [TYPES.SET_TOKEN](state, token): void{ 175 | state.token = token 176 | } 177 | } 178 | 179 | export default mutations 180 | 181 | actions.ts 182 | 183 | import { ActionTree } from 'vuex' 184 | import TYPES from './types' 185 | 186 | const actions: ActionTree = { 187 | 188 | initToken({commit}, token: string) { 189 | commit(TYPES.SET_TOKEN, token) 190 | } 191 | } 192 | 193 | export default actions 194 | 195 | /src/main.ts 196 | 197 | import Vue from 'vue'; 198 | import App from './App.vue'; 199 | import store from './store' 200 | import router from './router'; 201 | 202 | Vue.config.productionTip = false 203 | 204 | new Vue({ 205 | el: '#app', 206 | router, 207 | store, 208 | components: { App }, 209 | template: '' 210 | }) 211 | 212 |
213 | ##### 5.vue-property-decorator使用 214 | 215 | vue-property-decorator基于vue-class-component封装 216 | 提供了7种方法 217 | `import {Emit, Inject, Model, Prop, Provide, Watch, Componet}` 218 | 219 | 270 | 271 |
272 | 273 | ##### 错误汇总 274 | 1. typescript版本过新为3.1.x的情况会提示无法找到vue-loader,需要降级到2.8.x 275 | 2. TS2564:Property 'xx' has no initializer and in not definitely assigned in constructor 276 | 在ts新版本中加入的,属性初始化可能为undefined的情况需要考虑进去 277 | 解决方案1,在定义的时候例如 url: string | undefined (不太清楚方法要怎么定义,ts不太熟) 278 | 解决方案2,在tsconfig的compilerOptions中 添加`"strictPropertyInitialization": false,` -------------------------------------------------------------------------------- /Vue+ts下的vw适配(第三方库css问题).md: -------------------------------------------------------------------------------- 1 | @(TypeScript) 2 | ###Vue + ts 下的vw适配(第三方库css问题) 3 | --- 4 | 之前看了大漠老师的VW适配方案,觉得受益匪浅,对比flexible的方案,与js解耦,纯css的适配方案,关于VW的介绍里面有详细的[介绍](https://www.w3cplus.com/mobile/vw-layout-in-vue.html) 5 | 6 | 项目基于vue-cli 7 | 8 | 首先安装所需的插件 9 | `npm i postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-cssnext postcss-viewport-units cssnano --S` 10 | 11 | 接下来在根目录的.postcssrc.js对PostCSS插件进行配置 12 | 13 | module.exports = { 14 | "plugins": { 15 | "postcss-import": {}, 16 | "postcss-url": {}, 17 | // to edit target browsers: use "browserslist" field in package.json 18 | "postcss-write-svg": { 19 | uft8: false 20 | }, 21 | "postcss-cssnext": {}, 22 | "postcss-px-to-viewport": { 23 | viewportWidth: 750, // 设计稿宽度 24 | viewportHeight: 1334, // 设计稿高度,可以不指定 25 | unitPrecision: 3, // px to vw无法整除时,保留几位小数 26 | viewportUnit: 'vw', // 转换成vw单位 27 | selectorBlackList: ['.ignore', '.hairlines'], // 不转换的类名 28 | minPixelValue: 1, // 小于1px不转换 29 | mediaQuery: false // 允许媒体查询中转换 30 | }, 31 | "postcss-viewport-units": {}, 32 | "cssnano": { 33 | preset: "advanced", 34 | autoprefixer: false, // 和cssnext同样具有autoprefixer,保留一个 35 | "postcss-zindex": false 36 | } 37 | } 38 | } 39 | 40 | #####.postcss-px-to-viewport 41 | 用来把px单位转换为vw、vh、vmin或者vmax这样的视窗单位,也是vw适配方案的核心插件之一。 42 | 我们都是使用**750px宽度的视觉设计稿**,那么100vw = 750px,即1vw = 7.5px。那么我们可以根据设计图上的px值直接转换成对应的vw值。在实际撸码过程,不需要进行任何的计算,**直接在代码中写px**。 43 | 44 | #####.postcss-aspect-ratio-mini 45 | 用来处理元素容器宽高比。 46 | 47 | #####.postcss-write-svg 48 | 用来处理移动端1px的解决方案。 49 | 50 | #####vw兼容方案 51 | Viewport Units Buggyfill 52 | 1. 引入js文件 53 | `` 54 | 55 | 2. 在HTML文件中调用viewport-units-buggyfill 56 | 57 | 58 | 61 | 62 | ps: 使用vw的polyfill解决方案会在用到的vw的地方添加content,会影响到img和伪元素,需要全局添加 63 | 64 | `img { content: normal !important; } 65 | 66 | ![](https://i.imgur.com/uHZTO3l.png) 67 | 68 | 小结 69 | 这里作者用的是vue + ts环境,在引入第三方库mint-ui的时候,遇到了一些问题 70 | 71 | 1. 第三方库引入css问题 72 | 在main.ts文件中直接引入mint-ui的css文件会找不到文件 73 | 74 | 75 | import Vue from 'vue' 76 | import MintUI from 'mint-ui' 77 | import 'mint-ui/lib/style.css' // 提示找不到文件 78 | import App from './App.vue' 79 | 80 | Vue.use(MintUI) 81 | 82 | new Vue({ 83 | el: '#app', 84 | components: { App } 85 | }) 86 | 87 | ![](https://i.imgur.com/VEoTX0b.png) 88 | 89 | 笔者在这里的了解一下在github上其他人开源的vue + ts的项目,发现部分解决方案是新建一个style文件,引入node_module内所需的所有文件 90 | 91 | import Vue from 'vue' 92 | import MintUI from 'mint-ui' 93 | import '@/styles/mint-ui.styl' 94 | import App from './App.vue' 95 | 96 | Vue.use(MintUI) 97 | 98 | new Vue({ 99 | el: '#app', 100 | components: { App } 101 | }) 102 | 103 | 104 | mint-ui.styl中 105 | 106 | @import '../../node_modules/mint-ui/lib/style.css' 107 | 在这之前好像跟我们是vw适配没什么联系 108 | 109 | 紧接的就是第二个问题了 110 | 111 | 2. vw打包后,改变了第三方库的px也被转换成vw了 112 | 113 | 这里mint-ui使用的px为单位,所以run dev的时候看到的是正常的,但是build后,postcss-px-to-viewport会将引入的style转换成vw单位 114 | 115 | 这里以mint-ui的picker为例 116 | dev环境时 117 | ![](https://i.imgur.com/ICd7x4k.png) 118 | ![](https://i.imgur.com/lUf5C6E.png) 119 | 120 | build后再打开 121 | ![](https://i.imgur.com/AkOSctC.png) 122 | ![](https://i.imgur.com/gUTmNHh.png) 123 | 124 | 这里在网上查了一下,感觉关于vw的适配方案的文章并不多,没找到类似的解决方案 125 | 后来找到了一个改良版的postcss-px-to-viewport 126 | 添加了exclude选项,将node_modules目录排除掉,即不会受影响 127 | 非常开心的放到了github上 [postcss-px-to-viewport](https://github.com/linpenghui958/postcss-px-to-viewport),用vw的同学,有需要的可以看一下 128 | 129 | 也可以自己在node_modules中找到postcss-px-to-viewport,打开index.js 130 | 新增对exclude选项的处理 131 | 132 | module.exports = postcss.plugin('postcss-px-to-viewport', function (options) { 133 | 134 | var opts = objectAssign({}, defaults, options); 135 | var pxReplace = createPxReplace(opts.viewportWidth, opts.minPixelValue, opts.unitPrecision, opts.viewportUnit); 136 | 137 | return function (css) { 138 | 139 | css.walkDecls(function (decl, i) { 140 | if (options.exclude) { // 添加对exclude选项的处理 141 | if (Object.prototype.toString.call(options.exclude) !== '[object RegExp]') { 142 | throw new Error('options.exclude should be RegExp!') 143 | } 144 | if (decl.source.input.file.match(options.exclude) !== null) return; 145 | } 146 | // This should be the fastest test and will remove most declarations 147 | if (decl.value.indexOf('px') === -1) return; 148 | 149 | if (blacklistedSelector(opts.selectorBlackList, decl.parent.selector)) return; 150 | 151 | decl.value = decl.value.replace(pxRegex, pxReplace); 152 | }); 153 | 154 | if (opts.mediaQuery) { 155 | css.walkAtRules('media', function (rule) { 156 | if (rule.params.indexOf('px') === -1) return; 157 | rule.params = rule.params.replace(pxRegex, pxReplace); 158 | }); 159 | } 160 | 161 | }; 162 | }); 163 | 164 | 165 | 然后在.postcssrc.js添加postcss-px-to-viewport的exclude选项,亲测可用 166 | 167 | "postcss-px-to-viewport": { 168 | viewportWidth: 750, 169 | viewportHeight: 1334, 170 | unitPrecision: 3, 171 | viewportUnit: 'vw', 172 | selectorBlackList: ['.ignore', '.hairlines'], 173 | minPixelValue: 1, 174 | mediaQuery: false, 175 | exclude: /(\/|\\)(node_modules)(\/|\\)/ 176 | }, 177 | 178 | 当然也可以直接 `npm install postcss-px-to-viewport-opt -S` -------------------------------------------------------------------------------- /acme.sh配置ssl https.md: -------------------------------------------------------------------------------- 1 | @(Linux) 2 | ###acme.sh配置ssl https 3 | --- 4 | **使用acme.sh从letsencrypt生成免费证书** 5 | #####1.安装acme.sh 6 | 使用root用户安装 7 | > curl https://get.acme.sh | sh 8 | 9 |
10 | #####2.生产证书 11 | 第一种方式,http 方式需要在你的网站根目录下放置一个文件, 来验证你的域名所有权,完成验证. 然后就可以生成证书了. 12 | 13 |
14 | 第二种方式,dns 方式, 在域名上添加一条 txt 解析记录, 验证域名所有权. 15 | 首先登录你的dnspod账号。生产API ID和Key 16 | 例如阿里云 17 | ![Alt text](./1522049243596.png) 18 | 在.acme.sh目录下的account.conf添加id和key 19 | ![Alt text](./1522049541278.png) 20 | 然后使用 21 | 22 | acme.sh --issue --dns -dns_xx -d aa.com 23 | xx对应前面保存在account.conf里面的Ali 24 | 然后证书就自动生成了 25 | 26 |
27 | #####copy 28 | 注意, 默认生成的证书都放在安装目录下: ~/.acme.sh/, 29 | 正确的使用方法是使用 --installcert 命令,并指定目标位置, 然后证书文件会被copy到相应的位置, 例如: 30 | 31 | acme.sh --installcert -d .com \ 32 | --key-file /etc/nginx/ssl/.key \ 33 | --fullchain-file /etc/nginx/ssl/fullchain.cer \ 34 | --reloadcmd "service nginx force-reload" 35 | 36 | (一个小提醒, 这里用的是 service nginx force-reload, 不是 service nginx reload, 据测试, reload 并不会重新加载证书, 所以用的 force-reload) 37 | 38 | Nginx 的配置 ssl_certificate 使用 /etc/nginx/ssl/fullchain.cer ,而非 /etc/nginx/ssl/.cer ,否则 SSL Labs 的测试会报 Chain issues Incomplete 错误。 39 | 40 |
41 | #####更新acme.sh 42 | 43 | acme.sh --upgrade 44 | 自动升级 45 | acme.sh --upgrade --auto-upgrade 46 | 关闭自动更新 47 | acme.sh --upgrade --auto-upgrade 0 48 |
49 | 50 | #####出错了咋办 51 | 52 | acme.sh --issue ..... --debug 53 | acme.sh --issue ..... --debug 2 54 | 55 |
56 | #####最后还需要配置nginx才能生效 57 | 58 | ps:如果是在阿里云使用,别忘了配置`安全组` -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 生成静态文件 7 | npm run docs:build 8 | 9 | # 进入生成的文件夹 10 | cd docs/.vuepress/dist 11 | 12 | # 如果是发布到自定义域名 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # 如果发布到 https://.github.io 20 | git push -f git@github.com:linpenghui958/linpenghui958.github.io.git master 21 | 22 | # 如果发布到 https://.github.io/ 23 | # git push -f git@github.com:/.git master:gh-pages 24 | 25 | cd - -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: '林小辉的blog', 3 | description: 'Just recrod something', 4 | configureWebpack: { 5 | resolve: { 6 | alias: { 7 | '@alias': 'path/to/some/dir', 8 | } 9 | } 10 | }, 11 | themeConfig: { 12 | sidebar: [ 13 | { 14 | title: 'Flutter相关', // 必要的 15 | collapsable: true, // 可选的, 默认值是 true, 16 | sidebarDepth: 0, // 可选的, 默认值是 1 17 | children: [ 18 | '/flutter/Flutter零基础介绍.md', 19 | '/flutter/Futter插件管理之Pigeon.md', 20 | '/flutter/Flutter FPS 监控.md', 21 | '/flutter/本地编译FlutterEngine.md', 22 | ] 23 | }, 24 | { 25 | title: 'Vue2.0', // 必要的 26 | collapsable: true, // 可选的, 默认值是 true, 27 | sidebarDepth: 0, // 可选的, 默认值是 1 28 | children: [ 29 | '/vue/编译.md', 30 | '/vue/数据驱动.md', 31 | '/vue/响应式原理.md', 32 | '/vue/组件化.md', 33 | '/vue/Vue+ts下的vw适配(第三方库css问题).md', 34 | '/vue/Vue+TypeScript爬坑指南.md', 35 | ] 36 | }, 37 | { 38 | title: '面试', // 必要的 39 | collapsable: true, // 可选的, 默认值是 true, 40 | sidebarDepth: 0, // 可选的, 默认值是 1 41 | children: [ 42 | '/interview/2019面试.md', 43 | '/interview/2019前端性能优化.md', 44 | '/interview/手写js.md', 45 | ] 46 | }, 47 | { 48 | title: 'JavaScript', // 必要的 49 | collapsable: true, // 可选的, 默认值是 true, 50 | sidebarDepth: 0, // 可选的, 默认值是 1 51 | children: [ 52 | '/js/js代码规范.md', 53 | '/js/useHooks.md', 54 | '/js/使用Nodejs和Puppeteer从HTML中导出PDF.md', 55 | ] 56 | }, 57 | { 58 | title: '其他', // 必要的 59 | collapsable: true, // 可选的, 默认值是 true, 60 | sidebarDepth: 0, // 可选的, 默认值是 1 61 | children: [ 62 | '/other/acme.sh配置ssl https.md', 63 | '/other/WebComponents.md', 64 | ] 65 | }, 66 | ], 67 | nav: [ 68 | { text: '知乎', link: 'https://www.zhihu.com/column/linxiaohui' }, 69 | { text: 'Guide', link: '/guide/' }, 70 | { text: 'Github', link: 'https://github.com/linpenghui958' }, 71 | ] 72 | } 73 | } -------------------------------------------------------------------------------- /docs/.vuepress/public/dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/.vuepress/public/dog.png -------------------------------------------------------------------------------- /docs/assets/dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/assets/dog.png -------------------------------------------------------------------------------- /docs/flutter/Flutter FPS 监控.md: -------------------------------------------------------------------------------- 1 | ### 如何获取Flutter APP的FPS 2 | 3 | 众所周知,我们需要衡量一个APP的性能数据,其中FPS也作为其中一个非常重要的标准。 4 | 5 | 这里我们了解一下如何获取Flutter应用中的FPS性能数据。 6 | 7 | ### FPS是什么 8 | 9 | **帧率**是用于测量显示帧数的[量度](https://zh.wikipedia.org/wiki/量度),可产生的图像的数量 10 | 计量单位是帧/秒(Frame Per Second,FPS) 11 | 通常是评估硬件性能与游戏体验流畅度的指标 12 | 13 | ### Flutter的渲染过程 14 | 15 | Flutter 关注如何尽可能快地在两个硬件时钟的 VSync 信号之间计算并合成视图数据,然后通过 Skia 交给 GPU 渲染:UI 线程使用 Dart 来构建视图结构数据,这些数据会在 GPU 线程进行图层合成,随后交给 Skia 引擎加工成 GPU 数据,而这些数据会通过 OpenGL 最终提供给 GPU 渲染 16 | 17 | ![img](http://km.oa.com/files/photos/pictures/202008/1597306977_34_w1870_h450.png) 18 | 19 | ### 本地调试获取FPS 20 | 21 | 官方提供了许多在开发Flutter APP的过程中查看FPS等性能的工具。 22 | 23 | - [devtool](https://github.com/flutter/devtools) 24 | 25 | DevTool 的 [Timeline] 界面可以让开发者逐帧分析应用的 UI 性能,具体的使用方式可以看一下[官方文档](https://flutter.dev/docs/perf/rendering/ui-performance) 26 | 27 | - 性能图层 28 | 29 | ![Screenshot of overlay showing zero jank](https://flutter.cn/assets/tools/devtools/performance-overlay-green-bb41b466cf6bcd529b285e1510b638086fc5afb8921b8ac5a6565dee5bc44788.png) 30 | 31 | 在这些工具中我们只能在本地开发过程中获取FPS数据,如果要统计线上用户的真实数据,要在Flutter代码中计算FPS又该如何做呢? 32 | 33 | ### 生成环境获取FPS 34 | 35 | #### Flutter相关性能指标定义 36 | 37 | 在阅读官方文档的时候,有一个[FrameTiming](https://api.flutter.dev/flutter/dart-ui/FrameTiming-class.html)类描述了每一帧的时间相关的性能指标。 38 | 39 | > If you're using the whole Flutter framework, please use [SchedulerBinding.addTimingsCallback](https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addTimingsCallback.html) to get this. It's preferred over using [Window.onReportTimings](https://api.flutter.dev/flutter/dart-ui/Window/onReportTimings.html) directly because [SchedulerBinding.addTimingsCallback](https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addTimingsCallback.html) allows multiple callbacks. If [SchedulerBinding](https://api.flutter.dev/flutter/scheduler/SchedulerBinding-mixin.html) is unavailable, then see [Window.onReportTimings](https://api.flutter.dev/flutter/dart-ui/Window/onReportTimings.html) for how to get this. 40 | 41 | 这里更推荐使用SchedulerBinding.addTimingsCallBack来获取FPS相关数据。该回调允许多个回调方法,如果该方法不可用才考虑使用Window.onReportTimings。 42 | 43 | #### 性能数据获取 44 | 45 | 这里看一下文档中[addTimingsCallback](https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addTimingsCallback.html)的定义。 46 | 47 | > Add a [TimingsCallback](https://api.flutter.dev/flutter/dart-ui/TimingsCallback.html) that receives [FrameTiming](https://api.flutter.dev/flutter/dart-ui/FrameTiming-class.html) sent from the engine. 48 | 49 | 添加一个TimingsCallback从engine接受发送的FrameTiming信息。接下来看一下具体代码中的定义,了解一下如何使用该方法。 50 | 51 | `flutter/src/scheduler/binding.dart` 52 | 53 | ```dart 54 | void addTimingsCallback(TimingsCallback callback) { 55 | _timingsCallbacks.add(callback); 56 | if (_timingsCallbacks.length == 1) { 57 | assert(window.onReportTimings == null); 58 | window.onReportTimings = _executeTimingsCallbacks; 59 | } 60 | assert(window.onReportTimings == _executeTimingsCallbacks); 61 | } 62 | ``` 63 | 64 | 这里就是对window.onReportTimings的处理进行了封装了。首先用一个叫_timingsCallbacks的List保存了添加的回调,然后初始化时给window.onReportTimings赋值_executeTimingsCallbacks方法。这里_executeTimingsCallbacks会对前面保存的回调List进行遍历执行。 65 | 66 | 知道了addTimingsCallback做了什么,我们再看一下这里callback的定义。 67 | 68 | `sky_engine/ui/window.dart` 69 | 70 | ```dart 71 | /// {@template dart.ui.TimingsCallback.list} 72 | /// The callback takes a list of [FrameTiming] because it may not be 73 | /// immediately triggered after each frame. Instead, Flutter tries to batch 74 | /// frames together and send all their timings at once to decrease the 75 | /// overhead (as this is available in the release mode). The list is sorted in 76 | /// ascending order of time (earliest frame first). The timing of any frame 77 | /// will be sent within about 1 second (100ms if in the profile/debug mode) 78 | /// even if there are no later frames to batch. The timing of the first frame 79 | /// will be sent immediately without batching. 80 | /// {@endtemplate} 81 | typedef TimingsCallback = void Function(List timings); 82 | ``` 83 | 84 | 上方的注释写到,这个回调接受一个FrameTiming的List,Flutter会尝试将这些帧合并后一次性发送,以减少开销。正常情况下一秒内会发送完所有帧,如果在profile/debug模式下,时间会缩短到100毫秒内。 85 | 86 | 简而言之,callback将会得到一个**FrameTiming的List**。 87 | 88 | #### 具体信息分析 89 | 90 | 这里知道在回调中可以拿到的是FrameTiming了,接下来看一下,如果通过这个帧信息可以获取到那些信息呢。 91 | 92 | `sky_engine/ui/window.dart` 93 | 94 | ``` 95 | class FrameTiming { 96 | /// 使用以微秒为单位的原始时间戳来构造[FrameTiming] 97 | /// 98 | /// 这个构建函数仅在单元测试中使用,如果需要获取真实的[FrameTiming]数据请通过[Window.onReportTimings]中获取 99 | factory FrameTiming({ 100 | required int vsyncStart, 101 | required int buildStart, 102 | required int buildFinish, 103 | required int rasterStart, 104 | required int rasterFinish, 105 | }) { 106 | return FrameTiming._([ 107 | vsyncStart, 108 | buildStart, 109 | buildFinish, 110 | rasterStart, 111 | rasterFinish 112 | ]); 113 | } 114 | 115 | /// Construct [FrameTiming] with raw timestamps in microseconds. 116 | /// 117 | /// [timestamps]List必须要有一个同样长度的[FramePhase.values]List 118 | 119 | FrameTiming._(List timestamps) 120 | : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; 121 | 122 | int timestampInMicroseconds(FramePhase phase) => _timestamps[phase.index]; 123 | 124 | Duration _rawDuration(FramePhase phase) => Duration(microseconds: _timestamps[phase.index]); 125 | 126 | /// 在UI线程上构建帧持续的时间。 127 | /// 128 | /// 构建开始的时机大概是当[Window.onBeginFrame]被调用时。[Window.onBeginFrame]回调中的[Duration]就是`Duration(microseconds: timestampInMicroseconds(FramePhase.buildStart))` 129 | /// 130 | /// 构建结束的时机大概是当[Window.render]被调用时。 131 | /// 132 | /// {@template dart.ui.FrameTiming.fps_smoothness_milliseconds} 133 | /// 为了确保x fps平滑动画,这里的时间不应该超过1000/x毫秒。 (x即为fps值,例60, 120) 134 | /// {@endtemplate} 135 | /// {@template dart.ui.FrameTiming.fps_milliseconds} 136 | /// 60fps约为16ms,120fps约为8ms; 137 | /// {@endtemplate} 138 | Duration get buildDuration => _rawDuration(FramePhase.buildFinish) - _rawDuration(FramePhase.buildStart); 139 | 140 | /// 在GPU线程上光栅化帧的持续时间。 141 | /// 142 | /// {@macro dart.ui.FrameTiming.fps_smoothness_milliseconds} 143 | /// {@macro dart.ui.FrameTiming.fps_milliseconds} 144 | Duration get rasterDuration => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.rasterStart); 145 | 146 | /// 在接收到vsync信号并开始构建该帧所花费的时间。 147 | Duration get vsyncOverhead => _rawDuration(FramePhase.buildStart) - _rawDuration(FramePhase.vsyncStart); 148 | 149 | /// 构建开始到栅格化结束的时间。 150 | /// 151 | /// 继续强调这里的时间不应该超过1000/x毫秒。 152 | /// {@macro dart.ui.FrameTiming.fps_milliseconds} 153 | /// 154 | /// See also [vsyncOverhead], [buildDuration] and [rasterDuration]. 155 | Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.vsyncStart); 156 | 157 | final List _timestamps; // in microseconds 158 | 159 | String _formatMS(Duration duration) => '${duration.inMicroseconds * 0.001}ms'; 160 | 161 | @override 162 | String toString() { 163 | return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, vsyncOverhead: ${_formatMS(vsyncOverhead)}, totalSpan: ${_formatMS(totalSpan)})'; 164 | } 165 | } 166 | ``` 167 | 168 | 这里`FrameTiming`获取帧相关的时间,其实都是通过`FramePhase`上的属性来计算的。看一下该类的具体定义。 169 | 170 | ``` 171 | /// 帧的生命周期中各个重要的时间点。 172 | /// [FrameTiming]记录了用于性能分析的每个阶段的时间戳。 173 | enum FramePhase { 174 | /// 当接收到操作系统vsync信号的时间戳 175 | /// See also [FrameTiming.vsyncOverhead]. 176 | vsyncStart, 177 | 178 | /// 当UI线程开始绘制一个帧。 179 | /// See also [FrameTiming.buildDuration]. 180 | buildStart, 181 | 182 | /// 当UI线程结束帧的绘制。 183 | /// See also [FrameTiming.buildDuration]. 184 | buildFinish, 185 | 186 | /// 当GPU线程开始栅格化帧时。 187 | /// See also [FrameTiming.rasterDuration]. 188 | rasterStart, 189 | 190 | /// 当GPU线程完成栅格化帧时。 191 | /// See also [FrameTiming.rasterDuration]. 192 | rasterFinish, 193 | } 194 | ``` 195 | 196 | 现在知道了如果获取最近`N个FrameTiming`和每个FrameTiming中所含有的时间戳信息,接下来看一下如果进行实际的FPS计算了。 197 | 198 | #### 计算FPS 199 | 200 | 理所当然的去想,我们可以获取`总帧数`(FrameTiming List的长度),总共的`耗时`(尾帧时间减去首帧时间)。是不是轻而易举就能算出FPS了呢。 201 | 202 | ```dart 203 | double get fps { 204 | int frames = lastFrames.length; 205 | var start = lastFrames.last.timestampInMicroseconds(FramePhase.buildStart); 206 | var end = lastFrames.first.timestampInMicroseconds(FramePhase.rasterFinish); 207 | var duration = (end - start) / Duration.microsecondsPerMillisecond; 208 | 209 | return frames * Duration.millisecondsPerSecond / duration; 210 | } 211 | ``` 212 | 213 | 这样算出来的结果完全对不上,这是为什么呢。 214 | 215 | 其实,`window.onReportTimings` 只会在有帧被绘制时才有数据回调,换句话说,你没有和app发生交互、界面状态没有变化(setState)、没有定时刷布局(动画)等等没有新的帧产生,所以`lastFrames`里存的可能是分属不同“绘制时间段”的帧信息。 216 | 217 | **假设**一秒最多绘制 60 帧,每帧消耗的时间 `frameInterval` 为: 218 | 219 | ```dart 220 | const REFRESH_RATE = 60; 221 | const frameInterval = const Duration(microseconds: Duration.microsecondsPerSecond ~/ REFRESH_RATE); 222 | ``` 223 | 224 | Flutter引擎每次在收到vsync信号的时候会去调用drawFrame方法,这里如果一帧所花费的时间超过`frameInterval`,则可能会出现丢帧的情况。 225 | 226 | 并且如果`lastFrames`里面相邻的两个帧开始、结束时间相差过大 227 | 228 | ```dart 229 | List framesSet = []; 230 | // 每帧耗时 先写死16.6ms 231 | static double frameInterval = 16600; 232 | SchedulerBinding.instance.addTimingsCallback((List timings) { 233 | timings.forEach(framesSet.add); 234 | // 当时间间隔大于1s,则计算一次FPS 235 | if (shouldReport()) { 236 | startTime = getTime(); 237 | processor(framesSet); 238 | framesSet = []; 239 | } 240 | }); 241 | 242 | double processor(List timings) { 243 | int sum = 0; 244 | for (final FrameTiming timing in timings) { 245 | // 计算渲染耗时 246 | final int duration = timing.timestampInMicroseconds(FramePhase.rasterFinish) - 247 | timing.timestampInMicroseconds(FramePhase.buildStart); 248 | // 判断耗时是否在 Vsync 信号周期内 249 | if (duration < frameInterval) { 250 | sum += 1; 251 | } else { 252 | // 有丢帧,向上取整 253 | final int count = (duration / frameInterval).ceil(); 254 | sum += count; 255 | } 256 | } 257 | 258 | final double fps = timings.length / sum * 60; 259 | return fps; 260 | } 261 | 262 | 263 | ``` 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /docs/flutter/Flutter零基础介绍.md: -------------------------------------------------------------------------------- 1 | #### Flutter介绍 2 | --- 3 | 4 | > Flutter 框架是当下非常热门的跨端解决方案,能够帮助开发者通过一套代码库高效构建多平台精美应用,支持移动、Web、桌面等多端开发。但仍然有很多产品、设计、甚至开发同学并不了解 Flutter,所以本文将深入浅出和大家聊聊 Flutter 的设计背景、技术特点,以及与其他同类技术之间的对比,希望与大家一同交流。 5 | 6 | #### 跨平台背景 7 | 8 | **移动互联网的重要性** 9 | 10 | ![](../img/flutter/3CB87568-7C57-4848-98E5-46E5CE62C2F8.png) 11 | 12 | - 与2019年1月相比,全球使用互联网的人数已增加到45.4亿,增长了7%(2.98亿新用户)。 13 | - 到2020年1月,全球有38亿 社交媒体 用户,与去年同期相比,这个数字增长了9%以上(3.21亿新用户)。 14 | - 在全球范围内,现在有超过51.9亿人使用手机,在过去的一年中,用户数量增加了1.24亿(2.4%)。 15 | 16 | 现在,普通的互联网用户每天在线花费6个小时43分钟。这比去年同期减少了3分钟,但仍然相当于每个互联网用户每年连接时间超过100天。如果我们每天允许大约8个小时的睡眠,那意味着我们目前的醒来时间中,有40%以上是通过互联网度过的。 17 | 18 | ![](../img/flutter/F0B8566F-6D80-4B7A-939E-EC030527187C.jpg) 19 | 20 | 在移动互联网的浪潮下,开发效率和使用体验可以说是同等重要。但是,使用原生的方式来开发 App,就要求我们必须针对 **iOS** 和 **Android** 这两个平台分别开发。 21 | 因为这样的话,我们不仅需要在不同的项目间尝试用不同的语言去实现同样的功能,还要承担由此带来的维护任务。如果还要继续向其他平台(比如 Web、Mac 或 Windows)拓展的话,我们需要**付出的成本和时间将成倍增长**。而这,显然是难以接受的。于是,跨平台开发的概念顺势走进了我们的视野。 22 | 所以从本质上讲,`跨平台开发是为了增加业务代码的复用率,减少因为要适配多个平台带来的工作量,从而降低开发成本`。 23 | 24 | #### 跨平台开发方案的三个时代 25 | 26 | 根据实现方式的不同,业内常见的观点是将主流的跨平台方案划分为三个时代。 27 | - Web 容器时代:基于 Web 相关技术通过浏览器组件来实现界面及功能,典型的框架包括 Cordova(PhoneGap)、Ionic 和微信小程序。 28 | - 泛 Web 容器时代:采用类 Web 标准进行开发,但在运行时把绘制和渲染交由原生系统接管的技术,代表框架有 React Native、Weex 和快应用,广义的还包括天猫的 Virtual View 等。 29 | - 自绘引擎时代:自带渲染引擎,客户端仅提供一块画布即可获得从业务逻辑到功能呈现的多端高度一致的渲染体验。Flutter,是为数不多的代表。 30 | 31 | ##### web容器时代 32 | ![](../img/flutter/4274FBD2-1264-4B07-AFAE-F3125A1D3565.png) 33 | 34 | > Web 时代的方案,主要采用的是原生应用内嵌浏览器控件 WebView的方式进行 HTML5 页面渲染。 35 | > 由于采用了 Web 开发技术,社区和资源非常丰富,开发效率也很高。 36 | 37 | 但,**一个完整 HTML5 页面的展示要经历浏览器控件的加载、解析和渲染三大过程,性能消耗要比原生开发增加 N 个数量级**。 38 | 39 | ##### 泛 Web 容器时代 40 | 41 | ![](../img/flutter/AB694FB0-F007-48B0-A286-562E8B8FCBA2.jpg) 42 | 43 | > 泛 Web 容器时代的解决方案优化了 Web 容器时代的加载、解析和渲染这三大过程,把影响它们独立运行的 Web 标准进行了裁剪,以相对简单的方式支持了构建移动端页面必要的 Web 标准(如 Flexbox 等),也保证了便捷的前端开发体验;同时,采用原生自带的 UI 组件实现代替了核心的渲染引擎,仅保持必要的基本控件渲染能力,从而使得渲染过程更加简化,也保证了良好的渲染性能。 44 | 45 | ![](../img/flutter/95058CE5-F44C-4839-89CD-CE1E85D3C619.jpg) 46 | 47 | > 也就是说,在泛 Web 容器时代,我们仍然采用前端友好的 JavaScript 进行开发,整体加载、渲染机制大大简化,并且由原生接管绘制,即将原生系统作为渲染的后端,为依托于 JavaScript 虚拟机的 JavaScript 代码提供所需要的 UI 控件的实体。这,也是现在绝大部分跨平台框架的思路,而 React Native 和 Weex 就是其中的佼佼者。 48 | 49 | 50 | **总结起来其实就是利用 JS 来调用 Native 端的组件,从而实现相应的功能。** 51 | 52 | ##### 自绘引擎时代 53 | > 这一时期的代表 Flutter 则开辟了一种全新的思路,即从头到尾重写一套跨平台的 UI 框架,包括渲染逻辑,甚至是开发语言。 54 | > 1. 渲染引擎依靠跨平台的 `Skia 图形库`来实现,Skia 引擎会将使用 Dart 构建的抽象的视图结构数据加工成 GPU 数据,交由 OpenGL 最终提供给 GPU 渲染,至此完成渲染闭环,因此可以在最大程度上保证一款应用在不同平台、不同设备上的体验一致性。 55 | > 2. 而开发语言选用的是同时支持 JIT(Just-in-Time,即时编译)和 AOT(Ahead-of-Time,预编译)的 Dart,不仅保证了开发效率,更提升了执行效率(比使用 JavaScript 开发的泛 Web 容器方案要高得多) 56 | 57 | `Flutter 是什么?它出现的动机是什么,解决了哪些痛点?相比其他跨平台技术,Flutter 的优势在哪里?` 58 | 59 | --- 60 | 61 | ##### Flutter 出现的历史背景 62 | 63 | 为不同的操作系统开发拥有相同功能的应用程序,开发人员只有两个选择: 64 | 1. 使用原生开发语言(即 Java 和 Objective-C),针对不同平台分别进行开发。 65 | 2. 使用跨平台解决方案,对不同平台进行统一开发。 66 | 67 | 原生开发方式的体验最好,但研发效率和研发成本相对较高;而跨平台开发方式研发虽然效率高,但为了抹平多端平台差异,各类解决方案暴露的组件和 API 较原生开发相比少很多,因此研发体验和产品功能并不完美。 68 | 69 | 所以,最成功的跨平台开发方案其实是依托于浏览器控件的 Web。浏览器保证了 99% 的概率下 Web 的需求都是可以实现的,不需要业务将就“技术”。不过,Web 最大的问题在于它的性能和体验与原生开发存在肉眼可感知的差异,因此并不适用于对体验要求较高的场景。 70 | 71 | 对于用户体验更接近于原生的 React Native,对业务的支持能力却还不到浏览器的 5%,仅适用于中低复杂度的低交互类页面。面对稍微复杂一点儿的交互和动画需求,开发者都需要 case by case 地去 review,甚至还可能要通过原生代码去扩展才能实现。 72 | 73 | **带着这些问题,我们终于迎来了本次的主角——Flutter。** 74 | 75 | ![](../img/flutter/CE226313-D816-45CF-B457-994B66283A44.png) 76 | 77 | Flutter 是构建 Google 物联网操作系统 Fuchsia 的 SDK,主打`跨平台、高保真、高性能`。开发者可以通过 Dart 语言开发 App,一套代码可以同时运行在 iOS 和 Android 平台。 Flutter 使用 Native 引擎渲染视图,并提供了丰富的组件和接口,这无疑为开发者和用户都提供了良好的体验。 78 | 79 | 那么,**Flutter 是怎么完成组件渲染的呢**? 80 | 81 | 这需要从图像显示的基本原理说起。`在计算机系统中,图像的显示需要 CPU、GPU 和显示器一起配合完成:CPU 负责图像数据计算,GPU 负责图像数据渲染,而显示器则负责最终图像显示。` 82 | 83 | 随后视频控制器会以每秒 60 次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。 84 | 85 | ![](../img/flutter/219ADF42-E06A-4EA5-BDC0-AE5A73CD369A.png) 86 | 87 | 可以看到,Flutter 关注如何尽可能快地在两个硬件时钟的 VSync 信号之间计算并合成视图数据,然后通过 Skia 交给 GPU 渲染:UI 线程使用 Dart 来构建视图结构数据,这些数据会在 GPU 线程进行图层合成,随后交给 Skia 引擎加工成 GPU 数据,而这些数据会通过 OpenGL 最终提供给 GPU 渲染。 88 | 89 | ##### Skia 是什么? 90 | 91 | ![](../img/flutter/E5C248B6-4945-44BC-A709-1DD82D49FAAF.png) 92 | 93 | Skia是一个开源的 2D 图形库,提供各种常用的API,并可在多种软硬件平台上运行。谷歌Chrome浏览器、Chrome OS、安卓、Flutter、火狐浏览器、火狐操作系统以及其它许多产品都使用它作为图形引擎。 94 | 95 | Skia `在图形转换、文字渲染、位图渲染方面都表现卓越`,并提供了开发者友好的 API。 96 | 97 | 因此,架构于 Skia 之上的 Flutter,也因此拥有了彻底的跨平台渲染能力。通过与 Skia 的深度定制及优化,Flutter 可以最大限度地抹平平台差异,提高渲染效率与性能。 98 | 99 | 底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者再也不用操心平台相关的渲染特性了。`也就是说,Skia 保证了同一套代码调用在 Android 和 iOS 平台上的渲染效果是完全一致的。` 100 | 101 | 同样的在界面渲染、绘制的过程中,Flutter也做了很多优化处理,提升合成、渲染效率。 102 | 103 | ##### FLutte的优势 104 | 105 | 1. 在所有的平台下,都可以保持同样UI样式,同样的业务逻辑。 106 | 大多数跨平台框架中的UI呈现: 107 | 108 | ![](../img/flutter/7914A8AF-5CC3-47B2-A2EF-CFFBC015A330.png) 109 | 110 | 而Flutter直接通过画在画布上 111 | 112 | ![](../img/flutter/F4BD2112-146E-4E74-B9B5-FCADFD3E2984.png) 113 | 114 | 2. 减少开发所需的时间 115 | 116 | - Flutter的热重载可以高效快速的看到改变,甚至保留应用状态 117 | - 官方提供的各种现成的组件([Material](https://flutter.dev/docs/development/ui/widgets/material)和[Cupertino](https://flutter.dev/docs/development/ui/widgets/cupertino)) 118 | 119 | ![](../img/flutter/44444.gif) 120 | 121 | 3. 快速迭代上线 122 | 123 | - 不需要单独的是适配iOS、Android双端UI层面的 124 | 125 | 4. 更接近native的性能表现 126 | 127 | - Flutter不依赖任何中间代码,最终直接构建成机器码,提高了性能。 128 | 129 | 5. 自定义、复杂动画的 130 | 131 | - Flutter最大的优势之一就是可以定制你在屏幕上看到的任何东西,不管它有多复杂 132 | 133 | 6. 自己的渲染引擎 134 | 135 | - Flutter使用Skia将界面渲染到平台提供的画布上。(意味着不需调整,迁移到其他平台) 136 | 137 | 7. 更方便调用native api 138 | 139 | - 获取GPS坐标、蓝牙通信、收集传感器数据、权限处理等。(未支持的也可通过platform channel) 140 | 141 | 8. 更高的潜力 142 | 143 | - iOS、Android、Web、Desktop... 144 | 145 | --- 146 | 147 | ##### 接下来是相爱相杀时间,我们来对比一下Flutter和React Native(Hippy)。 148 | 149 | ##### UI方面 150 | 151 | ![](../img/flutter/392CF20F-2606-4966-92DE-6ECFC4065E2E.png) 152 | 153 | - 在新旧设备上也能保持一致 154 | 155 | android 5.1 vs android 8.1 156 | ![](../img/flutter/66666.gif)![](../img/flutter/77777.gif) 157 | 158 | 159 | 160 | - Flutter动画效果 161 | 162 | 163 | ![](../img/flutter/11111.gif) 164 | ![](../img/flutter/22222.gif) 165 | ![](../img/flutter/55555.gif) 166 | ![](../img/flutter/33333.gif) 167 | 168 | ##### 性能方面 169 | 170 | - 基于ListView的基准测试 171 | 172 | 在listView中中,我们有1000个元素,并且到达列表最后一个元素的滚动时间相同,这里使用到了一些第三方库。 173 | - ios Nuke 174 | - Android Glide 175 | - react native React-native-fast-image 176 | 177 | ![](../img/flutter/F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif) 178 | 179 | 结果 180 | 181 | ![](../img/flutter/6F8E5930-4F87-4BBE-8F63-DA832C77E882.jpg) 182 | ![](../img/flutter/91DB3A31-5812-478F-9133-14D67CA44BC3.jpg) 183 | 184 | ##### Flutter缺点 185 | 186 | 1. 开发者社区的规模和第三方库 187 | 2. 持续集成的能力 188 | 3. APK的大小 189 | 4. Dart语言学习成本 190 | 5. 动态更新能力 191 | 192 | 193 | 1. flutter是什么,原理和架构是怎样的 194 | 195 | - 拥有自绘引擎的跨平台开发框架 196 | 2. flutter和react-native(hippy)有什么区别,为什么用flutter,不用react-native(hippy) 197 | 198 | - 开发效率更高,减少客户端开发时间 199 | - 性能更优异 200 | - UI、动画支持更全面 201 | - 更接近native 202 | 203 | 3. flutter和react-native(hippy)在方案选型上的一些差异(比如自定义UI组件性能差异,接口怎么通信) 204 | 205 | - 自定义UI组件platformview性能有待提升 206 | - 接口通信,hippy与客户端通过jsbridge,flutter通过methodchannel 207 | 4. flutter混合栈的实现(flutter boost) 208 | 209 | ![](../img/flutter/1598105373710.jpg) 210 | 211 | 212 | 5. flutter为什么不支持动态更新(或者说我们为什么暂时不支持动态更新) 213 | 214 | - 初期考虑应用安全和苹果策略,所以不支持动态更新 215 | - 目前Android端可以通过整包方式实现动态更新, iOS 目前还不支持。 216 | - QQ团队MXFlutter 217 | 218 | 219 | 6. flutter代码多端复用(包括web,转web,转小程序) 220 | 221 | - flutter目前官方支持发布为web应用的能力 222 | - 京东flutter_mp,PCG flutter转手Q小程序 223 | 224 | 7. flutter的页面怎么请求后台(dartjce 类似wnsbuffer) 225 | 226 | - dartjce,在flutter端进行打解包 227 | 228 | 229 | 8. flutter动画能力(lottie? canvas? 序列帧?) 230 | 231 | - lottie - flutter_lottie、fluttie 232 | - canvas - CustomPainter(官方组件) 233 | - 序列帧动画 - (官方animation组件)https://verygood.ventures/blog/2020/2/25/a-deep-dive-into-the-flutter-animations-package -------------------------------------------------------------------------------- /docs/flutter/Futter插件管理之Pigeon.md: -------------------------------------------------------------------------------- 1 | ![enter image description here](../img/flutterPigeon/pigeon.jpg) 2 | 3 | ### Flutter插件开发之Pigeon 4 | 5 | > 导语:跨端开发中,经常会遇到插件,接口管理上的问题。了解完本文,你将会Flutter是如何通过Pigeon去解决plugin中多端开发难以管理的问题。 6 | 7 | [demo源码地址](https://github.com/linpenghui958/flutterPigeonDemo) 8 | 9 | warning:目前Pigeon还是prerelease版本,所以可能会有breaking change。下文以0.1.7版本为例。 10 | 11 | [toc] 12 | 13 | #### 为何需要Pigeon 14 | 15 | 在hybird开发中,前端需要native能力,需要native双端开发提供接口。这种情况下就如何规范命名,参数等就成了一个问题,如果单独维护一份协议文件,三端依照协议文件进行开发,很容易出现协议更改后,没有及时同步,又或者在实际开发过程没有按照规范,可能导致各种意外情况。 16 | 在Flutter插件包的开发中,因为涉及到native双端代码实现能力,dart侧暴露统一的接口给使用者,也会出现同样的问题,这里Flutter官方推荐使用Pigeon进行插件管理。 17 | 18 | #### Pigeon的作用 19 | 20 | Flutter官方提供的Pigeon插件,通过dart入口,生成双端通用的模板代码,Native部分只需通过重写模板内的接口,无需关心methodChannel部分的具体实现,入参,出参也均通过生成的模板代码进行约束。 21 | 假设接口新增,或者参数修改,只需要在dart侧更新协议文件,生成双端模板,即可达到同步更新。 22 | 23 | 以Flutter官方plugin中的video_player为例,接入pigeon后最终效果如下 24 | 25 | ![demo](../img/flutterPigeon/pigeon1.png) 26 | 27 | 可以看到接入pigeon后整体代码简洁了不少,而且规范了类型定义。接下来我们看一下如何从零接入Pigeon。 28 | 29 | #### 接入Pigeon 30 | 31 | 先看一下pub.dev上Pigeon的[介绍](https://pub.dev/packages/pigeon),Pigeon只会生成Flutter与native平台通信所需的模板代码,没有其他运行时的要求,所以也不用担心Pigeon版本不同而导致的冲突。(这里的确不同版本使用起来差异较大,笔者这里接入的时候0.1.7与0.1.10,pigeon默认导出和使用都不相同) 32 | 33 | ##### 创建package 34 | 35 | ps:如果接入已有plugin库,可以跳过此部分,直接看接入部分。 36 | 37 | 执行生成插件包命令: 38 | 39 | ``` 40 | flutter create --org com.exmple --template plugin flutterPigeonDemo 41 | ``` 42 | 43 | 要创建插件包,使用`--template=plugin`参数执行`flutter create` 44 | 45 | - `lib/flutter_pigeon_demo.dart` 46 | - 插件包的dart api 47 | - `android/src/main/kotlin/com/example/flutter_pigeon_demo/FlutterPigeonPlugin.kt` 48 | - 插件包Android部分的实现 49 | - `ios/Classes/FlutterPigeonDemoPlugin.m` 50 | - 插件包ios部分的实现。 51 | - `example/` 52 | - 使用该插件的flutterdemo。 53 | 54 | 这里常规通过methodChannel实现plugin的部分省略,主要讲解一下如何接入pigeon插件。 55 | 56 | ##### 添加依赖 57 | 58 | 首先在`pubspec.yaml`中添加依赖 59 | 60 | ``` 61 | dev_dependencies: 62 | flutter_test: 63 | sdk: flutter 64 | pigeon: 65 | version: 0.1.7 66 | ``` 67 | 68 | 然后按照官方的要求添加一个pigeons目录,这里我们放dart侧的入口文件,内容为接口、参数、返回值的定义,后面通过pigeon的命令,生产native端代码。 69 | 70 | 这里以`pigeons/pigeonDemoMessage.dart`为例 71 | 72 | ``` 73 | import 'package:pigeon/pigeon.dart'; 74 | 75 | class DemoReply { 76 | String result; 77 | } 78 | 79 | class DemoRequest { 80 | String methodName; 81 | } 82 | 83 | // 需要实现的api 84 | @HostApi() 85 | abstract class PigeonDemoApi { 86 | DemoReply getMessage(DemoRequest params); 87 | } 88 | 89 | // 输出配置 90 | void configurePigeon(PigeonOptions opts) { 91 | opts.dartOut = './lib/PigeonDemoMessage.dart'; 92 | opts.objcHeaderOut = 'ios/Classes/PigeonDemoMessage.h'; 93 | opts.objcSourceOut = 'ios/Classes/PigeonDemoMessage.m'; 94 | opts.objcOptions.prefix = 'FLT'; 95 | opts.javaOut = 96 | 'android/src/main/kotlin/com/example/flutter_pigeon_demo/PigeonDemoMessage.java'; 97 | opts.javaOptions.package = 'package com.example.flutter_pigeon_demo'; 98 | } 99 | ``` 100 | 101 | `pigeonDemoMessage.dart`文件中定义了请求参数类型、返回值类型、通信的接口以及pigeon输出的配置。 102 | 103 | 这里`@HostApi()`标注了通信对象和接口的定义,后续需要在native侧注册该对象,在Dart侧通过该对象的实例来调用接口。 104 | 105 | `configurePigeon`为执行pigeon生产双端模板代码的输出配置。 106 | 107 | - `dartOut`为dart侧输出位置 108 | - `objcHeaderOut、objcSourceOut`为iOS侧输出位置 109 | - `prefix`为插件默认的前缀 110 | - `javaOut、javaOptions.package`为Android侧输出位置和包名 111 | 112 | 之后我们只需要执行如下命令,就可以生成对应的代码到指定目录中。 113 | 114 | ``` 115 | flutter pub run pigeon --input pigeons/pigeonDemoMessage.dart 116 | ``` 117 | 118 | - `--input`为我们的输入文件 119 | 120 | 生成模板代码后的项目目录如下 121 | 122 | ![catalogue](../img/flutterPigeon/pigeon2.png) 123 | 124 | 我们在Plugin库中只需要管理标红的dart文件,其余标绿的则为通过Pigeon自动生成的模板代码。 125 | 126 | 我们接下来看一下双端如何使用Pigeon生成的模板文件。 127 | 128 | ##### Android端接入 129 | 130 | 这里Pigeon生产的`PigeonDemoMessage.java`文件中,可以看到入参和出参的定义`DemoRequest、DemoReply`,而`PigeonDemoApi`接口,后面需要在plugin中继承PigeonDemoApi并实现对应的方法,其中setup函数用来注册对应方法所需的methodChannel。 131 | 132 | > ps: 这里生成的PigeonDemoApi部分,setup使用了接口中静态方法的默认实现,这里需要api level 24才能支持,这里需要注意一下。 133 | > 134 | > 考虑到兼容性问题,可以将setup的定义转移到plugin中。 135 | 136 | 首先需要在plugin文件中引入生成的PigeonDemoMessage中的接口和类。 137 | FlutterPigeonDemoPlugin先要继承PigeonDemoApi。 138 | 然后在onAttachedToEngine中进行PigeonDemoApi的setup注册。并在plugin中重写PigeonDemoApi中定义的getMessage方法 139 | 140 | 141 | 142 | 伪代码部分 143 | 144 | ``` 145 | // ... 省略其他引入 146 | import com.example.flutter_pigeon_demo.PigeonDemoMessage.* 147 | 148 | // 继承PigeonDemoApi 149 | public class FlutterPigeonDemoPlugin: FlutterPlugin, MethodCallHandler, PigeonDemoApi { 150 | 151 | //... 152 | override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 153 | channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_pigeon_demo") 154 | channel.setMethodCallHandler(this); 155 | // pigeon生成的api进行初始化 156 | PigeonDemoApi.setup(flutterPluginBinding.binaryMessenger, this); 157 | } 158 | 159 | // 重写PigeonDemoApi中的getMessage方法 160 | override fun getMessage(arg: DemoRequest): DemoReply { 161 | var reply = DemoReply(); 162 | reply.result = "pigeon demo result"; 163 | return reply; 164 | } 165 | } 166 | 167 | ``` 168 | 169 | 170 | 171 | ##### iOS接入 172 | 173 | ios相关目录下的`PigeonDemoMessage.m`也有`FLTDemoReply、FLTDemoRequest、FLTPigeonDemoApiSetup`的实现。 174 | 首先需要在plugin中引入头文件`PigeonDemoMessage.h`,需要在registerWithRegistrar中注册setup函数,并实现getMessage方法。 175 | 176 | ``` 177 | #import "FlutterPigeonDemoPlugin.h" 178 | #import "PigeonDemoMessage.h" 179 | 180 | @implementation FlutterPigeonDemoPlugin 181 | + (void)registerWithRegistrar:(NSObject*)registrar { 182 | FlutterPigeonDemoPlugin* instance = [[FlutterPigeonDemoPlugin alloc] init]; 183 | // 注册api 184 | FLTPigeonDemoApiSetup(registrar.messenger, instance); 185 | } 186 | 187 | // 重写getMessage方法 188 | - (FLTDemoReply*)getMessage:(FLTDemoRequest*)input error:(FlutterError**)error { 189 | FLTDemoReply* reply = [[FLTDemoReply alloc] init]; 190 | reply.result = @"pigeon demo result"; 191 | return reply; 192 | } 193 | 194 | @end 195 | 196 | ``` 197 | 198 | ##### Dart侧使用 199 | 200 | 最终在dart侧如何调用呢 201 | 首先看一下lib下Pigeon生成的dart文件`PigeonDemoMessage.dart` 202 | `DemoReply、DemoRequest`用来实例化入参和出参 203 | 然后通过`PigeonDemoApi`的实例去调用方法。 204 | 205 | ``` 206 | import 'dart:async'; 207 | 208 | import 'package:flutter/services.dart'; 209 | import 'PigeonDemoMessage.dart'; 210 | 211 | class FlutterPigeonDemo { 212 | static const MethodChannel _channel = 213 | const MethodChannel('flutter_pigeon_demo'); 214 | 215 | static Future get platformVersion async { 216 | final String version = await _channel.invokeMethod('getPlatformVersion'); 217 | return version; 218 | } 219 | 220 | static Future testPigeon() async { 221 | // 初始化请求参数 222 | DemoRequest requestParams = DemoRequest()..methodName = 'requestMessage'; 223 | // 通过PigeonDemoApi实例去调用方法 224 | PigeonDemoApi api = PigeonDemoApi(); 225 | DemoReply reply = await api.getMessage(requestParams); 226 | return reply; 227 | } 228 | 229 | } 230 | 231 | ``` 232 | 233 | 至此,Pigeon的接入就已经完成了。 234 | 235 | #### 接入Pigeon后的效果 236 | 237 | 本文demo代码较为简单,接入Pigeon前后的差异并不明显,我们可以看下一Flutter官方plugin中的video_player接入前后的对比。 238 | 239 | 左侧为接入Pigeon前,处理逻辑都在onMethodCall中,不同的方法通过传入的call.method来区分,代码复杂后很容易变成面条式代码,而且返回的参数也没有约定,有较多不确定因素。 240 | 241 | 右侧接入Pigeon后,只需要重写对应的方法,逻辑分离,直接通过函数名区分,只需要关心具体的业务逻辑即可。 242 | 243 | ![](../img/flutterPigeon/pigeon4.png) 244 | 245 | 而在dart的调用侧,接入前都是通过invokeMethod调用,传入的参数map内也是dynamic类型的值。接入后直接调用api的实例对象上的方法,并且通过Pigeon生成的模板代码,直接实例化参数对象。 246 | 247 | ![pigeon3](../img/flutterPigeon/pigeon3.png) 248 | 249 | 总结:通过Pigeon来管理Flutter的plugin库,只需要在dart侧维护一份协议即可,即使在多端协同开发的情况下,也能达到约束和规范的作用。 250 | 251 | 在实现原生插件时我们可以省去很多重复代码,并且不需要关心具体methodchannel的name,也避免了常规情况下,可能出现的面条式代码,只需通过重写pigeon暴露的方法就可以完成双端的通信。而dart侧也只需要通过模板暴露的实例对象来调用接口方法。 252 | 253 | -------------------------------------------------------------------------------- /docs/flutter/使用Nodejs和Puppeteer从HTML中导出PDF.md: -------------------------------------------------------------------------------- 1 | ## 使用Nodejs和Puppeteer从HTML中导出PDF 2 | 我只是知识的搬运工~ 3 | 4 | [原文外网地址](https://dev.to/bmz1/generating-pdf-from-html-with-nodejs-and-puppeteer-5ln#clientside) 5 | [Markdown文件地址](https://github.com/linpenghui958/note/blob/master/%E4%BD%BF%E7%94%A8Nodejs%E5%92%8CPuppeteer%E4%BB%8EHTML%E4%B8%AD%E5%AF%BC%E5%87%BAPDF.md) 6 | [demo-code地址(文章中所有代码合集)](https://github.com/linpenghui958/pdf-download-demo) 7 | **在这篇文章里,我将会向你展示如何使用Nodejs、Puppeteer、无头浏览器、Docker从一个样式要求复杂的的React页面导出PDF** 8 | 9 | 背景:几个月前,一个RisingStack的客服要求我们实现一个用户可以以PDF格式请求React页面的功能。这个页面主要是含有数据可视化、很多SVG的报告/结å果。此外,还有一些改变布局和修改一些HTML元素样式的特殊需求。因此,这个PDF相对于原始React页面,需要有一些不同的样式和添加。 10 | 11 | **正如这个任务比可以用简单的css规则来解决的情况,要更复杂一些。在我们最开始寻找可行的方法中,我们主要找到了三种解决方法,这篇文章将会带你尽览这些可以使用的方法和最终的解决方案。** 12 | 13 | #### 目录: 14 | - 前端还是后端? 15 | - 方案1:使用一个DOM的屏幕快照 16 | - 方案2:只使用一个PDF的库 17 | - 最终方案3: Puppeteer,headless chrome和Nodejs 18 | - 样式操作 19 | - 往客户端发送 20 | - 在Docker下使用Puppeteer 21 | - 方案3 + 1:CSS打印规则 22 | - 总结 23 | 24 | #### 客户端还是服务端 25 | 在客户端和服务端都可以生产一个PDF文件。然而,如果你不想把用户的浏览器可以提供的资源用完,那还是更可能使用后端来处理。尽管如此,我还是会把两端的解决方法都展示给你看。 26 | 27 | #### 方案1:使用DOM的屏幕快照 28 | 乍看之下,这个解决方案可能是最简单的,并且它也被证明确实是这样,但是这个方案也有自己的局限性。如果你没有一些特殊的需求,这是一个很好的简单方法去生产一个PDF文件。 29 | 30 | 这个方法的思路简单清晰:从当前页面创建一个屏幕快照,并把它放入一个PDF文件。相当的直接了当。我们使用两个库来实现这个功能: 31 | 32 | - [html2canvas](https://html2canvas.hertzen.com/),从DOM中实现一个屏幕快照 33 | - [jsPDF](https://github.com/MrRio/jsPDF),一个生成PDF的库 34 | 35 | 代码如下 36 | 37 | ```javascript 38 | npm install html2canvas jspdf 39 | 40 | ``` 41 | ```javascript 42 | import html2canvas from 'html2canvas' 43 | import jsPdf from 'jspdf' 44 | 45 | printPDF () { 46 | const domElement = document.getElementById('your-id') 47 | html2canvas(domElement).then((canvas) => { 48 | const img = canvas.toDataURL('image/png') 49 | const pdf = new jsPdf() 50 | pdf.addImage(img, 'JPEG', 0, 0, width, height) 51 | pdf.save('your-filename.pdf') 52 | }) 53 | } 54 | ``` 55 | 56 | 请确保你看到了`html2canvas`、`onclone`方法。可以帮助你再获取照片前,便利的获取屏幕快照和操作DOM。我们可以看到需要使用这个工具的例子。不幸的是,这其中并没有我们想要的。我们需要再后端处理PDF的生成。 57 | 58 | #### 方案2:仅仅使用一个PDF库 59 | 在NPM上有许多库可以实现这样的要求,例如jsPDF或者[PDFKit](https://www.npmjs.com/package/pdfkit),随之而来的问题就是如果你想要使用这些库,你不得不再一次生成页面的架子。你还需要把后续的所有改变应用到PDF模板和React页面中。 60 | 看到上面得代码,我们需要自己创建一个PDF文档。现在你可以通过DOM找到如何去转换每一个元素变成PDF,但这是一个沉闷的工作。肯定有一些更简单的方法 61 | 62 | ```javascript 63 | const doc = new PDFDocument() 64 | doc.pipe(fs.createWriteStream(resolve('./test.pdf'))); 65 | doc.font(resolve('./font.ttf')) 66 | .fontSize(30) 67 | .text('测试添加自定义字体!', 100, 100) 68 | doc.image(resolve('./image.jpg'), { 69 | fit: [250, 300], 70 | align: 'center', 71 | valign: 'center' 72 | }) 73 | doc.addPage() 74 | .fontSize(25) 75 | .text('Here is some vector graphics...', 100, 100) 76 | doc.pipe(res); 77 | doc.end(); 78 | ``` 79 | 80 | 这个片段是根据PDFKit的文档写的,如果不需要在已有的HTML页面进行转变,它可以有效的帮助你快速的直接生产PDF文件。 81 | 82 | #### 最终方案3:Puppeteer,Headless Chrome和Nodejs 83 | 什么是Puppeteer呢,它的文档是这么说的 84 | > Puppeteer是一个通过开发者工具协议对Chrome和Chromium提供高级API的操纵。Puppeteer默认运行headless版本,但是可以配置成运行Chrome或者Chromium。 85 | > 这是一个可以在Nodejs环境运行的浏览器。如果你阅读它的文档,第一件事说的就是Puppeteer可以用来生产屏幕快照和页面的PDF。这也是我们为什么要使用它。 86 | 87 | ```javascript 88 | const puppeteer = require('puppeteer') 89 | (async () => { 90 | const brower = await puppeteer.launch() 91 | const page = await brower.newPage() 92 | await page.goto('https://github.com/linpenghui958', { waitUntil: 'networkidle0'}) 93 | const pdf = await page.pdf({ format: 'A4'}) 94 | await brower.close() 95 | return pdf 96 | })() 97 | ``` 98 | 99 | 这是一个导航到制定URL并生产该站点的PDF文件的简单函数。首先,我们启动一个浏览器(只有headless模式才支持生成PDF),然后我们打开一个新页面,设置视图并且导航到提供的URL。 100 | 101 | 设置 `waitUntil: 'networkidle0'`选项表示Puppeteer已经导航到页面并结束(500ms内没有网络请求)这里可以查看更详细的[文档](https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md) 102 | 随后,我们将PDF保存到一个变量,关闭浏览器并返回这个PDF文件。 103 | 104 | Note: `page.pdf`方法可以接受一个 options对象,你可以设置path属性把文件保存在本地中。如果没有提供path,pdf文件将不会保存到本地,你将会得到一个buffer流。稍后我们会讨论如何处理这个情况。 105 | 106 | 如果你需要从一个先登录才能访问的页面生成PDF,那么首先你需要导航到登录页面,检查表单元素的ID或者name,填上,并且提交表单。 107 | 108 | ```javascript 109 | await page.type('#email', process.env.PDF_USER) 110 | await page.type('#password', process.env.PDF_PASSWORD) 111 | await page.click('#submit') 112 | ``` 113 | 通常商城登录认证使用环境变量,不要硬编码它们。 114 | 115 | ##### 样式操作 116 | 117 | Puppeteer同样提供了一个样式操作的解决方案。你可以在生成PDF文件之前加入style标签,并且Puppeteer将会生成一个样式修改后的文件。 118 | 119 | ```javascript 120 | await page.addStyleTag({ content: '.nav { display: none} .navbar { border: 0px} #print-button {display: none}' }) 121 | ``` 122 | 123 | ##### 把资源发送给客户端并保存 124 | 现在我们已经在后端生成了一个PDF文件。接下来做什么呢?根据上面提到的,如果你讲文件保存到本地,你将会得到一个buffer。你只需要通过适当的内容格式将buffer发送给前端即可。 125 | ```javascript 126 | const result = await printPdf() 127 | res.set({'Content-Type': 'application/pdf', 'Content-Length': result.length}) 128 | res.send(result) 129 | ``` 130 | 现在你可以简单的像服务端发送骑牛,并生成PDF 131 | 132 | ```javascript 133 | getPDF() { 134 | return axios.get('//localhost:3001/puppeteer', { 135 | responseType: 'arraybuffer', 136 | headers: { 137 | 'Accept': 'application/pdf' 138 | } 139 | }) 140 | } 141 | ``` 142 | 当你发送一次请求,buffer将会开始下载。现在下一步就是将buffer转换成PDF文件。 143 | 144 | ```javascript 145 | savePDF() { 146 | this.getPDF() 147 | .then(res => { 148 | const blob = new Blob([res.data], { type: 'application/pdf'}) 149 | const link = document.createElement('a') 150 | link.href = window.URL.createObjectURL(blob) 151 | link.download = 'test.pdf' 152 | link.click() 153 | }) 154 | .catch(e => console.log(e)) 155 | } 156 | ``` 157 | 158 | ```javascript 159 | savePDF() { 160 | this.getPDF() 161 | .then(res => { 162 | const blob = new Blob([res.data], { type: 'application/pdf'}) 163 | const link = document.createElement('a') 164 | link.href = window.URL.createObjectURL(blob) 165 | link.download = 'test.pdf' 166 | link.click() 167 | }) 168 | .catch(e => console.log(e)) 169 | } 170 | ``` 171 | 172 | ```javascript 173 | 174 | ``` 175 | 现在如果你的点击按钮,那么PDF将会被浏览器下载。 176 | 177 | #### 在Docker使用Puppeteer 178 | 我认为这是最棘手的部分,因此让我来帮你节省好几个小时Google的时间。 179 | 官方文档只指出,“在Docker下运行headless Chrome可能会非常棘手”。文档有一个排除故障的章节,在那你可以找到使用Docker安装Puppeteer的所有必要信息。 180 | 如果你要在Alpine(linux)镜像中安装Puppeteer,确保你看到了[页面的这个部分](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-on-alpine)。否则你可能会掩盖掉你无法使用最新版本的Puppeteer并且你同样需要禁用shm而去使用一个flag 181 | 182 | ```javascript 183 | const browser = await puppeteer.launch({ 184 | headless: true, 185 | args: ['--disable-dev-shm-usage'] 186 | }); 187 | ``` 188 | 否则,Puppeteer的子进程甚至可能会它开始前就把内存用完了。更多关于故障排查的信息都在上面的链接里。 189 | 190 | #### 方案3 + 1:CSS打印规则 191 | 有人可能会认识从开发者的角度使用CSS打印规则很容易。但是当他们面临浏览器的兼容性时如何跨过这个问题? 192 | 当你选择使用CSS打印规则,你不得不在每一个浏览器测试并确认所有的结果都是同样的布局并且这还不是全部要做的事。 193 | 比如说,在一个给定的元素插入break after,这应该不是一个特别难懂的情况。但是你当你使用Firefox开展工作的时候,你会感到非常惊讶。[点击查看兼容性](https://developer.mozilla.org/en-US/docs/Web/CSS/break-after#Browser_compatibility) 194 | 除非你是一个身经百战在创建打印页面有许多经验的CSS魔术师,否则这将会花费很多的时间。 195 | 如果你可以保持打印样式比较简单那么使用打印规则还是很棒的。 196 | 比如下面这个例子。 197 | 198 | ```css 199 | @media print { 200 | .print-button { 201 | display: none; 202 | } 203 | 204 | .content div { 205 | break-after: always; 206 | } 207 | } 208 | ``` 209 | 上述的CSS隐藏了打印按钮,并且在每一个class名为content后代的div都加入一个break。这里有[一篇文章](https://www.smashingmagazine.com/2018/05/print-stylesheets-in-2018/)总结了你可以使用打印规则做什么,并且在浏览器兼容性方便会有哪些问题。 210 | 综合考虑,如果你不需要从一个如此负责的页面生成PDF,那么CSS打印规则还是很棒的。 211 | 212 | #### 总结:使用Nodejs和Puppeteer从HTML生成PDF 213 | 让我们快速的全览一遍所有的方案。 214 | - 使用DOM的屏幕快照:当你需要从一个页面创建一个快照(例如创建一个短文)这是很有用,但是当你需要处理大量的数据时,可能会出问题。 215 | - 仅使用一个PDF库:如果你需要从头开始以编程的方式创建一个PDF文件,这是一个完美的解决方法。否则,你需要保持HTML和PDF的模板肯定是不方便的。 216 | - Puppeteer:尽管让他在Docker下使用它很麻烦,但是它提供了我们需要的最好的结构并且它的代码也是最容易编写的。 217 | - CSS打印规则:如果你的用户知道如何打印一个文件并且你的页面也相对简单。这可能是最梧桐的解决方案。但是正如你所见,在文中的例子里它并不合适。Happy printing! 218 | -------------------------------------------------------------------------------- /docs/flutter/本地编译FlutterEngine.md: -------------------------------------------------------------------------------- 1 | # 本地编译FlutterEngine 2 | 3 | > 在Flutter的一些深度开发过程中,会遇到需要对Flutter Engine进行修改、定制的情况。这里就需要了解Flutter Engine的编译、打包等流程。这里简单介绍一下如果在本地编译Flutter Engine。 4 | 5 | 6 | ### 工具部分 7 | 8 | 1. 介绍 9 | - [gclient](https://www.chromium.org/developers/how-tos/depottools),谷歌开发的一套跨平台git仓库管理工具,用来将多个git仓库组成一个solution进行管理,通过gclient获取我们编译所需源码和依赖。 10 | - [ninja](https://ninja-build.org/),编译工具,负责最终编译可执行的文件。 11 | - [gn](https://gn.googlesource.com/gn),负责生产ninja所需的构建文件,像Flutter这种跨多操作系统、多平台、多CPU架构的,就需要gn生产多套不同的ninja构建文件(Ninja build files)。 12 | 13 | 2. 安装 14 | - homebrew 15 | 16 | ```bash 17 | /usr/bin/ruby -e "$(curl -fsSL [https://raw.githubusercontent.com/Homebrew/install/master/install](https://raw.githubusercontent.com/Homebrew/install/master/install))" 18 | ``` 19 | 20 | - 下载depot_tools 21 | 22 | ```bash 23 | // 下载 24 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git 25 | 26 | // 配置环境变量 27 | vim ~/.bash_profile 28 | #新增环境变量 29 | export PATH=$PATH:/Users/xxx/flutter/depot_tools #此处使用clone源码到本地的地址 30 | #刷新环境变量缓存,使生效 31 | source ~/.bash_profile 32 | ``` 33 | 34 | - ant、ninja 35 | 36 | ```bash 37 | brew install ant 38 | brew install ninja 39 | ``` 40 | 41 | ### 源码下载 42 | 43 | Flutter Engine的源码是通过gclient管理的,我们首先要创建一个engine目录,然后新建一个gclient的配置文件.gclient。([配置介绍](https://chromium.googlesource.com/chromium/tools/depot_tools.git/+/HEAD/README.gclient.md)) 44 | 45 | ```bash 46 | cd engine 47 | vim .gclient 48 | // .gclient 49 | solutions = [ 50 | { 51 | "managed": False, 52 | "name": "src/flutter", 53 | "url": "git@github.com:xxxxxx/engine.git", 54 | "custom_deps": {}, 55 | "deps_file": "DEPS", 56 | "safesync_url": "", 57 | }, 58 | ] 59 | ``` 60 | 61 | 这里的url也可以替换为自己fork的仓库,后续方便提交和修改。 62 | 63 | 配置host 64 | 65 | ```bash 66 | 172.217.160.112 storage.l.googleusercontent.com 67 | 172.217.160.112 commondatastorage.googleapis.com 68 | 172.217.160.68 googleapis.com 69 | 172.217.160.116 chrome-infra-packages.appspot.com 70 | 172.217.160.116 appspot-preview.l.google.com 71 | ``` 72 | 73 | 下载源码 74 | 75 | ```bash 76 | gclient sync --verbose // 拉取Flutter Engine的源码以及所需的依赖 77 | //--verbose可以看到下载的过程,方便下载过程中出现问题看到异常信息 78 | ``` 79 | 80 | 首次下载过程会比较漫长。 81 | 82 | ps: 这里需要注意本地的dart、flutter环境不要与engine的版本差异太多 83 | 84 | ### 编译本地Engine 85 | 86 | 编译相关基础知识 87 | 88 | - CPU架构 89 | 90 | 编译结果包括`arm`、`arm64`、`x86`这几种架构,arm对应Android的`armeabi-v7a`,arm64对应Android的`arm64-v8a`,x86还是`x86`一般是模拟器上用的。 91 | 92 | - 运行模式 93 | 94 | 根据flutter的模式是分为`debug`、`profile`、`release`这三种模式的。 95 | 96 | 常用的编译参数 97 | 98 | - `—-android-cpu`:cpu架构,对应`arm`、`arm64`、`x86`,例如:`gn —android-cpu arm` 99 | - `—-runtime-mode`:运行模式,对应`debug`、`profile`、`release`,例如:`gn —runtime-mode debug` 100 | - `—unoptiimized`:是否优化。 101 | 102 | 编译之前,最好将下载的engine的版本与本地的flutter依赖的engine版本调整一致,不然可能会报错。 103 | 104 | ```bash 105 | // 查看本地flutter依赖的engine版本 106 | vim $pwd/flutter/bin/internal/engine.version // xxxxxxxxxx 107 | cd $pwd/engine/src/flutter 108 | git reset --hard xxxxxxxxxx 109 | gclient sync -D --with_branch_heads --with_tags 110 | ``` 111 | 112 | 编译开始 113 | 114 | ```bash 115 | // 1、定位到engine/src目录 116 | cd $pwd/engine/src 117 | 118 | // 2、编译Android对应的代码 119 | ./flutter/tools/gn --android --runtime-mode release --android-cpu arm 120 | // 这里会在src目录下生产一个out/android_release的目录,里面就是ninja所需要的编译文件 121 | 122 | // 3、通过2中生产的ninja build files编译 123 | ninja -C out/android_release 124 | 125 | // 4、编译Android打包所需要的代码 126 | ./flutter/tools/gn --runtime-mode release --android-cpu arm 127 | 128 | // 5、编译4中生产的 129 | ninja -C out/host_android 130 | // 如果4中使用的是arm64,这里就需要用host_android_arm64文件夹了 131 | ``` 132 | 133 | ### 使用本地的Engine 134 | 135 | 创建一个demo工程 136 | 137 | ```bash 138 | flutter create engine_demo 139 | cd engine_demo 140 | flutter run --release --local-engine-src-path $pwd/engine/src --local-engine=android_release 141 | ``` -------------------------------------------------------------------------------- /docs/img/1522049243596.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/1522049243596.png -------------------------------------------------------------------------------- /docs/img/1522049541278.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/1522049541278.png -------------------------------------------------------------------------------- /docs/img/flutter/11111.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/11111.gif -------------------------------------------------------------------------------- /docs/img/flutter/1598105373710.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/1598105373710.jpg -------------------------------------------------------------------------------- /docs/img/flutter/219ADF42-E06A-4EA5-BDC0-AE5A73CD369A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/219ADF42-E06A-4EA5-BDC0-AE5A73CD369A.png -------------------------------------------------------------------------------- /docs/img/flutter/22222.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/22222.gif -------------------------------------------------------------------------------- /docs/img/flutter/33333.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/33333.gif -------------------------------------------------------------------------------- /docs/img/flutter/392CF20F-2606-4966-92DE-6ECFC4065E2E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/392CF20F-2606-4966-92DE-6ECFC4065E2E.png -------------------------------------------------------------------------------- /docs/img/flutter/3CB87568-7C57-4848-98E5-46E5CE62C2F8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/3CB87568-7C57-4848-98E5-46E5CE62C2F8.png -------------------------------------------------------------------------------- /docs/img/flutter/4274FBD2-1264-4B07-AFAE-F3125A1D3565.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/4274FBD2-1264-4B07-AFAE-F3125A1D3565.png -------------------------------------------------------------------------------- /docs/img/flutter/44444.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/44444.gif -------------------------------------------------------------------------------- /docs/img/flutter/55555.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/55555.gif -------------------------------------------------------------------------------- /docs/img/flutter/66666.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/66666.gif -------------------------------------------------------------------------------- /docs/img/flutter/6F8E5930-4F87-4BBE-8F63-DA832C77E882.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/6F8E5930-4F87-4BBE-8F63-DA832C77E882.jpg -------------------------------------------------------------------------------- /docs/img/flutter/77777.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/77777.gif -------------------------------------------------------------------------------- /docs/img/flutter/7914A8AF-5CC3-47B2-A2EF-CFFBC015A330.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/7914A8AF-5CC3-47B2-A2EF-CFFBC015A330.png -------------------------------------------------------------------------------- /docs/img/flutter/91DB3A31-5812-478F-9133-14D67CA44BC3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/91DB3A31-5812-478F-9133-14D67CA44BC3.jpg -------------------------------------------------------------------------------- /docs/img/flutter/95058CE5-F44C-4839-89CD-CE1E85D3C619.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/95058CE5-F44C-4839-89CD-CE1E85D3C619.jpg -------------------------------------------------------------------------------- /docs/img/flutter/AB694FB0-F007-48B0-A286-562E8B8FCBA2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/AB694FB0-F007-48B0-A286-562E8B8FCBA2.jpg -------------------------------------------------------------------------------- /docs/img/flutter/CE226313-D816-45CF-B457-994B66283A44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/CE226313-D816-45CF-B457-994B66283A44.png -------------------------------------------------------------------------------- /docs/img/flutter/E5C248B6-4945-44BC-A709-1DD82D49FAAF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/E5C248B6-4945-44BC-A709-1DD82D49FAAF.png -------------------------------------------------------------------------------- /docs/img/flutter/F0B8566F-6D80-4B7A-939E-EC030527187C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/F0B8566F-6D80-4B7A-939E-EC030527187C.jpg -------------------------------------------------------------------------------- /docs/img/flutter/F4BD2112-146E-4E74-B9B5-FCADFD3E2984.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/F4BD2112-146E-4E74-B9B5-FCADFD3E2984.png -------------------------------------------------------------------------------- /docs/img/flutter/F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif -------------------------------------------------------------------------------- /docs/img/flutter/FC16404E-0CF9-4274-9032-691C5F99FBB2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/FC16404E-0CF9-4274-9032-691C5F99FBB2.jpg -------------------------------------------------------------------------------- /docs/img/flutter/vue2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutter/vue2.png -------------------------------------------------------------------------------- /docs/img/flutterPigeon/pigeon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutterPigeon/pigeon.jpg -------------------------------------------------------------------------------- /docs/img/flutterPigeon/pigeon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutterPigeon/pigeon1.png -------------------------------------------------------------------------------- /docs/img/flutterPigeon/pigeon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutterPigeon/pigeon2.png -------------------------------------------------------------------------------- /docs/img/flutterPigeon/pigeon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutterPigeon/pigeon3.png -------------------------------------------------------------------------------- /docs/img/flutterPigeon/pigeon4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/flutterPigeon/pigeon4.png -------------------------------------------------------------------------------- /docs/img/lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/lifecycle.png -------------------------------------------------------------------------------- /docs/img/new-vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/new-vue.png -------------------------------------------------------------------------------- /docs/img/reactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/docs/img/reactive.png -------------------------------------------------------------------------------- /docs/interview/2019前端性能优化.md: -------------------------------------------------------------------------------- 1 | ### 2019前端性能优化 一 2 | --- 3 | - 起步:计划与指标 4 | 1.建立性能评估规范 5 | 2.目标:比你的对手快至少20%] 6 | 3.选择合适的指标 7 | 4.在目标用户的典型设备上收集数据 8 | 5.为测试设立“纯净”、“接近真实用户”的浏览器配置 9 | 6.与团队其他成员分享这份清单 10 | 11 | #### 1.建立性能评估规范 12 | 需要开发、设计与业务、市场团队的共同合作,从而确定页面速度会如何影响业务指标和大家多关心的KPI。 13 | 接着需要确定性能优化的指标,研究用户抱怨的常见问题,再看如何通过性能优化,接近这些问题。 14 | 15 | #### 2.目标:比你的对手快至少20% 16 | 如果你想比你的竞争对手快,至少要快20%。 17 | 你可以通过Chrome UX Report来了解对手的性能,并为具体的页面设置性能目标。 18 | 还有一点非常重要,那就是规划好加载的顺序和取舍。如果你预先规划好哪些部分更重要,并确定每部分的出现顺序,那么同事你也会知道哪些部分可以延迟加载。 19 | 20 | #### 3.选择合适的指标 21 | 这里有一些指标,首次绘制(First Paint)、首次有内容绘制(First Contentful Paint)、首次有意义绘制(First Meaning Paint)、视觉完备(Visual Complete)、首次可交互时间(Time To interactive)(ps:[区别](https://docs.google.com/presentation/d/1D4foHkE0VQdhcA5_hiesl8JhEGeTDRrQR4gipfJ8z7Y/present?slide=id.g21f3ab9dd6_0_33)) 22 | 通常情况被选用的,相对重要的指标有 23 | - 首次有效绘制 First Meaning Paint **FMP** 24 | - 首次可交互时间 Time to interactive **TII** 25 | - 首次输入延迟 First Input Delay **FID** 26 | - 速度指数 Speed Index 27 | - CPU耗时 28 | - 广告的影响 Ad Weight Impact 29 | - 偏离度指标 Deviation metrics 30 | - 自定义指标 Custom metrics 31 | 32 | #### 4.在目标用户的典型设备上收集数据 33 | 移动设备上的解析时间通常要比桌面设备长 36%。因此,一定要在一部平均水准的设备上进行测试 — 一部你的用户中最具代表性的设备。 34 | - 集成测试工具可以在预先规定了设备和网络配置的可复制环境中手机实验数据。例如 Lighthouse、WebPageTest 35 | - 真是用户监测(RUM)工具可以持续评估用户交互,收集实际数据。例如,SpeedCurve、New Relic,两者也都提供集成测试工具。 36 | 37 | ####5.为测试设立“纯净”、“接近真实用户”的浏览器配置(Profile) 38 | 使用被动测试监控工具时,可以关闭反病毒软件和CPU后台任务,关闭后台网络连接,使用没有安装任何插件的“干净的”浏览器配置,以避免结果失真。 39 | 当然了解你的用户会使用哪些插件也是个不错的主意。 40 | 41 | ####6.与团队其他成员共享这份清单 42 | 使每个人熟悉,从而根据性能预算和清单中定义的优先级来制定设计决策。 -------------------------------------------------------------------------------- /docs/interview/2019面试.md: -------------------------------------------------------------------------------- 1 | 金三银四的季节又来了,笔者一直在武汉工作,年前裸辞后,过完年一共面了接近两个星期,有阿里,腾讯,金山,万科,趣头条等公司,在武汉拿到了4个offer(金山、万科、航班管家、财人汇)后,又在深圳一站式面了腾讯,最终选择了腾讯,这里不完全记录一点面试中遇到的问题 2 | 3 | ### 面试不完全汇总 4 | 5 | float和flex的区别 6 | 为什么会有flex 7 | 了解CI CD吗 8 | React和Vue的差异 9 | 移动端高清问题 10 | 移动端点击穿透问题 11 | Nodejs的模块化和ES6的模块化区别 12 | http的优缺点 13 | restful和graphQL优缺点,对比 14 | 事件委托的优缺点 15 | 何时需要用到border-box 16 | csrf是什么,如何用预防 17 | fastclik的作用 18 | 前端性能优化 19 | CSS居中+等比宽高问题 20 | margin-top/margin-bottom百分数问题 21 | TCP和UDP区别 22 | 23 | 24 | #### float和flex的区别 25 | **float** 26 | 脱离文档流 27 | 布局的传统解决方案,基于盒状模型,依赖 display 属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。 28 | 缺点: 29 | - 容错性差,耦合度高,其中一个浮动元素出现问题,其他也会受影响 30 | - 必须要有固定的布局 31 | - 低版本IE不兼容 32 | 33 | **flex** 34 | W3C 提出了一种新的方案----Flex 布局,可以简便、完整、响应式地实现各种页面布局 35 | flex性能改进在于, 36 | - 相比传统margin,可伸缩性强(不算性能) 37 | - 相比float,关联性重绘好很多 38 | - 相比tranform,子元素和父元素样式控制操作更少 39 | 40 | RESTful优缺点 41 | 特点:充分利用 HTTP 协议本身语义。 42 | 无状态,这点非常重要。在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了复杂度。 43 | HTTP 本身提供了丰富的内容协商手段,无论是缓存,还是资源修改的乐观并发控制,都可以以业务无关的中间件来实现。 44 | 45 | #### React和Vue的差异 46 | 相似处 47 | Virtual DOM、组件化、Props 48 | 差异 49 | 1. 渲染优化,是否需要用shouldComponentUpdate,PureComponent 50 | 2. JSX 和 Template , CSS in JS 51 | 3. Vue-cli 和 create-react-app 52 | 4. 学习难度,学 React 前,你需要知道 JSX 和 ES2015 53 | 54 | #### 移动端高清问题 55 | - DPR (device pixel ratio)= device pixel(设备像素) \ CSS pixel(css像素) 56 | - 1px解决方案 57 | > - border-image 、 background-image 缺点:修改颜色麻烦,圆角需要特殊处理 58 | > - box-shadow 缺点:在Safari下不支持小数设置 59 | > - 伪类 + transform 60 | > - SVG 61 | > - viewport + rem 62 | 63 | #### 移动端点击穿透问题 64 | 场景:上层元素A绑定了tap事件,下层元素B绑定了click事件。当我们点击A时,A元素隐藏,此时也会触发下层B元素的click事件。 65 | 原因就是当上层A的tap事件发生后,其实是touchend结束后,会触发click事件,因为这几个事件的触发顺序是touchstart-touchmove-touchend-click。因此当click事件触发300ms后,上面的A元素已经消失,此时真正点击的就相当于下层的B元素,因此会发生点击穿透事件。 66 | **解决方案** 67 | - 都使用click或者tap 68 | - 在click事件触发前阻止,在touchend事件中使用e.preventDefault() 69 | - 使用fast-click。fastclick是一个专门解决300ms点击延迟的库。 70 | 71 | **FastClick实现原理** 72 | 在document.body绑定touchstart和touchend事件 73 | `touchstart` 74 | 用于记录当前点击的元素的targetElement 75 | `touchend` 76 | - 阻止默认事件(屏蔽之后的click事件) 77 | - 合成click事件,并添加可跟踪属性forwardedTouchEvent 78 | - 在targetElement上触发click事件 79 | - targetElement上绑定的事件立即执行,完成FastClick 80 | 81 | #### CommonJS的模块化和ES6的模块化区别 82 | CommonJS主要依赖module、exports、require、global这几个环境变量。 83 | CommonJS用同步方式加载模块。 84 | ES6 module的模块不是对象,`import`命令会被JS引擎动态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。(ps: 也正是因为这个,使得静态分析成为可能) 85 | **差异** 86 | 1. CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用 87 | - CommonJS模块输出值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。 88 | - ES6模块的运行机制与CommonJS不一样。JS引擎对脚本静态分析的时候,遇到模块加载命令`import`,就会生产一个只读引用。等到脚本真正执行时,再根据只读引用,到被加载的模块中取值。原始值变了,`import`加载的值也会跟着变。因此ES6模块是动态引用,并且不会缓存值,模块里的变量绑定其所在的模块。 89 | 2. CommonJS模块是运行时加载,ES6模块是编译时输出接口 90 | - 运行时加载:CommonJS模块就是对象,即在输入时先加载整个模块,生产一个对象,然后再从这个对象上面读取方法,这种加载成为”运行时加载“ 91 | - 编译时加载:ES6模块不是对象,而是通过`export`命令显示指定输出的代码,`import`时采用静态命令的形式。即在`import`时可以指定加载某个输出值,而不是加载整个模块,这种加载成为**编译时加载** 92 | CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 93 | 94 | #### RESTful优缺点 95 | **优点:** 96 | 1. 轻量,直接基于http,不需要任何别的消息协议。get/post/put/delete为CURD操作 97 | 2. 面向资源,一目了然,具有自解释性 98 | 3. 数据描述简单,一般是xml,json做数据交换 99 | 100 | **缺点:** 101 | 一个适用于简单操作的接口规范,复杂操作并不适用,适合CRUD并且只适合CRUD,有的浏览器可能不支持POST、GET以外的操作,要特殊处理。 102 | ps: 多个接口,更新订单收款状态、更新订单支付状态、更新订单结算状态。批量操作,get超出长度 103 | 104 | 1. 客户端-服务器:提供服务的服务器和使用服务的客户端分离解耦 105 | > - 提高客户端的便捷性(操作简单) 106 | > - 简化服务器提高可伸缩性(高性能、低成本) 107 | > - 允许客户端服务端分组优化不受影响 108 | 109 | 2. 无状态:来自客户的每一个请求必须包含服务器处理该请求所需的所有信息(请求信息唯一性) 110 | > - 提高了可见性(可以考虑每个请求) 111 | > - 提高了可靠性(更容易故障恢复) 112 | > - 提高了可扩展性(降低了服务器资源使用) 113 | 114 | 3. 可缓存:服务器必须让客户端知道请求是否可以被缓存?如果可以,客户端可以重用之前的请求信息发送请求 115 | > - 减少交互连接数 116 | > - 减少链接过程的网络延迟 117 | 118 | 4. 分层系统:允许服务器和客户端之前的中间层(代理,网关等)代替服务器对客户端的请求进行回应,而客户端不需要关心与它交互的组件之外的事情 119 | > - 提高了系统的可扩展性 120 | > - 简化了系统的复杂性 121 | 122 | 5. 统一接口:客户和服务器之前通信的方法必须是统一的。 123 | > 提高交互的可见性 124 | > 鼓励单独优化改善组件 125 | 126 | 6. 支持按需代码:服务器可以提供一些代码或者脚本并在客户的运行环境中执行 127 | > - 提高了可扩展性 128 | 129 | #### 事件委托的优缺点 130 | 优点: 131 | 1. 节省内存,减少事件注册。 132 | 2. 适合动态内容 133 | 134 | 确定: 135 | 1. 如果把所有事件都是用事件代理,可能会出现误判。即本不该被触发的事件被绑定上了事件。 136 | 2. 基于冒泡,对于不冒泡的事件不支持。 137 | 3. 层次过多时,可能会被某曾组织掉。 138 | 139 | #### CSRF 140 | 特点 141 | - CSRF通常发生在第三方域名 142 | - CSRF攻击者不能获取到Cookie等信息,只是使用。 143 | 144 | 防御策略 145 | - 阻止不明外域的访问 146 | - 同源监测 147 | - Samesite cookie 148 | 149 | - 提交时要求附加本域下才能获取的信息 150 | - CSRF TOken 151 | - 双重Cookie验证 152 | - 提交验证码 153 | 154 | #### 为什么用transform居中而不是marginLeft/Top 155 | 浏览器渲染过程 156 | > 解析DOM Tree,创建一个或多个渲染层(layer) 157 | > 将每个层独立的绘进位图中(计算样式->布局->栅格化) 158 | > 将层作为纹理(texture)上传至GPU 159 | > 复合(composite)多个层来生成最终的屏幕图像 160 | > 每个层的样式出现调整后,要重新计算样式->重新布局->重新栅格化->重新组合 161 | 162 | 使用top/left只会创建一个层,而是用transform会使浏览器将元素单独提取放在GPU单独的渲染层中,这样有**三点好处**: 163 | 1. 该元素任何合成属性(Composite Property)的变化将不会影响原有文档,不会导致文档被重新布局(重绘、回流) 164 | 2. 该层将由GPU负责渲染,从而节省CPU资源,不会阻塞主线程JS代码的执行 165 | 3. 动画更为平顺,这是因为使用transform可以小于像素的单位进行绘制 166 | 可能带来的负作用是额外的渲染层导致**更多的线程间通信**,如果过度使用,导致生成成百上千的渲染层,那反而会导致**组合各层图像的成本**迅速上升成为主要矛盾,且我们需要记住GPU也是有内存限制的。 167 | 168 | #### 使用css设置rem 169 | 使用calc(100vw / 屏幕宽度) 170 | 171 | ```javascript 172 | [] == ![] 这个要牵涉到 JavaScript 中不同类型 == 比较的规则, 具体是由相关标准定义的. ![] 的值是 false, 此时表达式变为 [] == false, 参照标准, 该比较变成了 [] == ToNumber(false), 即 [] == 0. 这个时候又变成了 ToPrimitive([]) == 0, 即 '' == 0, 接下来就是比较 ToNumber('') == 0, 也就是 0 == 0, 最终结果为 true. 173 | ``` 174 | 175 | 176 | 177 | ### CSS居中 + 宽高等比变化 178 | --- 179 | 1. font-size em-square 180 | 2. line-height 实际占地高度 181 | 3. 100px 100px -> 103px 182 | 4. vertical top middle bottom text-top text-bottom 183 | 5. 图片下面有空隙 184 | 1. vertical-align top 185 | 2. img{display: block} 186 | 3. font-size 0 傻逼才用 187 | 6. inline-block 元素对不齐 无解 用 flex或float 188 | 7. inline-block 有空隙 用 flex 或 float 189 | 190 | ##### css垂直居中对齐 191 | 192 |
193 | Center me, please! 194 |
195 | 1. display: flex + margin: auto 196 | ```css 197 | main{ 198 | width: 100%; 199 | min-height: 152px; 200 | display: flex; 201 | } 202 | main > span { 203 | margin: auto; 204 | } 205 | ``` 206 | 2. display: flex , grid 207 | ```css 208 | main{ 209 | width: 100%; 210 | min-height: 152px; 211 | display: grid; 212 | justify-content: center; 213 | align-items: center; 214 | } 215 | main > span { 216 | background: #b4a078; 217 | color: white; 218 | padding: .3em 1em .5em; 219 | border-radius: 3px; 220 | box-shadow: 0 0 .5em #b4a078; 221 | } 222 | ``` 223 | 3. position: absolute + calc() 224 | ```css 225 | main{ 226 | width: 100%; 227 | min-height: 152px; 228 | display: flex; 229 | } 230 | main > span { 231 | position: absolute; 232 | top: calc(50% - 16px); 233 | left: calc(50% - 72px); 234 | } 235 | ``` 236 | 4. position: absolute + translate 237 | ```css 238 | main{ 239 | width: 100%; 240 | min-height: 152px; 241 | display: flex; 242 | } 243 | main > span { 244 | position: absolute; 245 | top: 50%; left: 50%; 246 | transform: translate(-50%, -50%); 247 | } 248 | ``` 249 | 5. display: table / table-cell + vertical-align: middle 250 | ```css 251 | main { 252 | width: 100%; 253 | height: 152px; 254 | display: table; 255 | } 256 | main > div { 257 | display: table-cell; 258 | text-align: center; 259 | vertical-align: middle; 260 | } 261 | ``` 262 | 6. 伪元素 :after + vertical-align: middle 263 | ```css 264 | main { 265 | width: 100%; 266 | height: 152px; 267 | text-align: center; 268 | } 269 | main::after { 270 | content:''; 271 | display: inline-block; 272 | height: 100%; 273 | vertical-align: middle; 274 | } 275 | ``` 276 | 277 | #### 宽高等比用css如何实现 278 | 1. 使用隐藏图片 279 | 完美兼容PC端、移动端 280 | 如果div容器不给高度,它的高度会随容器内部元素的变化而撑大 281 | 在容器内添加一张等比宽高的图片,给图片设置宽度100% 高度auto 282 | 283 | ```javascript 284 | 296 |
297 |
298 | 299 |
300 |
301 | ``` 302 | 还可以用base64优化 303 | 304 | 2. 使用vmin 305 | 将父容器的宽度和高度定义为相同vmin,这样父容器宽高就是相同值,然后给子容器宽高设置百分比 306 | - vw 相对于视窗的宽度 307 | - vh 相对于视窗的高度 308 | - vmin 相对于视口的宽度或高度中较小的那个被均分为100单位的vmin 309 | - vmax 相对于视口的宽度或高度中较大的那个被均分为100单位的vmax 310 | 311 | 312 | ```css 313 | #container{ 314 | width: 100vmin; 315 | height: 100vmin; 316 | } 317 | 318 | .attr { 319 | width: 50%; 320 | height: 50%; 321 | background-color: orange; 322 | } 323 | ``` 324 | 325 | 3. 使用scale 326 | 327 | 328 | ```css 329 | .attr{ 330 | width:50%; 331 | height: calc(50%); 332 | } 333 | ``` 334 | 335 | 4. padding-top/padding-bottom 实现 336 | 因为padding-bottom的属性值百分比是按照父容器的宽度来计算。 337 | 338 | ```css 339 | 352 | 353 |
354 |
355 |
356 | ``` 357 | 358 | #### 为什么margin-top/margin-bottom的百分数也是相对于width而不是height呢? 359 | CSS权威指南中的解释: 360 | 361 | 我们认为,正常流中的大多数元素都会足够高以包含其后代元素(包括外边距),如果一个元素的上下外边距时父元素的height的百分数,就可能导致一个无限循环,父元素的height会增加,以适应后代元素上下外边距的增加,而相应的,上下外边距因为父元素height的增加也会增加,如果循环。 362 | 363 | 364 | 365 | ###UDP和TCP 366 | #### UDP 367 | 简介:UDP是面向无连接的,UDP只是数据报文的搬运工,不保证有序且不丢失的传递,UDP协议没有控制流量算法,相比TCP更加轻量 368 | 369 | #### 面向无连接 370 | UDP不需要和TCP一样在发送数据前进行三次握手建立连接。 371 | 并且只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接。 372 | 具体来说: 373 | - 在发送端,应用层的数据 -> 传输层UDP协议,UDP只会给数据增加一个UDP头标识 374 | - 在接收端,网络层将数据传给传输层,UDP也只去除IP报文头,就传给应用层 375 | 376 | #### 不可靠性 377 | 首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。 378 | 网络不佳的情况下,UDP因为没有拥塞控制,一直会以恒定的速度发送数据。**弊端**就是网络不好会丢包,**优点**就是在某些时效性要求高的场景适合。 379 | 380 | #### 高效 381 | UDP的头部开销小,只有八个字节(TCP至少二十字节) 382 | UDP头部包含以下几个数据: 383 | - 两个十六位端口号,分别为源端口和目标端口 384 | - 整个数据报文的长度 385 | - 整个数据报文的检验 386 | 387 | #### 传输方式 388 | UDP支持一对一,一对多,多对多,多对一,也就是单播,多播,广播 389 | 390 | #### 适用场景 391 | - 直播 392 | - 游戏 393 | 394 | ### TCP 395 | 建立和断开连接都要进行握手。在传输过程中,通过各种算法保证数据的可靠性。 396 | 相对UDP来说不那么的高效。 397 | 398 | #### 头部 399 | 400 | - Sequence number,这个序号保证了TCP传输的报文是有序的,对端可以通过序号顺序拼接报文 401 | - Acknowledgement number,这个序号表示数据接收端 期望接受的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到 402 | - Window Size,窗口大小,表示还能接受多少字节的数据,用于流量控制 403 | - 标识符 404 | 405 | #### 状态机 406 | 407 | - 建立连接三次握手 408 | 不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 是一个全双工的协议。 409 | 410 | ##### 第一次握手 411 | 客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。 412 | 413 | ##### 第二次握手 414 | 服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序列号,发送完成后便进入SYN-RECEIVED状态。 415 | 416 | ##### 第三次握手 417 | 当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。 418 | 客户端发完这个报文段便进入ESTABLISHED状态。 419 | 服务端发完这个应答后也会进入ESTABLISHED状态。 420 | 421 | ps: 为什么 TCP 建立连接需要三次握手,明明两次就可以建立起连接 422 | 因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。 423 | 424 | #### 断开连接四次握手 425 | - 第一次握手 426 | 若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。 427 | - 第二次握手 428 | B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。 429 | - 第三次握手 430 | B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。 431 | - 第四次握手 432 | A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。 433 | ps: **2MSL** 为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。 434 | 435 | #### ARQ协议 436 | ARQ协议就是超时重传机制。通过确认和超时机制保证数据的正确送达。 437 | ARQ协议包含**停止等待ARQ**和**连续ARQ**两种协议。 -------------------------------------------------------------------------------- /docs/interview/手写js.md: -------------------------------------------------------------------------------- 1 | @(MarkDown) 2 | ###手写JS问题 3 | --- 4 | #####函数节流 throttle 5 | throttle 策略的电梯。保证如果电梯第一个人进来后,50毫秒后准时运送一次,不等待。如果没有人,则待机。 6 | 7 | let throttle = (fn, delay = 50) => { // 节流 控制执行间隔时间 防止频繁触发 scroll resize mousemove 8 | let stattime = 0; 9 | return function (...args) { 10 | let curTime = new Date(); 11 | if (curTime - stattime >= delay) { 12 | fn.apply(this, args); 13 | stattime = curTime; 14 | } 15 | } 16 | } 17 |
18 | 19 | #####防抖动 debounce 20 | debounce 策略的电梯。如果电梯里有人进来,等待50毫秒。如果又人进来,50毫秒等待重新计时,直到50毫秒超时,开始运送。 21 | 22 | let debounce = (fn, time = 50) => { // 防抖动 控制空闲时间 用户输入频繁 23 | let timer; 24 | return function (...args) { 25 | let that = this; 26 | clearTimeout(timer); 27 | timer = setTimeout(fn.bind(that, ...args), time); 28 | } 29 | } 30 | 31 | #####Function的bind实现 32 | 33 | Function.prototype._bind = function (context) { 34 | let func = this 35 | let params = [].slice.call(arguments, 1) 36 | return function () { 37 | params = params.concat([].slice.call(arguments, 0)) 38 | func.apply(context, params) 39 | } 40 | } 41 |
42 | #####函数组合串联compose(koa reduce中间件) 43 | 44 | // 组合串联 45 | let fn1 = (a) => a + 1; 46 | let fn2 = (b) => b + 2; 47 | let fn3 = (c) => c + 3; 48 | 49 | let funs = [fn1, fn2, fn3]; 50 | 51 | let compose = (func) => { 52 | return arg => func.reduceRight((composed, fn) => fn(composed), arg); 53 | } 54 | console.log(compose(funs)(100)); // 相当于fn1(fn2(fn3(100))) 55 |
56 | 57 | #####数组展平 58 | 59 | let arr = [[1, 2], 3, [[[4], 5]]]; // 数组展平 60 | function flatten(arr) { 61 | return [].concat( 62 | ...arr.map(x => Array.isArray(x) ? flatten(x) : x) 63 | ) 64 | } 65 |
66 | 67 | #####插入排序 68 | > 插入排序 从后往前比较 直到碰到比当前项 还要小的前一项时 将这一项插入到前一项的后面 69 | 70 | function insertSort(arr) { 71 | let len = arr.length; 72 | let preIndex, current; 73 | for (let i = 1; i < len; i++) { 74 | preIndex = i - 1; 75 | current = arr[i]; // 当前项 76 | while (preIndex >= 0 && arr[preIndex] > current) { 77 | arr[preIndex + 1] = arr[preIndex]; // 如果前一项大于当前项 则把前一项往后挪一位 78 | preIndex-- // 用当前项继续和前面值进行比较 79 | } 80 | arr[preIndex + 1] = current; // 如果前一项小于当前项则 循环结束 则将当前项放到 前一项的后面 81 | } 82 | return arr; 83 | } 84 | 85 |
86 | #####选择排序 87 | > 选择排序 每次拿当前项与后面其他项进行比较 得到最小值的索引位置 然后把最小值和当前项交换位置 88 | 89 | function selectSort(arr) { 90 | let len = arr.length; 91 | let temp = null; 92 | let minIndex = null; 93 | for (let i = 0; i < len - 1; i++) { // 把当前值的索引作为最小值的索引一次去比较 94 | minIndex = i; // 假设当前项索引 为最小值索引 95 | for (let j = i + 1; j < len; j++) { // 当前项后面向一次比小 96 | if (arr[j] < arr[minIndex]) { // 比假设的值还要小 则保留最小值索引 97 | minIndex = j; // 找到最小值的索引位置 98 | } 99 | } 100 | // 将当前值和比较出的最小值交换位置 101 | if (i !== minIndex) { 102 | temp = arr[i] 103 | arr[i] = arr[minIndex]; 104 | arr[minIndex] = temp; 105 | } 106 | } 107 | return arr; 108 | } 109 |
110 | 111 | #####冒泡排序 112 | > 冒泡排序 相邻两项进行比较 如果当前值大于后一项 则交换位置 113 | 114 |
115 | 116 | #####快速排序(递归) 117 | 118 | 119 | function quickSort(arr) { 120 | if (arr.length <= 1) return arr; 121 | let midIndex = Math.floor(arr.length / 2); 122 | let midNum = arr.splice(midIndex, 1)[0]; 123 | let left = []; 124 | let right = []; 125 | for(let i = 0; i < arr.length; i++) { 126 | let cur = arr[i]; 127 | if (cur <= midNum) { 128 | left.push(cur); 129 | } else { 130 | right.push(cur); 131 | } 132 | } 133 | return quickSort(left).concat(midNum, quickSort(right)); 134 | } 135 | 136 | let arr = [2, 4, 12, 9, 22, 10, 18, 6]; 137 | quickSort(arr); 138 | 139 |
140 | 141 | #####数组去重的几种方法 142 | 143 | 144 | // 1 es6 145 | let newArr = [...new Set(arr)]; 146 | // 2 147 | Array.prototype.unique2 = function() { 148 | let newArr = []; 149 | let len = this.length; 150 | for(let i = 0; i < len; i++) { 151 | let cur = this[i]; 152 | if(newArr.indexOf(cur) === -1) { 153 | newArr[newArr.length] = cur; 154 | } 155 | } 156 | return newArr; 157 | } 158 | console.log(arr.unique1()); 159 | // 3 最快 160 | Array.prototype.unique4 = function() { 161 | let json = {}, newArr = [], len = this.length; 162 | for(var i = 0; i < len; i++) { 163 | let cur = this[i]; 164 | if (typeof json[cur] == "undefined") { 165 | json[cur] = true; 166 | newArr.push(cur) 167 | } 168 | } 169 | return newArr; 170 | } 171 | console.log(arr.unique4()); 172 | 173 | #####千叶符 174 | 175 | let str1 = '2123456789'; 176 | let str2 = '2123456789.12'; 177 | 178 | // 利用正向预查 匹配 开头一个数字\d 后面匹配这个数字后面必须是三个数字为一组为结尾或小数为结尾 179 | function thousandth(str) { 180 | let reg = /\d(?=(?:\d{3})+(?:\.\d+|$))/g; 181 | return str.replace(reg, (...rest) => rest[0] + ','); 182 | } 183 | console.log(thousandth(str1)); // 2,123,456,789 184 | console.log(thousandth(str2)); // 2,123,456,789.12 185 | 186 | 答案: 187 | 188 | str.replace(/(\d)(?=(?:\d{3})+$)/g, ' $1,') -------------------------------------------------------------------------------- /docs/js/使用Nodejs和Puppeteer从HTML中导出PDF.md: -------------------------------------------------------------------------------- 1 | ## 使用Nodejs和Puppeteer从HTML中导出PDF 2 | 我只是知识的搬运工~ 3 | 4 | [原文外网地址](https://dev.to/bmz1/generating-pdf-from-html-with-nodejs-and-puppeteer-5ln#clientside) 5 | [Markdown文件地址](https://github.com/linpenghui958/note/blob/master/%E4%BD%BF%E7%94%A8Nodejs%E5%92%8CPuppeteer%E4%BB%8EHTML%E4%B8%AD%E5%AF%BC%E5%87%BAPDF.md) 6 | [demo-code地址(文章中所有代码合集)](https://github.com/linpenghui958/pdf-download-demo) 7 | **在这篇文章里,我将会向你展示如何使用Nodejs、Puppeteer、无头浏览器、Docker从一个样式要求复杂的的React页面导出PDF** 8 | 9 | 背景:几个月前,一个RisingStack的客服要求我们实现一个用户可以以PDF格式请求React页面的功能。这个页面主要是含有数据可视化、很多SVG的报告/结å果。此外,还有一些改变布局和修改一些HTML元素样式的特殊需求。因此,这个PDF相对于原始React页面,需要有一些不同的样式和添加。 10 | 11 | **正如这个任务比可以用简单的css规则来解决的情况,要更复杂一些。在我们最开始寻找可行的方法中,我们主要找到了三种解决方法,这篇文章将会带你尽览这些可以使用的方法和最终的解决方案。** 12 | 13 | #### 目录: 14 | - 前端还是后端? 15 | - 方案1:使用一个DOM的屏幕快照 16 | - 方案2:只使用一个PDF的库 17 | - 最终方案3: Puppeteer,headless chrome和Nodejs 18 | - 样式操作 19 | - 往客户端发送 20 | - 在Docker下使用Puppeteer 21 | - 方案3 + 1:CSS打印规则 22 | - 总结 23 | 24 | #### 客户端还是服务端 25 | 在客户端和服务端都可以生产一个PDF文件。然而,如果你不想把用户的浏览器可以提供的资源用完,那还是更可能使用后端来处理。尽管如此,我还是会把两端的解决方法都展示给你看。 26 | 27 | #### 方案1:使用DOM的屏幕快照 28 | 乍看之下,这个解决方案可能是最简单的,并且它也被证明确实是这样,但是这个方案也有自己的局限性。如果你没有一些特殊的需求,这是一个很好的简单方法去生产一个PDF文件。 29 | 30 | 这个方法的思路简单清晰:从当前页面创建一个屏幕快照,并把它放入一个PDF文件。相当的直接了当。我们使用两个库来实现这个功能: 31 | 32 | - [html2canvas](https://html2canvas.hertzen.com/),从DOM中实现一个屏幕快照 33 | - [jsPDF](https://github.com/MrRio/jsPDF),一个生成PDF的库 34 | 35 | 代码如下 36 | 37 | ```javascript 38 | npm install html2canvas jspdf 39 | 40 | ``` 41 | ```javascript 42 | import html2canvas from 'html2canvas' 43 | import jsPdf from 'jspdf' 44 | 45 | printPDF () { 46 | const domElement = document.getElementById('your-id') 47 | html2canvas(domElement).then((canvas) => { 48 | const img = canvas.toDataURL('image/png') 49 | const pdf = new jsPdf() 50 | pdf.addImage(img, 'JPEG', 0, 0, width, height) 51 | pdf.save('your-filename.pdf') 52 | }) 53 | } 54 | ``` 55 | 56 | 请确保你看到了`html2canvas`、`onclone`方法。可以帮助你再获取照片前,便利的获取屏幕快照和操作DOM。我们可以看到需要使用这个工具的例子。不幸的是,这其中并没有我们想要的。我们需要再后端处理PDF的生成。 57 | 58 | #### 方案2:仅仅使用一个PDF库 59 | 在NPM上有许多库可以实现这样的要求,例如jsPDF或者[PDFKit](https://www.npmjs.com/package/pdfkit),随之而来的问题就是如果你想要使用这些库,你不得不再一次生成页面的架子。你还需要把后续的所有改变应用到PDF模板和React页面中。 60 | 看到上面得代码,我们需要自己创建一个PDF文档。现在你可以通过DOM找到如何去转换每一个元素变成PDF,但这是一个沉闷的工作。肯定有一些更简单的方法 61 | 62 | ```javascript 63 | const doc = new PDFDocument() 64 | doc.pipe(fs.createWriteStream(resolve('./test.pdf'))); 65 | doc.font(resolve('./font.ttf')) 66 | .fontSize(30) 67 | .text('测试添加自定义字体!', 100, 100) 68 | doc.image(resolve('./image.jpg'), { 69 | fit: [250, 300], 70 | align: 'center', 71 | valign: 'center' 72 | }) 73 | doc.addPage() 74 | .fontSize(25) 75 | .text('Here is some vector graphics...', 100, 100) 76 | doc.pipe(res); 77 | doc.end(); 78 | ``` 79 | 80 | 这个片段是根据PDFKit的文档写的,如果不需要在已有的HTML页面进行转变,它可以有效的帮助你快速的直接生产PDF文件。 81 | 82 | #### 最终方案3:Puppeteer,Headless Chrome和Nodejs 83 | 什么是Puppeteer呢,它的文档是这么说的 84 | > Puppeteer是一个通过开发者工具协议对Chrome和Chromium提供高级API的操纵。Puppeteer默认运行headless版本,但是可以配置成运行Chrome或者Chromium。 85 | > 这是一个可以在Nodejs环境运行的浏览器。如果你阅读它的文档,第一件事说的就是Puppeteer可以用来生产屏幕快照和页面的PDF。这也是我们为什么要使用它。 86 | 87 | ```javascript 88 | const puppeteer = require('puppeteer') 89 | (async () => { 90 | const brower = await puppeteer.launch() 91 | const page = await brower.newPage() 92 | await page.goto('https://github.com/linpenghui958', { waitUntil: 'networkidle0'}) 93 | const pdf = await page.pdf({ format: 'A4'}) 94 | await brower.close() 95 | return pdf 96 | })() 97 | ``` 98 | 99 | 这是一个导航到制定URL并生产该站点的PDF文件的简单函数。首先,我们启动一个浏览器(只有headless模式才支持生成PDF),然后我们打开一个新页面,设置视图并且导航到提供的URL。 100 | 101 | 设置 `waitUntil: 'networkidle0'`选项表示Puppeteer已经导航到页面并结束(500ms内没有网络请求)这里可以查看更详细的[文档](https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md) 102 | 随后,我们将PDF保存到一个变量,关闭浏览器并返回这个PDF文件。 103 | 104 | Note: `page.pdf`方法可以接受一个 options对象,你可以设置path属性把文件保存在本地中。如果没有提供path,pdf文件将不会保存到本地,你将会得到一个buffer流。稍后我们会讨论如何处理这个情况。 105 | 106 | 如果你需要从一个先登录才能访问的页面生成PDF,那么首先你需要导航到登录页面,检查表单元素的ID或者name,填上,并且提交表单。 107 | 108 | ```javascript 109 | await page.type('#email', process.env.PDF_USER) 110 | await page.type('#password', process.env.PDF_PASSWORD) 111 | await page.click('#submit') 112 | ``` 113 | 通常商城登录认证使用环境变量,不要硬编码它们。 114 | 115 | ##### 样式操作 116 | 117 | Puppeteer同样提供了一个样式操作的解决方案。你可以在生成PDF文件之前加入style标签,并且Puppeteer将会生成一个样式修改后的文件。 118 | 119 | ```javascript 120 | await page.addStyleTag({ content: '.nav { display: none} .navbar { border: 0px} #print-button {display: none}' }) 121 | ``` 122 | 123 | ##### 把资源发送给客户端并保存 124 | 现在我们已经在后端生成了一个PDF文件。接下来做什么呢?根据上面提到的,如果你讲文件保存到本地,你将会得到一个buffer。你只需要通过适当的内容格式将buffer发送给前端即可。 125 | ```javascript 126 | const result = await printPdf() 127 | res.set({'Content-Type': 'application/pdf', 'Content-Length': result.length}) 128 | res.send(result) 129 | ``` 130 | 现在你可以简单的像服务端发送骑牛,并生成PDF 131 | 132 | ```javascript 133 | getPDF() { 134 | return axios.get('//localhost:3001/puppeteer', { 135 | responseType: 'arraybuffer', 136 | headers: { 137 | 'Accept': 'application/pdf' 138 | } 139 | }) 140 | } 141 | ``` 142 | 当你发送一次请求,buffer将会开始下载。现在下一步就是将buffer转换成PDF文件。 143 | 144 | ```javascript 145 | savePDF() { 146 | this.getPDF() 147 | .then(res => { 148 | const blob = new Blob([res.data], { type: 'application/pdf'}) 149 | const link = document.createElement('a') 150 | link.href = window.URL.createObjectURL(blob) 151 | link.download = 'test.pdf' 152 | link.click() 153 | }) 154 | .catch(e => console.log(e)) 155 | } 156 | ``` 157 | 158 | ```javascript 159 | savePDF() { 160 | this.getPDF() 161 | .then(res => { 162 | const blob = new Blob([res.data], { type: 'application/pdf'}) 163 | const link = document.createElement('a') 164 | link.href = window.URL.createObjectURL(blob) 165 | link.download = 'test.pdf' 166 | link.click() 167 | }) 168 | .catch(e => console.log(e)) 169 | } 170 | ``` 171 | 172 | ```javascript 173 | 174 | ``` 175 | 现在如果你的点击按钮,那么PDF将会被浏览器下载。 176 | 177 | #### 在Docker使用Puppeteer 178 | 我认为这是最棘手的部分,因此让我来帮你节省好几个小时Google的时间。 179 | 官方文档只指出,“在Docker下运行headless Chrome可能会非常棘手”。文档有一个排除故障的章节,在那你可以找到使用Docker安装Puppeteer的所有必要信息。 180 | 如果你要在Alpine(linux)镜像中安装Puppeteer,确保你看到了[页面的这个部分](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-on-alpine)。否则你可能会掩盖掉你无法使用最新版本的Puppeteer并且你同样需要禁用shm而去使用一个flag 181 | 182 | ```javascript 183 | const browser = await puppeteer.launch({ 184 | headless: true, 185 | args: ['--disable-dev-shm-usage'] 186 | }); 187 | ``` 188 | 否则,Puppeteer的子进程甚至可能会它开始前就把内存用完了。更多关于故障排查的信息都在上面的链接里。 189 | 190 | #### 方案3 + 1:CSS打印规则 191 | 有人可能会认识从开发者的角度使用CSS打印规则很容易。但是当他们面临浏览器的兼容性时如何跨过这个问题? 192 | 当你选择使用CSS打印规则,你不得不在每一个浏览器测试并确认所有的结果都是同样的布局并且这还不是全部要做的事。 193 | 比如说,在一个给定的元素插入break after,这应该不是一个特别难懂的情况。但是你当你使用Firefox开展工作的时候,你会感到非常惊讶。[点击查看兼容性](https://developer.mozilla.org/en-US/docs/Web/CSS/break-after#Browser_compatibility) 194 | 除非你是一个身经百战在创建打印页面有许多经验的CSS魔术师,否则这将会花费很多的时间。 195 | 如果你可以保持打印样式比较简单那么使用打印规则还是很棒的。 196 | 比如下面这个例子。 197 | 198 | ```css 199 | @media print { 200 | .print-button { 201 | display: none; 202 | } 203 | 204 | .content div { 205 | break-after: always; 206 | } 207 | } 208 | ``` 209 | 上述的CSS隐藏了打印按钮,并且在每一个class名为content后代的div都加入一个break。这里有[一篇文章](https://www.smashingmagazine.com/2018/05/print-stylesheets-in-2018/)总结了你可以使用打印规则做什么,并且在浏览器兼容性方便会有哪些问题。 210 | 综合考虑,如果你不需要从一个如此负责的页面生成PDF,那么CSS打印规则还是很棒的。 211 | 212 | #### 总结:使用Nodejs和Puppeteer从HTML生成PDF 213 | 让我们快速的全览一遍所有的方案。 214 | - 使用DOM的屏幕快照:当你需要从一个页面创建一个快照(例如创建一个短文)这是很有用,但是当你需要处理大量的数据时,可能会出问题。 215 | - 仅使用一个PDF库:如果你需要从头开始以编程的方式创建一个PDF文件,这是一个完美的解决方法。否则,你需要保持HTML和PDF的模板肯定是不方便的。 216 | - Puppeteer:尽管让他在Docker下使用它很麻烦,但是它提供了我们需要的最好的结构并且它的代码也是最容易编写的。 217 | - CSS打印规则:如果你的用户知道如何打印一个文件并且你的页面也相对简单。这可能是最梧桐的解决方案。但是正如你所见,在文中的例子里它并不合适。Happy printing! 218 | -------------------------------------------------------------------------------- /docs/other/acme.sh配置ssl https.md: -------------------------------------------------------------------------------- 1 | ### acme.sh配置ssl https 2 | --- 3 | **使用acme.sh从letsencrypt生成免费证书** 4 | #####1.安装acme.sh 5 | 使用root用户安装 6 | > curl https://get.acme.sh | sh 7 | 8 |
9 | #####2.生产证书 10 | 第一种方式,http 方式需要在你的网站根目录下放置一个文件, 来验证你的域名所有权,完成验证. 然后就可以生成证书了. 11 | 12 |
13 | 第二种方式,dns 方式, 在域名上添加一条 txt 解析记录, 验证域名所有权. 14 | 首先登录你的dnspod账号。生产API ID和Key 15 | 例如阿里云 16 | ![Alt text](../img/1522049243596.png) 17 | 在.acme.sh目录下的account.conf添加id和key 18 | ![Alt text](../img/1522049541278.png) 19 | 然后使用 20 | 21 | acme.sh --issue --dns -dns_xx -d aa.com 22 | xx对应前面保存在account.conf里面的Ali 23 | 然后证书就自动生成了 24 | 25 |
26 | #####copy 27 | 注意, 默认生成的证书都放在安装目录下: ~/.acme.sh/, 28 | 正确的使用方法是使用 --installcert 命令,并指定目标位置, 然后证书文件会被copy到相应的位置, 例如: 29 | 30 | acme.sh --installcert -d .com \ 31 | --key-file /etc/nginx/ssl/.key \ 32 | --fullchain-file /etc/nginx/ssl/fullchain.cer \ 33 | --reloadcmd "service nginx force-reload" 34 | 35 | (一个小提醒, 这里用的是 service nginx force-reload, 不是 service nginx reload, 据测试, reload 并不会重新加载证书, 所以用的 force-reload) 36 | 37 | Nginx 的配置 ssl_certificate 使用 /etc/nginx/ssl/fullchain.cer ,而非 /etc/nginx/ssl/< domain >.cer ,否则 SSL Labs 的测试会报 Chain issues Incomplete 错误。 38 | 39 |
40 | #####更新acme.sh 41 | 42 | acme.sh --upgrade 43 | 自动升级 44 | acme.sh --upgrade --auto-upgrade 45 | 关闭自动更新 46 | acme.sh --upgrade --auto-upgrade 0 47 |
48 | 49 | #####出错了咋办 50 | 51 | acme.sh --issue ..... --debug 52 | acme.sh --issue ..... --debug 2 53 | 54 |
55 | #####最后还需要配置nginx才能生效 56 | 57 | ps:如果是在阿里云使用,别忘了配置`安全组` -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | ## 文章列表 6 | - [acme.sh配置ssl https](https://github.com/linpenghui958/note/blob/master/acme.sh%E9%85%8D%E7%BD%AEssl%20https.md "acme.sh配置ssl https") 7 | - [Vue+TypeScript爬坑指南1](https://github.com/linpenghui958/note/blob/master/Vue%2BTypeScript%E7%88%AC%E5%9D%91%E6%8C%87%E5%8D%971.md) 8 | - [Vue+半自动化多页Skeleton Page](https://github.com/linpenghui958/note/blob/master/vue+半动化多页skeleton.md) 9 | - [Vue+ts下的vw适配](https://github.com/linpenghui958/note/blob/master/Vue+ts下的vw适配(第三方库css问题).md) 10 | - [手写简单的js](./手写js.md) 11 | - [深入理解EventLoop](./event_loop/Eventloop学习.md) 12 | - [Flutter零基础介绍](./Flutter零基础介绍.md) 13 | 14 | #### vue深入解析 15 | - [数据驱动](./数据驱动.md) 16 | - [组件化](./组件化.md) 17 | - [编译](./编译.md) 18 | - [响应式原理](./响应式原理.md) 19 | 20 | #### 性能优化 21 | - [2019前端性能优化](./2019前端性能优化.md) 22 | 23 | #### 其他类型 24 | - [ES6代码规范](./js代码规范.md) 25 | - [面向对象](./面向对象.md) 26 | - [网页加载过程及优化](./网页加载过程及优化.md) 27 | - [2019面试不完全汇总](./2019面试.md) 28 | -------------------------------------------------------------------------------- /docs/vue/Vue+TypeScript爬坑指南.md: -------------------------------------------------------------------------------- 1 | ### Vue + TypeScript踩坑(初始化项目) 2 | --- 3 | - 前言,最近公司新项目需要进行重构,框架选择为Vue+TypeScript,记录初始化中不完全踩坑指南 4 |
5 | ##### 1.项目改造 6 | 安装所需的插件 7 | `npm install vue-property-decorator vuex-class --save` 8 | `npm install ts-loader typescript typescript-eslint-parser --save-dev` 9 |
10 | 这些库的作用,可以按需引入 11 | - [vue-property-decorator](https://github.com/kaorun343/vue-property-decorator "vue-property-decorator"):在vue-class-component的基础上优化{Emit, Inject, Model, Prop, Provide, Watch, Component} 12 | - [vuex-class](https://github.com/ktsn/vuex-class "vuex-class"):ts版的vuex插件 13 | - ts-loader: webpack插件 14 | - typescript: 必备插件 15 | - typescript-eslint-parser: eslint解析ts插件 16 | 17 |
18 | ##### 2.WebPack配置修改 19 | webpac.conf中添加ts的解析module 20 | extensions中添加ts结尾的文件 21 | 22 | entry: { 23 | app: './src/main.ts' 24 | }, 25 | // .... 26 | extensions: ['.js', '.vue', '.json', '.ts'], 27 | // .... 28 | { 29 | test: /\.ts$/, 30 | loader: 'ts-loader', 31 | exclude: /node_modules/, 32 | options: { 33 | appendTsSuffixTo: [/\.vue$/], 34 | } 35 | } 36 | 37 | ##### 3.添加tsconfig.json、d.ts文件,修改eslint 38 | 39 | 修改eslint解析规则为ts 40 | 41 | // ... 42 | parserOptions: { 43 | parser: 'typescript-eslint-parser' 44 | }, 45 | // ... 46 | plugins: [ 47 | 'vue', 48 | 'typescript' 49 | ], 50 | 51 | 在.eslintrc.js同级目录创建tsconfig.json文件 52 | 53 | { 54 | "include": [ 55 | "src/*", 56 | "src/**/*" 57 | ], 58 | "exclude": [ 59 | "node_modules" 60 | ], 61 | "compilerOptions": { 62 | // types option has been previously configured 63 | "types": [ 64 | // add node as an option 65 | "node" 66 | ], 67 | // typeRoots option has been previously configured 68 | "typeRoots": [ 69 | // add path to @types 70 | "node_modules/@types" 71 | ], 72 | // 以严格模式解析 73 | "strict": true, 74 | "strictPropertyInitialization": false, 75 | // 在.tsx文件里支持JSX 76 | "jsx": "preserve", 77 | // 使用的JSX工厂函数 78 | "jsxFactory": "h", 79 | // 允许从没有设置默认导出的模块中默认导入 80 | "allowSyntheticDefaultImports": true, 81 | // 启用装饰器 82 | "experimentalDecorators": true, 83 | "strictFunctionTypes": false, 84 | // 允许编译javascript文件 85 | "allowJs": true, 86 | // 采用的模块系统 87 | "module": "esnext", 88 | // 编译输出目标 ES 版本 89 | "target": "es5", 90 | // 如何处理模块 91 | "moduleResolution": "node", 92 | // 在表达式和声明上有隐含的any类型时报错 93 | "noImplicitAny": true, 94 | "lib": [ 95 | "dom", 96 | "es5", 97 | "es6", 98 | "es7", 99 | "es2015.promise" 100 | ], 101 | "sourceMap": true, 102 | "pretty": true 103 | } 104 | } 105 | 106 | 在src目录下添加/typings/vue-shims.d.ts,声明所有的.vue文件 107 | 108 | declare module "*.vue" { 109 | import Vue from "vue"; 110 | export default Vue; 111 | } 112 | 113 |
114 | 115 | ##### 4.对原有文件进行改造 116 | /src/router/index.ts 117 | 118 | import Vue, { AsyncComponent } from 'vue' 119 | import Router, { RouteConfig, Route, NavigationGuard } from 'vue-router' 120 | const Home: AsyncComponent = (): any => import('@/views/Home/index.vue') 121 | 122 | Vue.use(Router) 123 | 124 | const routes: RouteConfig[] = [ 125 | { 126 | path: '/', 127 | name: 'Home', 128 | component: Home 129 | } 130 | ] 131 | 132 | const router: Router = new Router({ 133 | mode: 'history', 134 | base: '/', 135 | routes 136 | }) 137 | 138 | export default router 139 | 140 | /src/store 141 | index.ts 142 | 143 | import Vue from 'vue' 144 | import Vuex, {ActionTree, MutationTree} from 'vuex' 145 | import actions from './actions' 146 | import mutations from './mutations'; 147 | import getters from './getters' 148 | 149 | Vue.use(Vuex) 150 | 151 | interface State { 152 | token: string, 153 | login: Boolean, 154 | } 155 | 156 | let state: State = { 157 | token: 'token', 158 | login: false 159 | } 160 | 161 | export default new Vuex.Store({ 162 | state, 163 | getters, 164 | mutations, 165 | actions 166 | }) 167 | 168 | mutations.ts 169 | 170 | import TYPES from './types' 171 | import { MutationTree } from 'vuex' 172 | 173 | const mutations: MutationTree = { 174 | [TYPES.SET_TOKEN](state, token): void{ 175 | state.token = token 176 | } 177 | } 178 | 179 | export default mutations 180 | 181 | actions.ts 182 | 183 | import { ActionTree } from 'vuex' 184 | import TYPES from './types' 185 | 186 | const actions: ActionTree = { 187 | 188 | initToken({commit}, token: string) { 189 | commit(TYPES.SET_TOKEN, token) 190 | } 191 | } 192 | 193 | export default actions 194 | 195 | /src/main.ts 196 | 197 | import Vue from 'vue'; 198 | import App from './App.vue'; 199 | import store from './store' 200 | import router from './router'; 201 | 202 | Vue.config.productionTip = false 203 | 204 | new Vue({ 205 | el: '#app', 206 | router, 207 | store, 208 | components: { App }, 209 | template: '' 210 | }) 211 | 212 |
213 | ##### 5.vue-property-decorator使用 214 | 215 | vue-property-decorator基于vue-class-component封装 216 | 提供了7种方法 217 | `import {Emit, Inject, Model, Prop, Provide, Watch, Componet}` 218 | 219 | 270 | 271 |
272 | 273 | ##### 错误汇总 274 | 1. typescript版本过新为3.1.x的情况会提示无法找到vue-loader,需要降级到2.8.x 275 | 2. TS2564:Property 'xx' has no initializer and in not definitely assigned in constructor 276 | 在ts新版本中加入的,属性初始化可能为undefined的情况需要考虑进去 277 | 解决方案1,在定义的时候例如 url: string | undefined (不太清楚方法要怎么定义,ts不太熟) 278 | 解决方案2,在tsconfig的compilerOptions中 添加`"strictPropertyInitialization": false,` -------------------------------------------------------------------------------- /docs/vue/Vue+ts下的vw适配(第三方库css问题).md: -------------------------------------------------------------------------------- 1 | ### Vue + ts 下的vw适配(第三方库css问题) 2 | --- 3 | 之前看了大漠老师的VW适配方案,觉得受益匪浅,对比flexible的方案,与js解耦,纯css的适配方案,关于VW的介绍里面有详细的[介绍](https://www.w3cplus.com/mobile/vw-layout-in-vue.html) 4 | 5 | 项目基于vue-cli 6 | 7 | 首先安装所需的插件 8 | `npm i postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-cssnext postcss-viewport-units cssnano --S` 9 | 10 | 接下来在根目录的.postcssrc.js对PostCSS插件进行配置 11 | 12 | module.exports = { 13 | "plugins": { 14 | "postcss-import": {}, 15 | "postcss-url": {}, 16 | // to edit target browsers: use "browserslist" field in package.json 17 | "postcss-write-svg": { 18 | uft8: false 19 | }, 20 | "postcss-cssnext": {}, 21 | "postcss-px-to-viewport": { 22 | viewportWidth: 750, // 设计稿宽度 23 | viewportHeight: 1334, // 设计稿高度,可以不指定 24 | unitPrecision: 3, // px to vw无法整除时,保留几位小数 25 | viewportUnit: 'vw', // 转换成vw单位 26 | selectorBlackList: ['.ignore', '.hairlines'], // 不转换的类名 27 | minPixelValue: 1, // 小于1px不转换 28 | mediaQuery: false // 允许媒体查询中转换 29 | }, 30 | "postcss-viewport-units": {}, 31 | "cssnano": { 32 | preset: "advanced", 33 | autoprefixer: false, // 和cssnext同样具有autoprefixer,保留一个 34 | "postcss-zindex": false 35 | } 36 | } 37 | } 38 | 39 | #####.postcss-px-to-viewport 40 | 用来把px单位转换为vw、vh、vmin或者vmax这样的视窗单位,也是vw适配方案的核心插件之一。 41 | 我们都是使用**750px宽度的视觉设计稿**,那么100vw = 750px,即1vw = 7.5px。那么我们可以根据设计图上的px值直接转换成对应的vw值。在实际撸码过程,不需要进行任何的计算,**直接在代码中写px**。 42 | 43 | #####.postcss-aspect-ratio-mini 44 | 用来处理元素容器宽高比。 45 | 46 | #####.postcss-write-svg 47 | 用来处理移动端1px的解决方案。 48 | 49 | #####vw兼容方案 50 | Viewport Units Buggyfill 51 | 1. 引入js文件 52 | `` 53 | 54 | 2. 在HTML文件中调用viewport-units-buggyfill 55 | 56 | 57 | 60 | 61 | ps: 使用vw的polyfill解决方案会在用到的vw的地方添加content,会影响到img和伪元素,需要全局添加 62 | 63 | `img { content: normal !important; } 64 | 65 | ![](https://i.imgur.com/uHZTO3l.png) 66 | 67 | 小结 68 | 这里作者用的是vue + ts环境,在引入第三方库mint-ui的时候,遇到了一些问题 69 | 70 | 1. 第三方库引入css问题 71 | 在main.ts文件中直接引入mint-ui的css文件会找不到文件 72 | 73 | 74 | import Vue from 'vue' 75 | import MintUI from 'mint-ui' 76 | import 'mint-ui/lib/style.css' // 提示找不到文件 77 | import App from './App.vue' 78 | 79 | Vue.use(MintUI) 80 | 81 | new Vue({ 82 | el: '#app', 83 | components: { App } 84 | }) 85 | 86 | ![](https://i.imgur.com/VEoTX0b.png) 87 | 88 | 笔者在这里的了解一下在github上其他人开源的vue + ts的项目,发现部分解决方案是新建一个style文件,引入node_module内所需的所有文件 89 | 90 | import Vue from 'vue' 91 | import MintUI from 'mint-ui' 92 | import '@/styles/mint-ui.styl' 93 | import App from './App.vue' 94 | 95 | Vue.use(MintUI) 96 | 97 | new Vue({ 98 | el: '#app', 99 | components: { App } 100 | }) 101 | 102 | 103 | mint-ui.styl中 104 | 105 | @import '../../node_modules/mint-ui/lib/style.css' 106 | 在这之前好像跟我们是vw适配没什么联系 107 | 108 | 紧接的就是第二个问题了 109 | 110 | 2. vw打包后,改变了第三方库的px也被转换成vw了 111 | 112 | 这里mint-ui使用的px为单位,所以run dev的时候看到的是正常的,但是build后,postcss-px-to-viewport会将引入的style转换成vw单位 113 | 114 | 这里以mint-ui的picker为例 115 | dev环境时 116 | ![](https://i.imgur.com/ICd7x4k.png) 117 | ![](https://i.imgur.com/lUf5C6E.png) 118 | 119 | build后再打开 120 | ![](https://i.imgur.com/AkOSctC.png) 121 | ![](https://i.imgur.com/gUTmNHh.png) 122 | 123 | 这里在网上查了一下,感觉关于vw的适配方案的文章并不多,没找到类似的解决方案 124 | 后来找到了一个改良版的postcss-px-to-viewport 125 | 添加了exclude选项,将node_modules目录排除掉,即不会受影响 126 | 非常开心的放到了github上 [postcss-px-to-viewport](https://github.com/linpenghui958/postcss-px-to-viewport),用vw的同学,有需要的可以看一下 127 | 128 | 也可以自己在node_modules中找到postcss-px-to-viewport,打开index.js 129 | 新增对exclude选项的处理 130 | 131 | module.exports = postcss.plugin('postcss-px-to-viewport', function (options) { 132 | 133 | var opts = objectAssign({}, defaults, options); 134 | var pxReplace = createPxReplace(opts.viewportWidth, opts.minPixelValue, opts.unitPrecision, opts.viewportUnit); 135 | 136 | return function (css) { 137 | 138 | css.walkDecls(function (decl, i) { 139 | if (options.exclude) { // 添加对exclude选项的处理 140 | if (Object.prototype.toString.call(options.exclude) !== '[object RegExp]') { 141 | throw new Error('options.exclude should be RegExp!') 142 | } 143 | if (decl.source.input.file.match(options.exclude) !== null) return; 144 | } 145 | // This should be the fastest test and will remove most declarations 146 | if (decl.value.indexOf('px') === -1) return; 147 | 148 | if (blacklistedSelector(opts.selectorBlackList, decl.parent.selector)) return; 149 | 150 | decl.value = decl.value.replace(pxRegex, pxReplace); 151 | }); 152 | 153 | if (opts.mediaQuery) { 154 | css.walkAtRules('media', function (rule) { 155 | if (rule.params.indexOf('px') === -1) return; 156 | rule.params = rule.params.replace(pxRegex, pxReplace); 157 | }); 158 | } 159 | 160 | }; 161 | }); 162 | 163 | 164 | 然后在.postcssrc.js添加postcss-px-to-viewport的exclude选项,亲测可用 165 | 166 | "postcss-px-to-viewport": { 167 | viewportWidth: 750, 168 | viewportHeight: 1334, 169 | unitPrecision: 3, 170 | viewportUnit: 'vw', 171 | selectorBlackList: ['.ignore', '.hairlines'], 172 | minPixelValue: 1, 173 | mediaQuery: false, 174 | exclude: /(\/|\\)(node_modules)(\/|\\)/ 175 | }, 176 | 177 | 当然也可以直接 `npm install postcss-px-to-viewport-opt -S` -------------------------------------------------------------------------------- /docs/vue/vue+半动化多页skeleton.md: -------------------------------------------------------------------------------- 1 | ###基于vue-cli实现自动生成Skeleton Page,多页skeleton 2 | --- 3 | 11月15号更新: 4 | 简单粗暴,先上 最新demo地址(有帮助的同学可以点个赞,或者给个star _(:з」∠)_ ) 5 | [vue-skeleton demo](https://github.com/linpenghui958/skeleton-test) 6 | 7 | 文章发布过去半年时间,文章中所提的两个插件都已经进行较大的更新,有朋友跟我咨询后续插件的支持情况。这里我再以page-skeleton-webpack-plugin(0.10.12)以下简称PSWP为例进行实验 8 | 可以看到PSWP[文档](https://github.com/ElemeFE/page-skeleton-webpack-plugin/blob/master/docs/i18n/zh_cn.md)中已经更新了对多页自动生成,和多路由骨架屏的支持。 9 | 下文还是以vue-cli为例 10 | 第一步,新建一个项目,并安装相关依赖 11 | ``` 12 | vue init webpack skeleton-test 13 | cd skeleton-test 14 | npm install 15 | npm install --save-dev page-skeleton-webpack-plugin 16 | npm install --save-dev html-webpack-plugin 17 | ``` 18 | 19 | 第二步,然后在build/webpack.base.conf.js中 20 | ```js 21 | const HtmlWebpackPlugin = require('html-webpack-plugin') 22 | const { SkeletonPlugin } = require('page-skeleton-webpack-plugin') 23 | const path = require('path') 24 | const webpackConfig = { 25 | entry: 'index.js', 26 | output: { 27 | path: __dirname + '/dist', 28 | filename: 'index.bundle.js' 29 | }, 30 | plugin: [ 31 | new HtmlWebpackPlugin({ 32 | // 本身的配置项 33 | }), 34 | new SkeletonPlugin({ 35 | pathname: path.resolve(__dirname, '../shell'), // customPath为来存储 shell 文件的地址 36 | staticDir: path.resolve(__dirname, '../dist'), // 最好和 `output.path` 相同 37 | routes: ['/', '/test'], // 将需要生成骨架屏的路由添加到数组中,测试根路径和/test路径 38 | }) 39 | ] 40 | } 41 | ``` 42 | 第三步,运行项目,生产skeleton-page 43 | - 还是在控制台输入toggleBar后点击页面上方的control bar唤醒 44 | ![](http://pi82b6lei.bkt.clouddn.com/111.png) 45 | 右上角依次为,预览不同路由生产的骨架屏,手机预览,写入本地文件 46 | 可以看到现在更新了许多功能,在预览无误后 47 | 点击右上方写入本地文件即可进行最后的打包预览 48 | ![](http://pi82b6lei.bkt.clouddn.com/222.png) 49 | 可以看到项目目录多了一个shell路径,这跟之前设置的保存路径一致 50 | 51 | 第四步,打包预览效果 52 | 53 | ```js 54 | npm run build 55 | cd dist 56 | http-server 57 | ``` 58 | ![](http://pi82b6lei.bkt.clouddn.com/skeleton11-15-3.png) 59 | 然后进入浏览器预览 60 | 看一下根路径 61 | ![](http://pi82b6lei.bkt.clouddn.com/screen12.gif) 62 | 看一下/test路径 63 | ![](http://pi82b6lei.bkt.clouddn.com/skeleton-screen13.gif) 64 | 65 | ps!!!前方高能预警 66 | 这里是以vue-cli为基础进行的测试 67 | 在初始化的模板中,webpack分为base、dev、 prod三个文件 68 | 这里因为PSWP为dev和prod都需要的插件,所以放在base文件中 69 | 但是在第一次打包后预览,笔者发现prod环境并没有生效 70 | 原因是在webpack.prod.conf.js中的HtmlWebpackPlugin配置中 71 | ```js 72 | plugins: [ 73 | new HtmlWebpackPlugin({ 74 | filename: config.build.index, 75 | template: 'index.html', 76 | inject: true, 77 | minify: { 78 | // removeComments: true, 移除注释 79 | collapseWhitespace: true, 80 | removeAttributeQuotes: true 81 | // more options: 82 | // https://github.com/kangax/html-minifier#options-quick-reference 83 | }, 84 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 85 | chunksSortMode: 'dependency' 86 | }), 87 | ] 88 | ``` 89 | 这里有一项minify的配置为移除注释,查看minify的[文档](https://github.com/kangax/html-minifier#options-quick-reference) 90 | ![](http://pi82b6lei.bkt.clouddn.com/skeleton11-15-4.png) 91 | 笔者觉得很有可能是这个配置,移除了index.html中的给移除了,导致打包后未能生效。 92 | 在注释掉这一选项后,dev和prod环境均正常 93 | 94 | 以下为原内容 95 | ----- 96 | 97 | 之前看eleme的专栏了解到骨架页面 98 | 这里刚好项目重构尝试将Skeleton Page引入项目中 99 | 其中遇到一些问题和一些坑,分享一下 100 | 101 | 首先用到的是[page-skeleton-webpack-plugin](https://github.com/ElemeFE/page-skeleton-webpack-plugin "page-skeleton-webpack-plugin") 102 | eleme开源的一款自动生成skeleton-page的插件 103 | 使用起来也是非常的简单粗暴 104 |
105 | 项目是基于vue-cli + ts 106 | #### page-skeleton-webpack-plugin使用 107 | 首先通过npm安装插件,该插件依赖于[html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) 108 | > npm install --save-dev page-skeleton-webpack-plugin 109 | > npm install --save-dev html-webpack-plugin 110 | 111 | - 配置篇 112 | 首先,因为dev和prod环境都需要用到这个插件,可以直接在webpack.base.conf.js中的config中添加对应的plugins 113 | 114 | 1. 第一步 115 | 116 | // 引入插件 117 | const { SkeletonPlugin } = require('page-skeleton-webpack-plugin') 118 | 119 | //...其他配置 120 | module.exports = { 121 | context: path.resolve(__dirname, '../'), 122 | entry: { 123 | app: './src/main.ts' 124 | }, 125 | output: { //... 126 | }, 127 | resolve: { //... 128 | }, 129 | module: { //... 130 | }, 131 | node: { // ... 132 | }, 133 | plugins: [ 134 | new SkeletonPlugin({ 135 | pathname: path.resolve(__dirname, '../static'), // 生成名为 shell 文件存放地址 136 | headless: false // 打开非headless chrome 137 | }) 138 | ] 139 | } 140 | 141 | 2. 第二步 142 | 在index.html里面添加skeleton-page要注入的地方 143 | 144 | 145 | 146 | 147 | 148 | Document 149 | 150 | 151 |
152 | // 在#app里添加 153 |
154 | 155 | 156 | 157 | 配置就这样完成了,是不是很简单粗暴,使用起来也是很简单 158 | 159 | - 使用篇 160 | 在chrome中运行项目后 161 | 1.使用Ctrl|Cmd + enter 呼出插件交互界面后者在控制台输入toggelBar呼出交互界面 162 | ![](https://i.imgur.com/kt3aPmW.png) 163 | 2.点击上放的按钮 164 | ![](https://i.imgur.com/wFZ9uxd.png) 165 | 点击写入,即可看到生在static(plugins里面配置的目录)里的shell.html了 166 | ![](https://i.imgur.com/q2eYEVP.png) 167 | 168 | 最后将项目打包,运行起来,就能看到对应的skeleton-page了 169 | ![](https://i.imgur.com/gDgaDn0.gif) 170 | 还有更多的参数配置,请参考仓库地址[page-skeleton-webpack-plugin](https://github.com/ElemeFE/page-skeleton-webpack-plugin "page-skeleton-webpack-plugin") 171 | 172 | **这里我还碰到了一个问题** 173 | 就是运行插件式,系统提示windows找不到chrome 174 | ![](https://i.imgur.com/7K9fB9a.gif) 175 | ![](https://i.imgur.com/vPvSfWd.png) 176 | 再使用Puppeteer的demo排除了生成skeleton界面的问题后 177 | 在inssue里跟大神交流后 178 | 知道插件用到了一个叫[opn](https://github.com/sindresorhus/opn)的库来打开chrome 179 | 这里提到了不同环境下的app名字是不一样的 180 | `app` 181 | `Type: string Array` 182 | `Specify the app to open the target with, or an array with the app and app arguments.` 183 | `The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is google chrome on macOS, google-chrome on Linux and chrome on Windows.` 184 | 所以在不同的环境下启动,需要寻找对应的名字 185 | 找到问题,大神给出了解决方案 186 | 在node_modules/page-skeleton-webpack-plugin/src/server.js 187 | 188 | //修改前 189 | open({this.previewPageUrl, app: google chrome}) 190 | //修改后 191 | open(this.previewPageUrl, { 192 | app: ['chrome', '--incognito'] 193 | }) 194 | 这样系统就能顺利的找到chrome,完成生成页面后的操作了! 195 | 196 | 但是这里我又有个问题了,如果用户通过不同的链接进入项目,那么每个地址都会显示这个固定的骨架屏, 197 | #### 如何才能为不同的页面,配置不同的骨架屏呢 198 | 199 | 这里我找到了另外一个库[vue-skeleton-webpack-plugin](https://github.com/lavas-project/vue-skeleton-webpack-plugin),是需要手动编写skeleton-page,但是可以配置多页面对应多骨架屏 200 | 201 | - 配置篇 202 | 203 | 首先需要安装对应的插件 204 | > npm install vue-skeleton-webpack-plugin 205 | 206 | 插件在dev可以配置预览的路由,在prod环境下使用 207 | 1. 在webpack.prod.conf.js中引入插件 208 | 209 | // 引入插件 210 | const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin') 211 | // plugin里添加配置 212 | plugins: [ 213 | new SkeletonWebpackPlugin({ 214 | webpackConfig: require('./webpack.skeleton.conf'), // skeleton配置文件 215 | router: { 216 | routes: [ // path是对应的路由,skeletonId是对应骨架屏的id(在后面的entry-skeleton.ts中可以看到) 217 | {path: '/index', skeletonId: 'skeleton2'}, 218 | {path: '/', skeletonId: 'skeleton1'} 219 | ] 220 | } 221 | }), 222 | ] 223 | 224 | 2. 同级目录下的webpack.skeleton.conf.js配置文件 225 | 226 | 'use strict'; 227 | 228 | const webpack = require('webpack'); 229 | const config = require('../config'); 230 | const path = require('path') 231 | const merge = require('webpack-merge') 232 | const baseWebpackConfig = require('./webpack.base.conf') 233 | const nodeExternals = require('webpack-node-externals') 234 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 235 | 236 | function resolve(dir) { 237 | return path.join(__dirname, dir) 238 | } 239 | 240 | module.exports = merge(baseWebpackConfig, { 241 | target: 'node', 242 | devtool: false, 243 | entry: { 244 | app: resolve('../src/entry-skeleton.ts') // skeleton入口文件 245 | }, 246 | output: Object.assign({}, baseWebpackConfig.output, { 247 | filename: 'skeleton-bundle.js', 248 | libraryTarget: 'commonjs2' 249 | }), 250 | externals: nodeExternals({ 251 | whitelist: /\.css$/ 252 | }), 253 | plugins: [ 254 | new webpack.DefinePlugin({ 255 | 'process.env': config.build.env 256 | }), 257 | 258 | // Compress extracted CSS. We are using this plugin so that possible 259 | // duplicated CSS from different components can be deduped. 260 | new OptimizeCSSPlugin({ 261 | cssProcessorOptions: { 262 | safe: true 263 | } 264 | }) 265 | ] 266 | }) 267 | 268 | 3. ../src/entry-skeleton.ts入口文件 269 | 270 | import Vue from 'vue'; 271 | import Skeleton1 from './skeleton/HomeSkeleton.vue'; // 事先写好的skeleton-page页面 272 | import Skeleton2 from './skeleton/HomeSkeleton2.vue'; 273 | 274 | export default new Vue({ 275 | components: { 276 | Skeleton1, // 注册对应的组件 277 | Skeleton2 278 | }, 279 | template: ` 280 |
// 这里对应的id就是plugins里面不同页面对应的skeletonId 281 |
284 | ` 285 | }); 286 | 效果图 287 | /index 对应的 id skeleton2 288 | ![](https://i.imgur.com/VABboqs.gif) 289 | / 对应的 id skeleton1 290 | ![](https://i.imgur.com/HSryVBj.gif) 291 | 更多的配置参考仓库地址 [vue-skeleton-webpack-plugin](https://github.com/lavas-project/vue-skeleton-webpack-plugin) 292 | 这样,就能实现多页对应多个skeleton-page了 293 | 再结合前面的自动生成skeleton-page是不是算半自动花了,- -笑哭 294 | 有心之人可以结合两个插件,全自动多页对应多skeleton-page指日可待了! 295 | 296 | -------------------------------------------------------------------------------- /event_loop/1531538160931.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531538160931.png -------------------------------------------------------------------------------- /event_loop/1531538317857.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531538317857.png -------------------------------------------------------------------------------- /event_loop/1531541696514.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541696514.png -------------------------------------------------------------------------------- /event_loop/1531541718526.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541718526.png -------------------------------------------------------------------------------- /event_loop/1531541742410.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541742410.png -------------------------------------------------------------------------------- /event_loop/1531541758392.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541758392.png -------------------------------------------------------------------------------- /event_loop/1531541777200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541777200.png -------------------------------------------------------------------------------- /event_loop/1531541780456.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541780456.png -------------------------------------------------------------------------------- /event_loop/1531541796985.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541796985.png -------------------------------------------------------------------------------- /event_loop/1531541834771.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541834771.png -------------------------------------------------------------------------------- /event_loop/1531541854818.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/event_loop/1531541854818.png -------------------------------------------------------------------------------- /event_loop/Eventloop学习.md: -------------------------------------------------------------------------------- 1 | @(JS基础) 2 | ####Eventloop学习 3 | --- 4 | ##### 为什么会有Event loop 5 | > 因为Javascript设计之初就是一门单线程语言,因此为了实现主线程的不阻塞,Event Loop这样的方案应运而生。 6 | #####先来两题开胃小菜 7 | ps: 没兴趣做题可以跳过直接看结论 8 | - 第一题 9 | 10 | setImmediate(function(){ 11 | console.log(1); 12 | },0); 13 | setTimeout(function(){ 14 | console.log(2); 15 | },0); 16 | new Promise(function(resolve){ 17 | console.log(3); 18 | resolve(); 19 | console.log(4); 20 | }).then(function(){ 21 | console.log(5); 22 | }); 23 | console.log(6); 24 | process.nextTick(function(){ 25 | console.log(7); 26 | }); 27 | console.log(8); 28 |
29 | 30 | 结果是: 3 4 6 8 7 5 2 1 31 | 32 | - 第二题 33 | 34 | console.log('golb'); 35 | 36 | setTimeout(function() { 37 | console.log('timeout'); 38 | process.nextTick(function() { 39 | console.log('timeout_nextTick'); 40 | }) 41 | new Promise(function(resolve) { 42 | console.log('timeout_promise'); 43 | resolve(); 44 | }).then(function() { 45 | console.log('timeout_then') 46 | }) 47 | }) 48 | 49 | setImmediate(function() { 50 | console.log('immediate'); 51 | process.nextTick(function() { 52 | console.log('immediate_nextTick'); 53 | }) 54 | new Promise(function(resolve) { 55 | console.log('immediate_promise'); 56 | resolve(); 57 | }).then(function() { 58 | console.log('immediate_then') 59 | }) 60 | }) 61 | 62 | process.nextTick(function() { 63 | console.log('glob_nextTick'); 64 | }) 65 | new Promise(function(resolve) { 66 | console.log('glob_promise'); 67 | resolve(); 68 | }).then(function() { 69 | console.log('glob_then') 70 | }) 71 | 72 |
73 | 74 | 答案:golb 75 | glob_promise 76 | glob_nextTick 77 | glob_then 78 | timeout 79 | timeout_promise 80 | timeout_nextTick 81 | timeout_then 82 | immediate 83 | immediate_promise 84 | immediate_nextTick 85 | immediate_then 86 | 87 | 不知道第二题你是否答对了呢 88 | #####先上结论 89 | v8实现中,两个队列各包含不同的任务 90 | 91 | macrotasks: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering 92 | microtasks: process.nextTick, Promises, Object.observe, MutationObserver 93 | 94 | ##### 执行过程如下: 95 | JavaScript引擎首先从macrotask queue中取出第一任务 96 | 执行完毕后,将microtask queue中的所有任务取出,按顺序执行 97 | 然后再从macrotask queue中取下一个, 98 | 执行完毕后,再次将microtask queue中的全部取出, 99 | 循环往复,直到两个queue中的任务都取完 100 | 101 | **这里有两个问题, 1、macrotasks中任务的执行顺序 2、process.nextTick和promise.then优先顺序** 102 |
103 | 104 | ##### nodejs源码libuv 105 | 找到libuv中跟event loop相关的源码 106 | ###### src/unix/core.c - uv_run 107 | 108 | ![Alt text](./1531538160931.png) 109 | 110 | 可以看到uv_run方法通过while循环依次运行 `uv__run_timers`、`uv__run_pending`、`uv__run_idle`、`uv__run_prepare`、`uv__io_poll`、`uv__run_check`、`uv__run_closing_handles` 111 | 通过这8个函数的执行,实现Event Loop 112 | 113 | 参考nodejs官网的doc中对Event Loop的概述 114 | ![Alt text](./1531538317857.png) 115 | 116 | - timers 定时器阶段(setTimeout setInterval) 对应 uv__run_timers 117 | - pending callback 执行某些系统错误的回调 对应 uv__run_pending 118 | - idel, prepare 仅在内部使用 119 | - poll 轮询阶段 对应 uv__io_poll 120 | - check setImmediate在这里执行 对应 uv__run_check 121 | - close 关闭回调 对应 uv__run_closing_handles 122 | 123 | 这里重点说一下poll阶段(poll阶段会执行到期的定时器回调,处理poll队列中的回调) 124 | 当Event Loop进入poll阶段,将会发生以下两种情况 125 | - `如果poll队列不为空的情况下` 126 | Event Loop将会循环同步执行他们的回调,知道队列执行完毕或者系统的硬件限制达到上限 127 | 128 | - `如果poll队列为空的情况下` 129 | 如果有注册的setImmediate则在该阶段执行完毕后,前往check阶段执行,如果没有则检查是否有timers阶段定时到期的回调,有的话则前往timers阶段执行 130 | 131 | 132 | `result:` 在Event Loop的6个阶段,从上到下依次轮转,每个阶段都有先进先出的一个**回调队列**,只有当任务队列**执行完毕**或者达到**硬件上限**,才会进入下一个阶段,而每一个阶段之间会**清空**microtask(process.nextTick和promise),直到运行完所有的队列 133 | 134 | ##### process.nextTick和Promise 135 | 首先我们要知道 process.nextTick, Microtasks 以及 Promise 的 callback 都是通过一个队列 nextTickQueue 调度, 而这一切都是从_tickCallback ( _tickDomainCallback )开始的。 136 | 137 | 在Node中,_tickCallback在每一次执行完TaskQueue(macrotask)中的一个任务后被调用 138 | `_tickCallback中实质上干了两件事` 139 | 1. nextTickQueue中所有任务执行掉 140 | 2. 第一步执行完后执行_runMicrotasks函数,执行microtask中的部分(promise.then注册的回调) 141 | 142 | 所以 process.nextTick > promise.then 143 | 144 | 145 | #### 最后分享一个题目详解 146 | [参考链接](https://www.jianshu.com/p/12b9f73c5a4f#) 147 | 代码 148 | 149 | console.log('golb1'); 150 | 151 | setTimeout(function() { 152 | console.log('timeout1'); 153 | process.nextTick(function() { 154 | console.log('timeout1_nextTick'); 155 | }) 156 | new Promise(function(resolve) { 157 | console.log('timeout1_promise'); 158 | resolve(); 159 | }).then(function() { 160 | console.log('timeout1_then') 161 | }) 162 | }) 163 | 164 | setImmediate(function() { 165 | console.log('immediate1'); 166 | process.nextTick(function() { 167 | console.log('immediate1_nextTick'); 168 | }) 169 | new Promise(function(resolve) { 170 | console.log('immediate1_promise'); 171 | resolve(); 172 | }).then(function() { 173 | console.log('immediate1_then') 174 | }) 175 | }) 176 | 177 | process.nextTick(function() { 178 | console.log('glob1_nextTick'); 179 | }) 180 | new Promise(function(resolve) { 181 | console.log('glob1_promise'); 182 | resolve(); 183 | }).then(function() { 184 | console.log('glob1_then') 185 | }) 186 | 187 | setTimeout(function() { 188 | console.log('timeout2'); 189 | process.nextTick(function() { 190 | console.log('timeout2_nextTick'); 191 | }) 192 | new Promise(function(resolve) { 193 | console.log('timeout2_promise'); 194 | resolve(); 195 | }).then(function() { 196 | console.log('timeout2_then') 197 | }) 198 | }) 199 | 200 | process.nextTick(function() { 201 | console.log('glob2_nextTick'); 202 | }) 203 | new Promise(function(resolve) { 204 | console.log('glob2_promise'); 205 | resolve(); 206 | }).then(function() { 207 | console.log('glob2_then') 208 | }) 209 | 210 | setImmediate(function() { 211 | console.log('immediate2'); 212 | process.nextTick(function() { 213 | console.log('immediate2_nextTick'); 214 | }) 215 | new Promise(function(resolve) { 216 | console.log('immediate2_promise'); 217 | resolve(); 218 | }).then(function() { 219 | console.log('immediate2_then') 220 | }) 221 | }) 222 | 223 | 这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。 224 | 225 | 第一步:宏任务script首先执行。全局入栈。glob1输出。 226 | ![Alt text](./1531541696514.png) 227 | 第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。 228 | 229 | setTimeout(function() { 230 | console.log('timeout1'); 231 | process.nextTick(function() { 232 | console.log('timeout1_nextTick'); 233 | }) 234 | new Promise(function(resolve) { 235 | console.log('timeout1_promise'); 236 | resolve(); 237 | }).then(function() { 238 | console.log('timeout1_then') 239 | }) 240 | }) 241 | 242 | ![Alt text](./1531541718526.png) 243 | 第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。 244 | 245 | setImmediate(function() { 246 | console.log('immediate1'); 247 | process.nextTick(function() { 248 | console.log('immediate1_nextTick'); 249 | }) 250 | new Promise(function(resolve) { 251 | console.log('immediate1_promise'); 252 | resolve(); 253 | }).then(function() { 254 | console.log('immediate1_then') 255 | }) 256 | }) 257 | ![Alt text](./1531541742410.png) 258 | 第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。 259 | 260 | process.nextTick(function() { 261 | console.log('glob1_nextTick'); 262 | }) 263 | ![Alt text](./1531541758392.png) 264 | 第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。 265 | 266 | new Promise(function(resolve) { 267 | console.log('glob1_promise'); 268 | resolve(); 269 | }).then(function() { 270 | console.log('glob1_then') 271 | }) 272 | ![Alt text](./1531541777200.png) 273 | ![Alt text](./1531541780456.png) 274 | 第六步:执行遇到第二个setTimeout。 275 | 276 | setTimeout(function() { 277 | console.log('timeout2'); 278 | process.nextTick(function() { 279 | console.log('timeout2_nextTick'); 280 | }) 281 | new Promise(function(resolve) { 282 | console.log('timeout2_promise'); 283 | resolve(); 284 | }).then(function() { 285 | console.log('timeout2_then') 286 | }) 287 | }) 288 | ![Alt text](./1531541796985.png) 289 | 第七步:先后遇到nextTick与Promise 290 | 291 | process.nextTick(function() { 292 | console.log('glob2_nextTick'); 293 | }) 294 | new Promise(function(resolve) { 295 | console.log('glob2_promise'); 296 | resolve(); 297 | }).then(function() { 298 | console.log('glob2_then') 299 | }) 300 | ![Alt text](./1531541813563.png) 301 | 第八步:再次遇到setImmediate。 302 | 303 | setImmediate(function() { 304 | console.log('immediate2'); 305 | process.nextTick(function() { 306 | console.log('immediate2_nextTick'); 307 | }) 308 | new Promise(function(resolve) { 309 | console.log('immediate2_promise'); 310 | resolve(); 311 | }).then(function() { 312 | console.log('immediate2_then') 313 | }) 314 | }) 315 | ![Alt text](./1531541834771.png) 316 | 这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。 317 | 318 | 其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。 319 | 320 | 当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。 321 | 322 | 这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。 323 | ![Alt text](./1531541854818.png) 324 | setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。 325 | 326 | 只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。 327 | 328 | setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。 329 | 330 | 当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。 -------------------------------------------------------------------------------- /flutter/Flutter FPS 监控.md: -------------------------------------------------------------------------------- 1 | ### 如何获取Flutter APP的FPS 2 | 3 | 众所周知,我们需要衡量一个APP的性能数据,其中FPS也作为其中一个非常重要的标准。 4 | 5 | 这里我们了解一下如何获取Flutter应用中的FPS性能数据。 6 | 7 | ### FPS是什么 8 | 9 | **帧率**是用于测量显示帧数的[量度](https://zh.wikipedia.org/wiki/量度),可产生的图像的数量 10 | 计量单位是帧/秒(Frame Per Second,FPS) 11 | 通常是评估硬件性能与游戏体验流畅度的指标 12 | 13 | ### Flutter的渲染过程 14 | 15 | Flutter 关注如何尽可能快地在两个硬件时钟的 VSync 信号之间计算并合成视图数据,然后通过 Skia 交给 GPU 渲染:UI 线程使用 Dart 来构建视图结构数据,这些数据会在 GPU 线程进行图层合成,随后交给 Skia 引擎加工成 GPU 数据,而这些数据会通过 OpenGL 最终提供给 GPU 渲染 16 | 17 | ![img](http://km.oa.com/files/photos/pictures/202008/1597306977_34_w1870_h450.png) 18 | 19 | ### 本地调试获取FPS 20 | 21 | 官方提供了许多在开发Flutter APP的过程中查看FPS等性能的工具。 22 | 23 | - [devtool](https://github.com/flutter/devtools) 24 | 25 | DevTool 的 [Timeline] 界面可以让开发者逐帧分析应用的 UI 性能,具体的使用方式可以看一下[官方文档](https://flutter.dev/docs/perf/rendering/ui-performance) 26 | 27 | - 性能图层 28 | 29 | ![Screenshot of overlay showing zero jank](https://flutter.cn/assets/tools/devtools/performance-overlay-green-bb41b466cf6bcd529b285e1510b638086fc5afb8921b8ac5a6565dee5bc44788.png) 30 | 31 | 在这些工具中我们只能在本地开发过程中获取FPS数据,如果要统计线上用户的真实数据,要在Flutter代码中计算FPS又该如何做呢? 32 | 33 | ### 生成环境获取FPS 34 | 35 | #### Flutter相关性能指标定义 36 | 37 | 在阅读官方文档的时候,有一个[FrameTiming](https://api.flutter.dev/flutter/dart-ui/FrameTiming-class.html)类描述了每一帧的时间相关的性能指标。 38 | 39 | > If you're using the whole Flutter framework, please use [SchedulerBinding.addTimingsCallback](https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addTimingsCallback.html) to get this. It's preferred over using [Window.onReportTimings](https://api.flutter.dev/flutter/dart-ui/Window/onReportTimings.html) directly because [SchedulerBinding.addTimingsCallback](https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addTimingsCallback.html) allows multiple callbacks. If [SchedulerBinding](https://api.flutter.dev/flutter/scheduler/SchedulerBinding-mixin.html) is unavailable, then see [Window.onReportTimings](https://api.flutter.dev/flutter/dart-ui/Window/onReportTimings.html) for how to get this. 40 | 41 | 这里更推荐使用SchedulerBinding.addTimingsCallBack来获取FPS相关数据。该回调允许多个回调方法,如果该方法不可用才考虑使用Window.onReportTimings。 42 | 43 | #### 性能数据获取 44 | 45 | 这里看一下文档中[addTimingsCallback](https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addTimingsCallback.html)的定义。 46 | 47 | > Add a [TimingsCallback](https://api.flutter.dev/flutter/dart-ui/TimingsCallback.html) that receives [FrameTiming](https://api.flutter.dev/flutter/dart-ui/FrameTiming-class.html) sent from the engine. 48 | 49 | 添加一个TimingsCallback从engine接受发送的FrameTiming信息。接下来看一下具体代码中的定义,了解一下如何使用该方法。 50 | 51 | `flutter/src/scheduler/binding.dart` 52 | 53 | ```dart 54 | void addTimingsCallback(TimingsCallback callback) { 55 | _timingsCallbacks.add(callback); 56 | if (_timingsCallbacks.length == 1) { 57 | assert(window.onReportTimings == null); 58 | window.onReportTimings = _executeTimingsCallbacks; 59 | } 60 | assert(window.onReportTimings == _executeTimingsCallbacks); 61 | } 62 | ``` 63 | 64 | 这里就是对window.onReportTimings的处理进行了封装了。首先用一个叫_timingsCallbacks的List保存了添加的回调,然后初始化时给window.onReportTimings赋值_executeTimingsCallbacks方法。这里_executeTimingsCallbacks会对前面保存的回调List进行遍历执行。 65 | 66 | 知道了addTimingsCallback做了什么,我们再看一下这里callback的定义。 67 | 68 | `sky_engine/ui/window.dart` 69 | 70 | ```dart 71 | /// {@template dart.ui.TimingsCallback.list} 72 | /// The callback takes a list of [FrameTiming] because it may not be 73 | /// immediately triggered after each frame. Instead, Flutter tries to batch 74 | /// frames together and send all their timings at once to decrease the 75 | /// overhead (as this is available in the release mode). The list is sorted in 76 | /// ascending order of time (earliest frame first). The timing of any frame 77 | /// will be sent within about 1 second (100ms if in the profile/debug mode) 78 | /// even if there are no later frames to batch. The timing of the first frame 79 | /// will be sent immediately without batching. 80 | /// {@endtemplate} 81 | typedef TimingsCallback = void Function(List timings); 82 | ``` 83 | 84 | 上方的注释写到,这个回调接受一个FrameTiming的List,Flutter会尝试将这些帧合并后一次性发送,以减少开销。正常情况下一秒内会发送完所有帧,如果在profile/debug模式下,时间会缩短到100毫秒内。 85 | 86 | 简而言之,callback将会得到一个**FrameTiming的List**。 87 | 88 | #### 具体信息分析 89 | 90 | 这里知道在回调中可以拿到的是FrameTiming了,接下来看一下,如果通过这个帧信息可以获取到那些信息呢。 91 | 92 | `sky_engine/ui/window.dart` 93 | 94 | ``` 95 | class FrameTiming { 96 | /// 使用以微秒为单位的原始时间戳来构造[FrameTiming] 97 | /// 98 | /// 这个构建函数仅在单元测试中使用,如果需要获取真实的[FrameTiming]数据请通过[Window.onReportTimings]中获取 99 | factory FrameTiming({ 100 | required int vsyncStart, 101 | required int buildStart, 102 | required int buildFinish, 103 | required int rasterStart, 104 | required int rasterFinish, 105 | }) { 106 | return FrameTiming._([ 107 | vsyncStart, 108 | buildStart, 109 | buildFinish, 110 | rasterStart, 111 | rasterFinish 112 | ]); 113 | } 114 | 115 | /// Construct [FrameTiming] with raw timestamps in microseconds. 116 | /// 117 | /// [timestamps]List必须要有一个同样长度的[FramePhase.values]List 118 | 119 | FrameTiming._(List timestamps) 120 | : assert(timestamps.length == FramePhase.values.length), _timestamps = timestamps; 121 | 122 | int timestampInMicroseconds(FramePhase phase) => _timestamps[phase.index]; 123 | 124 | Duration _rawDuration(FramePhase phase) => Duration(microseconds: _timestamps[phase.index]); 125 | 126 | /// 在UI线程上构建帧持续的时间。 127 | /// 128 | /// 构建开始的时机大概是当[Window.onBeginFrame]被调用时。[Window.onBeginFrame]回调中的[Duration]就是`Duration(microseconds: timestampInMicroseconds(FramePhase.buildStart))` 129 | /// 130 | /// 构建结束的时机大概是当[Window.render]被调用时。 131 | /// 132 | /// {@template dart.ui.FrameTiming.fps_smoothness_milliseconds} 133 | /// 为了确保x fps平滑动画,这里的时间不应该超过1000/x毫秒。 (x即为fps值,例60, 120) 134 | /// {@endtemplate} 135 | /// {@template dart.ui.FrameTiming.fps_milliseconds} 136 | /// 60fps约为16ms,120fps约为8ms; 137 | /// {@endtemplate} 138 | Duration get buildDuration => _rawDuration(FramePhase.buildFinish) - _rawDuration(FramePhase.buildStart); 139 | 140 | /// 在GPU线程上光栅化帧的持续时间。 141 | /// 142 | /// {@macro dart.ui.FrameTiming.fps_smoothness_milliseconds} 143 | /// {@macro dart.ui.FrameTiming.fps_milliseconds} 144 | Duration get rasterDuration => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.rasterStart); 145 | 146 | /// 在接收到vsync信号并开始构建该帧所花费的时间。 147 | Duration get vsyncOverhead => _rawDuration(FramePhase.buildStart) - _rawDuration(FramePhase.vsyncStart); 148 | 149 | /// 构建开始到栅格化结束的时间。 150 | /// 151 | /// 继续强调这里的时间不应该超过1000/x毫秒。 152 | /// {@macro dart.ui.FrameTiming.fps_milliseconds} 153 | /// 154 | /// See also [vsyncOverhead], [buildDuration] and [rasterDuration]. 155 | Duration get totalSpan => _rawDuration(FramePhase.rasterFinish) - _rawDuration(FramePhase.vsyncStart); 156 | 157 | final List _timestamps; // in microseconds 158 | 159 | String _formatMS(Duration duration) => '${duration.inMicroseconds * 0.001}ms'; 160 | 161 | @override 162 | String toString() { 163 | return '$runtimeType(buildDuration: ${_formatMS(buildDuration)}, rasterDuration: ${_formatMS(rasterDuration)}, vsyncOverhead: ${_formatMS(vsyncOverhead)}, totalSpan: ${_formatMS(totalSpan)})'; 164 | } 165 | } 166 | ``` 167 | 168 | 这里`FrameTiming`获取帧相关的时间,其实都是通过`FramePhase`上的属性来计算的。看一下该类的具体定义。 169 | 170 | ``` 171 | /// 帧的生命周期中各个重要的时间点。 172 | /// [FrameTiming]记录了用于性能分析的每个阶段的时间戳。 173 | enum FramePhase { 174 | /// 当接收到操作系统vsync信号的时间戳 175 | /// See also [FrameTiming.vsyncOverhead]. 176 | vsyncStart, 177 | 178 | /// 当UI线程开始绘制一个帧。 179 | /// See also [FrameTiming.buildDuration]. 180 | buildStart, 181 | 182 | /// 当UI线程结束帧的绘制。 183 | /// See also [FrameTiming.buildDuration]. 184 | buildFinish, 185 | 186 | /// 当GPU线程开始栅格化帧时。 187 | /// See also [FrameTiming.rasterDuration]. 188 | rasterStart, 189 | 190 | /// 当GPU线程完成栅格化帧时。 191 | /// See also [FrameTiming.rasterDuration]. 192 | rasterFinish, 193 | } 194 | ``` 195 | 196 | 现在知道了如果获取最近`N个FrameTiming`和每个FrameTiming中所含有的时间戳信息,接下来看一下如果进行实际的FPS计算了。 197 | 198 | #### 计算FPS 199 | 200 | 理所当然的去想,我们可以获取`总帧数`(FrameTiming List的长度),总共的`耗时`(尾帧时间减去首帧时间)。是不是轻而易举就能算出FPS了呢。 201 | 202 | ```dart 203 | double get fps { 204 | int frames = lastFrames.length; 205 | var start = lastFrames.last.timestampInMicroseconds(FramePhase.buildStart); 206 | var end = lastFrames.first.timestampInMicroseconds(FramePhase.rasterFinish); 207 | var duration = (end - start) / Duration.microsecondsPerMillisecond; 208 | 209 | return frames * Duration.millisecondsPerSecond / duration; 210 | } 211 | ``` 212 | 213 | 这样算出来的结果完全对不上,这是为什么呢。 214 | 215 | 其实,`window.onReportTimings` 只会在有帧被绘制时才有数据回调,换句话说,你没有和app发生交互、界面状态没有变化(setState)、没有定时刷布局(动画)等等没有新的帧产生,所以`lastFrames`里存的可能是分属不同“绘制时间段”的帧信息。 216 | 217 | **假设**一秒最多绘制 60 帧,每帧消耗的时间 `frameInterval` 为: 218 | 219 | ```dart 220 | const REFRESH_RATE = 60; 221 | const frameInterval = const Duration(microseconds: Duration.microsecondsPerSecond ~/ REFRESH_RATE); 222 | ``` 223 | 224 | Flutter引擎每次在收到vsync信号的时候会去调用drawFrame方法,这里如果一帧所花费的时间超过`frameInterval`,则可能会出现丢帧的情况。 225 | 226 | 并且如果`lastFrames`里面相邻的两个帧开始、结束时间相差过大 227 | 228 | ```dart 229 | List framesSet = []; 230 | // 每帧耗时 先写死16.6ms 231 | static double frameInterval = 16600; 232 | SchedulerBinding.instance.addTimingsCallback((List timings) { 233 | timings.forEach(framesSet.add); 234 | // 当时间间隔大于1s,则计算一次FPS 235 | if (shouldReport()) { 236 | startTime = getTime(); 237 | processor(framesSet); 238 | framesSet = []; 239 | } 240 | }); 241 | 242 | double processor(List timings) { 243 | int sum = 0; 244 | for (final FrameTiming timing in timings) { 245 | // 计算渲染耗时 246 | final int duration = timing.timestampInMicroseconds(FramePhase.rasterFinish) - 247 | timing.timestampInMicroseconds(FramePhase.buildStart); 248 | // 判断耗时是否在 Vsync 信号周期内 249 | if (duration < frameInterval) { 250 | sum += 1; 251 | } else { 252 | // 有丢帧,向上取整 253 | final int count = (duration / frameInterval).ceil(); 254 | sum += count; 255 | } 256 | } 257 | 258 | final double fps = timings.length / sum * 60; 259 | return fps; 260 | } 261 | 262 | 263 | ``` 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /flutter/本地编译FlutterEngine.md: -------------------------------------------------------------------------------- 1 | ## 本地编译FlutterEngine 2 | 3 | [toc] 4 | 5 | > 在Flutter的一些深度开发过程中,会遇到需要对Flutter Engine进行修改、定制的情况。这里就需要了解Flutter Engine的编译、打包等流程。这里简单介绍一下如果在本地编译Flutter Engine。 6 | 7 | 8 | ### 工具部分 9 | 10 | 1. 介绍 11 | - [gclient](https://www.chromium.org/developers/how-tos/depottools),谷歌开发的一套跨平台git仓库管理工具,用来将多个git仓库组成一个solution进行管理,通过gclient获取我们编译所需源码和依赖。 12 | - [ninja](https://ninja-build.org/),编译工具,负责最终编译可执行的文件。 13 | - [gn](https://gn.googlesource.com/gn),负责生产ninja所需的构建文件,像Flutter这种跨多操作系统、多平台、多CPU架构的,就需要gn生产多套不同的ninja构建文件(Ninja build files)。 14 | 15 | 2. 安装 16 | - homebrew 17 | 18 | ```bash 19 | /usr/bin/ruby -e "$(curl -fsSL [https://raw.githubusercontent.com/Homebrew/install/master/install](https://raw.githubusercontent.com/Homebrew/install/master/install))" 20 | ``` 21 | 22 | - 下载depot_tools 23 | 24 | ```bash 25 | // 下载 26 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git 27 | 28 | // 配置环境变量 29 | vim ~/.bash_profile 30 | #新增环境变量 31 | export PATH=$PATH:/Users/xxx/flutter/depot_tools #此处使用clone源码到本地的地址 32 | #刷新环境变量缓存,使生效 33 | source ~/.bash_profile 34 | ``` 35 | 36 | - ant、ninja 37 | 38 | ```bash 39 | brew install ant 40 | brew install ninja 41 | ``` 42 | 43 | ### 源码下载 44 | 45 | Flutter Engine的源码是通过gclient管理的,我们首先要创建一个engine目录,然后新建一个gclient的配置文件.gclient。([配置介绍](https://chromium.googlesource.com/chromium/tools/depot_tools.git/+/HEAD/README.gclient.md)) 46 | 47 | ```bash 48 | cd engine 49 | vim .gclient 50 | // .gclient 51 | solutions = [ 52 | { 53 | "managed": False, 54 | "name": "src/flutter", 55 | "url": "git@github.com:xxxxxx/engine.git", 56 | "custom_deps": {}, 57 | "deps_file": "DEPS", 58 | "safesync_url": "", 59 | }, 60 | ] 61 | ``` 62 | 63 | 这里的url也可以替换为自己fork的仓库,后续方便提交和修改。 64 | 65 | 配置host 66 | 67 | ```bash 68 | 172.217.160.112 storage.l.googleusercontent.com 69 | 172.217.160.112 commondatastorage.googleapis.com 70 | 172.217.160.68 googleapis.com 71 | 172.217.160.116 chrome-infra-packages.appspot.com 72 | 172.217.160.116 appspot-preview.l.google.com 73 | ``` 74 | 75 | 下载源码 76 | 77 | ```bash 78 | gclient sync --verbose // 拉取Flutter Engine的源码以及所需的依赖 79 | //--verbose可以看到下载的过程,方便下载过程中出现问题看到异常信息 80 | ``` 81 | 82 | 首次下载过程会比较漫长。 83 | 84 | ps: 这里需要注意本地的dart、flutter环境不要与engine的版本差异太多 85 | 86 | ### 编译本地Engine 87 | 88 | 编译相关基础知识 89 | 90 | - CPU架构 91 | 92 | 编译结果包括`arm`、`arm64`、`x86`这几种架构,arm对应Android的`armeabi-v7a`,arm64对应Android的`arm64-v8a`,x86还是`x86`一般是模拟器上用的。 93 | 94 | - 运行模式 95 | 96 | 根据flutter的模式是分为`debug`、`profile`、`release`这三种模式的。 97 | 98 | 常用的编译参数 99 | 100 | - `—-android-cpu`:cpu架构,对应`arm`、`arm64`、`x86`,例如:`gn —android-cpu arm` 101 | - `—-runtime-mode`:运行模式,对应`debug`、`profile`、`release`,例如:`gn —runtime-mode debug` 102 | - `—unoptiimized`:是否优化。 103 | 104 | 编译之前,最好将下载的engine的版本与本地的flutter依赖的engine版本调整一致,不然可能会报错。 105 | 106 | ```bash 107 | // 查看本地flutter依赖的engine版本 108 | vim $pwd/flutter/bin/internal/engine.version // xxxxxxxxxx 109 | cd $pwd/engine/src/flutter 110 | git reset --hard xxxxxxxxxx 111 | gclient sync -D --with_branch_heads --with_tags 112 | ``` 113 | 114 | 编译开始 115 | 116 | ```bash 117 | // 1、定位到engine/src目录 118 | cd $pwd/engine/src 119 | 120 | // 2、编译Android对应的代码 121 | ./flutter/tools/gn --android --runtime-mode release --android-cpu arm 122 | // 这里会在src目录下生产一个out/android_release的目录,里面就是ninja所需要的编译文件 123 | 124 | // 3、通过2中生产的ninja build files编译 125 | ninja -C out/android_release 126 | 127 | // 4、编译Android打包所需要的代码 128 | ./flutter/tools/gn --runtime-mode release --android-cpu arm 129 | 130 | // 5、编译4中生产的 131 | ninja -C out/host_android 132 | // 如果4中使用的是arm64,这里就需要用host_android_arm64文件夹了 133 | ``` 134 | 135 | ### 使用本地的Engine 136 | 137 | 创建一个demo工程 138 | 139 | ```bash 140 | flutter create engine_demo 141 | cd engine_demo 142 | flutter run --release --local-engine-src-path $pwd/engine/src --local-engine=android_release 143 | ``` -------------------------------------------------------------------------------- /img/._.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/._.DS_Store -------------------------------------------------------------------------------- /img/1522049243596.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/1522049243596.png -------------------------------------------------------------------------------- /img/1522049541278.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/1522049541278.png -------------------------------------------------------------------------------- /img/flutter/._1598105373710.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/._1598105373710.jpg -------------------------------------------------------------------------------- /img/flutter/._F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/._F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif -------------------------------------------------------------------------------- /img/flutter/11111.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/11111.gif -------------------------------------------------------------------------------- /img/flutter/1598105373710.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/1598105373710.jpg -------------------------------------------------------------------------------- /img/flutter/219ADF42-E06A-4EA5-BDC0-AE5A73CD369A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/219ADF42-E06A-4EA5-BDC0-AE5A73CD369A.png -------------------------------------------------------------------------------- /img/flutter/22222.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/22222.gif -------------------------------------------------------------------------------- /img/flutter/33333.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/33333.gif -------------------------------------------------------------------------------- /img/flutter/392CF20F-2606-4966-92DE-6ECFC4065E2E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/392CF20F-2606-4966-92DE-6ECFC4065E2E.png -------------------------------------------------------------------------------- /img/flutter/3CB87568-7C57-4848-98E5-46E5CE62C2F8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/3CB87568-7C57-4848-98E5-46E5CE62C2F8.png -------------------------------------------------------------------------------- /img/flutter/4274FBD2-1264-4B07-AFAE-F3125A1D3565.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/4274FBD2-1264-4B07-AFAE-F3125A1D3565.png -------------------------------------------------------------------------------- /img/flutter/44444.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/44444.gif -------------------------------------------------------------------------------- /img/flutter/55555.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/55555.gif -------------------------------------------------------------------------------- /img/flutter/66666.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/66666.gif -------------------------------------------------------------------------------- /img/flutter/6F8E5930-4F87-4BBE-8F63-DA832C77E882.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/6F8E5930-4F87-4BBE-8F63-DA832C77E882.jpg -------------------------------------------------------------------------------- /img/flutter/77777.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/77777.gif -------------------------------------------------------------------------------- /img/flutter/7914A8AF-5CC3-47B2-A2EF-CFFBC015A330.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/7914A8AF-5CC3-47B2-A2EF-CFFBC015A330.png -------------------------------------------------------------------------------- /img/flutter/91DB3A31-5812-478F-9133-14D67CA44BC3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/91DB3A31-5812-478F-9133-14D67CA44BC3.jpg -------------------------------------------------------------------------------- /img/flutter/95058CE5-F44C-4839-89CD-CE1E85D3C619.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/95058CE5-F44C-4839-89CD-CE1E85D3C619.jpg -------------------------------------------------------------------------------- /img/flutter/AB694FB0-F007-48B0-A286-562E8B8FCBA2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/AB694FB0-F007-48B0-A286-562E8B8FCBA2.jpg -------------------------------------------------------------------------------- /img/flutter/CE226313-D816-45CF-B457-994B66283A44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/CE226313-D816-45CF-B457-994B66283A44.png -------------------------------------------------------------------------------- /img/flutter/E5C248B6-4945-44BC-A709-1DD82D49FAAF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/E5C248B6-4945-44BC-A709-1DD82D49FAAF.png -------------------------------------------------------------------------------- /img/flutter/F0B8566F-6D80-4B7A-939E-EC030527187C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/F0B8566F-6D80-4B7A-939E-EC030527187C.jpg -------------------------------------------------------------------------------- /img/flutter/F4BD2112-146E-4E74-B9B5-FCADFD3E2984.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/F4BD2112-146E-4E74-B9B5-FCADFD3E2984.png -------------------------------------------------------------------------------- /img/flutter/F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/F8F6500C-A72B-4752-9D10-B2931CB36CF8.gif -------------------------------------------------------------------------------- /img/flutter/FC16404E-0CF9-4274-9032-691C5F99FBB2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/FC16404E-0CF9-4274-9032-691C5F99FBB2.jpg -------------------------------------------------------------------------------- /img/flutter/vue2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutter/vue2.png -------------------------------------------------------------------------------- /img/flutterPigeon/pigeon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutterPigeon/pigeon.jpg -------------------------------------------------------------------------------- /img/flutterPigeon/pigeon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutterPigeon/pigeon1.png -------------------------------------------------------------------------------- /img/flutterPigeon/pigeon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutterPigeon/pigeon2.png -------------------------------------------------------------------------------- /img/flutterPigeon/pigeon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutterPigeon/pigeon3.png -------------------------------------------------------------------------------- /img/flutterPigeon/pigeon4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/flutterPigeon/pigeon4.png -------------------------------------------------------------------------------- /img/lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/lifecycle.png -------------------------------------------------------------------------------- /img/new-vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/new-vue.png -------------------------------------------------------------------------------- /img/reactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linpenghui958/note/c2456f8063edb651813cbb5e6e42c107c20db644/img/reactive.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docs:dev": "vuepress dev docs", 4 | "docs:build": "vuepress build docs" 5 | }, 6 | "devDependencies": { 7 | "vuepress": "^1.8.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### [在线地址](https://linpenghui958.github.io/) 2 | 3 | ## 文章列表 4 | - [acme.sh配置ssl https](https://github.com/linpenghui958/note/blob/master/acme.sh%E9%85%8D%E7%BD%AEssl%20https.md "acme.sh配置ssl https") 5 | - [Vue+TypeScript爬坑指南1](https://github.com/linpenghui958/note/blob/master/Vue%2BTypeScript%E7%88%AC%E5%9D%91%E6%8C%87%E5%8D%971.md) 6 | - [Vue+半自动化多页Skeleton Page](https://github.com/linpenghui958/note/blob/master/vue+半动化多页skeleton.md) 7 | - [Vue+ts下的vw适配](https://github.com/linpenghui958/note/blob/master/Vue+ts下的vw适配(第三方库css问题).md) 8 | - [手写简单的js](./手写js.md) 9 | - [深入理解EventLoop](./event_loop/Eventloop学习.md) 10 | - [Flutter零基础介绍](./Flutter零基础介绍.md) 11 | 12 | #### vue深入解析 13 | - [数据驱动](./数据驱动.md) 14 | - [组件化](./组件化.md) 15 | - [编译](./编译.md) 16 | - [响应式原理](./响应式原理.md) 17 | 18 | #### 性能优化 19 | - [2019前端性能优化](./2019前端性能优化.md) 20 | 21 | #### 其他类型 22 | - [ES6代码规范](./js代码规范.md) 23 | - [面向对象](./面向对象.md) 24 | - [网页加载过程及优化](./网页加载过程及优化.md) 25 | - [2019面试不完全汇总](./2019面试.md) 26 | -------------------------------------------------------------------------------- /vue+半动化多页skeleton.md: -------------------------------------------------------------------------------- 1 | ###基于vue-cli实现自动生成Skeleton Page,多页skeleton 2 | --- 3 | 11月15号更新: 4 | 简单粗暴,先上 最新demo地址(有帮助的同学可以点个赞,或者给个star _(:з」∠)_ ) 5 | [vue-skeleton demo](https://github.com/linpenghui958/skeleton-test) 6 | 7 | 文章发布过去半年时间,文章中所提的两个插件都已经进行较大的更新,有朋友跟我咨询后续插件的支持情况。这里我再以page-skeleton-webpack-plugin(0.10.12)以下简称PSWP为例进行实验 8 | 可以看到PSWP[文档](https://github.com/ElemeFE/page-skeleton-webpack-plugin/blob/master/docs/i18n/zh_cn.md)中已经更新了对多页自动生成,和多路由骨架屏的支持。 9 | 下文还是以vue-cli为例 10 | 第一步,新建一个项目,并安装相关依赖 11 | ``` 12 | vue init webpack skeleton-test 13 | cd skeleton-test 14 | npm install 15 | npm install --save-dev page-skeleton-webpack-plugin 16 | npm install --save-dev html-webpack-plugin 17 | ``` 18 | 19 | 第二步,然后在build/webpack.base.conf.js中 20 | ```js 21 | const HtmlWebpackPlugin = require('html-webpack-plugin') 22 | const { SkeletonPlugin } = require('page-skeleton-webpack-plugin') 23 | const path = require('path') 24 | const webpackConfig = { 25 | entry: 'index.js', 26 | output: { 27 | path: __dirname + '/dist', 28 | filename: 'index.bundle.js' 29 | }, 30 | plugin: [ 31 | new HtmlWebpackPlugin({ 32 | // 本身的配置项 33 | }), 34 | new SkeletonPlugin({ 35 | pathname: path.resolve(__dirname, '../shell'), // customPath为来存储 shell 文件的地址 36 | staticDir: path.resolve(__dirname, '../dist'), // 最好和 `output.path` 相同 37 | routes: ['/', '/test'], // 将需要生成骨架屏的路由添加到数组中,测试根路径和/test路径 38 | }) 39 | ] 40 | } 41 | ``` 42 | 第三步,运行项目,生产skeleton-page 43 | - 还是在控制台输入toggleBar后点击页面上方的control bar唤醒 44 | ![](http://pi82b6lei.bkt.clouddn.com/111.png) 45 | 右上角依次为,预览不同路由生产的骨架屏,手机预览,写入本地文件 46 | 可以看到现在更新了许多功能,在预览无误后 47 | 点击右上方写入本地文件即可进行最后的打包预览 48 | ![](http://pi82b6lei.bkt.clouddn.com/222.png) 49 | 可以看到项目目录多了一个shell路径,这跟之前设置的保存路径一致 50 | 51 | 第四步,打包预览效果 52 | 53 | ```js 54 | npm run build 55 | cd dist 56 | http-server 57 | ``` 58 | ![](http://pi82b6lei.bkt.clouddn.com/skeleton11-15-3.png) 59 | 然后进入浏览器预览 60 | 看一下根路径 61 | ![](http://pi82b6lei.bkt.clouddn.com/screen12.gif) 62 | 看一下/test路径 63 | ![](http://pi82b6lei.bkt.clouddn.com/skeleton-screen13.gif) 64 | 65 | ps!!!前方高能预警 66 | 这里是以vue-cli为基础进行的测试 67 | 在初始化的模板中,webpack分为base、dev、 prod三个文件 68 | 这里因为PSWP为dev和prod都需要的插件,所以放在base文件中 69 | 但是在第一次打包后预览,笔者发现prod环境并没有生效 70 | 原因是在webpack.prod.conf.js中的HtmlWebpackPlugin配置中 71 | ```js 72 | plugins: [ 73 | new HtmlWebpackPlugin({ 74 | filename: config.build.index, 75 | template: 'index.html', 76 | inject: true, 77 | minify: { 78 | // removeComments: true, 移除注释 79 | collapseWhitespace: true, 80 | removeAttributeQuotes: true 81 | // more options: 82 | // https://github.com/kangax/html-minifier#options-quick-reference 83 | }, 84 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 85 | chunksSortMode: 'dependency' 86 | }), 87 | ] 88 | ``` 89 | 这里有一项minify的配置为移除注释,查看minify的[文档](https://github.com/kangax/html-minifier#options-quick-reference) 90 | ![](http://pi82b6lei.bkt.clouddn.com/skeleton11-15-4.png) 91 | 笔者觉得很有可能是这个配置,移除了index.html中的给移除了,导致打包后未能生效。 92 | 在注释掉这一选项后,dev和prod环境均正常 93 | 94 | 以下为原内容 95 | ----- 96 | 97 | 之前看eleme的专栏了解到骨架页面 98 | 这里刚好项目重构尝试将Skeleton Page引入项目中 99 | 其中遇到一些问题和一些坑,分享一下 100 | 101 | 首先用到的是[page-skeleton-webpack-plugin](https://github.com/ElemeFE/page-skeleton-webpack-plugin "page-skeleton-webpack-plugin") 102 | eleme开源的一款自动生成skeleton-page的插件 103 | 使用起来也是非常的简单粗暴 104 |
105 | 项目是基于vue-cli + ts 106 | #### page-skeleton-webpack-plugin使用 107 | 首先通过npm安装插件,该插件依赖于[html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) 108 | > npm install --save-dev page-skeleton-webpack-plugin 109 | > npm install --save-dev html-webpack-plugin 110 | 111 | - 配置篇 112 | 首先,因为dev和prod环境都需要用到这个插件,可以直接在webpack.base.conf.js中的config中添加对应的plugins 113 | 114 | 1. 第一步 115 | 116 | // 引入插件 117 | const { SkeletonPlugin } = require('page-skeleton-webpack-plugin') 118 | 119 | //...其他配置 120 | module.exports = { 121 | context: path.resolve(__dirname, '../'), 122 | entry: { 123 | app: './src/main.ts' 124 | }, 125 | output: { //... 126 | }, 127 | resolve: { //... 128 | }, 129 | module: { //... 130 | }, 131 | node: { // ... 132 | }, 133 | plugins: [ 134 | new SkeletonPlugin({ 135 | pathname: path.resolve(__dirname, '../static'), // 生成名为 shell 文件存放地址 136 | headless: false // 打开非headless chrome 137 | }) 138 | ] 139 | } 140 | 141 | 2. 第二步 142 | 在index.html里面添加skeleton-page要注入的地方 143 | 144 | 145 | 146 | 147 | 148 | Document 149 | 150 | 151 |
152 | // 在#app里添加 153 |
154 | 155 | 156 | 157 | 配置就这样完成了,是不是很简单粗暴,使用起来也是很简单 158 | 159 | - 使用篇 160 | 在chrome中运行项目后 161 | 1.使用Ctrl|Cmd + enter 呼出插件交互界面后者在控制台输入toggelBar呼出交互界面 162 | ![](https://i.imgur.com/kt3aPmW.png) 163 | 2.点击上放的按钮 164 | ![](https://i.imgur.com/wFZ9uxd.png) 165 | 点击写入,即可看到生在static(plugins里面配置的目录)里的shell.html了 166 | ![](https://i.imgur.com/q2eYEVP.png) 167 | 168 | 最后将项目打包,运行起来,就能看到对应的skeleton-page了 169 | ![](https://i.imgur.com/gDgaDn0.gif) 170 | 还有更多的参数配置,请参考仓库地址[page-skeleton-webpack-plugin](https://github.com/ElemeFE/page-skeleton-webpack-plugin "page-skeleton-webpack-plugin") 171 | 172 | **这里我还碰到了一个问题** 173 | 就是运行插件式,系统提示windows找不到chrome 174 | ![](https://i.imgur.com/7K9fB9a.gif) 175 | ![](https://i.imgur.com/vPvSfWd.png) 176 | 再使用Puppeteer的demo排除了生成skeleton界面的问题后 177 | 在inssue里跟大神交流后 178 | 知道插件用到了一个叫[opn](https://github.com/sindresorhus/opn)的库来打开chrome 179 | 这里提到了不同环境下的app名字是不一样的 180 | `app` 181 | `Type: string Array` 182 | `Specify the app to open the target with, or an array with the app and app arguments.` 183 | `The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is google chrome on macOS, google-chrome on Linux and chrome on Windows.` 184 | 所以在不同的环境下启动,需要寻找对应的名字 185 | 找到问题,大神给出了解决方案 186 | 在node_modules/page-skeleton-webpack-plugin/src/server.js 187 | 188 | //修改前 189 | open({this.previewPageUrl, app: google chrome}) 190 | //修改后 191 | open(this.previewPageUrl, { 192 | app: ['chrome', '--incognito'] 193 | }) 194 | 这样系统就能顺利的找到chrome,完成生成页面后的操作了! 195 | 196 | 但是这里我又有个问题了,如果用户通过不同的链接进入项目,那么每个地址都会显示这个固定的骨架屏, 197 | #### 如何才能为不同的页面,配置不同的骨架屏呢 198 | 199 | 这里我找到了另外一个库[vue-skeleton-webpack-plugin](https://github.com/lavas-project/vue-skeleton-webpack-plugin),是需要手动编写skeleton-page,但是可以配置多页面对应多骨架屏 200 | 201 | - 配置篇 202 | 203 | 首先需要安装对应的插件 204 | > npm install vue-skeleton-webpack-plugin 205 | 206 | 插件在dev可以配置预览的路由,在prod环境下使用 207 | 1. 在webpack.prod.conf.js中引入插件 208 | 209 | // 引入插件 210 | const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin') 211 | // plugin里添加配置 212 | plugins: [ 213 | new SkeletonWebpackPlugin({ 214 | webpackConfig: require('./webpack.skeleton.conf'), // skeleton配置文件 215 | router: { 216 | routes: [ // path是对应的路由,skeletonId是对应骨架屏的id(在后面的entry-skeleton.ts中可以看到) 217 | {path: '/index', skeletonId: 'skeleton2'}, 218 | {path: '/', skeletonId: 'skeleton1'} 219 | ] 220 | } 221 | }), 222 | ] 223 | 224 | 2. 同级目录下的webpack.skeleton.conf.js配置文件 225 | 226 | 'use strict'; 227 | 228 | const webpack = require('webpack'); 229 | const config = require('../config'); 230 | const path = require('path') 231 | const merge = require('webpack-merge') 232 | const baseWebpackConfig = require('./webpack.base.conf') 233 | const nodeExternals = require('webpack-node-externals') 234 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 235 | 236 | function resolve(dir) { 237 | return path.join(__dirname, dir) 238 | } 239 | 240 | module.exports = merge(baseWebpackConfig, { 241 | target: 'node', 242 | devtool: false, 243 | entry: { 244 | app: resolve('../src/entry-skeleton.ts') // skeleton入口文件 245 | }, 246 | output: Object.assign({}, baseWebpackConfig.output, { 247 | filename: 'skeleton-bundle.js', 248 | libraryTarget: 'commonjs2' 249 | }), 250 | externals: nodeExternals({ 251 | whitelist: /\.css$/ 252 | }), 253 | plugins: [ 254 | new webpack.DefinePlugin({ 255 | 'process.env': config.build.env 256 | }), 257 | 258 | // Compress extracted CSS. We are using this plugin so that possible 259 | // duplicated CSS from different components can be deduped. 260 | new OptimizeCSSPlugin({ 261 | cssProcessorOptions: { 262 | safe: true 263 | } 264 | }) 265 | ] 266 | }) 267 | 268 | 3. ../src/entry-skeleton.ts入口文件 269 | 270 | import Vue from 'vue'; 271 | import Skeleton1 from './skeleton/HomeSkeleton.vue'; // 事先写好的skeleton-page页面 272 | import Skeleton2 from './skeleton/HomeSkeleton2.vue'; 273 | 274 | export default new Vue({ 275 | components: { 276 | Skeleton1, // 注册对应的组件 277 | Skeleton2 278 | }, 279 | template: ` 280 |
// 这里对应的id就是plugins里面不同页面对应的skeletonId 281 |
284 | ` 285 | }); 286 | 效果图 287 | /index 对应的 id skeleton2 288 | ![](https://i.imgur.com/VABboqs.gif) 289 | / 对应的 id skeleton1 290 | ![](https://i.imgur.com/HSryVBj.gif) 291 | 更多的配置参考仓库地址 [vue-skeleton-webpack-plugin](https://github.com/lavas-project/vue-skeleton-webpack-plugin) 292 | 这样,就能实现多页对应多个skeleton-page了 293 | 再结合前面的自动生成skeleton-page是不是算半自动花了,- -笑哭 294 | 有心之人可以结合两个插件,全自动多页对应多skeleton-page指日可待了! 295 | 296 | -------------------------------------------------------------------------------- /使用Nodejs和Puppeteer从HTML中导出PDF.md: -------------------------------------------------------------------------------- 1 | ## 使用Nodejs和Puppeteer从HTML中导出PDF 2 | 我只是知识的搬运工~ 3 | 4 | [原文外网地址](https://dev.to/bmz1/generating-pdf-from-html-with-nodejs-and-puppeteer-5ln#clientside) 5 | [Markdown文件地址](https://github.com/linpenghui958/note/blob/master/%E4%BD%BF%E7%94%A8Nodejs%E5%92%8CPuppeteer%E4%BB%8EHTML%E4%B8%AD%E5%AF%BC%E5%87%BAPDF.md) 6 | [demo-code地址(文章中所有代码合集)](https://github.com/linpenghui958/pdf-download-demo) 7 | **在这篇文章里,我将会向你展示如何使用Nodejs、Puppeteer、无头浏览器、Docker从一个样式要求复杂的的React页面导出PDF** 8 | 9 | 背景:几个月前,一个RisingStack的客服要求我们实现一个用户可以以PDF格式请求React页面的功能。这个页面主要是含有数据可视化、很多SVG的报告/结å果。此外,还有一些改变布局和修改一些HTML元素样式的特殊需求。因此,这个PDF相对于原始React页面,需要有一些不同的样式和添加。 10 | 11 | **正如这个任务比可以用简单的css规则来解决的情况,要更复杂一些。在我们最开始寻找可行的方法中,我们主要找到了三种解决方法,这篇文章将会带你尽览这些可以使用的方法和最终的解决方案。** 12 | 13 | #### 目录: 14 | - 前端还是后端? 15 | - 方案1:使用一个DOM的屏幕快照 16 | - 方案2:只使用一个PDF的库 17 | - 最终方案3: Puppeteer,headless chrome和Nodejs 18 | - 样式操作 19 | - 往客户端发送 20 | - 在Docker下使用Puppeteer 21 | - 方案3 + 1:CSS打印规则 22 | - 总结 23 | 24 | #### 客户端还是服务端 25 | 在客户端和服务端都可以生产一个PDF文件。然而,如果你不想把用户的浏览器可以提供的资源用完,那还是更可能使用后端来处理。尽管如此,我还是会把两端的解决方法都展示给你看。 26 | 27 | #### 方案1:使用DOM的屏幕快照 28 | 乍看之下,这个解决方案可能是最简单的,并且它也被证明确实是这样,但是这个方案也有自己的局限性。如果你没有一些特殊的需求,这是一个很好的简单方法去生产一个PDF文件。 29 | 30 | 这个方法的思路简单清晰:从当前页面创建一个屏幕快照,并把它放入一个PDF文件。相当的直接了当。我们使用两个库来实现这个功能: 31 | 32 | - [html2canvas](https://html2canvas.hertzen.com/),从DOM中实现一个屏幕快照 33 | - [jsPDF](https://github.com/MrRio/jsPDF),一个生成PDF的库 34 | 35 | 代码如下 36 | 37 | ```javascript 38 | npm install html2canvas jspdf 39 | 40 | ``` 41 | ```javascript 42 | import html2canvas from 'html2canvas' 43 | import jsPdf from 'jspdf' 44 | 45 | printPDF () { 46 | const domElement = document.getElementById('your-id') 47 | html2canvas(domElement).then((canvas) => { 48 | const img = canvas.toDataURL('image/png') 49 | const pdf = new jsPdf() 50 | pdf.addImage(img, 'JPEG', 0, 0, width, height) 51 | pdf.save('your-filename.pdf') 52 | }) 53 | } 54 | ``` 55 | 56 | 请确保你看到了`html2canvas`、`onclone`方法。可以帮助你再获取照片前,便利的获取屏幕快照和操作DOM。我们可以看到需要使用这个工具的例子。不幸的是,这其中并没有我们想要的。我们需要再后端处理PDF的生成。 57 | 58 | #### 方案2:仅仅使用一个PDF库 59 | 在NPM上有许多库可以实现这样的要求,例如jsPDF或者[PDFKit](https://www.npmjs.com/package/pdfkit),随之而来的问题就是如果你想要使用这些库,你不得不再一次生成页面的架子。你还需要把后续的所有改变应用到PDF模板和React页面中。 60 | 看到上面得代码,我们需要自己创建一个PDF文档。现在你可以通过DOM找到如何去转换每一个元素变成PDF,但这是一个沉闷的工作。肯定有一些更简单的方法 61 | 62 | ```javascript 63 | const doc = new PDFDocument() 64 | doc.pipe(fs.createWriteStream(resolve('./test.pdf'))); 65 | doc.font(resolve('./font.ttf')) 66 | .fontSize(30) 67 | .text('测试添加自定义字体!', 100, 100) 68 | doc.image(resolve('./image.jpg'), { 69 | fit: [250, 300], 70 | align: 'center', 71 | valign: 'center' 72 | }) 73 | doc.addPage() 74 | .fontSize(25) 75 | .text('Here is some vector graphics...', 100, 100) 76 | doc.pipe(res); 77 | doc.end(); 78 | ``` 79 | 80 | 这个片段是根据PDFKit的文档写的,如果不需要在已有的HTML页面进行转变,它可以有效的帮助你快速的直接生产PDF文件。 81 | 82 | #### 最终方案3:Puppeteer,Headless Chrome和Nodejs 83 | 什么是Puppeteer呢,它的文档是这么说的 84 | > Puppeteer是一个通过开发者工具协议对Chrome和Chromium提供高级API的操纵。Puppeteer默认运行headless版本,但是可以配置成运行Chrome或者Chromium。 85 | > 这是一个可以在Nodejs环境运行的浏览器。如果你阅读它的文档,第一件事说的就是Puppeteer可以用来生产屏幕快照和页面的PDF。这也是我们为什么要使用它。 86 | 87 | ```javascript 88 | const puppeteer = require('puppeteer') 89 | (async () => { 90 | const brower = await puppeteer.launch() 91 | const page = await brower.newPage() 92 | await page.goto('https://github.com/linpenghui958', { waitUntil: 'networkidle0'}) 93 | const pdf = await page.pdf({ format: 'A4'}) 94 | await brower.close() 95 | return pdf 96 | })() 97 | ``` 98 | 99 | 这是一个导航到制定URL并生产该站点的PDF文件的简单函数。首先,我们启动一个浏览器(只有headless模式才支持生成PDF),然后我们打开一个新页面,设置视图并且导航到提供的URL。 100 | 101 | 设置 `waitUntil: 'networkidle0'`选项表示Puppeteer已经导航到页面并结束(500ms内没有网络请求)这里可以查看更详细的[文档](https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md) 102 | 随后,我们将PDF保存到一个变量,关闭浏览器并返回这个PDF文件。 103 | 104 | Note: `page.pdf`方法可以接受一个 options对象,你可以设置path属性把文件保存在本地中。如果没有提供path,pdf文件将不会保存到本地,你将会得到一个buffer流。稍后我们会讨论如何处理这个情况。 105 | 106 | 如果你需要从一个先登录才能访问的页面生成PDF,那么首先你需要导航到登录页面,检查表单元素的ID或者name,填上,并且提交表单。 107 | 108 | ```javascript 109 | await page.type('#email', process.env.PDF_USER) 110 | await page.type('#password', process.env.PDF_PASSWORD) 111 | await page.click('#submit') 112 | ``` 113 | 通常商城登录认证使用环境变量,不要硬编码它们。 114 | 115 | ##### 样式操作 116 | 117 | Puppeteer同样提供了一个样式操作的解决方案。你可以在生成PDF文件之前加入style标签,并且Puppeteer将会生成一个样式修改后的文件。 118 | 119 | ```javascript 120 | await page.addStyleTag({ content: '.nav { display: none} .navbar { border: 0px} #print-button {display: none}' }) 121 | ``` 122 | 123 | ##### 把资源发送给客户端并保存 124 | 现在我们已经在后端生成了一个PDF文件。接下来做什么呢?根据上面提到的,如果你讲文件保存到本地,你将会得到一个buffer。你只需要通过适当的内容格式将buffer发送给前端即可。 125 | ```javascript 126 | const result = await printPdf() 127 | res.set({'Content-Type': 'application/pdf', 'Content-Length': result.length}) 128 | res.send(result) 129 | ``` 130 | 现在你可以简单的像服务端发送骑牛,并生成PDF 131 | 132 | ```javascript 133 | getPDF() { 134 | return axios.get('//localhost:3001/puppeteer', { 135 | responseType: 'arraybuffer', 136 | headers: { 137 | 'Accept': 'application/pdf' 138 | } 139 | }) 140 | } 141 | ``` 142 | 当你发送一次请求,buffer将会开始下载。现在下一步就是将buffer转换成PDF文件。 143 | 144 | ```javascript 145 | savePDF() { 146 | this.getPDF() 147 | .then(res => { 148 | const blob = new Blob([res.data], { type: 'application/pdf'}) 149 | const link = document.createElement('a') 150 | link.href = window.URL.createObjectURL(blob) 151 | link.download = 'test.pdf' 152 | link.click() 153 | }) 154 | .catch(e => console.log(e)) 155 | } 156 | ``` 157 | 158 | ```javascript 159 | savePDF() { 160 | this.getPDF() 161 | .then(res => { 162 | const blob = new Blob([res.data], { type: 'application/pdf'}) 163 | const link = document.createElement('a') 164 | link.href = window.URL.createObjectURL(blob) 165 | link.download = 'test.pdf' 166 | link.click() 167 | }) 168 | .catch(e => console.log(e)) 169 | } 170 | ``` 171 | 172 | ```javascript 173 | 174 | ``` 175 | 现在如果你的点击按钮,那么PDF将会被浏览器下载。 176 | 177 | #### 在Docker使用Puppeteer 178 | 我认为这是最棘手的部分,因此让我来帮你节省好几个小时Google的时间。 179 | 官方文档只指出,“在Docker下运行headless Chrome可能会非常棘手”。文档有一个排除故障的章节,在那你可以找到使用Docker安装Puppeteer的所有必要信息。 180 | 如果你要在Alpine(linux)镜像中安装Puppeteer,确保你看到了[页面的这个部分](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-on-alpine)。否则你可能会掩盖掉你无法使用最新版本的Puppeteer并且你同样需要禁用shm而去使用一个flag 181 | 182 | ```javascript 183 | const browser = await puppeteer.launch({ 184 | headless: true, 185 | args: ['--disable-dev-shm-usage'] 186 | }); 187 | ``` 188 | 否则,Puppeteer的子进程甚至可能会它开始前就把内存用完了。更多关于故障排查的信息都在上面的链接里。 189 | 190 | #### 方案3 + 1:CSS打印规则 191 | 有人可能会认识从开发者的角度使用CSS打印规则很容易。但是当他们面临浏览器的兼容性时如何跨过这个问题? 192 | 当你选择使用CSS打印规则,你不得不在每一个浏览器测试并确认所有的结果都是同样的布局并且这还不是全部要做的事。 193 | 比如说,在一个给定的元素插入break after,这应该不是一个特别难懂的情况。但是你当你使用Firefox开展工作的时候,你会感到非常惊讶。[点击查看兼容性](https://developer.mozilla.org/en-US/docs/Web/CSS/break-after#Browser_compatibility) 194 | 除非你是一个身经百战在创建打印页面有许多经验的CSS魔术师,否则这将会花费很多的时间。 195 | 如果你可以保持打印样式比较简单那么使用打印规则还是很棒的。 196 | 比如下面这个例子。 197 | 198 | ```css 199 | @media print { 200 | .print-button { 201 | display: none; 202 | } 203 | 204 | .content div { 205 | break-after: always; 206 | } 207 | } 208 | ``` 209 | 上述的CSS隐藏了打印按钮,并且在每一个class名为content后代的div都加入一个break。这里有[一篇文章](https://www.smashingmagazine.com/2018/05/print-stylesheets-in-2018/)总结了你可以使用打印规则做什么,并且在浏览器兼容性方便会有哪些问题。 210 | 综合考虑,如果你不需要从一个如此负责的页面生成PDF,那么CSS打印规则还是很棒的。 211 | 212 | #### 总结:使用Nodejs和Puppeteer从HTML生成PDF 213 | 让我们快速的全览一遍所有的方案。 214 | - 使用DOM的屏幕快照:当你需要从一个页面创建一个快照(例如创建一个短文)这是很有用,但是当你需要处理大量的数据时,可能会出问题。 215 | - 仅使用一个PDF库:如果你需要从头开始以编程的方式创建一个PDF文件,这是一个完美的解决方法。否则,你需要保持HTML和PDF的模板肯定是不方便的。 216 | - Puppeteer:尽管让他在Docker下使用它很麻烦,但是它提供了我们需要的最好的结构并且它的代码也是最容易编写的。 217 | - CSS打印规则:如果你的用户知道如何打印一个文件并且你的页面也相对简单。这可能是最梧桐的解决方案。但是正如你所见,在文中的例子里它并不合适。Happy printing! 218 | -------------------------------------------------------------------------------- /手写js.md: -------------------------------------------------------------------------------- 1 | @(MarkDown) 2 | ###手写JS问题 3 | --- 4 | #####函数节流 throttle 5 | throttle 策略的电梯。保证如果电梯第一个人进来后,50毫秒后准时运送一次,不等待。如果没有人,则待机。 6 | 7 | let throttle = (fn, delay = 50) => { // 节流 控制执行间隔时间 防止频繁触发 scroll resize mousemove 8 | let stattime = 0; 9 | return function (...args) { 10 | let curTime = new Date(); 11 | if (curTime - stattime >= delay) { 12 | fn.apply(this, args); 13 | stattime = curTime; 14 | } 15 | } 16 | } 17 |
18 | 19 | #####防抖动 debounce 20 | debounce 策略的电梯。如果电梯里有人进来,等待50毫秒。如果又人进来,50毫秒等待重新计时,直到50毫秒超时,开始运送。 21 | 22 | let debounce = (fn, time = 50) => { // 防抖动 控制空闲时间 用户输入频繁 23 | let timer; 24 | return function (...args) { 25 | let that = this; 26 | clearTimeout(timer); 27 | timer = setTimeout(fn.bind(that, ...args), time); 28 | } 29 | } 30 | 31 | #####Function的bind实现 32 | 33 | Function.prototype._bind = function (context) { 34 | let func = this 35 | let params = [].slice.call(arguments, 1) 36 | return function () { 37 | params = params.concat([].slice.call(arguments, 0)) 38 | func.apply(context, params) 39 | } 40 | } 41 |
42 | #####函数组合串联compose(koa reduce中间件) 43 | 44 | // 组合串联 45 | let fn1 = (a) => a + 1; 46 | let fn2 = (b) => b + 2; 47 | let fn3 = (c) => c + 3; 48 | 49 | let funs = [fn1, fn2, fn3]; 50 | 51 | let compose = (func) => { 52 | return arg => func.reduceRight((composed, fn) => fn(composed), arg); 53 | } 54 | console.log(compose(funs)(100)); // 相当于fn1(fn2(fn3(100))) 55 |
56 | 57 | #####数组展平 58 | 59 | let arr = [[1, 2], 3, [[[4], 5]]]; // 数组展平 60 | function flatten(arr) { 61 | return [].concat( 62 | ...arr.map(x => Array.isArray(x) ? flatten(x) : x) 63 | ) 64 | } 65 |
66 | 67 | #####插入排序 68 | > 插入排序 从后往前比较 直到碰到比当前项 还要小的前一项时 将这一项插入到前一项的后面 69 | 70 | function insertSort(arr) { 71 | let len = arr.length; 72 | let preIndex, current; 73 | for (let i = 1; i < len; i++) { 74 | preIndex = i - 1; 75 | current = arr[i]; // 当前项 76 | while (preIndex >= 0 && arr[preIndex] > current) { 77 | arr[preIndex + 1] = arr[preIndex]; // 如果前一项大于当前项 则把前一项往后挪一位 78 | preIndex-- // 用当前项继续和前面值进行比较 79 | } 80 | arr[preIndex + 1] = current; // 如果前一项小于当前项则 循环结束 则将当前项放到 前一项的后面 81 | } 82 | return arr; 83 | } 84 | 85 |
86 | #####选择排序 87 | > 选择排序 每次拿当前项与后面其他项进行比较 得到最小值的索引位置 然后把最小值和当前项交换位置 88 | 89 | function selectSort(arr) { 90 | let len = arr.length; 91 | let temp = null; 92 | let minIndex = null; 93 | for (let i = 0; i < len - 1; i++) { // 把当前值的索引作为最小值的索引一次去比较 94 | minIndex = i; // 假设当前项索引 为最小值索引 95 | for (let j = i + 1; j < len; j++) { // 当前项后面向一次比小 96 | if (arr[j] < arr[minIndex]) { // 比假设的值还要小 则保留最小值索引 97 | minIndex = j; // 找到最小值的索引位置 98 | } 99 | } 100 | // 将当前值和比较出的最小值交换位置 101 | if (i !== minIndex) { 102 | temp = arr[i] 103 | arr[i] = arr[minIndex]; 104 | arr[minIndex] = temp; 105 | } 106 | } 107 | return arr; 108 | } 109 |
110 | 111 | #####冒泡排序 112 | > 冒泡排序 相邻两项进行比较 如果当前值大于后一项 则交换位置 113 | 114 |
115 | 116 | #####快速排序(递归) 117 | 118 | 119 | function quickSort(arr) { 120 | if (arr.length <= 1) return arr; 121 | let midIndex = Math.floor(arr.length / 2); 122 | let midNum = arr.splice(midIndex, 1)[0]; 123 | let left = []; 124 | let right = []; 125 | for(let i = 0; i < arr.length; i++) { 126 | let cur = arr[i]; 127 | if (cur <= midNum) { 128 | left.push(cur); 129 | } else { 130 | right.push(cur); 131 | } 132 | } 133 | return quickSort(left).concat(midNum, quickSort(right)); 134 | } 135 | 136 | let arr = [2, 4, 12, 9, 22, 10, 18, 6]; 137 | quickSort(arr); 138 | 139 |
140 | 141 | #####数组去重的几种方法 142 | 143 | 144 | // 1 es6 145 | let newArr = [...new Set(arr)]; 146 | // 2 147 | Array.prototype.unique2 = function() { 148 | let newArr = []; 149 | let len = this.length; 150 | for(let i = 0; i < len; i++) { 151 | let cur = this[i]; 152 | if(newArr.indexOf(cur) === -1) { 153 | newArr[newArr.length] = cur; 154 | } 155 | } 156 | return newArr; 157 | } 158 | console.log(arr.unique1()); 159 | // 3 最快 160 | Array.prototype.unique4 = function() { 161 | let json = {}, newArr = [], len = this.length; 162 | for(var i = 0; i < len; i++) { 163 | let cur = this[i]; 164 | if (typeof json[cur] == "undefined") { 165 | json[cur] = true; 166 | newArr.push(cur) 167 | } 168 | } 169 | return newArr; 170 | } 171 | console.log(arr.unique4()); 172 | 173 | #####千叶符 174 | 175 | let str1 = '2123456789'; 176 | let str2 = '2123456789.12'; 177 | 178 | // 利用正向预查 匹配 开头一个数字\d 后面匹配这个数字后面必须是三个数字为一组为结尾或小数为结尾 179 | function thousandth(str) { 180 | let reg = /\d(?=(?:\d{3})+(?:\.\d+|$))/g; 181 | return str.replace(reg, (...rest) => rest[0] + ','); 182 | } 183 | console.log(thousandth(str1)); // 2,123,456,789 184 | console.log(thousandth(str2)); // 2,123,456,789.12 185 | 186 | 答案: 187 | 188 | str.replace(/(\d)(?=(?:\d{3})+$)/g, ' $1,') -------------------------------------------------------------------------------- /网页加载过程及优化.md: -------------------------------------------------------------------------------- 1 | ## 页面加载过程 2 | ![Alt text](http://blogimg.linph.cc/html1.png) 3 | 4 | 1. **解析Html (不包含 js css外部文件)** 5 | 6 | 7 | - readstatechange`(第一个)` 8 | > 此时状态为interactive,表示文档已加载和解析但资源仍在加载,该状态通常紧接着会触发DOMContentLoaded 9 | 10 | 11 | - DOMContentLoaded 12 | > Html文档被加载和解析成功,DOM树构建完成时会触发 13 | 14 | - Recalculate Style (cssom 构建完成) 15 | > 通过添加和删除元素,更改属性、类或通过动画更改DOM,全都会导致浏览器重新计算元素样式,在很多情况下还会对页面和页面的一部分进行布局(layout )。 16 | 重新计算样式可以分为两步: 17 | 1. 浏览器计算出给指定元素应用哪些类、伪选择器和ID 18 | 2. 从匹配选择器中获取所有样式规则,并计算出此元素的最终样式 19 | 20 | - readystatechange`(第二个)` (文档已加载和解析,且资源也加载完成) 21 | > 此时状态为complete,表示文档和资源都已加载完成,该状态通常紧接着触发load 22 | 23 | - load事件 24 | > 文档和资源都已加载完成时会触发 25 | 26 | - layout 27 | > 布局几乎总是在作用整个文档,但还是主要看影响的节点个数 28 | 29 | ![Alt text](http://blogimg.linph.cc/html2.png) 30 | 31 | 2. 解析Html (包含js css外部文件) 32 | 33 | - Evaluate Script (执行js) 34 | - Layout 变为不在ParseHtml中执行 35 | 可能是因为CSS文件或JS文件的加载阻塞了整个页面的渲染过程,因为js和css都可能对标签进行样式的设置。如果不存在文件,就不会存在等待加载的问题。 36 | 37 | 3. 改变背景色 (重绘) 38 | 39 | ![Alt text](http://blogimg.linph.cc/html3.png) 40 | 41 | 4. 改变高度 (回流,重排) 42 | 43 | ![Alt text](http://blogimg.linph.cc/html4.png) 44 | 相当于重绘多个Layout 45 | 46 | 5. 图片资源加载(img或bg) 47 | 如果图片标签尺寸不变,则会触发一次重绘 48 | ![Alt text](http://blogimg.linph.cc/html5.png) 49 | 50 | #### 遵循的原则 51 | --- 52 | 1. 关于阻塞 53 | - css不会阻塞DOM树的解析 54 | - css加载会阻塞js,从而阻塞了DOM树的解析,页面渲染(内联样式性能较高,使用与第一屏) 55 | - js会阻塞DOM树的解析(因为js会改变DOM树的内容) 56 | - css引入的字体文件加载,也会阻塞js 57 | 58 | 2. 关于与页面渲染过程的对应 59 | 1. **js执行时**:这时应该只是构建了前面部分的DOM树和CSSOM树,因为js需要通过dom api和CSSOM api操作前面部分的标签内容和样式 60 | 2. **DOM树构建完成**:DomContentLoaded事件 61 | 3. **CSSOM构建完成、Render Tree构建完成**: Recalculate Style 62 | 4. **Layout**:Layout事件 63 | 5. **paint**:Paint(图片层绘制)和Composite Layers(图片层合并),除了transform或opacity属性之外,更改任何属性始终都会触发绘制paint 64 | 6. **reflow回流**: 3 4 5走一遍 65 | 7. **repaint重绘**:3 5 步走一遍 66 | 8. 更改一个既不要布局也不要绘制的属性: 3 步 + Composite Layers,此行为在 678重新渲染步骤中开销最小,适合动画或滚动,具体比如transform opacity 67 | 68 | 3. 关于chrome浏览器的一些行为 69 | 70 | - 渲染队列: 浏览器存在一个渲染队列,用于将多次连续的重拍和重绘操作变成一次。当你进行DOM的读操作时,如果队列不为空,chrome会清空队列,立即进行重排或者重绘。 71 | - 布局:布局或重排中浏览器需要计算元素要占据的空间大小及其在屏幕的位置,网页的布局模式意味着一个元素可能影响其他元素,例如``元素的宽度一般会影响其子元素的宽度以及树中各处节点。 72 | - 绘制与合成:绘制一般是在多个表面(通常称为层)上完成的,因此浏览器需要将它们按正确顺序绘制到屏幕上,以便正确渲染页面。 73 | - css选择器:对于复杂的css选择器,浏览器需要花更多的时候来确定元素的样式,因此以类为中心的css编写原则。 74 | 75 | ### 性能优化 76 | --- 77 | ##### 阻塞优化 78 | - js问题 通过script标签的async defer属性 79 | 80 | ##### 减少重新渲染 81 | **关于css** 82 | - 使用简单的样式表。样式表越简单,重排和重绘就越快。具体为 83 | 1. 减少选择器的复杂性,少用伪类; 84 | 2. 减少必须计算器样式的元素数量,应当尽可能减少声明为无效的元素的数量。 85 | - 减少DOM元素层级。重绘和重排的DOM元素层级越高,成本就越高。 86 | - 多利用display:none。 display:none的元素没有在渲染树,因而也不会进行重排和重绘 87 | - 使用css动画而不是js动画。(CSS动画优于JS动画,是由于CSS改变的是translate的值,不会引起offsetLeft、offsetTop等位置值的改变。) 88 | - 使用absolute而不是float。position属性为absolute和fixed的元素在重排的开销比float少,因为不用考虑它对其它元素的影响。 89 | - 使用div而不是table。 90 | - 使用classList代替className。 className只要赋值,就一定会重新渲染。 91 | 92 | **关于JS** 93 | - DOM的多个读操作(或多个写操作)应该放在一起,不要穿插进行。因为**连续地设置元素样式(写操作),浏览器会一次性执行,即只触发一次重排或重绘**,但如果在几个写操作间插入读取样式的操作,浏览器则不得不立即重排或重绘。 94 | - 一次性改变样式。不要一条条地改变样式,而要通过改变class,或者el.style.csstext属性。 95 | - 使用离线DOM来改变元素样式。比如`cloneNode()`克隆节点,然后再替换掉元素节点 或者 `display:none → 改动 → 显示`。 96 | - 尽量修改层级较低的DOM。 97 | - 不要在循环中重复读取DOM节点属性值。 98 | - 使用 window.requestAnimationFrame()、window.requestIdleCallback() 这两个方法控制重新渲染。 99 | 100 | **提高fps(frame per second)** 101 | 网页动画的每一帧(frame)都是一次重新渲染,将一帧送到屏幕会采用如下顺序: 102 | ![Alt text](http://blogimg.linph.cc/html7.png) 103 | 104 | **手动控制重新渲染** 105 | window.requestAnimationFrame() 方法可以将某些代码统一放到下一次重新渲染时执行。具体是将js代码放在下一帧开始时执行。如果使用setTimeout 或 setInterval 来执行动画之类的视觉变化,其回调可能在帧的某个时间点执行,可能在末尾,这会使我们丢失帧,导致卡顿。 106 | 107 | 1. 处理“布局抖动” 108 | 反复读写属性会导致布局抖动,导致长帧。 109 | 110 | ```javascript 111 | function doubleHeight(element) { 112 | var currentHeight = element.clientWidth; 113 | element.style.width = (currentHeight / 2) + 'px'; 114 | element.style.height = '80px'; 115 | } 116 | var elements = document.getElementsByTagName('tr'); 117 | for (var i = 0; i < elements.length; i++) { 118 | doubleHeight(elements[i]); 119 | } 120 | ``` 121 | ![Alt text](http://blogimg.linph.cc/html8.png) 122 | 123 | 将doubleHeight函数改成下面这样: 124 | ```javascript 125 | function doubleHeight(element) { 126 | var currentHeight = element.clientHeight; 127 | window.requestAnimationFrame(function () { 128 | element.style.height = (currentHeight * 2) + 'px'; 129 | }); 130 | } 131 | ``` 132 | ![Alt text](http://blogimg.linph.cc/html9.png) 133 | 134 | 2. 页面滚动事件 135 | ```javascript 136 | $(window).on('scroll', function() { 137 | window.requestAnimationFrame(scrollHandler); 138 | }); 139 | ``` 140 | 141 | #### 结合项目 142 | 1. 现在项目中,页面(以“任务”页面为例)在加载时都会请求一些ajax数据,比如datagrid,tree数据等等,还有些ajax数据只是预加载。如果这些ajax在页面渲染前完成请求,则会阻塞页面渲染。所以同一个页面不同网速下会有两种渲染顺序: 143 | - 在渲染之后执行 144 | ![Alt text](http://blogimg.linph.cc/html10.png) 145 | 146 | - 在渲染之前执行 147 | ![Alt text](http://blogimg.linph.cc/html11.png) 148 | 149 | **解决办法** 150 | 1. 在所有资源加载完后进行ajax请求。将datagrid等控件的数据加载放在$(window).load()事件中。 151 | 2. 延迟初始化modal中的内容 152 | 153 | **效果** 154 | ![Alt text](http://blogimg.linph.cc/html12.png) 155 | ![Alt text](http://blogimg.linph.cc/html13.png) 156 | 157 | #### question 158 | 159 | 1. 问:只要执行js,都会重排吗? 160 | 答:执行js的发生情况如下: 161 | - 载入页面时script标签:不管有无改变dom,都会重排 162 | - 异步ajax回调:需要判断有无改变dom 163 | - setTimeout:需要判断有无改变dom 164 | 165 | 2. 请求js文件时,请求和执行顺序是什么? 166 | 请求会一起发出;执行顺序按引入的顺序,不会因为后一个先返回数据而先执行。 167 | 168 | 3. 页面加载时提前ajax请求一些数据,会不会影响性能? 169 | 答:可能会,可能不会。ajax请求时不影响性能,请求后执行回调函数会影响。ajax回调函数会在请求完成且js主程序运行完后执行,等到我们能看到页面,需要经历页面解析和渲染这两个过程,如果页面渲染前ajax回调执行了,那将阻塞渲染过程。 170 | 171 | 4. 问:js是单线程的,浏览器也是单线程的吗? 172 | 答:不,浏览器是多线程的,但我觉得本质上也是单线程。网上的资料显示浏览器分为GUI渲染线程(解析渲染布局绘制)、JS引擎线程和事件队列线程(事件、定时器、ajax) -------------------------------------------------------------------------------- /面向对象.md: -------------------------------------------------------------------------------- 1 | ###面向对象 2 | --- 3 | **面向对象: 数据结构化,对代码的一种抽象,对外统一提供调用接口的变成思想。** 4 | 基于原型的面向对象方式中,对象(object)则是依靠构造器(constructor)利用原型(prototype)构造出来的 5 | 6 | 函数构造器 创建函数对象 7 | var obj = new Function(var1, var2, ..., functionBody()) 8 | var1,var2 正常变量 functionBody()自定义函数 9 | `构造器构造的对象,效率低,var1 var2顺序在functionBody中不能变` 10 | 11 |
12 | 13 | ###闭包 14 | 闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数) 15 | 用途: 1 读取函数内部变量 16 | 2 让内部变量保存在内存中 17 | 优点: 有利于封装,可以访问局部变量 18 | 缺点: 内存占用浪费严重,内存泄漏 19 | 20 |
21 | #####构造和工厂模式不同 22 | 1. 构造方式不会显示创建对象, 将属性赋值给this, 不需要return对象 23 | 2. 工厂在方法内部创建Object对象,返回object对象,属性和方法都是赋值给object 24 | 25 | 原型模式声明对象 26 | test。prototype。。 27 | 让所有的实例共享方法 28 | 29 |
30 | #####原型和原型链 31 | 原型:是利用prototype添加属性和方法 32 | 原型链:JS在创建对象(不论是普通对象还是函数对象)的时候,都会有一个叫做`__proto__`的内置属性,用于指向创建他的函数对象的原型对象prototype 33 | 34 | 创建对象三个阶段 35 | var person = function () {} 36 | var p = new person() 37 | 三个阶段 38 | 1. var p = {} 闯将对象 39 | 2. `p.__ptoto__ = person.prototype` proto自带的一个属性 40 | 3. 创建对象(初始化对象) p --> person.call(p) 41 |
42 | 43 | #####原型链继承、构造函数继承、call apply继承 44 |
45 | callee 返回正在执行的function对象,function内容 46 | arguments.callee 默认值 正在执行的function对象 47 | 48 | 49 | ##### 《UNIX/LINUX设计哲学》 50 | - 准则1:小即是美 51 | - 准则2:让每个程序只做好一件事 52 | - 准则3:快速建立原型 53 | - 准则4:舍弃高效率而取可移植性 54 | - 准则5:采用纯文本来存储数据 55 | - 准则6:杠杆效应 56 | - 准则7:使用shell脚本来提高杠杆效应和可移植性 57 | - 准则8:避免强制性的用户界面 58 | - 准则9:让每个程序成为过滤器 59 | 60 | ##### SOLID五大设计原则 61 | - S(single) 单一职责原则 62 | - O(open) 开放封闭原则 63 | - L 李式置换原则 64 | - I(interface) 接口独立原则 65 | - D 依赖导致原则 (依赖于抽象而不依赖于具体) 66 | 67 | ##### 23种设计模式 68 | 1、创建型 2、组合型 3、行为型 69 | 70 | 1. 创建型 71 | - 工厂模式 72 | - 单例模式 73 | - 原型模式 74 | 75 | 2. 结构型 76 | - 适配器模式 77 | - 装饰器模式 78 | - 代理模式 79 | - 外观模式 80 | - ~~桥接模式~~ 81 | - ~~组合模式~~ 82 | - ~~享元模式~~ 83 | 84 | 3. 行为型 85 | - ~~策略模式~~ 86 | - ~~模板方式模式~~ 87 | - 观察者模式 88 | - 迭代器模式 89 | - ~~职责连模式~~ 90 | - ~~命令模式~~ 91 | - 状态模式 92 | 93 | 94 | ##### 工厂模式 95 | ``` 96 | class Product{ 97 | constructor(name) { 98 | this.mame = name 99 | } 100 | } 101 | 102 | class Creator() { 103 | create(name) { 104 | return new Product(name) 105 | } 106 | } 107 | ``` 108 | `$('div') 和 new $('div')有何区别` 109 | React的createElement方法 110 | Vue的异步组件 111 | 设计原则验证 112 | - 构造函数和创建者分离 113 | - 114 | 115 | ##### 单例模式 116 | ``` 117 | public class SingleObject { 118 | private SingleObject () { 119 | 120 | } 121 | 122 | private SingleObject instance = null 123 | 124 | public SingleObject getInstance() { 125 | if (instance == null) { 126 | instance = new SingleObject () 127 | } 128 | return instance 129 | } 130 | 131 | public void login(username, password) { 132 | System.out.println('login') 133 | } 134 | } 135 | ``` 136 | 137 | #### 适配器模式 138 | ``` 139 | class Adaptee{ 140 | specificRequest() { 141 | return '德国标准接口' 142 | } 143 | } 144 | 145 | class Target { 146 | constructor() { 147 | this.adaptee = new Adaptee() 148 | } 149 | 150 | request() { 151 | let info = this.adaptee.specificRequest() 152 | return `${info} -> 转接器 -> 中国标准接口` 153 | } 154 | } 155 | 156 | let target = new Target() 157 | target.request() 158 | ``` 159 | - 将旧接口和使用者进行分离 160 | - 符合开放封闭原则 161 | 162 | #### 装饰器模式 163 | - 为对象添加新功能 164 | - 不改变其原有的接口和功能 165 | 166 | 167 | ``` 168 | class Circle { 169 | draw() { 170 | console.log('画一个圆') 171 | } 172 | } 173 | class Decorator { 174 | constructor() { 175 | this.circle = new Circle() 176 | } 177 | 178 | draw() { 179 | this.circle.draw() 180 | this.setRedLine() 181 | } 182 | 183 | setRedLine() { 184 | console.log('设置红色边框') 185 | } 186 | } 187 | 188 | let c1 = new Circle() 189 | c1.draw() 190 | console.log('------') 191 | let c2 = new Decorator() 192 | c2.draw() 193 | ``` 194 | 使用场景ES7 decorator 195 | 196 | 197 | ``` 198 | // 使用方法 199 | @testDec 200 | class Demo{ 201 | 202 | } 203 | function testDec(target) { 204 | target.isDec = true 205 | } 206 | console.log(Demo.isDec) // true 207 | 208 | // 使用方法2 209 | function testDec(isDecVal) { // 将变量传入 210 | return function (target) { // decorator返回一个函数 211 | target.isDec = isDecVal 212 | } 213 | } 214 | @testDec(false) 215 | class Demo{ 216 | 217 | } 218 | console.log(Demo.isDec) // false 219 | 220 | // 对属性添加readOnly 221 | function readOnly (target, name, descriptor) { 222 | descriptor.writable = false 223 | return descriptor 224 | } 225 | 226 | class Person { 227 | constructor(name, age) { 228 | this.name = name 229 | this.age = age 230 | } 231 | 232 | getName() { 233 | return `${this.name} and ${this.age}` 234 | } 235 | } 236 | 237 | var xiaoming = new Person('xiaoming', '18') 238 | xiaoming.getName() 239 | xiaoming.getName = function () {} // error 240 | 241 | // 打印日志 242 | function log(target, name, descriptor) { 243 | let oldValue = descriptor.value 244 | descriptor.value = function () { 245 | console.log('is doing ${name} ', arguments) 246 | return oldValue.call(this, arguments) 247 | } 248 | return descriptor 249 | } 250 | 251 | class Math() { 252 | 253 | @log 254 | add(a, b) { 255 | return a + b; 256 | } 257 | } 258 | 259 | var obj = new Math() 260 | obj.add(1, 2) // is doing add, 1 , 2 261 | ``` 262 | - 讲现有对象和装饰器分离,两者独立存在 263 | - 符合开放封闭原则 264 | 265 | 266 | ##### 代理模式 267 | ``` 268 | let star = { 269 | name: 'zhang ming', 270 | age: 25, 271 | phone: 'start 111111' 272 | } 273 | 274 | let agent = new Proxy(star, { 275 | get: function (target, key) { 276 | if (key === 'phone') { 277 | return 'agent 222222' 278 | } 279 | if (key === 'price') { 280 | return 'agemt 100K' 281 | } 282 | return target[key] 283 | }, 284 | set: function (target, key, val) { 285 | if (key === 'price') { 286 | if (val < 120) { 287 | throw new Error('价格太低') 288 | } else { 289 | target.customPrice = val 290 | } 291 | } 292 | } 293 | }) 294 | 295 | console.log(agent.name) 296 | console.log(agent.name) 297 | console.log(agent.phone) 298 | console.log(agent.price) 299 | agent.price = 130 300 | console.log(agent.customPrice) 301 | ``` 302 | - 适配器模式 vs 代理模式 303 | 适配器模式: 提供一个不同的接口(如不同版本的插头) 304 | 代理模式:提供一模一样的接口 305 | 306 | - 代理模式 vs 装饰器模式 307 | 装饰器模式:扩展功能,原有功能不变且可直接使用 308 | 代理模式:显示原有功能,但是经过限制或者阉割之后的 309 | 310 | ##### 外观模式 311 | - 为子系统中的一组接口提供一个高层接口 312 | - 使用者使用这个高层接口 313 | 314 | ##### `观察者模式` 315 | - 发布&订阅 316 | - 一对多 317 | 318 | 319 | ``` 320 | class Subject{ 321 | constructor() { 322 | this.state = 0 323 | this.notifyArr = [] 324 | } 325 | 326 | setState(state) { 327 | this.state = state 328 | this.notifyAll() 329 | } 330 | 331 | getState() { 332 | return this.state 333 | } 334 | 335 | notifyAll() { 336 | this.notifyArr.forEach(notify => { 337 | notify.update() 338 | }) 339 | } 340 | 341 | addNotify(fn) { 342 | this.notify.push(fn) 343 | } 344 | 345 | } 346 | 347 | class Observer{ 348 | constructor(name, subject) { 349 | this.name = name 350 | this.subject = subject 351 | this.subject.notifyArr.push(this) 352 | } 353 | 354 | update() { 355 | console.log(`${this.name} is notify, state is ${this.subject.getState()}`) 356 | } 357 | } 358 | 359 | var sub1 = new Subject() 360 | var o1 = new Observer('o1', sub1) 361 | var o2 = new Observer('o2', sub1) 362 | var o3 = new Observer('o3', sub1) 363 | sub1.setState(1) 364 | sub1.setState(2) 365 | sub1.setState(3) 366 | ``` 367 | 使用案例: 368 | 1. JQuery callback 369 | 2. Nodejs 自定义事件 370 | 其他场景 371 | - nodejs处理http请求,多进程通讯 372 | - vue和React组件生命周期触发 373 | - vue watch 374 | 375 | 376 | ``` 377 | // nodejs 自定义事件 378 | const EventEmitter = require('events').EventEmitter 379 | 380 | const emitter1 = new EventEmitter() 381 | 382 | emitter1.on('some', info => { 383 | console.log('fn1', info) 384 | }) 385 | 386 | emitter1.on('some', info => { 387 | console.log('fn2', info) 388 | }) 389 | 390 | emitter1.emit('some', 'message') 391 | ``` 392 | 设计原则验证 393 | - 主题和观察者分离,不是主动触发而是被动监听,两者解耦 394 | - 符合开放封闭原则 395 | 396 | ##### 迭代器模式 397 | - 顺序访问一个集合 398 | - 使用者无需知道集合的内部结构(封装) 399 | 400 | ES6 Iterator为何存在? 401 | - ES6语法中,有序集合的数据类型意见又很多 402 | - Array Map Set String TypedArray arguments NodeList 403 | - 需要有一个统一的遍历接口来遍历所有数据类型 404 | - (注意,object不是有序集合,可以用Map代替) 405 | - 以上数据类型,都有[Symbol,iterator]属性 406 | - 属性值是函数,执行函数返回一个迭代器 407 | - 这个迭代器就有next方法可以顺序迭代子元素 408 | - 可运行Array.protptype[Symbol.iterator]来测试 --------------------------------------------------------------------------------