webpack的初体验

444 阅读16分钟
  • 配置

整个配置中我们使用 Node 内置的 path 模块,并在它前面加上 __dirname这个全局变量。可以防止不同操作系统之间的文件路径问题,并且可以使相对路径按照预期工作

指南

  • 安装 安装最新版本或是定制版本 (对于大多数项目都建议本地安装)

version 版本号

    npm install --save-dev webpack
    npm isntall --save-deb webpack@<version>

如果你使用webpack 4+ 版本 你还需要安装

npm install --save-dev webpack-cli

webpack 通过运行一个或多个 npm scripts,会在本地 node_modules 目录中查找安装的 webpack:

"scripts": {
    "start": "webpack --config webpack.config.js"
}

当你在本地安装 webpack 后,你能够从 node_modules/.bin/webpack 访问它的 bin 版本。

npm install --global webpack (全局安装不推荐)

最新体验版本

npm install webpack@beta
npm install webpack/webpack#<tagname/branchname>

起步

  • 基本安装
mkdir webpack-demo && cd webpack-demo  创建文件夹
npm init -y                             初始化
npm install webpack webpack-cli --save-dev 安装
npm install --save lodash

执行 npx webpack,会将我们的脚本作为入口起点,然后 输出 为 main.js。Node 8.2+ 版本提供的 npx 命令,可以运行在初始安装的 webpack 包(package)的 webpack 二进制文件(./node_modules/.bin/webpack)

  • 模块 ES2015 中的 import 和 export 语句已经被标准化 注意,webpack 不会更改代码中除 import 和 export 语句以外的部分。如果你在使用其它 ES2015 特性,请确保你在 webpack 的 loader 系统中使用了一个像是 Babel 或 Bublé 的转译器

  • 使用一个配置文件 添加 webpack-config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

比起 CLI 这种简单直接的使用方式,配置文件具有更多的灵活性。我们可以通过配置方式指定 loader 规则(loader rules)、插件(plugins)、解析选项(resolve options),以及许多其他增强功能。了解更多详细信息,请查看配置文档。

  • NPM脚本 (NPM Scripts) 考虑到用 CLI 这种方式来运行本地的 webpack 不是特别方便,我们可以设置一个快捷方式。在 package.json 添加一个 npm 脚本(npm script):

package.json

{
    "name": "webpack-demo",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
+     "build": "webpack"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
      "webpack": "^4.0.1",
      "webpack-cli": "^2.0.9",
      "lodash": "^4.17.5"
    }
  }

运行 npm run build 正常打包 ok

管理资源

为了从 JavaScript 模块中 import 一个 CSS 文件,你需要在 module 配置中 安装并添加 style-loader 和 css-loader

  • 加载CSS :
npm install --save-dev style-loader css-loader

webpack.config.js:

      const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
+   module: {
+     rules: [
+       {
+         test: /\.css$/,
+         use: [
+           'style-loader',
+           'css-loader'
+         ]
+       }
+     ]
+   }
  };

webpack 根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的 loader。在这种情况下,以 .css 结尾的全部文件,都将被提供给 style-loader 和 css-loader。

  • 加载图片
npm install --save-dev file-loader
  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'style-loader',
            'css-loader'
          ]
        },
+       {
+         test: /\.(png|svg|jpg|gif)$/,
+         use: [
+           'file-loader'
+         ]
+       }
      ]
    }
  };

现在,当你 import MyImage from './my-image.png',该图像将被处理并添加到 output 目录,_并且_ MyImage 变量将包含该图像在处理后的最终 url。当使用 css-loader 时,如上所示,你的 CSS 中的 url('./my-image.png') 会使用类似的过程去处理。loader 会识别这是一个本地文件,并将 './my-image.png' 路径,替换为输出目录中图像的最终路径。html-loader 以相同的方式处理 <img src="./my-image.png" />

  • 加载字体 配置 test
+       {
+         test: /\.(woff|woff2|eot|ttf|otf)$/,
+         use: [
+           'file-loader'
+         ]
+       }

通过配置好 loader 并将字体文件放在合适的地方,你可以通过一个 @font-face 声明引入。本地的 url(...) 指令会被 webpack 获取处理,就像它处理图片资源一样

