今时今日,作前端不用个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能不能提供这样的服务,若是能够,那么这个服务应该在构建中的哪一个环节被处理。vue
抽丝剥茧以后,去理解这些的流程,你就能从webpack那一坨坨的配置中,定位到你需求被webpack处理的位置,最后加上相应的配置便可。node
webpack搞了不少东西,但最终产出的无非就是通过重重服务处理过的代码,那么这些代码是怎样的呢?
首先咱们先来看看入口文件index.js:webpack
console.log('index') const one = require('./module/one.js') const two = require('./module/two.js') one() two()
嗯,很简单,没什么特别,引入了两个模块,最后执行了它们一下。其中one.js和two.js的代码也很简单,就是导出了个函数:git
// one.js module.exports = function () { console.log('one') }
// two.js module.exports = function () { console.log('two') }
好了,就是这么简单的代码,放到webpack打包出来的是什么呢?es6
/******/ (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的处理后如此不堪入目!但为了搞清楚这坨东西究竟作了什么,我不得不忍丑去将它简化了一下。github
其实进过简化后就能够看到,这些代码意图十分明显,也是咱们十分熟悉的套路。web
(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)呢?npm
没有什么比本身撸一个理解得更透彻了。咱们根据上面的最终打包的结果来捋一捋要作一些什么事情。
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) {\n${content}\n},` /** * 获取模块信息 */ 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()}([\n${allModules}\n])` 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在哪里的)等等的细节处理,只为还原一个最简单易懂的结构。上面的源码能够在这里找到。