1. Presentation Stutdio

这个项目翻译过来就是演播厅,是给投资者提供一个自定义报告的desktop应用。投资者可以根据自己的需求,拖拉component到报告中,然后修改字体等自定义操作,然后生成自己的报告展示给客户。我们的工作就是提供组件给这个desktop应用使用。
我们在做这个产品的时候有个DNA lab产品,DNA lab这个产品就是data analysis lab,DNA lab产品是基于juptry notebook产品写的,juptry notebook类似于在线编译工具,不过juptry支持c++,支持python,我们将编写好的组件发布出去,然后通过juptry notebook这个在线编译工具再用python封装下,然后发布一个python文件到服务器,然后服务器通过nbconvert将这个python文件转换为html页面。然后再通过webview的方式讲我们的组件集成到desktop上。

因为是通过webview方式被加载到deskttop中去,所以需要实现和组通通讯,所以我们的实现方式是通过windoe.postMessage进行通讯,我将window.postMessage封装成了一个类,并且通过promise把window.postMessage封装成一个异步操作,允许等待返回之后再进一步操作,解决了组件和desktop之间的通讯问题。

然后我还封装了一个data service类,专门用来发送和取消请求的service,该service提供了发送请求和取消请求以及轮训请求,超时处理。

然后还开发了一个最复杂的base table组件,该组件支持根据容器大小自动计算行高和列宽,以及最合适的fontSize。当在默认的fontSize下,容器的宽高不够的时候,我们就需要通过减小fontSize的来找到一个合适的fontSize,能够使table能够被容器放下。

axios封装,比如设置baseUrl,withCredentials, 实现了无缝刷新token,首次获取token的时候,后端会返回一个access token和refresh token,在response阶段通过拦截response的状态,如果是401的话,就用refresh token获取新的access token和refresh token。然后重新发送请求。需要考虑的是并发请求,refresh token的处理,通过promise存储起来

后端redis做token缓存
image.png

2.Moringstar Web Component

这个项目是纯技术的一个项目,目的是想组件转成一个web component,使组件可以通过标签形式就可以被创建出来。借助的是web component里面的custom element的特性实现的。对于vue项目,使用了一个第三方组件库,vue-custom-element实现的。对于基于backbone实现的组件,我们自己实现了custom-element,自己实现的custom element相当于web应用和component之间的一个桥梁,将之间直接通讯的方式,改成在桥梁里面做个转发。实现了不需要重写组件代码,就能够很轻松的转化为web component,符合Morningstar web component的规范。

调用customElement.define(“”, class MyComponent extends HTMLELEMENT);
shadom可以将将js ,css和行为隐藏起来,与其他的代码做隔离,video标签就是一个shadow dom

3.Morningstar Base Grid

大数据渲染
大数据渲染一般采用的方案就是只渲染可视区域就可以了。其他的数据等滚动到对应的位置再渲染出来即可。
所以在设计这个组件的初期,我们是先把rows,columns层级结构打平,通过一个参数,比如tg_group_index,tg_sub_index,tg_level,tg_index,tg_group等参数来记录数据的层级关系。后面在增删改查的时候只需要操作把这些index改正确即可。
然后我们是支持固定行和固定列的,所以我们把当前页面分为6个部分,headerLeft,headerRight,bodyTopLeft,bodyTopRight,bodyBottomLeft,bodyBottomRight,这每一个部分都是一个scrollpan对象,每个scrollpane里面又分为scrollbarV,scrollbatH,scrollbody,每个scrollBody里面就是对应的每一个grid view,行列的渲染操作就是在每个grid view里面单独完成的,每个grid view只需要负责把自己这个view的数据渲染正确即可。
grid view之间的通讯,比如我拖动了bottomRight需要通知headerRight,topRight去更新位置,最开始的实现是通过trigger通知,后来发现通过trigger通知的话,会发生死循环,说来改成把想关联的view挂在到被拖动的view上,然后主动调用view.update方法去更新关联View的位置

该组件还支持用户自定义formatter,比如date formatter, string formatter,skeleton,进度条,cell loading等
还支持排序,当排序的内容相同时,可以按照第二种排序规则

4.HS-CLI

我们为什么自己做了一套cli,因为我们公司使用vue也是最近一两年的事情,之前一直使用的是cmd的命令模式的cli,所以我们想把cli再升级一下,升级成有图形界面的,便于开发工作的。于是我们把组内内部的cli改成了一个有图形界面的cli。
通过sokiet.io技术实现前后端通讯,前端界面点击了一个按钮之后,把需要执行的命令通过sokiet.io的postMessage通知到本地服务器,服务器接受到data之后,根据不同的type处理不同的操作。

