webpack必知必会基础篇(一)

106 阅读10分钟

快速上手

webpack核心概念

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
  • Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。
  • context: context即是项目打包的路径上下文,如果指定了context,那么entry和output都是相对于上下文路径的,contex必须是一个绝对路径

Webpack 启动后会从Entry里配置的Module开始递归解析 Entry 依赖的所有 Module。 每找到一个 Module, 就会根据配置的Loader去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑

基本配置文件

const path=require('path');
module.exports={ 
    context:process.cwd(),
    entry: './src/index.js', 
    output: { path: path.resolve(__dirname,'dist'), filename:'bundle.js' },
    module: {},
    plugins: [], 
    devServer: {} 
}

mode

  • webpack的mode配置用于提供模式配置选项告诉webpack相应地使用其内置的优化,mode有以下三个可选值
  • development
  • production
  • none

common

//parent chunk中解决了的chunk会被删除
optimization.removeAvailableModules:true 
//删除空的chunks 
optimization.removeEmptyChunks:true 
//合并重复的chunk 
optimization.mergeDuplicateChunks:true

development

//调试 
devtool:eval 
//缓存模块, 避免在未更改时重建它们。
cache:true
//缓存已解决的依赖项, 避免重新解析它们。
module.unsafeCache:true 
//在 bundle 中引入「所包含模块信息」的相关注释 
output.pathinfo:true 
//在可能的情况下确定每个模块的导出,被用于其他优化或代码生成。
optimization.providedExports:true 
//找到chunk中共享的模块,取出来生成单独的chunk 
optimization.splitChunks:true 
//为 webpack 运行时代码创建单独的chunk
optimization.runtimeChunk:true 
//编译错误时不写入到输出 
optimization.noEmitOnErrors:true 
//给模块有意义的名称代替ids 
optimization.namedModules:true 
//给模chunk有意义的名称代替ids 
optimization.namedChunks:true

production

//性能相关配置 
performance:{hints:"error"....} 
//某些chunk的子chunk已一种方式被确定和标记,这些子chunks在加载更大的块时不必加载 
optimization.flagIncludedChunks:true 
//给经常使用的ids更短的值 
optimization.occurrenceOrder:true 
//确定每个模块下被使用的导出 
optimization.usedExports:true 
//识别package.json or rules sideEffects 标志 
optimization.sideEffects:true 
//尝试查找模块图中可以安全连接到单个模块中的段。
optimization.concatenateModules:true 
//使用uglify-js压缩代码 
optimization.minimize:true

配置开发服务器

npm i webpack-dev-server –D
  • contentBase 配置开发服务运行时的文件根目录
  • host:开发服务器监听的主机地址
  • compress 开发服务器是否启动gzip等压缩
  • port:开发服务器监听的端口
devServer:{ 
  contentBase:path.resolve(__dirname,'dist'), 
  host:'localhost', 
  compress:true,
  port:8080 
}

支持加载css文件

通过使用不同的Loader,Webpack可以要把不同的文件都转成JS文件,比如CSS、ES6/7、JSX等

  • test:匹配处理文件的扩展名的正则表达式
  • use:loader名称,就是你要使用模块的名称
  • include/exclude:手动指定必须处理的文件夹或屏蔽不需要处理的文件夹
  • query:为loaders提供额外的设置选项 loader的三种写法
module: { 
 rules: [ 
    { 
       test: /\.css/, 
       loader:['style-loader','css-loader']
    } 
 ]
}

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

module: {
    rules: [ 
        { 
            test: /\.css/,
            include: path.resolve(__dirname,'src'), 
            exclude: /node_modules/, 
            use: [
                { 
                    loader: 'style-loader', 
                    options: { insert:'top' } 
                },
                'css-loader'
            ] 
        } 
    ] 
}

插件

  • 在 webpack 的构建流程中,plugin 用于处理更多其他的一些构建任务
  • 模块代码转换的工作由 loader 来处理
  • 除此之外的其他任何工作都可以交由 plugin 来完成
cnpm i html-webpack-plugin -D
  • minify 是对html文件进行压缩,removeAttrubuteQuotes是去掉属性的双引号
  • hash 引入产出资源的时候加上查询参数,值为哈希避免缓存
  • template 模版路径
entry:{
        index:'./src/index.js',  // chunk名字 index
        common:'./src/common.js' //chunk名字 common
     },
  
plugins: [
        new HtmlWebpackPlugin({
             template:'./src/index.html',//指定模板文件
             filename:'index.html',//产出后的文件名
             inject:false,
             hash:true,//为了避免缓存,可以在产出的资源后面添加hash值
             chunks:['common','index'],
             chunksSortMode:'manual'//对引入代码块进行排序的模式
          }),
      ]

支持图片

手动添加图片

npm i file-loader url-loader -D
  • file-loader 解决CSS等文件中的引入图片路径问题
  • url-loader 当图片小于limit的时候会把图片BASE64编码,大于limit参数的时候还是使用file-loader 进行拷贝
{ 
  test:/\.(jpg|png|bmp|gif|svg)/, 
  use:[ 
    { 
      loader:'url-loader',
      options:{limit:4096} 
    } 
  ] 
}
    .logo { 
        width:355px; 
        height:133px; 
        background-image: url(./images/logo.png); 
        background-size: cover;
    }

分离css

因为CSS的下载和JS可以并行,当一个HTML文件很大的时候,我们可以把CSS单独提取出来加载

npm install --save-dev mini-css-extract-plugin
  • filename 打包入口文件
  • chunkFilename 用来打包import('module')方法中引入的模块
plugins:  
//参数类似于webpackOptions.output 
 new MiniCssExtractPlugin({ 
   filename: '[name].css',
   chunkFilename:'[id].css' 
   }), 
   
{ 
    test: /\.css/, 
    include: path.resolve(__dirname,'src'), 
    exclude: /node_modules/, 
    use:[
          {
            loader: MiniCssExtractPlugin.loader 
          },
          'css-loader'
       ] 
}

内联css

  • 注意此插件要放在HtmlWebpackPlugin的下面
  • HtmlWebpackPlugin的inject设置为true
cnpm i html-inline-css-webpack-plugin -D
const HtmlInlineCssWebpackPlugin= require('html-inline-css-webpack-plugin').default;

plugins:[
    new HtmlWebpackPlugin({}), 
    new HtmlInlineCssWebpackPlugin() 
]

压缩js和css

  • terser-webpack-plugin替换掉uglifyjs-webpack-plugin解决uglifyjs不支持es6语法问题
cnpm i uglifyjs-webpack-plugin terser-webpack-plugin optimize-css-assets-webpack-plugin -D
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
    mode: 'production',
    optimization: {
        minimizer: [
           /*  new UglifyJsPlugin({
                cache: true,//启动缓存
                parallel: true,//启动并行压缩
                //如果为true的话,可以获得sourcemap
                sourceMap: true // set to true if you want JS source maps
            }), */
            new TerserPlugin({
                 parallel: true,
                 cache: true
            }),
            //压缩css资源的
            new OptimizeCSSAssetsPlugin({
                 assetNameRegExp:/\.css$/g,
                 //cssnano是PostCSS的CSS优化和分解插件。cssnano采用格式很好的CSS,并通过许多优化,以确保最终的生产环境尽可能小。
                 cssProcessor:require('cssnano')
            })
        ]
    }
}

css和image存放单独目录

  • 去掉HtmlInlineCssWebpackPlugin
  • outputPath 输出路径
  • publicPath指定的是构建后在html里的路径
  • 如果在CSS文件中引入图片,而图片放在了image目录下,就需要配置图片的publicPath为/images,或者
output: {
    path: path.resolve(__dirname, 'dist')
    filename: 'bundle.js'
    publicPath: '/'
}

{
    test: /\.(jpg|jpeg|png|bmp|gif|svg|ttf|woff|woff2|eot)/
    use: [
        {
            loader: 'url-loader',
            options: {
                limit: 4096,
                outputPath: 'images',
                publicPath: '/images'
            }
        }
    ]
}

plugins: [
    new MiniCssExtractPlugin({
        //filename: '[name].css',
        //chunkFilename: '[id].css',
        chunkFilename: 'css/[id].css',
        filename: 'css/[name].[hash].[chunkhash].[contenthash].css',//name是代码码chunk的名字
    })
] 

编译less与sass

npm i less less-loader -D
npm i node-sass sass-loader -D

less:

  @color:red; 
  .less-container { 
     color:@color; 
   }

sass:

   $color:green;
   .sass-container { 
      color:$color;
   }

webpack配置

{
    test: /\.less/
    include: path.resolve(__dirname, 'src')
    exclude: /node_modules/
    use: [
        {
            loader: MiniCssExtractPlugin.loader,
        },
        'css-loader',
        'less-loader'
    ]
}
{
    test: /\.scss/
    include: path.resolve(__dirname, 'src')
    exclude: /node_modules/
    use: [{
        loader: MiniCssExtractPlugin.loader,
    },
        'css-loader',
        'sass-loader'
    ]
}

处理css3属性前缀

npm i postcss-loader autoprefixer -D
  • PostCSS 的主要功能只有两个

    • 第一个就是前面提到的把 CSS 解析成 JavaScript 可以操作的 抽象语法树结构(Abstract Syntax Tree,AST)
    • 第二个就是调用插件来处理 AST 并得到结果 postcss.config.js
    module.exports={ 
        plugins:[
            require('autoprefixer')
        ] 
   }

webpack.config.js

   { 
      test:/\.css$/, 
      use:[ 
         MiniCssExtractPlugin.loader,
         'css-loader',
         'postcss-loader'
      ], 
      include:path.join(__dirname,'./src'),
      exclude:/node_modules/
      }

转义ES6/ES7/JSX

    npm i babel-loader @babel/core @babel/preset-env @babel/preset-react -D 
    npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D
{
    test: /.jsx?$/,
    use: {
        loader: 'babel-loader',
        options:{
         "presets": ["@babel/preset-env"],
         "plugins": [
            ["@babel/plugin-proposal-decorators", { "legacy": true }],
            ["@babel/plugin-proposal-class-properties", { "loose" : true }]
         ]
        }
    },
    include: path.join(__dirname,'src'),
    exclude:/node_modules/
}

babel runtime

  • babel 在每个文件都插入了辅助代码,使代码体积过大
  • @babel/plugin-transform-runtime 是开发时引入, @babel/runtime 是运行时引用
  • plugin-transform-runtime 已经默认包括了 @babel/polyfill,因此不用在独立引入
  • corejs 是一个给低版本的浏览器提供接口的库,如 Promise、Map和Set 等
  • 在 babel 中你设置成 false 或者不设置,就是引入的是 corejs 中的库,而且在全局中引入,也就是说侵入了全局的变量
npm install --save-dev @babel/plugin-transform-runtime 
npm install --save @babel/runtime

ESLint校验代码格式规范

npm install eslint eslint-loader babel-eslint --D

.eslintrc.js

module.exports = {
    root: true,
    //指定解析器选项
    parserOptions: {
        sourceType: 'module',
        ecmaVersion: 2015
    },
    //指定脚本的运行环境
    env: {
        browser: true,
    },
    // 启用的规则及其各自的错误级别
    rules: {
        "indent": ["error", 4],//缩进风格
        "quotes": ["error", "double"],//引号类型 
        "semi": ["error", "always"],//关闭语句强制分号结尾
        "no-console": "error",//禁止使用console
        "arrow-parens": 0 //箭头函数用小括号括起来
    }
}

webpack.config.js 配置

module: {
    //配置加载规则
    rules: [
        {
            test: /\.js$/,
            loader: 'eslint-loader',
            enforce: "pre",
            include: [path.resolve(__dirname, 'src')], // 指定检查的目录
            options: { fix: true } // 这里的配置项参数将会被传递到 eslint 的 CLIEngine   
        },
    ]
}

打包第三方类库

直接引入

import _ from 'lodash'; 
alert(_.join(['a','b','c'],'@'));

插件引入

  • webpack配置ProvidePlugin后,在使用时将不再需要import和require进行引入,直接使用即可
  • 函数会自动添加到当前模块的上下文,无需显示声明
new webpack.ProvidePlugin({
    _:'lodash'
})

externals 如果我们想引用一个库,但是又不想让webpack打包,并且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用,那就可以通过配置externals

<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
const jQuery = require("jquery");
import jQuery from 'jquery';
externals: {
    jquery: 'jQuery'//如果要在浏览器中运行,那么不用添加什么前缀,默认设置就是global
}
外链CDN,cdn引入,不用打包
const htmlWebpackExternalsPlugin= require('html-webpack-externals-plugin');
new htmlWebpackExternalsPlugin({
            externals:[
                {
                    module:'react',
                    entry:'https://cdn.bootcss.com/react/15.6.1/react.js',
                    global:'React'
                },
                 {
                    module:'react-dom',
                    entry:'https://cdn.bootcss.com/react/15.6.1/react-dom.js',
                    global:'ReactDOM'
                }
            ]
}) 

通过预编译资源模块,可以代替 cdn 分包的方式,解决每个模块都得引用一个 script 的缺陷

const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'production',
  entry: {
    library: ['react', 'react-dom'],
  },
  output: {
    filename: 'react-library.dll.js',
    path: path.resolve(__dirname, './dll'),
    library: '[name]_[hash]', // 对应的包映射名
  },
  plugins: [
    new webpack.DllPlugin({
      context: __dirname,
      name: '[name]_[hash]', // 引用的包映射名
      path: path.join(__dirname, './dll/react-library.json'),
    }),
  ],
};

const webpack = require('webpack');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('./dll/react-library.json'),
    }),
    // 打包后的 .dll.js 文件需要引入到 html中,可以通过 add-asset-html-webpack-plugin 插件自动引入
    new AddAssetHtmlPlugin({ 
      filepath: require.resolve('./dll/react-library.dll.js'),
      publicPath: '',
    }),
  ],
};

watch

module.exports = {
    //默认false,也就是不开启
    watch:true,
    //只有开启监听模式时,watchOptions才有意义
    watchOptions:{
        //默认为空,不监听的文件或者文件夹,支持正则匹配
        ignored:/node_modules/,
        //监听到变化发生后会等300ms再去执行,默认300ms
        aggregateTimeout:300,
        //判断文件是否发生变化是通过不停的询问文件系统指定议是有变化实现的,默认每秒问1000次
        poll:1000
    }
}

copy静态文件

npm i copy-webpack-plugin -D
new CopyWebpackPlugin([{ 
         from: path.resolve(__dirname,'src/assets'),//静态资源目录源地址 
         to:path.resolve(__dirname,'dist/assets') //目标地址,相对于output的path目录
   }])

打包前先清空输出目录

npm i clean-webpack-plugin -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [
    new CleanWebpackPlugin({ 
        cleanOnceBeforeBuildPatterns: ['**/*', '!static-files*'], 
    })
]

服务器代理

不修改路径

proxy: { 
    "/api": 'http://localhost:3000' 
}

修改路径

proxy: {
    "/api": { 
         target: 'http://localhost:3000',
         pathRewrite:{"^/api":""} 
     } 
}

before 在 webpack-dev-server 静态资源中间件处理之前,可以用于拦截部分请求返回特定内容,或者实现简单的数据 mock

before(app){
    app.get('/api/users', function(req, res) { 
      res.json([{id:1,name:'111'}])
    })
  }

webpack-dev-middleware就是在 Express 中提供 webpack-dev-server 静态服务能力的一个中间件

const express = require('express');
const app = express();
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackOptions = require('./webpack.config');
webpackOptions.mode = 'development';
const compiler = webpack(webpackOptions);
app.use(webpackDevMiddleware(compiler, {}));
app.listen(3000);

resolve解析

指定extension之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配

    resolve: { 
       extensions: [".js",".jsx",".json",".css"]
    },

alias

配置别名可以加快webpack查找模块的速度

const bootstrap = path.resolve(__dirname,'node_modules/_bootstrap@3.3.7@bootstrap/dist/css/bootstrap.css')
resolve: {
    alias:{
        "bootstrap":bootstrap
    }
}

modules

对于直接声明依赖名的模块(如 react ),webpack 会类似 Node.js 一样进行路径搜索,搜索node_modules目录

resolve: { modules: ['node_modules'], }

如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的 node_modules 中的话

resolve: { 
    modules: [path.resolve(__dirname, 'node_modules')], 
}

mainFields

默认情况下package.json 文件则按照文件中 main 字段的文件名来查找文件

resolve: {
    // 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
    mainFields: ['browser', 'module', 'main'],
    // target 的值为其他时,mainFields 默认值为:
    mainFields: ["module", "main"],
  }

mainFiles

当目录下没有 package.json 文件时,我们说会默认使用目录下的 index.js 这个文件,其实这个也是可以配置的

resolve: {
   mainFiles: ['index'], // 你可以添加其他默认使用的文件名 
},

resolveLoader

resolve.resolveLoader用于配置解析 loader 时的 resolve 配置,默认的配置

module.exports = {
    resolveLoader: {
      modules: [ 'node_modules' ],
      extensions: [ '.js', '.json' ],
      mainFields: [ 'loader', 'main' ]
    }
  };

noParse

  • module.noParse 字段,可以用于配置哪些模块文件的内容不需要进行解析
  • 不需要解析依赖(即无依赖) 的第三方大型类库等,可以通过这个字段来配置,以提高整体的构建速度
module.exports = {
    // ...
    module: {
      noParse: /jquery|lodash/, // 正则表达式
      // 或者使用函数
      noParse(content) {
        return /jquery|lodash/.test(content)
      },
    }
}

DefinePlugin

new webpack.DefinePlugin({
    PRODUCTION: JSON.stringify(true),
    VERSION: "1",
    EXPRESSION: "1+2",
    COPYRIGHT: {
        AUTHOR: JSON.stringify("珠峰培训")
    }
})

IgnorePlugin

  import moment from 'moment';
  console.log(moment);
new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
  • 第一个是匹配引入模块路径的正则表达式
  • 第二个是匹配模块的对应上下文,即所在目录名

对图片进行压缩和优化

npm install image-webpack-loader --save-dev

{
    test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
        use: [
            'file-loader',
            {
                loader: 'image-webpack-loader',
                options: {
                    mozjpeg: {
                        progressive: true,
                        quality: 65
                    },
                    optipng: {
                        enabled: false,
                    },
                    pngquant: {
                        quality: '65-90',
                        speed: 4
                    },
                    gifsicle: {
                        interlaced: false,
                    },
                    webp: {
                        quality: 75
                    }
                }
            },
        ]
}

libraryTarget 和 library

libraryTarget使用者的引入方式使用者提供给被使用者的模块的方式
var只能以script标签的形式引入我们的库只能以全局变量的形式提供这些被依赖的模块
commonjs只能按照commonjs的规范引入我们的库被依赖模块需要按照commonjs规范引入
amd只能按amd规范引入被依赖的模块需要按照amd规范引入
umd可以用script、commonjs、amd引入按对应的方式引入

var:编写的库将通过var被赋值给通过library指定名称的变量

module.exports = {
    add(a,b){
       return a+b;
    } 
}
var calculator=(function (modules) {}({})

commonjs:

exports["calculator"] = (function (modules) {}({})
require('npm-name')['calculator'].add(1,2);

this:编写的库将通过 this 被赋值给通过 library 指定的名称,输出和使用的代码如下

this["calculator"]= (function (modules) {}({})

使用方式:

this.calculator.add();

window

window["calculator"]= (function (modules) {}({})
window.calculator.add();

global

编写的库将通过 global 被赋值给通过 library 指定的名称,即把库挂载到 global 上,输出和使用的代码如下:

global["calculator"]= (function (modules) {}({})
global.calculator.add();

umd

(function webpackUniversalModuleDefinition(root, factory) {
    if(typeof exports === 'object' && typeof module === 'object')
      module.exports = factory();
    else if(typeof define === 'function' && define.amd)
      define([], factory);
    else if(typeof exports === 'object')
      exports['MyLibrary'] = factory();
    else
      root['MyLibrary'] = factory();
  })(typeof self !== 'undefined' ? self : this, function() {
    return _entry_return_;
  })