跳到主要内容

引言

· 阅读需 1 分钟
XOne
小站站长
提示

主要供程序员使用,客户朋友请忽略这里。

主要记录一些技术文档或者学习心得,项目经验有时也会写在这里。

electron学习笔记

· 阅读需 5 分钟
XOne
小站站长

Webpack + Typescript

npm init electron-app@latest my-new-app -- --template=webpack-typescript

React代码需要,增加jsx支持

{
"compilerOptions": {
"jsx": "react-jsx"
}
}

npm 换成 pnpm 需要 install

pnpm install @electron-forge/plugin-base @electron-forge/maker-base @types/webpack --save-dev

路由

pnpm install react-router-dom

less

pnpm install less less-loader --save-dev

webpack.rules.ts

{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
mode: 'local',
localIdentName: '[name]__[local]___[hash:base64:5]',
},
importLoaders: 1,
},
},
'less-loader',
],
}

antd pro

pnpm install antd --save
pnpm install @ant-design/pro-components --save
pnpm install @ant-design/pro-card
pnpm install @ant-design/pro-descriptions
pnpm install @ant-design/pro-field
pnpm install @ant-design/pro-form
pnpm install @ant-design/pro-layout
pnpm install @ant-design/pro-list
pnpm install @ant-design/pro-provider
pnpm install @ant-design/pro-skeleton
pnpm install @ant-design/pro-table
pnpm install @ant-design/pro-utils

编译使用 babel-plugin-import 优化 antd 打包体积

pnpm install babel-plugin-import --save-dev
pnpm install @ant-design/icons --save

数据存储 electron-store

conf 是处理类型报错安装

pnpm install electron-store conf

tsconfig.json中模版默认是:"moduleResolution": "node" ,electron-forge似乎很难或者不支持配置"moduleResolution": "node16" 或者其他属性值,这里采用投机取巧的方式配置别名,修复electron-store的类型错误
tsconfig.json

{
"compilerOptions": {
"paths": {
"conf": ["node_modules/conf/dist/source"]
}
}
}

设置路径别名

tsconfig.json

{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}

webpack.renderer.config.ts

{
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
}

除了 tsconfig.json webpack.renderer.config.ts 配置别名外 还需要修复 eslint 提示

pnpm install eslint-import-resolver-typescript --save-dev

.eslintrc.json

"settings": {
"import/resolver": {
"typescript": {}
}
}

进程通信(‌IPC)‌

使用ipcMain和ipcRenderer模块:‌主进程通过ipcMain模块监听事件,‌渲染进程通过ipcRenderer模块发送事件,‌实现双向通信。‌
使用remote模块:‌允许在渲染进程中直接调用主进程中的模块和方法,‌实现简单的跨进程通信‌。
使用webContents模块:‌通过此模块可以直接向指定的渲染进程发送消息,‌实现通信‌。‌
使用进程之间的共享变量:‌通过建立全局变量或共享内存来实现跨进程通信‌。‌

使用ipcMain和ipcRenderer模块

渲染进程 -> 主进程
方式一:

// 渲染进程 发出事件
ipcRenderer.send("channel1", 1, 2, 3);
// 主进程 监听事件
ipcMain.on("channel1", (e, a, b, c) => {
console.log("监听到渲染进程的发出的事件(callback)", a, b, c);
})

方式二:

// 渲染进程 发出事件
ipcRenderer.invoke("channel2", 1, 2, 3);
// 主进程 监听事件
ipcMain.handle("channel2", (e, a, b, c) => {
console.log("监听到渲染进程的发出的事件(Promise)", a, b, c)
})

主进程 -> 渲染进程
方式一:

// 渲染进程 监听事件
ipcRenderer.on(channel, handler)
// 主进程 发出事件
mainWindow.webContents.send("channel3");

网络请求

pnpm install axios
// forge.config.ts 配置Content-Security-Policy 不是配置html中
const config: ForgeConfig = {
plugins: [
new WebpackPlugin({
devContentSecurityPolicy: "default-src 'self'; connect-src *; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
devServer: {
//配置代理 解决跨域
proxy: {
'/login': {
target: 'http://localhost:8080', // 另一个API服务器地址
changeOrigin: true,
},
},
},
}),
],
};


//对应axios拓展文件 requestUtil.ts
const request = axios.create({
baseURL: 'http://localhost:4000',
});

requestUtil.ts 文件详细如下

import axios from 'axios';

const request = axios.create({
baseURL: 'http://localhost:4000',
timeout: 1000,
headers: {
'X-Custom-Header': 'foobar',
// 'Content-Type': 'application/json',
},
// withCredentials: true, //保持跨域请求的cookie信息
});

request.interceptors.request.use(
function (config) {
const token = localStorage.getItem('token');
config.headers.Authorization = token ? `Bearer ${token}` : '';
return config;
},
function (error) {
return Promise.reject(error);
}
);

request.interceptors.response.use(
function (response) {
// 对响应数据做一些处理
return response;
},
function (error) {
// 对响应错误做处理
return Promise.reject(error);
}
);

export default request;

旧Bull使用教程

· 阅读需 20 分钟
XOne
小站站长

基本概念

Bull 是什么?

