Skip to main content

· 2 min read
刘述新

Applescript

是苹果公司开发的一种脚本语言,可以用来控制运行于Mac OS上的程序,也可以写成独立运行的Applet

可以用于控制多个项目一键启动,配合iTrem2 效果可以说是比较不错的

set project1 to " cd ~/project1"
set project2 to "cd ~/project2"
set project3 to "cd ~/project3"
set project4 to "cd ~/project4"
#applescript

tell application "iTerm"
activate
end tell

tell application "System Events"

tell process "iTerm"

# project1 1
keystroke project1
delay 1 -- 延时一秒后执行
key code 36
delay 1

keystroke "pnpm start"
delay 1 -- 延时一秒后执行
key code 36
delay 1


# project2 2
key code 2 using {command down}
delay 1

keystroke project2
delay 1 -- 延时一秒后执行
key code 36
delay 1

keystroke "pnpm start"
delay 1
key code 36
delay 1



# project3
key code 2 using {command down}
delay 1

keystroke project3
delay 1
key code 36
delay 1

keystroke "pnpm start:debug"
delay 1 -- 延时一秒后执行
key code 36
delay 1

# project4
key code 2 using {command down}
delay 1
keystroke project4
delay 1
key code 36
delay 1

keystroke "npm run start:saasstg"
delay 1 -- 延时一秒后执行
key code 36
delay 1
end tell

end tell

· 4 min read
刘述新

1、如何进行移动端开发

真机调试

做移动开发的时候大多数要跟手机界面打交道,也就是说,你只能在电脑上开发写代码,最终的效果确是在另一个终端虽然各种浏览器为开发者提供了很多模拟手机设备的功能,这些功能总体来说基本可以满足我们对于调试移动设备的需求,但是模拟终究是模拟,无法完全还原场景。所以,必须在想办法在真实的机子上测试我们的代码最终的执行效果。办法当然是有的,下面是露珠在开发过程中用到的若干真机调试方法:

Chrome 真机调试

chrome 必须有硬件的接触, 首先确保手机上和 PC 机上装有最新版本的 chrome 浏览器,然后将数据线将两台设备连接起来。在 PC 机上打开 chorme,输入 chrome://inspect, 于是你就可以看到自己的设备和打开的网页了: 调试界面

设备网址下出现 inspect、reload、close 等选项,点击 inspect 会弹出一个窗口,阁下就可以在窗口中愉快地去调试了。 模拟界面

Chrome 手机调试工具优点:

在熟悉的开发模式下调试,操作比较简单。个人觉得 UC 在这方面胜过 chrome。可以跨平台,在 ios 上也可以用。

Chrome 手机调试工具缺点:

有目共睹,比起 UC 来,它步骤比较繁琐,不能 wifi 调试,必须指定手机和 PC 浏览器的类型(都是 chrome),这种模式无法调试微信或者其他 app 内置浏览器。

Safari

手机端:设置 → Safari → 高级 → Web 检查器 → 开 mac 端:Safari → 偏好设置 → 高级 → 在菜单栏中显示“开发”菜单 在 OS X 中启动 Safari 之后,以 USB 电缆正常接入 iOS 设备,并在此移动设备上启动 Safari。此时点击计算机上的 Safari 菜单中的“开发”,可以看到有 iOS 设备的名称显示,其子菜单项即为移动设备上 Safari 的所有标签页,点击任意一个开始调试。 苹果系列优点:

便捷,简单,高端大气上档次,可以调试外壳包裹的浏览器如微信。

苹果系列缺点:

设备限严重依赖其公司产品。

· 28 min read
刘述新

Docusaurus Plushie

Hello 大家好~~,今天给大家带来的分享是关于如何从零实现一个前端监控 SDK,(前段时间参与了性能监控平台的共建,有一些自己的心得,故此分享出来,共同进步)

接下来我将通过以下几个步骤带大家完成前端监控 SDK

一、 为什么要做前端监控,都需要监控什么?

场景:

  1. 客户手机端白屏了,系统无法使用,直到领导找到你,你紧张的汗都下来了,焦急的查着代码,但没有头绪
  2. 点击某个按钮在手机端的执行逻辑存在兼容性,而你缺不知道,导致部分客户流失
  3. 接口请求耗时较多,用户体验差,我们却全然不知

都需要做哪些监控

  • ✏️ 前端错误
  • 🔧 页面性能
  • ✏️ API 性能

