聊一聊npm init

本文内容适用的node版本范围是14+(npm6+),相关例子实在node16+(npm8+)下运行的。

在npm使用还比较初级的时代,如果想做一些项目初始化的脚手架,主要是依靠gulp、grunt、yeoman等工具进行搭建。

项目初始化,主要是将一套代码的模板(template)进行一系列互动和文件操作,拷贝到用户的项目目录,来进行提效。

而现在我们有了更多更简单的选择,比如npm init

如果你有一些做项目初始化的需求,那这篇文章应该可以帮到你。

什么是npm init

如果你阅读过create-react-appvue的官方指引,那再Quick Start中都提到了,一段代码。

# react
npm init react-app my-app
# vue
npm init vue@latest

通过执行这些命令,你会被问及一些问题,根据需要完成后,你就能创建一个react应用或者vue的应用。

这就是文章前面说的,项目初始化。

发现vite用的是npm create

在vite快速创建项目的时候,会发现文档中的命令有些变化。

npm create vite@latest

这里的npm create其实和npm init等价,是个别名

根据npm,version 8版本的文档,以下三种命令是等价的

npm init
npm create
npm innit

还发现有用npx的

前面提到的create-react-app中,还会看到这样的命令。

npx create-react-app my-app

npx又和npm init有什么关系呢,让我们先来看下npm init的执行逻辑。

npm init是如何工作的

npm init <initializer> can be used to set up a new or existing npm package.

initializer in this case is an npm package named create-<initializer>, which will be installed by npm-exec, and then have its main bin executed — presumably creating or updating package.json and running any other initialization-related operations.

https://docs.npmjs.com/cli/v8/commands/npm-init?v=true#description

npm init的工作方式,其实官网说的是比较清楚的,就是将包名前面加上create-后到npm package中找到对应包,并执行包中配置的bin。

也就是说执行的是npm init vue@latest,实际去找的包其实是npm init create-vue@latest

除了会自动在包名前面加create-去查找,还有一些其他的规则

npm init foo-&gt;npm exec create-foo
npm init @usr/foo-&gt;npm exec @usr/create-foo
npm init @usr-&gt;npm exec @usr/create
npm init @usr@2.0.0-&gt;npm exec @usr/create@2.0.0
npm init @usr/foo@2.0.0-&gt;npm exec @usr/create-foo@2.0.0

而文档中也很明确的说明了,npm init其实是依赖npm exec去执行的命令,因此你也可以通过npm exec实现所有npm init的能力,就是命令会复杂很多罢了。

传递参数给bin

大家可能知道npm的传参方式,这里以vite举例

# npm 6.x
npm create vite@latest my-vue-app --template vue

# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

从vite文档中给出的命令可以发现,npm不同的版本传参方式还有所不同。

这源于各个版本的process.argv的实现各不相同,npm6会将第一个--解析到结果中,而npm7+则不会在process.argv体现第一个--

而如果你想让你的用户不感知版本问题可以在代码中做个简单的兼容,将参数统一为npm7+的效果,如:

function removeFirstArgByString(argv, s = '--') {
  const newArgv = [...argv];
  const i = newArgv.indexOf(s);
  if ( i !== -1 ) {
    newArgv.splice(i, 1);
  }
  return newArgv;
}
removeFirstArgByString(process.argv);

深入npm init

文章写到这里,动手能力强的同学,就可以开始对自己已有的复制代码的工具进行一些改造了。

使用命令的时候你也会发现一些现象。

比如在使用这个命令进行项目创建的时候,命令行会先提出一个问题。

npm create vite@latest                                                                                                                                                                                                                                                                                  
Need to install the following packages:
  create-vite@4.1.0
Ok to proceed? (y)

命令行的问题是,你需要安装对应的安装包。

那有经验的同学可能就会有一些问题了:

  • 命令行为什么会知道我没有装这个包
  • 这个包安装后存到了哪里
  • 这个安装包中带了版本号,后续这个包更新了,要如何更新

以前做脚手架的话这些问题是都要考虑的。

npm init的优势就是在于,将这些问题都考虑到了,即使你不了解这些问题,也不会遇到什么问题。

而为了更严谨,我还是研究了一下这块的逻辑。

前文提到了npm init其实就是基于npm exec实现的,那咱们就看看对应的代码。

https://github.com/npm/cli/blob/v8.19.4/workspaces/libnpmexec/lib/index.js

怎么判断有没有这个包

简单来说,这部分代码的逻辑就是:

  1. 检查当前路径下是否有目标的包,如果有返回,没有往下
  2. 检查全局(通过 -g 安装的包)是否有目标的包,如果有返回,没有往下
  3. 检查缓存是否有目标包,如果有返回,没有往下
  4. 去当前全局配置的源中招是否有目标包,如果有返回,如果没有报错并结束

通过上面的流程,就能确定是不是有这个包了。

包安装到了哪里

首先你会发现这个包不在你执行命令的子目录下,另外你也会发现这个包也没有存储到全局包的位置。

那这个包去哪里了呢…

他其实在你npm的缓存目录中,通常在~/.npm/_npx中以包名的hash方式命名的目录下存储。

