将 D3 注入 AngularJS 的正确约定

Posted

技术标签:

【中文标题】将 D3 注入 AngularJS 的正确约定【英文标题】:Proper Convention for Injecting D3 into AngualrJS 【发布时间】:2016-03-04 18:48:56 【问题描述】:

我看到了只使用全局 D3 对象的指令,我还看到了通过在服务中返回全局 D3 对象来注入全局 D3 对象的指令,并且我看到了添加 D3 脚本并返回一个在提供 D3 对象的脚本加载时解决的承诺。

在可注入服务中使用它似乎最有意义(参见示例 1 和 2),但我不确定哪种方式更好。示例 2 将保证在运行任何代码之前已加载 D3,但似乎没有人这样做,另外这意味着您必须将整个指令包装在服务中,否则 d3 和创建的 svg对象超出范围或可能未定义(参见示例 2),但至少我相信编译的承诺总是首先解决,参见示例 3。

示例 1:服务传递 D3 全局对象

.factory('D3Service', [,
    function () 

        // Declare locals or other D3.js
        // specific configurations here.

        return d3;
    ]);

示例 2:将 D3 脚本添加到 DOM 并传递承诺的服务

.factory('D3Service', ['$window', '$document', '$q', '$rootScope',
    function ($window, $document, $q, $rootScope) 

        var defer = $q.defer();

        var scriptTag = $document[0].createElement('script');
        scriptTag.type = 'text/javascript';
        scriptTag.src = 'https://d3js.org/d3.v3.min.js';
        scriptTag.async = true;
        scriptTag.onreadystatechange = function () 

            if (this.readyState == 'complete') 
                onScriptLoad();
            
        
        scriptTag.onload = onScriptLoad;

        var script = $document[0].getElementsByTagName('body')[0];
        script.appendChild(scriptTag);

        //---
        // PUBLIC API
        //---

        return 
            d3: function () 
                return defer.promise;
            
        ;

        //---
        // PRIVATE METHODS.
        //---

        // Load D3 in the browser
        function onScriptLoad () 
            $rootScope.$apply(function () 
                defer.resolve($window.d3);
            );
        
    ]);

示例 3:使用 Compile 添加 SVG 并不意味着 SVG 在 Link 中可用,但至少 compile 的 promise 总是会首先解决

        // Perform DOM and template manipulations
        function compile ($element, $attrs, $transclude) 

            var svg;

            // Callback provides raw D3 object
            D3Service.d3().then(function (d3) 

                // Create a responsive SVG root element
                svg = d3.select($element[0])
                .append('svg')
                .style('width', '100%');
            );

            // Return the link function
            return function($scope, $element, $attrs) 

                // Is svg undefined? 

                // Maybe? so have to wrap everything again in service
                D3Service.d3().then(function (d3) 

                   function render() 
                       // d3 and svg guaranteed to be available, but code gets really ugly looking and untestable
                   
                );

                function render() 
                    // d3 and svg have to be passed in as they may not be available, but code is cleaner
                
            ;
        

【问题讨论】:

是的。除非 d3.js 除了将 d3 放在窗口范围内之外,没有什么比 $interval 承诺检查它是否最终存在更优雅的了。 [在 IE8 上,无论如何] 嗨@Brian,所以在每个图形指令的link 中等待$interval 承诺,然后使用D3 的全局引用?似乎解决方案可以/应该是 DRYer,但也许不是...... 我会实现“如果 d3 未定义,则加载 d3 并等待,否则立即解决”。如果有人对包括IE8在内的浏览器有更好的解决方案,请@我! 我要么使用全局方法,要么使用第一种方法。我只看到尝试将其注入 DOM 的方法带来的痛苦。为什么要打扰? 【参考方案1】:

在遇到d3Angular 的问题时,我也有类似的问题。似乎有几种方法可以解决这个问题;每个都是可行的,但没有一个感觉光滑或自然。从本质上讲,d3Angular 似乎是两种截然不同的技术,它们在开箱即用时不能很好地结合使用。不要误会我的意思,他们一起工作非常出色,但他们需要彼此热身。所以充其量,我们可以在Angular 框架内给d3 一个操场。我相信这个游乐场应该是directive

