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

来源:互联网 发布:mac表情 编辑:程序博客网 时间:2024/06/01 07:29

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

Build Status
NPM version

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-->

实战

  • 给源文件添加版权信息

../package.json

{  "name": "jdists",  "version": "0.9.7",  "description": "Code block processing tools",  ...  "author": {    "name": "zswang",    "url": "http://weibo.com/zswang"  },  "license": "MIT",  ...}

input

/*<jdists encoding="ejs" data="../package.json">*//** * @file <%- name %> scope * * <%- description %> * @author     <% (author instanceof Array ? author : [author]).forEach(function (item) { %> *   <%- item.name %> (<%- item.url %>)     <% }); %> * @version <%- version %>     <% var now = new Date() %> * @date <%- [      now.getFullYear(),      now.getMonth() + 101,      now.getDate() + 100    ].join('-').replace(/-1/g, '-') %> *//*</jdists>*/var colors = require('colors');var util = require('util');...

output

/** * @file jdists scope * * Code block processing tools * @author *   zswang (http://weibo.com/zswang) * @version 0.9.7 * @date 2015-08-03 */var colors = require('colors');var util = require('util');...
  • 代码混合加密

jdists 支持嵌套,这样就可以采用组合加密和嵌套加密的方式。让破解更困难

input

<!DOCTYPE html><html><head>  <meta charset="utf-8">  <title></title></head><body>  <h1></h1>  <h2></h2>  <h3></h3></body><script>/*<jdists encoding="zero">*/  /*<jdists encoding="aaencode">*/  document.querySelector('h1').innerHTML = '编译后不可见 1';  /*</jdists>*/  /*<jdists encoding="jjencode">*/  document.querySelector('h2').innerHTML = '编译后不可见 2';  /*</jdists>*/  document.querySelector('h3').innerHTML = '编译后不可见 3';/*</jdists>*/</script></html>

output

<!DOCTYPE html><html><head>  <meta charset="utf-8">  <title></title></head><body>  <h1></h1>  <h2></h2>  <h3></h3></body><script>(function(){}).constructor("".replace(/./g,function(a){return{"‌":0,"‍":1}[a]}).replace(/.{7}/g,function(a){return String.fromCharCode(parseInt(a,2))}))();</script></html>
  • 预制默认插件

通常预制插件都是不断添加或移除,怎么才能不修代码就完成插件的预制?可以借助 glob 和一个模板引擎完成。

input

var colors = require('colors/safe');/*<remove>*/var defaultProcessors = {  "ejs": require('../processor/processor-ejs'),  "glob": require('../processor/processor-glob'),  "jhtmls": require('../processor/processor-jhtmls'),};/*</remove>*//*<jdists encoding="glob" pattern="../processor/*.js" export="#processors" />*//*<jdists encoding="jhtmls" data="#processors">var path = require('path');!#{'var defaultProcessors = {'}forEach(function (process) {  "!#{path.basename(process, '.js').replace(/^processor-/, '')}": require('#{process.replace(/\.js$/, '')}'),});!#{'};'}</jdists>*/

output

var colors = require('colors/safe');var defaultProcessors = {  "aaencode": require('../processor/processor-aaencode'),  "autoprefixer": require('../processor/processor-autoprefixer'),  "base64": require('../processor/processor-base64'),  "candy": require('../processor/processor-candy'),  "clean-css": require('../processor/processor-clean-css'),  "ejs": require('../processor/processor-ejs'),  "glob": require('../processor/processor-glob'),  "html": require('../processor/processor-html'),  "indent": require('../processor/processor-indent'),  "jade": require('../processor/processor-jade'),  "jhtmls": require('../processor/processor-jhtmls'),  "jjencode": require('../processor/processor-jjencode'),  "jsdev": require('../processor/processor-jsdev'),  "less": require('../processor/processor-less'),  "md5": require('../processor/processor-md5'),  "quoted": require('../processor/processor-quoted'),  "regex": require('../processor/processor-regex'),  "slice": require('../processor/processor-slice'),  "svgo": require('../processor/processor-svgo'),  "trim": require('../processor/processor-trim'),  "uglify": require('../processor/processor-uglify'),  "url": require('../processor/processor-url'),  "yml2json": require('../processor/processor-yml2json'),  "zero": require('../processor/processor-zero'),};
  • 防止静态资源被搜索

通常静态资源的字符串是连续的,通过搜索关键字很容易定位并修改其内容。如果将连续的字符串分开赋值,那么定位就不能简单的搜索完成

input

