Skip to main content

Electron Study Notes

· 4 min read
XOne
webmaster

Webpack + Typescript

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

React code requires, add JSX support

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

Switch npm to pnpm and need to install

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

Routing

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',
],
}

Ant Design 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

Compile using babel-plugin-import to optimize the packaging volume of antd

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

Data storage electron-store

conf is installed to handle type errors

pnpm install electron-store conf

tsconfig.jsonThe default template is: "moduleResolution": "node", electron-forge seems to find it difficult or does not support configuring "moduleResolution": "node16" or other property values. Here, an opportunistic way to configure aliases is used to fix the type errors of electron-store.
tsconfig.json

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

Set up path aliases

tsconfig.json

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

webpack.renderer.config.ts

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

In addition to tsconfig.json webpack.renderer.config.ts configuring aliases, you also need to fix ESLint prompts

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

.eslintrc.json

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

Process Communication (IPC)

Using the ipcMain and ipcRenderer modules: The main process listens for events through the ipcMain module, and the rendering process sends events through the ipcRenderer module to achieve two-way communication.
Using the remote module: Allows direct calling of modules and methods in the main process from the rendering process, achieving simple cross-process communication.
Using the webContents module: You can directly send messages to a specified rendering process through this module to achieve communication.
Using shared variables between processes: Achieve cross-process communication by establishing global variables or shared memory.

Using ipcMain and ipcRenderer modules

Rendering Process -> Main Process
Method one:

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

Method two:

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

Main Process -> Rendering Process
Method one:

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

Network Requests

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',
});

The detailed requestUtil.ts file is as follows

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;

Introduction

· One min read
XOne
webmaster
tip

Primarily for programmers; clients and friends, please ignore this section.

This space is mainly used for recording technical documentation, learning experiences, and occasionally project experiences.

Bull Usage Tutorial

· 15 min read
XOne
webmaster

Basic Concepts

What is Bull?

Task queues are generally used for asynchronously processing time-consuming tasks such as video transcoding and sending SMS messages, preventing API connection timeouts.

Bull is a Node library that implements a fast and powerful queue system based on redis.

Although it is possible to implement a queue using direct Redis commands, this library provides an API that handles all the underlying details and enriches the basic functionality of Redis, making it easy to handle more complex use cases.

If you are not familiar with queues, you might wonder why you need them. Queues can elegantly solve many different problems, such as creating robust communication channels between microservices to smoothly handle CPU peaks, or distributing heavy work from one server to many smaller work intervals.

Redis

· 3 min read
XOne
webmaster

安装和启动

在 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

· 3 min read
XOne
webmaster
info

These are the personal study notes of the author on Three.js.

Introduction

Three.js is a JavaScript library for creating and displaying 3D graphics. It is built on top of WebGL technology, simplifying the process of creating complex 3D scenes in web pages. This article will introduce some basic knowledge and common tips for learning Three.js.

Getting Started

To begin learning Three.js, follow these steps:

  1. Install Three.js: You can download the latest version of the library from the Three.js 的官方网站 or install it via npm.
  2. Create a Scene: Include the Three.js library in your HTML file and create a scene object.
  3. Add Objects: Add objects to the scene, such as cubes, spheres, etc.
  4. Render the Scene: Create a renderer object and pass the scene and camera to the renderer to render the scene.

Basic Concepts

To learn Three.js, you need to understand some basic concepts:

  • Scene: Represents a 3D scene, containing all objects, lights, and cameras.
  • Camera: Defines the viewer's perspective and determines which parts of the scene will be displayed on the screen.
  • Renderer: Responsible for rendering the scene to a Canvas element on the HTML page.
  • Object: All entities visible in the scene, such as geometries, meshes, etc.

Example Code

Here is a simple example code of 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();

Advanced Learning

Once you have mastered the basic concepts and operations, you can delve into the following:

  • Materials and Lighting: Understand different types of materials and the impact of lighting on the scene.
  • Textures and Mapping: How to apply images or textures to the surfaces of objects.
  • Animation and Interaction: Methods for creating animation effects and interacting with users.
  • Optimization and Performance: Improving the performance and rendering efficiency of the scene.

Materials and Lighting

Three.js offers a variety of different materials, such as MeshBasicMaterialMeshPhongMaterial, etc., each with different lighting effects. By adjusting the material parameters, you can achieve various visual effects, such as diffuse reflection, specular reflection, etc.

Textures and Mapping

Using textures can give objects a more vivid appearance. You can load images or textures into materials and apply them to the surfaces of objects. By adjusting the mapping and repetition of textures, you can create a variety of effects.

