整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Babel 插件:30分钟从入门到实战

Babel 插件:30分钟从入门到实战

abel 是一个 source to source(源码到源码)的 JavaScript 编译器,简单来说,你为 Babel 提供一些 JavaScript 代码,Babel 可以更改这些代码,然后返回给你新生成的代码。Babel 主要用于将 ECMAScript 2015+ 代码转换为能够向后兼容的 JavaScript 版本。Babel 使用插件系统进行代码转换,因此任何人都可以为 babel 编写自己的转换插件,以支持实现广泛的功能。

Babel 编译流程

Babel 的编译流程主要分为三个部分:解析(parse),转换(transform),生成(generate)。

code -> AST -> transformed AST -> transformed code
  • 解析 Parse

将源码转换成抽象语法树(AST, Abstract Syntax Tree)。

比如:

function square(n) {
  return n * n;
}

以上的程序可以被转换成类似这样的抽象语法树:

- FunctionDeclaration:
  - id:
    - Identifier:
      - name: square
  - params [1]
    - Identifier
      - name: n
  - body:
    - BlockStatement
      - body [1]
        - ReturnStatement
          - argument
            - BinaryExpression
              - operator: *
              - left
                - Identifier
                  - name: n
              - right
                - Identifier
                  - name: n
  • 转换 Transform

转换阶段接受一个 AST 并遍历它,在遍历的过程中对树的节点进行增删改。这也是运行 Babel 插件的阶段。

  • 生成 Generate

将经过一系列转换之后的 AST 转换成字符串形式的代码,同时还会创建 sourcemap。

你会用到的一些工具库

对于每一个阶段,Babel 都提供了一些工具库:

  • Parse 阶段可以使用 @babel/parser 将源码转换成 AST。
  • Transform 阶段可以使用 @babel/traverse 调用 visitor 函数遍历 AST,期间可以使用 @babel/types 创建 AST 和检查 AST 节点的类型,批量创建 AST 的场景下可以使用 @babel/template 中途还可以使用 @babel/code-frame 打印报错信息。
  • Generate 阶段可以使用 @babel/generator 根据 AST 生成代码字符串和 sourcemap。

以上提及的包都是 @babel/core 的 dependencies,所以只需要安装 @babel/core 就能访问到它们。

除了上面提到的工具库,以下工具库也比较常用:

  • @babel/helper-plugin-utils:如果插件使用者的 Babel 版本没有您的插件所需的 API,它能给用户提供明确的错误信息。
  • babel-plugin-tester:用于帮助测试 Babel 插件的实用工具,通常配合 jest 使用。

本文不会深入讨论它们的详细用法,当你在编写插件的时候,可以根据功能需求找到它们,我们后文也会涉及到部分用法。

认识 Babel 插件

接下来让我们开始认识 Babel 插件吧。

babel 插件是一个简单的函数,它必须返回一个匹配以下接口的对象。如果 Babel 发现未知属性,它将抛出错误。

以下是一个简单的插件示例:

export default function(api, options, dirname) {
  return {
    visitor: {
      StringLiteral(path, state) {},
    }
  };
};

Babel 插件接受 3 个参数:

  • api:一个对象,包含了 types (@babel/types)、traverse (@babel/traverse)、template(@babel/template) 等实用方法,我们能从这个对象中访问到 @babel/core dependecies 中包含的方法。
  • options:插件参数。
  • dirname:目录名。

返回的对象有 name、manipulateOptions、pre、visitor、post、inherits 等属性:

  • name:插件名字。
  • inherits:指定继承某个插件,通过 Object.assign 的方式,和当前插件的 options 合并。
  • visitor:指定 traverse 时调用的函数。
  • pre 和 post 分别在遍历前后调用,可以做一些插件调用前后的逻辑,比如可以往 file(表示文件的对象,在插件里面通过 state.file 拿到)中放一些东西,在遍历的过程中取出来。
  • manipulateOptions:用于修改 options,是在插件里面修改配置的方式。

我们上面提到了一些陌生的概念:visitor、path、state,现在让我们一起来认识它们:

  • visitor 访问者

