# 第一章:本周导学

1-1 本周整体内容介绍和学习方法

  • 重点:脚手架安装 项目/组件 功能开发。
  • 技术栈:ejs模版渲染(项目模板安装)和glob文件筛选。
  • 加餐:ejs源码解析、require源码解析。

# 第二章:脚手架安装模版功能架构设计

2-1 脚手架安装项目模板架构设计 installTemplate

2-2 脚手架组件初始化架构设计 与项目大体过程没有改变。 tiny change:

  • 文本提示名称
  • 项目名称format
  • 组件需要填写描述信息

# 第三章 脚手架模板安装核心实现:ejs 库功能详解

3-1 ejs模板引擎的三种基本用法

ejs主要用于模版渲染的。jsp、php是之前模版渲染的代表。ejs的实现与jsp非常类似。

  • ejs.compile(html,options)(data)
const ejs = require('ejs')
const path = require('path')
//第一种方法
const html ='<div><%= user.name%></div>'
const options = {}
const data ={
    user:{
        name:'liugezhou'
    }
}

const template = ejs.compile(html,options) //// 返回一个用于解析html中模板的 function

const compileTemplate = template(data)

console.log(compileTemplate)   //<div>liugezhou</div>

//第二种用法
const renderTemplate = ejs.render(html,data,options)
console.log(renderTemplate)

//第三种用法
const renderFile = ejs.renderFile(path.resolve(__dirname,'template.html'),data,options)
renderFile.then(file => console.log(file))

3-2 ejs模板不同标签用法详解

  • <% : ‘脚本’标签,用于流程控制,无输出。
  • <%= : 输出数据到模版(输出是转义Html标签)
  • <%- : 输出非转义的数据到模版 :如果数据是
    liugehou
    ,那么输出的就是这样的格式。
  • <%# : 注释标签,不执行、不输出内容,但是会占空间。
  • <%_ : 删除前面空格空符
  • -%>: 删除紧随其后的换行符
  • _%>: 删除后面空格字符

3-3 ejs模板几种特殊用法

本节主要介绍ejs另外比较常用的三个辅助功能

  • 包含: include
  • 自定义分隔符: 我们上面默认使用的是%,我们只需要在options参数中定义 delimiter这个参数即可
  • 自定义文件加载器: 在使用ejs.renderFile读取文件之前,可以使用ejs.fileLoader做一些操作
ejs.fileLoader = function(filePath){
	const content = fs.readFileSync(filePath)
  return '<div><%= user.copyright %></div>' + content
}

3-4 glob用法小结

glob最早是出现在类Unix系统的命令中的,用来匹配文件路径。

const glob = require('glob')

glob('**/*.js',{ignore:['node_modules/**','webpack.config.js']},function(err,file){
    console.log(file)
})

# 第四章:脚手架项目模板安装功能开发

4-1 引入项目模板类型和标准安装逻辑开发

本节代码较少,主要是梳理流程,上一大周写到了下载模版到本地缓存,本节接着上周进度: 接着便需要安装模版,新建了安装模版 installTemplate()方法,并对拿到模版的type进行判断, 若为normal,则执行安装标准模版方法:installNormalTemplate() 若为custom,则执行安装自定义模版方法:installCustomTemplate()

4-2 拷贝项目模板功能开发

async installNormalTemplate(){
  //拷贝模板代码至当前目录
  const spinner = spinnerStart('正在安装模板...')
  await sleep()
  try {
    // 去缓存目录中拿template下的文件路径
    const templatePath = path.resolve(this.templateNpm.cacheFilePath,'template')
    //当前执行脚手架目录
    const targetPath = process.cwd()
    fse.ensureDirSync(templatePath)//确保使用前缓存生成目录存在,若不存在则创建
    fse.ensureDirSync(targetPath)   //确保当前脚手架安装目录存在,若不存在则创建
    fse.copySync(templatePath,targetPath) //将缓存目录下文件copy至当前目录
  } catch (error) {
    throw error
  } finally{ 
    spinner.stop(true)
    log.success('模板安装成功')
  }
}

4-3 项目模板安装依赖和启动命令 | 4-4 白名单命令检测功能开发