socket.io是对websocket的封装,兼容各个浏览器,还提供了更丰富的功能,比如心跳检查
websocket是一种协议,也是会基于Http发握手连接,连接后会告诉服务器端是websocket协议,服务端接受到消息之后就讲协议升级成websocket,然后实现双向通讯

粘包和拆包:发送的数据大于套接字的缓冲区,就会出现拆包
发送的数据小于套接字缓冲区,tcp就会把多个包放在一个套接字传输,就放声了粘包

解决办法就是给每个包添加一个包含length的header,或者在数据包之间设置特殊字符用来分包

socket.io是先发一个polling的请求,如果发现客户端服务器端都支持websocket,就升级为websoket

通过node multiple worker实现多进程执行命令。类似于webpack的多进程打包的方式

5.大文件上传

大文件上传需要后端服务器配合
前端的会首先会对大文件进行分片处理,分片处理可以根据size或者page分片,我们采用的是size分片。
分片之后需要给每个分片添加一个命名,我们采用的方式是通过spark-md5根据文件内容生成一个hash值,然后拼接上文件名和分片的index
然后通过分片数量进行并发请求,我们在这里做了request poll的功能,就是只允许同时最多发送5个请求的功能。
直到所有的分片都传到后端,后端就进行分片合并的功能。

在这中间,用户可以点击暂定,或者取消上次的操作
点击暂定的时候,我们就需要回复progress为真实的值
然后再下次恢复请求之前会向后台发个请求,获取已经上传的列表,如果发现全部都上传过了,就实现了秒传
如果是取消上传的话,就会取消正在发送的request

分片传输的数据结构是formdata

控制并发数量

  1. const wrapperList = list.map(request=>{
  2. return new Promise((resolve,reject)=>{
  3. return request().then(resolve, reject);
  4. });
  5. });
  6. const run = function() {
  7. if (index === wrapperAsyncList.length) {
  8. return Promise.resolve();
  9. }
  10. const item = wrapperAsyncList[index++];
  11. const promise = Promise.resolve().then(()=>{
  12. item();
  13. });
  14. res.push(promise);
  15. const e = promise.then(() => {
  16. pool.splice(pool.indexOf(e), 1);
  17. });
  18. pool.push(e);
  19. let r = Promise.resolve();
  20. if (pool.length >= limit) {
  21. r = Promise.race(pool);
  22. }
  23. return r.then(() => run());
  24. };
  25. return run().then(() => {
  26. return Promise.all(res);
  27. });

6.webpack4升级到5,以及babel的升级

webpack4到5:
loader和plugin的改变
5默认开启cache,所以像cache-loader,cacheDirectory
target升级,默认值为[“web”,”es5”]
资源模块(asset module),raw-loader,file-loader,url-loader通过asset module替换

babel升级: babel-core只包含转换成ast,然后生成vistors,最后通过generate生成js代码。中间的插件就是通过vistors实现的。实现原理观察者模式
babel/preset-env:预设,相当于es6语法的一些plugin合集,不包含api
如果想使用api的话,需要添加polly,但是babel7要去掉bable-pollfy插件了,可以通过preset-env的buildIns设置,设置为usage按需加载,然后配合corejs

bable/runtime和babel/plugin-transform-runtime: 将插入的重复代码提取到一个地方

7.项目工程化

规范化

