webpack整理

袁谭三
2023-12-01

传统模块化开发和问题

项目中有很多模块,一个模块就是一个js文件,项目中加载这些js的时候发送很多http请求,影响性能。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的网页</title>
    <!-- <style>
        body {
            background-color: grey;
        }
    </style> -->
    <!--  rel="stylesheet"必须写 -->
    <link href="./test.css" rel="stylesheet">
</head>
<body>
    <h1>我的网页</h1>
    <div id="app">
        <!-- <div>header</div>
        <div>content</div>
        <div>footer</div> -->
    </div>

    <!-- <script src="./test.js"></script> -->

    <script src="./header.js"></script>
    <script src="./content.js"></script>
    <script src="./footer.js"></script>
    <script src="./test.js"></script>
    <!-- 传统模块化开发 -->
    <!-- 多加载了js文件,发送了多次http请求 -->
    <!-- 变量的来源不是很确定  -->
    <!-- 加载顺序问题 -->

    <!-- 现在的模块化开发使用ES6的import export 使用模块 -->
    <!-- 但浏览器不能识别import export -->
    <!-- => webpack 解决以上问题 -->
</body>
</html>
body {
    background-color: rgb(212, 207, 207);
}
// alert('hello')
const app = document.getElementById('app')

const header = document.createElement('div')
header.innerText='header';
app.appendChild(header)

const content = document.createElement('div')
content.innerText='content';
app.appendChild(content)

const footer = document.createElement('div')
footer.innerText='footer';
app.appendChild(footer)

/***
 * 以上就是面向过程编程,所有的代码都写在一个js文件里面,
 * 对以后项目的维护,功能的扩展都不是很友好
 * 
 * => 面向对象编程
 * 
 */

new Header();
new Content();
new Footer();

//  header.js
function Header(){
    const header = document.createElement('div')
    header.innerText='header';
    app.appendChild(header)
}
// content.js
function Content(){
    const content = document.createElement('div')
    content.innerText='content';
    app.appendChild(content)
}
// footer.js
function Footer(){
    const footer = document.createElement('div')
    footer.innerText='footer';
    app.appendChild(footer) 
}

webpack输入输出

https://webpack.docschina.org/

  • 本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。
  • webpack刚开始默认只支持js文件的引入
  • 现在对于css,vue格式也可以引入,但需要做一些loader配置

入口(entry)->npx webpack

  1. npx webpack index.js
    告诉webpack模块打包的入口文件是index.js,然后开始打包

  2. 配置入口文件 webpack.config.js(默认配置名)

module.exports = {
  entry: './index.js',
};

可以在控制台直接执行npx webpack

  1. 自定义配置文件webpack.dev.config.js
    可以在控制台直接执行npx webpack --config ./webpack.dev.config.js

输出(output)

output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。

你可以通过在配置中指定一个 output 字段,来配置这些处理过程:

const path = require('path');
module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    //在任何模块文件内部,可以使用__dirname变量获取当前模块文件所在目录的完整绝对路径。
    filename: 'my-first-webpack.bundle.js',
    publicPath: 'http://localhost:8080/' //打入引入js文件的域名
  },
};
  • 为什么默认是main.js
const path = require('path');
module.exports = {
  entry: {
      main:'./index.js',
    // entry中key值为打包后的名字
    // 可以有多个入口
  },
};

多个入口的配置

const path = require('path');
module.exports = {
    entry: {
        main: './index.js',
        // entry中key值为打包后的名字
        // 可以有多个入口
        sub:'./index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
        // 输入两个文件main sub
    },
},
;

浏览器缓存

  • Performance
    https://webpack.docschina.org/configuration/performance/#root

  • 关闭性能优化的提示

module.exports = {
 //...
 performance:false,
 //关闭性能优化的相关内容,
//   发现npx weback 打包的一些报警信息没有了
};
  • 浏览器缓存
    每次打包生产相同的bundle.js文件,浏览如果开启了缓存就会从缓存中读取,不会更新最新的文件

  • output filename使用由生成的内容产生的 hash:
    webpack.config.js

module.exports = {
  //...
  output: {
    filename: '[name][contenthash:8].bundle.js',
  },
};