你可以通过npm config get cache来找到缓存目录

已经安装了包,但是包有了新的版本如何更新

前面说了npm已经考虑了这一点,在上面的代码中有一个getManifest方法,在检查包的过程会通过npm的pacote包获取当前源中的最新版本,最后检查缓存中目标包的时候,会check是否是最新版本,如果不是就仍然会提示需要安装包。

其他需要注意的

  • 前面提到的示例都没有提到指定版本号@usr/foo@2.0.0的场景,如果指定了版本,存入缓存的目录名也会带上版本号进行hash处理,因此同一个包的不同版本会分别缓存。
  • 另外这里虽然说是缓存,但当前其实并不会被动清理,需要主动去对应缓存目录下清理。
  • 如果你是使用npm i -g的方式安装了create能力的包,就不会走npm init的流程,也就不会检验包是否需要升级,而是需要工具包自己实现这个能力,并且需要自定义一个执行的命令,使用者还需要记一个使用的命令,体验不是很好。

总结

之前都是在使用npm init初始化各个框架,并没有想到是如此方便,如果在工作上有类似的需要,不放赶快尝试一下,为自己和小伙伴提效吧。

相关链接:

设计无侵入性的低代码编辑器

低代码(此处的低代码是广义的低代码,也包含零代码场景,后面都以低代码代称)是当前技术圈话题点比较多的一项技术。一些是商业化地APaaS或者SaaS等系统给客户使用的,一些是给团队内部提效使用的,虽然大同小异,但是场景不同针对点也有所不同。本篇文章主要针对开发团队内部使用的低代码编辑器来进行展开。

动机

之前碰到团队是一个多业务、多端、多组件库、多框架、多平台的大前端团队,在一些场景有大量的同质化页面,开发每次复制粘贴虽然能解决问题,但是对开发同学的成长没有什么帮助,沟通成本也非常高。

希望可以通过一些方案解放开发,进行提效,用低代码构建页面是我能想到的方案之一。

低代码这个想法开始于21年初,选用低代码的核心思路其实主要是想让需求的沟通链变短,让参与需求的开发尽可能少,最好做到不需要开发。

预研

当时市面上的低代码编辑器为了更好地把控编辑的过程,通常将画布中要展示的组件和编辑器本身用一个技术栈,且是编辑器所支持的组件库来实现的,有一定的封闭性。

也因此你想要选择一个好用的第三方低代码编辑器,就要跟他使用同一个技术栈,甚至同一套组件库,那如果原有的项目不是匹配的技术,就需要对自己原有的技术进行改造会支持。

而即使使用了同一套技术栈同一套组件库,因为要支持低代码编辑器的使用,可能还要在组件代码本体中进行声明和标注等操作满足编辑器的圈选等定制逻辑。

这样的设计在一定程度上降低了开发时的知识成本,更讲究开发的约定来让业务、组件、编辑器三位一体,更好掌控,比较适合一个技术栈规范严谨的团队闭环,但是对于外部团队,或者业务模块或技术栈较多的团队用起来就比较痛苦,也因为组件库和编辑器的联动导致了必然的侵入性。

也可能正是因为这种原因,业内出现了低代码的百家争鸣,主流大厂基本人手一个,有的厂子甚至有一堆低代码相关的建设,大家都在以自己的思考和自己擅长的方式输出了各种类型的低代码编辑器,像阿里也在AI到低代码上有了较深度的实践,让人钦佩。

但这样的低代码编辑器满足不了团队的多样性场景,总不能每个场景都做一个低代码编辑器吧。

那如果要自己做一个低代码编辑器,满足需求,又要做成一个什么样的东西呢:

  • 需要一个编辑器支持所有场景
  • 需要跨技术栈
  • 不希望对原有的技术做改造

提出假设

仔细分析上面的问题和矛盾点会发现,如果让业务、组件、编辑器三项内容分别解耦,然后通过某种约定再将他们相连,是不是就可以满足上面的要求了,这似乎正应该是一个在多元化团队中低代码编辑器该有的样子。

低代码编辑器只是个工具,简单来看主要做了拖拉拽组件元素,生成布局,修改配置,产生页面这几步。

不论你是什么样的团队,要做怎样的编辑器,只要需要人工介入,这几步都是避免不了的,而如果想让开发尽可能不介入,只要在这个前提下做好体验就可以了(当然做好这个体验也是个非常艰难的工程,本篇文章先不做该角度的讨论)。

那是不是可以就针对这个流程做编辑器,跟流程不直接相关的内容都分离出去,以接入的方式接入到低代码编辑器里面。

当前,阿里(lowcode-engine)腾讯(tmagic-editor)各有一个类似的方案已经开源,虽然我的解决方案并不相同,但是大方向的思考相似,大家有兴趣可以对比了解下。

画布设计

先来从下图看下低代码通常的基本结构和行为:

从图中可以了解到:

  • 页面中有三个主要元素,组件、画布、配置
  • 组件和配置都是输入,画布中主要是输出
  • 每次有输入行为的变化都有可能让画布重新渲染