eslint

  1. "eslint": "^7.28.0",
  2. "eslint-plugin-chain": "^1.0.4",
  3. "eslint-plugin-es": "^4.1.0",
  4. "eslint-plugin-html": "^6.1.2",
  5. "eslint-plugin-sonarjs": "^0.8.0-125",
  6. "eslint-plugin-vue": "^7.11.1"
  7. module.exports = {
  8. "root": true,
  9. //system globals
  10. "env": {
  11. "node": true,
  12. "browser": true,
  13. "amd": true,
  14. "commonjs": true,
  15. "es6": true,
  16. "mocha": true
  17. },
  18. //other globals
  19. "globals": {
  20. "assert": true,
  21. "delay": true,
  22. "ready": true
  23. },
  24. //should "npm install eslint-plugin-es -g" for VSCode in global
  25. "plugins": [
  26. "sonarjs",
  27. "chain",
  28. "vue"
  29. ],
  30. "extends": [
  31. "plugin:sonarjs/recommended",
  32. "plugin:chain/recommended",
  33. "plugin:vue/recommended",
  34. "eslint:recommended"
  35. ],
  36. "parserOptions": {
  37. //set to 3, 5 (default), 6, 7, 8, 9, or 10 to specify the version of ECMAScript syntax you want to use.
  38. //2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), or 2019 (same as 10) to use the year-based naming.
  39. "ecmaVersion": 2018,
  40. "sourceType": "module"
  41. },
  42. //https://eslint.org/docs/4.0.0/rules/
  43. "rules": {
  44. "arrow-spacing": "error",
  45. "block-spacing": "error",
  46. "comma-dangle": ["error", "never"],
  47. "comma-spacing": ["error", {
  48. "after": true,
  49. "before": false
  50. }],
  51. "complexity": ["error", 8],
  52. "curly": "error",
  53. "dot-location": ["error", "property"],
  54. "dot-notation": "error",
  55. "eqeqeq": ["error", "always"],
  56. "func-call-spacing": ["error", "never"],
  57. "indent": ["error", 4, {
  58. "ArrayExpression": "first",
  59. "ObjectExpression": 1,
  60. "SwitchCase": 1
  61. }],
  62. "key-spacing": ["error", {
  63. "afterColon": true,
  64. "mode": "strict"
  65. }],
  66. "keyword-spacing": ["error", {
  67. "after": true,
  68. "before": true
  69. }],
  70. "line-comment-position": ["error", {
  71. "position": "above"
  72. }],
  73. "lines-around-comment": ["error", {
  74. "beforeBlockComment": true
  75. }],
  76. "lines-between-class-members": ["error", "always", {
  77. "exceptAfterSingleLine": true
  78. }],
  79. "max-depth": ["error", 4],
  80. "max-len": ["error", {
  81. "code": 550,
  82. "ignoreStrings": true,
  83. "ignoreTrailingComments": true
  84. }],
  85. "max-nested-callbacks": ["error", 3],
  86. "max-params": ["error", 8],
  87. "max-statements": ["error", 50],
  88. "new-cap": ["error", {
  89. "capIsNew": false,
  90. "newIsCap": true,
  91. "properties": true
  92. }],
  93. "no-alert": "error",
  94. "no-array-constructor": "error",
  95. "no-confusing-arrow": "error",
  96. "no-console": "off",
  97. "no-constant-condition": ["error", {
  98. "checkLoops": false
  99. }],
  100. "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
  101. "no-duplicate-imports": "error",
  102. "no-else-return": "error",
  103. "no-empty": "off",
  104. "no-eq-null": "error",
  105. "no-eval": "error",
  106. "no-floating-decimal": "error",
  107. "no-inline-comments": "error",
  108. "no-multi-assign": "error",
  109. "no-multi-spaces": "error",
  110. "no-multi-str": "error",
  111. "no-multiple-empty-lines": ["error", {
  112. "max": 2,
  113. "maxBOF": 1,
  114. "maxEOF": 1
  115. }],
  116. "no-nested-ternary": "warn",
  117. "no-new-object": "error",
  118. "no-param-reassign": "off",
  119. "no-prototype-builtins": "off",
  120. "no-restricted-globals": ["error", "event", "fdescribe"],
  121. "no-return-assign": "warn",
  122. "no-return-await": "warn",
  123. "no-sequences": "error",
  124. "no-trailing-spaces": ["error", {
  125. "ignoreComments": true,
  126. "skipBlankLines": true
  127. }],
  128. "no-unneeded-ternary": "error",
  129. "no-unused-vars": ["error", {
  130. "args": "none",
  131. "vars": "local"
  132. }],
  133. "no-useless-return": "error",
  134. "no-var": ["warn"],
  135. "no-whitespace-before-property": "error",
  136. "no-with": "error",
  137. "object-curly-newline": ["error", {
  138. "ExportDeclaration": {
  139. "minProperties": 3,
  140. "multiline": true
  141. },
  142. "ImportDeclaration": {
  143. "minProperties": 3,
  144. "multiline": true
  145. },
  146. "ObjectExpression": {
  147. "consistent": true,
  148. "minProperties": 1,
  149. "multiline": true
  150. },
  151. "ObjectPattern": {
  152. "minProperties": 3,
  153. "multiline": true
  154. }
  155. }],
  156. "object-curly-spacing": ["error", "always"],
  157. "object-property-newline": ["error", {
  158. "allowAllPropertiesOnSameLine": true
  159. }],
  160. "one-var": ["error", "never"],
  161. "operator-linebreak": ["error", "before"],
  162. "padding-line-between-statements": ["error", {
  163. "blankLine": "always",
  164. "next": "*",
  165. "prev": "directive"
  166. }, {
  167. "blankLine": "any",
  168. "next": "directive",
  169. "prev": "directive"
  170. }, {
  171. "blankLine": "always",
  172. "next": "function",
  173. "prev": "*"
  174. }, {
  175. "blankLine": "always",
  176. "next": "block",
  177. "prev": "*"
  178. }],
  179. "prefer-const": "error",
  180. "prefer-template": "error",
  181. "quotes": ["error", "double", {
  182. "avoidEscape": true
  183. }],
  184. "require-atomic-updates": "off",
  185. "require-await": "error",
  186. "rest-spread-spacing": ["error", "always"],
  187. "semi": ["error", "always"],
  188. "semi-spacing": ["error", {
  189. "after": true,
  190. "before": false
  191. }],
  192. "sonarjs/no-duplicate-string": ["error", 6],
  193. "space-before-blocks": ["error", "always"],
  194. "space-before-function-paren": ["error", {
  195. "anonymous": "never",
  196. "asyncArrow": "always",
  197. "named": "never"
  198. }],
  199. "space-in-parens": "error",
  200. "space-infix-ops": ["error", {
  201. "int32Hint": false
  202. }],
  203. "template-curly-spacing": "error",
  204. "unicode-bom": "error"
  205. }
  206. };