模式(mode)

直接使用mode

通过选择 development, production 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production。

module.exports = {
  mode: 'development',
};
  • development 模式下打包生成的bundle.js不会压缩

使用对应mode的配置文件

同过不同的文件配置

  • webpack.deb.js
  • webpack.prod.js
  • packahe.json
    {
        "scripts":{
            "build":"webpack --config ./build/webpack.prod.js",
            "dev":"webpack-dev-server --config ./build/webpack.dev.js",
            "dev:build":"webpack --config ./build/webpack.dev.js",
        }
    }
    

webpack merge

  • 安装webpack merge
  • webpack.base.js 公共配置部分放在这儿
const path = require('path');
module.exports = {
    entry: {
        main: './index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
},
;
  • 分别在开发和生产模式的文件中合并配置
const {merge}   = require('webpack-merge')

// 公共配置
const baseConfig = require('./webpack.base')
// 开发配置
const devConfig = module.exports = {
    "mode":"development",
    "devtool":"",
    "devServer":{},
    "plugins":[]
}

module.exports = merge(baseConfig,devConfig)

loader基本使用

  • webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。
  • loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块

属性

在 webpack 的配置中,loader 有两个属性:

  • test 属性,识别出哪些文件会被转换。
  • use 属性,定义出在进行转换时,应该使用哪个 loader。
const path = require('path');
module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
};

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 ‘.txt’ 的路径」时,在你对它打包之前,先 use(使用) raw-loader 转换一下。”

https://webpack.docschina.org/loaders/在这里安装loader

处理图片的loader:file-loader,url-loader

  1. file-loader
    https://v4.webpack.docschina.org/loaders/file-loader/
  • 处理图片格式的配置
module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[hash].[ext]',
                            // 生成的dist中图片名是原图片名(默认文件名是一个md5生成的)
                            // name配置项是配置打包生成的文件的名字,使用的是placeholder语法
                            // [name]表示的是原文件的名字;
                            // [hash]  表示的是这次打包的hash值   
                            // [ext]表示的是原文件的后缀;
                            outputPath: 'images',
                            // 生成的图片在dist/images/路径下
                        },
                    }
                ],
            },
        ],
    },
};
  • 使用
import testJpg from './resource/test.jpg';
console.log('name',testJpg)
const img = new Image();
img.src = testJpg;
const app = document.getElementsByTagName('div')[0]
app.appendChild(img);

执行npx打包命令后,打开dist中的首页可以看到图片显示

  1. url-loader返回base64

https://v4.webpack.docschina.org/loaders/url-loader/

A loader for webpack which transforms files into base64 URIs.

  • url-loader works like file-loader, but can return a DataURL if the file is smaller than a byte limit.
module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/i,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192
                            // 防止图片过大,限制大小
                        }
                    }
                ]
            }
        ]
    }
}
  1. 综合使用url-loader的limit
    • 图片很大->url loader->base64字符串很大->bundle.js体积会很大->index.html加载bundle.js时间就很长

    • 图片很小->url loader->图片base64直接设置在img的src属性上->不需要发送额外的http请求图片

    • 图片很小->file-loader->单独生成图片文件->index引入->bunde.js很小->页面加载块->多发送一次http请求

    • 图片很大->file-loader->bunde.js很小->图片单独请求

    module.exports = {
        module: {
            rules: [
                {
                    test: /\.(png|jpg|gif)$/i,
                    use: [
                        {
                            loader: 'url-loader',
                            options: {
                                name: 'dirname/[hash].[ext]',
                                // 生成的dist中图片名是原图片名(默认文件名是一个md生成的)
                                outputPath: 'images',
                                // 生成的图片在dist/images/路径下
                                limit: 8192
                                // 防止图片过大,限制大小
                            }
                        }
                    ]
                }
            ]
        }
    }
    

样式loader:style-loader,css-loader

  • 可以通过在js通过操作loader来处理简单的样式
  • 如果引入css文件,需要安装对应的loader
    1. style-loader
    2. css-loader
    • style-loader 与 css-loader 结合使用,且顺序不能更改
    • css-loader 的作用是把 css文件进行转码 ,而 style-loader 的作用是把转码后的css文件插入到相应的文件中去
    • 打包生成的样式会把他插入到index.html中的style标签内
      webpack.config.js
{
  module: {
    rules: [
      {
        test: /\.css$/,
        // use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 
        uer: ['style-loader','css-loader']
      },
    ];
  }
}