知道了为什么要做监控,以及要监控 什么,那么我们接下来看要要如何实现,各类监控呢

二、 如何实现各类监控?

1、前端错误监控

Preview

如上图所示,前端错误捕获方式,主要分为以上四大类,

第一种是我们经常会遇到过的 JS 运行时错误, 一般我们采用 window.onerror 方法就可以对其进行捕获,该方法会返回 5 个参数,分别是,
  • message:错误信息(字符串)。可用于 HTML onerror=""处理程序中的 event。
  • source:发生错误的脚本 URL(字符串)
  • lineno:发生错误的行号(数字)
  • colno:发生错误的列号(数字)
  • error:Error 对象(对象)

另种运行时错误是用户利用 console.error 打印出来的我们也要进行拦截捕获。

第二种 是页面中资源(诸如图片img,脚本script)加载错误,找不到资源啦,资源加载超时啦,可以通过 window.addEventListener('error', function(event) ) 进行捕获,返回的event 中,包含

  • localName 资源名称
  • type 资源类型
  • src 资源源地址

第三种是当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件。可以通过 window.addEventListener('unhandledrejection', function(event) ) 进行捕获,返回的event 中,包含

  • reason reject 的原因

第四种是异步请求的错误,浏览器中发送ajax 请求共有两种方式分别是 XMLHttpRequest 和 Fetch, 所以相应的也分两种捕获类型。

  • 对于 XHR 我们可以通过监听它的 onerror 事件来捕获异常错误,通过监听 onabort 事件来监听中断请求异常,如果请求顺利返回,我们通过判断返回的状态码 status 是否是小于 200 或者大于等于 300 来判断这次请求是否失败。
  • 对于 Fetch 来说,我们可以通过重写 catch 方法来获得发生的错误,或者判断返回的 response.ok 是否为 false 来进行错误上报

2、页面性能监控

Preview
如上图所示,当页面load 之后我们通过调用window 上的performance.timing 对象上的相关属性进行计算,就可以获得我们想要的相关时间

3、API 性能监控

Preview
如上图所示,我们先来看

第一种情况,通过重写XMLHttpRequest对象的实现逻辑来达到监听,我们首先通过重写open 属性来进行注册请求基本信息,然后重写,onreadystatechange,onload,和onloaded 来监听请求成功返回, 接下来看

第二种情况Fetch,通过监听then 方法返回的response ,来获取status 和 获取响应大小

上报时机的思考

思考题

1、我们每次上报都需要发送请求吗?

2、如果需要进行合并,如何进行上报合并呢?

优化前:

common-report

解决方案

1、上报场景分为两类,初始化页面时,和后续ajax 操作

2、初始化页面:对ajax 请求,错误信息,分别进行缓存,在一定时间内,直到所有ajax 请求 都完成以及页面load,才进行整体上报

3、后续ajax 操作:对ajax 请求,错误信息,分别进行缓存,在一定时间内,直到所有ajax 请求 都完成,才进行整体上报

优化后:

common-report

三、 数据的上报和收集

首先上报方式分为两大类

  • SDK 监听特定事件,进行上报
  • 用户通过自己的技术手段(例如 try,catch,React ErrorBoundary ),通过调用 athena.addError(ErrorOptions); 来上报

上报类型分为三大类

  1. PAGE = 1 //页面级性能上报

  2. AJAX = 2 //页面 API 性能上报

  3. ERROR = 3 //页面内错误信息上报

info

接下来我们就针对具体场景进行分析

场景一:页面级性能上报

通过监听 load事件(标识页面资源已经加载完成)来进行上报, 以及将相关静态资源进行上报

🍎 1、 performance.timing

🥔 2、 performance.getEntriesByType('resource')

场景二:页面错误上报

通过监听一系列全局事件

🍎 1、 利用 window.addEventListener('error', listenErrorEvent, true); 捕获 img,script,css,jsonp 相关错误

🍌 2、 重写 window.onerror 用于捕获运行时错误

🍇 3、 当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件。window.addEventListener('unhandledrejection', unhandledrejectionEvent);

🍉 4、 重写console.error 方法调用

场景三:XMLHttpRequest 性能和错误上报

通过重写XMLHttpRequest 对象

🍎 1、 利用 window.realXhr = XMLHttpRequest; 缓存原始XMLHttpRequest对象

