webpack基础实践

简介


  • webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。用于前端代码的工程化
  • webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle
  • webpack + loader 可以支持多种语言和预处理器语法编写的模块。loaderwebpack 描述了如何处理非原生模块,并将相关依赖引入到你的 bundles中。
  • webpack 还支持可自由扩展的插件( plugin)体系。这是 webpack 的 支柱 功能。插件目的在于解决 loader 无法实现的其他功能。
  • webpack 天生支持如下模块类型:ECMAScript CommonJS AMD Assets``WebAssembly.

接下来,我们以一个 React + SASS 的简单web项目,来学习webpack的基础配置。

以下配置都是基于 webpack 4.30.0

基础配置


首先,我们要安装webpack系列npm包 (这些 loaderplugins,后面会逐步介绍):

// package.json
{
  "devDependencies": {
    /** webpack **/
    "webpack": "4.30.0",  
    "webpack-cli": "3.3.12",
    "webpack-dev-server": "^3.7.2", // 本地调试用
    /** loader **/
    "css-loader": "2.1.1",
    "style-loader": "^2.0.0",
    "file-loader": "^6.2.0",
    "node-sass": "^4.0.0",
    "sass-loader": "^7.1.0",
    "@babel/preset-env": "7.4.3",
    "@babel/preset-react": "7.0.0",
    /** plugins **/
    "html-webpack-plugin": "4.0.0",
    "mini-css-extract-plugin": "^0.6.0",
    "webpack-bundle-analyzer": "^4.4.0"
  }
}

webpack打包,需要做一系列的配置,而且 本地调试 和 生产打包 的配置是有一些差异的。一般情况下,我们在项目的根目录下设置两个配置文件:

|
|- webpack.config.js  # 生产打包
|- webpack.config.dev.js # 本地调试

// package.json
{
  //...
  "scripts": {
    "start": "webpack-dev-server --config webpack.config.dev.js",
    "build": "webpack-cli"
  }
  //...
}

webpack配置文件可以直接返回一个json对象,或者一个返回json对象的函数。

// webpack.config.js
export default {
  //...
}

//or

export default () => {
  return {
    //...
  }
}

接下来,我们开始具体配置:

entry

web项目中,我们一直有主JS,或者入口JS的概念。webpack要打包,构建依赖图谱。肯定也需要一个入口文件。
webpack 通过 entry 来配置一个(或多个)入口。默认值是 ./src/index.js

const path = require('path')

module.exports = {
  entry: './src/pages/app.js'
}

或者对象,可以标示入口文件name(一般用于多个)

module.exports = {
  entry: {
    app: './src/pages/app.js'
  }
}

output

有打包输入(entry),就有打包输出。
webpack通过 output 来配置输出文件信息。默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。

module.exports = {
  output: {
    path: path.resolve(__dirname, 'build'),  // 指定输出目录
    filename: '[name].js', // 指定输出文件名称;  [name] 动态使用entry指定的名称,对应上面是:app
  }
}

高阶配置

实际项目中,我们静态资源会使用CDN,或者通过其他特有目录引入。
这个时候,我们需要通过 publicPath来配置.

编译时指定:

module.exports = {
  output: {
    path: path.resolve(__dirname, 'build'), 
    filename: '[name].js',
    publicPath: '/my-project/static/resource/',
  }
}

运行时指定:

module.exports = {
  output: {
    path: path.resolve(__dirname, 'build'), 
    filename: '[name].js',
    publicPath: './', // 可随意填写,运行时会覆盖为 __webpack_public_path__ 
  }
}
// 重点在这里:
__webpack_public_path__ = window.global_info.staticCDN; 
// 如果项目在一个Java/Node容器,staticCDN 的值一般是服务端路由中读取环境变量赋予。
// 如果项目是纯前端,staticCDN的值一般是通过 域名+规则 判断确定。

