JavaScript高级 - 纯js实现文件上传大文件切片上传断点续传(客户端)_一笑程序猴的博客-程序员ITS304_js 大文件切片

技术标签: 断点续传  js文件上传  大文件上传  切片上传  base64文件上传  前端  WEB前端高级教程  


前言

一、环境准备及依赖库

  • axios v0.21.1: 用于调用服务端接口发送服务端请求
  • spark-md5 v3.0.1: 用于根据文件内容生成hash码
  • qs v6.9.6:用于将application/x-www-form-urlencoded格式从参数解析为a=x&b=y的格式

二、项目结构

web 项目根目录

  • scripts 存放js脚本目录
    • axios.min.js axios库(第三方)
    • qs.js qs库(第三方)
    • spark-md5.min.js spark-md5库(第三方)
    • axios2.js axios二次封装库(自定义)、
    • upload.js 上传文件功能代码(自定义)
  • css 样式文件目录
    • upload.css 页面样式
  • index.html 文件上传html页面

三、 功能实现

  • 结合上面的截图,将分为5个模块进行讲解,所有模块用到的上传控件都是html原生的类型为file的input控件。
  • 为了页面的美观,我们将input隐藏起来,并用普通按钮替代,当点击按钮时触发input的click事件。
  • 另外可以再额外加一些进度显示,图片缩略图显示,文件名称显示等。
  • 关于HTML和css部分不再过多说明,下面将分模块进行js部分重点讲解,每个模块都用闭包函数包裹,避免变量冲突

1、axios二次封装

在每个功能模块中,我们都将通过使用axios向服务端发送请求,这时我们就需要对axios做一些特殊处理,也就是二次封装

  • 创建axios对象,避免不同场景配置冲突
  • 设置baseURL
  • 设置默认Content-Type 为 multipart/form-data
  • 在transformRequest中判断Content-Type,如果是application/x-www-form-urlencoded 则利用qs库对参数进行格式化
//axios.js axios二次封装
let request = axios.create();
request.defaults.baseURL = 'http://127.0.0.1:3000';
request.defaults.headers['Content-Type'] = 'mutipart/form-data';
request.defaults.transformRequest = (data, headers) => {
    
    let contentType = headers['Content-Type'];
    if (contentType === 'application/x-www-form-urlencoded') return Qs.stringify(data);
    return data;
}

request.interceptors.response.use(response => {
    
    return response.data;
});

2、 单文件上传FROM-DATA,先选文件再上传

在这里插入图片描述

  • 简单步骤分析:
  • 首先应该先获取到需要用到的页面元素:上传控件input,选择按钮,上传按钮,缩略图展示,文件展示,进度条展示
  • 绑定选择按钮的click事件并在click事件中触发上传控件input的click事件
  • 绑定上传控件input的change事件,在该事件中获取已选择的文件
  • 绑定上传按钮的click事件,在该事件中组合参数并发送post请求调用服务端API实现文件上传
  • 文件上传的关键代码就是发送请求前的参数拼接部分
    • 这里我们利用js内置的FromData类将文件作为参数传输
    • new FormData().append(“file”, file);
  • 代码实现
//upload.js 单文件上传form-data
(function () {
    
    let upload1 = document.querySelector("#upload1"),
        upload_inp = upload1.querySelector('.upload-inp'),
        upload_select = upload1.querySelector('.upload-btn.select'),
        upload_upload = upload1.querySelector('.upload-btn.upload'),
        sel_files = upload1.querySelector('.files'),
        file1 = upload1.querySelector('.abbr'),
        cur_pro = upload1.querySelector('.cur-pro'),
        pro_val = upload1.querySelector('.pro-val'),
        progress = upload1.querySelector('.progress'),
        _file;

    upload_select.addEventListener('click', () => {
    
        upload_inp.click();
    });
    upload_inp.addEventListener('change', function () {
    
        let file = this.files[0];
        _file = file;
        sel_files.innerHTML = file.name;
        progress.style.display = 'inline-block';
        pro_val.innerHTML = '';
    })

    upload_upload.addEventListener('click', function () {
    
        let formData = new FormData();
        formData.append('file', _file);
        formData.append('filename', _file.name);
        request.post('/upload_single_file', formData, {
    
            onUploadProgress: function (ev) {
    
                let pro = ((ev.loaded / ev.total) * 100).toFixed(0) + '%';
                cur_pro.style.width = pro;
                pro_val.innerHTML = pro;
            }
        }).then(res => {
    
            console.log(res);
            file1.src = `http://${
      res.serverPath}`;
            file1.style.display = 'block';
        }).catch(err => {
    
            console.log(err);
        });
    });
})();

