1. 前提
stenciljs采用
JSX
和TypeScript
语法。
关于JSX
和TypeScript
如下:
- React中JSX 简介
- StencilJS中的的JSX
- Stencil官网中介绍的JSX
- TypeScript: 推荐阮一峰的博客。TypeScript
- stencil-component github源码(README.md)
- 开发一个组件的步骤:
Weather Widget
为例:Weather Widget
2. 初始化项目
// 1
git clone https://github.com/ionic-team/stencil-component-starter.git my-component
cd my-component
git remote rm origin
// 2
npm install // 此处报错:需要运行: npm set registry https://registry.npmjs.org/
npm start
3. 新建组件
在src/components
下运行npm run generate 组件名(例如: weather-widget)
。
4. 定制组件
import { Component, Prop, h } from '@stencil/core';
@Component({
tag: 'my-first-component',
})
export class MyComponent {
// 组件属性
@Prop() name: string;
render() {
return (
<p>
My name is {this.name}
</p>
);
}
}
呈现后,浏览器将显示My name is Max。
5. 语法
5.1 API
-
装饰器
@Component() 声明一个新的Web组件 @Prop() 声明暴露的属性/属性 @State() 声明组件的内部状态 @Watch() 声明在属性或状态更改时运行的钩子 @Element() 声明对host元素的引用 @Method() 声明一个公开的公共方法 @Event() 声明组件可能发出的DOM事件 @Listen() 监听DOM事件
-
生命周期钩子
connectedCallback() disconnectedCallback() componentWillLoad() componentDidLoad() componentShouldUpdate(newValue, oldValue, propName): boolean componentWillRender() componentDidRender() componentWillUpdate() componentDidUpdate() render()
-
其他
Host
: Host是一个功能组件,可以在render函数的根部使用它来为host元素本身设置属性和事件侦听器。
h()
: 用于render()将JSX转换为虚拟DOM元素。
readTask()
: 调度DOM读取任务。提供的回调将在最佳时机执行以执行DOM读取,而不会引起布局抖动。
writeTask()
: 调度DOM写入任务。提供的回调将在最佳时机执行以执行DOM突变而不会引起布局抖动。
forceUpdate()
: 即使状态未更改,也调度给定实例或元素的新渲染。注意forceUpdate()不是同步的,可能会在下一帧中执行DOM渲染。
5.2 组件
5.2.1 @Component()
@Component()
是@stencil/core
包装中的装饰器。在最简单的情况下,开发人员必须为tag
组件提供一个HTML
名称。通常,styleUrl
提供样式表,styleUrls
可以为不同的应用程序模式/主题提供多个不同的样式表。import { Component } from '@stencil/core'; @Component({ tag: 'todo-list', styleUrl: 'todo-list.css' }) export class TodoList { }
5.2.2 @Component(opts: ComponentOptions)
Component属性
export interface ComponentOptions {
/**
* web组件的标记名。理想情况下,标记名必须是全局唯一的,
* 因此,建议为同一集合中的所有组件选择一个唯一的前缀。
*
* 此外,标记名必须包含“-”
*/
tag: string;
/**
* 如果为“true”,则组件将使用作用域样式表。类似于 shadow-dom,
* 默认为“false”。
*/
scoped?: boolean; // true/false
/**
* 如果为“true”,则组件将使用本机shadow dom封装,如果浏览器, 如果浏览器本身不支持shadow-dom。默认
* 为“false”
*/
shadow?: boolean;
/**
* 指向某个外部样式表文件的相对URL。它应该是一个“.css”文件,除非外部插件的安装方式类似于“@stencil/sass”。
*/
styleUrl?: string;
/**
* 类似于“styleUrl”,但允许为不同的模式指定不同的样式表。
*/
styleUrls?: string[] | d.ModeStyles;
/**
* 包含内联CSS而不是使用外部样式表的字符串。
* 此特性的性能特性与使用外部样式表相同。
*
* 注意,您不能使用sass或less,只有“css”才允许使用“styles”,如果需要更高级的功能,
* 请使用“styleUrl”。
*/
styles?: string;
/**
* 指向组件所需资源文件夹的相对链接数组。
*/
assetsDirs?: string[];
/**
* 已弃用 推荐使用“assetsDirs”
*/
assetsDir?: string;
}
5.2.3 嵌入或嵌套组件
将子组件my-embedded-component
嵌入到my-parent-component
中。
子组件:
import { Component, Prop, h } from '@stencil/core';
@Component({
tag: 'my-embedded-component'
})
export class MyEmbeddedComponent {
@Prop() color: string = 'blue';
render() {
return (
<div>My favorite color is {this.color}</div>
);
}
}
父组件:
import { Component, h } from '@stencil/core';
@Component({
tag: 'my-parent-component'
})
export class MyParentComponent {
render() {
return (
<div>
<my-embedded-component color="red"></my-embedded-component>
</div>
);
}
}
5.2.4 组件生命周期
5.2.4.1 connectedCallback()
每次组件连接到DOM时调用。首次连接组件时,此方法在componentWillLoad之前调用。
注意:每次在DOM中附加或移动元素时,都可以多次调用此方法。
const el = document.createElement('my-cmp');
document.body.appendChild(el);
// 已调用connectedCallback()
// 调用了componentWillLoad()(第一次)
el.remove();
// disconnectedCallback()
document.body.appendChild(el);
// 已再次调用connectedCallback(),但“componentWillLoad”未调用。
5.2.4.2 offlineedCallback()
每当组件与DOM断开连接时即被调用,即可以将其调度多次,不要与
onDestroy
事件混淆。
5.2.4.3 componentWillLoad()
在组件首次连接到DOM后立即调用一次。由于此方法
仅被调用一次
,因此是异步加载数据的好方式。
可以返回一个promise,可以用来等待第一个渲染。
5.2.4.4 componentDidLoad()
组件完全加载后立即调用一次,第一次调用render()发生。
5.2.4.5 componentShouldUpdate()
当组件的Prop或State属性更改并且将要重新渲染时,将调用此钩子。
这个钩子接收三个参数:新值,旧值和更改状态的名称。它应返回一个布尔值,以指示组件是否应重新呈现。
需要注意的几件事是,该方法不会在初始渲染之前执行,即,在组件首次附加到dom时,或者在下一帧中已经计划了重新渲染时,都不会执行该方法。
component.somePropA = 42;
component.somePropB = 88;
componentShouldUpdate
会使用参数调用:42
、undefined
和somePropA
。如果确实返回true,则由于已计划进行重新渲染,因此不会再次调用该挂钩。
相反,如果第一个钩子返回false,componentShouldUpdate则将使用再次调用88,undefined
和 somePropB
作为参数,component.somePropB = 88触发。
由于此挂钩的执行可能是有条件的,因此依靠它来监视道具更改不是很好,而是使用@Watch装饰器。
5.2.4.6 componentWillRender()
在每个render()之前调用。
可以返回一个promise,可以用来等待即将到来的渲染。
5.2.4.7 componentDidRender()
在每个render()之后调用。
5.2.4.8 componentDidRender()
在每个render()之后调用。
5.2.4.9 componentWillUpdate()
当由于某些
Prop
或State
更改而将要更新组件时调用。 在第一个render
中从未调用过它。
可以返回一个promise,可以用来等待下一个渲染。
5.2.4.10 componentDidUpdate()
在组件更新后立即调用。在第一个过程中从未调用过它render()。
5.2.4.11 渲染状态
始终建议在componentWillRender()中进行任何渲染状态的更新,因为这是在render()方法之前调用的方法。 另外,使用componentDidLoad(),componentDidUpdate()和componentDidRender()方法更新渲染状态将导致另一次渲染,这对于性能而言并不理想。
如果必须在componentDidUpdate()或componentDidRender()中更新状态,则可能会使组件陷入无限循环。 如果不可避免要在componentDidUpdate()中更新状态,则该方法还应该带有一种检测道具或状态是否“脏”的方法(数据实际上是否不同或与以前相同)。 通过执行脏检查,componentDidUpdate()可以避免呈现相同的数据,从而再次调用componentDidUpdate()。
5.2.4.12 生命周期层次结构
<cmp-a>
<cmp-b>
<cmp-c></cmp-c>
</cmp-b>
</cmp-a>
cmp-a -- componentWillLoad()
cmp-b -- componentWillLoad()
cmp-c -- componentWillLoad()
cmp-c -- componentDidLoad()
cmp-b -- componentDidLoad()
cmp-a -- componentDidLoad()
5.2.4.13 异步生命周期方法
生命周期方法还可以返回promises,从而使该方法可以异步检索数据或执行任何异步任务。 一个很好的例子是获取要在组件中呈现的数据。 例如,您正在读取的这个站点首先在呈现之前获取内容数据。 但是因为fetch()是异步的,所以componentWillLoad()返回Promise以确保在其所有内容都呈现完毕之前不将其父组件视为“已加载”是很重要的。
5.2.5 Prop
import { Prop } from '@stencil/core';
...
export class TodoList {
@Prop() color: string;
@Prop() favoriteNumber: number;
@Prop() isSelected: boolean;
@Prop() myHttpService: MyHttpService;
}
HTML中:
<todo-list color="blue" favorite-number="24" is-selected="true"></todo-list>
在JSX中,您可以使用camelCase设置属性:
<todo-list color="blue" favoriteNumber={24} isSelected="true"></todo-list>
也可以从元素通过JS访问它们:
const todoListElement = document.querySelector('todo-list');
console.log(todoListElement.myHttpService); // MyHttpService
console.log(todoListElement.color); // blue
5.2.5.1 Prop options
@Prop(opts ?: PropOptions)装饰器接受一个可选参数来指定某些选项,例如可变性,DOM属性的名称或该属性的值是否应该反映到DOM中
export interface PropOptions {
attribute?: string;
mutable?: boolean;
reflect?: boolean;
}
5.2.5.2 Prop 变异性
默认情况下,Prop 从组件逻辑内部是不可变的。用户设置值后,组件将无法在内部对其进行更新。但是,可以通过将Prop声明为mutable,来明确允许其从组件内部进行更改,如下例所示:
import { Prop } from '@stencil/core';
...
export class NameElement {
@Prop({ mutable: true }) name: string = 'Stencil';
componentDidLoad() {
this.name = 'Stencil 0.7.0';
}
}
5.2.5.3 属性名称
默认情况下,Prop 从组件逻辑内部是不可变的。用户设置值后,组件将无法在内部对其进行更新。但是,可以通过将Prop声明为mutable,来明确允许其从组件内部进行更改,如下例所示:
import { Prop } from '@stencil/core';
...
export class NameElement {
@Prop({ mutable: true }) name: string = 'Stencil';
componentDidLoad() {
this.name = 'Stencil 0.7.0';
}
}
5.2.6 内部状态
5.2.6.1 状态装饰器
@State()
装饰器可用于管理组件的内部数据。 这意味着用户无法从组件外部修改此数据,但是组件可以修改它。@State()
属性的任何更改都将导致再次调用组件渲染函数。
5.2.7 @Watch
当组件上的道具或状态更改时,模具组件也会更新。 为了提高性能和简化操作,Stencil仅比较更改的引用,并且在数组或对象内部的数据更改时不会重新呈现。
对于数组,标准可变数组操作(例如push()和)unshift()不会触发组件更新。相反,应使用非可变数组运算符,因为它们会返回新数组的副本。其中包括map()和filter(),以及ES6扩展运算符语法。
5.2.8 @Event()
没有模板事件,而是模板鼓励使用DOM事件。 但是,Stencil确实提供了API,以指定组件可以发出的事件以及组件侦听的事件。 它是通过Event()和Listen()装饰器实现的。
5.2.9 @Method()
开发人员应尝试尽可能少地使用公开的方法,而应默认使用尽可能多的属性和事件。 随着应用程序的扩展,我们发现通过@Prop而非公共方法更易于管理和传递数据。
开发人员应确保在尝试调用公共方法之前,使用自定义元素注册表的whenDefined方法定义组件。
(async () => {
await customElements.whenDefined('todo-list');
const todoListElement = document.querySelector('todo-list');
await todoListElement.showPrompt();
})();
Stencil的架构在所有级别都是异步的,这带来了许多性能优势和易用性。 通过使用@Method装饰器确保公开的方法可返回
promise
。
只有具有@Method装饰器的公开方法才需要返回承诺。所有其他组件方法都是该组件专用的,不需要异步。
// 有效:使用 async
@Method()
async myMethod() {
return 42;
}
// 有效:使用 Promise.resolve()
@Method()
myMethod2() {
return Promise.resolve(42);
}
// 有效:即使它什么也不返回,也需要异步
@Method()
async myMethod3() {
console.log(42);
}
// 无效
@Method()
notOk() {
return 42;
}
私有方法:可以用于组织组件的业务逻辑,而不必返回Promise。
class Component {
getData() {
return this.someData;
}
render() {
return (
<div>{this.getData()}</div>
);
}
}
5.2.9 HOST
Host
组件可以在渲染函数的根部使用,为组件本身设置属性和事件侦听器。 就像其他JSX一样。
import { Component, Host, h } from '@stencil/core';
@Component({tag: 'todo-list'})
export class TodoList {
@Prop() open = false;
render() {
return (
<Host
aria-hidden={this.open ? 'false' : 'true'}
class={{
'todo-list': true,
'is-open': this.open
}}
/>
)
}
}
如果为this.open === true,它将呈现:
<todo-list class="todo-list is-open" aria-hidden="false"></todo-list>
相反,如果this.open === false:
<todo-list class="todo-list" aria-hidden="true"></todo-list>
<Host>
是一个虚拟组件,不会在Chrome Dev Tools中看到。
需要在根级别渲染多个组件时,这么使用:
@Component({tag: 'my-cmp'})
export class MyCmp {
render() {
return (
<Host>
<h1>Title</h1>
<p>Message</p>
</Host>
);
}
}
转化为=>
<my-cmp>
<h1>Title</h1>
<p>Message</p>
</my-cmp>
5.2.9.1 @Element()
可以返回组件element,从而使用DOM的methods
/events
。
import { Element } from '@stencil/core';
...
export class TodoList {
@Element() el: HTMLElement;
getListHeight(): number {
return this.el.getBoundingClientRect().height;
}
}
5.2.9.2 @Component
(参考5.2.1)
通过使用@Component装饰器中定义的元素标签,可以将CSS应用于元素。
@Component({
tag: 'my-cmp',
styleUrl: 'my-cmp.css'
})
...
my-cmp.css:
my-cmp {
width: 100px;
}
5.2.9.3 Shadow DOM
shadow
为true时,样式选择器需要使用host
而不是my-cmp
。
@Component({
tag: 'my-cmp',
styleUrl: 'my-cmp.css',
shadow: true
})
...
// my-cmp.css:
:host {
width: 100px;
}
5.2.10 style
5.2.10.1 Shadow DOM
Shadow DOM是浏览器内置的API,允许DOM封装和样式封装。 Shadow DOM使我们的组件不受周围环境的影响。 这意味着我们不必担心正确定义CSS的范围,也不必担心内部DOM会受到组件外部任何内容的干扰。
Shadow DOM 在 Stencil中使用:
默认情况下,使用Stencil生成的Web组件Shadow DOM默认为
false
。 要在Stencil构建的Web组件中使用Shadow DOM,可以在组件装饰器把shadow设置为true:
@Component({
tag: 'shadow-component',
styleUrl: 'shadow-component.css',
shadow: true
})
export class ShadowComponent {
}
注意:
QuerySelector
:使用Shadow DOM并要查询Web组件内的元素时,必须使用this.el.shadowRoot.querySelector()
。这是因为Web组件内的所有DOM都在Shadow DOM创建的shadowRoot中。
启用Shadow DOM后,shadow根目录中的元素将受到范围限制,并且组件外部的样式将不适用。结果,可以简化组件内部的CSS选择器。例如:
// 原来的样式
my-element {
color: black;
}
my-element div {
background: blue;
}
// 启用后样式
:host {
color: black;
}
div {
background: blue;
}
5.2.10.2 Scoped CSS
:
对于不支持
Shadow DOM
的浏览器,使用Stencil构建的Web组件将退回到使用范围CSS,而不是加载大型Shadow DOM polyfill。范围CSS通过在运行时将每个样式附加一个data属性来自动将CSS范围化为一个元素。类似Vue里面的scoped。
5.2.10.3 全局css
有时还是需要具有适用于整个文档的全局样式,需要在
stencil.config.ts
的globalStyle
属性中设置
编译后,因为指定名字和后缀,因为namespace
为gts-ui
,会生成在./www/build/gts-ui.css
这个地方,这个文件必须全局引入,在src/index.html
中:
<link rel="stylesheet" href="/build/app.css">
全局样式中一般使用如下:
- 主题:定义整个应用程序中使用的CSS变量
- 加载字体文件 @font-face
- font-family
- body样式
- CSS Resets
5.2.10.4 CSS变量
CSS变量与Sass变量非常相似,但是内置于浏览器中。CSS变量允许您指定可在您的应用程序中使用的CSS属性。
我们通常建议在以下位置src/global/下创建variables.css文件。然后,在stencil.config.ts文件中配置globalStyle: ‘src/global/variables.css’。
// variables.css
:root {
--app-primary-color: #488aff;
}
// 组件样式中
h1 {
color: var(--app-primary-color)
}
5.3 功能组件
功能组件与普通的Stencil Web组件完全不同,因为它们是Stencil的JSX编译器的一部分。 从本质上讲,功能组件是一个功能,它接受道具对象并将其转换为JSX。
在JSX中使用功能组件时,其名称必须以大写字母开头。
const Hello = props => <h1>Hello, {props.name}!</h1>;
如果传递第二个参数:children
const Hello = (props, children) => [
<h1>Hello, {props.name}</h1>,
children
];
JSX Transpiler就会将组件的所有子元素(p标签)作为数组传递给函数的children参数。
<Hello name="World">
<p>I'm a child element.</p>
</Hello>
Stencil提供了一种FunctionalComponent
通用类型,该类型允许为组件的属性指定接口。
import { FunctionalComponent, h } from '@stencil/core';
interface HelloProps {
name: string;
}
export const Hello: FunctionalComponent<HelloProps> = ({ name }) => (
<h1>Hello, {name}!</h1>
);
5.3.1 在子组件中使用
功能组件的第二个参数接收传递的子代,但是为了与它们一起使用,FunctionalComponent提供了一个
utils
对象,该对象公开了map()
方法以转换子代,并提供forEach()
来读取它们。 建议不要读取children
数组,因为模具编译器可以在prod
模式下重命名vNode
属性。
export interface FunctionalUtilities {
forEach: (children: VNode[], cb: (vnode: ChildNode, index: number, array: ChildNode[]) => void) => void;
map: (children: VNode[], cb: (vnode: ChildNode, index: number, array: ChildNode[]) => ChildNode) => VNode[];
}
export interface ChildNode {
vtag?: string | number | Function;
vkey?: string | number;
vtext?: string;
vchildren?: VNode[];
vattrs?: any;
vname?: string;
}
export const AddClass: FunctionalComponent = (_, children, utils) => (
utils.map(children, child => ({
...child,
vattrs: {
...child.vattrs,
class: `${child.vattrs.class} add-class`
}
}
))
);
6. 使用
这里主要介绍vue中如何配置使用,angular和react请参考官网。
- 为了在Vue应用程序中使用自定义元素库,必须在main.js文件中通知Vue编译器在编译过程中忽略哪些元素。
- 假设您已预先运行
npm install --save test-components
,并且该test-component
是我们发布到npm的组成的Web组件的名称,则可以通过以下方式将这些组件导入到main.js
文件中:
import Vue from 'vue';
import App from
@'./App.vue';
import { applyPolyfills, defineCustomElements
ComponentLibraryModule
} from 'test-components/loader';
Vue.config.productionTip = false;
// https://cn.vuejs.org/v2/api/#ignoredElements 忽略 test- 开头的element
Vue.config.ignoredElements = [/test-\w*/];
// 绑定到window上
applyPolyfills().then(() => {
defineCustomElements();
});
new Vue({
render: h => h(App)
}).$mount('#app');