带你熟悉webpack打包,提升自己

384 阅读4分钟

简介

屏幕快照 2021-10-21 下午5.55.32.png 当前市场下,无论angular,vue还是react,都离不开的构建工具的编译与协助,目前打包工具大多基于webpack、gulp

gulp侧重于前端开发的 整个过程 的控制管理(像是流水线)

webpack更侧重于模块打包

我们公司的项目基本上都是基于webpack打包的,了解webpack的构建原理对我们在业务上的理解会更加方便

本次分享会带你了解webpack的整体构建流程,loader和plugin的原理。在往后的业务中,你也能根据业务的需求去实现一个plugin或者loader

什么是webpack

webpack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。

webpack官网:webpack.js.org/

webpack核心概念

1.入口(entry)

webpack 创建应用程序所有依赖的关系图。图的起点被称之为入口起点(entry point)。(通常为src/index.js)

2.输出(output)

webpack 的 output 属性描述了如何处理归拢在一起的代码(bundled code)。output选项可以控制webpack如何向硬盘写入编译文件 (通常为dist)

3.loader

webpack把每个文件(.css, .html, .scss, .jpg, etc.)都作为模块处理。然而webpack自身只理解JavaScript。webpack loader 在文件被添加到依赖图中时,将文件源代码转换为模块。

4.插件-plugins

插件目的在于解决 loader无法实现的其他事。loader仅在每个文件的基础上执行转换,而插件(plugins) 常用于(但不限于)在打包模块的 “compilation” 和 “chunk” 生命周期执行操作和自定义功能。

\

分析webpack打包文件

(() => { 
  "use strict";
  var __webpack_modules__ = ({
    "./src/index.js":
      ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _pageA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./pageA */ "./src/pageA.js");\n\nfunction add(a, b) {\n  return a + b\n}\n\nlet addNum = add(_pageA__WEBPACK_IMPORTED_MODULE_0__.a, 2)\nconsole.log(addNum,'---')\n\n//# sourceURL=webpack://webpackShare/./src/index.js?");
      }),
    "./src/pageA.js":
      ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   "a": () => (/* binding */ a)\n/* harmony export */ });\nconst a = 'a'\n\n\n//# sourceURL=webpack://webpackShare/./src/pageA.js?");
      })
  });
  var __webpack_module_cache__ = {};
  function __webpack_require__(moduleId) {
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    var module = __webpack_module_cache__[moduleId] = {
      exports: {}
    };
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
  }
  (() => {
    __webpack_require__.d = (exports, definition) => {
      for (var key in definition) {
        if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key]
          });
        }
      }
    };
  })();
  (() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();
  (() => {
    __webpack_require__.r = (exports) => {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, {
          value: 'Module'
        });
      }
      Object.defineProperty(exports, '__esModule', {
        value: true
      });
    };
  })();
  var __webpack_exports__ = __webpack_require__("./src/index.js");
})();

1.modules

webpack_modules 包含所有解析路径和代码

\

2.require函数

webpack_require 函数对不同路径下的代码进行执行

3.入口文件

执行__webpack_require__函数,传入入口文件路径,递归执行依赖的代码

webpack是如何工作的

1.载入配置文件,拿到所有配置项

2.接入tapable事件流管理,初始化插件,在插件监听的不同方法触发回调函数

3.分析入口文件代码,处理loaders并解析生成AST

4.分析AST的特定语法,比如import,require等,寻找依赖,生成ES5代码

5.循环分析依赖,递归所有依赖,组合生成文件依赖图

6.通过执行函数eval执行代码

如何实现一个webpack?

1.读取配置文件,执行打包流程

const path = require('path');
const config = require(path.resolve('webpack.config.js'));
const WebpackCompiler = require('../lib/webpackCompiler.js');
const webpackCompiler = new WebpackCompiler(config);
webpackCompiler.run();

2.初始化事件流,执行插件方法

class WebpackCompiler {
  constructor(config) {
    this.config = config
    this.modules = {}
    this.root = process.cwd()
    this.entryPath =  path.join(this.root, this.config.entry) // 获取入口文件路径
    this.hooks = {
      entryInit: new tapable.SyncHook(),
      beforeCompile: new tapable.SyncHook(),
      afterCompile: new tapable.SyncHook(),
      afterPlugins: new tapable.SyncHook(),
      afterEmit:new tapable.SyncWaterfallHook(['hash'])
    }
    const plugins = config.plugins
    if (Array.isArray(plugins)) {
      plugins.forEach(item => {
        item.apply(this)
      })
    }
  }
}

3.执行run函数开始构建