在上一节,模板copy成功之后,紧接着:

//依赖安装
const { installCommand,startCommand } = this.templateInfo
//依赖安装
await this.execCommand(installCommand,'依赖过程安装失败!')
//启动命令执行
await this.execCommand(startCommand,'启动命令执行失败失败!')
const WHITE_COMMAND =['npm', 'cnpm'] 

async execCommand(command,errMsg){
        let ret 
        if(command){
            const cmdArray=command.split(' ')
            const cmd = this.checkCommand(cmdArray[0])
            if(!cmd){
                throw new Error(errMsg)
            }
            const args = cmdArray.slice(1)
            ret = await execAsync(cmd,args,{
                stdio:'inherit',
                cwd:process.cwd()
            })
            if(ret !== 0){//执行成功
                throw new Error('依赖安装过程失败')
            }
            return ret
        }
    }
    checkCommand(cmd){
        if(WHITE_COMMAND.includes(cmd)){
            return cmd
        }
        return null;
    }

4-5 项目名称自动格式化功能开发

本节使用了kebab-case这个库,将手动填入的项目名称保存在projectInfo中,以供后续package.json中的ejs渲染使用。

//生成className
if(projectInfo.projectName){
  projectInfo.name = projectInfo.projectName
  projectInfo.className = require('kebab-case')(projectInfo.projectName).replace(/^-/,'');
}
if(projectInfo.projectVersion){
  projectInfo.version = projectInfo.projectVersion
}

4-6 本章核心:ejs动态渲染项目模板

  • 首先将vue2模版中package.json文件中的name以及version使用<%= className%>和<%= version%>替代,并发布新的版本至npm。
  • commands/init模块安装 ejs和glob库。
  • 核心代码如下(在4-4节中依赖安装前,ejs动态渲染)
 async ejsRender(options){
   const dir = process.cwd()
   const projectInfo = this.projectInfo
   return new Promise((resolve,reject)=>{
     glob('**',{
       cwd:dir,
       ignore:options.ignore || '',
       nodir:true   //不输出文件夹,只输出文件
     },(err,files) =>{
       if(err){
         reject(err)
       }
       Promise.all(files.map(file=>{
         const filePath = path.join(dir,file)
         return new Promise( (resolve1,reject1) => {
           ejs.renderFile( filePath,projectInfo,{},(err,result) => {
             if(err){
               reject1(err)
             }
             fse.writeFileSync(filePath,result)
             resolve1(result)
           })
         })
       })).then(()=>{
         resolve()
       }).catch(err=>{
         reject(err)
       })
     })
   })
 }

4-7 init命令直接传入项目名称功能支持

本节完成的是 对命令行中传入项目名称的一个支持 通过判断脚手架命令是否传入项目名称,对inquirer中的prompt进行动态push。

# 第五章 组件模板开发及脚手架组件初始化功能支持

5-1 慕课乐高组件库模板开发

维护组件库发布至npm,然后在mongodb数据库中进行配置。

5-2 项目和组件模板数据隔离+动态配置ejs ignore

这部分完整代码如下

 //1.选取创建项目或组件
const { type } = await inquirer.prompt({
  type:'list',
  name:'type',
  message:'请选择初始化类型', 
  default:TYPE_PROJECT,
  choices: [{
    name: '项目',
    value: TYPE_PROJECT,
  }, {
    name: '组件',
    value: TYPE_COMPONENT,
  }]
})

// 数据隔离核心代码
this.template = this.template.filter(template =>template.tag && template.tag.includes(type))

const title =  type === TYPE_PROJECT ? '项目':'组件'
const projectNamePrompt = {
  type:'input',
  name:'projectName',
  message:`请输入${title}的名称`,
  default:'',
  validate:function(v){
    const done = this.async()
    setTimeout(function(){
      if(!isValidName(v)){
        done(`请输入合法的${title}名称`)
        return;
      }
      done(null,true)
    }, 0);
  },
  filter:function(v){
    return v
  }
}
const projectPrompt = []
if (!isProjectNameValid) {
  projectPrompt.push(projectNamePrompt);
}
projectPrompt.push({
  type:'input',
  name:'projectVersion',
  default:'1.0.0',
  message:`请输入${title}版本号`,
  validate:function(v){
    const done = this.async();
    // Do async stuff
    setTimeout(function() {
      if (!(!!semver.valid(v))) {
        done(`请输入合法的${title}版本号`);
        return;
      }
      done(null, true);
    }, 0);
  },
  filter:function(v){
    if(semver.valid(v)){
      return semver.valid(v)
    } else {
      return v
    }
  },
},{
  type:'list',
  name:'projectTemplate',
  message:`请选择${title}模板`,
  choices: this.createTemplateChoice()
})