src/style.css

+ @font-face {
+   font-family: 'MyFont';
+   src:  url('./my-font.woff2') format('woff2'),
+         url('./my-font.woff') format('woff');
+   font-weight: 600;
+   font-style: normal;
+ }

  .hello {
    color: red;
+   font-family: 'MyFont';
    background: url('./icon.png');
  }
  • 加载数据 此外,可以加载的有用资源还有数据,如 JSON 文件,CSV、TSV 和 XML。类似于 NodeJS,JSON 支持实际上是内置的,也就是说 import Data from './data.json' 默认将正常运行。要导入 CSV、TSV 和 XML,你可以使用 csv-loader 和 xml-loader。让我们处理这三类文件
npm install --save-dev csv-loader xml-loader

webpack.config.js

+       {
+         test: /\.(csv|tsv)$/,
+         use: [
+           'csv-loader'
+         ]
+       },
+       {
+         test: /\.xml$/,
+         use: [
+           'xml-loader'
+         ]
+       }
  • 全局资源 上述所有内容中最出色之处是,以这种方式加载资源,你可以以更直观的方式将模块和资源组合在一起。无需依赖于含有全部资源的 /assets 目录,而是将资源与代码组合在一起。例如,类似这样的结构会非常有用:
- |- /assets
+ |– /components
+ |  |– /my-component
+ |  |  |– index.jsx
+ |  |  |– index.css
+ |  |  |– icon.svg
+ |  |  |– img.png

这种配置方式会使你的代码更具备可移植性,因为现有的统一放置的方式会造成所有资源紧密耦合在一起。假如你想在另一个项目中使用 /my-component,只需将其复制或移动到 /components 目录下。只要你已经安装了任何扩展依赖(external dependencies),并且你已经在配置中定义过相同的 loader,那么项目应该能够良好运行。

但是,假如你无法使用新的开发方式,只能被固定于旧有开发方式,或者你有一些在多个组件(视图、模板、模块等)之间共享的资源。你仍然可以将这些资源存储在公共目录(base directory)中,甚至配合使用 alias 来使它们更方便 import 导入

管理输出

调整配置。我们将在 entry 添加 src/print.js 作为新的入口起点(print),然后修改 output,以便根据入口起点名称动态生成 bundle 名称:

webpack.config.js

  const path = require('path');

  module.exports = {
-   entry: './src/index.js',
+   entry: {
+     app: './src/index.js',
+     print: './src/print.js'
+   },
    output: {
-     filename: 'bundle.js',
+     filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

结果会生成 print.bundle.js 和 app.bundle.js 文件 这也是我们在 index.html 文件中指定的文件名称相对应。如果你在浏览器中打开index.html 就可以看到点击按钮会发生什么

但是 如果我们更改了我们的一个入口起点的名称 甚至添加一个新的名称 生成的重命名在一个构建中,但是我们的index。html 文件仍会引用新的名字 我们用 HtmlWebpackPlugin 来解决

  • 设定 HtmlWebpackPlugin 安装
npm install --save-dev html-webpack-plugin

webpack.congif.js 添加

+ const HtmlWebpackPlugin = require('html-webpack-plugin');  //大小写一定不能错


+   plugins: [
+     new HtmlWebpackPlugin({
+       title: 'Output Management'
+     })
+   ],
  • 清理 dist 文件夹 去除编译多余的文件 安装
npm install clean-webpack-plugin --save-dev
  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const CleanWebpackPlugin = require('clean-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
    plugins: [
+     new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Output Management'
      })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };
  • Manifest 通过 manifest,webpack 能够对「你的模块映射到输出 bundle 的过程」保持追踪

开发

  • 使用 source map 更容易地追踪错误和警告
  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const CleanWebpackPlugin = require('clean-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
+   devtool: 'inline-source-map',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Development'
      })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };
  • 自动编译 webpack's Watch Mode webpack-dev-server webpack-dev-middleware

  • 使用观察模式 package.json

    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
+     "watch": "webpack --watch",
      "build": "webpack"
    },
  • 使用 webpack-dev-server npm install webpack-dev-server

配置

