前端面试题webpack编译流程
webpack 编译流程初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象 用上一步得到的参数初始化 Compiler 对象 加载所有配置的插件 执行对象的 run 方法开始执行编译 根据配置中的entry找出入口文件 从入口文件出发,调用所有配置的Loader对模块进行编译 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk 再把每个 Chunk 转换成一个单独的文件加入到输出列表 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
1.1entry
srcentry1.js let title = require("./title") console.log("entry12", title)
srcentry2.js let title = require("./title.js") console.log("entry2", title)
src itle.js module.exports = "title" 1.2loader.jsloader 的本质就是一个函数,一个用于转换或者说翻译的函数 把那些 webpack 不认识的模块 less sass baxx 转换为 webpack 能认识的模块 js json
loaderslogger1-loader.js function loader1(source) { //let name= "entry1"; return source + "//logger1" //let name= "entry1";//logger1 } module.exports = loader1
loaderslogger2-loader.js function loader2(source) { //let name= "entry1"; return source + "//logger2" //let name= "entry1";//logger2 } module.exports = loader2 1.3 plugin.js
pluginsdone-plugin.js class DonePlugin { apply(compiler) { compiler.hooks.done.tap("DonePlugin", () => { console.log("done:结束编译") }) } } module.exports = DonePlugin
pluginsrun1-plugin.js class RunPlugin { apply(compiler) { //在此插件里可以监听run这个钩子 compiler.hooks.run.tap("Run1Plugin", () => { console.log("run1:开始编译") }) } } module.exports = RunPlugin
pluginsrun2-plugin.js class RunPlugin { apply(compiler) { compiler.hooks.run.tap("Run2Plugin", () => { console.log("run2:开始编译") }) } } module.exports = RunPlugin 1.4 webpack.config.js
webpack.config.js const path = require("path") const Run1Plugin = require("./plugins/run1-plugin") const Run2Plugin = require("./plugins/run2-plugin") const DonePlugin = require("./plugins/done-plugin") module.exports = { mode: "development", devtool: false, context: process.cwd, entry: { entry1: "./src/entry1.js", entry2: "./src/entry2.js", }, output: { path: path.resolve(__dirname, "dist"), filename: "[name].js", }, resolve: { extensions: [".js", ".jsx", ".tx", ".tsx"], }, module: { rules: [ { test: /.js$/, use: [path.resolve(__dirname, "loaders/loader2.js"), path.resolve(__dirname, "loaders/loader1.js")], }, ], }, plugins: [new DonePlugin(), new Run2Plugin(), new Run1Plugin()], } 1.5debugger.js
debugger.js const fs = require("fs") const webpack = require("./webpack2") // const webpack = require("webpack") const webpackConfig = require("./webpack.config") debugger const compiler = webpack(webpackConfig) //4.执行`Compiler`对象的 run 方法开始执行编译 compiler.run((err, stats) => { if (err) { console.log(err) } else { //stats代表统计结果对象 const statsJson = JSON.stringify( stats.toJson({ // files: true, //代表打包后生成的文件 assets: true, //其实是一个代码块到文件的对应关系 chunks: true, //从入口模块出发,找到此入口模块依赖的模块,或者依赖的模块依赖的模块,合在一起组成一个代码块 modules: true, //打包的模块 每个文件都是一个模块 }) ) fs.writeFileSync("./statsJson.json", statsJson) } }) 1.6 webpack.js
webpack2.js const Compiler = require("./Compiler") function webpack(options) { // 1.初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象 //argv[0]是Node程序的绝对路径 argv[1] 正在运行的脚本 // node debugger --mode=production const argv = process.argv.slice(2) const shellOptions = argv.reduce((shellOptions, options) => { // options = "--mode=development" const [key, value] = options.split("=") shellOptions[key.slice(2)] = value return shellOptions }, {}) console.log("shellOptions=>", shellOptions) const finalOptions = { ...options, ...shellOptions } //2.用上一步得到的参数初始化 `Compiler` 对象 const compiler = new Compiler(finalOptions) //3.加载所有配置的插件 const { plugins } = finalOptions for (let plugin of plugins) { //订阅钩子 plugin.apply(compiler) } return compiler } module.exports = webpack 1.7 Compilation
Compiler.js const { SyncHook } = require("tapable") const Compilation = require("./Compilation") const fs = require("fs") const path = require("path") // Compiler 模块是 webpack 的主要引擎 class Compiler { constructor(options) { this.options = options this.hooks = { run: new SyncHook(), //在开始编译之前调用 done: new SyncHook(), //在编译完成时执行 } } run(callback) { this.hooks.run.call() //在编译开始前触发run钩子执行 //在编译的过程中会收集所有的依赖的模块或者说文件 //stats指的是统计信息 modules chunks files=bundle assets指的是文件名和文件内容的映射关系 const onCompiled = (err, stats, fileDependencies) => { // 10.在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统 for (let filename in stats.assets) { let filePath = path.join(this.options.output.path, filename) fs.writeFileSync(filePath, stats.assets[filename], "utf8") } callback(err, { toJson: () => stats }) for (let fileDependency of fileDependencies) { //监听依赖的文件变化,如果依赖的文件变化后会开始一次新的编译 fs.watch(fileDependency, () => this.compile(onCompiled)) } } this.hooks.done.call() //在编译完成时触发done钩子执行 //调用compile方法进行编译 开始一次新的编译 this.compile(onCompiled) } //开启一次新的编译 compile(callback) { //每次编译 都会创建一个新的Compilation实例 let compilation = new Compilation(this.options, this) compilation.build(callback) } } module.exports = Compiler 1.8 Compilation
Compilation.js const path = require("path") const fs = require("fs") const parser = require("@babel/parser") const types = require("@babel/types") const traverse = require("@babel/traverse").default const generator = require("@babel/generator").default const baseDir = normalizePath(process.cwd()) function normalizePath(path) { return path.replace(//g, "/") } class Compilation { constructor(options, compiler) { this.options = options // 配置参数 this.options.context = this.options.context || normalizePath(process.cwd()) this.compiler = compiler this.modules = [] //这里放置本次编译涉及的所有的模块 this.chunks = [] //本次编译所组装出的代码块 this.assets = {} // 存放输出的文件 key是文件名,值是文件内容 this.files = [] //代表本次打包出来的文件 this.fileDependencies = new Set() //本次编译依赖的文件或者说模块 } build(callback) { //5.根据配置中的entry找出入口文件 let entry = {} //格式化入口文件 if (typeof this.options.entry === "string") { entry.main = this.options.entry } else { entry = this.options.entry } // 对入口进行遍历 for (let entryName in entry) { //获取入口文件的绝对路径 let entryFilePath = path.posix.join(baseDir, entry[entryName]) //把此入口文件添加到文件依赖列表中 this.fileDependencies.add(entryFilePath) //6.从入口文件出发,调用所有配置的Loader对模块进行编译 let entryModule = this.buildModule(entryName, entryFilePath) // this.modules.push(entryModule) // 8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk let chunk = { name: entryName, //入口名称 entryModule, //入口的模块 ./src/entry.js modules: this.modules.filter((module) => module.names.includes(entryName)), //此入口对应的模块 } this.chunks.push(chunk) } // 9.再把每个 Chunk 转换成一个单独的文件加入到输出列表 this.chunks.forEach((chunk) => { const filename = this.options.output.filename.replace("[name]", chunk.name) this.files.push(filename) //组装chunk this.assets[filename] = getSource(chunk) }) callback( null, { modules: this.modules, chunks: this.chunks, assets: this.assets, files: this.files, }, this.fileDependencies ) } /** * 编译模块 * @param {*} name 模块所属的代码块(chunk)的名称,也就是entry的name entry1 entry2 * @param {*} modulePath 模块的绝对路径 */ buildModule(entryName, modulePath) { //1.读取文件的内容 let rawSourceCode = fs.readFileSync(modulePath, "utf8") //获取loader的配置规则 let { rules } = this.options.module //根据规则找到所有的匹配的loader 适用于此模块的所有loader let loaders = [] rules.forEach((rule) => { //用模块路径匹配正则表达式 if (modulePath.match(rule.test)) { loaders.push(...rule.use) } }) //调用所有配置的Loader对模块进行转换 let transformedSourceCode = loaders.reduceRight((sourceCode, loaderPath) => { const loaderFn = require(loaderPath) return loaderFn(sourceCode) }, rawSourceCode) //7.再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理 //获取当前模块,也就是 ./src/entry1.js的模块ID let moduleId = "./" + path.posix.relative(baseDir, modulePath) //创建一个模块ID就是相对于根目录的相对路径 dependencies就是此模块依赖的模块 //name是模块所属的代码块的名称,如果一个模块属于多个代码块,那么name就是一个数组 let module = { id: moduleId, dependencies: new Set(), names: [entryName] } this.modules.push(module) let ast = parser.parse(transformedSourceCode, { sourceType: "module" }) //Visitor是babel插件中的概念,此处没有 traverse(ast, { CallExpression: ({ node }) => { //如果调用的方法名是require的话,说明就要依赖一个其它模块 if (node.callee.name === "require") { // .代表当前的模块所有的目录,不是工作目录 let depModuleName = node.arguments[0].value //"./title" let depModulePath //获取当前的模块所在的目录 if (depModuleName.startsWith(".")) { //暂时先不考虑node_modules里的模块,先只考虑相对路径 const currentDir = path.posix.dirname(modulePath) //要找当前模块所有在的目录下面的相对路径 depModulePath = path.posix.join(currentDir, depModuleName) //此绝对路径可能没有后续,需要尝试添加后缀 // 获取配置的扩展名后缀 const extensions = this.options.resolve.extensions //尝试添加后缀 返回最终的路径 depModulePath = tryExtensions(depModulePath, extensions) } else { //如果不是以.开头的话,就是第三方模块 depModulePath = require.resolve(depModuleName) } //把依赖的模块路径添加到文件依赖列表 this.fileDependencies.add(depModulePath) //获取此依赖的模块的ID, 也就是相对于根目录的相对路径 let depModuleId = "./" + path.posix.relative(baseDir, depModulePath) //修改语法树,把依赖的模块名换成模块ID node.arguments[0] = types.stringLiteral(depModuleId) //把依赖的模块ID和依赖的模块路径放置到当前模块的依赖数组中 module.dependencies.add({ depModuleId, depModulePath }) } }, }) //转换源代码,把转换后的源码放在_source属性,用于后面写入文件 let { code } = generator(ast) module._source = code ;[...module.dependencies].forEach(({ depModuleId, depModulePath }) => { //判断此依赖的模块是否已经打包过了或者说编译 过了 let existModule = this.modules.find((module) => module.id === depModuleId) if (existModule) { existModule.names.push(entryName) } else { let depModule = this.buildModule(entryName, depModulePath) this.modules.push(depModule) } }) return module } } /** * * @param {*} modulePath * @param {*} extensions * @returns */ function tryExtensions(modulePath, extensions) { if (fs.existsSync(modulePath)) { return modulePath } for (let i = 0; i < extensions.length; i++) { let filePath = modulePath + extensions[i] if (fs.existsSync(filePath)) { return filePath } } throw new Error(`找不到${modulePath}`) } function getSource(chunk) { return ` (() => { var modules = { ${chunk.modules .filter((module) => module.id !== chunk.entryModule.id) .map( (module) => ` "${module.id}": module => { ${module._source} } ` ) .join(",")} }; var cache = {}; function require(moduleId) { var cachedModule = cache[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } var module = cache[moduleId] = { exports: {} }; modules[moduleId](module, module.exports, require); return module.exports; } var exports = {}; (() => { ${chunk.entryModule._source} })(); })(); ` } module.exports = Compilation
webpack2.js 总结1. 文件作用webpack.js 文件
webpack 方法 接收 webpack.config.js 参数,返回 compiler 实例 初始化参数 始化 Compiler 对象实例 加载所有配置的插件 Compiler文件Compiler 模块是 webpack 的主要引擎 constructor 方法 : 初始化一些 hooks run 方法 执行插件订阅的一系列 hooks 创建 Compilation 实例并执行实例的 build(onCompiled)方法(开启一次新的编译) onCompiled 回调在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统执行 compiler.run 方法的回调,传入 info监听依赖的文件变化,如果依赖的文件变化后会开始一次新的编译 Compilation 文件
build 方法 .根据配置中的 entry 找出入口文件 从入口文件出发,调用所有配置的 Loader 对模块进行编译 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk 再把每个 Chunk 转换成一个单独的文件加入到输出列表 执行成功后的回调 2. 流程总结初始化参数:初始化参数 :从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象(命令行优先级高) 开始编译用上一步得到的参数 初始化 Compiler 对象 初始化 options 参数和 hooks ( run: new SyncHook(), //在开始编译之前调用...) 加载 所有配置的 插件 : 在配置中找到 plugins 数组 遍历 plugins 执行每个插件的 apply 方法,并把 compiler 实例传进去(每个插件都有一个 apply 方法) 执行 compiler.hooks.run.tap等方法注册事件 执行 compiler实例的 run 方法 开始执行编译 整个过程伴随着触发插件的注册个各种钩子函数 this.hooks.done.call()... 开启一次新的编译,创建一个新的 Compilation 实例 执行实例的 build 方法,传入完成的回调 编译模块根据配置中的 entry 找出入口文件 格式化入口文件,变成对象形式 对入口进行遍历,获取入口文件的绝对路径,添加到文件依赖列表中 loader 转换 :从入口文件出发,调用所有配置的 Loader 对模块进行转换 (最终返回 module 对象) 读取处理文件的内容 根据规则找到所有的匹配的 loader 调用所有配置的 Loader 对模块进行转换(从上到下,从右向左) 获取当前模块模块 id,相对于根目录的相对路径 创建一个 module 对象 const module = {
id : "./src/entry1.js" , //相对于根目录的相对路径
dependencies :[{ depModuleId :./ src/ title. js, depModulePath : "xxx" }], //dependencies就是此模块依赖的模块
names :[ "entry1" ], // name是模块所属的代码块的名称,如果一个模块属于多个代码块,那么name就是一个数组
2.
_source : "xxx" , //存放对应的源码
} 编译模块分析依赖 ,再 递归遍历 本步骤直到所有入口 依赖模块 的文件都经过了本步骤的处理 将 loader 编译后的代码调用 parse 转换为 ast 遍历语法树,如果存在 require 或者 import,说明就要依赖一个其它模块 获取依赖模块的绝对路径,添加到文件依赖列表中 获取此依赖的模块的 ID, 也就是相对于根目录的相对路径 修改语法树,把依赖的模块名换成模块 ID 把依赖的模块 ID 和依赖的模块路径放置到当前模块 module 的依赖数组中 调用 generator(ast),把转换后的源码放在 module._source 属性,用于后面写入文件 遍历module.dependencies,递归构建 module,构建好的存储到 this.modules 上,如果第二个入口也依赖该模块,直接取用,只需要给该模块的 name 属性上添加上入口信息 输出资源组装 chuck 对象: 组装 const chuck = {
name : "entry1" , //入口名称
entryModule, //入口的模块的module {id,name,dependencies,_source}
modules : [{}], // 入口依赖模块的集合
} this.chunks.push(chunk)生成 bundle 文件把每个 Chunk 转换成一个单独的文件加入到输出列表获取要生成的文件名称并把文件名添加到 this.files 中获取文件内容并给 this.assets 对象执行 compilation.build 方法的回调 写入文件在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
新岗位曝光,体育局官宣丁宁新职务,薪资曝光,不负刘国梁期待久违了丁宁,2月14日情人节,丁宁再一次登上了热搜,丁宁的这一次热搜并不是因为恋情,而是因为丁宁收获了另外一个好消息。众所皆知,在退役后,丁宁返回了学校读书,看到丁宁日常生活的点滴
李铁案即将大结局?李平康李璇同步发声,球迷瓜保熟吗?2月14日,相关部门针对李铁的调查已经持续3个月有余,在此期间,不断有教练球员被带走调查,甚至包括足协的高层官员!除此之外,有人为了躲避相关部门的调查,已经提前跑路。从目前的情况来
男篮罚球有多差?乔帅不满六大核心仅1人达标周琦郭艾伦差劲中国男篮目前正在上海进行集训,备战不久后的世预赛,新帅乔尔杰维奇上任第一次带训就发现不少问题,尤其是基本功方面,为此要求所有球员苦练运动和罚球,从细节做起。而据北京体育广播报道,乔
打疯了!69分!联手欧文东契奇!阿德也来了!森林狼客场挑战独行侠,124比121,以三分优势带走胜利。赢家是森林狼,热度却感觉全在独行侠这里。大概是因为欧文的表现太炸裂了,前三节底角抽烟,仅有10分入账,末节却单节入账26分
掌握这八句口头禅,想变穷都难导语当你张嘴的那一刻,我就知道你是穷人还是富人。穷人有穷人的一套语言系统。富人有富人的一套语言系统。这两套语言系统就像两条平行线互不相交。当你想要成为富人,那么就要先拥有富人的那套
华润入主赋能中药老字号,昆药集团聚焦研发创新,打造精品国药(报告出品方分析师信达证券刘嘉仁)一公司分析1公司概况昆药集团股份有限公司(以下简称昆药或公司)成立于1951年3月,2000年12月在上海证券交易所上市。公司拥有丰富制药经验,是
增量提质推动北海高端服务业加快发展政府工作报告解读今年市政府工作报告提出要开拓思路勇于创新,在高端服务业扩增量提质量上花功夫用实劲,推动高端服务业加快发展。当前,高端服务业已成为我市经济稳增长的重要支撑,彰显出强大韧性和蓬勃活力。
奶茶杯可乐罐国家烟草专卖局重拳打击非法电子烟视频加载中国家烟草专卖局近期开展奶茶杯可乐罐等调味电子烟整治工作,查破各类案件593起追究刑事责任188人查获各类奶茶杯可乐罐72。38万支,依法严厉打击该类违法犯罪工作取得明显成
多地中小学迎来新学期(2月13日,天津市和平区第五幼儿园的小朋友在学习手工制作。当日,多地学校迎来春季新学期,并开展形式多样的开学第一课活动。新华社记者赵子硕摄)(2月13日,在北京小学大兴分校亦庄学
生活随笔老井老井吉亚超随着家门口的那口老井被掩埋,十里八村再也没有一口老井了。封井的那天,正好我在家,老井的四周围满了人,却出奇的安静,像是在为一位德高望重的老者送行。几位七八十岁的老人坐在不
他觉得在互联网世界失联了!杭州的小张反映,去年10月份,自己的微信号被封了。一直到现在,他还是想不明白为什么。1818黄金眼微信号被封感觉与互联网世界失联,小伙想问为什么小张我没有办法给别人发消息了,我已经