前面有说过,之所以现有的低代码编辑器有侵入性,主要还是在于对组件库中的组件有干预,做这些干预的主要目的,就是为了能便捷地拖拽、点选、配置组件。

这些行为其实也主要是发生在画布上的,如果我能让画布和组件实体产生解耦,那么无侵入的设计就迈出了关键的一步。

实际上低代码侵入业务的原因也是多样的,一些低代码平台也会从打包甚至业务流程上侵入,只是当编辑器做到了无侵入的时候,其他环境的侵入就能做到更多的选择。
最容易想到的方式是让低代码部分和传统开发的部分完全走两条线,而这不是本文的重点,就不再过多展开。

此时我想到了现在手机的触摸操作,在触摸手机进行操作的行为时,其实也不是在直接操作手机的显示屏本身,而是在操作电容屏上的导电膜,然后将信号传递给各种程序再转换出画面的。

受到这个思路的启发,我想低代码编辑器也可以以这个思路进行设计。

和手机屏幕的结构对应,我也做了一些类似的分层:

  • 手机上的触点就像是我在低代码上要点选的元素,我称之为编辑层
  • 产生触发或者交互行为就会有行为信号传递给系统,也就是低代码编辑器
  • 手机系统回家行为信号处理成机器语言给到应用;而编辑器则对行为信号进行解析,变成约定的领域语言给到渲染引擎
  • 应用和渲染引擎根据对应的语言情况,进行渲染,就会产生用户能看到的结果

在这个分层中,其实已经将组件与画布本身进行了一定程度地解耦。

想接入这样低代码编辑器的团队,预计只需要了解DSL并开发渲染引擎即可,其他环节由低代码编辑器进行管理,也就能在低代码中想手机引入各种软件一样,引入渲染引擎和组件库了。

同时低代码的结构和操作流程也会变成下面的样子。

现在解耦的设计有了,那具体要怎么让开发团队接入呢?DSL和渲染引擎又是什么?

DSL

领域特定语言(Domain Specific Language),在低代码领域中几乎已经成为标配,通常用来作为中间语法的设计,来抹平不同技术间的差异性,做到一码多用。因此我也选用了DSL做低代码编辑器和组件库及画布之间的媒介。

以一个图片(Image)的节点进行举例:

这就是一个节点的基本结构,而开发者就是要做一个将这个DSL节点解析成对应技术栈布局节点的引擎,比如前端就是要解析成<img width=”100%” src=”url” />并渲染到浏览器中。DSL节点根据前面图片中的流程,是需要编辑器来生成的。这需要用户对画布操作的时候,可以将对组件进行的操作转变为节点的DSL,现在设计的目标是编辑器可以和组件库解耦,那这里就不能让编辑器直接去操作组件。因此可以使用声明的方式,将组件的描述给到编辑器。

通过以上的操作,就能做到编辑器生产这套节点信息,渲染引擎也就是开发者消费、解析、渲染这套信息,让上面画布的设计成为可能。

上图就是触发画布编辑后,而后借助编辑器中的DSL转换器,将用户的操作信号转为DSL并将其给到渲染引擎,而渲染层则会根据这段描述结合真实的组件画出页面,至此一套无侵入性的低代码编辑器就算是有了雏形了。
复制代码

下面再看看渲染层和编辑层是如何设计的。

渲染层

渲染层最主要的职责就是给渲染引擎和组件库一个容器,根据页面描述渲染出对应的页面内容。

从细节来看,还需要具备:

  • 组件的使用
  • 组件相关技术栈的运行时
  • 组件解析
  • 页面的渲染

按照这样的设计,渲染层相当于一个沙箱将组件库和页面渲染的行为隔离在了自己的容器中。

而关于沙箱,我的目标是尽可能避免低代码编辑器和渲染层的互相影响,可能的影响会有:

  • 样式影响
  • 脚本影响
  • 显示元素的影响

因此,可选的有ShadowDOM、iframe两种方案,在尝试了两种方案以后,发现如果要编辑对话框之类的组件,往往会有一个蒙版,而这个蒙版往往会去尝试让整个页面被遮盖,而ShadowDOM的方案并不能很好的对这个问题进行控制,因此我最终选择了iframe方案。

相对的,组件本身的使用也完全不感知编辑器本身,两边无法因为环境因素互相影响,只能通过主动的操作让两边进行通信。通过这个设计,甚至可以让这个渲染层画出与生产环境完全相同的页面,达到真正的所见即所得。

低代码编辑器的职责是构建和编辑页面描述,现在把绘制出来的可视内容隔离了,那怎么编辑内容呢?这就要靠编辑层了。

编辑层

从前面的流程图可以看出操作后的页面描述同时给了渲染层和编辑层,也就是说,编辑层也要绘制一个页面的模型,只不过这个模型是一个只有标注和布局的触发器。

从上图可以更形象地看出编辑层和渲染层的关系,其实就是将组件的布局和大小映射到了编辑层,让用户操作组件的时候感觉是点击到了渲染后的组件,其实用户点击的是编辑层的透明触点,以此来进行页面内容编辑行为的触发。

