Skip to content

出浅入深实践APM

Qian.Sicheng edited this page Mar 29, 2019 · 32 revisions

简介

APM (AMD Package Manager) is a npm-based package manager for AMD.

APM是一种包管理的方式,APM发布时满足AMD及APMJS规范即可,而内部实现是自由的。本文结合基于APM-TS-CLI实践,介绍一个标准的APM Package工程的内部设计。

APM-TS-CLI是一个能够快速创建APM Package工程的cli工具,构建出的工程集成TS编译、测试、覆盖率、本地server等功能。

快速实践

安装cli

sudo npm i apm-ts-cli -g

创建项目

在当前目录新建hello-apm目录,并以此目录创建工程,过程中的选择皆回车选择默认

apm-ts init hello-apm

进入到工程目录,并完成安装(如果网络不好,请自行使用镜像或cnpm等安装)

cd hello-apm
npm install
cd demo && apmjs install

预览项目

默认配置下,工程会包含一个简单的示例,现在让我们本地运行起这个示例。

回到工程目录,进行第一次编译(默认发布产出到dist目录)

# 回到hello-apm目录
cd ../ && pwd
# xxxxx/hello-apm

npm run build
npm start

按照提示访问 http://127.0.0.1:8077/demo/?debug 即可预览demo

目录结构

以下是安装完成后,项目的主要目录结构

.
├── package.json
├── tsconfig.json                   本地编译、测试ts编译配置
├── tscompile.json                  amd发布编译配置
├── tslint.json                     tslint规范配置
├── gulpfile.js                     构建代码
├── .travis.yml                     travis持续集成配置(Github等平台)
├── src                             代码目录
│   └── *.ts                        ts源码
├── dist                            发布目录
│   └── *                           编译后的js文件
├── test                            测试目录
│   └── *.ts                        测试源码
└── demo                            本地Server测试项目目录
│   ├── amd_modules                 测试项目依赖的apm包安装目录
│   ├── package.json                测试项目配置(主要配置其他apm依赖)
│   ├── index.html                  入口文件
│   └── *
├── docs                            执行npm run doc后生成的文档目录
├── coveralls                       执行npm run cover后生成的测试报告目录
└── README.md

文档与测试报告

执行npm run doc后会编译出文档,可以查看http://127.0.0.1:8077/docs/在线示例

执行npm run cover后会完成测试及覆盖率统计报告,可以查看http://127.0.0.1:8077/coverage/

Coverage

修改Demo

首先运行npm run watch启动服务并监听文件改动实时部署,浏览器中打开http://127.0.0.1:8077/demo/?debug

Demo包实现了一个简单的播放器,在demo页面中,初始化了三个播放器实例并置于页面中,并且定义了一个播放源。 每个播放器可以单独控制播放状态,但是三个播放器所使用的播放源是相同的。 页面的底部的文本框可以改变播放源的信息,当播放源信息改变后,播放器标题会联动变化。

首先修改src/assert/player.etpl,顶部追加一个div:

<div class='bd-play-test'>Header</div>

保存后,刷新访问demo页面,会看到生效的变化:三个播放器上方都增加了Header。

然后我们修改提供播放源信息的提供者service.ts,修改其set方法如下:

public set(title: string) {
   
   this.title = `[Live] ${title}`;
}

保存后,刷新访问demo页面,会看到播放器标题都前置了[LIVE] 字样。

需要注意的是,因为修改了代码而未修改测试,原先的测试和持续集成都会失败。

发布

首先确保本地npm或apmjs的登录信息有效以及package.json中的包名称,关键信息正确。

执行npm version patch新增一个三位版本号,执行此命令前会触发npm run preversion进行lint及cover检查,如果成功会增加版本号及tags。

执行npm publish发布到外网,或如npm publish --registry=http://registry.npm.baidu-int.com发布到你的内网。

安装使用

在支持APM的工程中安装你的模块(工程如何支持APM,可参考运行环境)

# npm外网
apmjs install --save hello-apm

# 内网如
apmjs install --save @baidu/hello-apm --registry=http://registry.npm.baidu-int.com

然后在工程中按照相应的模块规范引用,如amd方式

require(['hello-apm'], function(HelloModule) {
  var hello = new HelloModule.Hello();
});

功能设计与实现

熟悉了开发各阶段工作后,下面介绍创建新工程后如何实现功能(以上述Demo功能的实现为案例),并给出一些规则与建议。

