您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
webpack核心概念与基本实现
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
webpack核心概念与基本实现
京东ZERO团队
2021-02-08
IP归属:未知
972440浏览
前端
# webpack **官网:webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。** ### 核心概念 入口(entry) 输出(output) Loader 插件(plugins) ### 基本构建流程 1、初始化:启动构建,读取与合并配置参数,加载 `Plugin`,实例化 Compiler。 2、编译:从 `Entry` 发出,针对每个 Module 串行调用对应的 `Loader` 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。 3、输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。 ### loader loader是一个普通的Node.js模块,这个模块需要导出一个函数,将指定格式的资源文件并将其按照一定格式转换输出。 特点:单一职责,一个Loader只做一件事情,正因为职责越单一,所以Loaders的组合性强,可配置性好。loader支持链式调用,第一个 Loader将会拿到需处理的原内容,上一个Loader处理后的结果会传给下一个接着处理,最后的Loader将处理后的最终结果返回给Webpack。 配置:loader是配置在module.rules中,module.rules的含义是创建模块的规则,module.rules的值是一个数组,其中每一项都是一项规则。loader是用来生成符合Webpack的模块的。然后Webpack把这些模块打包起来生成对应的js文件,loader是在打包前执行的。 ### 自定义loader ```js const loaderUtils = require('loader-utils') module.exports = function(source){ // 获取当前用户给当前loader传入的参数对象options const options = loaderUtils.getOptions(this); console.log(options,'---options---') return source.replace('webpack','world') } ``` **同步loader&异步loader** 同步loader: return或者this.callback() 都可以同步的返回转换过的内容 function loader(source) { this.callback(null, source, map, meta); } 可以返回多个值 异步loader:使用this.async来获取 callback 函数。这里的callback函数,和同步loader里面的参数是一样的。 ```js function loader (source) { const callback = this.async(); callback(null, source); } ``` ### 注意点: 函数中调用this.async后,return的返回值将被忽略。 函数中调用this.async后,再调用this.callback,则会将当前loader当做同步loader来处理。 函数中调用this.async后,再调用this.callback且立即调用this.async返回的callback。程序会报错。 ### plugins Webpack通过Plugin机制让其更加灵活,以适应各种应用场景。 在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable。Tapable类似于node中的events库,核心原理就是一个订阅发布模式。Compiler和Compilation都继承自Tapable,可以直接在Compiler和Compilation对象上广播和监听事件。 ### 自定义plugins ```js const path = require('path') const fs = require('fs') const cheerio = require('cheerio') class HTMLPlugin { constructor(options){ // 在构造函数中获取用户给该插件传入的配置 this.options = options } // Webpack 会调用 HTMLPlugin 实例的 apply 方法给插件实例传入 compiler 对象 apply(compiler){ //emit 生成资源到 output 目录之前 afterEmit 生成资源到 output 目录之后 compiler.hooks.afterEmit.tap('HTMLPlugin', (compilation) => { debugger const result = fs.readFileSync(this.options.template, 'utf-8') let $ = cheerio.load(result) Object.keys(compilation.assets).forEach(item => { $(`<script src="/${item}"></script>`).appendTo('body') }) fs.writeFileSync(path.resolve(process.cwd(), 'dist', this.options.filename), $.html()) }) } } module.exports = HTMLPlugin ``` 1、读取配置的过程中会先执行 new HTMLPlugin(options) 初始化一个 HTMLPlugin 获得其实例。 2、初始化 compiler 对象后调用 HTMLPlugin.apply(compiler) 给插件实例传入 compiler 对象。 3、插件实例在获取到 compiler 对象后,就可以通过compiler.plugin(事件名称, 回调函数) 监听到 webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 webpack。 #### Compiler和Compilation Compiler对象包含了Webpack环境所有的的配置信息,包含options,loaders,plugins这些信息,这个对象在Webpack启动时候被实例化,它是全局唯一的,可以简单地把它理解为Webpack实例。 Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当Webpack以开发模式运行时,每当检测到一个文件变化,一次新的Compilation将被创建。Compilation对象也提供了很多事件回调供插件做扩展。通过Compilation也能读取到Compiler对象。 **区别在于**:Compiler代表了整个Webpack从启动到关闭的生命周期,而Compilation只是代表了一次新的编译。 **在开发插件时,还需要注意以下两点:** 只要能拿到Compiler或Compilation对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。 传给每个插件的Compiler和Compilation对象都是同一个引用。也就是说在一个插件中修改了Compiler或Compilation对象上的属性,会影响到后面的插件。 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知Webpack,才会进入下一处理流程。例如: ```js compiler.plugin('emit',function(compilation, callback) { // 支持处理逻辑 // 处理完毕后执行 callback 以通知 Webpack // 如果不执行 callback,运行流程将会一直卡在这不往下执行 callback(); }); ``` ### 实现简单的webpack打包功能 webpack 到底做了什么? 建立三个文件,分为别: message.js ```js var info = require('./info.js') module.exports = { content:'今天天气' + info.content } ``` info.js ```js module.exports = { content:"下雪" } ``` index.js ```js var message = require('./message.js') console.log(message.content) ``` 三个文件之间存在依赖关系,当我们用webpack进行打包的时候,打包之后的结果如下: ```js /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ function __webpack_require__(moduleId) { /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ module.l = true; /******/ return module.exports; /******/ } /******/ __webpack_require__.m = modules; /******/ __webpack_require__.c = installedModules; /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ __webpack_require__.t = function(value, mode) { /******/ if(mode & 1) value = __webpack_require__(value); /******/ if(mode & 8) return value; /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; /******/ var ns = Object.create(null); /******/ __webpack_require__.r(ns); /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); /******/ return ns; /******/ }; /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ __webpack_require__.p = ""; /******/ return __webpack_require__(__webpack_require__.s = "./src/index.js"); /******/ }) /******/ ({ /***/ "./src/index.js": /***/ (function(module, exports, __webpack_require__) { eval("var message = __webpack_require__(/*! ./message.js */ \"./src/message.js\")\n\nconsole.log(message.content)\n\n//# sourceURL=webpack:///./src/index.js?"); /***/ }), /***/ "./src/info.js": /***/ (function(module, exports) { eval("module.exports = {\n content:\"下雨\"\n}\n\n//# sourceURL=webpack:///./src/info.js?"); /***/ }), /***/ "./src/message.js": /***/ (function(module, exports, __webpack_require__) { eval("\nvar info = __webpack_require__(/*! ./info.js */ \"./src/info.js\")\n\nmodule.exports = {\n content:'今天天气' + info.content\n}\n\n//# sourceURL=webpack:///./src/message.js?"); /***/ }) /******/ }); ``` 分析webpack打包后的源码可知,要实现一个webpack,主要有以下几点: > 1. 读取webpack.config.js > 2. 解析文件的相互依赖关系 > 3. 替换require 为__webpack_reuqire__ > 4. 将模块中的所有依赖进行读取,拼接成一个对象,传入自调用函数里面去。 项目结构: ![image-20210104091034174](https://img12.360buyimg.com/imagetools/jfs/t1/152116/23/14446/75827/5ffbc701Ee1801510/17cfd981fadf1039.png) 1.bin目录下bundle.js,读取webpack.config.js配置目录,将读取的配置传入compiler模块,让compiler模块进行相应的处理。 ```js #!/usr/bin/env node const Compiler = require('../lib/compiler') const config = require('../bundle.config') new Compiler(config).start() ``` 2.compiler.js模块主要功能 1、读取配置入口文件,逐级递归识别依赖,构建依赖图谱 2、将代码转换成AST成抽象语法树 3、在AST阶段处理代码 4、将AST抽象语法树变成浏览器可以识别的代码,输出 **核心代码** ```js // 分析模块的代码 const path = require("path") const fs = require("fs") const ejs = require("ejs") const parser = require("@babel/parser") const traverse = require("@babel/traverse").default const generator = require("@babel/generator").default class Compiler{ constructor(config){ this.config = config this.entry = config.entry this.root = process.cwd() this.modules = {} } start(){ this.depAnalyes(path.resolve(this.root,this.entry)) console.log(this.modules) this.initFile() } getSource(filePath){ //读取文件路径 return fs.readFileSync(filePath,"utf-8") } // 分析模块的代码 depAnalyes(modulePath){ let source = this.getSource(modulePath) let dependencise = [] // @babel/parser 将文件转换成抽象语法树 let AST = parser.parse(source,{ sourceType: "module" }) // @babel/traverse 遍历AST分析转换代码 traverse(AST,{ CallExpression:function(v){ if(v.node.callee.name === "require"){ v.node.callee.name = "__webpack_require__" let val = v.node.arguments[0].value let value = "./" + path.join("./src",val) v.node.arguments[0].value = value.replace(/\\+/g,"/") // 根据路径去加载所有路径下的内容,然后去分析,将这个路径push到一个数组中 dependencise.push(v.node.arguments[0].value) } } }) // @babel/generator 修改后AST转成代码 // console.log(generator(AST).code) // 递归分析所有模块 dependencise.forEach(dep => { let newPath = path.resolve(this.root,dep) this.depAnalyes(newPath) }) // 构建出来的结果 let relativePath = path.relative(this.root,modulePath) relativePath = './' + relativePath.replace(/\\+/g,"/") this.modules[relativePath] = generator(AST).code } // template/template.ejs 生成webpack模版文件 initFile(){ let template = this.getSource(path.join(__dirname,"../template/template.ejs")) let result = ejs.render(template,{ entry:this.entry, modules:this.modules }) let outputPath = path.join(this.config.output.path,this.config.output.filename) this.mkdir(this.config.output.path,()=>{ fs.writeFileSync(outputPath,result) }) } mkdir(path,callback){ fs.exists(path,function(exists){ if(exists){ callback && callback() }else{ fs.mkdirSync(path) callback && callback() } }) } } module.exports = Compiler ``` ### babel相关的AST插件 > 1. @babel/parser :将源码string转成AST > 2. @babe/traverse:遍历AST > 3. @bebe/generator:将修改后的AST转换成源码string > 4. @babel/types:修改,添加,删除等,操作AST;用于 AST 的类 lodash 库,其封装了大量与 AST 有关的方法,大大降低了转 换 AST 的成本 以上实现了一个简单的webpack的打包功能。 ### 参考文章 1. [webpack官网](https://webpack.js.org/concepts/) 2. [webpack打包原理](https://www.jianshu.com/p/e24ed38d89fd) 3. [webpack运行机制和核心工作原理](https://blog.csdn.net/weixin_43334673/article/details/107598708) 4. [webpack中loader和plugin](https://zhuanlan.zhihu.com/p/129355980)
原创文章,需联系作者,授权转载
上一篇:webpack打包组件配置(React版本)
下一篇:前端如何优雅地使用枚举
相关文章
前端十年回顾 | 漫画前端的前世今生
Taro小程序跨端开发入门实战
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
京东ZERO团队
文章数
39
阅读量
91746
作者其他文章
01
webpack打包组件配置(React版本)
这篇文章是以打包react插件的形式,介绍webpack的一些配置信息。如果写简单插件的话还是推荐使用rollup,但是可以用写插件的形式去学习一下webpack的一些东西。(适用于初中级webpack学者)
01
webpack核心概念与基本实现
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。**
01
Typescript合成Webpack中
TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript,简称ts。相对于ES6,TypeScript最大的改善是增加了类型系统,国内外很多大型工程都用它,如AngularJs,白鹭引擎、Antd。
01
小程序加载svg图片
小程序的[组件](https://developers.weixin.qq.com/miniprogram/dev/component/)中是没有支持`SVG`标签的。 但是在前端小伙伴的实际开发中,UED经常提供SVG图片过来,如果不想用引入`iconfont`的话,那么妹子我将介绍个很好用的方法。
京东ZERO团队
文章数
39
阅读量
91746
作者其他文章
01
webpack打包组件配置(React版本)
01
Typescript合成Webpack中
01
小程序加载svg图片
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号