Giter VIP home page Giter VIP logo

favorites's Introduction

favorites

文章收集

favorites's People

Contributors

onlymisaky avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

cardsix zhitaoliu

favorites's Issues

script async defer module

相关链接

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>

image

从输入 url 开始能做哪些优化

相关链接

  1. DNS域名解析
    1. 缓存
    2. 监视解析的域名个数
  2. 建立链接
    1. CDN
    2. 较少请求数 (代码合并、雪碧图、base64、img src空属性)
    3. HTTP1.1 长链接,复用 TCP
    4. HTTP2 多路复用
    5. 减少证书数量
  3. 获取响应
    1. 减少重定向
    2. Gzip
    3. cookie
    4. 缓存
  4. 解析 & 渲染
    1. 处理HTML标记,构建DOM树。
    2. 处理CSS标记,构建CSSOM树。
    3. 将DOM树和CSSOM树融合成渲染树(会忽略不需要渲染的dom)。
    4. 根据渲染树来布局,计算每个节点的几何信息。
    5. 在屏幕上绘制各个节点。
    6. 中间遇到各种资源时,会进行资源的下载。
  • css下载时会阻塞渲染

script 解释

文件上传

相关链接

文件上传主要分为两步骤:

  1. 选取要上传的内容
  2. 发送到服务器

选择文件

input 选择

<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();
    }
  }
})

FileSystemApi

showOpenFilePicker().then(async (res) => {
  for (const fileSystemFileHandle of res) {
    if (fileSystemFileHandle.kind === 'file') {
      const blob = await fileSystemFileHandle.getFile();
    }
  }
})

上传前预览

对于体积较小媒体文本类型文件,可以通过 FileReader 读取内容,然后用相应的方式展示,如果体积
较大,可以考虑分段读取加载。

发送请求

文件上传一般使用 multipart/form-dataapplication/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--

上传无非也两种方式:

  1. form 表单
  2. ajax

form 表单上传

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>

ajax上传

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);

分片上传 & 断点续传 & 妙传

相较于在浏览器中做大文件切割下载,大文件切割上传的意义还是不叫重要的,其中最大的一个好处就是,上传失败后,用户不需要重头开始重新上传

  1. 文件切割
function file2Chunks(file, chunkSize) {
  const chunks = [];
  for (let start = 0; start < file.size; start += chunkSize) {
    chunks.push(file.slice(start, start + chunkSize));
  }
  return chunks;
}
  1. 计算hash
    采用 webwork + 增量计算
  1. 通过异步流程控制上传分片,需要和服务端约定好一些必要的字段信息
    1. 分片个数,文件信息
    2. 当前分片的位置信息
    3. 当前分片的 hash
  2. 服务端合并分片

Vue 项目接入 micro-app

基础操作

主应用

  1. 安装 mciro-app
npm i @micro-zoe/micro-app --save
  1. 在入口文件中添加并启动 micro-app
import microApp from '@micro-zoe/micro-app'
microApp.start()
  1. 在原有路由视图文件中引入子应用
<template>
  <micro-app name='my-app' url='http://localhost:8081he'/'></micro-app>
</template>
  1. 移除和子应用无关代码

子应用

  1. 开启跨域支持
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    }
  }
}
  1. 在入口文件中添加卸载函数
const app = new Vue();
window.unmount = () => {
  app.$destroy()
}
  1. 调整路由,将对应的子模块直接展示

数据通信

  • 需要在子应用加载前就使用到的公共数据,通过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会合并为一次。
    • 每次执行都会将发送的数据缓存下来,当再次发送时会遍历新旧值中的key,如果所有的值相同则不会发送,如果值有变化则将新旧值合并后发送
    • 比较是浅比较,只对第一次层做对比。
    • 可以通过 microApp.clearData('app1') 清空主应用发送给子应用的数据。
    • 也可以通过 destroyclear-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'
    })
    • 特性和主应用向子应用发送数据一致
  • 全局通信
    • setGlobalData forceSetGlobalData
    • addGlobalDataListener
    • getGlobalData
    • clearGlobalData

