前端工程化:WebPack

1,694 阅读5分钟

个人在学习或面试过程中,涉及和遇见的一些知识,进行简单归纳总结,由浅及深,学以致用

注:内容引自博客、网络、MDN或官方文档,先总结,再持续更新,融入自己的理解和实践。

WebPack

1、概念

官网:本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

2、作用

  1. 模块打包
  2. 编译兼容(loader)
  3. 功能扩展(plugin)

构建流程:

  1. 读取webpack的配置参数;

  2. 启动webpack,创建Compiler对象并开始解析项目;

  3. 从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;

  4. 对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;

  5. 整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

2、webpack如何实现代码分离

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 CommonsChunkPlugin 去重和分离 chunk
  • 动态导入:通过模块的内联函数调用来分离代码。

3、打包后的js、css文件

在文件名上加上通过对文件内容进行hash运算,生成一个文件的唯一hash字符串(如果文件修改则hash号会发生变化),替换文件名,生成一个带版本号的文件名 ,从而达到更改引用URL的目的

4、source map

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。

5、热更新原理

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS (webpack-dev-server) 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loadervue-loader 都是借助这些 API 实现 HMR。

6、文件指纹

文件指纹是打包后输出的文件名的后缀。

  • Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
  • Chunkhash:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash
  • Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变

JS的文件指纹设置

设置 outputfilename,用 chunkhash

CSS的文件指纹设置

设置 MiniCssExtractPluginfilename,使用 contenthash

图片的文件指纹设置

设置file-loadername,使用hash

const path = require('path');

module.exports = {    
  entry: './src/index.js',    
  output: {        
    filename: '[name][chunkhash:8].js', // js文件指纹     
    path:path.resolve(__dirname, 'dist')    
  },
  plugins:[
      new MiniCssExtractPlugin({		// css文件指纹
          filename: `[name][contenthash:8].css`
      })
  ]
  module:{        // 图片文件指纹
    rules:[{            
      test:/.(png|svg|jpg|gif)$/,            
      use:[{                
        loader:'file-loader',                
        options:{                    
          name:'img/[name][hash:8].[ext]'                
        }            
      }]        
    }]    
  }
}

7、如何优化Webpack 的构建速度

  1. 使用高版本WebpackNode.js

  2. 多进程/多实例构建thread-loader

  3. 压缩代码

    • 多进程并行压缩
      • webpack-paralle-uglify-plugin
      • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
      • terser-webpack-plugin 开启 parallel 参数
    • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
  4. 图片压缩

    • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
    • 配置 image-webpack-loader
  5. 缩小打包作用域

    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    • resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    • IgnorePlugin (完全排除模块)
    • 合理使用alias
  6. 提取页面公共资源

    • 基础包分离:

      • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
      • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
  7. DLL

    • 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。
    • HashedModuleIdsPlugin 可以解决模块数字id问题
  8. 充分利用缓存提升二次构建速度

    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin
  9. Tree shaking

    • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
    • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
    • 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
      • purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
  10. Scope hoisting

    • 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
    • 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
  11. 动态Polyfill

    • 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。 (部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)

8、Webpack的plugin和loader

1)loader

loaderwebpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

即文件加载器:用来预处理文件。操作的是文件,将文件A通过loader转换成文件B,是一个单纯的文件转化过程。

  • 处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,最后一个loader最先执行,第一个loader最后执行。

  • 第一个执行的loader接受源文件作为参数内容,其他的loader接受前一个loader的返回值作为自己的参数,最后一个执行的loader会返回此模块的JavaScript源码。

  • loader本质上是一个ESM模块,它导出一个函数,在函数中对打包资源进行转换

  • loader必须返回一段js代码,这样eval才能执行,否则会报错,多个loader,保证最后面一个返回js代码就行。

2)plugins

plugin即插件:插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

即本身是一个扩展器,用来增强功能。针对的是在loader结束之后,webpack打包的整个过程,他并不直接操作文件,而是基于事件机制工作,监听webpack打包过程中的某些节点,执行广泛的任务。

特性:

  • 是一个独立的模块
  • 模块对外暴露一个 js 函数
  • 函数的原型 (prototype) 上定义了一个注入 compiler对象的 apply方法
  • apply 函数中需要有通过 compiler 对象挂载的 webpack 事件钩子,钩子的回调中能拿到当前编译的 compilation 对象,如果是异步编译插件的话可以拿到回调 callback
  • 完成自定义子编译流程并处理 complition 对象的内部数据
  • 如果异步编译插件的话,数据处理完成后执行 callback回调
  • loader 运行在打包文件之前
  • plugins 在整个编译周期都起作用

3)常用loaderplugin

  • style-loader :把写入js的样式代码插入到 index.html文件中
  • css-loader :仅仅只是把样式代码写入 js
  • babel-loader:打包js
  • file-loader :将要加载的文件复制到指定目录;

9、Babel

