RNG牛逼

RNG牛逼 查看完整档案

广州编辑Digital Hollywood University  |  se 编辑某教育机构  |  前端开发 编辑 github.com/FEGuideTeam 编辑
编辑

公众号:前端指南,跪谢各位大佬关注

个人动态

RNG牛逼 提出了问题 · 8月30日

sql连表查询,求一个sql

现有三个表
如下
user 表

id 
name

order 表

id
storeId
userId
goods

其中storeId与store表中的id关联 userId 和user表中的id关联

store表

id 
ownnerId

store:order 是1:n ,order:user是1:1

怎么用sql查询出像

  {
    id,
    ownnerId,
    orderlist:[
        {
            id,
            storeId,
            userId,
            goods,
            userInfo:{
                id,
                name
            }
        },
        {
            id,
            storeId,
            userId,
            goods,
            userInfo:{
                id,
                name
            }
        }
        ....
    ]
  }

这样的结构
要求限制orderlist 只能取2条

关注 1 回答 0

RNG牛逼 赞了回答 · 8月4日

解决求助 ,sequelize怎么查询关联子表的数量

attributes 里面可以写sql查询语句!

ModelA.findAll({
    attributes: [
    'id',
    'name',
    [Sequelize.literal(`(
            SELECT COUNT(*)
            FROM tableB AS tableB
            WHERE
                tableB.foreign_id = tableA.id
        )`),
      'count']
    ],
    where:{
     //....
    },
})

result

[
{id: '',name: '',count: ''},
{id: '',name: '',count: ''},
]

不知道这样是否能满足你的查询需求!
参考链接 Sub Queries

关注 2 回答 1

RNG牛逼 提出了问题 · 7月4日

解决求助 ,sequelize怎么查询关联子表的数量

如题 有ModelA ModelB两个表 ModelA.hasMany(ModelB)

ModelA.findAll({
    where:{
        //....
    },
    include:[
        {
            model:ModelB
        }
    ]
})

我想查询ModelB有多少个符合条件的个数 请问需要怎么做

by the way : 不需要ModelB的数据,只需要统计个数

关注 2 回答 1

RNG牛逼 赞了文章 · 2019-08-02

TS与JS中的Getters和Setter究竟有什么用

作者:Khalil Stemmler

翻译:疯狂的技术宅

原文:https://www.freecodecamp.org/...

未经允许严禁转载

在本文中,我们讨论了getter 和 setter 在现代 Web 开发中的实用性。它们有用吗?什么时候使用它们是有意义的?

当 ECMAScript 5(2009)发布时,getters 和 setter(也称为访问器)被引入 JavaScript。

问题是,对于引入它们的原因及实用性存在很多困惑。

我在 reddit 看到了一个帖子,讨论的内容是它们是否是反模式。

不幸的是,该主题的普遍共识是 “yes”。我认为这是因为大多数情况下,你所做的前端编程都不会要求提供 getter 和 setter 这样的操作。

尽管我不同意 getter 和 setter 完全是一个反模式。但它们在几种情况下能带来更多的实用性。

它们是什么?

getter 和 setter 是另一种提供对象属性访问的方法。

一般的用法如下所示:

interface ITrackProps {
  name: string;
  artist: string;
}

class Track {  
  private props: ITrackProps;

  get name (): string {
    return this.props.name;
  }

  set name (name: string) {
      this.props.name = name;
  }

  get artist (): string {
    return this.props.artist;
  }

  set artist (artist: string) {
      this.props.artist = artist;
  }

  constructor (props: ITrackProps) {
    this.props = props;
  } 

  public play (): void {    
      console.log(`Playing ${this.name} by ${this.artist}`);
  }
}

现在问题变成了:“为什么不只使用常规类属性?”

那么,在这种情况下,是可以的

interface ITrackProps {
  name: string;
  artist: string;
}

class Track {  
  public name: string;
  public artist: string;

  constructor (name: string, artist: string;) {
    this.name = name;
    this.artist = artist;
  } 

  public play (): void {    
      console.log(`Playing ${this.name} by ${this.artist}`);
  }
}

这是一个非常简单的例子,让我们来看一个更好地描述,为什么我们应该关心使用 getter 和 settter 与常规类属性的场景。

防止贫血模式

你还记得贫血模式(译者注:一种反模式)是什么吗?尽早发现贫血模式的方法之一是,假如你的域实体的每个属性都有getter和setter(即:set 对域特定语言没有意义的操作)暴露的话。

如果你没有明确地使用 getset 关键字,那么会使所有 public 也有相同的负面影响。

思考这个例子:

class User {
  // 不好。你现在可以`set` 用户ID。 
  // 是否需要将用户的 id 变更为其他标识符? 
  // 这样安全吗? 你应该这样做吗?
  public id: UserId;

  constuctor (id: UserId) {
    this.id = id;
  }
}

在领域驱动设计中,为了防止出现贫血模式,并推进特定于领域的语言的创建,对于我们仅公开对领域有效的操作非常重要。

这意味着你需要了解自己正在工作的领域

我会让自己接受审查。让我们来看看 White Label 中的 Vinyl 类,这是一个开源的乙烯基交易程序,使用领域驱动进行设计并基于 TypeScript 构建。

import { AggregateRoot } from "../../core/domain/AggregateRoot";
import { UniqueEntityID } from "../../core/domain/UniqueEntityID";
import { Result } from "../../core/Result";
import { Artist } from "./artist";
import { Genre } from "./genre";
import { TraderId } from "../../trading/domain/traderId";
import { Guard } from "../../core/Guard";
import { VinylCreatedEvent } from "./events/vinylCreatedEvent";
import { VinylId } from "./vinylId";

interface VinylProps {
  traderId: TraderId;
  title: string;
  artist: Artist;
  genres: Genre[];
  dateAdded?: Date;
}

export type VinylCollection = Vinyl[];

export class Vinyl extends AggregateRoot<VinylProps> {

  public static MAX_NUMBER_GENRES_PER_VINYL = 3;

    //🔥1. 外观。 VinylId 键实际上并不存在
  //作为属性的 VinylProps,但我们仍然需要
  //提供对它的访问。
  get vinylId(): VinylId {
    return VinylId.create(this.id)
  }

  get title (): string {
    return this.props.title;
  }

  // 🔥2. 所有这些属性都作为 props 嵌套
  // 在一层,这样我们就可以控制对 ACTUAL 值
  // 的访问和变化。
  get artist (): Artist {
    return this.props.artist
  }

  get genres (): Genre[] {
    return this.props.genres;
  }

  get dateAdded (): Date {
    return this.props.dateAdded;
  }

  // 🔥3. 你会发现到目前为止还没有 setter,
  // 因为在创建之后去改变这些东西是没有意义的
  
  get traderId (): TraderId {
    return this.props.traderId;
  }

  // 🔥4. 这种方法称为“封装集合”。
  // 是的,我们需要添加类型。 但我们仍
  // 然没有公开 setter,因为这里有一
  // 些我们想要确保强制执行的不变逻辑。

