背景

埋点,是收集产品的数据的一种方式,其目的是上报相关行为数据(PV/UV/时长/曝光/点击等),由相关人员以此分析用户的使用习惯,助力产品不断迭代和优化。对于开发来说,通常不仅仅需要完成基础的业务需求,还需要完成埋点需求,所以,追求的是简单快捷的埋点工作。而一个完整的埋点体系由以下三个部分构成:

  • 产品应用(产生行为数据)
  • 数据分析平台(展示、分析行为数据)
  • 数据平台 SDK(上报行为数据):封装数据分析平台的各种接口,暴露简单的方法供调用,实现简易的埋点上传。

目前,前端埋点存在的痛点一般是:

  1. 埋点字段的手动拼接,存在出错风险;
  2. 复杂场景的曝光埋点实现繁琐:分页列表、虚拟列表等;

  3. 埋点代码的侵入性:尤其是曝光代码导致逻辑复用困难。

埋点类型一般有:

  • 页面埋点:统计用户进入或离开页面的各种维度信息,如页面浏览次数(PV)、浏览页面人数(UV)、页面停留时间、浏览器信息等。
  • 点击埋点:统计用户在应用内的每一次点击事件,如新闻的浏览次数、文件下载的次数、推荐商品的命中次数等。
  • 曝光埋点:统计具体区域是否被用户浏览到,如活动的引流入口的显示、投放广告的显示等。

市场上常见的埋点方案:

  1. 全埋点(无埋点):由前端自动采集全部事件并上报,前端也就没有埋点成本,由数据分析平台或后端过滤有用数据,优点是数据全面,缺点是数据量大,噪声数据多。
  2. 可视化埋点:由可视化工具进行配置采集指定元素——查找 dom 并绑定事件,优点是简单,缺点是准确性较低,针对性和自定义埋点能力较弱。
  3. 代码埋点:用户触发某个动作后手动上报数据,优点时准确性高,能满足自定义的场景,缺点是埋点逻辑容易与业务逻辑耦合(命令式埋点),不利于维护与复用。

综上,需要的是一种简单快速且准确,同时埋点逻辑与业务逻辑解耦的埋点方案,也就是本文分析的声明式的组件化埋点方案。

声明式的组件化埋点方案

名词解释

  • 页面 (Page):在浏览器中打开的网页,不同页面以路径 location.pathname 来作区分;
  • 页面可见时长:一个页面对用户可见的累计时长;
  • 页面活跃时长:用户在页面上进行有效的鼠标、键盘及触控活动的累计时长;
  • 组件 (Component):DOM 元素的集合,是页面的组成部分。一个页面内可包含多个组件;
  • 组件可见时长:一个组件对用户可见的累计时长。
  • 可见性(visiability)
    • visible:页面 viewport 中且位于前台;
    • invisible - 页面不 viewport 中,或处于后台。
  • 活跃性 (activity)
    • active - 用户在网页中有活动(例如鼠标、键盘活动及页面滚动等);
    • inactive - 用户在网页中没有任何活动。

根据概念可知,一个页面不可见时,则一定不活跃,且其中的所有组件一定也都不可见;页面活跃时长 ≤ 页面可见时长;组件可见时长 ≤ 页面可见时长,

原理与思路

该方案总体思路如下:

  1. 对于通用字段进行统一处理,既不容易出错,也方便后期拓展。对于运行时字段(异步),支持 extra 进行传入。
  2. 对于页面级事件,埋点库初始化后自动注册关于页面级曝光的相关事件,不需要在代码中进行维护。
  3. 考虑到存在高频场景,设置上报缓冲队列 pendingQueue,通过定时任务分批次上报数据,支持设置上报频率。根据实践,点击类上报频率 1000ms,曝光类 3000ms。
  4. 考虑到埋点 sdk 没初始完,上报行为就已经产生了,设置 unInitQueue 来存储。
  5. 以页面为维度来管理埋点配置,便于维护和迁移。

