从Webpack的基本配置到优化处理再到手写源码

224 阅读8分钟

webpack基本概念

是什么?

  • webpack是模块打包工具,以一个或者多个文件为打包的入口,分析各个模块的依赖关系,形成资源列表。输出bundle
  • 为什么需要打包
    • 因为开发时,会使用框架(vue、react)等,Es6模块化语法,还有less、sass等css预处理语言进行开发。但是这些代码必须经过编译,编译成浏览器可以识别的js、css语法才能运行。
    • 打包工具还可以压缩代码,做兼容性处理,提升代码性能
  • 有哪些打包工具?vite webpack gulp grunt等等

webpack基本功能?为什么要配置?

  • 因为webpack本身功能有限,开发模式仅能编译js代码和ES模块化语法。生产模式除了可以编译js,Es6模块化语法,还可以压缩js代码。

webpack的核心概念,基础配置

  • entry
    • 入口,用于指示webpack从哪个文件开始打包
  • output
    • 输出,输出文件路径,命名。配置clean(自动清除上次的打包文件)
  • loader
    • 因为webpack之恶能处理js、json等资源,像一些css资源、图片资源、音频、图标字体等需要在loader中进行配置才能被解析
    • 匹配各种css和css的loader
    • 匹配图片资源后缀,指定图片大小是否转化为base64,指定图片输出的文件名和路径
    • 匹配字体图标资源
    • 匹配音视频文件
    • babel-loader用于将Es6语法编写为向后兼容的语法。js兼容性处理
    • postcss-loader 用于css文件的兼容性处理
    • ...
  • plugins
    • 用于扩展webpack的功能。
    • ESLintWebpackPlugin:用于处理js文件,js语法检查
    • HtmlWebpackPlugin:用于处理html文件,指定输出路径
    • MiniCssExtractPlugin:将css文件单独打包,而不是和html文件合并成一个文件。因为在网速比较慢的时候,会先解析js再出现css用户体验较差。因此需要把css打包为单独的文件再通过link标签引入。
    • CssMinimizerWebpackPlugin:对Css文件进行压缩:因为生产环境下默认对js和html文件进行压缩,如果进一步减小体积,需要引入插件压缩css文件。
  • mode
    • 用于指定打包模式。development/production。
  • 其他配置
    • 开发服务器的配置 webpack-dev-server,配置完成,再次打包不会输出。
    • 配置脚本指令

webpack基本使用和基本配置思路

