第一章:本周导学


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)
  1. const ejs = require('ejs')
  2. const path = require('path')
  3. //第一种方法
  4. const html ='<div><%= user.name%></div>'
  5. const options = {}
  6. const data ={
  7. user:{
  8. name:'liugezhou'
  9. }
  10. }
  11. const template = ejs.compile(html,options) //// 返回一个用于解析html中模板的 function
  12. const compileTemplate = template(data)
  13. console.log(compileTemplate) //<div>liugezhou</div>
  14. //第二种用法
  15. const renderTemplate = ejs.render(html,data,options)
  16. console.log(renderTemplate)
  17. //第三种用法
  18. const renderFile = ejs.renderFile(path.resolve(__dirname,'template.html'),data,options)
  19. renderFile.then(file => console.log(file))

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

**

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

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

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

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

3-4 glob用法小结

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

  1. const glob = require('glob')
  2. glob('**/*.js',{ignore:['node_modules/**','webpack.config.js']},function(err,file){
  3. console.log(file)
  4. })

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


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

**

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

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

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

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

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

  1. //依赖安装
  2. const { installCommand,startCommand } = this.templateInfo
  3. //依赖安装
  4. await this.execCommand(installCommand,'依赖过程安装失败!')
  5. //启动命令执行
  6. await this.execCommand(startCommand,'启动命令执行失败失败!')
  1. const WHITE_COMMAND =['npm', 'cnpm']
  2. async execCommand(command,errMsg){
  3. let ret
  4. if(command){
  5. const cmdArray=command.split(' ')
  6. const cmd = this.checkCommand(cmdArray[0])
  7. if(!cmd){
  8. throw new Error(errMsg)
  9. }
  10. const args = cmdArray.slice(1)
  11. ret = await execAsync(cmd,args,{
  12. stdio:'inherit',
  13. cwd:process.cwd()
  14. })
  15. if(ret !== 0){//执行成功
  16. throw new Error('依赖安装过程失败')
  17. }
  18. return ret
  19. }
  20. }
  21. checkCommand(cmd){
  22. if(WHITE_COMMAND.includes(cmd)){
  23. return cmd
  24. }
  25. return null;
  26. }

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

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

  1. //生成className
  2. if(projectInfo.projectName){
  3. projectInfo.name = projectInfo.projectName
  4. projectInfo.className = require('kebab-case')(projectInfo.projectName).replace(/^-/,'');
  5. }
  6. if(projectInfo.projectVersion){
  7. projectInfo.version = projectInfo.projectVersion
  8. }

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

  • 首先将vue2模版中package.json文件中的name以及version使用<%= className%>和<%= version%>替代,并发布新的版本至npm。
  • commands/init模块安装 ejs和glob库。
  • 核心代码如下(在4-4节中依赖安装前,ejs动态渲染)
    1. async ejsRender(options){
    2. const dir = process.cwd()
    3. const projectInfo = this.projectInfo
    4. return new Promise((resolve,reject)=>{
    5. glob('**',{
    6. cwd:dir,
    7. ignore:options.ignore || '',
    8. nodir:true //不输出文件夹,只输出文件
    9. },(err,files) =>{
    10. if(err){
    11. reject(err)
    12. }
    13. Promise.all(files.map(file=>{
    14. const filePath = path.join(dir,file)
    15. return new Promise( (resolve1,reject1) => {
    16. ejs.renderFile( filePath,projectInfo,{},(err,result) => {
    17. if(err){
    18. reject1(err)
    19. }
    20. fse.writeFileSync(filePath,result)
    21. resolve1(result)
    22. })
    23. })
    24. })).then(()=>{
    25. resolve()
    26. }).catch(err=>{
    27. reject(err)
    28. })
    29. })
    30. })
    31. }

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

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

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


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

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

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