module.export = {
    + devServer: {
    +     contentBase: path.join(__dirname, "dist"),
    +     port: 8080,
    +     open: true
    + }
}

package.json

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "watch": "webpack --watch",
    "build": "webpack",
+   "start": "webpack-dev-server" 
  }

启动: npm run start

  • 使用webpack-dev-middleware

安装:

npm install --save-dev express webpack-dev-middleware

配置webpack.config.js文件确保中间件能够正确启用

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const CleanWebpackPlugin = require('clean-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
    devtool: 'inline-source-map',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Output Management'
      })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
+     publicPath: '/'
    }
  };

server.js (与src同级)

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));

// Serve the files on port 3000.
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

配置 package.json文件

 "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "watch": "webpack --watch",
      "start": "webpack-dev-server --open",
+     "server": "node server.js",
      "build": "webpack"
    },

启动 npm run server 打开 localhost:3000 可以打开

  • 调整文本编辑器 Sublime Text 3 - 在用户首选项(user preferences)中添加 atomic_save: "false"IntelliJ - 在首选项(preferences)中使用搜索,查找到 "safe write" 并且禁用它。 Vim - 在设置(settings)中增加 :set backupcopy=yesWebStorm- 在 Preferences > Appearance & Behavior > System Settings 中取消选中 Use "safe write"

模块热替换HMR

(HMR 不适用与生产环境,这意味着它应当只在开发环境使用)

  • 启用HMR 目的是更新 webpack-dev-server的配置 和使用webpack内置的HMR 插件
如果你使用了 webpack-dev-middleware 而没有使用 webpack-dev-server,请使用 webpack-hot-middleware package 包,以在你的自定义服务或应用程序上启用 HMR。
  • (1)在devServer属性中添加hot属性并赋值为true (2)引入两个插件到webpacke配置文件: (3)在入口文件底部添加代码 使得在所有代码发生变化时,能够通知webpack webpack.config.js
  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const webpack = require('webpack');

  module.exports = {
    entry: {
-      app: './src/index.js',
-      print: './src/print.js'
+      app: './src/index.js'
    },
    devtool: 'inline-source-map',
    devServer: {
      contentBase: './dist',
+     hot: true
    },
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement'
      }),
+     new webpack.NamedModulesPlugin(),
+     new webpack.HotModuleReplacementPlugin()
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

正常使用我们的热更新 npm run dev 启用我们的 dev-server

index.js 入口文件

  import _ from 'lodash';
  import printMe from './print.js';

  function component() {
    var element = document.createElement('div');
    var btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;

    element.appendChild(btn);

    return element;
  }

  document.body.appendChild(component());
+
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }
  • 热更新的修改样式表
npm install --save-dev style-loader css-loader

配置 webpack配置,让loader生效 webpack.config.js

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const webpack = require('webpack');

  module.exports = {
    entry: {
      app: './src/index.js'
    },
    devtool: 'inline-source-map',
    devServer: {
      contentBase: './dist',
      hot: true
    },
+   module: {
+     rules: [
+       {
+         test: /\.css$/,
+         use: ['style-loader', 'css-loader']
+       }
+     ]
+   },
    plugins: [
      new CleanWebpackPlugin(['dist'])
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement'
      }),
      new webpack.HotModuleReplacementPlugin()
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

热加载样式表 与将其导入模块一样简单: 新建 styles.css 文件

body{
  background: blue;
}

index.js 入口文件导入

import './styles.css'

修改样式表 会自动更新 社区还有许多其他 loader 和示例,可以使 HMR 与各种框架和库(library)平滑地进行交互…… React Hot Loader:实时调整 react 组件。 Vue Loader:此 loader 支持用于 vue 组件的 HMR,提供开箱即用体验。 Elm Hot Loader:支持用于 Elm 程序语言的 HMR。 Redux HMR:无需 loader 或插件!只需对 main store 文件进行简单的修改。 Angular HMR:No loader necessary! A simple change to your main NgModule file is all that's required to have full control over the HMR APIs.没有必要使用 loader!只需对主要的 NgModule 文件进行简单的修改,由 HMR API 完全控制。

  • 搭建生产环境
  1. 安装 webpack-merge:
