今时本日,做前端不必个webpack彷佛都被时期扬弃了一样,天天开辟的时刻npm run dev,该上线了npm run build,横竖实行个敕令刷刷地就打包好了,你基础无需晓得实行敕令以后全部历程终究干了什么。webpack就像个黑盒,你得战战兢兢遵照它的设置行事,配好了就万幸。这使得我很长一段时间以来,都对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) {\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在那里的)等等的细节处置惩罚,只为复原一个最简朴易懂的构造。上面的源码能够在这里找到。