最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Google的新前端框架Lit

    正文概述 掘金(头号前端)   2021-06-23   2049

    Google的新前端框架Lit

    Google的新框架 Lit ,有 Google 加持的框架引起了我的兴趣,就去简单的了解了一下,简单的给大家分享一下学习成果。由于Lit框架还在快递迭代中,文中讲到的一些代码实现很可能已经重构,感兴趣的同学,可以去翻一下Lit源码。

    什么是 Lit

    Lit 是一个基于 Web-Component 构建的前端框架,前身基本可以理解为即 PolymerLit 提供了如下具有竞争力的特性

    • 基于 Web-Component 的更高层封装,提供了现代前端开发习惯的响应式数据,声明式的模版,减少了web component的一部分样板代码.

    • 小。运行时仅有5K

    • 性能强悍。规避了 VDOM 的一些弊端,更新时仅处理 UI 中的异步部分(可以理解成仅处理响应式的部分)

    • 兼容性较好。因为 web-component 是 HTML 的原生能力,也就代表着 web-component 可以在任何使用 HTML 的地方使用,框架无关。

    小和框架无关是促使我关注这个框架的一个重点(svelte也是,有时间再说,学不动了),因为对于一些通用业务代码来说, 运行时尽可能的小框架无关 是最核心的两个技术选型指标。

    什么是 Web-Component

    Google的新前端框架Lit

    我个人认为,组件化是现在前端快速发展和规模壮大的一个比较重要的原因,想想写 JQ 的年代,HTML 代码基本都长这个样子

    <div>
        <div class="button1" >按钮</div>
        <div class="button2 active" >按钮</div>
        <div class="button3" >按钮</div>
    </div>
    

    哪怕后面出现了模版语法,状况也没有变得更好(与服务端协作共用模版 -> JSP or 要在JS中封装方法,通过模版语法注入 -> handlebars),单纯的loop循环渲染可能能方便的解决DOM复用的问题,但是跨层级的组件复用仍然是一个问题。

    因此任何一个想要进一步发展的前端框架技术,组件化是必不可少的一步。 Web-Component 就是这样一个浏览器原生支持的创建可重用元素(自定义组件)的能力,而 Lit 则是基于 Web-Component 构建的。

    那我们需要先了解下 Web-Component

    Web-Component 的简单开发流

    我们先了解 Web-Component 的两个核心点,了解了这两个属性就可以构建你自己的组件了

    Custom Element 自定义元素

    首先,我们需要通过浏览器提供的 **CustomElementRegistry** ****接口实例注册自定义元素,实例被挂载在 window.customElements

    CustomElementRegistry的 define 方法可以用来注册一个自定义的元素,有两种类型可选

    • 自定义元素 :独立元素,行为完全由开发者定义。

    • 自定义内置元素 :这些元素继承并扩展内置HTML元素

    API如下

    customElements.define(name, constructor, options);
    

    Google的新前端框架Lit

    name 就是你自定义的元素名称(符合 DOMString 标准,必须带短横线),以上述为例,可以通过 <my-component></my-component> 的形式使用 constructor 就是我们定义的组件 options 声明了我们定义的是哪种类型的自定义元素,目前只有一个extends可用,指定继承于什么元素。 当定义了继承什么元素之后,使用方式就与自定义元素不太一致了,需要用到 is 属性假设你定义了一个名为my-paragraph的继承自p标签的自定义内置元素,那么使用时,需要 <p is="my-paragraph"></p> 这样写

    这么看来, Web-Component 的逻辑核心就在constructor上了,我们如何定义一个自己的 component 呢? 不同于 React 和 Vue,可以在 render 方法中书写 JSX 或者模版语法来创建一颗 VDOM 树来定义组件结构。 Web-Component 并没有提供可以用来书写模版的方式(但这也代表着他可以使用任何模版语法)。通常是使用常规的 DOM 操作来在 constructor 中创建你的组件结构。举个栗子:

    class MyComponent extends HTMLElement {
        constructor() {
          super()
    
          const wrapper = document.createElement('span');
          wrapper.setAttribute('class','wrapper');
          const info = document.createElement('span');
          info.setAttribute('class','info');
          info.textContent = this.getAttribute('text') || 'default';
    
          this.appendChild(wrapper);
          wrapper.appendChild(info);
        }
    }
    

    上面这段代码创建了一个根据双层span嵌套的文本节点

    <my-component></my-component>
    <!-- 等价于 -->
    <span class="wrapper"><span class="info">default</span></span>
    
    <my-component text="test"></my-component>
    <!-- 等价于 -->
    <span class="wrapper"><span class="info">test</span></span>
    

    当然,单纯的创建UI没有什么意思,我们来思考下,现代框架还提供了什么能力?

    响应式!事件绑定!lifecycle!

    OK,安排。

    先说比较简单的事件绑定,既然 DOM 都是内部创建的,那么绑定事件也是轻而易举(注意this指向)

    class extends HTMLElement {
        constructor() {
          super()
          this.handleClick = this.handleClick.bind(this)
    
          const wrapper = document.createElement('span');
          wrapper.setAttribute('class','wrapper');
          const info = document.createElement('span');
          info.setAttribute('class','info');
          info.textContent = this.getAttribute('text') || 'default';
    
          this.appendChild(wrapper);
          wrapper.appendChild(info);
    
          const button = this.querySelector('#button');
          button.addEventListener('click', this.handleClick)
        }
    
        handleClick () {
          this.parentNode.removeChild(this)
        }
    }
    
    <my-life-cycle-component>
      <button id='button'>Remove this</button>
    </my-life-cycle-component>
    

    上述代码创建在之前的文本基础上,多了一个按钮,点一下这个按钮就会把整个 custom element 移除掉(当然,这个按钮也可以由组件自己创建)

    然后我们再来说 lifecycle ,customeElemnts.define接受的构造函数中,允许开发者定义如下几个生命周期,会在相关的时机被调用

    • connectedCallback :当 custom element 首次被插入文档DOM时,被调用。

    • disconnectedCallback :当 custom element 从文档DOM中删除时,被调用。

    • adoptedCallback :当 custom element 被移动到新的文档时,被调用。

    • attributeChangedCallback :当 custom element 增加、删除、修改自身属性时,被调用

    简单来说,就是

    • componentDidMount

    • componentWillUnmount

    • Not exist(这个我还没测试出什么场景会用出现)

    • componentDidUpdate

    因为没有 state 这个概念,所有的组件内部属性的变化监听都需要我们手动处理,心智负担可能会略重一些。 当然也可以把他们作为自定义元素的属性,通过 ~~attributeChangedCallback~~ 处理

    顺路,有了属性变化的回调响应式也就出来了(当然,只是响应式的基础,属性变化并不会直接作用到内部渲染逻辑上,心智负担 +1 ) 如果需要在元素属性变化后,触发回调函数,必须通过定义 observedAttributes() get函数来监听这个属性

    Google的新前端框架Lit

    <my-life-cycle-component>
          <button id='button1'>Remove this</button>
          <button id='button2'>Toggle this</button>
          <input />
    </my-life-cycle-component>
    

    看下代码,这里定义了一个带输入框的组件,输入框输入后,会重新渲染 componnet 内部的文本。同时监听 style 属性,变化后打印出来(无意义的demo +1)

    Shadow DOM 影子DOM

    Shadow DOM 的好处有很多。 Shadow DOM 主要的作用在于,“他可以将独立的一个DOM(style标签也属于)附加到元素上(这个DOM是隐藏的),且不会影响外层的样式,这给web component带来了十分强大的封装能力,能够完全的将组件的结构,样式和行为动作对外隐藏起来,对外隔离”]

    Google的新前端框架Lit

    有一个需要关注的概念 - Shadow boundary Shadow boundary 指的是 Shadow DOM 结束的地方,也是常规 DOM 开始的地方。整个 shadow dom 内的样式、元素都不会对外影响超过 shadow boundary 的范围。

    那么,如何使用呢? 有一个核心API, Element.attachShadow({mode: }) ,可以将一个 shadow root 附加到任何一个元素上,mode 的取值有 open 或者 closed。区别在于能否从外部获取到 shadowDOM 的结构。想要更深入的了解 open 和 close 的区别可以参考 blog.revillweb.com/open-vs-clo… 这篇文章,这里就不展开描述了。

    那么,拿上边最简单的那个例子做一下改造

    customElements.define('my-component', class extends HTMLElement {
        constructor() {
          super()
    
          const shadow = this.attachShadow({mode: 'open'});
          const info = document.createElement('span');
    
          info.setAttribute('class','info');
          info.textContent = this.getAttribute('text') || 'default';
    
          // this.appendChild(info)
          shadow.appendChild(info);
        }
    })
    

    这样就创建了一个基于 shadowDOM 的自定义元素了,看起来没什么差别,我们再添加一些自定义的样式试试

    customElements.define('my-component', class extends HTMLElement {
        constructor() {
          super()
    
          const shadow = this.attachShadow({mode: 'open'});
          const info = document.createElement('span');
    
          info.setAttribute('class','info');
          info.textContent = this.getAttribute('text') || 'default';
    
          const style = document.createElement('style')
          style.textContent = `
            span {
              color: red;
            }
          `
          // this.appendChild(info)
          shadow.appendChild(style);
          shadow.appendChild(info);
    
        }
    })
    

    可以尝试在页面的其他地方也添加一些span标签,但是你会发现,只有 下面的 span 标签生效了红色的样式。

    上述两个属性可以创建一个不受外部影响,且拥有内部JS运行逻辑、拥有独立CSS的自定义元素(也就是Web Component) 我觉得大家已经开始在吐槽,这种类JQ的写法简直是异类,DOM复杂起来之后就很难整了。那么如何组装更复杂的dom,难道无解了么?不,那么这里需要提到另外一个属性

    Template & Slot

    template 元素是浏览器一直以来都支持的一个特性,template 中的内容在渲染 HTML 到屏幕上的时候不会显示出来,需要通过 Javascript 获取到模版后才能实例化,并渲染到页面上。 那么,我们可以把 template 作为一个可以储存在文档中的内容片段,然后在组件渲染的时候把 template 填充到 Web-Componentshadow dom 里面。还是拿上边的那个例子做修改

    <template id="my-component">
        <style>
            span {
              color: red;
            }
        </style>
        <span class="info">
            12312312
        </span>
    </template>
    
    customElements.define('my-component', class extends HTMLElement {
        constructor() {
          super()
    
          const template = document.getElementById('my-paragraph');
          const templateContent = template.content.cloneNode(true);
          const shadow = this
              .attachShadow({mode: 'open'})
              .appendChild(templateContent);      
        }
    })
    

    这样以来,是不是有点 Vue 那个意思了,但是还缺点什么,my-component 上之前可以读取 text 属性填充到 span.info 里面,现在好像没有这个能力了。

    这个时候需要请出我们的 slot 槽来做这件事

    Slot 通过 name 属性标示,放置的位置表示他在模版中的位置,当有另外一个元素定义了同名的 slot 属性,那么这个元素就会被替换到模版中。我们修改下上边的那个例子

    <!-- 使用 -->
    <template id="my-component">
        <style>
            span {
              color: red;
            }
        </style>
        <span name="my-slot" class='info' >default</span>
    </template>
    
    <!-- 使用 -->
    <my-component>
        <span slot="my-slot">
            12312312
        </span>
    </my-component>
    
    customElements.define('my-component', class extends HTMLElement {
        constructor() {
          super()
    
          const template = document.getElementById('my-paragraph');
          const templateContent = template.content.cloneNode(true);
          const shadow = this
              .attachShadow({mode: 'open'})
              .appendChild(templateContent);      
        }
    })
    

    当然这里只是最为简单的用法,但至此,基本上 Web-Component 你就算入门了。整体写起来难度也不算太高,但还是有不少值得吐槽的地方 (更多关于Web Component) 。那么我们来看看 Lit 做了啥,能不能让 Web-Component 变得更好用些

    Lit做了啥

    看下我们刚才说到的 Web-Component 里面的几个槽点

    1. 响应式仅有回调,无法自动映射到UI上

    2. 没有 state 内部状态,自己维护的状态无法直接监听变化

    3. 没有模版语法(可以用 slot 和 template)

    明确一点,在学习 Lit 的过程中,可以认为没有 state 这个概念(实际上有,理解为私有的 reactive properties),只有名为 reactive properties 的成员属性。可以简单的理解成又是 state,又是 props。

    那么现在问题转变成了

    1. 如何响应reactive properties的变化,并应用到UI上

    2. 如何解决模版语法

    Lit 用了两个个核心库来解决这个问题,分别是 lit-elementlit-html

    Lit-html

    lit-htmlLit 的核心逻辑,可以理解为 Literal Html ,他异于JSX创造了另外一种高性能的字符流HTML模版引擎。 Lit选择了直接继承Polymer的LitHTML项目,并将整体框架重命名为 Lit 我们知道 jsx 是需要编译的它的底层最终还是 createElement ....。而 lit-html 就不一样了,它是基于 tagged template 的,使得它不用编译就可以在浏览器上运行,并且和 HTML Template 结合想怎么玩怎么玩,扩展能力更强。下面我们展开来看。

    lit-html 提供了两个核心方法 renderhtml

    lit-html.html

    html`<p>${content}</p>`
    

    这个是es6的原生语法 - 带标签的模板字符串 (tagged template),并不是什么magic,html 这个函数会接受到如下的参数

    type taggedFunc = (strings: string[], ...values: any[]) => any;
    // 上边的那个段代码接收到的参数就是
    // ['<p>', '</p'>], content
    

    经过 lit-html 的修饰上面这段代码最终会构造一个 Template Result 对象,形如

    declare class TemplateResult {
        readonly strings: TemplateStringsArray;
        readonly values: readonly unknown[];
        readonly type: string; // html or svg
        readonly processor: TemplateProcessor;
        constructor(strings: TemplateStringsArray, values: readonly unknown[], type: string, processor: TemplateProcessor);
        
        getHTML(): string;
        getTemplateElement(): HTMLTemplateElement;
    }
    
    const templateResult = {
        strings: ['<p>', '</p>'],
        value: [content]
        type: 'html'
    }
    

    这里需要注意一下 getHTMLgetTemplateElement 方法,这两个方法可以将strings转化成为一个 <template> 标记,也就是上面提到的 template

    const template = (title, content, className) => html`
      <h1 class="${className}">${title}</h1>
      <div>${content}</div>
    `;
    
    <template>
        <h1 class$lit$=\"{{lit-7227407027270176}}\"><!--{{lit-7227407027270176}}--></h1>
        <div><!--{{lit-7227407027270176}}--></div>
    </template>
    

    简单的解释一下,这个过程就是逐个处理strings中的数据,根据不同的情况

    • Attribute

    • Node

    • Comment

    拼接成一个完整的字符串,然后innerHTML插入到创建好的template标记中。 Q:如何区分代码中真正的comment?

    lit-html.render

    现在我们有了通过标签模版得到的 TemplateResult (一个纯值对象),接下来需要调用 render 方法去渲染模版到页面上,先看API render(templateResult, container, options?) render 接收一个 templateResult实例 和 container 渲染容器来完成一次渲染,这里分为首次渲染和更新渲染。

    首次渲染

    先创建一个 NodePart 对象(继承自Part,可以理解为节点的构造器controller,这个是核心实现,暂时不展开,后面来看),然后调用 NodePart 实例的 appendInto 方法,在渲染容器中加入两个 comment ,同时记录了两个 comment 的引用。后续 NodePart 会把 DOM 渲染到这两个 comment 中间

    <div id="container"><!---><!---></div>
    <!-- 他是使用comment作为占位符的。 -->
    

    然后会调用 part.commit 方法,将内容渲染到容器中 commit分为了几种情况

    • directive

    • primitive(原始类型)

    • templateResult

    • node

    • Iterable

    • 清空

    根据前面的逻辑,第一次一定会直接走进 templateResult 的分支,这里的逻辑可以简单这么描述, 通过 Factory ,使用 TemplateResult 中的模版部分 strings 创建一个 Template 对象(中间产物), Factory 这里做了一层缓存,如果使用 TemplateResult 的模版(strings)有现成的模版的话,直接使用现成的模版,如果没有,则重新创建。 在后续调用 render方法时,相同的模版(strings 值与第一次调用时是完全一致)是重用第一次的Template的,可以理解为编译时期就确定的一个常量值,而变化的只有 value 数组

    export declare class Template {
        readonly parts: TemplatePart[];
        readonly element: HTMLTemplateElement;
    }
    export type TemplatePart = {
      readonly type: 'node'; index: number;
    } | {
      readonly type: 'attribute';
      index: number;
      readonly name: string;
      readonly strings: ReadonlyArray<string>;
    };
    
    1. 先用TemplateResult的模版(string)找有没有现成的模版,如果有,直接复用

    2. 如果没有,则检查keyString的模版中有没有 模版.join markerKey的引用(markKey means lit-7227407027270176)

    3. 如果还是没有,则创建一个Template实例,并且将Template 使用模版 和 keyString缓存起来

    缓存流程不展开讲解,如果有兴趣自己看一下

    Template 对象中分为 parts 和 element,element就是TemplateResult转化出来的 <template> ,parts部分,是在遍历<template>(dom walker)的时候生成的。处理流程简化理解

    • 如果是Node节点

      • 判断是否有attribute,且属性名有特殊标记,有的话,移除template上的属性,并往part push一个 {type: 'attribute', index, name, strings: statics} 的结构,index是当前的walker下标,name是属性名,strings是这个属性的插值前后字符
    • 如果是Comment节点

      • 如果comment的内容等同于marker -(这里可以和真正的comment区分开),然后往part中推入一个node节点 {type: 'node', index}

        • 如果是第一个节点或者前面一个节点已经是一个part的标记了,会先在当前节点前添加一个空的comment节点,
    <template>
        <h1 class$lit$=\"{{lit-7227407027270176}}\"><!--{{lit-7227407027270176}}--></h1>
        <div><!--{{lit-7227407027270176}}--></div>
    </template>
    

    处理完成后

    {
        element: template
        parts: [
            {type: "attribute", index: 1, name: "class", strings: ["", ""]},
            {type: "node", index: 3},
            {type: "node", index: 7},
        ]
    }
    // templatee也会会简化成如下结构
    <template>
      <h1><!----><!----></h1>
      <div><!----><!----></div>
    </template>
    

    可以理解 Template 是一个已经成型的 DOM 模版,他拥有完整的 DOM 和需要插值的位置定位,但他还没渲染到 DOM

    接下来检查当前的 Template 是否已经创建了 TemplateInstance 实例,如果没有,实例化一个 TemplateInstance

    class TemplateInstance {
        private readonly __parts;
        readonly processor: TemplateProcessor;
        readonly options: RenderOptions;
        readonly template: Template;
        constructor(template: Template, processor: TemplateProcessor, options: RenderOptions);
        update(values: readonly unknown[]): void;
        _clone(): DocumentFragment;
    }
    

    TemplateInstance 会通过<template>创建 fragment ; 然后遍历 parts ,根据 TemplatePart 字面量的类型,分别创建 NodePart 和 AttributePart 实例。

    最终调用 TemplateInstance 实例的 update 方法,这个方法会逐个调用 Part 实例的 setValue (真实的值)和 commit (渲染方法)方法,至此,循环回了render的最开始的方法调用,剩下的就是递归调用,直到找到原始的值类型的那一层,渲染到Fragment上。

    • __commitText :直接修改文本节点的文本

    • __commitNode :清空父亲节点中的startNode到endNode(最开始提到的那两个comment占位),然后把node添加进去。

    当递归回到最顶层后, commitNode 拿到的就是完整的 fragment ,塞到容器中就可以了。

    核心流程

    至此,第一次的渲染完成,大致流程如下

    Google的新前端框架Lit

    可能听起来有些绕,我们可以暂时忽略 Template ,它是一个中间状态

    TemplateResult 是类似 JSX 的一种更轻量的对于模版的字面量描述,是一个模型 TemplateInstance 可以理解成一个小的 MVC 框架的嵌套

    • DOM(fragment) 是应用的外层框架,是不变的 View 部分

    • TemplateResult 中的成员 value 是 Model

    • Controller(Part)中连接了 View 和 Model。提供了更新数据的方法(setValue)和渲染到视图的方法(Commit)

    更新渲染

    可以类比SQL执行过程中的库缓存,如果SQL结构一致就复用已有的模型 逐层比较检查所有的缓存是否命中(对比类型 和 模版 - strings结构)

    1. 如果命中的话就使用已有模版,找到 TemplateInstancePart ,把 templateResultvalue 更新给 Part

    2. 如果没有命中的话,就走第一次渲染的流程

    效率

    • 带标签的模版字符串 执行相比 JSX 会更加高效。 JSX 的每次 render 都需要完整的构造一个虚拟DOM,而 lit-html ,则只是重新构建一个十分轻量的 TemplateResult 对象,变化的只有 value 集合。

    • 从 TemplateResult 到 < template> 的过程,是直接从 TemplateResult 构造 html ,然后使用 template.innerHTML 完成解析的。这个过程完全使用浏览器自己的语法解析器来完成的。由于使用了 template 技术,这个DOM是一个Fragement,并不是真实DOM的一部分,内存占用小

    • 实际渲染的DOM生成,是从 template.importNode 来完成DOM的复制。而不是像React一样逐个Dom节点的创建。对于较大的DOM,效率十分可观

    • 在增量更新的过程中,Lit 和 React 相类似,都是按照相同层次的节点重用的方式,React通过 diff(VDOM, DOM) 来实现增量更新,而LitHtml并没有使用diff算法,而是基于相同模板的渲染,只需要对动态部分进行更新即可。没有diff算法会更加的轻

    Lit-element

    OK,模版语法有了,剩下的就是如何把状态变化响应式的应用到模版里了。

    如何使用

    这部分实际上不复杂,有过Vue开发经历的同学一定都清楚Vue是如何将数据和视图绑定起来。 Lit-element 也是如此 在Lit中,你需要这样声明一个组件

    @customElement('simple-greeting')
    export class SimpleGreeting extends LitElement { /* ..*/ }
    

    customElement 实际上是 customElement.defined 的语法糖,而 LitElementLit 提供的一个基类,其中就处理了数据的响应式处理(实际上 LitElement 还继承了 UpdateElement ,由 UpdateElement 做响应式的处理)。

    Reactivity Property

    我们先看看,Lit的文档中要求怎么定义 reactivity property

    class MyElement extends LitElement {
      @property()
      name: string;
    }
    

    我们会会发现,如果需要响应式属性的话,需要使用 property 这个装饰器来装饰属性,property 这个装饰器的逻辑为,调用所在类的静态方法 createProperty 往类的静态成员 _classProperties 中注册这个属性,同时,给这个属性添加 getter 和 setter,到这里,类准备工作就做好了。

    • getter:直接取

    • setter:更新后触发更新

    每次在组件内部修改 reactive property 的时候,属性更新完成后会重新调用 lit-html 的 render 方法渲染到UI上。

    这个和 state 的概念十分的相似,那么 lit-element 又是如何处理外部传输属性(props)的变化呢? 这里我们需要应用到前面提到的 Web-Component 的生命周期get observedAttributesattributeChangedCallback 。每次当传递给 component 的属性发生变化的时候,这两个周期就会触发,只需要查询是否在 _classProperties 中,并主动更新 reactive property 即可。 除此之外,property 装饰器还可以接受一个 options 配置一些属性来进行适配

    1. attribute - 定义这个成员变量是否和元素属性绑定

    2. converter - 定义转化逻辑,从元素属性(都是string)到真实属性

    3. hasChanged - 判断属性是否发生变化

    4. type - 在没有定义converter时使用,转化元素类型

    5. state - 如果定义了state的话,象征这个成员变量是一个内部状态,是私有的,新版的 Lit 提供了一个单独的装饰器@state 来替代这个属性

    Lit 剩下的诸如装饰器,事件绑定之类的就不再展开细说了,有兴趣的同学可以去阅读下源码,整体上比 React 易读性高的多(Doge)。至此,一个完整的可用的面向未来的前端框架就完成了。

    小结

    Lit 因为兼容性问题现在还不能应用到实际的业务场景中,但是确实是一个值得关注和学习的框架。其中的一些设计思想和理念,跳出了 React 限制的框架,给出了前端框架的另一种可能的解决方案。在框架领域上一家独大不是什么好事,有更多的奇思妙想和拥抱未来才能让前端的发展更加广阔。

    还有个不能实际场景应用的问题,Lit 还在快速的迭代中,每次都是很大的 Breaking changes。比如刚才提到的UpdateElement又被拆成单独的包了。。。。


    起源地下载网 » Google的新前端框架Lit

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元