A feature rich code block preprocessing tool.
JavaScript HTML
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
processor-extend
processor
src
test
.editorconfig
.gitignore
.jdistsrc
.jshintrc
.travis.yml
LICENSE
README.md
cli.js
package.json
version.jdists

README.md

jdists 强大的代码块预处理工具

标签: jdists 教程


Build Status NPM version NPM download Coverage Status

jdists logo

背景

软件发布流程

code pretreatment

通常软件发布时会将源文件做一次「预处理」再编译成可执行文件,才发布到市场。

「预处理」的目的主要是出于以下几点

  • 配置线上运行环境,如调试服务地址需变更为实现线上地址;
  • 减少执行程序的大小,移除没有使用的代码或资源并压缩;
  • 增加逆向工程的成本,给代码做混淆(包括改变标识符和代码结构),降低可读性;
  • 移除或增加调试功能,关闭或开启一些特权后门。

一些 IDE 已在「编译」时集成了「预处理」功能。

什么是 jdists

jdists 是一款强大的代码块预处理工具。

什么是「代码块」(code block)?

通常就是注释或注释包裹的代码片段,用于表达各种各样的含义。

举个栗子

  • TODO 注释,表示代码中待完善的地方
/* TODO 功能待开发 */

  • wiredep 注释,表示引入 bower 组件依赖的 css 资源
	<!-- bower:css -->
	<link rel="stylesheet" href="bower_components/css/bootstrap.css" />
	<!-- endbower -->

  • jshint.js 顶部注释,表示版权声明
/*!
 * JSHint, by JSHint Community.
 *
 * This file (and this file only) is licensed under the same slightly modified
 * MIT license that JSLint is. It stops evil-doers everywhere:
 *
 *   Copyright (c) 2002 Douglas Crockford  (www.JSLint.com)
 * .........
 */

  • jshint.js 另一部分注释,表示代码检查配置项
/*jshint quotmark:double */
/*global console:true */
/*exported console */

总之,本文所指「代码块」就是有特殊意义的注释。

什么是「代码块预处理」?

指在代码编译之前,将代码文件按代码块粒度做一次编码或解析。

举个栗子,原本无效的代码片段,经过编码后变成了有效代码。

预处理前:

/*<jdists>
console.log('Hello World!');
</jdists>*/

预处理后:

console.log('Hello World!');

市面上还有哪一些「代码块预处理工具」?

市面上有不少,这里只列两个比较典型的。

  • 已被普遍使用的 JSDoc,功能是将代码中的注释抽离成 API 文档。
/**
 * Represents a book.
 * @constructor
 * @param {string} title - The title of the book.
 * @param {string} author - The author of the book.
 */
function Book(title, author) {
}

  • JSDev 是由 JSON 之父 Douglas Crockford 编写。jdists 与 JSDev 的功能类似,但 jdists 功能要复杂很多。

C command line example:

	 jsdev -comment "Devel Edition." <input >output test_expose enter:trace.enter exit:trace.exit unless:alert

JavaScript:

		output = JSDEV(input, [
				"test_expose",
				"enter:trace.enter",
				"exit:trace.exit",
				"unless:alert"
		] , ["Devel Edition."]);

input:

		// This is a sample file.

		function Constructor(number) {
				/*enter 'Constructor'*/
				/*unless(typeof number !== 'number') 'number', "Type error"*/
				function private_method() {
						/*enter 'private_method'*/
						/*exit 'private_method'*/
				}
				/*test_expose
						this.private_method = private_method;
				*/
				this.priv = function () {
						/*enter 'priv'*/
						private_method();
						/*exit 'priv'*/
				}
				/*exit "Constructor"*/
		}