🍌 2、 重写 XMLHttpRequest 对象创建过程,

🍇 3、 重写方法 overwriteMethod ,重写属性 overwriteAttributes

XHR 相关事件

Preview

XHR 重写流程图

Preview

Hooks 示意图

Preview

场景四:Fetch 性能和错误上报

通过重写fetch 对象

🍎 1、 利用 window.realFetch = window.fetch; 缓存原始Fetch对象

🍌 2、 重写 fetch函数调用过程,

🍇 3、 重写fetch的then方法 ,处理成功上报 ,重写fetch 的 catch方法处理失败错误上报

Preview

完整上报协议

tip

对于前端错误和性能监控来说,保证数据的准确性和完整性是非常重要的,这非常依托上报数据的稳定性和明确性,所以让我们一起来看一下这份上报协议

1、ReportData
上报参数描述类型示例备注
appId应用唯一标识stringax123dd32233
domain上报的地址stringhttp://www.athena.com/test
url当前页面地址 urlstringhttp://www.teest.com/test
markUser用户唯一标识string102346
markUv统计 UVstring102346
preUrl该页面来自的域名stringhttp://www.athena.com
performance性能相关数据Perform
type上报类型number1PAGE = 1 页面级性能上报; AJAX = 2 页面 API 性能上报;ERROR = 3 页面内错误信息上报;
time上报时间戳number1638237891804
screenwidth屏幕宽度number1559
screenheight屏幕高度number327
isFristIn是否第一次进入页面booleanfalse
resourceList资源相关列表Array<ResourceList>
errorList错误列表Array<ErrorList>
2、ErrorList
上报参数描述类型示例备注
n错误类型stringresourcejs, resource,ajax // ajax 请求错误上报
t当前时间戳number1638240521575
msg错误信息string[React Intl] Missing message: \"menu.manage-group\" for locale: \"en-US\", using default message as fallback.//
method请求方法stringGET
data附加信息ErrorData
{resourceUrl: "http://localhost:8000/home/15/group"} 
3、ErrorData
上报参数描述类型示例备注
target资源名称stringscriptscript, link
type类型stringerror
resourceUrl报错资源地址string"http://localhost:4005/face/app.css"
line报错触发所在行number200
col报错触发所在列number12
stack错误栈信息string
text响应信息stringOK
status响应状态码number200
4、Perform
上报参数描述类型示例备注
dnstDNS 解析时间number
tcptTCP 建立时间number
wit白屏时间number
domtdom 渲染完成时间number
lodt页面 onload 时间number
radt页面准备时间number
rdit页面重定向时间number
uodtunload 时间number
reqtrequest 请求耗时number
andt页面解析 dom 耗时number
5、ResourceList
上报参数描述类型示例备注
name资源名称stringhttp://localhost:8000/@@/devScripts.js
method请求方法methodGET
type资源类型stringscriptscript,img,xmlhttprequest, fetchrequest
duration加载时间string55.90
decodedBodySize资源大小number79
nextHopProtocol协议名称string
options其他项ResourceOptions{ }
6、ResourceOptions
上报参数描述类型示例备注
resourceUrl资源名称stringhttp://localhost:8000/@@/devScripts.js
text响应状态文本stringOK
status响应码number200200,400...

四、 SDK 整体架构介绍

基于以上的前提,故此产生了一个完整的 SDK 架构如下

Preview

五、 共享-输出 npm & 官网建设

输出 npm

info

那么我们如何把我们的成果与别人共享呢,让别人能够更快捷的接入呢? 故此我们封装成一个 npm 包来供大家使用

  1. 构建工具选择 TSDX:https://github.com/jaredpalmer/tsdx
  2. 构建工具支持 Rollup 进行库打包,可以输出三种模块类型文件,cjs,esm,umd

官网建设

info

建立官网有助于产品的宣传与接入,如何选择更高效的创建官网的工具,就很重要,故此,我调研了市面上存在的各类静态网站生成器方案如下