loader执行顺序

从右往左 从下往上

sass-loader

// webpack.config.js
module.exports = {
    ...
    module: {
        rules: [{
            test: /\.scss$/,
            use: [
                "style-loader", // 将 JS 字符串生成为 style 节点
                "css-loader", // 将 CSS 转化成 CommonJS 模块
                "sass-loader" // 将 Sass 编译成 CSS,默认使用 Node Sass
            ]
        }]
    }
};
  • importLoaders
    选项允许你配置在 css-loader 之前有多少 loader 应用于@imported 资源。
// webpack.config.js
{
  test: /\.scss/,
  use: [
    { loader: 'style-loader'},
    { loader: 'css-loader',
      options: {
            importLoaders: 2 //如果css中import scss文件 应用2个层级的loader
        },
    },
    { loader: 'postcss-loader' },
    { loader: 'sass-loader'}
  ]
}

postcss-loader+autoprefix配置样式前缀

https://v4.webpack.docschina.org/loaders/postcss-loader/#sourcemap

  • webpack.config.js设置loader
{
  test: /\.css/,
  use: [
    { loader: 'style-loader', options: { sourceMap: true } },
    { loader: 'css-loader', options: { sourceMap: true } },
    { loader: 'postcss-loader', options: { sourceMap: true } },
    { loader: 'sass-loader', options: { sourceMap: true } }
  ]
}
  1. webpack.config.js加载autoprefixer
    {
    test: /\.css$/,
    use: [
            {
            loader: 'postcss-loader',
            options: {
                    plugins: () => [require('autoprefixer')({
                        'browsers': ['> 1%', 'last 2 versions']
                    })],
                }
            },
        ]
    }
    

or

  1. postcss.config.js加载autoprefixer
    module.exports = ({ file, options, env }) => ({
    //   parser: file.extname === '.sss' ? 'sugarss' : false,
    plugins: {
        // require('autoprefixer')({...options}),
        // require('autoprefixer')({
        //     // 也可以在这设置目标浏览器
        //     browsers : ['last 100 versions'] 
        // })
        require('autoprefixer')({
            'browsers': ['> 1%', 'last 2 versions']
        })
    }
    })
    
  2. 设置目标浏览器范围
    https://github.com/browserslist/browserslist
    All tools will find target browsers automatically, when you add the following to
    • package.json:
      "browserslist":[
          "> %1",
          "last 2 versions"
      ]
      
    • Or in .browserslistrc config:

CSS模块化

在讲如何实现CSS模块化之前先讲讲要利用模块化来解决什么问题:

  1. 样式私有化
  2. 避免被其他样式文件污染
  3. 可复用

https://github.com/css-modules/css-modules

https://webpack.docschina.org/loaders/css-loader/#modules

{
    test: /\.css$/i,
    loader: "css-loader",
    options: {
        modules: true,
    },
}
  • 模块化原理:给当前css的选择器加上一个后缀
/* components/Button.css */
.normal { /* normal 相关的所有样式 */ }
.disabled { /* disabled 相关的所有样式 */ }
/* components/Button.js */
import styles from './Button.css';
console.log(styles);
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

生成的 HTML 是

<button class="button--normal-abc53"> Processing... </button>

打印结果

Object {
  normal: 'button--normal-abc546',
  disabled: 'button--disabled-def884',
}

CSS Modules 对 CSS 中的 class 名都做了处理,使用对象来保存原 class 和混淆后 class 的对应关系。

plugins插件

https://v4.webpack.docschina.org/plugins/
https://webpack.js.org/awesome-webpack/

html-webpack-plugin

https://v4.webpack.docschina.org/plugins/html-webpack-plugin/

var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');
module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
//   插件都是实例化使用
  plugins: [new HtmlWebpackPlugin()]
};

这将会产生一个包含以下内容的文件 dist/index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>webpack App</title>
  </head>
  <body>
    <script src="index_bundle.js"></script>
  </body>