3、 单文件上传BASE64,只能上传小于100K的png或jpg图片文件

在这里插入图片描述

  • 简单步骤分析:
  • 首先应该先获取到需要用到的页面元素:上传控件input,选择上传按钮,缩略图展示,文件名展示,进度条展示
  • 绑定选择并上传按钮的click事件并在click事件中触发上传控件input的click事件
  • 这里由于选择和上传按钮合并为一个,所以向服务端发送请求的步骤我们将放在input的change事件中
  • 绑定input的change事件,在该事件中组合参数并发送post请求调用服务端API实现文件上传
  • 在文件域的change事件中,我们需要
    • 根据获得的文件信息校验文件的大小不能超过100k(base64格式上传文件不能太大) file.size属性
    • 校验文件格式只能是png或jpg格式,file.type属性
    • 利用js内置的FileReader类将文件转换为BASE64格式
    • 并在filereader的onload函数中拿到转换为BASE64格式的内容并发送post请求实现文件上传
    • 另外在因为BASE64格式内容涉及到的字符比较多,为了避免一些特殊字符问题,在参数传递前需要用encodeURIComponent进行编码
  • 代码实现
//upload.js 单文件base64上传
(function () {
    
    let upload2 = document.querySelector("#upload2"),
        upload_inp = upload2.querySelector('.upload-inp'),
        upload_upload = upload2.querySelector('.upload-btn.upload'),
        sel_files = upload2.querySelector('.files'),
        file2 = upload2.querySelector('.abbr'),
        progress = upload2.querySelector('.progress'),
        cur_pro = upload2.querySelector('.cur-pro'),
        pro_val = upload2.querySelector('.pro-val'),
        _file;

    upload_upload.addEventListener('click', () => {
    
        upload_inp.click();
    });
    upload_inp.addEventListener('change', function () {
    
        progress.style.display = 'inline-block';
        pro_val.innerHTML = '';
        let file = this.files[0];
        _file = file;
        if (file.size > 100 * 1024) {
    
            alert('图片必须小于100k');
            return;
        }

        if (!/(jpg|jpeg|png)/.test(file.type)) {
    
            alert('只能上传png或jpg或jpeg格式的图片');
            return;
        }
        sel_files.innerHTML = file.name;
        let fr = new FileReader();
        fr.readAsDataURL(file);
        fr.onload = ev => {
    
            file2.src = ev.target.result;
            file2.style.display = 'block';
            console.log(file.name);
            request.post('/upload_base64', {
    
                file: encodeURIComponent(ev.target.result),
                filename: file.name
            }, {
    
                headers: {
    
                    "Content-Type": "application/x-www-form-urlencoded"
                },
                onUploadProgress: function (ev) {
    
                    let pro = ((ev.loaded / ev.total) * 100) + '%';
                    pro_val.innerHTML = pro;
                    cur_pro.style.width = pro;
                }
            }).then(res => {
    
                console.log(res);
                alert('上传成功了');
                return;
            }).catch(err => {
    
                console.log(err);
                alert('失败了?')
            });
        };

    })
})();

4、多文件上传FORM-DATA

