webpack全方位由浅到深讲解,做到简历上真正所谓的“熟悉”(系列一)

3,695 阅读4分钟

b840c72e59dd5b880ed68bb8d10b6c62.jpeg

注意:文章基于“webpack4”讲解

1.为什么需要构建工具

    1. 转换 ES6 语法
    1. 转换 JSX
    1. CSS 前缀补全/预处理器
    1. 压缩混淆
    1. 图片压缩

...欢迎补充

2.初识webpack

  • 1.默认配置文件:webpack.config.js。 可以通过 webpack --config 指定配置文件

  • 2.配置组成

    module.exports = {
        //打包的入口文件
        entry: './src/index.js',
        // 打包的输出
        output: './dist/main.js,
        // 环境
        mode: 'production',
        module: {
            // Loader 配置
            rules: [
                {
                    test:/\.txt$/,
                    use: 'raw-loader'
                }
            ]
        },
        plugins: [
            // 插件配置
            new HtmlWebpackPlugin({
                template: './src/index.html'
            })
        ]
    }
    

    从webpack4后,webpack将内核和cli分离,所以需要安装webpack和webpack-cli

3. 通过 npm script 运行 webpack

原理: 模块局部安装会在 node_modules/.bin 目录创建软连接, package.json会默认读取./bin目录下的命名

"scripts": {
   "build": "webpack"
 },

4. entry 用法

指定打包入口

    1. 单入口: entry是一个字符串(单页应用)
    module.exports = {
       entry: './src/index.js'
    }
    
    1. 多入口: entry是一个对象(多页应用)
    module.exports = {
       entry: {
            index: './src/index.js',  
            app: './src/app.js'
       }
    }
    

5.output

告诉 webpack 如何将编译后的文件输出到磁盘

  • 1.单入口配置
    module.exports = {
        entry: './src/index.js',
        output: {
            filename: 'bundle.js',
            path: __dirname+'/dist'
        }
    }
    
  • 2.多入口配置
    module.exports = {
        entry: {
             index: './src/index.js',  
             app: './src/app.js'
        },
        output: {
            filename: '[name].js',
            path: __dirname+'/dist'
        }
    }
    
    通过占位符确保文件名称的唯一

6.loaders

webpack默认只支持JS 和 JSON 两种文件类型, 通过Loaders去支持其它文件类型并且把它们转化成有效的模块, 并且可以添加到依赖图中。本身是一个函数,接收源文件作为参数, 返回转换的结果。

常见的Loaders

  • 1.babel-loader: 转换ES6、ES7等JS新特性语法
  • 2.css-loader: 支持css文件的加载和解析
  • 3.less-loader: 将less文件转换成css
  • 4.ts-loader: 将TS转换成JS
  • 5.file-loader: 进行图片、字体等的打包
  • 6.raw-loader: 将文件以字符串的形式导入
  • 7.thread-loader: 多进程打包JS和CSS

用法

module.exports = {
    output: {
        filename: 'bundle.js'
    },
    module: {
        rules: [
           {
               test: /\.txt$/,   // test指定匹配规则
               use: 'raw-loader' // use指定使用的loader名称
           } 
        ]
    }
}

7.plugins

插件用于 bundle 文件的优化,资源管理和环境变量注入 ,作用于整个构建过程

常见的Plugins

  • splitchunksplugin: 将chunks相同的模块代码提取成公共js
  • CleanWebpackPlugin: 清理构建目录
  • CopyWebpackPlugin: 将文件或者文件夹拷贝到构建的输出目录
  • HtmlWebpackPlugin: 创建html文件去承载输出的bundle
  • UglifyjsWebpackPlugin: 压缩JS
  • ZipWebpackPlugin: 将打包出的资源生成一个zip包
  • MiniCssExtractPlugin: 将CSS从bundle文件里提取成一个独立的CSS文件
  • OptimizeCssAssetsWebpackPlugin:CSS文件压缩
  • autoprefixer: PostCSS自动补齐CSS3前缀

用法

const path = require('path')
module.exports = {
    output: {
        filename: 'bundle.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html' // 放到plugins数组里
        })
    ]
}