</html>
  • 之前我们自己手动创建dist/index.html,然后引入打包后的dist/bundle.js
  • 使用html-webpack-plugin插件,webpack打包后创建index.html.并自动引入bundle.js
  • 但webpack创建的这个htm文件,仅仅是引入了bundle.js,再没有其他内容
  • 所以我们要对插件配置其他内容
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin({
      template:'./src/index.html'
      //配置dist/html模板
  })]
};

webpack常用插件

  • CleanWebpackPlugin
    用于在打包前清理上一次项目生成的 bundle文件。默认情况下,此插件将删除webpack的Output.Path目录中的所有文件

devtool->sourceMap

https://v4.webpack.docschina.org/configuration/devtool/#devtool

  • devtool -> parame:string false
  • 选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。
    webpack.config.js
const path = require('path');
module.exports = {
    mode:'development',
    devtool:'none',
    entry: {
        main: './index.js',
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
    },
}

index.js

console.logg('hello')
// 这行代码webpack打包的时候不会报错,打开网页后浏览器报错,问题定位到dist/main.js
  1. devtool:‘none’,当网页报错的时候浏览器会把错误定位到dist/main.js文件中,打包速度最快
  2. devtool:‘eval-source-map’,浏览器会把错误定位到原始源代码文件中
  3. devtool:‘source-map’,原始源代码,生成源代码和打包后代码关系的map文件
  • 最佳实践
    1. 开发环境 cheap-module-eval-source-map eval-cheap-module-source-map
    2. 生产环境 cheap-module-source-map
  • cheap:提示问题在哪一行但不提示列
  • module:cheap不管第三方模块和loader,加上module则会提示
  • eval:不会生成源代码和打包后代码的映射文件,而是放在main.js中

webpack-dev-server

  • 每次修改了源代码,都要重新运行npx webpackor npm run build进行构建打包,刷新页面才能看到修改的页面
  • webpack-dev-server可以优化这个过程
  • webpack-dev-server服务器使用node+express编写的

https://v4.webpack.js.org/configuration/dev-server/

  1. install npm install webpack-dev-server --save-dev
  2. config webpack.config.js
    var path = require('path');
    
    module.exports = {
        //...
        devServer: {
            // contentBase: path.join(__dirname, 'dist'),
            // webpack5中contentBase已经失效了
            static: {
                directory: path.join(__dirname, 'dist'),
            },
            compress: true,//压缩
            port: 9000,
            open: true //告诉 dev-server 在 server 启动后打开浏览器。默认禁用。
            // proxy 可以处理跨域问题
        },
    };
    
  3. config package.json
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack",
        "dev" : "webpack-dev-server"
    },
    
  4. npm run dev

devServer.proxy

  • 如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。
  • 通常通过请求转发来解决跨域问题
  • 只适用于开发环境
    在 localhost:3000 上有后端服务的话,你可以这样启用代理:

webpack.config.js

module.exports = {
    //...
    devServer: {
        // 1.
        // proxy: {
        //     '/api': 'http://localhost:3000'
        // },
        // 2.
        // proxy: {
        //     '/api': {
        //         target: 'http://localhost:3000'
        //     }
        // }
        proxy: {
            '/api': {
                // 一旦devServer(5000)服务器接收到 /api/xxx 的请求,就会把请求转发到另一个服务器(3000)
                // 浏览器和服务器之间有跨域,但是服务器和服务器之间没有跨域
                target: 'http://thirdserver.com:3000',
                changeOrigin: true,
                pathRewrite: { 
                    '^/api': '' 
                    // 所有以/api开头的请求,把/api转换为空字符串
                    // 发送请求时,请求路径重写:将 /api/xxx --> /xxx (去掉/api
                },
                secure: false,
                //默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。
                // 如果你想要接受,修改配置如下:
            }
        }
    }
};

请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/users。

webpack VS webpack-dev-server 构建区别

"scripts":{
    "build":"webpack", // 打包后文件在dist/main.js可以找到
    "dev":"webpack-dev-server" //打包后文件在内存中
}