5-3 获取组件信息功能开发

完整核心代码如下,添加了 descriptionPrompt

else if (type === TYPE_COMPONENT){
  // 获取组件的基本信息
  const descriptionPrompt = {
    type:'input',
    name:'componentDescription',
    message:'请输入组件描述信息',
    default:'',
    validate:function(v){
      const done = this.async()
      setTimeout(() => {
        if(!v){
          done('请输入组件描述信息')
          return 
        }
        done(null,true)
      }, 0);
    }
  }
  projectPrompt.push(descriptionPrompt)
  const component = await inquirer.prompt(projectPrompt)
  projectInfo = {
    ...projectInfo,
    type,
    ...component
  }
}

……
if(projectInfo.componentDescription){
  projectInfo.description = projectInfo.componentDescription
}

5-4 解决组件库初始化过程中各种工程问题

慕课乐高组件库,在发布到npm包时,安装出现问题,问题原因是 package.json中,需要将 "files":['dist'] 这行代码去除,这是因为files这里限定了上传发布到npm后只有dist这个目录。

# 第六章 脚手架自定义初始化项目模板功能开发

6-1 自定义项目模板开发

6-2 自定义模板执行逻辑开发 6-3 自定义模板上线

async installCustomTemplate(){
        //查询自定义模版的入口文件
  if(await this.templateNpm.exists()){
    const rootFile = this.templateNpm.getRootFilePath()
    if(fs.existsSync(rootFile)){
      log.verbose('开始执行自定义模板')
      const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template');
      const options = {
        templateInfo: this.templateInfo,
        projectInfo: this.projectInfo,
        sourcePath: templatePath,
        targetPath: process.cwd(),
      };
      const code = `require('${rootFile}')(${JSON.stringify(options)})`
      await execAsync('node',  ['-e', code], {stdio:'inherit',cwd: process.cwd()})
      log.success('自定义模版安装成功')
    }else{
      throw new Error('自定义模板入口文件不存在')
    }
  }
}

# 第七章 本周加餐:ejs 库源码解析 —— 彻底搞懂模板动态渲染原理

7-1 ejs.compile执行流程分析 ejs模版渲染的思路值得我们学习,于是我们就开始了了ejs的源码的学习。

点击查看【processon】 (opens new window)

本节内容较简单,我们打开webstore,从下面的代码开始调试(11行 打断点)

const ejs = require('ejs')

const html = '<div><%= user.name %></div>'
const options = {}
const data = {
	user:{
    name:'liugezhou'
  }
}

const template = ejs.compile(html,options)
const compiletemplate = template(data)
//ejs.js
exports.compile = function compile(template, opts) {
  var templ;
  if (opts && opts.scope) {   //我们的opts传进来的参数为空,暂不看此判断逻辑
    ……
  }
  templ = new Template(template, opts);
  return templ.compile();
};

templ = new Template(template,opts) 我们继续进去源码,重要的有两点

  • this.templateText = text
  • this.regex = this.createRegex()

下节开始 templ.compile()