stylelint

  1. "stylelint": "^13.13.1",
  2. "stylelint-config-recommended": "^5.0.0",
  3. "stylelint-config-standard": "^22.0.0"
  4. module.exports = {
  5. //https://github.com/stylelint/stylelint-config-recommended
  6. //https://github.com/stylelint/stylelint-config-standard
  7. "extends": ["stylelint-config-recommended", "stylelint-config-standard"],
  8. "rules": {
  9. "indentation": 4,
  10. "at-rule-no-unknown": null,
  11. "font-family-no-missing-generic-family-keyword": null
  12. }
  13. };

.vscode

  1. //settings.json
  2. {
  3. "[javascript]": {
  4. "editor.defaultFormatter": "dbaeumer.vscode-eslint"
  5. },
  6. "[vue]": {
  7. "editor.defaultFormatter": "dbaeumer.vscode-eslint"
  8. },
  9. "css.validate": false,
  10. "editor.codeActionsOnSave": {
  11. "source.fixAll.eslint": true,
  12. "source.fixAll.stylelint": true
  13. },
  14. "editor.formatOnSave": true,
  15. "eslint.format.enable": true,
  16. "eslint.validate": [
  17. "vue",
  18. "html",
  19. "javascript"
  20. ],
  21. "less.validate": false,
  22. "scss.validate": false
  23. }
  24. //extensions.json
  25. {
  26. "recommendations": [
  27. "dbaeumer.vscode-eslint",
  28. "eamodio.gitlens",
  29. "gruntfuggly.todo-tree",
  30. "streetsidesoftware.code-spell-checker",
  31. "stylelint.vscode-stylelint",
  32. "zengxingxin.sort-js-object-keys"
  33. ]
  34. }

husky+commitizen + convertional-changelog

对commit message进行校验,自动生成changeLog文件
https://www.cnblogs.com/mengfangui/p/12634845.html

axios封装

  • 首先是通过axios.create创建一个axios的实例,这样做的目的是当前组件的axios不会影响其他组件的axios,自身也不会被其他外部的全局axios影响
  • baseUrl的处理是放在api层处理的,没有通过环境变量设置baseUrl,因为这个接口可以接到不同域名情况
  • 添加了断网处理,以及根据后台返回的status状态码,前端做一些提示消息。
  • 在实例的intance拦截request请求,给给个请求带上token
  • 在入口处将api挂载在Vue.prototype上,这样每个文件都不用再引入api的文件了
  • 在api层做了缓存,以及业务数据的转换。比如url是name = {name},params:{name:”bella”},会做个url的替换。然后每个request都有一个对应的fetchId,这个fetchId可以自定义,也可以自己生成。在get或者post的时候会返回一个promise,或者xth,如果返回的时promise就把xth.abort方法挂载到promise上去。在组件里面就可以通过调用abort方法取消请求。也通过通过传入fetchId去调用abort请求

高级处理技巧:接口环境切换,断网处理

  • 通过环境变量设置baseUrl
  • 设置超时时间和跨域是否允许携带凭证
    1. axios.default.timeout = 60 * 10000;
    2. axios.default.withCredentials = true;
    axios请求拦截器用来做TOKEN校验:接受服务器返回的token,储存到本地,在下次请求的时候带上token