在这里插入图片描述

  • 简单步骤分析:
  • 多文件上传的步骤跟单文件FORM-DATA的步骤类似
  • 首先也是先获取到需要用到的页面元素:上传控件input,选择上传按钮
  • 绑定选择按钮的click事件并在click事件中触发上传控件input的click事件
  • 绑定上传控件input的change事件,在该事件中获取已选择的文件,并遍历所选的文件组合参数发送post请求调用服务端API实现文件上传
  • 与单文件FORM-DATA上传不同的是,我们获取到的是一个文件列表
    • 在change事件中需要遍历所有所选的文件,组合参数然后循环发送请求调用服务器API接口实现多文件上传
    • 因为是不固定个数文件上传,所以在遍历所选文件后需要动态将文件名和上传进度拼接为HTML元素展示在页面上
  • 代码实现
// upload.js 多文件上传form-data
(function () {
    
    let upload3 = document.querySelector("#upload3"),
        upload_inp = upload3.querySelector('.upload-inp'),
        upload_upload = upload3.querySelector('.upload-btn.upload'),
        sel_files = upload3.querySelector('.list');


    upload_upload.addEventListener('click', () => {
    
        upload_inp.click();
    });
    upload_inp.addEventListener('change', function () {
    
        let files = this.files;
        sel_files.innerHTML = '';
        [].forEach.call(files, (file, index) => {
    
            sel_files.innerHTML += `<div><span class="files" style="margin-right:8px;font-size:12px">${
      file.name}</span><span class="pro-val" id="myfile${
      index}"></span></div>`
            let formData = new FormData();
            formData.append('file', file);
            formData.append('filename', file.name);
            request.post('/upload_single_file', formData, {
    
                onUploadProgress: function (ev) {
    
                    let pro = ((ev.loaded / ev.total) * 100).toFixed(0) + '%';
                    document.querySelector(`#myfile${
      index}`).innerHTML = pro;
                    // sel_files.innerHTML += `<span class="files">${file.name}</span> <span class="pro-val" >${pro}</span>`
                }
            }).then(res => {
    
                console.log(res);
                // alert('上传成功了');
            }).catch(err => {
    
                console.log(err);
            });
        });
    });
})();

5、多文件拖拽上传FORM-DATA

在这里插入图片描述

  • 简单步骤分析:
  • 在该模块中,实现拖拽上传的重点就是在**拖拽**上,而其它上传步骤跟上面的多文件上传是一样的。
  • 除了要实现拖拽上传,原来的点击上传我们也需要保留
  • 所以首先我们还是要获取到上传控件input,div块作为拖拽区域和触发点击事件的区域
  • 然后把主要的上传逻辑封装为一个单独的方法uploadFiles,接收一个数组或类数组类型的参数,以方便点击或拖拽时调用
  • 点击上传:
    • 这里我们需要给拖拽域(div)绑定一个click事件,并在该事件中触发input的click事件
    • 绑定input的change事件,在该事件中调用获取到选择的文件列表,并调用上传方法uploadFiles实现文件上传
  • 拖拽上传:
    • 要实现拖拽上传需要借助两个事件,分别是dragover和drop,就是将文件拖拽到拖拽域(div)上或是落在拖拽域(div)上时要触发的事件
    • 需要注意的是:如果将一个文件拖拽到网页上时,有些格式的文件(如:txt,jpg,png等)会被默认打开直接显示在网页上,所以我们需要在这两个事件中阻止浏览器的默认行为:ev.preventDefault()
    • 然后在drop事件中也就是当我们将文件拖拽到div上并松开鼠标时,调用uploadFiles将文件上传
    • 在drop事件中有个dataTransfer属性的files属性可以获取到拖拽进来的文件列表,这样就可以通过ev.dataTransfer.files获取到所有的多文件了
  • 代码实现