function Template(text, opts) {
  opts = opts || {};
  var options = {};
  this.templateText = text;    //⭐️⭐️⭐️
  this.mode = null;
  this.truncate = false;
  this.currentLine = 1;
  this.source = '';
  options.client = opts.client || false;
  options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;
  options.compileDebug = opts.compileDebug !== false;
  options.debug = !!opts.debug;
  options.filename = opts.filename;
  options.openDelimiter = opts.openDelimiter || exports.openDelimiter || _DEFAULT_OPEN_DELIMITER;
  options.closeDelimiter = opts.closeDelimiter || exports.closeDelimiter || _DEFAULT_CLOSE_DELIMITER;
  options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER;
  options.strict = opts.strict || false;
  options.context = opts.context;
  options.cache = opts.cache || false;
  options.rmWhitespace = opts.rmWhitespace;
  options.root = opts.root;
  options.includer = opts.includer;
  options.outputFunctionName = opts.outputFunctionName;
  options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME;
  options.views = opts.views;
  options.async = opts.async;
  options.destructuredLocals = opts.destructuredLocals;
  options.legacyInclude = typeof opts.legacyInclude != 'undefined' ? !!opts.legacyInclude : true;

  if (options.strict) {
    options._with = false;
  }
  else {
    options._with = typeof opts._with != 'undefined' ? opts._with : true;
  }

  this.opts = options;

  this.regex = this.createRegex();  // ⭐️⭐️⭐️:该方法是对ejs标识符号%与开始结尾符号<>,进行定制化操作
}

7-2 深入讲解ejs编译原理

上一节我们看到了 return templet.compile()处,源代码如下

  compile: function () {
 
    var src;
    var fn;
    var opts = this.opts;
    var prepended = '';
    var appended = '';
    var escapeFn = opts.escapeFunction;
    var ctor;
    var sanitizedFilename = opts.filename ? JSON.stringify(opts.filename) : 'undefined';

    if (!this.source) {
      this.generateSource();   //⭐️⭐️⭐️⭐️⭐️
      prepended +=
        '  var __output = "";\n' +
        '  function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
      if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
      }
      if (opts.destructuredLocals && opts.destructuredLocals.length) {
        var destructuring = '  var __locals = (' + opts.localsName + ' || {}),\n';
        for (var i = 0; i < opts.destructuredLocals.length; i++) {
          var name = opts.destructuredLocals[i];
          if (i > 0) {
            destructuring += ',\n  ';
          }
          destructuring += name + ' = __locals.' + name;
        }
        prepended += destructuring + ';\n';
      }
      if (opts._with !== false) {
        prepended +=  '  with (' + opts.localsName + ' || {}) {' + '\n';
        appended += '  }' + '\n';
      }
      appended += '  return __output;' + '\n';
      this.source = prepended + this.source + appended;
    }

    if (opts.compileDebug) {
      src = 'var __line = 1' + '\n'
        + '  , __lines = ' + JSON.stringify(this.templateText) + '\n'
        + '  , __filename = ' + sanitizedFilename + ';' + '\n'
        + 'try {' + '\n'
        + this.source
        + '} catch (e) {' + '\n'
        + '  rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
        + '}' + '\n';
    }
    else {
      src = this.source;
    }

    if (opts.client) {
      src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
      if (opts.compileDebug) {
        src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
      }
    }

    if (opts.strict) {
      src = '"use strict";\n' + src;
    }
    if (opts.debug) {
      console.log(src);
    }
    if (opts.compileDebug && opts.filename) {
      src = src + '\n'
        + '//# sourceURL=' + sanitizedFilename + '\n';
    }

    try {
      if (opts.async) {
        try {
          ctor = (new Function('return (async function(){}).constructor;'))();
        }
        catch(e) {
          if (e instanceof SyntaxError) {
            throw new Error('This environment does not support async/await');
          }
          else {
            throw e;
          }
        }
      }
      else {
        ctor = Function;
      }
      fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
    }
    catch(e) {
      // istanbul ignore else
      if (e instanceof SyntaxError) {
        if (opts.filename) {
          e.message += ' in ' + opts.filename;
        }
        e.message += ' while compiling ejs\n\n';
        e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
        e.message += 'https://github.com/RyanZim/EJS-Lint';
        if (!opts.async) {
          e.message += '\n';
          e.message += 'Or, if you meant to create an async function, pass `async: true` as an option.';
        }
      }
      throw e;
    }

    var returnedFn = opts.client ? fn : function anonymous(data) {
      var include = function (path, includeData) {
        var d = utils.shallowCopy({}, data);
        if (includeData) {
          d = utils.shallowCopy(d, includeData);
        }
        return includeFile(path, opts)(d);
      };
      return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
    };
    if (opts.filename && typeof Object.defineProperty === 'function') {
      var filename = opts.filename;
      var basename = path.basename(filename, path.extname(filename));
      try {
        Object.defineProperty(returnedFn, 'name', {
          value: basename,
          writable: false,
          enumerable: false,
          configurable: true
        });
      } catch (e) {/* ignore */}
    }
    return returnedFn;
  },

