webpack是一个打包模块化 JavaScript 的工具,在 webpack里一切文件皆模块,经过 Loader 转换文件,经过 Plugin 注入钩子,最后输出由多个模块组合成的文件。 webpack专一于构建模块化项目。前端
咱们先从简单的入手看,当 webpack 的配置只有一个出口时,不考虑分包的状况,其实咱们只获得了一个bundle.js的文件,这个文件里包含了咱们全部用到的js模块,能够直接被加载执行。那么,我能够分析一下它的打包思路,大概有如下4步:node
咱们会可使用这几个包:webpack
ImportDeclaration
获取经过import引入的模块,FunctionDeclaration
获取函数由这几个模块的做用,其实已经能够推断出应该怎样获取单个文件的依赖模块了,转为Ast->遍历Ast->调用ImportDeclaration。代码以下:git
// exportDependencies.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const exportDependencies = (filename)=>{
const content = fs.readFileSync(filename,'utf-8')
// 转为Ast
const ast = parser.parse(content, {
sourceType : 'module' //babel官方规定必须加这个参数,否则没法识别ES Module
})
const dependencies = {}
//遍历AST抽象语法树
traverse(ast, {
//调用ImportDeclaration获取经过import引入的模块
ImportDeclaration({node}){
const dirname = path.dirname(filename)
const newFile = './' + path.join(dirname, node.source.value)
//保存所依赖的模块
dependencies[node.source.value] = newFile
}
})
//经过@babel/core和@babel/preset-env进行代码的转换
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return{
filename,//该文件名
dependencies,//该文件所依赖的模块集合(键值对存储)
code//转换后的代码
}
}
module.exports = exportDependencies
复制代码
能够跑一个例子:github
//info.js
const a = 1
export a
// index.js
import info from './info.js'
console.log(info)
//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))
复制代码
控制台输出以下图: web
有了获取单个文件依赖的基础,咱们就能够在这基础上,进一步得出整个项目的模块依赖图谱了。首先,从入口开始计算,获得entryMap,而后遍历entryMap.dependencies,取出其value(即依赖的模块的路径),而后再获取这个依赖模块的依赖图谱,以此类推递归下去便可,代码以下:数组
const exportDependencies = require('./exportDependencies')
//entry为入口文件路径
const exportGraph = (entry)=>{
const entryModule = exportDependencies(entry)
const graphArray = [entryModule]
for(let i = 0; i < graphArray.length; i++){
const item = graphArray[i];
//拿到文件所依赖的模块集合,dependencies的值参考exportDependencies
const { dependencies } = item;
for(let j in dependencies){
graphArray.push(
exportDependencies(dependencies[j])
)//关键代码,目的是将入口模块及其全部相关的模块放入数组
}
}
//接下来生成图谱
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
//能够看出,graph实际上是 文件路径名:文件内容 的集合
return graph
}
module.exports = exportGraph
复制代码
这里就不贴测试例子图了,有兴趣的能够本身跑如下~~缓存
首先,咱们的代码被加载到页面中的时候,是须要当即执行的。因此输出的bundle.js实质上要是一个当即执行函数。咱们主要注意如下几点:微信
所以,咱们要作这些工做:babel
const exportGraph = require('./exportGraph')
// 写入文件,能够用fs.writeFileSync等方法,写入到output.path中
const exportBundle = require('./exportBundle')
const exportCode = (entry)=>{
//要先把对象转换为字符串,否则在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object]
const graph = JSON.stringify(exportGraph(entry))
exportBundle(` (function(graph) { //require函数的本质是执行一个模块的代码,而后将相应变量挂载到exports对象上 function require(module) { //InnerRequire的本质是拿到依赖包的exports变量 function InnerRequire(relativePath) { return require(graph[module].dependencies[relativePath]); } var exports = {}; (function(require, exports, code) { eval(code); })(InnerRequire, exports, graph[module].code); return exports; //函数返回指向局部变量,造成闭包,exports变量在函数执行后不会被摧毁 } require('${entry}') })(${graph})`)
}
module.exports = exportCode
复制代码
这里,直接使用node的内置模块,fs来写入文件。根据webpack的output.path,来输出到对应的目录便可。我这里,为了简便,直接固定了输出路径,代码以下:
const fs = require('fs')
const path = require('path')
const exportBundle = (data)=>{
const directoryPath = path.resolve(__dirname,'dist')
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath)
}
const filePath = path.resolve(__dirname, 'dist/bundle.js')
fs.writeFileSync(filePath, `${data}\n`)
}
const access = async filePath => new Promise((resolve, reject) => {
fs.access(filePath, (err) => {
if (err) {
if (err.code === 'EXIST') {
resolve(true)
}
resolve(false)
}
resolve(true)
})
})
module.exports = exportBundle
复制代码
至此,简单打包完成。我贴一下我跑的demo的结果。bundle.js的文件内容为:
(function(graph) {
//require函数的本质是执行一个模块的代码,而后将相应变量挂载到exports对象上
function require(module) {
//InnerRequire的本质是拿到依赖包的exports变量
function InnerRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(InnerRequire, exports, graph[module].code);
return exports;//函数返回指向局部变量,造成闭包,exports变量在函数执行后不会被摧毁
}
require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./info.js":"./src/info.js"},"code":"\"use strict\";\n\nvar _info = _interopRequireDefault(require(\"./info.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_info[\"default\"]);"},"./src/info.js":{"dependencies":{"./name.js":"./src/name.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _name = require(\"./name.js\");\n\nvar info = \"\".concat(_name.name, \" is beautiful\");\nvar _default = info;\nexports[\"default\"] = _default;"},"./src/name.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.name = void 0;\nvar name = 'winty';\nexports.name = name;"}})
复制代码
至此,简单打包模型完成。须要看例子的移步至:github.com/LuckyWinty/…
webpack的运行流程是一个串行的过程,从启动到结束会依次执行如下流程:
在以上过程当中, Webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,井且插件能够调用 Webpack 提供的 API 改变 Webpack 的运行结果。其实以上7个步骤,能够简单概括为初始化、编译、输出,三个过程,而这个过程其实就是前面说的基本模型的扩展。
下面,咱们直接看看,webpack4 打包后的代码长什么样,跟咱们上文的简化模型有何区别(为了格式好看点,我精简了一下,用图片表示):
__webpack_require__
和
__webpack_exports__
是否是很眼熟?而后再认真观察,有个Module对象,key是模块名,value是代码块。输出的也是当即执行函数,从入口开始执行...
这里也是放一个简化的图,由于源码,太多啦!以下:
要注意的是,webpack4中只有optimization.namedModules
为true,此时moduleId才会为模块路径,不然是数字id。为了方便开发者调试,在development
模式下optimization.namedModules参数默认为true。
其实简单模型仍是很好理解的。咱们理解了以后,就能够更方便地深刻去了解webpack的多入口打包(应该一样的机制跑2次就能够了吧),公共包抽离(由于模块加载时有缓存,只有加上一个次数记录就能够知道这个包被加载了多少次,就能够抽离出来作公共包)了。固然仍是不少细节的地方,须要耐心细致地去理解的。持续学习吧!