8.mode

mode用来指定当前的构建环境是:production、development 还是 none, 设置mode可以使用 webpack内置的函数, 默认值为 production

mode的内置函数功能

    1. development: 设置 process.env.NODE_ENV 的值为 development, 开启NamedChunksPlugin 和 NamedModulesPlugin.(模块热更新相关,会在控制台打印出哪个模块更改和对应路径)
    1. production: 设置 process.env.NODE_ENV 的值为 production, 开启FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 TerserPlugin
    1. none: 不开启任何优化选项

9.解析ES6

使用 babel-loader。 babel的配置文件是: .babelrc

modules: {
    rules: [
        {
            test: /\.js$/,
            use: 'babel-loader'
        }
    ]
}
.babelrc文件
{
    "presets": [
        "@babel/preset-env",
    ] 
}

10.解析css

    1. css-loader用于加载.css文件, 并且转换成 commonjs对象
    1. style-loader将样式通过 标签插入到head中
{
    test:/\.css$/,
    use: [
        'style-loader',
        'css-loader'
    ]
}

loader是链式调用,执行顺序是从右到左。

11.解析图片和字体

    1. file-loader 用于处理文件,也可用于处理字体
{
    test: /\.(png|svg|jpg|gif)$/,
    use: [
        'file-loader'
    ]
}

{
    test: /\.(woff|woff2|eot|tff|otf)$/,
    use: [
        'file-loader'
    ]
}
    1. url-loader也可以处理图片和字体, 可以设置较小资源自动base64
{
    test: /\.(png|svg|jpg|gif)$/,
    use: [
       {
            loader: 'url-loader',
            options: {
                limit: 10240 //单位字节
            }
       }
    ] 
}

12. webpack中的文件监听

开启监听模式,有两种方式

    1. 启动webpack命令时,带上 --watch 参数
    1. 在配置 webpack.config.js中设置watch:true
module.export = {
    // 默认false,也就是不开启
    watch:true,
    // 只有开启监听模式时,watchOptions才有意义
    watchOptions: {
        // 默认为空, 不监听的文件 或 文件夹, 支持正则匹配
        ignored: /node_modules/,
        // 监听到变化发生后会等300ms再去执行, 默认300ms
        aggregateTimeout: 300,
        // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,         // 默认每秒问1次
        poll: 1000
    }
}

13. 热更新: webpack-dev-server

    1. 不刷新浏览器
    1. 不输出文件,而是放在内存中

使用HotModuleReplacementPlugin插件

// package.json
{
    scripts: {
        "dev": "webpack-dev-server --open"
    }
}

// webpack.config.js
module.export = {
    plugins: [
        new webpack.HotModuleReplacementPlugin() // 配置了 hot: true 会自动引入这个 plugin
    ],
    devServer: {
        contentBase: './dist',
        hot: true  // 会自动引入这个 HotModuleReplacementPlugin
    }
}

还有一种热更新: 使用 webpack-dev-middleware, 将webpack输出的文件传输给服务器,适用于灵活的定制场景。

热更新的原理分析

    1. Webpack Compile: 将JS编译成 Bundle
    1. HMR Server: 将热更新的文件输出 HMR Runtime
    1. Bundle server: 提供文件在浏览器的访问
    1. HMR Runtime: 会被注入到浏览器,更新文件的变化
    1. bundle.js: 构建输出的文件

热更新有最核心的是 HMR Server 和 HMR runtime。HMR Server 是服务端,用来将变化的 js 模块通过 websocket 的消息通知给浏览器端。HMR Runtime是浏览器端,用于接受 HMR Server 传递的模块数据,浏览器端可以看到 .hot-update.json 的文件过来。

  • HotModuleReplacementPlugin是做什么用的?

    • webpack 构建出来的 bundle.js 本身是不具备热更新的能力的,HotModuleReplacementPlugin 的作用就是将 HMR runtime 注入到 bundle.js,使得bundle.js可以和HMR server建立websocket的通信连接

