webpack 入门+实践心得

241 阅读11分钟

webpack核心概念

entry(入口)

entry作用是在于指定一个项目的入口文件,从入口文件开始分析来构建其内部的依赖图,webpack会找到其依赖的模块和库并打包到指定的出口(output)文件中。

  • 单个入口(多作用单页应用)
// 将src下的index.js文件及其依赖的模块和库打包到./dist目录下,生成entry.js文件
module.exports = {
 entry: './src/index.js'
};
  • 多个入口
应用场景:
1.分离 应用程序(app) 和 第三方库(vendor) 入口;
2.多页面应用
module.exports = {
  vendor: ["jquery", "react","react-dom"],//多入口简写,数组写法 
};
module.exports = {
  page1: ./src/page1/index.js, // 对象写法
  page2: ./src/page2/index.js,
  page3: ./src/page3/index.js
};

output(出口)

output作用是指定接收webpack打包后的文件输出目录,以及如何命名这些文件。默认情况下,webpack会直接输出到./dist目录下。

module.exports ={
    output:{
        //  输出的文件名(指定名称+目录)
        filename:js/[name].js
        // 输出的文件目录
        path:./dist
        // 一般在生产环境使用,所有静态资源引入时的公共前缀
        // 当设置为/时 静态资源路径为localhost:8080/index.js
        // 当设置为/js/时 静态资源路径为localhost:8080/js/index.js
        publicPath:'/',
        // 场景:按需加载(异步)模块时候,指定生成的文件名
        chunkFilename:'js/[name]_chunk.js' //非入口 chunk 的名称
        library:'[name]' // 整个库向外暴露的的变量名,多用于自己造的轮子里
        libraryTarget:'window||global|common.js|...' 是控制 webpack 打包的内容是如何暴露的
    }
}

loader(解释器)

默认情况下,webpack只能处理js以及json格式的文件,为了能让wenpack处理其他类型格式的文件,需要指定对应的loader去做转换。

  • 用法
    module.export ={
        module:{
            rules[
                  // 存放详细的loader配置
                  // 不同的文件必须配置不同的loader处理
                  {
                    // 匹配那些文件 /\.js$/ 匹配以.js结尾的文件
                    test:/\.jsx$/,
                    // 指定loader 来处理jsx文件
                    loader:'babel-loader',
                    // 使用loader时的一些配置项
                    option:{}
                  },{
                    //如果一个文件要用多个loader处理
                    test:/\.css$/,
                    // use数组中loader执行顺序:从右到左
                    // 首先执行css-loader将css文件变成common.js模块加载js中,内容是样式字符串
                    // 接着执行style-loader 在js中创建style标签,将样式字符串插入到style标签中
                    use:['style-loader','css-loader']
                  }
              ]  
        }
        
    }

plugins [插件]

插件(Plugins)可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量等。

  • 用法
module.exports={
    plugins:[
        new HtmlWebpackPlugin({ template: './src/index.html' }), 
    ]
}

mode (模式)

通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化

// 设置mode为‘development’webpack会自动启用以下功能
module.exports = {
   mode: 'development'
// plugins: [
// new webpack.NamedModulesPlugin(), 开启 HMR 的时候使用该插件会显示模块的相对路径
// new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }), // 定义全局常量
//  ]
}

// 设置mode为‘production’webpack会自动启用一下功能
module.exports = {
   mode: 'development'
// plugins: [
//   new UglifyJsPlugin(/* ... */), // 压缩js代码
//   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),// 定义全局常量
//   new webpack.optimize.ModuleConcatenationPlugin(), // 提升js执行速度,主要作用es6模块
    //当打包遇到错误时,会将错误页面打包出去。这时页面就会白屏,
    //这个插件的作用就是跳过这个打包页面,将错误信息打包出去,错误信息只在控制台中输出
//   new webpack.NoEmitOnErrorsPlugin()
//  ]
}

如果配置loader来打包各种资源,为什么要这么配置?

  • webpack默认只能打包js,json资源,如果打包其他类型的资源,就要添加loader来进行处理。

打包样式文件

打包css