generateSource:(最终拿到结果this.source)

 generateSource: function () {
    var opts = this.opts;


    // Slurp spaces and tabs before <%_ and after _%>
    this.templateText =
      this.templateText.replace(/[ \t]*<%_/gm, '<%_').replace(/_%>[ \t]*/gm, '_%>');

    var self = this;
    var matches = this.parseTemplateText();    //⭐️⭐️⭐️⭐️⭐️
    var d = this.opts.delimiter;
    var o = this.opts.openDelimiter;
    var c = this.opts.closeDelimiter;

    if (matches && matches.length) {
      matches.forEach(function (line, index) {   //⭐️⭐️⭐️⭐️⭐️
        var closing;
        if ( line.indexOf(o + d) === 0        // If it is a tag
          && line.indexOf(o + d + d) !== 0) { // and is not escaped
          closing = matches[index + 2];
          if (!(closing == d + c || closing == '-' + d + c || closing == '_' + d + c)) {
            throw new Error('Could not find matching close tag for "' + line + '".');
          }
        }
        self.scanLine(line); ////⭐️⭐️⭐️⭐️⭐️
      });
    }

  },

7-3 动态生成Function+with用法讲解

上一节代码没有继续追踪,根据自己的源码一步一步调试,生一节调试到的代码为:

// ejs.js  line662
fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);

代码讲解: const ctor = Function; const fn = new ctor('a,b','console.log(a,b)') fn(1,2)

我们回到7-1节中基础代码,在options加入参数debug为true,控制台输出内容为:

var __line = 1
  , __lines = "<div><%= user.name%></div>"
  , __filename = undefined;
try {
  var __output = "";
  function __append(s) { if (s !== undefined && s !== null) __output += s }
  with (locals || {}) {
    ; __append("<div>")
    ; __append(escapeFn( user.name))
    ; __append("</div>")
  }
  return __output;
} catch (e) {
  rethrow(e, __lines, __filename, __line, escapeFn);
}

通过代码,我们看到了‘with’,现在前端with的使用已经很不常见且不推荐使用了,这里简单了解下:

const ctx = {
	user:{
  	name:'liugezhou'
  }
} 

with(ctx){
  console.log(user.name)   
}

7-4 ejs compile函数执行流程分析

apply简要解释

function test(a,b,c){
	console.log(a,b,c)
  console.log(this.a)
}
test(1,2,3) //通常调用   // 1 2 3

test.apply({a:'applt'},[2,3,4]) // 2 3 4 
test.call({a:'call',2,3,4)    // 2 3 4

7-5 ejs.render和renderFile原理讲解

ejs.render的代码执行流程为:

  • const renderTemplate = ejs.render(html,data,options)
  • exports.render ==> handleCache(opts, template)
  • handleCache ==> return exports.compile(template, options);
  • handleCache(opts, template)(data)

renderFile的原理讲解

  1. const renderFile = ejs.renderFile(path.resolve(__dirname,'template.html'),data,options)
  2. exports.renderFile
  3. tryHandleCache(opts, data, cb)
  4. handleCache(options)(data)

# 第八章 加餐:require源码解析,彻底搞懂 npm 模块加载原理

8-1 require源码执行流程分析

  • require使用场景

**

  • 加载模块类型

    • 加载内置模块: require('fs')
    • 加载node_modules模块:require('ejs')
    • 加载本地模块:require('./utils')
  • 支持加载文件

    • js
    • json
    • node
    • mjs
    • 加载其它类型
  • require执行流程

6-1 我们在调试这行代码的时候,在执行栈中可以看到,之前也执行了很多代码,这里的流程以及上面分析的使用场景,我们可以先引出一些思考:

  • CommonJS模块的加载流程

  • require如何加载内置模块? loadNativeModule

  • require如何加载node_modules模块?

  • require为什么会将非js/json/node文件视为js进行加载

  • require源码

  1. 我们从 require('./ejs') 这行代码在webStorm中开始调试。(点击step into )
  2. 打开 Scripts --> no domain --> internal --> modules --> cjs --> helpers.js
  3. return mod.require(path); ----> line of 77 [helpers.js]

6-2

  1. 这里的mod就是指Module对象,调试后每个字段含义为:

    1. id:源码文件路径
    2. path:源码文件对应的文件夹,通过path.dirname(id)生成
    3. exports:模块输出的内容,默认为{}
    4. parent:父模块信息
    5. filename:源码文件路径
    6. loaded:是否已经加载完毕
    7. children:子模块对象集合
    8. paths:模块查询范围
  2. 继续step into到下一步,进去Module对象的require方法

  3. 代码如下: (校验参数为 string类型且不为空)

Module.prototype.require = function(id) {
  validateString(id, 'id');
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id,
                                    'must be a non-empty string');
  }
  requireDepth++;
  try {
    return Module._load(id, this, /* isMain */ false);
  } finally {
    requireDepth--;
  }
};

  1. Module._load(id,this,false) :
  • id:传入的字符串
  • this:Module对象
  • isMain:flase表示加载的不是一个主模块