14.文件指纹

    1. Hash: 和整个项目的构建相关,只要项目文件有修改, 整个项目构建的hash值就会修改
    1. Chunkhash: 和webpack打包的chunk有关,不同的entry会生成不同的chunkhash值
    filename: '[name][chunkhash:8].js'
    
    1. Contenthash: 根据文件内容来定义hash, 文件内容不变,则contenthash不变(css文件一般设置这个hash)
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name][contenthash:8].css'
        })
    ]
    

    图片的文件指纹设置

    设置file-loader的name,使用[hash]

      1. [ext]: 资源后缀名
      1. [name]: 文件名称
      1. [path]: 文件的相对路径
      1. [folder]: 文件所在的文件夹
      1. [contenthash]: 文件的内容hash,默认是md5生成
      1. [hash]: 文件的内容hash,默认是md5生成
      1. [emoji]: 一个随机的指代文件内容的emoji
    {
        test: /\.(png|svg|jpg|gif)$/,
        use: [{
            loader: 'file-loader',
            options: {
                name: 'img/[name][hash:8].[ext]'
            }
        }]
    }
    

15.代码压缩

    1. JS文件压缩: webpack内置了uglifyjs-webpack-plugin
    1. CSS文件压缩: 使用optimize-css-assets-webpack-plugin,同时使用 cssnano
plugins: [
    new OptimizeCSSAssetsPlugin({
        assetNameRegExp: /\.css$/g,
        cssProcessor: require('cssnano')
    })
]
    1. HTML文件压缩:使用 html-webpack-plugin, 设置压缩参数
new HtmlWebpackPlugin({
    template: path.join(__dirname,'src/search.html'),
    filename: 'search.html',
    chunks: ['search'],
    inject: true,
    minify: {
        html5: true,
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true,
        removeComments: false
    }
})

16. 自动清除构建目录

避免构建前每次都需要手动删除 dist

使用 clean-webpack-plugin, 默认会删除output指定的输出目录

plugins:[
    new CleanWebpackPlugin()
]

17.PostCss插件autoprefixer自动补齐CSS3前缀

配置:

{
    test: /\.css$/,
    use: [
        MiniCssExtractPlugin.loader,
        'css-loader',
        'postcss-loader'
    ]
},

// .browserslistrc
# Browsers that we support

last 2 version 
>1%

//postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

loader顺序:less-loader -> postcss-loader -> css-loader -> style-loader 或者 MiniCssExtractPlugin.loader

18.静态资源内联

    1. 代码层面:
    • 页面框架的初始化脚本
    • 上报相关打点
    • css内联避免页面闪动
  • 2.请求层面:减少HTTP网络请求数
    • 小图片或者字体内联(url-loader)

HTMl和JS内联

raw-loader(版本0.5.1)

  • raw-loader内联html

    • require('raw-loader!./meta.html')
  • raw-loader 内联JS

    • require('raw-loader!babel-loader!../node_modules/lib-fixible')

CSS内联

  • 1.html-inline-css-webpack-plugin

19.多页面打包通用方案

  • 动态获取entry和设置html-webpack-plugin数量

    利用 glob.sync
    entry: glob.sync(path.join(__dirname,'./src/*/index.js'))
    

    具体配置:

    const glob = require('glob');
    
    const setMPA = () => {
      const entry = {};
      const htmlWebpackPlugins = [];
      const entryFiles = glob.sync(path.join(__dirname,     './src/*/index.js'));
    
    Object.keys(entryFiles)
      .map((index) => {
          const entryFile = entryFiles[index];
    
          const match = entryFile.match(/src\/(.*)\/index.js/);
          const pageName = match && match[1];
    
          entry[pageName] = entryFile;
          htmlWebpackPlugins.push(
              new HtmlWebpackPlugin({
                  template: path.join(__dirname,`src/${pageName}/index.html`),
                  filename: `${pageName}.html`,
                  chunks: [`${pageName}`],
                  inject: true,
                  minify: {
                      html5: true,
                      collapseWhitespace: true,
                      preserveLineBreaks: false,
                      minifyCSS: true,
                      minifyJS: true,
                      removeComments: false
                  }
              })
          )
      })
    
      return {
          entry, 
          htmlWebpackPlugins
      }    
    }
    const {entry, htmlWebpackPlugins} = setMPA();
    