output:

		// Devel Edition.
		// This is a sample file.

		function Constructor(number) {
				{trace.enter('Constructor');}
				if (typeof number !== 'number') {alert('number', "Type error");}
				function private_method() {
						{trace.enter('private_method');}
						{trace.exit('private_method');}
				}
				{
						this.private_method = private_method;
				}
				this.priv = function () {
						{trace.enter('priv');}
						private_method();
						{trace.exit('priv');}
				}
				{trace.exit("Constructor");}
		}

lightly minified:

		function Constructor(number) {
				function private_method() {
				}
				this.priv = function () {
						private_method();
				}
		}

预处理以「代码块」为粒度有什么优势?

  • 处理速度快,按需对代码块部分进行指定编码;
  • 控制力更强,可以控制每个字符的变化;
  • 不干扰编译器,编译器天然忽略注释。

现有「代码块预处理工具」存在什么问题?

  • 不容易学习和记忆。begin 还是 start,前缀还是后缀?
<!-- 乐居广告脚本 begin-->
/* jshint ignore:start */
/* TODO 待开发功能 */
  • 是否存在闭合不明显。什么时候生效,什么时候失效?
/*jshint unused:true, eqnull:true*/
/*test_expose
		this.private_method = private_method;
	*/
  • 没有标准,不能跨语言。JSDev 和 JSDoc 不能用于其他主流语言,如 Python、Lua 等。

代码预处理的思考

问题也就是:怎么定义、怎么处理、什么情况下触发。

怎么定义「代码块」?

本人拟订了一个基于「XML 标签」+「多行注释」的代码块规范: CBML

CBML

优势:

  • 学习成本低,XML、多行注释都是大家熟知的东西;
  • 标签是否闭合很明显;
  • 支持多种主流编程语言。

怎么处理「代码块」?

处理的步骤无外乎就是:输入、编码、输出

processor

经过解析 CBML 的语法树,获取 tagattribute 两个关键信息。

如果 tag 值为 <jdists> 就开始按 jdists 的规则进行处理。

整个处理过程由四个关键属性决定:

  1. import= 指定输入媒介
  2. export= 指定输出媒介
  3. encoding= 指定编码集合
  4. trigger= 指定触发条件

举个例子

/*<jdists export="template.js" trigger="@version < '1.0.0'">
	var template = /*<jdists encoding="base64,quoted" import="main.html?template" />*/
/*</jdists>

这里有两个代码块,还是一个嵌套结构

  • 外层代码块属性 export="template.js" 指定内容导出到文件 template.js(目录相对于当前代码块所在的文件)。
  • 外层代码块属性 trigger="@version < '1.0.0'" 指定命令行参数 version 小于 '1.0.0' 才触发。
  • 内层代码块属性 encoding="base64,quoted" 表示先给内容做一次 base64 编码再做一次 quoted 即,编码成字符串字面量。

什么情况下触发?

有两个触发条件:

  1. tag 值为 <jdists> 或者是被配置为 jdists 标签
  2. 当属性 trigger= 表达式判断为 true

jdists 基本概念

代码块 block

由 tag 标识的代码区域

代码块主要有如下三种形式:

  • 空内容代码块,没有包裹任何代码
/*<jdists import="main.js" />*/
  • 有效内容代码块,包裹的内容是编译器会解析
/*<jdists encoding="uglify">*/
	function format(template, json) {
		if (typeof template === 'function') { // 函数多行注释处理
			template = String(template).replace(
				/[^]*\/\*!?\s*|\s*\*\/[^]*/g, // 替换掉函数前后部分
				''
			);
		}
		return template.replace(/#\{(.*?)\}/g, function(all, key) {
				return json && (key in json) ? json[key] : "";
		});
	}
/*</jdists>*/
  • 无效内容代码块,包裹的内容也在注释中
/*<jdists>
console.log('version: %s', version);
<jdists>*/

标签 tag

  • <jdists> | 自定义

属性 attribute

  • import= 指定输入媒介
  • export= 指定输出媒介
  • encoding= 指定编码集合
  • trigger= 指定触发条件