  public addGenre (genre: Genre): void {
    const maxLengthExceeded = this.props.genres
      .length >= Vinyl.MAX_NUMBER_GENRES_PER_VINYL;

    const alreadyAdded = this.props.genres
      .find((g) => g.id.equals(genre.id));

    if (!alreadyAdded && !maxLengthExceeded) {
      this.props.genres.push(genre);
    }
  }

  // 🔥 5. 提供一种删除方式。

  public removeGenre (genre: Genre): void {
    this.props.genres = this.props.genres
      .filter((g) => !g.id.equals(genre.id));
  }

  private constructor (props: VinylProps, id?: UniqueEntityID) {
    super(props, id);
  }

  // 🔥 6. 这就是我们创建 Vinyl 的方法。
  // 创建之后,除了 Genre 之外,所有属性
  // 都会变为“只读”,因为启用修改是有意义的。
  public static create (props: VinylProps, id?: UniqueEntityID): Result<Vinyl> {
    const propsResult = Guard.againstNullOrUndefinedBulk([
      { argument: props.title, argumentName: 'title' },
      { argument: props.artist, argumentName: 'artist' },
      { argument: props.genres, argumentName: 'genres' },
      { argument: props.traderId, argumentName: 'traderId' }
    ]);

    if (!propsResult.succeeded) {
      return Result.fail<Vinyl>(propsResult.message)
    } 

    const vinyl = new Vinyl({
      ...props,
      dateAdded: props.dateAdded ? props.dateAdded : new Date(),
      genres: Array.isArray(props.genres) ? props.genres : [],
    }, id);
    const isNewlyCreated = !!id === false;

    if (isNewlyCreated) {
      // 🔥 7. 这就是我们需要 VinylId 的原因:
      // 为这个域事件的所有订阅者提供标识符。
      vinyl.addDomainEvent(new VinylCreatedEvent(vinyl.vinylId))
    }

    return Result.ok<Vinyl>(vinyl);
  }
}

充当外观、维护只读值、强制执行模型表达、封装集合以及创建域事件领域驱动设计中 getter 和 setter 的一些非常可靠的用例。

在 Vue.js 中更改检测

Vue.js 是一个较新的前端框架,以其快速和响应式而闻名。

Vue.js 能够如此有效地检测改变的原因是它们用 Object.defineProperty()API监视对 View Models 的更改!

来自 Vue.js 关于响应式的文档:

当你将纯 JavaScript 对象作为其数据选项传递给 Vue 实例时,Vue 将遍历其所有属性并用 Object.defineProperty 将它们转换为 getter/setter。 getter/setter 对用户是不可见的,但是在幕后,它们使 Vue 能够在访问或修改属性时执行依赖关系跟踪和更改通知。 —— Vue.js 文档:响应式

总之,getter 和 setter 针对很多问题有很大的实用性。不过在现代前端 Web 开发中,这些问题并没有太多出现。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


查看原文

赞 9 收藏 8 评论 0

RNG牛逼 关注了专栏 · 2019-08-02

疯狂的技术宅

本专栏文章首发于公众号:前端先锋 。

关注 24568

RNG牛逼 赞了文章 · 2019-08-02

深入理解Shadow DOM v1

翻译:疯狂的技术宅
https://blog.logrocket.com/un...

本文首发微信公众号:前端先锋
欢迎关注,每天都给你推送新鲜的前端技术文章


shadow DOM不是超级英雄电影中的恶棍,也不是DOM的黑暗面。 shadow DOM只是一种解决文档对象模型(或简称DOM)中缺少的树封装方法。

网页通常使用来自外部源的数据和小部件,如果它们没有封装,那么样式可能会影响HTML中不必要的部分,迫使开发人员使用特定的选择器和!important 规则来避免样式冲突。

尽管如此,在编写大型程序时,这些努力似乎并不是那么有效,并且大量的时间被浪费在防止CSS和JavaScript的冲突上。 Shadow DOM API旨在通过提供封装DOM树的机制来解决这些问题。

Shadow DOM是用于创建Web组件的主要技术之一,另外两个是自定义元素和HTML模板。 Web 组件的规范最初是由Google提出的,用于简化Web小部件的开发。

虽然这三种技术旨在协同工作,不过你可以自由地分别使用每种技术。本教程的范围仅限于shadow DOM。

什么是DOM?

在深入研究如何创建shadow DOM之前,了解DOM是什么非常重要。 W3C文档对象模型(DOM)提供了一个平台和语言无关的应用程序编程接口(API),用于表示和操作存储在HTML和XML文档中的信息。

通过使用DOM,程序员可以访问、添加、删除或更改元素和内容。 DOM将网页视为树结构,每个分支以节点结束,每个节点包含一个对象,可以使用JavaScript等脚本语言对其进行修改。请考虑以下HTML文档:

<html>
  <head>
    <title>Sample document</title>
  </head>
  <body>
    <h1>Heading</h1>
    <a href="https://example.com">Link</a>
  </body>
</html>

此HTML的DOM表示如下:

clipboard.png

此图中所有的框都是节点。

用于描述DOM部分的术语类似于现实世界中的家谱树:

  • 给定节点上一级节点是该节点的父节点
  • 给定节点下一级节点是该节点的子节点
  • 具有相同父级的节点是兄弟节点
  • 给定节点上方的所有节点(包括父节点和祖父节点)都称为该节点的祖先
  • 最后,给定节点下所有的节点都被称为该节点的后代

节点的类型取决于它所代表的HTML元素的类型。 HTML标记被称为元素节点。嵌套标签形成一个元素树。元素中的文本称为文本节点。文本节点可能没有子节点,你可以把它想象成是一棵树的叶子。

为了访问树,DOM提供了一组方法,程序员可以用这些方法修改文档的内容和结构。例如当你写下document.createElement('p');时,就在使用DOM提供的方法。没有DOM,JavaScript就无法理解HTML和XML文档的结构。

下面的JavaScript代码显示了如何使用DOM方法创建两个HTML元素,将一个嵌套在另一个内部并设置文本内容,最后把它们附加到文档正文:

const section = document.createElement('section');
const p = document.createElement('p');
p.textContent = 'Hello!';
section.appendChild(p);
document.body.appendChild(section);

这是运行这段JavaScript代码后生成的DOM结构:

<body>
  <section>
    <p>Hello!</p>
  </section>
</body>

什么是 shadow DOM?

封装是面向对象编程的基本特性,它使程序员能够限制对某些对象组件的未授权访问。

在此定义下,对象以公共访问方法的形式提供接口作为与其数据交互的方式。这样对象的内部表示不能直接被对象的外部访问。

Shadow DOM将此概念引入HTML。它允许你将隐藏的,分离的DOM链接到元素,这意味着你可以使用HTML和CSS的本地范围。现在可以用更通用的CSS选择器而不必担心命名冲突,并且样式不再泄漏或被应用于不恰当的元素。

实际上,Shadow DOM API正是库和小部件开发人员将HTML结构、样式和行为与代码的其他部分分开所需的东西。

Shadow root 是 shadow 树中最顶层的节点,是在创建 shadow DOM 时被附加到常规DOM节点的内容。具有与之关联的shadow root的节点称为shadow host。

你可以像使用普通DOM一样将元素附加到shadow root。链接到shadow root的节点形成 shadow 树。通过图表应该能够表达的更清楚:

clipboard.png