这部分完整代码如下

  1. //1.选取创建项目或组件
  2. const { type } = await inquirer.prompt({
  3. type:'list',
  4. name:'type',
  5. message:'请选择初始化类型',
  6. default:TYPE_PROJECT,
  7. choices: [{
  8. name: '项目',
  9. value: TYPE_PROJECT,
  10. }, {
  11. name: '组件',
  12. value: TYPE_COMPONENT,
  13. }]
  14. })
  15. // 数据隔离核心代码
  16. this.template = this.template.filter(template =>template.tag && template.tag.includes(type))
  17. const title = type === TYPE_PROJECT ? '项目':'组件'
  18. const projectNamePrompt = {
  19. type:'input',
  20. name:'projectName',
  21. message:`请输入${title}的名称`,
  22. default:'',
  23. validate:function(v){
  24. const done = this.async()
  25. setTimeout(function(){
  26. if(!isValidName(v)){
  27. done(`请输入合法的${title}名称`)
  28. return;
  29. }
  30. done(null,true)
  31. }, 0);
  32. },
  33. filter:function(v){
  34. return v
  35. }
  36. }
  37. const projectPrompt = []
  38. if (!isProjectNameValid) {
  39. projectPrompt.push(projectNamePrompt);
  40. }
  41. projectPrompt.push({
  42. type:'input',
  43. name:'projectVersion',
  44. default:'1.0.0',
  45. message:`请输入${title}版本号`,
  46. validate:function(v){
  47. const done = this.async();
  48. // Do async stuff
  49. setTimeout(function() {
  50. if (!(!!semver.valid(v))) {
  51. done(`请输入合法的${title}版本号`);
  52. return;
  53. }
  54. done(null, true);
  55. }, 0);
  56. },
  57. filter:function(v){
  58. if(semver.valid(v)){
  59. return semver.valid(v)
  60. } else {
  61. return v
  62. }
  63. },
  64. },{
  65. type:'list',
  66. name:'projectTemplate',
  67. message:`请选择${title}模板`,
  68. choices: this.createTemplateChoice()
  69. })

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

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

  1. else if (type === TYPE_COMPONENT){
  2. // 获取组件的基本信息
  3. const descriptionPrompt = {
  4. type:'input',
  5. name:'componentDescription',
  6. message:'请输入组件描述信息',
  7. default:'',
  8. validate:function(v){
  9. const done = this.async()
  10. setTimeout(() => {
  11. if(!v){
  12. done('请输入组件描述信息')
  13. return
  14. }
  15. done(null,true)
  16. }, 0);
  17. }
  18. }
  19. projectPrompt.push(descriptionPrompt)
  20. const component = await inquirer.prompt(projectPrompt)
  21. projectInfo = {
  22. ...projectInfo,
  23. type,
  24. ...component
  25. }
  26. }
  27. ……
  28. if(projectInfo.componentDescription){
  29. projectInfo.description = projectInfo.componentDescription
  30. }

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

**

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

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


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

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

6-3 自定义模板上线

  1. async installCustomTemplate(){
  2. //查询自定义模版的入口文件
  3. if(await this.templateNpm.exists()){
  4. const rootFile = this.templateNpm.getRootFilePath()
  5. if(fs.existsSync(rootFile)){
  6. log.verbose('开始执行自定义模板')
  7. const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template');
  8. const options = {
  9. templateInfo: this.templateInfo,
  10. projectInfo: this.projectInfo,
  11. sourcePath: templatePath,
  12. targetPath: process.cwd(),
  13. };
  14. const code = `require('${rootFile}')(${JSON.stringify(options)})`
  15. await execAsync('node', ['-e', code], {stdio:'inherit',cwd: process.cwd()})
  16. log.success('自定义模版安装成功')
  17. }else{
  18. throw new Error('自定义模板入口文件不存在')
  19. }
  20. }
  21. }

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


7-1 ejs.compile执行流程分析

ejs模版渲染的思路值得我们学习,于是我们就开始了了ejs的源码的学习。

点击查看【processon】

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

  1. const ejs = require('ejs')
  2. const html = '<div><%= user.name %></div>'
  3. const options = {}
  4. const data = {
  5. user:{
  6. name:'liugezhou'
  7. }
  8. }
  9. const template = ejs.compile(html,options)
  10. const compiletemplate = template(data)
  1. //ejs.js
  2. exports.compile = function compile(template, opts) {
  3. var templ;
  4. if (opts && opts.scope) { //我们的opts传进来的参数为空,暂不看此判断逻辑
  5. ……
  6. }
  7. templ = new Template(template, opts);
  8. return templ.compile();
  9. };

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

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

