我创建了一种编程语言,可以更快地构建前端 Web 应用程序
在为 Web 构建前端应用程序时,我总是发现在应用程序声明部分使用 HTML/XML 语法,在逻辑部分使用 JavaScript/TypeScript 之间切换很不直观。对于大多数工具和框架来说,这似乎总是不必要的复杂性 - 为什么不使用一种语言来同时处理这两种语言呢?
这就是我创建 Live Elements 的原因,它是 javascript(或 typescript)的编程语言扩展,它完全消除了对类似 XML 的语言的需求,并提供了逻辑和 UI 声明之间的无缝过渡。
Live Elements 还补充道:
本文将简要概述 Live Elements 背后的主要概念,但如果您想深入了解,可以在 liveelements.io/documentation 上找到分步指南,带您了解所有概念。
还有一个现场游乐场,您可以在其中现场查看和尝试不同的示例。
概述
Live Elements 引入了组件的概念。组件的工作方式与 javascript 类类似,但具有附加功能。以下是使用“component”关键字声明基本组件的方法:
component MyComponent{}
要从另一个组件继承,我们使用 `<` 符号:
component MySecondComponent < MyComponent{}
继承的工作方式与 javascript 类类似,这意味着组件可以从其父组件继承函数和属性。
创建组件与 javascript 略有不同,因为我们不使用“new”关键字,而是使用组件名称后跟花括号:
const mycomponent = MyComponent{} // similar to 'new MyComponent()' in javascript
在组件主体内部,我们可以声明属性:
component MyComponent{ number a: 100 }; const m = MyComponent{} console.log(m.a) // Output: 100
注意我们如何将 javascript 与 Live Elements 结合起来。`const mc = ...` 和 `console.log(mc.a)` 是 javascript 表达式,而组件声明和创建是 Live Elements 语法的一部分。
创建组件时,我们可以在组件主体内部分配属性:
component MyComponent{ number a: 100 } const m = MyComponent{ a: 200 // reassign 'a' with 200 } console.log(m.a) // Output: 200
属性会自动绑定到其他属性。在下面的例子中,当 `a` 改变时,`b` 也会改变:
component MyComponent{ number a: 100 number b: this.a // b is now bound to a } const m = MyComponent{} console.log(m.b) // Output: 100 m.a = 200 // b is now 200 as well, since it's bound to a console.log(m.b) // Output: 200
构造函数的声明方式与 javascript 构造函数类似,但是必须明确调用 `super()` 方法和 `this{}` 表达式。`this{}` 表达式用于初始化组件,以及创建和分配组件属性:
component ComponentWithConstructor{ constructor(a:number, b:number){ super() this{} console.log("Initialized with:", a, b) } }
要创建使用构造函数的组件,请在组件名称后、花括号前添加一个点和括号:
const c = ComponentWithConstructor.(100, 200){} // Output: Initialized with 100 200
Live Elements 提供了一种快捷方式,即使用一个带有单个字符串参数的构造函数来创建组件。除了使用括号,您还可以在组件名称后立即使用标记字符串。以下是示例:
component ComponentWithString{ constructor(s:string){ super() this{} console.log("Initialized with:", s) } }
此语法允许您编写:
const c = ComponentWithString`Hello!` // Output: Initialized with: Hello
组件可以在组件主体内声明子组件。以下组件是使用子组件数组创建的:
component A{} component ComponentWithChildren{ Arraychildren } const c = ComponentWithChildren{ children: [ A{}, A{} ] } console.log(c.children.length) // Output: 2
但是,可以使用“默认”属性声明同一组件,该属性是使用组件主体内部声明的子项自动分配的属性:
component A{} component ComponentWithChildren{ // declares a default property, which is implicitly of type Arraydefault children } const c = ComponentWithChildren{ A{} // Add children directly to the component body A{} } console.log(c.children.length) // Output: 2
“默认”属性使得以内联方式添加子组件变得更加自然,从而保持语法的干净和直观。
DOM 和网页
现在我们可以看看上述概念如何应用于网络。Html 标签如` `,` `,` 等在 Live Elements 中被视为组件。此外,还有一个表示文本节点的 `T` 组件。例如,一个段落可以这样写: `P` 有一个 `children` 属性,被视为 `default`,上面实际上是缩写: 为了进一步缩短表达式,可以使用上面提到的标记字符串构造函数创建“T”: `P` 也可以用标记字符串构造函数来创建,因此上面的代码可以进一步缩短: 您可以在 Play 上尝试这些示例。只需粘贴以下代码即可查看其运行情况: 为了让本文更简短、更集中,我们没有介绍 Live Elements 中的导入,但这里有一个快速概述:导入使用“import”关键字,后跟模块 URI,并将模块带入全局范围。例如,导入“live-elements-web-server.view”可使“PageView”全局可访问,导入“live-web.dom”将使“P”和“Div”等 DOM 元素全局可访问。 下面是另一个示例,演示如何创建“ ` 带有一个段落和一个标题: 创建按钮 组件还支持**事件**和**监听器**。您可以使用 `on` 关键字声明事件的监听器,并为其分配一个函数。例如,您可以这样创建一个监听 `click` 事件的按钮: 引用其他组件 层次结构内的组件可以使用“id”属性相互引用: 创建计数器 到目前为止,我们已经介绍了: 组件、构造函数和子项 组件属性绑定 Web DOM 集成 事件监听器 组件 ID 有了这些概念,我们可以构建一个简单的反例来展示一切是如何协同工作的。你可以将以下代码粘贴到 Playground 中来查看它的实际运行情况: 段落“P”有一个文本节点“T”,它绑定到“index.counter.value”,一旦点击按钮就会更新。 计数器样式 Live Elements 中的组件可以使用作用域样式将样式应用于自身及其子项。要将作用域样式添加到组件,请将“ScopedStyle”组件添加到静态“use”属性: 在 Playground 中,我们包含了 `index.css` 样式表,它已作为 Playground 工作区的一部分提供。这使我们能够通过在 `index.css` 文件中定义样式来直接设置计数器的样式: Live Elements 中的组件可以使用样式表,但它们也可以从其他组件导入样式。例如,如果组件“A”使用“a.css”样式表,而组件“Index”使用组件“A”,则“a.css”中的样式将自动包含在组件“Index”中。 为了避免冲突,“a.css”中的所有样式选择器都以组件“A”的唯一选择器作为前缀,确保样式保持在“A”范围内并且不会干扰其他组件。 此功能可让您轻松创建和重用具有自己封装样式的现成组件。例如,在“live-web-view.button”模块中有一个名为“PrimaryButton”的预制按钮组件。以下是您可以将其用于计数器示例的方法: 结论 在本文中,我们探讨了 Live Elements 的一些核心概念。我们研究了如何构建计数器等实际示例,并演示了如何利用样式表和预样式组件来创建可重复使用的封装 UI 元素。 通过将声明性和逻辑性方面结合成一种统一的语言,Live Elements 简化了开发过程并加快了功能构建速度。
P{ T{ text: 'This is a paragraph' }} // equivalent toThis is a paragraph
`P` has a `children` property that is treated as `default`, the above is actually short for:
P{ children: [T{ text: 'This is a paragraph' }] }
To shorten the expression even more, `T` can be created with a tagged string constructor mentioned above:
P{ T`This is a paragraph` }
`P` can also be created with a tagged string constructor, so the above can be shortened even more:
P`This is a paragraph`
You can try these examples in the playground. Simply paste the following code to see it in action:
import live-web.dom import live-elements-web-server.view component Index < PageView{ P`This is a paragraph` }
We haven’t covered imports in Live Elements to keep this article shorter and more focused, but here’s a quick overview: an import uses the `import` keyword followed by the module URI, and it brings the module into the global scope. For example, importing `live-elements-web-server.view` makes `PageView` globally accessible, and importing `live-web.dom` will make DOM elements like `P` and `Div` globally accessible.
Here's another example that demonstrates creating a `
import live-web.dom import live-elements-web-server.view component Index < PageView{ Div{ H1`This is a header.` P`This is a paragraph` } }
Creating a Button
Components also support **events** and **listeners**. You can declare a listener for an event using the `on` keyword, and assign it a function. For example, this is how you can create a button that listens to for a `click` event:
Button{ on click: () => { console.log('button clicked') } T`Click here` }
Referencing other components
Components inside hierarchies can reference each other using the `id` property:
component D{ id: myid string data: 'Welcome Message' P{ T{ text: myid.data } // text is now 'Welcome Message' } }
Creating a Counter
So far, we've covered:
With these concepts, we can build a simple counter example to show how everything works together. You can paste the following code into the playground to see it in action:
import live-web.dom import live-elements-web-server.view import live-elements-web-server.style component Counter{ // Counter state number value: 0 } component Index < PageView{ id: index // this can now be referenced via 'index' // counter property is now a new Counter Counter counter: Counter{} Div{ // we reference index.counter.value P{ T{ text: index.counter.value }} Button{ // increment index.counter.value when button is clicked on click: () => { index.counter.value++ } T`Increment` } } }
The paragraph `P` has a text node `T`, which is bound to `index.counter.value`, which will update once the button is clicked.
Styling the counter
Components in Live Elements can use scoped styles to apply styles to themselves and their children. To add a scoped style to a component, include a `ScopedStyle` component to the static `use` property:
import live-web.dom import live-elements-web-server.view import live-elements-web-server.style component Counter{ number value: 0 } component Index < PageView{ id: index static any[] use = [ // use index.css stylesheet ScopedStyle{ src: './index.css'} ] Counter counter: Counter{} Div{ P{ T{ text: index.counter.value }} Button{ on click: () => { index.counter.value++ } T`Increment` } } }
In the playground, we’ve included the `index.css` stylesheet, which is already available as part of the playground workspace. This allows us to style the counter directly by defining styles in the `index.css` file:
p{ font-size: 30px; color: blue; }
Components in Live Elements can use stylesheets, but they can also import styles from other components. For example, if component `A` uses a `a.css` stylesheet, and component `Index` uses component `A`, the styles from `a.css` will be automatically included in component `Index`.
To avoid conflicts, all style selectors in `a.css` are prefixed with a unique selector for component `A`, ensuring the styles remain scoped to `A` and don’t interfere with other components.
This feature makes it easy to create and reuse ready-made components with their own encapsulated styles. For instance, there's a pre-styled button component in `live-web-view.button` module called `PrimaryButton`. Here’s how you can use it for the counter example:
import live-web.dom import live-web-view.button import live-elements-web-server.view import live-elements-web-server.style component Counter{ number value: 0 } component Index < PageView{ id: index static any[] use = [ // use PrimaryButton styles PrimaryButton, // use index.css stylesheet ScopedStyle{ src: './index.css'} ] Counter counter: Counter{} Div{ P{ T{ text: index.counter.value }} // use PrimaryButton instead of Button PrimaryButton{ on click: () => { index.counter.value++ } T`Increment` } } }
Conclusion
In this article, we’ve explored some of the core concepts of Live Elements. We looked at building a pratical example like a counter and demonstrated how to leverage stylesheets and pre-styled components to create reusable, encapsulated UI elements.
By combining the declarative and logical aspects into a single, unified language, Live Elements simplifies the development process and makes it faster to build features.