angular-modal-service源码深潜:showModal从获取模板到$compile编译的完整流程拆解
【免费下载链接】angular-modal-serviceModal service for AngularJS - supports creating popups and modals via a service.项目地址: https://gitcode.com/gh_mirrors/an/angular-modal-service
angular-modal-service 是 AngularJS 生态中一个轻量级的弹窗(Modal)服务,核心 API 只有一个 ——showModal。一次调用就能完成:获取模板 → 创建作用域 → $compile 编译 → 注入控制器 → 插入 DOM。今天我们把整个流程完整拆解一遍,新手也能看懂。🔍
先看整体:showModal 的 7 步流程
整个服务实现集中在单文件 src/angular-modal-service.js(约 320 行),showModal定义在 src/angular-modal-service.js。调用一次ModalService.showModal(options)后,源码按以下顺序执行:
| 步骤 | 做什么 | 关键代码位置 |
|---|---|---|
| 1️⃣ 参数校验 | 检查controller,缺失则 reject | L130-L134 |
| 2️⃣ 获取模板 | getTemplate取 template/templateUrl | L29-L44 |
| 3️⃣ 创建作用域 | modalScope+close函数注入 | L144-L197 |
| 4️⃣ 编译模板 | $compile+ link 生成 DOM | L204-L206 |
| 5️⃣ 创建控制器 | $controller注入 inputs 和 close | L210 |
| 6️⃣ 插入 DOM | $animate.enter添加到 body/自定义容器 | L217-L228 |
| 7️⃣ 返回 modal 对象 | 携带 element/scope/close 等属性 | L236-L251 |
调用侧的完整长这样(摘自 samples/sampleapp.js):
ModalService.showModal({ templateUrl: "yesno/yesno.html", controller: "YesNoController" }).then(function(modal) { modal.element.modal(); modal.close.then(function(result) { /* 拿到弹窗结果 */ }); });第 1 步:参数校验与组件模式
showModal入口先做一件"防御性"的事:检查是否传了controller,没有则直接 reject 并返回(src/angular-modal-service.js)。这是整个 Promise 链最早的失败点。
如果传的是component(AngularJS 1.5+ 组件),源码会先调用setupComponentOptions(L113-L116),自动把组件名转成 kebab-case 模板,并生成一个内联控制器,把bindings透传给组件 —— 也就是说组件模式最终仍然走"模板 + 控制器"这条主线。
第 2 步:getTemplate —— 模板从哪里来
模板获取封装在getTemplate函数(src/angular-modal-service.js),优先级如下:
template:直接给了 HTML 字符串 → 立即 resolve,不发请求;templateUrl:走 AngularJS 的$templateRequest(templateUrl, true)发 HTTP 请求,第二个参数true表示启用$templateCache缓存——同一个弹窗第二次打开就不会再请求服务器;- 两者都没有:reject 出
"No template or templateUrl has been specified."。
模板缓存行为有专门的测试覆盖,见 test/template.spec.js。请求失败时错误会沿 Promise 链传播到最外层(L308-L310),这就是为什么调用方可以统一用.catch兜底。
第 3 步:创建独立 scope 与 close 机制
拿到模板后(getTemplate(...).then(function (template) {...})),源码开始组装弹窗的"三件套":
- scope:
(options.scope || $rootScope).$new()(L144)——弹窗默认挂在$rootScope下拥有独立子作用域,也可以通过scope选项指定父级,避免路由切换时误关弹窗; - inputs 对象:包含
$scope和一个核心的close(result, delay)函数(L177-L197)。close是弹窗内部"自杀按钮":调用后先触发可选的preClose钩子,再通过$timeout(delay)延迟销毁,给关闭动画留时间; - 防重入:
hasAlreadyBeenClosed标志位保证close只生效一次。
此外还会注册$locationChangeSuccess监听(L148-L164):路由一变就自动关闭弹窗,可通过locationChangeSuccess选项设为false禁用或传毫秒数延迟,相关行为可在 test/dom.spec.js 中对照阅读。
第 4 步:$compile 编译 —— 本文标题的关键
这是整个流程的技术核心,只有两行(src/angular-modal-service.js):
var linkFn = $compile(template); var modalElement = linkFn(modalScope); inputs.$element = modalElement;$compile(template)把 HTML 字符串变成链接函数linkFn;linkFn(modalScope)执行链接:生成真实 DOM 元素modalElement,并把模板里的ng-click、{{ }}等指令与modalScope绑定;- 生成后立刻把元素挂到
inputs.$element,这样弹窗控制器里可以注入$element直接操作 DOM(例如手动调用 Bootstrap 的modal('hide'))。
模板只有一个根元素这一点很重要:链接函数返回的元素集合会被整体插入 DOM 并交给modal.element,多个顶层元素(哪怕多一条 HTML 注释)会导致行为异常,README 的 FAQ 专门讨论了这个坑。
第 5 步:$controller 创建控制器
var modalController = $controller(options.controller, inputs, false, options.controllerAs);(src/angular-modal-service.js)注意三个细节:
- 第二个参数是上一步组装的
inputs(已用angular.extend合并了用户传入的options.inputs,见 L200),所以close、$element和自定义数据都能按名称注入控制器构造函数; - 第三个参数
false表示不自动把控制器实例挂到 scope 上,改由controllerAs手动控制; - 支持内联控制器函数 +
controllerAs的写法,源码会用angular.extend合并同名实例(L212-L214)。
控制器侧只需声明注入close,如 samples/yesno/yesnocontroller.js:close(result, 500)即"携带结果关闭,预留 500ms 动画时间"。
第 6 步:$animate.enter 插入 DOM
插入前会先确定目标容器(L216-L228):
- 默认挂到
<body>; appendElement为字符串时按 CSS 选择器查询 DOM;appendElement为元素对象时直接挂上去。
真正的插入由appendChild辅助函数完成(L49-L55),它调用$animate.enter而不是append,因此进入时会自动触发ng-enter动画钩子 —— 配合 ngAnimate 就能零 JS 实现淡入。如果设置了bodyClass,还会在 body 上加一个临时 class(L231-L233),方便用 CSS 控制背景滚动锁定等效果。
第 7 步:返回 modal 对象,Promise resolve
最后源码填充modal对象并deferred.resolve(modal)(L236-L251),调用方在.then中拿到的就是它:
| 属性 | 说明 |
|---|---|
modal.element | 编译好的 DOM 元素(可modal.element.modal()调用 Bootstrap) |
modal.scope/modal.controller | 弹窗的 scope 和控制器实例 |
modal.close | close函数被调用时立即 resolve 的 Promise |
modal.closed | 关闭动画播完、元素彻底移除后才 resolve 的 Promise |
resolve 之前还有一个细节:若控制器定义了$onInit(1.6+ 组件生命周期),会主动调用(L246-L248);随后document.activeElement.blur()清除输入焦点,防止连续按回车重复弹出多个弹窗。每个打开的弹窗都会 push 进self.openModals数组(L257),供closeModals(result, delay)(L58-L64)一键全关。
关闭与清理:防内存泄漏的最后一步
close触发的cleanUpClose(src/angular-modal-service.js)值得单独看,它是"优雅销毁"的范本:
- resolve
closePromise; - 移除
bodyClass; $animate.leave(modalElement)触发ng-leave退出动画;- 动画结束后:resolve
closed→modalScope.$destroy()→ 从openModals移除; - 把
inputs、deferred、modal等闭包引用全部置null——源码注释直言这是为了防止内存泄漏。
close(点了关闭)与closed(彻底移除)两个 Promise 的分工,就是"回调时机"与"清理时机"的分离,值得在自己的服务设计里借鉴。
上手练习:跑示例、读测试
想亲手验证以上每一步,建议按这个顺序:
- 获取源码:
git clone https://gitcode.com/gh_mirrors/an/angular-modal-service npm install && npm test跑测试,重点看 test/template.spec.js(模板获取与缓存)、test/dom.spec.js(DOM 插入与移除)、test/close.spec.js(close 行为);npm start后打开 localhost:8080 玩 samples/ 下的三个示例:yesno(基础)、complex(inputs + preClose)、custom(bodyClass);- 对照本文的 7 步,在
showModal里下断点单步走一遍。
全局默认值(如closeDelay)通过 Provider 配置,见 ModalServiceProvider.configureOptions 及示例 samples/sampleapp.js。
小结
angular-modal-service 用不到 320 行代码给出了一个教科书级的弹窗服务实现:$q 驱动的全流程 Promise 化、$compile 编译复用、inputs 注入解耦、$animate 动画钩子、显式置 null 防泄漏。读懂showModal这一个函数,基本就掌握了 AngularJS 1.x 里 service + compile + controller 协作的完整套路。✅
【免费下载链接】angular-modal-serviceModal service for AngularJS - supports creating popups and modals via a service.项目地址: https://gitcode.com/gh_mirrors/an/angular-modal-service
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考