任务列队一般用于异步处理视频转码,发送短信等耗时任务,不至于 API 接口连接卡死

Bull 是一个 Node 库,它基于 redis实现了快速而强大的队列系统。

尽管可以使用 Redis 命令直接实现队列,但是该库提供了一个 API,该 API 可以处理所有底层细节并丰富了 Redis 基本功能,因此可以轻松处理更复杂的用例。

如果您不熟悉队列,您可能会想知道为什么需要它。队列可以用一种优雅的方式解决许多不同的问题,比如在微服务之间创建健壮的通信通道来平滑地处理 CPU 高峰,或将繁重的工作从一台服务器转移到许多较小的工作区间等。

Redis

· 阅读需 3 分钟
XOne
小站站长

安装和启动

在 Linux 上安装 Redis

  1. 通过包管理器安装 Redis(例如,使用 apt-get) sudo apt-get update sudo apt-get install redis-server

  2. 启动 Redis 服务: sudo service redis-server start

Three.js

· 阅读需 5 分钟
XOne
小站站长
信息

本文档是笔者Three.js的学习笔记

介绍

Three.js 是一个用于创建和显示 3D 图形的 JavaScript 库。它建立在 WebGL 技术之上,简化了在网页中创建复杂 3D 场景的过程。本文将介绍一些学习 Three.js 的基础知识和常见技巧。

开始

要开始学习 Three.js,可以按照以下步骤进行:

  1. 安装 Three.js: 可以从 Three.js 的官方网站 下载最新版本的库文件,或者通过 npm 进行安装。
  2. 创建场景: 在 HTML 文件中引入 Three.js 库文件,并创建一个场景对象。
  3. 添加对象: 向场景中添加物体,如立方体、球体等。
  4. 渲染场景: 创建一个渲染器对象,并将场景和相机传递给渲染器以渲染场景。

基本概念

学习 Three.js 需要了解一些基本概念:

  • 场景 (Scene): 代表了一个 3D 场景,包含了所有的物体、光源和相机。
  • 相机 (Camera): 定义了观察者的视角,决定了场景中哪些部分会显示在屏幕上。
  • 渲染器 (Renderer): 负责将场景渲染到 HTML 页面上的 Canvas 元素中。
  • 物体 (Object): 在场景中可以看到的所有实体,如几何体、网格等。

示例代码

以下是一个简单的 Three.js 示例代码:

import * as THREE from 'three';

// 创建场景
const scene = new THREE.Scene();

// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建立方体
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// 渲染场景
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();

进阶学习

一旦掌握了基本概念和操作,可以深入学习以下内容:

  • 材质与光照: 了解不同类型的材质以及光照对场景的影响。
  • 纹理与贴图: 如何将图片或纹理应用到物体表面上。
  • 动画与交互: 创建动画效果和与用户交互的方法。
  • 优化与性能: 提高场景的性能和渲染效率。

材质与光照

Three.js 提供了多种不同类型的材质,如 MeshBasicMaterialMeshPhongMaterial 等,每种材质都具有不同的光照效果。通过调整材质的参数,您可以实现各种视觉效果,如漫反射、镜面反射等。

纹理与贴图

使用纹理可以为物体赋予更加生动的外观。您可以将图片或纹理加载到材质中,并应用到物体的表面上。通过调整纹理的映射方式和重复次数,可以创建出丰富多彩的效果。

动画与交互

通过 Three.js 中提供的动画库,您可以创建物体的运动、旋转等动画效果。同时,可以监听用户的交互事件,如鼠标点击、键盘输入等,实现与用户的互动体验。

优化与性能

在设计复杂的 3D 场景时,需要考虑性能优化以确保流畅的渲染效果。您可以使用 Three.js 提供的优化技巧,如减少三角形数量、使用缓存、合并几何体等方法来提高性能。

通过不断练习和尝试这些进阶内容,您将能够更加熟练地使用 Three.js,并创作出更具有吸引力和交互性的 3D 场景。

ts-decorator

· 阅读需 27 分钟
XOne
小站站长
提示

在看本文前最好先看一下《阮一峰-es6 中的装饰器》

装饰器用于给类,方法,属性以及方法参数等增加一些附属功能而不影响其原有特性。其在 Typescript 应用中的主要作用类似于 Java 中的注解,在 AOP(面向切面编程)使用场景下非常有用。

面向切面编程(AOP) 是一种编程范式,它允许我们分离横切关注点,藉此达到增加模块化程度的目标。它可以在不修改代码自身的前提下,给已有代码增加额外的行为(通知)

装饰器一般用于处理一些与类以及类属性本身无关的逻辑,例如: 一个类方法的执行耗时统计或者记录日志,可以单独拿出来写成装饰器。

看一下官方的解释更加清晰明了

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

如果有使用过 spring boot 或者 php 的 symfony 框架的话,就基本知道装饰器的作用分别类似于以上两者注解和 annotation,而 node 中装饰器用的比较好的框架是 nest.js。不过不了解也没关系,接下来我就按我的理解讲解一下装饰器的使用。

不过目前装饰器还不属于标准,还在建议征集的第二阶段,但这并不妨碍我们在 ts 中的使用。只要在 tsconfig.json中开启 experimentalDecorators编译器选项就可以愉快地使用啦^^