module.export ={
        module:{
            rules[
                {
                    test:/\.css$/,
                    use:['style-loader','css-loader']
                },
                {
                    test:/\.less$/,
                    use:['style-loader','css-loader','less-loader']
                }
              ]  
        }
        
    }

单独输出css

  • 这样配置后的输入的文件将不会单独的css文件,所有的css样式都会打包到js文件中。
  • 如果要将css文件单独打包出来,则需要mini-css-extract-plugin插件的帮忙,具体使用方法如下
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.export ={
        module:{
            rules[
                {
                    test:/\.css$/,
                    use:[
                        // 这个loader取代style-loader。作用:提取js中的css成单独文件
                        MiniCssExtractPlugin.loader,
                        'css-loader'
                    ]
                },
                {
                    test:/\.less$/,
                    use:[
                        MiniCssExtractPlugin.loader,
                        'css-loader',
                        'less-loader'
                    ]
                }
              ]  
        },
        plugins:[
            new MiniCssExtractPlugin({
                // 对输出的css文件进行命名
                filename: 'css/built.css'
            })
        ]
        
    }

处理css兼容

  • 当项目里需要对css的兼容性做要求时,这时候就需要用到postcss,它提供了一种方式用 JavaScript 代码来处理 CSS
  • postcss比较常用的两个功能介绍:
    • Autoprefixer:基于当前浏览器支持的特性和属性数据去为你的css添加前缀
    • postcss-cssnext: 可以使用一些css4语法,cssnext包含了 autopreafixer,所以autopreafixercssnext只安装一个,避免报错
  • 可以在loader下的option中些postcss的配置,也可以目录下新建一个postcss.config.js进行配置
const autoprefixer = require('autoprefixer');
const postcssCssnext = require('postcss-cssnext');
{
    test:/\.css$/,
    use:[
        MiniCssExtractPlugin.loader,
        'css-loader',
        {
            loader:'postcss-loader',
            options:{
                // 固定写法
                ident:'postcss',
                // 这些plugins都需要用npm安装下
                plugins: [
                  autoprefixer({
                    // 指定要兼容的浏览器
                    // 如果提示autoprefixer版本过高则替换成overrideBrowserslist
                    browsers:['last 10 Chrome versions', 'last 5 Firefox versions', 'Safari >= 6', 'ie> 8']
                  })
                  require('postcss-cssnext')(),
                ]
            }
        }
    ]
},

压缩css

  • 这里用到的插件是optimize-css-assets-webpack-plugin,也需要手动安装
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.export ={
        plugins:[
            // 用法很简单 直接在plugins中添加即可
            new OptimizeCssAssetsWebpackPlugin()
        ]
    }

打包js文件

处理js兼容 babel-loader

  • 目前大都都是使用es6,es7的语法进行开发,这个时候就要用到babel处理我们的js文件
// 需要安装依赖 babel-loader @babel/core @babel/preset-env
module.export={
    module:{
        rules:[
            {
                test:/\.js$/,
                // 不转换node_modules目录下的文件
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    // 预设:指示babel做怎么样的兼容性处理
                    presets:[
                        '@babel/preset-env', // 按需加载 需要支持那些js新特性
                    ]
                }
            }
        ]
    }
}
  • 上面的配置只是做了基本的js语法兼容,但是浏览器暂时还不能支持如Promise,Generator等全局对象,以及定义在这些全局对象的下方法。所以我们还得安装@babel/polyfill这个包来做兼容
  • 需要在入口文件中引入 '@babel/polyfill'