run() {
    this.hooks.entryInit.call()
    this.hooks.beforeCompile.call()
    this.makeDependenciesGraph(this.entryPath)
    this.hooks.afterCompile.call()
    this.outputFile()
    this.hooks.afterPlugins.call()
    this.hooks.afterEmit.call()
  }

4.解析文件代码,循环递归依赖

makeDependenciesGraph(entry) { // 递归查找依赖全部找到所有文件代码和关联关系
    let entryModule = this.parse(entry)
    let graphArr = [entryModule]
    for (let i = 0; i < graphArr.length; i++) {
      const item = graphArr[i]
      const {
        dependencies
      } = item
      if (dependencies) {
        for (let j in dependencies) {
          graphArr.push(this.parse(dependencies[j]))
        }
      }
    }
    const graph = {}
    graphArr.forEach(i => {
      graph[i.filename] = {
        dependencies: i.dependencies,
        code: i.sourceCode
      }
    })
    this.modules =  graph // 生成的图谱代码会有一个require函数,需要自己写一个require函数,以及exports对象
  }

  parse(modulePath) {
    const source = this.getSourceByPath(modulePath); //根据路径拿到源码
    let ast = parser.parse(source, {
      sourceType:'module'
    })
    let dependencies = {}
    traverse(ast, {
      ImportDeclaration(p) {
        let node = p.node
        const dirname = path.dirname(modulePath)
        const moduleName = path.join(dirname, node.source.value)
        dependencies[node.source.value] = moduleName
      }
    })
    let sourceCode = babel.transformFromAstSync(ast, 
     null, {
       presets: ["@babel/preset-env"]
     }).code
    return {
      filename: modulePath,
      sourceCode,
      dependencies
    }
  }

AST Explorer:astexplorer.net/

5.生成代码文件

 outputFile() {
    let modules = JSON.stringify(this.modules)
    let code = `(function(graph){
			function require(module) { 
				function localRequire(relativePath) { //找到真实路径
					return require(graph[module].dependencies[relativePath]);
				}
				var exports = {};
				(function(require, exports, code){ 
          //将require和exports传入进去eval里面code里的代码
					eval(code)
				})(localRequire, exports, graph[module].code);
				return exports; // 让其他模块可以使用
			};
			require('${this.entryPath}') //把入口传入
		})(${modules});`
    let outPath = path.join(this.config.output.path, this.config.output.filename)
    findHasPath(this.config.output.path)
    fs.writeFileSync(outPath, code)
  }

如何写一个plugin

plugin主要是通过webpack初始化的时候执行plugin里的apply方法,给apply方法传递了webpack实例,通过实例里的tapable的不同钩子触发时机不同回调到不同的plugin的回调函数

 class InitPlugin {
  apply(compiler) {
    compiler.hooks.entryInit.tap('Init',function (res) {
      console.log('开始编译')
    })
  } 
}

class JsCopyPlugin {
  apply(compiler) {
    compiler.hooks.afterEmit.tap('JsCopyPlugin', function (res) {
      const randNum = parseInt(Math.random() * 100000000)
      const {
        path: configPath,
        filename
      } = compiler.config.output
      fs.copyFile(path.join(configPath, filename), path.join(configPath, filename.split('.')[0] +'.' + randNum + '.js'), function (err) {
        if (err) console.log(err)
        delFileByName(path.join(configPath, filename))
      })
      return randNum
    })
  }
}

class CleanDistPlugin {
  apply(compiler) {
    compiler.hooks.beforeCompile.tap('CleanDistPlugin', function (res) {
      delFileFolderByName(path.join(compiler.config.output.path))
    })
  }
}


class HtmlReloadPlugin {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    let self = this
    compiler.hooks.afterEmit.tap('HtmlReloadPlugin', function (res) {
      let root = process.cwd()
      let content = fs.readFileSync(path.join(root, self.options.src), 'utf-8')
      content = content.replace('main.js', `main.${res}.js`)
      fs.writeFileSync(path.join(compiler.config.output.path,'./index.html'),content)
    })
  }
}

如何写一个loader

loader是通过webpack匹配正则后再读取文件源码执行loader函数,将源码传递给loader函数进行处理,loader将处理完后的源码再返回给webpack进行babel的处理

less-loader

const less = require('less')

function loader(source) {
  let css = ''
  less.render(source,function (err,output) {
    css = output.css
  })
  let style = `
    let style =document.createElement('style')
    style.innerHTML = \n${JSON.stringify(css)}
    document.head.appendChild(style)
  `
  return style
}

module.exports = loader

style-lodaer

function loader(source) {
  let style = `
  let style =document.createElement('style')
  style.innerHTML = \n${JSON.stringify(source)}
  document.head.appendChild(style)
  `
  return style
}

module.exports = loader