Webpack 2 入门教程

阅读 8657
收藏 682
2016-12-01
原文链接:llp0574.github.io

本文写于 Webpack 2 正式发布之前(完善文档阶段),不仅是 Webpack 2 的入门教程,也介绍了 Webpack 是什么。

Webpack 2 将在其文档完成之后正式发布。但这并不意味着不可以开始使用它,如果你知道怎么配置的话。

什么是 Webpack?

简单来说,Webpack 就是一个针对 JavaScript 代码的模块打包工具。但是自发布以来,它演变成了一个针对所有前端代码的管理工具(不管是其本身有意还是社区的意愿)。

旧的任务运行工具处理方式:HTML、CSS 和 JavaScript 都是分离的。必须分别对每一项进行管理,并且还要确保所有东西正确地部署到生产环境。

Gulp 这样的任务运行工具可以操作很多不同的预处理器和编译器,但是在所有情况下,它都需要接收一个源码输入并将其处理成一个编译好的输出。然而,它是在不关心整个系统的情况下逐个去处理的。这就是开发者的负担了:检查任务运行工具有无遗漏的地方,并为所有改动的部分找到正确的方式,将它们在生产环境上协调一致。

Webpack 通过一个大胆的询问试图减轻开发者的负担:如果开发过程的某个部分可以自己管理依赖会怎么样?如果我们可以以这样一种方式来简单地写代码:构建过程仅基于最后所需要的东西来管理它自己,会怎么样?

Webpack 处理方式:如果是 Webpack 知道的代码,那么它就只会打包实际在生产环境当中使用的部分。

如果你是过去几年里 Web 社区当中的一员,那么你肯定已经知道首选解决问题的方法:使用 JavaScript 构建。所以 Webpack 试图通过用 JavaScript 传递依赖来使构建过程变得更加容易。但是它设计的精妙之处并不在于简单的代码管理部分,而在于它的管理层面是百分百有效的 JavaScript(还有 Node 特性)。Webpack 使你有机会写出一些对整体系统有更好感知的 JavaScript 代码。

换句话说:你不是为了 Webpack 写代码,而是为了你的项目写代码。而且 Webpack 在保持进步(当然包括某些配置)。

总而言之,如果你曾经挣扎于下面这些情况中的其中之一:

  • 不小心将一些不需要的样式表或者 JS 库引入生产环境,导致项目体积变大
  • 遇到作用域问题 - 不管是来自 CSS 还是 JavaScript
  • 不停寻找一个好的系统好让你可以在 JavaScript 代码里使用 Node 或 Bower 的模块,或者依赖一系列疯狂的后端配置来正确地使用那些模块
  • 需要优化资源分发机制却又担心会破坏掉某些东西

…那么你就可以受益于 Webpack 了。它通过让 JavaScript 取代开发者的大脑来关心依赖和加载顺序,轻松地解决了上面这些问题。最好的部分是什么?Webpack 甚至可以在服务端无缝运行,这意味着你仍然可以使用 Webpack 来构建渐进式增强的网站。

第一步

在这篇教程里我们将使用 Yarnbrew install yarn)来替代 npm,但这完全取决于你自己,它们做的是同样的事情。打开到项目文件夹,在命令行窗口运行下面的命令添加 Webpack 2 到全局包和本地项目里:

yarn global add webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.10
yarn add --dev webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.10

然后在根目录新建一个 webpack.config.js 文件用来声明 Webpack 的配置:

'use strict';
const webpack = require("webpack");
module.exports = {
  context: __dirname + "/src",
  entry: {
    app: "./app.js",
  },
  output: {
    path: __dirname + "/dist",
    filename: "[name].bundle.js",
  },
};

注意:__dirname指的是根目录。

还记得 Webpack “知道”项目里发生了什么吗?它是通过读取你的代码知道的(不用担心,它签署了一份保密协议)。Webpack 基本做了下面这些事情:

  1. context 对应的文件夹开始…
  2. …寻找 entry 里所有的文件名…
  3. …然后读取它们的内容。在解析代码时,每一个通过 import(ES6) 或 require()(Node) 引入的依赖都会被打包到最终的构建结果当中。它会接着搜索那些依赖,以及那些依赖的依赖,直到“依赖树”的叶子节点 — 只打包它所需要的依赖,没有其他的东西。
  4. 接着,Webpack 将所有东西打包到 output.path 对应的文件夹里,使用 output.filename 对应的命名模板来命名([name]entry 里的对象键值所替代)