Babel 是一个工具链,主要用于在当前和旧的浏览器或环境中,将 ECMAScript 2015+ 代码转换为 JavaScript 向后兼容版本的代码。以下是 Babel 可以做的主要事情:

  • 转换语法
  • Polyfill 目标环境中缺少的功能(通过如 core-js 的第三方 polyfill
  • 源代码转换(codemods)

webpack loader 的概念,因此就出现了 babel-loader

10、css-loader 和 style-loader

如果在JS中导入了css,那么就需要使用 css-loader 来识别这个模块,通过特定的语法规则进行转换内容最后导出

css-loader会处理 import / require() / @import / url 引入的内容。

style-loader 是通过一个JS脚本创建一个style标签,里面包含一些样式。style-loader是不能单独使用的,应为它并不负责解析 css 之前的依赖关系,每个loader的功能都是单一的,各自拆分独立。

即:css-loader只是帮我们解析了css文件里面的css代码,想要应用样式我们要把解析完的css代码拿出来加入到style标签中。style-loader就是帮我们直接将css-loader解析后的内容挂载到html页面当中

11、Vue项目中webpack配置vue.config.js

// vue.config.js
const path = require('path');
const resolve = (dir) => path.join(__dirname, dir);

const CompressionWebpackPlugin = require("compression-webpack-plugin"); // 开启gzip压缩, 按需引用
const productionGzipExtensions = /.(js|css|json|txt|html|ico|svg)(?.*)?$/i; // 开启gzip压缩, 按需写入
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; // 打包分析

const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV); // 生产环境判断

module.exports = {
    publicPath: process.env.NODE_ENV === 'production' ? '/site/vue-demo/' : '/',  // 公共路径
    indexPath: 'index.html', // 相对于打包路径index.html的路径
    outputDir: process.env.outputDir || 'dist', // 'dist', 生产环境构建文件的目录
    assetsDir: 'static', // 相对于outputDir的静态资源(js、css、img、fonts)目录
    lintOnSave: process.env.NODE_ENV === 'development', // 是否在开发环境下通过 eslint-loader 在每次保存时 lint 代码
    runtimeCompiler: true, // 是否使用包含运行时编译器的 Vue 构建版本
    productionSourceMap: !IS_PROD, // 生产环境的 source map
    parallel: require("os").cpus().length > 1, // 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。
    pwa: {}, // 向 PWA 插件传递选项。

    devServer: {
        overlay: { // 让浏览器 overlay 同时显示警告和错误
            warnings: true,
            errors: true
        },
        host: "localhost",
        port: 8080, // 端口号
        https: false, // https:{type:Boolean}
        open: false, //配置自动启动浏览器
        hotOnly: true, // 热更新
        // proxy: 'http://localhost:8080'   // 配置跨域处理,只有一个代理
        proxy: { //配置多个跨域
            "/api": {
                target: "http://172.11.11.11:7071",
                changeOrigin: true,
                // ws: true,//websocket支持
                secure: false,
                pathRewrite: {
                    "^/api": "/"
                }
            },
            "/api2": {
                target: "http://172.12.12.12:2018",
                changeOrigin: true,
                //ws: true,//websocket支持
                secure: false,
                pathRewrite: {
                    "^/api2": "/"
                }
            },
        }
    },

    configureWebpack: config => {
        // 开启 gzip 压缩
        // 需要 npm i -D compression-webpack-plugin
        const plugins = [];
        if (IS_PROD) {
            plugins.push(
                new CompressionWebpackPlugin({
                    filename: "[path].gz[query]",
                    algorithm: "gzip",
                    test: productionGzipExtensions,
                    threshold: 10240,
                    minRatio: 0.8
                })
            );
        }
        config.plugins = [...config.plugins, ...plugins];
    },

    css: {
        extract: IS_PROD,
        requireModuleExtension: false,// 去掉文件名中的 .module
        loaderOptions: {
            // 给 less-loader 传递 Less.js 相关选项
            less: {
                // `globalVars` 定义全局对象,可加入全局变量
                globalVars: {
                    primary: '#333'
                }
            }
        }
    },

    chainWebpack: config => {
        config.resolve.symlinks(true); // 修复热更新失效
        config.resolve.alias // 添加别名
            .set('@', resolve('src'))
            .set('@assets', resolve('src/assets'))
            .set('@components', resolve('src/components'))
            .set('@views', resolve('src/views'))
            .set('@store', resolve('src/store'));

        
        // 如果使用多页面打包,使用vue inspect --plugins查看html是否在结果数组中
        config.plugin("html").tap(args => {
            // 修复 Lazy loading routes Error
            args[0].chunksSortMode = "none";
            return args;
        });

        config.plugins.delete('prefetch')

        config.module
            .rule('svg')
            .exclude.add(resolve('src/icons'))
            .end()
        
        config.module
            .rule('icons')
            .test(/\.svg$/)
            .include.add(resolve('src/icons'))
            .end()
            .use('svg-sprite-loader')
            .loader('svg-sprite-loader')
            .options({
                symbolId: 'icon-[name]'
            })
            .end()
        
        // 压缩图片
        // 需要 npm i -D image-webpack-loader
        config.module
            .rule("images")
            .use("image-webpack-loader")
            .loader("image-webpack-loader")
            .options({
                mozjpeg: { progressive: true, quality: 65 },
                optipng: { enabled: false },
                pngquant: { quality: [0.65, 0.9], speed: 4 },
                gifsicle: { interlaced: false },
                webp: { quality: 75 }
            });

        // 打包分析
        // 打包之后自动生成一个名叫report.html文件(可忽视)
        if (IS_PROD) {
            config.plugin("webpack-report").use(BundleAnalyzerPlugin, [
                {
                    analyzerMode: "static"
                }
            ]);
        }
    },
}