如果想让点击选框的时候准确的识别到对应的组件,就需要编辑层的选框和对应的组件完全贴合,想要做到这个贴合,只需要将组件的布局信息进行收集,传递给低代码编辑器,再由低代码编辑器给到编辑层按照坑位进行布局即可,而确定一对一的关系,只要在页面描述生成的时候给每个组件节点一个唯一的ID即可。

因此就可以通过传统低代码编辑器的开发能力对页面进行编辑,完整的编辑器运作流程如下:

再说无侵入性

讲到这里,再回顾一下最开始的动机,这套设计能不能解决问题。

多样性的问题,前端部分比较典型的就是有多个组件库。

当前的设计,团队不同业务模块的组件库各自包装一层组件库的声明,再加上对应的渲染引擎,就可以把一个低代码编辑器多用的流程跑起来了。

有同学看到这里可能下意识觉得做多个渲染引擎的成本似乎很高,而在实践中也发现,其实React同一个技术栈只用一套渲染引擎就可以了,因为解析的方式都是相同的。

而想用小程序这种也可以扩展类似Taro这种React的引擎就可以进行接入了。

引擎的大部分代码其实也都趋于源生,在Vue上也有一定的复用性,甚至可以做到SSR,因此随着实践这样的成本将会越来越低,总归是比建设多个低代码编辑器好太多了。

抛砖引玉

整体设计简单来说就是,给传统的低代码编辑器加一层渲染层。

在原编辑器产生编辑行为后生成的页面描述给到渲染层的容器,只要渲染层能解析上层的描述,就可以渲染任何的组件库。基于这一句话,可以说当前任何一个低代码编辑器都可以进行跨技术栈跨组件库的改造。

而理论在实践后将不出意外地会遇到一系列的挑战:

  • 编辑层如何和渲染层的布局进行同步
  • 如何让渲染层和低代码编辑器很好的进行隔离
  • 隔离后是否可以完全像传统的方式进行开发编辑

当前我正在尝试以这样的模式写一个可以快速接入到团队的低代码拖拽框架,当前还只是demo阶段,欢迎交流。

whimsical: https://github.com/gaofeiyu/whimsical

Whimsical开发第三个月总结

进度

本月比较刺激,全家老小都阳了,工作也是斗转星移,不过项目本来的计划没有太耽误。

  • 补充了主要功能的单元测试
  • 构建了基本的文档
  • å处理了一些运维问题å
  • 添加了monaco编辑
  • 增加了类型转为JSONSCHEMA的能力
  • 启动了数据绑定功能的开发
  • 重构了部分底层配置

原本的计划基本完成了,但是进度还是不快,随着新一年的到来,我会开始对结构树及数据绑定的你能力进行开发,希望能在2月份完成playground的上线。

一些心得

随着开发的深入,也发现了自己开发越来越多的问题,一边开发一边学习,也是我启动这个项目的目的。

TS类型定义

做项目之前整体的TS类型定义还比较基础,12月份的开发中发现自己类型会的那点东西完全不够用,从项目现有的代码也可以很清晰地看到这一点。

不过我现在正在通过https://github.com/type-challenges/type-challenges项目进行类型的联系,还是很有收获的,1月份我会对全局的类型进行一次重构,希望可以尽可能地弄透类型的使用。

JSONSchema

经过12月的开发,发现JSONSchema的应用范围还是挺广的。

  • DSL标准
  • monaco编辑器语法提示
  • JSON格式验证

因此后面我会对JSONSchema也加大理解的力度,这个和TS类型定义也是相辅相成的,现在JSONSchema和TS的类型是可以做到一定程度的互转的,而这个互转还不仅限于这两个技术,后面做各种工具和BFF的时候应该也用得到。

23年1月规划

过年月,还是想休息一下:)如上文所说,本月主要想在类型上多动动脑,争取分享个相关文章出来。

编辑器功能上,本月争取把数据绑定的交互写好,提前祝大家新年快乐。

Whimsical开发第二个月总结

进度

  • 新增功能
  • 产物的导出和内容的导入
  • 属性配置表单能力
  • 样式配置的模块
  • 表达式解析能力
  • 数据绑定能力及相关表达式
  • 画布自适应
  • 其他
  • 对工具包的功能进行了丰富
  • 补充部分单元测试
  • vite升级到4.0.0-beta.0版本(为了处理库打包多入口问题)
  • 引擎及组件库支持按需加载
  • 增加了性能比对工具

一些问题和思考

使用vite做支持按需加载的组件库

vite默认打包是单入口的,也就是在build.lib.entry里面只能穿一个字符串。

而支持按需加载主要是要将打包产物的内容进行分离,那这个vite也在不断做改进。

根据我现有项目的样本

  • vite2.x版本支持通过rollupOptions中的input来实现多入口分包,但是entry需要传''空字符串,来保证路径和命名符合预期。
  • vite3.2.x这个机制产生了变化,用2.x的配置进行打包会报错,因为entry不允许传''空字符串了,而在看源码的时候发现main分支已经进入到4.0.0-beta.0版本了
  • vite4.x在我使用的时候还没发布正式版,但是打包的时候build.lib.entry配置实现了rollupOptions.input相似的功能了,可以看代码段github.com/vitejs/vite…

