一起学习webpack

Posted by zack on May 20, 2019

前言

webpack是一个Javascript应用程序的静态模块打包器,接下来我们将通过理解以下四个核心概念:

  • entry(入口)
  • output(输出)
  • loader(转换器)
  • plugins(插件)
    一起来使用webpack定制前端开发环境

安装和使用

安装

npm install webpack webpack-cli -g

使用
  • 创建一个项目在src目录下新建index.html,index.css,index.js以及assets文件夹等一个基础的项目结构
  • 在项目根目录下使用npm init 创建package.json文件
  • npm install webpack -D npm install webpack-cli -D 分别安装webpack和webpack-cli
  • 在package.json中,添加一个npm scripts
    1
    2
    3
    4
    5
    6
    7
    
      "scripts": {
          "build": "webpack --mode production"
      },
      "devDependencies": {
          "webpack": "^4.37.0",
          "webpack-cli": "^3.3.6",
      }
    

    此时npm run build就会发现新增了一个dist目录,里面存放着webpack构建的main.js文件

  • 最后还需要一个webpack.config.js配置文件,配置实际项目中需要的功能,一起来配置吧~
  • 关联HTML
    html-webpack-plugin 为html文件引入外部资源如script、link等,可以生成创建html入口文件,比如单页面可以生成一个html文件入口,配置n个html-webpack-plugin可以生成n个页面入口 它是一个独立的npm package,使用前先安装到项目的开发依赖中:
    npm install html-webpack-plugin -D
    然后在webpack.config.js中配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cosnt HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    // ...
    plugins: [
        new HtmlWebpackPlugin({     // 打包输出html
            title: 'webpack test app',     // 生成的html文件的标题
            minify: {   // 压缩html文件
                removeComments: true, //移除HTML中的注释
                collapseWhitespace: true, // 删除空白符与换行符
                miniFyCSS: true   // 压缩内联css
            },
            filename: 'index.html',     // 输出的html文件的名称
        })
    ]
}

更多关于html-webpack-plugin

  • 构建CSS
    css-loader 负责解析CSS代码,主要是处理CSS中的依赖,例如@importurl()等引用外部文件的声明
    style-loader 将css-loader解析的结果转变成JS代码,运行时动态插入style标签来让CSS代码生效
    此时CSS代码会转变成JS和index.js一起打包了,需要单独把CSS分离需要extract-text-webpack-plugin插件
    上述基础上我们还会使用Less/Sass等CSS预处理器,请看下面例子:
    预先安装css-loader、style-loder、less-loader:
1
 npm install css-loader --save-dev
1
 npm install style-loder --save-dev
1
 npm install less-loader --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({ 
          fallback: 'style-loader',
          use: 'css-loader',
        }), 
      },
      {
        test: /\.less$/,
        use: ExtractTextPlugin.extract({ 
          fallback: 'style-loader',
          use: [
              'css-loader',
              'less-loader'
          ],
        }), 
      },
    ],
  },
  plugins: [
    new ExtractTextPlugin('index.css'),
  ],
}   
  • 处理图片文件
    file-laoder 用于处理很多类型的文件,主要作用是直接输出文件,把构建后的文件路径返回 预先安装file-loader
1
 npm install css-loader --save-dev

index.js

1
    import img from './img.png'