FAQ

  • 当我通过主应用访问子应用时,页面总是不停地刷新
    • 关掉 webpack 的优化项,通过增量添加配置方式逐步排查是哪个配置项出现了错误
    • clue 中是由于optimization.runtimeChunk = 'single'导致
  • 我已经设置了允许跨域,为什么还是会报 CORS 相关的错误
    • 除了设置 origin 外,有些项目可能也需要设置 headerscredentialsmethods
      module.exports = {
        devServer: {
          headers: {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'appId,clientId', // 根据接口所用到的headers来设置
            'Access-Control-Allow-Credentials': 'ture',
            'Access-Control-Allow-Methods': '*',
          }
        }
      }
    • 如果 headers 较多,可通过通行时动态添加(Vue Cli 5.0+ 支持此写法)
      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
        }
      }
    • 需要将代理的接口预检请求的status设置为 200
      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)
        }
      }
  • 底座应用和子应用单独访问都是正常的,但是通过主应用访问子应用总是出现js报错
    • 可能是子应用资源加载混乱导致。如果主应用和子应用存在较多的相同代码,那么构建出来的部分chunk会高度相似甚至完全相同。此时若通过主应用访问子应用,可能会导致子应用运行时,访问属性会访问到主应用的属性。设置子应用的webpack.jsonpFunction即可
      module.exports = {
        output: {
          // webpack 4
          jsonpFunction: 'webpackJsonp_child1',
          // webpack 5
          chunkLoadingGlobal: 'webpackJsonp_special_subject',
          globalObject: 'window'
        }
      }
  • 开启了umd模式,子应用渲染空白
    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);
      }
    }
  • 想通过webpackModule Federation代码共享
    • 首先要确保这些共享的代码时绝对独立的,比如一个单纯的函数、一个单纯的组件。如果这些共享的代码存在或间接存在一些需要运行时的代码,建议不要做代码共享,比如一个共享组件中,使用了 store this.$xx 等等这些需要依赖先实例化才能使用的方法。
    • remote
      // 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'
              }
            }])
        }
      }
    • local
      // 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)
  • 有些字典数据,权限信息、登录信息、接口数据我想优先通过主应用发送给子应用
    • 一般情况下这些数据都会存在项目的store中,如果原项目编码比较规范可以通过 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)
          }
        }
      })

1px 边框

相关文章

缩放

.half-border {
  height: 1px;
  transform: scaleY(0.5);
  /* 防止在 chrome 中线变虚 */
  transform-origin: 50% 100%; 
}

线性渐变 linear-gradient

.half-border {
  height: 1px;
  background: linear-gradient(0deg, #fff, #000);
}

boxshadow

.half-border {
  height: 1px;
  background: none;
  box-shadow: 0 0.5px 0 #000;
}

svg

.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

<meta name="viewport" content="width=device-width,initial-sacle=0.5">

webpack 详解

相关文章

tapable

1.0 之前 (webpack3)

  • plugin(name:string, handler:function) 注册插件到 Tapable 对象中
  • apply(…pluginInstances: (AnyPlugin|function)[])调用插件的定义,将事件监听器注册到 Tapable 实例注册表中
  • applyPlugins*(name:string, …) 多种策略细致地控制事件的触发,包括applyPluginsAsync、applyPluginsParallel等方法实现对事件触发的控制,实现
    • 多个事件连续顺序执行
    • 并行执行
    • 异步执行
    • 一个接一个地执行插件,前面的输出是后一个插件的输入的瀑布流执行顺序
    • 在允许时停止执行插件,即某个插件返回了一个undefined的值,即退出执行
function CustomPlugin() { }
CustomPlugin.prototype.apply = function (compiler) {
  compiler.plugin('emit', pluginFunction)
}

this.apply*('emit',options)

1.0之后

钩子名称 执行方式 要点
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)
})

webpack 入口

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
}

构建 & 编译

  • before-run 清除缓存
  • run 注册缓存数据钩子
  • compile 开始编译
  • make 从入口分析依赖以及间接依赖模块,创建模块对象
  • build-module 模块构建
  • seal 构建结果封装, 不可再更改
  • after-compile 完成构建,缓存数据
  • emit 输出到dist目录

文件下载

相关链接