下节开始 templ.compile()

  1. function Template(text, opts) {
  2. opts = opts || {};
  3. var options = {};
  4. this.templateText = text; //⭐️⭐️⭐️
  5. this.mode = null;
  6. this.truncate = false;
  7. this.currentLine = 1;
  8. this.source = '';
  9. options.client = opts.client || false;
  10. options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;
  11. options.compileDebug = opts.compileDebug !== false;
  12. options.debug = !!opts.debug;
  13. options.filename = opts.filename;
  14. options.openDelimiter = opts.openDelimiter || exports.openDelimiter || _DEFAULT_OPEN_DELIMITER;
  15. options.closeDelimiter = opts.closeDelimiter || exports.closeDelimiter || _DEFAULT_CLOSE_DELIMITER;
  16. options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER;
  17. options.strict = opts.strict || false;
  18. options.context = opts.context;
  19. options.cache = opts.cache || false;
  20. options.rmWhitespace = opts.rmWhitespace;
  21. options.root = opts.root;
  22. options.includer = opts.includer;
  23. options.outputFunctionName = opts.outputFunctionName;
  24. options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME;
  25. options.views = opts.views;
  26. options.async = opts.async;
  27. options.destructuredLocals = opts.destructuredLocals;
  28. options.legacyInclude = typeof opts.legacyInclude != 'undefined' ? !!opts.legacyInclude : true;
  29. if (options.strict) {
  30. options._with = false;
  31. }
  32. else {
  33. options._with = typeof opts._with != 'undefined' ? opts._with : true;
  34. }
  35. this.opts = options;
  36. this.regex = this.createRegex(); // ⭐️⭐️⭐️:该方法是对ejs标识符号%与开始结尾符号<>,进行定制化操作
  37. }

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

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

  1. compile: function () {
  2. var src;
  3. var fn;
  4. var opts = this.opts;
  5. var prepended = '';
  6. var appended = '';
  7. var escapeFn = opts.escapeFunction;
  8. var ctor;
  9. var sanitizedFilename = opts.filename ? JSON.stringify(opts.filename) : 'undefined';
  10. if (!this.source) {
  11. this.generateSource(); //⭐️⭐️⭐️⭐️⭐️
  12. prepended +=
  13. ' var __output = "";\n' +
  14. ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
  15. if (opts.outputFunctionName) {
  16. prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
  17. }
  18. if (opts.destructuredLocals && opts.destructuredLocals.length) {
  19. var destructuring = ' var __locals = (' + opts.localsName + ' || {}),\n';
  20. for (var i = 0; i < opts.destructuredLocals.length; i++) {
  21. var name = opts.destructuredLocals[i];
  22. if (i > 0) {
  23. destructuring += ',\n ';
  24. }
  25. destructuring += name + ' = __locals.' + name;
  26. }
  27. prepended += destructuring + ';\n';
  28. }
  29. if (opts._with !== false) {
  30. prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
  31. appended += ' }' + '\n';
  32. }
  33. appended += ' return __output;' + '\n';
  34. this.source = prepended + this.source + appended;
  35. }
  36. if (opts.compileDebug) {
  37. src = 'var __line = 1' + '\n'
  38. + ' , __lines = ' + JSON.stringify(this.templateText) + '\n'
  39. + ' , __filename = ' + sanitizedFilename + ';' + '\n'
  40. + 'try {' + '\n'
  41. + this.source
  42. + '} catch (e) {' + '\n'
  43. + ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
  44. + '}' + '\n';
  45. }
  46. else {
  47. src = this.source;
  48. }
  49. if (opts.client) {
  50. src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
  51. if (opts.compileDebug) {
  52. src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
  53. }
  54. }
  55. if (opts.strict) {
  56. src = '"use strict";\n' + src;
  57. }
  58. if (opts.debug) {
  59. console.log(src);
  60. }
  61. if (opts.compileDebug && opts.filename) {
  62. src = src + '\n'
  63. + '//# sourceURL=' + sanitizedFilename + '\n';
  64. }
  65. try {
  66. if (opts.async) {
  67. try {
  68. ctor = (new Function('return (async function(){}).constructor;'))();
  69. }
  70. catch(e) {
  71. if (e instanceof SyntaxError) {
  72. throw new Error('This environment does not support async/await');
  73. }
  74. else {
  75. throw e;
  76. }
  77. }
  78. }
  79. else {
  80. ctor = Function;
  81. }
  82. fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
  83. }
  84. catch(e) {
  85. // istanbul ignore else
  86. if (e instanceof SyntaxError) {
  87. if (opts.filename) {
  88. e.message += ' in ' + opts.filename;
  89. }
  90. e.message += ' while compiling ejs\n\n';
  91. e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
  92. e.message += 'https://github.com/RyanZim/EJS-Lint';
  93. if (!opts.async) {
  94. e.message += '\n';
  95. e.message += 'Or, if you meant to create an async function, pass `async: true` as an option.';
  96. }
  97. }
  98. throw e;
  99. }
  100. var returnedFn = opts.client ? fn : function anonymous(data) {
  101. var include = function (path, includeData) {
  102. var d = utils.shallowCopy({}, data);
  103. if (includeData) {
  104. d = utils.shallowCopy(d, includeData);
  105. }
  106. return includeFile(path, opts)(d);
  107. };
  108. return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
  109. };
  110. if (opts.filename && typeof Object.defineProperty === 'function') {
  111. var filename = opts.filename;
  112. var basename = path.basename(filename, path.extname(filename));
  113. try {
  114. Object.defineProperty(returnedFn, 'name', {
  115. value: basename,
  116. writable: false,
  117. enumerable: false,
  118. configurable: true
  119. });
  120. } catch (e) {/* ignore */}
  121. }
  122. return returnedFn;
  123. },

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

  1. generateSource: function () {
  2. var opts = this.opts;
  3. // Slurp spaces and tabs before <%_ and after _%>
  4. this.templateText =
  5. this.templateText.replace(/[ \t]*<%_/gm, '<%_').replace(/_%>[ \t]*/gm, '_%>');
  6. var self = this;
  7. var matches = this.parseTemplateText(); //⭐️⭐️⭐️⭐️⭐️
  8. var d = this.opts.delimiter;
  9. var o = this.opts.openDelimiter;
  10. var c = this.opts.closeDelimiter;
  11. if (matches && matches.length) {
  12. matches.forEach(function (line, index) { //⭐️⭐️⭐️⭐️⭐️
  13. var closing;
  14. if ( line.indexOf(o + d) === 0 // If it is a tag
  15. && line.indexOf(o + d + d) !== 0) { // and is not escaped
  16. closing = matches[index + 2];
  17. if (!(closing == d + c || closing == '-' + d + c || closing == '_' + d + c)) {
  18. throw new Error('Could not find matching close tag for "' + line + '".');
  19. }
  20. }
  21. self.scanLine(line); ////⭐️⭐️⭐️⭐️⭐️
  22. });
  23. }
  24. },

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

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

  1. // ejs.js line662
  2. 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节中基础代码,在optons加入参数debug为true,控制台输出内容为:

  1. var __line = 1
  2. , __lines = "<div><%= user.name%></div>"
  3. , __filename = undefined;
  4. try {
  5. var __output = "";
  6. function __append(s) { if (s !== undefined && s !== null) __output += s }
  7. with (locals || {}) {
  8. ; __append("<div>")
  9. ; __append(escapeFn( user.name))
  10. ; __append("</div>")
  11. }
  12. return __output;
  13. } catch (e) {
  14. rethrow(e, __lines, __filename, __line, escapeFn);
  15. }

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

  1. const ctx = {
  2. user:{
  3. name:'liugezhou'
  4. }
  5. }
  6. with(ctx){
  7. console.log(user.name)
  8. }

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