// index.js
import '@babel/polyfill';
const promise = new Promise(resolve=>{
    setTimeout(()=>{
        console.log('定时器执行完了~');
        resolve();
    })
)
conosle.log(promise);
  • 首先打包下,发现在mode='production'情况下打包体积就已经有80多k了,这肯定不能接受,特别是在多页应用的时候,体积会更大。
  • 所以还需引入core-js,来做兼容处理的按需加载
// 需要安装依赖 core-js
module.export={
    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:/node_modules/,
                loader:'babel-loader',
                options:{
                    presets:[
                        '@babel/preset-env',
                        {
                            // 按需加载
                            useBuiltIns: 'usage',
                            // 指定core-js版本
                            corejs: {
                              version: 3
                            },
                            // 指定兼容性做到哪个版本浏览器
                            targets: {
                              chrome: '60',
                              firefox: '60',
                              ie: '9',
                              safari: '10',
                              edge: '17'
                            }
                        }
                    ]
                }
            }
        ]
    }
}
  • 配置完之后,打包体积直接减少了4/3
  • 当项目变大的时候,通过babel编译的时长就会变长。我们也可以在option下添加cacheDirectory:true,,开启缓存,下一次构建代码的时候会读取第一次的缓存
  • 搭配ts 直接安装typescript ts-loader,然后在rules里直接配置上ts-loader即可。

处理js语法检查 eslint-loader

// 需要安装依赖 
// eslint eslint-loader  
// eslint-config-airbnb-base   airbnbd的eslint规则 
// eslint-plugin-import  // import语句规则
module.export={
    module:{
        rules:[
            {
                test:/\.js$/, // 如果用react的话可以直接指定 /\.js$/
                exclude:/node_modules/,
                loader:'eslint-loader',
                // eslint-loader规则必须要先于babel-loader规则,
                // 所以这里添加enforce:true预处理eslint-loader规则
                enforce: 'pre'
                options:{
                    // 自动修复eslint的错误
                    fix: true
                }
            }
        ]
    }
}
// 在根目录下你新建一个.eslintrc.josn文件
{
    "extends":"aib",
    "env":{
        "browser":true // 指定宿主环境
    }
}

字体、图片静态资源打包

// 安装url-loader  file-loader 
// url-loader 可以将图片转成base64
// file-loader 可以打包图片,字体等一些静态资源
{
    test:/\.(jpg|png|gif)/,
    loader:'url-loader',
    options: {
          limit: 8 * 1024, // 如果小于8k 直接转base64
          name: '[hash:6].[ext]', // 输出的文件名
          outputPath: 'images', // 打包图片文件的输出目录
    }
},{
    // 字体文件直接用file-loader处理即可
}

html-webpack-plugin

  • 这个plugins就是将打包后的样式link插入到head元素中,script插入到head或者body中,并且生产html入口文件,单页面应用则配置单个html-webpack-plugin,多页面则配置多个。
安装 html-webpack-plugin
   const HtmlWebpackPlugin = require('html-webpack-plugin'); 
   module.exports={
       plugins:[
           // 当什么都不配置的时候,该插件还是会把打包后js,css插入到html中,并生产新的html入口文件
           new HtmlWebpackPlugin({
               // 介绍几个常用的配置
               filename:'index.html' 输出的html文件名,默认为index.html
               template:'./index.html' 指定模板html文件
               chunks:['index'] 允许插入html的chunk,不配置默认会将entry里所有的chunk都注入模板中
               // true:默认为true,表示script标签位于html文件的 body 底部
               // body 同上
               // head 所有JavaScript资源插入到head元素中
               // false 所有css js不插入 单纯生成一个html文件
               inject:true|body| head|false
               // 压缩html文件
               minify:{
                   // 移除空格
                   collapseWhitespace: true,
                   // 移除注释
                   removeComments: true
                   // 去掉属性引用
                   removeAttributeQuotes:true
                   // 给生成的 html 文件生成一个 favicon。属性值为 favicon 文件所在的路径名
                   favicon
               } 
           })
       ]
   }

配置优化

HMR介绍

  • HMR:hot module replacement 热模块替换
  • 作用:主要用作开发环境:当我们项目过大的时候,每次修改文件重新编译的时长很长,影响开发效率,而启用HMR就可以做到一个模块发生变化,只会重新构建这一个模块,不会构建全部文件
安装依赖 webpack-dev-server
module.export={
    mode:'development',
    devServer:{
        // 开启HMR功能
        hot: true
    }
}

oneOf

  • 提高loader匹配效率,使用方法如下,匹配到一个loader后,后面的就不会再继续匹配了。可以理解为loader在匹配的时候用的时else if匹配,而oneof用的switch case来进行匹配。