所以如果 src/app.js 文件看起来像下面这样的话(假设提前运行了 yarn add --dev moment):

'use strict';
import moment from 'moment';
var rightNow = moment().format('MMMM Do YYYY, h:mm:ss a');
console.log( rightNow );
// "October 23rd 2016, 9:30:24 pm"

接着运行:

webpack -p

注意:p 标记表示“生产(production)”模式并且会压缩或丑化(uglify/minify)输出。

然后它将输出一个 dist/app.bundle.js 文件,作用就是打印出当前日期和时间到控制台。注意到 Webpack 自动知道了 'moment' 指的是什么(但如果已经有一个 moment.js 在你的目录当中,那么 Webpack 默认就会优先使用这个而不是 moment 的 Node 模块)。

处理多个文件

你只需要通过修改 entry 对象就可以指定任意数量所期望的 entry 或 output 点。

多个文件,打包在一起

'use strict';
const webpack = require("webpack");
module.exports = {
  context: __dirname + "/src",
  entry: {
    app: ["./home.js", "./events.js", "./vendor.js"],
  },
  output: {
    path: __dirname + "/dist",
    filename: "[name].bundle.js",
  },
};

所有文件会按数组顺序一起打包到 dist/app.bundle.js 一个文件当中。

多个文件,多个输出

const webpack = require("webpack");
module.exports = {
  context: __dirname + "/src",
  entry: {
    home: "./home.js",
    events: "./events.js",
    contact: "./contact.js",
  },
  output: {
    path: __dirname + "/dist",
    filename: "[name].bundle.js",
  },
};

另外,你还可以选择打包成多个 JS 文件来将应用拆解成几个部分。像上面这样做就可以打包成三个文件:dist/home.bundle.jsdist/events.bundle.jsdist/contact.bundle.js

进阶自动打包

如果你正在将应用拆解,打包成多个 output 的话(如果应用的某部分有大量不需要提前加载的 JS 的话,这样做会很有用),那么在这些文件里就有可能出现重复的代码,因为在解决依赖问题的时候它们是互相不干预的。幸好,Webpack 有一个内建插件 CommonsChunk 来处理这个问题:

module.exports = {
  // …
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: "commons",
      filename: "commons.js",
      minChunks: 2,
    }),
  ],
// …
};

现在,在 output 的文件里,如果有任意模块加载了两次或更多(通过 minChunks 设置该值),它就会被打包进一个叫 commons.js 的文件里,后面你就可以在客户端缓存这个文件了。当然,这肯定会造成一次额外的请求,但是却避免了客户端多次下载相同库的问题。所以在很多场景下,这都是提升速度的举措。

开发

实际上 Webpack 有它自己的开发服务器,所以无论你正在开发一个静态网站,或者只是正在原型化前端阶段,这个服务器都是完美可用的。想要运行它,只需要在 webpack.config.js 里添加一个 devServer 对象:

module.exports = {
  context: __dirname + "/src",
  entry: {
    app: "./app.js",
  },
  output: {
    filename: "[name].bundle.js",
    path: __dirname + "/dist/assets",
    publicPath: "/assets",            // New
  },
  devServer: {
    contentBase: __dirname + "/src",  // New
  },
};

现在新建一个 src/index.html 文件,加入下面这行:

然后在命令行中,运行:

webpack-dev-server

服务器现在就运行在了 localhost:8080 上。注意 script 标签中的 /assets 对应的是 output.publicPath 的值 - 可以随便填成你想要的命名(如果需要一个 CDN,这就很有用了)。

当你更改 JavaScript 代码的时候,Webpack 就会实时更新页面而无需手动刷新浏览器。但是,任何对 webpack.config.js 的更改都需要重启服务器才可以生效。

全局可访问方法

需要在全局命名空间里使用某些自己的方法吗?只需简单地在 webpack.config.js 里设置 output.library

module.exports = {
  output: {
    library: 'myClassName',
  }
};

…这样就会把打包结果绑定到一个 window.myClassName 实例上。所以使用这种命名作用域,就可以调用 entry 点里面的方法了(可以阅读文档获取更多关于这个配置的信息)。

Loaders

目前为止,我们所讲到的都是关于 JavaScript 代码的使用。从 JavaScript 代码开始是非常重要的,因为这是 Webpack 唯一使用的语言。我们可以处理任何文件类型,只要将它们传进 JavaScript 代码中。这个功能用 Loaders 来实现。

