0%

Django-Echarts系列:js依赖文件管理

本文已收录于 《pyecharts 开发专辑》

django-echarts 是本人正在开发的一个开源项目,该项目旨在将 pyecharts 库整合到Django web框架中,从而形成echarts-python-django 大整合的项目。

继之前简单的一个使用示例之后,最近花了几天完成了一种的一个功能插件:js依赖文件管理。

1 Django-Echarts概述

django-echarts这个项目的目标和pyecharts是一致的,即在目标html页面上渲染图表。要完成一个Echarts图表,从最后渲染完成的HTML结构来看,至少应当包含以下三个部分:

  • 图表容器控件,比如<div id="id_mycharts"></div>
  • js依赖文件,比如 <script src='/static/echarts/echarts.min.js'></script>
  • 图表初始化代码,代码中 myCharts.setOptions(Foo) 所在的script标签。

这个排序按照一般出现的顺序,将所有 script 标签放置在body标签的最后,有利于页面的加载。

在这一过程中,pyecharts 项目为此做了大量的工作,使得我们能够快速地依据功能要求构建出模板渲染所需的数据,这些数据在Django模板系统中称之为 Context

在实际应用过程中,每个页面的结构都是各式各样的,不能一概而论,因此django-echarts的主要职责:

  • 如何创建上述三个标签(代码片段)。
  • 上述标签在目标html的位置和结构由用户选择。
  • 对于一些简单的页面,可以提供一些shortcut工具。

2 设计思路

2.1 目标

三者之前没有太大的关联性,是可以单独拿出来讨论其设计思想和实现方式的。js 依赖文件管理最终的目标是构建js文件路径字符串

1
2
<script src='/static/echarts/echarts.min.js'></script>
<script src='/static/echarts/map/china.js'></script>

虽然最后生成的是一个或多个script标签,但依据 Django MTV 原则,标签的构建应当由模板系统负责。

核心的src属性由路径和js文件名两部分组成。路径的意义在于,对于同一个 echarts.min.js 可以由不同的地方提供,这称之为repository。而文件名是由用户输入提供的。使用代码表示如下:

1
2
3
def generte_js_link(js_name):
host = '' # TODO Where to pick a host according the settings
return '{host}/{js_name}.js'.format(host=host, js_name=js_name)

2.2 问题

综上所述,js 依赖文件管理解决的问题:

  • 需要管理哪些依赖文件
  • 哪些repository可以提供js文件,其中哪些可以实现对其的支持。
  • 如何在不同repository之间尽可能平稳的切换,即它们之间必须提供统一的API
  • 需要对外提供哪些API,即在哪些情况下可能使用到这个功能

3 仓库(repository)与文件

按照正常逻辑,文件名由用户根据实际功能需求指定,其有效性应当交由用户确保。在实际过程不同repository可提供的文件是不一样。js文件分为核心库文件和地图数据文件两种。pyecharts能够提供本地和远程两种类型的repository,加上Django整合时,项目静态文件也可以作为一种repository存在,因此共有三种。

将repository和文件类型进行交叉分析,可整理出以下的一张表格:

仓库 核心库文件 地图数据文件 核心库版本支持 远程/ 本地
pyecharts本地 可提供 可提供 无,不可控 本地
pyecharts远程 可提供 可提供 无,不可控 远程
官方CDN 不提供 提供 - 远程
公共CDN 可提供 不提供 支持 远程
项目静态目录 可自定义 可自定义 可自定义 本地

分析如下:

  • 首先的是pyecharts本地作为存储仓库是不太合适,同为本地文件存储,Django项目静态库显然是一个更为合适的选择。
  • pyecharts远程库,路径为 https://chfw.github.io/jupyter-echarts/echarts, 该库的优势在于提供了一些列的自定义地图,但弱势是核心库文件没有版本管理,而且 github 仓库不建议作为静态文件托管服务。
  • 官方CDN:其实指的是地图文件数据下载的源地址。
  • 公共CDN:优势在于支持版本管理,缺点是不提供地图数据文件。公共CDN仅选择Echarts官方教程提及的三个CDN,其余的已经很久没有更新了。
  • 项目静态目录:通常由 setttings.STATIC_URL指定。 这是开发者自己从零开始构建的,因此自由度最大。为了方便,可开发从其他远程仓库下载文件的功能。

一个通常的使用场景如下:

  • 试验django-echarts,仅使用远程仓库,这样不必配置静态文件设置等。
  • 如果可用,通过下载工具下载到本地,并进行一系列开发。
  • 部署上线时,根据需要切换到CDN。

4 配置

4.1 基本配置

配置是项目初始化需要使用的。默认的配置如下:

1
2
3
4
5
6
DEFAULT_SETTINGS = {
'echarts_version': '3.7.0',
'lib_js_host': 'bootcdn',
'map_js_host': 'echarts',
'local_host': None
}

在实际运行之前,会将用户自定义配置和默认配置进行合并,并向外提供统一的模块变量用于访问。

关于这一部分可以期待之后的《django-echarts系列:配置模块》一文。

由于核心库文件和地图数据文件需要分开托管,因此需要使用两个变量分别指定和设置。二者都有自己有效的可选值。

4.2 远程/本地切换

local_host的作用有两点:

  • 提供公用变量,当 lib_js_hostmap_js_host 同时指定本地仓库时,可以借助该变量
  • 下载工具的目标目录。

5 运行分析

