webpack核心功能

118 阅读6分钟

浏览器端的模块化

浏览器端存在的问题?

  1. 效率问题,模块划分带来了更多的js文件,更多的js文件带来了更多的请求,降低了页面的访问效率
  2. 兼容性问题,目前浏览器只支持ES6模块化标准,并且还存在兼容性问题
  3. 工具问题,浏览器不支持npm下载的第三方包
    • es6标准中,import时的路径必须以./或者../开头
    • 第三方模块使用其他模块化标准

所以浏览器端很难与npm结合

node端是否存在这些问题?

不存在,因为:

  1. node端运行的js文件在本地,node可以本地读取文件,效率比浏览器远程传输文件高得多
  2. node端支持es6模块化
  3. node端支持npm下载的包(require(路径不以./或者../开头),则会去查找node内置模块或者node_modules中的依赖包)

并且在浏览器端,开发时态和运行时态的侧重点不一样(根本原因)

开发时态:

  1. 模块划分越细越好
  2. 支持多种模块化标准
  3. 支持npm或其他包管理器下载的模块
  4. 能够解决其他工程化的问题

运行时态:

  1. 文件越少越好
  2. 文件体积越小越好
  3. 代码内容越乱越好
  4. 所有浏览器都要兼容
  5. 能够解决其他运行时的问题,主要是执行效率问题

如何解决浏览器端的这些问题?

需要一个工具,利用工具把开发时编写的代码转换为运行时需要的东西,这样的工具,就叫做构建工具

常见的构建工具:

  • webpack
  • vite
  • grunt
  • gulp
  • ....

webpack中的模块化兼容性

问题:webpack同时支持commonjs和es6 module,不同的模块化可交互,在互操作时webpack是如何处理的?

1、同模块化标准

如果导入导出使用的是同一种模块化标准,打包后的结果和模块化的没有任何差异

2、不同模块化标准

  • es6导出,commonjs导入
// a.js( es6导出)
export let a=1
export let b=2
export default 3


// index.js(commonjs导入)
const a = require('./a')

console.log('a',a)

结果:

image.png

  • commonjs导出,es6导入
// a.js(commonjs导出)
module.exports={
    a:1,
    b:2,
    c:3
}

// index.js (es6导入)
import a from './a'
import * as amodule from './a'

console.log('a=>',a)

console.log('amodule=>',amodule)

结果: image.png

注意:

es默认导出,commonjs导入

// a.js
export default{
    a:1,
    b:2,
    c:3
}

//index.js
const a  = require('./a')
console.log('a=>',a)

结果:

image.png

最佳实践:选择一个合适的模块化标准,然后贯彻整个开发阶段

编译结果分析

//a.js
console.log('a module')
module.exports = 'a'

//index.js
console.log('index module')
const a = require('./a')
console.log('a=>',a)

//自行实现打包后的结果
//myMain.js
(function(modules){
    //模块缓存对象
    const cachedModule={}
    // moduleId 模块路径
    function __webpack_require(moduleId){
        // 判断模块是否缓存
        if(cachedModule[moduleId]){
            return cachedModule[moduleId]
        }
        const func = modules[moduleId] //得到该模块对应的函数
        const module={exports:{}}
        //执行该模块对应的函数
        func(module,module.exports,__webpack_require)
        //缓存模块
        cachedModule[moduleId] = module.exports
        //返回模块导出结果
         return module.exports
    }
    // 执行入口模块
    __webpack_require('./src/index.js') //运行一个模块,得到模块导出结果

})({
    //该对象包含了所有模块和模块对应代码
    './src/a.js':function(module,exports,__webpack_require){
        console.log('a module')
        module.exports = 'a'

    },
    './src/index.js':function(module,exports,__webpack_require){
        console.log('index module')
        const a = __webpack_require('./src/a.js')
        console.log('a=>',a)
    }
})

实现效果:

image.png