属性配置能力选型formily

因为之前做相关建设就选用的formily,所以这次还是用了formily,但是没用全家桶,状态管理仍然使用了mobx,这里会有一些冗余,但不是当前的重点,后续再进行优化。

引擎解析性能比预计的好一些

这里测试性能的样本使用了深度为4,每个节点广度为10的全树总计10000个节点进行100次渲染的测试,下面可以看下结果。

从图中可以看出,还是多了一些解析代价的,毕竟要在运行时构建JSX接口再生成VNode,在上面的样本中有多接近10%的解析代价,在正常业务场景是可以接受的,而且这个解析思路也可以做到SSR上,那这个解析代价就会变得更小,这里还有很多细节,后面单独写文章说一下。

不过这不是最终的数据,后面随着功能的增加还会做增量的更复杂场景的对比,如:

  • 有大量表达式的时候
  • 打包文件和DSL文件因大小差异带来的优势和代价,从当前的用例来看,用DSL的包大小会比JSX打包出来的大小更大,这个跟我之前的预期是有差距的,但是这也是因为节点都是最简单的节点导致的。
  • 因状态变化带来的重新渲染的代价。

那就结而论,这个结果是比我预计要好的,在对性能要求没那么极致,但是对搭建有需求的场景,是很OK的。

12月规划

前面两个月的工作积累了一些功能,12月我会暂停新功能的开发,对项目补充单元测试和文档,争取一个月把现有的内容稳定下来。

Whimsical开发第一个月总结

进度

项目已经启动了一个月了,编辑器部分的整体进度在15%左右,进度不是很快,当前核心内容已经有了雏形,主要实现了一下功能:

  • 编辑器Layout
  • 事件调度
  • 历史快照
  • 画布分层
  • 属性配置联动
  • 设计了logo

演示

record-1m.2022-11-11 16_58_33.gif

一些技术选型

这里部分内容借鉴了formily的编辑器designable,属性配置联动部分的表单更是选用了formily进行驱动。

但是在事件调度和状态管理上方案有所不同

  • 状态管理:mobx
    • 使用mobx是为了降低项目维护的复杂度
    • mobx使用了严格模式,不允许直接通过赋值进行状态改变
  • 事件调度:rxjs
    • 当前使用rxjs是为了不去仔细考虑这里的设计,将重点放在整体框架上
    • 因为仅使用了rxjs中的subject,后续可能会对该部分进行重写
  • 拖拽系统使用了react-DND
    • 为了能更快速的开发,干脆对react-DND进行了汉化,当前是用到什么汉化什么,预计今年内会将文档全部汉化

这里没有详细写技术选型中的思考,如果有需要可以私信我,我可以多水一篇文章。

感想

项目启动至今,基本每天会抽1个小时左右写一点代码,从现在的心态上来看还OK,很充实。 中间有一段时间还有一些功利心作祟,想着无论如何要提交些什么,为了提交而提交, 现在就很释然了,尽力而为,心态放松后好像效率反而更高了。 总体来说,是个不错的尝试,当前除了时间比较少,还没有什么太大的瓶颈。

11月也会努力更新,期待感兴趣的同学和我交流。

项目地址:https://github.com/gaofeiyu/whimsical

我有一个Whimsical的项目启动了

I have a Whimsical project started

Whimsical[ˈwɪmzɪkl] 是异想天开的、古怪的、怪诞的意思,在我准备做这个项目之前并不认识这个单词,只是在想用什么作为项目名字的时候,想找个npm包中没有的名字,无意中碰到了这个词,而这个词意外地和我做这件事的目标和可执行性有些贴合,且为我要做的这件事的半途而废找到了一个很好的后路,就很完美。

我想做的东西是一个以低(零)代码编辑器为中心辐射出的一套工具包。面对这个时间点的低代码发展情况,这个赛道已经是一块让个人难以耕耘的盐碱地了,而我想做的不是一个低代码的整体平台,而是平台中每一块的零件,为低代码平台的整体架构进行解构,对页面编辑器、debugTools、设计稿自动解析、PRD自动解析、数据层等等方面输出针对的工具包和思路。

我做低代码相关的工作已经有较长一段时间了,在技术职场中,低代码是一个大多数人都用过,技术团队总想要,但又被团队十分嫌弃的一个课题。尤其是在重业务的团队,低代码相关的开发者在搞出惊天动地的结果之前,都是一直要被否定和挑战的。

不过这些困难和实践让我产生了更多的思考,于是我便启动这个项目,想将我的一些思考和实践,且跟现有工作不直接相关的内容提炼出来,期望能在更开放的平台学习和产出,解决一些低代码的通用问题。

对我的意义

这是一个从利己角度出发,产出利他结果的项目。

为什么是利己?

我是一个互联网某厂的码工,工作节奏快且充实,而长时间投入在业务和实操让我对技术最原始的驱动力变得懒惰和迟缓,简单讲就是生锈了。