Module._load = function(request, parent, isMain) {
  let relResolveCacheIdentifier;
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
    relResolveCacheIdentifier = `${parent.path}\x00${request}`;
    const filename = relativeResolveCache[relResolveCacheIdentifier];
    if (filename !== undefined) {
      const cachedModule = Module._cache[filename];
      if (cachedModule !== undefined) {
        updateChildren(parent, cachedModule, true);
        return cachedModule.exports;
      }
      delete relativeResolveCache[relResolveCacheIdentifier];
    }
  }

  // ✨✨✨ 
  // Module._resolveFilename是require.resolve()的核心实现,在lerna源码讲解时学过--> Module._resolveLookupPaths()
  const filename = Module._resolveFilename(request, parent, isMain);

  const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  //✨✨✨
  // loadNativeModule 中 加载内置模块,进入该源码:通过NativeModule.map我们可以看到所有的内置模块
  const mod = loadNativeModule(filename, request, experimentalModules);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // 不是内置模块,new Module,其中children在new的时候完成
  const module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename;
  }

  let threw = true;
  try {
    if (enableSourceMaps) {
      try {
        module.load(filename);
      } catch (err) {
        rekeySourceMap(Module._cache[filename], err);
        throw err; /* node-do-not-add-exception-line */
      }
    } else {
      // 🌟🌟🌟:模块加载
      module.load(filename);
    }
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
      if (parent !== undefined) {
        delete relativeResolveCache[relResolveCacheIdentifier];
      }
    }
  }

  return module.exports;
};

8-2 require加载模块原理详解

上一节我们走到了Module._load(filename)

Module.prototype.load = function(filename) {
  debug('load %j for module %j', filename, this.id);

  assert(!this.loaded);
  
  // this.filename为上一节new的时候定义的filename
  this.filename = filename;
  
  // 从这个文件的文件目录开始查到,拿到所有的可能有node_modules的路径
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  // 拿到该文件名的后缀:进入该方法可以看到定义的加载的后缀名有四种:js json node mjs
  const extension = findLongestRegisteredExtension(filename);
  // allow .mjs to be overridden
  if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) {
    throw new ERR_REQUIRE_ESM(filename);
  }
  // 这里就是require模块加载的真正逻辑,包含 js node json,源码内容见下
  Module._extensions[extension](this, filename);
  this.loaded = true;

  if (experimentalModules) {
    const ESMLoader = asyncESM.ESMLoader;
    const url = `${pathToFileURL(filename)}`;
    const module = ESMLoader.moduleMap.get(url);
    // Create module entry at load time to snapshot exports correctly
    const exports = this.exports;
    // Called from cjs translator
    if (module !== undefined && module.module !== undefined) {
      if (module.module.getStatus() >= kInstantiated)
        module.module.setExport('default', exports);
    } else {
      // Preemptively cache
      // We use a function to defer promise creation for async hooks.
      ESMLoader.moduleMap.set(
        url,
        // Module job creation will start promises.
        // We make it a function to lazily trigger those promises
        // for async hooks compatibility.
        () => new ModuleJob(ESMLoader, url, () =>
          new ModuleWrap(url, undefined, ['default'], function() {
            this.setExport('default', exports);
          })
        , false /* isMain */, false /* inspectBrk */)
      );
    }
  }
};