{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}

基本概念

可能有些时候,我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。

比如,我们要在用户登录的时候记录一下登录时间

const logger = (now: number) => console.log(`lasted logged in ${now}`);

class User {
async login() {
await setTimeout(() => console.log('login success'), 100);
logger(new Date().valueOf());
}
}

以上代码把记录日志的代码强行写入登录的逻辑处理,这样代码量越高则代码越冗余。我们需要把日志逻辑单独拿出来,使 login 方法更专注于处理登录的逻辑,接下去我们用高阶函数模拟一下装饰器的原理,以便于后面更好的理解装饰器。

/**
* 使用高阶函数
* 柯里化解构登录与日志记录
*/

type DecoratorFunc = (
target: any,
key: string,
descriptor: PropertyDescriptor,
) => void;

// 模拟的装饰器工厂函数
const createDecorator =
(decorator: DecoratorFunc) => (Model: any, key: string) => {
// 获取即将使用装饰器的类原型
const target = Model.prototype;
// 获取这个原型上某个方法的描述
const descriptor = Object.getOwnPropertyDescriptor(target, key);
// 更改描述,生成新的方法
decorator(target, key, descriptor);
};

const logger: DecoratorFunc = (target, key, descriptor) =>
// 将修改后的函数重新定义到原型链上
Object.defineProperty(target, key, {
...descriptor,
value: async (...args: any[]) => {
try {
return descriptor.value.apply(this, args); // 调用之前的函数
} finally {
const now = new Date().valueOf();
console.log(`lasted logged in ${now}`);
}
},
});

class User {
async login() {
console.log('login success');
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
}

export const exp1 = () => {
console.log();
console.log(
'-----------------------示例1:高阶函数柯里化(装饰器内部原理)-----------------------',
);
console.log(
'-----------------------实现登录和日志记录解耦-----------------------',
);
console.log();
const loggerDecorator = createDecorator(logger);
loggerDecorator(User, 'login');
const user = new User();
user.login();
console.log();
console.log('-----------------------示例1:执行完毕-----------------------');
};

// 控制台输出
// login success
// 停顿100ms
// lasted logged in 1571771681793

了解了以上概念,接下去让我们学习真正的装饰器。

装饰器类型

TS 中的装饰器有几种类型,如下:

  • 参数装饰器

  • 方法装饰器

  • 访问符装饰器

  • 属性装饰器