我想通过这个项目来给自己一些强制的训练,复习以前的知识,并跟进新的知识,因此我在该项目中不会考虑兼容性的问题,会使用一些我感兴趣的技术栈和方案。

项目会尽可能以开源项目的思路进行一边学习一边建设,因为还有本职工作,原则上是有时间就多做点没时间就少做点,但不会不做,且有时也会为了我现有的工作当做试验场。

利他的结果是什么?

虽然这是以“我”为中心任性的项目,但是我仍然想要产出的内容有附加价值,在项目出现里程碑结果的时候,我也会进行一些运营,把我认为好的内容分享给社区,即使我的项目只是一个试错的炮灰。

比如该项目的第一个课题即是一个通用的低代码编辑器,这个通用的目标是可以融合任何技术栈的组件库的可拖拽低代码编辑器。因为个人当前的知识储备优先,因此第一阶段只面向web端,随着时间和阅历的推移,我会以整个大前端平台为目标进行学习和推进。

具体要做个什么东西

低代码是一个大课题,SaaS和aPaaS的低代码、流程图的低代码、页面搭建的低代码、低代码的上下游支撑,只从基本内容来看就能看出其建设成本之高往往让团队望而却步,这也是当前低代码相关建设被人主要诟病的原因,也是为什么很多大厂都在做低代码相关的付费服务。

如何将投入开发低代码的成本从逆差变成顺差,是每个相关开发团队需要解决的问题,这不免要进入鸡生蛋和蛋生鸡的扯皮循环。

那我想以我对低代码部分内容的理解,做出低代码整体建设中的零件,让相关团队在进行开发时少走一些弯路,或者可以直接使用我的部分成果为低代码的同僚们的KPI或OKR加把柴。

项目在当前主要涉及的内容包括:

主要内容

看到这里是不是觉得更加Whimsical了呢?

写在最后

我喜欢踢足球,但是不爱看球赛;我喜欢打dota,但是一个电竞选手都叫不出;我喜欢编程,但是已经很流行而我却不知道的技术和框架越来越多;我喜欢网上冲浪,但很少写文章,不知道看文章的你是不是像我一样,习惯把自己缩成一团。2022年里我接触了很多新东西,也尝试了不少,挫败感很多,但也总有新鲜的事物吸引我的注意力,而重要的是我舒展了自己,敢于去拥抱我自以为距离我很远的事物。

我想藉由这个项目,为我已经定型的脑袋敲出一些新花样,这篇文章可能短期不会有人能看到,但希望有那么一天会有人挖出这个文章来刺激一下未来的那个不争气的我,要么就不要异想天开,要么就坚持到底。

项目地址:https://github.com/gaofeiyu/whimsical

在qiankun微前端中做性能统计

我所在的团队中跨业务、跨平台的内容比较多,微前端越来越广泛的应用到了团队中,当前从易用性来讲还是qiankun比较适合我的团队,但是在最近看数据的时候感觉报表上的页面性能数据好的超出了我的预期,深入思考一下,猛然想到,这些该不会都只是主应用的数据,完全没统计到子应用吧。

为什么统计不到

qiankun微前端工程是主应用加载后再进行子应用文档的请求进行挂载的。

而主应用加载后子应用挂在前当前页面已经完成了LCP的指标收集,也就是说还没等主体内容被渲染,页面的性能统计行为已经结束了…

为了让数据更准确,我决定追加自定义埋点,获取更可靠的性能数据。

解决思路以及问题

在网上搜索资料的时候,竟然没有发现这方面的讨论和方案,那只能完全自己动手了。

我的目标是获取子应用完全加载后的时间是一个介于LCPTTI之间的指标。

关于页面指标的说明可以查看:https://web.dev/metrics/

想要达到这个目标,第一反应就是在页面挂载后如reactcomponentDidMountvue中的mounted进行自定义埋点,即可统计到相对准确的性能数据。

但是想要这么做需要项目使用了router统一管理路由,才能比较简单便捷的进行这个埋点,否则可能就要每个页面都做单独的埋点,或者对页面组件进行统一的封装,不是很通用。

而且还有一些更麻烦的场景,如:

  • 一些项目是使用目录方式管理路由的,没有统一的挂载入口
  • 同一个主应用不同子应用间切换,只走了挂载钩子,如何正常统计到对应的性能

下面我介绍一些我的团队实际场景中的一些解决方案。

实际方案

统一Router场景

我的团队统一router的都是react场景,其他场景请举一反三,或评论区补充。

如果你没有使用React.lazy进行页面组件的加载,那么只要在RoutercomponentDidMount时机进行自定义质量埋点即可.

那如果你使用了React.lazyrouter里面componentDidMount执行时,你的页面组件还没加载,那埋点的数据还是有误的,但是我也不想埋到每个页面上,那我还能怎么做呢?

借助Suspensefallback,示例代码如下:

import React, { Suspense, useEffect } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';

const startTime = window.performance.timing.navigationStart;

const RenderFallback = () => {
  useEffect(() => {
    return () => {
      const mountedRenderTime = +new Date() - startTime;
      // 在这里添加性能埋点代码
    };
  }, []);
  return <div>页面加载中,请稍后...</div>;
};