术语light DOM通常用于区分正常DOM和shadow DOM。shadow DOM和light DOM被并称为逻辑DOM。light DOM与shadow DOM分离的点被称为阴影边界。 DOM查询和CSS规则不能到达阴影边界的另一侧,从而创建封装。

创建一个shadow DOM

要创建shadow DOM,需要用Element.attachShadow()方法将shadow root附加到元素:

var shadowroot = element.attachShadow(shadowRootInit);

来看一个简单的例子:

<div id="host"><p>Default text</p></div>
    
<script>
  const elem = document.querySelector('#host');
     
  // attach a shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
     
  // create a <p> element
  const p = document.createElement('p');
     
  // add <p> to the shadow DOM
  shadowRoot.appendChild(p);
     
  // add text to <p> 
  p.textContent = 'Hello!';
</script>

此代码将一个shadow DOM树附加到div元素,其idhost。这个树与div的实际子元素是分开的,添加到它之上的任何东西都将是托管元素的本地元素。

clipboard.png

Chrome DevTools中的 Shadow root。

注意#host中的现有元素是如何被shadow root替换的。不支持shadow DOM的浏览器将使用默认内容。

现在,在将CSS添加到主文档时,样式规则不会影响shadow DOM:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach a shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  // set the HTML contained within the shadow root
  shadowRoot.innerHTML = '<p>Shadow DOM</p>';
</script>
 
<style>
  p {color: red}
</style>

在light DOM中定义的样式不能越过shadow边界。因此,只有light DOM中的段落才会变为红色。

clipboard.png

相反,你添加到shadow DOM的CSS对于hosting元素来说是本地的,不会影响DOM中的其他元素:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>p {color: red}</style>`;
 
</script>

clipboard.png

你还可以将样式规则放在外部样式表中,如下所示:

shadowRoot.innerHTML = `
  <p>Shadow DOM</p>
  <link rel="stylesheet" href="style.css">`;

要获取 shadowRoot 附加到的元素的引用,使用host属性:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  console.log(shadowRoot.host);    // => <div id="host"></div>
</script>

要执行相反操作并获取对元素托管的shadow root的引用,可以用元素的shadowRoot属性:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  console.log(elem.shadowRoot);    // => #shadow-root (open)
</script>

shadowRoot mod

当调用Element.attachShadow()方法来附加shadow root时,必须通过传递一个对象作为参数来指定shadow DOM树的封装模式,否则将会抛出一个TypeError。该对象必须具有mode属性,其值为 openclosed

打开的shadow root允许你使用host元素的shadowRoot属性从root外部访问shadow root的元素,如下例所示:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach an open shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
  // Nodes of an open shadow DOM are accessible
  // from outside the shadow root
  elem.shadowRoot.querySelector('p').innerText = 'Changed from outside the shadow root';
  elem.shadowRoot.querySelector('p').style.color = 'red';
</script>

clipboard.png

但是如果mode属性的值为“closed”,则尝试从root外部用JavaScript访问shadow root的元素时会抛出一个TypeError

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach a closed shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'closed'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
 
  elem.shadowRoot.querySelector('p').innerText = 'Now nodes cannot be accessed from outside';
  // => TypeError: Cannot read property 'querySelector' of null 
</script>

当mode设置为closed时,shadowRoot属性返回null。因为null值没有任何属性或方法,所以在它上面调用querySelector()会导致TypeError。浏览器通常用关闭的 shadow roo 来使某些元素的实现内部不可访问,而且不可从JavaScript更改。

要确定shadow DOM是处于open还是closed模式,你可以参考shadow root的mode属性:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'closed'});
 
  console.log(shadowRoot.mode);    // => closed
</script>

从表面上看,对于不希望公开其组件的shadow root 的Web组件作者来说,封闭的shadow DOM看起来非常方便,然而在实践中绕过封闭的shadow DOM并不难。通常完全隐藏shadow DOM所需的工作量超过了它的价值。

并非所有HTML元素都可以托管shadow DOM

只有一组有限的元素可以托管shadow DOM。下表列出了支持的元素:

+----------------+----------------+----------------+
|    article     |      aside     |   blockquote   |
+----------------+----------------+----------------+
|     body       |       div      |     footer     |
+----------------+----------------+----------------+
|      h1        |       h2       |       h3       |
+----------------+----------------+----------------+
|      h4        |       h5       |       h6       |
+----------------+----------------+----------------+
|    header      |      main      |      nav       |
+----------------+----------------+----------------+
|      p         |     section    |      span      |
+----------------+----------------+----------------+

尝试将shadow DOM树附加到其他元素将会导致“DOMException”错误。例如:

document.createElement('img').attachShadow({mode: 'open'});    
// => DOMException

<img>元素作为shadow host是不合理的,因此这段代码抛出错误并不奇怪。你可能会收到DOMException错误的另一个原因是浏览器已经用该元素托管了shadow DOM。

浏览器自动将shadow DOM附加到某些元素

Shadow DOM已存在很长一段时间了,浏览器一直用它来隐藏元素的内部结构,比如<input><textarea><video>

当你在HTML中使用<video>元素时,浏览器会自动将shadow DOM附加到包含默认浏览器控件的元素。但DOM中唯一可见的是<video>元素本身:

clipboard.png

要在Chrome中显示此类元素的shadow root,请打开Chrome DevTools设置(按F1),然后在“elements”部分下方选中“Show user agent shadow DOM”:

clipboard.png

选中“Show user agent shadow DOM”选项后,shadow root节点及其子节点将变为可见。以下是启用此选项后相同代码的显示方式:

clipboard.png

在自定义元素上托管shadow DOM

Custom Elements API 创建的自定义元素可以像其他元素一样托管shadow DOM。请看以下示例:

<my-element></my-element>
<script>
  class MyElement extends HTMLElement {
    constructor() {
 
      // must be called before the this keyword
      super();
 
      // attach a shadow root to <my-element>
      const shadowRoot = this.attachShadow({mode: 'open'});
 
      shadowRoot.innerHTML = `
        <style>p {color: red}</style>
        <p>Hello</p>`;
    }
  }
 
  // register a custom element on the page
  customElements.define('my-element', MyElement);
</script>

此代码了创建一个托管shadow DOM的自定义元素。它调用了customElements.define()方法,元素名称作为第一个参数,类对象作为第二个参数。该类扩展了HTMLElement并定义了元素的行为。

在构造函数中,super()用于建立原型链,并且把Shadow root附加到自定义元素。当你在页面上使用<my-element>时,它会创建自己的shadow DOM:

clipboard.png

请记住,有效的自定义元素不能是单个单词,并且名称中必须包含连字符( - )。例如,myelement不能用作自定义元素的名称,并会抛出 DOMException 错误。

样式化host元素

通常,要设置host元素的样式,你需要将CSS添加到light DOM,因为这是host元素所在的位置。但是如果你需要在shadow DOM中设置host元素的样式呢?

这就是host()伪类函数的用武之地。这个选择器允许你从shadow root中的任何地方访问shadow host。这是一个例子:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>
      :host {
        display: inline-block;
        border: solid 3px #ccc;
        padding: 0 15px;
      }
    </style>`;