20. source map

作用: 通过 source map 定位到源代码

开发环境开启,线上环境关闭

  • source map 关键字
      1. eval: 使用eval包裹模块代码
      1. source map: 产生.map文件
      1. cheap: 不包含列信息
      1. inline: 将.map作为DataURI嵌入, 不单独生成.map文件
      1. module: 包含loader的sourcemap
devtool首次构建二次构建是否适合生产环境可以定位的代码
(none)++++++yes最终输出的代码
eval++++++nowebpack生成的代码(一个个的模块)
cheap-eval-source-map+++no经过loader转换后的代码(只能看到行)
cheap-module-eval-source-map0++no源代码(只能看到行)
eval-source-map-+no源代码
cheap-source-map+0yes经过loader转换后的代码(只能看到行)
cheap-module-source-map0-yes源代码(只能看到行)
inline-cheap-source-map+0no经过loader转换后的代码(只能看到行)
inline-cheap-module-source-map0-no源代码(只能看到行)
source-map----yes源代码
inline-source-map----no源代码
hidden-source-map----yes源代码

官网链接:webpack.docschina.org/configurati…

21.提取页面公共资源

基础库分离

方法一:

    1. html-webpack-externals-plugin
new HtmlWebpackExternalsPlugin({
        externals: [
            {
                module: 'react',
                entry: '//now8.gtimg.com/now/lib/16.8.6/react.min.js?_bid=4042',
                global: 'React'
            },
            {
                module: 'react-dom',
                entry: '//now8.gtimg.com/now/lib/16.8.6/react-dom.min.js?_bid=4042',
                global: 'ReactDOM'
            }
        ]
    })

方法二:

    1. SplitChunksPlugin
optimization: {
    splitChunks: {
        cacheGroups: {
            commons: {
                test: /(react|react-dom)/,
                name: 'vendors',
                chunks: 'all'
            }
        }
    }
}

利用SplitChunksPlugin进行公共脚本分离

optimization: {
    splitChunks: {
        minSize: 0,
        cacheGroups: {
            commons: {
                name: 'commons',
                chunks: 'all',
                minChunks: 2
            }
        }
    }
}
module.exports = {
    //...
    optimization: {
      splitChunks: {
        // async:异步引入的库进行分离(默认), initial: 同步引入的库进行分离, all:所有引入的库进行分离(推荐)
        chunks: 'async',
        minSize: 30000, // 抽离的公共包最小的大小,单位字节
        maxSize: 0, // 最大的大小
        minChunks: 1, // 资源使用的次数(在多个页面使用到), 大于1, 最小使用次数
        maxAsyncRequests: 5, // 并发请求的数量
        maxInitialRequests: 3, // 入口文件做代码分割最多能分成3个js文件
        automaticNameDelimiter: '~', // 文件生成时的连接符
        automaticNameMaxLength: 30, // 自动自动命名最大长度
        name: true, //让cacheGroups里设置的名字有效
        cacheGroups: { //当打包同步代码时,上面的参数生效
          vendors: {
            test: /[\\/]node_modules[\\/]/, //检测引入的库是否在node_modlues目录下的
            priority: -10, //值越大,优先级越高.模块先打包到优先级高的组里
            filename: 'vendors.js'//把所有的库都打包到一个叫vendors.js的文件里
          },
          default: {
            minChunks: 2, // 上面有
            priority: -20, // 上面有
            reuseExistingChunk: true //如果一个模块已经被打包过了,那么再打包时就忽略这个上模块
          }
        }
      }
    }
  };

22. Tree Shaking(摇树优化)的使用和原理分析

概念: 1个模块可能有多个方法,只要其中的某个方法使用到了, 则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会在uglify阶段被擦除掉。

使用: webpack默认支持, 在.babelrc里设置moudules:false即可, production mode的情况下默认开启

要求: 必须是ES6的语法, CJS的方式不支持