媒介 medium

  • &content 默认为 "&"

  • file 文件 > 如: > main.js > index.html

  • #variant 变量 > 如: > #name > #data

  • [file]?block readonly 代码块,默认 file 为当前文件 > 如: > filename?tagName > filename?tagName[attrName=attrValue] > filename?tagName[attrName=attrValue][attrName2=attrValue2]

  • @argument readonly 控制台参数 > 如: > @output > @version

  • :environment readonly 环境变量 > 如: > :HOME > :USER

  • [...]{...} readonly 字面量 > 如: > [1, 2, 3, 4] > {title: 'jdists'}

  • 'string' readonly 字符串 > 如: > 'zswang'

触发器 trigger

触发器有两种表达式

  • 触发器名列表与控制台参数 --trigger 是否存在交集,存在则被触发

$ jdists ... --trigger release 触发

<!--remove trigger="release"-->
<label>release</label>
<!--/remove-->
  • 将变量、属性、环境变量表达式替换后的字面量结果是否为 true

$ jdists ... --version 0.0.9 触发

<!--remove trigger="@version < '1.0.0'"-->
<label>1.0.0+</label>
<!--/remove-->

如何扩展 jdists

可以参考项目中 processor 目录,中自带编码器的写法

举个栗子

var ejs = require('ejs');

/**
 * ejs 模板渲染
 *
 * @param {string} content 文本内容
 * @param {Object} attrs 属性
 * @param {string} attrs.data 数据项
 * @param {Object} scope 作用域
 * @param {Function} scope.execImport 导入数据
 * @param {Function} scope.compile 二次编译 jdists 文本
 */
module.exports = function processor(content, attrs, scope) {
	if (!content) {
		return content;
	}
	var render = ejs.compile(content);
	var data;
	if (attrs.data) {
		/*jslint evil: true */
		data = new Function(
			'return (' +
			scope.execImport(attrs.data) +
			');'
		)();
	}
	else {
		data = null;
	}
	return scope.compile(render(data));
};

详情参考:jdists Scope

用例

代码编译成 dataurl

通过块导入

<!--remove-->
<script>
/*<jdists encoding="base64" id="code">*/
console.log('hello world!');
/*</jdists>*/
</script>
<!--/remove-->

<!--jdists>
<script src="data:application/javascript;base64,/*<jdists import="?[id=code]" />*/"></script>
</jdists-->

通过变量导入

<!--remove-->
<script>
/*<jdists encoding="base64" export="#code">*/
console.log('hello world!');
/*</jdists>*/
</script>
<!--/remove-->

<!--jdists>
<script src="data:application/javascript;base64,/*<jdists import="#code" />*/"></script>
</jdists-->

实战

如何使用

jdists 依赖 node v0.10.0 以上的环境

安装

$ npm install jdists [-g]

命令行

Usage:

		jdists <input list> [options]

Options:

		-r, --remove                 Remove block tag name list (default "remove,test")
		-o, --output                 Output file (default STDOUT)
		-v, --version                Output jdists version
		-t, --trigger                Trigger name list (default "release")
		-c, --config                 Path to config file (default ".jdistsrc")

JS

var content = jdists.build(filename, {
		remove: 'remove,debug',
		trigger: 'release'
});

问题反馈和建议

https://github.com/zswang/jdists/issues

开发

复制项目代码

$ git clone https://github.com/zswang/jdists.git

初始化依赖

$ npm install

执行测试用例

$ npm test

预处理

$ npm run dist

代码覆盖率

$ npm run cover

关键文件目录结果

[lib]                 --- 发布后的代码目录
		jdists.js         --- jdists 业务代码
		scope.js          --- jdists 作用域
[processor]           --- 预制编码器
[processor-extend]    --- 未预制的编码器,可能会常用的
[src]                 --- 开发期代码
[test]                --- 测试目录
		[fixtures]        --- 测试用例
		test.js           --- 测试调度文件
index.js              --- jdists 声明
cli.js                --- jdists 控制台