1 | var debug = require('debug')('clam:compile');
|
2 | var fs = require('fs');
|
3 | var path = require('path');
|
4 | var isUtf8 = require('is-utf8');
|
5 | var _ = require('underscore');
|
6 | var iconv = require('iconv-lite');
|
7 | var util = require('util');
|
8 | var config = require('./config.js');
|
9 | var proxy = require("./proxy.js");
|
10 | var assetUrl = require('./asseturl.js');
|
11 |
|
12 | var J = require("juicer");
|
13 | J.register('stringify', JSON.stringify);
|
14 | J.register("random", function(){ return (new Date()).valueOf(); });
|
15 |
|
16 | var mocker = require('./mocker.js');
|
17 | var assetsTool = require('./assetsTool');
|
18 |
|
19 | var scriptExtern = /<script[^>]*? src=['"]([^"']*?)['"].*?><\/script>/g;
|
20 | var styleExtern = /<link[^>]*? href=['"]([^"']*?)['"].*>/g;
|
21 | var assetsToolExtern = /\$assetsTool.(use|require|injectScript|injectStyle)\((['"][^\s^\$]*['"]\s*,?\s*){0,2}\)/g;
|
22 | var recoverAssetsToolExtern = /\{\{__assetsTool__\d*\}\}/g;
|
23 | var assetsToolPrefix = '__assetsTool__';
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | function takeOutAssets(pageContent) {
|
31 | pageContent = pageContent.replace(/\<\!\-\-^#.*\-\-\>/g, '');
|
32 | var head_scripts = [], tail_scripts = [];
|
33 | var styles = [];
|
34 | var movetoReg = /clam-moveto=['"]([^"']*?)['"]/;
|
35 | pageContent = pageContent.replace(scriptExtern, function (mm) {
|
36 | var m = mm.match(movetoReg);
|
37 | mm = mm.replace(movetoReg, '');
|
38 | if (m && typeof m[1] != "undefined") {
|
39 | if (m[1] == "head") {
|
40 | head_scripts.push(mm);
|
41 | return '';
|
42 | }
|
43 | else if (m[1] == "tail") {
|
44 | tail_scripts.push(mm);
|
45 | return '';
|
46 | }
|
47 | else {
|
48 | return mm;
|
49 | }
|
50 | }
|
51 | else {
|
52 | head_scripts.push(mm);
|
53 | return '';
|
54 | }
|
55 | });
|
56 | pageContent = pageContent.replace(styleExtern, function ($1) {
|
57 | if ($1.match(/rel\s{0,}=\s{0,}[\'\"]stylesheet[\"\']/) || $1.match(/type\s{0,}=\s{0,}[\'\"]text\/css[\"\']/)) {
|
58 | styles.push($1);
|
59 | return '';
|
60 | }
|
61 | else {
|
62 | return $1;
|
63 | }
|
64 | });
|
65 | head_scripts = _.uniq(head_scripts);
|
66 | tail_scripts = _.uniq(tail_scripts);
|
67 | styles = _.uniq(styles);
|
68 | return {head_scripts: head_scripts, tail_scripts: tail_scripts, styles: styles, pageContent: pageContent};
|
69 | }
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 | function parseAssetsTool(content){
|
76 | var assetsToolPositionCount = -1;
|
77 | var assetsMap = [];
|
78 | var pageContent = content.replace(assetsToolExtern, function ($1, $2, $3) {
|
79 | saveAssetsMap(assetsMap,$1,++assetsToolPositionCount);
|
80 | return '{{'+assetsToolPrefix + assetsToolPositionCount + '}}';
|
81 | });
|
82 | return recoverAssetsTool(pageContent,assetsMap);
|
83 | }
|
84 |
|
85 |
|
86 |
|
87 | function saveAssetsMap(root,content,pos){
|
88 | root.push({
|
89 | express:content,
|
90 | id:pos
|
91 | });
|
92 | }
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | function recoverAssetsTool(content,map){
|
99 |
|
100 | var requireMap = [];
|
101 | var placeholderMap = [];
|
102 |
|
103 | assetsTool.clearCacheMod();
|
104 |
|
105 | map.forEach(function(i) {
|
106 | /\$assetsTool.inject(Script|Style).*/g.test(i.express) ?
|
107 | placeholderMap.push(i):
|
108 | requireMap.push(i);
|
109 | });
|
110 |
|
111 | map = requireMap.concat(placeholderMap);
|
112 | var assetsResult = assetsTool.feLoader(map);
|
113 |
|
114 | function getAssets(id){
|
115 | var ret = '';
|
116 | assetsResult.forEach(function(i){
|
117 | if(i.id == id){
|
118 | ret = i.express;
|
119 | return;
|
120 | }
|
121 | });
|
122 | return ret;
|
123 | }
|
124 | content = content.replace(recoverAssetsToolExtern, function ($1, $2, $3) {
|
125 | return getAssets(parseInt($1.split(assetsToolPrefix)[1]));
|
126 | });
|
127 | return content;
|
128 | }
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 | function parseInfo(page, libs, cdnPath, rootContent, url) {
|
138 | debug('--------parseInfo--------%s',url);
|
139 | var pageContent = ssi(page, libs, cdnPath, null, rootContent, url);
|
140 | var asset = takeOutAssets(pageContent);
|
141 | var head_scripts = asset.head_scripts, tail_scripts = asset.tail_scripts;
|
142 | var styles = asset.styles;
|
143 | pageContent = asset.pageContent;
|
144 | var headTail = pageContent.indexOf('</head>');
|
145 | var bodyContent = headTail!=-1 ? pageContent.slice(headTail) : '';
|
146 |
|
147 | return {file: page, pageContent: pageContent, headTail: headTail, bodyContent: bodyContent, head_scripts: head_scripts, tail_scripts: tail_scripts, styles: styles};
|
148 | }
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | var render = function(page, libs, cdnPath, rootContent, url) {
|
159 | debug('--------render--------%s',url);
|
160 | var ret = parseInfo(page, libs, cdnPath, rootContent, url);
|
161 | var pageContent = ret.pageContent;
|
162 | var headTail = ret.headTail;
|
163 | var bodyContent = ret.bodyContent;
|
164 | var head_scripts = ret.head_scripts, tail_scripts = ret.tail_scripts;
|
165 | var styles = ret.styles;
|
166 |
|
167 | var headArea = pageContent.slice(0, headTail);
|
168 | var bodyTail = bodyContent.indexOf('</body>');
|
169 | var bodyArea = bodyContent.slice(0, bodyTail);
|
170 | var bodyTailArea = bodyContent.slice(bodyTail);
|
171 |
|
172 | var head_scriptArea = '';
|
173 | head_scripts.forEach(function (s) {
|
174 | head_scriptArea += '\n' + s;
|
175 | });
|
176 | var tail_scriptArea = '';
|
177 | tail_scripts.forEach(function (s) {
|
178 | tail_scriptArea += '\n' + s;
|
179 | });
|
180 |
|
181 | var styleArea = '';
|
182 | styles.forEach(function (s) {
|
183 | styleArea += '\n' + s;
|
184 | });
|
185 |
|
186 | if (headTail == -1) {
|
187 | pageContent = '<html><head><meta charset="'+config.get('project').charset[0]+'"><meta name="viewport" content="initial-scale=1" />' + styleArea + head_scriptArea + '$assetsTool.injectStyle()</head><body>' + pageContent + tail_scriptArea + '$assetsTool.injectScript("foot")</body></html>'; }
|
188 | else {
|
189 | pageContent = headArea + styleArea + head_scriptArea + bodyArea + tail_scriptArea + bodyTailArea;
|
190 | }
|
191 |
|
192 | pageContent = pageContent.replace(/\$CLAM_VER\$[\/]?/g, '');
|
193 | return pageContent;
|
194 | }
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 | function parseParam(content, reg) {
|
202 | var matched = content.match(reg);
|
203 | var ret = {};
|
204 | if (matched && matched[1]) {
|
205 | matched[1].replace(/[\n\r]/g, '');
|
206 | try {
|
207 | ret = JSON.parse(matched[1]);
|
208 | } catch (e) {
|
209 | console.log('格式错误的模板变量:%s', matched[1]);
|
210 | return {};
|
211 | }
|
212 | return ret;
|
213 | }
|
214 | return ret;
|
215 | }
|
216 |
|
217 | function parsePageParam(content) {
|
218 | return parseParam(content, /<\!--#def([\s\S]*?)-->/);
|
219 | }
|
220 |
|
221 | function parseTmsParam(content) {
|
222 | return parseParam(content, /<\!--#tms([\s\S]*?)-->/);
|
223 | }
|
224 |
|
225 | function delPageParamArea(content){
|
226 | return content.replace(/<\!--#def([\s\S]*?)-->/, '');
|
227 | }
|
228 |
|
229 | /**
|
230 | * 返回携带内容携带真正线上资源路径的内容
|
231 | * 处理相对资源引用等
|
232 | * @param rootContent 文本内容,如果没有,则从page所指向的路径中获取
|
233 | * @param page 文件路径,如果rootContent指定,则失效
|
234 | * @param cdnPath 项目assets上线后的路径前缀
|
235 | * @returns 处理完毕后的页面/模块内容
|
236 | */
|
237 | function textWithOnlineAssetsUrl(rootContent, page, cdnPath) {
|
238 | var pageContent = null;
|
239 | if (rootContent) {
|
240 | pageContent = rootContent;
|
241 | }
|
242 | else {
|
243 | pageContent = fs.readFileSync(page);
|
244 | pageContent = isUtf8(pageContent) ? pageContent.toString() : iconv.decode(pageContent, 'gbk');
|
245 | }
|
246 |
|
247 | //获取当前页面到源代码根目录的相对路径
|
248 | var srcRoot = path.join(config.root(), 'src');
|
249 | var urlDir = path.relative(srcRoot, path.dirname(page));
|
250 | debug('引用文件相对路径:%s', urlDir);
|
251 | pageContent = assetUrl.toAbsolutePath(pageContent, urlDir);
|
252 | return pageContent;
|
253 | }
|
254 |
|
255 | /**
|
256 | * 混合模块定义和内容
|
257 | * 根据modefs中定义的偏移量,替换内容信息,重组content
|
258 | * @param modDefs
|
259 | * @param pageContent
|
260 | * @returns {string}
|
261 | */
|
262 | function mixContentAndModules(modDefs, pageContent) {
|
263 | var snippets = [];
|
264 | var i;
|
265 | for (i = 0; i < modDefs.length; i++) {
|
266 | if (i === 0) {
|
267 | snippets.push(pageContent.slice(0, modDefs[i].begin));
|
268 | continue;
|
269 | }
|
270 | snippets.push(pageContent.slice(modDefs[i - 1].end, modDefs[i].begin));
|
271 | }
|
272 | snippets.push(pageContent.slice(modDefs[modDefs.length - 1].end, pageContent.length));
|
273 |
|
274 | var output = "";
|
275 | for (i = 0; i < modDefs.length; i++) {
|
276 | output = output + snippets[i] + (modDefs[i].content ? modDefs[i].content : '');
|
277 | if (i === modDefs.length - 1) {
|
278 | output += snippets[i + 1];
|
279 | }
|
280 | }
|
281 | return output;
|
282 | }
|
283 |
|
284 | /**
|
285 | * 按照SSI解析输出html
|
286 | * 解析html时会依次遍历所有模块
|
287 | * 改函数处理所有与相对路径有关的内容替换,如根据相对路径替换其中的资源路径
|
288 | *
|
289 | * @param page 页面或者模块文件名的绝对路径
|
290 | * @param libs 其他模块搜索目录
|
291 | * @param cdnPath 最终assets上线后所在的cdn域名+路径前缀
|
292 | * @param parentParam 父级容器传递给当然页面片段的变量
|
293 | * 变量使用优先级
|
294 | * 网络>本地http接口模拟>if接口>文件中即时定义的变量
|
295 | * @param rootContent 页面路径对应的内容。如果有,则无需从页面路径获取内容
|
296 | * @param url 页面访问url,如果该渲染请求由网络请求触发,则为请求url
|
297 | * 如果渲染请求由本地触发,则为到src目录的相对路径
|
298 | * @return {*}
|
299 | */
|
300 | var ssi = function(page, libs, cdnPath, parentParam, rootContent, url) {
|
301 | debug('开始渲染, 文件:%s,库目录:%s', page, util.inspect(libs));
|
302 | debug('--------ssi--------%s',url);
|
303 | //页面不存在,返回空内容
|
304 | if (!fs.existsSync(page)) {
|
305 | return '';
|
306 | }
|
307 | //处理cdnpath中最后一个'/'符号。如果有,去掉。
|
308 | if (cdnPath[cdnPath.length - 1] === '/') {
|
309 | cdnPath = cdnPath.slice(0, cdnPath.length - 1);
|
310 | }
|
311 |
|
312 | var pageContent = rootContent;
|
313 | if (!rootContent) {
|
314 | pageContent = fs.readFileSync(page);
|
315 | pageContent = isUtf8(pageContent) ? pageContent.toString() : iconv.decode(pageContent, 'gbk');
|
316 |
|
317 | }
|
318 | var pageParam = parsePageParam(pageContent);
|
319 |
|
320 | var param = {};
|
321 | debug('getMixedMockDate1%s', util.inspect(param));
|
322 | //本地参数覆盖外部参数
|
323 | if (parentParam) {
|
324 | for (var attr in pageParam) { parentParam[attr] = pageParam[attr]; }
|
325 | param = parentParam;
|
326 | }
|
327 | else {
|
328 | debug('内部获取参数%s', util.inspect(pageParam));
|
329 | for (var attr in pageParam) {
|
330 | if(attr[0] === '#'){
|
331 | var attrFixed = attr.slice(1, attr.length);
|
332 | param[attrFixed] = pageParam[attr];
|
333 | continue;
|
334 | }
|
335 | param[attr] = pageParam[attr];
|
336 | }
|
337 | }
|
338 | debug('getMixedMockDate2%s', util.inspect(param));
|
339 | param = mocker.getMixedMockDate(param, url, (!!rootContent));
|
340 | debug('真实运作的参数%s', util.inspect(param));
|
341 |
|
342 | try {
|
343 | pageContent = J(pageContent, param);
|
344 | }
|
345 | catch (e) {
|
346 | debug('Juicer出错%s\n 对象:%s', pageContent, util.inspect(param));
|
347 | }
|
348 |
|
349 | pageContent = textWithOnlineAssetsUrl(pageContent, page, cdnPath);
|
350 | pageContent = delPageParamArea(pageContent);
|
351 |
|
352 | //获得模块及其起始位置信息
|
353 | var modDefs = getModuleDefs(pageContent);
|
354 | if (modDefs.length === 0) {
|
355 | return pageContent;
|
356 | }
|
357 |
|
358 | //获取子模块内容
|
359 | var i = 0, j = 0, k = 0, d = 0,
|
360 | modFile = '';
|
361 | for (; i < modDefs.length; i++) {
|
362 | //在当前目录寻找子模块,当前目录有可能是页面目录,也有可能在模块目录中
|
363 | modFile = modDefs[i].file;
|
364 | if (modDefs[i].tms.match(/^\d+\:/)) {
|
365 | modDefs[i].content = "<!--#tms file=\""+modDefs[i].tms.replace(/^\d+\:/, '')+"\"-->";
|
366 | }
|
367 | else if (modFile) {
|
368 | var modPath = path.join(path.dirname(page), modFile);
|
369 | var modExist = fs.existsSync(modPath);
|
370 | debug('模块%s, 路径%s,在页面库中%s', modFile, modPath, modExist);
|
371 |
|
372 | //当前目录没找到,到模块库中寻找
|
373 | if (!modExist) {
|
374 | for (j = 0; j < libs.length && !modExist; j++) {
|
375 | modPath = path.join(libs[j], modFile);
|
376 | modExist = fs.existsSync(modPath);
|
377 | if (modExist) {
|
378 | break;
|
379 | }
|
380 | }
|
381 | }
|
382 |
|
383 | //模块库中仍然没找到,令内容为空,处理其他模块
|
384 | if (!modExist) {
|
385 | modDefs[i].content = '';
|
386 | continue;
|
387 | }
|
388 |
|
389 | //处理模块传参
|
390 | var paramAttr = modDefs[i].param, passedParam = null;
|
391 | if (param) {
|
392 | if (paramAttr) {
|
393 | if (paramAttr.match(/\:/)) {
|
394 | paramAttr = paramAttr.replace(/\s{0,}([\:\,])\s{0,}/g, function(a,b) {return b;});
|
395 | try {
|
396 | var arr = paramAttr.split(","), m = [], deep = [], pp = {};
|
397 | passedParam = {};
|
398 | for (k=0; k<arr.length; k++) {
|
399 | m = arr[k].match(/(.+)\:(.+)/);
|
400 | if (m && typeof m[1] != "undefined" && typeof m[2] != "undefined") {
|
401 | if (m[2].match(/^\$(.+)/)) {
|
402 | deep = m[2].replace(/^\$/,'').split('.');
|
403 | pp = param;
|
404 | for (d= 0; d<deep.length; d++) pp = pp[deep[d]];
|
405 | passedParam[m[1]] = pp;
|
406 | }
|
407 | else {
|
408 | passedParam[m[1]] = m[2];
|
409 | }
|
410 | }
|
411 | }
|
412 | }
|
413 | catch (e) {console.log(e);}
|
414 | }
|
415 | else {
|
416 | passedParam = param[paramAttr];
|
417 | }
|
418 | }
|
419 | else {
|
420 | passedParam = param;
|
421 | }
|
422 | }
|
423 |
|
424 | debug('传递的参数是%s', util.inspect(passedParam));
|
425 | //渲染所有子模块内容
|
426 | modDefs[i].content = ssi(modPath, libs, cdnPath, passedParam, null, url ? url : page);
|
427 | }
|
428 | }
|
429 |
|
430 | var output = mixContentAndModules(modDefs, pageContent);
|
431 | return output;
|
432 | }
|
433 |
|
434 | /**
|
435 | *
|
436 | * 返回一个Pagelet集合,一个Pagelet以数组的型式记录了以下信息
|
437 | * {
|
438 | * file : 文件名=>eachFile,
|
439 | * begin : 在内容中的开始位置=>itBegin,
|
440 | * end : 在内容中的结束位置=>itEnd
|
441 | * content : Pagelet的内容
|
442 | * }
|
443 | * @param c
|
444 | * @return {Array}
|
445 | */
|
446 | var getModuleDefs = function(c) {
|
447 | var file2offset = [], itBegin = 0, itEnd = 0, itFile='', itParam='', itTMS='',
|
448 | it = 0, line = '', file_args = [], file = [], args = [], tms = [];
|
449 |
|
450 | while( (it = c.indexOf('<!--#include', it) ) !== -1) {
|
451 | itTMS = itFile = itParam = '';
|
452 |
|
453 | itBegin = it;
|
454 | it = it + 12;
|
455 | while(c[it] === ' ') it++;
|
456 | itEnd = c.indexOf('-->', it);
|
457 | itEnd = (itEnd === -1) ? (c.length-1) : (itEnd+3);
|
458 |
|
459 | // 区分简写和完整写法
|
460 | line = c.slice(it, itEnd-3);
|
461 | if (c[it].match(/["']/)) {
|
462 | file_args = line
|
463 | .replace(/(^\s{0,}["']\s{0,})|(\s{0,}["']\s{0,}$)/g, '')
|
464 | .split(/\s{0,}["']\s+["']\s{0,}/g);
|
465 | itFile = file_args[0];
|
466 | if (itFile.match(/^tms\:\d+\:/)) {
|
467 | itTMS = itFile.replace(/^tms\:/, '');
|
468 | itFile = '';
|
469 | }
|
470 | itParam = file_args[1] ? file_args[1] : '';
|
471 | }
|
472 | else {
|
473 | file = line.match(/file\s{0,}=\s{0,}["']\s{0,}([^"']*?)\s{0,}["']/);
|
474 | args = line.match(/data\s{0,}=\s{0,}["']\s{0,}([^"']*?)\s{0,}["']/);
|
475 | tms = line.match(/tms\s{0,}=\s{0,}["']\s{0,}([^"']*?)\s{0,}["']/);
|
476 | itFile = (file && file[1]) ? file[1] : '';
|
477 | itParam = (args && args[1]) ? args[1] : '';
|
478 | itTMS = (tms && tms[1]) ? tms[1] : '';
|
479 | }
|
480 |
|
481 | file2offset.push({file: itFile, begin: itBegin, end: itEnd, content:'', param: itParam, tms:itTMS});
|
482 | it = itEnd;
|
483 | }
|
484 |
|
485 | return file2offset;
|
486 | }
|
487 |
|
488 | exports.render = render;
|
489 | exports.ssi = ssi;
|
490 | exports.parseInfo = parseInfo;
|
491 | exports.textWithOnlineAssetsUrl = textWithOnlineAssetsUrl;
|
492 | exports.getModuleDefs = getModuleDefs;
|
493 | exports.mixContentAndModules = mixContentAndModules;
|
494 | exports.takeOutAssets = takeOutAssets;
|
495 | exports.parsePageParam = parsePageParam;
|
496 | exports.parseTmsParam = parseTmsParam;
|
497 | exports.parseAssetsTool = parseAssetsTool;
|