考虑到埋点 sdk 没初始完,上报行为就已经产生了,比如曝光,新增如果这时候生成对应的点进入缓冲队列,就是属于无效的点因为没有加载到坑位信息、配置参数等,所以针对这种场景下产生的点位信息,我们新开一个队列存储,等到初始化完成再去处理;

因此,埋点上报总体流程为:埋点 sdk 接受返回埋点的函数,将其返回值上报,支持上报多个埋点;埋点事件由应用发送给埋点 sdk 后,埋点 sdk 首先会对数据进行处理,再调用数据平台暴露的方法, 将埋点事件上报给数据平台。

具体实现

判断页面可见性

虽然 Page Visibility API 的浏览器兼容情况不错,但对于Android、iOS 和最新的 Windows 系统可以随时自主地停止后台进程,及时释放系统资源。因此,基于 Google 描述网页生命周期的 Page Lifecycle API 兼容库 PageLifecycle.js 来监听页面可见性变化——一个网页从载入到销毁的过程中,会通过浏览器的各种事件在以下六种生命周期状态 (Lifecycle State) 之间相互转化,通过监听页面生命周期的变化并记录其时间,就可以相应获取页面可见性的统计数据:

  • active:网页可见且具有焦点;
  • passive:网页可见但处于失焦状态;
  • hidden:网页不可见但未被浏览器冻结,一般由用户切换到别的 tab 或最小化浏览器触发;
  • frozen:网页被浏览器冻结(后台任务比如定时器、fetch等被挂起以节约 CPU 资源);
  • terminated:网页被浏览器卸载并从内存中清理。一般用户主动将网页关闭时触发此状态;
  • discarded:网页被浏览器强制清理。一般由系统资源严重不足引起。

由此可得,页面生命周期状态和页面可见状态之间的映射关系为

  • active +  passive =  visible;
  • hidden + terminated + frozen +  discarded =  invisible。

因此,通过监听 statechange 来识别页面可见状态的改变,在生命周期状态为 activepassive 时标记页面为 visible 状态,在生命周期状态为其他几个时标记页面为 invisible 状态,更新最后一次可见的时间戳,并累加页面可见时间。PageLifecycle.js 是无法推送 discarded 事件的,因为网页已经被销毁并从内存中清理,无法向外传递任何事件——解决方案是需要在页面进入 invisible 状态时,对数据使用 JSON.stringify 序列化并储存在 localStorage 中,若页面后续转为 visible 状态即将其清空,否则在页面被强制清除后,在下一次初始进入页面时先将 localStorage 中的数据通过事件推送出去。另外, PageLifecycle.js 无法感知单页面应用的history 或 hash 路由切换,需要在埋点 sdk 中额外添加对路由变化事件(popstate/replacestate)的监听,等同于进入 terminated 生命周期:

