0%

小窥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
}
  1. 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
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
15
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
42
43
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
25
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');

补图