npm install webpack- merge --save-dev
  1. 建立三个配置文件: 自定义名字 webpack.common.sj 等 dev prod webpack.base.conf.js webpack.dev.conf.js webpack.prod.conf.js

其中 webpack.base.conf.js表示最基础的配置信息,开发环境和生产环境都需要配置的信息 比如 entry output module等 在另外两个文件中配置一些 对应环境特有的信息,然后通过webpack-merge模块 与 webpack.base.conf.js 整合 3. 添加npm script

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

此外建议设置mode属性 因为生产环境下回自动开启代码压缩 免去了配置的麻烦

  webpack-demo
 |- package.json
- |- webpack.config.js
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
 |- /dist
 |- /src
   |- index.js
   |- math.js
 |- /node_modules
webpack.common.js

+ const path = require('path');
+ const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+ module.exports = {
+   entry: {
+     app: './src/index.js'
+   },
+   plugins: [
+     new CleanWebpackPlugin(['dist']),
+     new HtmlWebpackPlugin({
+       title: 'Production'
+     })
+   ],
+   output: {
+     filename: '[name].bundle.js',
+     path: path.resolve(__dirname, 'dist')
+   }
+ };
webpack.dev.js

+ const merge = require('webpack-merge');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+   devtool: 'inline-source-map',
+   devServer: {
+     contentBase: './dist'
+   }
+ });
webpack.prod.js
+ const merge = require('webpack-merge');
+ const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+   plugins: [
+     new UglifyJSPlugin()
+   ]
+ });

配置package.json

 "scripts": {
-     "start": "webpack-dev-server --open",
+     "start": "webpack-dev-server --open --config webpack.dev.js",
-     "build": "webpack"
+     "build": "webpack --config webpack.prod.js"
   },

到此 就基本完成了 各类环境的配置 注意 在生产环境中 有个UglifyJSPlugin 代码压缩

另外还有 BabelMinifyWebpackPlugin ClosureCompilerPlugin 如果决定尝试以上这些,只要确保新插件也会按照 tree shake 指南中所陈述的,具有删除未引用代码(dead code)的能力足矣。

  • source map 鼓励使用 因为它们对调试源码(debug)和运行基准测试(benchmark tests)很有帮助。虽然有如此强大的功能,然而还是应该针对生成环境用途,选择一个构建快速的推荐配置(具体细节请查看 devtool)。对于本指南,我们将在生产环境中使用 source-map 选项,而不是我们在开发环境中用到的 inline-source-map
webpack.prod.js

 const merge = require('webpack-merge');
 const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
 const common = require('./webpack.common.js');

 module.exports = merge(common, {
+   devtool: 'source-map',
   plugins: [
-     new UglifyJSPlugin()
+     new UglifyJSPlugin({
+       sourceMap: true
+     })
   ]
 });
避免在生产中使用 inline-*** 和 eval-***,因为它们可以增加 bundle 大小,并降低整体性能。
  • 指定环境 许多library将通过 process.env.NODE_ENV环境变量关联,以决定library中应该引用哪些内容。例如 当不处于生产环境中时,某些library为了使调试变得容易, 可能会添加额外的日志记录(log)和测试(test)其实当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量:
webpack.prod.js
+ const webpack = require('webpack');
  const merge = require('webpack-merge');
  const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
  const common = require('./webpack.common.js');

  module.exports = merge(common, {
    devtool: 'source-map',
    plugins: [
      new UglifyJSPlugin({
        sourceMap: true
-     })
+     }),
+     new webpack.DefinePlugin({
+       'process.env.NODE_ENV': JSON.stringify('production')
+     })
    ]
  });
技术上讲,NODE_ENV 是一个由 Node.js 暴露给执行脚本的系统环境变量。通常用于决定在开发环境与生产环境(dev-vs-prod)下,服务器工具、构建脚本和客户端 library 的行为。然而,与预期不同的是,无法在构建脚本 webpack.config.js 中,将 process.env.NODE_ENV 设置为 "production",请查看 #2537。因此,例如 process.env.NODE_ENV === 'production' ? '[name].[hash].bundle.js' : '[name].bundle.js' 这样的条件语句,在 webpack 配置文件中,无法按照预期运行。

