Compare commits

..

7 Commits

10 changed files with 231 additions and 93 deletions

5
.gitignore vendored
View File

@ -1,12 +1,9 @@
*.min.js
.httpserver
index.html
test.js
.vscode
node_modules/
dist/
*.sublime-project
*.sublime-workspace
package-lock.json

1
.npmignore Normal file
View File

@ -0,0 +1 @@
test/

View File

@ -1,5 +1,5 @@
## Wkit
> A library for building fast, lightweight web components.
> 一个简单易用、功能完善的用于开发`web components`的轻量级开发库。模板解析基于`lit-html`二次开发
![downloads](https://img.shields.io/npm/dt/wkit.svg)
![version](https://img.shields.io/npm/v/wkit.svg)
@ -15,6 +15,7 @@
- `:xxx=yyy`, 属性绑定, 类似`vue`,在属性前加上`:`, 该属性不再使用`setAttribute()`, 而是直接赋值, 可满足你需要传递复杂数据结构的需求。
- `#animation={type}`, 开箱即用的动画配置, 内置6种动画`fade(默认), scale, bounce, micro-bounce, rotate, slide`
- `ref=xxx`, 类似`vue`的节点标识, 可方便的在`mounted()`之后通过 `this.$refs.xxx` 来访问该节点
- 内置特色的双向绑定方法`live()`
- 用内置的`bind()`方法来给当前组件绑定事件时, 组件移除时事件也会自动销毁,无需手动销毁。
- 灵活的`props`定义
@ -84,3 +85,7 @@ Hello.reg('hello')
### 开源协议
- BSD 3-Clause License (Lit框架的html模板解析的源码)
- MIT (除Lit代码之外的所有代码)

View File

@ -1,6 +1,6 @@
{
"name": "wkit",
"version": "1.11.2",
"version": "1.12.0",
"type": "module",
"description": "A library for building fast, lightweight web components.",
"main": "dist/index.js",
@ -14,7 +14,7 @@
},
"repository": {
"type": "git",
"url": "git+https://git.wkit.fun/bytedo/wkit.git"
"url": "https://git.wkit.fun/bytedo/wkit.git"
},
"keywords": [
"wkit",

View File

@ -83,16 +83,14 @@ function getType(v) {
let type = String
let attribute = true
if (v.includes('!')) {
v = v.split('!')
let _t = v.shift()
if (_t === 'str') {
v = v.join('!')
} else if (_t === 'num') {
if (v.startsWith('str!')) {
v = v.slice(4)
} else if (v.startsWith('num!')) {
type = Number
v = +v.shift() || 0
} else if (_t === 'bool') {
v = +v.slice(4) || 0
} else if (v.startsWith('bool!')) {
type = Boolean
v = v.shift()
v = v.slice(5)
v = v !== 'false' && v !== ''
}
attribute = false

View File

@ -6,31 +6,30 @@
import { boolMap, WC_PART, NOTHING } from './constants.js'
import { animate, MODES } from './anim.js'
import { nextTick } from './utils.js'
import { nextTick, noop } from './utils.js'
const boundAttributeSuffix = '$wc$'
const marker = `wc$${String(Math.random()).slice(9)}$`
const markerMatch = '?' + marker
const nodeMarker = `<${markerMatch}>`
const createMarker = (v = '') => document.createComment(v)
const BIND_ATTR_SUFFIX = '{{$wkit$}}'
const MARKER = `{{^wkit${String(Math.random()).slice(-8)}^}}`
const MARKER_MATCH = '?' + MARKER
const NODE_MARKER = `<${MARKER_MATCH}>`
// 是否原始值
const isPrimitive = value =>
value === null || (typeof value != 'object' && typeof value != 'function')
value === null || (typeof value !== 'object' && typeof value !== 'function')
const isArray = Array.isArray
const isIterable = value =>
isArray(value) ||
typeof (value === null || value === void 0
? false
: value[Symbol.iterator]) === 'function'
value ? isArray(value) || typeof value[Symbol.iterator] === 'function' : false
const SPACE_CHAR = `[ \n\f\r]`
const ATTR_VALUE_CHAR = `[^ \n\f\r"'\`<>=]`
const NAME_CHAR = `[^\\s"'>=/]`
const textEndRegex = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g
const TEXT_END_REGEX = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g
const COMMENT_START = 1
const TAG_NAME = 2
const DYNAMIC_TAG_NAME = 3
const commentEndRegex = /-->/g
const comment2EndRegex = />/g
const tagEndRegex = new RegExp(
const COMMENT_END_REGEXP = /-->/g
const COMMENT_END_REGEXP_2 = />/g
const TAG_END_REGEXP = new RegExp(
`>|${SPACE_CHAR}(?:(${NAME_CHAR}+)(${SPACE_CHAR}*=${SPACE_CHAR}*(?:${ATTR_VALUE_CHAR}|("|')|))|$)`,
'g'
)
@ -38,9 +37,9 @@ const ENTIRE_MATCH = 0
const ATTRIBUTE_NAME = 1
const SPACES_AND_EQUALS = 2
const QUOTE_CHAR = 3
const singleQuoteAttrEndRegex = /'/g
const doubleQuoteAttrEndRegex = /"/g
const rawTextElement = /^(?:script|style|textarea|title)$/i
const SINGLE_QUOTE_REGEXP = /'/g
const DOUBLE_QUOTE_REGEXP = /"/g
const RAW_TEXT_ELEM_REGEXP = /^(?:script|style|textarea|title)$/i
const HTML_RESULT = 1
const SVG_RESULT = 2
const ATTRIBUTE_PART = 1
@ -52,14 +51,12 @@ const COMMENT_PART = 7
const TEMPLATE_CACHE = new Map()
const walker = document.createTreeWalker(document, 129, null, false)
function noop() {}
function getTemplateHtml(strings, type) {
let len = strings.length - 1
let attrNames = []
let html2 = type === SVG_RESULT ? '<svg>' : ''
let rawTextEndRegex
let regex = textEndRegex
let regex = TEXT_END_REGEX
for (let i = 0; i < len; i++) {
let s = strings[i]
let attrNameEndIndex = -1
@ -73,25 +70,26 @@ function getTemplateHtml(strings, type) {
break
}
lastIndex = regex.lastIndex
if (regex === textEndRegex) {
if (regex === TEXT_END_REGEX) {
if (match[COMMENT_START] === '!--') {
regex = commentEndRegex
regex = COMMENT_END_REGEXP
} else if (match[COMMENT_START] !== void 0) {
regex = comment2EndRegex
regex = COMMENT_END_REGEXP_2
} else if (match[TAG_NAME] !== void 0) {
if (rawTextElement.test(match[TAG_NAME])) {
if (RAW_TEXT_ELEM_REGEXP.test(match[TAG_NAME])) {
rawTextEndRegex = new RegExp(`</${match[TAG_NAME]}`, 'g')
}
regex = tagEndRegex
regex = TAG_END_REGEXP
} else if (match[DYNAMIC_TAG_NAME] !== void 0) {
regex = tagEndRegex
regex = TAG_END_REGEXP
}
} else if (regex === tagEndRegex) {
} else if (regex === TAG_END_REGEXP) {
if (match[ENTIRE_MATCH] === '>') {
regex =
rawTextEndRegex !== null && rawTextEndRegex !== void 0
? rawTextEndRegex
: textEndRegex
: TEXT_END_REGEX
attrNameEndIndex = -1
} else if (match[ATTRIBUTE_NAME] === void 0) {
attrNameEndIndex = -2
@ -100,52 +98,65 @@ function getTemplateHtml(strings, type) {
attrName = match[ATTRIBUTE_NAME]
regex =
match[QUOTE_CHAR] === void 0
? tagEndRegex
? TAG_END_REGEXP
: match[QUOTE_CHAR] === '"'
? doubleQuoteAttrEndRegex
: singleQuoteAttrEndRegex
? DOUBLE_QUOTE_REGEXP
: SINGLE_QUOTE_REGEXP
}
} else if (
regex === doubleQuoteAttrEndRegex ||
regex === singleQuoteAttrEndRegex
regex === DOUBLE_QUOTE_REGEXP ||
regex === SINGLE_QUOTE_REGEXP
) {
regex = tagEndRegex
} else if (regex === commentEndRegex || regex === comment2EndRegex) {
regex = textEndRegex
regex = TAG_END_REGEXP
} else if (
regex === COMMENT_END_REGEXP ||
regex === COMMENT_END_REGEXP_2
) {
regex = TEXT_END_REGEX
} else {
regex = tagEndRegex
regex = TAG_END_REGEXP
rawTextEndRegex = void 0
}
}
let end =
regex === tagEndRegex && strings[i + 1].startsWith('/>') ? ' ' : ''
regex === TAG_END_REGEXP && strings[i + 1].startsWith('/>') ? ' ' : ''
html2 +=
regex === textEndRegex
? s + nodeMarker
regex === TEXT_END_REGEX
? s + NODE_MARKER
: attrNameEndIndex >= 0
? (attrNames.push(attrName),
s.slice(0, attrNameEndIndex) +
boundAttributeSuffix +
BIND_ATTR_SUFFIX +
s.slice(attrNameEndIndex)) +
marker +
MARKER +
end
: s +
marker +
MARKER +
(attrNameEndIndex === -2 ? (attrNames.push(void 0), i) : end)
}
let htmlResult =
html2 + (strings[len] || '<?>') + (type === SVG_RESULT ? '</svg>' : '')
if (!Array.isArray(strings) || !strings.hasOwnProperty('raw')) {
if (!isArray(strings) || !strings.hasOwnProperty('raw')) {
throw new Error('invalid html ast')
}
return [htmlResult, attrNames]
}
function createElement(v = '') {
let el = document.createElement('template')
el.innerHTML = v
return el
}
function createMarker(v = '') {
return document.createComment(v)
}
class Template {
parts = []
constructor({ strings, ['__dom_type__']: type }, options) {
constructor({ strings, values, __dom_type__: type }, options) {
let node
let nodeIndex = 0
let attrNameIndex = 0
@ -153,7 +164,7 @@ class Template {
let parts = this.parts
let [html2, attrNames] = getTemplateHtml(strings, type)
this.el = Template.createElement(html2)
this.el = createElement(html2)
walker.currentNode = this.el.content
@ -170,17 +181,14 @@ class Template {
let attrsToRemove = []
for (let name of node.getAttributeNames()) {
if (
name.endsWith(boundAttributeSuffix) ||
name.startsWith(marker)
) {
if (name.endsWith(BIND_ATTR_SUFFIX) || name.startsWith(MARKER)) {
let realName = attrNames[attrNameIndex++]
attrsToRemove.push(name)
if (realName !== void 0) {
let value = node.getAttribute(
realName.toLowerCase() + boundAttributeSuffix
realName.toLowerCase() + BIND_ATTR_SUFFIX
)
let statics = value.split(marker)
let statics = value.split(MARKER)
let m = /([#:@])?(.*)/.exec(realName)
let decorates = []
@ -216,8 +224,8 @@ class Template {
node.removeAttribute(name)
}
}
if (rawTextElement.test(node.tagName)) {
let strings2 = node.textContent.split(marker)
if (RAW_TEXT_ELEM_REGEXP.test(node.tagName)) {
let strings2 = node.textContent.split(MARKER)
let lastIndex = strings2.length - 1
if (lastIndex > 0) {
node.textContent = ''
@ -231,24 +239,19 @@ class Template {
}
} else if (node.nodeType === 8) {
let data = node.data
if (data === markerMatch) {
if (data === MARKER_MATCH) {
parts.push({ type: CHILD_PART, index: nodeIndex })
} else {
let i = -1
while ((i = node.data.indexOf(marker, i + 1)) !== -1) {
while ((i = node.data.indexOf(MARKER, i + 1)) !== -1) {
parts.push({ type: COMMENT_PART, index: nodeIndex })
i += marker.length - 1
i += MARKER.length - 1
}
}
}
nodeIndex++
}
}
static createElement(html2) {
let el = document.createElement('template')
el.innerHTML = html2
return el
}
}
class TemplateInstance {
@ -378,7 +381,7 @@ class ChildPart {
} else if (value !== this.#value) {
this.#commitText(value)
}
} else if (value['__dom_type__'] !== void 0) {
} else if (value.__dom_type__ !== void 0) {
this.#commitTemplateResult(value)
} else if (value.nodeType !== void 0) {
this.#commitNode(value)
@ -415,12 +418,11 @@ class ChildPart {
}
#commitTemplateResult(result) {
let { values, ['__dom_type__']: type } = result
let { values, __dom_type__: type } = result
let template =
typeof type === 'number'
? this.#getTemplate(result)
: (type.el === void 0 && (type.el = Template.createElement(type.h)),
type)
: (type.el === void 0 && (type.el = createElement(type.h)), type)
if (this.#value?.$template === template) {
this.#value.update(values)
@ -527,6 +529,10 @@ class AttributePart {
let strings = this.strings
let changed = false
if (typeof value === 'function') {
value = value(this.element)
}
if (strings === void 0) {
changed = !isPrimitive(value) || value !== this.#value
if (changed) {
@ -548,7 +554,8 @@ class AttributePart {
this.#value[i] = v
}
}
if (changed) {
// value属性做特殊处理, 解决输入框无法清空的问题
if (changed || this.name === 'value') {
this.commitValue(value)
}
}
@ -571,6 +578,10 @@ class AttributePart {
} else {
elem.removeAttribute(attr)
}
} else {
// value 绑定, 特殊处理
if (attr === 'value') {
elem.value = value
} else {
if (value === null || value === void 0) {
elem.removeAttribute(attr)
@ -579,6 +590,7 @@ class AttributePart {
}
}
}
}
}
// 赋值属性
class PropertyPart extends AttributePart {

View File

@ -59,6 +59,42 @@ export function styleMap(data = {}) {
return output
}
// 三元运算符的函数封装(只为了省个参数)
export function when(condition, trueCase = '', falseCase = '') {
return condition ? trueCase : falseCase
}
// swicth语句的封装
export function which(target, list = [], defaultCase = '') {
for (let [name, content] of list) {
if (target === name) {
return content
}
}
return defaultCase
}
/**
* 双向绑定
* @param key<String> 绑定的字段
* @param el 当前对象, 无需传递, 由框架内部处理
*
*/
export function live(key, el) {
let callback = ev => {
Function('o', 'v', `o.${key} = v`)(this, ev.target.value)
this.$requestUpdate()
}
if (el && !el.__live__) {
bind(el, 'input', callback)
this.$events.input ??= []
this.$events.input.push({ el, listener: callback, options: false })
el.__live__ = true
}
return Function('o', `return o.${key}`)(this)
}
export class Component extends HTMLElement {
/**
* 声明可监听变化的属性列表
@ -387,6 +423,7 @@ export class Component extends HTMLElement {
this.activated()
}
this.mounted()
this.$requestUpdate()
})
} else {
nextTick(_ => this.updated(props))

View File

@ -4,7 +4,7 @@
* @date 2023/03/07 22:11:30
*/
function noop() {}
export function noop() {}
export function $(selector, container, multi) {
let fn = multi ? 'querySelectorAll' : 'querySelector'
@ -108,7 +108,7 @@ export function offset(node) {
/**
* 事件绑定
*/
export function bind(dom, type = '', selector, fn, phase = true) {
export function bind(dom, type = '', selector, fn, phase = false) {
let events = type.split(',')
let callback
let isWc = dom && dom.host === dom
@ -121,7 +121,7 @@ export function bind(dom, type = '', selector, fn, phase = true) {
}
if (typeof selector === 'function') {
phase = fn
phase = fn || false
fn = selector
selector = null
} else {
@ -168,12 +168,22 @@ export function bind(dom, type = '', selector, fn, phase = true) {
events.forEach(function (t) {
t = t.trim()
if (isWc) {
if (host.$events[t]) {
host.$events[t].push({ el: dom, listener: callback, options: phase })
} else {
host.$events[t] = [{ el: dom, listener: callback, options: phase }]
host.$events[t] ??= []
let _list = host.$events[t]
if (_list.length) {
let idx = _list.findIndex(
it =>
it.el === dom && it.listener === callback && it.options === phase
)
if (idx > -1) {
let item = _list[idx]
_list.splice(idx, 1)
dom.removeEventListener(t, item.listener, item.options)
}
}
_list.push({ el: dom, listener: callback, options: phase })
}
dom.addEventListener(t, callback, phase)
})
return callback

47
test/index.html Normal file
View File

@ -0,0 +1,47 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title>wkit test page</title>
<style>
body {line-height: 1.5;font-size:14px;background:var(--page-color);color:var(--text-color);}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
border-radius: 10px;
background: var(--summary-bg-color);
}
::-webkit-scrollbar-thumb {
border-radius: 3px;
background: var(--border-color-dark);
}
::-webkit-scrollbar-thumb:hover {
background: var(--summary-color);
}
@media screen and (max-width: 480px) {
::-webkit-scrollbar {
display: none;
width: 0;
}
}
</style>
<script type="importmap">
{
"imports":{
"es.shim":"//jscdn.ink/es.shim/latest/index.js",
"wkit":"//127.0.0.1:9999/src/index.js",
"wkitd":"//jscdn.ink/wkitd/1.3.10/index.js",
"fetch":"//jscdn.ink/@bytedo/fetch/latest/next.js",
"@bd/ui/":"//jscdn.ink/@bd/ui/0.1.16/"
}
}
</script>
<script type="module" src="./test.js"></script>
</head>
<body>
<wc-app></wc-app>
</body>
</html>

31
test/test.js Normal file
View File

@ -0,0 +1,31 @@
import { html, Component, live } from 'wkit'
class App extends Component {
foo = ''
bar = { a: 333 }
created() {
// live = live.bind(this)
}
reset() {
this.foo = ''
this.bar.a = '666'
this.$requestUpdate()
}
render() {
return html`
<p>${this.foo}</p>
<p>${this.bar.a}</p>
<input value=${live.bind(this, 'bar.a')} />
<input value=${this.foo} />
<input value=${live.bind(this, 'foo')} />
<button @click=${this.reset}>666</button>
`
}
}
App.reg('app')