lifecycleInstance.addEventListener("statechange", (event: StateChangeEvent) => {const { newState } = event;if (["active", "passive"].includes(newState)) {// page visible, do somethingreturn;}if (["hidden", "terminated", "frozen"].includes(newState)) {// page invisible, do something else}
})

判断页面活跃性

通过监听以下的六种浏览器事件,就可以判断用户是否在当前页面上有活动,此时页面标记为 active 状态,并记录当前时间戳,用于累加活跃时长。:

  1. keydown:用户敲击键盘时触发;
  2. mousedown:用户点击鼠标按键时触发;
  3. mouseover:用户移动鼠标指针时触发;
  4. touchstart:用户手指接触触摸屏时触发(仅限触屏设备);
  5. touchend:用户手指离开触摸屏时触发(仅限触屏设备);
  6. scroll:用户滚动页面时触发。

而页面被标记为 inactive 状态,有以下两种情况:

  • 在初始化埋点 sdk 时自定义,visible 状态下超过一定的时间阈值(比如 15 秒)没有监测到表示页面活跃的六种事件;
  • 页面状态为 invisible,因为如果页面对用户不可见,那么它一定是不活跃的。

判断组件可见性

首先需要获取需要统计的所有 DOM 元素,并指定埋点的标准。MutationObserver API 提供了 DOM 节点增减以及属性变化检测的能力。该 API 是异步触发,即要等到当前所有 DOM 操作都结束才触发,避免DOM频繁变动造成性能损耗,因此,可以用来监听 DOM 结构变化。

const mutationObserver = new MutationObserver(function (mutations, observer) {mutations.forEach(function (mutation) {console.log(mutation.target); // target: 发生变动的 DOM 节点});
});// 观察整个文档
mutationObserver.observe(document.documentElement, {childList: true, // 子节点的变动(指新增,删除或者更改)attributes: true, // 属性的变动characterData: true, // 节点内容或节点文本的变动subtree: true, // 表示是否将该观察器应用于该节点的所有后代节点attributeOldValue: false, // 表示观察 attributes 变动时,是否需要记录变动前的属性值characterDataOldValue: false, // 表示观察 characterData 变动时,是否需要记录变动前的值。attributeFilter: false, // 表示需要观察的特定属性,比如['class','src']
});mutationObserver.disconnect(); // 用来停止观察。调用该方法后,DOM 再发生变动则不会触发观察器

其中, mutations 是所有被触发改动的 MutationRecord 对象数组。

考虑到可能存在被监控组件是第三方库的,自定义属性 data-tracking-pv 会被过滤,为了统一,利用babel 插件在编译过程中寻找添加了 data-tracking-pv 属性的组件,并在其外层包裹一个自定义的 <tracking></tracking> 标签,自定义标签的优点是没有任何样式,所以包裹该标签也不会影响到原有组件的样式。埋点 sdk 收集这些元素供 MutationObserver 监听 DOM 变化。

const Component = () => {<div data-tracking-pv='{ "event": "component_custom_pv", "params": { ... } }'>component_custom</div>
}const Component = () => {return <Button data-tracking-pv={{ event: "component_antd_pv", params: { ... } }}>点击按钮</Button>
}// 插件最终生成的组件为</trackingdata-tracking-pv='{ "event": "component_custom_pv", "params": { ... } }'
>// ...真实组件
</tracking>

组件可见性,即曝光的三个判断标准:

  • 是否处于 viewport 中:使用 IntersectionObserver API 判断, 该 API 提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法;我们知道,用户的感兴趣程度 = 点击率(点击次数/曝光次数),考虑曝光的有效性,即需要判断组件出现在 viewpoint 内的达到一定的比例(0.5 或 0.75 或 1.0)和时长(3s)以及次数(是否重复曝光)标准;

const intersectionObserver = new IntersectionObserver((entries) => {entries.forEach(function (entry) {/**entry.boundingClientRect // 返回包含目标元素的边界信息。 边界的计算方式与 Element.getBoundingClientRect() 相同。entry.intersectionRatio // 目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为 1,完全不可见时小于等于 0entry.intersectionRect // 目标元素与视口(或根元素)的交叉区域的信息entry.isIntersecting // 返回一个布尔值, 如果根与目标元素相交(即从不可视状态变为可视状态),则返回 true。如果返回 false, 变换是从可视状态到不可视状态。entry.rootBounds // 根元素的矩形区域的信息, getBoundingClientRect 方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回 nullentry.target // 被观察的目标元素,是一个 DOM 节点对象entry.time // 可见性发生变化的高精度时间戳,单位为毫秒**/});},{root: document.querySelector('#root'), // 根 dom 元素, 默认 null 为 viewportrootMargin: '0px', // root元素的外边距threshold: [0, 0.25, 0.5, 0.75, 1], // Number 或 Number 数组,该属性决定了什么时候触发回调函数。默认为 0.0;}
);intersectionObserver.observe(document.getElementById("item")); // 开始监听一个目标元素intersectionObserver.disconnect(); // 停止全部监听工作

兼容性方面有对应的 intersection-observer-polyfill,

  • 样式是否可见:使用 CSS 的 display/ visibility / opacity 样式属性判断;以下是会被标记为 invisible 的情况:

    • visibility: hidden
    • display: none
    • opacity: 0
  • 页面是否可见:使用页面可见性判断。页面 invisible 状态下,所有组件的状态也标记为 invisible。

使用 requestIdleCallback 方法,浏览器会在空闲时执行传入的埋点函数,避免埋点影响主业务。其浏览器兼容情况如下:

可以使用 requestIdleCallback shim/polyfill,以下是简化版:

const requestIdleCallback =window.requestIdleCallback ||function(callback, options) {var options = options || {};var relaxation = 1;var timeout = options.timeout || relaxation;var start = performance.now();return setTimeout(function() {callback({get didTimeout() {return options.timeout? false: performance.now() - start - relaxation > timeout;},timeRemaining: function() {return Math.max(0, relaxation + (performance.now() - start));}});}, relaxation);};

MutationObserver + IntersectionObserver + requestIdleCallback 曝光埋点实现

const observerOptions = {childList: true, // 观察目标子节点的变化,是否有添加或者删除attributes: true, // 观察属性变动subtree: true, // 观察后代节点,默认为 false
}function callback(mutationList, observer) {mutationList.forEach((mutation) => {switch (mutation.type) {case 'childList':collectTargets()breakcase 'attributes':break}})
}function getParams(el) {const { exposureTrackerAction, exposureTrackerParams } = el.dataset;const Key = exposureTrackerAction;const pageEl = document.querySelector('[data-exposure-tracker-page-params]');let pageParams;if (pageEl?.dataset?.exposureTrackerPageParams) {try {pageParams = JSON.parse(pageEl?.dataset?.exposureTrackerPageParams);} catch (error) {console.error('parse pageParams fail');}}let params;if (exposureTrackerParams) {try {params = JSON.parse(exposureTrackerParams);} catch (error) {console.error('parse params fail');}}return {Key,...pageParams,...params};
}const intersectionObserver = new IntersectionObserver(function callback(entries, observer) {const list = [];entries.forEach(entry => {if (entry.target.dataset.exposureTrackerExposed !== '1' &&entry.intersectionRatio >= 0.5) {list.push(entry);observer.unobserve(entry.target);} else if (entry.intersectionRatio === 0) {delete entry.target.dataset.exposureTrackerExposed;}requestIdleCallback(() => {// ...上报});});
}, options);collectTargets() {const els = Array.from(document.querySelectorAll('[data-exposure-tracker-action]')).filter(el => !el.dataset.exposureTrackerTracked);if (els.length > 0) {// console.log('collectTargets', els);els.forEach(el => {intersectionObserver.observe(el);el.dataset.exposureTrackerTracked = true;});}export function getExposureTrackerPageParamsProps(params) {return {'data-exposure-tracker-page-params': params? JSON.stringify(params): undefined};
}export function getExposureTrackerParamsProps(action, params) {return {'data-exposure-tracker-action': action,'data-exposure-tracker-params': params ? JSON.stringify(params) : undefined};
}export default function App() {return (<div {...getExposureTrackerPageParamsProps({ pageData: 'some page data' })}><div{...getExposureTrackerParamsProps('item_content_expose', {itemData: 'xxx'})}>item content</div></div>);
}

自定义类指令式埋点实现

该方式适合简单的埋点上报,埋点逻辑与业务逻辑清晰分离,埋点 sdk 给 document 对象加上监听 click / hover 事件触发时,从当前触发事件的 target 逐级向上遍历,查看是否有对应此事件的指令。如果有,则上报此埋点事件,直至遇到一个没有事件指令的元素节点。这样也可以在指令中控制是否要继续向上遍历。

// 类指令式埋点实现逐级上报
<section data-tracking-hover={JSON.stringify({ type: 'func_operation', params: { value: 3 }})}><div data-tracking-click={JSON.stringify({ type: 'func_operation', params: { value: 2 }})}><Button data-tracking-click={JSON.stringify({ type: 'func_operation', params: { value: 1 }})}>点击</Button></div>
</section>

但是如果我们需要在上报事件前,对所上报的数据进行处理,那么这种方式就无法满足了。并且,并不是所有的场景都可以被 DOM 事件所覆盖。如果我想在用户在搜索框输入某个值时,上报埋点,那么我就需要对用户输入的值进行分析,而不能在 input 事件每次触发时都上报埋点。

装饰器式埋点实现

装饰器只能用于类组件,@tracking 修饰器接受一个函数形式的参数,其返回值即是要上报的事件。在 handleClick 函数被调用的时候,埋点 sdk 会首先上报埋点事件,然后再执行 handleClick 函数的业务逻辑。

  • target - 装饰器所在的类
  • propertyKey - 被装饰的函数的属性名
  • descriptor - 被装饰的函数的属性描述符
// tracking 函数简化源代码
tracking = (event: TrackingEvent) => {return (target: object, propertyKey: string, descriptor: object) => {if (isFunction(event) || isObject(event) || isArray(event)) {const oldMethod = descriptor.value;const _event = this.evalEvent(event)(...arguments);const composedFn = () => {this.sendEvent(_event);oldMethod.apply(this, arguments);}set(descriptor, "value", composedFn);return descriptor;}}
};/** * @tracking 使用示例*/class Test extends React.component {...@tracking((value: string) => ({type: 'func_operation',params: { keyword: value },}))handleClick() {console.log('执行点击的业务逻辑',);}render() {return (<Button onClick={handleClick} />)}}

因此,会先上报埋点,然后执行 descriptor.value 的逻辑,即被装饰的函数。

React Hook 埋点实现

与装饰器实现类似,useTracking 接受两个函数:埋点函数和业务函数,返回组合函数。

// useTracking 源代码
useTracking = (fn: () => any /** 业务函数 */, event: TrackingEvent /** 埋点函数 */) => {if (!event) return fn;return (...args) => {const _event = this.evalEvent(event)(...args);this.sendEvent(_event);return fn.apply(this, args);};
};// useTracking 使用示例
const Example = (props: object) => {const handleClick = useTracking(// 业务逻辑() => {console.log('业务逻辑');},// 埋点逻辑() => {return {type: "func_operation",params: { data, props.data },};});return <Button onClick={handleClick} />;
};

组件化点击埋点实现

使用者只需要把触发的回调(handleClick)绑定到对应的事件上即可,埋点逻辑由 埋点sdk 负责组合上去。

function setClickEvent(ele) {// 对于列表,也可以遍历return React.cloneElement(ele, {onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => {const originClick = ele.props?.onClick || noop;doClickTracking();originClick.call(ele, event);}});
}<TrackerClick name='button.click'>
{({ handleClickTrack }) => <button onClick={() => { handleClick() /** 业务逻辑 */; handleClickTrack() /** 埋点逻辑 */; }}>点击</button>
}
</TrackerClick>/**
class TrackerClick {constructor(props) {super(props);}handleClick() {}render() {return this.props.children({ handleClick });}
}
*/

组件化曝光埋点实现

仅向外暴露一个 setRef 用以获取 dom 执行监听工作,其他工作都交给埋点 sdk 来处理,同时支持配置以下曝光规则:

  • threshold: 曝光阈值;
  • visibleTime:组件曝光时长;
  • once:是否进行重复曝光埋点监听。
// case1: 直接绑定dom
return (<TrackerExposure name='button.exposure' extra={{ data }}>{({ setRef }) => <div ref={setRef}>{i + 1}</div>}</TrackerExposure>))
);// case2: 自定义组件
const Test = React.forwardRef((props, ref) => (<div ref={ref} style={{width: '150px',height: '150px',border: '1px solid gray'}}>TEST</div>)
)return (<TrackerExposure name="button.exposure" extra={{ data }}>{({ setRef }) => <Test ref={setRef} />}</TrackerExposure>
)/**
class TrackerExposure {constructor(props) {super(props);}setRef() {}render() {return this.props.children({ setRef });}
}
*/

补充--微信小程序曝光埋点

微信小程序的 节点布局相交状态 API (IntersectionObserver),可用于监听两个或多个组件节点在布局位置上的相交状态。这一组 API 常常可以用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。

/** 创建并返回一个 IntersectionObserver 对象实例。* 在自定义组件或包含自定义组件的页面中,* 应使用 this.createIntersectionObserver([Object options]) 来代替。*/
wx.createIntersectionObserver(/** 自定义组件实例 */[Object component],/** * thresholds: 一个数值数组,包含所有阈值。* initialRatio: 初始的相交比例,如果调用时检测到的相交比例与这个值不相等且达到阈值,则会触发一次监听器的回调函数。* observeAll: 是否同时观测多个目标节点(而非一个),如果设为 true ,observe 的 targetSelector 将选中多个节点(注意:同时选中过多节点将影响渲染性能)*/[Object options]
)

相关概念:

  • 参照节点:监听的参照节点,取它的布局区域作为参照区域。如果有多个参照节点,则会取它们布局区域的 交集 作为参照区域。页面显示区域也可作为参照区域之一。
  • 目标节点:监听的目标,默认只能是一个节点(使用 selectAll 选项时,可以同时监听多个节点)。
  • 相交区域:目标节点的布局区域与参照区域的相交区域。
  • 相交比例:相交区域占参照区域的比例。
  • 阈值:相交比例如果达到阈值,则会触发监听器的回调函数。阈值可以有多个。

IntersectionObserver 一共有四个方法

  1. IntersectionObserver.relativeTo(string selector, Object margins) 使用选择器指定一个节点,作为参照区域之一;
  2. IntersectionObserver.relativeToViewport(Object margins) 指定页面显示区域作为参照区域之一;
  3. IntersectionObserver.observe(string targetSelector, function callback) 指定目标节点并开始监听相交状态变化情况;
    callback: (res) => {};
    res: {id    string  节点 IDdataset    Record.<string, any>  节点自定义数据属性intersectionRatio  number  相交比例intersectionRect    Object  相交区域的边界boundingClientRect   Object  目标边界relativeRect    Object  参照区域的边界time number  相交检测时的时间戳
    }
    
  4. IntersectionObserver.disconnect() 停止监听。回调函数将不再触发。

显然,曝光上报的默认参照是 viewport,所以使用 IntersectionObserver.relativeToViewport 作为参照物,进行曝光埋点:

Page({data: {list: [{ value: 1, hadReport: false }, { value: 2, hadReport: false },{ value: 3, hadReport: false },]},onLoad() {this._observer = this.createIntersectionObserver({ thresholds: [0.5], observeAll: true });this._observer.relativeToViewport({ bottom: 0 }).observe('.item', (res) => {const { index } = res.dataset; // item 下标;if (!this.data.list[index].hadReport) {console.log(`report ${index}`) this.data.list[index].hadReport = true;this.setData({ list: [].concat(this.data.list)})}})},onUnload() {if (this._observer) this._observer.disconnect()}
})

前端组件化埋点方案与实现相关推荐

  1. 前端组件化的实施方案

    常规系统开发流程 一个典型的系统,它的开发过程可能是这样的: 前端独立系统 后端独立系统 中间有类似 swagger 的接口约定机制 缺点: 形成这个问题的原因是整个研发过程两极分化,断裂得很厉害,前 ...

  2. 爱奇艺知识WEB前端组件化实践

    组件化作为一种开发模式,其在代码复用,提高开发效率上的效果被广泛认可.组件化思想适用于移动端.Web前端.PC端.TV端等多种类型的客户端和前端开发. 本文主要讲述爱奇艺知识 WEB 前端团队如何结合 ...

  3. 谈谈我对前端组件化中“组件”的理解,顺带写个Vue与React的demo

    谈谈我对前端组件化中"组件"的理解,顺带写个Vue与React的demo 前言 前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的APP的 ...

  4. 如何通过 Vue+Webpack 来做通用的前端组件化架构设计

    目录:   1. 架构选型     2. 架构目录介绍     3. 架构说明     4. 招聘消息 目前如果要说比较流行的前端架构哪家强,屈指可数:reactjs.angularjs.emberj ...

  5. 组件化开发实战_一篇文章搞懂什么是前端“组件化”开发

    学过网页的朋友都知道,制作一个网页离不开HTML.CSS和JavaScript技术.对于初学者来来说,掌握这3门技术就已经很不容易了,为什么前端为什么又要搞出来一个"组件化"开发的 ...

  6. 「前端组件化」该怎么理解?

    大家好,我是若川.今天分享一篇关于「前端组件化」的好文.欢迎点击下方卡片关注我. 以下是正文~ 这里我们一起来学习前端组件化的知识,而组件化在前端架构里面是最重要的一个部分. 讲到前端架构,其实前端架 ...

  7. 前端组件化-抽象公共组件类

    优化上次的组件化小demo 上次的组件化demo只是为了简单的实现前端组件化的思想,这次我们稍微优化一下抽离公共类 下面代码 html <div id="wrapper"&g ...

  8. 前端组件化和模块化最大的区别是什么_7招提升你的前端开发效率

    前言 前端工程师其实是一个工作很杂的职位,除了要负责切图.写html/css/js外,还要解决一系列的浏览器兼容性.网页性能优化等问题,所以提高前端工程师的开发效率是势在必行的,也是前端工程化的体现. ...

  9. React.js 小书 Lesson1-2 - 前端组件化(一):从一个简单的例子讲起

    React.js 小书 Lesson1-2 - 前端组件化(一):从一个简单的例子讲起 本文作者:胡子大哈 本文原文:http://huziketang.com/books/react/lesson2 ...

  10. 前端组件化思想与实践

    前端组件化思想与实践 组件化思想 什么是组件化? 简单的说组件就是:将一段UI样式和其对应的功能作为独立的整体去看待,无论这个整体放在哪里去使用,它都具有一样的功能和样式,从而实现复用,这种整体化的思 ...

最新文章

  1. C语言发展历史,C语言特点,C语言利于弊,入门须知三招
  2. html页面tableview,用JS写的一个TableView控件代码
  3. 深入学习Java多线程——并发机制底层实现原理
  4. 如何判定括号是否匹配
  5. C#——orm-FulentData(sqlite3)——异常捕获
  6. 7. Linux 环境
  7. 2018年的人工智能将如何发展?看看专家怎么说
  8. 基础篇:数据库 SQL 入门教程
  9. ADS仿真6_PA设计【未完成】
  10. nvidia 卸载驱动
  11. 龙珠直播php,斗鱼、全民TV、龙珠等直播平台排行榜 看视频直播发展趋势
  12. 夏普linux电视安装apk,智能电视不能安装APK文件?看完就知道怎么做了!
  13. 怎样才能画好古代汉服?画好古代汉服有哪些技巧?
  14. 一个很好的看电子书的软件
  15. 怎么把MP4视频进行压缩
  16. 《深入理解计算机系统》 练习题3.9-3.11 移位操作
  17. php函数库快速记忆法_史上最全的php函数大全
  18. JavaScript Light BOX相册预览功能
  19. 裁员10%,涉万人!Tesla这次“玩”大了…
  20. Windows Phone 7与Android和iPhone的比较

热门文章

  1. Unity Blend命令
  2. 国民岳父的“屁民理论”
  3. cadence导入dxf文件_CADENCE16.3导入DXF文件
  4. 10 分钟了解 Flutter 跨平台方案
  5. Python拓展dict类
  6. 刻录服务器系统光盘,系统光盘制作
  7. Si24R2F+超低功耗高性能2.4GHz无线射频NTC测温单发射芯片 兼容Si24R2E
  8. 初学C语言的感受(张森)
  9. vscode风格化设置
  10. 「 神器 」极简网速监控悬浮窗软件