胖蔡叨叨叨
你听我说

你真的会用script吗?noscript又是什么?

胖蔡阅读(17)

在Html中引入script脚本可以说是一大创举,它最早被网景公司在Netscape Navigator 2中实现。那么,script你真的会用script标签吗?通常我们说script是网页“动起来”的一个标志,为了更好的控制script脚本的加载时机,html规范规定了script的多个属性:

 

async

可选属性,用于表示立即开始下载脚本,但不能阻止其他页面动作,如下载资源、等待其他脚本下载。只对外部脚本有效。

charset

可选属性,用于配置src属性指定的代码字符集。基本可以忽略,大多浏览器已经做了兼容。

crossorigin

配置相关请求的CORS设置。默认不开启CORS。可以用来配置文件请求的凭据标志,默认不包含凭据。可能的值如下:

  • anonymous:不包含凭据
  • use-credentials:包含凭据

 

defer

表示文档解析和显示完成后再执行脚本,只对外部脚本有效。属于异步加载的一种方式。在IE7及更早版本,对行内script脚本也可以指定这个属性。

integrity

可选。允许对接收的资源和指定的加密签名以验证子资源的完成新。若接收到的资源签名与这个属性指定的签名不匹配,则页面报错,脚本停止执行。这个属性可以确保CDN不会提供恶意内容

 

src

可选属性,用于指定外部js的网络地址

 

type

可选属性。类似文件的Mimetype属性,默认为text/javascript,若是需要加载包含es模块的代码可以指定值为”module’。

 

nomodule

可选属性,兼容不支持ES6的浏览器,主要是IE11。

 

 

对于async、defer以及module,async情况下关于js的下载、解析任务拆分情况如下图:

d7431f53faf9eac

 

综上可以总结如下:

  • defer外部请求js与html异步请求,且defer会在html文档解析完成后再去执行js文件,即为js执行在domcontentload时间触发后再执行Js脚本。
  • async外部请求js时会异步文件,文件请求完成后会中断html的解析去执行js文件,js执行完成后若Html还未解析完成则继续进行html解析。
  • type=”module”的script标签若是未设置async或defer虎山行,则默认使用defer方式进行加载。
  • 如上type=”module”的fetch分叉为引入外部js的请求过程。

 

noscript

 

针对之前不支持js的浏览器的问题,需要一个页面降级处理的方式,其实也就是兼容提示处理。所以,有了noscript标签这样的一个规范,可以用来被使用提示不支持js的浏览器提供的替代内容。现在基本是用于被禁用js的浏览器,可以提示让它开启js支持。

 

<!DOCTYPE html>
<html>
<head>
<title>示例页面</title>
<script src="example01.js" defer></script>
</head>
<body>
<noscript>本页面需要浏览器开启js支持</noscript>
</body>
</html>

 

面试经典之前端跨域是怎么跨的?

胖蔡阅读(12)

大家好,今天我们一起看下前端面试的经典问题:什么是跨域?首先拿到问题后,我们需要先思考下问这个问题的人即面试官,他希望获得到什么答案。下面,我们可以模拟下面试官可能是怎么想的:

  • 第一种:遇到跨域问题,我觉得这里有一两个点还挺有意思,想知道你知不知道,能不能答出来
  • 第二种:跨域就是基础,一般都能答出来,看看你的知识面,想往内知识点再深入挖点,看有没有惊喜
  • 第三种:网上都是这些问题,我也问问看

4ffce04d92a4d6c-9

一般情况就这几种情况了,那针对这几种情况我们该怎么去回答才能让面试官满意呢?我们一个个来分析剖析下。 (更多…)

JavaScript中Worker初探

胖蔡阅读(62)

什么是Worker?他有什么作用?

因为js是单线程运行的,在遇到一些需要处理大量耗时的数据时,可能会阻塞页面的加载,造成面的假死。针对这个问题,H5出了新的功能worker,支持多线程,这时我们可以使用worker来开辟一个独立于主线程的子线程来进行一些耗时的大量运算。就可以用来解决因为大量耗时运算是造成页面假死的问题,详细内容查看Worker

如何使用Worker

   //首先创建 Worker 对象,第一个参数 worker 需要执行的 JS 文件的 URL。相对地址 URL 是相对于创建 Worker 的脚本所在文档(宿主文档)的访问地址。绝对地址 URL 必须要和宿主文档同源。
   //第二个参数是可选参数,可以是一个对象,name属性,可以给当前Worker定义一个名称,可用来区分其他的worker
   const worker = new Worker("worker.js",{name:'自定义的名称'})

      //通过onmessage ,监听子线程的消息
      worker.onmessage((e)=>`接收worker线程传递值:${e.data}`)
       //通过postMessage ,向子线程发送消息
      worker.postMessage('向子线程发送消息')

      //worker.js 文件

      //通过self.onmessage,来接收主线程传递的信息
      self.onmessage((e)=>{
       console.log(e)
      })
      //通过self.postMessage,来向主线程传递信息
      self.postMessage('要传递的值')

方法和属性

主线程worker()对象,用来供主线程操作 Worker。worker 线程对象的属性和方法如下:

  • worker.onerror:指定 error 事件的监听函数。
  • worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。
  • worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  • worker.postMessage():向 Worker 线程发送消息。
  • worker.terminate():立即终止 Worker 线程。

worker 子线程全局属性和方法:

  • self.onmessage:指定message事件的监听函数。
  • self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  • self.close():关闭 worker 线程。
  • self.postMessage():向产生这个 worker 线程发送消息。
  • self.importScripts():加载 JS 脚本。
  • self.name: worker 的名字。该属性只读,由构造函数指定。

worker 使用注意点

  1. 同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

  1. DOM 限制

worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。

  1. 通信联系

worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息传递完成。

  1. 脚本限制

worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

  1. 文件限制

worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

简单使用demo

模拟页面中插入耗时处理数据然后渲染元素,暂时不考虑页面渲染性能问题