Module._extensionsextension

Module._extensions['.js'] = function(module, filename) {
  if (filename.endsWith('.js')) {
    const pkg = readPackageScope(filename);
    // Function require shouldn't be used in ES modules.
    if (pkg && pkg.data && pkg.data.type === 'module') {
      const parentPath = module.parent && module.parent.filename;
      const packageJsonPath = path.resolve(pkg.path, 'package.json');
      throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
    }
  }
  
  //content内容就是我们加载的ejs/index.js问的内容,这里返回一个字符串
  const content = fs.readFileSync(filename, 'utf8');
  
  // 拿到ejs.index.js中的内容,Module原型链上执行_compile,代码如下:
  module._compile(content, filename);
};
Module.prototype._compile = function(content, filename) {
  let  moduleURL;
  let redirects;
  if (manifest) {
    moduleURL = pathToFileURL(filename);
    redirects = manifest.getRedirector(moduleURL);
    manifest.assertIntegrity(moduleURL, content);
  }

  maybeCacheSourceMap(filename, content, this);
  const compiledWrapper = wrapSafe(filename, content, this);

  var inspectorWrapper = null;
  if (getOptionValue('--inspect-brk') && process._eval == null) {
    if (!resolvedArgv) {
      // We enter the repl if we're not given a filename argument.
      if (process.argv[1]) {
        try {
          resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
        } catch {
          // We only expect this codepath to be reached in the case of a
          // preloaded module (it will fail earlier with the main entry)
          assert(ArrayIsArray(getOptionValue('--require')));
        }
      } else {
        resolvedArgv = 'repl';
      }
    }

    // Set breakpoint on module start
    if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) {
      hasPausedEntry = true;
      inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
    }
  }
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  let result;
  const exports = this.exports;
  const thisValue = exports;
  const module = this;
  if (requireDepth === 0) statCache = new Map();
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, thisValue, exports,
                              require, module, filename, dirname);
  } else {
    result = compiledWrapper.call(thisValue, exports, require, module,
                                  filename, dirname);
  }
  hasLoadedAnyUserCJSModule = true;
  if (requireDepth === 0) statCache = null;
  return result;
};

8-3 require加载内置模块和四种文件类型原理

  1. 加载内置模块:流程到 loadNativeModule结束。
  2. 加载node_modules模块:通过 Module._resolveFilename(request, parent, isMain)找到路径。
  3. 加载不存在模块:Module._resolveFilename中抛出异常。
  4. 加载.js/.json/.node/mjs文件:Module._extensions['XXX' ]
  5. 加载其它文件后缀名:默认按js执行

8-4 require缓存机制解析和CommonJS加载主模块原理

连续加载两次同一个文件,require是如何处理的? require的缓存机制,使得在第二次加载相同的文件时,不会再次执行源文件,直接从缓存中去拿。

CommonJS加载主模块流程:

  • require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);
  • Module._load(main, null, true);
  • module.load(filename);
  • Module._extensions[extension](this, filename);
  • module._compile(content, filename);

与require的区别为:isMain为true,parent为null

8-5 require原理总结和回顾

  • relativeResolveCache[relResolveCacheIdentifier]查询缓存路径
  • Module._cache[filename]查询缓存模块
  • Module._resolveFilename查询模块的真实路径
  • Module._resolveFilename查询模块的真实路径
  • new Module实例化 Module 对象
  • module.load(filename)加载模块
  • findLongestRegisteredExtension获取文件后缀
  • Module._extensions[extension](this, filename)解析模块并执行模块
  • module._compile编译模块代码
  • compileFunction将模块代码生成可执行函数
  • exports, require, module, filename, dirname生成入参
  • compiledWrapper.call执行模块函数
  • return module.exports 输出模块返回结果
上次更新: 10/18/2022, 5:58:40 PM