Animation and Interaction

With the animation library provided by Three.js, you can create motion and rotation animation effects for objects. At the same time, you can listen for user interaction events, such as mouse clicks, keyboard inputs, etc., to achieve an interactive experience with users.

Optimization and Performance

When designing complex 3D scenes, it is necessary to consider performance optimization to ensure smooth rendering effects. You can use optimization techniques provided by Three.js, such as reducing the number of triangles, using caching, merging geometries, and other methods to improve performance.

By continuously practicing and trying these advanced topics, you will be able to use Three.js more proficiently and create more attractive and interactive 3D scenes.

ts-decorator

· 27 min read
XOne
webmaster
tip

It's best to take a look at it before the article《阮一峰-es6 中的装饰器》

Decorators are used to add some auxiliary functions to classes, methods, properties, and method parameters without affecting their original characteristics. Its main role in Typescript applications is similar to annotations in Java, and it is very useful in AOP (Aspect Oriented Programming) usage scenarios.

面向切面编程(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

· 8 min read
XOne
webmaster
info

This document records the issues encountered and the solutions adopted when the site administrator carried out a technical upgrade for a medical company in Changsha, using the umijs + antd + qiankun technology stack.

🚀概述

tip

Umi, pronounced as "Wu Mi" in Chinese, is an extendable enterprise-level front-end application framework. Umi is based on routing, supporting both configured routing and conventional routing, ensuring the completeness of routing functions and expanding based on this. It is equipped with a life cycle comprehensive plugin system, covering every life cycle from source code to build products, supporting various functional extensions and business needs. 🐳

Static configuration is in config/config.ts, and runtime configuration is in src/app.tsx. Dependencies are injected correspondingly when plugins are enabled in config.ts. For example:

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

Runtime access to qiankun configuration

/**
* 乾坤配置 加载微应用数据
* @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;
}

Parent-child communication,if both parent and child applications use umijs, it is relatively simple to perform data interaction and responsive processing according to the official website. However, this micro application is written in vue, and considering that subsequent applications will have various architectures, a more general approach is adopted, and some special processing is carried out due to business requirements.

/**
* 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 }
}
}

When loading the child application, the <MicroApp/>component provided by umijs is used, compared with<MicroAppWithMemoHistory/> , there is no need to specify the URL, which is related to a subsequent pitfall.
The code is too much, only the key code is posted here

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]);

🚀Start Pitfall

Pitfall 1 Child application not found

The data of the child application is as follows

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

Situation one: Do not enable proxy, looking for problems can go crazy. Tracking to the bottom, you will find that the loaded address is the address before proxying. The cross-domain issue has been processed by the component itself, and we don't need to consider it, although the front end may report a cross-domain error, but the actual loading address is wrong.
Situation two: 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);
}

If you can't find these functions, it may be due to the webpack configuration of the child application

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

Other reasons can be seen on the official website: qiankun common questions

Pitfall 2 Parent-child communication, data needs to be distinguished as local/global, global data is all responsive data, responsive data needs to consider how to provide corresponding callback processing, or how to notify the child application of data changes

//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])

In the child application, I used the callback method

/**
* 加载
*/
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)
}

Pitfall 3 Child application route interception leads to infinite loop loading

Applications generally have route interception to determine whether the user is logged in and whether the access path is legal. Since the path passed by the main application belongs to the main application's routing rules, the child application's routing rules may not be consistent with the main application's, nor should they be. For example, I have two applications, both of which have a route called /home. This cannot be distinguished in the main application. My approach is to add a prefix in front of each application to indicate the application, such as: /app1/home.
At this time, the child application should not have routing because the main application's routing can already tell the child application which page to load, and it can load directly.

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');
}
}

Pitfall 4 The child application uses antd, and the main application also uses antd, resulting in the loss of the child application's popup style

At first, it was thought to be a style conflict issue. A unified prefix was configured in the main application's config.ts

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

In fact, this was not the reason, but this step is still necessary to avoid class conflicts of antd components.
It must be mentioned that antd's popup will append elements to the bottom of the outermost layer, directly outside the qiankun wrapper, which is the reason. The solution is to specify the container for mounting:

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

Different components have different ways of writing, which can be seen in the official documentation of the components.

🚀Conclusion

The above are the issues that the author is more impressed with, and most of these issues cannot be solved by facing Baidu during the author's processing process. As for other issues, they can generally be found on Baidu. All the writing methods or solutions mentioned above are personally explored by the author and do not represent authoritative solutions, and are for reference only.