(() => {
  let arrList = []  
  let indexNum = 1000
  for (let index = 0; index < indexNum; index++) {
    let obj = {
      name: index + 1
    }
    arrList.push(obj)
    if (arrList.length == indexNum) {
      //在子线程中,self代表worker子线程的全局对象,也可以用this代替self,或者省略也行
      //this.postMessage(arrList);
      //self.postMessage(arrList); 
      postMessage(arrList); //向主线程发送消息
    }
    onmessage = function(e) {
        console.log(e.data)
        console.log("我是worker线程接收到了你的信息,不客气,我就是做耗时操作的")
        console.log("2妙后我还会给你发送个信息")
        setTimeout(() => {
          postMessage('如果还有耗时的操作,你还可以在开启一个worker线程呦')
        }, 2000)

      }
      //worker自己关闭
      // self.close()
  }
})()
<!DOCTYPE html>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<html>
<body>
    <div id="bt">
        <p style="color:red">模拟元素1</p>
        <p style="color:red">模拟元素2</p>
        <p style="color:red">模拟元素3</p>
        <p style="color:red">模拟元素4</p>
        <div class="ld" style="color:green">数据加载中。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。</div>
        <div class="main"></div>
        <p style="color:red">模拟元素5</p>
        <p style="color:red">模拟元素6</p>
        <p style="color:red">模拟元素7</p>
    </div>
    <script>
        // 引入的js文件必须和当前页面同源,不可跨域
        let w = new Worker("./worker2.js");
        $(".ld").show()
            // console.log("Worker线程处理完耗时操作后,主线程接会收到处理数据")
        w.onmessage = function(e) {
            let listData = e.data
            console.log(listData)
            let strs = ''
            for (let j = 0; j < listData.length; j++) {
                const element = listData[j];
                strs += '<p>' + '这是接收到的数据' + j + '</p>'
            }
            $(".main").append(strs)
            $(".ld").hide()
        }
        w.postMessage('你发送的数据我收到了,辛苦了')
            //主线程关闭子线程
            // w.terminate()
    </script>
</body>
</html>

9ff7ae012e2b073

03cb10abca4d2d5

SharedWorker一种特殊的worker,详细内容查看:SharedWorker

sharedWorker 顾名思义,是 worker 的一种,可以由所有同源的页面共享。同Worker的api一样,传入js的url,就可以注册一个 sharedWorker 实例, sharedWorker通过port来发送和接收消息

  let myWorker = new SharedWorker('worker.js');
    
    //主线程和共享线程都是通过 postMessage() 方法来发送消息
   myWorker.port.postMessage('发送消息');

   //接收消息都是使用onmessage = (e)=>{},或者 addEventListener('message', (e)=>{})
   myWorker.port.onmessage = (e)=>{console.log(e.data)} ;

    //关闭启动和worker是有点区别的
  线程通过 worker.port.start() 启动
  线程通过 worker.port.close() 关闭

  SharedWorker使用注意要点同worKer一样

简单使用Demo

  • shareWk.js 共享线程,里面存储计数器counter
  • A.html 刷新页面,通知counter+1
  • B.html 刷新页面,查看counter
    // 计时器
    let counter = 0
    // 监听连接
    self.addEventListener('connect', (e) => {
      const port = e.ports[0]
      port.onmessage = (res) => {
        console.log('A,B页面共享的信息:', res.data)
        switch (res.data) {
          case 'add':
            counter++
            break
        }
        console.log('counter:', counter)
        port.postMessage(counter)
      }
    })
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>A页面</title>
</head>
<body>
    <h1>A页面</h1>
    <script>
        // 兼容性判断
        if (!SharedWorker) {
            throw new Error('当前浏览器不支持SharedWorker')
        }
        // 创建共享线程
        const worker = new SharedWorker('shareWk.js')
            // 启动线程端口
        worker.port.start()
            // 向共享线程发送消息
        worker.port.postMessage('add')
            // 线程监听消息 2种方式都可以
            // worker.port.addEventListener('message', (e) => {
            //     console.log('A页面共享线程counter值:', e.data)
            // })
        worker.port.onmessage = (e) => {
            console.log('A页面取到counter值:', e.data)
        }
        console.log(worker.port)
    </script>
</body>

</html>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>B页面</title>
</head>
<body>
    <h1>B页面</h1>
    <script>
        // 兼容性判断
        if (!SharedWorker) {
            throw new Error('当前浏览器不支持SharedWorker')
        }
        // 创建共享线程
        const worker = new SharedWorker('shareWk.js')
            // 启动线程端口
        worker.port.start()
            // 向共享线程发送消息
        worker.port.postMessage('get counter')
            // 线程监听消息
            // 线程监听消息 2种方式都可以
            // worker.port.addEventListener('message', (e) => {
            //     console.log('B页面共享线程counter值:', e.data)
            // })
        worker.port.onmessage = (e) => {
            console.log('B页面取到counter值:', e.data)
        }
    </script>
</body>
</html>

001a5be723d7ee7

SharedWorker调试注意点

SharedWorker不能直接在页面调试面板查看,需要浏览器输入 chrome://inspect 然后点击inspect查看

78ee54aa8f81388

JS数组有关如何使用reduce函数详解

胖蔡阅读(45)

reduce()方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

reduce() 方法有两个参数:

  • 第一个是回调函数;
  • 第二个是初始值;

回调函数

回调函数在数组的每个元素上执行。回调函数的返回值是累加结果,并作为下一次调用回调函数的参数提供。回调函数带有四个参数。

  • accumulator 累计器 ——累加器累加回调函数的返回值。
  • currentValue 当前值——处理数组的当前元素。
  • currentIndex 当前索引 (可选)
  • array 数组 (可选)

初始值

如果指定了初始值,则将累加器设置为 initialValue 作为初始元素。否则,将累加器设置为数组的第一个元素作为初始元素。
arr.reduce(callback(accumulator, currentValue[,index[,array]])[, initialValue])
下面的代码片段中,第一个累加器(accumulator)被分配了初始值0。currentValue 是正在处理的 numbersArr 数组的元素。在这里,currentValue 被添加到累加器,在下次调用回调函数时,会将返回值作为参数提供。
const numbersArr = [67, 90, 100, 37, 60];
const total = numbersArr.reduce(function(accumulator, currentValue){
     console.log("accumulator is " + accumulator + " current value is " + currentValue);
     return accumulator + currentValue;
}, 0);

console.log("total : "+ total);

输出:
accumulator is 0 current value is 67
accumulator is 67 current value is 90
accumulator is 157 current value is 100
accumulator is 257 current value is 37
accumulator is 294 current value is 60
total : 354


案例

1. 对数组的所有值求和

onst studentResult = [67, 90, 100, 37, 60];
const total = studentResult.reduce((accumulator, currentValue) => accumulator +currentValue, 0);
console.log(total); // 354
代码中,studentResult 数组具有5个数字。使用 reduce() 方法,将数组减少为单个值,该值将 studentResult 数组的所有值和结果分配给 total。

2. 对象数组中的数值之和

const studentResult = [
   { subject: '数学', marks: 78 },
   { subject: '物理', marks: 80 },
   { subject: '化学', marks: 93 }
];

const total = studentResult.reduce((accumulator, currentValue) => accumulator + currentValue.marks, 0);
console.log(total); // 251
我们从后端获取数据作为对象数组,因此,reduce() 方法有助于管理我们的前端逻辑。在下面的代码中,studentResult 对象数组有三个科目,这里,currentValue.marks 取了 studentResult 对象数组中每个科目的分数。