  • 类装饰器

以上每中装饰器分别可以作用于类原型(prototype属性)和类本身

类装饰器

TS 官方文档中举了一个类装饰器的例子,也可以看一下。类装饰器其实就是把我们本身的类传入装饰器注解中,并对这个类或类的原型进行一些处理,仅此而已。例如:

const HelloDerorator = <T extends new (...args: any[]) => any>(
constructor: T,
) => {
return class extends constructor {
newProperty = 'new property';

hello = 'override';

sayHello() {
return this.hello;
}
};
};

@HelloDerorator
export class Hello {
[key: string]: any; // 此处用于防止eslint提示sayHello方法不存在

hello: string;

constructor() {
this.hello = 'test';
}
}

const exp2 = () => {
console.log(
'-----------------------示例2:简单的类装饰器-----------------------',
);
console.log(
'-----------------------动态添加一个sayHello方法以及覆盖hello的值-----------------------',
);
console.log();
const hello = new Hello();
console.log(hello.sayHello());
console.log();
console.log('-----------------------示例2:执行完毕-----------------------');
};


// 控制台打印 override

装饰器工厂

上面的方法我们为UserService添加了一个HelloDerorator装饰器,这个装饰器的属性将覆盖UserService的默认属性。为了方便给装饰器添加其它参数,我们把HelloDerorator改造成为一个装饰器工厂,如下:

const SetNameDerorator = (firstname: string, lastname: string) => {
const name = `${firstname}.${lastname}`;
return <T extends new (...args: any[]) => any>(target: T) => {
return class extends target {
_name: string = name;

getMyName() {
return this._name;
}
};
};
};

@SetNameDerorator('jesse', 'pincman')
class UserService {
getName() {}
}

const exp3 = () => {
console.log();
console.log(
'-----------------------示例3:装饰器工厂-----------------------',
);
console.log(
'-----------------------通过继承方式 重载getName方法-----------------------',
);
console.log();
const user = new UserService();
console.log(user.getName());
console.log();
console.log('-----------------------示例3:执行完毕-----------------------');
};

// 控制台打印 jesse.pincman

其它用法

我们还可以对类原型链property上的属性/方法和类本身的静态属性/方法进行赋值或重载操作,还可以重载构造函数,如下:

type UserProfile = Record<string, any> & {
phone?: number;
address?: string;
};

const ProfileDecorator = (profile: UserProfile) => (target: any) => {
const Original = target;
let userinfo = '';
Object.keys(profile).forEach((key) => {
userinfo = `${userinfo}.${profile[key].toString()}`;
});
// 添加一个原型属性
Original.prototype.userinfo = userinfo;
// 使用函数创建一个新的类(类构造器),返回值为传入类的对象,这样就重载了构造函数
function constructor(...args: any[]) {
console.log('contruct has been changed');
return new Original(...args);
}
// 赋值原型链
constructor.prototype = Original.prototype;
// 添加一个静态属性
// constructor.myinfo = `myinfo ${userinfo}`;
return constructor as typeof Original;
};

// 因为静态属性是无法通过[key: string]: any;获取类型提示的,所以这里添加一个接口用于动态各类添加静态属性
interface StaticUser {
new (): UserService;
myinfo: string;
}

@ProfileDecorator({ phone: 133, address: 'zhejiang' })
class ProfileService {}

const exp4 = () => {
console.log();
console.log(
'-----------------------示例4:修类的构造函数,原型属性,静态属性等-----------------------',
);
console.log(
'-----------------------设置原型属性值,重载构造防反,添加静态属性-----------------------',
);
console.log();
// console.log((ProfileService as unknown as StaticUser).myinfo);
const profile = new ProfileService();
console.log((profile as any).userinfo);
console.log();
console.log('-----------------------示例4:执行完毕-----------------------');
};

// 打印静态属性则控制台输出 myinfo .133.zhejiang
// 控制台输出 contruct has been changed
// 控制台输出 .133.zhejiang

属性装饰器

属性装饰器一般不单独使用,主要用于配合类或方法装饰器进行组合装饰

参数

属性装饰器函数有两个参数:

target

对于普通属性,target 就是当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是 Employee.prototype

对于静态属性,target 就是当前对象的类

propertyKey

属性的名称

使用示例

const userRoles: string[] = [];

// 通过属性装饰器把角色赋值给userRoles
const RoleDerorator = (roles: string[]) => (target: any, key: string) => {
roles.forEach((role) => userRoles.push(role));
};

// 根据userRoles生成Roles对象并赋值给类原型的roles属性
const SetRoleDerorator = <
T extends new (...args: any[]) => {
[key: string]: any;
},
>(
constructor: T,
) => {
const roles = [
{ name: 'super-admin', desc: '超级管理员' },
{ name: 'admin', desc: '管理员' },
{ name: 'user', desc: '普通用户' },
];
return class extends constructor {
constructor(...args: any) {
super(...args);
this.roles = roles.filter((role) => userRoles.includes(role.name));
}
};
};

@SetRoleDerorator
class UserEntity {
@RoleDerorator(['admin', 'user'])
roles: string[] = [];
}

export const exp5 = () => {
console.log();
console.log(
'-----------------------示例5:属性装饰器-----------------------',
);
console.log(
'-----------------------使用装饰器根据权限过滤用户列表-----------------------',
);
console.log();
const user = new UserEntity();
console.log(user.roles);
console.log();
console.log('-----------------------示例5:执行完毕-----------------------');
};

// 控制台输出 [ { name: 'admin', desc: '管理员' }, { name: 'user', desc: '普通用户' } ]

方法装饰器

在一开始我们介绍了装饰器的原理,其实这就是方法装饰器的原始实现。与属性装饰器不同的是,方法装饰器接受三个参数

方法装饰器重载的时候需要注意的一点是定义 value 务必使用 function,而不是箭头函数,因为我们在调用原始的旧方法使用会使用到 this,如:method.apply(this, args),这里的 this 指向需要 function 来定义,具体原因可参考我的另一篇文章apply,bind,call 使用

参数

target

对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

key

方法名称

descriptor: PropertyDescriptor

方法的属性描述符(最重要的参数)

属性描述符

属性描述包含以下几个属性

  • configurable?: boolean; // 能否使用 delete、能否修改方法特性或修改访问器属性

  • enumerable?: boolean; 是否在遍历对象的时候存在

  • value?: any; 用于定义新的方法代替旧方法

  • writable?: boolean; 是否可写

  • get?(): any; // 访问器

  • set?(v: any): void; // 访问器

接下来我们使用方法装饰器修改一开始的装饰器原理中的登录日志记录器

const loggerDecorator = () => {
return function logMethod(
target: any,
propertyName: string,
propertyDescriptor: PropertyDescriptor,
): PropertyDescriptor {
const method = propertyDescriptor.value;

// 重载方法
propertyDescriptor.value = function async(...args: any[]) {
try {
return method.apply(this, args); // 调用之前的函数
} finally {
const now = new Date().valueOf();
console.log(`lasted logged in ${now}`);
}
};
return propertyDescriptor;
};
};

class UserService {
@loggerDecorator()
async login() {
console.log('login success');
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
}

export const exp6 = () => {
console.log();
console.log(
'-----------------------示例6:方法装饰器-----------------------',
);
console.log(
'-----------------------使用装饰器重写示例1-----------------------',
);
console.log();
const user = new UserService();
user.login();
console.log();
console.log('-----------------------示例6:执行完毕-----------------------');
};

// 控制台输出结果与前面的示例1相同

参数装饰器

一个类中每个方法的参数也可以有自己的装饰器。

与属性装饰器类似,参数装饰器一般不单独使用,而是配合类或方法装饰器组合使用

参数

  1. target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

  2. key:方法名称