HotModuleReplacementPlugin热更新插件

  1. 认识HMR热更新
    • input输入框输入文字,修改源码后,网页自动刷新然后重新请求,状态丢失,
    • 修改源码网页自动刷新属于热部署不属于热更新
  2. 解决:HotModuleReplacementPlugin插件
    https://webpack.docschina.org/plugins/hot-module-replacement-plugin/
    https://webpack.js.org/guides/hot-module-replacement/#enabling-hmr
    :::warning
    HMR 绝对不能被用在生产环境。
    :::
const webpack = require('webpack');
// ........
module.exports = {
    plugins:[new webpack.HotModuleReplacementPlugin()]
    // webpack内置插件
}

https://stackoverflow.com/questions/69102254/webpack-options-has-an-unknown-property-hotonly-invalid-options-object-dev-s

The latest versions automatically apply HotModuleReplacementPlugin plugin when you set hot: true, so please check you don’t have HotModuleReplacementPlugin in your plugins if you have hot: true/hot: “only”. You will get a warning as " [webpack-dev-server] “hot: true” automatically applies HMR plugin, you don’t have to add it manually to your webpack configuration." if you have the preceding settings.

  • style-loader 自动配置了热更新,更改了css,样式自己就改变了,react、vue框架自身也配置了,如果需要自定义
if(module.hot){    //在你要监听的文件里面写
    console.log('hot')
    // './print.js'
    // Ignored an update to unaccepted module ./src/print.js -> ./src/index.js
    // accept第一个参数必须是当前依赖的模块路径
    module.hot.accept('./print.js',()=>{    // ./src值得是更新文件路径
        console.log('代码热更新,写自己的代码控制')   
        const oldSpan = document.getElementsByTagName('span')[0];
        oldSpan.innerText=`HRP-sp${print.a}`;  
    })
}

babel->ES6

https://www.babeljs.cn/docs/index.html

  • Babel 是一个 JavaScript 编译器

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:

  1. 语法转换
  2. 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js)
  3. 源码转换(codemods)
  • preset-env 使用
    https://www.babeljs.cn/setup#installation
    https://webpack.js.org/loaders/babel-loader/
    https://babel.docschina.org/docs/en/next/configuration/
  1. npm install -D babel-loader @babel/core @babel/preset-env

  2. config

    module: {
    rules: [
        {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,//排除第三方代码
        use: {
            loader: 'babel-loader',
            options: {
                presets: ['@babel/preset-env']
            }
         }
        }
      ]
    }
    
  3. babel.config.json
    https://babel.docschina.org/docs/en/next/usage/

Tree Shaking

  • Tree Shaking 只支持ES Moudle,不支持CommonJs
  • ES Moudle->静态引入,编译时引入 import export
  • CommonJS->动态引入,执行时引入 require module.exports
const flag = true;
if(flag){
    const {add} = require('./math')
}

if(flag){
    import {add} from ('./math')
    // 这种动态引入会报错
    // import and export may only appear at the top level 
}

Tree Shaking指的就是当我引入一个模块的时候,我不引入这个模块的所有代码,我只引入我需要的代码,这就需要借助webpack里面自带的Tree Shaking这个功能,帮助我们实现。Tree Shaking只支持ES Module(import…) 不支持require…

  • https://developer.mozilla.org/zh-CN/docs/Glossary/Tree_shaking

  • https://webpack.js.org/guides/tree-shaking/

  • 当在development模式下配置tree shaking时:

    mode:'development',
    optimization: {
        usedExports: true,
    },
    
    • 开发模式中为了调试方便未使用的代码并没有被删除,只是注释了未被引用的方法
  • 在production 模式下不用在webpack.config.js中配置.usedExports默认自动开启

  • 如果Tree Shaking不生效请检查source-map

side-effect-free(无副作用)

  • 除了返回值以外,影响到函数外部的任何东西(显示器、网络、文件、全局变量等)的效果都叫side effect

通过 package.json 的 “sideEffects” 属性,来实现这种方式。

{
  "name": "your-project",
  "sideEffects": false
}

如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack 它可以安全地删除未用到的 export。

因为Tree Shaking会删除掉我们未使用的export函数,因为polyfill都在window上定义的,没有export,所以会被删除掉.
配置side effect排除副作用的检查
:::tip
“side effect(副作用)” 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。
:::