//upload.js 拖拽上传form-data
(function () {
    
    let upload5 = document.querySelector("#upload5"),
        upload_inp = upload5.querySelector('.upload-inp'),
        upload_upload = upload5.querySelector('.upload-btn'),
        sel_files = upload5.querySelector('.list');

    const uploadFiles = function uploadFiles(files) {
    
        sel_files.innerHTML = '';
        [].forEach.call(files, (file, index) => {
    
            sel_files.innerHTML += `<div><span class="files" style="margin-right:8px;font-size:12px">${
      file.name}</span><span class="pro-val" id="myfile${
      index}"></span></div>`
            let formData = new FormData();
            formData.append('file', file);
            formData.append('filename', file.name);
            request.post('/upload_single_file', formData, {
    
                onUploadProgress: function (ev) {
    
                    let pro = ((ev.loaded / ev.total) * 100).toFixed(0) + '%';
                    document.querySelector(`#myfile${
      index}`).innerHTML = pro;
                }
            }).then(res => {
    
                console.log(res);
                // alert('上传成功了');
            }).catch(err => {
    
                console.log(err);
            });
        });
    }
    upload5.addEventListener('dragover', function (ev) {
    
        ev.preventDefault();
    });
    upload5.addEventListener('drop', (ev) => {
    
        ev.preventDefault();
        uploadFiles(ev.dataTransfer.files);
    });

    upload_inp.addEventListener('change', function () {
    
        uploadFiles(this.files);
    });
    upload5.addEventListener('click', (ev) => {
    
        upload_inp.click();
    });
})();

6、大文件切片上传,断点续传FORM-DATA

接下来就是本文的最后一个模块,也是最复杂和最重点的模块:大文件切片上传和断点续传,要实现切片上传和断点续传逻辑较前面几个模块都稍有些复杂,下面我们依然来一步步分析一下:

  • 简单逻辑分析
  • 切片上传顾名思义就是将一个大文件分割成多个小文件进行分别上传,待所有切片上传完成后再将它们合并成一个文件,这样就实现了一个大文件的切片上传,同时如果上传过程出现问题,下次继续上传时还能实现断点续传
  • 切片上传的关键在于上传后要将所有切片文件合并,那么合并时就需要考虑一下问题了:
    • 需要将哪些文件进行合并?
    • 找到需要合并后的文件,要按怎么样的顺序进行合并?
      =>首先第一问题,为了能够快速方便的找到哪些文件是需要合并的,在将切片文件上传到服务器时,我们需要在服务器端建立一个单独的临时文件夹用于保存所有的切片
      =>第二个问题,为了保证合并后的文件与原文件保持一致,在切片时需要给每个切片添加一个索引,这样就能在合并时按照索引进行按顺序合并了。
  • 如果切片在上传过程中出现了问题,导致上传中断,那么下次上传时为了实现不重复上传,也就是所说的断点续传,就需要进行判断,如果文件存在则直接跳过,那么如何去判断一个文件(或切片)是否存在了呢?
    • 这时就需要用到我们前面提到的spark-md5库了,该库可根据文件内容生成一串hash值,只要文件内容不变那么生成出来的hash值也永远都是一样的,所以我们可以利用hash值加索引的形式进行文件切片的命名
  • 切片思路分析:
    • 要将一个文件进行切片,需要借助文件的**size属性和slice**方法
    • 方法一(固定个数):将一个文件切成固定个数,比如20个,然后用size/20计算出每个切片文件的大小,再利用slice进行截取
    • 方法二(固定大小):固定每个切片文件的大小,比如100k,然后用size/100计算需要分成几个切片,同样也是再用slice截取
    • 在本案例中,我们将采取方法一和方法二合并的方式进行切片:我们先根据方法二固定每个切片的大小,计算出切片的个数,然后再规定一个最大个数,如果计算出的个数超过了最大个数,就需要根据方法一进行重新切片。如果没有超出个数,则按固定大小切片。
  • 简单步骤分析