</script>

值得注意的是:host仅在shadow root中有效。还要记住,在shadow root之外定义的样式规则比:host中定义的规则具有更高的特殊性。

例如,#host { font-size: 16px; } 的优先级高于 shadow DOM的 :host { font-size: 20px; }。实际上这很有用,这允许你为组件定义默认样式,并让组件的用户覆盖你的样式。唯一的例外是!important规则,它在shadow DOM中具有特殊性。

你还可以将选择器作为参数传递给:host(),这允许你仅在host与指定选择器匹配时才会定位host。换句话说,它允许你定位同一host的不同状态:

<style>
  :host(:focus) {
    /* style host only if it has received focus */
  }
 
  :host(.blue) {
    /* style host only if has a blue class */
  }
 
  :host([disabled]) {
    /* style host only if it's disabled */
  }
</style>

基于上下文的样式

要选择特定祖先内部的shadow root host ,可以用:host-context()伪类函数。例如:

:host-context(.main) {
  font-weight: bold;
}

只有当它是.main的后代时,此CSS代码才会选择shadow host :

<body class="main">
  <div id="host">
  </div>
</body>

:host-context()对主题特别有用,因为它允许作者根据组件使用的上下文对组件进行样式设置。

样式钩子

shadow DOM的一个有趣地方是它能够创建“样式占位符”并允许用户填充它们。这可以通过使用CSS自定义属性来完成。我们来看一个简单的例子:

<div id="host"></div>
 
<style>
  #host {--size: 20px;}
</style>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>p {font-size: var(--size, 16px);}</style>`;
 
</script>

这个shadow DOM允许用户覆盖其段落的字体大小。使用自定义属性表示法( — size: 20px)设置该值,并且shadow DOM用var()函数(font-size: var( — size, 16px))检索该值。在概念方面,这类似于<slot>元素的工作方式。

可继承的样式

shadow DOM允许你创建独立的DOM元素,而不会从外部看到选择器可见性,但这并不意味着继承的属性不会通过shadow边界。

某些属性(如colorbackgroundfont-family)会传递shadow边界并应用于shadow树。因此,与iframe相比,shadow DOM不是一个非常强大的障碍。

<style>
  div {
    font-size: 25px;
    text-transform: uppercase;
    color: red;
  }
</style>
 
<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
</script>

解决方法很简单:通过声明all: initial将可继承样式重置为其初始值,如下所示:

<style>
  div {
    font-size: 25px;
    text-transform: uppercase;
    color: red;
  }
</style>
 
<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>
      :host p {
        all: initial;
      }
    </style>`;
</script>

在此例中,元素被强制回到初始状态,因此穿过shadow边界的样式不起作用。

重新定位事件

在shadow DOM内触发的事件可以穿过shadow边界并冒泡到light DOM;但是,Event.target的值会自动更改,因此它看起来好像该事件源自其包含的shadow树而不是实际元素的host元素。

此更改称为事件重定向,其背后的原因是保留shadow DOM封装。请参考以下示例:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <ul>
      <li>One</li>
      <li>Two</li>
      <li>Three</li>
    <ul>
    `;
 
  document.addEventListener('click', (event) => {
    console.log(event.target);
  }, false);
</script>

当你单击shadow DOM中的任何位置时,这段代码会将 <div id =“host”> ... </div> 记录到控制台,因此侦听器无法看到调度该事件的实际元素。

但是在shadow DOM中不会发生重定目标,你可以轻松找到与事件关联的实际元素:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <ul>
      <li>One</li>
      <li>Two</li>
      <li>Three</li>
    </ul>`;
   
  shadowRoot.querySelector('ul').addEventListener('click', (event) => {
    console.log(event.target);
  }, false);  
</script>

请注意,并非所有事件都会从shadow DOM传播出去。那些做的是重新定位,但其他只是被忽略了。如果你使用自定义事件的话,则需要使用composed:true标志,否则事件不会从shadow边界冒出来。

Shadow DOM v0 与 v1

Shadow DOM规范的原始版本在 Chrome 25 中实现,当时称为Shadow DOM v0。该规范的新版本改进了Shadow DOM API的许多方面。

例如,一个元素不能再承载多个shadow DOM,而某些元素根本不能托管shadow DOM。违反这些规则会导致错误。