3. 展平数组(去扁平化)

const twoDArr = [ [1,2], [3,4], [5,6], [7,8] , [9,10] ];
const oneDArr = twoDArr.reduce((accumulator, currentValue) => accumulator.concat(currentValue));
console.log(oneDArr);
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
“展平数组”是指将多维数组转换为一维。在下面的代码中,twoDArr 2维数组被转换为 oneDArr 一维数组。此处,第一个 [1,2] 数组分配给累加器 accumulator,然后 twoDArr 数组的其余每个元素都连接到累加器。

4. 按属性分组对象

const result = [
   {subject: '物理', marks: 41},
   {subject: '化学', marks: 59},
   {subject: '高等数学', marks: 36},
  {subject: '应用数学', marks: 90},
   {subject: '英语', marks: 64},
];


let initialValue = {
   pass: [],
   fail: []
}


const groupedResult = result.reduce((accumulator, current) => {
   (current.marks >= 50) ? accumulator.pass.push(current) : accumulator.fail.push(current);
   return accumulator;
}, initialValue);

console.log(groupedResult);
根据对象的属性,我们可以使用 reduce() 方法将对象数组分为几组。通过下面的代码片段,你可以清楚地理解这个概念。这里,result 对象数组有五个对象,每个对象都有 subject 和 marks 属性。如果分数大于或等于50,则该主题通过,否则,主题失败。reduce() 用于将结果分组为通过和失败。首先,将 initialValue 分配给累加器,然后 push() 方法在检查条件之后将当前对象添加到 pass 和 fail 属性中作为对象数组。
输出:
{
   pass: [
    { subject: ‘化学’, marks: 59 },
    { subject: ‘应用数学’, marks: 90 },
    { subject: ‘英语’, marks: 64 }
   ],
  fail: [
    { subject: ‘物理’, marks: 41 },
    { subject: ‘高等数学’, marks: 36 }
  ]
}

5. 删除数组中的重复项

const duplicatedsArr = [1, 5, 6, 5, 7, 1, 6, 8, 9, 7];

const removeDuplicatedArr = duplicatedsArr.reduce((accumulator, currentValue) => {
  if(!accumulator.includes(currentValue)){
    accumulator.push(currentValue);
  }
  return accumulator;
}, []);

console.log(removeDuplicatedArr);
// [ 1, 5, 6, 7, 8, 9 ]
在下面的代码片段中,删除了 plicatedArr 数组中的重复项。首先,将一个空数组分配给累加器作为初始值。accumulator.includes() 检查 duplicatedArr 数组的每个元素是否已经在累加器中可用。如果 currentValue 在累加器中不可用,则使用 push() 将其添加。

如何使用docxjs?

胖蔡阅读(88)

docxjs是一款较为强大的docx格式文档处理的js库,基本可以处理我们遇到的所有有关于docx的文档问题。支持内置方式的word生成、解析、格式渲染。除此之外,我们还可以通过自定义方式直接通过xml方式渲染word包括但不限于字体样式、字体大小、段落样式、间距、颜色等一系列我们可能遇到的问题,下面我就把docxjs如何使用简单整理下,喜欢的朋友,帮忙点个广告支持小编,创作不易。

992c5e4ce0c46c8

 

安装

$npm install --save docx // 或者
$yarn add docx -D

 

基础使用

import * as fs from "fs";
import { Document, Packer, Paragraph, TextRun } from "docx";

// 文档由章节数组[sections]组成  
// 这里示例只是创建了一个章节
const doc = new Document({
    sections: [
        {
            properties: {},
            children: [
                new Paragraph({
                    children: [
                        new TextRun("Hello World"),
                        new TextRun({
                            text: "Foo Bar",
                            bold: true,
                        }),
                        new TextRun({
                            text: "\tGithub is the best",
                            bold: true,
                        }),
                    ],
                }),
            ],
        },
    ],
});

// Used to export the file into a .docx file
Packer.toBuffer(doc).then((buffer) => {
    fs.writeFileSync("My Document.docx", buffer);
});

// Done! My Document.docx文件就创建好了

 

docxjs中的基本概念

Document

Document是word文档的顶级对象,是最后生成.docx的最终封装类。您可以将所有的Paragraphs 添加到Document中,注意,一个.docx文档只有一个Document对象。

如何创建一个Document对象?

const doc = new docx.Document();

 

文档属性

您可以通过指定选项将属性添加到 Word 文档,例如:

const doc = new docx.Document({
    creator: "Dolan Miu",
    description: "My extremely interesting document",
    title: "My Document",
});

 

Document 配置属性

  • creator
  • description
  • title
  • subject
  • keywords
  • lastModifiedBy
  • revision
  • externalStyles
  • styles
  • numbering
  • footnotes
  • hyperlinks
  • background

如何使用配置项?

我们可以看下通过修改配置项实现改变document背景色的示例来了解下:

const doc = new docx.Document({
    background: {
        color: "C45911",
    },
});

 

Sections

每一个文档都是由多一个或者多个Sections构成的。一个Section是一组具有一组特定属性的Paragraphs ,这些属性用于定义将出现文本的页面。属性包括页面大小、页码、页面方向、页眉、边框和边距。例如,您可以有一个带有页眉和页脚的纵向部分,另一个没有页脚的横向部分,以及一个显示当前页码的页眉。

 

示例

const doc = new Document({
    sections: [{
        children: [
            new Paragraph({
                children: [new TextRun("Hello World")],
            }),
        ],
    }];
});

 

 

Section Type

设置节类型决定了节的内容相对于前一节的放置方式。例如,有五种不同的类型:

  • CONTINUOUS
  • EVEN_PAGE
  • NEXT_COLUMN
  • NEXT_PAGE
  • ODD_PAGE

 

const doc = new Document({
    sections: [{
        properties: {
            type: SectionType.CONTINUOUS,
        }
        children: [
            new Paragraph({
                children: [new TextRun("Hello World")],
            }),
        ],
    }];
});

 

 

Paragraph

docx文件中OpenXML 中的所有内容(文本、图像、图表等)都按段落组织。Paragraphs 需要对Sections的理解。

 

创建

import { Paragraph } from "docx";

const paragraph = new Paragraph("Short hand Hello World");

 

创建子组件

const paragraph = new Paragraph({
    children: [new TextRun("Lorem Ipsum Foo Bar"), new TextRun("Hello World"), new SymbolRun("F071")],
});

 

直接创建

const paragraph = new Paragraph({
    text: "Short hand notation for adding text.",
});

 

创建paragraph后需要将它添加到section上.

const doc = new Document({
    sections: [{
        children: [paragraph],
    }];
});

 

选项

这是段落的选项列表。详细解释如下:

