小窥nodejs的Module reuqire模块加载

#前言
事情是这样的…
俺从某大神那抄过一个模板引擎,仿ASP.NET MVC的Razor模板引擎,俺也一直维护着这个razor-tmpl

虽然没人用,但偶自己用啊,老早就想看看模板引擎咋回事来着,亲自造一款发现没那么简单,但也不是太困难,繁琐的很…不过也成了我折腾的玩具,下文就是如此…

模板引擎 for nodejs,一箩筐,jade/ejs不可避免的要接触到,是可以在模板里执行一部分代码,做做循环什么的,大家都一样,哈哈…偶之前也是玩过WordPress的,不过那个站写了几篇之后弃了,现在觉得应该是写作体验不好…PHP的特性,没有模板,php既输出html标记,也在执行server端代码,我想node可以这样吗???

#折腾
这就是本篇想说的,让模板有能力执行服务端代码 :

  • 像往其他地方写入个文件啊…
  • 连下数据库什么的啊…
  • 如果数据是json,或者js exports出类json结构的,模板直接require这个数据就好了,不需要js来个require数据,再调用模板引擎的render(data),这个只需要告诉模板,你来执行下涩~

说白了,就是要require…

#require是神马
使用node-inspector + --debug-brk的同学肯定见过node代码,被wrapper在一个function里面

exports, require, module, __filename, __dirname
require根module一样是传进来的…
而且

  • exports = module.exports
  • require实际上是调用module.require,直接用module.require也是一样的,参数也一样

在Module._compile()

1
2
3
4
5
Module.prototype._compile = function(content, filename) {
var self = this;
function require(path) {
return self.require(path);
}

实际上是调用一个Module实例的require方法,顺藤摸瓜,找到 Module._load(request,parent)就是它了,require模块就是它

1
2
3
Module._load(request,parent) =>
Module._resolveFilename(request, parent) =>
Module._resolveLookupPaths(request, parent)

接着调用_resolveFilename => _resolveLookupPaths

##Native 模块
假设是fs

  1. _load里 : filename = Module._resolveFilename(request, parent);
  2. _resolveFilename里 :

    1
    2
    3
    if (NativeModule.exists(request)) {
    return request; //返回fs
    }
  3. var module = new Module(filename, parent);
    找到filename之后 => new Module => 存放到Module._cache => module.load会去执行代码,后来会return module.exports

##第三方模块
如async模块
在_resolveLookuppath里:

1
2
3
4
5
6
7
8
9
10
var start = request.substring(0, 2);
if (start !== './' && start !== '..') {
var paths = modulePaths;
if (parent) {
if (!parent.paths)
parent.paths = [];
paths = parent.paths.concat(paths);
}
return [request, paths];
}

就是用modulePaths + 它的parent的paths属性,作为paths返回,modulePaths是全局模块地址,win上是C:\User\XXXX\.node_modules 或者 .node library目录,不是npm i xxx -g那个地方

这个paths作为async库的可能的位置,因此与父Module的paths属性有关,这个属性是在Module._nodeModulePaths(module)这个函数完成的

##绝对路径模块
如require(‘d:\js\abcd’)
也满足上面第三方模块的条件,开头不是./,也不是..
所以依赖跟上面一样

##相对路径
_resolveLookuppath代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var isIndex = /^index\.\w+?$/.test(path.basename(parent.filename));
var parentIdPath = isIndex ? parent.id : path.dirname(parent.id);
var id = path.resolve(parentIdPath, request);

// make sure require('./path') and require('path') get distinct ids, even
// when called from the toplevel js file
if (parentIdPath === '.' && id.indexOf('/') === -1) {
id = './' + id;
}

debug('RELATIVE: requested:' + request +
' set ID to: ' + id + ' from ' + parent.id);

return [id, [path.dirname(parent.filename)]];

大概就是看这个parent是不是index.xxx,是或否,id不一样,lookuppath就是parent的所在目录,path.dirname(parent.filename)

require是根哪个模块相关的,也就是我不能把我的库(razor-tmpl)里面可以access到的require传给模板,那样它只能require到native模块…
分析好了这个,可以制作require了

#自定义基路径的require函数
根据给定的文件夹,或文件,假设有个js在这个文件夹,这个require就是要达到相当于这个js的require效果…

getRequire.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var vm = require('vm');
var pathFn = require('path');
var os = require('os');
var assert = require('assert');

module.exports = getRequire;

function getRequire(file) {
file = pathFn.resolve(file);
// abcd
// abcd\
if (pathFn.basename(file).indexOf('.') === -1) {
file = pathFn.join(file, "virtual.js");//不管它存不存在,没用
}

var Module = module.constructor;
var m = new Module(); // id parent

m.id = file;
m.filename = file; // filename = "d:/js/abcd.js"
m.loaded = true;
m.paths = Module._nodeModulePaths(pathFn.dirname(file)); // m.paths = [d:/js/node_modules ]
if (os.platform() === 'win32') {
var home = process.env.USERPROFILE;
m.paths.push(pathFn.join(home, "AppData", "Roaming", "npm", "node_modules")); //npm i xxx -g那个地方
}

return function(request) {
return Module._load(request, m);
}
}

if (process.mainModule === module) {
//do test
var req = getRequire("D:\\blog\\hexo\\b.js");
req('fs');
req('yamljs');
req('D:\\js\\test\\node_modules\\marked');
req('./package.json');
console.log("通过测试 Native/Third Party/absolute path/relative path...");
}

如你所见: 

  • Module包装模块的类,不能直接require得到,不过我们的module.constructor可是直接指向它的
  • 我会用模板的路径构造一个require函数

    1
    2
    假设index.razor模板
    那个构造的require就跟同目录index.js内的require相同的效果...
  • 我还将windows上的npm install -g那个目录给添加到paths了,就是可以require到那里的包…

#razor.directRenderSync
通常是不要ViewBag的,实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
razor.directRenderSync = function(viewPath, ViewBag) {
viewPath = path.resolve(viewPath); // absolute path
ViewBag = ViewBag || {};

//新数据
var data = ViewBag;
ViewBag = {
"ViewBag": data,
"require": require('./getRequire.js')(viewPath),
"__dirname": path.dirname(viewPath)
};

//模板
var pre_template = "@{\
var require = ViewBag.require;\
var __dirname = ViewBag.__dirname;\
ViewBag = ViewBag.ViewBag;\
}";
var template = getFullTemplateSync(viewPath, ViewBag);
template = pre_template + template;

//返回
return razor.render(template, ViewBag);
};

因此razor模板可以这样写

1
2
3
4
5
6
7
8
9
10
11
@{
var data = require('../data/index.json');
}

data进行 template

最后
@{
var fs = require('fs');
fs.writeFileSync("output/index.html",$result);
}

  • $result是razor-tmpl中存放结果的变量,在最后使用的话,根render的return结果一样
  • 只有Sync形式的,这种边进行模板,边执行异步代码,想不出来怎么用…所以只有Sync的

直接razor.directRenderSync('index.razor');

补图