此外,Shadow DOM v1提供了一组新功能,例如打开 shadow 模式、后备内容等。你可以找到由规范作者之一编写的 v0 和 v1 之间的全面比较(https://hayato.io/2016/shadow...)。可以在W3C找到Shadow DOM v1的完整描述。

浏览器对Shadow DOM v1的支持

在撰写本文时,Firefox和Chrome已经完全支持Shadow DOM v1。不幸的是,Edge尚未实现v1,Safari 只是部分支持。在 Can I use…上提供了支持的浏览器的最新列表。

要在不支持Shadow DOM v1的浏览器上实现shadow DOM,可以用shadydomshadycss polyfills。

总结

DOM开发中缺乏封装一直是个问题。 Shadow DOM API为我们提供了划分DOM范围的能力,从而为这个问题提供了一个优雅的解决方案。

现在,样式冲突不再是一个令人担忧的问题,选择器也不会失控。 shadow DOM改变了小部件开发的游戏规则,能够创建从页面其余部分封装的小部件,并且不受其他样式表和脚本的影响,这是一个巨大的优势。

如前所述,Web 组件由三个主要技术组成,而shadow DOM是其中的关键部分。希望在阅读本文之后,你将更容易理解这三种技术是如何协同构建Web组件的。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:

查看原文

赞 32 收藏 21 评论 0

RNG牛逼 赞了文章 · 2019-07-13

黄文俊:Serverless小程序后端技术分享

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~

黄文俊,现任腾讯云SCF无服务器云函数高级产品经理,多年企业级系统开发和架构工作经验,对企业级存储、容器平台、微服务架构、无服务器计算等领域均有涉猎。

clipboard.png

今天讲的是怎么使用Serverless做后端技术分享。我的职业偏向是后端,可能不是写前端,不是使用Node.js,更多是使用CR做后端语言,今天关注的微信小程序,我这一块的分享更多是怎么实现它的后端。我所使用的是Serverless技术,也是在近两年新出来的一种架构。讲Serverless这个架构之前,我也给大家讲一下小程序和传统的后台技术。

clipboard.png

小程序的后台技术

小程序,是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体验。它的加载方式比传统的APP方式更快速上线,体验也不差,除了它本身的界面展示和刷新之外,小程序里面的数据获取通过微信和后端进行交互,小程序的运行实际上是一个类前端的运行方式,整个运行是在微信内,它和后端的交互实际上通过微信进行转发的,运行起来之后,它会提出一个api的请求,这个请求首先给到微信,微信再通过网络apr转到你自己的服务器上,服务器拿到这个请求以后进行数据的处理,然后再响应到前端,这就是小程序和后台交互的一种架构。

clipboard.png

对于后端服务,这张图是大家传统做的方式,暴露api,这些都可以用来开发业务应用,业务应用之后需要有相应的存储文件,结构化的数据存储,或者是非结构化的数据存储,需要有数据库和缓存,为了实现这一套架构且不会由于某一块的服务器宕掉,或者有一些漏洞等等,我们通常的实现是一个较为复杂的过程。比如说,我们为了保证的Serverless和服务器不会垮掉,需要建立一个集群,我们要对外提供服务,需要LB的请求,请求到之后分到某一台服务器上。比如说文件存储,如果单纯只用一台设备,这台设备挂了,整个文件服务就挂了,所以我们要使用分布式存储来解决文件存储的问题。数据库和缓存也是一样的,需要构建集群,无论是两台还是三台还是多台,构建集群以后能确保不会由于某一个单点的问题导致整个服务不可用,从而导致服务瘫痪。

clipboard.png

clipboard.png

如果作为一个小程序开发者,这套架构在互联网公司已经搭建了,作为个人来说这一道太重了,需要了解这里面的某一块和它的配置,比如说数据库的集群怎么配?让大家没法把精力集中到你的业务和小程序本身,而是过多耗在运维和支撑上。

Serverless架构

clipboard.png

我下面要介绍的Serverless架构,采用无服务器的方式,主要会介绍无服务器和云怎么结合,怎么利用云的服务减轻架构化的工作。介绍它的架构之前,我介绍一下Serverless架构,英文称之为Serverless,中文称之为无服务器,大家不用购买服务器,不用购买虚拟机或者物理机,这一块怎么运行呢?它使用计算托管的方式,在Serverless这里,我们可以看成两块,第一块就是函数即服务,它真正实现了你业务的托管计算。另外一种是后端即服务,包括对象存储,大家不用自己构建分布式存储,不用担心数据的丢失和安全性问题;同时在云上提供的数据库,消息队列和对象存储都是一样的,不用购买服务器自己搭建,在购买使用的过程当中我们可以称之为Serverless。因为这些都是托管型的,使用的时候不用关心它的安全性,不用关心可能服务器宕机导致的故障。

clipboard.png

Serverless的计算托管式云在服务函数内,下面来讲一下云函数的架构。大家看到这个架构以后,我们后面在拿一个实际案例来看怎么把具体的api服务落地。云服务器架构本身是计算托管型的,计算托管意味着把真正的业务代码托管到云上面,然后在云上面运行,它的运行方式有一个特点是触发式运营,跟各个产品打通以后,各个产品产生的事件,后面的案例就是和API网关进行结合,从api网关来的事件就是api事件,当这个请求到达api网关时,我们就认为是一个事件,然后再运行。大家最初进行托管的时候,把代码和触发期的配置提交到云上面来,并不是说提交之后代码就运行起来,而是事件到达才运行起来,代码对这个事件进行处理。在这个过程中,对于每一次的事件,每一个代码拉起的过程,实际上都是单独处理一个事件,为什么呢?因为我们在这儿使用并发的模式,如果你有上万个用户同时访问你的小程序,要同时对上万的用户进行服务,启动上万的实例,它是在事件时运行起来,没有事件不能运行,这与微信小程序本身点开即用、用完即走的概念是符合的,有请求时才运行,没有请求时不运行。产品的计费模式也是根据实际运行的时间计费的。

Serverless的使用

clipboard.png

怎么使用Serverless呢?传统的架构就是前面说的web服务,然后是使用存储、缓存,我们对外服务以后,有对外暴露相应的api,实际上用户的业务逻辑都是放在云函数内,需要结构化存储,需要进行缓存或者对象存储,我们需要数据服务或者云缓存服务等,其他的服务都可以直接在线服务,这些服务直接通过代码调用。

前面讲了Serverless的架构介绍,后面是对于这个后台开发的介绍,后面也是基于这个方式进行详细案例的说明。

clipboard.png

小程序除了本身的页面启动,后续与网络的交互都是由小程序发起,经过微信本身以后,首先请求到达api网关,对于对外的api的管理,把这个api暴露到官网上,可以被要程序访问得到。它本身也能够提供api的发布和版本的切换能力,api网关之后就是云函数。云函数就是实际处理业务的逻辑,如果你需要使用数据库,就在代码内发数据库的连接,需要存储文件,就调用相应的窗口写文件。

基于这个方案我们来看一下,传统提供的是中间的一块,因为前端是用户的小程序,后端是微信本身提供的接口服务,中间建议的是开发者自己的服务器。我们现在要展示的一个案例,也就是怎么把中间的开发者服务器替换掉,用Serverless的方案落地,我们使用了api网关加云数据库实现开发者服务器所能够做到的事情,不需要购买服务器而落地我们的api。

首先从最前面的小程序来看,这个案例也是小程序开发者上面的demo,demo的前端包括登陆端口,以及session展示,我们在小程序这端首先获取一个talk,开发者拿到以后再跟微信交互,验证合法以后,我们这里选择了记录到云数据库,这就是小程序界面提供点击登陆的位置,后面的业务会发送请求到云上面来。

对于这个小程序的核心,我们在某一个api上面的路径就是在hos的login url上发起GET操作,根据Wx.login构造请求的头部,body内容,发送获取到的code及加密数据到后台。

clipboard.png

api网关

我们看一下api网关做的事情,它对外以一个api的接口呈现出来,我们直接提供了对外访问的域名,用户基于这个域名绑定自己所拥有的域名,这种情况下可以实现发布的要求,微信小程序的开发者要求域名要备案,把自己的域名绑定到api的服务上面来,对外提供,在右侧api的网关上,配一个/login Get,在后台还未实现之前,可以配置为mock方法,解耦前后端,小程序可以基于api构造的mock数据开发。实现云函数后,对接api到云函数并更新发布api,避免开发的同时影响到在线业务。

云函数的处理流程

从云函数的方面来说,用户会承载计算业务。按照我们最开始用户给的官方图,拿到api的请求以后,解析请求内容,根据规范连接微信认证服务器,获取认证情况并记录session,返回session信息给到请求端。拿到微信服务器的返回以后可以判断用户的登陆过程是成功还是失败,如果成功以后可以拿到用户相应的值,这个地方我们发起到数据库的连接,建立一个masico的连接,完成session的记录。

云函数的处理流程之后,下面展示的是我们怎么和数据库建立连接,登陆信息的细节流程,创建连接并可复用连接,拼装SQL语句并执行。

数据库的配置

这个地方就是我们购买以后,数据库启动并且做了登陆以后,可以进入到数据库里面查看数据,查看session的记录。

实操案例——用户登陆及session展示

创建并初始化实例,按照mysql标准化使用方式操作,计算托管式的优势用户关心核心的代码,不用关心周边的运维,由于托管式业务,无论是个人请求,个人开发者的小程序,很有可能你的一个小程序就成为爆款,爆款以后可能访问量就是突增的形式。

我们利用session的架构实现小程序,而且不用去担心运维;秒级启动,弹性计算能力满足用户上万的并发。核心点关注业务代码,而不用关注web,这就是快速的应用实现小程序的落地的方法。

clipboard.png

Q/A

Q:在api网关部署HTTPS证书吗?
A:对,这个证书是腾讯云提供的,绑定你自己域名的时候,可以实现HTTPS的支持。

Q:我们在生产环境的时候由腾讯云提供的证书?
A:对。

Q:在生产环节需要腾讯云提供的证书上线我们的服务?
A:对,这是小程序方面的要求,因为小程序要求必须使用自有域名和小程序打通。

Q:小程序即用即删,如果手机里面有很多会很卡,如果小程序用过在微信上面的页面会显示出来,如果上万个对微信本身有什么影响?
A:可以从一些限制可以看到,现在对于微信小程序的大小有限制的,它本身的大小是要求,目前我记得是5兆包2兆包的大小,如果是上千个,对于你手机来说可能是多存储了一些数据,每个包最大用满可能就是5兆,本身小程序页面的加载都是有限制的,这个限制从微信角度来考虑,都是为了保证小程序的流畅运行,不会对用户的手机造成很大的冲击,这一块要微信的同学介绍,我是偏后端的。

Q:我再举个例子,小程序进入的时候加载速度比较快的,举一个比较极端的,比如说跳一跳,第一次使用的时候,它的加载速度,比如说今天用完了,删掉,过一段时间再进去,相比来说加载速度有差别吗?或者第二次用的时候快一些吗?原因是什么?
A:这个要由微信的同学解答比较好一些,因为这些都是小程序本身的体现,或者说速度的一种体现。

Q:你好,我是做后端开发的,什么样的模型不是用Serverless来做的。
A:Serverless它本身的一些特性也限制了它的使用场景,比如说对于内存的配置,cpu的配置,运行时间的限制,不是所有场合都适用,它本身对运行时间有限制的,不能长时间的运用,包括内存的使用,包括cpu的应用,比如说动画的渲染,长时间的批量计算,这些都不适合api的服务,由于它的请求到达以后必须快速响应用户,api比较适合的。

更多分享资料,请戳下面的链接:
使用 serverless 构建小程序后台.pdf

问答
微信小程序如何与数据库交互?
相关阅读
朱展:腾讯云小程序解决方案
施德来:有赞电商小程序的实践
邹伟:如何开发一款小游戏

此文已由作者授权腾讯云+社区发布,原文链接:https://cloud.tencent.com/dev...
图片描述

查看原文

赞 2 收藏 0 评论 0

RNG牛逼 赞了回答 · 2019-07-13

解决nodejs中如何防mySQL注入

使用escape()对传入参数进行编码

var userId = 1, name = 'test';
var query = connection.query('SELECT * FROM users WHERE id = ' + connection.escape(userId) + ', name = ' + connection.escape(name), function(err, results) {
    // ...
});
console.log(query.sql); // SELECT * FROM users WHERE id = 1, name = 'test'

使用connection.query()的查询参数占位符

var userId = 1, name = 'test';
var query = connection.query('SELECT * FROM users WHERE id = ?, name = ?', [userId, name], function(err, results) {
    // ...
});
console.log(query.sql); // SELECT * FROM users WHERE id = 1, name = 'test'

使用escapeId()编码SQL查询标识符

var sorter = 'date';
var sql    = 'SELECT * FROM posts ORDER BY ' + connection.escapeId(sorter);
connection.query(sql, function(err, results) {
  // ...
});

使用mysql.format()转义参数

var userId = 1;
var sql = "SELECT * FROM ?? WHERE ?? = ?";
var inserts = ['users', 'id', userId];
sql = mysql.format(sql, inserts); // SELECT * FROM users WHERE id = 1

Ref: http://www.dengzhr.com/node-j...

PS: Google第一页就是答案

关注 3 回答 1

RNG牛逼 赞了文章 · 2019-07-01

Node连接MySQL并封装其增删查改

图片描述

Node连接Mysql

说到node,可能大家会想到MOngoDB作为数据库,这里将会介绍node与mysql的连接,并分享了封装好的实例代码,在项目开发中可直接使用。下一篇博客将会讲node连接MongoDB。

安装Mysql模块

npm install mysql

连接Mysql

    const mysql = require('mysql');
    
    let connection = mysql.createConnection({
        host : 'localhost',
        user : 'root', 
        password : 'password',
        database : 'test'
    });

connection.connect(function(err) {
  if (err) {
    console.error('连接失败: ' + err.stack);
    return;
  }

  console.log('连接成功 id ' + connection.threadId);
});

host:连接的服务器
user:数据库用户名
password:设置的MySQL密码
database: 要连接的数据库名

常用的SQL语句

具体的使用这里不做详细说明,包括select、insert、update、delete等语句。

Node操作Mysql

查询

connection.query('SELECT * FROM t_user WHERE username = "whg"', (err, results, fields) => {
    if(err){
        console.log(err);
    }
    console.log(results);
})

添加

connection.query('INSERT INTO t_user(username, pass) VALUES(?, ?)',['whg', '123'], (err, results) => {
    if(err){
        console.log(err);
    }
    console.log(results);
})

删除

connection.query('DELETE FROM t_user  WHERE id = 1', (err, results) => {
    if(err){
        console.log(err);
    }
    console.log(results);
})

更新

connection.query('UPDATE t_user SET pass = "321" WHERE username = "whg"', (err, results) => {
    if(err){
        console.log(err);
    }
    console.log(results);
})

结束连接

connection.end(function(err) {
  
});
connection.destroy();

这两种都行,第二种是强制结束。

封装

说了这么多,感觉操作起来还是挺简单的。在实际开发中,我们想要操作起来更方便,那就让我们自己封装一下来使用。直接上代码:

封装好的代码

1.数据库配置文件

//配置链接数据库参数
module.exports = {
    host : 'localhost',
    port : 3306,//端口号
    database : 'nodetest',//数据库名
    user : 'root',//数据库用户名
    password : '123456'//数据库密码
};

2.封装、暴露方法

let mysql = require('mysql');//引入mysql模块
var databaseConfig = require('./mysql.config');  //引入数据库配置模块中的数据

//向外暴露方法
module.exports = {
    query : function(sql,params,callback){
        //每次使用的时候需要创建链接,数据操作完成之后要关闭连接
        var connection = mysql.createConnection(databaseConfig);        
        connection.connect(function(err){
            if(err){
                console.log('数据库链接失败');
                throw err;
            }
         //开始数据操作
         //传入三个参数,第一个参数sql语句,第二个参数sql语句中需要的数据,第三个参数回调函数
        connection.query( sql, params, function(err,results,fields ){
           if(err){
                console.log('数据操作失败');
                throw err;
            }
            //将查询出来的数据返回给回调函数
            callback && callback(results, fields);
            //results作为数据操作后的结果,fields作为数据库连接的一些字段
            //停止链接数据库,必须再查询语句后,要不然一调用这个方法,就直接停止链接,数据操作就会失败
             connection.end(function(err){
                  if(err){
                      console.log('关闭数据库连接失败!');
                      throw err;
                  }
              });
           });
       });
    }
};

3.演示实例

var db=require('../model/mysql.js');
// 查询实例
db.query('select * from t_user', [],function(result,fields){
    console.log('查询结果:');
    console.log(result);
});
//添加实例
var  addSql = 'INSERT INTO websites(username,password) VALUES(?,?)';
var  addSqlParams =['咕噜先森', '666'];
db.query(addSql,addSqlParams,function(result,fields){
    console.log('添加成功')
})

结束

想要使用的朋友,可以直接把封装好的两个文件copy到项目中,改一下配置就可以使用了,大大提高开发效率。下一篇将会分享Node连接MongoDB。

查看原文

赞 6 收藏 10 评论 1

RNG牛逼 赞了文章 · 2019-06-27

[译] 关于 `ExpressionChangedAfterItHasBeenCheckedError` 错误你所需要知道的事情

原文链接:Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error
关于 ExpressionChangedAfterItHasBeenCheckedError,还可以参考这篇文章,并且文中有 youtube 视频讲解:Angular Debugging "Expression has changed after it was checked": Simple Explanation (and Fix)

最近 stackoverflow 上几乎每天都有人提到 Angular 抛出的一个错误:ExpressionChangedAfterItHasBeenCheckedError,通常提出这个问题的 Angular 开发者都不理解变更检测(change detection)的原理,不理解为何产生这个错误的数据更新检查是必须的,甚至很多开发者认为这是 Angular 框架的一个 bug(译者注:Angular 提供变更检测功能,包括自动触发和手动触发,自动触发是默认的,手动触发是在使用 ChangeDetectionStrategy.OnPush 关闭自动触发的情况下生效。如何手动触发,参考 Triggering change detection manually in Angular)。当然不是了!其实这是 Angular 的警告机制,防止由于模型数据(model data)与视图 UI 不一致,导致页面上存在错误或过时的数据展示给用户。

本文将解释引起这个错误的内在原因,检测机制的内部原理,提供导致这个错误的共同行为,并给出修复这个错误的解决方案。最后章节解释为什么数据更新检查是如此重要。

It seems that the more links to the sources I put in the article the less likely people are to recommend it ?. That’s why there will be no reference to the sources in this article.(译者注:这是作者的吐槽,不翻译)

相关变更检测行为

一个运行的 Angular 程序其实是一个组件树,在变更检测期间,Angular 会按照以下顺序检查每一个组件(译者注:这个列表称为列表 1):

  • 更新所有子组件/指令的绑定属性
  • 调用所有子组件/指令的三个生命周期钩子:ngOnInitOnChangesngDoCheck
  • 更新当前组件的 DOM
  • 为子组件执行变更检测(译者注:在子组件上重复上面三个步骤,依次递归下去)
  • 为所有子组件/指令调用当前组件的 ngAfterViewInit 生命周期钩子

在变更检测期间还会有其他操作,可以参考我写的文章:《Everything you need to know about change detection in Angular》

在每一次操作后,Angular 会记下执行当前操作所需要的值,并存放在组件视图的 oldValues 属性里(译者注:Angular Compiler 会把每一个组件编译为对应的 view class,即组件视图类)。在所有组件的检查更新操作完成后,Angular 并不是马上接着执行上面列表中的操作,而是会开始下一次 digest cycle,即 Angular 会把来自上一次 digest cycle 的值与当前值比较(译者注:这个列表称为列表 2):

  • 检查已经传给子组件用来更新其属性的值,是否与当前将要传入的值相同
  • 检查已经传给当前组件用来更新 DOM 值,是否与当前将要传入的值相同
  • 针对每一个子组件执行相同的检查(译者注:就是如果子组件还有子组件,子组件会继续执行上面两步的操作,依次递归下去。)

记住这个检查只在开发环境下执行,我会在后文解释原因。

让我们一起看一个简单示例,假设你有一个父组件 A 和一个子组件 B,而 A 组件有 nametext 属性,在 A 组件模板里使用 name 属性的模板表达式:

template: '<span>{{name}}</span>'

同时,还有一个 B 子组件,并将 A 父组件的 text 属性以输入属性绑定方式传给 B 子组件:

@Component({
    selector: 'a-comp',
    template: `
        <span>{{name}}</span>
        <b-comp [text]="text"></b-comp>
    `
})
export class AComponent {
    name = 'I am A component';
    text = 'A message for the child component`;

那么当 Angular 执行变更检测的时候会发生什么呢?首先是从检查父组件 A 开始,根据上面列表 1 列出的行为,第一步是更新所有子组件/指令的绑定属性(binding property),所以 Angular 会计算 text 表达式的值为 A message for the child component,并将值向下传给子组件 B,同时,Angular 还会在当前组件视图中存储这个值:

view.oldValues[0] = 'A message for the child component';

第二步是执行上面列表 1 列出的执行几个生命周期钩子。(译者注:即调用子组件 BngOnInitOnChangesngDoCheck 这三个生命周期钩子。)

第三步是计算模板表达式 {{name}} 的值为 I am A component,然后更新当前组件 A 的 DOM,同时,Angular 还会在当前组件视图中存储这个值:

view.oldValues[1] = 'I am A component';

第四步是为子组件 B 执行以上第一步到第三步的相同操作,一旦 B 组件检查完毕,那本次 digest loop 结束。(译者注:我们知道 Angular 程序是由组件树构成的,当前父组件 A 组件做了第一二三步,完事后子组件 B 同样会去做第一二三步,如果 B 组件还有子组件 C,同样 C 也会做第一二三步,一直递归下去,直到当前树枝的最末端,即最后一个组件没有子组件为止。这一次过程称为 digest loop。)

如果处于开发者模式,Angular 还会执行上面列表 2 列出的 digest cycle 循环核查。现在假设当 A 组件已经把 text 属性值向下传入给 B 组件并保存该值后,这时 text 值突变为 updated text,这样在 Angular 运行 digest cycle 循环核查时,会执行列表 2 中第一步操作,即检查当前digest cycle 的 text 属性值与上一次时的 text 属性值是否发生变化:

AComponentView.instance.text === view.oldValues[0]; // false
'A message for the child component' === 'updated text'; // false

结果是发生变化,这时 Angular 会抛出 ExpressionChangedAfterItHasBeenCheckedError 错误。

列表 1 中第三步操作也同样会执行 digest cycle 循环检查,如果 name 属性已经在 DOM 中被渲染,并且在组件视图中已经被存储了,那这时 name 属性值突变同样会有同样错误:

AComponentView.instance.name === view.oldValues[1]; // false
'I am A component' === 'updated name'; // false

你可能会问上面提到的 textname 属性值发生突变,这会发生么?让我们一起往下看。

属性值突变的原因

属性值突变的罪魁祸首是子组件或指令,一起看一个简单证明示例吧。我会先使用最简单的例子,然后举个更贴近现实的例子。你可能知道子组件或指令可以注入它们的父组件,假设子组件 B 注入它的父组件 A,然后更新绑定属性 text。我们在子组件 BngOnInit 生命周期钩子中更新父组件 A 的属性,这是因为 ngOnInit 生命周期钩子会在属性绑定完成后触发(译者注:参考列表 1,第一二步操作):

export class BComponent {
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngOnInit() {
        this.parent.text = 'updated text';
    }
}

果然会报错:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'A message for the child component'. Current value: 'updated text'.

现在我们再同样改变父组件 Aname 属性:

ngOnInit() {
    this.parent.name = 'updated name';
}

纳尼,居然没有报错!!!怎么可能?

如果你往上翻看列表 1 的操作执行顺序,你会发现 ngOnInit 生命周期钩子会在 DOM 更新操作执行前触发,所以不会报错。为了有报错,看来我们需要换一个生命周期钩子,ngAfterViewInit 是个不错的选项:

export class BComponent {
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngAfterViewInit() {
        this.parent.name = 'updated name';
    }
}

还好,终于有报错了:

AppComponent.ngfactory.js:8 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'I am A component'. Current value: 'updated name'.

当然,真实世界的例子会更加复杂,改变父组件属性从而引发 DOM 渲染,通常间接是因为使用服务(services)或可观察者(observables)引发的,不过根本原因还是一样的。

现在让我们看看真实世界的案例吧。

共享服务(Shared service)

这个模式案例可查看代码 plunker。这个程序设计为父子组件有个共享的服务,子组件修改了共享服务的某个属性值,响应式地导致父组件的属性值发生改变。我把它称为非直接父组件属性更新,因为不像上面的示例,它明显不是子组件立刻改变父组件属性值。

同步事件广播

这个模式案例可查看代码 plunker。这个程序设计为子组件抛出一个事件,而父组件监听这个事件,而这个事件会引起父组件属性值发生改变。同时这些属性值又被父组件作为输入属性绑定传给子组件。这也是非直接父组件属性更新。

动态组件实例化

这个模式有点不同于前面两个影响的是输入属性绑定,它引起的是 DOM 更新从而抛出错误,可查看代码 plunker。这个程序设计为父组件在 ngAfterViewInit 生命周期钩子动态添加子组件。因为添加子组件会触发 DOM 修改,并且 ngAfterViewInit 生命周期钩子也是在 DOM 更新后触发的,所以同样会抛出错误。

解决方案

如果你仔细查看错误描述的最后部分:

Expression has changed after it was checked. Previous value:… Has it been created in a change detection hook ?

根据上面描述,通常的解决方案是使用正确的生命周期钩子来创建动态组件。例如上面创建动态组件的示例,其解决方案就是把组件创建代码移到 ngOnInit 生命周期钩子里。尽管官方文档说 ViewChild 只有在 ngAfterViewInit 钩子后才有效,但是当创建视图时它就已经填入了子组件,所以在早期阶段就可用。(译者注:Angular 官网说的是 View queries are set before the ngAfterViewInit callback is called,就已经说明了 ViewChild 是在 ngAfterViewInit 钩子前生效,不明白作者为啥要说之后才能生效。)

如果你 google 下就知道解决这个错误一般有两种方式:异步更新属性和手动强迫变更检测。尽管我列出这两个解决方案,但不建议这么去做,我将会解释原因。

异步更新

这里需要注意的事情是变更检测和核查循环(verification digests)都是同步的,这意味着如果我们在核查循环(verification loop)运行时去异步更新属性值,会导致错误,测试下吧:

export class BComponent {
    name = 'I am B component';
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngOnInit() {
        setTimeout(() => {
            this.parent.text = 'updated text';
        });
    }

    ngAfterViewInit() {
        setTimeout(() => {
            this.parent.name = 'updated name';
        });
    }
}

实际上没有抛出错误(译者注:耍我呢!),这是因为 setTimeout() 函数会让回调在下一个 VM turn 中作为宏观任务(macrotask)被执行。如果使用 Promise.then 回调来包装,也可能在当前 VM turn 中执行完同步代码后,紧接着在当前 VM turn 继续执行回调:(译者注:VM turn 就是 Virtual Machine Turn,等于 browser task,这涉及到 JS 引擎如何执行 JS 代码的知识,这又是一块大知识,不详述,有兴趣可以参考这篇经典文章 Tasks, microtasks, queues and schedules ,或者这篇详细描述的文档 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理 。)

Promise.resolve(null).then(() => this.parent.name = 'updated name');

与宏观任务(macrotask)不同,Promise.then 会把回调构造成微观任务(microtask),微观任务会在当前同步代码执行完后再紧接着被执行,所以在核查之后会紧接着更新属性值。想要更多学习 Angular 的宏观任务和围观任务,可以查看我写的  I reverse-engineered Zones (zone.js) and here is what I’ve found

如果你使用 EventEmitter 你可以传入 true 参数实现异步:

new EventEmitter(true);

强迫式变更检测

另一种解决方案是在第一次变更检测和核查循环阶段之间,再一次迫使 Angular 执行父组件 A 的变更检测(译者注:由于 Angular 先是变更检测,然后核查循环,所以这段意思是变更检测完后,再去变更检测)。最佳时期是在 ngAfterViewInit 钩子里去触发父组件 A 的变更检测,因为这个父组件的钩子函数会在所有子组件已经执行完它们自己的变更检测后被触发,而恰恰是子组件做它们自己的变更检测时可能会改变父组件属性值:

export class AppComponent {
    name = 'I am A component';
    text = 'A message for the child component';

    constructor(private cd: ChangeDetectorRef) {
    }

    ngAfterViewInit() {
        this.cd.detectChanges();
    }

很好,没有报错,不过这个解决方案仍然有个问题。如果我们为父组件 A 触发变更检测,Angular 仍然会触发它的所有子组件变更检测,这可能重新会导致父组件属性值发生改变。

为何需要循环核查(verification loop)

Angular 实行的是从上到下的单向数据流,当父组件改变值已经被同步后(译者注:即父组件模型和视图已经同步后),不允许子组件去更新父组件的属性,这样确保在第一次 digest loop 后,整个组件树是稳定的。如果属性值发生改变,那么依赖于这些属性的消费者(译者注:即子组件)就需要同步,这会导致组件树不稳定。在我们的示例中,子组件 B 依赖于父组件的 text 属性,每当 text 属性改变时,除非它已经被传给 B 组件,否则整个组件树是不稳定的。对于父组件 A 中的 DOM 模板也同样道理,它是 A 模型中属性的消费者,并在 UI 中渲染出这些数据,如果这些属性没有被及时同步,那么用户将会在页面上看到错误的数据信息。

数据同步过程是在变更检测期间发生的,特别是列表 1 中的操作。所以如果当同步操作执行完毕后,在子组件中去更新父组件属性时,会发生什么呢?你将会得到不稳定的组件树,这样的状态是不可测的,大多数时候你将会给用户展现错误的信息,并且很难调试。

那为何不等到组件树稳定了再去执行变更检测呢?答案很简答,因为它可能永远不会稳定。如果把子组件更新了父组件的属性,作为该属性改变时的响应,那将会无限循环下去。当然,正如我之前说的,不管是直接更新还是依赖的情况,这都不是重点,但是在现实世界中,更新还是依赖一般都是非直接的。

有趣的是,AngularJS 并没有单向数据流,所以它会试图想办法去让组件树稳定。但是它会经常导致那个著名的错误 10 $digest() iterations reached. Aborting!,去谷歌这个错误,你会惊讶发现关于这个错误的问题有很多。

最后一个问题你可能会问为什么只有在开发模式下会执行 digest cycle 呢?我猜可能因为相比于一个运行错误,不稳定的模型并不是个大问题,毕竟它可能在下一次循环检查数据同步后变得稳定。然而,最好能在开发阶段注意可能发生的错误,总比在生产环境去调试错误要好得多。

查看原文

赞 36 收藏 32 评论 2

认证与成就

  • 获得 376 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2018-04-16
个人主页被 992 人浏览