**

apply简要解释

  1. function test(a,b,c){
  2. console.log(a,b,c)
  3. console.log(this.a)
  4. }
  5. test(1,2,3) //通常调用 // 1 2 3
  6. test.apply({a:'applt'},[2,3,4]) // 2 3 4
  7. 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执行流程

image.png

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

  • 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]

    image.png

    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方法
    1. 代码如下: (校验参数为 string类型且不为空)
  1. Module.prototype.require = function(id) {
  2. validateString(id, 'id');
  3. if (id === '') {
  4. throw new ERR_INVALID_ARG_VALUE('id', id,
  5. 'must be a non-empty string');
  6. }
  7. requireDepth++;
  8. try {
  9. return Module._load(id, this, /* isMain */ false);
  10. } finally {
  11. requireDepth--;
  12. }
  13. };
  1. Module._load(id,this,false) :
  • id:传入的字符串
  • this:Module对象
  • isMain:flase表示加载的不是一个主模块 ``javascript 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) {
    1. const cachedModule = Module._cache[filename];
    2. if (cachedModule !== undefined) {
    3. updateChildren(parent, cachedModule, true);
    4. return cachedModule.exports;
    5. }
    6. 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; };

  1. <a name="raWF5"></a>
  2. ####
  3. <a name="FXcmb"></a>
  4. #### 8-2 require加载模块原理详解
  5. > 上一节我们走到了Module._load(filename)
  6. ```javascript
  7. Module.prototype.load = function(filename) {
  8. debug('load %j for module %j', filename, this.id);
  9. assert(!this.loaded);
  10. // this.filename为上一节new的时候定义的filename
  11. this.filename = filename;
  12. // 从这个文件的文件目录开始查到,拿到所有的可能有node_modules的路径
  13. this.paths = Module._nodeModulePaths(path.dirname(filename));
  14. // 拿到该文件名的后缀:进入该方法可以看到定义的加载的后缀名有四种:js json node mjs
  15. const extension = findLongestRegisteredExtension(filename);
  16. // allow .mjs to be overridden
  17. if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) {
  18. throw new ERR_REQUIRE_ESM(filename);
  19. }
  20. // 这里就是require模块加载的真正逻辑,包含 js node json,源码内容见下
  21. Module._extensions[extension](this, filename);
  22. this.loaded = true;
  23. if (experimentalModules) {
  24. const ESMLoader = asyncESM.ESMLoader;
  25. const url = `${pathToFileURL(filename)}`;
  26. const module = ESMLoader.moduleMap.get(url);
  27. // Create module entry at load time to snapshot exports correctly
  28. const exports = this.exports;
  29. // Called from cjs translator
  30. if (module !== undefined && module.module !== undefined) {
  31. if (module.module.getStatus() >= kInstantiated)
  32. module.module.setExport('default', exports);
  33. } else {
  34. // Preemptively cache
  35. // We use a function to defer promise creation for async hooks.
  36. ESMLoader.moduleMap.set(
  37. url,
  38. // Module job creation will start promises.
  39. // We make it a function to lazily trigger those promises
  40. // for async hooks compatibility.
  41. () => new ModuleJob(ESMLoader, url, () =>
  42. new ModuleWrap(url, undefined, ['default'], function() {
  43. this.setExport('default', exports);
  44. })
  45. , false /* isMain */, false /* inspectBrk */)
  46. );
  47. }
  48. }
  49. };

Module._extensionsextension

  1. Module._extensions['.js'] = function(module, filename) {
  2. if (filename.endsWith('.js')) {
  3. const pkg = readPackageScope(filename);
  4. // Function require shouldn't be used in ES modules.
  5. if (pkg && pkg.data && pkg.data.type === 'module') {
  6. const parentPath = module.parent && module.parent.filename;
  7. const packageJsonPath = path.resolve(pkg.path, 'package.json');
  8. throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
  9. }
  10. }
  11. //content内容就是我们加载的ejs/index.js问的内容,这里返回一个字符串
  12. const content = fs.readFileSync(filename, 'utf8');
  13. // 拿到ejs.index.js中的内容,Module原型链上执行_compile,代码如下:
  14. module._compile(content, filename);
  15. };
  1. Module.prototype._compile = function(content, filename) {
  2. let moduleURL;
  3. let redirects;
  4. if (manifest) {
  5. moduleURL = pathToFileURL(filename);
  6. redirects = manifest.getRedirector(moduleURL);
  7. manifest.assertIntegrity(moduleURL, content);
  8. }
  9. maybeCacheSourceMap(filename, content, this);
  10. const compiledWrapper = wrapSafe(filename, content, this);
  11. var inspectorWrapper = null;
  12. if (getOptionValue('--inspect-brk') && process._eval == null) {
  13. if (!resolvedArgv) {
  14. // We enter the repl if we're not given a filename argument.
  15. if (process.argv[1]) {
  16. try {
  17. resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
  18. } catch {
  19. // We only expect this codepath to be reached in the case of a
  20. // preloaded module (it will fail earlier with the main entry)
  21. assert(ArrayIsArray(getOptionValue('--require')));
  22. }
  23. } else {
  24. resolvedArgv = 'repl';
  25. }
  26. }
  27. // Set breakpoint on module start
  28. if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) {
  29. hasPausedEntry = true;
  30. inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
  31. }
  32. }
  33. const dirname = path.dirname(filename);
  34. const require = makeRequireFunction(this, redirects);
  35. let result;
  36. const exports = this.exports;
  37. const thisValue = exports;
  38. const module = this;
  39. if (requireDepth === 0) statCache = new Map();
  40. if (inspectorWrapper) {
  41. result = inspectorWrapper(compiledWrapper, thisValue, exports,
  42. require, module, filename, dirname);
  43. } else {
  44. result = compiledWrapper.call(thisValue, exports, require, module,
  45. filename, dirname);
  46. }
  47. hasLoadedAnyUserCJSModule = true;
  48. if (requireDepth === 0) statCache = null;
  49. return result;
  50. };

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._extensionsextension;
  • 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 输出模块返回结果