在一个html中,想引入另一个html,最简单的方法是使用iframe, 但iframe的难点在于高度固定,很难根据文档高度自适应高度。 本文另辟蹊径,使用 AJAX 及 Shadow DOM 来解决这个难题
背景
项目文档使用.docx或.md格式编写,现有需求把文档转成html,放到独立的静态服务器中,把相关地址存入数据库。在用户通过浏览器访问另一个项目时,有一个接口会返回文档生成的html的地址,则我要做的是:根据地址,拿到html,再把其内容展示到当前页面中。
约定:把引入html的文档称之为主文档(main document
或master document
)
iframe
使用iframe是最简单的方法,但有一个问题:必须预先设置iframe的高度,如果文档内容过长,iframe内部就会产生一个滚动条。如果当前文档内容已经超出了视窗高度,则当前文档也会有一个滚动条,所以很容易出现有两个滚动条的页面,难看得要死,滚来滚去的也不方便,总之用户体验不好。如果想把iframe的滚动条去掉,请继续往下看。
此时的解决思路是:iframe加载完成后,使用js获取iframe内容的高度,重设iframe高度。
然而,这必须是当前文档与iframe文档处于同源情况下才能做到,而我所遇到的是喜闻乐见的跨域情况,此时浏览器不允许我用js访问iframe内document对象
难道跨域就没法玩了?也不尽然,可以使用这个类库https://github.com/davidjbradshaw/iframe-resizer,解决跨域iframe高度自适应问题。不过这有个前提,那就是你必须修改需要引入的html,详情可见这篇文章的分析https://css-tricks.com/cross-domain-iframe-resizing
虽然我拥有html的控制权,但html是由.docx或.md文件生成的,并不是源文件。如果这次我通过修改html来解决滚动条的问题,那以后文档修改了,重新生成html,我又得再修改html,维护性低,我决定找找更好的方法
HTML Imports
我首先想到的是Web Components的HTML Imports
|
|
当然这里有个前提条件,服务端必须开启CORS,以Nginx为便,配置如下
|
|
页面上有多个相同URI资源的HTML Imports,浏览器只会加载一次
HTML Import完成时,会触发onload事件,可以使用js动态创建HTML Imports的link标签
|
|
经测试,Vue2.x无法对link标签绑定事件,即不支持以下写法
|
|
如果使用Vue,则必须在的mounted钩子里动态创建link标签
触发onload
事件后,html需要引入进来了,但是内容并没有呈现,还需要js控制一下
|
|
不过存在的问题是,import进来的html如果有style
元素,或有外链样式,则HTML Imports onload
事件触发后,引入进来的html的样式会影响主文档,造成全局样式污染
所以说,先不论兼容性的问题,这个样式被污染的问题,就很蛋疼。关于这个问题,谷歌团队在github已有相关资料,最终的结论是:
目前HTML Imoprts的样式表(内联在
<style>
, 外链样式<link rel="stylesheet">
)会作用在主文档上。反对并最终会移除这种行为,把HTML Imports标准的第九章节也移除掉。当前的计划是,在Chrome61(2017年9月)开始在控制台显示警告信息,最终在Chrome65移除该行为(2018年5月)
之所以这样做是因为,目前而言HTML Imports只有Blink内核才生效,也即Safari,Firefox,IE,Edge是不支持的,而HTML Imports的样式表的规则应用到主文档上,既给开发者都带来困扰,也增加了Blink内核的复杂度,所以谷歌团队最终决定:
移除HTML Imports行为,而反对其中的样式系统是整个计划的第一步
看到这里,我也是惊呆了,居然HTML Imports不被鼓励使用,而且最终要弃用,难怪谷歌开发者文档上Web Components里的内容主要讲Shadow DOM
和Custom Elements
, 文档本身并没有多少HTML Imports的内容。
我再上chromium论坛查看相关讨论 ,看到移除HTML Imports的日子还未确定,于是决定还是先用着顶下数,解决眼前的问题先。
Shadow DOM
上面提到,HTML Imports进来后,引入文档的样式污染主文档了。如果引入文档的样式是scoped styles
,就可以解决上述问题,而这正是Shadowm DOM做的事情。
因为<link rel="import" href="/path/to/imports/stuff.html">
一旦加载,样式就会作用到主文档,所以我的想法是在Shadow DOM里创建HTML Imports的link元素,link加载完成,整个样式是scoped styles
, 就不会污染全局了
于是有代码如下:
|
|
想法很好,但实际上却只得到控制台的一个警告:
HTML element is ignored in shadow tree.
好吧,想通过link来import HTML的想法泡汤了,只能使用最后的大招了
AJAX + Shadow DOM
思路是:AJAX获取CORS的html,然后把html内容放到Shadow DOM中
直接上代码
|
|
思路不难,主要障碍在于另外两个问题:
- 文件乱码
- 资源路径
这两个问题使用iframe都不会有,所以说如果不是因为滚动条问题,iframe真的是省心呀。
下面分享这两个问题的解决思路
文件乱码
因为html是由.docx或.md转换而成的,所以乱码问题又可以分为两个方面
- 导出的html编码不对
- 浏览器没有使用正确的编码解析文档
解决方案如下:
- 建议使用Mac/Linux机器
- 找一个靠谱的转换html的工具docx转换html可以使用这个,md转换html可以使用MacDown, Typora或其他,不建议使用马克飞象,原因后面会说
- 确保导出的html编码格式为UTF-8,可以使用命令
file
查看 - 修改Nginx配置,在http模块设置
charset utf-8;
资源路径
众所周知,图片是个老大难问题,难点有二:
- 截图/复制图片到文档里
- 图片地址
写.docx的时候,1不是问题; 至于2,图片是保存在文档内的,导出html时,会有一个文件夹专门放图片,此时html里对图片的引用路径是相对路径
写.md的时候,一般没有截图/复制的功能,如果有,一般也是自己找个靠谱的图床并安装一些插件,需要折腾一番,或使用付费服务; 此时文档里图片是外链,也即图片是绝对路径,此时转成html不需要一个图片文件夹。
如果不想折腾,又不想给钱,就得截图然后保存到本地,也即本地需要一个文件夹存放图片,导出的html图片引用是相对路径。
有人会想到马克飞象,它既可以截图/复制图片,又可以只导出一个html文件,图片是作为Base64形式内嵌其中的,看起来很炫酷,实则一不能让浏览器缓存图片,二让html变得臃肿无比,并不推荐
最终,不想花钱又不想再折腾的我们,选择的做法是跟.docx一样,导出html后,本地有一个图片文件夹,html里图片的路径是相对路径
另外, 仍然建议使用Mac/Linux机器进行上述操作,因为Windows的目录符与是\
, 而URL的目录符其实是/
那么,问题就来了:
在A站点,使用AJAX获取到B站点的文档后,此时只拿到了html,其本质还是内存里的字符串,并没有拿到样式表/图片等资源,只有使用innerHTML
把内容添加到主文档之后,才会去获取样式表/图片等资源; 而因为B站点的html对资源的引用是相对路径,则把html添加到A站点的主文档后,html已经作为主文档的一部分,此时html资源请求路径是相对A站点的路径,而并非B站点的路径,所以会报404,资源找不到!
所以,在使用AJAX获取到B站点的html字符串后,还要使用正则式对字符串进行处理一下,这让我想起了一本经典书籍<Master Regular Expression>, 果然基本功很重要啊~