如果你正在使用像 react 这样的 library,那么在添加此 DefinePlugin 插件后,你应该看到 bundle 大小显著下降。还要注意,任何位于 /src 的本地代码都可以关联到 process.env.NODE_ENV 环境变量,所以以下检查也是有效的:

src/index.js 检测

  import { cube } from './math.js';
+
+ if (process.env.NODE_ENV !== 'production') {
+   console.log('Looks like we are in development mode!');
+ }

  function component() {
    var element = document.createElement('pre');

    element.innerHTML = [
      'Hello webpack!',
      '5 cubed is equal to ' + cube(5)
    ].join('\n\n');

    return element;
  }

  document.body.appendChild(component());
  • tips
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
注意使用 引入必须用 {} 下面new CleanWebpackPlugin(), 里面试一个对象option 默认是空

注意按照 需要的依赖 loader和Plugin 如果报错是

ERROR in app.bundle.js from UglifyJs
Unexpected token: name «src_element», expected: punc «;» [app.bundle.js:17705,4]

尝试 babel版本降低

npm install --save-dev babel-loader@7

还是出错

当项目中用了es6、jsx之类的,就必须要先通过babel转换一下,再交给webpack去打包。

需要先安装babel: npm install babel-loader babel-core babel-preset-es2015 --save-dev 然后在webpack.config.js的同级目录下,创建一个 .babelrc 的文件,注意:babelrc 前面有个点

文件里面添加这样一句话:

1 { "presets": ["es2015"] }

但是还是报错 如果你有什么解决方法请留言 联系,感谢。

  • Split CSS 正如在管理资源中最后的 加载 CSS 小节中所提到的,通常最好的做法是使用 ExtractTextPlugin 将 CSS 分离成单独的文件。在插件文档中有一些很好的实现例子。disable 选项可以和 --env 标记结合使用,以允许在开发中进行内联加载,推荐用于热模块替换和构建速度。

  • CLI 替代选项 以上描述也可以通过命令行实现。例如,--optimize-minimize标记将在后台引用UglifyJSPlugin。和以上描述的 DefinePlugin实例相同,--define process.env.NODE_ENV="'production'" 也会做同样的事情。并且,webpack -p将自动地调用上述这些标记,从而调用需要引入的插件。

这些简便方式虽然都很不错,但是我们通常建议只使用配置方式,因为在这两种场景中下,配置方式能够更好地帮助你了解自己正在做的事情。配置方式还可以让你更方便地控制这两个插件中的其他选项。

性能优化 TreeShaing

通常配置package.json文件中的 sideEffects属性 可以指定哪些文件需要移除多余代码 如果sideEffects设置为false 那么表示文件的未使用的代码可以放心移除 如果有些文件的冗余代码不能移除。可以尝试设置sideEffects属性为一个数组,数组内容为文件的路径字符串 TreeShaing 会移除你在 js文件中使用不到的代码

压缩输出 指定无副作用的文件之后,设置mode为"production",再次构建代码,可以发现未使用到的代码已经被移除。

  • Tips
  • module.rules属性中,设置include属性以指定哪些文件需要被loader处理。
  • 只使用必要的loader。
  • 保持最新版本。
  • 减少项目文件数
{
  "name": "your-project",
  "sideEffects": false
}
{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js",
    "*.css"
  ]
}

最后,还可以在 module.rules 配置选项 中设置 "sideEffects"。

webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
- }
+ },
+ mode: "production"
};
  • 注意
  • 1 使用 ES2015 模块语法(即 import 和 export)。
  • 2 在项目 package.json 文件中,添加一个 "sideEffects" 入口。
  • 3 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin)。

代码分离

  • 常用的三种分离方式

    • 入口起点: 使用entry配置手动分离代码
    • 防止重复:使用CommonsChunkPlugin 去重和分离 Chunk
    • 动态导入:通过模块的内联函数调用来分离代码
  • 入口分离 entry

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- another-module.js
|- /node_modules
another-module.js

import _ from 'lodash';

console.log(
  _.join(['Another', 'module', 'loaded!'], ' ')
);
webpack.config.js
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

后果就是我们引入的依赖包 会重复打包。比如 lodash

  • 防止重复 解决方案 CommonsChunkPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将之前的示例中重复的 lodash 模块去除