方案简介优点缺点地址GitHub Star
VuePressVuePress 是一款由 vue 驱动的,markdown 为项目结构中心的高性能静态网站生成器,是 vue 生态的组成部分,对集成 vue 项目的 API 文档比较契合支持 Vue 定制化对 React 不友好https://github.com/vuejs/vuepress19.8k
Docusaurus 2Docusaurus 是 Facebook 开源的基于 React 的静态网站生成器支持 React 定制化Vue 不友好https://github.com/facebook/docusaurus29.5k
GitBookGitBook 是一款可多人协作的文档平台,项目结构清晰,适合编写 API 接口文档以及内部知识库,简单快速UI 层面的自定义较困难https://github.com/GitbookIO/gitbook24.3k
GatsbyGatsby Gatsby 不仅仅是一个静态网站生成器,它更是一个渐进式 Web 应用生成器灵活,支持 graphQL,对 React 友好太灵活,不易上手https://github.com/gatsbyjs/gatsby52.1k

基于灵活性,技术栈,上手难度,所以我们选择了 Docusaurus 作为我们官网的建站工具,也就是这篇博客所在的页面

· 2 min read
刘述新

Hello 大家好,今天给大家带来的文章是关于如何封装一个对不定高度的内容,显示隐藏的 react 动画组件

import React, { FC, useRef, useEffect, useState, CSSProperties } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { Transition } from 'react-transition-group';
import type { Property } from 'csstype';
import { debounce } from 'lodash';
/**
*
* @returns 动画版显示隐藏组件
*/
interface AnimationShowProps {
// default 300ms
duration?: number;
// whether visible
visible: boolean;
timingFunction?: Property.AnimationTimingFunction;
style?: CSSProperties;
}
const resize = (setConHeight: () => void) => debounce(setConHeight, 300);

const AnimationShow: FC<AnimationShowProps> = ({
duration = 300,
timingFunction = 'linear',
visible,
children,
style,
}) => {
const [height, setHeight] = useState(0);
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// listen container resize
const setConHeight = (): void => {
const clientRect = divRef.current?.getBoundingClientRect();
if (clientRect) {
setHeight(clientRect ? clientRect.height : 0);
}
};

const resizeObserver = new ResizeObserver(resize(setConHeight));

resizeObserver.observe(divRef.current as HTMLDivElement);

return function cleanup() {
resizeObserver.disconnect();
};
}, []);

//css transition

const defaultStyle = {
transition: `height ${duration}ms ${timingFunction}`,
height: 0,
overflow: 'hidden',
};

const transitionStyles = {
entering: {
height,
},
entered: {
height,
},
exiting: { height: 0 },
exited: { height: 0 },
};
// css transition
return (
<Transition in={visible} timeout={duration}>
{(state) => (
<div
style={{
...style,
...defaultStyle,
...transitionStyles[state],
}}
>
<div ref={divRef} style={{ overflow: 'hidden' }}>
{children}
</div>
</div>
)}
</Transition>
);
};

export default AnimationShow;

· 2 min read
刘述新
import React, { FC, useRef, useEffect, useState, CSSProperties } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { Transition } from 'react-transition-group';
import type { Property } from 'csstype';
import { debounce } from 'lodash';
/**
*
* @returns 动画版显示隐藏组件
*/
interface AnimationShowProps {
// default 300ms
duration?: number;
// whether visible
visible: boolean;
timingFunction?: Property.AnimationTimingFunction;
style?: CSSProperties;
}
const resize = (setConHeight: () => void) => debounce(setConHeight, 300);

const AnimationShow: FC<AnimationShowProps> = ({
duration = 300,
timingFunction = 'linear',
visible,
children,
style,
}) => {
const [height, setHeight] = useState(0);
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// listen container resize
const setConHeight = (): void => {
const clientRect = divRef.current?.getBoundingClientRect();
if (clientRect) {
setHeight(clientRect ? clientRect.height : 0);
}
};

const resizeObserver = new ResizeObserver(resize(setConHeight));

resizeObserver.observe(divRef.current as HTMLDivElement);

return function cleanup() {
resizeObserver.disconnect();
};
}, []);

//css transition

const defaultStyle = {
transition: `height ${duration}ms ${timingFunction}`,
height: 0,
overflow: 'hidden',
};

const transitionStyles = {
entering: {
height,
},
entered: {
height,
},
exiting: { height: 0 },
exited: { height: 0 },
};
// css transition
return (
<Transition in={visible} timeout={duration}>
{(state) => (
<div
style={{
...style,
...defaultStyle,
...transitionStyles[state],
}}
>
<div ref={divRef} style={{ overflow: 'hidden' }}>
{children}
</div>
</div>
)}
</Transition>
);
};

export default AnimationShow;