  3. index: 参数数组中的位置

比如我们需要格式化一个方法的参数,那么可以创建一个转么用于格式化的装饰器

// 参数格式化配置
const parseConf: ((...args: any[]) => any)[] = [];

export const parse =
(parseTo: (...args: any[]) => any) =>
(target: any, propertyName: string, index: number) => {
parseConf[index] = parseTo;
};

// 在函数调用前执行格式化操作
export const parseDecorator = (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor => {
console.log('开始格式化数据');
return {
...descriptor,
value(...args: any[]) {
// 获取格式化后的参数列表
const newArgs = args.map((v, i) =>
parseConf[i] ? parseConf[i](v) : v,
);
console.log('格式化完毕');
return descriptor.value.apply(this, newArgs);
},
};
};

export interface UserType {
id: number;
username: string;
}

class UserService {
private users: UserType[] = [
{ id: 1, username: 'admin' },
{ id: 2, username: 'pincman' },
];

getUsers() {
return this.users;
}

@parseDecorator
delete(@parse((arg: any) => Number(arg)) id: number) {
this.users = this.users.filter((userObj) => userObj.id !== id);
return this;
}
}

export const exp78 = () => {
console.log();
console.log(
'-----------------------示例7:参数装饰器-----------------------',
);
console.log('-----------------------格式化参数-----------------------');
console.log();
const userService = new UserService();
userService.delete(1);
console.log(userService.getUsers());
console.log();
console.log('-----------------------示例7:执行完毕-----------------------');
};


// 控制台输出: [ { id: 2, username: 'pincman' } ]

访问器装饰器

访问器其实只是那些添加了get,set前缀的方法,用于使用调用属性的方式获取和设置一些属性的方法,类似于 PHP 中的魔术方法__get,__set。其装饰器使用方法与普通方法并无差异,只是在获取值的时候是调用描述符的getset来替代value而已。

例如,我们添加一个nickname字段,给设置nickname添加一个自定义前缀,并禁止在遍历user对象时出现nickname的值,添加一个fullname字段,在设置nickname时添加一个字符串后缀生成。

export const HiddenDecorator = () => {
return (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) => {
descriptor.enumerable = false;
};
};

export const PrefixDecorator = (prefix: string) => {
return (
target: any,
propertyName: string,
descriptor: PropertyDescriptor,
) => {
return {
...descriptor,
set(value: string) {
descriptor.set.apply(this, [`${prefix}_${value}`]);
},
};
};
};

export class UserEntity {
private _nickname: string;

// @ts-ignore
private fullname: string;

@HiddenDecorator()
@PrefixDecorator('jesse_')
get nickname() {
return this._nickname;
}

set nickname(value: string) {
this._nickname = value;
this.fullname = `${value}_fullname`;
}
}

export const exp78 = () => {
// ...

console.log();
console.log(
'-----------------------示例8:get/set装饰器-----------------------',
);
console.log(
'-----------------------禁止nickname出现在遍历中,为nickname添加前缀-----------------------',
);
console.log();
const user = new UserEntity();

user.nickname = 'pincman';
console.log(user);
console.log(user.nickname);
console.log();
console.log('-----------------------示例8:执行完毕-----------------------');
};

// 第一个console.log控制台输出,可以看到遍历对象后并没有nickname字段的值
// UserService {
// users: [ { id: 1, username: 'admin' }, { id: 2, username: 'pincman' } ],
// roles: [],
// hello: 'test',
// password: '123456',
// _nickname: 'gkr__lichnow',
// fullname: 'gkr__lichnow_fullname'
//}
// 第二个console.log控制台输出
// gkr__lichnow

装饰器写法

通过装饰器重载方法有许多写法,可以根据自己的喜好来,以下举例几种

继承法

一般用于类装饰器中添加属性或方法,例如:

 return <T extends new (...args: any[]) => any>(target: T) => {
return class extends target {
getMyName() {
return this._name;
}
};
};

原型法

一般用于类装饰器上重载构造函数以及添加属性或方法,例如:

const ProfileDerorator = (profile: UserProfile) => {
return (target: any) => {
const original = target;
function constructor(...args: any[]) {
console.log('contruct has been changed');
return new original(...args);
}
// 赋值原型链
constructor.prototype = original.prototype;
// 添加一个静态属性
constructor.myinfo = `myinfo ${userinfo}`;
return constructor as typeof original;
};
};

赋值法

一般用于方法装饰器上修改某个描述符,例如

const loggerDecorator = () => {
return function logMethod(
target: Object,
propertyName: string,
propertyDescriptor: PropertyDescriptor,
): PropertyDescriptor {
const method = propertyDescriptor.value;
// 重载方法
propertyDescriptor.value = function async (...args: any[]) {...};
return propertyDescriptor;
};
};

展开法

与赋值法类似,只不过使用 ES6+的展开语法,更容易理解和使用,例如

const parseFunc = (
target: Object,
propertyName: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor => {
return {
...descriptor,
value(...args: any[]) {
// 获取格式化后的参数列表
const newArgs = parseConf.map((toParse, index) => toParse(args[index]));

return descriptor.value.apply(this, newArgs);
},
};
};

元信息反射 API

元信息反射 API (例如 Reflect)能够用来以标准方式组织元信息。而装饰器中的元信息反射使用非常简单,外观上仅仅可以看做在类的某个方法上附加一些随时可以获取的信息而已。

使用之前我们必须先安装reflect-metadata这个库

npm i reflect-metadata --save

并且在tsconfig.json中启用原信息配置

{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

基本使用

我们看一下 TS 官方的示例是如何通过反射 API 获取属性设计阶段的类型信息的。

需要注意的是目前预定义的元信息只有三种

  • 类型元信息: design:type

  • 参数类型元信息: design:paramtypes

  • 返回类型元信息: design:returntype

import 'reflect-metadata';

import { parse, parseDecorator, UserType } from './exp7-8';

class Point {
x: number;

y: number;
}

class Line {
private _p0: Point;

private _p1: Point;

@validate
// 这句可以省略,因为design:type是预定义属性
// @Reflect.metadata('design:type', Point)
set p0(value: Point) {
this._p0 = value;
}

get p0() {
return this._p0;
}

@validate
// @Reflect.metadata("design:type", Point)
set p1(value: Point) {
this._p1 = value;
}

get p1() {
return this._p1;
}
}

function validate<T>(
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<T>,
) {
const { set } = descriptor;
descriptor.set = function (value: T) {
const type = Reflect.getMetadata('design:type', target, propertyKey);
if (!(value instanceof type)) {
throw new TypeError('Invalid type.');
}
set.apply(this, [value]);
};
return descriptor;
}

export const exp910 = () => {
console.log();
console.log(
'-----------------------示例9:基本元元素类型反射-----------------------',
);
console.log(
'-----------------------为访问器的set方法添加类型验证-----------------------',
);
console.log();
const line = new Line();
const p0 = new Point();
p0.x = 1;
p0.y = 2;
line.p1 = p0;
console.log(line);
console.log();
console.log('-----------------------示例9:执行完毕-----------------------');
};


// 控制台输出: Line { _p1: Point { x: 1, y: 2 } }

自定义元信息

除了使用类似design:type这种预定义的原信息外,我们也可以自定义信息,因为一般我们都是用reflect-metadata来自定义原信息的。比如我们可以在删除用户的方法上添加一个角色判断,只有拥有我们设定角色的用户才能删除用户,比如管理员角色,具体可参考以下代码:

// 角色守卫
export const RoleGuardDecorator = (roles: string[]) => {
console.log('开始验证角色');
return function roleGuard(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
// 根据传入的参数定义守卫所需的角色
Reflect.defineMetadata('roles', roles, target, propertyKey);
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
// 获取当前用户的角色
const currentRoles = target.getRoles();
// 获取我们定义的操作此方法所需的角色
const needRoles = Reflect.getMetadata('roles', target, propertyKey);
// 判断当前用户是否拥有所需的角色,没有则抛出异常
for (const role of needRoles) {
if (!currentRoles.includes(role)) {
throw new Error(
`you have not permission to run ${propertyKey}`,
);
}
}
console.log('验证角色完毕');
return method.apply(this, args);
};
return descriptor;
};
};

export class UserService {
protected users: UserType[] = [
{ id: 1, username: 'admin' },
{ id: 2, username: 'pincman' },
];

getUsers() {
return this.users;
}

// 设定当前用户的角色
getRoles() {
return ['user'];
}

@RoleGuardDecorator(['admin'])
// 在装饰器中使用Reflect.defineMetadata()放定义roles只是为了方便封装
// 当然,我们也可以在方法上直接定义roles,如下
// Reflect.metadata('roles',['admin'])
@parseDecorator
delete(@parse((arg: any) => Number(arg)) id: number): UserService {
this.users = this.getUsers().filter((userObj) => userObj.id !== id);
return this;
}
}

export const exp910 = () => {
// ...
console.log();
console.log(
'-----------------------示例10:自定义元元素反射-----------------------',
);
console.log(
'-----------------------添加角色守卫来判断当前用户是否有删除权限-----------------------',
);
console.log();
const user = new UserService();
user.delete(1);
console.log(user.getUsers());
console.log();
console.log(
'-----------------------示例10:执行完毕-----------------------',
);
};


// 控制台将输出异常
// Error: you have not permission to run delete

组合与顺序

每一个属性,参数或方法都可以使用多组装饰器。每个类型的装饰器的调用顺序也是不同的。

组合使用

我们可以对任意一个被装饰者调用多组装饰器,多组装饰器一般书写在多行上(当然你也可以写在一行上,多行书写只不过是个约定俗成的惯例),比如

@RoleGuardDecorator
@parseDecorator
delete(@parse((arg: any) => Number(arg)) id): UserService

当多个装饰器应用于一个声明上,它们求值方式与高阶函数相似。在这个模型下,当复合*RoleGuardDecoratorparseDecorator时,复合的结果等同于RoleGuardDecorator*(parseDecorator(delete))

同时,我们可以参考 react 中的高阶,原理相似

它们的调用步骤类似剥洋葱法,即:

  1. 由上至下依次对装饰器表达式求值。

  2. 求值的结果会被当作函数,由下至上依次调用。

比如

export const parseDecorator = () => {
console.log('开始格式化数据');
return (
target: Object,
propertyName: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor => {
return {
...descriptor,
value(...args: any[]) {
const newArgs = parseConf.map((toParse, index) => toParse(args[index]));
console.log('格式化完毕');
return descriptor.value.apply(this, newArgs);
},
};
};
};

export const RoleGuardDecorator = (roles: string[]) => {
return function roleGuard(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
console.log('开始验证角色');
...
descriptor.value = function(...args: any[]) {
...
console.log('验证角色完毕');
return method.apply(this, args);
};
return descriptor;
};
};

export class UserService {
...
@RoleGuardDecorator(['admin'])
// 把parseDecorator改成parseDecorator()
@parseDecorator()
getRoles() {
// 提供验证角色为admin
return ['admin'];
}
}

调用顺序

特别需要注意的是getMetadatagetOwneMetadata区别

每种类型的装饰器的调用顺序是不同的,具体顺序如下:

  1. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个实例成员(即类原型的成员)。

  2. 参数装饰器,然后依次是方法装饰器访问符装饰器,或属性装饰器应用到每个静态成员。

  3. 参数装饰器应用到构造函数(即类原型)。

  4. 类装饰器应用到类。

例如:我们使用元信息结合方法和参数装饰器来验证参数的required,其调用顺序为参数装饰器->方法装饰器

/**
* 装饰器调用顺序
*/
import { UserType } from './exp7-8';
import { UserService as ParentUserService } from './exp9-10';

const requiredMetadataKey = Symbol('required');

export const RequiredDecorator = (
target: any,
propertyKey: string | symbol,
parameterIndex: number,
) => {
const existingRequiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
requiredMetadataKey,
existingRequiredParameters,
target,
propertyKey,
);
};

export const ValidateDecorator = (
target: any,
propertyName: string,
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
) => {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] = Reflect.getOwnMetadata(
requiredMetadataKey,
target,
propertyName,
);
if (requiredParameters) {
for (const parameterIndex of requiredParameters) {
if (
parameterIndex >= args.length ||
args[parameterIndex] === undefined
) {
throw new Error('Missing required argument.');
}
}
}

return method.apply(this, args);
};
};

class UserService extends ParentUserService {
@ValidateDecorator
createUser(@RequiredDecorator username?: string, id?: number) {
const ids: number[] = this.users.map((userEntity) => userEntity.id);
const newUser: UserType = {
// 如果不提供ID参数,则新用户的ID为所有用户的最大ID + 1
id: id || Math.max(...ids) + 1,
// 如果不提供username参数,则生成随机字符串作为用户名
username: username || Math.random().toString(36).substring(2, 15),
};
this.users.push(newUser);
return newUser;
}
}

export const exp11 = () => {
console.log();
console.log(
'-----------------------示例11:装饰器组合-----------------------',
);
console.log(
'-----------------------为username参数提供必填验证-----------------------',
);
console.log();
const user = new UserService();
user.createUser();
console.log(user.getUsers());
console.log();
console.log(
'-----------------------示例11:执行完毕-----------------------',
);
};

umijs + qiankun

· 阅读需 10 分钟
XOne
小站站长
信息

本文档是记录站长为长沙一家医疗公司做技术升级改造,采用umijs + antd + qiankun 技术栈时遇到的一些问题,以及解决这些问题的方案。

🚀概述

提示

Umi,中文发音为「乌米」,是可扩展的企业级前端应用框架。Umi 以路由为基础,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。 🐳

静态配置在config/config.ts,运行时配置在src/app.tsx。config.ts开启对应的插件才会注入相应的依赖。如:

// 开启qiankun依赖
{
qiankun: {
master: {},
}
};

运行时接入qiankun配置

/**
* 乾坤配置 加载微应用数据
* @returns
*/
export function qiankun() {
const config: QiankunConfigType & { master: { prefetch: boolean } } = {
master: {
apps: [],
prefetch: false,//这里实际是有用的
},
}

fetchAppData().then((res) => {
if (res.data) {
const microData = res.data;
microData.map((item: AppAPI.AppItemType) => {
config.master.apps.push({
name: item.number,
entry: item.url || '',
})
})
}
})

return config;
}

父子应用通信,如果父子应用采用的都是umijs,是可以按官网的做法,比较简单的进行数据交互和响应式处理。然此次微应用是用vue写的,也考虑到后续接入的应用会有各种架构,所以采用的方案是更加通用的做法,并且由于业务的要求,进行了一些特殊处理

/**
* const { micro,kp } = useModel('@@qiankunStateForSlave');
* 父子应用通信 该函数的返回值将传递给子应用
* @returns
*/
export function useQiankunStateForSlave() {
//API函数
const [api, setApi] = useState({
...microUtil
})

//响应式API
const [respApi, setRespApi] = useState({})

//响应式数据
const [state, doSetState]: UseStateType<MicroGlobalStateType> = useState({
callback: {}
})

//回调函数
kpSetCallback({ useEffect, state });

// 实际给子应用调用修改state的方法
// 传参和实现可以自定义,子应用直接调用setState是不生效的,所以才需要这个额外的方法,这是一个坑
const setState = (state: any) => {
doSetState(state)
}

return {
micro: { api, respApi, state, setState },
kp: { setApi, setRespApi, doSetState }
}
}

在加载子应用的方案时,采用umijs提供的组件<MicroApp/>,相较于<MicroAppWithMemoHistory/> ,不需要指定url,这个和后续的一个坑有关。
代码过多,这里只贴出关键代码

const element = React.useMemo(() => {
let cachedElement = getCachedElement(url);

if (cachedElement) {
return cachedElement;
}

cachedElement = <MicroApp
settings={settings}
wrapperClassName={cns('microContainer')}
name={name}
url={url}
autoCaptureError
autoSetLoading
loader={loaderHadler}
errorBoundary={ErrorComponent}
{...microData}
loadingState
base={base}
pageContext={pageContext}
/>
saveElement(url, cachedElement);

return cachedElement;
}, [name, url, microData.data.userLayout.component]);

🚀开始踩坑

坑1 子应用找不到

子应用数据举例如下

const microList = [
{
name: 'app1',
entry: '//localhost:5556',
},
];

情况一:不要开启代理,找问题能找疯掉。跟踪到底层,你会发现load的地址是代理前的地址。跨域问题组件本身已处理,不需要我们考虑,虽然前端可能报的是跨域的错,但实际是加载地址错误。
情况二:Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry

/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}

/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}