const Index = (): React.ReactElement => {
  return (
    <BrowserRouter>
      <Suspense fallback={<RenderFallback />}>
        <Routes>
          <Route 
            path="/test" 
            element={React.lazy(() => import('@/pages/test'))}>
          </Route>
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
};

export default Index;

什么是Suspense

主要是用来配合React.lazy进行使用的,在lazy的组件未加载好之前,通过fallback中的内容进行临时的填充如loading,详细内容参考:https://zh-hans.reactjs.org/docs/react-api.html#reactsuspense

通过目录自动产生路由的场景

这里以vuenuxt的场景举例,其他场景请举一反三或大家评论补充。

nuxt这个场景前面有提到,因为没有显式的路由组件进行声明,通过上面的思维就无法解决这个问题了,因此探索了一下其他的方案,我最终选择了为nuxt增加plugin的方式进行处理。

大概思路是为全局注入一个mixin,如果当前是page组件就在mounted的时候进行埋点操作。

实现步骤

  1. 我在nuxt.config.js文件中,增加了一个performance.ts的配置,并且关闭ssr

plugin文档请参考:https://nuxtjs.org/docs/configuration-glossary/configuration-plugins#the-plugins-property

plugins: [
    {
      src: '@/plugins/performance-tracker.ts',
      ssr: false
    }
]
  1. 编写performance.ts
import Vue from 'vue'

declare global {
  interface Window {
    __POWERED_BY_QIANKUN__: any
  }
}

const startTime = window.performance.timing.navigationStart
const startFromMainApp = !!window.__POWERED_BY_QIANKUN__

let SUBMIX_PERFORMANCE_TRACKER_DONE = false

Vue.mixin({
  mounted() {
    if (!(this as any).$parent && !SUBMIX_PERFORMANCE_TRACKER_DONE) {
      SUBMIX_PERFORMANCE_TRACKER_DONE = true
      const mountedRenderTime = +new Date() - startTime
      // 在这里添加性能埋点代码
    }
  }
})

在代码中有个我通过判断某个组件的this.$parent为空,来确定这是个是当前页面的page组件,而在埋点后,还要记录一下埋点状态,避免场景考虑的不周到带来的误埋点。

单页面应用性能统计

子应用间切换跟单页面跳转和上述情况还不太一样,因为会重新请求资源,子应用也要重新渲染,不关注性能也会有很大的隐患,但是乾坤本身给到的挂载钩子并不能满足统计的需要,那我如何统计呢?

上面两个方案解决了第一次进入应用时的解决方案,但是单页面应用和子应用间切换时又统计不到了。

可以在页面卸载的时候,在子应用全局注入一个新的页面起始时间,以此来进行子应用切换场景的性能埋点。

我还是以nuxtplugin代码为例。

import Vue from 'vue'

declare global {
  interface Window {
    __POWERED_BY_QIANKUN__: any
  }
}

let startTime = window.performance.timing.navigationStart
let startFromMainApp = !!window.__POWERED_BY_QIANKUN__

let SUBMIX_PERFORMANCE_TRACKER_PATH = window.location.pathname
let SUBMIX_PERFORMANCE_TRACKER_DONE = false

const startRenderTime = +new Date() - startTime

Vue.mixin({
  mounted() {
    if (!(this as any).$parent && !SUBMIX_PERFORMANCE_TRACKER_DONE) {
      SUBMIX_PERFORMANCE_TRACKER_DONE = true
      const mountedRenderTime = +new Date() - startTime
      
    }
  },
  destroyed() {
  	// 在当前页面组件销毁时进行性能埋点的前置准备
    if (SUBMIX_PERFORMANCE_TRACKER_PATH !== window.location.pathname) {
      SUBMIX_PERFORMANCE_TRACKER_PATH = window.location.pathname
      SUBMIX_PERFORMANCE_TRACKER_DONE = false
      // 这里可以在全局缓存一个状态表示下一个挂载的子应用是被切换过去的,这个也可以通过主应用标记
      startFromMainApp = false
      // 这里可以将startTime注入到全局给其他子应用进行获取,进行子应用切换的性能统计
      startTime = +new Date()
    }
  }
})

单页面应用因为往往做了应用级别的预加载,后续页面的加载性能不是一个很复杂的课题,课题往往在页面中的交互,所以性能这部分的性能根据场景也可以选择性忽略。


单页面应用其实和微前端关系不大,只是给子应用切换做一个铺垫,下面我们来看下子应用切换的性能统计。

子应用切换的性能统计

子应用切换的成本比单页面切换的成本高不少,因为新挂载的子应用的行为,会重新请求资源,且重新渲染,不关注性能也会有很大的隐患,但是乾坤本身给到的挂载钩子并不能满足统计的需要,那我如何统计呢?

那经过前面单页面应用性能统计的启发,统计是单页面应用统计的一个升级版,差异在于需要在主应用上更新启动时间。

这个时候如果你的项目有类似的需要就封装一个更新时间的API通过给到子应用,或者干脆将主应用的window传递给子应用(不推荐)。

window.updateStartTime = function(newTime) {
  window.SUBAPP_START_TIME = newTime;
};

// 给子应用配置要传递的方法
{
  props: {
    updateStartTime: window.updateStartTime,
    // otherProps
  }
}

结语

至此,虽然我列举的场景可能不全,但是通过上面的方案对有需要的同学应该能提供一些灵感,如果你有更好的方案可以在评论区给出你的想法。

自从上线了这套自定义性能统计的埋点,才发现之前的性能数据真的都是虚假繁荣,而没有很快发现,也是因为qiankun主应用的性能本身也是个问题,先说到这里,我要和同学们琢磨优化的事情去了。

项目代码规范

接手的业务和团队走过了一段适应期,想要对团队的代码风格开始进行规范,在渐进落地后整理出一套比较通用的方案。

这里主要描述结论,选型和团队中方案PK的流程暂且先跳过,后面单独起一个专题讨论。

我的团队技术栈主要是React+TS,比较有针对性,因此请选择性食用。

下面的规范主要分美化和lint两个部分,且只讨论自动的部分,不讨论风格指南相关,那我们直接开始。

Lint

什么是Lint?

什么是Lint?

In computer programming, lint is a Unix utility that flags some suspicious and non-portable constructs (likely to be bugs) in C language source code; generically, lint or a linter is any tool that flags suspicious usage in software written in any computer language. The term lint-like behavior is sometimes applied to the process of flagging suspicious language usage. Lint-like tools generally perform static analysis of source code.

ESLint

lint的核心毫无疑问是eslint,但是eslint中用哪些plugin就比较有讲究了。

必须:

可选:

StyleLint

css出现bug的几率比较小,不太容易引起人们的重视,但其实css亦是组件质量的重要部分,在很多场景仍然要消耗大量的开发成本,因此有个好的写css习惯是非常重要的。

我们的项目中并没有严格采用BEM(窟窿比较多,这个优先级很低),因此为了让有好习惯的同学看其他人的代码不会太痛苦,就采用了StyleLint,StyleLint也是前端项目必备的Lint。

这里不需要太多的配置,在我们的项目中仅使用了stylelint-config-idiomatic-order插件来控制大家代码的css样式顺序。

美化

Prettier

团队中对使不使用Prettier有一些争议,不建议使用的主要观点是认为eslint可以cover这一块了,那针对这个疑问和一些其他疑问下面给了一些思考和解答:

module.exports = {
  // 行宽,即一行内容超过配置的数字会触发换行行为
  printWidth: 80,
  // 缩进数
  tabWidth: 2,
  // 缩进是否用tab,如果不是则用空格
  useTabs: false,
  // 行尾是否有分号
  semi: true,
  // 是否要使用单引号
  singleQuote: false,
  /**
   * 对象中的Key展示引号的规则
   * as-needed: 仅在必要时展示
   * consistent: 如果对象中有属性需要引号,那么就都要使用
   * preserve: 使用你本来的输入
   */
  quoteProps: 'as-needed',
  // jsx 是否使用单引号
  jsxSingleQuote: false,
  /**
   * 末尾逗号
   * es5: 在es5中的数组和对象增加尾随逗号,TS中的类型参数不会追加
   * none: 没有尾随逗号
   * all: 尽可能使用尾随逗号
   */
  trailingComma: 'es5',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // 标签的尖括号是否要跟随到最后一行的末尾,而不是单独一行(不包括自闭合)
  bracketSameLine: false,
  /**
   * 箭头函数参数周围的括号
   * always: 始终需要括号
   * avoid: 尽可能省略括号
   */
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 是否需要在文件开头写 @prettier
  requirePragma: false,
  // 是否在文件开头插入 @prettier
  insertPragma: false,
  /**
   * 折行方式
   * always: 超过print width则换行
   * never: 每个文本块为一行
   * preserve: 保持输入的原样
   */
  proseWrap: 'preserve',
  /**
   * HTML的空白换行敏感度
   * css: 根据css的标准进行换行
   * strict: 所有标签周围的空格都会造成换行
   * ignore: 所有标签周围的空格都忽略
   */
  htmlWhitespaceSensitivity: 'css',
  /**
   * 换行符
   * lf: \n mac or linux or git repos
   * crlf: \r\n windows
   * cr: \r 不太常用
   * auto: 保持现有的换行
   */
  endOfLine: 'lf',
  // 是否强制每行单个属性
  singleAttributePerLine: false
};
  • 和其他lint配合使用,值得一提的是,配合使用的方式就是将prettier的配置整合到对应lint中,也就是说不使用prettier也将是一种可能。

工作流相关工具

除了增加lint和美化的配置来让辅助让代码符合规范,为了让代码更确切的能达到规范的目的,我们还希望可以在提交代码的时候进行一次自动验证并进行fix,因此还需要一些其他的工具。

Lint-staged

在代码提交前进行lint,以此来强制仓库的代码格式,这个lint是对当前提交增量的文件进行lint的,官网:https://github.com/okonet/lint-staged