Property 类型 强制的? 值集
text string 可选的
heading HeadingLevel 可选的 HEADING_1HEADING_2HEADING_3HEADING_4HEADING_5HEADING_6,TITLE
border IBorderOptions 可选的 topbottomleftright. 其中每一个都是 IBorderPropertyOptions 类型。单击此处查看示例
spacing ISpacingProperties 可选的 请参阅下面的 ISpacingProperties
outlineLevel number 可选的
alignment AlignmentType 可选的
heading HeadingLevel 可选的
bidirectional boolean 可选的
thematicBreak boolean 可选的
pageBreakBefore boolean 可选的
contextualSpacing boolean 可选的
indent IIndentAttributesProperties 可选的
keepLines boolean 可选的
keepNext boolean 可选的
children (TextRun or ImageRun or Hyperlink)[] 可选的
style string 可选的
tabStop { left?: ITabStopOptions; right?: ITabStopOptions; maxRight?: { leader: LeaderType; }; center?: ITabStopOptions } 可选的
bullet { level: number } 可选的
numbering { num: ConcreteNumbering; level: number; custom?: boolean } 可选的
widowControl boolean 可选的
frame IFrameOptions 可选的

文本

const paragraph = new Paragraph({
    text: "Hello World",
});

标题

将标题 1 段落设置为“Hello World”作为文本:

const paragraph = new Paragraph({
    text: "Hello World",
    heading: HeadingLevel.HEADING_1,
});


Border

使用IBorderPropertyOptions给Paragraph添加边框配置

 

topbottomleftright of the border

Property Type Notes
color string Required
space number Required
style string Required
size number Required

 

const paragraph = new Paragraph({
    text: "I have borders on my top and bottom sides!",
    border: {
        top: {
            color: "auto",
            space: 1,
            style: "single",
            size: 6,
        },
        bottom: {
            color: "auto",
            space: 1,
            style: "single",
            size: 6,
        },
    },
});

 

Shading

为整个段落块添加颜色

const paragraph = new Paragraph({
    text: "shading",
    shading: {
        type: ShadingType.REVERSE_DIAGONAL_STRIPE,
        color: "00FFFF",
        fill: "FF0000",
    },
});

 

 

Widow Control

允许第一行/最后一行显示在单独的页面上

const paragraph = new Paragraph({
    text: "shading",
    widowControl: true,
});

 

Spacing

通过ISpacingProperties配置Spacing属性

Property Type Notes Possible Values
after number Optional
before number Optional
line number Optional
lineRule LineRuleType Optional AT_LEASTEXACTLYAUTO
const paragraph = new Paragraph({
    text: "Paragraph with spacing before",
    spacing: {
        before: 200,
    },
});

 

Outline Level

const paragraph = new Paragraph({
    outlineLevel: 0,
});

 

更多详细配置请参考:paragraph:https://docx.js.org/#/usage/paragraph

建议参照mircsoft提供的docx规范进行参照学习: 开发 Office 解决方案

如何使用slatejs实现富文本编辑器

胖蔡阅读(81)

介绍

SlateJs是一个完全可定制的框架,用于构建富文本编辑器。它可让您构建丰富、直观的编辑器,例如MediumDropbox PaperGoogle Docs中的编辑器——这些编辑器正在成为网络应用程序的筹码——而您的代码库不会陷入复杂性的泥潭。
它可以做到这一点,因为它的所有逻辑都是用一系列插件实现的,所以你永远不会被“核心”中的内容所限制contenteditable您可以将其视为构建在React之上的可插拔实现。它的灵感来自Draft.jsProsemirrorQuill等库。
3369ec18b964c70

为什么要使用Slate?

在创建 Slate 之前,我尝试了很多其他的富文本库—— Draft.jsProsemirrorQuill等。我发现虽然让简单的示例工作起来很容易,但一旦你开始尝试构建类似的东西MediumDropbox PaperGoogle Docs,你遇到了更深层次的问题……
  • 编辑器的“模式”是硬编码的,很难定制。像粗体和斜体这样的东西是开箱即用的,但是评论,嵌入,甚至更多特定领域的需求呢?
  • 以编程方式转换文档非常复杂。以用户身份编写可能会奏效,但对构建高级行为至关重要的程序化更改是不必要的复杂。
  • 序列化为 HTML、Markdown 等似乎是事后才想到的。将文档转换为 HTML 或 Markdown 等简单的事情涉及编写大量样板代码,这似乎是非常常见的用例。
  • 重新发明视图层似乎效率低下且受到限制。大多数编辑都采用了自己的观点,而不是使用像 React 这样的现有技术,所以你必须学习一个带有新“陷阱”的全新系统。
  • 协作编辑不是预先设计的。通常,编辑器对数据的内部表示使其无法用于实时、协作编辑用例,除非基本上重写编辑器。
  • 存储库是整体的,不小且可重复使用。许多编辑器的代码库通常没有公开可能被开发人员重复使用的内部工具,导致不得不重新发明轮子。
  • 构建复杂的嵌套文档是不可能的。许多编辑器是围绕简单的“平面”文档设计的,使得表格、嵌入和标题等内容难以推理,有时甚至是不可能的。
当然,并不是每个编辑器都会出现所有这些问题,但是如果您尝试使用其他编辑器,您可能会遇到类似的问题。为了绕过他们 API 的限制并实现您所追求的用户体验,您必须求助于非常 hacky 的东西。有些经验是完全不可能实现的。
如果这听起来很熟悉,您可能会喜欢 Slate。
这让我想到了 Slate 如何解决所有这些问题……

Slatejs设计原则

Slate 试图通过以下几个原则来解决“为什么? ”的问题:
  • 一流的插件。Slate 最重要的部分是插件是一流的实体。这意味着您可以完全自定义编辑体验,构建像 Medium 或 Dropbox 这样的复杂编辑器,而无需与库的假设作斗争。
  • 无模式核心。Slate 的核心逻辑很少假设您将要编辑的数据的模式,这意味着当您需要超越最基本的用例时,库中没有任何假设会绊倒您。
  • 嵌套文档模型。Slate 使用的文档模型是嵌套的递归树,就像 DOM 本身一样。这意味着可以为高级用例创建复杂的组件,如表格或嵌套块引号。但也很容易通过只使用一个层次结构来保持简单。
  • 与 DOM 平行。Slate 的数据模型基于 DOM — 文档是一个嵌套树,它使用选择和范围,并且公开所有标准事件处理程序。这意味着诸如表格或嵌套块引号之类的高级行为是可能的。您可以在 DOM 中执行的几乎所有操作,都可以在 Slate 中执行。
  • 直观的命令。Slate 文档是使用“命令”进行编辑的,这些命令被设计为高级且非常直观的读写,因此自定义功能尽可能具有表现力。这极大地提高了您推理代码的能力。
  • 协作就绪的数据模型。Slate 使用的数据模型(特别是如何将操作应用于文档)旨在允许协作编辑在顶部进行分层,因此如果您决定让您的编辑器协作,则无需重新考虑所有内容。
  • 清除“核心”界限。借助插件优先架构和无模式核心,“核心”和“自定义”之间的界限变得更加清晰,这意味着核心体验不会在边缘情况下陷入困境。