/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}

找不到这几个函数,原因可能是子应用的webpack配置问题

 output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
// chunkLoadingGlobal: `webpackJsonp_${name}`,
// globalObject: 'window',
}

其他原因可到官网看看:qiankun常见问题

坑2 父子应用通信,数据要区分局部的/全局的,全局的都是响应式的数据,响应式的数据要考虑如何提供对应的回调处理,或者说怎么通知子应用数据变更

//qiankun  fun state
const { kp } = useModel('@@qiankunStateForSlave');
//set函数
const { doSetState, setRespApi } = kp;

//简单数据
const microData = kpGetMicroData(initialState!, name, component);
//响应式函数
const respApi = kpGetMicroRespApi(setInitialState);

useEffect(() => {
//追加响应式数据
kpSetMicroState({ doSetState, initialState, antdToken });

//追加响应式函数
setRespApi((pre: any) => ({
...pre,
...respApi,
}))
}, [initialState!.currentUser!, antdToken])

子应用中我采用了callback方式

/**
* 加载
*/
export async function mount(props) {
//1.存储props为全局方便其他地方使用
await store.dispatch('micro/setMicroProps', props);

//事件
const onSettingsChange = (settings) => {
console.log('onSettingsChange settings', settings)
}

const onCurrentUserChange = (currentUser) => {
console.log('onCurrentUserChange currentUser', currentUser)
}

const onAntdTokenChange = (antdToken) => {
console.log('onAntdTokenChange antdToken', antdToken)
}

//2.设置回调函数
props.micro.setState((pre) => ({
...pre,
callback: {
onSettingsChange,
onCurrentUserChange,
onAntdTokenChange,
}
}))

//3.渲染目标页面
render(props);

//4.关闭加载动画
props.setLoading(false)
}

