HTML5 进阶系列:文件上传下载
如何用 JavaScript 下载文件
vue+axios上传文件
axios全攻略
小tips: 纯前端JS读取与解析本地文本类文件
通常思路
隐藏掉很丑的 input type="file"
,在自定义的上传按钮上绑定点击事件,通过 id 调用这个 input ,然后 .click()
,在这个 input 的 change
事件内获取到 event.target.file
做各种操作
axios
let param = new FormData(); // 创建form对象
param.append('后台要你传的文件参数属性名', file, fileName); // 通过append向form对象添加数据
param.append('其他参数属性名', '其他参数数据');
// console.log(param.get('file')); // FormData私有类对象,访问不到,可以通过get判断值是否传进去
let config = {
headers: {'Content-Type': 'multipart/form-data'}
};
axios.post(url, param, config);
上传进度条
progress
事件onUploadProgress
事件,在 config
里定义中断上传
abort
方法cancelToken
属性,在 config
里定义七牛文件上传重名文件
scope
属性),可以设置同名而内容不一样的文件上传时是覆盖还是不允许上传.?attname=
后面加指定的文件名, 把未传 key 的文件下载成指定名称的文件上传前检测图片的宽高,大小
let file = event.target.files[0];
let reader = new FileReader();
reader.onload = (e) => {
let img = new Image();
img.src = e.target.result;
img.onload = () => {
if (img.width >= 300 && img.height >= 300) {
if (file.size <= Math.pow(1024, 2)) {
// 大小,宽高都符合要求,可以上传了
} else {
// 报错提示图片最大 1M
}
} else {
// 报错提示图片宽高不符合要求(300x300)
}
}
}
reader.readAsDataURL(file);
fetch
fetch(path).then(response => {
return response.blob();
}).then(blob => {
let a = document.createElement('a');
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = name;
a.click();
window.URL.revokeObjectURL(url);
});
download
<a href="url" download="文件名.后缀">文字</a>
后台设置了打开即下载
window.open(`地址[?参数]`, '_parent');
form 提交法
**content-disposition:attachment **
content-type: application/octet-stream
七牛云直接设置 content-disposition
window.open(`${URL}?attname=${name}`);
使用 axios 库, 请求时设置
responseType: 'blob',
withCredentials: false,
返回的数据就是文件的 blob 数据, 要想下载, 可以 URL.createObjectURL() 生成 url , 点击下载
或者不使用 axios 自己原生 XMLHttpRequest 实现
FileReader
对象 + readAsDataURL
方法document.createElement
创建 a 标签, display: none
, 设置好 href
和 target
, document.appendChild
, .click()
后就 removeChild
window.open
方法FileReader
对象 + readAsText
方法https://view.officeapps.live.com/op/view.aspx?src=${文件路径}
获取预览 URL ,在新页面打开如果是 textArea 或 input Text 之类的可以使用 select
方法选中文本的 DOM 元素
DOM元素.select();
if (document.execCommand('Copy')) {
// 提示已经复制
} else {
// 提示不能复制
}
如果不是
设置一个隐藏的可以 select()
的 DOM 元素,用户点击复制按钮或者其他操作时,动态把目标文字复制给隐藏 DOM 元素的 value
,随后同上.
DOM 事件
监控 paste
事件, 从 event.clipboardData.files
(或 event.clipboardData.items
) 中取文件, clipboardData
是一个 DataTransfer
对象
注意:
参考:
ClipboardEvent.clipboardData
拖拽献祭中的黑山羊-DataTransfer对象
监控 drop
事件, 从 event.dataTransfer.files
(或 event.dataTransfer.items
) 中取文件
注意:
参考:
DataTransfer
拖拽献祭中的黑山羊-DataTransfer对象
访问网站 ngrok.cc , 按着网站上的教程来,选择免费服务器,一般来说就可以了
但是这次用的时候报错了
Webpack
出现 Invalid Host header
错误 ,可将 webpack-dev-server
disableHostCheck
设置为true
直接在 webpack
配置文件中 module.exports
的对象中加上 devServer
属性,值是对象,在里面设置 true
另有其他方法
vue-cli 新建的项目, 运行时因为 vue-cli-service server
, 运行成功后会有
App running at:
- Local: http://localhost:8080/...
- Network: http://xxxxxxx:8080/...
Network 的地址可以给同一局域网下的其他用户看, 比如前端改了个页面显示效果, 发这个网址让同一公司网络下的 UI 验收
参考文章:
浏览器同源政策及其规避方法
场景描述:
两个 里包着同一个
Vue
项目的两个页面,在一个页面点击了,传递参数过去到另一个页面
跳到另一个页面是通过 DOM 取到左边目录所有目录项的路径属性,检测到第一个符合另一个页面的路径就手动 click 该目录项一下,就切换到了另一个
初步实现:
通过 sessionStorage
+ 每次切换标签到这两个 Vue
页面就刷新该页面(启动生命周期钩子,检查 sessionStorage
是否有传值),初步实现了数据传递
后续思路:
有天在群里有人提到了类似的问题,大佬们就提出了一些思路:
最后实现:
本来这个很令人捉急的项目结构只是项目重构完成前给用户凑合着用的,而且几乎没人用到这个模块,但是领导坚持要让他手下另一个公司当用户使用这部分功能,然后他们反映这样每次一切换这两个就刷新,体验很差,于是重改
使用 postMessage
, 失败,不会玩
使用 onhashChange
+ sessionStorage
// 具体的做法各有不同,但是思想基本上就是在该页面第一次加载的时候 mounted 钩子中加上 onhashchange 函数
window.onhashchange = (val, oldVal) => {
if (top.window.sessionStorage.getItem('另一个页面存入的数据属性名')) {
if (this.view === '想要显示的页面组件') {
this.view = undefined;
this.$nextTick(() => {
this.view = '想要显示的页面组件'; // 刷新
});
} else {
this.view = '想要显示的页面组件';
}
}
}
如题, 找"监控 div 的 resize 事件" 时, 发现 iview 框架中也用了这个
直接装 npm 就行
编辑器用的 webStorm , 正则搜索替换感觉不错, 但它的搜索界面一次只能显示出前100个匹配结果, 此时可以点击搜索界面右下角 OPEN IN FIND WINDOW
展示出全部匹配结果
/^[a-z0-9]+([._-]*[a-z0-9])*@([a-z0-9]+[-.a-z0-9]*[a-z0-9]+\.){1,63}[a-z0-9]+$/i
[效果查看](https://jex.im/regulex/#!flags=&re=%5E%5Ba-z0-9%5D%2B(%5B._-%5D*%5Ba-z0-9%5D)%40(%5Ba-z0-9%5D%2B%5B-.a-z0-9%5D%5Ba-z0-9%5D%2B%5C.)%7B1%2C63%7D%5Ba-z0-9%5D%2B%24
注意: 邮箱的格式千奇百怪, 有的邮箱甚至还可以输入中文, 这个只能做参考, 还有一个简易版
/^\w+?@\S+?\.[A-Za-z\d]{2,4}$/
/^((\+\d{1,3}[- _]?\(?\d\)?[- _]?\d{1,5})|(\(?\d{2,6}\)?))[- _]?(\d{3,4})[- _]?(\d{4})(( x| ext)\d{1,5})?$/
同事给的, 然后改 BUG 遇到一个有 _
的奇葩电话号码, 就把 _
也加进去了
/^(. )*?(?!.*(console\.\w+\(('|")|<!-- ?|/\* ?|// ?)).*[\u4e00-\u9fa5]/
这个是全局搜索这类中文, 给网站做多语言用的
一般我就用的 (?!vue)
这种
/(.*?)/
// 再要加上换行和空格就是
/([.\s\S]*?)/
一般我用这个来替换, .*
任意长度的任意字符, ?
是不贪婪匹配, 只匹配自己要的, 打 ()
是为了匹配, 在 webStorm 里用 $数字
替换, 蛮方便的
flat函数
我的运用:
async await return new promise 笔记
原理是: 利用 sortable 配置的 onEnd 函数和 sortable 实例的 toArray 方法, 将不能移动的选项在移动完成后把它又放回到原来的位置;
代码:
endFunc(e) {
// onEnd: endFunc
if (e.newIndex !== e.oldIndex) {
// 发生了移动
let sortedArr = this.sortInstance.toArray(); // sortable 实例 toArray 方法, 导出顺序数组(移动后的顺序)
sortedArr.splice(e.oldIndex, 0, ...sortedArr.splice(e.newIndex, 1)); // 按旧顺序重组数组
if (e.newIndex >= this.boundEnd) { // 移动到底部 "不可移动选项" 之下
this.sortInstance.sort(sortedArr); // 按旧顺序重新排列
} else {
this.list_options.splice(e.newIndex, 0, ...this.list_options.splice(e.oldIndex, 1)); // 否则, 不管 sortable 实例, 将需要排序的数据(如果没有就不管)按新顺序排序
}
}
}
endFunc 中 e.newIndex 和 e.oldIndex 不是可排序的 dom 的顺序, 而是被移动的可排序 dom 在其父级 children 中的位置
echarts 实例 options 对象的 dataZoom 属性修改之后, 先 echarts 实例 .clear() 清除, 再 .setOption 重新渲染
背景:
客户
实现:
// @ 功能
at_user(name) {
// 设置筛选用户 Select 搜索词为空
console.log(name);
console.log(this.$refs.filterUser);
this.$refs.filterUser.setSelected(null);
// 获取编辑框对象
const dom_input = this.$refs.reply_textarea;
// 获取输入框对象
const sale = JSON.parse(name);
const dom_at = document.createElement('span');
dom_at.innerText = `@${sale.fullname}`;
dom_at.contentEditable = false;
dom_at.style['user-select'] = 'none';
dom_at.setAttribute('userId', sale.id);
// 编辑框设置焦点
dom_input.focus();
// 获取选定对象
let selection = null;
if (window.getSelection) {
selection = window.getSelection();
} else if (window.document.getSelection) {
selection = window.document.getSelection();
} else if (window.document.selection) {
selection = window.document.selection.createRange().text;
}
// 如果获取不到, 退出流程
if (!selection) {
this.$Message.error(this.$t('followupReply.browserError'));
return false;
}
// 判断是否有最后光标对象存在
if (this.temp_replyObj.lastEditRange) {
// 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态
selection.removeAllRanges();
selection.addRange(this.temp_replyObj.lastEditRange);
}
// 根据所在位置的不同以不同的方式插入结点
if (selection.anchorNode == dom_input) {
// 焦点就在文本框, 则直接 append node 到最后
dom_input.appendChild(dom_at);
} else if (selection.anchorNode.nodeName != '#text') {
// 焦点在非文本结点, 则插入到焦点节点后面
dom_input.insertBefore(dom_at, selection.anchorNode.nextSibling);
} else {
// 焦点在文本结点, 则在焦点位置文字后插入结点
this.temp_replyObj.lastEditRange.insertNode(dom_at);
}
// 创建新的光标对象
const range = document.createRange();
// 光标对象的范围界定为新建的表情节点
range.setStartAfter(dom_at);
// 插入空格, 否则光标可能不显示
dom_input.insertBefore(document.createTextNode(' '), dom_at.nextSibling);
range.setStart(dom_at.nextSibling, 1);
// 使光标开始和光标结束重叠
range.collapse(true);
// 清除选定对象的所有光标对象
selection.removeAllRanges();
// 插入新的光标对象
selection.addRange(range);
// 无论如何都要记录最后光标对象
this.temp_replyObj.lastEditRange = selection.getRangeAt(0);
}
我的用法是: 配合 webpack-bundle-analyzer 分析首页加载的 js , 把其中的部分静态资源分开到不同的文件, 减小单次请求体积(但相应地, 请求此数组增加了, 总大小是没变的);
打包产生的库文件可以放到 CDN , 加载更快;
之后其他页面引用被打包的静态资源时也是请求库文件, 而不是打包到自身的 js
具体转载自:
vue-cli3 DllPlugin 提取公用库
利用DllPlugin分割你的第三方库
七牛的 ?imageView2/0 可解
首先, 确定 windows 相关通知设置开了! windows 允许 chrome 浏览器通知, 浏览器允许该网站通知
之前发现 Chrome (版本: 81) 浏览器不显示 Notification , 且 Notification.requestPermisson() 也没弹出请求框, 最后定位, 发现原因如下:
第二点有点乱, 没总结规律
另外, 从张鑫旭大神的文章简单了解HTML5中的Web Notification桌面通知中, 找到了两个测试页面, 正常测试, https 测试, 如果客户始终不显示通知的话, 让客户点击这两个页面看看, 要还是不行的话那就没办法了
第 10 题:常见异步笔试题,请写出代码的运行结果
pointer-events
背景: 柱状图数据中, label 默认显示在柱内, 当数据最大值和最小值相差过大时, 短柱放不下 label , 样式不美观
${当前实际数据}.${数据最大值}
(数据本身不能为小数)series.label.formatter
中, 拿到了之前出入的数据, 得到实际数据和数据最大值label.padding
, top 为负数(自己试), 判断当前数据与数据最大值相差过大时(自己试), 把 formatter
返回的字符串加上一个换行 \n
, 否则加上两个换行符, 这样一来, 正常柱显示在柱内, 短柱显示在柱外excel隐藏的空间
UniCode 8236和8237 看着像空格(或者看不到), 但却不是空格, 可能导致错误
实际场景: 用户输入的数据未通过正则校验, 但是看起来完全符合要求, 最后复制该字符串逐个打印字符, 发现有一个"空格", 最终发现此问题
背景: 给客户网站上加入埋点表单, js 中请求后端接口生成留言表单给客户网站访客填写
问题: 表单中有些字段是多选字段, 之前代码使用输入框 + 浮层 div , 勾选浮层 div 中的选项后显示到输入框, 点击输入框 trigger() 浮层 div ; 现在产品要求改为点击空白处关闭浮层
方案:
实例: jquery 通过 $.deferred 实现同步请求
场景: 保存云信聊天信息到自己的系统, 保存时后台会再确认一次接收消息客服是否还在线, 如果不在线则重新请求接口分配客服, 并转发该消息到新客服
// 定义变量
var serviceOffline = false;
// 如果发送云信消息成功
serviceOffline = $.deferred();
// 如果本地未取到云信存储的客服昵称等数据(用来保存到自己的系统), 则请求云信, 并在 getUser 的完成回调中调用保存方法
(function(dtd) {
saveChatMsg(..., dtd);
})(serviceOffiline)
// 否则直接保存聊天信息, saveChatMsg 中 $.ajax 请求完成后, dtd.resolve(true/false)
// 调用 saveChatMsg 后, 直接使用 $.when 包裹后续代码, 在 then 中进行判断 resolve 的内容
$.when(serviceOffline).then(function(val) {
if (val) {
// 如果接收消息客服不在线, 重新请求客服并转发消息 --- resolve(true)
} else {
// 否则正常执行后续步骤 --- serviceOffline 不是 $.deferred() 或 resolve(false)
}
});
啊…忘了这个是啥了
推测: 这个是不是说, 遮罩显示时, 给最外层加一个 class , 调整它的 z-index , 使得遮罩能正常显示?
// 插入用户在表单设置里定义的样式 style
function insertXHLStyle(styleStr) {
if (document.all) { // document.createStyleSheet(url)
window.customStyle = styleStr;
document.createStyleSheet("javascript:customStyle");
} else { //document.createElement(style)
var styleNode = document.createElement('style');
styleNode.type = 'text/css';
var styleTextNode = document.createTextNode(styleStr);
styleNode.appendChild(styleTextNode);
document.getElementsByTagName('HEAD').item(0).appendChild(styleNode);
}
}
做翻译邮件内容功能时, 调用 google 翻译 api 翻译, 但 api 返回的结果会省略连续多个空格为 1 个, 于是想到, 把连续两个空格替换为 1 个空格和一个
其他场景(比如渲染邮件内容)应该也能用到, 另外, 忘了为啥不把空格全部替换为 了
调用 google 翻译 api 翻译邮件内容时, google 自动省略掉连续多个空格, 于是想到把内容正则处理一遍, 把两个空格替换为一个空格
参考 禁止浏览器自动填充到表单 的这个回答
要求将不可见的input框放在页面的最前面,如body起始处, chrome之类的浏览器会填充最前面的输入框。
<input type="text" name="_prevent_auto_complete_name" autocomplete="off" readonly="readonly" style="display: none !important;"/>
<input type="password" name="_prevent_auto_complete_pass" autocomplete="new-password" readonly="readonly" style="display: none !important;" />
chrome有两种填充, 一种是自动填充表单 autofill, 一种是自动完成密码 autocomplete; 请在需要的页面中进行设置。最新的问题,Chrome-72版本,将用户确定保存的用户名和密码强制填充到表单之中了,导致显示问题。以上代码实测生效。
另外, 放到 body 起始处无用的话, 可以放到表单起始处试试
遇到一个 BUG , 水平条形图, 当前时间段筛选后得到三组数据, 换一个时间段筛选后只剩一组数据, 此时图标显示错误, 仍然显示三组数据, 其他两组全部是 NaN , undefined 之类的;
之后在 setOption 之前先 clear 一下就行了
https://segmentfault.com/a/1190000020655133
https://www.zhangxinxu.com/wordpress/2020/10/text-as-css-background-image/
https://developer.mozilla.org/zh-CN/docs/Web/Guide/Events/Creating_and_triggering_events
https://segmentfault.com/q/1010000005744881
https://www.html.cn/qa/css3/14944.html
chrome浏览器中搜索下面地址:
chrome://flags/#same-site-by-default-cookies
chrome://flags/#cookies-without-same-site-must-be-secure
这两项设置为Disabled,并重启浏览器
补充:
现在不行了, 版本越往后限制越严, 还是下载免安装的老版本 chrome 浏览器(禁掉自动更新)调试吧
场景: 用户 paypal 埋点和聊天埋点冲突, 加上聊天埋点后就不能显示 paypal 按钮(不显示或显示后消失)
解决过程:
错误原因: 最终发现是聊天代码中 util.js 的一句 Array.protoType.each , 注释后就没问题了
https://developer.mozilla.org/zh-CN/docs/Web/API/Broadcast_Channel_API
window.addEventListener(‘storage’
背景: 网站登录成功后, 连接 websocket 接收消息, 网站可多标签页打开, 造成多个标签页都接受消息, 重复提醒
我接手时已经是方案一状态了, 随后改成二, 发现问题后改成三, 最终方案为三
方案一:
方案二:
方案三:
后续:
方案三还是不行, sessionStorage 同步其他标签页的数据, 需要一定时间, 而 websocket 连接更快, 有个需求是 websocket 导致 websocket onopen 检测当前标签页数组时, 只能检查到当前标签页
于是改为延时 5s 再去检测标签页数组, 如果检测到数组当前标签页是标签页数组最小时间戳, 则发送 websocket 消息踢掉其他端; 但是, 如果用户点的块, 快速打开两个窗口, 这样会导致第二个窗口被踢掉, 等于还是有问题
背景: 公司之前弄了邮件系统, 但显示邮件内容时是直接 v-html , 导致邮件内容和项目样式互相污染; 之前同事改的是去掉邮件内容的样式文件, 结果导致部分内容显示错位, 现在想不改邮件内容, 用 iframe 包裹邮件内容显示
思路:
需要解决几个难点
解决:
代码以 Vue 形式写的, 下面的代码是最终代码(iframe 渲染 + 自定义水平固定滚动条 + 打印 + 其他删除功能)拆分而来, 可能有些错漏/未删除变量
新建一个空白 HTML 页面, mailDetail.html , 只有最基本的 html 格式, 无任何内容
详情页新增 iframe 框, 引用此 html , 但先设置 height=“0” , 不显示内容
请求到邮件内容, 并对邮件内容做了处理, 调用方法渲染邮件内容到 iframe 中
// 处理邮件详情代码并渲染到iframe中
renderCodeToIframe(mailContent) {
/** handleMailHTML方法 和 renderMailHTML方法都来源mixins(mailContent) */
// 生成 iframe documentElement 代码
this.content = this.handleMailHTML(mailContent);
// 渲染代码到 iframe 中
this.renderMailHTML(this.mailFrameName, this.content);
},
渲染相关方法, from mixins(mailContent), 可以理解为提取出公共的方法到某处, 方便其他地方复用
对原始邮件内容做一些处理, 返回 html 字符串
handleMailHTML(mailContent) {
// 解析邮件内容为 Document 对象
const parser = new DOMParser();
const doc = parser.parseFromString(mailContent, 'text/html');
// 邮件内容中的 base 标签会导致页面跳转时,指向 base 标签指定的地址,而非当前系统的页面,因此去除全部的 base 标签
Array.from(doc.querySelectorAll('base')).forEach(node => {
node.remove();
});
// 邮件详情页的正文中超链接更改为新标签窗口打开
Array.from(doc.querySelectorAll('a')).forEach(node => {
if (node.target && node.target !== '_blank') {
node.target = '_blank';
}
});
// 设置 body margin 默认为 0 , 避免浏览器默认样式给 body 加上 margin
doc.body.style.margin = '0';
// 设置 body overflow-x hidden , 不允许出现横向滚动条 --- 外部模拟水平滚动条
doc.body.style['overflow-x'] = 'hidden';
// 设置 body overflow-y hidden + 去掉 body 的高度限制, 避免出现右侧滚动条
doc.body.style['overflow-y'] = 'hidden';
doc.body.style['min-height'] = 'auto';
doc.body.style['max-height'] = 'auto';
doc.body.style.height = 'auto';
// 添加高度自适应 script
const heightWatcher = doc.createElement('script');
heightWatcher.type = 'text/javascript';
heightWatcher.innerHTML = `
// 监听元素高度变化(200ms 定时查询元素 offsetHeight 是否发生变化)
// 注意, 不同浏览器, 不同版本, 对各种 height 实现不同, documentElement 和其他元素也有区别
// 这里是用 documentElement.offsetHeight 来获取整个文档高度, 别的元素的行为不确定, 可能要用 scrollHeight 来获取高度
function onElementHeightChange(elm, callback){
var lastHeight = elm.offsetHeight, newHeight;
(function run(){
newHeight = elm.offsetHeight;
if( lastHeight != newHeight ) {
callback(newHeight, lastHeight);
}
lastHeight = newHeight;
if( elm.onElementHeightChangeTimer ) {
clearTimeout(elm.onElementHeightChangeTimer);
}
// 更新 hash 值, 供外部监听获取相应传参
// iframe document 实际宽度
var hashStr = 'documentWidth=' + elm.scrollWidth + ';'
// iframe 元素宽度
hashStr += 'iframeWidth=' + window.frameElement.clientWidth + ';'
// 转码, 赋值
location.hash = encodeURIComponent(hashStr);
elm.onElementHeightChangeTimer = setTimeout(run, 200);
})();
}
// 监听 documentElement offsetHeight 变化, 变化后设置父页面 frame 元素 height 属性为变化后的高度
onElementHeightChange(document.documentElement, function(newHeight, oldHeight){
console.error('onElementHeightChange', newHeight, oldHeight)
if (window.frameElement) {
// 设置 frame height 为变化后的新高度 + 滚动条高度, 以避免元素底部出现水平滚动条时垂直方向不能占满
window.frameElement.height = (newHeight || 50) + (window.innerWidth - document.documentElement.clientWidth);
}
});
// 初次加载完成时, 设置父页面 frame 元素 height 属性为 documentElement.offsetHeight
window.addEventListener('DOMContentLoaded', function(e) {
console.error('DOMContentLoaded');
if (window.frameElement) {
// 设置 frame height 为页面高度 + 滚动条高度, 以避免元素底部出现水平滚动条时垂直方向不能占满
window.frameElement.height = document.documentElement.offsetHeight + (window.innerWidth - document.documentElement.clientWidth);
}
})`;
doc.body.append(heightWatcher);
// 设置 DOCTYPE 以避免页面内容缩小时, iframe 高度不变, 导致多出空白区域(参考 https://segmentfault.com/a/1190000014586956#item-3)
const docType = '';
// 返回最终的 HTML 字符串
return docType + doc.documentElement.outerHTML;
}
渲染 html 到 iframe 中
renderMailHTML(frameName, strHTML, callBack) {
// 不加 $nextTick 或 $nextTick 位置放错(参见 git 文件提交日志), 可能导致内容不显示 --- 实际原因是多了一个 iframe , 不清楚咋出现的
this.$nextTick(() => {
// 获取指定 iframe 的 window
let ifr = window.frames[frameName];
if (!ifr) {
return;
}
// 清除原有 iframe , 避免其内容对新 iframe 造成影响, 同时也避免原有 iframe 中的各种监听之类的残留
const ifrElm = ifr.frameElement;
const newIfrElm = ifr.frameElement.cloneNode();
ifrElm.parentElement.replaceChild(newIfrElm, ifrElm);
// 写入新 iframe 内容
ifr = window.frames[frameName];
if (ifr) {
// 写入 HTML
ifr.document.open();
ifr.document.write(strHTML);
ifr.document.close();
// 触发回调函数
if (callBack) {
callBack();
}
}
});
}
新增一个打印用的 iframe , 隐藏不显示
<iframe id="printf" name="printf" style="display: none;">iframe>
调用下方打印方法打印
// strHTML: 原始邮件内容
// containerNode: 邮件完整内容(包括 iframe 和其他信息如收件人发件人等)所在的 node
// frameName: 邮件详情页 iframe 的 name
// printFrameName: 之前初始化的打印用 iframe 的 name
printMailHtml(strHTML, containerNode, frameName, printFrameName) {
// 初始化打印 Document
const parser = new DOMParser();
// 指定打印样式和 onload 打印
const doc = parser.parseFromString(`${containerNode.innerHTML}`, 'text/html');
// 替换邮件内容 iframe 为 iframe 内部文档
const ifr = doc.querySelector(`iframe[name=${frameName}]`);
if (ifr) {
ifr.outerHTML = strHTML;
}
// 写入数据到打印 iframe 中, 打印
const printWin = window.frames[printFrameName];
if (printWin) {
printWin.document.write(doc.documentElement.outerHTML);
printWin.document.close();
}
}
背景: 产品提出, 邮件过长时, 页面要滚动到最底部才能拖拽 iframe 的水平滚动条, 操作不方便; 因此希望水平滚动条固定显示在视口底部, 用户可以直接拖拽查看详情; 并且, 在 iframe 垂直方向滚动到底后, 水平滚动条应取消固定, 随着 iframe 继续向上移动
解决:
做一个模拟滚动条满足此需求, 其原理为:
具体代码如下:
滚动条 div
<div @scroll="handleMailHorizontalScroll"
ref="mailIframeScroll"
:style="mailScrollContainerStyleObj">
<div :style="mailScrollInnerStyleObj">div>
div>
相关变量/滚动监听器定义
data() {
return {
// 邮件自定义水平滚动条样式 -- 外部与 iframe 等宽 div 的样式
mailScrollContainerStyleObj: {
// 固定属性
// 允许出现水平滚动条, 此水平滚动条即为最终显示的水平滚动条
'overflow-x': 'auto',
// 尽量减少滚动条占位高度
'line-height': '0',
// 背景透明
'background-color': 'transparent',
// 变动属性
// 控制鼠标穿透, 确保滚动条不显示时鼠标不会误触滚动条
'pointer-events': 'none',
// 滚动条外部宽度, 因为显示区域和 offsetParent 不一定等宽, 这个也是要调整的, 避免滚动条从固定变为正常时宽度发生变化
width: '100%',
// 固定显示时 absolute, 正常显示时 relative
position: 'relative',
bottom: '0'
},
// 邮件自定义水平滚动条样式 -- 内部与 iframe documentElement 等宽 div 的样式
mailScrollInnerStyleObj: {
// 固定属性
// 高度尽可能小
height: '1px',
// 背景透明
'background-color': 'transparent',
// 变动属性
// 模拟 iframe 内部文档宽度, 保证外部 div 滚动条显示逻辑和 iframe 系统水平滚动条逻辑一致
width: '0'
},
// 监听: iframe 所在 overflow div 发生垂直滚动 ($debounce 是自己写的防抖方法)
handleMailVerticalScroll: this.$debounce(() => {
// 暂存 iframe 元素
const ifrEle = document.querySelector(`iframe[name="${this.mailFrameName}"]`);
if (ifrEle) {
// 获取 iframe rect.bottom 和其 offsetParent rect.bottom , 以判断 iframe 底部是否在其 offsetParent 下方(还要算上指定 bottom , 避免 offsetParent 和滚动容器位置不一致)
const ifrRec = ifrEle.getBoundingClientRect();
const scrollRec = ifrEle.offsetParent.getBoundingClientRect();
if (ifrRec.bottom > scrollRec.bottom + this.scrollBarBottom) {
// iframe 底部在其 offsetParent 下方
// 固定显示水平滚动条在 offsetParent 底部
this.mailScrollContainerStyleObj.position = 'absolute';
this.mailScrollContainerStyleObj.bottom = `${this.scrollBarBottom}px`;
} else {
// iframe 底部不在其 offsetParent 下方
// 水平滚动条正常显示在原位置(iframe 之下)
this.mailScrollContainerStyleObj.position = 'relative';
this.mailScrollContainerStyleObj.bottom = '0';
}
}
}, 10),
// 监听: iframe 下方模拟水平滚动条 发生水平滚动
handleMailHorizontalScroll: this.$debounce((e) => {
// 暂存 iframe window
const ifr = window.frames[this.mailFrameName];
// 控制 iframe documentElement 左偏移量
if (ifr && ifr.document && ifr.document.documentElement) {
ifr.document.documentElement.scrollLeft = e.target.scrollLeft;
}
}, 10)
};
}
监听 iframe hash 值变化(之前的渲染方法里写了, 文档宽度变化时更新数据到 hash 中), 调整自定义滚动条和其容器的 width
// 之前 renderMailHtml 方法预留有参数 callBack , 调用时给此参数传入下面的方法就行了
// 监听: iframe hash 值变化
handleIframeHashChange() {
setTimeout(() => {
// 暂存 iframe window
const ifr = window.frames[this.mailFrameName];
ifr.onhashchange = () => {
// hash 值解码
const hashVal = decodeURIComponent(ifr.location.hash);
if (hashVal) {
// 取到 iframe clientWidth
let temp = hashVal.match(/iframeWidth=(.*?);/);
// 设置模拟滚动条外部 div 宽度为 iframe clientWidth
if (temp[1]) {
this.mailScrollContainerStyleObj.width = `${temp[1]}px`;
}
// 取到 iframe document scrollWidth
temp = hashVal.match(/documentWidth=(.*?);/);
// 设置模拟滚动条内部 div 宽度为 iframe documentElement scrollWidth
if (temp[1]) {
this.mailScrollInnerStyleObj.width = `${temp[1]}px`;
}
}
};
}, 100);
}
监听滚动条的宽度, 避免页面宽度足够, 不用显示水平滚动条时, 水平滚动条仍然占位, 导致底部无法点击
watch: {
'mailScrollInnerStyleObj.width': {
handler(val) {
// 获取模拟滚动条容器
const scrollBarDiv = this.$refs.mailIframeScroll;
if (scrollBarDiv) {
// 模拟滚动条内部 div 宽度大于容器宽度时, 才允许鼠标点击滚动条区域(避免用户想点击邮件内容却点中滚动条, 导致点击无效)
this.mailScrollContainerStyleObj['pointer-events'] = parseFloat(val) > scrollBarDiv.clientWidth ? 'auto' : 'none';
// 主动触发垂直滚动方法, 判断当前滚动条应该固定显示还是正常显示
this.handleMailVerticalScroll();
}
}
}
}
背景: 埋点代码埋到客户网站后, 新引入的字体文件始终不显示
解决过程:
.className *::before { display: none; }
, 在结合之前搜索到的font-face 指定多个字体文件, 未用到的字体文件不加载, 推测是因为该样式导致字体不显示, 于是 font-face 不引入文件结论:
背景: 参照网页版 whatsapp 做聊天消息已读未读状态显示, UI 给的需求是, 单行文本显示在最右侧, 多行文本显示在文本底部
解决过程: 参照网页版 WhatsApp , 在文本后面加一个隐形的有宽度无高度 div , 来确保文本不和 float 的已读未读状态元素重叠
背景:
现在在做的网站支持同步 whatsApp(之后简称 wa) 消息实现消息收发, 要求接收到 wa 带 emoji 表情的消息时, 网站能正常显示 emoji , 同时, 网站发出去的带 emoji 的消息, wa 也能正常显示 emoji
概述:
实现过程:
引入 twemoji
<script src="https://twemoji.maxcdn.com/v/13.1.0/twemoji.min.js" integrity="sha384-gPMUf7aEYa6qc3MgqTrigJqf4gzeO6v11iPCKv+AP2S4iWRWCoWyiR+Z7rWHM/hU" crossorigin="anonymous">script>
爬取 wa 表情
在网页版 whatsapp 上聊天, 一栏栏地点选表情, 发送, 在网站处接收, 此时接收到的内容已经是字符了, 把这些字符按顺序提取为数组;
这个需要耐心, 这些个字符千奇百怪, 有的字符电脑系统不支持不能渲染出来, 有的字符后面需要接一个空格, 有的字符看上去只有一位但实际占了多位, 最多的还是由多种字符组合显示成一个表情的(字符人, 可加修饰字符: 性别, 发型, 职业, with another one …), 千万别弄错了
新增 emoji 组件
渲染表情部分由全局的 MutationObserver 负责(twemoji.parse)
选中表情部分如下:
// 点击选中 emoji
handleClickEmoji(e) {
// 取选中的 emoji DOM 标签
let emojiImg;
if (e.target.classList.contains('emoji-item')) {
emojiImg = e.target.querySelector('img.emoji');
} else if (e.target.classList.contains('emoji')) {
emojiImg = e.target;
}
// 取标签上的 alt (实体字符, twemoji 转换后自带)传给外部
if (emojiImg) {
this.$emit('checkEmoji', emojiImg.getAttribute('alt'));
}
}
输入框接收选中表情, 加入到输入框中
输入框 div
<div :contentEditable="true"
ref="sendMsg"
@click="save_range"
@keyup="save_range"
@keydown="inputOnKeyDown"
@paste="handlePaste"
:placeholder="$t('chat.inputbox')"
:class="{'waInputDiv__disabled': inputDisabled}"
class="waInputDiv">div>
输入框 div 相关事件
// inputOnKeyDown 处理回车, ctrl 等事件, 与表情主逻辑无关, 略过
// 离开焦点时先保存状态(光标等信息)
save_range() {
let range = null;
if (window.getSelection) {
const sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
}
this.lastEditRange = range;
}
// 粘贴内容到可编辑 div (参考 https://www.zhangxinxu.com/wordpress/2016/01/contenteditable-plaintext-only/)
handlePaste(e) {
e.preventDefault();
let text;
if (window.clipboardData && window.clipboardData.setData) {
// IE
text = window.clipboardData.getData('text');
} else {
text = (e.originalEvent || e).clipboardData.getData('text/plain');
}
if (document.body.createTextRange) {
let textRange;
if (document.selection) {
textRange = document.selection.createRange();
} else if (window.getSelection) {
const sel = window.getSelection();
const range = sel.getRangeAt(0);
// 创建临时元素,使得TextRange可以移动到正确的位置
const tempEl = document.createElement('span');
tempEl.innerHTML = 'FEFF;';
range.deleteContents();
range.insertNode(tempEl);
textRange = document.body.createTextRange();
textRange.moveToElementText(tempEl);
tempEl.parentNode.removeChild(tempEl);
}
textRange.text = text;
textRange.collapse(false);
textRange.select();
} else {
// Chrome之类浏览器
document.execCommand('insertText', false, text);
}
}
选中表情相关事件
// 接收"选中 emoji 表情"事件
handleCheckEmoji(val) {
// 获取待插入表情 Node
let dom_insert = document.createElement('span');
dom_insert.innerHTML = twemoji.parse(val);
dom_insert = dom_insert.childNodes[0];
// 插入 Node 到输入框
this.insertInputMsg(dom_insert);
}
// 插入 emoji 表情到输入框
insertInputMsg(val) {
// 获取待插入结点
let dom_insert;
if (val instanceof Node) {
// 是 Node 结点, 不用做处理
dom_insert = val;
} else {
// 否则当做文本结点处理
dom_insert = document.createTextNode(String(val || ''));
}
// 获取编辑框对象
const dom_input = this.$refs.sendMsg;
// 编辑框设置焦点
dom_input.focus();
// 获取选定对象
let selection = null;
if (window.getSelection) {
selection = window.getSelection();
} else if (window.document.getSelection) {
selection = window.document.getSelection();
} else if (window.document.selection) {
selection = window.document.selection.createRange().text;
}
// 如果获取不到, 退出流程
if (!selection) {
this.$Message.error(this.$t('whatsapp_manage.browserError'));
return false;
}
// 判断是否有最后光标对象存在
if (this.lastEditRange) {
// 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态
selection.removeAllRanges();
selection.addRange(this.lastEditRange);
}
// 根据所在位置的不同以不同的方式插入结点
if (this.lastEditRange) {
// 有光标对象, 直接插入
this.lastEditRange.insertNode(dom_insert);
} else if (selection.anchorNode == dom_input) {
// 焦点就在文本框, 则直接 append node 到最后
dom_input.appendChild(dom_insert);
} else if (selection.anchorNode.nodeName != '#text') {
// 焦点在非文本结点, 则插入到焦点节点后面
dom_input.insertBefore(dom_insert, selection.anchorNode.nextSibling);
}
// 创建新的光标对象
const range = document.createRange();
// 光标对象的范围界定为新建的内容节点
range.setStartAfter(dom_insert);
// 插入空格, 否则光标可能不显示
// dom_input.insertBefore(document.createTextNode(' '), dom_insert.nextSibling);
// range.setStart(dom_insert.nextSibling, 1);
// 使光标开始和光标结束重叠
range.collapse(true);
// 清除选定对象的所有光标对象
selection.removeAllRanges();
// 插入新的光标对象
selection.addRange(range);
// 无论如何都要记录最后光标对象
this.lastEditRange = selection.getRangeAt(0);
}
主界面监听 DOM 变动, twemoji.parse 转化指定 class 元素内部的实体字符为表情
mounted() {
// 监听 DOM 变化, 变化时使用 twemoji 库转化 emoji 实体字符为 twemoji emoji
this.observer = new MutationObserver(function(mutations, observe) {
const domList = document.querySelectorAll('.twemoji-convert');
for (let i = 0; i < domList.length; i++) {
twemoji.parse(domList[i]);
}
});
this.observer.observe(document.body, {
'childList': true,
'characterData': true,
'subtree': true
});
}
补充:
配合 Vue 使用时, 表情和 emoji 混杂的文本, 使用 twemoji.parse 后会破坏 vue 的响应式监听, 导致视图不随数据的更新而更新; 解决方法 — 给需要更新的地方加上 key , key 上绑定原数据, 这样, 当原数据变化时, 组件会重新渲染
概述:
背景:
解决过程:
https://www.bookstack.cn/read/html-tutorial/spilt.2.docs-iframe.md allow-popups-to-escape-sandbox 属性解决
补充:
概述:
给定一个网络文件 url , 下载文件到浏览器缓存, 完毕后转换其为 blob / File 对象
背景:
项目中现有的 whatsApp 发送文件接口, 以及云信 web 端发送文件, 不支持发文件 url , 于是想办法把文件 url 转换为对象
解决过程:
用的 axios (也可以用别的库, 或自己原生 XMLHttpRequest 实现), 设置
responseType: 'blob',
withCredentials: false,
之后
补充:
直接用 blob 文件上传, 服务器如果取文件名, 取到的就是 “blob”
如果要加名字, 网上搜了下, 带上 name 转换成 File 文件就行了 — new File([tmpBlob], name)
复制 blob 和 File , 直接 .slice() 复制就行, 复制 File 后重命名 — new File([oldFile.slice()], name);
概述:
背景:
项目登录页需要加上"记住密码"功能, 取消勾选"记住密码"时, 要求输入框置空, 与浏览器自动填充冲突了
解决过程:
off 和 new-password 都不行, 找到文章 https://www.cnblogs.com/chenzeyongjsj/p/7115285.html , 使用 readonly 方法实现
背景:
需要做一个登录安全校验功能, 非常用设备登录网站时, 弹出安全验证框进行验证
调研
设备/浏览器唯一标志 ID 调研
背景:
产品要求新登录设备弹出安全验证框, 因此需要标识每一台设备, 但是前端做不到设备级, 只能尝试标识浏览器
方案:
浏览器本地存储直接设置标识
往 localStorage 中存入一个新变量, 值为登录时间 + 随机数, 之后始终不清除此数据, 每次登录时取该数据一起发给后端, 后端判断与数据库中数据是否一致
优点:
- 最简单
缺点:
- 用户可以通过浏览器控制台自己修改数据
- 隐身模式需要重新校验
- 本地存储被清除需要重新校验
使用浏览器音频 API 标识客户
https://audiofingerprint.openwpm.com/
搜集浏览器音频播放的特征值, 计算得到唯一标志
优点:
- 纯前端实现, 前端直接使用页面上的代码即可实现
- 不同版本的 chrome 浏览器生成的标识相同
- 隐身模式/清除本地存储, 标识不变
缺点:
- 是否有版权问题?
- 公司内同一批同配置电脑 + 驱动 + 同一款浏览器, 可能会重复(再加个随机数? 比如时间之类的?)
使用开源库
https://github.com/fingerprintjs/fingerprintjs/
查询浏览器属性(包括并不限于音频, 画布, 字体, 屏幕, 操作系统, 设备名称等信息), 从中计算出标识
优点:
- 纯前端实现, 前端直接引用库即可实现
- 隐身模式/清除本地存储, 标识不变
缺点:
- 不同版本的 chrome 浏览器生成的标识不同
- 还是可能会重复(再加个随机数? 比如请求时间之类的?)
使用前者的升级版
开源版和专业版对比: https://dev.fingerprintjs.com/docs/pro-vs-open-source
demo: https://fingerprintjs.com/demo/
优点:
- 在服务器端处理所有信息,将浏览器指纹识别与大量辅助数据(IP 地址、访问时间模式、URL 更改等)相结合,能够可靠地对拥有相同设备的不同用户进行重复数据删除,从而实现 99.5% 的识别准确率
缺点:
- 调用 api 次数超过 20K/月, 则开始收费
最终代码:
// 生成浏览器指纹, 目前仅使用了 audio api 生成指纹, 参考 https://audiofingerprint.openwpm.com/ 网站源码
export const gen_fingerPrint = (useFullPrint = true) => {
return new Promise((resolve, reject) => {
try {
let shortPrint = '';
let fullPrint = '';
const context = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 44100, 44100);
const pxi_oscillator = context.createOscillator();
pxi_oscillator.type = 'triangle';
pxi_oscillator.frequency.value = 1e4;
// Create and configure compressor
const pxi_compressor = context.createDynamicsCompressor();
pxi_compressor.threshold && (pxi_compressor.threshold.value = -50);
pxi_compressor.knee && (pxi_compressor.knee.value = 40);
pxi_compressor.ratio && (pxi_compressor.ratio.value = 12);
pxi_compressor.reduction && (pxi_compressor.reduction.value = -20);
pxi_compressor.attack && (pxi_compressor.attack.value = 0);
pxi_compressor.release && (pxi_compressor.release.value = 0.25);
// Connect nodes
pxi_oscillator.connect(pxi_compressor);
pxi_compressor.connect(context.destination);
// Start audio processing
pxi_oscillator.start(0);
context.startRendering();
context.oncomplete = function (evnt) {
let pxi_output = 0;
const sha1 = CryptoJS.algo.SHA1.create();
for (let i = 0; i < evnt.renderedBuffer.length; i++) {
sha1.update(evnt.renderedBuffer.getChannelData(0)[i].toString());
}
const hash = sha1.finalize();
// Fingerprint using DynamicsCompressor (hash of full buffer):
fullPrint = hash.toString(CryptoJS.enc.Hex);
for (let i = 4500; i < 5e3; i++) {
pxi_output += Math.abs(evnt.renderedBuffer.getChannelData(0)[i]);
}
// Fingerprint using DynamicsCompressor (sum of buffer values):
shortPrint = pxi_output.toString();
pxi_compressor.disconnect();
resolve(`${useFullPrint ? fullPrint : shortPrint}`);
};
} catch (err) {
console.error(err);
resolve('');
}
});
};
补充:
实际开发中, 发现 fingerprintjs 库也用的相似的参数值, 项目的src/sources/audio.ts 中使用的数值与本方法中的数据基本相同,
之后如果要获取其他数据生成指纹, 可参考此项目的 src/source/index.ts , 其顶部引入了很多获取设备信息的方法, 想要啥信息直接去引入文件找
给可编辑 div 末尾插入换行符, 发现仍然未换行;
解决方法: 提前给 div 末尾插入一个
就行了, 之后看自己情况要不要去掉
示例代码:
// 如果输入框末尾没有 BR 换行符, 则自动加一个, 避免 Ctrl + Enter 两次才显示
const currLastEl = dom_input.lastElementChild;
if (currLastEl) {
if (currLastEl.tagName !== 'BR') {
dom_input.appendChild(document.createElement('br'));
}
} else {
dom_input.appendChild(document.createElement('br'));
}
参考资料:
一篇文章教你如何捕获前端错误
备用: 一篇文章教你如何捕获前端错误
备用: 一篇文章教你如何捕获前端错误
自己实际使用场景:
全局监听 css js 等 chunk 文件是否加载失败, 失败则提示用户刷新页面, 以此解决发版后部分用户点击页面无反应问题(用户请求的还是旧版的 chunk 文件)
背景:
做一个根据第一个下拉框的值来决定第二个下拉框选项的功能
第二个下拉框有一个包含全部选项的数组, 要求当’第一个下拉框值是 A’ 时, ‘第二个下拉框的选项是 B’, 否则, ‘第二个下拉框的选项从全部选项中过滤掉 B’
实现:
于是, 逻辑简化为: A ? B : !B
A 是’条件 === xxx’, Boolean 型; B 是[options].includes(全选项数组当前遍历的选项值), 也是 Boolean 型;
因为不存在"根据 A 来决定是否在 B 前面加上 ! 符"这样的写法, 所以, 进一步转化条件为: (A && B) || (!A && !B), 即数学上的’同或’运算
查到 JS 没有同或运算, 但有按位异或操作符号 ^ , 因为 A 和 B 都是 Boolean 型, 直接使用 A ^ B 得到 0 或 1 , 能满足我的"遍历过滤选项"需求, 所以此处可以用 ^ 符
又因为异或是同或的取反, 于是使用 !(A ^ B) 来做为遍历过滤条件, 成功解决问题
背景:
列表项可选中, 选中后顶部出现操作栏, 包括全选框 + 操作按钮
要求当按钮只有两个时, 他们显示在同一行, 全选框左对齐, 操作按钮右对齐
当按钮有三个时, 一行放不下, 要求全选框独占一行, 按钮另起一行左对齐显示
实现:
<div class="container">
<div class="checkbox">div>
<div class="button-group">div>
div>
<style>
.container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.checkbox {
flex: 1;
}
.button-group {
}
style>
背景:
页面上有两个 元素, 一个
onclick="xxx()"
, 一个 $('xxx').on('click', xxx)
在 ios 的 chrome 浏览器上, 页面刷新后第一次点击其中一个元素时, 事件不触发, 之后再点就正常, 且 safari 没有这个问题, 将 chrome 改为桌面版网站, 也没有这个问题
解决思路:
确定是浏览器而不是绑定方法的问题(我们提供的)后就没继续排查了, 未解决, 只是写下解决思路:
看能否电脑调试 ios
电脑连手机调试 chrome 很方便, 于是想看下能否电脑调试 ios
最后发现, 连 ios 设备只能调试 Safari , 无法调试 chrome
看是不是绑定方法的问题
onclick 绑定的方法是我们写的, 埋点成功后提供给客户的页面使用
看了一遍, 没发现啥问题, 而且要说不行, 为啥第二遍点就没问题呢, 于是改从客户页面上来看, 看其他元素有没有问题
对比问题元素和其他元素
如果实在需要我们来给客户解决, 准备怎么做
之后想了下, 虽然不好调试 chrome on ios , 但是可以自己写一个简单的 html , 复刻相关代码结构, ios 上打开 html , 然后逐步排查
某客户检测网站性能, 发现公司的埋点 js 有 unload , 影响性能, 该检测网站给出了替换方案 pagehide , 并且之后搜了下张鑫旭大神的文章, 确定了修改方案
https://gtmetrix.com/avoid-unload-event-listeners.html
https://www.zhangxinxu.com/wordpress/2021/11/js-visibilitychange-pagehide-lifecycle/
https://www.zhangxinxu.com/wordpress/2021/11/js-visibilitychange-pagehide-lifecycle/
看同事代码发现的
for (const key in dataObj) {
if (dataObj[key]) {
ajaxData = {
...ajaxData,
[key]: dataObj[key]
};
}
}
https://markdowner.net/skill/259698173348618240
https://juejin.cn/post/7134684645228347400
背景:
某页面有个输入框, 用的应该是什么组件, 直接修改内容不生效/机制不明确, 于是使用 paste event 粘贴到输入框, 结果发现也不行
定位:
结论:
把逻辑从 content-script 抽取出来, 由 content-script 生成 script 标签, 插入到页面上去, 这样就实现了
原理不清楚, 可能 content-script 运行的环境跟 script 运行的环境不同导致的吧, 从控制台里打印的语句也能看出来, 控制台也分了层级, 默认当前页面, 底下还有各种插件的控制台
插入页面具体方法, 参考插件, 单独弄一个可独立运行的 js 文件, 打包好, content-script 里使用 chrome.extension.getURL 获取此 js 的地址, 生成 script , 指定地址, 加载
背景:
为提高页面加载速度, 根据地址的不同加载不同的 script (Vue 项目, 一份代码放到多个地址, 根据地址不同显示不同登录页), 结果引用的百度地图 api 失效
定位:
发现埋点 js 引入成功, 该 js 生成的内容是 document.write 再引入 js , css 文件, 这些文件没有加载出来
试了下放到各个位置, 都不行, 网上搜了下, 说是必须在 document.write 文档流还没关闭时引入, 所以不能异步引入 — 动态插入 script , 或者 async , defer 之类的
结论:
在 之前加上 script, 里面加上地址判断, 通过判断则用 document.write(‘’) 加载百度地图埋点代码