这个名字来源于设计模式中的访问者模式(https://en.wikipedia.org/wiki/Visitor_pattern)。简单的说它就是一个对象,指定了在遍历 AST 过程中,访问指定节点时应该被调用的方法。

  • 假如我们有这样一段程序:
function foo() {
	return 'string'
}
  • 这段代码对应的 AST 如下:
- Program
	- FunctionDeclaration (body[0])
		- Identifier (id)
		- BlockStatement (body)
			- ReturnStatement (body[0])
			- StringLiteral (arugument)
  • 当我们对这颗 AST 进行深度优先遍历时,每次访问 StringLiteral 都会调用 visitor.StringLiteral。

当 visitor.StringLiteral 是一个函数时,它将在向下遍历的过程中被调用(即进入阶段)。当 visitor.StringLiteral 是一个对象时({ enter(path, state) {}, exit(path, state) {} }),visitor.StringLiteral.enter 将在向下遍历的过程中被调用(进入阶段),visitor.StringLiteral.exit 将在向上遍历的过程中被调用(退出阶段)。

  • Path 路径

Path 用于表示两个节点之间连接的对象,这是一个可操作和访问的巨大可变对象。

Path 之间的关系如图所示:

除了能在 Path 对象上访问到当前 AST 节点、父级 AST 节点、父级 Path 对象,还能访问到添加、更新、移动和删除节点等其他方法,这些方法提高了我们对 AST 增删改的效率。

  • State 状态

在实际编写插件的过程中,某一类型节点的处理可能需要依赖其他类型节点的处理结果,但由于 visitor 属性之间互不关联,因此需要 state 帮助我们在不同的 visitor 之间传递状态。

一种处理方式是使用递归,并将状态往下层传递:

const anotherVisitor={
	Identifier(path) {
		console.log(this.someParam) //=> 'xxx'
	}
};

const MyVisitor={
	FunctionDeclaration(path, state) {
		// state.cwd: 当前执行目录
		// state.opts: 插件 options
		// state.filename: 当前文件名(绝对路径)
		// state.file: BabelFile 对象,包含当前整个 ast,当前文件内容 code,etc.
		// state.key: 当前插件名字
		path.traverse(anotherVisitor, { someParam: 'xxx' });
	}
};

另外一种传递状态的办法是将状态直接设置到 this 上,Babel 会给 visitor 上的每个方法绑定 this。在 Babel 插件中,this 通常会被用于传递状态:从 pre 到 visitor 再到 post。

export default function({ types: t }) {
	return {
		pre(state) {
			this.cache=new Map();
		},
		visitor: {
			StringLiteral(path) {
				this.cache.set(path.node.value, 1);
			}
		},
		post(state) {
			console.log(this.cache);
		}
	};
}

常用的 API

Babel 没有完整的文档讲解所有的 api,因此下面会列举一些可能还算常用的 api(并不是所有,主要是 path 和 types 上的方法或属性),我们并不需要全部背下来,在你需要用的时候,能找到对应的方法即可。

你可以通过 babel 的 typescript 类型定义找到以下列举的属性和方法,还可以通过 Babel Handbook 找到它们的具体使用方法。

Babel Handbook:https://astexplorer.net/

  • 查询
    • path.node:访问当前节点
    • path.get():获取属性内部的 path
    • path.inList:判断路径是否有同级节点
    • path.key:获取路径所在容器的索引
    • path.container:获取路径的容器(包含所有同级节点的数组)
    • path.listKey:获取容器的key
    • path.getSibling():获得同级路径
    • path.findParent():对于每一个父路径调用 callback 并将其 NodePath 当作参数,当 callback 返回真值时,则将其 NodePath 返回
    • path.find():与 path.findParent 的区别是,该方法会遍历当前节点
  • 遍历
    • path.stop():跳过遍历当前路径的子路径
    • path.skip():完全停止遍历
  • 判断
    • types.isXxx():检查节点的类型,如 types.isStringLiteral(path.node)
    • path.isReferencedIdentifier():检查标识符(Identifier)是否被引用
  • 增删改
    • path.replaceWith():替换单个节点
    • path.replaceWithMultiple():用多节点替换单节点
    • path.replaceWithSourceString():用字符串源码替换节点
    • path.insertBefore() / path.insertAfter():插入兄弟节点
    • path.get('listKey').unshiftContainer() / path.get('listKey').pushContainer():插入一个节点到数组中,如 body
    • path.remove():删除一个节点
  • 作用域
    • path.scope.hasBinding(): 从当前作用域开始向上查找变量
    • path.scope.hasOwnBinding():仅在当前作用域中查找变量
    • path.scope.generateUidIdentifier():生成一个唯一的标识符,不会与任何本地定义的变量相冲突
    • path.scope.generateUidIdentifierBasedOnNode():基于某个节点创建唯一的标识符
    • path.scope.rename():重命名绑定及其引用

AST Explorer

在 @babel/types 的类型定义中,可以找到所有 AST 节点类型。我们不需要记住所有节点类型,社区内有一个 AST 可视化工具能够帮助我们分析 AST:axtexplorer.net。

在这个网站的左侧,可以输入我们想要分析的代码,在右侧会自动生成对应的 AST。当我们在左侧代码区域点击某一个节点,比如函数名 foo,右侧 AST 会自动跳转到对应的 Identifier AST 节点,并高亮展示。

我们还可以修改要 parse 的语言、使用的 parser、parser 参数等。

自己实现一个插件吧

现在让我们来实现一个简单的插件吧!以下是插件需要实现的功能:

  1. 将代码里重复的字符串字面量(StringLiteral)提升到顶层作用域。
  2. 接受一个参数 minCount,它是 number 类型,如果某个字符串字面量重复次数大于等于 minCount 的值,则将它提升到顶层作用域,否则不做任何处理。

因此,对于以下输入:

const s1="foo";
const s2="foo";

const s3="bar";

function f1() {
  const s4="baz";
  if (true) {
    const s5="baz";
  }
}

应该输出以下代码:

var _foo="foo",
  _baz="baz";
const s1=_foo;
const s2=_foo;
const s3="bar";

function f1() {
  const s4=_baz;

  if (true) {
    const s5=_baz;
  }
}

通过 https://astexplorer.net/,我们发现代码里的字符串在 AST 上对应的节点叫做 StringLiteral,如果想要拿到代码里所有的字符串并且统计每种字符串的数量,就需要遍历 StringLiteral 节点。

我们需要一个对象用于存储所有 StringLiteral,key 是 StringLiteral 节点的 value 属性值,value 是一个数组,用于存储拥有相同 path.node.value 的所有 path 对象,最后把这个对象存到 state 对象上,以便于在遍历结束时能统计相同字符串的重复次数,从而可以判断哪些节点需要被替换为一个标识符。

export default function() {
  return {
    visitor: {
      StringLiteral(path, state) {
        state.stringPathMap=state.stringPathMap || {};
        const nodes=state.stringPathMap[path.node.value] || [];
        nodes.push(path);
        state.stringPathMap[path.node.value]=nodes;
      }
    }
  };
}

通过 https://astexplorer.net/,我们发现如果想要往顶层作用域中插入一个变量,其实就是往 Program 节点的 body 上插入 AST 节点。Program 节点也是 AST 的顶层节点,在遍历过程的退出阶段,Program 节点是最后一个被处理的,因此我们需要做的事情是:根据收集到的字符串字面量,分别创建一个位于顶层作用域的变量,并将它们统一插入到 Program 的 body 中,同时将代码中的字符串替换为对应的变量。

export default function() {
  return {
    visitor: {
      StringLiteral(path, state) { /** ... */ },
      Program: {
        exit(path, state) {
          const { minCount=2 }=state.opts || {};
      
          for (const [string, paths] of Object.entries(state.stringPathMap || {})) {
            if (paths.length < minCount) {
              continue;
            }
      
            const id=path.scope.generateUidIdentifier(string);
      
            paths.forEach(p=> {
              p.replaceWith(id);
            });
      
            path.scope.push({ id, init: types.stringLiteral(string) });
          }
        },
      },
    }
  };
}

完整代码

import { PluginPass, NodePath } from '@babel/core';
import { declare } from '@babel/helper-plugin-utils';

interface Options {
  /**
   * 当字符串字面量的重复次数大于或小于 minCount,将会被提升到顶层作用域
   */
  minCount?: number;
}

type State=PluginPass & {
  // 以 StringLiteral 节点的 value 属性值为 key,存放所有 StringLiteral 的 Path 对象
  stringPathMap?: Record<string, NodePath[]>;
};

const HoistCommonString=declare<Options>(({ assertVersion, types }, options)=> {
  // 判断当前 Babel 版本是否为 7
  assertVersion(7);

  return {
    // 插件名字
    name: 'hoist-common-string',

    visitor: {
      StringLiteral(path, state: State) {
        // 将所有 StringLiteral 节点对应的 path 对象收集起来,存到 state 对象里,
        // 以便于在遍历结束时能统计相同字符串的重复次数
        state.stringPathMap=state.stringPathMap || {};

        const nodes=state.stringPathMap[path.node.value] || [];
        nodes.push(path);

        state.stringPathMap[path.node.value]=nodes;
      },

      Program: {
        // 将在遍历过程的退出阶段被调用
        // Program 节点是顶层 AST 节点,可以认为 Program.exit 是最后一个执行的 visitor 函数
        exit(path, state: State) {
          // 插件参数。还可以通过 state.opts 拿到插件参数
          const { minCount=2 }=options || {};

          for (const [string, paths] of Object.entries(state.stringPathMap || {})) {
            // 对于重复次数少于 minCount 的 Path,不做处理
            if (paths.length < minCount) {
              continue;
            }

            // 基于给定的字符串创建一个唯一的标识符
            const id=path.scope.generateUidIdentifier(string);

            // 将所有相同的字符串字面量替换为上面生成的标识符
            paths.forEach(p=> {
              p.replaceWith(id);
            });

            // 将标识符添加到顶层作用域中
            path.scope.push({ id, init: types.stringLiteral(string) });
          }
        },
      },
    },
  };
});

测试插件

测试 Babel 插件有三种常用的方法:

  • 测试转换后的 AST 结果,检查是否符合预期
  • 测试转换后的代码字符串,检查是否符合预期(通常使用快照测试)
  • 执行转换后的代码,检查执行结果是否符合预期

我们一般使用第二种方法,配合 babel-plugin-tester 可以很好地帮助我们完成测试工作。配合 babel-plugin-tester,我们可以对比输入输出的字符串、文件、快照。

import pluginTester from 'babel-plugin-tester';
import xxxPlugin from './xxxPlugin';

pluginTester({
  plugin: xxxPlugin,
  fixtures: path.join(__dirname, '__fixtures__'),
  tests: {
    // 1. 对比转换前后的字符串
    // 1.1 输入输出完全一致时,可以简写
    'does not change code with no identifiers': '"hello";',
    // 1.2 输入输出不一致
    'changes this code': {
      code: 'var hello="hi";',
      output: 'var olleh="hi";',
    },
    // 2. 对比转换前后的文件
    'using fixtures files': {
      fixture: 'changed.js',
      outputFixture: 'changed-output.js',
    },
    // 3. 与上一次生成的快照做对比
    'using jest snapshots': {
      code: `
        function sayHi(person) {
          return 'Hello ' + person + '!'
        }
      `,
      snapshot: true,
    },
  },
});

本文将以快照测试为例,以下是测试我们插件的示例代码:

import pluginTester from 'babel-plugin-tester';
import HoistCommonString from '../index';

pluginTester({
  // 插件
  plugin: HoistCommonString,
  // 插件名,可选
  pluginName: 'hoist-common-string',
  // 插件参数,可选
  pluginOptions: {
    minCount: 2,
  },
  tests: {
    'using jest snapshots': {
      // 输入
      code: `const s1="foo";
      const s2="foo";

      const s3="bar";

      function f1() {
        const s4="baz";
        if (true) {
          const s5="baz";
        }
      }`,
      // 使用快照测试
      snapshot: true,
    },
  },
});

当我们运行 jest 后(更多关于 jest 的介绍,可以查看 jest 官方文档https://jestjs.io/docs/getting-started),会生成一个 snapshots 目录:

有了快照以后,每次迭代插件都可以跑一下单测以快速检查功能是否正常。快照的更新也很简单,只需要执行 jest --updateSnapshot

使用插件

如果想要使用 Babel 插件,需要在配置文件里添加 plugins 选项,plugins 选项接受一个数组,值为字符串或者数组。以下是一些例子:

// .babelrc
{
    "plugins": [
        "babel-plugin-myPlugin1",
        ["babel-plugin-myPlugin2"],
        ["babel-plugin-myPlugin3", { /** 插件 options */ }],
        "./node_modules/asdf/plugin"
    ]
}

Babel 对插件名字的格式有一定的要求,比如最好包含 babel-plugin,如果不包含的话也会自动补充。以下是 Babel 插件名字的自动补全规则:

到这里,Babel 插件的学习就告一段落了,如果大家想继续深入学习 Babel 插件,可以访问 Babel 的仓库(https://github.com/babel/babel/tree/main/packages)这是一个 monorepo,里面包含了很多真实的插件,通过阅读这些插件,相信你一定能对 Babel 插件有更深入的理解!

参考文档

Babel plugin handbook:https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md

Babel 官方文档:https://babeljs.io/docs/en/

Babel 插件通关秘籍:https://juejin.cn/book/6946117847848321055

里云播放器SDK(ApsaraVideo for Player SDK)是阿里视频云端到云到端服务的重要一环,除了支持点播和直播的基础播放功能外,还深度融合视频云业务,支持视频的加密播放、安全下载、首屏秒开、低延时等业务场景,为用户提供简单、快速、安全、稳定的视频播放服务。本文衔接上文,详细介绍web播放器的功能及实现。

一、基本概况及功能

  • 播放器架构

Aliplayer Web播放器分为H5和Flash两个,Flash播放器随着技术的发展会逐渐被边缘化,所以我们以后只做维护,不会更新功能了,重点会放在H5播放器上。H5播放器架构主要分四层,底层H5 Video,播放能力和H5原生Video紧密相关。第二层是基础播放器,它不依赖于具体业务,通过URL的方式来播放。第三层是为各种业务场景准备的不同的播放器,可以很容易的扩展,相互隔离不依赖。最上面一层是适配的播放器,会根据终端类型、浏览器类型、播放格式和用户指定来进行智能适配。

  • 播放器功能

最近,我们在播放器端上也实现了截图、国际化、变速、UI自定义、微信同层播放、自适应播放、加密播放、H5播放flv、自定义插件等功能。后续,我们还会通过插件的形式实现弹幕、广告等功能,并会开源到github上,也会支持用户根据自己业务需求来自定义SDK包。

  • 播放器支持视频格式

  • 适配播放

我们整个视频播放的基本原则是H5优先,能用H5播放的肯定不用Flash去播放。所以在移动端,我们肯定是用H5来播放的,PC端也依照这个原则尽量使用H5。同时,我们会判断浏览器类型支持哪种播放格式,比如m3u8在PC端IE11以上的浏览器才能播放,如果遇到IE11以下的浏览器,我们自动会选择Flash播放。在视频格式方面,假设视频是rtmp和flv,我们会自动选择Flash播放。另外,如果用户自主设置useH5Prism和useFlashPrism属性,那我们也会依照用户的选择。

  • 浏览器支持情况

FLASH支持IE8以上,在浏览器上启动允许FLASH运行即可;H5支持IE9以上,m3u8需要在IE11以上才可以运行;其他浏览器都也都是可以支持的。

  • 两种播放方式

  1. source,通过url 去播放

  2. 通过点播vid+playauth去播放,第二种方式和视频云结合比较紧密

  • 点播播放格式的选择

点播服务中转码生成的视频格式有很多,包括m3u8、flv、mp4等。播放器有自己的一套逻辑去选择播放格式。对于H5来说,默认播放低清版本来节省流量,如果用户使用了切换清晰度的功能,那我们会默认打开他选择的版本。格式方面,则默认播放mp4,用户也可以设置qualitySort来优先播放高清的的版本。对于Flash来说,默认格式顺序是m3u8、flv、mp4。

二、功能介绍及启用

  • 创建播放器

  1. 引用正确的JS和CSS文件

  2. 添加播放器容器 需要设置容器的id属性,另外2.0.1之前的版本要添加prism-player类型。

  • New Aliplayer创建播放

  • 在线配置,用户可以预先体验下播放器的情况

  • Aliplayer-Cli创建演示例子

用户需要演示例子的时候,不需要写很多代码,通过这个命令,就可以创建例子,直接体验AliPlayer。

  • PC端支持m3u8

播放域名启用允许跨域访问

  • 订阅和取消事件

  • 清晰度切换

H5 1.9.9以后的版本和id+playauth播放方式才支持清晰度切换;支持记忆选择的清晰度,当选择的清晰度不能播放时,自动选择下一个清晰度播放。

  • 手动切换视频-H5

这个功能播放器内比较常见。我们把它分成两种情况去处理,如果是地址播放,我们通过loadByUrl来播放;如果是vid+playauth播放,我们通过replayByVidAndPlayAuth的方法来播放。

  • 手动切换视频-flash

地址播放方法与H5的方法一样,vid+playauth播放则需要先销毁播放器,再重新创建播放。

  • 不同地址格式的切换

只能先销毁播放器,再重新选择正确的播放器播放。Github地址看simple demo:https://github.com/alilmq/aliplayer-simple-demo

![b_3_7]

  • UI自定义

很多用户有这个需求,所以我们的UI是可以隐藏掉的。提供了一个skinLayout的属性,当这个属性没有指定值的时候,UI组件是全部显示。如果是空数组的时候,UI组件全部不显示。并且可以自定义组件的显示和位置,在默认UI基础上去裁剪,2.3.0版本以后,用户也可以通过自定义插件的方式自定义自己的UI。

  • 截屏

H5启用:

FLASH启用:snapshot:true

H5播放器,播放域名需添加允许跨域访问的header

支持订阅snapshoted事件,获取截屏的时间点和数据:

支持设置截图的大小和质量:

支持添加文字水印:

  • 边转变播功能

边转边播是MTS的功能,播放器可以支持这种场景的播放。第一次观看的时候调用MTS API启动转码,边转码边播放,而且可以设置延迟播放。转码中使用直播播放器,转码完成后使用点播方式播放。

  • H5 android微信同层播放

因为H5在android端微信打开时,会自动全屏播放,覆盖Dom元素。

同层播放一般有两种业务场景,一种是点播的,视频在某个地方播放,下面的评论、播放列表等,demo地址:https://github.com/alilmq/h5demo

还要一种场景是直播场景,视频需要全屏。可以通过设置x5_type:h5启用同层播放。Demo 地址:https://github.com/alilmq/h5livedemo

另外H5微信同层播放,有两篇文章可以参考:

http://player.alicdn.com/aliplayer/docs/blogs/how-to-handle-h5-same-layer.html

http://player.alicdn.com/aliplayer/docs/blogs/how-to-handle-h5-same-layer.html

  • 国际化

提供language属性,用于启用各种语言,默认为zh-cn,可选值为zh-cn or en-us。

  • 倍速播放

提供UI的版本,只提供了0.5、1、1.5、2四种倍速播放;而setspeed方法,可以随意设置倍速播放。这个可能会有一些限制,移动端有的浏览器会不支持,比如android微信。

  • 对于直播播放失败的处理

在播放失败时候,会尝试重新播放,触发onM3u8Retry事件,事件里可以做一些提示,比如主播离开请稍等;如果几次尝试后还是失败,会出发livestreamstop事件,事件里做一些直播失败或结束的提示。


三、其他辅助功能及工具

我们也做了一些辅助工具,方便用户去接入和排查问题。

  • 诊断工具

通过错误码描述的映射关系,大概能知道用户的错误所在;

通过vid知道用户播放的是哪个视频;

通过uuid这个唯一标识,可以在日志系统中查到用户的播放状态;

通过requestid和播放时间,可以定位到用户的错误是哪次播放的错误和具体的播放时间。

这里还有一个诊断的功能,可以知道用户环境的具体信息,省去手工获取视频的繁琐,可以快速诊断问题。

地址:http://player.alicdn.com/detection.html

  • 检测工具

关于视频播放失败,我们提供了三种方式,原生H5、阿里云H5、阿里云Flash。我们把播放的日志调出来,通过日志来情况来判断播放失败的原因。举个例子,如果用户刚开始请求数据时就失败的话,那我们会猜测存在鉴权失败的情况;如果加载数据出错,那可能是用户的网络的原因;如果是开始播放后出错,可能就问题就出在解析或播放器不支持等方面。

  • ffmpeg查看视频信息

有的用户只有画面,没有声音。我们可以通过ffmpeg可以看下视频的格式、流的情况、码率、帧率等。

最后,阿里云播放器的所有情况都聚合在以下的网站上:

http://player.alicdn.com/detection.html,其中包括帮助文档、在线配置、诊断工具、产品demo等,大家可以登录了解详情。

们都知道Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven- compiler-plugin完成的。进一步说,每个任务对应了一个插件目标(goal),每个插件会有一个或者多个目标,例如maven- compiler-plugin的compile目标用来编译位于src/main/java/目录下的主源码,testCompile目标用来编译位于src/test/java/目录下的测试源码。


用户可以通过两种方式调用Maven插件目标。第一种方式是将插件目标与生命周期阶段(lifecycle phase)绑定,这样用户在命令行只是输入生命周期阶段而已,例如Maven默认将maven-compiler-plugin的compile目标与 compile生命周期阶段绑定,因此命令mvn compile实际上是先定位到compile这一生命周期阶段,然后再根据绑定关系调用maven-compiler-plugin的compile目标。第二种方式是直接在命令行指定要执行的插件目标,例如mvn archetype:generate 就表示调用maven-archetype-plugin的generate目标,这种带冒号的调用方式与生命周期无关。


认识上述Maven插件的基本概念能帮助你理解Maven的工作机制,不过要想更高效率地使用Maven,了解一些常用的插件还是很有必要的,这可 以帮助你避免一不小心重新发明轮子。多年来Maven社区积累了大量的经验,并随之形成了一个成熟的插件生态圈。Maven官方有两个插件列表,第一个列 表的GroupId为org.apache.maven.plugins,这里的插件最为成熟,具体地址为:http://maven.apache.org/plugins/index.html。第二个列表的GroupId为org.codehaus.mojo,这里的插件没有那么核心,但也有不少十分有用,其地址为:http://mojo.codehaus.org/plugins.html。


接下来笔者根据自己的经验介绍一些最常用的Maven插件,在不同的环境下它们各自都有其出色的表现,熟练地使用它们能让你的日常构建工作事半功倍。



# maven-antrun-plugin


http://maven.apache.org/plugins/maven-antrun-plugin/


maven-antrun-plugin能让用户在Maven项目中运行Ant任务。用户可以直接在该插件的配置以Ant的方式编写Target, 然后交给该插件的run目标去执行。在一些由Ant往Maven迁移的项目中,该插件尤其有用。此外当你发现需要编写一些自定义程度很高的任务,同时又觉得

Maven不够灵活时,也可以以Ant的方式实现之。maven-antrun-plugin的run目标通常与生命周期绑定运行。


# maven-archetype-plugin


http://maven.apache.org/archetype/maven-archetype-plugin/


Archtype指项目的骨架,Maven初学者最开始执行的Maven命令可能就是mvn archetype:generate,这实际上就是让maven-archetype-plugin生成一个很简单的项目骨架,帮助开发者快速上手。可能也有人看到一些文档写了mvn archetype:create, 但实际上create目标已经被弃用了,取而代之的是generate目标,该目标使用交互式的方式提示用户输入必要的信息以创建项目,体验更好。maven-archetype-plugin还有一些其他目标帮助用户自己定义项目原型,例如你有一个产品需要交付给很多客户进行二次开发,你就可以为 他们提供一个Archtype,帮助他们快速上手。



# maven-assembly-plugin


http://maven.apache.org/plugins/maven-assembly-plugin/


maven-assembly-plugin的用途是制作项目分发包,该分发包可能包含了项目的可执行文件、源代码、readme、平台脚本等等。maven-assembly-plugin支持各种主流的格式如zip、tar.gz、jar和war等,具体打包哪些文件是高度可控的,例如用户可以 按文件级别的粒度、文件集级别的粒度、模块级别的粒度、以及依赖级别的粒度控制打包,此外,包含和排除配置也是支持的。maven-assembly- plugin要求用户使用一个名为assembly.xml的元数据文件来表述打包,它的single目标可以直接在命令行调用,也可以被绑定至生命周期。


# maven-dependency-plugin


http://maven.apache.org/plugins/maven-dependency-plugin/


maven-dependency-plugin最大的用途是帮助分析项目依赖,dependency:list能够列出项目最终解析到的依赖列表,dependency:tree能进一步的描绘项目依赖树,dependency:analyze可以告诉你项目依赖潜在的问题,如果你有直接使用到的却未声明的依赖,该目标就会发出警告。maven-dependency-plugin还有很多目标帮助你操作依赖文件,例如dependency:copy-dependencies能将项目依赖从本地Maven仓库复制到某个特定的文件夹下面。


# maven-enforcer-plugin


http://maven.apache.org/plugins/maven-enforcer-plugin/


在一个稍大一点的组织或团队中,你无法保证所有成员都熟悉Maven,那他们做一些比较愚蠢的事情就会变得很正常,例如给项目引入了外部的 SNAPSHOT依赖而导致构建不稳定,使用了一个与大家不一致的Maven版本而经常抱怨构建出现诡异问题。maven-enforcer- plugin能够帮助你避免之类问题,它允许你创建一系列规则强制大家遵守,包括设定Java版本、设定Maven版本、禁止某些依赖、禁止 SNAPSHOT依赖。只要在一个父POM配置规则,然后让大家继承,当规则遭到破坏的时候,Maven就会报错。除了标准的规则之外,你还可以扩展该插 件,编写自己的规则。maven-enforcer-plugin的enforce目标负责检查规则,它默认绑定到生命周期的validate阶段。


# maven-help-plugin


http://maven.apache.org/plugins/maven-help-plugin/


maven-help-plugin是一个小巧的辅助工具,最简单的help:system可以打印所有可用的环境变量和Java系统属性。help:effective-pom和help:effective-settings最 为有用,它们分别打印项目的有效POM和有效settings,有效POM是指合并了所有父POM(包括Super POM)后的XML,当你不确定POM的某些信息从何而来时,就可以查看有效POM。有效settings同理,特别是当你发现自己配置的 settings.xml没有生效时,就可以用help:effective-settings来验证。此外,maven-help-plugin的describe目标可以帮助你描述任何一个Maven插件的信息,还有all-profiles目标和active-profiles目标帮助查看项目的Profile。


# maven-release-plugin


http://maven.apache.org/plugins/maven-release-plugin/


maven-release-plugin的用途是帮助自动化项目版本发布,它依赖于POM中的SCM信息。release:prepare用来准备版本发布,具体的工作包括检查是否有未提交代码、检查是否有SNAPSHOT依赖、升级项目的SNAPSHOT版本至RELEASE版本、为项目打标签等等。release:perform则 是迁出标签中的RELEASE源码,构建并发布。版本发布是非常琐碎的工作,它涉及了各种检查,而且由于该工作仅仅是偶尔需要,因此手动操作很容易遗漏一 些细节,maven-release-plugin让该工作变得非常快速简便,不易出错。maven-release-plugin的各种目标通常直接在 命令行调用,因为版本发布显然不是日常构建生命周期的一部分。


# maven-resources-plugin


http://maven.apache.org/plugins/maven-resources-plugin/


为了使项目结构更为清晰,Maven区别对待Java代码文件和资源文件,maven-compiler-plugin用来编译Java代码,maven-resources-plugin则用来处理资源文件。默认的主资源文件目录是src/main/resources,很多用户会需要添加额外的资源文件目录,这个时候就可以通过配置maven-resources-plugin来实现。此外,资源文件过滤也是Maven的一大特性,你可以在资源文件中使用${propertyName}形式的Maven属性,然后配置maven-resources-plugin开启对资源文件的过滤,之后就可以针对不同环境通过命令行或者Profile传入属性的值,以实现更为灵活的构建。


# maven-surefire-plugin


http://maven.apache.org/plugins/maven-surefire-plugin/


可能是由于历史的原因,Maven 2/3中用于执行测试的插件不是maven-test-plugin,而是maven-surefire-plugin。其实大部分时间内,只要你的测试 类遵循通用的命令约定(以Test结尾、以TestCase结尾、或者以Test开头),就几乎不用知晓该插件的存在。然而在当你想要跳过测试、排除某些 测试类、或者使用一些TestNG特性的时候,了解maven-surefire-plugin的一些配置选项就很有用了。例如 mvn test -Dtest=FooTest 这样一条命令的效果是仅运行FooTest测试类,这是通过控制maven-surefire-plugin的test参数实现的。


# build-helper-maven-plugin


http://mojo.codehaus.org/build-helper-maven-plugin/


Maven默认只允许指定一个主Java代码目录和一个测试Java代码目录,虽然这其实是个应当尽量遵守的约定,但偶尔你还是会希望能够指定多个 源码目录(例如为了应对遗留项目),build-helper-maven-plugin的add-source目标就是服务于这个目的,通常它被绑定到 默认生命周期的generate-sources阶段以添加额外的源码目录。需要强调的是,这种做法还是不推荐的,因为它破坏了 Maven的约定,而且可能会遇到其他严格遵守约定的插件工具无法正确识别额外的源码目录。

build-helper-maven-plugin的另一个非常有用的目标是attach-artifact,使用该目标你可以以classifier的形式选取部分项目文件生成附属构件,并同时install到本地仓库,也可以deploy到远程仓库。


# exec-maven-plugin


http://mojo.codehaus.org/exec-maven-plugin/


exec-maven-plugin很好理解,顾名思义,它能让你运行任何本地的系统程序,在某些特定情况下,运行一个Maven外部的程序可能就是最简单的问题解决方案,这就是exec:exec的 用途,当然,该插件还允许你配置相关的程序运行参数。除了exec目标之外,exec-maven-plugin还提供了一个java目标,该目标要求你 提供一个mainClass参数,然后它能够利用当前项目的依赖作为classpath,在同一个JVM中运行该mainClass。有时候,为了简单的 演示一个命令行Java程序,你可以在POM中配置好exec-maven-plugin的相关运行参数,然后直接在命令运行 mvn exec:java 以查看运行效果。


# jetty-maven-plugin


http://wiki.eclipse.org/Jetty/Feature/Jetty_Maven_Plugin


在进行Web开发的时候,打开浏览器对应用进行手动的测试几乎是无法避免的,这种测试方法通常就是将项目打包成war文件,然后部署到Web容器 中,再启动容器进行验证,这显然十分耗时。为了帮助开发者节省时间,jetty-maven-plugin应运而生,它完全兼容 Maven项目的目录结构,能够周期性地检查源文件,一旦发现变更后自动更新到内置的Jetty Web容器中。做一些基本配置后(例如Web应用的contextPath和自动扫描变更的时间间隔),你只要执行 mvn jetty:run ,然后在IDE中修改代码,代码经IDE自动编译后产生变更,再由jetty-maven-plugin侦测到后更新至Jetty容器,这时你就可以直接 测试Web页面了。需要注意的是,jetty-maven-plugin并不是宿主于Apache或Codehaus的官方插件,因此使用的时候需要额外 的配置settings.xml的pluginGroups元素,将org.mortbay.jetty这个pluginGroup加入。

# versions-maven-plugin


http://mojo.codehaus.org/versions-maven-plugin/


很多Maven用户遇到过这样一个问题,当项目包含大量模块的时候,为他们集体更新版本就变成一件烦人的事情,到底有没有自动化工具能帮助完成这件 事情呢?(当然你可以使用sed之类的文本操作工具,不过不在本文讨论范围)答案是肯定的,versions-maven- plugin提供了很多目标帮助你管理Maven项目的各种版本信息。例如最常用的,命令 mvn versions:set -DnewVersion=1.1-SNAPSHOT 就能帮助你把所有模块的版本更新到1.1-SNAPSHOT。该插件还提供了其他一些很有用的目标,display-dependency- updates能告诉你项目依赖有哪些可用的更新;类似的display-plugin-updates能告诉你可用的插件更新;然后use- latest-versions能自动帮你将所有依赖升级到最新版本。最后,如果你对所做的更改满意,则可以使用 mvn versions:commit 提交,不满意的话也可以使用 mvn versions:revert 进行撤销。

# 小结


本文介绍了一些最常用的Maven插件,这里指的“常用”是指经常需要进行配置的插件,事实上我们用Maven的时候很多其它插件也是必须的,例如 默认的编译插件maven-compiler-plugin和默认的打包插件maven-jar-plugin,但因为很少需要对它们进行配置,因此不在 本文讨论范围。了解常用的Maven插件能帮助你事倍功半地完成项目构建任务,反之你就可能会因为经常遇到一些难以解决的问题而感到沮丧。本文介绍的插件 基本能覆盖大部分Maven用户的日常使用需要,如果你真有非常特殊的需求,自行编写一个Maven插件也不是难事,更何况还有这么多开放源代码的插件供 你参考。


本文的这个插件列表并不是一个完整列表,读者有兴趣的话也可以去仔细浏览一下Apache和Codehaus Mojo的Maven插件列表,以得到一个更为全面的认识。最后,在线的Maven仓库搜索引擎如http://search.maven.org/也能帮助你快速找到自己感兴趣的Maven插件。


来源:http://sina.lt/gkxW