参考这个方法,但不想修改d3 https://gist.github.com/biovisualize/373c6216b5634327099a
虽然也绕了点弯,但还算很快了,比较满意,也学到了,记下来。
问题描述:希望在选择集操作时,在当前节点下挂新节点。
d3.select("xx").each(function(d){ // do some thing });
约束条件:
1 不是用d3的方式append,而是直接从别处的字符串形式的svg片段"<g>...</g>"。其实,这个片段是在这个函数现场,由别的非d3工具根据当前绑定的数据d生成的。
2 方法要兼容node和browser端
——业务和需求(问题域)是清晰的,难点在技术上(解空间)的约束条件。
把“业务”语言,一点点翻译成解空间的“技术”语言。
所以最初始的思路还是"如何把大象放进冰箱"的套路:
1 parse str_svg成当前dom中的某物(node?SVGelement?)
2 然后把这个东西挂到当前选择集/Element/Node下面。
参考上面的答案和别人家的做法,1 主要是2步
1 用DOMParser 把str_svg 解析掉,但返回的是一个新dom
2 依赖adoptNode,把新dom中的element 过继到d3选择集所在的dom,
function (d) { // parse and get node1 this.appendChild(node1); }
直接一句appendChild(node1)完事。注意选择集.each匿名回调函数中this,表示当前选中SVGElement。
这个文档里说得明白http://rajapradhan.com/blogs/d3-js-v4-essentials/d3-selections/
继续分析1,DOMParser的常见用法是
new DOMParser()
但这在浏览器端没问题,在node+jsdom下,会报错。不满足约束条件2了。
仔细看了jsdom的github,这个是在2016年才加入的。在jsdom模式,这样创建:
new window.DOMParser();
这里就清楚了,DOMParser位于当前dom的window 下。
但这里约束1又来了,我们是在d3的回调函数中,只有选择集和选择集对应的Element this
对浏览器没问题,对node+jsdom环境:没有当前dom文档的window对象。
如果不想给层层函数开天窗,传入window,(修改N个函数的接口),那么就引出新的问题:
如何从d3 selection or SVGElement get 当前dom的 window?
老问题没解决,又层层嵌套依赖,遇到新问题。这就是技术工作的常态。急不得,习以为常就好。
有点像评书,原问题本来是突围、破阵、闯关,然后解决方案是需要去XX地方搬兵,请YY高人;结果请人过程中又扯远了,遇到很多问题。
——层层嵌套的调用,什么时候才能层层返回啊?
直接去搜索,不太容易找到答案。但jsdom,我们知道是dom.window.document.Element的关系。
把这个问题 通过document做过渡,再分成2步就容易了:
//1 SVGElement-> document
const doc = element_dest.ownerDocument;//2 document-> window
const window1 = doc.parentWindow || doc.defaultView;
这里问题和解决方案已经基本合一了,标志是说明意图的注释,已经基本和代码一致。没有注释必要了。
这里一旦突破,一切就都自然而然了,最终,搞2个函数:
function create_DOMParser_from_document(doc){ //统一获得DOMParser的方式,防止node和浏览器有区别 const window1 = doc.parentWindow || doc.defaultView; return new window1.DOMParser(); } function append_str_xml_to_element(element_dest,str_xml){ //把str_xml字符串添加到element (node)上 //兼容浏览器和node,不使用全局变量 document window //element-> document const doc_dest = element_dest.ownerDocument; const domparser = create_DOMParser_from_document(doc_dest); //临时创建的dom2 doc2 const doc2 = domparser.parseFromString(str_xml, "text/xml"); //过继node到doc_dest const node1 = doc_dest.adoptNode(doc2.documentElement); //添加节点 element_dest.appendChild(node1); }
使用时:
d3.select("xxx") .each(function (d) { //得到需要添加的字符串形式的svg片段 const str_svg = some_function(d); //this表示当前element 而 d表示绑定的数据 append_str_xml_to_element(this,str_svg); });
这种方法,通过jsdom和浏览器dom中 dom window document element的回溯访问方式(element.ownerDocument,doc.parentWindow || doc.defaultView)的不变性,保证了行为在不同运行环境下的效果统一。
没得到这个方法的时候,还要判断代码运行在node还是browser,分而治之。这里就不对比了。
总结——从一个函数看软件工程:
而对外暴露的接口,只接受str实现细节完全隐藏,没有把DOMParser前后依赖的这些破事暴露出来。
1 从价值上看,函数的名字,参数入口和返回值,一定要把问题域描述清楚(需要依赖什么输入)。
尽量把全部依赖都清晰写在入口,“收窄接口”只是最终效果,“明确目的+减少外部依赖”才是意图。
2 从函数定义那行到函数body部分语句块,其实还有一道隐形的鸿沟——问题域->解空间。
必须要有跨度。在能实现的前提下,越不相干,跨度越大,越有价值。
最好是
函数名人话(领域问题),
函数体外星咒语(解空间魔法)
3 body实现部分之间,尽量跨度小,关系尽量紧密,尽量没有冗余。每一行都要在这个领域最简单。
我喜欢用军队组织的层级编成来比喻代码:
每个源代码文件200-400行,大概相当于分队的管理机构。
每个函数100行以内,大概相当于1个分队。想这次这样的小函数,10几行,相当于1个班。
每行语句相当于1个战士完成的任务。
源代码=作战计划
编码=参谋指定计划,
测试,调试= 推演协同动作计划
需要分解、翻译、parse的思路,最终把作战计划翻译成每个最笨的战士也能听懂完成的简单任务。
尽量挖掘、利用标准、接口上显式的不变性,不要隐式依赖编译器、浏览器环境的特殊行为。
具体到混乱邪恶的js世界,要吃透es6,w3c 标准,用标准化的方式做事。