Loaders

  • 面向现代JS,我们大都不是通过直接编写原生JS来开发项目,而是运用各种框架(ReactVue ……),各种JS+语言(TSJSX ……),ES5+ 语法糖等。
  • 但是我们的浏览器只识别原生JS,虽然主流浏览器也开始兼容绝大多数ES5+语法,但是我们也得考虑兼容性问题。
  • 所以,webpack 提供了 loaders 机制,我们可以通过使用 loader 用于对模块的源代码进行转换。
  • 不同的模块类型,应用不同的 loaders
  • loaders 是通过 module 对象下的 rules 数组来配置。

简单示例:

module.exports = {
  module: {
    rules: [
      { test: /\.css$/, use: 'css-loader' }, // 对每个 .css 使用 css-loader
      { test: /\.ts$/, use: 'ts-loader' }  // 对所有 .ts 文件使用 ts-loader
    ]
  }
}; 
  • testRegExp,用来匹配需要处理的文件类型。
  • useString | Object | Array,指定具体的 loader。上述示例,使用 单个默认配置的 loader,可以使用String类型指定即可。

** loader 可以通过 options 对象配置更多参数。带参数配置的单个loader示例:**

module.exports = {
  module: {
    rules: [
       // 图片资源处理
      {
        test: /\.(png|jpg|jpeg|gif)$/,
        use: [
          {
            loader: require.resolve('file-loader'),
            options: {   // loader参数配置
              esModule: false,
              name: './image/[name].[hash:8].[ext]'
            }
          }
        ]
      }
    ]
  }
}

负责图片处理的 loader 有两种, 一般一起使用:

  • file-loader 可以把jscss中导入的图片替换成正确的地址,并把图片文件输出到对应的位置。文件名是根据文件内容计算出的hash值。

  • url-loader 可以把文件通过 base64 编码后注入到JS 或者 css中去。图片的数据量太大,会导致JSCSS文件变大,一般利用url-loader把页面需要的小图片注入到代码中去,以减少加载次数。url-loader 通过 limit 参数来控制,小于 limit的图片才会处理。

多个 loader 配置,将按照相反的顺序执行。以支持 scss 为例:

module.exports = {
  module: {
    rules: [
      // 处理sass样式文件
      {
        test: /\.(css|scss)$/,
        // 使用多个 loaders;都使用默认配置;
        use: ["style-loader", "css-loader","sass-loader"]  
      } 
    ]
  }
}
  • sass-loader 负责把Scss源码转换为CSS代码,再交给css-loader处理。
  • css-loader 会找出 @importURL() 这样的导入语句,告诉webpack依赖这些资源。同时还支持css modules,压缩css等功能。
  • style-loader 会把css代码转换成字符串,然后注入到JS代码中。通过JSDOM增加样式。也可以通过pluginMiniCssExtractPlugin )把css提取到单独的文件中。

插件 (plugin) 可以为 loader 带来更多特性:

module.exports = {
  module: {
    rules: [
      {
        test:  /\.(js|jsx)$/,
        exclude: /(node_modules)/, // 正则表达式,设置该loader需要忽略/排除的目录/文件
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env','@babel/preset-react'],
            cacheDirectory: true,
            plugins: ['@babel/plugin-transform-runtime','@babel/plugin-proposal-class-properties']
          }
        }
      }
    ]
  }
}
  • @babel/runtime和@babel/plugin-transform-runtimebabel 编译时只转换语法,几乎可以编译所有时新的 JavaScript 语法,但并不会转化BOM(浏览器)里面不兼容的API。比如 Promise,Set,Symbol,Array.from,async 等等的一些API。这2个包就是来搞定这些api的。

  • @babel/plugin-proposal-class-properties:用来解析类的属性的。

  • babel执行编译的过程中,会从项目的根目录下的 .balelrc 文件中读取配置。.balelrc是一个json格式的文件,当babel loader配置项很多的时候可以使用。