示例

1、 纯文本示例

import React, { useMemo } from 'react'
import { createEditor, Descendant } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'
import { withHistory } from 'slate-history'

const PlainTextExample = () => {
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
return (
<Slate editor={editor} value={initialValue}>
<Editable placeholder="Enter some plain text..." />
</Slate>
)
}

const initialValue: Descendant[] = [
{
type: 'paragraph',
children: [
{ text: 'This is editable plain text, just like a <textarea>!' },
],
},
]

export default PlainTextExample

2、富文本示范

import React, { useCallback, useMemo } from 'react'
import isHotkey from 'is-hotkey'
import { Editable, withReact, useSlate, Slate } from 'slate-react'
import {
Editor,
Transforms,
createEditor,
Descendant,
Element as SlateElement,
} from 'slate'
import { withHistory } from 'slate-history'

import { Button, Icon, Toolbar } from '../components'

const HOTKEYS = {
'mod+b': 'bold',
'mod+i': 'italic',
'mod+u': 'underline',
'mod+`': 'code',
}

const LIST_TYPES = ['numbered-list', 'bulleted-list']
const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify']

const RichTextExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const editor = useMemo(() => withHistory(withReact(createEditor())), [])

return (
<Slate editor={editor} value={initialValue}>
<Toolbar>
<MarkButton format="bold" icon="format_bold" />
<MarkButton format="italic" icon="format_italic" />
<MarkButton format="underline" icon="format_underlined" />
<MarkButton format="code" icon="code" />
<BlockButton format="heading-one" icon="looks_one" />
<BlockButton format="heading-two" icon="looks_two" />
<BlockButton format="block-quote" icon="format_quote" />
<BlockButton format="numbered-list" icon="format_list_numbered" />
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
<BlockButton format="left" icon="format_align_left" />
<BlockButton format="center" icon="format_align_center" />
<BlockButton format="right" icon="format_align_right" />
<BlockButton format="justify" icon="format_align_justify" />
</Toolbar>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder="Enter some rich text…"
spellCheck
autoFocus
onKeyDown={event => {
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event as any)) {
event.preventDefault()
const mark = HOTKEYS[hotkey]
toggleMark(editor, mark)
}
}
}}
/>
</Slate>
)
}

const toggleBlock = (editor, format) => {
const isActive = isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
)
const isList = LIST_TYPES.includes(format)

Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
LIST_TYPES.includes(n.type) &&
!TEXT_ALIGN_TYPES.includes(format),
split: true,
})
let newProperties: Partial<SlateElement>
if (TEXT_ALIGN_TYPES.includes(format)) {
newProperties = {
align: isActive ? undefined : format,
}
} else {
newProperties = {
type: isActive ? 'paragraph' : isList ? 'list-item' : format,
}
}
Transforms.setNodes<SlateElement>(editor, newProperties)

if (!isActive && isList) {
const block = { type: format, children: [] }
Transforms.wrapNodes(editor, block)
}
}

const toggleMark = (editor, format) => {
const isActive = isMarkActive(editor, format)

if (isActive) {
Editor.removeMark(editor, format)
} else {
Editor.addMark(editor, format, true)
}
}

const isBlockActive = (editor, format, blockType = 'type') => {
const { selection } = editor
if (!selection) return false

const [match] = Array.from(
Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n[blockType] === format,
})
)

return !!match
}

const isMarkActive = (editor, format) => {
const marks = Editor.marks(editor)
return marks ? marks[format] === true : false
}

const Element = ({ attributes, children, element }) => {
const style = { textAlign: element.align }
switch (element.type) {
case 'block-quote':
return (
<blockquote style={style} {...attributes}>
{children}
</blockquote>
)
case 'bulleted-list':
return (
<ul style={style} {...attributes}>
{children}
</ul>
)
case 'heading-one':
return (
<h1 style={style} {...attributes}>
{children}
</h1>
)
case 'heading-two':
return (
<h2 style={style} {...attributes}>
{children}
</h2>
)
case 'list-item':
return (
<li style={style} {...attributes}>
{children}
</li>
)
case 'numbered-list':
return (
<ol style={style} {...attributes}>
{children}
</ol>
)
default:
return (
<p style={style} {...attributes}>
{children}
</p>
)
}
}

const Leaf = ({ attributes, children, leaf }) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}

if (leaf.code) {
children = <code>{children}</code>
}

if (leaf.italic) {
children = <em>{children}</em>
}

if (leaf.underline) {
children = <u>{children}</u>
}

return <span {...attributes}>{children}</span>
}

const BlockButton = ({ format, icon }) => {
const editor = useSlate()
return (
<Button
active={isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
)}
onMouseDown={event => {
event.preventDefault()
toggleBlock(editor, format)
}}
>
<Icon>{icon}</Icon>
</Button>
)
}

const MarkButton = ({ format, icon }) => {
const editor = useSlate()
return (
<Button
active={isMarkActive(editor, format)}
onMouseDown={event => {
event.preventDefault()
toggleMark(editor, format)
}}
>
<Icon>{icon}</Icon>
</Button>
)
}

const initialValue: Descendant[] = [
{
type: 'paragraph',
children: [
{ text: 'This is editable ' },
{ text: 'rich', bold: true },
{ text: ' text, ' },
{ text: 'much', italic: true },
{ text: ' better than a ' },
{ text: '<textarea>', code: true },
{ text: '!' },
],
},
{
type: 'paragraph',
children: [
{
text:
"Since it's rich text, you can do things like turn a selection of text ",
},
{ text: 'bold', bold: true },
{
text:
', or add a semantically rendered block quote in the middle of the page, like this:',
},
],
},
{
type: 'block-quote',
children: [{ text: 'A wise quote.' }],
},
{
type: 'paragraph',
align: 'center',
children: [{ text: 'Try it out for yourself!' }],
},
]

export default RichTextExample

3、MarkDown示范

import Prism from 'prismjs'
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Text, createEditor, Descendant } from 'slate'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'

