├── .gitattributes ├── .gitignore ├── README.md ├── contents ├── API-design-and-trade-off-based-on-HTTP.md ├── CORS-preflight.md ├── OAuth2-and-SDK-design.md ├── UI-component-split-rule.md ├── WebRTC-communication-model.md ├── a-backend-configuration-solution.md ├── a-solution-to-avoid-too-many-timer-on-backend.md ├── add-binary-mirror.md ├── async-await-solution.md ├── automatic-investment-plan-of-index-fund-cash-flow-and-sense-of-security.md ├── backend-version-strategy.md ├── cases-need-try-catch.md ├── cases-of-functional-abstract.md ├── code-review-in-companies.md ├── communication-between-UI-components-solutions.md ├── data-backup-and-recovery-strategy.md ├── data-configurations.md ├── data-model-design.md ├── data-storage-tools.md ├── debug-mobile-page.md ├── design-to-prevent-conflicts-from-multiple-user-operation.md ├── develop-photoshop-plugin-with-adobe-cep.md ├── develop-workflow-design.md ├── developing-document-related-tools.md ├── distributed-lock-on-redis.md ├── drag-program-design.md ├── encapsulation.md ├── frontend-backend-seperation.md ├── frontend-build.md ├── generator-function-abstract.md ├── gists.md ├── history-of-generating-html-page.md ├── idea-of-web-component-as-props-of-web-component.md ├── interface-and-trait.md ├── js-engineer-capability.md ├── js-project-package-version-strategy.md ├── keep-api-compatible-strategy.md ├── lerna-project-ci.md ├── map-reduce-filter.md ├── mine-sweeper-strategies.md ├── mysql-and-mongodb-storage-density-test-result.md ├── nodejs-code-strcture.md ├── nodejs-deploy-strategy.md ├── nodejs-introduction.md ├── orthogonal-rule-of-technology-selection.md ├── package-design.md ├── pattern-match.md ├── permission-design.md ├── program-design-based-on-expression.md ├── push-component-design.md ├── refactor-and-upgrade-old-js-project-strategy.md ├── rust-ownership.md ├── self-learn-off-work.md ├── serialize-object-with-binary-data-to-binary-data.md ├── several-ways-for-complex-package.md ├── several-ways-for-dependent-tasks.md ├── socket-io-room-design.md ├── some-investigations.md ├── team-work.md ├── test-strategies.md ├── timer-mechanism-design.md ├── token-design.md ├── tslint-rules.md ├── type-safe-physical-quantity.md ├── type-tool-for-js.md ├── typescript-type-system-best-practice.md ├── ui-component-for-muitiple-framework.md ├── undo-redo.md ├── unit-test-weakness-and-baseline-test.md ├── use-vue-as-store-rather-than-vuex.md ├── various-data-schema.md ├── vuejs-reactjs-angular-usage-detail.md ├── ways-of-binary-data-transfer-between-backend-and-frontend.md ├── web-backend-extension.md ├── web-render-target-interface.md ├── web-security.md ├── websocket-and-http-trade-off.md ├── websocket-protocol-and-debug.md └── ws-and-socket-io-stability-of-transfer-data.md ├── index.html ├── script.js ├── service-worker.bundle.js ├── vendor.bundle-bff321611bdd7ea907dfe701b4cc4aa6.js └── vendor.bundle-e1f50027c0492b980cf89c94c5f2ae04.css /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-language=Markdown 2 | index.html linguist-generated=true 3 | service-worker.bundle.js linguist-generated=true 4 | vendor.bundle-*.js linguist-vendored 5 | vendor.bundle-*.css linguist-vendored 6 | *.md linguist-documentation=false 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | ### 程序设计 4 | 5 | + [撤销重做的程序设计](.?src=./contents/undo-redo.md) 6 | + [拖拽交互的程序设计](.?src=./contents/drag-program-design.md) 7 | + [基于表达式的程序设计](.?src=./contents/program-design-based-on-expression.md) 8 | + [有依赖的任务的几种执行方式](.?src=./contents/several-ways-for-dependent-tasks.md) 9 | + [复杂 package 的几种实现方式](.?src=./contents/several-ways-for-complex-package.md) 10 | + [支持多个框架的 UI 组件设计](.?src=./contents/ui-component-for-muitiple-framework.md) 11 | + [基于 HTTP 协议的 API 设计和取舍](.?src=./contents/API-design-and-trade-off-based-on-HTTP.md) 12 | + [权限设计](.?src=./contents/permission-design.md) 13 | + [推送组件设计](.?src=./contents/push-component-design.md) 14 | + [token 的设计](.?src=./contents/token-design.md) 15 | + [OAuth2 和供第三方调用的 SDK 设计](.?src=./contents/OAuth2-and-SDK-design.md) 16 | + [socket.io 的 room 设计](.?src=./contents/socket-io-room-design.md) 17 | + [几种定时机制设计](.?src=./contents/timer-mechanism-design.md) 18 | + [一种防多用户操作引起冲突的设计](.?src=./contents/design-to-prevent-conflicts-from-multiple-user-operation.md) 19 | + [一类避免服务端大量定时器的思路](.?src=./contents/a-solution-to-avoid-too-many-timer-on-backend.md) 20 | + [一种后台配置方案](.?src=./contents/a-backend-configuration-solution.md) 21 | + [UI 组件间通信的几个解决方案](.?src=./contents/communication-between-UI-components-solutions.md) 22 | + [js 的几种表示类型的工具](.?src=./contents/type-tool-for-js.md) 23 | + [一个 web 渲染 target 抽象](.?src=./contents/web-render-target-interface.md) 24 | 25 | ### 测试和调试 26 | 27 | + [测试策略总结](.?src=./contents/test-strategies.md) 28 | + [移动端页面调试的方法总结](.?src=./contents/debug-mobile-page.md) 29 | + [常规单元测试的缺点和 baseline 测试](.?src=./contents/unit-test-weakness-and-baseline-test.md) 30 | 31 | ### 语言特性 32 | 33 | + [Generator 函数的抽象](.?src=./contents/generator-function-abstract.md) 34 | + [几个简单的函数式抽象的实现](.?src=./contents/cases-of-functional-abstract.md) 35 | + [rust 的 ownership](.?src=./contents/rust-ownership.md) 36 | + [interface 和 trait](.?src=./contents/interface-and-trait.md) 37 | + [模式匹配](.?src=./contents/pattern-match.md) 38 | + [async / await 方案](.?src=./contents/async-await-solution.md) 39 | + [map / reduce / filter 相关模式](.?src=./contents/map-reduce-filter.md) 40 | + [需要 try catch 的场景](.?src=./contents/cases-need-try-catch.md) 41 | 42 | ### 流程 43 | 44 | + [在公司内部采用 code review 流程的总结](.?src=./contents/code-review-in-companies.md) 45 | + [多人协作的感想](.?src=./contents/team-work.md) 46 | + [开发工作流设计](.?src=./contents/develop-workflow-design.md) 47 | + [开发文档相关工具的取舍](.?src=./contents/developing-document-related-tools.md) 48 | 49 | ### 总结 50 | 51 | + [使用 Adobe CEP 开发 Photoshop 插件](.?src=./contents/develop-photoshop-plugin-with-adobe-cep.md) 52 | + [前后端分离实践总结](.?src=./contents/frontend-backend-seperation.md) 53 | + [前端构建过程实践总结](.?src=./contents/frontend-build.md) 54 | + [web 安全总结](.?src=./contents/web-security.md) 55 | + [vuejs, reactjs 和 angular 使用细节总结](.?src=./contents/vuejs-reactjs-angular-usage-detail.md) 56 | + [封装总结](.?src=./contents/encapsulation.md) 57 | + [js 工程师的能力总结](.?src=./contents/js-engineer-capability.md) 58 | 59 | ### 策略 60 | 61 | + [保证接口兼容性的策略](.?src=./contents/keep-api-compatible-strategy.md) 62 | + [后端的版本策略](.?src=./contents/backend-version-strategy.md) 63 | + [数据备份和恢复策略](.?src=./contents/data-backup-and-recovery-strategy.md) 64 | + [nodejs 部署策略](.?src=./contents/nodejs-deploy-strategy.md) 65 | + [扫雷游戏的策略](.?src=./contents/mine-sweeper-strategies.md) 66 | + [js 项目的 package 版本策略](.?src=./contents/js-project-package-version-strategy.md) 67 | + [技术选型时的正交原则](.?src=./contents/orthogonal-rule-of-technology-selection.md) 68 | + [UI 组件拆分原则](.?src=./contents/UI-component-split-rule.md) 69 | + [websocket 和 http 的权衡](.?src=./contents/websocket-and-http-trade-off.md) 70 | + [重构升级旧 js 项目的思路](.?src=./contents/refactor-and-upgrade-old-js-project-strategy.md) 71 | 72 | ### 通信协议 73 | 74 | + [WebRTC 的通信模型](.?src=./contents/WebRTC-communication-model.md) 75 | + [websocket 协议及其调试](.?src=./contents/websocket-protocol-and-debug.md) 76 | 77 | ### 结构 78 | 79 | + [package 的设计](.?src=./contents/package-design.md) 80 | + [web 后端结构扩展方向](.?src=./contents/web-backend-extension.md) 81 | + [nodejs 的代码组织形式](.?src=./contents/nodejs-code-strcture.md) 82 | + [lerna 项目的 CI 优化](.?src=./contents/lerna-project-ci.md) 83 | 84 | ### 记录 85 | 86 | + [为 npm 包里的二进制文件增加 mirror](.?src=./contents/add-binary-mirror.md) 87 | + [一些技术的调查记录](.?src=./contents/some-investigations.md) 88 | + [gists](.?src=./contents/gists.md) 89 | + [nodejs 概要介绍分享记录](.?src=./contents/nodejs-introduction.md) 90 | + [mysql 和 mongodb 的存储密度测试结果](.?src=./contents/mysql-and-mongodb-storage-density-test-result.md) 91 | 92 | ### 数据 93 | 94 | + [数据模型设计](.?src=./contents/data-model-design.md) 95 | + [多种 schema 的维护](.?src=./contents/various-data-schema.md) 96 | + [把含二进制数据的对象序列化成二进制](.?src=./contents/serialize-object-with-binary-data-to-binary-data.md) 97 | + [前后端交互二进制数据的方式](.?src=./contents/ways-of-binary-data-transfer-between-backend-and-frontend.md) 98 | + [几种数据存储工具](.?src=./contents/data-storage-tools.md) 99 | + [各个级别的数据配置](.?src=./contents/data-configurations.md) 100 | 101 | ### 其它 102 | 103 | + [使用 vuejs 实例而非 vuex 作为 store](.?src=./contents/use-vue-as-store-rather-than-vuex.md) 104 | + [基于 redis 的分布式锁](.?src=./contents/distributed-lock-on-redis.md) 105 | + [向 web 组件的 props 传入 web 组件的思想](.?src=./contents/idea-of-web-component-as-props-of-web-component.md) 106 | + [类型安全的物理量抽象](.?src=./contents/type-safe-physical-quantity.md) 107 | + [tslint 规则相关](.?src=./contents/tslint-rules.md) 108 | + [工作之外的自学思路](.?src=./contents/self-learn-off-work.md) 109 | + [typescript 类型系统的最佳实践](.?src=./contents/typescript-type-system-best-practice.md) 110 | + [生成 html 页面的方式演化](.?src=./contents/history-of-generating-html-page.md) 111 | + [CORS 的 preflight](.?src=./contents/CORS-preflight.md) 112 | + [ws 和 socket.io 传输数据的可靠性](.?src=./contents/ws-and-socket-io-stability-of-transfer-data.md) 113 | 114 | ### 技术之外 115 | 116 | + [指数基金定投、现金流和安全感](.?src=./contents/automatic-investment-plan-of-index-fund-cash-flow-and-sense-of-security.md) 117 | -------------------------------------------------------------------------------- /contents/API-design-and-trade-off-based-on-HTTP.md: -------------------------------------------------------------------------------- 1 | # 基于 HTTP 协议的 API 设计和取舍 2 | 3 | 现在常见的两种 API 形式是: 4 | 5 | 1、命令和查询:即所有查询都是 GET 请求,所有命令都是 POST 请求,URL 中用名词和动词表示查询的内容和命令的内容 6 | 7 | 2、RESTful:是一种基于资源的形式,用 HTTP METHOD 来表示增删改查的动作,URL 中不会存在动词 8 | 9 | API 设计时也要考虑前端平台的支持度,比如某些平台只支持 GET/POST,比如 jsonp 只支持 GET。 10 | 11 | 对于 RESTful 形式,一般是 `api.example.com` 或 `example.com/api/` 的形式,版本号可以是 `/api/v1` 或 `/api/user?v=1` 的形式,也可以放在 header 中。 12 | 13 | 常见的 RESTful 实践是: 14 | 15 | + `GET` `/api/users`: 查询用户的信息,查询条件会在参数中 16 | + `GET` `/api/users/:id`: 查询某个 ID 的用户的信息 17 | + `GET` `/api/users/:id/books`: 查询某个 ID 的用户的书,查询条件会在参数中 18 | + `GET` `/api/users/:id/books/:id`: 查询某个 ID 的用户的某个 ID 的书 19 | 20 | 可以看出明显的层级关系,这里 `GET` 表示查询,可以用 `POST` 表示创建、获得,`PUT` 表示修改、更新,`DELETE` 表示删除。 21 | 22 | + `GET` `/api/users/:id/name`: 如果是唯一或不可数的资源,可以直接使用单数形式 23 | + `GET` `/api/user/books`: 查询当前用户的书,如果 URL 中有表示角色的词,表示当前用户作为这个角色来操作,这时需要验证身份并鉴权 24 | + `GET` `/api/teacher/students`: 查询当前教师的学生 25 | 26 | 当然也有纯粹的命令,不好对应到资源上来,可以考虑由于这种命令,某些对象的状态发生变化,把这种状态当成一种资源,例如: 27 | 28 | + `POST` `/api/user/books/:id/burnt`: 当前用户烧了某本书 29 | + `POST` `/api/user/books/:id/sold`: 当前用户卖了某本书 30 | 31 | 另外,RESTful 用响应的 status code 来区分不同的结果,但是在国内,对于非 https 的请求,运营商可能会劫持非 200 的响应,所以有必要采用 https,或者可以考虑放弃这样的规则,统一返回 200,并在 body 中反映响应的结果。 32 | -------------------------------------------------------------------------------- /contents/CORS-preflight.md: -------------------------------------------------------------------------------- 1 | # CORS 的 preflight 2 | 3 | 当 CORS 带 preflight 时,会发现发出了 2 次请求,第 1 次的 method 是 OPTIONS,第 2 次才是实际需要的请求; 4 | 当 CORS 不带 preflight:会发现发出了 1 次请求,是实际需要的请求; 5 | 6 | 从服务端角度看,如果服务端含有没有考虑 cors 的旧代码,应该支持 preflight,这样可以兼容这些旧代码; 7 | 否则可以不带 preflight,以降低请求数,从而提高性能; 8 | CORS 内的 preflight 的存在是为了保持服务器端的兼容性; 9 | 当服务端不支持处理 OPTIONS 请求时,就算是不支持 preflight 了。 10 | 11 | 从前端角度看,当请求的 Content-Type 是 `application/x-www-form-urlencoded`、`multipart/form-data` 或 `text/plain` 时,不会触发 preflight,否则会触发 preflight; 12 | 根据这个特性可以控制请求请求是否需要触发 preflight; 13 | jquery 默认的 Content-Type 是 `application/x-www-form-urlencoded; charset=UTF-8`,是不触发 preflight 的。 -------------------------------------------------------------------------------- /contents/OAuth2-and-SDK-design.md: -------------------------------------------------------------------------------- 1 | # OAuth2 和供第三方调用的 SDK 设计 2 | 3 | 总体来说,都需要安全性,即要求采用 https。 4 | 5 | 它们都涉及到同第三方交互,接入功能时都会生成 id 和 secret 给第三方。 6 | 7 | OAuth2 本质是这样一种机制,用户可以授权第三方访问自己在本站的资源(例如个人信息、头像),或代替自己在本站执行某种操作(例如发微博)。 8 | 从第三方的角度看,第三方通过 id 和 secret 获得 access_token,再通过 access_token 访问公开 API,获得 scopes 内的资源。 9 | 从用户的角度看,用户的浏览器会跳到本站域名下,如果需要登陆,则跳到登录页面登录,登陆后,如果没有授权过,或者授权范围扩大了,则跳转到授权页面授权,授权后,跳转到设置好的第三方页面,显示出第三方获得的本站资源。 10 | 这个过程中,用户可以随时选择授权,即使授权过,也可以在本站吊销对第三方网站的授权。 11 | 可以看到,用户可以意识到本站提供的授权过程。 12 | 13 | 供第三方调用的 SDK,通常只提供少量接口,没有中间的 access_token,可能是异步调用,例如支付,也可能是同步调用,例如 sms。 14 | 15 | 对于同步调用: 16 | 从第三方角度看,把接口需要的参数,以及 id 和 secret,组合起来,按照一定的加密方式,加密成签名,签名同参数一起,传递给接口,接口会返回结果。 17 | 从用户的角度看,不会直接接触到第三方页面,用户意识不到本站的这个服务。 18 | 19 | 对于异步调用: 20 | 从第三方角度看,把接口需要的参数,以及 id 和 secret,组合起来,按照一定的加密方式,加密成签名,签名同参数一起,传递给接口,接口会返回一个本站地址,浏览器跳转到这个本站地址,之后异步通知接口会收到来自本站的请求,验证签名后,执行收尾操作。 21 | 从用户的角度看,浏览器会跳转到本站页面,完成操作后,浏览器跳回第三方页面。 22 | 可以看到,用户在本站做了操作,但是没有授权过程。 23 | 24 | 对于本站来说,如果部分第三方账户被滥用,威胁其它正常账户的服务,应该及时这些异常账户。 25 | 26 | 如果提供的服务很简单,不用和用户交互,或者提供公开的数据,可以采用同步调用的方式; 27 | 如果提供的服务提供的数据比较私密,需要和用户交互,但重要性一般,或者只需要用户粗粒度的授权,不需要精确到数量,可以采用 OAuth2 的方式; 28 | 如果提高的服务提供的数据比较私密,需要和用户交互,且重要性强,或者需要用户细粒度的授权,精确到数量,可以采用异步调用并通知的方式。 -------------------------------------------------------------------------------- /contents/UI-component-split-rule.md: -------------------------------------------------------------------------------- 1 | # UI 组件拆分原则 2 | 3 | + 重复:原则上组件会在现在或未来被使用超过一次,否则不需要抽象成组件 4 | + 复杂:内部应该有一定的复杂度,如果简单到只有一个元素,组件会非常细碎 5 | + 独立:应该是一个独立的概念,而不仅仅是元素的组合 6 | + 减低实现难度:在实现复杂的组件时,如果可以拆分成多个互斥、独立的子组件,可以通过分别实现子组件,然后在组合起来,以减低实现难度,即使各个子组件不满足 “重复” 原则 7 | -------------------------------------------------------------------------------- /contents/WebRTC-communication-model.md: -------------------------------------------------------------------------------- 1 | # WebRTC 的通信模型 2 | 3 | ### 准备 4 | 5 | 使用 https://github.com/webrtc/adapter 库来屏蔽 chrome、firefox、edge 之间的差异,简化编程。 6 | 7 | ### 连接的建立 8 | 9 | 双方都要初始化 RTCPeerConnection,并创建 RTCDataChannel: 10 | 11 | ```js 12 | const peerConnection = new RTCPeerConnection(); 13 | peerConnection.ondatachannel = event => { 14 | event.channel.onopen = e => { 15 | // connection opened 16 | }; 17 | event.channel.onclose = e => { 18 | // connection closed 19 | }; 20 | event.channel.onmessage = e => { 21 | // get message 22 | }; 23 | }; 24 | const dataChannel = peerConnection.createDataChannel("test_channel_name"); // 双方的 channel 名要一致 25 | ``` 26 | 27 | 这时发起方需要发出邀约: 28 | 29 | ```js 30 | peerConnection.createOffer() 31 | .then(offer => peerConnection.setLocalDescription(offer)) 32 | .then(() => { 33 | // offer created 34 | }, error => { 35 | // error 36 | }); 37 | ``` 38 | 39 | 其中产生的 offer 是一个 RTCSessionDescription 对象 40 | 41 | RTCPeerConnection 对象有一个 localDescription 和一个 remoteDescription,它们都是 RTCSessionDescription 对象,这里需要把发起方创建的 offer 设置为自己的 localDescription 42 | 43 | RTCSessionDescription 对象由 type 和 sdp 字段组成,下面看一下一个 offer 例子: 44 | 45 | ```js 46 | { 47 | type: "offer", 48 | sdp: `v=0 49 | o=- 5188642327392386728 2 IN IP4 127.0.0.1 50 | s=- 51 | t=0 0 52 | a=msid-semantic: WMS 53 | m=application 9 DTLS/SCTP 5000 54 | c=IN IP4 0.0.0.0 55 | a=ice-ufrag:/VT3 56 | a=ice-pwd:ls9SkhIZU0j+sJ1vUkoopaOs 57 | a=fingerprint:sha-256 C0:92:D6:0F:2B:14:98:30:18:AA:45:A7:FD:05:71:26:DE:2C:D8:4F:BB:E2:FC:17:1B:1E:29:07:02:7F:68:9B 58 | a=setup:actpass 59 | a=mid:data 60 | a=sctpmap:5000 webrtc-datachannel 1024 61 | ` 62 | } 63 | ``` 64 | 65 | 这个例子里,type 字段表明这是一个 offer,dsp 中包含了 ip、端口、password 等消息。这个 offer 还不包含任何 candidate,可以稍后再执行一次 `createOffer` 过程,这样会创建一个新的 offer: 66 | 67 | ```js 68 | { 69 | type: "offer", 70 | sdp: `v=0 71 | o=- 5188642327392386728 3 IN IP4 127.0.0.1 72 | s=- 73 | t=0 0 74 | a=msid-semantic: WMS 75 | m=application 61598 DTLS/SCTP 5000 76 | c=IN IP4 192.168.20.104 77 | a=candidate:4254130987 1 udp 2113937151 192.168.20.104 61598 typ host generation 0 network-cost 50 78 | a=ice-ufrag:/VT3 79 | a=ice-pwd:ls9SkhIZU0j+sJ1vUkoopaOs 80 | a=fingerprint:sha-256 C0:92:D6:0F:2B:14:98:30:18:AA:45:A7:FD:05:71:26:DE:2C:D8:4F:BB:E2:FC:17:1B:1E:29:07:02:7F:68:9B 81 | a=setup:actpass 82 | a=mid:data 83 | a=sctpmap:5000 webrtc-datachannel 1024 84 | ` 85 | } 86 | ``` 87 | 88 | 获得有效的 offer,可以把这个 offer 序列化成字符串,再通过 websocket 等其它方式传递给接收方: 89 | 90 | ```js 91 | const offerString = JSON.stringify(offer.toJSON()); 92 | ``` 93 | 94 | 接收方获得这个字符串后,可以反序列化成 RTCSessionDescription 对象: 95 | 96 | ```js 97 | const offer = new RTCSessionDescription(JSON.parse(offerString)); 98 | ``` 99 | 100 | 然后,接收方开始针对这个 offer 给予反馈: 101 | 102 | ```js 103 | peerConnection.setRemoteDescription(offer) 104 | .then(() => peerConnection.createAnswer()) 105 | .then(answer => peerConnection.setLocalDescription(answer)) 106 | .then(() => { 107 | // get answer 108 | }, error => { 109 | // error 110 | }); 111 | ``` 112 | 113 | 这里需要把发起方发出的 offer 设为 remoteDescription,再把自己的 answer 设为 localDescription。 114 | 115 | offer 和 answer 都是 RTCSessionDescription 对象,自己产生的就设为 localDescription,别人发给我的就设为 remoteDescription。 116 | 117 | 下面是一个 answer 的例子: 118 | 119 | ```js 120 | { 121 | type: "answer", 122 | sdp: `v=0 123 | o=- 6737343248045853171 3 IN IP4 127.0.0.1 124 | s=- 125 | t=0 0 126 | a=msid-semantic: WMS 127 | m=application 51524 DTLS/SCTP 5000 128 | c=IN IP4 192.168.20.104 129 | b=AS:30 130 | a=candidate:4254130987 1 udp 2113937151 192.168.20.104 51524 typ host generation 0 network-cost 50 131 | a=ice-ufrag:g3Yt 132 | a=ice-pwd:XPhmlHFTadm0MVCtr2OgrrOc 133 | a=fingerprint:sha-256 85:06:C4:1E:64:70:D5:3F:2F:6A:23:58:1E:B2:1C:D5:9F:64:12:02:AA:AA:BD:C2:0B:7D:0E:92:D0:D5:63:60 134 | a=setup:active 135 | a=mid:data 136 | a=sctpmap:5000 webrtc-datachannel 1024 137 | ` 138 | } 139 | ``` 140 | 141 | answer 里也可能没有 candidate,如果那样,可以再试一次,没有 candidate 的 offer 和 answer 很可能建立不了连接。 142 | 143 | 现在,需要需要把这个 answer 序列化成字符串后,再通过 websocket 等方式传递给发起方,发起方再反序列化成 RTCSessionDescription 对象。 144 | 145 | 发起方获得 answer 后,再把它设为自己的 remoteDescription。 146 | 147 | 这时候,点对点的连接就应该建立起来了,双方的 onopen 事件都会被触发,之后发起方和接收方会直接通信,就不需要 websocket 参与了。 148 | 149 | ### 发送数据 150 | 151 | 连接建立后,双方都可以向对方发送消息,对方会在 onmessage 事件中收到消息: 152 | 153 | ```js 154 | dataChannel.send("Hello world!"); 155 | ``` 156 | 157 | 发送的内容可以是字符串和二进制数据,例如用于传输文本、protobuf 编码的数据、文件、音频、视频。 158 | -------------------------------------------------------------------------------- /contents/a-backend-configuration-solution.md: -------------------------------------------------------------------------------- 1 | # 一种后台配置方案 2 | 3 | 经常会有一类后台配置需求,要配置的数据结构复杂且多变,但数据量小; 4 | 5 | 因为是内部使用,对浏览器要求不高,支持最新版本的 chrome 即可。 6 | 7 | 从数据结构上看,适合用 json 格式来传递数据,而前端可以用 angular、vuejs 等 MVVM 框架来简化开发。 8 | 9 | 这里说的方案的前端开发速度更快,是 [json-editor](https://github.com/jdorn/json-editor) 前端库,通过 json schema 来定义要配置的数据的格式,UI 提供与这个 json schema 匹配的操作。 10 | 11 | 配置完成后,会得到配置好的 json 数据,再传递给后端进行验证并保存。 12 | 13 | 后端验证数据时也可以使用这个 json schema 来验证,所以是前后端共用的,可以在后端通过 API 的形式提供给前端,维护这个 schema 就可以了。 14 | 15 | 如果后端是 nodejs,对 json schema 的验证,可以使用库:https://github.com/epoberezkin/ajv 16 | 17 | 这个 json-editor 库支持前端数据验证、界面多语言、多 UI,基本满足功能需要。 18 | 19 | 对于多媒体形式的资源,例如图片,可以在统一的上传界面上传后,配置最终的 URL 即可。 20 | 21 | 另外,如果后端使用 typescript,可以通过 https://github.com/YousefED/typescript-json-schema 由 typescript 的类型生成相应的 json schema,这样只需要维护一份数据结构。 22 | -------------------------------------------------------------------------------- /contents/a-solution-to-avoid-too-many-timer-on-backend.md: -------------------------------------------------------------------------------- 1 | # 一类避免服务端大量定时器的思路 2 | 3 | 例如下面一个例子: 4 | 5 | + 逻辑上,每个用户都有一个 energy 属性,初始是最大容量(每个人的最大容量可能不一样) 6 | + 用户会使用 energy 来完成一些其它操作(不同操作需要的 energy 可能不一样) 7 | + 用户也会偶尔获得一些 energy,但是不会超过最大容量 8 | + 如果 energy 不是最大容量,energy 会随着时间逐渐线性增长(每个人的增长速率可能不一样) 9 | + energy 增长到最大容量后就不再增长 10 | 11 | 直接的解决方案是,为每个 energy 没有达到最大容量的用户配备一个定时器,定时增加 energy,直到达到最大容量。 12 | 13 | 上面这个方案,在活跃用户很多时,会需要大量的定时器,也有大量的数据操作,很消耗资源。 14 | 15 | 不那么消耗资源的一种方法是: 16 | 17 | + 没有定时器来操作 energy 18 | + 在增减 energy 数量后,保存 energy 数量和操作时间 19 | + 在查询和增减 energy 数量前,根据 energy 数量、增减速度、容量、记录的操作时间,计算出当前的实际 energy,代码类似于: 20 | 21 | ```ts 22 | function getRealEnergy(energy: number, rate: number, limit: number, time: number) { 23 | if (energy >= limit) { 24 | return limit; 25 | } 26 | energy += Math.floor((Date.now() - time) * rate); 27 | return energy >= limit ? limit : energy; 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /contents/add-binary-mirror.md: -------------------------------------------------------------------------------- 1 | # 为 npm 包里的二进制文件增加 mirror 2 | 3 | ## 原因 4 | 5 | 很多二进制文件保存在 aws s3 上,下载慢,并且容易失败 6 | 7 | 增加 mirror 后,可以加快 CI 里 npm 包的安装速度,提高成功率 8 | 9 | ## cnpm 的 mirror 10 | 11 | cnpm 的 mirror 了很多常用包里的二进制文件: 12 | 13 | ## 放在 oss 上 14 | 15 | 某些包的二进制文件,cnpm 的 mirror 没有提供,可以把二进制文件下载下来,放在 oss 上 16 | 17 | 其中一部分包提供了详细的二进制文件下载说明、mirror 地址环境变量名称等,例如 sharp ,按照文件操作即可 18 | 19 | 也有一部分包,虽然没有提供 mirror 文档,但依赖 node-pre-gyp 包进行二进制包的下载等操作,例如 canvas,它们的 mirror 方式是: 20 | 21 | 1. 环境变量名称:{module_name}_binary_host_mirror,其中 module_name 在 package.json 里的 binary 字段下 22 | 2. 二进制文件下载地址:{host}{remote_path}/${package_name},其中 host、remote_path、package_name 都在 package.json 里的 binary 字段下 23 | 3. 可以在 `yarn add {foo} --verbose` 的信息中搜到下载地址,可以用作查看和验证 24 | -------------------------------------------------------------------------------- /contents/async-await-solution.md: -------------------------------------------------------------------------------- 1 | # async / await 方案 2 | 3 | javascript 的 callback hell 一直被认为是一个大坑 4 | 5 | 为了解决这个问题,可以采用 promise、generator,或者第三方的库(比如 Async) 6 | 7 | 而 ES7 中的 async/await 则可以很优雅地解决这类问题。 8 | 9 | 有人会问,ES7 还早吧?ES7 标准确定了吗?node 支持吗?浏览器支持吗?具体有什么好处呢?现在看不懂 async/await 逻辑怎么办? 10 | 11 | ### ES7 的进度 12 | 13 | ES7 的进度可以看 https://github.com/tc39/ecma262 ,可以看到 async/await 已经~~达到 stage 3~~完成,语法已经被确定,会基于 Promise 来实现(~~ 具体 stage 3 的含义可见 https://tc39.github.io/process-document/ ,stage 3 是 “all semantics, syntax and API are completed described”~~)。 14 | 15 | ### 支持情况 16 | 17 | v8 和 ChakraCore 这两个 js 引擎的最新版已经支持 async/await,但 node(更新:node v7.x 开始支持了,将于 2016-10 发布)和大多数浏览器(目前只有 Edge 支持)还不支持。不过这个不是问题,因为有工具可以把 ES7 代码编译成 ES6(在 node 中使用,因为 node v4 后支持大部分 ES6 特性)或 ES5(在浏览器中使用),常用的工具有 babel.js 和 typescript。 18 | 19 | ### 简单例子 20 | 21 | 下面举个例子来说明具体用法,需求是先执行 action1,如果结果是 true,执行 action2,否则执行 action3。由于原生接口还是 callback 的形式,需要先把这些 action 转换成 promise 形式: 22 | 23 | ```javascript 24 | "use strict"; 25 | 26 | function action1Async() { 27 | return new Promise((resolve, reject) => { 28 | setTimeout(() => { 29 | resolve(true); 30 | }, 1000); 31 | }); 32 | } 33 | function action2Async() { 34 | return new Promise((resolve, reject) => { 35 | setTimeout(() => { 36 | resolve(); 37 | }, 1000); 38 | }); 39 | } 40 | function action3Async() { 41 | return new Promise((resolve, reject) => { 42 | setTimeout(() => { 43 | resolve(123); 44 | }, 1000); 45 | }); 46 | } 47 | ``` 48 | 49 | 代码是: 50 | 51 | ```javascript 52 | async function main() { // void -> Promise 53 | let result1 = await action1Async(); // Promise -> boolean 54 | if (result1 === true) { 55 | await action2Async(); // Promise -> void 56 | } else { 57 | let result3 = await action3Async(); // Promise -> number 58 | console.log(result3); 59 | } 60 | } 61 | 62 | main(); 63 | ``` 64 | 65 | 代码中的 async 关键字是个 “修饰符”,类似于 public/private/static 等,async 关键字会改变函数的返回值,如果函数返回一个 number,加上 async 后会变成返回 Promise(`T `-> `Promise`)。 66 | 67 | 代码中的 await 关键字是一个 “运算符”,一元,类似于负号 “-”,它的右边应该是个 Promise,它的返回值会是这个 Promise 的结果(`Promise` -> `T`)。 68 | 69 | ### 编译后的代码 70 | 71 | 如果我不信任编译后的代码,怎么办?那就先看一下生成后的代码是什么样的,首先是 typescript 编译成 ES6 后的代码: 72 | 73 | ```javascript 74 | "use strict"; 75 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promise, generator) { 76 | return new Promise(function (resolve, reject) { 77 | generator = generator.call(thisArg, _arguments); 78 | function cast(value) { return value instanceof Promise && value.constructor === Promise ? value : new Promise(function (resolve) { resolve(value); }); } 79 | function onfulfill(value) { try { step("next", value); } catch (e) { reject(e); } } 80 | function onreject(value) { try { step("throw", value); } catch (e) { reject(e); } } 81 | function step(verb, value) { 82 | var result = generator[verb](value); 83 | result.done ? resolve(result.value) : cast(result.value).then(onfulfill, onreject); 84 | } 85 | step("next", void 0); 86 | }); 87 | }; 88 | function action1Async() { 89 | return new Promise((resolve, reject) => { 90 | setTimeout(() => { 91 | resolve(true); 92 | }, 1000); 93 | }); 94 | } 95 | function action2Async() { 96 | return new Promise((resolve, reject) => { 97 | setTimeout(() => { 98 | resolve(); 99 | }, 1000); 100 | }); 101 | } 102 | function action3Async() { 103 | return new Promise((resolve, reject) => { 104 | setTimeout(() => { 105 | resolve(123); 106 | }, 1000); 107 | }); 108 | } 109 | function main() { 110 | return __awaiter(this, void 0, Promise, function* () { 111 | let result1 = yield action1Async(); 112 | if (result1 === true) { 113 | yield action2Async(); 114 | } 115 | else { 116 | let result3 = yield action3Async(); 117 | console.log(result3); 118 | } 119 | }); 120 | } 121 | main(); 122 | ``` 123 | 124 | 可以看到,增加了一个公用的__awaiter 函数,三个 action 的代码没有变化,async/await 被 ES6 的 generator 和 yield 替代,逻辑上并没有大的变化,编译前后的代码片段容易一一对应。 125 | 126 | 再看用 babel.js 编译成 ES5 后的代码: 127 | 128 | ```javascript 129 | "use strict"; 130 | var __awaiter = undefined && undefined.__awaiter || function (thisArg, _arguments, Promise, generator) { 131 | return new Promise(function (resolve, reject) { 132 | generator = generator.call(thisArg, _arguments); 133 | function cast(value) { 134 | return value instanceof Promise && value.constructor === Promise ? value : new Promise(function (resolve) { 135 | resolve(value); 136 | }); 137 | } 138 | function onfulfill(value) { 139 | try { 140 | step("next", value); 141 | } catch (e) { 142 | reject(e); 143 | } 144 | } 145 | function onreject(value) { 146 | try { 147 | step("throw", value); 148 | } catch (e) { 149 | reject(e); 150 | } 151 | } 152 | function step(verb, value) { 153 | var result = generator[verb](value); 154 | result.done ? resolve(result.value) : cast(result.value).then(onfulfill, onreject); 155 | } 156 | step("next", void 0); 157 | }); 158 | }; 159 | function action1Async() { 160 | return new Promise(function (resolve, reject) { 161 | setTimeout(function () { 162 | resolve(true); 163 | }, 1000); 164 | }); 165 | } 166 | function action2Async() { 167 | return new Promise(function (resolve, reject) { 168 | setTimeout(function () { 169 | resolve(); 170 | }, 1000); 171 | }); 172 | } 173 | function action3Async() { 174 | return new Promise(function (resolve, reject) { 175 | setTimeout(function () { 176 | resolve(123); 177 | }, 1000); 178 | }); 179 | } 180 | function main() { 181 | return __awaiter(this, void 0, Promise, regeneratorRuntime.mark(function callee$1$0() { 182 | var result1, result3; 183 | return regeneratorRuntime.wrap(function callee$1$0$(context$2$0) { 184 | while (1) switch (context$2$0.prev = context$2$0.next) { 185 | case 0: 186 | context$2$0.next = 2; 187 | return action1Async(); 188 | 189 | case 2: 190 | result1 = context$2$0.sent; 191 | 192 | if (!(result1 === true)) { 193 | context$2$0.next = 8; 194 | break; 195 | } 196 | 197 | context$2$0.next = 6; 198 | return action2Async(); 199 | 200 | case 6: 201 | context$2$0.next = 12; 202 | break; 203 | 204 | case 8: 205 | context$2$0.next = 10; 206 | return action3Async(); 207 | 208 | case 10: 209 | result3 = context$2$0.sent; 210 | 211 | console.log(result3); 212 | 213 | case 12: 214 | case "end": 215 | return context$2$0.stop(); 216 | } 217 | }, callee$1$0, this); 218 | })); 219 | } 220 | main(); 221 | ``` 222 | 223 | 可以看到,增加了一个公用的__awaiter 函数,三个 action 的代码没有变化,async/await 被替代,逻辑上变化要大一些,编译前后的代码片段一一对应起来要难一些。 224 | 225 | ### 更复杂的例子 226 | 227 | 上面的例子比较简单,只有顺序执行和条件,可以看一些更复杂的例子,比如循环。一般如果要不定长数组里的 promise 依次执行,可以用函数式 / 递归来做,每次只处理第一个 promise,处理后把数组的其它部分作为新数组递归执行,直到全部完成,比较麻烦。 228 | 229 | 而使用 async/await 的话,逻辑就很清晰: 230 | 231 | ```javascript 232 | async function main(promises) { // promises 是一个 promise 数组 233 | let result = 0; 234 | for(let promise of promises) { 235 | result += await promise; 236 | } 237 | return result; 238 | } 239 | ``` 240 | 241 | ### 还可以使用 Promise 242 | 243 | 当然,由于 async/await 是基于 Promise,`Promise.all()` 和 `Promise.race()` 也可以很方便地配合使用,从而更灵活地实现复杂逻辑。 244 | 245 | ```javascript 246 | async function main(promises) { // promises 是一个 promise 数组 247 | const results = await Promise.all(promises); 248 | return results.reduce((previousValue, currentValue) => { 249 | return previousValue + currentValue; 250 | }, 0); 251 | } 252 | ``` 253 | 254 | ### error 处理 255 | 256 | async/await 的另一个好处是:error 处理。 257 | 258 | try...catch 不能捕获 callback 里的抛出的异常,但是却可以捕获 await 的 promise 里抛出的异常(又可以做到像 C/C++/JAVA/C# 那样,一个 try...catch 捕获所有错误了)。这样就可以非常方便地处理 error 了,不用再担心哪个 callback 是不是忘了处理 error,从而导致程序崩溃了。 259 | 260 | ### 结论 261 | 262 | 目前, 263 | 1. 如果使用的 node 版本支持 ES6,并且业务有点复杂度,可以考虑使用 async/await 编译到 ES6。 264 | 2. 如果前端 js 的业务有点复杂度,并且可以容忍编译到 ES5 后的代码,可以考虑使用 async/await 编译到 ES5。 265 | 266 | 另外 async/await 并不是 ES7 独有的,C#5.0 和 python3.5 都有类似的语法,可以看下面的例子: 267 | 268 | ```csharp 269 | public async Task Register(User user) 270 | { 271 | await Account.Value.Register(user); // Task -> void 272 | await Mail.Value.NeedApproval(); // Task -> void 273 | return RedirectToAction("Index", "Home"); // ActionResult -> Task 274 | } 275 | ``` 276 | 277 | ```python 278 | import asyncio 279 | 280 | async def http_get(domain): 281 | reader, writer = await asyncio.open_connection(domain, 80) 282 | 283 | writer.write(b'\r\n'.join([ 284 | b'GET / HTTP/1.1', 285 | b'Host: %b' % domain.encode('latin-1'), 286 | b'Connection: close', 287 | b'', b'' 288 | ])) 289 | 290 | async for line in reader: 291 | print('>>>', line) 292 | 293 | writer.close() 294 | 295 | loop = asyncio.get_event_loop() 296 | try: 297 | loop.run_until_complete(http_get('example.com')) 298 | finally: 299 | loop.close() 300 | ``` 301 | -------------------------------------------------------------------------------- /contents/automatic-investment-plan-of-index-fund-cash-flow-and-sense-of-security.md: -------------------------------------------------------------------------------- 1 | # 指数基金定投、现金流和安全感 2 | 3 | ## 指数基金定投 4 | 5 | 指数基金定投是个人理财领域低风险、较高收益的一种理财方式,网络上已有大量介绍,是适合工薪个人的理财方式。 6 | 7 | 这种方式有几个不太明显的特点: 8 | 9 | + 需要持续的资金投入 10 | + 不能快速提高指数基金定投所占的投资比例,因为一次性的大量投入不能平摊成本 11 | + 无法预知卖出时间,如果因急需用钱而卖出,可能收益达不到预期,甚至亏本 12 | 13 | ## 现金流 14 | 15 | 因为指数基金定投的特点,需要提前准备投入的资金,如果过于保守而准备了大量的流动性资金,会浪费掉资金的时间价值,如果过于激进而导致急需用钱而不得不卖掉已有的基金,投资收益也就大打折扣。 16 | 17 | 所以需要一种工具来解决这个问题,可以既能保持需要的流动性资金,也最大化投资收益。 18 | 19 | 现金流就是这样一种工具,这种方式更广泛用于企业,企业有各类收入,也有各类支出,企业通过现金流规划,可以预留短期的支出,例如发员工工资,把多余的流动性资金投入扩大生产,或进行价值投资,实现资产增值。 20 | 21 | 个人现金流是个人未来一段时间内每天的收支计划估算,以及计算出每天的现金量。 22 | 23 | 具体来说,可以分为一下步骤: 24 | 25 | 1. 在一个 Excel (或类似工具,甚至自己开发的程序)中,第一列是日期,从今天开始的一段时间,例如一年 26 | 2. 第二列是现金流,今天的现金流是今天个人拥有的所有流动性资金总和,今天之后的现金流是一个 Excel 公式,等于前一天的现金流 + 当天的收支估计 27 | 3. 第三列之后是分类了的收支估计,根据个人实际情况增减列数 28 | 4. 预估未来的收支情况,例如每天生活支出(可以根据过去的生活支出估计)、工资收入、定期到期、定投投入、各类贷款还款、计划内大项支出等 29 | 5. 根据第一列的日期和第二列的现金流,创建一个现金流图 30 | 6. 观察现金流图中有没有现金流为负的情况,如果有,说明当前的投资策略过于激进,应当适当保守一些 31 | 7. 观察今天的现金流是否远超实际的收支需要,如果是,说明当前的投资策略过于保守,应当适当激进一些。 32 | 33 | 可以把超出的部分投入定期理财,现在的定期理财有一年、半年、三个月、两个月、一个月、两周、一周等各种期限,买多长时间的定期也是一个问题,买久了可能导致未来流动现金枯竭。 34 | 35 | 现金流也可以解决这个问题,方式是可以在现金流表中模拟一下买各个期限的定期,观察现金流图的相应变化,例如: 36 | 37 | + 如果模拟买一年期的定期后,现金流图出现了现金流为负的情况,说明激进了,不该买一年期的定期 38 | + 如果模拟买半年期的定期后,现金流图中现金流都为正,说明是合适的,可以买半年期的定期 39 | 40 | ## 安全感 41 | 42 | 有一种说法是,有了经济独立,才有人格独立。含义是工作得到稳定的收入后,人格上也不再依赖于父母。 43 | 44 | 实际很多人虽然不再依赖父母,却转而依赖企业。经济独立的广义含义,是经济上的真正独立,而不只是不依赖父母,还包括不依赖企业、配偶等等。 45 | 46 | 现金流也是一个指示经济独立性的工具。具体方法是: 47 | 48 | 1. 增加“经济独立现金流”一列,和现金流一列类似,但排除掉依赖性的收入,例如未来的工资 49 | 2. 现金流图的数据源中加入“经济独立现金流”一列的数据,这样就有了经济独立现金流图 50 | 3. 当经济独立现金流图中数据都为正数时,就可以认为是经济独立了 51 | 52 | 经济独立后,也就有了真正的人格独立,和满满的安全感。 53 | -------------------------------------------------------------------------------- /contents/backend-version-strategy.md: -------------------------------------------------------------------------------- 1 | # 后端的版本策略 2 | 3 | 这里说的 “版本” 不是 “源代码版本控制” 里的版本概念,也不是商业方面的版本概念,而是 API 接口变更的最小单位。 4 | 5 | 版本号可以是语义化版本号 `x.y.z`,也可以是简单的整数 `x`。 6 | 7 | 版本号可以体现在 URL 上,例如,`/api/v1/books`,或者 `/api/books?v=1`;也可以体现在请求的 header 中,例如 `version:1`。 8 | 9 | 后端的接口有多种类的调用方,常见的调用方如下: 10 | 11 | 1. web 页面:特点是部署方便、版本一致,没有特别要考虑的地方 12 | 2. app:特点部署麻烦,所以会有多个版本共存,后端相应的,也要同时支持多个版本,一般不会对每个版本都部署,而是在内部,根据相应的版本,执行相应的逻辑 13 | 3. 第三方开发者:这种更新频率更慢,也不会一直支持所有的旧版本,一般的策略是对旧版本限时淘汰,并在开发者文档中注明 14 | 15 | 所以总的来说,后端要支持多个版本,最新的版本一直有效,旧版本限时有效。 16 | 17 | 例如:某个接口,当版本号大于 `10` 时,执行某种策略;当版本号是 `5-9` 时,如果当前时间早于某个截止时间,执行某种策略,否则返回版本过期错误;当版本号是 `1-4` 时,如果当前时间早于另一个截止时间,执行另一种策略,否则返回版本过期错误。。。 18 | 19 | 开发时,也需要定期删除确定已过期版本的逻辑,简化代码,保持可维护性。 -------------------------------------------------------------------------------- /contents/cases-need-try-catch.md: -------------------------------------------------------------------------------- 1 | # 需要 try catch 的场景 2 | 3 | #### CLI 程序 4 | 5 | + 一般不需要 try...catch ,如果出现错误,直接退出 6 | + 对于 watch 类的 CLI,如果需要出现错误时继续 watch ,不退出程序,则需要 try...catch 7 | 8 | #### 服务端程序 9 | 10 | + 对于启动时的加载错误、初始化错误、连接错误,不需要 try...catch ,如果出现错误,直接退出 11 | + 对于程序启动后,传入了数据(例如请求参数、请求体),如果存在 throw error 的可能,需要 try...catch ,以避免因为单个请求的出错导致程序崩溃,从而影响其它请求的处理过程 12 | 13 | #### promise catch 14 | 15 | 如果有 promise ,在 promise 中 catch 异常, 要更简单一些 16 | 17 | #### 客户端程序 18 | 19 | 如果要对用户输入的数据做处理的过程可能存在错误,需要 try...catch ,并在界面上给予必要的提示 20 | 21 | #### 库 22 | 23 | 对于可能抛出异常的方法,需要在 jsDoc 上增加 @throws 声明 24 | 25 | #### 常见的可能 throw error 的场景 26 | 27 | + parse:例如 JSON、protobuf、TextDecoder 28 | + decode:例如 protobuf、TextDecoder 29 | + fs 或网络相关的 constructor:例如 new WebSocket(url) 30 | + require js 模块 31 | 32 | 规律是,如果需要的参数不符合规则时,会导致失败,但方法没有返回 Promise ,也没有 error callback ,则肯定会 throw error 33 | 34 | 例如,`fs.read` 有 error callback,而 `fs.readSync` 则没有返回 Promise 和 error callback,如果出错,则会 throw error 35 | 36 | #### 大量的验证逻辑 37 | 38 | 如果有大量的验证逻辑,如果改成每个验证逻辑都 throw error,或 assert,再统一 catch,则会简化代码 39 | 40 | #### 出错时释放资源 41 | 42 | #### 出错时设置 fall back 值,并继续执行 43 | -------------------------------------------------------------------------------- /contents/cases-of-functional-abstract.md: -------------------------------------------------------------------------------- 1 | # 几个简单的函数式抽象的实现 2 | 3 | 数学表示: 4 | 5 | ``` 6 | sum(f, g) = f + g; 7 | ``` 8 | 9 | js 代码实现: 10 | 11 | ```js 12 | const sum = (f, g) => x => f(x) + g(x); 13 | ``` 14 | 15 | 加上类型: 16 | 17 | ```ts 18 | const sum = (f: (x: number) => number, g: (x: number) => number) => (x: number) => f(x) + g(x); 19 | ``` 20 | 21 | 测试: 22 | 23 | ``` 24 | const f1 = (x: number) => x * x; 25 | const f2 = (x: number) => x * 2 + 1; 26 | const a = sum(f1, f2); // 应该是 x * x + x * 2 + 1 27 | console.log(a(1)); // 应该是 4 28 | console.log(a(2)); // 应该是 9 29 | console.log(a(3)); // 应该是 16 30 | ``` 31 | 32 | 不止是 js,其它大部分常见语言都可以实现这样的效果,例如 rust: 33 | 34 | ```rust 35 | let sum = |f: Box i32>, g: Box i32>| Box::new(move |x| f(x) + g(x)); 36 | let a = sum(Box::new(|x| x * x), Box::new(|x| 2 * x + 1)); // 应该是 x * x + x * 2 + 1 37 | println!("{}", a(1)); // 应该是 4 38 | println!("{}", a(2)); // 应该是 9 39 | println!("{}", a(3)); // 应该是 16 40 | ``` 41 | 42 | 另一个更明显的例子是 “导函数”: 43 | 44 | 数学表示: 45 | 46 | ``` 47 | g(f) = f`; 48 | ``` 49 | 50 | js 代码实现: 51 | 52 | ```js 53 | const g = f => x => (f(x) - f(x - 0.000001)) / 0.000001; 54 | ``` 55 | 56 | 加上类型: 57 | 58 | ```ts 59 | const g = (f: (x: number) => number) => (x: number) => (f(x) - f(x - 0.000001)) / 0.000001; 60 | ``` 61 | 62 | 测试: 63 | 64 | ``` 65 | const a = g(x => x * x); // 应该是 x * 2, 也就是 x * x 的导函数 66 | console.log(a(1)); // 应该是 2,实际是 1.999999000079633 67 | console.log(a(2)); // 应该是 4,实际是 3.999998999582033 68 | console.log(a(3)); // 应该是 6,实际是 5.999999000749767 69 | ``` 70 | 71 | 导函数的定义中存在极限,这里取 0.000001,如果取更接近 0 的值,结果会更接近理论值。 72 | 73 | 下面是 rust 里的例子: 74 | 75 | ```rust 76 | let g = |f: Box f64>| Box::new(move |x: f64| (f(x) - f(x - 0.000001)) / 0.000001); 77 | let a = g(Box::new(|x| x * x)); // 应该是 x * 2, 也就是 x * x 的导函数 78 | println!("{}", a(1f64)); // 应该是 2,实际是 1.999999000079633 79 | println!("{}", a(2f64)); // 应该是 4,实际是 3.999998999582033 80 | println!("{}", a(3f64)); // 应该是 6,实际是 5.999999000749767 81 | ``` 82 | -------------------------------------------------------------------------------- /contents/code-review-in-companies.md: -------------------------------------------------------------------------------- 1 | # 在公司内部采用 code review 流程的总结 2 | 3 | ### 问题 4 | 5 | + 需求紧,很多代码在 deadline 前开发完成,马上要上线,没有时间给 code review 6 | + reviewer 自己忙,没有时间做 code review,或者临时不在 7 | + 结果 code review 过程只是在走形式,不仅没有提高代码质量,反而增加了额外的流程、沟通,降低了大家的工作效率 8 | + 因为效果不好,大家对未来类似的创新行为会更倾向抵制 9 | 10 | ### 解决方案(技术方面) 11 | 12 | + 引入自动化工具,提交 pull request 后,检查代码风格、自动运行测试,能自动化的尽量自动化,减少 reviewer 的工作量 13 | + 要求提交的 pull request 中要包含测试,对于不同的项目类型,可以是单元测试并要求通过测试、集成测试并自动 UI 截图等等,这样可以让 reviewer 可以更容易 review 改动的代码的输入输出、界面变化等等 14 | + 采用非固定 reviewer 的机制,而不是固定 reviewer 的机制,避免 reviewer 人不在时导致的延误,同时让每个开发者都参与 review 过程,甚至把review 指标加入考核指标 15 | + 更高的要求是,提交 pull request 后,会自动创建对应的临时测试环境,reviewer 可以直接在该测试环境测试 API、 UI 交互等,代码 merge 后,测试环境自动释放 16 | 17 | ### 解决方案(人的方面) 18 | 19 | + 能够尊重代码质量,可以大幅减少出问题的可能性,也减少了未来需求变更时所花的时间 20 | + 规划进度时,根据功能复杂度,预留至少半天的 code review 时间 21 | + 开发者要有测试和 lint 意识 22 | + 要求高工作效率,除了完成功能之外,还有时间可以 review 代码、写测试 23 | -------------------------------------------------------------------------------- /contents/communication-between-UI-components-solutions.md: -------------------------------------------------------------------------------- 1 | # UI组件间通信的几个解决方案 2 | 3 | 组件间通信,如果采用父子组件传递 props 的方式,需要通过它们的公共父节点逐层传递 props,非常麻烦,同时也会污染传递链上的组件接口 4 | 5 | 常见的解决方案有: 6 | 7 | ### data store 8 | 9 | 定义一个全局的 store,把各个组件的 state 都移到这个定义的 store state 中,各个组件依赖全局 store 中的 state 来渲染页面,通过触发 store 中定义的 mutation 或 action 来修改 store 中的 state。 10 | 11 | 因为 state 和 state 的修改都集中在 store 中,组件间的通信问题也就消失了 12 | 13 | ```js 14 | const store = new Vuex.Store({ 15 | state: { 16 | blogs: [] 17 | }, 18 | mutations: { 19 | addBlog(state, payload) { 20 | state.blogs.push(payload.blog); 21 | } 22 | } 23 | }); 24 | 25 | // const blogs = this.$store.state.blogs; 26 | // this.$store.commit("addBlog", { blog: "abc" }); 27 | ``` 28 | 29 | ```js 30 | class AppState { 31 | @observable 32 | blogs = []; 33 | 34 | @action 35 | addBlog(payload) { 36 | this.blogs.push(payload.blog); 37 | } 38 | } 39 | const appState = new AppState(); 40 | // const blogs = this.props.appState.blogs; 41 | // this.props.appState.addBlog({ blog: "abc" }); 42 | ``` 43 | 44 | 缺点: 45 | 46 | + 破坏了组件对 state 的封装,组件的 state 在公共的 store 中,其它组件能够随意修改,容易产生 BUG 47 | + 不能应用类型检查,例如 vuex 中 commit 的 mutation 是字符串形式,定义的 mutation 是函数形式,类型不匹配 48 | 49 | ### 自定义事件 50 | 51 | 定义一个全局的 html element 作为事件的载体,消息接收组件 mounted 后监听事件并处理获得的数据,消息发送组件通过 dispatchEvent 方法发送数据 52 | 53 | ```js 54 | const catEventHost = document.createElement("div"); 55 | 56 | catEventHost.addEventListener("cat", e => { 57 | console.log(e.detail); 58 | }); 59 | 60 | catEventHost.dispatchEvent(new CustomEvent("cat", { 61 | detail: "hello world!" 62 | })); 63 | ``` 64 | 65 | 缺点: 66 | 67 | + 是一个 hack 方法,不直观 68 | + 对于不支持 CustomEvent API 的浏览器,需要引入 polyfill 库 69 | 70 | ### rxjs 的 Subject 71 | 72 | 定义一个全局的 Subject 对象,消息接收组件 mounted 后 subscribe 这个 Subject 并处理获得的数据,消息发送组件通过 next 方法发送数据 73 | 74 | ```js 75 | import { Subject } from "rxjs/Subject"; 76 | const catSubject = new Subject(); 77 | 78 | catSubject.subscribe(message => { 79 | console.log(message); 80 | }); 81 | 82 | catSubject.next("hello world!"); 83 | ``` 84 | 85 | ### 考虑组件的可复用性 86 | 87 | 当组件像上面所说的,依赖全局的 data store、全局的事件载体、rxjs 库来实现组件间交互,会降低组件的可复用性,这种情况下把组件状态分散在各个组件中更好一些,对于要长期维护的树状组件库,逐层传递 props 的复杂性可以接受。 88 | 89 | 下面总结一下几种组件间交互的场景: 90 | 91 | #### 父组件决定子组件的状态 92 | 93 | 例如,一般 tooltip 设计为鼠标移开提示框和触发提示的元素时隐藏提示框,提示框是否显示的状态在 tooltip 组件内部,但在数字滑动输入组件中,要求拖动滑块时,即使拖动快到触发了 mouseleave 事件,滑块也要一直提示当前数值。 94 | 95 | ```tsx 96 | function Tooltip(props: { 97 | visible?: boolean 98 | }) { 99 | const [visible, setVisible] = React.useState(false) 100 | // 从父组件传入的 visible 为 boolean 时强制一致提示或不提示,为 undefined 时由组件内部状态来决定 101 | const actualVisible = typeof props.visible === 'boolean' ? props.visible : visible 102 | } 103 | ``` 104 | 105 | #### 父组件改变一次子组件的状态 106 | 107 | 例如,一般可折叠的面板是否折叠的状态在组件内部,但对于“全部折叠”功能,可以把当前的若干个面板全部折叠一下,各个面板仍然可以展开。 108 | 109 | ```tsx 110 | function Panel(props: { 111 | foldedTrigger?: { value: boolean } 112 | }) { 113 | const [folded, setFolded] = React.useState(false) 114 | // 收到 trigger 时更新一下状态 115 | React.useEffect(() => { 116 | if (props.foldedTrigger) { 117 | setFolded(props.foldedTrigger.value) 118 | } 119 | }, [props.foldedTrigger]) 120 | } 121 | 122 | function App() { 123 | const [foldedTrigger, setFoldedTrigger] = React.useState<{ value: boolean }>() 124 | return ( 125 | <> 126 | 127 | 133 | 134 | ) 135 | } 136 | ``` 137 | -------------------------------------------------------------------------------- /contents/data-backup-and-recovery-strategy.md: -------------------------------------------------------------------------------- 1 | # 数据备份和恢复策略 2 | 3 | + 首先应该有这个意识,有自动备份机制,也有恢复脚本 4 | + 有 cron 任务来定时生成新备份,例如每隔 1 小时 5 | + 有 cron 任务来定时清理旧备份,例如每隔 1 个月 6 | + 备份的文件名中应该带有时间信息 7 | + 有 cron 任务来把备份复制到物理机器之外的地方,例如每天 8 | + 有监测程序,可以监视备份机制是否失效,这种程序一般也可以用来检测 API 的状态、每个机器的状态、数据库的状态等 9 | -------------------------------------------------------------------------------- /contents/data-configurations.md: -------------------------------------------------------------------------------- 1 | # 各个级别的数据配置 2 | 3 | ## 如果配置不会变化且不敏感 4 | 5 | 例如第三方服务的公开 URL,可以写死在代码,因为如果更换第三方服务,肯定要更新代码的 6 | 7 | ## 如果配置不会变化且敏感 8 | 9 | 例如密钥,可以配置在环境变量中,开发和生产环境的配置也不应该一样,从而保证安全性 10 | 11 | ## 如果配置在不同的运行环境(production, staging 等)、机器上都不同 12 | 13 | 例如本地的内网 IP,可以配置在环境变量中 14 | 15 | 一般也会在代码中配置默认值,当环境变量中没有配置值时,也可以正常启动程序 16 | 17 | ## 如果配置在不同的应用节点上都不同 18 | 19 | 例如程序监听的端口,作为程序启动时的参数传入 20 | 21 | ## 对于频繁变化的公共配置,且不是非常敏感 22 | 23 | 可以配置在 redis 中,程序启动时读取配置,配置更新时,publish 一条更新配置的消息,程序监听到该消息后,重新读取配置,配置权限由后台程序通过角色来控制。 24 | 25 | 也可以配置在 zookeeper 中,这时配置更新时,zookeeper 会自动通知程序,不需要 publish 过程。 26 | -------------------------------------------------------------------------------- /contents/data-model-design.md: -------------------------------------------------------------------------------- 1 | # 数据模型设计 2 | 3 | 在实际较复杂的业务中,会需求很多互相引用的情况,例如: 4 | 5 | ```ts 6 | interface Foo { 7 | foo: number 8 | children: Bar[] 9 | } 10 | 11 | interface Bar { 12 | bar: number 13 | parent: Foo 14 | } 15 | ``` 16 | 17 | 如果设计了上面的 model,序列化为文件,或序列化到数据库时,会报循环引用错误;而如果去掉 children 字段,或去掉 parent 字段,使用时又不太方便 18 | 19 | 另外,模型的方法放在 model 外,还是 model 内(贫血模型和充血模型),也需要作权衡,例如 20 | 21 | ```ts 22 | // 贫血 23 | interface Foo { 24 | foo: number 25 | children: Bar[] 26 | } 27 | 28 | function findBar(foo: Foo) { 29 | return foo.children.find(...) 30 | } 31 | 32 | // 充血 33 | class Foo { 34 | foo: number 35 | children: Bar[] 36 | 37 | findBar(foo: Foo) { 38 | return this.foo.children.find(...) 39 | } 40 | } 41 | ``` 42 | 43 | ## 易于序列化以方便存储和传输的贫血模型 44 | 45 | + 没有循环引用 46 | + 只使用 number, string, boolean, null, array, object, interface 等容易序列化的数据类型,不使用 Map, class 等 47 | + 可以定义为 readonly 和 ReadonlyArray,这样执行写操作时,强制通过 data access 层操作 48 | + 一定要在 interface 和字段上增加相应的文档,因为不管是用于存储,还是用于传输,应用广泛,方便代码阅读 49 | + 可以使用 types-as-schema 从 model 里的 type 生成 json schema, protobuf 等文件 50 | + 字段尽量是 optional 的形式,可以减少数据存储和传输大小 51 | + 新加的字段需要以 optional 的形式添加,因为是协议型的 model,需要一直确保 model 的兼容 52 | + 如果因为业务需要,model 的含义出现不兼容的改动,可以增加 version 字段,新 model 的 version 是 1,并在将来再次出现不兼容的改动时加一。只读处理 model 时,按照 version 执行相应的逻辑。写处理 model 时(例如用编辑工具打开和保存),对数据进行转换,version 也更新为最新的版本。这样也是为了保证兼容性,类似于新的 Word 打开旧格式的文件。 53 | 54 | ## 易用、reactive 以方便 UI 操作的充血模型 55 | 56 | + 可以有循环引用、复杂类型 57 | + mutable 58 | + reactive(mobx、vuejs) 59 | + 使用 class,可以加一些业务上紧密相关的 method(method 内可以调用贫血模型的 data access 函数) 60 | + 负责和充血模型 model 的转换 61 | + 测试时,测试用例来自贫血模型,转换为充血模型后,执行相应的方法,再转为贫血模型进行测试结果验证 62 | + 可以收集充血模型里的 computed 结果后进行测试结果验证 63 | 64 | ## 针对多数据模型的开发 65 | 66 | 对于某些复杂的逻辑,如果每个 model 都写一遍代码,难以维护 67 | 68 | 可以通过下面的方式合并到一起 69 | 70 | ### 针对 Foo1 | Foo2 | Foo3 来写 71 | 72 | 适用于几个 model 的字段比较类似,或容易区分不同的 model 73 | 74 | 而且当需要增加支持 model 时,代码改动较大 75 | 76 | ### 把负责操作 model 的 Context 也作为输入 77 | 78 | ```ts 79 | interface Context { 80 | getBar: (m: T) => number 81 | getBaz: (m: T) => number 82 | } 83 | ``` 84 | 85 | 不同的 model 都需要实现一个 Context,例如 `Context`,`Context`,`Context`,负责操作各个 model, 86 | 87 | 代码量较多,但容易维护 88 | -------------------------------------------------------------------------------- /contents/data-storage-tools.md: -------------------------------------------------------------------------------- 1 | # 几种数据存储工具 2 | 3 | ### 传统关系数据库 4 | 5 | mysql/mariadb/pg/sql server/oracle 6 | 7 | 稳定,而且都在借鉴 nosql 的特性,例如 json 支持 8 | 9 | ### nosql 10 | 11 | mongodb 12 | 13 | 查询速度更快,支持结构化的数据 14 | 15 | ### 缓存 16 | 17 | redis/memcache 18 | 19 | 前者因为丰富的特性,更优于后者 20 | 21 | ### 权衡时要考虑的点: 22 | 23 | #### 1. 维护难度角度 24 | 25 | 组件越多,约容易出错,所以在满足目标的前提下,使用工具的种类要尽量少 26 | 27 | 而 redis 由于好用的计时、集合、排序、订阅发布、hash、列表、GEO 支持,基本上是必需的一个工具 28 | 29 | #### 2. redis 的缺陷 30 | 31 | redis 容量有限,所以大数据量时,不能全部保存,一般只保存 ID,或者热数据,或者限时的缓存 32 | 33 | 另外 redis 不原生支持复杂的数据结构,虽然可以以字符串的形式保存,但是仍然不方便 34 | 由于以上两点,需要有额外的工具,来保存大量的数据,也支持复杂的数据结构 35 | 36 | #### 3. 具体需求 37 | 38 | 目前这个时间点,传统关系数据库和 mongodb 都可以满足这个两个需求 39 | 40 | 对于传统关系数据库,复杂的数据结构,可以以 json 的形式保存在字段中 41 | 42 | 理论上 mongodb 的查询仍然要略快,还支持 GEO 43 | 44 | 传统关系数据库仍然支持 transaction 45 | 46 | 传统关系数据库和 mongodb 之间权衡,要看具体的需求了。 47 | -------------------------------------------------------------------------------- /contents/debug-mobile-page.md: -------------------------------------------------------------------------------- 1 | # 移动端页面调试的方法总结 2 | 3 | ## 原生的 alert 或把消息文本 write 到某个可见的页面元素中 4 | 5 | 侵入性强;只能查看文本 6 | 7 | ## vConsole 8 | 9 | https://github.com/WechatFE/vConsole 10 | 11 | 通过引入 js 文件的方式,在页面上注入调试相关的 UI 来显示劫持到的 console 消息和网络消息 12 | 13 | 有 js 侵入性和页面侵入性;可以查看文本、object、网络消息 14 | 15 | ## jsconsole 16 | 17 | https://github.com/remy/jsconsole 18 | 19 | 通过引入 js 文件的方式,在页面上劫持 console 日志,并通过服务端,转发给具有相同 id 的客户端 20 | 21 | 有 js 侵入性;可以查看文本 22 | 23 | ## chrome 远程调试 24 | 25 | 移动端 chrome 的调试信息通过 USB 传给桌面端的 chrome 26 | 27 | 无侵入性;可以查看非常丰富的调试信息;要求有桌面端浏览器、移动端浏览器、Webview 和 USB 连接;需要翻墙 28 | 29 | 注意要把 `appspot.com` 加入要翻墙的 URL 规则 30 | 31 | 在桌面端打开 `chrome://inspect/#devices` 以查看调试信息 32 | -------------------------------------------------------------------------------- /contents/design-to-prevent-conflicts-from-multiple-user-operation.md: -------------------------------------------------------------------------------- 1 | # 一种防多用户操作引起冲突的设计 2 | 3 | 场景是,一种操作,可以被同一角色的多个用户操作,如果同时在操作,可能有的改动会被覆盖掉。 4 | 5 | 所以需要一个设计来避免这种情况。 6 | 7 | 首先,不只是验证权限,还需要验证 key,由服务端随机产生,每次改动后都会变化。 8 | 9 | 用户会先获得这个当前有效的 key。 10 | 11 | 如果需要保存修改,携带获得的 key,如果 key 仍然有效,说明还没有修改过,保存成功;否则保存失败。 12 | 13 | 失败后,操作者可以选择放弃本地修改、覆盖远程修改、对比变化等方式解决冲突。 14 | 15 | 当修改成功后,key 立即变更,并把 key 和最新数据通过 ws 通知给所有正在操作的用户,客户端提示某某已经做了修改。 16 | -------------------------------------------------------------------------------- /contents/develop-photoshop-plugin-with-adobe-cep.md: -------------------------------------------------------------------------------- 1 | # 使用 Adobe CEP 开发 Photoshop 插件 2 | 3 | CEP 官方文档: 4 | 5 | CEP 官方资源: 6 | 7 | ExtendScript 文档: 8 | 9 | ## 总体介绍 10 | 11 | Adobe CEP 是用来开发 Adobe 产品(包括 Photoshop)扩展的技术。 12 | 13 | 内部使用了 Chromium Embedded Framework 和 ExtendScript,前者可以让插件可以有 GUI(chromium),可以访问本地文件,可以发出网络请求(nodejs),后者可以操作 Adobe 产品。 14 | 15 | 插件包括一些几个部分: 16 | 17 | 1. manifest.xml:用于定义插件名称、版本、client 入口 html 文件路径、ExtendScript 脚本路径等信息 18 | 2. client 入口 html 文件:插件打开时显示的就是这个文件的 UI,开发方式和普通前端开发一样 19 | 3. ExtendScript 脚本 20 | 21 | ExtendScript 是类似于 javascript 的语言,一般是 jsx 文件,里面定义了一些函数,在 client 端可以通过 `CSInterface.evalScript` 执行相应的脚本,并在 callback 中获得返回值。 22 | 23 | ## nodejs 支持 24 | 25 | 因为安全方面的原因,需要在 manifest.xml 里设置 `--enable-nodejs` 启用 nodejs 支持: 26 | 27 | ```xml 28 | 29 | ./client/index.html 30 | ./host/index.jsx 31 | 32 | --enable-nodejs 33 | 34 | 35 | ``` 36 | 37 | client 代码里可以直接写 nodejs 代码,例如 `require('fs')`,但如果 client 代码使用 webpack 等工具打包,打包结果里的 `require`、`__dirname`等会被移除掉。 38 | 39 | 所以,需要把 nodejs 代码从 client 的代码里独立出来,在 html 文件中通过 script 标签先于 client 代码加载;在 client 中以函数调用的形式调用 nodejs 代码中定义的函数。 40 | 41 | nodejs 代码中 require 不支持相对路径,可以通过 `path.dirname(decodeURI(window.location.pathname))` 来转换为绝对路径后再 require。 42 | 43 | ## typescript 支持 44 | 45 | nodejs 和普通前端都可以使用 typescript;对于 CSInterface,可以使用 作为类型。 46 | 47 | 对于 ExtendScript,可以使用 tsx 作为扩展名,对于 Photoshop 的接口,可以使用 作为类型。 48 | 49 | ## ExtendScript 50 | 51 | ExtendScript 类似于 ECMAScript 3。 52 | 53 | ExtendScript 脚本和 client 代码不在同一个进程中执行,所以传递复杂对象时必须要序列化为字符串,而 ExtendScript 里没有内置的 JSON.stringify,可以通过 json2 等 polyfill JSON 对象。 54 | 55 | ExtendScript 可以通过 `#include` 来引入代码文件,但 `#include` 是不被 ECMAScript 和 Typescript 支持的,使用后 Typescript 编译不过,且导致智能提示等失效,所以不能用来加载 json2 文件。 56 | 57 | 还有一种方式是,通过 `$.evalFile(pluginDir + '/host/json2.js')` 来加载 json2 文件,其中 pluginDir 需要作为函数参数传入。 58 | 59 | ExtendScript 脚本执行报错后不会提示具体的错误消息,解决方案是把代码放在 try...catch 中,具体错误提示就在 catch 到的 error 中。 60 | 61 | 对于文件等大对象,可以先以文件的形式保存到系统临时文件夹中,再把文件路径作为函数的参数或返回值传递。 62 | 63 | ## 调试 64 | 65 | 把项目文件夹 link 到插件文件夹,可以避免开发时复制文件。如果提示签名错误,可以执行 `defaults write com.adobe.CSXS.9 PlayerDebugMode 1` 以启用 debug 模式,然后杀死所有 `cfprefsd` 进程,让配置生效。 66 | 67 | client 端的调试见:,对于 chrome 80 提示 document.registerElement is not a function,可以参考 解决 68 | 69 | 对于 ExtendScript,用 PS 打开 jsx 文件执行,代码里可以使用 alert 来打印变量。 70 | 71 | ## 签名和制作安装包 72 | 73 | 使用 `ZXPSignCMD` 工具()对安装包进行签名、打包。 74 | 75 | 因为 `ZXPSignCMD` 只支持 Mac 和 Windows,如果 CI 里的操作系统不是 Mac 和 Windows,也就不能在 CI 中进行自动打包了。 76 | 77 | 因为 `ZXPSignCMD` 打包时会把源代码、node-modules 和 .git 等其它文件也打包进去,可以使用 `clean-release` 来避免把不需要的文件打包进去。 78 | 79 | 因为 nodejs 外部包的 node_modules 可能体积太大,可以使用 `prune-node-modules` 删除不需要的文件,以减少安装包的体积。 80 | 81 | 1. 生成证书:`sudo ~/Downloads/ZXPSignCMD/ZXPSignCmd -selfSignedCert CN SH foo bar baz foo.p12` 82 | 2. 签名、打包:`sudo ~/Downloads/ZXPSignCMD/ZXPSignCmd -sign "[dir]" ${destDir}/foo.zxp ${repositoryDir}/foo.p12 baz` 83 | 84 | 对于 Mac 安装包,可以使用 dmg 安装包,签名、打包后的 zxp 文件本质是 zip 打包的文件夹,通过 unzip 解包成文件夹后,即可通过 Mac 磁盘工具的命令行工具来制作 dmg 安装包:`hdiutil create -fs HFS+ -volname foo -srcfolder ${destDir}/${dmgDirName} ${destDir}/foo.dmg` 85 | 86 | 如果需要发布到 Adobe Exchange,把打包后的 zxp 文件在 Adobe Exchange 的网页上上传即可。 87 | 88 | ## localStorage、配置文件、插件间通信 89 | 90 | client 上可以使用 localStorage 来存储一些非敏感信息。 91 | 92 | 但不同插件之间的 localStorage 并不共用,如果需要在插件之间同步数据,对于都处于使用状态的插件,可以通过插件间通信来实时发送数据,否则可以把数据保存在用户文件夹下,例如 `~/.foo/a.json`,离线状态的插件打开时,从这个文件中读取到最新的数据。 93 | 94 | 插件间通信的数据也需要先序列化为字符串后在传输: 95 | 96 | ```ts 97 | const csInterface = new CSInterface() 98 | 99 | const event = new CSEvent('event name', 'APPLICATION') 100 | event.data = JSON.stringify({ foo: 1 }) 101 | csInterface.dispatchEvent(event) 102 | 103 | csInterface.addEventListener('event name', (value: { data: any }) => { 104 | 105 | }) 106 | ``` 107 | 108 | ## 对旧版本的 PhotoShop 的支持 109 | 110 | 用旧版本 PS 打开插件时,即使有兼容性的报错,Chrome 的 devtool 上也显示不了报错信息,估计是因为旧版本 PS 打包的 Chromium 的调试协议和最新版本的 Chrome 不兼容。 111 | 112 | 解决方式是,用旧版本 PS 对应的 Chromium 来调试。 113 | 114 | 可以在这个查询打包的 Chromium 版本: 115 | 116 | 可以根据这个链接下载对应版本的 Chromium: 117 | 118 | 对于 CSInterface,应该使用插件需要支持的最低 PS 版本对应的 CSInterface 文件。 119 | -------------------------------------------------------------------------------- /contents/develop-workflow-design.md: -------------------------------------------------------------------------------- 1 | # 开发工作流设计 2 | 3 | 不合适的开发工作流,会造成麻烦,推迟开发进度,甚至会 block 所有人的开发任务。 4 | 5 | 例如,如果使用 git flow 工作流,因为流程复杂,会造成大量的沟通成本。 6 | 7 | 再例如,如果系统划分的过于细碎,一个简单的业务修改,也会牵涉到 2 个以上的仓库的改动,单独提交改动时,都会 break 测试,当依赖关系复杂时,会更头疼。 8 | 9 | 对于系统的划分,推荐把业务系统放在一个仓库,包含服务端和各个客户端,提供详细的启动文档;独立可复用的组件,放在各个独立的仓库中,提供详细的使用文档和严格的版本号控制,可以发布到包管理器中,或者以 git 子模块的形式被使用。 10 | 11 | 这样划分的话,不独立可复用的代码也可以被复用,测试时,系统是作为一个整体被测试的,不用处理复杂的依赖关系。 12 | 13 | 工作流推荐简洁有效的 Github flow,只有一个主分支 master branch,和其它多个 feature branches,主分支设为 protected,不能直接 push,必须通过 pull/merge request 合并到主分支。 14 | 15 | 相应的各种环境: 16 | 17 | + 生产环境:面对真实用户,根据发布计划,从 master 分支拉取代码,发布 18 | + 测试环境:合并 pull/merge request 后,会从 master 分支拉取代码,进行集成测试 19 | + 开发环境:对应各个 feature branches,创建 pull/merge request 时,指定目录和端口,自动搭建环境,用于在 merge 前查看效果,merge 或 close 后自动删除对应的环境 20 | + 本地开发环境:开发者本地环境,在开发时可以查看效果 21 | 22 | 所以,开发流程是: 23 | 24 | 1. 在本地创建本地开发环境 25 | 2. 以 master 分支为基准,创建新的 feature 分支,在这个分支下开发 26 | 3. 开发完成后在本地测试 27 | 4. 创建 pull/merge request,开发者开始下一项任务 28 | -------------------------------------------------------------------------------- /contents/developing-document-related-tools.md: -------------------------------------------------------------------------------- 1 | # 开发文档相关工具的取舍 2 | 3 | 从书写的方便性角度,可以使用流行的 markdown,再通过插件支持公式、图表等; 4 | 5 | 从备份的方便性,可以考虑使用流行的 git,每次 commit 都会保存; 6 | 7 | 最好是在线 HTML 页面的形式,方便通过 URL 来传播、链接; 8 | 9 | 所以,可以通过 gitbook 工具,把 markdown 文件,转换成静态文件,并 host 在内网; 10 | 11 | 也可以代码仓库自带的 wiki,例如 github、gitlab; 12 | 13 | 或者使用不需要 build,也不需要 backend 的 doc 程序:。 14 | 15 | 具体使用上,避免过大的文件,可以按类别分成若干文件; 16 | 17 | 有关联的几个部分,应该相互链接,这样可以方便地从一个地方,找到所有相关的文档; 18 | 19 | 对于复杂的流程,可以用 graphviz 绘制流程图或时序图,比纯文字更具表达力; 20 | 21 | 对于重要的程序设计,应该要有文字描述,或者示例。 22 | -------------------------------------------------------------------------------- /contents/distributed-lock-on-redis.md: -------------------------------------------------------------------------------- 1 | # 基于 redis 的分布式锁 2 | 3 | 当多个进程访问相同的资源时,如果不希望产生数据错误,可以使用这种方法。 4 | 5 | 大致上说,当访问这个资源时,会尝试获取锁,如果成功,可以访问这个资源,结束后释放锁;如果失败,则不能继续操作。 6 | 7 | 锁要保证,在任何时刻,最多只会有一个进程拥有访问这个资源的权限,也要防止因为拥有锁的进程崩溃导致锁不能释放的问题。 8 | 9 | 传统的方法是,利用 redis 的 SETNX,该命令的文档(http://redis.io/commands/setnx )中包含了详细的介绍。 10 | 11 | 更推荐的方法是,redlock,redis 文档也有介绍,http://redis.io/topics/distlock ,nodejs 项目中可以使用 https://github.com/mike-marcacci/node-redlock 。 12 | 13 | 不只有是常见的多请求访问同一数据,redlock 锁还可以用来实现一类定时任务,要求任何时刻,都有且只有 1 个进程在运行这种定时任务, 14 | 15 | 例如,对于定时检测数据库、api 可用性的逻辑,只需要在一个进程内被执行,但不能没有任何进程在执行。 16 | 17 | 每个节点可以都定时执行逻辑:如果已经获得锁,不作任何操作,否则尝试获取锁。 18 | 19 | 如果成功获得锁,运行任务,并开启定时器,定时尝试更新锁。 20 | 21 | 如果更新锁成功,把本地的锁换成新锁;如果更新锁失败,说明已经失去了锁,开始停止任务,并清理资源。 22 | 23 | 当拥有锁的进程挂掉,并且在锁过期之后,其它进程中会有一个手快的获取到锁,并使任务继续运行。 24 | 25 | 这样就保证了,有且只有 1 个进程在运行任务,不过理论上会有最多锁的过期时间 + 定时的周期的 offline 时间。 26 | -------------------------------------------------------------------------------- /contents/drag-program-design.md: -------------------------------------------------------------------------------- 1 | # 拖拽交互的程序设计 2 | 3 | 这里说的拖拽交互是指拖拽元素触发的交互,例如: 4 | 5 | + 拖拽元素改变位置 6 | + 拖拽元素边缘改变元素大小 7 | + 拖拽滑块设置一个数值 8 | + 拖拽时钟指针设置时间 9 | 10 | ## 直接思路 11 | 12 | 这类问题的直接思路是,在待拖拽的元素上设置 drag 相关的事件处理程序,例如: 13 | 14 | + mousedown 事件:记录初始位置 15 | + mousemove 事件:根据当前位置和记录的初始位置,改变元素的位置或大小 16 | + mousedown 事件:根据最后的位置和记录的初始位置,返回需要的数据 17 | 18 | 这种思路某些复杂需求下会有缺点,包括: 19 | 20 | + 如果拖拽区域较小,且用户拖拽速度较快,会导致拖拽结束前就离开了拖拽区域,导致没有触发 mousedown 事件 21 | + 拖拽到别的不相关的元素上,触发不相关的元素的意外行为 22 | 23 | ## 透明蒙版 24 | 25 | 透明蒙版可以解决上面所说的问题,具体思路为: 26 | 27 | + 增加覆盖整个页面的透明蒙版,初始位置数据一旦设置,透明蒙版就出现,清空初始位置数据时透明蒙版就会隐藏 28 | + 拖拽元素的 mousedown 事件:记录初始位置 29 | + 透明蒙版的 mousemove 事件:根据当前位置和记录的初始位置,改变元素的位置或大小 30 | + 透明蒙版的 mouseup、mouseleave 事件:根据最后的位置和记录的初始位置,返回需要的数据,清空初始位置数据 31 | 32 | ## 如果拖拽过程有耗时操作 33 | 34 | 例如一个元素从初始尺寸拖拽成一个新的尺寸,元素内容需要复杂的计算才能得到新尺寸下的显示效果,而拖拽时会频繁触发 mousemove 可能导致性能问题。解决思路有: 35 | 36 | + 拖拽时只更新元素框,拖拽结束时才更新元素内容:交互效果不完美,但可以接受。 37 | + 通过 debounce 限制更新元素内容的频率,提高交互效果的同时,避免性能问题。 38 | 39 | ## 如果有回撤功能 40 | 41 | 拖拽时产生的大量数据记录到操作历史中,会导致回撤基本不可用,解决思路为: 42 | 43 | + 拖拽时返回的数据直接存储在组件本地 state 上,元素框根据这个 state 的数据显示 44 | + 拖拽时返回的数据 debounce 后存储在组件本地 state 上,元素内容根据这个 state 的数据做复杂计算后显示 45 | + 拖拽结束后返回的数据存储在编辑器操作历史 state 上,元素内容根据这个 state 的数据做复杂计算后显示,清空本地 state 46 | + 元素内容的计算中,如果本地 state 上有数据,说明在拖拽中,使用这个数据进行计算,否则拖拽已经结束,使用编辑器操作历史 state 上的数据进行计算 47 | 48 | ## 如果元素内容的计算耗时不可控 49 | 50 | 可能出现拖拽完成后的计算结束后,拖拽中的数据的计算结果才返回。 51 | 52 | 解决方案是,拖拽中的数据的计算后,如果已经拖拽结束,丢弃计算结果。 53 | -------------------------------------------------------------------------------- /contents/encapsulation.md: -------------------------------------------------------------------------------- 1 | #### function:对行为的封装 2 | 3 | ```ts 4 | // callee 5 | function increase(count: number) { 6 | return a + 1; 7 | } 8 | 9 | // caller 10 | const count1 = increase(10); 11 | console.log(count1); 12 | 13 | const count2 = increase(20); 14 | console.log(count2); 15 | ``` 16 | 17 | 这种方式,函数内部应该只依赖函数的参数,应该是无状态的。 18 | 19 | 如果有状态,应该用 class 的方式封装状态,避免全局状态的污染。 20 | 21 | #### class:对状态和行为的封装 22 | 23 | ```ts 24 | // callee 25 | class Counter { 26 | constructor(public count = 0) { } 27 | increase() { 28 | this.count++; 29 | } 30 | } 31 | 32 | // caller 33 | const counter1 = new Counter(10); 34 | counter1.increase(); 35 | console.log(counter1.count); 36 | 37 | const counter2 = new Counter(20); 38 | counter2.increase(); 39 | counter2.increase(); 40 | console.log(counter2.count); 41 | ``` 42 | 43 | 这种方式,不同 instances 之间的状态不会相互影响。 44 | 45 | 像 UI 组件那样,如果对状态和行为的封装仍然不够,还需要指定模板,再预编译成模板函数,可以考虑通过加 decorator 的方式,增加额外的信息。 46 | 47 | #### decorator:再封装其它信息 48 | 49 | ```ts 50 | // callee 51 | function Component(component: { template: string }): ClassDecorator { 52 | return (target: any) => { 53 | target.__render = function (instance: any) { 54 | console.log(instance.__props); // `__props` array can be used in compilation 55 | return `${instance.count}`; // should be compiled from `component.template` string 56 | }; 57 | }; 58 | }; 59 | 60 | function Input(): PropertyDecorator { 61 | return (target: any, propertyKey: string | symbol) => { 62 | if (!target.__props) { 63 | target.__props = []; 64 | } 65 | target.__props.push(propertyKey); 66 | } 67 | } 68 | 69 | // caller 70 | @Component({ 71 | template: `{{count}}`, 72 | }) 73 | class Counter1 { 74 | constructor(public count = 0) { } 75 | increase() { 76 | this.count++; 77 | } 78 | } 79 | 80 | const counter1 = new Counter1(10); 81 | counter1.increase(); 82 | console.log((Counter1 as any).__render(counter1)); 83 | 84 | @Component({ 85 | template: ``, 86 | }) 87 | class Counter2 { 88 | constructor(public count = 0) { } 89 | increase() { 90 | this.count++; 91 | } 92 | 93 | @Input() 94 | prop1: number; 95 | @Input() 96 | prop2: string; 97 | } 98 | 99 | const counter2 = new Counter2(10); 100 | counter2.increase(); 101 | counter2.increase(); 102 | console.log((Counter2 as any).__render(counter2)); 103 | ``` 104 | -------------------------------------------------------------------------------- /contents/frontend-backend-seperation.md: -------------------------------------------------------------------------------- 1 | # 前后端分离实践总结 2 | 3 | ### 什么时候需要分离 4 | 5 | 一般来说,工具网站、内部网站等,例如邮箱,没有 SEO 要求,完全可以做前后端分离,可以大幅提高程序可维护性。 6 | 7 | 一般来说,内容型网站,例如博客、新闻、问答、讨论等,有 SEO 的硬性要求,可以考虑对正文、标题、答案等内容在服务端进行渲染,其它内容则完全可以在异步获得数据后,在前端进行渲染。 8 | 9 | 内容型网站在访问频率很高时,可以把页面以静态文件的形式静态化,根据具体场景,定时渲染,更新生成的静态文件。 10 | 11 | ### 主要效果 12 | 13 | 1. 后端只提供 API,独立的代码,独立部署,可能会有多个域名,例如 https://example.com/api/ 和 https://api.example.com/ 14 | 2. 前端自己渲染,提供 html/js/css 等,独立的代码,独立部署,通过 https://example.com/ 访问 15 | 16 | ### 前端 web server 用 nginx 17 | 18 | 这是一个常见的方案,如果前端文件在 `/opt/static`,则 nginx 的配置文件类似: 19 | 20 | ```nginx 21 | server { 22 | listen 80; 23 | listen [::]:80; 24 | server_name example.com www.example.com; 25 | 26 | location ~*\.(js|css|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|map|mp4|ogg|ogv|webm|htc|json|ttf|woff)$ { 27 | root /opt/static; 28 | expires 1M; 29 | access_log off; 30 | add_header Cache-Control public; 31 | } 32 | location ~*\.html$ { 33 | root /opt/static; 34 | index index.html; 35 | access_log off; 36 | } 37 | } 38 | ``` 39 | 40 | 这里 html 和其它文件不同在于:Cache-Control 是不一样的,html 文件一般不会加版本控制,更新时会改变文件的更新时间,所以可以采用默认策略;而其它文件,一般是在文件名上加版本控制,或者创建后就不再变化,这样使用 public 要更好一些。 41 | 42 | ### 配置 https 和 http2.0 43 | 44 | https 正在普及,成本也不高。如果 crt 和 key 文件分别放在 `/opt/1.crt` 和 `/opt/1.key`,配置会变成: 45 | 46 | ```nginx 47 | server { 48 | # changes start 49 | listen 443 ssl http2; 50 | listen [::]:443; 51 | ssl on; 52 | ssl_certificate /opt/1.crt; 53 | ssl_certificate_key /opt/1.key; 54 | # changes end 55 | server_name example.com www.example.com; 56 | 57 | location ~*\.(js|css|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|map|mp4|ogg|ogv|webm|htc|json|ttf|woff)$ { 58 | root /opt/static; 59 | expires 1M; 60 | access_log off; 61 | add_header Cache-Control public; 62 | } 63 | location ~*\.html$ { 64 | root /opt/static; 65 | index index.html; 66 | access_log off; 67 | } 68 | } 69 | ``` 70 | 71 | 其中 nginx 在 1.9.5 之后才支持 http2.0,如果 nginx 版本不够,要去掉 `http2`。 72 | 73 | ### 加上后端 74 | 75 | 后端需要自己监听一个端口,以 node 为例,可以以 pm2/forever 运行,监听 localhost:3000,这时候就需要 nginx 反代这个服务: 76 | 77 | ```nginx 78 | # changes start 79 | upstream backend { 80 | server localhost:3000; 81 | } 82 | # changes end 83 | server { 84 | listen 443 ssl http2; 85 | listen [::]:443; 86 | ssl on; 87 | ssl_certificate /opt/1.crt; 88 | ssl_certificate_key /opt/1.key; 89 | server_name example.com www.example.com; 90 | 91 | location ~*\.(js|css|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|map|mp4|ogg|ogv|webm|htc|json|ttf|woff)$ { 92 | root /opt/static; 93 | expires 1M; 94 | access_log off; 95 | add_header Cache-Control public; 96 | } 97 | location ~*\.html$ { 98 | root /opt/static; 99 | index index.html; 100 | access_log off; 101 | } 102 | # changes start 103 | location / { 104 | proxy_pass http://backend; 105 | } 106 | # changes end 107 | } 108 | ``` 109 | 110 | 这时候目标都已完成。 111 | 112 | ### 如果使用了 socket.io 113 | 114 | nginx 是支持 websocket 的,可以观察到 socket.io 的通信 url 形式是 `/socket.io/*`。这时候的配置: 115 | 116 | ```nginx 117 | upstream backend { 118 | server localhost:3000; 119 | } 120 | # changes start 121 | map $http_upgrade $connection_upgrade { 122 | default upgrade; 123 | '' close; 124 | } 125 | # changes end 126 | server { 127 | listen 443 ssl http2; 128 | listen [::]:443; 129 | ssl on; 130 | ssl_certificate /opt/1.crt; 131 | ssl_certificate_key /opt/1.key; 132 | server_name example.com www.example.com; 133 | 134 | # changes start 135 | location ~*/socket.io/* { 136 | proxy_pass http://backend; 137 | proxy_http_version 1.1; 138 | proxy_set_header Upgrade $http_upgrade; 139 | proxy_set_header Connection "Upgrade"; 140 | } 141 | # changes end 142 | location ~*\.(js|css|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|map|mp4|ogg|ogv|webm|htc|json|ttf|woff)$ { 143 | root /opt/static; 144 | expires 1M; 145 | access_log off; 146 | add_header Cache-Control public; 147 | } 148 | location ~*\.html$ { 149 | root /opt/static; 150 | index index.html; 151 | access_log off; 152 | } 153 | location / { 154 | proxy_pass http://backend; 155 | } 156 | } 157 | ``` 158 | 159 | ### 跨域问题 160 | 161 | 主要有两种解决方案,jsonp 和 cors,jsonp 本质上是 get 一个 js script,优点是浏览器支持度好,缺点是只支持 GET,而 cors 可以支持 GET/POST/PUT/DELETE 等等,但 IE6 不支持 cors。 162 | 163 | 跨域问题主要靠后端来处理,以 node 为例,可以使用库 https://www.npmjs.com/package/cors ,把支持跨域的域名加入配置就好。 164 | 165 | 如果跨域,请求是默认不带 cookie 的,如果需要,可以在前端设置,以 jquery 为例: 166 | 167 | ```javascript 168 | $.ajaxSetup({ 169 | xhrFields: { 170 | withCredentials: true 171 | } 172 | }); 173 | ``` 174 | 175 | ### 前端开发环境的跨域问题 176 | 177 | 主要有三种解决方案,改 host、使用 fiddler script 修改 host 和使用本地 nginx,以后者为例,如果通过本地 3000 端口访问到的是 https://example.com : 178 | 179 | ```nginx 180 | map $http_upgrade $connection_upgrade { 181 | default upgrade; 182 | '' close; 183 | } 184 | server { 185 | listen 3000; 186 | server_name localhost; 187 | 188 | location ~*/socket.io/* { 189 | proxy_pass https://example.com; 190 | proxy_http_version 1.1; 191 | proxy_set_header Upgrade $http_upgrade; 192 | proxy_set_header Connection "Upgrade"; 193 | } 194 | 195 | location / { 196 | add_header Access-Control-Allow-Origin http://localhost:8000; 197 | proxy_pass https://example.com; 198 | } 199 | } 200 | ``` 201 | 202 | 这里可以支持 websocket,其中 8000 是前端页面的监听端口。 203 | 204 | ### 如果需要加入多个 web server 205 | 206 | 强大的 nginx 就可以做到,还可以做负载均衡。 207 | 208 | 配置类似于: 209 | 210 | ```nginx 211 | upstream backend { 212 | server localhost:3000 weight=2; 213 | server localhost:3001 weight=1; 214 | } 215 | ``` 216 | 217 | ### 如果要规避 nginx 单点的风险 218 | 219 | 为 DNS 解析增加多个 A 纪录,根据运营商的不同,解析到不同的 IP 220 | 221 | ### 如果前端是单页应用 222 | 223 | 这种情况,一般是前端切换路由时,URL 变化了,如果没有特殊地处理,页面被刷新后,会出现 404。 224 | 225 | 以 react-router 为例,同时假设前端路由都以 `.html` 结尾,nginx 配置变成: 226 | 227 | ```nginx 228 | upstream backend { 229 | server localhost:3000; 230 | } 231 | map $http_upgrade $connection_upgrade { 232 | default upgrade; 233 | '' close; 234 | } 235 | server { 236 | listen 443 ssl http2; 237 | listen [::]:443; 238 | ssl on; 239 | ssl_certificate /opt/1.crt; 240 | ssl_certificate_key /opt/1.key; 241 | server_name example.com www.example.com; 242 | 243 | location ~*/socket.io/* { 244 | proxy_pass http://backend; 245 | proxy_http_version 1.1; 246 | proxy_set_header Upgrade $http_upgrade; 247 | proxy_set_header Connection "Upgrade"; 248 | } 249 | location ~*\.(js|css|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|map|mp4|ogg|ogv|webm|htc|json|ttf|woff)$ { 250 | root /opt/static; 251 | expires 1M; 252 | access_log off; 253 | add_header Cache-Control public; 254 | } 255 | # changes start 256 | location ~*\.html$ { 257 | root /opt/static; 258 | try_files $uri /index.html; 259 | access_log off; 260 | } 261 | location =/ { 262 | root /opt/static; 263 | index index.html; 264 | access_log off; 265 | } 266 | # changes end 267 | location / { 268 | proxy_pass http://backend; 269 | } 270 | } 271 | ``` 272 | 273 | 这里使用 try_files,对于以 `.html` 结尾的路由,如果找不到文件,会返回 `index.html`,react-router 会根据当前 URL 路由到正确的组件。 274 | 275 | ### 如果后端需要获得客户端真实 IP 276 | 277 | 这种情况,一般是后端需要根据真实 IP,来限制 API 访问频率。 278 | 279 | nginx 配置变成: 280 | 281 | ```nginx 282 | upstream backend { 283 | server localhost:3000; 284 | } 285 | map $http_upgrade $connection_upgrade { 286 | default upgrade; 287 | '' close; 288 | } 289 | server { 290 | listen 443 ssl http2; 291 | listen [::]:443; 292 | ssl on; 293 | ssl_certificate /opt/1.crt; 294 | ssl_certificate_key /opt/1.key; 295 | server_name example.com www.example.com; 296 | 297 | location ~*/socket.io/* { 298 | proxy_pass http://backend; 299 | proxy_http_version 1.1; 300 | proxy_set_header Upgrade $http_upgrade; 301 | proxy_set_header Connection "Upgrade"; 302 | } 303 | location ~*\.(js|css|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|map|mp4|ogg|ogv|webm|htc|json|ttf|woff)$ { 304 | root /opt/static; 305 | expires 1M; 306 | access_log off; 307 | add_header Cache-Control public; 308 | } 309 | location ~*\.html$ { 310 | root /opt/static; 311 | try_files $uri /index.html; 312 | access_log off; 313 | } 314 | location =/ { 315 | root /opt/static; 316 | index index.html; 317 | access_log off; 318 | } 319 | location / { 320 | proxy_pass http://backend; 321 | # changes start 322 | proxy_set_header Host $host; 323 | proxy_set_header X-Real-IP $remote_addr; 324 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 325 | # changes end 326 | } 327 | } 328 | ``` 329 | 330 | 这时,客户端真实 IP 存在于名为 `X-Real-IP` 或 `X-Forwarded-For` 的 header 中 331 | -------------------------------------------------------------------------------- /contents/frontend-build.md: -------------------------------------------------------------------------------- 1 | # 前端构建过程实践总结 2 | 3 | 把所有东西都写在 html 文件中应该是最原始的前端了: 4 | 5 | ```c 6 | index.html 7 | ``` 8 | 9 | ### html/css/js 分离 10 | 11 | 分离后的目录结构如下: 12 | 13 | ```c 14 | index.html 15 | // changes start 16 | scripts 17 | index.js 18 | styles 19 | index.css 20 | // changes end 21 | ``` 22 | 23 | 其中 html 文件里类似于: 24 | 25 | ```html 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ``` 37 | 38 | ### 如果要引入第三方文件 39 | 40 | 可以通过 bower/npm 安装到本地,或直接使用 CDN 地址。 41 | 42 | 以主流的 npm 方式为例,如果引入 bootstrap,html 文件变成: 43 | 44 | ```html 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ``` 64 | 65 | 在这个阶段,一般需要自定义一个 npm 命令来把 node_modules 中的文件复制出来。 66 | 67 | ### 如果要给 css 和 js 加上版本 68 | 69 | 除了可以借助后端之外,前端可用的方案有 grunt-rev、gulp-rev、webpack 的 long term caching。 70 | 71 | webpack 的方案目前只能给 js 文件加上版本,如果 css 不想被打包进去,还要找其它的方案来处理 css。 72 | 73 | 这里以 rev-static 为例,在 js/css 文件名上加版本,并修改 html 文件中对应的文件名。 74 | 75 | 下面是 `rev-static.config.js` 文件: 76 | 77 | ```js 78 | module.exports = { 79 | inputFiles: [ 80 | "styles/index.css", 81 | "scripts/index.js", 82 | "index.ejs.html" 83 | ], 84 | outputFiles: [ 85 | "index.html" 86 | ], 87 | json: false, 88 | ejsOptions: { 89 | rmWhitespace: false 90 | }, 91 | sha: 256, 92 | customNewFileName: (filePath, fileString, md5String, baseName, extensionName) => baseName + "-" + md5String + extensionName, 93 | } 94 | ``` 95 | 96 | 这里需要把 `index.html` 文件的文件名修改为 `index.ejs.html`: 97 | 98 | ```c 99 | // changes start 100 | index.ejs.html 101 | // changes end 102 | scripts 103 | index.js 104 | styles 105 | index.css 106 | ``` 107 | 108 | 内容修改为: 109 | 110 | ```html 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | ``` 130 | 131 | 执行 `rev-static` 后的目录结构如下: 132 | 133 | ```c 134 | // changes start 135 | index.html 136 | // changes end 137 | scripts 138 | index.js 139 | // changes start 140 | index-caa02e8ba0c5af68e9ac7728da2bed75.js 141 | // changes end 142 | styles 143 | index.css 144 | // changes start 145 | index-f695dca31d31e7c85e3442e5ca88da6d.css 146 | // changes end 147 | ``` 148 | 149 | 其中 `index.html` 的内容为: 150 | 151 | ```html 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | ``` 171 | 172 | 这样版本化就完成了。 173 | 174 | ### 如果 js 变得复杂,需要模块化 175 | 176 | 常见的前端 js 模块化方式有 commonjs 和 AMD,相应的工具是 webpack/browserify 和 require.js/webpack。 177 | 178 | 采用哪种方式往往会受到采用的前端框架影响。例如对于 react 和相关的工具链,官方推荐 commonjs,vuejs 也是官方推荐 commonjs,而对于 angular2,官方推荐 system.js,当然也可以不用推荐的方式。 179 | 180 | 下面以用 webpack 打包 commonjs 模块为例,部分目录结构如下: 181 | 182 | ```c 183 | scripts 184 | index.js 185 | // changes start 186 | a.js 187 | b.js 188 | // changes end 189 | ``` 190 | 191 | 下面是 webpack 的配置文件,`webpack.config.js` 文件: 192 | 193 | ```javascript 194 | const webpack = require("webpack"); 195 | const path = require("path"); 196 | 197 | module.exports = { 198 | entry: { 199 | index: "scripts/index" 200 | }, 201 | output: { 202 | path: path.join(__dirname, "scripts"), 203 | filename: "[name].bundle.js" 204 | } 205 | }; 206 | ``` 207 | 208 | 运行 `webpack` 后,生成 `scripts/index.bundle.js`,目录结构如下: 209 | 210 | ```c 211 | scripts 212 | index.js 213 | a.js 214 | b.js 215 | // changes start 216 | index.bundle.js 217 | // changes end 218 | ``` 219 | 220 | 这时 `index.bundle.js` 取代 `index.js`,成为 js 的入口文件,`index.ejs.html`、`rev-static.config.js` 中都有做相应的更新。 221 | 222 | ### 使用 lint 控制代码格式 223 | 224 | 以 ESlint 和 css lint 为例,直接执行相应的 CLI 命令即可。 225 | 226 | npm 脚本变成: 227 | 228 | ```js 229 | { 230 | "build": "npm run lint && webpack && rev-static", 231 | "lint": "eslint scripts/*.js && csslint styles/*.css" 232 | } 233 | ``` 234 | 235 | 执行 `npm run build` 即可把这几个构建过程串联起来。 236 | 237 | ### css、js、html 的压缩 238 | 239 | js 可以用 webpack 的插件来压缩,css 可以用 cleancss 来压缩,html 可以用 rev-static 来压缩: 240 | 241 | 下面是 `rev-static.config.js` 文件: 242 | 243 | ```js 244 | module.exports = { 245 | inputFiles: [ 246 | "styles/index.bundle.css", 247 | "scripts/index.bundle.js", 248 | "index.ejs.html" 249 | ], 250 | outputFiles: [ 251 | "index.html" 252 | ], 253 | json: false, 254 | ejsOptions: { 255 | // changes start 256 | "rmWhitespace": true 257 | // changes end 258 | }, 259 | sha: 256, 260 | customNewFileName: (filePath, fileString, md5String, baseName, extensionName) => baseName + "-" + md5String + extensionName, 261 | } 262 | ``` 263 | 264 | 下面是 webpack 的配置文件,`webpack.config.js` 文件: 265 | 266 | ```javascript 267 | const webpack = require("webpack"); 268 | const path = require("path"); 269 | 270 | module.exports = { 271 | entry: { 272 | index: "scripts/index" 273 | }, 274 | output: { 275 | path: path.join(__dirname, "scripts"), 276 | filename: "[name].bundle.js" 277 | }, 278 | // changes start 279 | plugins: [ 280 | new webpack.optimize.UglifyJsPlugin({ 281 | compress: { 282 | warnings: false, 283 | }, 284 | output: { 285 | comments: false, 286 | }, 287 | }) 288 | ] 289 | // changes end 290 | }; 291 | ``` 292 | 293 | npm 脚本变成: 294 | 295 | ```js 296 | { 297 | // changes start 298 | "build": "npm run lint && webpack && npm run cleancss && rev-static", 299 | // changes end 300 | "lint": "eslint scripts/*.js && csslint styles/*.css", 301 | // changes start 302 | "cleancss": "cleancss styles/index.css -o styles/index.bundle.css" 303 | // changes end 304 | } 305 | ``` 306 | 307 | 这时 `index.bundle.css` 取代 `index.css`,成为 css 的入口文件,`index.ejs.html`、`rev-static.config.js` 中都要做相应的更新。 308 | 309 | ### 如果使用 LESS 或 SCSS,并编译成 css 310 | 311 | 以 SCSS 为例,可以执行 shell 命令:`sass styles/index.scss > build/index.css`,scss-lint 也可以以命令的形式执行 `scss-lint styles/*.scss`: 312 | 313 | npm 脚本变成: 314 | 315 | ```js 316 | { 317 | "build": "npm run lint && webpack && npm run cleancss && rev-static", 318 | // changes start 319 | "lint": "eslint scripts/*.js && scss-lint styles/*.css", 320 | "cleancss": "sass styles/index.scss > styles/index.css && cleancss styles/index.css -o styles/index.bundle.css" 321 | // changes end 322 | } 323 | ``` 324 | 325 | ### 如果使用 css 后处理器工具 326 | 327 | 如果支持最新的 2 个浏览器版本: 328 | 329 | 配置文件 `postcss.json` 是: 330 | 331 | ``` 332 | { 333 | "autoprefixer": { 334 | "browsers": ["last 2 versions"] 335 | } 336 | } 337 | ``` 338 | 339 | npm 脚本变成: 340 | 341 | ```js 342 | { 343 | "build": "npm run lint && webpack && npm run cleancss && rev-static", 344 | "lint": "eslint scripts/*.js && scss-lint styles/*.css", 345 | // changes start 346 | "cleancss": "sass styles/index.scss > styles/index.css && postcss --use autoprefixer -c postcss.json -o styles/index.css styles/index.css && cleancss styles/index.css -o styles/index.bundle.css" 347 | // changes end 348 | } 349 | ``` 350 | 351 | ### 如果写 ES6 代码,并编译成 ES5 352 | 353 | 这里以 babel 为例: 354 | 355 | 配置文件是: 356 | 357 | ``` 358 | { 359 | presets: ["es2015"] 360 | } 361 | ``` 362 | 363 | npm 脚本变成: 364 | 365 | ```js 366 | { 367 | // changes start 368 | "build": "babel && npm run lint && webpack && npm run cleancss && rev-static", 369 | // changes end 370 | "lint": "eslint scripts/*.js && scss-lint styles/*.css", 371 | "cleancss": "sass styles/index.scss > styles/index.css && postcss --use autoprefixer -c postcss.json -o styles/index.css styles/index.css && cleancss styles/index.css -o styles/index.bundle.css" 372 | } 373 | ``` 374 | 375 | ### 如果使用 typescript 或 coffeescript,并编译成 ES5 376 | 377 | 这里以 typescript 为例: 378 | 379 | 配置文件为: 380 | 381 | ```json 382 | { 383 | "compilerOptions": { 384 | "module": "commonjs", 385 | "target": "es5" 386 | } 387 | } 388 | ``` 389 | 390 | npm 脚本变成: 391 | 392 | ```js 393 | { 394 | // changes start 395 | "build": "tsc && npm run lint && webpack && npm run cleancss && rev-static", 396 | // changes end 397 | "lint": "eslint scripts/*.js && scss-lint styles/*.css", 398 | "cleancss": "sass styles/index.scss > styles/index.css && postcss --use autoprefixer -c postcss.json -o styles/index.css styles/index.css && cleancss styles/index.css -o styles/index.bundle.css" 399 | } 400 | ``` 401 | 402 | ### 第三方大文件独立打包 403 | 404 | 以 react 和 react-router 为例,它们都要通过 npm 安装,默认会被打包进去。 405 | 406 | 需要把它们打包到 vendor 中,这样当程序变化时,vendor 的变化频率不会很大。 407 | 408 | ```c 409 | scripts 410 | index.js 411 | a.js 412 | b.js 413 | index.bundle.js 414 | // changes start 415 | vendor.js 416 | // changes end 417 | ``` 418 | 419 | 其中 `vendor.js`: 420 | 421 | ``` 422 | import "react"; 423 | import "react-router"; 424 | ``` 425 | 426 | 下面是 webpack 的配置文件,`webpack.config.js` 文件: 427 | 428 | ```javascript 429 | const webpack = require("webpack"); 430 | const path = require("path"); 431 | 432 | module.exports = { 433 | entry: { 434 | index: "scripts/index", 435 | // changes start 436 | vendor: "scripts/vendor" 437 | // changes end 438 | }, 439 | output: { 440 | path: path.join(__dirname, "scripts"), 441 | filename: "[name].bundle.js" 442 | }, 443 | plugins: [ 444 | // changes start 445 | new webpack.optimize.DedupePlugin(), 446 | // changes end 447 | new webpack.optimize.UglifyJsPlugin({ 448 | compress: { 449 | warnings: false, 450 | }, 451 | output: { 452 | comments: false, 453 | }, 454 | }), 455 | // changes start 456 | new webpack.optimize.CommonsChunkPlugin({ 457 | name: ["index", "vendor"] 458 | }) 459 | // changes end 460 | ] 461 | }; 462 | ``` 463 | 464 | 对于 CSS,cleancss 可以用于合并第三方包: 465 | 466 | npm 脚本变成: 467 | 468 | ```js 469 | { 470 | "build": "babel && npm run lint && webpack && npm run cleancss && rev-static", 471 | "lint": "eslint scripts/*.js && scss-lint styles/*.css", 472 | // changes start 473 | "cleancss": "sass styles/index.scss > styles/index.css && postcss --use autoprefixer -c postcss.json -o styles/index.css styles/index.css && cleancss styles/index.css -o styles/index.bundle.css && cleancss node_modules/bootstrap/dist/css/bootstrap.min.css node_modules/bootstrap/dist/css/bootstrap-theme.min.css > styles/vendor.css" 474 | // changes end 475 | } 476 | ``` 477 | 478 | 另外对于 bootstrap,还需要复制一下字体文件,到发布目录,并保持 css 和字体文件的相对位置一致。 479 | 480 | ### 从 js 文件中抽出模板 481 | 482 | js 文件中经常会有模版(例如在 vuejs 和 angular 中),模板直接写在 js 中,一般没有语法高亮,容易出错。为了避免这个问题,可以把模板字符串抽取到独立的 html 模板文件中,再利用代码打包工具(例如 webpack 的 raw-loader)在打包时加载模板。 483 | 484 | ```ts 485 | @Component({ 486 | template: require("raw-loader!./foo.html"), 487 | }) 488 | class Bar extends Vue { } 489 | ``` 490 | 491 | 模板文件分离后,可以利用 `html-minifier` 等消除模板中无效的换行和空格,以减少模板的大小: 492 | 493 | ```bash 494 | html-minifier --collapse-whitespace --case-sensitive --collapse-inline-tag-whitespace foo.template.html -o foo.html 495 | ``` 496 | 497 | 在制作组件库时,不希望依赖 `raw-loader` 这样的模板加载工具(因为使用方不一定使用 webpack 来打包),可以利用 `file2variable-cli` 来把模板文件转换为 ES6 文件: 498 | 499 | ```bash 500 | file2variable-cli foo.html -o foo-variables.js 501 | ``` 502 | 503 | 执行后,生成类似如下的 js 文件: 504 | 505 | ```js 506 | export const fooHtml = `{{foo}}`; 507 | ``` 508 | 509 | 模板的使用过程也就变为: 510 | 511 | ```ts 512 | import { fooHtml } from "./foo-variables"; 513 | 514 | @Component({ 515 | template: fooHtml, 516 | }) 517 | class Bar extends Vue { } 518 | ``` 519 | 520 | ### 打包时,把小图标转为 base64 格式,并内联到 css 文件中 521 | 522 | 这样做的话,可以在加载时减少 http 请求数。而具体的方法,可以使用 `image2base64-cli` 来实现: 523 | 524 | ```bash 525 | image2base64-cli images/*.png --less variables.less 526 | ``` 527 | 528 | 生成的 less 文件类似于: 529 | 530 | ```less 531 | @foo-png: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUAAAAAAAAAAAAAAAAAAAADAwMEBAQAAADHx8e1tbUAAAD////p6enz8/P39/f7+/vu7u5nZ34DAAAAC3RSTlMAMTwMGE8rBqWkYZ+r9QEAAACPSURBVDjL7ZJbDoMgEADdF2oXhfuftksogcA2PUCdL5cZQwhsf8bOATqB9zngQ0cOnoOg0n5HRNEwB6Dw+SJURAUvaF5V3KD5rwExVe9vUVaRqyfwAjH1qn7zg5RuI5l3g4R4FcyXaQ1uIMw5I9VpDS6wI0TzdVqDbEt20D5NhCjQkbjcBZ9x5OTf7+Fh5A1DkAXNMEdnOQAAAABJRU5ErkJggg=='; 532 | @bar-png: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAIVBMVEUAAAAAAAAAAAAAAAAAAAABAQH////7+/v39/fy8vLr6+sN7QgRAAAABnRSTlMADD0xGCVFoQlVAAAAQklEQVQoz2MYakBEAE0gTBFNwCxJAE2gTBFNIA2qBCGQpogukIIiYF5ejqrCoqMJ1QzLmWi2WC1Cc0eUIqZfBjsAAO4ACr4wbS2OAAAAAElFTkSuQmCC'; 533 | ``` 534 | 535 | 在使用时,就可以使用这些变量了: 536 | 537 | ```less 538 | @import "./variables.less"; 539 | 540 | .foo { 541 | background-image: url("@{foo-png}"); 542 | } 543 | .bar { 544 | background-image: url("@{bar-png}"); 545 | } 546 | ``` 547 | 548 | ### 动态导入(dynamic import) 549 | 550 | 打包时,静态导入(`import * as foo from "foo";`)和 commonjs(`const foo = require("foo");`)的模块会被直接打包。 551 | 552 | 某些情况,静态导入不能满足需求,需要动态倒入(`import("foo").then(foo=>{//todo})`)。 553 | 554 | 例如:多语言、UI 皮肤和主题、插件、非首屏页面才需要的大体积模块。 555 | 556 | ```ts 557 | import * as foo from "foo"; 558 | // do something with module `foo` 559 | 560 | setTimeout(()=>{ 561 | // some time later, dynamic import module "bar" 562 | import("bar").then(bar=>{ 563 | // do something with module `bar` 564 | }); 565 | },1000); 566 | ``` 567 | 568 | 目前 webpack 支持 “动态导入” 代码的打包,它会生成多份包文件,运行 `webpack` 后,生成 `scripts/index.bundle.js`,目录结构如下: 569 | 570 | ```c 571 | scripts 572 | index.js 573 | a.js 574 | b.js 575 | // changes start 576 | 0.index.bundle.js 577 | // changes end 578 | index.bundle.js 579 | ``` 580 | 581 | 其中,以数字开始的包,会在动态导入时被动态加载。 582 | 583 | 对于 typescript,需要把 `module` 设为 `ESNext`,以在生成 js 文件时,保留 `import` 语法。 584 | 585 | ### prerender 586 | 587 | 解决 SEO 问题和加快首屏速度的一个方式是利用 prerender 588 | 589 | 首先可以观察到: 590 | 591 | + vuejs 的 el 元素在 js 执行前是可见的,js 执行后,整个 el 元素都会被生成的 html 替换掉 592 | + reactjs 和 angular 的容器元素在 js 执行前是可见的,js 执行后,生成的 html 代码会作为 innerHTML 替换掉原来的 innerHTML 593 | 594 | 利用这个特性,可以 prerender 的 html 片段插入到 el 元素或容器元素中,使得在 js 执行前就存在首屏的 html 代码 595 | 596 | 对于具体的 build 脚本: 597 | 598 | https://github.com/plantain-00/prerender-js 可以 prerender 页面,根据元素 ID 获取生成的 html 并保存到本地文件中 599 | 600 | `http-server -p 8000` 601 | 602 | ```ts 603 | import * as puppeteer from "puppeteer"; 604 | import * as fs from "fs"; 605 | 606 | const browser = await puppeteer.launch(); 607 | const page = await browser.newPage(); 608 | await page.emulate({ viewport: { width: 1440, height: 900 }, userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" }); 609 | await page.waitFor(1000); 610 | await page.goto("http://localhost:8000"); 611 | await page.waitFor(2000); 612 | const content = await page.evaluate(() => { 613 | const element = document.querySelector("#prerender-container"); 614 | return element ? element.innerHTML.trim() : ""; 615 | }); 616 | fs.writeFileSync("prerender/index.html", content); 617 | 618 | browser.close(); 619 | ``` 620 | 621 | 在 `rev-static.config.js` 中读取保存的 html 代码片段,并配置到 `context` 中 622 | 623 | ```ts 624 | const fs = require('fs') 625 | ... 626 | context: { 627 | prerender: fs.readFileSync('prerender/index.html') 628 | } 629 | ... 630 | ``` 631 | 632 | 在 `index.ejs.html` 模版内,嵌入该代码片段 633 | 634 | ```html 635 |
636 |
<%-context.prerender %>
637 |
638 | ``` 639 | 640 | ### precache 641 | 642 | 使用 precache 可以离线运行程序,例如计算器。一般通过 service worker 来实现 643 | 644 | ```js 645 | // sw-precache.config.js 646 | module.exports = { 647 | staticFileGlobs: [ 648 | 'index.html', 649 | 'index.bundle-*.js', 650 | 'vendor.bundle-*.js', 651 | 'index.bundle-*.css', 652 | 'vendor.bundle-*.css' 653 | ] 654 | } 655 | ``` 656 | 657 | `sw-precache --config sw-precache.config.js` 658 | 659 | `uglifyjs service-worker.js -o service-worker.bundle.js` 660 | 661 | ```js 662 | if (navigator.serviceWorker) { 663 | navigator.serviceWorker.register("service-worker.bundle.js").catch(error => { 664 | console.log("registration failed with error: " + error); 665 | }); 666 | } 667 | ``` 668 | -------------------------------------------------------------------------------- /contents/generator-function-abstract.md: -------------------------------------------------------------------------------- 1 | # Generator 函数的抽象 2 | 3 | 例如需要遍历一个带复杂业务逻辑的树结构,遍历后的操作由用户决定 4 | 5 | 首先想到的是 callback 6 | 7 | ```ts 8 | interface TreeNode { 9 | children: TreeNode[] 10 | // other fields 11 | } 12 | 13 | function foo(node: TreeNode, callback: (data: any) => void) { 14 | for (const child of node.children) { 15 | callback(1) // get some data 16 | foo(child, callback) 17 | } 18 | } 19 | ``` 20 | 21 | 如果 callback 里有异步操作,需要等异步操作执行成功后,再继续遍历,可以改成 async 函数 22 | 23 | ```ts 24 | async function foo(node: TreeNode, callback: (data: any) => Promise) { 25 | for (const child of node.children) { 26 | await callback(1) // get some data 27 | foo(child, callback) 28 | } 29 | } 30 | ``` 31 | 32 | 如果也需要能在同步执行环境中被使用,例如 @computed 装饰的函数内,可以使用 generator 函数 33 | 34 | ```ts 35 | declare const root: TreeNode 36 | declare const request: (data: any) => Promise 37 | 38 | const tasks = foo(root) 39 | for(const task of tasks) { 40 | console.info(task) // 同步执行 41 | } 42 | 43 | for(const task of tasks) { 44 | await request(task) // 逐步异步执行 45 | } 46 | 47 | await Promise.all(Array.from(tasks).map((task) => request(task))) // 同时异步执行 48 | ``` 49 | -------------------------------------------------------------------------------- /contents/history-of-generating-html-page.md: -------------------------------------------------------------------------------- 1 | # 生成 html 页面的方式演化 2 | 3 | ### 在后端以字符串的形式拼接 4 | 5 | ```js 6 | var a = "" + b + ""; 7 | ``` 8 | 9 | 缺点是,太难维护 10 | 11 | ### Webform 的控件 12 | 13 | 缺点是,多了一层抽象,还不能精细控制 html 14 | 15 | ### 后端模板 16 | 17 | 常用的后端语言都有这种方式,主要是为了解决字符串拼接不易维护的问题 18 | 19 | 缺点是,只能产生首屏的 html,作用有限,很多效果还需要前端 js 来处理 20 | 21 | ### 在前端以字符串的形式拼接,操作 DOM 22 | 23 | 早期为了抹平浏览器的差异,可以使用 jquery 操作 DOM; 24 | 25 | 后来浏览器之间的差异越来越小,或者在移动端,一般使用原生 js 来操作 DOM 26 | 27 | 缺点是,字符串拼接太难维护,同时逻辑复杂时,代码量太大 28 | 29 | ### 前端模板 30 | 31 | 为了解决字符串拼接不易维护的问题,可以使用前端模板,例如 handlebars。 32 | 33 | handlebars 模板可以在构建时,被预编译成模板函数,以提高性能。 34 | 35 | ### MVVM 框架 36 | 37 | 为了解决操作 DOM 的代码量太大,不易维护的问题,可以使用 MVVM 框架,常见的有 angularjs,vuejs。 38 | 39 | ### 虚拟 DOM 40 | 41 | 由 react 引入。 42 | 43 | 通过对虚拟 DOM 的 diff,来优化 DOM 操作,从而提高性能。 44 | 45 | ### 未来:web component? -------------------------------------------------------------------------------- /contents/idea-of-web-component-as-props-of-web-component.md: -------------------------------------------------------------------------------- 1 | # 向 web 组件的 props 传入 web 组件的思想 2 | 3 | 以函数为类比,函数的参数也可以是函数,典型的例子就是数组的 map/filter 方法。 4 | 5 | #### vuejs 的例子 6 | 7 | 以 vuejs 的组件为例,先设计一个组件,props 有 child(string, 子组件名))、elements(string[], 列表数据),要求子组件可以接收一个名为 name 的 props: 8 | 9 | ```ts 10 | Vue.component("my-list", { 11 | template: ` 12 |
13 | 16 | 17 |
18 | `, 19 | props: ["child", "elements"], 20 | }); 21 | ``` 22 | 23 | 调用这个组件前,需要先定义子组件,类似于函数的实参,这里分别定义两个子组件(实参): 24 | 25 | ```ts 26 | Vue.component("my-element-1", { 27 |    template: "", 28 | props: ["name"] 29 | }); 30 | Vue.component("my-element-2", { 31 | template: "{{name}}", 32 | props: ["name"] 33 | }); 34 | ``` 35 | 36 | 然后就像函数的传参那样分别调用一下: 37 | 38 | ```html 39 |
40 | 41 | 42 |
43 | ``` 44 | 45 | ```ts 46 | new Vue({ 47 | el: '#app', 48 | data: { 49 | elements: ["test 1", "test 2"] 50 | } 51 | }); 52 | ``` 53 | 54 | 最后生成的 html 代码是: 55 | 56 | ```html 57 |
58 |
59 | 60 | 61 |
62 |
63 | test 1 64 | test 2 65 |
66 |
67 | ``` 68 | 69 | #### reactjs 的例子 70 | 71 | 再以 reactjs 的组件为例,先设计一个组件,props 有 child(string, 子组件类))、elements(string[], 列表数据),要求子组件可以接收一个名为 name 的 props: 72 | 73 | ```tsx 74 | class MyList extends React.Component<{ elements: string[]; child: React.ComponentClass<{ name: string }> }, {}>{ 75 | render() { 76 | return ( 77 |
78 | {this.props.elements.map(e => React.createElement(this.props.child, { name: e }))} 79 |
80 | ); 81 | } 82 | } 83 | ``` 84 | 85 | 调用这个组件前,需要先定义子组件,类似于函数的实参,这里分别定义两个子组件(实参): 86 | 87 | ```tsx 88 | class MyElement1 extends React.Component<{ name: string }, {}>{ 89 | render() { 90 | return ( 91 | 92 | ); 93 | } 94 | } 95 | class MyElement2 extends React.Component<{ name: string }, {}>{ 96 | render() { 97 | return ( 98 | {this.props.name} 99 | ); 100 | } 101 | } 102 | ``` 103 | 104 | 或者使用更简洁的形式: 105 | 106 | ```tsx 107 | const MyElement1: React.StatelessComponent<{ name: string }> = props => ; 108 | const MyElement2: React.StatelessComponent<{ name: string }> = props => {props.name}; 109 | ``` 110 | 111 | 然后就像函数的传参那样分别调用一下: 112 | 113 | ```tsx 114 | class Main extends React.Component<{}, {}>{ 115 | elements = ["test 1", "test 2"]; 116 | render() { 117 | return ( 118 |
119 | 120 | 121 |
122 | ); 123 | } 124 | } 125 | ReactDOM.render(
, document.getElementById("container")); 126 | ``` 127 | 128 | 最后生成的 html 代码是: 129 | 130 | ```html 131 |
132 |
133 |
134 | 135 | 136 |
137 |
138 | test 1 139 | test 2 140 |
141 |
142 |
143 | ``` 144 | 145 | 然后是更简单的例子: 146 | 147 | ```tsx 148 | class MyList extends React.Component<{ elements: string[]; child: React.StatelessComponent<{ name: string }> }, {}>{ 149 | render() { 150 | return ( 151 |
152 | {this.props.elements.map(e => React.createElement(this.props.child, { name: e }))} 153 |
154 | ); 155 | } 156 | } 157 | 158 | class Main extends React.Component<{}, {}>{ 159 | elements = ["test 1", "test 2"]; 160 | render() { 161 | return ( 162 |
163 | } > 164 | {props.name}} > 165 |
166 | ); 167 | } 168 | } 169 | ReactDOM.render(
, document.getElementById("container")); 170 | ``` 171 | 172 | #### angular 的例子 173 | 174 | 再以 angular 的组件为例,先设计一个组件,props 有 child(子组件类)、elements(string[], 列表数据),要求子组件可以接收一个名为 name 的 props: 175 | 176 | ```ts 177 | @Component({ 178 | selector: "my-list", 179 | template: ` 180 |
181 | 182 | 183 |
184 | `, 185 | }) 186 | class MyListComponent { 187 | @Input() 188 | child: any; 189 | @Input() 190 | elements: string[]; 191 | 192 | @ViewChild("children", { read: ViewContainerRef }) 193 | children: ViewContainerRef; 194 | 195 | ngOnInit() { 196 | const resolver: ComponentFactoryResolver = this.children.injector.get(ComponentFactoryResolver); 197 | const factory = resolver.resolveComponentFactory<{ name: string }>(this.child); 198 | for (let i = 0; i < this.elements.length; i++) { 199 | const component = this.children.createComponent<{ name: string }>(factory, i); 200 | component.instance.name = this.elements[i]; 201 | } 202 | } 203 | } 204 | ``` 205 | 206 | 调用这个组件前,需要先定义子组件,类似于函数的实参,这里分别定义两个子组件(实参): 207 | 208 | ```ts 209 | @Component({ 210 | selector: "my-element-1", 211 | template: ``, 212 | }) 213 | class MyElement1Component { 214 | @Input() 215 | name: string; 216 | } 217 | 218 | @Component({ 219 | selector: "my-element-2", 220 | template: `{{name}}`, 221 | }) 222 | class MyElement2Component { 223 | @Input() 224 | name: string; 225 | } 226 | ``` 227 | 228 | 然后就像函数的传参那样分别调用一下: 229 | 230 | ```ts 231 | @Component({ 232 | selector: "app", 233 | template: ` 234 | 235 | 236 | `, 237 | }) 238 | export class MainComponent { 239 | elements = ["test 1", "test 2"]; 240 | MyElement1Component = MyElement1Component; 241 | MyElement2Component = MyElement2Component; 242 | } 243 | ``` 244 | 245 | 最后生成的 html 代码是: 246 | 247 | ```html 248 | 249 | 250 |
251 | 252 | 253 | 254 | 255 | 256 | 257 |
258 |
259 | 260 |
261 | 262 | test 1 263 | 264 | 265 | test 2 266 | 267 |
268 |
269 |
270 | ``` 271 | 272 | 注意需要 MainModule 的 entryComponents 中声明子组件: 273 | 274 | ```ts 275 | @NgModule({ 276 | entryComponents: [MyElement1Component, MyElement2Component], 277 | }) 278 | class MainModule { } 279 | ``` 280 | -------------------------------------------------------------------------------- /contents/interface-and-trait.md: -------------------------------------------------------------------------------- 1 | # interface 和 trait 2 | 3 | 一般是抽象于一种能力,内部包含一个或多个函数,然后会有 class 来实现它,从而该 class 就拥有这种能力。 4 | 5 | 在 java/C#/typescript 中被称为 interface,在 rust/scala 中被称为 trait。 6 | 7 | 下面是一个 interface 的例子: 8 | 9 | ```ts 10 | interface HasArea { 11 | area(): number; 12 | is_larger(other: HasArea): boolean; 13 | } 14 | 15 | class Circle implements HasArea { 16 | constructor(public x: number, public y: number, public radius: number) { } 17 | area() { 18 | return Math.PI * (this.radius * this.radius); 19 | } 20 | is_larger(other: Circle) { 21 | return this.area() > other.area(); 22 | } 23 | } 24 | ``` 25 | 26 | 下面是 rust 中的 trait 的例子: 27 | 28 | ```rust 29 | trait HasArea { 30 | fn area(&self) -> f64; 31 | fn is_larger(&self, &Self) -> bool; 32 | } 33 | 34 | struct Circle { 35 | x: f64, 36 | y: f64, 37 | radius: f64, 38 | } 39 | 40 | impl HasArea for Circle { 41 | fn area(&self) -> f64 { 42 | std::f64::consts::PI * (self.radius * self.radius) 43 | } 44 | fn is_larger(&self, other: &Self) -> bool { 45 | self.area() > other.area() 46 | } 47 | } 48 | ``` 49 | 50 | interface 可以作为函数的参数的类型,例如: 51 | 52 | ```ts 53 | function print_area(shape: HasArea) { 54 | console.log(`This shape has an area of ${shape.area()}`); 55 | } 56 | const circle1 = new Circle(1, 1, 10); 57 | print_area(circle1); 58 | ``` 59 | 60 | 而 rust 中的 trait 目前则不能这样做,不过可以通过增加泛型来达到目的,例如: 61 | 62 | ```rust 63 | fn print_area(shape: T) { 64 | println!("This shape has an area of {}", shape.area()); 65 | } 66 | let circle1 = Circle { 67 | x: 1f64, 68 | y: 1f64, 69 | radius: 10f64, 70 | }; 71 | print_area(circle1); 72 | ``` 73 | 74 | 因为 rust 中的这种限制,一般面向接口编程时,会使用大量的泛型,这也是为什么一般的 rust 代码中,出现泛型漫天飞的原因。 75 | 76 | rust 的 trait 可以有不同的调用方式: 77 | 78 | ```rust 79 | let circle = Circle { 80 | x: 0f64, 81 | y: 0f64, 82 | radius: 10f64, 83 | }; 84 | circle.area(); // 普通函数调用 85 | HasArea::area(&circle); // 从 trait 开始的函数调用 86 | ``` 87 | -------------------------------------------------------------------------------- /contents/js-engineer-capability.md: -------------------------------------------------------------------------------- 1 | # js 工程师的能力总结 2 | 3 | ### 沟通能力 4 | 5 | + 能够通过沟通理解需求 6 | + 如果实现时遇到困难(需求冲突、需求不明确、技术上不可实现),能够及时告知、协商 7 | + 实现完成后,能够及时反馈 8 | 9 | ### 程序设计能力 10 | 11 | + 能够把控程序的整体结构,清楚数据的整体流向 12 | + 能够选用合适的数据结构 13 | + 能够设计合理的数据存储结构(数据库、文件、nosql) 14 | + 能够设计稳定、易用的组件(后端库、前端 UI 组件) 15 | + 能够设计稳定、易用的服务(http、websocket、rpc) 16 | + 有好的技术视野,清楚技术的发展方向,能够提出多种实现方案,理解各个方案的优缺点,根据具体需求选用合适的方案来实现 17 | 18 | ### 程序实现能力 19 | 20 | + 能实现所有可行的程序设计方案 21 | + 能实现设计图 22 | 23 | ### 程序表达能力 24 | 25 | + 能让写出的代码持续明晰 26 | + 能够书写明晰的组件、服务使用文档 27 | + 能够书写明晰的开发文档 28 | -------------------------------------------------------------------------------- /contents/js-project-package-version-strategy.md: -------------------------------------------------------------------------------- 1 | # js 项目的 package 版本策略 2 | 3 | #### 发布到 npm 的 package 的版本号应该是什么? 4 | 5 | 总体上,版本号应该遵循 semver(Semantic Versioning,` 主版本号. 次版本号. 修订号 `)的规则。具体来说: 6 | 7 | + 第一次发布,版本号是 `1.0.0` 8 | + 如果有不兼容的 API 修改,主版本号增加 1,次版本号和修订号变为 0。例如 `1.2.3->2.0.0` 9 | + 如果没有不兼容的 API 修改,如果增加了新功能,主版本号不变,次版本号增加 1,修订号变为 0。例如 `1.2.3->1.3.0` 10 | + 如果没有不兼容的 API 修改,也没有增加新功能,只是做了问题修复,主版本号和次版本号不变,修订号增加 1。例如 `1.2.3->1.2.4` 11 | 12 | #### 为项目新增加了一个依赖,是放在 dependencies 下,还是 devDependencies 下? 13 | 14 | 总体上,dependencies 下应该放置运行时的依赖;devDependencies 下应该放置开发时的依赖。具体来说: 15 | 16 | + 如果是 nodejs 程序,或者 CLI 程序,程序运行时的依赖包放到 dependencies 下,程序运行时不依赖的包放到 devDependencies 下。例如,一个网站后端,`express` 应该放在 dependencies 下,而 `@types/express` 和 `typescript` 应该放在 devDependencies 下 17 | + 如果是前端程序,因为前端程序普遍会有打包流程,程序运行时不会依赖 node_modules 内的包,但未来依赖包里的文件可能需要 host 在 http2 下,这时放在 dependencies 下也就有必要了,所以建议把所有的依赖都放到 dependencies 下。例如,一个网站前端,`react` 应该放在 dependencies 下 18 | + 如果是库,安装这个库时也需要安装的依赖包放到 dependencies 下,安装这个库时不需要安装的依赖包放到 devDependencies 下。例如,一个 express 中间件,`express` 和 `@types/express` 应该放在 dependencies 下,而 `typescript` 应该放在 devDependencies 下 19 | 20 | #### dependencies 下 package 的版本号应该是什么形式? 21 | 22 | 总体上,应该在保证不会出现因为依赖更新导致程序失败的基础上,含括尽可能多的依赖版本。具体来说: 23 | 24 | + 如果是 nodejs 程序,或者 CLI 程序,为了稳定性,包的版本形式可以是 `"x.y.z"` 25 | + 如果是库,应该尽可能和其它库复用依赖,如果某个包在 `x.y.0` 下正常工作,在 `x.{y-1}.z` 下不能正常工作(可以查看 changelog,一般是 `x.y.0` 时加入了某些使用到的功能),这个包的版本形式应该是 `"^x.y"`(如果 y 是 0,可以简化为 `"x"`, 如果 x 是 0,可以简化为`"0.y"`),表示 `[x.y.0, {x+1}.0.0)` 内的所有包都会支持;对于 @types 下的库,为了避免类型冲突,使用 `"*"` 26 | + 如果是一些经常打破 semver 的库(例如 @types 下的几千个库),为了稳定性,包的版本形式应该是 `"x.y.z"` 27 | + 如果包的主版本号是 0,说明这个包还不稳定,API 很可能会变,为了稳定性,包的版本形式应该是 `"x.y.z"` 28 | + 如果希望依赖多个主版本号,可以使用 `||`,例如 `1.1 || 2` 29 | 30 | #### devDependencies 下 package 的版本号应该是什么形式? 31 | 32 | 为了稳定性,包的版本形式应该是 `"x.y.z"` 33 | 34 | #### 怎么只安装 dependencies 下的包? 35 | 36 | `npm i --production` 37 | 38 | #### 怎么让安装包后,package.json 中保存的是 `"x.y.z"`,而不是 `"^x.y.z"`? 39 | 40 | `npm i abc --save --save-exact` 41 | 42 | #### 如果程序依赖了 a 和 b,a 依赖 b,程序还需要显式依赖 b 吗? 43 | 44 | 需要,因为如果不显式依赖 b,如果未来 a 不再依赖 b,程序也就 break 了 45 | 46 | 可以使用 `no-unused-package` 来在 CI 中检查 47 | -------------------------------------------------------------------------------- /contents/keep-api-compatible-strategy.md: -------------------------------------------------------------------------------- 1 | # 保证接口兼容性的策略 2 | 3 | ## 新增字段 4 | 5 | 新增的字段需要是 `optional` 的 6 | 7 | ```ts 8 | interface Foo { 9 | bar: number 10 | } 11 | 12 | interface Foo { 13 | bar: number 14 | baz?: string // <- 新增的字段 15 | } 16 | ``` 17 | 18 | ## 字段命名 19 | 20 | 已有的字段不要改名字,而是通过注释的方式解释实际的含义 21 | 22 | ```ts 23 | interface Foo { 24 | bar: number 25 | } 26 | 27 | interface Foo { 28 | /** 29 | * 红色 bar 30 | */ 31 | bar: number 32 | /** 33 | * 绿色 bar 34 | */ 35 | greenBar: string 36 | } 37 | ``` 38 | 39 | ## 函数新增参数 40 | 41 | 函数新增的参数需要是 `optional` 的 42 | 43 | ```ts 44 | function foo(bar: number) {} 45 | 46 | function foo(bar: number, baz?: string) {} 47 | ``` 48 | 49 | 但不要有多个 `optional` 的参数,需要转换为 `options` 字段 50 | 51 | ```ts 52 | function foo(bar: number, baz?: string) {} 53 | 54 | function foo(bar: number, options?: string | Partial) { 55 | options = getOptions(options) 56 | } 57 | 58 | interface Options { 59 | baz: string 60 | qux: number 61 | } 62 | 63 | function getOptions(options?: string | Partial): Partial { 64 | if (options) { 65 | if (typeof options === 'string') { 66 | return { 67 | baz: options 68 | } 69 | } 70 | return options 71 | } 72 | return {} 73 | } 74 | ``` 75 | 76 | ## 函数返回值增加字段 77 | 78 | 如果函数的返回值不是 object,比较 dirty 的方式是,通过增加 optional 参数 callback 的方式返回新字段 79 | 80 | ```ts 81 | function foo() { 82 | return 1 83 | } 84 | 85 | function foo(getBar?: () => string) { 86 | if (getBar) { 87 | getBar('bar') 88 | } 89 | return 1 90 | } 91 | ``` 92 | 93 | 最理想的方式,还是函数一开始就返回 object 94 | 95 | ```ts 96 | function foo() { 97 | return { 98 | foo: 1 99 | } 100 | } 101 | 102 | function foo() { 103 | return { 104 | foo: 1, 105 | bar: 'bar' // 注意这里不需要是 optional 的 106 | } 107 | } 108 | ``` 109 | 110 | ## 函数参数的 callback 函数增加参数 111 | 112 | 这时不需要设为 optional 就可以保证兼容 113 | 114 | ```ts 115 | function foo(bar: (baz: number) => void) {} 116 | 117 | function foo(bar: (baz: number, qux: string) => void) {} 118 | ``` 119 | 120 | ## 新增 union 类型 121 | 122 | ```ts 123 | interface Foo { 124 | bar: number 125 | } 126 | 127 | type Foo = Bar | Baz 128 | 129 | interface Bar { 130 | type?: 'bar' 131 | bar: number 132 | } 133 | 134 | interface Baz { 135 | type: 'baz' 136 | baz: string 137 | } 138 | ``` 139 | -------------------------------------------------------------------------------- /contents/lerna-project-ci.md: -------------------------------------------------------------------------------- 1 | # lerna 项目的 CI 优化 2 | 3 | 用 lerna 管理的 monorepo 项目形式,相比多仓库的项目形式,带来了很多好处,也引起了一些缺点。 4 | 5 | 其中一个就是,当 package 变多时,CI 内还是会完全 build 所有 package,导致 build 时间变长。 6 | 7 | 一个直观的优化方案是,只对作了修改的 package 执行 build / lint / test 即可。 8 | 9 | ## lint 的优化方案 10 | 11 | + 可以并行执行 lint 和 build / test 12 | + 只对作了修改的 package 执行 lint 即可 13 | + 可以并行执行各个 package 的 lint,可以通过 `lerna run lint --parallel --scope package-a --scope package-b` 来实现 14 | 15 | ## build 和 test 的优化方案 16 | 17 | build 和 test 过程的情况要复杂一些,例如 `package-a` 依赖 `package-b`,`package-b` 依赖 `package-c` 18 | 19 | 如果 `package-b` 里有代码改动,需要先 build `package-c`,再 build `package-b`;而且 test `package-b` 后,也需要 test `package a`,所以 `package-a` 也需要 build 20 | 21 | + 对作了修改的 package 需要执行 build 和 test 22 | + 对作了修改的 package 的依赖 packages 和被依赖 packages 都需要执行 build 23 | + 对作了修改的 package 的被依赖 packages 都需要执行 test 24 | + 执行 `lerna updated` 后返回获得的是修改的 package 和被依赖 packages 25 | + 通过递归读取 package 里的 `package.json` 里的 `dependencies`, `devDependencies` 和 `peerDependencies`,获得依赖 packages 26 | + 通过 `lerna run build --scope package-a --scope package-b --scope package-c` 来执行 build 27 | + build 完成后,通过 `lerna run test --parallel --scope package-a --scope package-b` 来执行 test,注意可以并行执行 test 的 28 | + 如果执行过 `lerna version`,`lerna updated` 会返回空结果,作了修改的 package 可以通过下面的 `npm view` 来获得 29 | + 进一步的优化是,作了修改的 package 的依赖 packages 不执行 build,而是删除对应的代码后执行 `lerna bootstrap`,这时这些依赖 packages 会当作外部依赖包被安装,也就避免了这些包的 build 过程 30 | 31 | ## 在 CI 中进行 package 自动发布 32 | 33 | + 一般在 build, lint 和 test 后执行 34 | + 一般只在 tag 上或 master 分支上开启,tag、分支名、commit message 可以在 CI 的环境变量中拿到 35 | + 可以通过 `npm view @foo/bar versions` 来获得 package 已经发布的所以版本,如果 `package.json` 里的版本没有发布,或者错误消息里包含 `404 Not Found`(说明这个 package 还没有发布过),就通过执行 `cd ./packages/bar && npm publish` 来发布 36 | -------------------------------------------------------------------------------- /contents/map-reduce-filter.md: -------------------------------------------------------------------------------- 1 | # map / reduce / filter 相关模式 2 | 3 | map / reduce / filter 的概念来自函数式编程,是针对集合的操作,用于表示映射、聚合、过滤。 4 | 5 | 有一种数据处理模型以此命名,称为 MapReduce。 6 | 7 | 而在具体的软件开发中,这三个概念被分别抽象出三个函数,而具体通过什么方式来映射、聚合、过滤,则由调用者来决定。 8 | 9 | 如果要实现这样的效果,需要 ` 函数作为参数传递 `,传统的过程式编程和 OO 都没有办法,而函数式编程,或者说是带有函数式编程的多范式语言就可以做到。 10 | 11 | js 就是其中之一,以调用者的角度来看: 12 | 13 | ```js 14 | const books = [ 15 | { name: "a", pages: 10 }, 16 | { name: "b", pages: 20 }, 17 | { name: "c", pages: 30 }, 18 | ]; 19 | 20 | const filteredBooks = books.filter(function(book) { 21 | return book.pages > 12; 22 | }); 23 | const mappedBooks = filteredBooks.map(function(book) { 24 | return { 25 | name: book.name, 26 | pages: book.pages / 2, 27 | }; 28 | }); 29 | const totalPages = mappedBooks.reduce(function(result, book) { 30 | return result + book.pages; 31 | }, 0); 32 | ``` 33 | 34 | 如果引入 `lambda 表达式 `,使用时变得更加简洁,代码可以写成: 35 | 36 | ```js 37 | const filteredBooks = books.filter(book => book.pages > 12); 38 | const mappedBooks = filteredBooks.map(book => { 39 | return { 40 | name: book.name, 41 | pages: book.pages / 2, 42 | }; 43 | }); 44 | const totalPages = mappedBooks.reduce((result, book) => result + book.pages, 0); 45 | ``` 46 | 47 | 甚至可以有链式的写法,例如: 48 | 49 | ```js 50 | const totalPages = books 51 | .filter(book => book.pages > 12) 52 | .map(book => { 53 | return { 54 | name: book.name, 55 | pages: book.pages / 2, 56 | }; 57 | }) 58 | .reduce((result, book) => result + book.pages, 0); 59 | ``` 60 | 61 | 从实现者的角度看,不考虑参数验证的话,类似于: 62 | 63 | ```ts 64 | function filter(array: T[], func: (t: T) => boolean): T[] { 65 | let result: T[] = []; 66 | for (const t of array) { 67 | if (func(t)) { 68 | result.push(t); 69 | } 70 | } 71 | return result; 72 | } 73 | 74 | function map(array: T[], func: (t: T) => U): U[] { 75 | let result: U[] = []; 76 | for (const t of array) { 77 | result.push(func(t)); 78 | } 79 | return result; 80 | } 81 | 82 | function reduce(array: T[], func: (u: U, t: T) => U, u0: U): U { 83 | for (const t of array) { 84 | u0 = func(u0, t); 85 | } 86 | return u0; 87 | } 88 | ``` 89 | 90 | python 中也有类似的模式: 91 | 92 | ```py 93 | print map(lambda x: x * x, [1, 2, 3]) 94 | print reduce(lambda x, y: x + y * y, [1, 2, 3], 10) 95 | print filter(lambda x: x > 1, [1, 2, 3]) 96 | ``` 97 | 98 | C# 中也有类似的模式: 99 | 100 | ```csharp 101 | var books = new[] { 102 | new{ Name = "a", Pages = 10 }, 103 | new{ Name = "b", Pages = 20 }, 104 | new{ Name = "c", Pages = 30 }, 105 | }; 106 | var totalPages = books 107 | .Where(book => book.Pages > 12) 108 | .Select(book => 109 | { 110 | return new 111 | { 112 | Name = book.Name, 113 | Pages = book.Pages / 2, 114 | }; 115 | }) 116 | .Aggregate(0, (result, book) => result + book.Pages); 117 | ``` 118 | 119 | 这里的实现特色是,并不是逐步执行、上一步的结果作为下一步的参数的,而是延迟执行的,只会计算一次,而不是三次。这是 LINQ 的一个特色,可以提高性能。 120 | 121 | C++11 中也有类似的模式: 122 | 123 | ```C++ 124 | #include 125 | #include 126 | #include 127 | 128 | using namespace std; 129 | 130 | void main() { 131 | vector vec{ 1, 2, 3 }; 132 | transform(vec.begin(), vec.end(), vec.begin(), [](int i) { return i * i; }); 133 | auto it = remove_if(vec.begin(), vec.end(), [](int i) { return i > 1; }); 134 | accumulate(vec.begin(), vec.end(), 1, [](int a, int b) { return a * b; }); 135 | } 136 | ``` 137 | 138 | ruby 中也有类似的模式: 139 | 140 | ```ruby 141 | nums = [1,2,3,4,5] 142 | result = nums.map{|x| x * x}.select{|x| x % 2 == 0}.reduce{|a, x| a + x} 143 | p result 144 | ``` 145 | 146 | swift 中也有类似的模式: 147 | 148 | ```swift 149 | var a = [1,2,3,4].map{ $0 * $0 }.filter{ $0 > 1 }.reduce(0) { $0 + $1 } 150 | print(a) 151 | ``` 152 | 153 | scala 中也有类似的模式: 154 | 155 | ```scala 156 | val a = (1 to 5).map(x => x * x).filter(x => x > 1).reduceLeft(_ + _) 157 | println(a) 158 | ``` 159 | 160 | rust 中也有类似的模式: 161 | 162 | ```rust 163 | let a = [1, 2, 3, 4, 5].iter().map(|x| x * x).filter(|x| x % 2 == 0).fold(0, |acc, x| acc + x); 164 | println!("{}", a); 165 | ``` 166 | 167 | 不过,C、Java、Go 却没有相关的用法。 168 | -------------------------------------------------------------------------------- /contents/mine-sweeper-strategies.md: -------------------------------------------------------------------------------- 1 | # 扫雷游戏的策略 2 | 3 | ``` 4 | 2 1 5 | 1 1 0 6 | 0 0 0 7 | ``` 8 | 9 | 以上面的例子为例,按照中间的 1,可以推断左上角是一个雷 10 | 11 | 抽象出具体规则是: 12 | 13 | ```js 14 | const mineCount = 1; // 周围的雷的数量 15 | const flaggedCount = 0; // 周围的旗帜的数量 16 | const unknownCount = 1; // 周围的未确认的位置数量 17 | const unknownMineCount = mineCount - flaggedCount; // 周围未确认的的雷的数量 = 1 18 | if (unknownMineCount === 0) { 19 | // 周围的未确认位置都不是雷,可以挖 20 | } else if (unknownMineCount === unknownCount) { 21 | // 周围的未确认位置都是雷,可以插旗 22 | } else { 23 | // 没有结论 24 | } 25 | ``` 26 | 27 | ``` 28 | F 2 0 | 29 | 2 2 1 | 30 | | 31 | ------- 32 | ``` 33 | 34 | 以上面的例子为例,按照其中的 1,可以推断下面的后 2 个未确认位置中有 1 个雷;按照最中间的 2,可以推断下面的 3 个未确认位置中有 1 个雷。 35 | 36 | 根据上面的 2 个条件,可以推断最左边的那个未确认位置不含雷,可以挖 37 | 38 | 抽象出具体规则是: 39 | 40 | ``` 41 | // 定义三个未确认位置,从左到右分别是 A、B、C 42 | const condition1 = { 43 | positions: [A, B, C], // 未确认位置 44 | mineCount: 1, // 上面几个未确认位置中,所含雷的个数 45 | }; 46 | const condition2 = { 47 | positions: [B, C], // 未确认位置 48 | mineCount: 1, // 上面几个未确认位置中,所含雷的个数 49 | }; 50 | // 确认 condition1.positions 完全包含了 condition2.positions 51 | const mineCount = condition1.mineCount - condition2.mineCount; // 雷的数量差 = 0 52 | const unknownCount = condition1.positions.length - condition2.positions.length; // 未确认位置的数量差 = 1 53 | if (mineCount === 0) { 54 | const positions = condition1.positions.filter(p1 => condition2.positions.every(p2 => p1 !== p2)); // 计算未确认位置的差值集合 = [C] 55 | // 上述差值集合都不是雷,可以挖 56 | } else if (mineCount === unknownCount) { 57 | const positions = condition1.positions.filter(p1 => condition2.positions.every(p2 => p1 !== p2)); // 计算未确认位置的差值集合 = [C] 58 | // 上述差值集合都是雷,可以插旗 59 | } else { 60 | // 没有结论 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /contents/mysql-and-mongodb-storage-density-test-result.md: -------------------------------------------------------------------------------- 1 | # mysql 和 mongodb 的存储密度测试结果 2 | 3 | 数据量 | mongodb | mysql 4 | --- | --- | --- 5 | 20k | 300MB | 3.5MB 6 | 100k | 312MB | 14.5MB 7 | 1M | 423MB | 145MB 8 | 10M | 1.5GB | 1.4GB 9 | 100M | 9.03GB | 13.6GB 10 | 11 | + mongodb 占用的初始空间比较大 12 | + mysql 的受字段类型和长度影响大 13 | + mongodb 的字段名也会和数据保存起来,所以字段名越长,占用的空间越大 -------------------------------------------------------------------------------- /contents/nodejs-code-strcture.md: -------------------------------------------------------------------------------- 1 | # nodejs 的代码组织形式 2 | 3 | ### `npm init` + 单文件 4 | 5 | `npm init` 可以产生 `package.json` 文件,配合单 js 文件,组成最简单的形式。 6 | 7 | ### 分拆模块 8 | 9 | 当逻辑越来越复杂,有必要把代码拆分成不同的模块,也可以提高代码复用; 10 | 11 | nodejs 的模块形式是 commonjs,同步加载,一个文件就是一个模块,文件名就是模块名。 12 | 13 | ### 多入口 14 | 15 | 如果要进行业务拆分,可以拆分成不同的仓库,不过,这样降低代码的复用,有一些公共逻辑,例如类型、原型(prototype)、公共方法,就被复制在不同的仓库,或者比较麻烦的 submodule 的方式,甚至由于库的版本不一致,而产生不必要的 BUG。 16 | 17 | 多入口则是一个比较好的方式,共用一个仓库,共用所有模块,对于每个模块,如果有必要初始化,暴露一个初始化方法,由入口文件决定是否初始化。 18 | 19 | 多入口的缺点是,每个入库都会同步加载所有的模块代码,即使用不到其中一部分,不过这只是代码的大小,可以预期不会很大。 20 | 21 | ```js 22 | // app1.js 23 | const libs = require("./libs"); 24 | const services = require("./services"); 25 | services.foo.init(); 26 | services.baz.init(); 27 | ``` 28 | 29 | ```js 30 | // app2.js 31 | const libs = require("./libs"); 32 | const services = require("./services"); 33 | services.foo.init(); 34 | services.bar.init(); 35 | ``` 36 | 37 | ### 公共外部库 38 | 39 | 当模块越来越多,每个模块都会或多或少导入一些外部模块,但是由于 commonjs 的特性,不会重复导入已经导入的模块,这样外部模块的导入声明,大部分是重复的声明,并没有实际的动作; 40 | 41 | 可以使用公共的专用于引入外部模块的模块,例如 `libs.js`,导入模块后,例如导出,在其它模块中,只需要用如下的方式来引入外部模块了: 42 | 43 | ```js 44 | // libs.js 45 | export const url = require("url"); 46 | export const express = require("express"); 47 | export const http = require("http"); 48 | ``` 49 | 50 | ```js 51 | // foo.js 52 | const libs = require("./libs"); 53 | libs.url; 54 | libs.express; 55 | libs.http; 56 | ``` 57 | 58 | ```js 59 | // bar.js 60 | const libs = require("./libs"); 61 | libs.url; 62 | libs.express; 63 | libs.http; 64 | ``` 65 | 66 | ### 公共内部模块 67 | 68 | 由上一条,可以联想到,有很多内部模块,也会或多或少引用其它内部模块,也有很大重复,可以利用类似的策略: 69 | 70 | ```js 71 | // services.js 72 | export const foo = require("./foo"); 73 | export const bar = require("./bar"); 74 | export const a = require("./a"); 75 | export const b = require("./b"); 76 | ``` 77 | 78 | ```js 79 | // a.js 80 | const libs = require("./libs"); 81 | const services = require("./services"); 82 | libs.url; 83 | libs.express; 84 | libs.http; 85 | services.foo.something(); 86 | services.bar.anotherThing(); 87 | ``` 88 | 89 | ```js 90 | // b.js 91 | const libs = require("./libs"); 92 | const services = require("./services"); 93 | libs.url; 94 | libs.express; 95 | libs.http; 96 | services.foo.something(); 97 | services.bar.anotherThing(); 98 | ``` 99 | 100 | ### 用目录来组织代码 101 | 102 | 例如,内部模块可以全部放在 `services/` 或 `modules/` 里; 103 | 104 | 代码可以都放在 `src/` 里,生成的代码放在 `dist/` 里,测试文件放在 `tests/` 里; 105 | 106 | 其它类似的还有 `docs/`、`scripts/`、`resources/` 等等。 107 | 108 | ### view 层 109 | 110 | 一般不推荐在后台渲染页面,如果一定要这样做,可以放在 `views/` 或 `templates/` 里。 111 | 112 | ### 内部模块的可测试性 113 | 114 | 当一个内部模块直接调用其它内部模块,或外部库时,如果涉及到文件操作、网络操作、数据库操作、其它服务等,因为代码耦合严重,不便测试。 115 | 116 | 这时可以通过,把所要调用的的函数,移到参数中,以减少耦合度。 117 | 118 | ```ts 119 | function foo() { 120 | return 123; 121 | } 122 | 123 | function bar(_foo: typeof foo) { 124 | return 1 + _foo(); 125 | } 126 | 127 | // 调用时 128 | bar(foo); 129 | 130 | // 测试时 131 | bar(() => 234); 132 | ``` 133 | 134 | 上面是移到函数参数的形式。如果使用了 class,可以把 method 公共的依赖,移到 constructor 中。 135 | 136 | ```ts 137 | function foo() { 138 | return 123; 139 | } 140 | class Bar { 141 | constructor(private _foo: typeof foo) { } 142 | bar() { 143 | return 1 + this._foo(); 144 | } 145 | } 146 | 147 | // 调用时 148 | const bar1 = new Bar(foo); 149 | bar1.bar(); 150 | 151 | // 测试时 152 | const bar2 = new Bar(() => 234); 153 | bar2.bar(); 154 | ``` 155 | -------------------------------------------------------------------------------- /contents/nodejs-deploy-strategy.md: -------------------------------------------------------------------------------- 1 | # nodejs 部署策略 2 | 3 | ### 日志策略 4 | 5 | 代码中用 `console.log(info)` 记录日志,这样会记录在 `pm2` 的日志中; 6 | 为了避免日志文件过大,可以使用 `pm2-logrotate` 切分日志; 7 | 可以用日志工具,例如 ELK,收集、转换、查询这些日志文件,logstash 把 pm2 的日志和系统日志,转换结构后传输给 elastic search 索引保存,再通过 kibana 搜索和展示; 8 | 9 | ### 进程管理 10 | 11 | pm2 12 | 13 | ### 发布更新 14 | 15 | 待发布的 js 文件发布到独立的 git 仓库中(通过 rsync 或 xcopy),在服务器上 clone 这个仓库,这样,git pull 时就更新了代码,之后再通过 pm2 restart 来重启程序; 16 | 17 | ### 恢复本地包 18 | 19 | `npm i --production` 20 | 21 | ### 备份和回滚 22 | 23 | 因为本身就是 git 仓库,自带备份; 24 | 如果要回滚到上一次 commit,`git reset --hard HEAD^`; 25 | 26 | ### 运行监视 27 | 28 | 程序中收集计数,通过监视程序,定时汇总,并实时显示出来; 29 | 30 | ### 批量管理 31 | 32 | 对于多台机器,可以使用一些运维工具,例如 ansible,来批量执行命令; 33 | 34 | ### 环境变量 35 | 36 | 可以配置在. bashrc 或. profile 等文件中,打开 bash 后会自动加载; 37 | 更新环境变量后,使用 `source` 命令加载,再通过 `pm2 restart` 使环境变量在程序中生效; 38 | 39 | ### 灰度发布 40 | 41 | 指定一个节点用于灰度发布,通过 nginx 中的配置的权重控制访问量; 42 | 通过后,执行完整发布; 43 | -------------------------------------------------------------------------------- /contents/nodejs-introduction.md: -------------------------------------------------------------------------------- 1 | # nodejs 概要介绍分享记录 2 | 3 | ### nodejs: 服务端 js 运行环境 4 | 5 | ### Js + nodejs 运行环境 + Npm 包管理器 6 | 7 | + Js:ECMAScript 的实现 8 | + Nodejs 中的 global 对象 / 浏览器中的 window 对象 9 | + Nodejs 标准库:fs/http/net/dgram/os/process… 10 | + Npm:类似于 maven 11 | 12 | ### 问题和方案 13 | 14 | + Js 的缺陷:严格模式、代码检查、 ES 新特性 15 | + CPU 核心:负载均衡 16 | + 服务守护:pm2 17 | + 包依赖和管理:语义化版本 / npm-check 18 | + Callback:Promise/Generator/async/await 19 | + Prototype 继承:Class 继承 20 | + 动态语言:类型系统(typescript/flowtype) 21 | + 模块系统:commonjs/ES2015 22 | + For..in:for…of 23 | + Var:const let 24 | + this in function:arrow function 25 | + ES 新特性:rest/spread/destructuring 26 | 27 | ### 常见的库 28 | 29 | + 推送:ws/uws/socket.io 30 | + http 服务:express.js 31 | + 数据流处理:Rxjs 32 | + oracle:oracledb 33 | + ActiveMQ:stompjs 34 | + 工具库:lodash/underscore 35 | + Redis:ioredis 36 | + http 请求调用:node-fetch/requestjs 37 | -------------------------------------------------------------------------------- /contents/orthogonal-rule-of-technology-selection.md: -------------------------------------------------------------------------------- 1 | # 技术选型时的正交原则 2 | 3 | 对于两组技术,方向正交、相关性和依赖性更弱的那一组更好一些。 4 | 5 | 一是因为,更新更及时;二是因为,灵活性高,每个工具的可替换性更强,未来被替换时代码改动小。 6 | 7 | 对于某些大而全的工具,为了保证正交性,有时只需要使用其中的一部分功能。 8 | 9 | 举个例子,有一些 commonjs 模块的 ES6 代码,需要转换成 ES5 代码,并打包成一个文件。 10 | 11 | 有两个过程,6to5 的转换和 commonjs 模块的打包,两个过程可以分别用工具来实现,也可以使用 webpack 的 babel-loader 来一次性实现。 12 | -------------------------------------------------------------------------------- /contents/package-design.md: -------------------------------------------------------------------------------- 1 | # package 的设计 2 | 3 | ## package 的常见使用场景 4 | 5 | + web 前端 webpack 打包 6 | + web 前端 ` 162 | ``` 163 | 164 | 加入基于 cookie 的身份验证后: 165 | 166 | ```js 167 | // server 168 | import * as express from "express"; 169 | import * as socketIO from "socket.io"; 170 | // changes start 171 | import * as cookie from "cookie"; 172 | import * as validator from "validator"; 173 | import * as services from "./services"; 174 | // changes end 175 | 176 | const app = express(); 177 | const server = app.listen(3000); 178 | const io = socketIO(server); 179 | const promotions = io.of("/promotions"); 180 | 181 | promotions.on("connection", socket => { 182 | // changes start 183 | const cookies = libs.cookie.parse(validator.trim(socket.handshake.headers.cookie)); 184 | services.validate(cookies["cookie_name"]).then(userId => { 185 | if (!userId) { 186 | socket.disconnect(true); 187 | } 188 | }); 189 | // changes end 190 | }); 191 | ``` 192 | 193 | 加入广播功能后: 194 | 195 | ```js 196 | // server 197 | import * as express from "express"; 198 | import * as socketIO from "socket.io"; 199 | import * as cookie from "cookie"; 200 | import * as validator from "validator"; 201 | import * as services from "./services"; 202 | 203 | const app = express(); 204 | const server = app.listen(3000); 205 | const io = socketIO(server); 206 | const promotions = io.of("/promotions"); 207 | 208 | promotions.on("connection", socket => { 209 | const cookies = libs.cookie.parse(validator.trim(socket.handshake.headers.cookie)); 210 | services.validate(cookies["cookie_name"]).then(userId => { 211 | if (!userId) { 212 | socket.disconnect(true); 213 | // changes start 214 | } else { 215 | socket.join("room_name"); 216 | } 217 | // changes end 218 | }); 219 | }); 220 | 221 | // changes start 222 | const promotion = { 223 | 224 | }; 225 | promotions.to("room_name").emit("promotion", promotion); 226 | // changes end 227 | ``` 228 | 229 | ```js 230 | // client 231 | socket.on("promotion", promotion => { 232 | 233 | }); 234 | ``` 235 | 236 | 加入客户端到服务器端的通信后: 237 | 238 | ```js 239 | // server 240 | import * as express from "express"; 241 | import * as socketIO from "socket.io"; 242 | import * as cookie from "cookie"; 243 | import * as validator from "validator"; 244 | import * as services from "./services"; 245 | 246 | const app = express(); 247 | const server = app.listen(3000); 248 | const io = socketIO(server); 249 | const promotions = io.of("/promotions"); 250 | 251 | promotions.on("connection", socket => { 252 | const cookies = libs.cookie.parse(validator.trim(socket.handshake.headers.cookie)); 253 | services.validate(cookies["cookie_name"]).then(userId => { 254 | if (!userId) { 255 | socket.disconnect(true); 256 | } else { 257 | socket.join("room_name"); 258 | // changes start 259 | socket.on("promotion accepted", promotionId => { 260 | 261 | }); 262 | // changes end 263 | } 264 | }); 265 | }); 266 | 267 | const promotion = { 268 | 269 | }; 270 | promotions.to("room_name").emit("promotion", promotion); 271 | ``` 272 | 273 | ```js 274 | // client 275 | socket.on("promotion", promotion => { 276 | // changes start 277 | socket.emit("promotion accepted", promotion.id); 278 | // changes end 279 | }); 280 | ``` 281 | 282 | 对于多 socket.io 节点,可以使用 https://github.com/socketio/socket.io-redis : 283 | 284 | ```js 285 | // server 286 | import * as express from "express"; 287 | import * as socketIO from "socket.io"; 288 | import * as cookie from "cookie"; 289 | import * as validator from "validator"; 290 | import * as services from "./services"; 291 | // changes start 292 | import * as socketioRedis from "socket.io-redis"; 293 | // changes end 294 | 295 | const app = express(); 296 | const server = app.listen(3000); 297 | const io = socketIO(server); 298 | const promotions = io.of("/promotions"); 299 | // changes start 300 | io.adapter(socketioRedis({ 301 | host: "127.0.0.1", 302 | port: 6379, 303 | })); 304 | // changes end 305 | 306 | promotions.on("connection", socket => { 307 | const cookies = libs.cookie.parse(validator.trim(socket.handshake.headers.cookie)); 308 | services.validate(cookies["cookie_name"]).then(userId => { 309 | if (!userId) { 310 | socket.disconnect(true); 311 | } else { 312 | socket.join("room_name"); 313 | socket.on("promotion accepted", promotionId => { 314 | 315 | }); 316 | } 317 | }); 318 | }); 319 | 320 | const promotion = { 321 | 322 | }; 323 | promotions.to("room_name").emit("promotion", promotion); 324 | ``` 325 | 326 | ```js 327 | // client 328 | socket.on("promotion", promotion => { 329 | socket.emit("promotion accepted", promotion.id); 330 | }); 331 | ``` 332 | 333 | ### 全文搜索 334 | 335 | 当数据库中的模糊搜索效率太低时,可以使用专用的搜索工具来实现搜索功能,例如 elastic search。 336 | 337 | 存储或查询时,直接向 elastic search 服务发送 http 请求即可。 338 | 339 | ### 消息队列 340 | 341 | 消息队列可以实现异步调用,也可以缓冲任务。 342 | 343 | 除了各种 MQ,redis 也可以作为轻量级的消息队列。 344 | 345 | 也可以使用分布式的 https://github.com/apache/kafka 。 346 | 347 | ### 业务拆分和横向扩展 348 | 349 | 拆分业务后,可以简化系统复杂度。横向扩展可以分摊负载。 350 | 351 | 对于 session 不能共享的问题,可以通过把 session 内容保存到缓存中来解决。 352 | 353 | ### 数据库的主从复制 354 | 355 | 考虑到读操作的频率要远大于写操作,读写分离后,可以大幅减轻读库压力,还方便扩展。 356 | 357 | 具体操作时,每个数据库都建立一个连接池,所以会有一个写连接池和多个读连接池,当需要读取操作时,按权选择一个读连接池,再执行该查询语句。 358 | 359 | ### 分库和分表 360 | 361 | ### 异地双机热备 362 | 363 | 为了解决机房断电断网后的系统可用性问题,可以设计异地双机热备方案,各种服务都有能够相互同步。 364 | -------------------------------------------------------------------------------- /contents/web-security.md: -------------------------------------------------------------------------------- 1 | # web安全总结 2 | 3 | ### 浏览器 4 | 5 | #### 子资源完整性检查(Subresource Integrity, SRI) 6 | 7 | 较新的浏览器支持 SRI,即在 script 或 link 标签上增加 integrity 属性后,浏览器在执行前,会先计算子资源的 hash,并与 integrity 属性的值比较是否一致,如果不一致,浏览器则认为此资源已被污染,不会执行,从而避免资源文件被第三方 CDN 等修改产生的安全问题 8 | 9 | ### Content-Security-Policy Header(CSP) 10 | 11 | 较新的浏览器支持 CSP 头检查,包括是否允许 eval 操作、js / css 内联、限制 js / css / img / font 来源域名,从而避免 XSS 和 js 注入攻击 12 | 13 | ### X-XSS-Protection Header 14 | 15 | 较新的浏览器支持 X-XSS-Protection 头,支持 XSS 过滤,从而避免 XSS 攻击 16 | 17 | ### X-Frame-Options Header 18 | 19 | 较新的浏览器支持 X-Frame-Options 头,可以让浏览器避免把本页面作为其它网站的 frame 源,从而避免网站的 js 执行权限被其它网站通过 frame 的方式获得 20 | 21 | ### 通信协议 22 | 23 | #### https 24 | 25 | 通信加密,避免内容被中间的网络供应商等获取、修改 26 | 27 | ### 浏览器端开发 28 | 29 | 用户的内容:包括输入框等表单输入、URL地址(参数、hash)、从服务端接收的其他用户的数据 30 | 31 | #### 用户的内容在 html 中的展示前,要进行 html 转义 32 | 33 | 避免 js 执行权限泄漏 34 | 35 | #### 用户的内容不能被当成 js 来执行 36 | 37 | 避免 js 执行权限泄漏 38 | 39 | #### 用户的内容中的第三方链接和图片打开时,不要附带 cookie 和 token 40 | 41 | 避免 cookie 和 token 泄漏 42 | 43 | #### 不要手工拼接 json, html, js,使用库或者安全的模版 44 | 45 | 避免 js 执行权限泄漏 46 | 47 | ### 服务端开发 48 | 49 | #### secure & httpOnly cookie 50 | 51 | 某个 cookie 被设置为 httpOnly 后,将不会在浏览器中通过 `document.cookie` 的方式查到,所以即使浏览器端的 js 执行权限被人拿到,cookie 也不会丢失 52 | 53 | 某个 cookie 被设置为 secure 后,发送的 `http://domain.com` 请求将不会包含这个 cookie,从而避免了 cookie 通过非 https 的方式被泄漏 54 | 55 | #### http 请求跳转到 https 56 | 57 | 堵死非安全的通信方式 58 | 59 | #### 鉴权、数据验证 60 | 61 | 避免有人绕过浏览器端的鉴权、数据验证,直接构造请求,导致的权限泄漏、非法操作 62 | 63 | #### 密码 bcrypt 存储 64 | 65 | 机制上保证,只能检查密码是否正确,不能还原出原始密码,避免数据库泄漏后,用户的密码被泄漏,避免使用了相同密码的其它网站的密码泄漏 66 | 67 | #### 强制用户使用长密码,不强制使用大小写、特殊符号,不强制定期修改密码 68 | 69 | 太复杂的密码容易变成一次性密码,用户会频繁找回,鼓励用户使用个性化的多个单词、拼音的组合密码 70 | 71 | #### 用户登出或修改密码后,要保证此用户所有的 session 和 token 都失效 72 | 73 | 避免用户密码泄漏后,已修改密码,或在公共机器上下线,但不安全的 session 或 token 仍然有效导致的安全问题 74 | 75 | #### 有副作用的接口不能开放 GET 请求 76 | 77 | 避免 CSRF 攻击 78 | 79 | #### 为接口的访问增加频率限制 80 | 81 | 避免机器人耗用太多资源 82 | 83 | #### 使用参数化查询 84 | 85 | 避免 SQL 注入 86 | 87 | #### 谨慎开启 CORS,设置允许的域名、http method、header 88 | 89 | CORS 突破了浏览器的同源策略,不加限制时,会让第三方网站也有发起跨域请求的能力 90 | -------------------------------------------------------------------------------- /contents/websocket-and-http-trade-off.md: -------------------------------------------------------------------------------- 1 | # websocket 和 http 的权衡 2 | 3 | websocket 的特色是主动推送以及广播效果,效果比 http 轮询要好。 4 | 5 | 传输格式上,http 目前只支持文本,还要携带 header 和 cookie,而 websocket 可以支持传输二进制数据,可以利用 protobuf 等压缩 size,所以传输的量 websocket 明显小于 http。 6 | 7 | 传输大量内容时,例如文件上传,如果使用 websocket,会阻塞其它数据的传输,这种情况使用 http 要更好一些。使用 http 获取数据时,可以利用浏览器的缓存机制,避免重复下载。 8 | 9 | websocket 的浏览器支持程度是,>=IE10,不过可以使用 socket.io 等库,来支持低版本的浏览器。 10 | 11 | 一个小特点是,websocket 是不受同源策略限制的。 12 | 13 | 模型上 http 是请求-响应模型,而 websocket 是基于事件的的模型,发出消息后,不一定会有对应的响应,也可能有多个响应。 14 | 实际使用时,http 类似于常规的接口同步调用,服务端无状态,客户端有状态,收到响应时有请求时的 context,知道是哪个请求的结果,而 websocket 不是这样,服务端有状态,有连接时就产生的 context,客户端没有局部状态,发送消息时的 context 可以保存在消息中,也可以保存在全局的 hash 结构中,在各个事件中,从消息体中,或者全局的 hash 结构中,恢复出消息发送时的 context,执行相应的操作。 15 | 这样消息的发出和接收不再一一对应,也就完全解藕出来。 16 | 对于具体的命令,可以默认它成功,如果出现错误时,推送这个错误通知,并还原消耗的资源。 17 | 对于查询,可以做成连接时主动返回的形式,在变更时,主动通知变更后的结果,使开发模型更简单。 18 | 另外某些情况下,可以把客户端的定时转移到服务端,定时查询并组合数据后,统一推送,从而使系统负载更低。 19 | 20 | 设计 http 接口时,需要从资源的增删改查角度设计,服务端驱动。 21 | 设计 websocket 接口时,需要从客户端需要的事件角度设计,客户端驱动。 22 | 23 | 最后给出一个例子,某个用户用自己的积分,换取某个物品: 24 | 如果使用 http,客户端在请求时,扣除积分,并携带 token,成功后加入仓库,失败后恢复积分,并提示错误原因。 25 | 如果使用 websocket,客户端在发送消息时,扣除积分,(不需要携带 token,服务端也不需要验证,因为服务端是有状态的,连接时已经验证过了),成功后在仓库更新事件收到消息,把物品加入仓库,失败后在积分更新事件收到消息,更新积分,在消息提示事件收到消息,提示错误消息。 26 | 27 | 结论,http 和 websocket 有各自的用武之地,完全不同的编程模型,结合着使用,可以发挥完美的效果。 28 | 29 | 另外,通过 websocket 连接,可以使用传统的 json-rpc,宏观上类似于方法调用,但是实现时,如果不借助第三方库,客户端需要生成不重复的 id,并在收到消息时,匹配消息的 id 和已发出的消息的 id,还要管理已发出消息的请求的生命周期,会更复杂一些。 -------------------------------------------------------------------------------- /contents/websocket-protocol-and-debug.md: -------------------------------------------------------------------------------- 1 | # websocket 协议及其调试 2 | 3 | 对于 websocket,handshake 阶段基于 http 协议,成功后可以异步传输 frame 数据。 4 | 5 | frame 数据传输阶段基于 tcp 协议,实现了事件驱动,消息作为一个整体,而接收时则不必关心 frame 的大小和组装问题。 6 | 7 | websocket 需要额外的 ping/pong 机制(类似于心跳)来保证长连接,socket.io 内部默认会每隔 25 秒 ping 一次服务端,默认超时 60 秒,超时就可以认为连接断开。 8 | 9 | 不同于 http 协议,postman 等调试工具没办法调试 websocket。 10 | 11 | websocket 调试工具可以是 chrome 的 developer tool,它可以查看 handshake 阶段的请求,以及实时的 frame 数据。 12 | 13 | 如果需要从浏览器端向服务端发送数据,可以在 chrome 的 console 中发送,frame 中的发出和接收到的数据会以不同的颜色加以区分。 14 | 15 | 其它在线的调试工具有:http://www.websocket.org/echo.html 。 16 | 17 | 使用 socket.io 作为服务端时,可以观察到传输的数据是如下格式: 18 | 19 | ``` 20 | 5 21 | 42["test",{"key":"value"}] 22 | 2 23 | 3 24 | ``` 25 | 26 | 由于 socket.io 在 1.0 之后,是基于 engine.io 来实现的,所以这些数据格式是由 engine.io 的协议来确定的,链接在:https://github.com/socketio/engine.io-protocol 。 27 | 28 | 简单来说,5 表示升级协议,2 标识 ping,3 标识接收到的 pong,4 表示消息,`test` 是事件名称,`{"key":"value"}` 是 JSON 字符串形式的数据。 29 | 30 | 可以看到,要传输同样的数据,websocket 传输的数据量要远远小于 http 协议,这也是 websocket 的优势之一。 31 | -------------------------------------------------------------------------------- /contents/ws-and-socket-io-stability-of-transfer-data.md: -------------------------------------------------------------------------------- 1 | # ws 和 socket.io 传输数据的可靠性 2 | 3 | ### ws 4 | 5 | 对于 ws,发送数据结束时,会调用回调函数,如果存在 error,可以按照策略(次数、时间间隔)重传。 6 | 7 | ### socket.io 8 | 9 | 对于 socket.io,因为没有回调函数,所以是不知道消息的发送结果的,也不能保证传输的可靠。 10 | 11 | 这时候,可以悲观认为消息的传输很可能失败,发送前就保存起来,当客户端收到消息后,回复一个确认消息,服务端就可以确认消息传输成功了。 12 | 13 | 可以按照策略(次数、时间间隔),定时获取没有结果的消息,排出几秒内的消息后,重传。 14 | 15 | 如果网络完全断开,没有结果的消息越积越多,需要根据需求来实现消息持久化、告警、消息恢复。 16 | 17 | ### web 前端 18 | 19 | 后端的发送失败的数据可以保存在数据库、redis 中,前端可以保存在 localstorage 中。 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | Powered By simple-doc 12 |
13 | 14 | 17 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process') 2 | const fs = require('fs') 3 | 4 | const files = fs.readdirSync('contents'); 5 | for (const file of files) { 6 | console.log(`${file}...`) 7 | childProcess.execSync(`pangu "contents/${file}" "contents/${file}"`) 8 | } 9 | -------------------------------------------------------------------------------- /service-worker.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";var precacheConfig=[["index.html","73c486e3f8906f8e14d6aea739ee94e4"],["vendor.bundle-bff321611bdd7ea907dfe701b4cc4aa6.js","bff321611bdd7ea907dfe701b4cc4aa6"]];var cacheName="sw-precache-v3-sw-precache-"+(self.registration?self.registration.scope:"");var ignoreUrlParametersMatching=[/^utm_/];var addDirectoryIndex=function(originalUrl,index){var url=new URL(originalUrl);if(url.pathname.slice(-1)==="/"){url.pathname+=index}return url.toString()};var cleanResponse=function(originalResponse){if(!originalResponse.redirected){return Promise.resolve(originalResponse)}var bodyPromise="body"in originalResponse?Promise.resolve(originalResponse.body):originalResponse.blob();return bodyPromise.then(function(body){return new Response(body,{headers:originalResponse.headers,status:originalResponse.status,statusText:originalResponse.statusText})})};var createCacheKey=function(originalUrl,paramName,paramValue,dontCacheBustUrlsMatching){var url=new URL(originalUrl);if(!dontCacheBustUrlsMatching||!url.pathname.match(dontCacheBustUrlsMatching)){url.search+=(url.search?"&":"")+encodeURIComponent(paramName)+"="+encodeURIComponent(paramValue)}return url.toString()};var isPathWhitelisted=function(whitelist,absoluteUrlString){if(whitelist.length===0){return true}var path=new URL(absoluteUrlString).pathname;return whitelist.some(function(whitelistedPathRegex){return path.match(whitelistedPathRegex)})};var stripIgnoredUrlParameters=function(originalUrl,ignoreUrlParametersMatching){var url=new URL(originalUrl);url.hash="";url.search=url.search.slice(1).split("&").map(function(kv){return kv.split("=")}).filter(function(kv){return ignoreUrlParametersMatching.every(function(ignoredRegex){return!ignoredRegex.test(kv[0])})}).map(function(kv){return kv.join("=")}).join("&");return url.toString()};var hashParamName="_sw-precache";var urlsToCacheKeys=new Map(precacheConfig.map(function(item){var relativeUrl=item[0];var hash=item[1];var absoluteUrl=new URL(relativeUrl,self.location);var cacheKey=createCacheKey(absoluteUrl,hashParamName,hash,false);return[absoluteUrl.toString(),cacheKey]}));function setOfCachedUrls(cache){return cache.keys().then(function(requests){return requests.map(function(request){return request.url})}).then(function(urls){return new Set(urls)})}self.addEventListener("install",function(event){event.waitUntil(caches.open(cacheName).then(function(cache){return setOfCachedUrls(cache).then(function(cachedUrls){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(cacheKey){if(!cachedUrls.has(cacheKey)){var request=new Request(cacheKey,{credentials:"same-origin"});return fetch(request).then(function(response){if(!response.ok){throw new Error("Request for "+cacheKey+" returned a "+"response with status "+response.status)}return cleanResponse(response).then(function(responseToCache){return cache.put(cacheKey,responseToCache)})})}}))})}).then(function(){return self.skipWaiting()}))});self.addEventListener("activate",function(event){var setOfExpectedUrls=new Set(urlsToCacheKeys.values());event.waitUntil(caches.open(cacheName).then(function(cache){return cache.keys().then(function(existingRequests){return Promise.all(existingRequests.map(function(existingRequest){if(!setOfExpectedUrls.has(existingRequest.url)){return cache.delete(existingRequest)}}))})}).then(function(){return self.clients.claim()}))});self.addEventListener("fetch",function(event){if(event.request.method==="GET"){var shouldRespond;var url=stripIgnoredUrlParameters(event.request.url,ignoreUrlParametersMatching);shouldRespond=urlsToCacheKeys.has(url);var directoryIndex="index.html";if(!shouldRespond&&directoryIndex){url=addDirectoryIndex(url,directoryIndex);shouldRespond=urlsToCacheKeys.has(url)}var navigateFallback="";if(!shouldRespond&&navigateFallback&&event.request.mode==="navigate"&&isPathWhitelisted([],event.request.url)){url=new URL(navigateFallback,self.location).toString();shouldRespond=urlsToCacheKeys.has(url)}if(shouldRespond){event.respondWith(caches.open(cacheName).then(function(cache){return cache.match(urlsToCacheKeys.get(url)).then(function(response){if(response){return response}throw Error("The cached response that was expected is missing.")})}).catch(function(e){console.warn('Couldn\'t serve response for "%s" from cache: %O',event.request.url,e);return fetch(event.request)}))}}}); --------------------------------------------------------------------------------