npx webpack --mode development打包结果:

 (() => { 
    //  //该对象包含了所有模块和模块对应代码
	var __webpack_modules__ = ({

 "./src/a.js":

 ((module) => {

eval("console.log('a module')\r\nmodule.exports = 'a'\n\n//# sourceURL=webpack:///./src/a.js?");

 }),

 "./src/index.js":

 ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {

eval("console.log('index module')\r\n\r\nconst a = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\n\r\nconsole.log('a=>',a)\r\n\n\n//# sourceURL=webpack:///./src/index.js?");

 })

	});
    // 模块缓存存储对象
	var __webpack_module_cache__ = {};
 	

    // 定义__webpack_require__函数
	function __webpack_require__(moduleId) {

        // 查找缓存
		var cachedModule = __webpack_module_cache__[moduleId];
        //有缓存,直接返回缓存结果
		if (cachedModule !== undefined) {
			return cachedModule.exports;
		}
        // 没有缓存,往缓存对象中添加
		var module = __webpack_module_cache__[moduleId] = {

			exports: {}
 		};
	

        //执行该模块对应的函数
		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
	

        // 返回该模块导出结果
		return module.exports;
	}

    // 执行入口文件
	var __webpack_exports__ = __webpack_require__("./src/index.js");
 })()
;

编译过程

webpack的作用:是将源代码(构建、打包)成最终代码

整个过程大致分为

  1. 初始化
  2. 编译
  3. 输入

初始化

webpack会将cli参数,配置文件,默认配置进行融合,形成一个最终的配置对象

编译

1、创建chunk

chunk是webpack在内部构建过程中的一个概念,译为块,他表示通过某一个入口找到的所有依赖的统称

根据入口模块,创建一个chunk,每个chunk都有至少两个属性:

  • name:默认为main
  • id:唯一编号,开发环境下和name相同,生产环境下是一个数字,从0开始

2、构建所有依赖模块 image.png

检查记录:在模块记录中存在,则直接返回模块id对应的转换后的代码,不加载该文件

读取文件内容:只读取,不执行

替换依赖函数:把导入语句替换为 __webpack_require__函数,并把函数的路径参数替换为moduleId

3、产生chunk assets

在第二步骤完成后,chunk中会产生一个模块列表,该列表中包含了模块id和模块转换后的代码

接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets,资源列表可以理解为是生成到最终文件的文件名和文件内容

image.png

chunk hash:根据所有chunk assets的内容生成一个hash字符串

4、合并chunk assets

将多个chunk的assets合并到一起,并产生一个总的hash

image.png

输出(emit)

image.png

总结

image.png

开启watch后,每次文件变化都会从编译开始,重新编译重新输出。

loader

本质上是一个函数,在打包过程中,将一个源码字符串转为另一个源码字符串返回。

loader的工作时机

在读取文件内容之后

image.png

处理loader的流程

image.png

loader配置

// myLoader.js
function myLoader(sourceCode){
    return sourceCode.replace(/abc/g,'let')
}
module.exports = myLoader
module.exports={
    mode:'development',
    module:{
            rules:[
                {
                    test:/index\.js$/,
                    use:['./loaders/myLoader']
                }
            ]
     }
}
//index.js
abc name='hello'

编译后的结果:

// main.js
eval("let name='hello'\n\n//# sourceURL=webpack:///./src/index.js?");

loader的本质是一个函数,函数参数是读取的文件内容或者上一个loader返回的内容,当检测到当前模块满足rules的某个规则,就会把该规则对应的loader放入loaders数组中,之后按照一定的顺序执行loader

loader的执行顺序

module.exports={
    mode:'development',
    module:{
            rules:[
                {
                    test:/index\.js$/,
                    use:[ './loaders/loader1', './loaders/loader2', ]
                },
                {
                    test:/\.js$/,
                    use:[ './loaders/loader3', './loaders/loader4', ]              
                }
            ]
     }
}

image.png

可以理解为在匹配模块规则时,先定义一个空数组loaders,模块规则全部匹配失败,则返回这个空数组。模块规则匹配成功,则把该规则下的use中的loader都push到这个loaders,最后所有规则匹配结束,将loaders中的loader从尾部开始取出进行处理(loaders先进后出,可以看成是一个栈结构)。

