文章收集
onlymisaky / favorites Goto Github PK
View Code? Open in Web Editor NEW或许真的老了,记不住知识了
Home Page: https://onlymisaky.github.io/favorites/demo/
或许真的老了,记不住知识了
Home Page: https://onlymisaky.github.io/favorites/demo/
相关链接
script | 下载方式 | 执行时机 | 是否阻塞解析html | DOMContentLoaded |
---|---|---|---|---|
<script> | 并行 | 下载完立即依次执行 | 下载和执行都会阻塞解析 | 等待 |
<script async> | 并行 | 下载完立即执行,顺序不可控 | 下载不会,解析会 | 不等待 |
<script defer> | 并行 | 下载完所有,html解析完成,依次执行 | 都不会 | 等待 |
<script type="module"> | 并行 | |||
<script async type="module"> | 并行 |
<p>...content before script...</p>
<script async src="https://javascript.info/article/script-async-defer/long.js?speed=0"></script>
<script src="https://javascript.info/article/script-async-defer/small.js?speed=0"></script>
<p>...content after script...</p>
相关链接
script 解释
相关链接
文件上传主要分为两步骤:
<input type="file" />
<input type="file" accept="image/*" />
<input type="file" accept="image/*" multiple />
<input type="file" accept="image/*" webkitdirectory />
主要是利用了 drop
事件获取事件对象中的 dataTransfer
拿到拖拽的文件
<div id="drop" style="width: 200px; height: 200px; background-color: skyblue;">
拖拽文件到此处
</div>
const dropEle = document.querySelector('#drop');
['dragenter', 'dragover', 'drop', 'dragleave'].forEach((eventName) => {
[document.body, dropEle].forEach((ele) => {
ele.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
}, false)
})
});
dropEle.addEventListener('drop', (e) => {
console.log(e.dataTransfer.files);
})
监听 paste
事件,获取剪贴板内容,可以通过 navigator.clipboard
获取,但该 api 貌似只能获取剪贴板中的图片,对于其它类型的文件,会报错;也可以通过 e.clipboardData.items
获取剪贴板中的内容,需要判断 kind
是否为 file
。
ele.addEventListener('paste', async (e) => {
e.preventDefault();
e.stopPropagation();
if (navigator.clipboard) {
const clipboardItems = await navigator.clipboard.read().catch((err) => {
console.log(err);
return []
})
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
}
}
} else {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
const blob = items[i].getAsFile();
}
}
})
showOpenFilePicker().then(async (res) => {
for (const fileSystemFileHandle of res) {
if (fileSystemFileHandle.kind === 'file') {
const blob = await fileSystemFileHandle.getFile();
}
}
})
对于体积较小的媒体或文本类型文件,可以通过 FileReader
读取内容,然后用相应的方式展示,如果体积
较大,可以考虑分段读取加载。
文件上传一般使用 multipart/form-data
和 application/octet-stream
这两种格式上传请求体。
multipart/form-data
需要通过 boundary
来分割请求头,请求体
Content-Type: multipart/form-data; boundary=aaa
--aaa
Content-Disposition: form-data; name="text"
Content-Type: text/plain;charset=UTF-8
title
--aaa
Content-Disposition: form-data; name="file"; filename="image.png"
Content-Type: image/png
二进制数据
--aaa--
上传无非也两种方式:
当 method
属性值为 post
时,enctype
就是将表单的内容提交给服务器的 MIME 类型 。可能的取值有:
application/x-www-form-urlencoded
:未指定属性时的默认值。multipart/form-data
:当表单包含 type=file 的 元素时使用此值。text/plain
:出现于 HTML5,用于调试。这个值可被 、 或 元素上的 formenctype 属性覆盖。<form action="/upload" method="post" enctype="multipart/form-data">
文件1:<input type="file" name="file1"><br>
文件2:<input type="file" name="file2"><br>
<input type="submit" value="提交">
</form>
const xhr = new XMLHttpRequest();
xhr.addEventListener('readystatechange', (e) => {
if (xhr.readyState === xhr.DONE) {
console.log(xhr.responseText);
}
})
xhr.upload.addEventListener('progress', (e) => {
const percent = Math.floor((e.loaded / e.total) * 100);
console.log('上传进度:', percent, '%');
})
xhr.open('POST', '/upload');
xhr.setRequestHeader('Content-Type', 'application/octet-stream')
const fd = new FormData();
fd.append('file', file);
xhr.send(fd);
相较于在浏览器中做大文件切割下载,大文件切割上传的意义还是不叫重要的,其中最大的一个好处就是,上传失败后,用户不需要重头开始重新上传
function file2Chunks(file, chunkSize) {
const chunks = [];
for (let start = 0; start < file.size; start += chunkSize) {
chunks.push(file.slice(start, start + chunkSize));
}
return chunks;
}
hash
hash
mciro-app
npm i @micro-zoe/micro-app --save
micro-app
import microApp from '@micro-zoe/micro-app'
microApp.start()
<template>
<micro-app name='my-app' url='http://localhost:8081he'/'></micro-app>
</template>
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
}
}
}
const app = new Vue();
window.unmount = () => {
app.$destroy()
}
data
属性设置,这些数据通常是固定的、简单的,当然要注意做兼容性和错误处理。
<!-- 主应用 -->
<template>
<micro-app name="app1" url="http://localhost:8081" :data="data"></micro-app>
</template>
<script>
export default {
data() {
return {
count: 1
}
}
}
</script>
// 子应用
const data = window.microApp.getData()
setData
由主应用向子应用发送数据
// 主应用
import microApp from '@micro-zoe/micro-app'
microApp.setData('my-app', { count: 1 })
microApp.setData('my-app', { unit: 'g' })
microApp.setData('my-app', { count: 2 }, (...args) => {
console.log('发送完成,子应用监听函数的返回值为:'...args)
})
// 子应用
window.microApp.addDataListener((data) => {
console.log('来自主应用的数据:', data)
retrun 'this is app1, i have successfully acquired the data'
})
setData
是异步的,多个setData
会合并为一次。 microApp.clearData('app1')
清空主应用发送给子应用的数据。destroy
或 clear-data
属性在子应用卸载时清空数据// 子应用
window.microApp.dispatch({count: 1})
window.microApp.dispatch({unit: 'g'})
window.microApp.dispatch({count: 2}, (...args) => {
console.log('发送完成,主应用监听函数的返回值为:'...args)
})
//主应用
import microApp from '@micro-zoe/micro-app'
microApp('app1', (data) => {
console.log('来自子应用 app1 的数据:', data)
retrun 'this is base app, i have successfully acquired the data'
})
optimization.runtimeChunk = 'single'
导致headers
、credentials
、methods
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'appId,clientId', // 根据接口所用到的headers来设置
'Access-Control-Allow-Credentials': 'ture',
'Access-Control-Allow-Methods': '*',
}
}
}
function genServerHeaders(...args) {
const [IncomingMessage] = agrs;
const headers = IncomingMessage.rawHeaders.reduce((prev, curr, index) => {
if (index % 2 === 0) {
return prev + ', ' + curr;
}
return prev;
});
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': headers,
'Access-Control-Allow-Credentials': 'ture',
'Access-Control-Allow-Methods': '*',
}
}
module.exports = {
devServer: {
headers: genServerHeaders
}
}
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
},
bypass: (req, res) => {
if (req.method && req.method.toLowerCase() === 'options') {
res.status(200).send()
}
}
}
}
}
}
function genProxyTable(proxyTable) {
const bypassFactory = (perfix) => {
return (req, res) => {
if (req.method && req.method.toLowerCase() === 'options') {
res.status(200).send()
}
}
}
return Object.keys(proxyTable).reduce((prev, current) => {
return {
...prev,
[current]: {
...proxyTable[current],
bypass: bypassFactory(current)
}
}
}, {})
}
module.exports = {
devServer: {
proxy: genProxyTable(proxyTable)
}
}
chunk
会高度相似甚至完全相同。此时若通过主应用访问子应用,可能会导致子应用运行时,访问属性会访问到主应用的属性。设置子应用的webpack.jsonpFunction
即可
module.exports = {
output: {
// webpack 4
jsonpFunction: 'webpackJsonp_child1',
// webpack 5
chunkLoadingGlobal: 'webpackJsonp_special_subject',
globalObject: 'window'
}
}
let app = null;
window.mount = () => {
app = new Vue();
if (!window.__MICRO_APP_ENVIRONMENT__) {
app.$mount('#app');
} else {
app.$mount();
document.querySelector('#app').appendChild(app.$el);
}
}
webpack
的Module Federation
代码共享
store
this.$xx
等等这些需要依赖先实例化才能使用的方法。// vue.config.js
module.exports = {
/* 其他配置 */
publicPath: 'auto',
devServer: {
/* 其他配置 */
headers: {
'Access-Control-Allow-Origin': '*'
}
},
chainWebpack: config => {
if (process.env.NODE_ENV === 'development') {
config.optimization.delete('splitChunks')
}
config
.plugin('module-federation-plugin')
.use(require('webpack').container.ModuleFederationPlugin, [{
name: 'remote',
filename: 'remote_entry.js',
library: { type: 'window', name: 'remote' },
exposes: {
'./Xxx': './src/Xxx.vue'
}
}])
}
}
// vue.config.js
module.exports = {
/* 其他配置 */
publicPath: 'auto',
chainWebpack: config => {
config
.plugin('module-federation-plugin')
.use(require('webpack').container.ModuleFederationPlugin, [{
remotes: {
remote: `remote@${process.env.REMOTE_MODULE}remote_entry.js`
}
}])
}
}
// src/public-path.js
// __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
// src/mian.js 构建的入口文件
import './public-path';
import('./bootstrap')
// src/bootstrap.js
// 将原 src/mian.js 文件内内容复制到此处即可
// 远程模块具使用方式
// import Xxx from 'src/Xxx.vue';
import Xxx from 'remote/Xxx';
console.log(Xxx)
Vuex
的插件来完成
// 主应用
import microApp from '@micro-zoe/micro-app'
const myPlugin = store => {
store.subscribe((mutation, state) => {
if (['xxx','yyy'].includes(mutation.type)) {
microApp.setData('app1', mutation)
}
})
}
axios
的拦截器中操作
// 需要共享数据的接口
export const apiSuffixMap = {
'auth/userInfo': {
method: 'get',
defaultValue: {},
// 如果有对数据校验的需求,也可以在此处添加回调函数
}
}
export const apiList = Object.key(apiSuffixMap).map((suffix) => {
// 这里的做法是将需要共享的数据专门存在一个 store 中
const mutation = `micro/SET_${toUpperCaseSnakeCase(suffix)}`
const stateKey = toCamelCase(suffix)
return {
suffix,
method: apiSuffixMap[suffix].method,
mutation,
getData(baseAppData) {
if (baseAppData && baseAppData.store && baseAppData.store[stateKey]) {
return baseAppData.store[stateKey]
}
return new Error(stateKey)
}
}
})
// 主应用 store
const micro = {
namespaced: true,
state: apiList.reduce((prev, current) => {
return {
[toCamelCase(current.suffix)]: current.defaultValue,
...prev
}
}, {}),
mutations: apiList.reduce((prev, current) => {
return {
[`SET_${toUpperCaseSnakeCase(current.suffix)}`] (state, payload) {
state[toCamelCase(current.suffix)] = payload
// state[toCamelCase(current.url)] = Object.freeze(JSON.parse(JSON.stringify(payload)))
},
...prev
}
}, {})
}
// 主应用拦截器
axios.interceptors.response.use((response) => {
const url = response.config.url.split('?')[0]
const api = apiList.find((item) => url.endsWith(item.suffix) && response.config.method.toLowerCase() === item.method)
if (api) {
store.commit(api.mutation, response.data)
// 也可以直接通过micro-app发送数据
}
})
<!-- 主应用 -->
<template>
<micro-app name="app1" url="http://localhost:8081" :data="data"></micro-app>
</template>
<script>
export default {
computed: {
data() {
return {
sharedApiList: apiList,
store: this.$store.state.micro
}
}
}
}
</script>
// 子应用拦截器
const microSymbol = Symbol('has loaded data from base')
function getParentAppData () {
if (window.__MICRO_APP_ENVIRONMENT__) {
const parentAppData = window.microApp.getData()
return parentAppData
}
}
axios.interceptors.request.use((config) => {
const baseAppData = getParentAppData()
if (baseAppData && Array.isArray(baseAppData.sharedApiList) && baseAppData.sharedApiList.length) {
const url = config.url.split('?')[0]
const api = baseAppData.sharedApiList.find(item => url.endsWith(item.suffix) && config.method.toLowerCase() === item.method)
if (api) {
return Promise.reject({ message: microSymbol, ...api })
}
}
})
axios.interceptors.response.use((response) => response, (error) => {
if (error && error.message === microSymbol) {
const baseAppData = getParentAppData()
const data = error.getData(baseAppData)
if (Object.prototype.toString.call(data).slice(8, -1) !== 'Error') {
return Promise.resolve(data)
}
}
})
相关文章
.half-border {
height: 1px;
transform: scaleY(0.5);
/* 防止在 chrome 中线变虚 */
transform-origin: 50% 100%;
}
.half-border {
height: 1px;
background: linear-gradient(0deg, #fff, #000);
}
.half-border {
height: 1px;
background: none;
box-shadow: 0 0.5px 0 #000;
}
.hr.svg {
background: none;
height: 1px;
background: url("data:image/svg+xml;utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='1px'><line x1='0' y1='0' x2='100%' y2='0' stroke='#000'></line></svg>");
/* 兼容 Firefox */
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMDAlJyBoZWlnaHQ9JzFweCc+PGxpbmUgeDE9JzAnIHkxPScwJyB4Mj0nMTAwJScgeTI9JzAnIHN0cm9rZT0nIzAwMCc+PC9saW5lPjwvc3ZnPg==");
}
<meta name="viewport" content="width=device-width,initial-sacle=0.5">
相关文章
plugin(name:string, handler:function)
注册插件到 Tapable 对象中apply(…pluginInstances: (AnyPlugin|function)[])
调用插件的定义,将事件监听器注册到 Tapable 实例注册表中applyPlugins*(name:string, …)
多种策略细致地控制事件的触发,包括applyPluginsAsync、applyPluginsParallel等方法实现对事件触发的控制,实现
function CustomPlugin() { }
CustomPlugin.prototype.apply = function (compiler) {
compiler.plugin('emit', pluginFunction)
}
this.apply*('emit',options)
钩子名称 | 执行方式 | 要点 |
---|---|---|
SyncHook | 同步串行 | 不关心监听函数的返回值 |
SyncBailHook | 同步串行 | 只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑 |
SyncWaterfallHook | 同步串行 | 上一个监听函数的返回值可以传给下一个监听函数 |
SyncLoopHook | 同步循环 | 返回true反复执行,否则退出循环 |
AsyncParallelHook | 异步并行 | 不关心监听函数的返回值 |
AsyncParallelBailHook | 异步并行 | 只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数 |
AsyncSeriesHook | 异步串行 | 不关心callback()的参数 |
AsyncSeriesBailHook | 异步串行 | callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数 |
AsyncSeriesWaterfallHook | 异步串行 | 上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数 |
const sync = new SyncHook(['id'])
sync.tap('SyncPlugin', (id) => { console.log('SyncHookPlugin', id) })
sync.call(996)
const async = new AsyncParallelHook()
async.tapAsync('AsyncPlugin', () => {
return new Promise((resolve) => {
console.log('AsyncPlugin')
resolve('hello')
})
})
async.promise().then((res) => {
console.log(res)
})
const webpack = (options, callback) => {
// ...
// 验证options正确性
// 预处理options
options = new WebpackOptionsDefaulter().process(options) // webpack4的默认配置
compiler = new Compiler(options.context) // 实例Compiler
// ...
// 若options.watch === true && callback 则开启watch线程
compiler.watch(watchOptions, callback)
compiler.run(callback)
return compiler
}
相关链接
fetch('/xxx')
.then((res) => {
let filename = res.headers['content-disposition'];
if (filename) {
filename = filename.substring(filename.indexOf('filename=') + 9);
filename = decodeURI(escape(filename));
}
filename = filename || 'test.png';
return Promise.all([filename, res.blob()])
})
.then(([filename, blob]) => {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
})
showSaveFilePicker({
suggestedName: 'test.png',
types: [
{ description: 'PNG file', accept: { 'image/png': ['.png'], }, },
{ description: 'Jpeg file', accept: { 'image/jpeg': ['.jpeg'], }, },
],
})
.then((handle) => handle.createWritable())
.then((writable) => {
return fetch('/xxx')
.then((res) => res.blob())
.then((blob) => writable.write(blob))
.then(() => writable.close());
saveAs(blob, 'test.png')
saveAs(file)
saveAs('/xxx', 'test.png');
Promise.all([fetch('/xxx'),fetch('/xxx'),])
.then((res) => Promise.all(res.map((item) => item.blob())))
.then((blobs) => {
const zip = new JSZip()
blobs.forEach((blob, index) => {
zip.file(`${index}.png`, blob)
})
return zip.generateAsync({ type: 'blob' })
})
.then((blob) => {
saveAs(blob, 'test.zip')
})
其实这一部分主要还是靠后端,在响应头中配置 Transfer-Encoding
,或者服务端返回一个流
Transfer-Encoding: chunked
Transfer-Encoding: gzip, chunked
以下代码只是展示如果通过 fetch
读取流
fetch('/xxx')
.then((res) => {
const arr = [];
const reader = res.body.getReader();
return readChunk(reader, arr);
})
.then((res) => {
const blob = new Blob(res, { type: 'image/png' })
saveAs(blob, 'test.png')
})
function readChunk(reader, chunkArr = []) {
return reader.read().then(({ value, done }) => {
if (done) {
return chunkArr;
}
chunkArr.push(value);
return readChunk(reader, chunkArr);
});
}
如果在响应中存在 Accept-Ranges
首部(并且它的值不为 “none”),那么表示该服务器支持范围请求。
fetch('/xxx', { method: 'head' })
.then((res) => {
const length = res.headers.get('Content-Length');
return fetch('/xxx', {
headers: { range: `bytes=0-${Math.floor(length / 2)}` }
})
})
.then((res) => res.blob())
.then((blob) => { saveAs(blob, 'test.png') })
head
请求判断文件是否支持范围请求,获取文件大小const links = document.querySelectorAll('a[data-thunder]');
links.forEach((link) => {
const base64 = btoa(`AA${link.href}ZZ`)
link.href = `thunder://${base64}`
})
<a download="test.pdf" href="test.pdf">
触发下载Content-Disposition: attachment; filename="test.pdf"
触发浏览器下载ajax
请求下载文件,获取 blob
,利用 BlobURL
下载文件由于 BlobURL
会先文件保存再内存中,所以对于比较大的文件,还是建议通过 1,2 两种方式来实现。但再实际开发中,下载文件也需要带上鉴权信息,所以我们以用 token
换临时 cookie
等方式
相关链接
const head = document.head;
const body = document.body;
const forms = document.forms;
const activeElement = document.activeElement;
document.createDocumentFragment();
由于文档片段只是创建在内存中,并不在 DOM 树中,通常会使用它来进行批量操作元素,避免重复引起页面回流,从而提升性能。
<ul id="list"></ul>
const fragment = document.createDocumentFragment();
[1, 2, 3, 4, 5].forEach(num => {
const li = document.createElement('li');
li.textContent = num;
fragment.appendChild(li);
});
list.appendChild(fragment);
parent.insertBefore(newChild, targetChild);
function insertAfter(newChild, targetChild) {
const parent = targetChild.parentNode;
if (parent.lastChild === targetChild) {
parent.appendChild(newChild);
} else {
parent.insertBefore(newChild, targetChild.nextSibling);
}
}
parent.replaceChild(newChild, targetChild);
parent.removeChild(targetChild);
ele.parentNode
:父节点,节点包括 Element 和 Document;ele.parentElement
:父元素,与 parentNode 区别是,其父节点必须是一个 Element 元素。ele.children
: 返回子元素集合,只返回元素节点;ele.childNodes
:返回 Node 节点列表,可能包含文本节点(换行也会转为文本节点)、注释节点。ele.firstChild
:返回第一个子节点(元素、文本、注释),不存在返回 null;ele.lastChild
:返回最后一个子节点(元素、文本、注释),不存在返回 null;ele.firstElementChild
:返回第一个元素节点;ele.lastElementChild
:返回最后一个元素节点;ele.previousSibling
:返回节点的前一个节点(元素、文本、注释);ele.nextSibling
:返回节点的后一个节点(元素、文本、注释);ele.previousElementSibling
:返回节点的前一个元素节点;ele.nextElementSiblng
:返回节点的后一个元素节点。element.attributes;
element.dataset;
delete element.dataset.xxx
article::before {
content: attr(data-xxx);
}
ELEMENT_NODE
元素节点ATTRIBUTE_NODE
属性节点TEXT_NODE
文本节点CDATA_SECTION_NODE
CDATA区段ENTITY_REFERENCE_NODE
实体引用元素ENTITY_NODE
实体PROCESSING_INSTRUCTION_NODE
表示处理指令COMMENT_NODE
注释节点DOCUMENT_NODE
指 documentDOCUMENT_TYPE_NODE
<!DOCTYPE>DOCUMENT_FRAGMENT_NODE
文档碎片节点NOTATION_NODE
DTD中声明的符号节点onmouseover
: 移入事件,移入到目标元素或其子元素时触发;onmouseout
: 移出事件,移除目标元素或其子元素时触发;onmouseenter
: 移入事件,移入目标元素时触发;onmouseleave
: 移出事件,移出目标元素时触发;区别
onmouseover/onmouseout
会在目标元素及其子元素中触发,比如 移入目标元素后再移入到子元素,会依次触发:目标元素 onmouseover(移入) -> 目标元素 onmouseout(移出) -> 子元素 onmouseover(移入) (示例 1)onmouseenter/onmouseleave
移入到目标元素或其子元素时,过程中仅触发一次事件,但在 event.target 属性会返回触发事件的元素或其子元素;(示例 2)<div class="target">
<p class="child"></p>
</div>
示例一:onmouseover/onmouseout
const target = document.querySelector('.target'),
child = document.querySelector('.child');
target.addEventListener('mouseover', event => {
console.log('移入 ', event.target);
});
target.addEventListener('mouseout', event => {
console.log('移出 ', event.target);
});
// 输出:
// 移入 <div class="target">...</div>
// 移出 <div class="target">...</div>
// 移入 <p class="child"></p>
示例二:onmouseenter/onmouseleave
const target = document.querySelector('.target'),
child = document.querySelector('.child');
target.addEventListener('mouseenter', event => {
console.log('移入 ', event.target);
// event.target 属性会返回触发事件的元素或其子元素
// 如果你希望在事件处理程序中获取绑定事件的元素,而不是子元素,你可以使用 event.currentTarget 属性。
// event.currentTarget 属性始终指向绑定事件的元素,而不是触发事件的元素。
// 另外,要避免在 await 语句的下方去使用 event.currentTarget,否则你可能拿到的是 null。
// 这是因为:event.currentTarget 不能在异步代码中获取该信息,只能以同步方式去访问。
});
target.addEventListener('mouseleave', event => {
console.log('移出 ', event.target);
});
// 输出:
// 移入 <div class="target">...</div>
// 移出 <div class="target">...</div>
TODO
TODO
相关链接:
const observer = new PerformanceObserver((list) => {
const map = new Map()
const perfEntries = list.getEntries()
for (const entry of perfEntries) {
if (entry.name === 'first-paint') {
console.log('first-paint', entry.toJSON())
map.has('first-contentful-paint') ? observer.disconnect() : map.set('first-paint')
}
if (entry.name === 'first-contentful-paint') {
console.log('first-contentful-paint', entry.toJSON())
map.has('first-paint') ? observer.disconnect() : map.set('first-contentful-paint')
}
}
})
observer.observe({ type: 'paint', buffered: true })
const observer = new PerformanceObserver((list) => {
const perfEntries = list.getEntries()
console.log(perfEntries)
for (const entry of perfEntries) {
console.log('largest-contentful-paint', entry)
observer.disconnect()
}
})
observer.observe({ type: 'largest-contentful-paint', buffered: true })
performance.timing.domContentLoadedEventEnd - performance.timing.domContentLoadedEventStart
performance.timing.loadEventStart - performance.timing.fetchStart
const observer = new PerformanceObserver((list, observer) => {
const entries = list.getEntries()
for (const entry of entries) {
console.log({
dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS 耗时
tcp: entry.connectEnd - entry.connectStart, // 建立 tcp 连接耗时
redirect: entry.redirectEnd - entry.redirectStart, // 重定向耗时
responseHeaderSize: entry.transferSize - entry.encodedBodySize, // 响应头部大小
...entry.toJSON()
})
}
})
observer.observe({ type: 'resource', buffered: true })
((window) => {
const originalSend = window.XMLHttpRequest.prototype.send
window.XMLHttpRequest.prototype.send = function (...args) {
const startTime = Date.now()
XMLHttpRequest.prototype.addEventListener('loadend', (...args) => {
console.log(
`method: ${this._method}`,
`url: ${this._url}`,
`status: ${this.status}`,
`timing${Date.now() - startTime}ms`
)
})
return originalSend.apply(this, args)
}
const originalFetch = window.fetch
Object.defineProperty(window, 'fetch', {
configurable: true,
enumerable: true,
writable: true,
value: (...args) => {
const startTime = Date.now()
const promise = originalFetch.apply(this, args)
promise.then((res) => {
let url = ''
let method = ''
const [input, init = {}] = args
if (typeof input === 'string') {
url = input
} else {
url = init.url
method = init.method || input.method || 'GET'
}
method = method || 'GET'
console.log(
`method: ${method.toLowerCase()}`,
`url: ${url}`,
`status: ${res.status}`,
`timing${Date.now() - startTime}ms`
)
})
return promise
}
})
})(globalThis || window)
window.addEventListener('error', e => {
const target = e.target
if (!target) return
if (target.src || target.href) {
const url = target.src || target.href
console.log({
url,
startTime: e.timeStamp,
html: target.outerHTML,
resourceType: target.tagName,
paths: e.path.map(item => item.tagName).filter(Boolean)
})
}
})
window.onerror = (msg, url, lineNo, columnNo, error) => { }
window.addEventListener('unhandledrejection', () => {})
上述方式结合起来时候,用一个变量存储需要上报的数据,当缓存达到一定数量后,通过requestIdelCallback/setTimeout延迟上报。在页面离开时统一将未上报的数据进行上报
相关链接:
git commit --amend --only
git commit --amend --only -m 'xxxxx'
git commit --amend --author "xxx [email protected]"
git filter-branch
git checkout HEAD^ filename
git add -A
git commit --amend
git reset HEAD^ --hard
git push -f [remote] [branch]
git reset HEAD^ --soft
git commit --amend
git add --patch fiename
git add -p fiename
git add .
git stash push -m xxx
git push origin --delete branch-name
git push origin :branch-name
git branch -D branch-name
git fsck --unreachable | grep tag
git update-ref refs/tage/<tag_name> <hash>
git config --global credential.helper cache
git config --global credential.helper 'cache --timeout=3600'
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.