UNPKG

17.4 kBJavaScriptView Raw
1var debug = require('debug')('clam:compile');
2var fs = require('fs');
3var path = require('path');
4var isUtf8 = require('is-utf8');
5var _ = require('underscore');
6var iconv = require('iconv-lite');
7var util = require('util');
8var config = require('./config.js');
9var proxy = require("./proxy.js");
10var assetUrl = require('./asseturl.js');
11
12var J = require("juicer");
13J.register('stringify', JSON.stringify);
14J.register("random", function(){ return (new Date()).valueOf(); });
15
16var mocker = require('./mocker.js');
17var assetsTool = require('./assetsTool');
18
19var scriptExtern = /<script[^>]*? src=['"]([^"']*?)['"].*?><\/script>/g;
20var styleExtern = /<link[^>]*? href=['"]([^"']*?)['"].*>/g;
21var assetsToolExtern = /\$assetsTool.(use|require|injectScript|injectStyle)\((['"][^\s^\$]*['"]\s*,?\s*){0,2}\)/g;
22var recoverAssetsToolExtern = /\{\{__assetsTool__\d*\}\}/g;
23var assetsToolPrefix = '__assetsTool__';
24
25/**
26 * 从一段文本中抽离Assets
27 * @param pageContent
28 * @returns {{scripts: Array, styles: Array, pageContent: XML}}
29 */
30function 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 * 解析assetsTool内容
72 * @param content
73 * @returns content
74 */
75function 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 * 生成assetsTool的模块列表
86 */
87function saveAssetsMap(root,content,pos){
88 root.push({
89 express:content,
90 id:pos
91 });
92}
93/**
94 * 复原assetsTool
95 * @param content
96 * @returns content
97 */
98function recoverAssetsTool(content,map){
99 // 对map排序,将injectStyle和injectStyle排到最后执行
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 * @param page 页面路径
132 * @param url 对应url(用于替换js和css引用)
133 * @param libs 库路径
134 * @param rootContent 页面路径对应的内容。如果有,则无需从页面路径获取内容
135 * @return {Object}
136 */
137function 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 * 渲染一个页面为最终样式。js和css提到头部。
152 * @param page
153 * @param url
154 * @param libs
155 * @param rootContent 页面路径对应的内容。如果有,则无需从页面路径获取内容
156 * @return {*}
157 */
158var 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 * @param content
199 * @param reg
200 */
201function 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
217function parsePageParam(content) {
218 return parseParam(content, /<\!--#def([\s\S]*?)-->/);
219}
220
221function parseTmsParam(content) {
222 return parseParam(content, /<\!--#tms([\s\S]*?)-->/);
223}
224
225function 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 */
237function 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 */
262function 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 */
300var 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 */
446var 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
488exports.render = render;
489exports.ssi = ssi;
490exports.parseInfo = parseInfo;
491exports.textWithOnlineAssetsUrl = textWithOnlineAssetsUrl;
492exports.getModuleDefs = getModuleDefs;
493exports.mixContentAndModules = mixContentAndModules;
494exports.takeOutAssets = takeOutAssets;
495exports.parsePageParam = parsePageParam;
496exports.parseTmsParam = parseTmsParam;
497exports.parseAssetsTool = parseAssetsTool;