#include<stdio.h>int main(void){    /*<jdists export="#encode">    function (content) {        return content.replace(/char\s+(\w+)\[\s*(\d+)\s*\]\s*=\s*"(.*?)"/g,            function (all, name, len, value) {                var items = value.split('').concat([0]);                items = items.map(function (item, index) {                    var value = item === 0 ? "'\\0'" : "'" + item + "'";                    return '\t' + name + '[' + index + ']=' + value;                });                return 'char ' + name + '[' + len + '];\n' + items.join(';\n');            }        );    }    </jdists>*/    /*<jdists encoding="#encode">*/    char _link1[100] = "http://legend.baidu.com/";    char _link2[100] = "http://shushuo.baidu.com/";    /*</jdists>*/    printf("link1: %s\n link2", _link2, _link2);    return 0;}

output

#include<stdio.h>int main(void){    char _link1[100];    _link1[0]='h';    _link1[1]='t';    _link1[2]='t';    _link1[3]='p';    _link1[4]=':';    _link1[5]='/';    _link1[6]='/';    _link1[7]='l';    _link1[8]='e';    _link1[9]='g';    _link1[10]='e';    _link1[11]='n';    _link1[12]='d';    _link1[13]='.';    _link1[14]='b';    _link1[15]='a';    _link1[16]='i';    _link1[17]='d';    _link1[18]='u';    _link1[19]='.';    _link1[20]='c';    _link1[21]='o';    _link1[22]='m';    _link1[23]='/';    _link1[24]='\0';    char _link2[100];    _link2[0]='h';    _link2[1]='t';    _link2[2]='t';    _link2[3]='p';    _link2[4]=':';    _link2[5]='/';    _link2[6]='/';    _link2[7]='s';    _link2[8]='h';    _link2[9]='u';    _link2[10]='s';    _link2[11]='h';    _link2[12]='u';    _link2[13]='o';    _link2[14]='.';    _link2[15]='b';    _link2[16]='a';    _link2[17]='i';    _link2[18]='d';    _link2[19]='u';    _link2[20]='.';    _link2[21]='c';    _link2[22]='o';    _link2[23]='m';    _link2[24]='/';    _link2[25]='\0';    printf("link1: %s\n link2", _link2, _link2);    return 0;}
  • 引入其他代码处理工具

先将其他「代码处理工具」定义为「处理器」,然后按正常编码解析即可。

processor-jsdev.js

var jsdev = require('JSDev');/** * JSDev 编码 * * @see https://github.com/douglascrockford/JSDev * @param {string} content 文本内容 * @param {Object} attrs 属性 * @param {string} attrs.data 数据项 * @param {Object} scope 作用域 * @param {Function} scope.execImport 导入数据 */module.exports = function processor(content, attrs, scope) {  if (!content) {    return content;  }  var tags;  if (attrs.tags) {    /*jslint evil: true */    tags = scope.execImport(attrs.tags);    if (/^\[[^]*\]$/.test(tags)) { // array      tags = new Function(        'return (' +        tags +        ');'      )();    } else {      tags = tags.split(/\s*[,\n]\s*/);    }  } else {    tags = null;  }  var comments;  if (attrs.comments) {    comments = scope.execImport(attrs.comments);  } else {    comments = null;  }  return jsdev(content, tags, comments);};

input

<!DOCTYPE html><html><head>  <meta charset="utf-8">  <title>jsdev</title></head><body></body><script>/*<jdists encoding="uglify" trigger="#env === 'release'">*//*<jdists export="#env">debug</jdists>*//*<jdists export="#tags" trigger="#env === 'debug'">test_exposeenter:trace.enterexit:trace.exitunless:alert</jdists>*//*<jdists  encoding="jsdev"  tags="#tags"  comments="Devel Edition"  trigger="#env === 'debug'">*/// 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"*/}/*</jdists>*//*<jdists trigger="#env === 'debug'">var trace = {  stack: [],  enter: function (name) {    this.stack.push(new Date);    console.log('enter %s', name);  },  exit: function (name) {    var t = this.stack.pop();    if (t) {      console.log('exit %s (%dms)', name, new Date() - t);    } else {      console.log('exit %s', name);    }  }};</jdists>*/new Constructor(1);new Constructor('1');/*</jdists>*/</script></html>

output

<!DOCTYPE html><html><head>  <meta charset="utf-8">  <title>jsdev</title></head><body></body><script>/*<jdists encoding="uglify" trigger="#env === 'release'">*/// 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");}}var trace = {  stack: [],  enter: function (name) {    this.stack.push(new Date);    console.log('enter %s', name);  },  exit: function (name) {    var t = this.stack.pop();    if (t) {      console.log('exit %s (%dms)', name, new Date() - t);    } else {      console.log('exit %s', name);    }  }};new Constructor(1);new Constructor('1');/*</jdists>*/</script></html>

如何使用

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

关键文件目录结果

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

项目地址:https://github.com/zswang/jdists
欢迎前往了解和交流

0 0
原创粉丝点击