webpack.config.js
  const path = require('path');
+ const webpack = require('webpack');
  const HTMLWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: {
      index: './src/index.js',
      another: './src/another-module.js'
    },
    plugins: [
      new HTMLWebpackPlugin({
        title: 'Code Splitting'
-     })
+     }),
+     new webpack.optimize.CommonsChunkPlugin({
+       name: 'common' // 指定公共 bundle 的名称。
+     })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

这里我们使用 CommonsChunkPlugin 之后,现在应该可以看出,index.bundle.js 中已经移除了重复的依赖模块。需要注意的是,CommonsChunkPlugin 插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。执行 npm run build

代码分离的插件和loader

  • ExtractTextPlugin: 用于将 CSS 从主应用程序中分离。

  • bundle-loader: 用于分离代码和延迟加载生成的 bundle。

  • promise-loader: 类似于 bundle-loader ,但是使用的是 promises。

  • CommonsChunkPlugin插件还可以通过使用显式的 vendor chunks 功能,从应用程序代码中分离 vendor 模块。

  • 动态导入 对于动态到入 webpack 有两个类似的技术 对于动态导入 第一种,优先选择 使用符合 ECMAScript 提案 的 import() 语法。第二种,则是使用 webpack 特定的 require.ensure

  webpack.config.js
    const path = require('path');
- const webpack = require('webpack');
  const HTMLWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: {
+     index: './src/index.js'
-     index: './src/index.js',
-     another: './src/another-module.js'
    },
    plugins: [
      new HTMLWebpackPlugin({
        title: 'Code Splitting'
-     }),
+     })
-     new webpack.optimize.CommonsChunkPlugin({
-       name: 'common' // 指定公共 bundle 的名称。
-     })
    ],
    output: {
      filename: '[name].bundle.js',
+     chunkFilename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };
src/index.js
  const path = require('path');
- const webpack = require('webpack');
  const HTMLWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: {
+     index: './src/index.js'
-     index: './src/index.js',
-     another: './src/another-module.js'
    },
    plugins: [
      new HTMLWebpackPlugin({
        title: 'Code Splitting'
-     }),
+     })
-     new webpack.optimize.CommonsChunkPlugin({
-       name: 'common' // 指定公共 bundle 的名称。
-     })
    ],
    output: {
      filename: '[name].bundle.js',
+     chunkFilename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

注意,在注释中使用了 webpackChunkName。这样做会导致我们的 bundle 被命名为 lodash.bundle.js ,而不是 [id].bundle.js 。想了解更多关于 webpackChunkName 和其他可用选项,请查看 import() 相关文档。让我们执行 webpack,查看 lodash 是否会分离到一个单独的 bundle

由于 import() 会返回一个 promise,因此它可以和 async 函数一起使用。但是,需要使用像 Babel 这样的预处理器和Syntax Dynamic Import Babel Plugin。下面是如何通过 async 函数简化代码

- function getComponent() {
+ async function getComponent() {
-   return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
-     var element = document.createElement('div');
-
-     element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
-     return element;
-
-   }).catch(error => 'An error occurred while loading the component');
+   var element = document.createElement('div');
+   const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
+
+   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+   return element;
  }

  getComponent().then(component => {
    document.body.appendChild(component);
  });

bundle 分析(bundle analysis) 如果我们以分离代码作为开始,那么就以检查模块作为结束,分析输出结果是很有用处的。官方分析工具 是一个好的初始选择。下面是一些社区支持(community-supported)的可选工具:

  • webpack-chart: webpack 数据交互饼图。

  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。

  • webpack-bundle-analyzer: 一款分析 bundle 内容的插件及 CLI 工具,以便捷的、交互式、可缩放的树状图形式展现给用户。

  • 懒加载 在代码分割的时候使用懒加载了

function getComponent() {
  const element = document.createElement('div');
  return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
    const element = document.createElement('div');
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    return element;
  }).catch(error => 'An error occurred while loading the component');
}


// 按需加载,当点击了页面,才会引入lodash,也是单页应用路由懒加载的实现原理
window.addEventListener('click', function(){
 getComponent().then(component => {
    document.body.appendChild(component);
  })
});