module.export={
    module:{
        rules:{
            oneOf:[
                ...loader
            ]
        }
    }
}

tree shaking

  • 直译:树摇,这个功能就是像摇树木的时候,将坏掉的叶子也摇下来。换到tree shaking中来就是将js中无用或者没有使用过的代码给剔除掉。
  • 例子
// a.js

export function add1(a, b) {
    return a + b
}

export function add2(a, b) {
    return a + b
}

// b.js

import {add2} from './a.js'

console.log(add2(1, 2))
  • 如果使用tree shaking,add2函数将不会被打包进去。
  • 有一个前提是,源码必须遵循 ES6 的模块规范 (import & export),如果是 CommonJS 规范 (require) 则无法使用。
  • 开启mode='production'
  • 注意:所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 package.json->sideeffect 列表中,以免在生产模式中无意中将它删除。
"sideEffects": ["*.css]

多进程构建

  • 多进程构建常用的有两种thread-loaderHappyPack,推荐使用thread-loader。多进程构建的应用场景是在项目过大,构建速度和打包速度较慢时使用,项目较小时不建议使用,因为开启多进程构建也会消耗一定时间和资源。
// 安装thread-loader
module.exports = {
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: ['thread-loader']
                }
            ]
        }
}

externals

  • 当我们需要一个引入一个库,但是又不想被打包(节约打包时长),那么就可以配置externals。
  • 举例 当我们引用了loadsh,并且这个库也不需要经常去更新。
module.exports ={
    externals:{
        // 配置完后,webpack在打包时碰到import _ from 'loadsh'时不会打包进bundle文件,而是会去script标签访问这个依赖
        loadsh:'loadsh'
    }
}
  • 在入口的html文件中,添加script标签,引入loadsh库的cdn地址,就可以了。

DllPlugin

  • 当项目偏大的时候,有很多不需要经常更新的库,我们可以把他们单独打包,这样就不用每次在打包的时候又要重新打包这些库。
  • 首先创建webpack.dll.conf.js
// import webpack from 'webpack';
module.exports = {
  mode:'production',
  entry: {
    vendor: ['react', 'react-dom','antd'],
  },

  output: {
    path: path.join(__dirname, 'dist/dll'),
    filename: '[name].dll.js',
    library: '[name]'
  },

  plugins: [
    // 打包生成一个 manifest.json --> 提供库和打包文件的映射
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dist/[name]-manifest.json'),
      name: '[name]'
    }),
  ]
}
  • react react-dom antd打包出来之后,还需要用到DLLPlugin,将我们写的代码跟dll打包出来的代码完全分离,这样我们每次打包只需要打包自己写的代码了。
- webpack.config.js
import webpack from 'webpack';

module.exports = {
  mode:'production',
  entry: {
    app: './src/index'
  },
  output: {
    filename: 'index.js',
    path: 'dist/',
  },
  plugins: [
  // 告诉webpack哪些库不参与打包
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('./dist/vendors-manifest.json')
    })
  ]
}
  • 这样就完成了DllPlugin的配置,第一次运行时,先根据webpack.dll.js编译第三方库,然后运行webpack.config.js。

resolve

  • 解析模块的规则
resolve: {
    // 配置解析模块路径别名: 优点简写路径 缺点路径没有提示
    alias: {
      @: resolve(__dirname, 'src')
    },
    // 配置省略文件路径的后缀名
    extensions: ['.js', '.json', '.jsx'],
  }

optimization

  • 作用,提取公共代码,减少打包体积。应用场景:多页面应用。
  • 一般来说,多页面应用各个页面都采用的相同的技术栈,如果每个页面都打包一遍这些库的代码,那么整个应用的体积就会变的很大。
  • 如果我们将公共代码抽出来,这样不仅打包体积会减少,在用户切换不同页面时,公共代码的js也会直接从缓存中拿,加快了页面的响应速度