但是关于返回承诺的模块化d3Service 方法(根据d3.js 文件的加载):

angular.module('myApp.directives', ['d3'])
  .directive('barChart', ['d3Service', function(d3Service) 
    return 
      link: function(scope, element, attrs) 
        d3Service.d3().then(function(d3) 
          // d3 is the raw d3 object
        );
      
  ]);

虽然ngNewsletter 中对此进行了很好的详细说明,但使用将script 标记直接写入 DOM 的服务似乎有点过头了,因为它可以与所有其他 javascript 文件一起包含在index.html 中.我的意思是,我们有一个directive,我们知道它使用了这个文件,那么为什么不故意加载它呢?似乎不需要跳过箍,只是:

<script src="/js/third-party/d3js/d3.min.js"></script>

但是,这种方法确实提供了模块化——假设我们正在构建多个应用程序并且每个应用程序都需要d3,那么是的,能够非常轻松地在应用程序级别注入我们的d3 模块非常棒。但是,您总是必须等待该承诺,即使我们知道它会在初始加载后立即解决,但您仍然需要解决它。在任何使用它的指令或控制器中。总是。无赖。

正如我所说,我选择在我的index.html 中包含d3.js,因此我可以在我的指令中访问它而无需解决承诺。这可能是一个并行:FWIW,我使用 JQuery 承诺而不是 Angular 承诺,那么当我需要 JQuery 时我该怎么办?好吧,我只是在需要时调用它 ($.Deferred()),我的意思是,以类似的方式调用 d3 对我来说似乎并不那么令人震惊。

虽然我确实使用了d3Service,但它更多的是用于辅助功能。例如,当我想获得一个 SVG 来进行工作时,为什么不直接调用一个函数来给我一个响应式 SVG:

指令(链接)

var svg = d3Service.getResponsiveCanvas(scope.id, margin, height, width);

服务

app.service('d3Service', function() 

  return 
      getResponsiveCanvas: function(id, margin, height, width) 
        return d3.select('#' + id)
              .append('div')
              .classed('svg-container', true)
              .append('svg')
              .attr('id', 'svg-' + id)
              .attr('preserveAspectRatio', 'xMinYMin meet')
              .attr('viewBox', '0 0 ' + (width + margin.left + margin.right) + ' ' + (height + margin.top + margin.bottom))
              .classed('svg-content-responsive', true)
              .append('g')
              .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
      
  
);

我有将轴添加到 SVG 的类似功能。这确实有代码味道,但同样,就其本质而言,d3 我们正在直接操作 DOM,所以我的经验是,无论我们把它放在哪里,它都会很丑陋,而且感觉不像 Angular,所以您不妨制作一些让您的生活更轻松的服务。

【讨论】:

哇,谢谢,这很有帮助。在将它添加到 index.html 并在指令中访问它时,您是否遇到过 d3 未定义的问题?似乎您必须添加一个 init() 方法,如 cmets 中建议的 @Brian,它在加载每个单独的图表之前运行 $interval 检查 $window.d3。从两个角度欣赏答案。 @mtpultz 因此,通过在我们的 HTML 文件中直接包含 d3,我们保证它会在 Angular 初始化时被加载并准备好使用。因此,我没有遇到 d3 未定义的情况。可以这样想,只要包含在我们的 HTML 中,它就只是另一个由浏览器加载并提供给 JS 引擎的 JS 文件。如果你使用 underscore.js 或 jquery,或任何其他第三方 JS 库,它是相同的流程。 @Brian 似乎正在引用承诺的加载,如果未正确加载,则可能未定义。但我注意到的方法不使用承诺 好吧,我总是忘记脚本是如何加载的。因此,这是基于它们按顺序加载的,只要您不将它们设置为异步即可。谢谢:)

以上是关于将 D3 注入 AngularJS 的正确约定的主要内容,如果未能解决你的问题,请参考以下文章

_servicename_ 中的下划线在 AngularJS 测试中是啥意思?

将模拟注入 AngularJS 服务

将 angularjs 服务注入 Angular

AngularJS - 将工厂注入指令的链接功能

D3.js 4 与 AngularJS 1.5(组件或指令?)

未捕获的类型错误:无法读取 AngularJS 和 D3 中未定义的属性“弧”