5.1 分开托管和文件识别

由于不同仓库提供的文件不同,通常分为可提供核心库文件和地图数据文件,因此需要分别两个查询表(在Python使用一个dict表示即可)。

这就带来了一个问题:用户输入的是 js_name,只有这个参数,而且由于自定义地图文件,理论上可以是任何一个有效的文件名字符串,如何识别为核心库文件还是地图数据文件,即从哪个字典查询,成了一个待解决的问题。

这个没有一个百分百正确的答案。目前采用一个简单办法:由于公共CDN提供的核心库文件是一定的,可提供一个核心库文件列表,判断js_name是否在其中即可。

5.2 输出URL

当确定完某一个仓库后,之后的url构建就比较简单了,本质上来说是python string format的一些封装。仓库路径具体和一些因素有关,这些因素都需要在项目初始化就已经确定了,放在 settings 模块是最为合适了。目前支持以下字段:

  • echarts_version:版本字符串,一些公共CDN需要指定版本号。
  • STATIC_URL:静态文件目录,通常用于项目本地仓库,该值等于settings.STATIC_URL.

在实现过程中,也有两点问题需要注意:

  • 字段的大小写问题,为了和settings.STATIC_URL一致,也采用了大写变量
  • 目录后缀/ ,依据Django规范 STATIC_URL是带有/的,而pyecharts的远程路径没有带有/,以及自己设置的第三方CDN中,这个问题需要作统一处理。目前是按照pyecharts没有带有/,不排除之后会依照Django规范,但是对外部使用是没有任何影响的。

基本代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

class Host(object):
HOST_LOOKUP = {}

def __init__(self, name_or_host, context=None, host_lookup=None, **kwargs):
context = context or {}
host_lookup = host_lookup or self.HOST_LOOKUP
host = host_lookup.get(name_or_host, name_or_host)
try:
self._host = host.format(**context).rstrip('/')
except KeyError as e:
self._host = None
raise KeyError('The "{0}" value is not applied for the host.'.format(*e.args))

@property
def host_url(self):
return self._host

def generate_js_link(self, js_name):
return '{0}/{1}.js'.format(self._host, js_name)

5.3 渲染html

这一过程是Django模板系统负责的,为了方面可以自定义一个模板标签echarts_js_dependencies解决这个问题。

以下是基本代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@register.simple_tag(takes_context=True)
def echarts_js_dependencies(context, *args):
links = []
for option_or_name in args:
if isinstance(option_or_name, Base):
for js_name in option_or_name.get_js_dependencies():
if js_name not in links:
links.append(js_name)
elif isinstance(option_or_name, six.text_type):
if option_or_name not in links:
links.append(option_or_name)
links = map(DJANGO_ECHARTS_SETTING.host_store.generate_js_link, links)

return template.Template('<br/>'.join(['<script src="{link}"></script>'.format(link=l) for l in links])).render(
context)

有几个注意点:

  • 标签是支持多个script渲染的。
  • 为了方便,列表的每一项支持 文件名或者 pyecharts.base.Base对象。
  • 在多个标签输出时,需要去掉那些重复的文件。
  • 因为html结构简单,所以使用register.simple_tag 就可以了。

6 CLI与Django命令

基于 django manage command 实现一个简单的 CLI,其核心功能是js文件下载。

下载工具提供将远程的js文件同步到本地静态文件目录中。该功能为manage命令,需符合其的一些用法规范。

1
2
3
4
5
usage: manage.py download_echarts_js [-h] [--version] [-v {0,1,2,3}]
[--settings SETTINGS]
[--pythonpath PYTHONPATH] [--traceback]
[--no-color] [--js_host JS_HOST]
js_name [js_name ...]

远程仓库的选择和限制条件可以使用伪代码表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
host = ''
if(命令提供了--js_host参数){
host = js_host参数值
}else{
if(js_name是否和核心库文件){
host = settings.DJANGO_ECHARTS['lib_js_host']
}else{
host = settings.DJANGO_ECHARTS['map_js_host']
}
}
if(host不是本地仓库){
执行后续操作
}

本地仓库的选择:只有一个限制条件就是符合本地仓库的要求,即必须以 settings.STATIC_URL 开头。

在配置方面(源目录、目标目录)仅支持 DJANGO_ECHARTS ,暂时还不支持命令行参数传入。这是下一阶段的重点内容。

7 和pyecharts的异同

7.1 扩展和取舍

Django-Echarts是pyecharts在Django环境的适配,在此过程中难免有所扩展和舍弃。

  • 在本地存储(离线模式)中,使用Django项目静态目录取代pyecharts本地js存储,并提供一个下载工具将远程js文件下载到Django静态目录,以便平稳过渡。
  • 提供一些常用CDN。
  • 分析ECharts组成,提供一些模板标签渲染Echarts的每个部件。

由于 pyecharts 尚未实现本地 js 库的完全独立,django_echarts 只是从形式上实现独立,在实际运行过程中还会引用js相关内容,期待pyecharts在这方面有所发展。

7.2 展望

本项目是基于 pyecharts 而发展的。pyecharts是一个非常棒的项目,解决在Python中使用echarts的问题,加强了Python在数据可视化方面的应用。

另一方面pyecharts刚刚面世两三个,目前着重于Echarts实例创建这一问题上,对于外围环境的问题涉及有所不足。

坚持原创技术分享,您的支持将鼓励我继续创作!