// eslint-disable-next-line
;Prism.languages.markdown=Prism.languages.extend("markup",{}),Prism.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},code:[{pattern:/^(?: {4}|\t).+/m,alias:"keyword"},{pattern:/``.+?``|`[^`\n]+`/,alias:"keyword"}],title:[{pattern:/\w+.*(?:\r?\n|\r)(?:==+|--+)/,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:/(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^\*\*|^__|\*\*$|__$/}},italic:{pattern:/(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^[*_]|[*_]$/}},url:{pattern:/!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/,inside:{variable:{pattern:/(!?\[)[^\]]+(?=\]$)/,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),Prism.languages.markdown.bold.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.italic.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.bold.inside.italic=Prism.util.clone(Prism.languages.markdown.italic),Prism.languages.markdown.italic.inside.bold=Prism.util.clone(Prism.languages.markdown.bold); // prettier-ignore

const MarkdownPreviewExample = () => {
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const decorate = useCallback(([node, path]) => {
const ranges = []

if (!Text.isText(node)) {
return ranges
}

const getLength = token => {
if (typeof token === 'string') {
return token.length
} else if (typeof token.content === 'string') {
return token.content.length
} else {
return token.content.reduce((l, t) => l + getLength(t), 0)
}
}

const tokens = Prism.tokenize(node.text, Prism.languages.markdown)
let start = 0

for (const token of tokens) {
const length = getLength(token)
const end = start + length

if (typeof token !== 'string') {
ranges.push({
[token.type]: true,
anchor: { path, offset: start },
focus: { path, offset: end },
})
}

start = end
}

return ranges
}, [])

return (
<Slate editor={editor} value={initialValue}>
<Editable
decorate={decorate}
renderLeaf={renderLeaf}
placeholder="Write some markdown..."
/>
</Slate>
)
}

const Leaf = ({ attributes, children, leaf }) => {
return (
<span
{...attributes}
className={css`
font-weight: ${leaf.bold && 'bold'};
font-style: ${leaf.italic && 'italic'};
text-decoration: ${leaf.underlined && 'underline'};
${leaf.title &&
css`
display: inline-block;
font-weight: bold;
font-size: 20px;
margin: 20px 0 10px 0;
`}
${leaf.list &&
css`
padding-left: 10px;
font-size: 20px;
line-height: 10px;
`}
${leaf.hr &&
css`
display: block;
text-align: center;
border-bottom: 2px solid #ddd;
`}
${leaf.blockquote &&
css`
display: inline-block;
border-left: 2px solid #ddd;
padding-left: 10px;
color: #aaa;
font-style: italic;
`}
${leaf.code &&
css`
font-family: monospace;
background-color: #eee;
padding: 3px;
`}
`}
>
{children}
</span>
)
}

const initialValue: Descendant[] = [
{
type: 'paragraph',
children: [
{
text:
'Slate is flexible enough to add **decorations** that can format text based on its content. For example, this editor has **Markdown** preview decorations on it, to make it _dead_ simple to make an editor with built-in Markdown previewing.',
},
],
},
{
type: 'paragraph',
children: [{ text: '## Try it out!' }],
},
{
type: 'paragraph',
children: [{ text: 'Try it out for yourself!' }],
},
]

export default MarkdownPreviewExample

4、更多

更多请移步GitHub:https://github.com/ianstormtaylor/slate/edit/main/site/examples

前端缓存实现介绍

胖蔡阅读(63)

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术

缓存的种类

我相信只要你经常使用某个浏览器(Chrome,Firefox,IE等),肯定知道这些浏览器在设置里面都是有个清除缓存功能,这个功能存在的作用就是删除存储在你本地磁盘上资源副本,也就是清除缓存。 缓存存在的意义就是当用户点击back按钮或是再次去访问某个页面的时候能够更快的响应。尤其是在多页应用的网站中,如果你在多个页面使用了一张相同的图片,那么缓存这张图片就变得特别的有用。

代理服务器缓存

代理服务器缓存原理和浏览器端类似,但规模要大得多,因为是为成千上万的用户提供缓存机制,大公司和大型的ISP提供商通常会将它们设立在防火墙上或是作为一个独立的设备来运营。 由于缓存服务器不是客户端或是源服务器的一部分,它们存在于网络中,请求路由必须经过它们才会生效,所以实际上你可以去手动设置浏览器的代理,或是通过一个中间服务器来进行转发,这样用户自然就察觉不到代理服务器的存在了。 代理服务器缓存就是一个共享缓存,不只为一个用户服务,经常为大量用户使用,因此在减少相应时间和带宽使用方面很有效:因为同一个缓存可能会被重用多次。

网关缓存

也被称为代理缓存或反向代理缓存,网关也是一个中间服务器,网关缓存一般是网站管理员自己部署,从让网站拥有更好的性能。🙂 CDNS(网络内容分发商)分布网关缓存到整个(或部分)互联网上,并出售缓存服务给需要的网站,比如国内的七牛云、又拍云都有这种服务。

数据库缓存

数据库缓存是指当我们的应用极其复杂,表自然也很繁杂,我们必须进行频繁的进行数据库查询,这样可能导致数据库不堪重负,一个好的办法就是将查询后的数据放到内存中,下一次查询直接从内存中取就好了。关于数据库缓存本篇不会展开。​

浏览器的缓存策略

浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。

Age:23146
Cache-Control:max-age=2592000
Date:Tue, 28 Nov 2021 12:26:41 GMT
ETag:W/"5a1cf09a-63c6"
Expires:Thu, 28 Dec 2021 05:27:45 GMT
Last-Modified:Tue, 28 Nov 2017 05:14:02 GMT
Vary:Accept-Encoding

强缓存阶段

以上请求头来自百度首页某个CSS文件的响应头。去除了一些和缓存无关的字段,只保留了以上部分。Expires是HTTP/1.0中的定义缓存的字段,它规定了缓存过期的一个绝对时间。Cache-Control:max-age=2592000是HTTP/1.1定义的关于缓存的字段,它规定了缓存过期的一个相对时间。 这就是强缓存阶段,当浏览器再次试图访问这个CSS文件,发现有这个文件的缓存,那么就判断根据上一次的响应判断是否过期,如果没过期,使用缓存。否则重新加载文件。

协商缓存阶段

利用ETag和Last-Modified这两个字段浏览器可以进入协商缓存阶段,当浏览器再次试图访问这个CSS文件,发现缓存过期,于是会在本次请求的请求头里携带If-Moified-Since和If-None-Match这两个字段,服务器通过这两个字段来判断资源是否有修改,如果有修改则返回状态码200和新的内容,如果没有修改返回状态码304,浏览器收到200状态码,该咋处理就咋处理(相当于首次访问这个文件了),发现返回304,于是知道了本地缓存虽然过期但仍然可以用,于是加载本地缓存。然后根据新的返回的响应头来设置缓存。

If-Moified-Since: Tue, 28 Nov 2017 05:14:02 GMT
If-None-Match: W/"5a1cf09a-63c6"

启发式缓存阶段

Age:23146
Cache-Control: public
Date:Tue, 28 Nov 2017 12:26:41 GMT
Last-Modified:Tue, 28 Nov 2017 05:14:02 GMT
Vary:Accept-Encoding

根据响应头中2个时间字段 Date 和 Last-Modified 之间的时间差值,取其值的10%作为缓存时间周期。 这就是启发式缓存阶段。这个阶段很容让人忽视,但实际上每时每刻都在发挥着作用。所以在今后的开发过程中如果遇到那种默认缓存的坑,不要生气,浏览器只是在遵循启发式缓存协议而已。

5c8d4448d9d129e

浏览器缓存控制

Cache-Control

请求指令
指令 参数 说明
no-cache 强制源服务器再次验证
no-store 不缓存请求或是响应的任何内容
max-age=[秒] 缓存时长,单位是秒 缓存的时长,也是响应的最大的Age值
min-fresh=[秒] 必需 期望在指定时间内响应仍然有效
no-transform 代理不可更改媒体类型
only-if-cached 从缓存获取
cache-extension 新的指令标记(token)
响应指令
指令 参数 说明
public 任意一方都能缓存该资源(客户端、代理服务器等)
private 可省略 只能特定用户缓存该资源
no-cache 可省略 缓存前必须先确认其有效性
no-store 不缓存请求或响应的任何内容
no-transform 代理不可更改媒体类型
must-revalidate 可缓存但必须再向源服务器进确认
proxy-revalidate 要求中间缓存服务器对缓存的响应有效性再进行确认
max-age=[秒] 缓存时长,单位是秒 缓存的时长,也是响应的最大的Age值
s-maxage=[秒] 必需 公共缓存服务器响应的最大Age值
cache-extension 新指令标记(token)

服务端缓存控制

当Expires和Cache-Control:max-age=xxx同时存在的时候取决于缓存服务器应用的HTTP版本。应用HTTP/1.1版本的服务器会优先处理max-age,忽略Expires,而应用HTTP/1.0版本的缓存服务器则会优先处理Expires而忽略max-age。

用户操作行为对缓存的影响

操作 说明
打开新窗口 如果指定cache-control的值为private、no-cache、must-revalidate,那么打开新窗口访问时都会重新访问服务器。而如果指定了max-age值,那么在此值内的时间里就不会重新访问服务器,例如:Cache-control: max-age=5 表示当访问此网页后的5秒内不会去再次访问服务器.
在地址栏回车 如果值为private或must-revalidate,则只有第一次访问时会访问服务器,以后就不再访问。如果值为no-cache,那么每次都会访问。如果值为max-age,则在过期之前不会重复访问。
按后退按扭 如果值为private、must-revalidate、max-age,则不会重访问,而如果为no-cache,则每次都重复访问.
按刷新按扭 无论为何值,都会重复访问.(可能返回状态码:200、304,这个不同浏览器处理是不一样的,FireFox正常,Chrome则会启用缓存(200 from cache))
按强制刷新按钮 当做首次进入重新请求(返回状态码200)

HTML5的缓存

这部分准确的说应该叫离线存储。现在比较普遍用的是Appcache,但Appcache已经从web标准移除了,在可预见的未来里,ServiceWorker可能会是一个比较适合的解决方案。

Appcache

这是HTML5的一个新特性,通过离线存储达到用户在没有网络连接的情况下也能访问页面的功能。离线状态下即使用户点击刷新都能正常加载文档。 使用方法如下,在HTML文件中引入appcache文件:

<!DOCTYPE html>
<html manifest="manifest.appcache">
<head>
  <meta charset="UTF-8">
  <title>***</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

web 应用中的 manifest 特性可以指定为缓存清单文件的相对路径或一个绝对 URL(绝对 URL 必须与应用同源)。缓存清单文件可以使用任意扩展名,但传输它的 MIME 类型必须为 text/cache-manifest。 下面是一个完整的缓存清单文件的示例。

CACHE MANIFEST
# 注释:需要缓存的文件,无论在线与否,均从缓存里读取
# v1 2017-11-30
# This is another comment
/static/logo.png

# 注释:不缓存的文件,始终从网络获取
NETWORK:
example.js

# 注释:获取不到资源时的备选路径,如index.html访问失败,则返回404页面
FALLBACK:
index.html 404.html

**注意:**主页一定会被缓存起来的,因为AppCache主要是用来做离线应用的,如果主页不缓存就无法离线查看了,因此把index.html添加到NETWORK中是不起效果的。 实际上这个特性已经web标准中删除,但现在为止还有很多浏览器支持它,所以这里提一下。 AppCache之所以不受待见我想了下面几个原因:

  1. 一旦使用了manifest后,没办法清空这些缓存,只能更新缓存,或者得用户自己去清空浏览器的缓存;
  2. 假如更新的资源中有一个资源更新失败了,那么所有的资源就会全部更新失败,将用回上一版本的缓存
  3. 主页会被强制缓存(使用了manifest的页面),并且无法清除
  4. appache文件可能会无法被及时更新,因为各大浏览器对于appcache文件的处理方式不同
  5. 以上几个弊端一旦出问题,会让用户抓狂更会让开发者抓狂

实现html页面如何不缓存js

不缓存JS的方法其实挺简单,下面给出代码。 原理就是可以给页面后面设定个不同的值,让页面保持没错访问的不同即可达到不缓存的目的了。但是这种方式解决不了代理服务器缓存。

<script>
    document.write("<script type='text/javascript' src='/storage/exam/js/js_url.js?ver="+Math.random()+"'><\/script>");
</script>

完美的兼容性好的前端文件下载方式探讨

胖蔡阅读(48)

在项目中,经常会遇到文件下载的需求。一开始,都是copy既有项目的下载方法,用于自己的项目。但是,渐渐发现,copy过来的方法,有时会出现这样那样的问题,而且不同的项目,用的方法又是不一样的,没有一个通用的方法。

故感觉自己在文件下载这块,思绪还是乱的,没有一个清晰且系统概念。所以想通过本篇文章,好好地整理一下。

我们下载的文件,通常有三种形式:

  • 有明确地址路径的文件
  • 二进制数据文件
  • base64文件

而根据浏览器的特性,又可以分为:

  • 浏览器可直接浏览的文件,如txt、png、jpg、gif等格式的文件
  • 浏览器不能直接浏览的文件,如doc、excel、zip等格式的文件

在下载方式这块,针对不同文件类型,常用的方式也不同。

一、对于有明确地址路径的文件进行下载

  1. window API 实现下载
window.open(fileUrl)

window.location.href = fileUrl

这两种方式虽然简单方便直接,但是浏览器能直接打开的文件只能预览,不能下载。

  1. form表单实现下载
const fromObj = document.createElement('form')
fromObj.action = fileUrl
formObj.method = 'get'
formObj.style.display = 'none'
const formItem = document.createElement('input')
formItem.value = fileName
formItem.name = 'fileName'
formObj.appendChild(formItem)
document.body.appendChild(formObj)
formObj.submit()
document.body.removeChild(formObj)

这是以前常用的传统方式,利用表单提交的功能来实现文件的下载。兼容性好,但是也无法下载浏览器能直接预览的文件。

  1. a标签实现下载
<a href="fileUrl"></a>

如果仅仅这样写,对于浏览器能直接打开的文件也是只能预览,不能下载。

要想能够直接下载浏览器能直接打开的文件,可以利用download属性。

<a href="fileUrl" download="fileName"></a>

但download属性实现浏览器可预览文件下载也有限制:

  • 同源,即所要下载的文件与当前页面同源。
  • 非IE浏览器。

也就是说,如果不同源或是IE浏览器,即使加了download属性,也不能实现浏览器可预览文件的下载。

二、对于二进制数据文件进行下载

downFile(params).then(res=> {
 
      const fileName = res.headers['content-disposition'].split('=')[1];
 
      const data = res.data;
 
      const blob = new Blob([data]);
      
      //如果浏览器不支持download属性(也就是使用IE10及以上的时候,使用msSaveOrOpenBlob方法,但IE10以下也不支持msSaveOrOpenBlob
      
      if(window.navigator.msSaveOrOpenBlob) {
      
        window.navigator.msSaveOrOpenBlob(blob, fileName )
        return
      }
      //
      const a = document.createElement('a');
 
      const href = window.URL.createObjectURL(blob); // 创建下载的链接
 
      a.href = href;
 
      a.download = decodeURI(fileName); // 下载后文件名
 
      document.body.appendChild(a);
 
      a.click(); // 点击下载
 
      document.body.removeChild(a); // 下载完成移除元素
 
      window.URL.revokeObjectURL(href); // 释放掉blob对象

这种方式主要将文件流转换成Blob对象,并利用URL.createObjectURL生成url地址,然后再利用a标签下载。

但这种同样存在兼容性问题,IE10以下不可用。

三、对于base64文件进行下载

downFile(fileBase64) {
      // fileBase64是获取到的图片base64编码
      const imgUrl = `data:image/png;base64,${fileBase64}`
      if (window.navigator.msSaveOrOpenBlob) {
        const bstr = atob(imgUrl.split(',')[1])
        let n = bstr.length
        const u8arr = new Uint8Array(n)
        while (n--) {
          u8arr[n] = bstr.charCodeAt(n)
        }
        const blob = new Blob([u8arr])
        window.navigator.msSaveOrOpenBlob(blob, fileName + '.' + 'png')
      } else {
        const a = document.createElement('a')
        a.href = imgUrl
        a.setAttribute('download', fileName)
        a.click()
      }
}

IE10以下的兼容性问题依然存在。

以上文件下载方式,或多或少都存在一些限制或兼容性问题,有没有完美的解决方案,欢迎大佬们指教!

如何给web页面添加一个水印

胖蔡阅读(106)

水印原理

我们经常看到很多网站图片上有水印信息,那么如何也在我们自己的网站上添加水印呢?其实原理很简单,接下来我们来简单了解下实现水印的思路:

  • canvas绘制水印文字
  • 创建一个顶层div,并将canvas放在上面
  • js插入水印层div

实现水印

这里就不做过多的解释和铺垫了,咱直接上代码:

1、 添加水印实现

// 页面添加水印效果
const setWatermark = (str) => {
    const id = '1.99654.234';
    if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id));
    const can = document.createElement('canvas');
    can.width = 200;
    can.height = 130;
    const cans = can.getContext('2d');
    cans.rotate((-20 * Math.PI) / 180);
    cans.font = '12px Vedana';
    cans.fillStyle = 'rgba(200, 200, 200, 0.30)';
    cans.textBaseline = 'Middle';
    cans.fillText(str, can.width / 10, can.height / 2);
    const div = document.createElement('div');
    div.id = id;
    div.style.pointerEvents = 'none';
    div.style.top = '15px';
    div.style.left = '0px';
    div.style.position = 'fixed';
    div.style.zIndex = '10000000';
    div.style.width = `${document.documentElement.clientWidth}px`;
    div.style.height = `${document.documentElement.clientHeight}px`;
    div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`;
    document.body.appendChild(div);
    return id;
};

