@k3000/ce 0.0.1 → 0.1.0

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 CHANGED
@@ -1,84 +1,147 @@
1
- 用最新H5技术实现前端组件
1
+ # @k3000/ce - Custom Element 响应式框架
2
2
 
3
+ 这是一个极轻量、**零构建 (No Build)** 的原生 Web Components 框架。它利用现代浏览器的 ES Modules 和 Custom Elements API,无需任何打包工具。
4
+
5
+ 链接:[讨论沟通链接](https://qshfu.com/doc/index.html#/public/MKjPmZhaz)
6
+ ---
7
+
8
+ ## ✨ 核心特性
9
+
10
+ - 🚀 **零构建**: 无需 Webpack/Vite,直接在浏览器运行。
11
+ - ⚛️ **响应式系统**: 基于 `Object.defineProperty` 的自动数据追踪与视图更新。
12
+ - 模板指令**: 内置 `each` (循环)、`connect` (条件渲染) 指令。
13
+ - 🛠️ **响应式 Hooks**: 提供 `useAttr`, `useWatch`, `useRef`, `useEvent` 等现代 API。
14
+
15
+ ---
16
+
17
+ ## 🚀 快速上手
18
+
19
+ ### 1. 项目引入
20
+ 在 `index.html` 中通过 Script 标签引入核心框架:
21
+
22
+ ```html
23
+ <script type="module">
24
+ import { config } from '../index.mjs'
25
+
26
+ config({
27
+ dir: './console/components',
28
+ ext: 'mjs'
29
+ })
30
+ </script>
3
31
  ```
4
- <head>
5
- <meta charset="UTF-8">
6
- <title>Title</title>
7
- <script src="ce.mjs?dir=/comp&ext=mjs&dev=true"></script>
8
- </head>
9
- <body>
10
- <div-box>
11
- <span slot="title">标题</span>
12
- <form is="new-form" get="info.json">
13
- <input name="name"/>
14
- <input name="age"/>
15
- </form>
16
- </div-box>
17
- </body>
18
- ```
19
- /comp目录下
20
- div-box.mjs
21
- ```
32
+
33
+ ### 2. 定义组件
34
+ 每个组件为一个独立的 `.mjs` 文件。
35
+
36
+ **示例: `my-counter.mjs`**
37
+
38
+ **更多示例参照: `/app`,`/console`**
39
+
40
+ ```javascript
41
+ import { useWatch } from "../../index.mjs";
42
+
43
+ // 1. 定义 HTML 结构 (支持插值 {{ }})
44
+ export const innerHTML = `
45
+ <div class="p-4 border rounded-xl shadow-lg bg-white/60 backdrop-blur-md">
46
+ <h3 class="text-lg font-bold">计数器: {{count}}</h3>
47
+ <div class="mt-4 gap-2 flex">
48
+ <button onclick="{{increment}}" class="px-4 py-2 bg-blue-500 text-white rounded-lg">+1</button>
49
+ <button onclick="{{() => this.count = 0}}" class="px-4 py-2 bg-gray-200 rounded-lg">重置</button>
50
+ </div>
51
+ </div>
52
+ `;
53
+
54
+ // 2. 定义逻辑
22
55
  export default class extends HTMLElement {
56
+ count = 0;
23
57
 
24
58
  constructor() {
59
+ super();
60
+ this.style.display = 'contents';
61
+
62
+ // 开启响应式监听 (可选,模板中使用的变量会自动监听)
63
+ const watch = useWatch(this);
64
+ watch({
65
+ count: (val) => console.log('Count changed to:', val)
66
+ });
67
+ }
25
68
 
26
- super()
27
-
28
- this.attachShadow({ mode: 'open' });
29
- this.shadowRoot.innerHTML = `
30
- <style>
31
- :host {
32
- display: block;
33
- background-color: #f0f0f0;
34
- padding: 10px;
35
- }
36
- </style>
37
- <h2><slot name="title"></slot></h2>
38
- <slot></slot>
39
- `;
69
+ increment() {
70
+ this.count++;
40
71
  }
41
72
  }
42
73
  ```
43
- new-form.mjs
44
- ```
45
- export default class extends HTMLFormElement {
46
- // connected前执行,此处处理好数据逻辑,入参是父级的数据
47
- data(data) {
48
74
 
49
- return {}
50
- }
51
- // node挂载时执行,入参是data()返回的数据
52
- connected(data) {
53
-
54
- if (this.attributes.get) {
75
+ ---
55
76
 
56
- this.getInfo(this.attributes.get.value)
57
- }
58
- }
77
+ ## 📖 核心 API (Hooks)
59
78
 
60
- find(name) {
79
+ ### `useAttr(this)`
80
+ 获取当前组件的所有 HTML 属性,返回一个简单的键值对对象。
81
+ ```javascript
82
+ const attr = useAttr(this);
83
+ console.log(attr.title); // 获取 <my-comp title="xxx"></my-comp> 的值
84
+ ```
61
85
 
62
- for (const item of this) {
86
+ ### `useRef(this)`
87
+ 获取模板中标记了 `ref` 属性的 DOM 元素。
88
+ ```javascript
89
+ // HTML: <path ref="pathNode"></path>
90
+ ready() {
91
+ const refs = useRef(this);
92
+ refs.pathNode.setAttribute('d', 'M10 10...');
93
+ }
94
+ ```
63
95
 
64
- if (item.name === name) return item
65
- }
96
+ ### `useWatch(target)`
97
+ 手动对对象属性进行响应式监听。
98
+ ```javascript
99
+ const watch = useWatch(this.data);
100
+ watch({
101
+ username: (newVal, oldVal) => {
102
+ // 返回 false 可以阻止赋值
103
+ return true;
66
104
  }
105
+ }, true); // 第二个参数为 true 表示立即执行一次
106
+ ```
67
107
 
68
- async getInfo(url) {
69
-
70
- const info = await fetch(url).then(res => res.json())
108
+ ### `useEvent()`
109
+ 获取全局事件总线,用于跨组件通信。
110
+ ```javascript
111
+ const event = useEvent();
112
+ event.dispatch('user-login', { id: 1 }); // 发送事件
113
+ event.add('user-login', (data) => { ... }); // 监听事件
114
+ ```
71
115
 
72
- for (const [key, value] of Object.entries(info || {})) {
116
+ ---
73
117
 
74
- const item = this.find(key)
118
+ ## 🏗️ 模板指令
75
119
 
76
- if (item) {
120
+ ### `each="{{list}}"` (循环)
121
+ ```html
122
+ <tr each="{{userList}}" class="hover:bg-gray-50">
123
+ <td>{{username}}</td>
124
+ <td>{{email}}</td>
125
+ </tr>
126
+ ```
77
127
 
78
- item.value = value
79
- }
80
- }
81
- }
82
- }
128
+ ### `connect="{{isVisible}}"` (条件渲染)
129
+ 类似于 `v-if`,控制元素在 DOM 树中的物理存在。
130
+ ```html
131
+ <div connect="{{status === 'active'}}">
132
+ 现在可见
133
+ </div>
134
+ ```
83
135
 
136
+ ### `on[event]="{{handler}}"` (事件绑定)
137
+ ```html
138
+ <button onclick="{{handleClick}}">方法绑定</button>
139
+ <button onclick="{{() => this.count++}}">内联绑定</button>
84
140
  ```
141
+
142
+ ---
143
+
144
+ ## ⚠️ 注意事项
145
+ - **ES Modules**: 必须在 HTTP 服务器环境下运行。
146
+ - **作用域**: 模板指令中的代码默认运行在组件实例的作用域下。
147
+ - **ready()**: 当你需要操作 DOM 时,请在 `ready()` 生命周期中执行。
@@ -0,0 +1,24 @@
1
+ import {useAttr, useRef} from "../../index.mjs";
2
+
3
+ export const shadowRoot = `
4
+ <label>
5
+ <input ref="fileInput" type="file" />
6
+ </label>
7
+ `
8
+
9
+ export default class extends HTMLElement {
10
+
11
+ constructor() {
12
+
13
+ super()
14
+ }
15
+
16
+ ready() {
17
+
18
+ const refs = useRef(this.shadowRoot)
19
+ const attr = useAttr(this)
20
+
21
+ refs.fileInput.accept = attr.accept
22
+ refs.fileInput.onchange = () => this.change(refs.fileInput.files)
23
+ }
24
+ }
@@ -1,12 +1,9 @@
1
1
 
2
2
  export default class extends HTMLFormElement {
3
3
 
4
- data() {
4
+ ready() {
5
5
 
6
- return {}
7
- }
8
-
9
- connected() {
6
+ console.log('ready')
10
7
 
11
8
  if (this.attributes.get) {
12
9
 
@@ -0,0 +1,92 @@
1
+ import {useEvent} from "../../index.mjs";
2
+
3
+ export const shadowRoot = `
4
+ <style>
5
+ .container {
6
+ display: flex;
7
+ justify-content: space-between;
8
+
9
+ .left {
10
+ cursor: pointer;
11
+ user-select: none;
12
+ padding-left: 20px; /* 给箭头留位置 */
13
+
14
+ &:before {
15
+ content: "";
16
+ position: absolute;
17
+ left: 8px;
18
+ top: 50%;
19
+
20
+ /* 1. 设置正方形 */
21
+ width: 10px;
22
+ height: 10px;
23
+
24
+ /* 2. 只给相邻的两条边上色,形成 L 型 */
25
+ border-left: 2px solid #333;
26
+ border-bottom: 2px solid #333;
27
+
28
+ /* 3. 旋转 45度,让 L 指向左边 */
29
+ /* translateY(-50%) 是为了修正垂直居中 */
30
+ transform: translateY(-50%) rotate(45deg);
31
+
32
+ /* 4. 可选:设置转角圆润一点 */
33
+ border-radius: 1px;
34
+ }
35
+ }
36
+ }
37
+ </style>
38
+ <nav class="container" style="display: {{none ? 'none' : 'flex'}};">
39
+ <div class="left" onclick="{{back}}">
40
+ <span></span>
41
+ <span>
42
+ <slot name="left"></slot>
43
+ </span>
44
+ </div>
45
+ <div class="title">
46
+ <slot name="title"></slot>
47
+ </div>
48
+ <div class="right">
49
+ <slot name="right"></slot>
50
+ </div>
51
+ </nav>
52
+ `
53
+
54
+ export default class extends HTMLElement {
55
+
56
+ none = true
57
+
58
+ except = []
59
+
60
+ prePath = ''
61
+
62
+ constructor() {
63
+
64
+ super()
65
+
66
+ this.display(location.hash.slice(1) || '/')
67
+
68
+ const event = useEvent()
69
+
70
+ event.add('route.change', (...args) => this.display(...args))
71
+
72
+ event.add('except routers', (...args) => {
73
+
74
+ this.except = args
75
+
76
+ this.display(this.prePath)
77
+
78
+ }, {preValue: true})
79
+ }
80
+
81
+ display(path) {
82
+
83
+ this.prePath = path
84
+
85
+ this.none = this.except.includes(path)
86
+ }
87
+
88
+ back() {
89
+
90
+ history.back()
91
+ }
92
+ }
@@ -0,0 +1,55 @@
1
+ import {useEvent} from "../../index.mjs";
2
+
3
+ export const shadowRoot = `
4
+ <style>
5
+ #page-container {
6
+ min-height: 100vh;margin: 0;display: flex;flex-direction: column;
7
+ > header {position: sticky;top: 0}
8
+ > main {flex: 1}
9
+ > footer {position: sticky;bottom: 0}
10
+ }
11
+ </style>
12
+ <div id="page-container">
13
+ <header><slot name="header"></slot></header>
14
+ <main><slot name="main"></slot></main>
15
+ <footer><slot name="footer"></slot></footer>
16
+ </div>
17
+ `
18
+
19
+ export default class extends HTMLElement {
20
+ constructor() {
21
+ super()
22
+ document.body.style.margin = '0'
23
+ const event = useEvent()
24
+ event.dispatch('except routers', '/', '/home', '/category', '/my')
25
+ event.dispatch('init routers', [
26
+ {
27
+ path: '/',
28
+ dir: './app/pages',
29
+ component: 'page-home'
30
+ },
31
+ {
32
+ path: '/home',
33
+ dir: './app/pages',
34
+ component: 'page-home'
35
+ },
36
+ {
37
+ path: '/category',
38
+ dir: './app/pages',
39
+ component: 'page-category'
40
+ },
41
+ {
42
+ path: '/my',
43
+ dir: './app/pages',
44
+ component: 'page-my'
45
+ },
46
+ {
47
+ path: '/test',
48
+ dir: './app/pages',
49
+ component: 'page-test'
50
+ }
51
+ ])
52
+ }
53
+ test() {
54
+ }
55
+ }
@@ -0,0 +1,60 @@
1
+ import {useEvent} from "../../index.mjs";
2
+
3
+ export const shadowRoot = `
4
+ <style>
5
+ .container {
6
+ display: flex;
7
+ justify-content: space-evenly;
8
+ cursor: pointer;
9
+ user-select: none;
10
+ }
11
+ </style>
12
+ <div class="container" style="display: {{none ? 'none' : 'flex'}};">
13
+ <slot></slot>
14
+ </div>
15
+ `
16
+
17
+ export const innerHTML = `
18
+ <div data-to="/home" onclick="{{routeTo}}" class="tab-bar-item {{['/home', '/'].includes(current) ? 'active' : ''}}">首页</div>
19
+ <div data-to="/category" onclick="{{routeTo}}" class="tab-bar-item {{current === '/category' ? 'active' : ''}}">分类</div>
20
+ <div data-to="/my" onclick="{{routeTo}}" class="tab-bar-item {{current === '/my' ? 'active' : ''}}">我的</div>
21
+ `
22
+
23
+ export default class extends HTMLElement {
24
+
25
+ none = false
26
+
27
+ current = location.hash.slice(1)
28
+
29
+ except = []
30
+
31
+ event = useEvent()
32
+
33
+ constructor() {
34
+
35
+ super()
36
+
37
+ this.event.add('except routers', (...routes) => {
38
+
39
+ this.except = routes
40
+
41
+ this.change(this.current)
42
+
43
+ }, {preValue: true})
44
+
45
+ this.event.add('route.change', path => this.change(path), {preValue: true})
46
+ }
47
+
48
+ change(path) {
49
+
50
+ this.current = path
51
+
52
+ this.none = !this.except.includes(path)
53
+ }
54
+
55
+ routeTo(event) {
56
+
57
+ this.event.dispatch('route.to', event.target.dataset.to)
58
+ // .then(res => console.log(res))
59
+ }
60
+ }
package/app/index.html ADDED
@@ -0,0 +1,39 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport"
6
+ content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"/>
7
+ <title>demo</title>
8
+ <script type="module">
9
+ import { config } from '../index.mjs'
10
+
11
+ config({
12
+ dir: './app/comp',
13
+ ext: 'mjs'
14
+ })
15
+ </script>
16
+ </head>
17
+ <body>
18
+ <page-container>
19
+ <nav-bar slot="header">
20
+ <span slot="left">返回</span>
21
+ <span slot="title">标题</span>
22
+ <span slot="right">菜单</span>
23
+ </nav-bar>
24
+ <router-view dir="../../comm" slot="main"></router-view>
25
+ <style>
26
+ .tab-bar-item {
27
+ flex: 1;
28
+ text-align: center;
29
+
30
+ &.active {
31
+ font-weight: bold;
32
+ background-color: lightyellow;
33
+ }
34
+ }
35
+ </style>
36
+ <tab-bar slot="footer"></tab-bar>
37
+ </page-container>
38
+ </body>
39
+ </html>
@@ -0,0 +1,7 @@
1
+ export default class extends HTMLElement {
2
+ constructor() {
3
+ super()
4
+ this.attachShadow({mode: 'open'})
5
+ this.shadowRoot.innerHTML = `404 Not Found`
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ export default class extends HTMLElement {
2
+ constructor() {
3
+ super()
4
+ this.attachShadow({mode: 'open'})
5
+ this.shadowRoot.innerHTML = `category`
6
+ }
7
+ }
@@ -0,0 +1,14 @@
1
+
2
+ export const shadowRoot = `
3
+ <div>home</div>
4
+ <b onclick="{{routeTo('/test')}}" style="user-select: none">test</b>
5
+ `
6
+
7
+ export default class extends HTMLElement {
8
+ constructor() {
9
+ super()
10
+ }
11
+ routeTo(to) {
12
+ location.hash = '#' + to
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ export default class extends HTMLElement {
2
+ constructor() {
3
+ super()
4
+ this.attachShadow({mode: 'open'})
5
+ this.shadowRoot.innerHTML = `my`
6
+ }
7
+ }
@@ -0,0 +1,22 @@
1
+
2
+ export const shadowRoot = `
3
+ <form is="my-form" get="../info.json" test="{{test}}" change="{{change}}">
4
+ <input name="name"/>
5
+ <input name="age"/>
6
+ <comp-test accept="image/*" change="{{change}}"></comp-test>
7
+ <button type="button" onclick="{{test}}">test</button>
8
+ </form>
9
+ `
10
+
11
+ export default class extends HTMLElement {
12
+
13
+ change(files) {
14
+
15
+ console.log(files)
16
+ }
17
+
18
+ test() {
19
+
20
+ console.log('test')
21
+ }
22
+ }
@@ -0,0 +1,96 @@
1
+ import {bindNode, useEvent} from "../index.mjs";
2
+
3
+ export default class RouterView extends HTMLElement {
4
+
5
+ routes = []
6
+
7
+ event = useEvent()
8
+
9
+ constructor() {
10
+
11
+ super()
12
+
13
+ this.event.add('route.to', (...args) => this.routeTo(...args))
14
+
15
+ this.event.add('init routers', routes => {
16
+
17
+ this.routes = routes
18
+
19
+ setTimeout(() => this.init(), 43)
20
+
21
+ }, {preValue: true, once: true})
22
+ }
23
+
24
+ routeTo(target, params) {
25
+
26
+ const route = this.routes.find(route => route.path === target)
27
+
28
+ if (!route) return new ErrorEvent(`route to "${target}" not found`)
29
+
30
+ location.hash = '#' + target
31
+
32
+ route.component.params = params
33
+ }
34
+
35
+ init() {
36
+
37
+ for (const route of this.routes) {
38
+
39
+ if(typeof route.component === 'string') {
40
+
41
+ route.component = document.createElement(route.component)
42
+
43
+ if (route.dir) {
44
+
45
+ route.component.setAttribute('dir', route.dir)
46
+ }
47
+ }
48
+ }
49
+
50
+ if (this.routes.some(route => route.path !== '/404')) {
51
+
52
+ const route = {
53
+ path: '/404',
54
+ dir: './pages',
55
+ component: document.createElement('not-found'),
56
+ }
57
+
58
+ route.component.setAttribute('dir', route.dir)
59
+
60
+ this.routes.push(route)
61
+ }
62
+
63
+ addEventListener('hashchange', () => this.hashListener())
64
+
65
+ this.hashListener()
66
+ }
67
+
68
+ hashListener() {
69
+
70
+ const currentHash = location.hash.slice(1) || '/';
71
+
72
+ this.event.dispatch('route.change', currentHash)
73
+
74
+ const route = this.routes.find(route => route.path === currentHash) || this.routes.find(route => route.path === '/404')
75
+
76
+ if (!route) return
77
+
78
+ const component = route.component.cloneNode(true)
79
+
80
+ if (currentHash === '/system/user') {
81
+
82
+ // debugger
83
+ }
84
+
85
+ bindNode(this, component, true)
86
+
87
+ if (this.firstElementChild) {
88
+
89
+ this.replaceChild(component, this.firstElementChild)
90
+
91
+ } else {
92
+
93
+ this.appendChild(component)
94
+ }
95
+ }
96
+ }