1
2
3
4
5
6
7
8
9
10
11
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {},
          },
        ],
      },
    ]
  • 使用Babel
    Babel 可以编译ES新特性的工具,配置Babel以便使用ES6,ES7标准来编写js代码
    安装:
    1
    
      npm install -D babel-loader @babel/core @babel/preset-env webpack
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      module: {
          rules: [
              {
                  test: /\.js$/,
                  include:[
                      path.resolve(__dirname, 'src'),
                  ],
                  use: {
                      loader: 'babel-loader'
                  }
              }
          ]
      }
    
  • 启动静态服务
    以上步骤已经处理了多种的文件类型的webpack配置,接下来可以使用webpack-dev-server在本地开启服务来进行开发和调试。
    安装:
    1
    
      npm install webpack-dev-server --save-dev
    

    package.json添加启动命令:

    1
    2
    3
    4
    
      "scripts": {
          "build": "webpack --mode production",
          "start": "webpack-dev-server --mode development"
      }
    

    当 mode 为 development 时,具备 hot reload 的功能,即当源码变化时,会更新当前页面 尝试运行npm run start然后就可以访问 http://localhost:8080 查看页面啦! 可以通过devServer字段配置 webpack-dev-server,下面是几个常用配置:
    sevServer.proxy:
    访问单独的后端开发服务器APi,并且希望在同域名下发送API请求,可以使用proxy代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    module.exports = {
      // ...
       devServer: {
          proxy: {
              '/api': {
                  target: 'http://localhost:3000',
                  pathRewrite: {'^/api' : ''}
              }
          }
      }
    }
    

    同域名下请求到 /api/orders 将会被代理到请求 http://localhost:3000/orders
    sevServer.port:
    指定要监听的端口号

    1
    2
    3
    
      devServer: {
          port: 8000
      }
    

    sevServer.publicPath :
    用于指定构建好的文件在浏览器中用什么路径去访问,默认是’/’,假设运行在 http://localhost:8000 ,并且output.filename 被设置为bundle.js,则此包可通过http://localhost:8000/bundle.js 访问,也可修改指定目录访问或使用一个完整的URL。

    1
    2
    3
    
      devServer: {
          publicPath: '/assets/'
      }
    

    sevServer.contentBase :
    用于配置提供额外静态文件内容的目录,即不经过wabpack构建,但是需要在webpack-dev-server中提供访问的静态资源。
    publicPath的优先级高于contentBase,publicPath是配置构建好的结果以什么路径去访问,而contentBase是配置额外的静态内容的访问路径。

    1
    2
    3
    
      devServer: {
          contentBase: path.join(__dirname, 'public')
      }
    
    1
    2
    3
    4
    
      devServer: {
          // 多个目录提供内容
          contentBase: [path.join(__dirname, 'public'), path.join(__dirname, 'assets')]
      }
    

    devServer.after:
    在服务内部所有其它中间件之后,提供执行自定义中间件的功能。

    1
    2
    3
    4
    5
    
      devServer: {
          after: function(app, server) {
              //do something
          }
      }
    

    devServer.before:
    在服务内部的所有其他中间件之前,执行自定义中间件的功能。

    1
    2
    3
    4
    5
    6
    7
    
      devServer: {
          before: function(app, server) {
             app.get('/some/path', function(req, res){
                 res.json({ custom: 'response'})
             })
          }
      }
    

    更多配置请看dev-server

核心概念

entry

多个代码模块中会有一个起始的.js文件,这便是webpack构建的入口,常见项目中可能入口有一个也可能有多个。

  • 单个入口
    1
    2
    3
    4
    5
    
      module.exports = {
          entry: {
              main: './src/index.js'  
          }
      }
    
  • 多个入口
    1
    2
    3
    4
    5
    6
    7
    8
    
      module.exports = {
          entry: {
              main: [
                  './src/foo.js',
                  './src/bar.js'
              ]
          }
      }
    
resolve
  • resolve.alias
    模糊匹配:如import ‘utils/query.js’ 等同于 ‘import [项目绝对路径]/src/utils/query.js’
    1
    2
    3
    
      alias: {
          utils: path.resolve(__dirname, 'src/utils')
      }
    

    精确匹配:给定对象的键后末尾添加$,如匹配 import ‘utils’

    1
    2
    3
    
      alias: {
          utils$: path.resolve(__dirname, 'src/utils')
      }
    
  • resolve.extensions
    此配置是一个数组,数组的顺序代表匹配后缀的优先级
    1
    
      extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.css']
    

    如在src/utils下面有一个common.js文件时,就可以这样引用 import * as common from ‘utils/common’

  • resolve.modules
    告诉webpack解析模块时应该搜索的目录,绝对路径和相对路径都能使用 相对路径将类似于 Node 查找 ‘node_modules’ 的方式进行查找 使用绝对路径,将只在给定目录中搜索
    1
    2
    3
    
      resolve: {
          modules: ['node_modules']
      }
    

    可以添加目录到模块搜索目录,此目录优先级高于node_modules,这样配置可以在某种程度上简化模块的查找,提升构建速度

    1
    2
    3
    
      resolve: {
          modules:[path.resolve(_dirname, 'src'), 'node_modules']
      }
    
  • resolve.mainFiles
    默认解析使用index.js这个文件,也可配置
    1
    2
    3
    
      resolve: {
          mainFiles: ['index']  // 可以添加其他默认使用的文件名
      }
    
  • resolve.mainFields


更多关于resolve,请移步官网

loader

loader就是webpack提供的一种处理多种格式文件格式的机制,可以理解是一个转换器,负责把某种文件格式的内容转换为webpack可以支持打包的模块。 默认webpack会把所有的依赖打包成js文件,当我们需要处理不同类型的文件时可以在module.rules配置

  • 条件

  • {test: Condition} 匹配特定条件
  • {include: Condition } 匹配特定条件
  • {exclude: Condition } 排除特定条件
  • {and: Condition } 必须匹配数组中的所有条件
  • {or: Condition } 匹配数组中任何一个条件
  • {not: Condition } 必须排除这个条件
