作者:字节跳动转发链接:https://www.yuque.com/docs/share/a86a12a1-77c4-4f78-854b-af185f90bec4目录细品西瓜播放器功能分析(上)「实践」本篇细品西瓜播放器功能分析(下)「实践」简介西瓜视频播放器(HTML5)一款带解析器、能节省流量的 HTML5 视频播放器。它从底层解析 MP4、HLS、FLV 探索更大的视频播放可控空间。它具有以下功能特色:易扩展:灵活的插件体系、PC/移动端自动切换、安全的白名单机制;更丰富:强大的 MP4 控制、点播的无缝切换、有效的带宽节省;较完整:完整的产品机制、错误的监控上报、自动的降级处理。背景字节跳动的视频业务大多数是短视频,早期的时候我们在 video.js 基础上做二次开发。后来发现很多功能达不到我们的要求,比如自定义 UI 的成本、视频的清晰度无缝切换、视频流量的节省。考虑到当前点播依旧是 mp4 居多,我们做了个大胆的假设:在播放器端加载视频、解析视频、转换格式,让不支持分段播放的 mp4 动态支持,这样就无须转换源视频的格式,服务器端也无其他开销。在这个动力下,我们在 2017 年年底完成了这项开发任务,并与2018 年年初测试了稳定性和经济收益。在这个背景下,我们一次解析了 hls、flv 等视频,这样我们不再简单的依赖第三方的视频库,只有掌握了底层技术才有优化的可能性。在不断攻克 hls、flv 解析的背景下,我们增强了产品体验,比如交互效果、进场动画等。直到最近,我们想完善文档并把播放器源代码开源出来给更多的视频从业者一个参考,我们一起交流学习,共同进步。一、西瓜播放器架构1.1 功能分析思维导图二、西瓜播放器 UML三、内置插件功能分析西瓜视频播放器主张一切设计都是插件,小到一个播放按钮大到一项直播功能支持。想更好的自定义播放器完成自己业务的契合,理解插件机制是非常重要的,播放器本身有很多内置插件,比如报错、loading、重播等,如果大家想自定义效果可以关闭内置插件,自己开发即可。3.1 本地预览功能播放器预览本地视频功能,不建议改动。源码地址:https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/control/localPreview.js使用方式new Player({
el:document.querySelector('#mse'),
url: 'video_url',
preview: {
uploadEl: uploadDom
}
});配置项:uploadEl含义:Dom 元素,用于放置上传视频文件的相关控件源码实现let localPreview = function() {
let player = this;
let util = Player.util;
// util.createDom = function (el = 'div', tpl = '', attrs = {}, cname = '')
let preview = util.createDom(
"xg-preview",
'<input type="file">',
{},
"xgplayer-preview"
);
let upload = preview.querySelector("input");
if (player.config.preview && player.config.preview.uploadEl) {
player.config.preview.uploadEl.appendChild(preview);
upload.onchange = function() {
// https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file
// 选中文件通过 HTMLInputElement.files 属性返回 — 返回值是一个 FileList 对象,
// 这个对象是一个包含了许多 File 文件的列表
player.uploadFile = upload.files[0];
// objectURL = URL.createObjectURL(object);
// 参数 object:用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。
// URL.revokeObjectURL() 静态方法用来释放一个之前已经存在的、通过调用 URL.createObjectURL()
// 创建的 URL 对象。
// 当你结束使用某个 URL 对象之后,应该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件
// 的引用了。比如 window.URL.revokeObjectURL(objectURL);
let url = URL.createObjectURL(player.uploadFile);
if (util.hasClass(player.root, "xgplayer-nostart")) {
player.config.url = url;
player.start();
} else {
player.src = url;
player.play();
}
};
}
};
Player.install("localPreview", localPreview);原理分析视频本地预览的功能主要利用 URL.createObjectURL() API 来实现。URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。 这个新的 URL 对象表示指定的 File 对象或 Blob 对象。注意: 此特性在 Web Worker 中可用。注意:此特性在 Service Worker 中不可用,因为它有可能导致内存泄漏。input file 获取本地 File 对象把 File 对象转换为 ObjectURL设置 Video 元素的 src 为视频本地的 ObjectURL 地址扩展分析1、前端本地图片预览的实现方式前端本地图片预览,可以通过 FileReader.readAsDataURL 或 URL.createObjectURL 的方式来实现。2、FileReader vs URL.createObjectURL2.1 是否同步createObjectURL 是同步执行FileReader.readAsDataURL 是异步执行的2.2 内存使用createObjectURL 方法会返回一个本地的 URL 地址,对象会一直保存在内存中,直到文档触发 unload 事件(比如 document close)或者手动调用 revokeObjectURL。FileReader.readAsDataURL 会返回 base64 格式的字符串,比 Blob URL 的方式会占用更多的内存空间,当你不需要使用它的时候,可以通过系统的垃圾回收机制来自动进行回收。2.3 兼容性createObjectURL:支持 IE 10 以上的主流浏览器,详细的兼容性,可以查看 caniuse – createObjectURL。FileReader.readAsDataURL:支持 IE 10 以上的主流浏览器,详细兼容性,可以查看 caniuse – readAsDataURL参考资源 —— filereader-vs-window-url-createobjecturl地址:https://stackoverflow.com/questions/31742072/filereader-vs-window-url-createobjecturl/31743665#317436653、createObjectURL 特性检测function createObjectURL (file) {
if (window.webkitURL) {
return window.webkitURL.createObjectURL(file);
} else if (window.URL && window.URL.createObjectURL) {
return window.URL.createObjectURL(file);
} else {
return null;
}
}参考资源 —— how-to-choose-between-window-url-createobjecturl-and-window-webkiturl-create地址:https://stackoverflow.com/questions/11277677/how-to-choose-between-window-url-createobjecturl-and-window-webkiturl-creat4、插件优化项为了避免大的视频通过 createObjectURL 方式进行预览会造成大量的内存占用,是否提供一套智能的回收方案,比如在特定时机点调用 revokeObjectURL 方法。源码中 player.config.preview.uploadEl.appendChild(preview) 只是保证对象是否存在,并没有保证传入的对象一定是 DOM 元素。判断是否为 DOM 元素,可以使用以下的方法来判断:function isElement(obj) {
try {
//Using W3 DOM2 (works for FF, Opera and Chrome)
return obj instanceof HTMLElement;
}
catch(e){
//Browsers not supporting W3 DOM2 don't have HTMLElement and
//an exception is thrown and we end up here. Testing some
//properties that all elements have (works on IE7)
return (typeof obj==="object") &&
(obj.nodeType === 1) && (typeof obj.style === "object") &&
(typeof obj.ownerDocument ==="object");
}
}Update 2//Returns true if it is a DOM node
function isNode(o){
return (
typeof Node === "object" ? o instanceof Node :
o && typeof o === "object" && typeof o.nodeType === "number" && typeof
o.nodeName==="string"
);
}
//Returns true if it is a DOM element
function isElement(o){
return (
typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof
o.nodeName==="string"
);
}参考资源 —— how-do-you-check-if-a-javascript-object-is-a-dom-object地址:https://stackoverflow.com/questions/384286/how-do-you-check-if-a-javascript-object-is-a-dom-object3.2 播放器截图播放器内部截图,截图格式可以自定义。源码地址:https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/control/screenShot.js截图功能支持用户对当前视频播放窗口进行即时截屏,截图尺寸即为当前视频播放窗口的尺寸,截图默认为 .png 格式。使用方式new Player({
el:document.querySelector('#mse'),
url: 'video_url',
screenShot: true
})配置项:screenShot默认值:false参考值:true | false源码实现let screenShot = function () {
let player = this
let util = Player.util
if (!player.config.screenShot) {
return
}
let btn = util.createDom(
'xg-screenShot',
'<p class="name"><span>截图</span></p>',
{tabindex: 11},
'xgplayer-screenShot'
);
let canvas = document.createElement('canvas')
let canvasCtx = canvas.getContext('2d')
let img = new Image()
// 截图尺寸即为当前视频播放窗口的尺寸
canvas.width = this.config.width || 600
canvas.height = this.config.height || 337.5
// 添加截图按钮到播放器控件上
let root = player.controls
root.appendChild(btn)
let array = ['click', 'touchstart']
array.forEach(item => {
btn.addEventListener(item, function (e) {
e.preventDefault()
e.stopPropagation()
img.onload = (function () {
// https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/drawImage
// 例如:CSSImageValue,HTMLImageElement,SVGImageElement,HTMLVideoElement,
// HTMLCanvasElement,ImageBitmap 或者OffscreenCanvas。
canvasCtx.drawImage(player.video, 0, 0, canvas.width, canvas.height)
// https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image
// https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_settings_attributes
// anonymous:对此元素的 CORS 请求将不设置凭据标志。
// use-credentials:对此元素的CORS请求将设置凭证标志;这意味着请求将提供凭据。
// "":设置一个空的值,如 crossorigin 或 crossorigin="",和设置 anonymous 的效果一样。
img.setAttribute('crossOrigin', 'anonymous')
img.src = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream')
let screenShotImg = img.src.replace(/^data:image\/[^;]+/,
'data:application/octet-stream');
saveScreenShot(screenShotImg, '截图.png')
})()
})
})
}
Player.install('screenShot', screenShot)原理分析播放器截图功能主要利用 CanvasRenderingContext2D.drawImage() API 来实现。Canvas 2D API 中的 CanvasRenderingContext2D.drawImage() 方法提供了多种方式在 Canvas 上绘制图像。drawImage API 的语法如下:void ctx.drawImage(image, dx, dy);void ctx.drawImage(image, dx, dy, dWidth, dHeight);void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);其中 image 参数表示绘制到上下文的元素。允许任何的 canvas 图像源(CanvasImageSource),例如:CSSImageValue,HTMLImageElement,SVGImageElement,HTMLVideoElement,HTMLCanvasElement,ImageBitmap 或者 OffscreenCanvas。创建截屏按钮并添加到播放器控制条上创建 Canvas 元素和一个 Image 实例为截屏按钮绑定事件监听,如 click 和 touchstart 事件用户点击截屏时,触发对应的回调函数并通过 drawImage() 获取当前视频帧扩展分析1、Image 元素的 crossOrigin 属性在 HTML5 中,一些 HTML 元素提供了对 CORS 的支持, 例如 audio、image、link、script 和 video 均有一个跨域属性(crossOrigin property),它允许你配置元素获取数据的 CORS 请求。默认情况下(即未指定 crossOrigin 属性时),CORS 根本不会使用。如 Terminology section of the CORS specification 中的描述,在非同源情况下,设置 "anonymous" 关键字将不会通过 cookies,客户端 SSL 证书或 HTTP 认证交换用户凭据。即使是无效的关键字和空字符串也会被当作 anonymous 关键字使用。参考资源 —— CORS_settings_attributes地址:https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_settings_attributes2、HTMLCanvasElement.toDataURL()HTMLCanvasElement.toDataURL() 方法返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为 96dpi。如果画布的高度或宽度是 0,那么会返回字符串 "data:,。"如果传入的类型非 "image/png",但是返回的值以 "data:image/png" 开头,那么该传入的类型是不支持的。Chrome 支持 "image/webp" 类型。HTMLCanvasElement.toDataURL() 语法如下:canvas.toDataURL(type, encoderOptions);type 可选,图片格式,默认为 image/pngencoderOptions 可选在指定图片格式为 image/jpeg 或image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。const canvas = document.getElementById("canvas");
const dataURL = canvas.toDataURL();
console.log(dataURL);
// "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby
// blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"3、前端文件下载方案一:let saveScreenShot = function (data, filename) {
let saveLink = document.createElement('a')
saveLink.href = data
saveLink.download = filename
let event = document.createEvent('MouseEvents')
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false,
false, false, 0, null)
saveLink.dispatchEvent(event)
}方案二:const a = document.createElement('a');
const url = window.URL.createObjectURL(blob);
const filename = 'what-you-want.txt';
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);DataURL 与 createObjectURL 方式的区别,可以阅读本地预览功能 > 扩展分析部分。参考资源 —— 这应该是你见过的最全前端下载总结地址:https://zhuanlan.zhihu.com/p/552243974、自定义事件let event = document.createEvent('MouseEvents')
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false,
false, false, 0, null)
saveLink.dispatchEvent(event)5、插件优化项let array = ['click', 'touchstart'] 变量命名规范化,建议 array 命名为 events。saveScreenShot(screenShotImg, '截图.png') 插件支持定义图片的格式和截图的名称,截图名称可以考虑使用 时间戳 + 固定后缀 的格式。在播放器卸载的时候,执行相关的清理操作。比如回收 let img = new Image() 插件内创建的 Image 元素并解除该元素上已绑定的事件。3.3 下载功能播放器下载控件,可以自定义。源码地址:https://github.com/bytedance/xgplayer/blob/dev/packages/xgplayer/src/control/download.js使用方式视频下载控件,点击后下载视频。let player = new Player({
download: true //设置download控件显示
});配置项:download默认值:false参考值:true | false源码实现import downloadUtil from "downloadjs";
const download = function() {
const player = this;
if (!this.config.download) {
return;
}
let container = player.root;
let util = Player.util;
// util.createDom = function (el = 'div', tpl = '', attrs = {}, cname = '')
let downloadEl = util.createDom(
"xgplayer-download",
`<xg-icon class="xgplayer-download-img"></xg-icon>`,
{},
"xgplayer-download"
);
let root = player.controls;
root.appendChild(downloadEl);
let tipsDownload =
player.config.lang && player.config.lang === "zh-cn" ? "下载" : "Download";
// util.createDom = function (el = 'div', tpl = '', attrs = {}, cname = '')
let tips = util.createDom("xg-tips", tipsDownload, {}, "xgplayer-tips");
downloadEl.appendChild(tips);
player.download = function() {
const url = getAbsoluteURL(player.config.url);
downloadUtil(url);
};
downloadEl.addEventListener("click", e => {
e.stopPropagation();
// must pass an absolute url for download
player.download();
});
downloadEl.addEventListener("mouseenter", e => {
e.preventDefault();
e.stopPropagation();
tips.style.left = "50%";
let rect = tips.getBoundingClientRect();
let rootRect = container.getBoundingClientRect();
if (rect.right > rootRect.right) {
tips.style.left = `${-rect.right + rootRect.right + 16}px`;
}
});
};
Player.install("download", download);原理分析下载功能主要是通过动态创建下载按钮并为该按钮绑定 click 事件,当用户触发 click 事件时,在相应的回调函数中使用 downloadjs 这个库实现下载功能。扩展分析1、获取绝对路径export const getAbsoluteURL = function (url) {
// Check if absolute URL
if (!url.match(/^https?:\/\//)) {
const div = document.createElement('div')
div.innerHTML = `<a href="${url}">x</a>`
url = div.firstChild.href
}
return url
}参考资源 —— 前端下载文件的6种方法的对比地址:https://juejin.im/post/5e50fa23518825494b3cccd73.4 播放器贴图播放器贴图,不建议改动。源码地址:https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/control/poster.js使用方式封面图是当播放器初始化后在用户点击播放按钮前显示的图像。new Player({
el:document.querySelector('#mse'),
url: 'video_url',
poster: '//abc.com/**/*.png'
});配置项:poster默认值:''参考值:'https://i.ytimg.com/vi/lK2ZbbQSHww/hqdefault.jpg'源码实现let poster = function() {
let player = this;
let util = Player.util;
// util.createDom = function (el = 'div', tpl = '', attrs = {}, cname = '')
let poster = util.createDom("xg-poster", "", {}, "xgplayer-poster");
let root = player.root;
// 是否使用默认的封面图
if (player.config.poster) {
poster.style.backgroundImage = `url(${player.config.poster})`;
root.appendChild(poster);
}
// 监听播放事件,播放时隐藏封面图
function playFunc() {
poster.style.display = "none";
}
player.on("play", playFunc);
// 监听销毁事件,执行清理操作
function destroyFunc() {
player.off("play", playFunc);
player.off("destroy", destroyFunc);
}
player.once("destroy", destroyFunc);
};
Player.install("poster", poster);原理分析xgplayer 的插件基于 EventEmitter 事件系统,通过监听播放器的 play 事件来隐藏 poster 海报。此外通过监听播放器的 destory 事件来实现清理操作,比如移除 play 事件的监听器和 destroy 事件。扩展分析1、poster.style.backgroundImage = url(${player.config.poster}) 未判断是否为有效 URL 地址考虑到代码的健壮性,最好判断一下 player.config.poster 的属性值是否为合法的 URL 地址或 DataURL。另外为了避免用户设置的图片出现问题,导致页面出现异常,可以考虑降级处理,即设置 Fallback 图片。div {
background-image: url('http://placehold.it/1000×1000'),
url('http://placehold.it/500×500');
background-repeat:no-repeat;
background-size: 100%;
height:200px;
width:200px;
}参考资源 —— fallback-background-image-if-default-doesnt-exist地址:https://stackoverflow.com/questions/37588017/fallback-background-image-if-default-doesnt-exist3.5 画中画播放器画中画功能,不建议改动。源码地址:https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/control/pip.js画中画功能支持用户在浏览网页其它内容时继续以小窗的形式观看视频,同时可以拖拽改变小窗在页面中的 fix 位置。使用方式new Player({
el:document.querySelector('#mse'),
url: 'video_url',
pip: true
});配置项:pip默认值:false参考值:true | false本篇未完结,请见下一篇推荐JavaScript学习相关文章《细品西瓜播放器功能分析(上)「实践」》《细品西瓜播放器功能分析(下)「实践」》《webpack4主流程源码解说以及动手实现一个简单的webpack(上)》《webpack4主流程源码解说以及动手实现一个简单的webpack(下)》《细聊前端架构师的视野》《细聊应用场景再谈防抖和节流「进阶篇」》《前端埋点统一接入方案实践》《细聊微内核架构在前端的应用「干货」》《一种高性能的Tree组件实现方案「干货」》《进击的JAMStack》《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)上》《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)中》《前后端全部用 JS 开发是什么体验(Hybrid + Egg.js经验分享)下》《一文带你搞懂 babel-plugin-import 插件(上)「源码解析」》《一文带你搞懂 babel-plugin-import 插件(下)「源码解析」》《JavaScript常用API合集汇总「值得收藏」》《推荐10个常用的图片处理小帮手(上)「值得收藏」》《推荐10个常用的图片处理小帮手(下)「值得收藏」》《JavaScript 中ES6代理的实际用例》《12 个实用的前端开发技巧总结》《一文带你搞懂搭建企业级的 npm 私有仓库》《教你如何使用内联框架元素 IFrames 的沙箱属性提高安全性?》《细说前端开发UI公共组件的新认识「实践」》《细说DOM API中append和appendChild的三个不同点》《细品淘系大佬讲前端新人如何上王者「干货」》《一文带你彻底解决背景跟随弹窗滚动问题「干货」》《推荐常用的5款代码比较工具「值得收藏」》《Node.js实现将文字与图片合成技巧》《爱奇艺云剪辑Web端的技术实现》《我再也不敢说我会写前端 Button组件「实践」》《NodeX Component – 滴滴集团 Node.js 生态组件体系「实践」》《Node Buffers 完整指南》《推荐18个webpack精美插件「干货」》《前端开发需要了解常用7种JavaScript设计模式》《浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务》《了不起的 Webpack HMR 学习指南(上)「含源码讲解」》《了不起的 Webpack HMR 学习指南(下)「含源码讲解」》《10个打开了我新世界大门的 WebAPI(上)「实践」》《10个打开了我新世界大门的 WebAPI(中)「实践」》《10个打开了我新世界大门的 WebAPI(下)「实践」》《「图文」ESLint 在中大型团队的应用实践》《Deno是代码的浏览器,你认同吗?》《前端存储除了 localStorage 还有啥?》《Javascript 多线程编程的前世今生》《微前端方案 qiankun(实践及总结)》《「图文」V8 垃圾回收原来这么简单?》《Webpack 5模块联邦引发微前端的革命?》《基于 Web 端的人脸识别身份验证「实践」》《「前端进阶」高性能渲染十万条数据(时间分片)》《「前端进阶」高性能渲染十万条数据(虚拟列表)》《图解 Promise 实现原理(一):基础实现》《图解 Promise 实现原理(二):Promise 链式调用》《图解 Promise 实现原理(三):Promise 原型方法实现》《图解 Promise 实现原理(四):Promise 静态方法实现》《实践教你从零构建前端 Lint 工作流「干货」》《高性能多级多选级联组件开发「JS篇」》《深入浅出讲解Node.js CLI 工具最佳实战》《延迟加载图像以提高Web网站性能的五种方法「实践」》《比较 JavaScript 对象的四种方式「实践」》《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》《前端如何一次性处理10万条数据「进阶篇」》《推荐三款正则可视化工具「JS篇」》《如何让用户选择是否离开当前页面?「JS篇」》《JavaScript开发人员更喜欢Deno的五大原因》《仅用18行JavaScript实现一个倒数计时器》《图文细说JavaScript 的运行机制》《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》《推荐Web程序员常用的15个源代码编辑器》《10个实用的JS技巧「值得收藏」》《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》《深入JavaScript教你内存泄漏如何防范》《手把手教你7个有趣的JavaScript 项目-上「附源码」》《手把手教你7个有趣的JavaScript 项目-下「附源码」》《JavaScript 使用 mediaDevices API 访问摄像头自拍》《手把手教你前端代码如何做错误上报「JS篇」》《一文让你彻底搞懂移动前端和Web 前端区别在哪里》《63个JavaScript 正则大礼包「值得收藏」》《提高你的 JavaScript 技能10 个问答题》《JavaScript图表库的5个首选》《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》《可视化的 JS:动态图演示 – 事件循环 Event Loop的过程》《教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」》《可视化的 js:动态图演示 Promises & Async/Await 的过程》《原生JS封装拖动验证滑块你会吗?「实践」》《如何实现高性能的在线 PDF 预览》《细说使用字体库加密数据-仿58同城》《Node.js要完了吗?》《Pug 3.0.0正式发布,不再支持 Node.js 6/8》《纯JS手写轮播图(代码逻辑清晰,通俗易懂)》《JavaScript 20 年 中文版之创立标准》《值得收藏的前端常用60余种工具方法「JS篇」》《箭头函数和常规函数之间的 5 个区别》《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》《「前端篇」不再为正则烦恼》《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》《深入细品浏览器原理「流程图」》《JavaScript 已进入第三个时代,未来将何去何从?》《前端上传前预览文件 image、text、json、video、audio「实践」》《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》《推荐13个有用的JavaScript数组技巧「值得收藏」》《前端必备基础知识:window.location 详解》《不要再依赖CommonJS了》《犀牛书作者:最该忘记的JavaScript特性》《36个工作中常用的JavaScript函数片段「值得收藏」》《Node + H5 实现大文件分片上传、断点续传》《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》《【实践总结】关于小程序挣脱枷锁实现批量上传》《手把手教你前端的各种文件上传攻略和大文件断点续传》《字节跳动面试官:请你实现一个大文件上传和断点续传》《谈谈前端关于文件上传下载那些事【实践】》《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》《最全的 JavaScript 模块化方案和工具》《「前端进阶」JS中的内存管理》《JavaScript正则深入以及10个非常有意思的正则实战》《前端面试者经常忽视的一道JavaScript 面试题》《一行JS代码实现一个简单的模板字符串替换「实践」》《JS代码是如何被压缩的「前端高级进阶」》《前端开发规范:命名规范、html规范、css规范、js规范》《【规范篇】前端团队代码规范最佳实践》《100个原生JavaScript代码片段知识点详细汇总【实践】》《关于前端174道 JavaScript知识点汇总(一)》《关于前端174道 JavaScript知识点汇总(二)》《关于前端174道 JavaScript知识点汇总(三)》《几个非常有意思的javascript知识点总结【实践】》《都2020年了,你还不会JavaScript 装饰器?》《JavaScript实现图片合成下载》《70个JavaScript知识点详细总结(上)【实践】》《70个JavaScript知识点详细总结(下)【实践】》《开源了一个 JavaScript 版敏感词过滤库》《送你 43 道 JavaScript 面试题》《3个很棒的小众JavaScript库,你值得拥有》《手把手教你深入巩固JavaScript知识体系【思维导图】》《推荐7个很棒的JavaScript产品步骤引导库》《Echa哥教你彻底弄懂 JavaScript 执行机制》《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》《深入解析高频项目中运用到的知识点汇总【JS篇】》《JavaScript 工具函数大全【新】》《从JavaScript中看设计模式(总结)》《身份证号码的正则表达式及验证详解(JavaScript,Regex)》《浏览器中实现JavaScript计时器的4种创新方式》《Three.js 动效方案》《手把手教你常用的59个JS类方法》《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》《深入浅出讲解 js 深拷贝 vs 浅拷贝》《手把手教你JS开发H5游戏【消灭星星】》《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》《手把手教你全方位解读JS中this真正含义【实践】》《书到用时方恨少,一大波JS开发工具函数来了》《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》《手把手教你JS 异步编程六种方案【实践】》《让你减少加班的15条高效JS技巧知识点汇总【实践】》《手把手教你JS开发H5游戏【黄金矿工】》《手把手教你JS实现监控浏览器上下左右滚动》《JS 经典实例知识点整理汇总【实践】》《2.6万字JS干货分享,带你领略前端魅力【基础篇】》《2.6万字JS干货分享,带你领略前端魅力【实践篇】》《简单几步让你的 JS 写得更漂亮》《恭喜你获得治疗JS this的详细药方》《谈谈前端关于文件上传下载那些事【实践】》《面试中教你绕过关于 JavaScript 作用域的 5 个坑》《Jquery插件(常用的插件库)》《【JS】如何防止重复发送ajax请求》《JavaScript+Canvas实现自定义画板》《Continuation 在 JS 中的应用「前端篇」》作者:字节跳动转发链接:https://www.yuque.com/docs/share/a86a12a1-77c4-4f78-854b-af185f90bec4
本文出自快速备案,转载时请注明出处及相应链接。