由此可见, plugins 可以在 loader 配置中配合使用。当然,也可以单独配置使用。下面,我们来详解了解一下plugins

点击这里查看常用 Loader List

Plugins

  • 功能: 用于解决 loader 无法实现的其他事,进行功能扩展。

  • 原理:webpack plugins 是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。你可以基于次编写自定义插件。

  • 用法: 可以通过在 webpack.config.js 增加Plugins配置使用。也可以通过 NodeAPI 的形式使用。

下面,我们通过几个常用的 plugin 来了解如何配置使用:

// 该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。 
const HtmlWebpackPlugin = require('html-webpack-plugin')

// 一般,plugins都是通过 new 一个自身的实例来使用
// 支持option对象参数配置
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './temp.html',  // 可以自定义用于生成Html的模版文件,非必填
      filename: 'home.html'  //  指定html文件名称;默认为 index.html
    })
  ]
}

** 使用默认配置生成的 html 文件如下:**

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>webpack App</title>
  </head>
  <body>
    <script src="index_bundle.js"></script>
  </body>
</html>

多个 entry,对应多个 script 标签。

前面有提到,有的 plugins 可作为有些 loaderoption 配置项使用。

还有一些插件需要在PluginsLoader中都需要增加配置:

// 本插件会将 CSS 提取到单独的文件中
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.(css|scss)$/,
        use: [MiniCssExtractPlugin.loader, "css-loader","sass-loader"]
      },
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '[name].css'})
  ]
}

使用MiniCssExtractPlugin 生成的css文件会在生成的Html文件中通过<link>引入。所以不再需要使用 style-loader
style-loader: 将模块导出的内容作为样式并添加到 DOM 中。

mode

  • production or development.用来指定是生产模式还是开发模式。
  • 在代码中可以通过process.env.NODE_ENV获取,用来做两种模式的兼容处理。

到此,我们这个简单项目的完整webpack.config.js配置如下:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = () => {
  return {
    mode: 'production',
    entry: {
      app: path.resolve(__dirname, 'src/pages/app.js'),
    },
    output: {
      path: path.resolve(__dirname, 'build'),
      publicPath: '/my-project/static/resource/',
      filename: '[name].js',
    },
    resolve: {
      extensions: ['.js', '.jsx', '.ts'],
    },
    module: {
      rules: [
        {
          test: /\.(css|scss)$/,
          use: [MiniCssExtractPlugin.loader, "css-loader","sass-loader"]
        },
        {
          test:  /\.(js|jsx)$/,
          exclude: /(node_modules)/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env','@babel/preset-react'],
              cacheDirectory: true,
              plugins: ['@babel/plugin-transform-runtime','@babel/plugin-proposal-class-properties']
            }
          }
        },
        {
          test: /\.(png|jpg|jpeg|gif)$/,
          use: [
            {
              loader: require.resolve('file-loader'),
              options: {
                esModule: false,
                name: './image/[name].[hash:8].[ext]'
              }
            }
          ]
        }  
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({template: './temp.html', filename: 'home.html'}),
      new MiniCssExtractPlugin({ filename: '[name].css'})
    ],
    optimization: {
      splitChunks: {
        cacheGroups: {
          default: {
            name: 'common',
            chunks: 'initial'
          }
        }
      }
    }
  }
}

resolve.extensions : 如果文件引入的时候没有后缀名,将自动按配置的文件名匹配查找。
optimization: 用于提取公共js配置。

开发模式


我们在本地调试的时候,webpack 配置和生产会有一些差异。比如:

  • 生产可能配置特殊的publicPath,本地则不能配置。
  • 本地调试专用的 devServer 配置。
  • 开发模式专用的一些plugins

我们可以设置单独的配置文件(如 webpack.config.dev.js):

// 下面只展示差异点配置
// new plugin 用来做打包后文件的成分分析,以做某些优化。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 

