StencilJS

1. 前提

stenciljs采用JSXTypeScript语法。

关于JSXTypeScript如下:

  1. React中JSX 简介
  2. StencilJS中的的JSX
  3. Stencil官网中介绍的JSX
  4. TypeScript: 推荐阮一峰的博客。TypeScript
  5. stencil-component github源码(README.md)
  6. 开发一个组件的步骤: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
  1. 装饰器

    @Component() 声明一个新的Web组件
    @Prop() 声明暴露的属性/属性
    @State() 声明组件的内部状态
    @Watch() 声明在属性或状态更改时运行的钩子
    @Element() 声明对host元素的引用
    @Method() 声明一个公开的公共方法
    @Event() 声明组件可能发出的DOM事件
    @Listen() 监听DOM事件
    
  2. 生命周期钩子

    connectedCallback()
    disconnectedCallback()
    componentWillLoad()
    componentDidLoad()
    componentShouldUpdate(newValue, oldValue, propName): boolean
    componentWillRender()
    componentDidRender()
    componentWillUpdate()
    componentDidUpdate()
    render()
    
  3. 其他
    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会使用参数调用:42undefinedsomePropA。如果确实返回true,则由于已计划进行重新渲染,因此不会再次调用该挂钩。
 相反,如果第一个钩子返回false,componentShouldUpdate则将使用再次调用88,undefinedsomePropB 作为参数,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()

当由于某些PropState更改而将要更新组件时调用。 在第一个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中。

启用S​​hadow 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.tsglobalStyle属性中设置

在这里插入图片描述
编译后,因为指定名字和后缀,因为namespacegts-ui,会生成在./www/build/gts-ui.css这个地方,这个文件必须全局引入,在src/index.html中:

<link rel="stylesheet" href="/build/app.css">

全局样式中一般使用如下:

  1. 主题:定义整个应用程序中使用的CSS变量
  2. 加载字体文件 @font-face
  3. font-family
  4. body样式
  5. 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');
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值