一个JavaScript文件,放在books页面要输出"books",放在authors页面要输出"authors",你会怎么写?

最直接的办法是堆if语句。判断当前路径,走不同分支。代码能跑,但每加一个页面就多一个if,逻辑越缠越紧,改一处怕碰坏另一处。

打开网易新闻 查看精彩图片

有没有一种写法,让调用方根本不知道具体逻辑从哪来,只管执行?Django的模板系统加上JavaScript的import maps,正好能拼出这个效果。

先搞清楚要解决什么

策略模式是一种行为型设计模式,核心是在运行时选择算法。代码不直接实现某个算法,而是接收运行时的指令,决定用一组算法里的哪一个。

放到前端场景里,就是同一段调用逻辑,在不同页面加载不同的实现。

传统写法长这样:

  • 判断路径是不是books,是就写入books的文案
  • 判断路径是不是authors,是就写入authors的文案
  • 每多一个页面,就多一个判断分支

这种代码的膨胀速度,写过的人都懂。真正想做的,是把"用哪个实现"这件事从调用方剥离出去。

理想状态下,调用方只写一行:从strategy模块导入renderText,然后执行。至于strategy指向哪个文件,调用方不关心。

import maps补上了关键一环

这里说的是JavaScript的ES模块,语法是import ... from "module-name"。

ES模块有个特点:它是静态的,运行时不能改。而且直到不久前,浏览器根本不支持裸导入——也就是import { renderText } from "strategy"这种写法,strategy不是路径,浏览器不知道去哪找。

这种裸导入以前只有webpack这类模块打包器支持。

变化来自JavaScript import maps。它的作用是控制import语句最终去抓取哪个URL。有了它,浏览器里也能用裸导入,而且导入的URL可以在运行时被替换。

再叠加上Django模板系统的灵活性,一个简化版的策略模式就能落地了。

靠模板继承来换实现

具体做法利用了Django模板的一个能力:子模板可以继承父模板,并覆盖指定的模板块。

流程拆开就两步:

  1. 子模板继承父模板
  2. 子模板覆盖js块,在里面声明自己的import map

示例项目由三个应用组成。core放基础模板和基础JavaScript文件,authors和books各自继承core的基础模板。

core应用里,base.html声明了content和js两个可被覆盖的模板块,同时加载一个JavaScript模块。这个基础文件Base.js的内容很干净:从strategy导入renderText,然后执行。它完全不知道strategy这个文件在哪。

authors应用的list.html继承core的base.html,覆盖js块,声明自己的import map,把strategy指向authors/js/Authors.js,然后用block.super保留父模板原有的内容。

在Authors.js里,导出renderText函数,写入对应的文案。books应用照同样的方式做一遍,把strategy指向自己的文件。

结果就是:Base.js一个字没改,在books页面和authors页面跑出了不同的行为。

几个值得注意的细节

模块导入名不一定是strategy,可以叫任何名字,只要子模板里引用的是同一个名字就行。

另外,Django社区已经在推进相关支持。Matthias Kestenholz基于Thibaud Colas此前的工作,提出了一个给Django添加ImportMap对象的提案,值得关注后续进展。

这个方案的价值不在于多巧妙,而在于它把"选择哪个实现"这件事,从JavaScript代码里挪到了模板层。调用逻辑保持稳定,变化被隔离在各自的子模板中。对多应用、多页面共享同一段前端逻辑的Django项目来说,这是一种值得一试的组织方式。