152022.3

项目CSS管理方案对比

在项目中,我们不仅要管理js代码,还要管理css代码,但是我们现在业界对js研究的比较多,对css比较少,不过这两年我注意到已经有不少有关css的新思想。不过,抛开这些新东西,我们的项目中,应该怎么管理我们的css呢?我做了调研,大致结果如下:

方案 解释 优点 缺点
Atomic CSS Tailwind, https://acss.io/ 复用性,css代码量 html代码量,多处修改易漏
AMCSS <div button="large blue">Button</div>
OOCSS 结构和设计的分离,容器和内容的分离
.mt20 { margin-top: 20px } -> 样式
.flex .flex1 { flex: 1 } -> 结构
.tc { text-align: center } -> 内容
.abs { position: absolute } -> 容器
.clearfix:after { content: '', display: block; clear: both; height: 0 }
SMACSS Base 基本规则,整体样式,比如 body、input、button、form
Layout 布局规则,比如 顶部,页脚,边栏,模块的大小等
Module 可复用的模块样式规则
State 状态样式 比如 隐藏 当前高亮
Theme 主题 控制整体UI
层次分明 需要整体掌控易出错
MCSS multilayer CSS,层层覆盖叠加
BEM .block__element--modifier {} 代码量
scoped css vue 隔离 依赖vue
css modules import * as Css from './some.css' 隔离,tree shaking,编译为RN Stylesheet对象 依赖编译
css-in-js styled-components 隔离,组件化 依赖运行时,代码量

实际上,没有哪一种是最好的,只有适合不适合。对于我个人而言,样式隔离是一个必须选项,因为我开发工具库比较多一些,我提供库给别人用,别人可能还会使用其他人的库,两个库之间撞名的情况很容易发生,所以,隔离对我来说非常重要。在考虑到各种场景后,我最终使用css modules比较多,在nautil中我就坚持css和css modules一起用。不过,除了隔离之外,复用性就不是很好,因为很少能复用其他库的样式,最终会导致使用了我的库的站点,css代码量可能会篇多一些。所以我说,没有那个最好,只有最适合。

18:05:10 已有2条回复
  1. 方案列为什么会折行呢
    #1174 1188 2022-03-18 14:35 回复
  2. 页面宽度不够,已经调整了
    #1175 回复给#1174 否子戈 2022-03-18 19:48 回复
042022.3

今天遇到一个怪事,用Macbook,前几天还比较正常,今天刷B站的时候,视频卡的跟狗一样,重启了光猫,也是一样,在偏好设置里面搞了很久,也没有解决,但是平板上刷B站一点问题没有,难道它知道我是用网页或app?但是在另外一台windows上却也稳如老狗。于是一通搜索,在一个角落里面找到一句话,大概意思是“USB拖太多东西,导致网卡供电不足”,日妈还有这种事?于是真的是了一下,拔掉USB Hub上的东西,真的恢复了,草啊!!!

20:01:24 已有0条回复
252022.2

最近的一些感悟

已经很久没有更新博客的内容了,便随意写点什么。最近经历了工作上的很多事,有了一些感悟。但是我不会一条一条的列出来,也不会说我经历了什么。感悟这种东西,它不一定对其他人有用,或者说你并不认同。但是,有些东西,表面上的差异,本质上是一致的,感悟这类东西,就往往是表面的,深层的东西埋在感悟下面,更加世俗,更加有价值,但是却失去了一些浪漫。所以,从这个角度去想,我这个人还是有一点浪漫主义,但是我也并不稀奇,毕竟浪漫是不能当饭吃,很多人可悲的是,在生存的边缘挣扎,却在追求极致的浪漫主义。