在梳理出切片逻辑后,下面就是要一步步实现了:

  • 首先我们先来封装一个返回promise实例的方法retrieveHash,该方法主要用于根据文件内容生成一个hash值,需要借助spark-md5和FileReader
  • 封装一个所有切片上传完成后发送合并请求方法uploadComplete
    • 在该方法外面需要定义一个计数器,每上传完成一个切片需要调用一次该方法,每调用一次该方法,计数器就需要累加1
    • 当计数器的值等于切片的个数时,则说明所有切片已经上传完成,这时就可以发送合并请求进行切片合并了
    • 这里有一点需要注意:就是在发送合并请求前,最好是延迟几秒再发送,以避免一些不必要的错误
  • 在文件域的change事件中得到要上传的文件
  • 调用上面封装好的获取hash值的方法retrieveHash,然后根据hash值向服务端发送一个请求来获取已经上传过的切片列表filelist(用于断点续传判断)
  • 根据上面分析的切片逻辑进行切片,并将切片文件信息保存在数组中
  • 遍历切片数组,首先判断该切片是否已经上传,也就是看该切片文件是否已经存在于上面所获取到的文件列表filelist中
    • 如果存在则调用uploadComplete,让计数器累加
    • 如果不存在,则调用服务端切片上传接口进行文件上传,同时在上传完成后,仍需调用uploadComplete方法进行计数器累加,一旦计数器的值跟切片个数相等,则会自动调用合并接口进行文件合并
  • 至此大文件的切片上传和断点续传就实现了。
  • 代码实现