断网页面

  1. //服务器返回的error请求
  2. if(error.response){
  3. switch(response.status){
  4. case 401: //权限
  5. break;
  6. case 403: //服务器已经进入请求,但是拒绝 了.(token过期)
  7. break;
  8. case 404: //找不到地址,当前网络有问题
  9. break;
  10. }
  11. } else if{!window.navigator.onLine}{
  12. //断网处理:可以跳转到断网页面
  13. }

Vue国际化支持

借助的是i18next的库实现的。首先我们的组件有用vue写的,有用banckbone写的,其他的组有用anglaur写的,所以这里我们并没有用vue-i18去做这个事情。而且是自己封装了一套国际化的支持。
当时支持国际化是遵循了两步,
第一步是封装跟使用的技术无关的功能,这一步就是借助i18next提供的一些api,当然这里还有其他的功能,注册全局的logger,替换console.log,还有提供全部的formatter(Intl)的功能

第二步就是跟技术相关的api,当时关于vue平台的国际化支持,我们有考虑过是用mixin的形式还是插件的形式。经过研究,我们组件是.vue的单文件形式,所以用Mixin比用插件形式更合理。

本地绕过一些cookie限制

我们在做Import项目的时候,采用的是storybook来展示界面,sotrybook对静态组件比较友好,但是对于业务逻辑负责的,需要实时跟后台通讯的组件,就需要在本地实现登录功能。然后我就在responseProxy中把secure设置成空,把domain改成morningstar.com,因为后端没有设置HttpOnly,前端是可以cookies,然后把cookies的一些参数改下,前端请求可以实现携带cookie,然后也可以通过cookie验证身份。
之前的开发模式就是前端先登录到产品上去,然后在启动本地开发,才能携带cookies,有了这个改进之后,前端开发不需要先登录产品,然后再在本地做开发。

采用websockt技术,实现cli的图形界面

我们也是有内部实现的cli的,只是在cmd这样的控制台去操作。后面我们想把这个升级成一个傻瓜式的GUI界面。
所以在原有的基础上采用socket.io实现前后端双向通讯,服务器端实时把运行的结果推送到浏览器端,然后显示出来。浏览器端需要执行的操作发送到服务器端

HS-CLI

  1. 使用socket.io进行前后端通讯
  2. 使用node多进程充分利用CPU资源

    1. 先开启一个主进程,然后通过判断cpu核数和需要执行的job的长度,决定创建几个子进程
    2. 然后每个子进程接收到start消息后,就告诉主进程自己已经上线了,可以准备工作了
    3. 然后主进程在分配job到该子进程,然后子进程接受到消息之后,开始执行脚本
    4. 执行结束之后通知主进程,如果还有job没有完成就继续给子进程分配job,如果jobs执行完毕了
    5. 主进程就kill掉子进程 `` master.js console.log('###---start---###') var cluster = require('cluster') const numCpus = 3 cluster.setupMaster({ exec: 'worker.js', slient: true }); if (cluster.isMaster) { console.log("主进程:",process.pid); var st = Date.now() for (let i = 0; i < numCpus; i++) { var wk = cluster.fork() wk.send("from 父进程") } cluster.on('fork',worker=>{ console.log(fork 子进程 ${worker.id} ${worker.process.pid}) }) cluster.on('exit',(worker,code,signal)=>{ console.log(子进程 ${worker.id} ${worker.process.pid} died`) })

      var numCount = 0 Object.keys(cluster.workers).forEach(id=>{ cluster.workers[id].on(‘message’,msg=>{

      1. console.log(`主进程 receive message from [worker ${id}]:${msg}`)
      2. numCount++
      3. if(numCount == numCpus){
      4. console.log(`master finish all work adn using ${Date.now() - st} ms`)
      5. cluster.disconnect()
      6. }

      }) }) }

worker.js var cluster = require(‘cluster’) function fibo(n) { return n == 0 ? 0 : n > 1 ? fibo(n - 1) + fibo(n - 2) : 1 } console.log(子进程 ${cluster.worker.id} ${cluster.worker.process.pid}) process.on(‘message’,msg=>{ console.log(${cluster.worker.id} receive data ${msg}) var st = Date.now()

  1. console.log(`worker ${cluster.worker.id} start to work`)
  2. var result = fibo(msg)
  3. console.log(`worker ${cluster.worker.id} finish work and using ${Date.now() - st} ms`)
  4. setTimeout(() => {
  5. process.send(result)
  6. }, 1000);

}) ```

  1. 使用mocha的browser方式,利用puppeteer-chromie-resolver实现本地Unit test框架
  2. preview一个组件的时候需要重新开启一个server,如果端口固定写死的话,就会报端口被占用,我采用的方法是先用net创建当前端口,看是否被占用,如果被占用就自动加1