Tuesday, November 16, 2021

SegmentFault 最新的文章

SegmentFault 最新的文章


源码泄露“危机“?阿里云、字节等大厂服务器疑似被海外黑客组织入侵,源代码被出售

Posted: 15 Nov 2021 08:06 PM PST

11 月 11日,海外黑客组织 "AgainstTheWest" (简称ATW)在 raidforums 论坛放猛料,宣称他们窃取了中国服务器的源码,并疯狂将其泄露出来打包售卖。据悉,其攻击目标疑似已涉及到国内阿里、腾讯及字节等科技"大厂"。

该消息传入国内后,一时间引发科技圈"震动"。尽管尚不得知此次泄露数据和源代码事件的真实性,但关于该事件的讨论已经开始发酵。

11月12日,ATW 继续在 raidforums 论坛发文宣称他们入侵了阿里云的服务器并窃取了大量源码,窃取的源码以 5000 美元的价格打包售卖,支付方式为比特币或门罗币。

该 ATW 人士在论坛的发文中提到,他们泄漏了一些用于腾讯开发和测试平台的 SRC,并附上了当天泄漏的 SRC:

他们还扬言称,到目前为止已经成功地对中国许多公司实施了供应链攻击。同时表示,现在中国还没有对此事置评,未来还将继续泄漏更多 SRC。

据了解,该 raidforums 论坛发帖公布资料的方式分为买卖制和积分查看制。前者,卖家报价寻找心动买家,成功交易之前买家无法预览资料内容;后者是用户可通过花 8 点积分(人民币 29.234 元)来查看帖子附带的资料。

据相关报道,目前该 ATW 组织正在出售的疑似阿里云部分源代码如下图:






对于此次入侵事件,知名云安全服务商 Lace Work 的 云安全研究员也在其推特账号上发文对此事表示关注。推文中,Lace Work 分别附上了此次 ATW 组织关于入侵阿里云和腾讯的声明截图:


有报道显示,或许是因为该事件泄露资料的真伪暂时无法查明,因此当前 ATW 挂在买卖制板块的资料一份都没卖出去,一些买家想要 ATW 公布更多资料的细节信息,但未得到其回应。

而一些已经花了积分查看帖子中附带资料的用户则大呼 ATW 这是在骗钱,因为这些所谓"泄露文件"根本就不是阿里云的源代码!

据相关信息显示,截止11月15日, ATW 组织入侵的国内科技企业疑似已涉及CN 科技部、阿里云用户数据 、钉钉控制台数据、腾讯温江项目、微信用户消息样本、字节跳动少量源码、中天科技、方正电子、滴普科技、渤海保险有限公司源码。

关于该事件及所谓泄露源代码的真伪,国内大厂们暂时未予置评。对于该事件是否属实以及 ATW 组织的下一步动向,本站也会持续关注。入股您对此事有任何看法,欢迎在评论区家交流互动。

你不知道的 Node.js Util

Posted: 15 Nov 2021 06:56 PM PST

从类型判断说起

在 JavaScript 中,进行变量的类型校验是一个非常令人头疼的事,如果只是简单的使用 typeof 会到各种各样的问题。

举几个简单的🌰:

console.log(typeof null) // 'object' console.log(typeof new Array) // 'object' console.log(typeof new String) // 'object'

后来,大家发现可以使用 Object.prototype.toString() 方法来进行变量类型的判断。

const getTypeString = obj => Object.prototype.toString.call(obj)  getTypeString(null) // '[object Null]' getTypeString('string') //'[object String]' getTypeString(new String) //'[object String]'

toString() 方法进行代理,可以得到一个类型字符串,我们就可以在这个字符串上面搞事情。

const getTypeString = obj => {   return Object.prototype.toString.call(obj) } const isType = type => {   return obj => {     return getTypeString(obj) === `[object ${type}]`   } }  const isArray = isType('Array') // 该方法一般通过 Array.isArray 代替  const isNull = isType('Null') const isObject = isType('Object') const isRegExp = isType('RegExp') const isFunction = isType('Function') const isAsyncFunction = isType('AsyncFunction')
isNull(null) // true isObject({}) // true isRegExp(/\w/) // true isFunction(() => {}) // true isAsyncFunction(async () => {}) // true

But,在 Node.js 中,内部其实是有一组用来判断变量类型的 api 的。而且功能异常丰富,除了基础类型的判断,还支持判断 Promise 对象、Date 对象、各种ArrayBuffer。

const types = require('util/types')  types.isDate(new Date) // true types.isPromise(new Promise(() => {})) // true types.isArrayBuffer(new ArrayBuffer(16)) // true

严格相等

在 JavaScript 中,对象、数组等变量在判断相等的过程中,如果用 === 通常只会判断这两个变量是否指向同一内存地址。如果想判断对象的键对应的所有值是否相等,需要对两个对象进行遍历。在 util 中,也提供了一个方法可以用来判断两个对象是否严格相等:util.isDeepStrictEqual(val1, val2)

const util = require('util')  const val1 = { name: 'shenfq' } const val2 = { name: 'shenfq' }  console.log('val1 === val2', val1 === val2) // false console.log('isDeepStrictEqual', util.isDeepStrictEqual(val1, val2)) // true

该方法同样可以用来判断数组,是否严格相等:

const util = require('util')  const arr1 = [1, 3, 5] const arr2 = [1, 3, 5]  console.log('arr1 === arr2', arr1 === arr2) // false console.log('isDeepStrictEqual', util.isDeepStrictEqual(arr1, arr2)) // true

Error First & Promise

早期的 Node API 都是 Error First 风格的,也就是所有的异步函数都会接受一个回调函数,该回调的一个参数为 error 对象,如果正常返回 error 对象为 null,后面的参数为成功响应的结果。

// 下面是一个读取文件的示例 const fs = require('fs') fs.readFile('nginx.log', (error, data) => {   if (error) {     // 读取文件失败     console.error(error)     return   }   // 读取文件成功,打印结果   console.log(data) })

在 Node 8 发布的时候,新增了一个 promisify 接口,用于将 Error First 风格的 API 转为 Promise API。