loader的配置参数options

// webpack.config.js
module.exports={
    mode:'development',
    module:{
            rules:[
                {
                    test:/index\.js$/,
                    use:[ 
                        {
                            loader:'./loaders/myLoader',
                             options:{
                                        replaceChar:'abc'
                                    }
                        },
                    ]
                }
            ]
    }
}

//myLoader.js
function myLoader(sourceCode){
    // 通过this.query获取options中的数据
    const {replaceChar} = this.query
    const reg = new RegExp(replaceChar,'g')
    return sourceCode.replace(reg,'let')
}
module.exports = myLoader

还可以直接将参数以query的形式写到路径之后: ./loaders/myLoader?replaceChar=abc&a=1',此时的this.query的值是:?replaceChar=abc&a=1

loader示例

loader处理css文件

// cssLoader.js
module.exports=function(sourceCode){
    const data = `const style = document.createElement('style')
    style.innerHTML = \`${sourceCode}\`
    document.head.appendChild(style)`
    return data
}

loader处理图片

根据参数,判断图片是转成base64的形式还是输出一张图片

// webpack.config.js
    module:{
        rules:[
            {
                test:/\.(png)|(jpg)|(jpeg)$/,
                use:[{
                    loader:'./loaders/pictureLoader',
                    options:{
                        limit:10000,
                        filename:"img-[contenthash:5].[exrt]"
                    }
                }]
            }
        ]
    }
//pictureLoader.js
const loaderUtils = require('loader-utils')

const pictureLoader = function(buffer){

    const {limit,filename}  = this.query
    const size = buffer.byteLength
    let concent;
    if(size<limit){
        concent = getBase64(buffer)
    }else{
        concent =getFilePath.call(this,buffer)
    }
    
    return `module.exports=\`${concent}\``
    
}
// 该loader要处理的是原始数据
pictureLoader.raw = true

module.exports=pictureLoader


// 图片转base64
function getBase64(buffer){
    return "data:image/png;base64,"+ buffer.toString('base64');
}

function getFilePath(buffer){
    const filename = loaderUtils.interpolateName(this,"[contenthash:5].[ext]",{
        content:buffer
    })
    // 生成文件
    this.emitFile(filename,buffer)
    return filename
}

plugin

plugin概念

作用:在webpack的某些编译节点添加监听事件,去做一些处理,当XXX时,XXX样,把某些功能嵌入到webpack的编译流程中

本质上是一个有apply方法的对象,通常会将该对象写成构造函数的模式

class MyPlugin{
    apply(compiler){
    
    }
}

compiler对象是在初始化阶段构建的,整个webpack打包期间只有一个compiler对象,后续完成打包工作的是compiler对象内部创建的compilation

apply方法会在创建好compier对象后调用,并向方法传入一个compiler对象

complier对象提供了大量的钩子函数,plugin中可以注册这次钩子函数,参与webpack的编译和生成

class MyPlugin{
    apply(compiler){
        compiler.hooks.事件名称.事件类型(name,function(complilation){
            //事件处理函数
        })
    
    }
}

事件名称可在官网API的Plugins中查看,事件类型可在官网找到Tapable文档进行查看

image.png

plugin配置

// webpack.config.js
//引入plugin
const MyPlugin = require('./plugins/MyPlugin)

module.exports={
    plugins:[
        new MyPlugin()
    ]
}

plugin示例

FilelistPlugin:将编译输出的文件的文件名称,文件大小写入filelist.text文件中,并一起输出

class FileListPlugin{
    apply(compiler){
        compiler.hooks.emit.tap('FileListPlugin',compilation=>{
            const fileNames= Object.keys(compilation.assets)
            const contentList=[]
            for(let key of fileNames){
                let current = ''
                current+=`【${key}】
大小:${compilation.assets[key].size()/1000}KB`
                contentList.push(current)
            }
            const str  = contentList.join('\n\n')
            compilation.assets['filelist.text'] = {
                source(){
                    return str
                },
                size(){
                    return str.length
                }
            }
        } )
        
    }
}

module.exports=FileListPlugin

image.png