1
2
3
4
5
6
7
8
9
10
11
    rules: [
        {
            test: /\.jsx?/,  //正则,
            include: [
                path.resolve(_dirname, 'src') // 字符串,注意是绝对路径
            ],
            not: [
                (value) => {/*...*/ return true;} // 函数,高度自定义
            ]
        }
    ]
  • 模块类型 不同的模块类型类似于配置了不同的loader,webpack会针对性的进行处理,目前有5种模块类型:
  1. javascript/auto 支持现有各种JS代码模块类型————CommonJS、AMD、ESM
  2. javascript/esm ECMAScript modules,例如CommonJS和AMD等不支持,是.mjs文件的默认类型
  3. javascript/dynamic CommonJS和AMD,排除ESM
  4. javascript/json JSON格式数据,是.json的文件默认类型
  5. webassembly/experimental WebAssembly modules 是.wasm文件的默认类型

举例,如所有JS代码文件都设置为强制使用ESM类型:

1
2
3
4
5
6
7
    {
        test: /\.js/,
        include: [
            path.resolve(_dirname, 'src),
        ],
        type: 'javascript/esm'
    }
  • 使用loader module.rules 的匹配规则主要是用于匹配loader,使用use字段:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      rules: [
          {
              test: /\.less/,
              use: [
                  'style-loader',
                  {
                      loader: 'css-loader',
                      options: {
                          importLoaders: 1   //数字1代表当前loader之后的一个loader
                      }
                  },
                  {
                      loader: 'less-loader',
                      options: {
                          noIeCompat: true
                      }
                  }
              ]
          }
      ]
    

    一个模块文件可以经过多个loader的转换处理,在同一个rule中执行顺序是从最后配置的loader开始往前。 上述从后往前的顺序是在同一个rule中进行,那如果多个rule匹配同一个模块文件,此时的loader的应用顺序又是怎样的呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      rules: [
          {
              enforce: 'pre',
              test: /\.js$/,
              exclude: /node_modules/,
              loader: 'eslint-loader'
          },
          {
              test: /\.js$/,
              exclude: /node_modules/,
              loader: 'babel-loader'
          }
      ]
    

    webpack提供了enforce字段来配置当前rule的loader类型,倘若没配置就是普通类型,还可配置prepost,分别对应前置类型和后置类型的loader

  • tips:还有一种行内loader,即如const json=require('json-loader!./file.json') ,不建议在开发中使用

总结下来,loader按照前置->行内->普通->后置的顺序执行

  • module.noParse 用于配置哪些模块文件的内容不需要解析,忽略的文件不应该含有import,require,define的调用,或其他导入机制,可以提高整体的构建速度
    1
    2
    3
    4
    5
    
      module.exports = {
          module: {
              noParse: /jquery|lodash/
          }
      }
    
plugin

用于处理更多其他的一些构建任务,通过添加我们需要的plugin,可以满足更多构建任务中的特殊需求。 webpack内置的插件可以通过webpack.插件名获取,内置插件,非内置插件需要先在package.json安装下 例如,要使用压缩JS代码的uglifyjs-webpack-plugin插件,只需在配置中通过plugins字段添加新的plugin即可:

1
2
3
4
5
6
7
    const UglifyPlugin = require('uglifyjs-webpack-plugin')
    
    module.exports = {
        plugins: [
            new UglifyPlugin()
        ]
    }
  • DefinePlugin 用途:允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和生产模式的构建允许不同的行为非常有用
    用法:

    每个传进 DefinePlugin 的键值都是一个标志符或者多个用 . 连接起来的标志符。
    如果这个值是一个字符串,它会被当作一个代码片段来使用。
    如果这个值不是字符串,它会被转化为字符串(包括函数)。
    如果这个值是一个对象,它所有的 key 会被同样的方式定义。
    如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    plugins: [
      new webpack.DefinePlugin({
        'process.host': getDeployConfigDefine(),
        VERSION: 'v1'
      })
    ]
    // ....
    function getDeployConfigDefine() {
        let config = {}
            Object.keys(env).forEach(function(key) {
                config[key] = `'${env[key].api}'`
            })
        return config
    }
1
    console.log(VERSION)
  • copy-webpack-plugin 用途:将不经过webpack处理,但我们希望也能出现在build目录下的文件,使用CopyWebpackPlugin处理
    用法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      const CopyWebpackPlugin = require('copy-webpack-plugin')
      const resolve = (file) => path.resolve(__dirname, file)
    
      plugins: [
          new CopyWebpackPlugin([
              {
                  from:  resolve('./static'),     // 配置来源
                  to:  to: resolve('./dist/static'),      // 配置目标路径
              }   
          ])
      ]
    

    更多配置请参考copy-webpack-plugin

  • extract-text-webpack-plugin 用途:用来将依赖的CSS分离出来成为单独的文件
    用法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      const ExtracTextPlugin = require('extract-text-webpack-plugin')
    
      module.exports = {
          // ...
          module:{
              rules: [
                  {
                      test: /\.css$/,
                      // 因为此插件需要干涉模块转换的内容,所以需要使用它对应的loader
                      use: ExtracTextPlugin.extract({
                          fallback: 'style-loader',
                          use: 'css-loader'
                      })
                  }
              ]
          }
          plugins: [
              new ExtracTextPlugin('index.css')
          ]
      }