一个 loader 可以指向一个像 Sass 的预处理器,或者像 Babel 的编译器。在 NPM 中,它们通常是像 sass-loaderbabel-loader 这样命名为 *-loader

Babel + ES6

如果我们想要在项目中通过 Babel 来使用 ES6,首先要在本地正确地安装一些 loader:

yarn add --dev babel-loader babel-core babel-preset-es2015

…然后把它们添加进 webpack.config.js 好让 Webpack 知道哪里使用它们。

module.exports = {
  // …
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [{
          loader: "babel-loader",
          options: { presets: ["es2015"] }
        }],
      },
      // Loaders for other file types can go here
    ],
  },
  // …
};

一个给 Webpack 1 用户的提示:Loaders 的核心概念仍然是一样的,但语法上做了改进。在他们完成文档之前这可能不是确切的首选语法。

这样做就可以为 /\.js$/ 正则表达式寻找以 .js 结尾的文件,最后通过 Babel 编译加载。Webpack 依赖正则表达式给予你完整的控制 - 但它不会限制你的文件后缀,或者假设你的代码必须以某种特定形式组织起来。举个例子:也许你的 /my_legacy_code/ 文件夹里的代码不是用 ES6 写的,那么你就可以把上面的 test 修改为 /^((?!my_legacy_code).)*\.js$/,这样就可以绕过这个文件夹,其余文件用 Babel 编译。

CSS + Style Loader

如果我们只想加载应用需要的 CSS,也可以那么做。假设有一个 index.js 文件,在里面引入:

import styles from './assets/stylesheets/application.css';

就会得到一个错误:You may need an appropriate loader to handle this file type。记住 Webpack 只能读取 JavaScript,所以我们必须安装正确的 loader:

yarn add --dev css-loader style-loader

然后在 webpack.config.js 里添加一个规则:

module.exports = {
  // …
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      // …
    ],
  },
};

这些 loader 会以数组逆序运行。这意味着 css-loader 会在 style-loader 之前运行。

你可能注意到甚至在生产构建的结果中,也把 CSS 打包进了 JavaScript 里面,并且 style-loader 手动地将样式写进了 中。乍一看这可能有点奇怪,但当你考虑足够多的时候就会慢慢发现这其实是有道理的。你保存了一个头部请求(在某些连接上节省宝贵的时间),并且如果你用 JavaScript 来加载 DOM,这么做基本上就消除了它自身的无样式闪屏问题。

还注意到 Webpack 已经通过把所有文件打包成一个从而自动解决了所有的 @import 查询问题(比起依赖 CSS 默认的引入导致不必要的头部请求和缓慢的资源加载,这么做显然更好)。

从 JS 里加载 CSS 相当爽,因为你可以用一种强有力的新方式去模块化 CSS 代码了。假设你只通过 button.js 加载了 button.css,这就意味着如果 button.js 没有实际用到的话,它的 CSS 也不会打包进我们的生产构建结果。如果你坚持使用像 SMACSS 或者 BEM 那样的面向组件的 CSS,就会知道把 CSS 和 HTML + JavaScript 代码放更近的价值了。

CSS + Node modules

我们可以在 Webpack 里用 Node 的 ~ 前缀去引入 Node Modules。假设我们提前运行了 yarn add normalize.css,就可以这么用:

@import "~normalize.css";

这样就可以全面使用 NPM 来管理第三方样式库(版本及其他)而对我们而言就无需复制粘贴了。更进一步的是,Webpack 打包 CSS 比使用默认的 CSS 引入有着显而易见的优势,让客户端远离不必要的头部请求和缓慢的资源加载。

更新:这个部分和下面的部分为了更准确都进行了更新,不用再困扰于使用 CSS Modules 去简单地引入 Node Modules 了。感谢 Albert Fernández 的帮助!

CSS Modules

你可能已经听说过 CSS Modules,它将 CSS(Cascading Style Sheets)里的 C(Cascading)给提出来了。它只在用 JavaScript 构建 DOM 的时候使用有最佳效果,但本质上来说,它巧妙地将 CSS 在加载它的 JavaScript 里作用域化了(点击这个链接学习更多相关知识)。如果你计划使用它,CSS Modules 对应的 loader 是 css-loaderyarn add --dev css-loader):

module.exports = {
  // …
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          "style-loader",
          { loader: "css-loader", options: { modules: true } }
        ],
      },
      // …
    ],
  },
};

