airx 0.2.3 → 0.3.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -25
- package/output/esm/element.d.ts +2 -1
- package/output/esm/index.d.ts +2 -3
- package/output/esm/index.js +100 -91
- package/output/esm/index.js.map +1 -1
- package/output/esm/render/common/index.d.ts +5 -3
- package/output/esm/signal/index.d.ts +6 -0
- package/output/esm/types.d.ts +2 -3
- package/output/umd/element.d.ts +2 -1
- package/output/umd/index.d.ts +2 -3
- package/output/umd/index.js +99 -92
- package/output/umd/index.js.map +1 -1
- package/output/umd/render/common/index.d.ts +5 -3
- package/output/umd/signal/index.d.ts +6 -0
- package/output/umd/types.d.ts +2 -3
- package/package.json +2 -1
- package/output/esm/reactive.d.ts +0 -10
- package/output/umd/reactive.d.ts +0 -10
package/README.md
CHANGED
|
@@ -1,50 +1,56 @@
|
|
|
1
|
-
# airx
|
|
1
|
+
# airx
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/airx) [](https://github.com/airxjs/airx/actions/workflows/check.yml)
|
|
4
4
|
|
|
5
5
|
☁️ Airx is a lightweight JSX web application framework.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[中文文档](https://github.com/airxjs/airx/blob/main/README_CN.md)
|
|
8
|
+
|
|
|
9
|
+
[English Document](https://github.com/airxjs/airx/blob/main/README.md)
|
|
10
|
+
|
|
11
|
+
Airx is a frontend development framework based on `JSX` and `Signal`, aimed at providing a simple and direct solution for building web applications.
|
|
8
12
|
|
|
9
13
|
## Features
|
|
10
14
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
15
|
+
- Seamlessly integrates with [Signal](https://github.com/tc39/proposal-signals) and its ecosystem!
|
|
16
|
+
- Developed entirely using TypeScript, TypeScript-friendly
|
|
17
|
+
- Defines components using JSX functional syntax
|
|
18
|
+
- No hooks like React 😊
|
|
19
|
+
- Minimal API for easy learning
|
|
14
20
|
|
|
15
21
|
## Getting Started
|
|
16
22
|
|
|
17
|
-
To
|
|
23
|
+
To begin using Airx, follow these steps:
|
|
18
24
|
|
|
19
25
|
1. Install Airx using npm or yarn:
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
```shell
|
|
28
|
+
npm install airx
|
|
29
|
+
```
|
|
24
30
|
|
|
25
|
-
2. Import
|
|
31
|
+
2. Import necessary functions and components into your project:
|
|
26
32
|
|
|
27
33
|
```javascript
|
|
28
34
|
import * as airx from 'airx'
|
|
29
35
|
|
|
30
|
-
//
|
|
31
|
-
const
|
|
36
|
+
// All values based on Signal automatically trigger updates
|
|
37
|
+
const state = new Signal.State(1)
|
|
38
|
+
const computed = new Signal.Computed(() => state.get() + 100)
|
|
32
39
|
|
|
33
|
-
// define a component
|
|
34
40
|
function App() {
|
|
35
|
-
|
|
36
|
-
const innerCount = airx.createSignal(1)
|
|
41
|
+
const innerState = new Signal.State(1)
|
|
37
42
|
|
|
38
43
|
const handleClick = () => {
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
state.set(state.get() + 1)
|
|
45
|
+
innerState.set(innerState.get() + 1)
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
//
|
|
48
|
+
// Return a rendering function
|
|
44
49
|
return () => (
|
|
45
50
|
<button onClick={handleClick}>
|
|
46
|
-
{
|
|
47
|
-
{
|
|
51
|
+
{state.get()}
|
|
52
|
+
{computed.get()}
|
|
53
|
+
{innerState.get()}
|
|
48
54
|
</button>
|
|
49
55
|
)
|
|
50
56
|
}
|
|
@@ -53,18 +59,60 @@ const app = airx.createApp(<App />);
|
|
|
53
59
|
app.mount(document.getElementById('app'));
|
|
54
60
|
```
|
|
55
61
|
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
We have only a few APIs because we pursue a minimal core design. In the future, we will also open up a plugin system.
|
|
65
|
+
|
|
66
|
+
### createApp
|
|
67
|
+
|
|
68
|
+
Create an application instance.
|
|
69
|
+
|
|
70
|
+
### provide
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
function provide: <T = unknown>(key: unknown, value: T): ProvideUpdater<T>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Inject a value downwards through the `context`, must be called synchronously directly or indirectly within a component.
|
|
77
|
+
|
|
78
|
+
### inject
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
function inject<T = unknown>(key: unknown): T | undefined
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Look up a specified value upwards through the `context`, must be called synchronously directly or indirectly within a component.
|
|
85
|
+
|
|
86
|
+
### onMounted
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
type MountedListener = () => (() => void) | void
|
|
90
|
+
function onMounted(listener: MountedListener): void
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Register a callback for when the DOM is mounted, must be called synchronously directly or indirectly within a component.
|
|
94
|
+
|
|
95
|
+
### onUnmounted
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
type UnmountedListener = () => void
|
|
99
|
+
function onUnmounted(listener: UnmountedListener): void
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Register a callback for when the DOM is unmounted, must be called synchronously directly or indirectly within a component.
|
|
103
|
+
|
|
56
104
|
## License
|
|
57
105
|
|
|
58
|
-
This project
|
|
106
|
+
This project uses the MIT License. For detailed information, please refer to the [LICENSE](LICENSE) file.
|
|
59
107
|
|
|
60
|
-
##
|
|
108
|
+
## Contribution
|
|
61
109
|
|
|
62
110
|
Contributions are welcome! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request.
|
|
63
111
|
|
|
64
|
-
##
|
|
112
|
+
## Acknowledgements
|
|
65
113
|
|
|
66
|
-
We
|
|
114
|
+
We want to thank all contributors and supporters of the Airx project.
|
|
67
115
|
|
|
68
116
|
---
|
|
69
117
|
|
|
70
|
-
For more information,
|
|
118
|
+
For more information, please refer to the [official documentation](https://github.com/airxjs/airx)
|
package/output/esm/element.d.ts
CHANGED
|
@@ -38,8 +38,9 @@ export interface AirxComponentLifecycle {
|
|
|
38
38
|
onMounted: (listener: AirxComponentMountedListener) => void;
|
|
39
39
|
onUnmounted: (listener: AirxComponentUnmountedListener) => void;
|
|
40
40
|
}
|
|
41
|
+
type ProvideUpdater<T = unknown> = (newValue: T | ((old: T) => T)) => void;
|
|
41
42
|
export type AirxComponentContext = AirxComponentLifecycle & {
|
|
42
|
-
provide: <T = unknown>(key: unknown, value: T) =>
|
|
43
|
+
provide: <T = unknown>(key: unknown, value: T) => ProvideUpdater<T>;
|
|
43
44
|
inject: <T = unknown>(key: unknown) => T | undefined;
|
|
44
45
|
};
|
|
45
46
|
export declare function component<P = unknown>(comp: AirxComponent<P>): AirxComponent<P>;
|
package/output/esm/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { AirxElement } from './element';
|
|
2
1
|
import { Plugin } from './render';
|
|
2
|
+
import { AirxComponent, AirxElement } from './element';
|
|
3
3
|
export * from './types';
|
|
4
4
|
export { Plugin } from './render';
|
|
5
|
-
export { createSignal, Signal, watchSignal } from './reactive';
|
|
6
5
|
export { Fragment, component, createElement, AirxComponent, AirxElement, AirxChildren, AirxComponentContext } from './element';
|
|
7
6
|
export { inject, provide, onMounted, onUnmounted } from './render';
|
|
8
7
|
export interface AirxApp {
|
|
@@ -10,4 +9,4 @@ export interface AirxApp {
|
|
|
10
9
|
mount: (container: HTMLElement) => AirxApp;
|
|
11
10
|
renderToHTML: () => Promise<string>;
|
|
12
11
|
}
|
|
13
|
-
export declare function createApp(element: AirxElement<any>): AirxApp;
|
|
12
|
+
export declare function createApp(element: AirxElement<any> | AirxComponent): AirxApp;
|
package/output/esm/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const globalContext
|
|
1
|
+
const globalContext = {
|
|
2
2
|
current: null
|
|
3
3
|
};
|
|
4
4
|
function useContext() {
|
|
5
|
-
if (globalContext
|
|
5
|
+
if (globalContext.current == null) {
|
|
6
6
|
throw new Error('Unable to find a valid component context');
|
|
7
7
|
}
|
|
8
|
-
return globalContext
|
|
8
|
+
return globalContext.current;
|
|
9
9
|
}
|
|
10
10
|
const onMounted = (listener) => {
|
|
11
11
|
return useContext().onMounted(listener);
|
|
@@ -20,85 +20,65 @@ const provide = (key, value) => {
|
|
|
20
20
|
return useContext().provide(key, value);
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const isPrintLogs = typeof process != 'undefined'
|
|
24
|
+
&& process?.env?.NODE_ENV === 'development'
|
|
25
|
+
&& process?.env?.AIRX_DEBUG === 'true';
|
|
24
26
|
function createLogger(name) {
|
|
25
27
|
function getPrintPrefix() {
|
|
26
28
|
const date = new Date().toLocaleString();
|
|
27
29
|
return `[${date}][${name}]`;
|
|
28
30
|
}
|
|
29
31
|
function debug(...args) {
|
|
30
|
-
if (
|
|
32
|
+
if (isPrintLogs)
|
|
31
33
|
console.log(getPrintPrefix(), ...args);
|
|
32
34
|
}
|
|
33
35
|
return { debug };
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
38
|
+
let firstSignal = undefined;
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
const globalNS = (function () {
|
|
41
|
+
// the only reliable means to get the global object is
|
|
42
|
+
// `Function('return this')()`
|
|
43
|
+
// However, this causes CSP violations in Chrome apps.
|
|
44
|
+
if (typeof self !== 'undefined') {
|
|
45
|
+
return self;
|
|
46
|
+
}
|
|
47
|
+
if (typeof window !== 'undefined') {
|
|
48
|
+
return window;
|
|
49
|
+
}
|
|
50
|
+
if (typeof global !== 'undefined') {
|
|
51
|
+
return global;
|
|
52
|
+
}
|
|
53
|
+
throw new Error('unable to locate global object');
|
|
54
|
+
})();
|
|
55
|
+
// 通过函数包装来延迟加载 Signal
|
|
56
|
+
// 这对使用 Polyfill 的应用来说更友好
|
|
57
|
+
function getSignal() {
|
|
58
|
+
const globalSignal = globalNS['Signal'];
|
|
59
|
+
if (globalSignal == null)
|
|
60
|
+
throw new Error('Signal is undefined');
|
|
61
|
+
if (firstSignal == null)
|
|
62
|
+
firstSignal = globalSignal;
|
|
63
|
+
if (firstSignal !== globalSignal)
|
|
64
|
+
throw new Error('Signal have multiple instances');
|
|
65
|
+
return globalSignal;
|
|
64
66
|
}
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
configurable: false,
|
|
69
|
-
enumerable: false,
|
|
70
|
-
writable: true,
|
|
71
|
-
value: new Set()
|
|
72
|
-
});
|
|
73
|
-
return object;
|
|
67
|
+
function createWatch(notify) {
|
|
68
|
+
const signal = getSignal();
|
|
69
|
+
return new signal.subtle.Watcher(notify);
|
|
74
70
|
}
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
return () => { deps.delete(listener); };
|
|
71
|
+
function createComputed(computation, options) {
|
|
72
|
+
const signal = getSignal();
|
|
73
|
+
return new signal.Computed(computation, options);
|
|
79
74
|
}
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
globalContext.dependencies.add(ref);
|
|
84
|
-
}
|
|
85
|
-
let value = ref.value;
|
|
86
|
-
Reflect.defineProperty(ref, 'value', {
|
|
87
|
-
get() {
|
|
88
|
-
if (!globalContext.dependencies.has(ref)) {
|
|
89
|
-
globalContext.dependencies.add(ref);
|
|
90
|
-
}
|
|
91
|
-
return value;
|
|
92
|
-
},
|
|
93
|
-
set(newValue) {
|
|
94
|
-
value = newValue;
|
|
95
|
-
triggerRef(ref);
|
|
96
|
-
return value;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
return ref;
|
|
75
|
+
function isState(target) {
|
|
76
|
+
const signal = getSignal();
|
|
77
|
+
return target instanceof signal.State;
|
|
100
78
|
}
|
|
101
79
|
|
|
80
|
+
const airxElementSymbol = Symbol('airx-element');
|
|
81
|
+
|
|
102
82
|
/**
|
|
103
83
|
* createElement 是用于创建 AirxElement 的工具函数
|
|
104
84
|
*/
|
|
@@ -177,7 +157,15 @@ class InnerAirxComponentContext {
|
|
|
177
157
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
158
|
provide(key, value) {
|
|
179
159
|
this.providedMap.set(key, value);
|
|
180
|
-
return v =>
|
|
160
|
+
return v => {
|
|
161
|
+
if (typeof v === 'function') {
|
|
162
|
+
const old = this.providedMap.get(key);
|
|
163
|
+
const func = v;
|
|
164
|
+
this.providedMap.set(key, func(old));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.providedMap.set(key, v);
|
|
168
|
+
};
|
|
181
169
|
}
|
|
182
170
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
183
171
|
inject(key) {
|
|
@@ -372,9 +360,11 @@ function reconcileChildren(appContext, parentInstance, childrenElementArray) {
|
|
|
372
360
|
// 添加 ref 处理
|
|
373
361
|
if ('ref' in instance.memoProps) {
|
|
374
362
|
context.onMounted(() => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
363
|
+
const ref = instance.memoProps.ref;
|
|
364
|
+
// 如果组件有自己的 dom 并且 ref 为 state
|
|
365
|
+
if (instance.domRef && isState(ref)) {
|
|
366
|
+
ref.set(instance.domRef);
|
|
367
|
+
return () => ref.set(undefined);
|
|
378
368
|
}
|
|
379
369
|
});
|
|
380
370
|
}
|
|
@@ -439,32 +429,44 @@ function performUnitOfWork(pluginContext, instance, onUpdateRequire) {
|
|
|
439
429
|
}
|
|
440
430
|
// airx 组件
|
|
441
431
|
if (typeof element?.type === 'function') {
|
|
442
|
-
|
|
443
|
-
|
|
432
|
+
if (instance.signalWatcher == null) {
|
|
433
|
+
// Watch 是惰性的,只有当 Signal 被读取时才会触发 --!
|
|
434
|
+
const signalWatcher = createWatch(async () => {
|
|
435
|
+
instance.needReRender = true;
|
|
436
|
+
onUpdateRequire?.(instance);
|
|
437
|
+
queueMicrotask(() => {
|
|
438
|
+
signalWatcher.watch();
|
|
439
|
+
const paddings = signalWatcher.getPending();
|
|
440
|
+
for (const padding of paddings)
|
|
441
|
+
padding.get();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
instance.signalWatcher = signalWatcher;
|
|
445
|
+
instance.context.addDisposer(() => signalWatcher.unwatch());
|
|
446
|
+
}
|
|
447
|
+
if (instance.childrenRender == null) {
|
|
444
448
|
const component = element.type;
|
|
445
|
-
const beforeContext = globalContext
|
|
446
|
-
globalContext
|
|
447
|
-
const componentReturnValue =
|
|
449
|
+
const beforeContext = globalContext.current;
|
|
450
|
+
globalContext.current = instance.context.getSafeContext();
|
|
451
|
+
const componentReturnValue = component(instance.memoProps);
|
|
448
452
|
if (typeof componentReturnValue !== 'function') {
|
|
449
453
|
throw new Error('Component must return a render function');
|
|
450
454
|
}
|
|
451
|
-
globalContext
|
|
452
|
-
instance.
|
|
453
|
-
const
|
|
455
|
+
globalContext.current = beforeContext;
|
|
456
|
+
instance.childrenRender = componentReturnValue;
|
|
457
|
+
const childrenComputed = createComputed(() => componentReturnValue());
|
|
458
|
+
instance.signalWatcher.watch(childrenComputed);
|
|
459
|
+
const children = childrenComputed.get();
|
|
454
460
|
reconcileChildren(pluginContext, instance, childrenAsElements(children));
|
|
455
461
|
}
|
|
456
462
|
if (instance.needReRender) {
|
|
457
|
-
|
|
463
|
+
// 这里有个问题,如果是由于父组件导致的子组件渲染
|
|
464
|
+
// 直接使用 childrenComputed.get() 将读取到缓存值
|
|
465
|
+
// const children = instance.childrenRender?.()
|
|
466
|
+
const children = instance.childrenRender();
|
|
458
467
|
reconcileChildren(pluginContext, instance, childrenAsElements(children));
|
|
459
468
|
delete instance.needReRender;
|
|
460
469
|
}
|
|
461
|
-
// 处理依赖触发的更新
|
|
462
|
-
collector.complete().forEach(ref => {
|
|
463
|
-
instance.context.addDisposer(watchSignal(ref, () => {
|
|
464
|
-
instance.needReRender = true;
|
|
465
|
-
onUpdateRequire?.(instance);
|
|
466
|
-
}));
|
|
467
|
-
});
|
|
468
470
|
}
|
|
469
471
|
// 浏览器组件/标签
|
|
470
472
|
if (typeof element?.type === 'string') {
|
|
@@ -669,7 +671,7 @@ function render$1(pluginContext, element, onComplete) {
|
|
|
669
671
|
Object.keys(prevProps)
|
|
670
672
|
.filter(isProperty)
|
|
671
673
|
.filter(isGone(prevProps, nextProps))
|
|
672
|
-
.forEach(name => dom.
|
|
674
|
+
.forEach(name => dom.removeAttribute(name));
|
|
673
675
|
// Set new or changed properties
|
|
674
676
|
Object.keys(nextProps)
|
|
675
677
|
.filter(isProperty)
|
|
@@ -1137,7 +1139,7 @@ class BasicLogic {
|
|
|
1137
1139
|
Object.keys(prevProps)
|
|
1138
1140
|
.filter(isProperty)
|
|
1139
1141
|
.filter(isGone(prevProps, nextProps))
|
|
1140
|
-
.forEach(name => dom.
|
|
1142
|
+
.forEach(name => dom.removeAttribute(name));
|
|
1141
1143
|
// Set new or changed properties
|
|
1142
1144
|
Object.keys(nextProps)
|
|
1143
1145
|
.filter(isProperty)
|
|
@@ -1190,23 +1192,30 @@ class PluginContext {
|
|
|
1190
1192
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1191
1193
|
function createApp(element) {
|
|
1192
1194
|
const appContext = new PluginContext();
|
|
1195
|
+
const ensureAsElement = (element) => {
|
|
1196
|
+
if (typeof element === 'function') {
|
|
1197
|
+
return createElement(element, {});
|
|
1198
|
+
}
|
|
1199
|
+
return element;
|
|
1200
|
+
};
|
|
1193
1201
|
const app = {
|
|
1194
1202
|
plugin: (...plugins) => {
|
|
1195
1203
|
appContext.registerPlugin(...plugins);
|
|
1196
1204
|
return app;
|
|
1197
1205
|
},
|
|
1198
1206
|
mount: (container) => {
|
|
1199
|
-
|
|
1207
|
+
container.innerHTML = ''; // 先清空再说
|
|
1208
|
+
render(appContext, ensureAsElement(element), container);
|
|
1200
1209
|
return app;
|
|
1201
1210
|
},
|
|
1202
1211
|
renderToHTML: () => {
|
|
1203
1212
|
return new Promise(resolve => {
|
|
1204
|
-
render$1(appContext, element, resolve);
|
|
1213
|
+
render$1(appContext, ensureAsElement(element), resolve);
|
|
1205
1214
|
});
|
|
1206
1215
|
}
|
|
1207
1216
|
};
|
|
1208
1217
|
return app;
|
|
1209
1218
|
}
|
|
1210
1219
|
|
|
1211
|
-
export { Fragment, component, createApp, createElement,
|
|
1220
|
+
export { Fragment, component, createApp, createElement, inject, onMounted, onUnmounted, provide };
|
|
1212
1221
|
//# sourceMappingURL=index.js.map
|