{
  "name": "your-project",
  "sideEffects": ["@babeli/polyfill","*.css"]
}

代码分割

https://webpack.docschina.org/guides/code-splitting/

  • 代码一般分为业务逻辑代码和其他工具包代码,全部打包到main.js中,文件很大
  • 如果修改了业务逻辑代码重新打包的时候也会打包其他工具代码,浏览器会重新下载这个很大的main.js
    • src/index.js
    import _ form 'lodash'
    console.log('hello world!');//每次修改业务逻辑,webpack打包的时候也会将lodash一块重新打包到bundle.js
    const result = _.join(['test1','test2','test3'],'-');
    console.log('result',result)
    
  • 解决->代码分隔->配置多个入口

手动解决->配置多个入口和出口

  • 入口起点:使用 entry 配置手动地分离代码。
  • src/index,js
    console.log('hello world!');
    const result = _.join(['test1','test2','test3'],'-');
    console.log('result',result)
    
  • src/lodash.js
    import _ form 'lodash'
    window._= _
    
  • webpack.config.js
    module.exports = {
        entry:{
            main:'./src/index.js',
            lodash:'./src/lodash.js'
            //打包生成 main.js和lodash.js并引入到index.html中
        },
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: '[name].js',
        }
    },
    }
    

SplitChunksPlugin 插件解决

  • SplitChunksPlugin 去重和分离 chunk。
    https://webpack.docschina.org/plugins/split-chunks-plugin/

  • src/index.js

import _ form 'lodash'
console.log('hello world!');//每次修改业务逻辑,webpack打包的时候也会将lodash一块重新打包到bundle.js
const result = _.join(['test1','test2','test3'],'-');
console.log('result',result)
  • webpack配置SplitChunks
module.exports = {
  //...
  optimization: {
    splitChunks: {
        chunks:'all'
  },
};

import懒加载->代码分隔

https://webpack.docschina.org/guides/lazy-loading/#root

// Note that because a network request is involved, some indication
// of loading would need to be shown in a production-level site/app.
button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
    const print = module.default;
// 注意当调用 ES6 模块的 import() 方法(引入模块)时,必须指向模块的 .default 值,因为它才是 promise 被处理后返回的实际的 module 对象。
    print();
});
  • import懒加载代码分隔也是通过SplitChunksPlugin配置的.

splitChunks.chunks:This indicates which chunks will be selected for optimization. When a string is provided, valid values are all, async, and initial. Providing all can be particularly powerful, because it means that chunks can be shared even between async and non-async chunks.