a 标签下载

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 API

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());

FileSaver

saveAs(blob, 'test.png')
saveAs(file)
saveAs('/xxx', 'test.png');

JSZip

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') })

大文件分段下载

  1. 通过 head 请求判断文件是否支持范围请求,获取文件大小
    2. 通过访问首页事件,计算网速
  2. 计算文件分块数
  3. 结合异步控制,并发请求获取分块
  4. 分块全部下载完成后合并分块
  5. 利用BlobURL触发最终下载

迅雷下载

const links = document.querySelectorAll('a[data-thunder]');
links.forEach((link) => {
  const base64 = btoa(`AA${link.href}ZZ`)
  link.href = `thunder://${base64}`
})

总结

  1. 通过 <a download="test.pdf" href="test.pdf"> 触发下载
  2. 服务端设置响应头 Content-Disposition: attachment; filename="test.pdf" 触发浏览器下载
  3. 通过 ajax 请求下载文件,获取 blob ,利用 BlobURL 下载文件

由于 BlobURL 会先文件保存再内存中,所以对于比较大的文件,还是建议通过 1,2 两种方式来实现。但再实际开发中,下载文件也需要带上鉴权信息,所以我们以用 token 换临时 cookie 等方式

原生 DOM 操作

相关链接

查找

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);
}

节点类型

  1. ELEMENT_NODE 元素节点
  2. ATTRIBUTE_NODE 属性节点
  3. TEXT_NODE 文本节点
  4. CDATA_SECTION_NODE CDATA区段
  5. ENTITY_REFERENCE_NODE 实体引用元素
  6. ENTITY_NODE 实体
  7. PROCESSING_INSTRUCTION_NODE 表示处理指令
  8. COMMENT_NODE 注释节点
  9. DOCUMENT_NODE 指 document
  10. DOCUMENT_TYPE_NODE <!DOCTYPE>
  11. DOCUMENT_FRAGMENT_NODE 文档碎片节点
  12. 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

位置大小

前端监控

相关链接:

数据采集

性能监控

  • FP(First Paint)首次绘制,浏览器开始绘制页面的时间点
  • FCP(First Contentful Paint)首次内容绘制,浏览器首次绘制DOM内容的时间点
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 })
  • LCP(Largest Contentful Paint)最大内容绘制,即视口中最大的图像或文本块的渲染完成的时间点
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 })
  • DCL DOMContentLoaded
performance.timing.domContentLoadedEventEnd - performance.timing.domContentLoadedEventStart
  • onload
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)
    })
  }
})
  • js错误
window.onerror = (msg, url, lineNo, columnNo, error) => { }
  • promise错误
window.addEventListener('unhandledrejection', () => {})

行为监控

  • UV(Page View)页面浏览量
  • PV(Unique Visitor)独立访客
  • 页面停留时长 根据业务在 beforeunload 上报
  • 用户点击 事件冒泡

数据上报

上报方法

  • xhr
  • image
  • sendBeacon
    • 异步非阻塞
    • 页面卸载仍可发送
    • 低优先级
    • 只支持POST,无法接收服务器响应

上报时机

  • requestIdelCallback/setTimeout
  • beforeUnload
  • 缓存上限

上述方式结合起来时候,用一个变量存储需要上报的数据,当缓存达到一定数量后,通过requestIdelCallback/setTimeout延迟上报。在页面离开时统一将未上报的数据进行上报

Git 常用操作

相关链接:


修改 commit message

git commit --amend --only
git commit --amend --only -m 'xxxxx'

修改 commit author

git commit --amend --author "xxx [email protected]"
git filter-branch

从一个 commit 中移除文件

git checkout HEAD^ filename
git add -A
git commit --amend

删除 commit

git reset HEAD^ --hard
git push -f [remote] [branch]
git reset HEAD^ --soft

将暂存的代码添加到 commit

git commit --amend

git add 文件中某一部分代码

git add --patch fiename
git add -p fiename

暂存代码 stash

git add .
git stash push -m xxx

删除分支

远程分支

git push origin --delete branch-name
git push origin :branch-name

本地分支

git branch -D branch-name

恢复删除的 tag

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'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.