注意:对于 css-loader 我们使用了展开的对象语法来为它添加配置。你可以写简单的字符串代表使用默认配置,style-loader 就还是这么做的。


值得注意的是实际上在使用 CSS Modules 引入 Node Modules 的时候可以去掉 ~ 符号(如 @import "normalize.css";)。但是,当 @import 你自己的 CSS 时可能会遇到错误。如果你得到了 “can’t find ___” 这样的错误,尝试添加一个 resolve 对象到 webpack.config.js 里,好让 Webpack 更好地理解你预期的模块顺序。

const path = require("path");
module.exports = {
  //…
  resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"]
  },
};

首先指定了我们自己的源文件目录,然后是 node_modules。这样子 Webpack 解决起来就会处理得更好一些,按照那个顺序先找我们的源文件目录,然后是已安装的 Node Modules(分别用你自己的源码和 Node Modules 目录替换其中的 srcnode_modules)。

Sass

想用 Sass?没问题,安装:

yarn add --dev sass-loader node-sass

然后添加另一条规则:

module.exports = {
  // …
  module: {
    rules: [
      {
        test: /\.(sass|scss)$/,
        use: [
          "style-loader",
          "css-loader",
          "sass-loader",
        ]
      }
      // …
    ],
  },
};

接下来当 JavaScript 调用 import 引入一个 .scss.sass 文件时,Webpack 就会做它该做的事情了。

分开打包 CSS

或许你正在处理渐进式增强的网站,又或许因为其他的原因你需要一个分离的 CSS 文件。我们可以简单地实现,只需要在配置里用 extract-text-webpack-plugin 替换掉 style-loader,而无需改变其他任何代码。以 app.js 文件为例:

import styles from './assets/stylesheets/application.css';

本地安装插件(我们需要这个的测试版本,2016年10月发布):

yarn add --dev extract-text-webpack-plugin@2.0.0-beta.4

添加到 webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
  // …
  module: {
    rules: [
      {
        test: /\.css$/,
        loader:  ExtractTextPlugin.extract({
          loader: 'css-loader?importLoaders=1',
        }),
      },
      // …
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      filename: "[name].bundle.css",
      allChunks: true,
    }),
  ],
};

现在运行 webpack -p 的时候就可以看到一个 app.bundle.css 文件出现在 output 目录里了。像往常一样简单地添加一个 标签到 HTML 文件里就可以了。

HTML

你可能已经猜到,Webpack 还有一个 [html-loader](https://github.com/webpack/html-loader) 插件。但是,当我们开始用 JavaScript 加载 HTML 的时候,这其实是一个可以分支成不同方法的地方,而且我想不到一个单独的简单示例可以覆盖所有下一步操作的可能性。通常,你可能会在用 ReactAngularVue 或者 Ember 构建的大型系统中加载诸如 JSXMustache 或者 Handlebars 这样偏向 JavaScript 的模板 HTML;或者你可能使用一个像 Pug(以前的 Jade)或者 Haml 这样的 HTML 预处理器;或者你可能只是想简单地将源文件目录里的 HTML 复制到构建结果目录里。不管你想做什么,我没办法假设。

所以我准备在此结束本教程:你可以用 Webpack 加载 HTML,但这一点你必须自己根据你的架构做出决策,不管是我还是 Webpack 都没办法帮到你。不过使用上述例子作为参考并在 NPM 上找到正确的 loader 应该足够让你继续下去了。

从模块角度思考

为了最大程度发挥 Webpack 的作用,你不得不从模块的角度去思考(小、可复用、自包含进程),一件件事情慢慢去做好。这意味着下面这样的东西:

└── js/
    └── application.js   // 300KB of spaghetti code

把它变成:

└── js/
    ├── components/
    │   ├── button.js
    │   ├── calendar.js
    │   ├── comment.js
    │   ├── modal.js
    │   ├── tab.js
    │   ├── timer.js
    │   ├── video.js
    │   └── wysiwyg.js
    │
    └── application.js  // ~ 1KB of code; imports from ./components/

结果是干净且可复用的代码。每个独立的组件取决于导入自身的依赖,并按照它想要的方式导出到其他模块。配合 Babel + ES6 使用,还可以利用 JavaScript Classes 做出更好的模块化,并且不要去想它,作用域只是在起作用。

想知道更多关于模块的内容,可以看这篇 Preethi Kasreddy 写的很棒的文章

延伸阅读

评论