moudle chunks bundle 的理解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtOQyPHA-1654528404640)(https://gitee.com/jingyu7/pic/raw/master/202201061256177.png)]

  • module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:
  • 我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。

SplitChunksPlugin的常见配置

  • splitChunks.chunks
    This configuration object represents the default behavior of the SplitChunksPlugin.
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',//默认只有异步代码进行代码分隔 all, async, and initial
      minSize: 20000, //如果引入的包>20000KB就进行代码分隔
      minRemainingSize: 0,//通过确保拆分后剩余的最小 chunk 体积
      minChunks: 1, //拆分前必须共享模块的最小 chunks 数。
    // 这个配置表示split前单个非按需导入的module的并行数的最低下限
    // 注:只对import或require直接引入的module有效。
    // 1. minChunks设置为n
    // 2. 假设有m个入口点,这m个入口点都直接引入了某个模块module(通过import或require直接或间接地引入了模块),也就是共享次数为m
    // 3. 当m至少等于n时,module才会被单独拆分成一个bundle

      maxAsyncRequests: 30,//按需加载时的最大并行请求数。
      maxInitialRequests: 30,//入口点的最大并行请求数
      enforceSizeThreshold: 50000,//强制执行拆分的体积阈值和其他限制
      cacheGroups: { //缓存组
    //   对于缓存组是一个对象,处了可以有上面的chunks、minSize、minChunks、maxAsyncRequests、maxInitialRequests、name外,还有其他的一些参数:
    // 如果不在缓存组中重新赋值,缓存组默认继承上面的选项,但是还有一些参数是必须在缓存组进行配置的
    // 缓存组的概念理解
    // 源代码中引入了lodash jquery 都分别分隔到了对应的js
    // 假如希望它两合并后分隔
    // 当分析到引入lodash时缓存到defaultVendors,分析到jquery满足规则也缓存起来
    // 当所有模块到分析结束后,再把defaultVendors组一起分隔
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/, //捕获模块规则
          priority: -10, //优先级
          reuseExistingChunk: true,
          //如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
          //假如源代码中不同文件中都引入lodash模块,
          //在第一次引入分析后被当前缓存组缓存,后面的此模块的引入分析复用之前缓存结果
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

CSS代码分割MiniCssExtractPlugin

本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载

  • 之前css样式文件被styleLoader处理打包后放进了main.js->css in js->最后放在head的style中
  • 一般是应用于生产环境
  • mini-css-extract-plugin
    https://webpack.docschina.org/plugins/mini-css-extract-plugin/
    webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [new MiniCssExtractPlugin()],
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
};

垫片Shimming 预置依赖ProvidePlugin

Automatically load modules instead of having to import or require them everywhere.

  • 可以用于全局配置插件,也可以用于老代码中jquery的兼容
  • 语法
new webpack.ProvidePlugin({
  identifier: 'module1',
  // ...
});
  • Usage: jQuery
    To automatically load jquery we can point both variables it exposes to the corresponding node module:
const path = require('path');
const webpack = require('webpack');
 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
  plugins: [
    new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
        // webpack为我们提供了ProvidePlugin的插件,上面配置的意思是,当我们在模块中遇到‘$’就会去自动的引入jquery
    });
  ],
 };

Then in any of our source code:

// in a module
$('#item'); // <= works
jQuery('#item'); // <= also works
// $ is automatically set to the exports of module "jquery"

版本4和版本5的区别

性能优化

有两种优化方向

  1. 打包构建的速度
  2. 产出代码体积

babel-loader-exclude,cacheDirectory

  • 主要是提高js打包构建的速度,第一个是缓存第二个是exclude
  • css loader没有构建缓存
  1. exclude,include

    exclude:path.resolve(__dirname,'node_modules')
    
    • 问题

      • 在webpack中配置babel-loader时,为什么要排除node_modules这个文件夹呢?
      • 这个文件夹里js模块万一也有类似箭头函数的ES6语法呢?
    • 因为如果你不排除这个文件夹的话,你在自己的代码中引用的其他工具库的代码,也会被babel读取并重新编译,JavaScript代码的编译比较慢,很多情况下,你自己的业务代码只占了10%,但引用的其他工具库的代码,编译完以后可能占到80~90%,假如编译你自己的业务代码需要10秒钟,那整个项目的代码,就需要100秒,你会发现速度非常慢,不利于提高开发效率。另一方面,现在发布到npm上面的工具库,一般都包含了源代码和编译以后的代码,同时默认引用的是编译以后的代码,这样相当于你把编译完以后的代码又编译了一遍,实际上并没有任何效果,反而浪费了时间。所以我们在配置的时候,一般都将node_modules当中的工具库代码,排除在babel的编译范围之外。

    • 虽然 npm 中的 package 没有明确规范需要编译成 es5/es3的语法,但是目前事实就是绝大部分公开的 package 都是编译到这个规范的,所以已经是一个事实标准了。

    • 如果node_modules里的某些包有es6语法怎么办?

    答:通过正则指定node_modules中的某些包不排除

  2. babel 缓存:在babel-loader中的options中开启 cacheDirectory:true
    把已经编译好的兼容性代码就放在缓存中,下次编译的时候如果缓存中有就直接使用编译后的代码,
    只对js生效

    module:{
        rules:[
            test: /\,js$/,
            options:{
                cacheDirectiry:true
            },
            inclued:srcPath,
            exclude:path.resolve(__dirname,'node_modules')
        ]
    }
    

webpack.IgnorePlugin对moment的处理

webpack 中的 IgnorePlugin 在打包时忽略本地化内容,如引入了一个插件,只用到了中文语言包,打包的时候把非中文语言包排除掉