坑3 子应用的路由拦截导致无限循环加载

一般应用都会有路由拦截,判断用户是否登录,访问路径是否合法等。由于主应用传过来的path是属于主应用的路由规则的path,子应用的路由规则不一定和主应用一致,也不应该一致。比如我有2个应用,都有个路由叫/home,这在主应用无法区分,我的做法是每个应用前增加表示该应用的前缀,如:/app1/home
此时,子应用不该存在路由,因为主应用的路由已经可以告诉子应用该加载哪个页面了,直接load即可。

function render(props = {}) {
if (window.__POWERED_BY_QIANKUN__ && props.data.userLayout.component) {
const component = props.data.userLayout.component;
let path = component.replace('@/', './');
import(path + '/index.vue').then((module) => {
const container = module.default;
instance = createApp(MicroLayout);

instance
.use(store)
.use(Antd)
.use(Print)
.mount(props.container ? props.container.querySelector('#root') : '#root');
}).catch((error) => {
console.error('Failed to load component:', error);
});
} else {
instance = createApp(App);
instance
.use(store)
.use(router)
.use(Antd)
.use(Print)
.mount(props.container ? props.container.querySelector('#root') : '#root');
}
}

坑4 子应用使用了antd,主应用也是用antd,子应用弹窗样式丢失

一开始以为是样式冲突问题,在主应用配置了统一前缀config.ts

  antd: {
//设置class前缀 避免和微应用冲突
configProvider:{
prefixCls: 'kp-ant',
iconPrefixCls: 'kp-anticon',
}
}

事实上不是这个原因,但是这一步还是有必要的,可以避免antd组件的class冲突。
这里不得不提antd的弹窗会把元素追加到最外层的最下面,直接在qiankun包裹之外,这就是原因。解决方案是指定挂载的容器:

 <a-drawer :get-container="$el" 
:title="title"
:width="720"
:visible="visible"
:body-style="{ paddingBottom: '80px' }"
@close="onClose"
>...<a-drawer/>

不同的组件写法不同,可以看组件的官方文档

🚀最后

上述列举的都是笔者印象比较深刻的问题,且笔者在处理过程中,这些问题大多无法面向百度解决,至于其他问题一般都可在百度找到答案。
上述所有写法或解决方案,仅为笔者个人摸索出来,不代表权威的解决方案,仅供参考。