需求分析与设计

  • 针对功能需求,先尝试按功能分模块。
  • 对于复杂的场景(如交互多、事件多、分支多),也可以通过整理数据流图或流程图,确定模块。
  • 产出模块图,主要描述清楚模块间的关系以及接口。
  • 检查模块间的关系,是否有循环依赖、多重依赖,如果有需要通过设计模式消除,最常见的方式就是面向接口编程。
  • 检查整个包对外围的依赖,如果有,尽量以注入方式实现,避免直接依赖。

如上述需求,可以分为以下模块实现:

  • View主要聚焦视图的展现,但不保存状态;View不依赖任何模块
  • Service提供播放需要的信息;Service实现IService接口与IServiceSettable接口,前者定义了获取播放信息的接口,后者定义了初始化信息的接口
  • Player负责管理状态,驱动View渲染,并且通过调用IService接口,通过回调获取数据流;Player依赖IService接口与View

整个包的导出是Player与Service,外部模块使用时先创建Service,并通过IServiceSettable接口方法设置信息,再创建Player注入Service与渲染容器Container。

// src/index.ts
export { Player } from './player';
export { Service } from './service';

编码实现

在有了模块图(UML)后,我们首先可以定义好接口,进而一步一步实现类、方法与各种功能,具体的编码工作相信会成为乐趣。

关于上述功能的实现,在线可以浏览Demo源码

重构与改善

即便是预先设计,也可能存在不完美的地方,但不用担心,前期的设计已经打下一定基础,结合TypeScript重构会轻而易举。

如果不知道该如何重构,通常可以从以下常见方向着手,让包的结构看起来更好:

  • 职责独立
  • 最少知道
  • 消除依赖

按照上述方向,再分析刚才的模块图,可以发现2个问题:

  • Player仍然直接依赖了View
  • 包导出的Service类存在非IServiceSettable接口上的方法

第一个问题因为其调用了View的onClickIcon方法来绑定事件,如果有必要的话可以把事件的传递通过其他模式来实现,来消除Player与View的耦合(当然如果你打算这样做,那么你即将开始实现一套框架)。

第二个问题在于外部创建可用的Service,需要用到IServiceSettable接口方法;但Service同时实现了IService接口,所以后者的接口方法可能会被滥用。解决这个问题可以通过工厂等设计模式来实现。

运行环境与开发调试

APM大部分情况不是独立运行的,所以要在运行时环境中解决好依赖于被依赖的问题,所以本小节以Hello-apm包的本地运行环境为例进行介绍。

页面可能会存在几种的依赖情形,需要不同的解决方案。

  • 始终依赖一个线上已发布包
    • 解释:如依赖线上unpkg的包
    • 方案:require.config options.paths指定线上unpkg服务路径,如//unpkg.zhimg.com/etpl
  • 始终依赖本地
    • 解释:如内网包,通过apmjs安装后,存放于amd_modules目录
    • 方案:设置baseUrl:amd_modules路径
  • 正常依赖线上,开启debug时依赖本地
    • 解释:开发阶段希望依赖本地,部署到线上(如github.io)希望依赖线上unpkg的包

以Hello-apm demo页面为例,相应的配置代码如下:

// https://github.com/apmjs/apm-ts-cli/blob/master/project/gulp/demo/common.js

if (/debug/.test(location.search)) {
  localStorage.debug = 'app:*';
}
var debug = localStorage.debug;
if (debug) {
  console.log('启用debug模式,使用amd_modules中的模块');
} else {
  console.log('使用在线npm包,如果未发布到在线,apmjs install后,可以使用?debug参数进行本地包调试');
}
require.config({
  baseUrl: debug ? 'amd_modules' : '//unpkg.zhimg.com',
    paths: {
        // 强制etpl在任何使用使用线上版本
        "etpl": '//unpkg.zhimg.com/etpl'
    },
    waitSeconds: 30
});

通常还有一类常见场景,如当前开发的APM模块Hello-apm依赖另一个APM模块XXX,如果希望同时开发两个模块并在Hello-apm运行环境看效果,可以按如下步骤操作:

  • 在Hello-apm的运行环境(Demo)目录安装XXX,安装完成后位于amd_modules/XXX
  • 按照上述配置debug时,config baseUrl指向到线下amd_modules/目录
  • 进入XXX包的开发目录执行sudo apmjs link
  • 进入Hello-apm的运行环境(Demo)目录执行apmjs link XXX
  • 至此,完成amd_modules/XXX软链到XXX包的开发目录,可以一边开发XXX包,一边在Hello-apm环境看效果

欢迎贡献

APM-TS-CLI目前还有待完善,欢迎代码贡献,以下是TODO:

  • 增加webpack的持续集成
  • cli集成持续集成命令(模块不关注构建工具)
  • jest替换mocha
  • e2e测试