2、水印添加和删除

 

/**

 * 页面添加水印效果
 * @method set 设置水印
 * @method del 删除水印
 */
const watermark = {

    // 设置水印
    set: (str) => {
        let id = setWatermark(str);
        if (document.getElementById(id) === null) id = setWatermark(str);
    },

    // 删除水印
    del: () => {
        let id = '1.99654.234';
        if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id));
    },
};

 

3、水印使用

// 开启水印
watermark.set('Enjoytoday.cn')

// 关闭水印
watermark.del()

 

4、效果

e47d5c671aaeff4

 

968ff8836572f97

如何在vue中引入外部的js库

胖蔡阅读(172)

前段时间有朋友问如何在vue中引入外部的js文件。乍一听觉得奇怪,不是可以通过yarn,npm工具去下载依赖引入吗?仔细了解才知道有些三方的JS比较古早,并不支持vue的方式去模块化引又或者远程环境容易被墙,我就想要直接引用下载好的三方库,这时候我们该如何引入呢?

 

Html中引入

最简单的方式就比较简单粗暴了,直接在项目中的index.html中引入三方js。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="user-scalable=0,width=device-width, initial-scale=1.0">
  <!-- 引入iconfont图标 -->
  <script src="<%= BASE_URL %>iconfont.js" defer ></script>
  <link rel="stylesheet" href="<%= BASE_URL %>iconfont.css" />
  <title>
    <%= webpackConfig.name %>
  </title>
</head>
<body>
  <noscript>
    <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it
        to continue.</strong>
  </noscript>
  <div id="app"></div>
  <!-- built files will be auto injected -->
  <script></script>
</body>
</html>

通过defer实现js的异步加载,等待文件下载完成后才执行该js,若存在多个js的话也会根据顺序去执行。

 

VUE中通过JS手动插入DOM

我们可以根据Vue的加载周期通过js手动创建一个script标签并插入到dom中,这样等dom更新的时候就会自动更新下载了。

export default {
  mounted() {
      let script = document.createElement(‘script’);
      script.type = ‘text/javascript’;
      script.src = ‘//at.alicdn.com/t/font_2741962_jliv7rnraus.js’;
      document.body.appendChild(script);
      script.onload = ()=>{
          // js加载成功
      }
      script.onerror = ()=>{
            // js加载失败
      }

   },
}

 

如上,在插入下载结束后通过回调监听是否下载成功。

 

创作不易,感觉对你有用的话就点下广告支持下作者呗!