1 | # jdists 强大的代码块预处理工具
|
2 |
|
3 | 标签: jdists 教程
|
4 |
|
5 | ---
|
6 |
|
7 | [![Build Status](https://img.shields.io/travis/zswang/jdists/master.svg)](https://travis-ci.org/zswang/jdists)
|
8 | [![NPM version](https://img.shields.io/npm/v/jdists.svg)](http://badge.fury.io/js/jdists)
|
9 | [![NPM download](https://img.shields.io/npm/dm/jdists.svg)](https://www.npmjs.com/package/jdists)
|
10 | [![Coverage Status](https://coveralls.io/repos/zswang/jdists/badge.svg?branch=master&service=github)](https://coveralls.io/github/zswang/jdists?branch=master)
|
11 |
|
12 | ![jdists logo](https://cloud.githubusercontent.com/assets/536587/9022251/4d33427c-38a1-11e5-98e5-37b6a1c69a85.png)
|
13 |
|
14 | ## 背景
|
15 |
|
16 | ### 软件发布流程
|
17 |
|
18 | ![code pretreatment](https://cloud.githubusercontent.com/assets/536587/9024268/5275fe58-38f8-11e5-9306-89e6c1840f97.png)
|
19 |
|
20 | 通常软件发布时会将源文件做一次「预处理」再编译成可执行文件,才发布到市场。
|
21 |
|
22 | ### 「预处理」的目的主要是出于以下几点
|
23 |
|
24 | * 配置线上运行环境,如调试服务地址需变更为实现线上地址;
|
25 | * 减少执行程序的大小,移除没有使用的代码或资源并压缩;
|
26 | * 增加逆向工程的成本,给代码做混淆(包括改变标识符和代码结构),降低可读性;
|
27 | * 移除或增加调试功能,关闭或开启一些特权后门。
|
28 |
|
29 | > 一些 IDE 已在「编译」时集成了「预处理」功能。
|
30 |
|
31 | ## 什么是 jdists
|
32 |
|
33 | jdists 是一款强大的代码块预处理工具。
|
34 |
|
35 | ### 什么是「代码块」(code block)?
|
36 |
|
37 | 通常就是注释或注释包裹的代码片段,用于表达各种各样的含义。
|
38 |
|
39 | > 举个栗子
|
40 |
|
41 | + TODO 注释,表示代码中待完善的地方
|
42 | ```js
|
43 | /* TODO 功能待开发 */
|
44 | ```
|
45 | ----
|
46 | + [wiredep][1] 注释,表示引入 bower 组件依赖的 css 资源
|
47 | ```html
|
48 | <!-- bower:css -->
|
49 | <link rel="stylesheet" href="bower_components/css/bootstrap.css" />
|
50 | <!-- endbower -->
|
51 | ```
|
52 | ----
|
53 | + [jshint.js][2] 顶部注释,表示版权声明
|
54 | ```js
|
55 | /*!
|
56 | * JSHint, by JSHint Community.
|
57 | *
|
58 | * This file (and this file only) is licensed under the same slightly modified
|
59 | * MIT license that JSLint is. It stops evil-doers everywhere:
|
60 | *
|
61 | * Copyright (c) 2002 Douglas Crockford (www.JSLint.com)
|
62 | * .........
|
63 | */
|
64 | ```
|
65 | ----
|
66 | + jshint.js 另一部分注释,表示代码检查配置项
|
67 | ```js
|
68 | /*jshint quotmark:double */
|
69 | /*global console:true */
|
70 | /*exported console */
|
71 | ```
|
72 | 总之,本文所指「代码块」就是有特殊意义的注释。
|
73 |
|
74 | ### 什么是「代码块预处理」?
|
75 |
|
76 | 指在代码编译之前,将代码文件按代码块粒度做一次编码或解析。
|
77 |
|
78 | > 举个栗子,原本无效的代码片段,经过编码后变成了有效代码。
|
79 |
|
80 | 预处理前:
|
81 | ```js
|
82 | /*<jdists>
|
83 | console.log('Hello World!');
|
84 | </jdists>*/
|
85 | ```
|
86 |
|
87 | 预处理后:
|
88 | ```js
|
89 | console.log('Hello World!');
|
90 | ```
|
91 |
|
92 | ### 市面上还有哪一些「代码块预处理工具」?
|
93 |
|
94 | 市面上有不少,这里只列两个比较典型的。
|
95 |
|
96 | + 已被普遍使用的 [JSDoc][3],功能是将代码中的注释抽离成 API 文档。
|
97 |
|
98 | ```js
|
99 | /**
|
100 | * Represents a book.
|
101 | * @constructor
|
102 | * @param {string} title - The title of the book.
|
103 | * @param {string} author - The author of the book.
|
104 | */
|
105 | function Book(title, author) {
|
106 | }
|
107 | ```
|
108 | ----
|
109 | + [JSDev][4] 是由 JSON 之父 Douglas Crockford 编写。jdists 与 JSDev 的功能类似,但 jdists 功能要复杂很多。
|
110 |
|
111 | C command line example:
|
112 |
|
113 | ```shell
|
114 | jsdev -comment "Devel Edition." <input >output test_expose enter:trace.enter exit:trace.exit unless:alert
|
115 | ```
|
116 |
|
117 | JavaScript:
|
118 | ```js
|
119 | output = JSDEV(input, [
|
120 | "test_expose",
|
121 | "enter:trace.enter",
|
122 | "exit:trace.exit",
|
123 | "unless:alert"
|
124 | ] , ["Devel Edition."]);
|
125 | ```
|
126 | input:
|
127 | ```js
|
128 | // This is a sample file.
|
129 |
|
130 | function Constructor(number) {
|
131 | /*enter 'Constructor'*/
|
132 | /*unless(typeof number !== 'number') 'number', "Type error"*/
|
133 | function private_method() {
|
134 | /*enter 'private_method'*/
|
135 | /*exit 'private_method'*/
|
136 | }
|
137 | /*test_expose
|
138 | this.private_method = private_method;
|
139 | */
|
140 | this.priv = function () {
|
141 | /*enter 'priv'*/
|
142 | private_method();
|
143 | /*exit 'priv'*/
|
144 | }
|
145 | /*exit "Constructor"*/
|
146 | }
|
147 | ```
|
148 |
|
149 | output:
|
150 |
|
151 | ```js
|
152 | // Devel Edition.
|
153 | // This is a sample file.
|
154 |
|
155 | function Constructor(number) {
|
156 | {trace.enter('Constructor');}
|
157 | if (typeof number !== 'number') {alert('number', "Type error");}
|
158 | function private_method() {
|
159 | {trace.enter('private_method');}
|
160 | {trace.exit('private_method');}
|
161 | }
|
162 | {
|
163 | this.private_method = private_method;
|
164 | }
|
165 | this.priv = function () {
|
166 | {trace.enter('priv');}
|
167 | private_method();
|
168 | {trace.exit('priv');}
|
169 | }
|
170 | {trace.exit("Constructor");}
|
171 | }
|
172 | ```
|
173 |
|
174 | lightly minified:
|
175 |
|
176 | ```js
|
177 | function Constructor(number) {
|
178 | function private_method() {
|
179 | }
|
180 | this.priv = function () {
|
181 | private_method();
|
182 | }
|
183 | }
|
184 | ```
|
185 |
|
186 | ### 预处理以「代码块」为粒度有什么优势?
|
187 |
|
188 | * 处理速度快,按需对代码块部分进行指定编码;
|
189 | * 控制力更强,可以控制每个字符的变化;
|
190 | * 不干扰编译器,编译器天然忽略注释。
|
191 |
|
192 | ### 现有「代码块预处理工具」存在什么问题?
|
193 |
|
194 | + 不容易学习和记忆。`begin` 还是 `start`,前缀还是后缀?
|
195 | ```
|
196 | <!-- 乐居广告脚本 begin-->
|
197 | /* jshint ignore:start */
|
198 | /* TODO 待开发功能 */
|
199 | ```
|
200 |
|
201 | + 是否存在闭合不明显。什么时候生效,什么时候失效?
|
202 | ```
|
203 | /*jshint unused:true, eqnull:true*/
|
204 | /*test_expose
|
205 | this.private_method = private_method;
|
206 | */
|
207 | ```
|
208 |
|
209 | + 没有标准,不能跨语言。JSDev 和 JSDoc 不能用于其他主流语言,如 Python、Lua 等。
|
210 |
|
211 | ## 代码预处理的思考
|
212 |
|
213 | 问题也就是:怎么定义、怎么处理、什么情况下触发。
|
214 |
|
215 | ### 怎么定义「代码块」?
|
216 |
|
217 | 本人拟订了一个基于「XML 标签」+「多行注释」的代码块规范: [CBML][5]
|
218 |
|
219 | ![CBML](https://cloud.githubusercontent.com/assets/536587/9024562/a4dbd27a-3908-11e5-9c2c-50156a04d398.png)
|
220 |
|
221 | 优势:
|
222 |
|
223 | * 学习成本低,XML、多行注释都是大家熟知的东西;
|
224 | * 标签是否闭合很明显;
|
225 | * 支持多种主流编程语言。
|
226 |
|
227 | ### 怎么处理「代码块」?
|
228 |
|
229 | 处理的步骤无外乎就是:输入、编码、输出
|
230 |
|
231 | ![processor](https://cloud.githubusercontent.com/assets/536587/9024576/3bdbae70-3909-11e5-9b3e-f4ba83b5e842.png)
|
232 |
|
233 | 经过解析 CBML 的语法树,获取 `tag` 和 `attribute` 两个关键信息。
|
234 |
|
235 | 如果 `tag` 值为 `<jdists>` 就开始按 jdists 的规则进行处理。
|
236 |
|
237 | > 整个处理过程由四个关键属性决定:
|
238 | > 1. `import=` 指定输入媒介
|
239 | > 2. `export=` 指定输出媒介
|
240 | > 3. `encoding=` 指定编码集合
|
241 | > 4. `trigger=` 指定触发条件
|
242 |
|
243 | 举个例子
|
244 | ```js
|
245 | /*<jdists export="template.js" trigger="@version < '1.0.0'">
|
246 | var template = /*<jdists encoding="base64,quoted" import="main.html?template" />*/
|
247 | /*</jdists>
|
248 | ```
|
249 |
|
250 | 这里有两个代码块,还是一个嵌套结构
|
251 |
|
252 | * 外层代码块属性 `export="template.js"` 指定内容导出到文件 `template.js`(目录相对于当前代码块所在的文件)。
|
253 | * 外层代码块属性 `trigger="@version < '1.0.0'"` 指定命令行参数 `version` 小于 `'1.0.0'` 才触发。
|
254 | * 内层代码块属性 `encoding="base64,quoted"` 表示先给内容做一次 `base64` 编码再做一次 `quoted` 即,编码成字符串字面量。
|
255 |
|
256 | ### 什么情况下触发?
|
257 |
|
258 | 有两个触发条件:
|
259 |
|
260 | 1. 当 `tag` 值为 `<jdists>` 或者是被配置为 `jdists` 标签
|
261 | 2. 当属性 `trigger=` 表达式判断为 `true`
|
262 |
|
263 | ## jdists 基本概念
|
264 |
|
265 | ### 代码块 block
|
266 |
|
267 | 由 tag 标识的代码区域
|
268 |
|
269 | 代码块主要有如下三种形式:
|
270 | * 空内容代码块,没有包裹任何代码
|
271 | ```js
|
272 | /*<jdists import="main.js" />*/
|
273 | ```
|
274 |
|
275 | * 有效内容代码块,包裹的内容是编译器会解析
|
276 | ```js
|
277 | /*<jdists encoding="uglify">*/
|
278 | function format(template, json) {
|
279 | if (typeof template === 'function') { // 函数多行注释处理
|
280 | template = String(template).replace(
|
281 | /[^]*\/\*!?\s*|\s*\*\/[^]*/g, // 替换掉函数前后部分
|
282 | ''
|
283 | );
|
284 | }
|
285 | return template.replace(/#\{(.*?)\}/g, function(all, key) {
|
286 | return json && (key in json) ? json[key] : "";
|
287 | });
|
288 | }
|
289 | /*</jdists>*/
|
290 | ```
|
291 |
|
292 | * 无效内容代码块,包裹的内容也在注释中
|
293 | ```js
|
294 | /*<jdists>
|
295 | console.log('version: %s', version);
|
296 | <jdists>*/
|
297 | ```
|
298 |
|
299 | ### 标签 tag
|
300 |
|
301 | * `<jdists>` | 自定义
|
302 |
|
303 | ### 属性 attribute
|
304 |
|
305 | * `import=` 指定输入媒介
|
306 | * `export=` 指定输出媒介
|
307 | * `encoding=` 指定编码集合
|
308 | * `trigger=` 指定触发条件
|
309 |
|
310 | ### 媒介 medium
|
311 |
|
312 | * `&content` 默认为 "&"
|
313 | * `file` 文件
|
314 | > 如:
|
315 | > `main.js`
|
316 | > `index.html`
|
317 |
|
318 | * `#variant` 变量
|
319 | > 如:
|
320 | > `#name`
|
321 | > `#data`
|
322 |
|
323 | * `[file]?block` *readonly* 代码块,默认 `file` 为当前文件
|
324 | > 如:
|
325 | > `filename?tagName`
|
326 | > `filename?tagName[attrName=attrValue]`
|
327 | > `filename?tagName[attrName=attrValue][attrName2=attrValue2]`
|
328 |
|
329 | * `@argument` *readonly* 控制台参数
|
330 | > 如:
|
331 | > `@output`
|
332 | > `@version`
|
333 |
|
334 | * `:environment` *readonly* 环境变量
|
335 | > 如:
|
336 | > `:HOME`
|
337 | > `:USER`
|
338 |
|
339 | * `[...]`、`{...}` *readonly* 字面量
|
340 | > 如:
|
341 | > `[1, 2, 3, 4]`
|
342 | > `{title: 'jdists'}`
|
343 |
|
344 | * `'string'` *readonly* 字符串
|
345 | > 如:
|
346 | > `'zswang'`
|
347 |
|
348 | ### 触发器 trigger
|
349 |
|
350 | 触发器有两种表达式
|
351 |
|
352 | * 触发器名列表与控制台参数 `--trigger` 是否存在交集,存在则被触发
|
353 |
|
354 | > 当 `$ jdists ... --trigger release` 触发
|
355 |
|
356 | ```html
|
357 | <!--remove trigger="release"-->
|
358 | <label>release</label>
|
359 | <!--/remove-->
|
360 | ```
|
361 |
|
362 | * 将变量、属性、环境变量表达式替换后的字面量结果是否为 true
|
363 |
|
364 | > 当 `$ jdists ... --version 0.0.9` 触发
|
365 |
|
366 | ```html
|
367 | <!--remove trigger="@version < '1.0.0'"-->
|
368 | <label>1.0.0+</label>
|
369 | <!--/remove-->
|
370 | ```
|
371 |
|
372 | ## 如何扩展 jdists
|
373 |
|
374 | 可以参考项目中 processor 目录,中自带编码器的写法
|
375 |
|
376 | 举个栗子
|
377 | ```js
|
378 | var ejs = require('ejs');
|
379 |
|
380 | /**
|
381 | * ejs 模板渲染
|
382 | *
|
383 | * @param {string} content 文本内容
|
384 | * @param {Object} attrs 属性
|
385 | * @param {string} attrs.data 数据项
|
386 | * @param {Object} scope 作用域
|
387 | * @param {Function} scope.execImport 导入数据
|
388 | * @param {Function} scope.compile 二次编译 jdists 文本
|
389 | */
|
390 | module.exports = function processor(content, attrs, scope) {
|
391 | if (!content) {
|
392 | return content;
|
393 | }
|
394 | var render = ejs.compile(content);
|
395 | var data;
|
396 | if (attrs.data) {
|
397 | /*jslint evil: true */
|
398 | data = new Function(
|
399 | 'return (' +
|
400 | scope.execImport(attrs.data) +
|
401 | ');'
|
402 | )();
|
403 | }
|
404 | else {
|
405 | data = null;
|
406 | }
|
407 | return scope.compile(render(data));
|
408 | };
|
409 | ```
|
410 |
|
411 | 详情参考:[jdists Scope](https://github.com/zswang/jdists/wiki/Scope)
|
412 |
|
413 | ## 用例
|
414 |
|
415 | ### 代码编译成 dataurl
|
416 |
|
417 | 通过块导入
|
418 |
|
419 | ```html
|
420 | <!--remove-->
|
421 | <script>
|
422 | /*<jdists encoding="base64" id="code">*/
|
423 | console.log('hello world!');
|
424 | /*</jdists>*/
|
425 | </script>
|
426 | <!--/remove-->
|
427 |
|
428 | <!--jdists>
|
429 | <script src="data:application/javascript;base64,/*<jdists import="?[id=code]" />*/"></script>
|
430 | </jdists-->
|
431 | ```
|
432 |
|
433 | 通过变量导入
|
434 |
|
435 | ```html
|
436 | <!--remove-->
|
437 | <script>
|
438 | /*<jdists encoding="base64" export="#code">*/
|
439 | console.log('hello world!');
|
440 | /*</jdists>*/
|
441 | </script>
|
442 | <!--/remove-->
|
443 |
|
444 | <!--jdists>
|
445 | <script src="data:application/javascript;base64,/*<jdists import="#code" />*/"></script>
|
446 | </jdists-->
|
447 | ```
|
448 |
|
449 | ## 实战
|
450 |
|
451 | * [给源文件添加版权信息](https://github.com/zswang/jdists/wiki/%5Bcase%5DBuild-copyright)
|
452 | * [代码混合加密](https://github.com/zswang/jdists/wiki/%5Bcase%5DCode-mixed-encryption)
|
453 | * [预制默认插件](https://github.com/zswang/jdists/wiki/%5Bcase%5DPrefabricated-default-plugin)
|
454 | * [防止静态资源被搜索](https://github.com/zswang/jdists/wiki/%5Bcase%5DTo-prevent-the-reverse-engineering)
|
455 | * [引入其他代码处理工具](https://github.com/zswang/jdists/wiki/%5Bcase%5DThe-introduction-of-third-party-code-processing-tools)
|
456 |
|
457 | ## 如何使用
|
458 |
|
459 | jdists 依赖 node v0.10.0 以上的环境
|
460 |
|
461 | ### 安装
|
462 |
|
463 | `$ npm install jdists [-g]`
|
464 |
|
465 | ### 命令行
|
466 |
|
467 | ```
|
468 | Usage:
|
469 |
|
470 | jdists <input list> [options]
|
471 |
|
472 | Options:
|
473 |
|
474 | -r, --remove Remove block tag name list (default "remove,test")
|
475 | -o, --output Output file (default STDOUT)
|
476 | -v, --version Output jdists version
|
477 | -t, --trigger Trigger name list (default "release")
|
478 | -c, --config Path to config file (default ".jdistsrc")
|
479 | ```
|
480 |
|
481 | ### JS
|
482 |
|
483 | ```js
|
484 | var content = jdists.build(filename, {
|
485 | remove: 'remove,debug',
|
486 | trigger: 'release'
|
487 | });
|
488 | ```
|
489 |
|
490 | ### 问题反馈和建议
|
491 |
|
492 | https://github.com/zswang/jdists/issues
|
493 |
|
494 | ## 开发
|
495 |
|
496 | ### 复制项目代码
|
497 |
|
498 | `$ git clone https://github.com/zswang/jdists.git`
|
499 |
|
500 | ### 初始化依赖
|
501 |
|
502 | `$ npm install`
|
503 |
|
504 | ### 执行测试用例
|
505 |
|
506 | `$ npm test`
|
507 |
|
508 | ### 预处理
|
509 |
|
510 | `$ npm run dist`
|
511 |
|
512 | ### 代码覆盖率
|
513 |
|
514 | `$ npm run cover`
|
515 |
|
516 | ## 关键文件目录结果
|
517 |
|
518 | ```
|
519 | [lib] --- 发布后的代码目录
|
520 | jdists.js --- jdists 业务代码
|
521 | scope.js --- jdists 作用域
|
522 | [processor] --- 预制编码器
|
523 | [processor-extend] --- 未预制的编码器,可能会常用的
|
524 | [src] --- 开发期代码
|
525 | [test] --- 测试目录
|
526 | [fixtures] --- 测试用例
|
527 | test.js --- 测试调度文件
|
528 | index.js --- jdists 声明
|
529 | cli.js --- jdists 控制台
|
530 | ```
|
531 |
|
532 | [1]: https://github.com/taptapship/wiredep
|
533 | [2]: https://github.com/jshint/jshint
|
534 | [3]: https://github.com/jsdoc3/jsdoc
|
535 | [4]: https://github.com/douglascrockford/JSDev
|
536 | [5]: https://github.com/cbml/cbml
|