摘要:今时今日,做前端不用个好像都被时代抛弃了一样,每天开发的时候,该上线了,反正执行个命令刷刷地就打包好了,你根本无需知道执行命令之后整个过程究竟干了什么。终于有一天,我忍不住要搞清楚究竟做了什么。
今时今日,做前端不用个webpack好像都被时代抛弃了一样,每天开发的时候npm run dev,该上线了npm run build,反正执行个命令刷刷地就打包好了,你根本无需知道执行命令之后整个过程究竟干了什么。webpack就像个黑盒,你得小心翼翼遵循它的配置行事,配好了就万幸。这使得我很长一段时间以来,都对webpack毕恭毕敬,能跑起来的代码就是最好的代码,千万别乱动配置。
终于有一天,我忍不住要搞清楚webpack究竟做了什么。
去搞清楚webpack做了什么之前,我觉得首先要思考一下我们为什么需要webpack,它究竟解决了什么痛点。想想我们日常搬砖的场景:
1.开发的时候需要一个开发环境,要是我们修改一下代码保存之后浏览器就自动展现最新的代码那就好了(热更新服务)
2.本地写代码的时候,要是调后端的接口不跨域就好了(代理服务)
3.为了跟上时代,要是能用上什么ES678N等等新东西就好了(翻译服务)
4.项目要上线了,要是能一键压缩代码啊图片什么的就好了(压缩打包服务)
5.我们平时的静态资源都是放到CDN上的,要是能自动帮我把这些搞好的静态资源怼到CDN去就好了(自动上传服务)
巴拉巴拉等等服务,那么多你需要的服务,如果你打一个响指,这些服务都有条不紊地执行好,岂不是美滋滋!所以我们需要webpack帮我们去整合那么多服务,而node的出现,赋予了我们去操作系统的能力,这才有了我们今天的幸福(kubi)生活(manong)。
所以我觉得要根据自己的需求来使用webpack,知道自己需要什么样的服务,webpack能不能提供这样的服务,如果可以,那么这个服务应该在构建中的哪个环节被处理。
如果与输入相关的需求,找entry(比如多页面就有多个入口)
如果与输出相关的需求,找output(比如你需要定义输出文件的路径、名字等等)
如果与模块寻址相关的需求,找resolve(比如定义别名alias)
如果与转译相关的需求,找loader(比如处理sass处理es678N)
如果与构建流程相关的需求,找plugin(比如我需要在打包完成后,将打包好的文件复制到某个目录,然后提交到git上)
抽丝剥茧之后,去理解这些的流程,你就能从webpack那一坨坨的配置中,定位到你需求被webpack处理的位置,最后加上相应的配置即可。
webpack打包出来的什么webpack搞了很多东西,但最终产出的无非就是经过重重服务处理过的代码,那么这些代码是怎样的呢?
首先我们先来看看入口文件index.js:
console.log("index") const one = require("./module/one.js") const two = require("./module/two.js") one() two()
嗯,很简单,没什么特别,引入了两个模块,最后执行了它们一下。其中one.js和two.js的代码也很简单,就是导出了个函数:
// one.js module.exports = function () { console.log("one") }
// two.js module.exports = function () { console.log("two") }
好了,就是这么简单的代码,放到webpack打包出来的是什么呢?
/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __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; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { console.log("index") const one = __webpack_require__(1) const two = __webpack_require__(2) one() two() /***/ }), /* 1 */ /***/ (function(module, exports) { module.exports = function () { console.log("one") } /***/ }), /* 2 */ /***/ (function(module, exports) { module.exports = function () { console.log("two") } /***/ }) /******/ ]);
真是不忍直视……我写得这么简洁优雅的代码,经过webpack的处理后如此不堪入目!但为了搞清楚这坨东西究竟做了什么,我不得不忍丑去将它简化了一下。
简化webpack打包出来的代码其实进过简化后就可以看到,这些代码意图十分明显,也是我们十分熟悉的套路。
(function (modules) { const require = function (moduleId) { const module = {} module.exports = null modules[moduleId].call(module, module, require) return module.exports } require(0) })([ function (module, require) { console.log("index") const one = require(1) const two = require(2) one() two() }, function (module, require) { module.exports = function () { console.log("one") } }, function (module, require) { module.exports = function () { console.log("two") } }])
这样看可能会直观一点:
你会看到这不就是我们挂在嘴边的自执行函数吗?然后参数是一个数组,这个数组就是我们的模块,当require(0)的时候就会执行这个数组索引为0的代码,以此类推而达到模块化的效果。这里有个关键点,就是我们明明写的时候是require("./module/one.js"),怎么最后出来可以变成require(1)呢?
没有什么比自己撸一个理解得更透彻了。我们根据上面的最终打包的结果来捋一捋要做一些什么事情。
1.观察一下,我们需要一个自执行函数,这里面需要控制的是这个自执行函数的传参,就是那个数组
2.这个数组是毋容置疑是根据依赖关系来形成的
3.我们要找到所有的require然后将require的路径替换成对应数组的索引
4.将这个处理好的文件输出出来
ok,上代码:
const fs = require("fs") const path = require("path") const esprima = require("esprima") const estraverse = require("estraverse") // 定义上下文 即所有的寻址都按照这个基准进行 const context = path.resolve(__dirname, "../") // 处理路径 const pathResolve = (data) => path.resolve(context, data) // 定义全局数据格式 const dataInfo = { // 入口文件源码 source: "", // 分析入口文件源码得出的依赖信息 requireInfo: null, // 根据依赖信息得出的各个模块 modules: null } /** * 读取文件 * @param {String} path */ const readFile = (path) => { return new Promise((resolve, reject) => { fs.readFile(path, function (err, data) { if (err) { console.log(err) reject(err) return } resolve(data) }) }) } /** * 分析入口源码 */ const getRequireInfo = () => { // 各个依赖的id 从1开始是因为0是入口文件 let id = 1 const ret = [] // 使用esprima将入口源码解析成ast const ast = esprima.parse(dataInfo.source, {range: true}) // 使用estraverse遍历ast estraverse.traverse(ast, { enter (node) { // 筛选出require节点 if (node.type === "CallExpression" && node.callee.name === "require" && node.callee.type === "Identifier") { // require路径,如require("./index.js"),则requirePath = "./index.js" const requirePath = node.arguments[0] // 将require路径转为绝对路径 const requirePathValue = pathResolve(requirePath.value) // 如require("./index.js")中"./index.js"在源码的位置 const requirePathRange = requirePath.range ret.push({requirePathValue, requirePathRange, id}) id++ } } }) return ret } /** * 模块模板 * @param {String} content */ const moduleTemplate = (content) => `function (module, require) { ${content} },` /** * 获取模块信息 */ const getModules = async () => { const requireInfo = dataInfo.requireInfo const modules = [] for (let i = 0, len = requireInfo.length; i < len; i++) { const file = await readFile(requireInfo[i].requirePathValue) const content = moduleTemplate(file.toString()) modules.push(content) } return modules } /** * 将入口文件如require("./module/one.js")等对应成require(1)模块id */ const replace = () => { const requireInfo = dataInfo.requireInfo // 需要倒序处理,因为比如第一个require("./module/one.js")中的路径是在源码字符串42-59这个区间 // 而第二个require("./module/two.js")中的路径是在源码字符串82-99这个区间,那么如果先替换位置较前的代码 // 则此时源码字符串已经少了一截(从"./module/one.js"变成1),那第二个require的位置就不对了 const sortRequireInfo = requireInfo.sort((item1, item2) => item1.requirePathRange[0] < item2.requirePathRange[0]) sortRequireInfo.forEach(({requirePathRange, id}) => { const start = requirePathRange[0] const end = requirePathRange[1] const headerS = dataInfo.source.substr(0, start) const endS = dataInfo.source.substr(end) dataInfo.source = `${headerS}${id}${endS}` }) } /** * 输出打包好的文件 */ const output = async () => { const data = await readFile(pathResolve("./template/indexTemplate.js")) const indexModule = moduleTemplate(dataInfo.source) const allModules = [indexModule, ...dataInfo.modules].join("") const result = `${data.toString()}([ ${allModules} ])` fs.writeFile(pathResolve("./build/output.js"), result, function (err) { if (err) { throw err; } }) } const main = async () => { // 读取入口文件 const data = await readFile(pathResolve("./index.js")) dataInfo.source = data.toString() // 获取依赖信息 dataInfo.requireInfo = getRequireInfo() // 获取模块信息 dataInfo.modules = await getModules() // 将入口文件如require("./module/one.js")等对应成require(1)模块id replace() // 输出打包好的文件 output() console.log(JSON.stringify(dataInfo)) } main()
这里的关键是将入口源码转成ast从而分析出require的路径在源码字符串中所在的位置,我们这里用到了esprima去将源码转成ast,然后用estraverse去遍历ast从而筛选出我们感兴趣的节点,这时我们就可以对转化成ast的代码为所欲为了,babel就是这样的原理为我们转化代码的。
最后到这里我们可以知道,除去其他杂七杂八的服务,webpack本质上就是一个将我们平时写的模块化代码转成现在浏览器可以直接执行的代码。当然上面的代码是非常简陋的,我们没有去递归处理依赖,没有去处理require的寻址(比如require("vue")是怎样找到vue在哪里的)等等的细节处理,只为还原一个最简单易懂的结构。上面的源码可以在这里找到。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/96859.html
摘要:例如允许我们在打包时将脚本分块利用浏览器缓存我们能够有的放矢的加载资源。文章的内容大体分为两个方面,一方面在思路制定模块分离的策略,另一方面从技术上对方案进行落地。我之前提到测试之下是什么样具体的场景并不重要。前言 随着前端代码需要处理的业务越来越繁重,我们不得不面临的一个问题是前端的代码体积也变得越来越庞大。这造成无论是在调式还是在上线时都需要花长时间等待编译完成,并且用户也不得不花额外的...
摘要:一介绍随着社区的框架的发布,社区也终于诞生了属于自己的前后端同构框架。本文主要研究的运行原理,分析它从接收一条指令,到完成指令背后所发生的一系列事情。最后,通过来检查输出的是否存在问题,然后发出通知,表明可用。 showImg(https://segmentfault.com/img/bVIc9l?w=536&h=136); 一、介绍 Nuxt.js - Universal Vue.j...
摘要:面试造航母,工作拧螺丝,新公司面试技术官要求会技术栈。然而公司项目暂时并没有用到,不过为了提升实战经验,还是在业余时间捣腾出一个,以下是项目介绍。前段为了学习小程序的开发,做了个小程序名叫口袋吉他,这也是个人兴趣驱使的开发想法。 面试造航母,工作拧螺丝,新公司面试技术官要求会react技术栈。 问:有使用过React么?答:没,只使用过Vue。又问:给你一星期能上手开发么?答:可以(一...
摘要:显然,要理解,首先要了解迭代器,接着了解什么是生成器。生成器上述代码中,就是一个迭代器,循环部分就是迭代过程。迭代器和生成器的执行效率因为生成器边迭代边生成,所以占用内存极少,执行效率也更高。 显然,要理解yield,首先要了解迭代器(iterator),接着了解什么是生成器(generator)。 迭代器 通俗的讲,迭代器就是可以逐个访问的容器,而逐个逐步访问的过程成为迭代。 ite...
摘要:在上述过程再细化为浏览器搜索自己的缓存。至此,浏览器已经得到了域名对应的地址。具体过程如下在中这一过程如下首先是字节流,经过解码之后是字符流,然后通过词法分析器会被解释成词语,之后经过语法分析器构建成节点,最后这些节点被组建成一棵树。 面试的时候,我们经常会被问从在浏览器地址栏中输入 url 到页面展现的短短几秒内浏览器究竟做了什么?那么浏览器到底做了啥? 浏览器的多进程架构一个好的程...
阅读 1850·2021-11-25 09:43
阅读 3690·2021-11-24 10:32
阅读 1079·2021-10-13 09:39
阅读 2332·2021-09-10 11:24
阅读 3348·2021-07-25 21:37
阅读 3466·2019-08-30 15:56
阅读 861·2019-08-30 15:44
阅读 1451·2019-08-30 13:18