const fs = require('fs') const util = require('util')  const readFile = util.promisify(fs.readFile) readFile('./2021-11-11.log', { encoding: 'utf-8' })   .then(text => console.log(text))      .catch(error => console.error(error))

不过,后来也有很多人觉得这些原生 API 支持 Promise 的方式太过繁琐,每个 API 都需要单独的包装一层 promisify 方法。在 Node 10 发布的时候,原生模块都新增了一个 .promises 属性,该属性下的所有 API 都 Promise 风格的。

const fs = require('fs').promises fs.readFile('./2021-11-11.log', { encoding: 'utf-8' })   .then(text => console.log(text))      .catch(error => console.error(error))

注意:Node 14 后,promises API 又新增了一种引入方式,通过修改包名的方式引入。

const fs = require('fs/promises') fs.readFile('./2021-11-11.log', { encoding: 'utf-8' })   .then(text => console.log(text))      .catch(error => console.error(error))

除了将 Error First 风格的 API 转为 Promise API,util 中还提供 callbackify 方法,用于将 async 函数转换为 Error First 风格的函数。

下面通过 callbackify 将 promise 化的 fs 还原为 Error First 风格的函数。

const fs = require('fs/promises') const util = require('util')  const readFile = util.callbackify(fs.readFile) readFile('./2021-11-12.log', { encoding: 'utf-8' }, (error, text) => {   if (error) {     console.error(error)     return   }   console.log(text) })

调试与输出

如果有开发过 Node 服务,应该都用过 debug 模块,通过该模块可以在控制台看到更加明晰的调试信息。

const debug = require('debug') const log = debug('app')  const user = { name: 'shenfq' }  log('当前用户: %o', user)

其实,通过 util.debug 也能实现类似的效果:

const debug = require('debug') const log = debug('app')  const user = { name: 'shenfq' }  log('当前用户: %o', user)

只是在启动时,需要将 DEBUG 环境变量替换为 NODE_DEBUG

如果你有认真看上面的代码,应该会发现,在 log('当前用户: %o', user) 方法前面的字符串中,有一个 %o 占位符,表示这个地方将会填充一个对象(object)。这与 C 语言或 python 中的,printf 类似。同样,在 util 模块中,直接提供了格式化的方法:util.format

const { format } = require('util')  console.log(   format('当前用户: %o', {     name: 'shenfq', age: 25   }) )

除了 %o 占位符,不同的数据类型应使用不同的占位符。

占位符类型
%s字符串
%d数字(包括整数和浮点数)
%i整数
%f浮点数
%jJSON
%oObject

JavaScript 中的对象是一个很复杂的东西,除了直接使用 util.format 外加 %o 占位符的方式格式化对象,util 中还提供了一个叫做 inspect 方法来进行对象格式化。

const { inspect } = require('util')  const user = {   age: 25,   name: 'shenfq',   work: {     name: 'coding',     seniority: 5   } }  console.log(inspect(user))

这么看 inspect 好像什么都没做,但是 inspect 方法还有第二个参数,用来进行格式化时的一些个性化配置。

  • depth: number:控制显示层级;
  • sorted: boolean|Function: 是否按照key的编码值进行排序;
  • compact: boolean:是否进行单行显示;

当然上面只是一部分配置,更详细的配置可查阅 node 文档,下面我们写几个案例:

所有的属性都换行显示:

inspect(user, {     compact: false })

只格式化对象第一层的值:

inspect(user, {   depth: 0,     compact: false })

按照key值的编码倒序输出:

inspect(user, {     compact: false,   sorted: (a, b) => a < b ? 1 : -1 })

浅析Web components的痛点

Posted: 10 Nov 2021 09:31 PM PST

写在前面

最近致力于研究 Web components(以下简称WC),并且也初有成效的拿到了一定的结果,但今天想回过头来重新审视一下 WC。

WC 到底是什么?

援引MDN上的解释:

Web Components consists of several separate technologies. You can think of Web Components as reusable user interface widgets that are created using open Web technology. They are part of the browser and so they do not need external libraries like jQuery or Dojo. An existing Web Component can be used without writing code, simply by adding an import statement to an HTML page. Web Components use new or still-developing standard browser capabilities.

简单的讲,Web Component 就是把组件封装成 html 标签的形式,并且在使用时不需要写额外的 js 代码。

组件是前端的发展方向,抛开周边技术生态,单纯看 React 和 Vue 都是组件框架。因此,WC 可以视为原生标签的拓展/延伸,说到底,它依旧是一个标签!

类似 <video></video> 标签,相比于原生标签,它多了更为丰富的样式和可操作属性。
image.png

谷歌公司由于掌握了 Chrome 浏览器,一直在推动浏览器的原生组件,即 Web Components API。相比第三方框架,原生组件简单直接,符合直觉,不用加载任何外部模块,代码量小。貌似一切完美,似乎大有可以用来替换React、Vue之类的趋势。

很多人也提出过将它与三大前端框架比较,比如《Web Component 和类 React、Angular、Vue 组件化技术谁会成为未来?》,其实它们是两个领域的东西,不具有可比性。WC 最大的优势在于 CSS 防污染,浏览器原生支持的组件化实现;而 Vue 等 MVVM 框架注重数据分离和自动化绑定的实现。且 WC 中包含的 4 大 web api 是标准规范,使用上滞后性(比如新落地的规范往往要等几年后才会被人拿出来使用),但 vue、react、ng 等框架不会。

目前存在的缺陷

与其它 web 框架一起使用存在一些小问题,会给开发体验上造成一些困扰。

1、组件内部事件的回调

比如,一个弹窗组件(<my-dialog></my-dialog>)中的确定按钮,那么它的事件是如何触发的呢?