DCE(dead code elimination): 无用代码消除

    1. 代码不会被执行
    1. 代码执行的结果不会被用到
    1. 代码只会影响死变量(只读不写)

例如:

if(false) {
    console.log('这段代码永远不会执行')
}

Tree-shaking原理

  • 1.利用ES6模块的特点:
      1. 只能作为模块顶层的语句出现
      1. import的模块只能是字符串常量
      1. import binding是immutable的
    1. 代码擦除: uglify阶段删除无用代码

编写的代码不能有副作用,不然tree-shaking会失效。副作用这个概念来源于函数式编程(FP),纯函数是没有副作用的,也不依赖外界环境或者改变外界环境。纯函数的概念是:接受相同的输入,任何情况下输出都是一样的。非纯函数存在副作用,副作用就是:相同的输入,输出不一定相同。或者这个函数会影响到外部变量、外部环境。函数如果调用了全局对象或者改变函数外部变量,则说明这个函数有副作用。

23. Scope Hoisting使用和原理分析

现象: 构建后的代码存在大量闭包代码

导致:

  • 1.大量函数闭包包裹代码, 导致体积增大(模块越多越明显)
  • 2.运行代码时创建的函数作用域变多,内存开销变大

scope hoisting 原理

原理: 将所有模块的代码按照引用顺序放在一个函数作用域里, 然后适当的重命名一些变量以防止变量名冲突

对比:通过scope hoisting可以减少函数申明代码和内存开销

webapck mode 为 production默认开启,必须是ES6语法, CJS不支持

plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
]

24. 代码分割和动态import

  • 适用的场景:

      1. 抽离相同代码到一个共享块
      1. 脚本懒加载,使得初始下载的代码更小
  • 懒加载 JS 脚本的方式

    • ES6: 动态 import (目前还没有原生支持, 需要babel转换),安装babel插件: @babel/plugin-syntax-dynamic-import
    // .babelrc配置
    {
        "presets": [
            ["@babel/preset-env"],
        ],
        "plugins": [
            "@babel/plugin-syntax-dynamic-import"
        ]
    }
    

25. 在webpack中使用ESLint

{
    test: /\.js$/,
    use: [
        'babel-loader',
        'eslint-loader'
    ]
}
// .eslintrc.js
module.exports = {
    "parser": "babel-eslint",
    "extends": "airbnb",
    "env": {
        "browser": true,
        "node": true
    },
    "rules": {
        "indent": ["error", 4]
    }
}

26. webpack打包库和组件

1.如何将库暴露出去

  • 1.library: 指定库的全局变量
  • 2.libraryTarget: 支持库引入的方式
entry: {
     "input": './src/components/input.js',
     "form": './src/components/form.js',
     "formItem": './src/components/form-item.js',
     "index": './src/components/index.js'
},
output: {
    filename: '[name].js',
    library: 'soloForm',
    libraryExport: 'default',
    libraryTarget: 'umd'
}

27. 优化构建时命令行的显示日志

PresetAlternativeDescription
"errors-only"none只在发生错误时输出
"minimal"none只在发生错误或有新的编译时输出
"none"false没有输出
"normal"true标准输出
"verbose"none全部输出
// webpack.config.js
stats: 'errors-only'
  1. 优化命令行的构建日志
  • 使用 friendly-errors-webpack-plugin
    • success: 构建成功的日志提示
    • warning:构建警告的日志提示
    • error: 构建报错的日志提示
  • stats 设置成 errors-only
plugins: [
  ...     
  new FriendlyErrorsWebpackPlugin(),
  ...
],
stats: 'errors-only'

28.构建异常和中断处理

  • compiler在每次构建结束后会触发 done 这个hook

  • process.exit 主动处理构建报错

plugins: [
    new FriendlyErrorsWebpackPlugin(),
    function() {
        this.hooks.done.tap('done', (stats) => {
            if (stats.compilation.errors &&
                stats.compilation.errors.length &&
                process.argv.indexOf('--watch') == -1) {
                    console.log('build error');
                    process.exit(1);
                }
        })
    }
]

image.png

最后

其它系列文章链接