在项目中经常用到moment库处理时间,有没有发现引入moment之后,项目build完的文件大小明显大了不少,下面就来分析一下原因, 以及如何做优化。

//
import moment from 'moment'
// 引入中文
import 'moment/locale/zh-cn'
// 设置中文
moment.locale('zh-cn');
let momentStr = moment().subtract(10, 'days').calendar();
console.log('现在时间:', momentStr);

网上搜了一波才知道, 打包时把所有的locale都打包进去了, 初步解决方案是用webpack.IgnorePlugin来处理。
IgnorePlugin又是做了什么操作?

  • IgnorePlugin 原理分析
    IgnorePlugin的原理是会移除moment的所有本地文件,因为我们很多时候在开发中根本不会使用到。 这个插件的使用方式如下:
...
plugins: [
    new webpack.IgnorePlugin(/^./locale$/, /moment$/) // 配置忽略规则
    // 忽略moment库中的locale文件夹啊
]

那么你可能会有疑问,所有本地文件都被移除了,但我想要用其中的一个怎么办。不用担心,你依然可以在代码中这样使用:

const moment = require('moment');
require('moment/locale/ja');//如果需要请单独引用
moment.locale('ja');

https://github.com/zhanzizhen/how-to-optimize-momentjs-with-webpack/blob/master/README.md

noParse配置

一般使用.min.js结尾的文件,都是已经经过模块化处理的,那么这个时候就没必要在进行loder或者webpack分析了,noParer的字面意思也是不再解析。

module: {
    noParse: /jquery/, // 不解析模块中的依赖关系 提高打包效率
    rules: []
}
  • ignorePlugin和noParse的对比

    1. IgnorePlugin 直接就将符合匹配条件的模块,不再进行引入,代码中没有。
    2. noParse 该引入还是会引入,只是不参与loader或webpack的解析及打包。
  • noParse不会改变打包体积,只会加快构建速度

happPack多进程打包

由于 JavaScript 是单线程模型,要想发挥多核 CPU 的能力,只能通过多进程去实现,而无法通过多线程实现。
由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的任务要一个一个进行操作。而 Happypack 的作用就是将文件解析任务分解成多个子进程并发执行。子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构件速度

watch 和 watchOptions

Webpack 可以监听文件变化,当它们修改后会重新编译。
启用 Watch 模式。这意味着在初始构建之后,webpack 将继续监听任何已解析文件的更改。

module.exports = {
  //...
  watch: true,
};

webpack-dev-server 和 webpack-dev-middleware 里 Watch 模式默认开启。

  • watchOptions一组用来定制 watch 模式的选项:
module.exports = {
  //...
  watchOptions: {
    ignored: /node_modules/,
    //监听大量文件会导致大量的 CPU 或内存占用。可以使用正则排除像 node_modules 如此庞大的文件夹:
    aggregateTimeout: 200,//批量更新
    // 当第一个文件更改,会在重新构建前增加延迟。这个选项允许 webpack 将这段时间内进行的任何其他更改都聚合到一次重新构建里。以毫秒为单位:
  },
};

DllPlugin

DllPlugin,DllReferencePlugin就是事先把常用但又构建时间长的代码提前打包好(例如 react、react-dom),取个名字叫 dll。后面再打包的时候就跳过原来的未打包代码,直接用 dll。这样一来,构建时间就会缩短,提高 webpack 打包速度。

  • 就是个缓存吗!拿空间换时间。

  • autodll-webpack-plugin 也是类式插件, 之前被 vue-cli 使用

  • webpack 4 有着比 dll 更好的打包性能。所以已经不使用autodll-webpack-plugin了

当我后续找到 autodll-webpack-plugin,并发现 dll 已经被抛弃时,其实还是有些失望,觉得自己的之前的努力都白费了,不由自主产生 学不动 的想法。但是当我仔细想了一下 dll 的原理,发现也就是那么一会事儿,拿空间换时间,只不过配置复杂了一些。
所以这也提醒我们,学习新知识的时候,不要专注于流程的配置和调参。因为流程终会简化,参数(API)终会升级。要抓大放小,把精力放在最核心的内容上,因为核心的思想是最不容易过时的。

 类似资料: