webpack入门实操

223 阅读10分钟

  webpack是一个前端模块化打包工具,它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。
  webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过 loader 的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 SASS 等。

  webpack可以说就说一个模块打包器,它自己本身只认js和json。
  webpack是一个打包模块化JavaScript的工具。它会从入口模块出发,识别出源码中的模块化导入语句,递归找出入口文件的所有依赖,将入口和其所有依赖打包到一个单独的文件中。是工程化、自动化思想在前端开发中的体现。

1、初始化项目

npm init -y

安装webpack

npm i webpack webpack-cli -D

  注意,这里不推荐全局安装webpack,全局安装会造成版本指定,如果多个项目依赖的版本不同,会造成构建失败。所以最好是局部安装webpack。

全局webpack版本的查看方式

webpack -v

局部webpack版本的查看方式,npx是npm自带的,会到当前项目的路径去找

npx webpack -v

2、执行打包

npx webpack

或者使用软连接(shell脚本)
在package.json文件中

"scripts": {
  "test": "webpack",
},

3、webpack默认配置

  • 默认入口:
    • 路径:src/index.js
  • 默认出口:
    • 名称:main.js
    • 路径: ./dist
  • webpack默认支持多种模块类型:commonJS、esmodule、AMD
  • webpack默认支持js模块和json模块

在根目录下新建文件:webpack.config.js(webpack的默认配置文件)
webpack本身是基于node的,所以要使用commonJS规范来导出一个对象
webpack不适合用于JavaScript库的构建,因为不够纯粹,它会多一个启动函数。(一般用rollup)

const path = require('path')

module.exports = {
  // webpack执行入口
  entry: './src/index.js',
  output: {
    // 输出到哪里,必须是绝对路径
    path: path.resolve(__dirname, './build'), 
    filename: 'index.js'
  },
  // 模式:development or production
  mode: 'development',
}

使用其他的配置
在根目录下新建文件:webpack.dev.config.js

const path = require('path')

module.exports = {
  entry: './src/index.js',
  output: {
    // 这里输出到dist目录下
    path: path.resolve(__dirname, './dist'), 
    filename: 'index.js'
  },
  mode: 'development',
}

然后修改package.json中的scripts,通过--config来指定使用哪一份配置

"scripts": {
  "test": "webpack",
  "dev": "webpack --config webpack.dev.config.js"
},


4、loader使用

在module中使用loader(各种模块的支持)

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'main.js'
  },
  mode: 'development',
  // 处理模块
  module: {
  },
}

接下来设置支持图片

module: {
  rules: [
    // 处理图片
    {
      test: /\.(png|jpe?g|gif)$/,
      use: [
        {
          // file-loader 是一种专门支持静态文件的loader
          loader: "file-loader",
          options: {
            name: "[name]_[hash:8].[ext]",
            // 这里输出后会让所有匹配的图片输出到 dist/images  文件夹下面
            outputPath: 'images/' 
          },
        }
      ]
    }
  ]
},

支持CSS

module: {
  rules: [
    {
      // css-loader会将css用css in js的方式,将css的内容打包进js文件
      // style-loader会将js中的css那段拿出来,在html文件的head标签以js的操作方式,生成一个style标签
      test: /\.css$/,
      use: ["style-loader", "css-loader"]
    },
  ]
},

如果引入了多个css文件的话,上述的方法会生成多个style标签(每个css文件对应一个style标签)
可以使用下面的配置,将多个style标签合并成一个

module: {
  rules: [
    {
      test: /\.css$/,
      use: [{
        loader: "style-loader",
        options: {
          injectType: "singletonStyleTag"
        }
      }, "css-loader"]
    },
  ]
},

如果想要es678向下兼容,或者用ts的话,可以使用babel。


支持less

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

让浏览器支持CSS3,自动带上前缀的方式处理CSS3的兼容性
使用postcss-loader、autoprefixer(插件)
在根目录下新增文件:postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')({
      // 兼容浏览器最近的两个版本,兼容市场份额大于1%的浏览器
      overrideBrowserslist: ["last 2 versions", ">1%"]
    })
  ]
}

在webpack.config.js文件中

module: {
  rules: [
    {
      test: /\.less$/,
      use: ["style-loader", "css-loader", "postcss-loader", "less-loader"]
    },
  ]
},

5、plugins

  plugins作用于webpack整个构建的生命周期,webpack给我们提供了很多生命周期钩子,plugins可以作用于某个特定的阶段,给我们产生一些效果。

html-webpack-plugin
首先在src目录下新增一个html模板:index.html

<body>
  <div id="root"></div>
</body>

在webpack.config.js中使用

const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'main.js'
  },
  mode: 'development',
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      // 输出的文件名
      filename: "index.html"
    })
  ]
}

clean-webpack-plugin
作用:自动的在构建之前,把原来的构建目录给删掉

const CleanWebpackPlugin = require("clean-webpack-plugin")
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      filename: "index.html"
    }),
    new CleanWebpackPlugin()
  ]
}

mini-css-extract-plugin
作用:将css提取成一个独立文件

const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        // 这里不能再用style-loader了,因为我们要用独立文件的方式
        // 这里使用MiniCssExtractPlugin自带的一个loader
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "postcss-loader",
          "less-loader"
        ]
      },
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name]_[chunkhash:8].css"
    })
  ]
}

sourcemap

module.exports = {
  // none:关掉 
  // inline-source-map:内联,有source-map功能,但不会生成单独的map文件
  devtoll: "source-map"
}

打包后会包含一个.map文件,是与原代码的映射关系,可以帮助我们快速定位错误


6、WebpackDevServer

  • 提升开发效率的利器
      每次改完代码都需要重新打包一次,打开浏览器,刷新一次,很麻烦,我们可以使用 webpackdevserver 来改善这块的体验

  • 安装

npm i webpack-dev-server -D
  • 配置

(1)修改package.json

"script": {
  "server": "webpack-dev-server"
}

(2)在webpack.config.js配置

devServer:{
  contentBase: "./dist", // 设置服务器启动之后的服务地址
  open: true, // 服务启动后,自动帮我们打开浏览器
  port: 8081, // 设置服务的端口号
  proxy: { // 代理
    // 当我们的请求里面带上 "/api"时,走下面的代理
    "/api":{
        target: "http://localhost:9092"
    }
  }
}

7、Hot Module Replacement(HMR:热模块替换)

启动hmr

const webpack = require('webpack')
devServer: {
  hotOnly: true
},
plugins: [
    new webpack.HotModuleReplacementPlugin()
]

HMR支持style-loader css的处理方式,不支持抽离成独立文件的方式
js需手动监听要做HRM的模块,当该模块内容发生改变,会发生回调

// index.js
if(module.hot){
  module.hot.accept("./number.js", function(){
    console.log('这个模块改动了')
  })
}

原理:HMR其实将每一个模块都自编了一个ID,然后会监听这些模块。当某个模块发生了内容改变,就会触发HMR的回调,从而先删除原先的模块,再生成改变后的模块。


8、Babel处理ES6

  babel是javascript编译器,能将ES6代码转换成ES5代码,让我们开发过程中放心使用JS新特性而不用担心兼容性问题。并且还可以通过插件机制根据需求灵活扩展。
  babel在执行编译的过程中,会从项目跟目录下的 .babelrc JSON文件中读取配置。没有该文件则会从loader的options地方读取配置。

  babel会从入口模块进行分析依赖 => AST(抽象语法树) => 通过语法转换规则来转换代码 => 生成代码

安装

npm i -D babel-loader @babel/core @babel/preset-env
  • babel-loader 是webpack与babel的通信桥梁,不会做把es6转成es5的工作,这部分工作需要 @babel/preset-env 来做。
  • @babel/preset-env 里包含了es6/7/8 转es5的转换规则。

接下来使用loader

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: "babel-loader",
        options: ["@babel/preset-env"]
      }
    }
  ]
},

  通过上面几步还不够,默认的babel只支持 let 等一些基础特性的转换,Promise等内容还没有转换过来,这时候需要借助 @babel/polyfill,把es的新特性都装进来,来弥补低版本浏览器缺失的特性。

@babel/polyfill

npm i -S @babel/polyfill

使用

// index.js 顶部
import "@babel/polyfill"

按需加载,减少冗余
上述操作后,会发现打包体积大了很多,这是因为polyfill默认会把所有的新特性注入进来。
按需加载:假如我想用到es6+,才会注入,没用到的不注入,减少打包体积。

修改webpack.config.js

  useBuiltInsbabel7的新功能,这个选项告诉babel如何配置**@babel/polyfill**。它有三个参数可以使用:

  • entry:需要在webpack的入口文件里,import "@babel/polyfill" 一次。babel会根据你的情况导入垫片,没有使用的功能不会导入垫片。
  • usage:不需要import,全自动检测,但是需要安装 @babel/polyfill(试验阶段)
  • false:如果你 import "@babel/polyfill",它不会排除掉没有使用的垫片,程序体积会庞大。(不推荐)
use: {
  loader: "babel-loader",
  options: {
    presets: [
      [
        "@babel/preset-env",
        {
          targets:{
            edge: "17",
            firefox: "60",
            chrome: "67",
            safari: "11.1"
          },
          corejs: 2,
          useBuiltIns: "usage" // 按需引入
        }
      ]
    ]
  }
}

9、多页面打包

entry: {
  index: "./src/index.js",
  list: "./src/list.js",
  detail: "./src/detail.js"
},
output: {
  path: path.resolve(__dirname, './dist'),
  filename: '[name].js'
},
plugins: [
  new HtmlWebpackPlugin({
    template: "./src/index.html",
    filename: "index.html",
    chunks: ["index", "list"]
  }),
  new HtmlWebpackPlugin({
    template: "./src/index.html",
    filename: "list.html",
    chunks: ["list"]
  }),
  new HtmlWebpackPlugin({
    template: "./src/index.html",
    filename: "detail.html",
    chunks: ["detail"]
  }),
  new CleanWebpackPlugin()
]

通用的解决方案:
新建文件 webpack.mpa.config.js

const setMpa = ()=>{
  const entry = {}
  const htmlwebpackplugin = []
  return {
    entry,
    htmlwebpackplugin
  }
}

const { entry, htmlwebpackplugin } = setMpa()

module.exports = {
  entry,
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js'
  },
  mode: 'development',
  plugins: [
    ...htmlwebpackplugin
  ]
}

使用以下文件夹结构

安装 glob 来处理正则信息,根据正则信息返回正确的路径

npm i glob -D

完整代码:

const glob = require('glob')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

const setMpa = () => {
  const entry = {}
  const htmlwebpackplugin = []

  // 分析入口文件路径
  const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"))

  // 过滤信息,拿到入口名称
  entryFiles.map((item, index) => {
    const match = item.match(/src\/(.*)\/index\.js/)
    // 拿到文件夹的name
    const pageName = match && match[1]
    // 将文件夹的name给到entry对象
    entry[pageName] = item

    // 配置plugin
    htmlwebpackplugin.push(
      new HtmlWebpackPlugin({
        // 可以用不同的模板(需要多个模板)
        template: `src/index.html`,
        filename: `${pageName}.html`,
        chunks: [pageName]
      })
    )
  })
  console.log(entry)
  return {
    entry,
    htmlwebpackplugin
  }
}

const { entry, htmlwebpackplugin } = setMpa()

module.exports = {
  entry,
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js'
  },
  mode: 'development',
  plugins: [
    ...htmlwebpackplugin,
    new CleanWebpackPlugin(),
  ]
}

修改scripts

"scripts": {
  "test": "webpack",
  "dev": "webpack --config webpack.mpa.config.js"
},

执行 npm run dev
就可以获得以下的打包文件了


10、加快打包构建速度的方法

  webpack的打包速度一直令人诟病,如果不用上一些优化手段,单单打包两三个文件就能花上好几秒,项目大一点的话,几分钟妥妥的。


开发环境不做无意义的操作

  很多配置在开发阶段是不需要去做的,我们可以区分出开发个线上两套配置,这样在需要上线的时候再全量编译即可。
  比如:代码压缩、目录内容清理、计算文件hash、提取CSS文件等。


选择一个合适的 devtool

  配置 devtool 可以支持 sourceMap,但有些是耗时严重的,这个可以多试试。


代码压缩用 ParallelUglifyPlugin 代替自带的 UglifyJsPlugin 插件

  自带的JS压缩插件是单线程执行的,而 webpack-parallel-uglify-plugin 可以并行的执行。


使用fast-sass-loader代替sass-loader

   fast-sass-loader 可以并行的处理sass,在提交构建之前会先组织好代码,速度也会快一些。


babel-loader开启缓存

  babel-loader在执行的时候,可能会产生一些运行期间重复的文件,造成代码体积变大、冗余,同时也会减慢编译效率。
  可以加上cacheDirectory参数,或者使用transform-runtime 插件。

// webpack.config.js
use: [{
    loader: 'babel-loader',
    options: {
        cacheDirectory: true
    }
]


// .bablerc
{
    "presets": [
        "env",
        "react"
    ],
    "plugins": ["transform-runtime"]
}

不需要打包编译的插件库,换成 script 标签的方式引入

  比如 jquery、react、react-dom等,代码量是很多的,打包起来会很耗时。
  可以直接用标签引入,然后在webpack配置里使用 expose-loaderexternalsProvidePlugin 提供给模块内部使用相应的变量

// @1
use: [{
    loader: 'expose-loader',
        options: '$'
    }, {
    loader: 'expose-loader',
        options: 'jQuery'
    }]


// @2
externals: {
    jquery: 'jQuery'
},


// @3
new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    'window.jQuery': 'jquery'
}),

使用 DllPlugin 和 DllReferencePlugin

  这种方式其实和externals是类似的,主要用于某些模块没有可以在 script 标签中引入的资源(纯npm包)。
  Dll是动态链接库的意思,实际上就是将这些npm打包生成一个json文件,这个文件里包含了npm包的路径对应信息。这两个插件要一起用。
  首先,新建一个 dll.config.js 配置文件,先用webpack来打包这个文件

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

module.exports = {
    output: {
        // 将会生成./ddl/lib.js文件
        path: path.resolve(__dirname, 'ddl'),
        filename: '[name].js',
        library: '[name]',
    },
    entry: {
        "lib": [
            'react',
            'react-dom',
            'jquery'
            // ...其它库
        ],
    },
    plugins: [
        new webpack.DllPlugin({
            // 生成的映射关系文件
            path: 'manifest.json',
            name: '[name]',
            context: __dirname,
        }),
    ],
};

manifest.json文件中就是相应的包对应的信息
然后在我们的项目配置文件中配置DllReferencePlugin 使用这个清单文件

// 插件配置
plugins: [
    new webpack.DllReferencePlugin({
        context: __dirname,
        manifest: require('./manifest.json')
    }),
]

提取公共代码

  使用CommonsChunkPlugin提取公共的模块,可以减少文件体积,也有助于浏览器层的文件缓存。

// 提取公共模块文件
new webpack.optimize.CommonsChunkPlugin({
    chunks: ['home', 'detail'],
    // 开发环境下需要使用热更新替换,而此时common用chunkhash会出错,可以直接不用hash
    filename: '[name].js' + (isProduction ? '?[chunkhash:8]' : ''),
    name: 'common'
}),


// 切合公共模块的提取规则,有时后你需要明确指定默认放到公共文件的模块
// 文件入口配置
entry: {
    home: './src/js/home',
    detail: './src/js/detail',
    // 提取jquery入公共文件
    common: ['jquery', 'react', 'react-dom']
},

使用HappyPack来加速构建

  HappyPack会采用多进程去打包构建,但并不支持所有的loader。
  首先引入,定义一下这个插件所开启的线程,推荐是四个,其实也可以直接使用默认的就行了。

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

然后在module的规则里改动一下,引入它,其中 id是一个标识符

{
    test: /\.jsx?$/,
    // 编译js或jsx文件,使用babel-loader转换es6为es5
    exclude: /node_modules/,
    loader: 'HappyPack/loader?id=js'
    // use: [{
        //     loader: 'babel-loader',
        //     options: {

        //     }
    // }]
}

然后我们调用插件,设置匹配的id,然后相关的配置可以直接把use:的规则部分套在loaders上

new HappyPack({
    id: 'js',
    loaders: [{
        loader: 'babel-loader',
        options: {
            // cacheDirectory: true
        }
    }]
}),
  • 要注意的第一点是:它对file-loaderurl-loader支持不好,所以这两个loader就不需要换成happypack了,其他loader可以类似地换一下
  • 要注意的第二点是,使用ExtractTextWebpackPlugin提取css文件也不是完全就能转换过来,所以需要小小的改动一下
module: {
        rules: [{
            test: /\.css$/,
            // loader: 'HappyPack/loader?id=css'
            // 提取CSS文件
            use: cssExtractor.extract({
                // 如果配置成不提取,则此类文件使用style-loader插入到<head>标签中
                fallback: 'style-loader',
                use: 'HappyPack/loader?id=css'
                // use: [{
                //         loader: 'css-loader',
                //         options: {
                //             // url: false,
                //             minimize: true
                //         }
                //     },
                //     // 'postcss-loader'
                // ]
            })
        }, {
            test: /\.scss$/,
            // loader: 'HappyPack/loader?id=scss'
            // 编译Sass文件 提取CSS文件
            use: sassExtractor.extract({
                // 如果配置成不提取,则此类文件使用style-loader插入到<head>标签中
                fallback: 'style-loader',
                use: 'HappyPack/loader?id=scss'
                // use: [
                //     'css-loader',
                //     // 'postcss-loader',
                //     {
                //         loader: 'sass-loader',
                //         options: {
                //             sourceMap: true,
                //             outputStyle: 'compressed'
                //         }
                //     }
                // ]
            })
        }

因为它是直接函数调用的,我们就放到里层的use规则就行了,然后配置插件即可

plugins: [
    new HappyPack({
        id: 'css',
        loaders: [{
            loader: 'css-loader',
            options: {
                // url: false,
                minimize: true
            }
        }]
    }),
    new HappyPack({
        id: 'scss',
        loaders: [{
            'loader': 'css-loader'
        }, {
            loader: 'fast-sass-loader',
            options: {
                sourceMap: true,
                outputStyle: 'compressed'
            }
        }]
    }),
]

优化构建时的搜索路径

在webpack打包时,会有各种各样的路径要去查询搜索,我们可以加上一些配置,让它搜索的更快。

  • 方便改成绝对路径的就改一下,以纯模块名来引入的可以加上一些目录路径
  • 还可以善用下resolve alias别名,这个字段来配置
  • 还有exclude等配置,避免多余查找的文件,比如使用babel别忘了剔除不需要遍历的文件

整理打包构建涉及的模块

  (导出编译JSON文件)整理打包构建涉及的模块,分析哪些是不需要打包的,只打包需要的模块。
  在webpack编译时加上参数 --json > stat.json 后,可以上传到 webpack-analysewebpack-visualizer 等分析站点上,看看打包的模块信息。


以模块化来引入

  有些模块是可以以模块化来引入的,就是说可以只引入其中的一部分,比如lodash

// 原来的引入方式
 import {debounce} from 'lodash';

//按模块化的引入方式
import debounce from 'lodash/debounce';