我不能完全体会到“命运”,但是呢,最近在网上看到一些知名的,但是不像是装出来的名人,他们多次提到“信命”这个事情。我也深刻理解到,“命”不是迷信,更多的是对生命的敬畏,有所为,有所不为。信命的人总是带着这种敬畏,所以看上去总是蹑手蹑脚,但是他自己在其中并不觉得可惜,并且总能做到脚踏实地问心无愧。而不信命的人,往往追求“我命由我不由天”,所以往往比较激进,甚至不惜代价去冲击,有的跳跃了,有的败下来,但是,当这些过去之后,他们总是会有诸多遗憾,甚至内心惭愧悔恨。所以,我的感悟总结起来就一句话“尽人事以听天命”。也就是跟着命运的方向走,在这个过程中自己张开翅膀享受这个过程。这里面所遇到的困难,有的时候很痛苦,但是总是会在之后获得内心的慰藉。一时的得失也好,所谓的人生重大决定也罢,都不是特别重要,关键是,当这件事摆在你面前的时候,不逃避,而是去积极准备和面对它。

你说我信命,但我并不信,我只是遵从了一种理念去生活,是积极的,和那些信命躺平的是截然相反的。不过,我也因此知道,并不是任何努力都有你想要的结果,在这里面,95%的努力是没有用的,这些努力仅仅是你付出的信仰,只有剩下的那5%是有用的。但是,你可能会说,现在那么多人一朝就翻身了,难道是因为遵从你这套理论吗?当然不完全是,但是,他们的命和你的命不同,所以你的努力和他的努力不同。就像我一样,我时常会觉得不公平,有些人明明能力不如我,思想不如我,甚至品德不如我,为什么却比我混的好,赚的钱比我多。然而,当我回过来,并不是我不够好,而是在这个世界的规律里面,在我们这条通道里,他才是适应规律的,就像管道里顺着水流的枯叶,虽然已经腐败不堪,但是和逆流而上的水虫而言,却是更早离开管道看见阳光。我们越是挣扎,或许里得到越远。但我们是没有办法做枯叶的,每个人的性质不同,我们是金子,沉在水底,纹丝不动,永不见光明,这是我们骨子里的倔强决定的,不是可以改变的。所以,即使我们心想我们可以同流合污,最后还是发现不可能得到。这就是命。

那我们就不作为,也是不可取。信命是积极的,信命总是往前,因为信命会顺其自然,尽人事以听天命,而不信命则不然,会逆天而行,所以消耗的更快,看上去轰轰烈烈,实际上很快弹尽粮绝。不过,奇怪的是,竟然有人把“就是死了,也要轰轰烈烈”当作格言,甚至人不在少数。我想,这样的人归根结底,是内在的病态,也是一种性质,就是逆流而上的性质,非要挣扎,一身伤残。这样的人自然是可以存在,但是性质不是这样的人,不该去学,世界上大部分人都是水流地下的泥沙,不快,但总归还是会随着水流慢慢前行,而若这些人非觉得要不一样,那他们只会成为逆流而上的那种,而不可能顺流而下的那种,逆流而上本质上是降级,是更容易的事,而泥沙的性质不可能升格为枯叶般轻快,这是性质决定,但非要降级自己的性质,终究会消散在时间里。

今日就讲这么多,并不值得去深入。

20:49:07 已有1条回复
  1. 明灯不息的熊熊烈火才正是最为纯粹的生命本质(逃
    #1169 rxliuli 2022-02-26 09:03 回复
232022.2

装修参考文献

162022.2

Emoji 到底是什么

082022.2

将行内渲染到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就不包含这部分样式,那自然就会有样式问题。

19:35:14 已有0条回复
072022.1

审批流设计指南

012021.12

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 文本单独保存,这样我们可以极为清晰的了解每一个接口对于前端而言,需要的是哪些内容,且有值描述,对于前后端而言,都可以作为参考,辅助前后端在开发过程中进行沟通。

17:51:13 已有0条回复
232021.11

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

通过这个方法,就可以更新证书了。

23:29:49 已有0条回复
152021.11

前端如何优雅建模?

这篇文章放在“杂”栏目下面,说明会是一篇不成体系的文章。我想谈一下如何在前端优雅的建模。直入正题!

前端建模包括两个层面的建模:业务领域建模和交互领域建模。这两者基本上没有本质联系,但是在前端这个场景下,有的时候又有一些特殊的情况。我们来看看如何在这两个层面建模。

业务领域建模

简单讲,业务领域建模,就是把业务实体与其逻辑进行建模。在知乎有小伙伴留言说,前端不怎么适合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来实现重新渲染。

23:19:42 已有0条回复