今天遇到一个怪事,用Macbook,前几天还比较正常,今天刷B站的时候,视频卡的跟狗一样,重启了光猫,也是一样,在偏好设置里面搞了很久,也没有解决,但是平板上刷B站一点问题没有,难道它知道我是用网页或app?但是在另外一台windows上却也稳如老狗。于是一通搜索,在一个角落里面找到一句话,大概意思是“USB拖太多东西,导致网卡供电不足”,日妈还有这种事?于是真的是了一下,拔掉USB Hub上的东西,真的恢复了,草啊!!!
最近的一些感悟
已经很久没有更新博客的内容了,便随意写点什么。最近经历了工作上的很多事,有了一些感悟。但是我不会一条一条的列出来,也不会说我经历了什么。感悟这种东西,它不一定对其他人有用,或者说你并不认同。但是,有些东西,表面上的差异,本质上是一致的,感悟这类东西,就往往是表面的,深层的东西埋在感悟下面,更加世俗,更加有价值,但是却失去了一些浪漫。所以,从这个角度去想,我这个人还是有一点浪漫主义,但是我也并不稀奇,毕竟浪漫是不能当饭吃,很多人可悲的是,在生存的边缘挣扎,却在追求极致的浪漫主义。
我不能完全体会到“命运”,但是呢,最近在网上看到一些知名的,但是不像是装出来的名人,他们多次提到“信命”这个事情。我也深刻理解到,“命”不是迷信,更多的是对生命的敬畏,有所为,有所不为。信命的人总是带着这种敬畏,所以看上去总是蹑手蹑脚,但是他自己在其中并不觉得可惜,并且总能做到脚踏实地问心无愧。而不信命的人,往往追求“我命由我不由天”,所以往往比较激进,甚至不惜代价去冲击,有的跳跃了,有的败下来,但是,当这些过去之后,他们总是会有诸多遗憾,甚至内心惭愧悔恨。所以,我的感悟总结起来就一句话“尽人事以听天命”。也就是跟着命运的方向走,在这个过程中自己张开翅膀享受这个过程。这里面所遇到的困难,有的时候很痛苦,但是总是会在之后获得内心的慰藉。一时的得失也好,所谓的人生重大决定也罢,都不是特别重要,关键是,当这件事摆在你面前的时候,不逃避,而是去积极准备和面对它。
你说我信命,但我并不信,我只是遵从了一种理念去生活,是积极的,和那些信命躺平的是截然相反的。不过,我也因此知道,并不是任何努力都有你想要的结果,在这里面,95%的努力是没有用的,这些努力仅仅是你付出的信仰,只有剩下的那5%是有用的。但是,你可能会说,现在那么多人一朝就翻身了,难道是因为遵从你这套理论吗?当然不完全是,但是,他们的命和你的命不同,所以你的努力和他的努力不同。就像我一样,我时常会觉得不公平,有些人明明能力不如我,思想不如我,甚至品德不如我,为什么却比我混的好,赚的钱比我多。然而,当我回过来,并不是我不够好,而是在这个世界的规律里面,在我们这条通道里,他才是适应规律的,就像管道里顺着水流的枯叶,虽然已经腐败不堪,但是和逆流而上的水虫而言,却是更早离开管道看见阳光。我们越是挣扎,或许里得到越远。但我们是没有办法做枯叶的,每个人的性质不同,我们是金子,沉在水底,纹丝不动,永不见光明,这是我们骨子里的倔强决定的,不是可以改变的。所以,即使我们心想我们可以同流合污,最后还是发现不可能得到。这就是命。
那我们就不作为,也是不可取。信命是积极的,信命总是往前,因为信命会顺其自然,尽人事以听天命,而不信命则不然,会逆天而行,所以消耗的更快,看上去轰轰烈烈,实际上很快弹尽粮绝。不过,奇怪的是,竟然有人把“就是死了,也要轰轰烈烈”当作格言,甚至人不在少数。我想,这样的人归根结底,是内在的病态,也是一种性质,就是逆流而上的性质,非要挣扎,一身伤残。这样的人自然是可以存在,但是性质不是这样的人,不该去学,世界上大部分人都是水流地下的泥沙,不快,但总归还是会随着水流慢慢前行,而若这些人非觉得要不一样,那他们只会成为逆流而上的那种,而不可能顺流而下的那种,逆流而上本质上是降级,是更容易的事,而泥沙的性质不可能升格为枯叶般轻快,这是性质决定,但非要降级自己的性质,终究会消散在时间里。
今日就讲这么多,并不值得去深入。
-
明灯不息的熊熊烈火才正是最为纯粹的生命本质(逃#1169 rxliuli 2022-02-26 09:03
将行内渲染到svg保存到本地
在一些情况下,我们想要将类似d3.js之类的框架渲染的svg保存到本地,这要怎么实现呢?其实非常简单。
首先,让我们创建一个用来下载的函数,这个函数不仅仅支持svg,甚至可以支持任意内容。
function download(href, name) { var a = document.createElement('a'); a.download = name; a.href = href; document.body.appendChild(a); a.click(); document.body.removeChild(a); }
我们创建了一个通用的下载函数,但是这个href只支持url,所以,我们要创建一个函数来获得svg的url。
function createObjectUrl(content, type) { return window.URL.createObjectURL(new Blob(content, { type: type })) }
这个函数的content是可以支持多个类型的,因为它会被传入new Blob中,但是需要注意,如果传入字符串,要传入一个数组。
download(createObjectUrl(['<div></div>'], 'text/html'), 'index.html')
现在就好办了,我们只要把svg元素的outerHTML读出来,然后传给download函数就可以了。
download(createObjectUrl([document.querySelector('svg').outerHTML, 'image/svg'), 'some.svg')
对上面操作进行简单封装
function downloadSvg(svg, name) { download(createObjectUrl([document.querySelector(svg).outerHTML, 'image/svg'), name) }
如果要下载为其他类型的图片,那在download之前,要进行一个转码,我们用canvas作为中转,把svg放到一个canvas中之后在下载。
function downloadSvgAsPng(svg, name) { const text = document.querySelector(svg).outerHTML const { width, height } = svg // 或者通过其他方式也可以获得宽高 const image = new Image() image.onload = () => { const canvas = document.createElement('canvas') canvas.width = width cnavas.height = height const context = canvas.getContext('2d') context.fillStyle = context.createPattern(image, 'repeat') context.fillRect(0, 0, width, height) download(canvas.toDataURL('image/png'), name) } image.src = 'data:image/svg+xml;base64,' + btoa(text) }
此处我们没有用到createObjectUrl,因为我们直接将svg转化为base64然后渲染到canvas里面,变成了位图。
最后,需要注意的是,使用这种方法,我们必须在svg内完全定义自身的样式,如果你是在svg之外定义了svg内部元素的样式,那么下载的svg就不包含这部分样式,那自然就会有样式问题。
ScopedQuery: 查询你需要的数据,剪裁无用字段
作为前端,你肯定经常有这样的烦恼:我明明只需要这几个字段,结果你全部给我返回了,何必呢?今天我发了一个包 scopedquery 以解决这个问题。这里 scoped 的意思是“限定的”,也就是你进行的查询是在限定的内容里面。它基于一个新的语言,样子大概如下:
query "https://xxx.com/api/articles/:id" -> { article_title create_time: date('YYYY-MM-DD') article_content view_count: number comments: [ { comment_author comment_content comment_time: data('YYYY-MM-DD') } ] }
上面这段代码所表达的意思是,往 "http...." 发送了一个请求,这个请求要求返回的结果的形状,以及节点上对节点值的格式,需要按照 -> 后面的内容进行返回。
它看上去和 graphql 有点像,但又很不同。它是一个描述性语言,类似 JSON,而非一个编程性语言,即 graphql。graphql 虽然很好,但是,它依赖底层的建设和比较难理解的语法组织。而 ScopedQuery 则纯粹是为了解决数据裁剪而生,不负责底层库的查询,因此,它更轻量,且开箱即用。
npm i scopedquery
安装好后,在业务代码中这样写作:
import { Query } from 'scopedquery' const data = await Query.run(` query "http..." -> { // 支持注释 // 裁剪后的内容 } `)
默认情况下,内部会使用全局的 fetch 进行 ajax 请求,但是你可以自己定义:
const query = new Query({ fetch(url, params) { // ... } }) const data = await query.run(`...`)
在 node 端,可以使用 Query.parse 方法直接裁剪数据:
const data = Query.parse(dataFromBackend, `{ name: string age: number }`)
冒号后面的 string, number 看上去是类型,实际上是格式化工具,它们可以被自定义,让开发者自己觉得不同情况下怎么返回值。
基于 webpack 的能力,我们可以把这些 query 文本单独保存,这样我们可以极为清晰的了解每一个接口对于前端而言,需要的是哪些内容,且有值描述,对于前后端而言,都可以作为参考,辅助前后端在开发过程中进行沟通。
certbot依赖变动导致https失效
今天进博客发现https又失效了,内心个崩溃,莫非终有一天我还是会走上付费的道路吗?之前记录过一次由于certbot依赖版本低导致的无法自动更新证书,今天再次用那个方法,发现没有用。遇到的错误提示如下:
Attempting to renew cert from produced an unexpected error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:645). Skipping.
然后去外网搜索,找到一个线索,是由于依赖的DST_Root_CA_X3.crt过期了,这个DST_Root_CA_X3.crt应该是大部分服务器都一样,所以可以通过以下的方法解决:
sudo apt-get update
sudo apt-get upgrade
sudo sed -i
's/\(.*DST_Root_CA_X3.crt\)/!\1/' /etc/ca-certificates.conf sudo update-ca-certificates sudo certbot renew
通过这个方法,就可以更新证书了。
前端如何优雅建模?
这篇文章放在“杂”栏目下面,说明会是一篇不成体系的文章。我想谈一下如何在前端优雅的建模。直入正题!
前端建模包括两个层面的建模:业务领域建模和交互领域建模。这两者基本上没有本质联系,但是在前端这个场景下,有的时候又有一些特殊的情况。我们来看看如何在这两个层面建模。
业务领域建模
简单讲,业务领域建模,就是把业务实体与其逻辑进行建模。在知乎有小伙伴留言说,前端不怎么适合DDD,因为前端是贫血模型。但在我实际实践中,我更多是充血模型。业务模型需要包含字段本身,以及复杂的业务逻辑。你可能会讲,业务逻辑会被放在后端,但是实际上前端也要这个业务逻辑,比如当一个订单的负责人是组织中的某个职位的角色时,需要在订单推进过程中填写审核时间这个信息。那么,在前端,必须去判断当前用户是否是该角色,或者获得当前用户的某个权限。这个逻辑是跑不掉的。
最近,我升级了tyshemo,支持了装饰器的方式进行meta的定义。现在,你可以这样定义自己的模型:
import { Model, meta, state } from 'tyshemo' class OrderModel extends Model { @meta({ type: { user_id: String, }, }) master = null @meta({ type: Number, }) total_price = 0 @state() role = 'member' canFillDate() { return this.role === 'admin' } }
通过@meta来装饰字段,通过@state来装饰状态属性。这样撰写模型,会有更舒服的感觉,而且可以更好的兼容typescript,避免以前使用static属性定义时,无法与typescript很好结合的问题。
简单讲,通过业务建模,我们得到了一些模型,这些模型是独立的,自治的,在不被使用的时候,它独立描述了该业务对象的各种字段及其逻辑,但由于不在具体的业务场景中被使用,因此也只能表现有限的业务信息,它只能告诉读代码的人“我有什么,能做什么”,而不能告诉“我做了什么”。只有使用这些模型的实例,放到具体的业务模块中,才能完成真正的模块编程。所以,单纯讲,业务建模虽然重要且有用,但是如果不被适当的人使用,就会非常混乱,毫无头绪。
建立业务模型,可以把有关需求文档中,有关核心业务的东西分出一层。
交互领域建模
这是后端没有的东西。直白讲,交互领域建模就是写类似Vue一样的View Model,但是没有template那块。简单说,就是建立一个模型,考虑到将来在view层使用它,所以该模型的所有api,都是为view设计的,主要目标,是和需求文档中有关交互相关的描述一一对应。在交互模型中,实例化业务模型,把业务模型变成交互模型内部的状态。当在view中实例化交互模型,就看不到业务模型了,view拿着交互模型的接口进行渲染和事件回调。
我在nautil中提供了可用于建立交互模型的一个体系。举个例子:
import { Component } from 'nautil' import { SomeController } from './some.controller' // 写好的交互模型 class MyPage extends Component { controller = new SomeController() // 实例化交互模型 SubmitButton = this.controller.turn((props) => { const { someModel } = this.controller // 读取交互模型内的某个业务模型 const { total_price } = someModel // 读取业务字段值 return ( <button className={total_price > 100 ? 'sale-count' : undefined}>Submit</button> ) }) render() { const { SubmitButton } = this // 读取定义好的组件 } }
上面这段代码中,SomeController是一个交互模型,里面使用了另外一个SomeModel业务模型。但是,对于view层而言,你不需要知道它是一个业务模型,你只需要调用它即可。
Nautil在controller中提供了turn方法,用于把一个用到controller的普通的组件转化为一个被controller控制的组件,当controller中某些特定信息发生变化时,这些组件就会自动更新。
分层
前端代码分层管理,从代码量上,并不比铁板一块的管理多多少,毕竟所有的代码,都来自产品的需求描述。但是,分层管理所带来的灵活性、可维护性是不可估量的。
如果你写一个有很多块的页面的vue组件,你就会发现,你的这个组件会越来越多交织在一起的代码,从一开始很容易理解,这几个状态和这几个方法是关于最顶上这一块的,但是,随着页面其他块的交互代码的增多,你就会发现,这个状态会在哪块用?这个方法会在什么情况下调?能不能删?可不可以改?都需要上下反复读代码来确认。
而如果你采取代码分层,你会先针对业务本身的实体进行建模,然后对业务中的交互进行建模,最后才是view层的编写。此时,view层的代码会清晰很多,因为它不再去管理属于业务的逻辑,而更多的是用和回调。
依赖
在view层,我们用vue或react来写,我更多使用react,因为react没有使用Proxy或defineProperty,可以使得我们建模时,使用更多魔法。但是,有些时候,由于view层的特殊机制,导致我们如果完全脱离框架进行建模时,不得不提供一些多余的接口,来帮助和view进行依赖绑定。
以我工作的项目为例,我们使用angularjs作为主体框架,如果我单纯使用ESModule的模块,就没有办法直接使用$rootScope等这种angularjs内置的服务,但是如果我提供一个angular factory,就会牺牲模型的可移植性,为之后跨平台复用带来问题。所以,nautil中提供了controller.turn这个方法来实现模型层和视图层的连接,简单说就是在框架层面调用forceUpdate来实现重新渲染。
Typescript中如何表达一个类是继承至某个类的?
在平时写代码时,我们常常会有这样的写法:
class BaseClass {} function init(SomeClass) { if (isInherited(SomeClass, BaseClass)) { ... } }
上面代码中,表示的是,init这个函数的参数,只接收继承自BaseClass的类,那么这种表达,在typescript中如何表示呢?
function init(SomeClass: ???) { ... }
你看,这比较麻烦吧。你可能会想到:
function init(SomeClass: BaseClass): void
但这显然是不对的,这样表达的意思是SomeClass是BaseClass的实例,而不是类。
正确的表达是如下:
function init(SomeClass: new () => BaseClass): void
其中 new() 是 typescript 中的特殊表达形式,表示以该类型作为具体类进行实例化(需要注意,在typescript中,某个class既是类型,也是值)。而 new () => BaseClass
这句话的意思就是实例化之后得到的实例的类型是BaseClass,或者说,实例化之后是BaseClass的实例(由于在typescript中,某个class既是类型,也是值,因此,比较难用纯粹类型语言进行表达)。这样一来,我们就可以表达出“SomeClass是继承至BaseClass”这一要求。