常用配置
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.export={
    optimization: {
    splitChunks: {
      chunks: 'all' 三个可选值:initial(初始块)、async(按需加载块)、all(全部块),默认为all;
      minSize: 30 * 1024, // 分割的chunk最小为30kb。默认为30k
      maxSiza: 0, // 最大没有限制
      minChunks: 1, // 要提取的chunk最少被引用1次
      maxInitialRequests: 3, // 入口js文件最大并行请求数量
      automaticNameDelimiter: '~', // 名称连接符
      name: true, // 可以使用命名规则
      cacheGroups: {
        common:{
                chunks: 'initial',
                name:'testCommon', // 打包后的文件名
                minSize: 0, 
                minChunks: 2 // 重复2次才能打包到此模块
            },
      }*/
    },
    minimizer: [
      // 配置生产环境的压缩方案:js和css
      new TerserWebpackPlugin({
        // 开启缓存
        cache: true,
        // 开启多进程打包
        parallel: true,
        // 启动source-map
        sourceMap: true
      })
    ]
  }
}

Hash Chunkhash Contenthash

Hash:

module.exports = {
  mode:'production',
  entry: {
    main: './src/index'
  },
  output: {
    filename: '[name].js',
    path: 'dist/',
  },
}
  • 当我们修改完内容,按上面配置,打包出来的文件名称是main.js。当发布完之后,服务器会拉取main.js,这时浏览器发现跟上一个版本的main.js资源名一样,默认会采用之前缓存的main.js,这时我们修改的内容并没有被获取到。
module.exports = {
 output: {
   filename: '[hash].[name].js',
 },
}

  • 所以我们要在输出文件的名字前面加上hash值,打包之后文件名都是唯一的,这样就能解决文件被缓存的问题了。

Chunkhash

  • 如果我们配置了多个入口文件或者配置其他资源时也使用hash做文件名输出,那么每次打包的时候都是一样的hash值
  • 这时候修改一下main.js的文件内容,vendor.js的文件名的hash值也会发生改变,用户在请求js资源时,就会重新获取vendor.js。
  • chunkhash是根据对应的chunk生产hash值,[chunkhash].[name].js,这样当修改main.js时,vendor.js的hash值就不会发生改变。

Contenthash

  • 如果在main.js里面 import './index.css',打包出单独的css文件,那么index.css和main.js的hash值都会改变,因为index.css被main.js应用了,公用一个hash值。
  • 这个时候,配置[Contenthash].[name].js,根据文件内容生产对应的hash值,index.css的hash不会发生改变

devServer

  • devserver是配合webpack启动一个本地服务,能够让我们快速开发一个应用程序。
安装依赖webpack-dev-server
module.exports = {
    mode:'development'
    devServer: {
    // 运行代码的目录
    contentBase: resolve(__dirname, 'build'),
    // 监视 contentBase 目录下的所有文件,一旦文件变化就会 reload
    watchContentBase: true,
    watchOptions: {
      // 忽略文件
      ignored: /node_modules/
    },
    // 启动gzip压缩
    compress: true,
    // 端口号
    port: 5000,
    // 域名
    host: 'localhost',
    // 自动打开浏览器
    open: true,
    // 开启HMR功能
    hot: true,
    // 不要显示启动服务器日志信息
    clientLogLevel: 'none',
    // 除了一些基本启动信息以外,其他内容都不要显示
    quiet: true,
    // 如果出错了,不要全屏提示~
    overlay: false,
    // 服务器代理 --> 解决开发环境跨域问题
    proxy: {
      // 一旦devServer(5000)服务器接受到 /api/xxx 的请求,就会把请求转发到另外一个服务器(3000)
      '/api': {
        target: 'http://localhost:3000',
        // 发送请求时,请求路径重写:将 /api/xxx --> /xxx (去掉/api)
        pathRewrite: {
          '^/api': ''
        }
      }
    }
}

总结

  • 这篇实践心得是基础webpack4.x去做配置的,webpack配置并不复杂,最重要的是要去了解为什么这么配,如果不懂的可以简单配一下webpack,尝试那些方案更适合你。本文大部分都是开发中常用的解决方案,主要是对最近升级文webpack的一个小总结,如果能帮助到大家那就更好啦。