class myDialog extends HTMLElement {   // ...   connectedCallback() {     const shadowRoot = this.attachShadow({ mode: 'open' });     shadowRoot.innerHTML = `       <div class="dialog">         <div class="dialog-content">           <div class="dialog-body">             弹窗内容           </div>            <button id="okBtn">确定</button>         </div>       </div>     `;      shadowRoot.querySelector('#okBtn').addEventListener('click', () => {       // 组件内部定义事件       this.dispatchEvent(new CustomEvent('okBtnFn'));     });   } }  customElements.define('my-dialog', myDialog); 

现在方案是 custom element 内部自定义事件 new CustomEvent(),外部用 addEventListener监听。这样的写法是很丑陋的,仿佛又回到了原生 JS 写应用的时代。

<my-dialog></my-dialog>  <script>   export default {     created() {       document.addEventListener('okBtnFn', function(){         // 点击弹窗按钮,触发回调事件       });     }   } </script>

2、组件样式覆盖

对于开发者来说,难免会遇到需要调整组件内部样式的时候。无论你是使用antdvant还是使用其它组件库,但 WC 的 CSS 防污染机制导致你很难修改内部样式。这需要你付出一些代价来变相的修改内部样式,具体方式我在上一篇文章中有写《Web Components中引入外部CSS的 8 种方法》,其实是很繁琐,且不符合开发者直觉的。

3、组件内部资源相对路径问题

就目前来说,任何直接基于 Custom Element v1, Template 和 HTML Import 的组件都无法做到完全资源独立 —— 在不知道使用方环境且不给使用方增加额外限制的情况下使用内部封装的任何资源文件。比如如果你有一个自定义 icon 组件:

class MyIcon extends HTMLElement {     static get observedAttributes() { return ['name','size','color'] }     constructor() {         super();         const shadowRoot = this.attachShadow({ mode: 'open' });         shadowRoot.innerHTML = `             <svg class="icon" id="icon" aria-hidden="true" viewBox="0 0 1024 1024">                 <use id="use"></use>             </svg>         `     }      attributeChangedCallback (name, oldValue, newValue) {         if( name == 'name' && this.shadowRoot){             // 如果使用的项目中,根目录没有 icon.svg 文件,那就 gg             this.use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `./icon.svg#icon-${newValue}`);         }     } }  customElements.define('my-icon', MyIcon);

如果使用的项目中,根目录没有 icon.svg 文件,那就 gg。如果你在这里使用 cdn 路径,就会出现跨域问题。

4、form表单类组件 value 获取问题

Shadow DOM 中包含有 <input>、<textarea> 或 <select> 等标签的 value 不会在 form 表单中自动关联。

示例代码:

// web component class InputAge extends HTMLElement {   constructor() {     super();   }      // connect component   connectedCallback() {     const shadow = this.attachShadow({ mode: 'closed' });     shadow.innerHTML = `<input type="number" placeholder="age" min="18" max="120" />`;   } }  // register component customElements.define( 'input-age', InputAge );

WC 组件被使用后

<form id="myform">   <input type="text" name="your-name" placeholder="name" />   <input-age name="your-age"></input-age>    <button>submit</button> </form>  <script>  const form = document.getElementById('myform');    form.addEventListener('submit', e => {          e.preventDefault();     console.log('Submitted data:');      const data = new FormData(form);     for (let nv of data.entries()) {       console.log(`  ${ nv[0] }: ${ nv[1] }`);     }    }); </script>

提交的时候无法获取 input-agevalue。当然会有解决方案,但会很复杂。

点击查看

其它表单标签获取 value 解决方案参考:

image.png

点击查看

5、其它

此外,缺少数据绑定和状态管理也是 WC 存在的缺陷,此处不再赘述。

写在后面

  • WC 指在丰富 HTML 的 DOM 特性,让 HTML 拥有更强大的复用能力
  • WC 可以直接当做原生标签,在任何前端框架和无框架中运行
  • 结合当下的主流技术栈来说,WC 当前主要问题在于复杂的组件中,数据通信和事件传递存在一定使用成本
  • 兼容问题,比如可以覆盖内部样式的 :part 方法

以上。

处理可能超时的异步操作

Posted: 15 Nov 2021 03:30 PM PST

自从 ECMAScript 的 Promise ES2015 和 async/await ES2017 特性发布以后,异步在前端界已经成为特别常见的操作。异步代码和同步代码在处理问题顺序上会存在一些差别,编写异步代码需要拥有跟编写同步代码不同的"意识",为此我还专门写了一篇「异步编程需要"意识"」,不过看的人不多,可能确实"无趣"。

本文要聊的问题可能仍然"无趣",但很现实 —— 如果一段代码久久不能执行完成,会怎么样?

如果这是同步代码,我们会看到一种叫做"无响应"的现象,或者通俗地说 —— "死掉了";但是如果是一段异步代码呢?可能我们等不到结果,但别的代码仍在继续,就好像这件事情没有发生一般。

当然事情并不是真的没发生,只不过在不同的情况下会产生不同的现象。比如有加载动画的页面,看起来就是一直在加载;又比如应该进行数据更新的页面,看不到数据变化;再比如一个对话框,怎么也关不掉 …… 这些现象我们统称为 BUG。但也有一些时候,某个异步操作过程并没有"回显",它就默默地死在那里,没有人知道,待页面刷新之后,就连一点遗迹都不会留下。

当然,这不是小说,我们得聊点"正事"。

Axios 自带超时处理

使用 Axios 进行 Web Api 调用就是一种常见的异步操作过程。通常我们的代码会这样写:

try {     const res = await axios.get(url, options);     // TODO 正常进行后续业务 } catch(err) {     // TODO 进行容错处理,或者报错 }

这段代码一般情况下都执行良好,直到有一天用户抱怨说:怎么等了半天没反应?

然后开发者意识到,由于服务器压力增大,这个请求已经很难瞬时响应了。考虑到用户的感受,加了一个 loading 动画:

try {     showLoading();     const res = await axios.get(url, options);     // TODO 正常业务 } catch (err) {     // TODO 容错处理 } finally {     hideLoading(); }

然而有一天,有用户说:"我等了半个小时,居然一直在那转圈圈!"于是开发者意识到,由于某种原因,请求被卡死了,这种情况下应该重发请求,或者直接报告给用户 —— 嗯,得加个超时检查。

幸运的是 Axios 确实可以处理超时,只需要在 options 里添加一个 timeout: 3000 就能解决问题。如果超时,可以在 catch 块中检测并处理:

try {...} catch (err) {     if (err.isAxiosError && !err.response && err.request         && err.message.startsWith("timeout")) {         // 如果是 Axios 的 request 错误,并且消息是延时消息         // TODO 处理超时     } } finally {...}

Axios 没问题了,如果用 fetch() 呢?

处理 fetch() 超时

fetch() 自己不具备处理超时的能力,需要我们判断超时后通过 AbortController 来触发"取消"请求操作。

如果需要中断一个 fetch() 操作,只需从一个 AbortController 对象获取 signal,并将这个信号对象作为 fetch() 的选项传入。大概就是这样:

const ac = new AbortController(); const { signal } = ac; fetch(url, { signal }).then(res => {     // TODO 处理业务 });  // 1 秒后取消 fetch 操作 setTimeout(() => ac.abort(), 1000);

ac.abort() 会向 signal 发送信号,触发它的 abort 事件,并将其 .aborted 属性置为 truefetch() 内部处理会利用这些信息中止掉请求。

上面这个示例演示了如何实现 fetch() 操作的超时处理。如果使用 await 的形式来处理,需要把 setTimeout(...) 放在 fetch(...) 之前:

const ac = new AbortController(); const { signal } = ac; setTimeout(() => ac.abort(), 1000); const res = await fetch(url, { signal }).catch(() => undefined); 

为了避免使用 try ... catch ... 来处理请求失败,这里在 fetch() 后加了一个 .catch(...) 在忽略错误的情况。如果发生错误,res 会被赋值为 undefined。实际的业务处理可能需要更合理的 catch() 处理来让 res 包含可识别的错误信息。

本来到这里就可以结束了,但是对每一个 fetch() 调用都写这么长一段代码,会显得很繁琐,不如封装一下:

async function fetchWithTimeout(timeout, resoure, init = {}) {     const ac = new AbortController();     const signal = ac.signal;     setTimeout(() => ac.abort(), timeout);     return fetch(resoure, { ...init, signal }); }

没问题了吗?不,有问题。

如果我们在上述代码的 setTimeout(...) 里输出一条信息:

setTimeout(() => {     console.log("It's timeout");     ac.abort(); }, timeout);

并且在调用的给一个足够的时间:

fetchWithTimeout(5000, url).then(res => console.log("success"));

我们会看到输出 success,并在 5 秒后看到输出 It's timeout

对了,我们虽然为 fetch(...) 处理了超时,但是并没有在 fetch(...) 成功的情况下干掉 timer。作为一个思维缜密的程序员,怎么能够犯这样的错误呢?干掉他!

async function fetchWithTimeout(timeout, resoure, init = {}) {     const ac = new AbortController();     const signal = ac.signal;          const timer = setTimeout(() => {         console.log("It's timeout");         return ac.abort();     }, timeout);          try {         return await fetch(resoure, { ...init, signal });     } finally {         clearTimeout(timer);     } }

完美!但问题还没结束。

万物皆可超时

Axios 和 fetch 都提供了中断异步操作的途径,但对于一个不具备 abort 能力的普通 Promise 来说,该怎么办?

对于这样的 Promise,我只能说,让他去吧,随便他去干到天荒地老 —— 反正我也没办法阻止。但生活总得继续,我不能一直等啊!

这种情况下我们可以把 setTimeout() 封装成一个 Promise,然后使用 Promise.race() 来实现"过时不候":

race 是竞速的意思,所以 Promise.race() 的行为是不是很好理解?
function waitWithTimeout(promise, timeout, timeoutMessage = "timeout") {     let timer;     const timeoutPromise = new Promise((_, reject) => {         timer = setTimeout(() => reject(timeoutMessage), timeout);     });      return Promise.race([timeoutPromise, promise])         .finally(() => clearTimeout(timer));    // 别忘了清 timer }

可以写一个 Timeout 来模拟看看效果:

(async () => {     const business = new Promise(resolve => setTimeout(resolve, 1000 * 10));      try {         await waitWithTimeout(business, 1000);         console.log("[Success]");     } catch (err) {         console.log("[Error]", err);    // [Error] timeout     } })();

至于如何写可以中止的异步操作,下次再聊。

详解电子表格中的json数据:序列化与反序列化

Posted: 15 Nov 2021 06:50 PM PST

从XML到JSON

当下应用开发常见的B/S架构之下,我们会遇到很多需要进行前后端数据传输的场景。而在这个传输的过程中,数据通过何种格式传输、方式是否迅速便捷、书写方式是否简单易学,都成为了程序员在开发时要考量的问题。

在1996年,W3C(World Wide Web Consortium,万维网联盟)正式公布了XML1.0标准,

XML采用标准格式为基于Web的应用提供了一个统一进行数据描述和数据交换的标准,不同于HTML侧重于解决":如何将文件显示在浏览器中",XML更加侧重于解决:"如何将数据以结构化方式描述"。

(需要注意的是,XML并不是一种编程语言,而是一种跨语言的数据格式。)

XML本身并不复杂,但是加上W3C制定的DTD、XSD、XPath、XSLT等二十多个标准之后,这个简单的数据交换格式平白变得复杂了起来。程序员但凡遇到,只能头大。苦心孤诣研究大半个月,也不好轻言自己全部清楚了。

而此时,推动着技术前进的另一台蒸汽机也被点燃——Ajax技术开始流行,映衬出XML越来越不容忽视的缺点。XML得以实现是基于DOM树,而DOM在各种浏览器中的实现细节不尽相同,所以XML的跨浏览器兼容性并不好,这时需要一种新的数据负载格式集成到HTML页面中,以满足Ajax的要求。

终于,在XML诞生后的第八年——2002年,由Douglas Crockford开始使用JSON这种轻量级数据交换格式。

首条JSON信息发出后,最让人们惊讶的是,这并不是一个全新的数据格式,它就是JavaScript。

document.domain = 'fudco';parent.session.receive( { to: "session", do: "test", text: "Hello world" } )  

而由于这条数据内容本身就是JavaScript,因此不再需要做任何额外解析,使用JS编译器就可以解决一切。

由于JSON非常简单,很快就风靡Web世界,并且成为ECMA标准。几乎所有编程语言都有解析JSON的库,而在JavaScript中,我们可以直接使用JSON,因为JavaScript内置了JSON的解析。把JavaScript对象变成JSON,就是把这个对象序列化成一个JSON格式的字符串,这样才能够通过网络传递给其他计算机。如果我们收到一个JSON格式的字符串,只需要把它反序列化成一个JavaScript对象,就可以在JavaScript中直接使用这个对象了。

Json的序列化和反序列化

正如一道菜做好后,需要装在盘子里端给顾客,前后端的数据传输也是如此。数据通过指定格式,将传输的对象序列化为二进制数据流,然后再通过反序列化将数据流内容转化成为对应的数据对象。

JSON中的数据形式与转化方式

在JSON中,数据有以下几种形式:

  • 对象:一个没有顺序的"键/值",格式如

  • 数组:用以设置数值顺序,格式如

  • 字符串:任意数量的Unicode字符,格式如

进行数据序列化和反序列化的方式有以下三种:

  • 使用JavaScriptSerializer类
  • 使用DataContractJsonSerializer类
  • 使用JSON.NET类库

以JavaScriptSerializer类为例,

//创建用户列表 List<UserInfo> userList = new List<UserInfo>(); userList.Add(new UserInfo() { ID = 1, Name = "张三", CreateTime = DateTime.Now }); userList.Add(new UserInfo() { ID = 2, Name = "李四", CreateTime = DateTime.Now }); userList.Add(new UserInfo() { ID = 2, Name = "王五" });   //创建一个JavaScriptSerializer对象 JavaScriptSerializer serializer = new JavaScriptSerializer();   //将用户列表序列化成JSON string serializedResult = serializer.Serialize(userList);   //将JOSN反序列化成用户列表 List<UserInfo> deserializeResult = serializer.Deserialize<List<UserInfo>>(serializedResult); 

只需要调用对应方法,就可以直接实现对数据内容的序列化。

你以为到这里就结束了吗,当然没有。在实际应用中,数据本身的处理并没有什么难度,真正需要考虑解决的问题是,数据本身附加的属性、设置。就以我们自身为例,客户在纯前端电子表格中对JSON数据传输的真实需求是,这段数据需要保证所有可视化内容的完整传输。

纯前端表格中的JSON数据处理

在实际处理用户需求时,用户在设置好如下图单元格后,不仅仅是单元格内存在数字,还会遇到单元格本身的样式、自定义函数、 自定义格式、自定义函数迷你图、自定义标签,以及自定义行筛选。

我们打开相关的代码,可以清楚地看到在格式中这些对单元格的设置,都被保存了下来。

在这个图中,我们可以看到不同类型的数据内容都可以完成序列化和反序列化的过程。在使用自定义序列化的过程中,查看相关代码,处理序列化的核心是typeName 字段在调用toJSON函数的过程,比如,可以将此类姓名和window对象联系。而反序列化时,调用 getTypeFromString 函数来获取类型名并且构造类型实例对象,然后调用类型实例上的 fromJSON方法。

此外还有许多其他的属性内容,下面列举其他样式设置的例子:

背景图片:

//这个例子设置了backgroundImageLayout属性。 var style = new GC.Spread.Sheets.Style(); style.backColor = "lightgreen"; style.backgroundImage = "/css/images/quarter1.png"; style.backgroundImageLayout  = GC.Spread.Sheets.ImageLayout.center; activeSheet.setStyle(1,1,style,GC.Spread.Sheets.SheetArea.viewport); 

水印设置:

//此示例设置水印的单元格填充。 var type = new GC.Spread.Sheets.Style(); type.watermark = "User name"; type.cellPadding = "20"; type.labelOptions = {alignment:GC.Spread.Sheets.LabelAlignment.topLeft, visibility: GC.Spread.Sheets.LabelVisibility.visible}; activeSheet.setStyle(0, 1, type); activeSheet.getRange(0, -1, 1, -1, GC.Spread.Sheets.SheetArea.viewport).height(60); activeSheet.getRange(-1, 1, -1, 1).width(150); var combo = new GC.Spread.Sheets.CellTypes.ComboBox(); combo.items([{ text: "Oranges", value: "11k" }, { text: "Apples", value: "15k" }, { text: "Grape", value: "100k" }]); combo.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); activeSheet.setCellType(2, 1, combo, GC.Spread.Sheets.SheetArea.viewport); activeSheet.getCell(2, 1, GC.Spread.Sheets.SheetArea.viewport).watermark("ComboBox Cell Type").cellPadding('10 10 20 10'); activeSheet.getCell(2, 1, GC.Spread.Sheets.SheetArea.viewport).labelOptions({alignment: GC.Spread.Sheets.LabelAlignment.bottomCenter, foreColor: 'yellowgreen', font: 'bold 15px Arial'}); activeSheet.getRange(2, -1, 1, -1, GC.Spread.Sheets.SheetArea.viewport).height(60); 

主题字体:

//这个例子使用了themeFont属性。 var style = new GC.Spread.Sheets.Style(); style.formatter = "0.000%"; style.themeFont = "Body"; activeSheet.setStyle(1,1,style,GC.Spread.Sheets.SheetArea.viewport); activeSheet.getCell(1,1).value("11"); 

还有许多对于单元格的设置,这些样式内容都可以被完整保存下来,作为json数据进行传输,带来真正的表格json数据传输的便利。

使用过程中需要注意以下问题:

  • 给 typeName 字段设置完整的类型名字符串(如果有命名空间也应包含命名空间)。
  • 如果自定义类型有循环依赖或是你希望减小JSON 数据的大小,亦或是你有其他更高级的需求,那么你的自定义类型需要重写toJSON和fromJSON方法。
  • 如果自定义类型定义在一个闭包中,换句话说,你不希望将自定义类型定义在 window 对象上,你需要重写 getTypeFromString 函数来手动解析类型的字符串。

代码示例:

 GC.Spread.Sheets.getTypeFromString = function(typeString) {         switch (typeString) {             case "MyFormatter":                 return MyFormatter;             case "MyRowFilter":                 return MyRowFilter;             default:                 return oldFun.apply(this, arguments);         }     };  MyTag.prototype.toJSON = function() {     return {         typeName: this.typeName, //necessary         name: this.name,         age: this.age     }; }; MyTag.prototype.fromJSON = function(settings) {     if (settings.name !== undefined) {         this.name = settings.name;     }     if (settings.age !== undefined) {         this.age = settings.age;     } };  

总结

本文详细为大家介绍了数据传输从XML到JSON的故事,以及json进行序列化和反序列化的工作原理,同时带大家了解了在前端电子表格中要想完全实现整个内容的数据序列化和反序列化应该如何做。

后续也会为大家带来更多有趣或者严肃的内容~

觉得不错,点个赞再走吧。

扩展阅读

使用Docker Compose、Nginx、SSH和Github Actions实现前端自动化部署测试机

Posted: 08 Nov 2021 07:40 AM PST

开篇,我们先来看一下远古时代的构建部署流程。想必大家对这个都不陌生:

  • 开发将源码经过编译、压缩打包生成打包文件
  • 将打包生成的文件上传服务器

显然这个流程不仅繁琐,而且效率也不高,开发每次发布都要耗费很长的时间在部署构建上面。

后面为了解决这个问题,就出现了CI/CD

接下来我们来聊一下什么是CI/CD?

CI/CDContinuous Intergration/Continuous Deploy 的简称,翻译过来就是持续集成/持续部署CD 也会被解释为持续交付(Continuous Delivery

再具体一点就是:

  • 持续集成:当代码仓库代码发生变更,就会自动对代码进行测试和构建,反馈运行结果。
  • 持续交付:持续交付是在持续集成的基础上,可以将集成后的代码依次部署到测试环境、预发布环境、生产环境中

聊了这么多,相信很多同学一定会说:

  • 这一般不都是运维搞的吗?
  • 和业务也不相关啊,了解它有什么用?
  • 全是服务器相关的东西,dockernginx、云服务器啥的,我该怎么学习呢?

很早之前,我也是这么想的,感觉与自己的业务也没啥关系,没有太大的必要去了解。

但是最近我在搞一个全栈项目(做这个项目是为了突破自己的瓶颈)时,就遇到了这些问题,发现陷入了知识盲区。

没办法,只能一顿恶补。

但是当我通过学习这些知识和在项目中实践这些流程后,我在知识面上得到了很大的扩展。对操作系统,对实际的构建部署,甚至对工程化拥有了全新的认识。

这里也放下前面提到的全栈项目的架构图吧:

这个大的项目以low code为核心,囊括了编辑器前端编辑器后端C端H5组件库组件平台后台管理系统前端后台管理系统后台统计服务自研CLI九大系统。

其中的编辑器前端如何设计实现 H5 营销页面搭建系统文章中已经有很详细的说明。

目前整个项目做了 70%左右,过程中遇到了很多问题,也得到了很大的提升。后续会有一波文章是关于项目中的一个个小点展开的,也都是满满的干货。

回到本篇文章的主题:使用Docker Compose、Nginx、SSH和Github Actions实现前端自动化部署测试机。本文是以后台管理系统前端为依托详细说明了如何借助DockernginxGithub CI/CD能力自动化发布一个纯前端项目。选这个项目来讲解自动化发布测试机有两个出发点:

  • 后台管理系统业务较简单,可将重心放在自动化部署流程上
  • 纯前端项目更适用于大部分前端同学现状,拿去即用

整体思路

前端代码,打包出来的是静态文件,可用nginx做服务。思路:

  • 构建一个Docker容器(有nginx
  • dist/目录拷贝到Docker容器中
  • 启动nginx服务
  • 宿主机端口,对应到Docker容器端口中,即可访问

核心代码变动:

  • nginx.conf(给Docker容器的nginx使用)
  • Dockerfile
  • docker-compose.yml
⚠️ 本文将采用理论知识和实际相结合的方式,即先讲述一下对应知识点,同时会放一下与此知识点相关的项目代码或配置文件。

下面会依次讲解Dockerdocker-composesshgithub actions等知识点。

Docker

Docker很早之前,在公众号的一篇文章谁说前端不需要学习 docker?就有过详细说明。这里简单再阐述下。

docker 可以看成是一个高性能的虚拟机,主要用于 linux 环境的虚拟化。开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 linux 机器上。容器完全使用沙箱机制,相互之间不会有任何接口。

在容器中你可以做任何服务器可以做的事,例如在有 node 环境的容器中运行 npm run build 打包项目,在有 nginx 环境的容器中部署项目等等。

centos 上安装 docker

由于这次的云服务器是centos的,所以这里就提一下如何在 centos 上安装 docker

 $ sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine  $ sudo yum install -y yum-utils device-mapper-persistent-data lvm2  $ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo  $ sudo yum install docker-ce docker-ce-cli containerd.io  $ sudo systemctl start docker  $ sudo docker run hello-world

dockerfile

docker 使用 Dockerfile 作为配置文件进行镜像的构建,简单看一个 node 应用构建的 dockerfile

FROM node:12.10.0  WORKDIR /usr/app  COPY package*.json ./  RUN npm ci -qy  COPY . .  EXPOSE 3000  CMD ["npm", "start"]

说明一下每个关键字对应的含义。

FROM

基于这个 Image 开始

WORKDIR

设置工作目录

COPY

复制文件

RUN

新层中执行命令

EXPOSE

声明容器监听端口

CMD

容器启动时执行指令默认值

看下项目中的Dockerfile文件:

# Dockerfile FROM nginx  # 将 dist 文件中的内容复制到 /usr/share/nginx/html/ 这个目录下面 # 所以,之前必须执行 npm run build 来打包出 dist 目录,重要!!! COPY dist/ /usr/share/nginx/html/  # 拷贝 nginx 配置文件 COPY nginx.conf /etc/nginx/nginx.conf  # 设置时区 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone  # 创建 /admin-fe-access.log ,对应到 nginx.conf CMD touch /admin-fe-access.log && nginx && tail -f /admin-fe-access.log

在这个文件里面,我们做了下面几件事:

1、我们用了 NginxDocker image 作为 base image

2、把打包生成的文件夹dist/的全部内容放进 Nginx Docker 的默认 HTML 文件夹,也就是/usr/share/nginx/html/里面。

3、把自定义的 Nginx 配置文件nginx.conf放进 Nginx Docker 的配置文件夹/etc/nginx/nginx.conf中。

4、设置时区。

5、创建 /admin-fe-access.log,启动nginx并使用tail -f模拟类似pm2的阻塞式进程。

这里提到了nginx.conf文件:

#nginx进程数,通常设置成和cpu的数量相等 worker_processes auto;  #全局错误日志定义类型 #error_log  logs/error.log; #error_log  logs/error.log  notice; #error_log  logs/error.log  info;  #进程pid文件 #pid        logs/nginx.pid;  #参考事件模型 events {     #单个进程最大连接数(最大连接数=连接数+进程数)     worker_connections  1024; }  #设定http服务器 http {     #文件扩展名与文件类型映射表     include       mime.types;     #默认文件类型     default_type  application/octet-stream;      #日志格式设定     #$remote_addr与 $http_x_forwarded_for用以记录客户端的ip地址;     #$remote_user:用来记录客户端用户名称;     #$time_local: 用来记录访问时间与时区;     #$request: 用来记录请求的url与http协议;     #$status: 用来记录请求状态;成功是200,     #$body_bytes_sent :记录发送给客户端文件主体内容大小;     #$http_referer:用来记录从那个页面链接访问过来的;     #$http_user_agent:记录客户浏览器的相关信息;     log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '                      '$status $body_bytes_sent "$http_referer" '                      '"$http_user_agent" "$http_x_forwarded_for"';      # access_log  logs/access.log  main;      sendfile        on;     #tcp_nopush     on;      #keepalive_timeout  0;     #长连接超时时间,单位是秒     keepalive_timeout  65;      #gzip  on;      #设定通过nginx上传文件的大小     client_max_body_size   20m;      #虚拟主机的配置     server {         #监听端口         listen       80;         #域名可以有多个,用空格隔开         server_name  admin-fe;          #charset koi8-r;          #定义本虚拟主机的访问日志         access_log  /admin-fe-access.log  main; # 注意,在 Dockerfile 中创建 /admin-fe-access.log          #入口文件的设置         location / {             root   /usr/share/nginx/html;   #入口文件的所在目录             index  index.html index.htm;    #默认入口文件名称             try_files $uri $uri/ /index.html;         }         #error_page  404              /404.html;          # redirect server error pages to the static page /50x.html         error_page   500 502 503 504  /50x.html;         location = /50x.html {             root   html;         }     } }

核心点就是监听80端口,定义日志文件为admin-fe-access.log,入口文件根目录为/usr/share/nginx/html,这些都是与Dockerfile中一一对应的。

说完了Dockerfile及其相关的配置文件,下面接着来看下docker中几个核心的概念。

docker 核心概念

docker中有三个非常重要的概念:

  • 镜像(image)
  • 容器(container)
  • 仓库(repository)

一张图来表明其中的关系:

如果把容器比作轻量的服务器,那么镜像就是创建它的模版,一个 docker镜像可以创建多个容器,它们的关系好比 JavaScript实例的关系。

镜像(image)常用命令:

  • 下载镜像:docker pull <image-name>:<tag>
  • 查看所有镜像:docker images
  • 删除镜像:docker rmi <image-id>
  • 上传镜像:docker push <username>/<repository>:<tag>
如果docker images出现repository<none>的情况,可以运行docker image prune删除

容器(container)常用命令

  • 启动容器:docker run -p xxx:xxx -v=hostPath:containerPath -d --name <container-name> <image-name>

    • -p 端口映射
    • -v 数据卷,文件映射
    • -d 后台运行
    • --name 定义容器名称
  • 查看所有容器:docker ps(加-a显示隐藏的容器)
  • 停止容器:docker stop <container-id>
  • 删除容器:docker rm <container-id>(加-f强制删除)
  • 查看容器信息(如 IP 地址等):docker inspect <container-id>
  • 查看容器日志:docker logs <container-id>
  • 进入容器控制台:docker exec -it <container-id> /bin/sh

镜像构建完成后,可以很容易的在当前宿主上运行,但是, 如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。

一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。所以说:镜像仓库是 Docker 用来集中存放镜像文件的地方,类似于我们之前常用的代码仓库。

docker-compose

docker-compose项目是Docker官方的开源项目,负责实现对Docker容器集群的快速编排。允许用户通过一个单独的docker-compose.yml模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。

使用 compose 的最大优点是你只需在一个文件中定义自己的应用程序栈(即应用程序需要用到的所有服务),然后把这个 YAML 文件放在项目的根目录下,与源码一起受版本控制。其他人只需 clone 你的项目源码之后就可以快速启动服务。

通常适用于项目所需运行环境(对应多个docker容器)较多的场景,例如同时依赖于nodejsmysqlmongodbredis等。

这里放下docker-compose.yml文件:

version: '3' services:   admin-fe:     build:       context: .       dockerfile: Dockerfile     image: admin-fe # 引用官网 nginx 镜像     container_name: admin-fe     ports:       - 8085:80 # 宿主机可以用 127.0.0.1:8085 即可连接容器中的数据库 

基于上文的Dockerfile创建镜像,端口映射是8085:80,这里的8085是宿主机端口,80对应的是nginx暴露的 80 端口

常用命令

  • 构建容器:docker-compose build <service-name>
  • 启动所有服务器:docker-compose up -d(后台启动)
  • 停止所有服务:docker-compose down
  • 查看服务:docker-compose ps

ssh 及云服务器

首先说下云服务器,既然要一键部署测试机,那么肯定要有台测试机,也就是云服务器,这里我用的是阿里云CentOS 8.4 64位的操作系统。

有了服务器,那怎么登陆呢?

本地登陆云服务器的方式一般有两种,密码登陆和 ssh 登陆。但是如果采用密码登陆的话,每次都要输入密码,比较麻烦。这里采用ssh登陆的方式。关于如何免密登录远程服务器,可以参考SSH 免密登陆配置

此后每次登陆都可以通过ssh <username>@<IP>的方式直接免密登陆了。

云服务器安装指定包

接着要给云服务器安装基础包,在CentOS安装指定包一般用的是yum,这个不同于npm

docker

# Step 1: 卸载旧版本 sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine # Step 2: 安装必要的一些系统工具 sudo yum install -y yum-utils # Step 3: 添加软件源信息,使用阿里云镜像 sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # Step 4: 安装 docker-ce sudo yum install docker-ce docker-ce-cli containerd.io # Step 5: 开启 docker服务 sudo systemctl start docker # Step 6: 运行 hello-world 项目 sudo docker run hello-world

如果你像我一样,有Hello from Docker!的话那么Docker就安装成功了!

docker-compose

通过访问 https://github.com/docker/compose/releases/latest 得到最新的 docker-compose 版本(例如:1.27.4),然后执行一下命令安装 docker-compose

# 下载最新版本的 docker-compose 到 /usr/bin 目录下 curl -L https://github.com/docker/compose/releases/download/1.27.4/docker-compose-`uname -s`-`uname -m` -o /usr/bin/docker-compose  # 给 docker-compose 授权 chmod +x /usr/bin/docker-compose

安装完,命令行输入docker-compose version来验证是否安装成功:

node

首先确保可以访问到EPEL库,通过运行以下命令来安装:

sudo yum install epel-release

现在可以使用yum命令安装Node.js了:

sudo yum install nodejs

验证一下:

nginx

yum 安装 nginx 非常简单,输入一条命令即可:

$ sudo yum -y install nginx   # 安装 nginx

git

同样也是使用yum来安装:

yum install git

最后来看一下github actions,也是串联起了上面提到的各个点。

github actions

大家知道,持续集成由很多操作组成,比如拉取代码、执行测试用例、登录远程服务器,发布到第三方服务等等。GitHub 把这些操作就称为 actions

我们先来了解一下一些术语

  • workflow(工作流程):持续集成一次运行的过程,就是一个 workflow。
  • job(任务):一个 workflow 由一个或多个 jobs 构成,含义是一次持续集成的运行,可以完成多个任务。
  • step(步骤):每个 job 由多个 step 构成,一步步完成。
  • action(动作):每个 step 可以依次执行一个或多个命令(action)。

workflow 文件

GitHub Actions 的配置文件叫做 workflow 文件,存放在代码仓库的.github/workflows目录。

workflow 文件采用 YAML 格式,文件名可以任意取,但是后缀名统一为.yml,比如deploy.yml。一个库可以有多个 workflow 文件。GitHub 只要发现.github/workflows目录里面有.yml文件,就会自动运行该文件。

workflow 文件的配置字段非常多,这里列举一些基本字段。

name

name字段是 workflow 的名称。

如果省略该字段,默认为当前 workflow 的文件名。
name: deploy for feature_dev

on

on字段指定触发 workflow 的条件,通常是pushpull_request

指定触发事件时,可以限定分支或标签。

on:   push:     branches:       - master

上面代码指定,只有master分支发生push事件时,才会触发 workflow

jobs

jobs字段,表示要执行的一项或多项任务。其中的runs-on字段指定运行所需要的虚拟机环境。

runs-on: ubuntu-latest

steps

steps字段指定每个 Job 的运行步骤,可以包含一个或多个步骤。每个步骤都可以指定以下三个字段。

  • jobs.<job_id>.steps.name:步骤名称。
  • jobs.<job_id>.steps.run:该步骤运行的命令或者 action。
  • jobs.<job_id>.steps.env:该步骤所需的环境变量。

下面放一下项目中的.github/workflows/deploy-dev.yml文件:

name: deploy for feature_dev  on:   push:     branches:       - 'feature_dev'     paths:       - '.github/workflows/*'       - '__test__/**'       - 'src/**'       - 'config/*'       - 'Dockerfile'       - 'docker-compose.yml'       - 'nginx.conf'  jobs:   deploy-dev:     runs-on: ubuntu-latest      steps:       - uses: actions/checkout@v2       - name: Use Node.js         uses: actions/setup-node@v1         with:           node-version: 14       - name: lint and test # 测试          run: |            npm i            npm run lint            npm run test:local       - name: set ssh key # 临时设置 ssh key         run: |           mkdir -p ~/.ssh/           echo "${{secrets.COSEN_ID_RSA}}" > ~/.ssh/id_rsa           chmod 600 ~/.ssh/id_rsa           ssh-keyscan "106.xx.xx.xx" >> ~/.ssh/known_hosts       - name: deploy         run: |           ssh work@106.xx.xx.xx "             cd /home/work/choba-lego/admin-fe;             git remote add origin https://Cosen95:${{secrets.COSEN_TOKEN}}@github.com/Choba-lego/admin-fe.git;             git checkout feature_dev;             git config pull.rebase false;             git pull origin feature_dev;             git remote remove origin;              # 构建 prd-dev             # npm i;             # npm run build-dev;              # 启动 docker             docker-compose build admin-fe; # 和 docker-compose.yml service 名字一致             docker-compose up -d;           "       - name: delete ssh key         run: rm -rf ~/.ssh/id_rsa 

这里概述一下:

1️⃣ 整个流程在代码pushfeature_dev分支时触发。

2️⃣ 只有一个job,运行在虚拟机环境ubuntu-latest

3️⃣ 第一步使用的是最基础的action,即actions/checkout@v2,它的作用就是让我们的workflow可以访问到我们的repo

4️⃣ 第二步是在执行工作流的机器中安装node,这里使用的actionactions/setup-node@v1

5️⃣ 第三步是执行linttest

6️⃣ 第四步是临时设置 ssh key,这也是为了下一步登录服务器做准备。

7️⃣ 第五步是部署,这里面先是ssh登录服务器,拉取了最新分支代码,然后安装依赖、打包,最后启动docker,生成镜像。到这里测试机上就有了Docker服务。

8️⃣ 最后一步是删除ssh key

最后来github看一下完整的流程:

其中deploy阶段算是核心了:

总结

洋洋洒洒写了这么多,也不知道你看明白了不 😂

如果有任何问题,欢迎评论区留言,看到后会第一时间解答 😊

后续会有很多关于这个项目的文章,也请大家多多关注~

No comments:

Post a Comment