基本配置

  • 开发模式
    • 处理样式资源:在loader的rules中配置,test正则表达式匹配到相应的文件后缀名。use指明使用的多个loader。
    • 处理图片资源:webpack5的方式,配置test,配置asset,配置小于10的转化为base64
    • 处理字体图标资源
    • 处理其他资源(音视频
    • 处理js资源:webpack只能处理es6模块化。其他语法不能处理,比如兼容性和eslint。分别使用eslint和babel处理,eslint在webpack4中是一个loader,在webpack5中是一个插件。使用eslint需要引入插件,并且书写语法检查规则,babel属于编译器,是一个loader。
    • 处理html资源:使用html插件自动引入相关资源,不需要手动在html文件中引入入口文件main.js.下载插件htmlWebpackPlugin,引入,调用。
    • 打包分为不同文件。
    • clean自动清空上次打包的内容
    • 开发服务器,自动化:webpack-dev-server
  • 生产模式
    • Css处理:分为兼容性处理和压缩处理。miniCssExtractPlugin,将CSS提取到单独的文件中,为每一个包含CSS的js文件创建一个CSS文件,支持按需加载。兼容性处理:postcss-loader postcss postcss-preset-env
    • html压缩:使用插件 CssMinimizerWebpackPlugin 对CSS文件进行压缩 生产环境下默认对js文件和html文件进行压缩。

webpack的优化处理

提升开发体验

  • SourceMap
    • why:开发时能够有准确的代码提示
    • what:源代码映射,能够输出一个.map文件,指出源代码出错的具体位置
    • how:开发模式:cheap-module-source-map只提示行数。生产模式:source-map,既提示出错的行数也提示出错的列。

提升打包构建速度

  • 热重载HMR(HotModuleReplacement)只重新编译打包代码中更新的部分
    • why:如果开发过程中只修改了一个模块的代码,webpack还是会重新打包编译。如果可以只重新打包编译更新的部分,其他不变的部分使用缓存。更新速度会更快
    • how:属于webpack5的默认配置。hot默认值为true。样式资源中style-loader本身具备热重载的功能。
  • Oneof 设置每个文件只能被一个loader处理
    • 编译打包时所有文件都需要进行loader的遍历处理,如果设置每个文件只能被一个loader处理,就会提升打包构建速度
  • Include、Exclude:排除或者只解析某些文件。
    • 比如一些第三方的库和插件,下载到node_modules中
  • Cache:对js之前的一些耗时操作进行缓存,比如eslint和babel的处理结果
    • 如果每次编译都执行所有的eslint和babel,编译速度会很慢。因此如果能够缓存之前的eslint和babel的编译结果,会提升打包构建速度
  • Thread 开启多线程处理eslint和babel任务
    • 在需要处理较多代码的时候才会有效果

减少代码体积

  • TreeShaking
    • 剔除代码中没有使用的多余代码,让代码的体积更小
  • 处理bable:@babel/plugin-transfrom-runtime
    • 通过插件对babel进行处理,从而处理文件时不需要每一个文件中都生成辅助代码,而是抽离出一个文件,让需要引入辅助代码的文件从公共文件中引入
  • ImageMinimizer
    • 对项目中的图片进行压缩,使体积更小请求速度更快。分为无损压缩和有损压缩

优化代码运行性能(用户体验)

  • CodeSplit代码分割
    • 使用code-split对代码进行分割,分割为多个js文件,从而使单个文件体积更小,加载更快
    • 并且通过import()动态导入语法进行按需加载,从而达到按需加载。
  • Preload/Prefetch
    • 对代码进行提前加载,再未来需要使用时直接使用
  • Network Cache
    • 对输出的资源文件做好缓存
  • Core-js
    • 对js进行兼容性处理,可以运行低版本浏览器
  • PWA
    • 使用PWA能让代码离线时也可以访问。

loader和plugins的原理,区别

为什么配置loader和plugins

  • 因为webpack本身功能有限,开发模式仅能编译js代码和ES模块化语法。生产模式除了可以编译js,Es6模块化语法,还可以压缩js代码。
  • 因此需要loader对一些css,sass,png图片资源等等,将文件解析。需要plugins对webpack功能做扩展。

loader

  • 概念
    • loader是文件加载器。用于帮助webpack将不同类型的文件转换为webpack可以识别的模块。
  • 分类,执行顺序
    • 分为pre-loader,normal-loader,inline-loader,post-loader
    • pre>normal>inline>post
    • 相同优先级的loader从右至左,从下到上执行
  • 原理
    • loader实际上是一个函数,webpack根据文件后缀名对不同的文件执行不同的loader,把文件内容作为loader函数的参数传入,然后做一些操作,返回文件内容
    • loader接收参数:content文件内容,map为sourcemap数据,meta其他数据,可以是上次loader传来的数据
    • 几种不同的loader函数:同步loader(this.callback),异步loader(this.async(),raw-loader(UTF-8),Pitching-loader(优先执行的pitch())
module.exports = function loader1(content) {
  console.log("hello loader");
  return content;
};
  • 各个loader都有什么作用
    • 通过webpack的loader进行处理,首先是各自的loader(less/sass/-loader),然后是css-loader,然后是style-loader进行分别处理
    • less/xxx-loader是将预处理语言转化为css-loader
    • css-loader主要用来解析css文件中的@import和url语句的,处理css-modules的,将结果作为一个js模块返回
    • style-loader用于将样式内容以标签的方式插入到DOM树中
  • 预处理语言的不同
    • 预处理语言的不同,为什么要预处理器
    • 因为css预处理语言可以更加简洁,代码更加直观,支持嵌套、变量、运算、mixins、、、
    • sass 不包括花括号,引用变量$开始
    • less 变量@,简单
    • stylus 同时支持两种语法

plugins

  • 概念
    • plugins是插件,用于扩展webpack的功能,从而使webpack有更强大的构建能力。比如打包优化,资源管理等等,插件会运行在webpack编译过程中的不同生命周期中。
  • 原理
    • plugins会在webpack的不同钩子函数中注册事件,从而webpack在构建过程中触发相应的钩子函数,从而执行了相应的事件,扩展webpack的功能
  • 实现plugins
    • plugins的本质是构造函数,拥有apply方法。
    • 因为webpack编译过程,在引入配置文件后,会通过new 插件()的方式执行constructor构造函数,然后创建compiler对象,遍历所有插件,调用插件的apply方法。
    • plugins中的apply方法使用tap、tapasync、tapPromise注册同步或者异步钩子。

loader和plugins的区别

  • 是什么
    • loader是文件加载器,用于将不能识别的文件类型转换为能够识别的模块
    • plugins是webpack中的插件,用于扩展webpack的功能,比如打包优化,资源管理等等
  • 运行时机不同
    • loader运行在打包文件之前,在模板编译的时候将所有的文件使用loader进行处理
    • plugins在整个文件的编译周期都起作用,在不同的生命周期钩子中注册事件
  • 本质,原理
    • loader本质是一个函数,传入文件内容,返回处理过后的文件内容
    • plugins本质是一个构造函数,包含apply方法,通过tap向compiler的不同生命周期函数中注册事件,在编译执行时随着生命周期函数被执行而实现相应的功能。
  • 常用的loader和plugins
    • style-loader css-loader less-loader...
    • HtmlWebpackPlugin mini-css-extract-plugins Eslint-webpack-plugin...

HMR热更新

  • 是什么?
    • Hot Module Replacement是webapck的一个配置,比如在开发过程中,修改了某个模块webpack还是会全部进行打包编译,但是如果使用热重载,webpack就会只编译打包改变的部分,而不是全部编译整个应用。
    • webpack5中默认hot为开启状态。
  • 原理
    • 使用webpack-dev-server创建两个服务器,分别用来托管静态资源和提供soket服务
    • 当socket-server监听到对应的模块发生变化会生成两个文件,一个是menifest文件说明变化的内容和chunk.js文件
    • 通过长连接,socket-server可以将这两个文件发送给浏览器,浏览器通过HMR runtime机制加载这两个文件并且针对修改的模块进行更新。

webpack proxy的工作原理

  • proxy
    • webpack-proxy是webpack提供的代理服务。通过提供一个中间服务器,当客户端需要发送请求时可以转发给代理服务器,然后由代理服务器发送给目标服务器。用于解决跨域问题

其他打包工具,webpack和vite的区别

  • vite的特点
    • webpack会先打包,然后启动开发服务器请求服务器时直接给予打包结果。而vite直接启动开发服务器请求哪个模块再对该模块进行实时编译。利用现代浏览器支持ES Module的特性,按需动态编译。
    • 启动速度更快:vite在启动的时候不需要打包,因此不需要分析模块的依赖不需要编译。当浏览器请求某个模块时再根据需要对模块进行编译。
    • 更好的热更新方式:vite在修改了一个模块以后,仅需要将浏览器重新请求该模块即可。而webpack的热更新是当对代码修改并保存时,webpack会对修改的模块重新打包,并将新的模块发送给浏览器,浏览器用新的模块代替旧的模块从而实现在不刷新浏览器的前提下更新页面。
    • 不支持CommonJS,只支持ES Module
  • webpack的优势
    • webpack的功能更多,支持更多loader,CommJS、AMD、ES6语法都能兼容,有更多的插件
    • 代码分割,treeshakiing,sourceMap
  • rollup
    • ES Modules打包器,更加小巧,支持treeSaking,代码更加简洁
    • 不支持HMR,不支持Commjs,因此很多第三方模块不能使用,加载其他资源也不能使用。

webpack打包构建流程

  • 初始化Compiler:
    • 根据生产环境或者开发环境得到相应的webpack配置文件config
    • 初始化一个Compiler对象,传入相应的配置。
  • compile开始编译:
    • 调用Compiler对象的run方法开始执行编译,触发compile()构建一个compilation对象
  • make 编译模块
    • 确定入口:根据配置中的entry找到所有的入口文件
    • 从入口文件出发,调用所有配置的loader对模块进行编译,再找出模块依赖的模块,不断递归直到所有模块被loader处理然后加载出来
  • build-module完成模板编译:
    • 使用loader编译完所有的模块以后,得到了每个模块的最终内容以及他们的依赖关系
  • seal输出资源:
    • 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,把每个Chunk根据依赖关系整合成相应的文件,加入输出列表中。(conpilation的最后一个钩子函数)
  • emit输出完成
    • 在确定好输出内容以后,根据配置确定输出的路径和文件名,通过fs模块,把文件内容写入到文件系统中。 emit钩子是修改文件的最终机会。

webpack打包构建流程的简单实现

代码实现

  • 打包指令:执行npm run build:"build": "node ./script/build.js"开始打包,会寻找到build.js文件开始运行。
  • 初始化Compiler,执行run方法开始编译:
    • build.js文件中,引入打包配置config,创建一个Compiler对象,传入webpack配置,然后执行run方法开始打包。
    • run方法中执行一系列钩子函数,模拟生成bundle的过程
const config = require('../config/webpack.config')

//创建compiler对象,执行webpack函数并且传入配置
const compiler = myWebpack(config)
function myWebpack(config){
    return new Compiler(config)
}
//执行compiler的run方法开始打包
compiler.run();
  • 创建一个Compiler类,包含run方法。执行一系列的钩子函数。
    • run方法需要读取入口文件,buile方法构建入口文件的依赖信息
    • 需要从入口文件开始不断递归依赖的依赖,不断build(),直到得到所有文件的依赖信息
    • 由依赖生成依赖关系图
    • generate方法生成输出资源
  • 定义几个简单的钩子函数:
    • Compiler类的build方法,将文件解析成抽象语法树ast,从ast中获取依赖,并且transform成可以进行编译的js代码(从而在generate这一步中可以拼接字符串成能够运行的js文件)
    • Compiler类中的generate其实就是webpack中emit输出内容的过程,根据配置输出的文件名文件地址,通过fs写入文件系统中。
class Compiler {
    constructor(options = {}) {
        //将options放在this中,就可以通过this访问options
        this.options = options;
        //所有依赖容器
        this.modules = [];

    }
    //compiler中的钩子函数
    //run()启动webpack打包
    run() {
        //读取入口文件
        const filePath = this.options.entry
        //第一次构建,添加入口文件信息
        const fileInfo = this.build(filePath);
        this.modules.push(fileInfo);

        //遍历所有依赖,不断递归里面依赖的依赖
        this.modules.forEach((fileInfo)=>{
            //取出当前文件的所有依赖
            const deps = fileInfo.deps
            //遍历
            for(const relativePath in deps){
                const absolutePath = deps[relativePath];
                const fileInfo = this.build(absolutePath);
                this.modules.push(fileInfo);
            }
        })
        // console.log(this.modules)
        //将依赖生成依赖关系图
        const depsGragh = this.modules.reduce((gragh,module)=>{
            return{
                ...gragh,
                [module.filePath]:{
                    code:module.code,
                    deps:module.deps
                }
            }
        },{})
        // console.log(depsGragh)
        this.generate(depsGragh);

    }
    //开始构建依赖
    build(filePath) {
        const ast = getAst(filePath);
        //获取所有依赖
        const deps = getDeps(ast, filePath);

        const code = getCode(ast);
        return{
            filePath,
            deps,
            code
        }
    }

    //生成输出资源
    generate(depsGragh){
        const bundle = `
        (function (depsGragh){
            function require(module){
                function localRequire(relativePath) {
                    return require(depsGragh[module].deps[relativePath])
                }
                var exports = {};
                (function(require,exports,code){
                    eval(code)
                })(localRequire,exports,depsGragh[module].code);
                return exports
            }
            //加载入口文件
            require('${this.options.entry}')
        })(${JSON.stringify(depsGragh)})
        `
        const filePath = path.resolve(this.options.output.path,this.options.output.filename)
        fs.writeFileSync(filePath,bundle,'utf-8')
    }
    
}
module.exports = Compiler;
//定义build方法中的parse过程:包括解析ast,transform过程生成code
const fs = require('fs');
const babelParse = require('@babel/parser')
const traverse = require("@babel/traverse").default;
const path = require('path')
const { transformFromAst } = require('@babel/core');
const parser = {
    //将文件解析成抽象语法树
    getAst(filePath) {
        const file = fs.readFileSync(filePath, 'utf-8');
        //将其解析成ast抽象语法树 
        const ast = babelParse.parse(file, {
            sourceType: 'module'
        })
        return ast;
    },
    //获取依赖
    getDeps(ast, filePath) {
        //获取文件夹路径
        const dirname = path.dirname(filePath);
        //定义存储依赖的容器
        const deps = {}
        //收集依赖
        traverse(ast, {
            ImportDeclaration({ node }) {
                const relativePath = node.source.value;
                const absolutePath = path.resolve(dirname, relativePath)
                //添加依赖
                deps[relativePath] = absolutePath;
            }
        })
        return deps
    },
    //将ast解析code
    getCode(ast) {
        const { code } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env']
        })
        return code;
    }

}

module.exports = parser