module.exports = () => {
  return {
    mode: 'development', // 指定为开发环境
    // 下无 publicPath配置
    output: {
      path: path.resolve(__dirname, 'pcDist'),
      filename: '[name].js',
    },
    // 专用plugin
    plugins: [
      new BundleAnalyzerPlugin()
    ],
    // 本地调试专用配置
    devServer: {
      contentBase: path.join(__dirname, 'pcDist'),
      port: 8009,
      hot:true,
      open: true
    }
  }
}

DevServer 其实是一个方便开发的小型http服务器,是基于webpack-dev-middlewareExpress 实现的。webpack-dev-middleware 会导出一个函数。该函数接收一个 webpackCompiler 实例作为参数,导出一个 Express 中间件。该中间件具有以下功能:

  • 接收 Compiler 实例输出的文件,但不会存在硬盘,而是放入内存。
  • Express app 上注册路由,拦截http请求,根据请求路径响应对应的文件。

明显,开发和生产配置大部分还是通用。所以在实际项目中我们使用一个通用配置文件,然后在自有的cli工具库中去抽象,处理,复用。

module,chunk,bundle理解

  • moule 是模块,webpack中一切皆模块,所以 module 就是我们编写的一个个文件。
  • chunk 是指 webpack 根据文件引用关系生成的 chunk 文件。一般来说,一个 entry 对应一个 chunk 文件。
  • bundle 是指 webpack 最终生成的浏览器可以直接运行的 bundle 文件。一般来说,一个 chunk 对应一个 bundle 文件。但是也有例外,比如我们使用 MiniCssExtractPlugin 插件会从一个 chunk 中抽取出单独的 css bundle文件。

webpack 异步加载


在使用 webpack 打包的应用中,我们可以使用 require.ensure 进行异步加载,也有人称为代码切割。他其实就是将指定的 js 模块独立导出一个.js 文件,然后使用这个模块的时候,再创建一个 script 对象,加入到 document.head 对象中,浏览器会自动帮我们发起请求,去请求这个 js 文件,然后写个回调函数,让请求到的 js 文件做一些业务操作。

require.ensure 这个函数是一个代码分离的分割线,表示回调里面的 require 是我们想要进行分割出去的,webpack 会打包成单独 js 文件。它的语法如下:

// 语法如下:
`require.ensure(dependencies: String[], callback: function(require), chunkName: String)`

按需加载

webpack4 官方文档提供了模块按需切割加载,配合 es6 的按需加载 import() 方法,可以做到减少首页包体积,加快首页的请求速度,只有其他模块,只有当需要的时候才会加载对应 js

import()的语法十分简单。该函数只接受一个参数,就是引用包的地址,并且使用了 promise 式的回调,获取加载的包。在代码中所有被 import()的模块,都将打成一个单独的包,放在 chunk 存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。

常用的优化手段

  • 优化loader配置: 通过 include 等配置,不需要处理的文件尽量不处理。

  • 异步按需加载:(上面有专门提到)。

  • 分包:提取公共的一些模块 + 缓存提高性能。

    • SplitChunksPlugin: 提取公共模块,问题是业务模块依赖改变也会影响公共包,哈希会改变,缓存会失效。
    • Dllplugin & DllReferencePlugin: 将指定的公共模块打包成动态链接库形式的js文件。其他模块引用到这些指定的公共模块会直接在公共js文件中加载,不会打包在业务模块中。问题是这些动态链接库文件要独立先行打包,并提前引入。
  • Tree Shaking: webpack依赖静态的 ES6 模块化语法,分析出都要哪些功能被用到了,然后剔除没有的代码。可以在启动 Webpack 时带上 --optimize-minimize 参数,快速接入 Tree Shaking;也可以使用 UglifyJSPlugin 来处理。

  • Scope Hoisting(作用域提升): 分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。

    • 使用内置的 ModuleConcatenationPlugin() 即可开启。

参考文献