//upload.js 大文件切片上传,断点续传
(function () {
    
    let upload4 = document.querySelector("#upload4"),
        upload_inp = upload4.querySelector('.upload-inp'),
        upload_upload = upload4.querySelector('.upload-btn'),
        sel_files = upload4.querySelector('.files'),
        cur_pro = upload4.querySelector('.cur-pro'),
        pro_val = upload4.querySelector('.pro-val'),
        progress = upload4.querySelector('.progress');

    const retriveHash = function retriveHash(file) {
    
        return new Promise((resolve, reject) => {
    
            let spark = new SparkMD5.ArrayBuffer();
            let fr = new FileReader();
            fr.readAsArrayBuffer(file);
            fr.onload = (ev) => {
    
                spark.append(ev.target.result);
                let hash = spark.end();
                let suffix = /\.([0-9a-zA-Z]+)$/.exec(file.name)[1];
                resolve({
    
                    hash,
                    suffix
                });
            };
        });


    }

    let complete = 0;
    const uploadComplete = function uploadComplete(hash, count) {
    
        complete++;
        let progerss = (complete / count * 100).toFixed(2) + '%';
        cur_pro.style.width = progerss;
        pro_val.innerHTML = progerss;
        if (complete < count) return;
        cur_pro.style.width = '100%';
        pro_val.innerHTML = '100%';
        setTimeout(() => {
    
            request.post('/upload_merge', {
    
                hash,
                count
            }, {
    
                headers: {
    
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            }).then(res => {
    
                console.log(res);
                // alert('上传成功了');
            }).catch(err => {
    
                console.log(err);
            });
        }, 3000);
    }
    upload_upload.addEventListener('click', function () {
    
        upload_inp.click();
    });

    upload_inp.addEventListener('change', async function () {
    
        let file = this.files[0];
        progress.style.display = 'inline-block';
        cur_pro.style.width = '0%';
        pro_val.innerHTML = '0%';
        let chunks = [];
        let {
    
            hash,
            suffix
        } = await retriveHash(file);
        sel_files.innerHTML = `${
      hash}.${
      suffix}`;
        let {
    
            filelist
        } = await request.get('/uploaded', {
    
            params: {
    
                hash
            }
        });

        let maxSize = 100 * 1024; //100k
        let count = Math.ceil(file.size / maxSize);
        //限制切片的数量不能超过20个,并重新计算每个切片的大小
        if (count > 20) {
    
            maxSize = file.size / 20;
            count = 20;
        }

        let index = 0;
        while (index < count) {
    
            chunks.push({
    
                file: file.slice(index * maxSize, (index + 1) * maxSize),
                filename: `${
      hash}_${
      index+1}.${
      suffix}`
            });
            index++;
        }

        chunks.forEach((item, index) => {
    
            //如果已经上传过就不再上传了
            if (filelist && filelist.length > 0 && filelist.includes(item.filename)) {
    
                uploadComplete(hash, count);
                return;
            }
            let formData = new FormData();
            formData.append('file', item.file);
            formData.append('filename', item.filename);
            request.post('/upload_chunk', formData).then(res => {
    
                uploadComplete(hash, count);
                // console.log(res);
                // alert('上传成功了');
            }).catch(err => {
    
                console.log(err);
            });
        });
    });


})()

总结

本文中我们进行了分模块分场景实现了关于端文件上传的几个功能点,并重点分析了大文件切片上传和断点续传的功能点。每个模块对应的功能代码都已经提供。
在下一篇文章JavaScript高级 - 实现文件上传大文件切片上传断点续传前后端完整代码中我们将会把服务端和客户端的全部完整代码展示出来

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lixiaosenlin/article/details/114931287

智能推荐

Seasar的ORM框架Doma学习笔记系列1——安装设置_死鸡的博客-程序员ITS304_doma框架

官方网站:http://doma.seasar.org/index.htmlDoma的一大优势是完全实现了代码跟sql文件的分离。1. 安装设置 1)doma要求JDK1.6以上的JDBC。 2)把doma-x.x.x.jar包导入工程。 3)注解处理设定    工程属性,【Java Compiler】 - 【Annotation Processing】里,

这可能是目前最全的!这些细节在Java面试上要注意了_普通网友的博客-程序员ITS304

前言提到IT人员,人们的第一印象就是高薪资,包括转行来学Java的人绝大多数都是冲着高薪以及就业广泛来的。前段时间跟同学聊天,她说她老公在一家互联网公司做Java后台开发,年薪四十万,最近在准备复习,想着明年冲一波阿里p6或者p7。很多人都听过阿里的p级职位层级,那今天就跟大家“揭秘”一下,想要成为p7岗的Java技术专家你需要掌握哪些技术。本篇文章我将分成三个内容来讲:①了解阿里p级职位,大厂岗位薪资(2020)②阿里p7技术专家水平(Java岗)③如何让自己一步步贴近p7技术专家分享第

基于多重继承与信息内容的知网词语相似度计算 - 论文及代码讲解_机智翔学长的博客-程序员ITS304

论文:《基于多重继承与信息内容的知网词语相似度计算》-2017-张波,陈宏朝等 查看代码:https://github.com/yaleimeng/Final_word_Similarity总体感受:太乱了,有可能是之前没怎么接触这块。看论文,搞不懂怎么回事,义项、义原是啥,怎么这么多定义,到头来还是不懂两个词的相似度怎么计算,比哈工大词林那篇论文复杂多了。看代码,函数调来调去,一会这个...

《计算机操作系统》重点知识笔记整理(一)_Barry Yan的博客-程序员ITS304_计算机操作系统笔记

《计算机操作系统》重点知识总结1(1-4章)????注意:​ 这篇总结文档参考的配套书籍为《计算机操作系统》(第四版) 相关知识点关联的页码可能只与本书配套。????说明:​ 由于时间关系,该总结的部分知识点可能有所疏落或存在错误,请认真研读不要盲目学习,读者如有补充或问题更正请联系作者[[email protected]],作者将会表示感谢!​ 最后,希望尊重作者劳动成果,请大家转载时注明出处,Thanks!????第一章 操作系统引论1 操作系统的定义

高斯模糊(高斯滤波)原理以及计算过程_StriveZs的博客-程序员ITS304_高斯模糊公式

高斯模糊/高斯滤波通常,图像处理软件会提供模糊滤镜,使图片产生模糊效果。模糊的算法有很多,其中有一种叫高斯模糊(Gaussian Blur),它将正态分布用于图像处理。文本介绍了高斯模糊的算法,你会看到这是一个非常简单易懂的算法。本质上,它是一种数据平滑技术(data smoothing),适用于多个场合,图像处理恰好提供了一个直观的应用实例。高斯模糊的原理所谓模糊,可以理解成每一个像素都取周边像素的平均值。上图中,2是中间点,周边点都是1.中间点取周围点的平均值之后,就会从2变成了1.

ubuntu安装mysql离线包_lmlby的博客-程序员ITS304

Ubuntu安装mysql离线包测试环境:ubuntu12.04-amd64Mysql离线包:mysql-5.5.25-linux2.6-x86_64.tar.gz官方安装步骤如下:注意事项:如果执行scripts/mysql_install_db –user=mysql时出现如下错误:InstallingMySQL system tables..../bin/mysq

随便推点

EXT4.0 (4~9章)学习资料_clever027的博客-程序员ITS304

第四章 MVC学习 从这个图中我们可以很清楚的看到M 、V、C在ExtJS4.0里面所对应数据类型。 靠右边是对应的代码结构。 下描述一下这model、store、view、controller以及application这几者之间的关系。(1)application:它是MVC的入口,用来告诉ExtJS到那里去找对应js文件以及启动加载controlle

2021年华为认证考试费用是多少_20004的博客-程序员ITS304_华为ip证书多少钱

有一些朋友打算在2021年参加华为网络工程师这方面的考试,所以想知道这方面的考试费用是多少,自己好有一个准备,那么网络工程师成长日记,作者小编来给大家介绍好让大家有一个准备如果你是找工作为目的的话,一般来说你至少要考华为hcip,也就是中级网络工程师以上这个认证,如果你非常清楚华为的初级,中级高级这三个级别,那么你也可以把最高级别这个认证作为你的考试目标所以首先你要考哪个级别是你需要自己心里清楚的华为的初级考试费用也就是1000多块钱华为的中级考试费用是480美金,折合成人民...

Flutter筑基——学好 Dart,才能玩转 Flutter_willwaywang6的博客-程序员ITS304

目录前言正文Dart 开发环境的搭建最后参考前言我们知道 Flutter 这个 UI 框架是使用 Dart 语言开发的,这说明要玩转 Flutter,就要先学好 Dart。那么,怎么学好 Dart 呢?有的同学抱着“不就是一门语言嘛”的心态,直接开始写 Flutter,然后遇到问题了,再去查看 Dart 的文档。这也是一种学习 Dart 的方式,但这种方式可能不适合大多数同学。庆幸地是,可以去查看 Dart 官网上的示例,比如Language-tour,就讲解了 Dart 的语法。但是,官网上的

破解使用radius实现802.1x认证的企业无线网络_Sword-heart的博客-程序员ITS304

0x01前言概述针对开放式(没有密码)无线网络的企业攻击,我个人感觉比较经典的攻击方式有2种,一种是eviltwin,一种是karma。karma应该是eviltwin攻击手法的升级版,攻击者只需要简单的监听客户端发的ssid探测和响应包就可以实现中间人了,受害者很少会有察觉。而且坊间曾有一个错误的认识,认为隐藏的ssid是不受karma影响的。但是实际情况是,客户端如果曾经连接过隐藏的ssid,也会广播这些网络的探测包。尽管karma这种攻击方式已经有10多年的历史了,但是在MAC OSX,ubunt

ffmpeg代码分析(1)--编译裁剪_茜茜她老爹的博客-程序员ITS304_ffmpeg 裁剪编译

我们很少使用到ffmpge完整的功能库,大部分时候,只需要特定的编解码 传输协议,那么该怎么裁剪ffmpeg呢?答案就是configure命令。configure 是一个文本文件,打开以后我们会发现它有很多命令,如下Help options:  --help                   print this message  --list-decoders        

推荐文章

热门文章

相关标签