开始使用 Webpack 2

1,739 阅读10分钟
原文链接: www.zcfy.cc

Webpack 2 的文档完成就会推出 beta 版本。如果你已经知道怎么去配置它,那么你无需等待文档就可以使用 Webpack 2 了。

什么是 Webpack?

最初的时候,Webpack 仅是一个 JavaScript 的模块打包工具。随着 Webpack 日渐流行,逐渐演变成了前端代码的管理工具(不论是人为故意还是社区推动的)。

以前的运行方式是:标记文件、样式文件和 JavaScript 文件都被分割开的。你必须要独立管理每一个文件,使得所有文件可以正确的运行。

像 Gulp 这样的构建工具可以操作许多不同的预处理器和编译器,不过基本上所有的工作都看成是把一个源文件作为输入,经过处理后生成一个编译后的输出文件。Gulp 做的工作更像是一个任务接着一个任务的进行的,没有从全局的管理上考虑。这就会加重开发者的负担:在生产环境下,开发者需要知道任务在哪里结束,然后需要正确地把所有任务都组装在一起。

Webpack 尝试询问一个大胆的问题来减轻开发者的负担:是否有一个开发过程可以处理所引用到的依赖?我们是否可以简单地用某种方式去写代码,而构建程序会去管理最终所必需使用到的代码?

Webpack 的方式是:如果 Webpack 知道了它,它会把你实际用到的东西打包在构建产物里。

如果过去几年你已经混迹在 web 社区里,你应该知道解决问题更好的方式是:用 JavaScript 构建。Webpack 可以通过 JavaScript 传递依赖让构建过程更加容易。Webpack 的设计真正厉害之处并不在于代码的管理,而是它的管理层 100% 都是由 JavaScript 写的(Node 特性)。Webpack 使得你有能力在写 JavaScript 时候对系统全局有更好的把握和掌控。

换句话说:你不需要为 Webpack 写任何代码,只需要给项目写代码,然后Webpack会自动运行(当然,一些配置文件是少不了的)

简而言之,如果你已经与以下任意一个问题纠结过:

  • 加载依赖项

  • 在生产版本中包含了未使用的 CSS 或者 JS

  • 意外多次加载同一个库

  • 遇到 CSS 和 JavaScript 作用域的问题

  • 在 JavaScript 里使用一个例如 Node/Bower 这样很好的管理系统,或者依赖一个可怕疯狂的配置来正确使用那些模块

  • 需要更好地优化生产出资源,但又怕破坏到某些事情

那么,你可以从 Webpack 获得非常多的帮助。Webpack 可以轻易地解决上面问题,因为它可以通过 JavaScript 管理你的依赖和加载顺序,而不是通过你的开发者头脑。最棒的部分?Webpack 甚至可以直接地运行在服务端,这意味着你可以使用 Webpack 构建渐进加强的网站。

第一步

在这篇指导里,我们会使用 Yarn (brew 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 配置文件 webpack.config.js

'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) 依赖项时,它会解析这些代码,并且打包到最终构建里。接着它会不断递归搜索实际需要的依赖项,直到它到达了“树”的底部。

  1. 从上一步接着,Webpack 把所有东西打包到 output.path 的文件夹里,并使用 output.filename 命名( [name] 表示使用 entry 项的 key)

所以我们的 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 表示 “生产” 模式,这时候的输出文件会被 uglifies/minifies。

它会输出一个 dist/app.bundle.js 文件,同时在控制台里打出当前日期时间的日志。注意,Webpack 会自动知道 'moment' 模块是指向哪里(即使在目录里,你有一个 moment.js 文件,默认情况下 Webpack 还是会优先使用 moment Node 模块)。

多文件工作

你可以通过修改 entry 对象来指定任意数量的输入或者输出点。

多个文件打包在一起

'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.js, 和 dist/contact.bundle.js

先进的自动打包

如果你将应用分开打包到多个 output 文件里(如果你的应用有非常多的 JS 不需要在前期加载,这样做是非常有效的),这里面是有可能会出现冗余代码的,因为 Webpack 是独立解析每个文件的依赖的。幸运的是,Webpack 已经有个内置的 CommonsChunk 插件处理这个问题:

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

现在,纵观所有 output 文件,如果你有任何模块需要加载 2 次或者更多次(由 minChunks 设置),这些模块会被打包在 commons.js 里,那么你就可以让它在客户端里缓存起来。当然,这会导致产生一个额外的头部请求,但是你可以阻止客户端多次下载同一个库。在许多场景下,会有一个速度的净收益。

开发环境

Webpack 实际上有自己的开发服务器,因此无论你是想开发一个静态网站还是做一个前端的原型,它都可以满足你。如果想这样开发,只需要添加一个 devServer 对象到 webpack.config.js 就可以:

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
  },
};

在终端执行:

webpack-dev-server

现在你的服务器跑在 localhost:8080。_注意 script 标签里的 /assets 路径由 _output.publicPath_ 决定——你可以随便命名(如果你需要一个 CDN,这就非常有用了)_。

你无需刷新浏览器,Webpack 会热加载任何 JavaScript 的改变。但是,任何对 webpack.config.js 文件的改变都需要重启服务器才会生效。

全局调用方法

需要使用在全局作用域下的函数?只需要在 webpack.config.jsoutput.library 进行简单的设置:

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

它会把你的打包文件捆绑在 window.myClassName 实例上。当设置了声明作用域时,你可以在入口处调用这个方法(更多的设置可在文档中查询)。

Loaders

直到现在,我们仅仅处理了 JavaScript 文件。从 JavaScript 开始是很重要的,因为 JavaScript 是 Webpack 唯一识别的语言。但实际上,只要是用 JavaScript 传递的文件,我们都可以使用 Loaders 来处理任何种类的文件。

loader 可以是像 Sass 这样的预处理器,也可以是像 Babel 这样的编译器。在 NPM 里,它们通常被命名为 *-loader,例如:sass-loader 或者 babel-loader

Babel + ES6

如果你想在项目里通过 Babel 使用 ES6,我们首先需要本地安装合适的 loaders。

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 的老用户需要注意一点:Loaders 的核心理念还是保持一致的,但是它的语法有所改善。直到文档完成了,我们才能知道准确合适的语法。

/\.js$/ 这条正则表达式会去搜索以 .js 后缀结束的文件,并通过 Babel 加载。Webpack 使用正则表达式来让你可以完整的控制所有文件,而无需限定在某一个文件扩展名,又或者假定你是使用某种方式进行文件组织的。

CSS + Style Loader

如果我们只想加载应用所需的 CSS,我们同样可以利用 Webpack 做到。如果我们直接在 index.js 文件里引入 CSS:

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"],
      },
      // …
    ],
  },
};

Loaders 会根据数组的逆序运行,也就是说 css-loader 会跑在 style-loader 前面。

你可能会注意到,在生产构建下,CSS 是会被打包到 JavaScript 里的,style-loader 会把你的样式写在 Style 标签里。刚开始看起来是有点奇怪,但是随着深入思考你会觉的这是有意义的。因为你可以在一些网络连接上节省一个头部请求的时间,并且只要你用了 JavaScript 来加载 Dom 节点,这可以有效地减少它自身的 FOUC

你也会发现 Webpack 黑盒的构建产物已经自动地通过将文件打包在一起,把所有的 @import 查询都解析了(而不是依靠 CSS 默认会导致浪费头部请求和加载资源非常慢的 import 功能)。

从 JS 里加载 CSS 是非常神奇的,因为这样你可以用新的方式将 CSS 模块化。也就是说你可以通过 button.js 加载 button.css,而如果 button.js 实际上没有用到,对应的 CSS 也不会被调价到生产构建中。如果你是坚持使用面向组件 CSS 实践方法的,例如:SMACSS 或者 BEM,你会体会到将 CSS 和 标记语言 + JavaScript 更加紧密联系的价值。

CSS + Node 模块

我们可以使用 Webpack 里的 ~ 前缀来引入 Node 模块。假如我们执行了 yarn add normalize.css,那么就可以这么用:

@import "~normalize.css";

这样可以充分利用 NPM 管理第三方样式文件的版本,还可以避免了我们进行复制黏贴。更长远地看,用 Webpack 打包 CSS 相比使用 CSS 默认的 import 有明显的优势,这是因为它可以为客户端消除头部请求以及缓慢的加载时间。

更新:本章节和下一章节为了解答对通过 CSS 模块来简单引入 Node 模块的疑惑,以及提高文章的准确性进行了更新。感谢 Albert Fernández 的帮助。

CSS 模块

你可能已经听说过 CSS 模块,当你使用 JavaScript 构建 Dom 节点,它可以不出意料地运行地非常好,它通过 JavaScript 加载,然后把你的 CSS 类神奇地设置好层叠作用域。如果你准备使用 CSS 模块,可以用 css-loader 来打包(yarn add --dev css-loader):

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

注意:我们通过使用对象字面量的语法给 css-loader 传递选项。你也可以用一个字符串作为简写来传递,这样就会使用默认的选项,就像我们用 style-loader 这样。


在用 Node 模块引入 CSS 模块的时候,需要注意的是你实际上可以抛弃 ~ 来直接引入。但是,当你 @import CSS 时,你可能会遇到一个构建错误。如果你得到是类似 "can’t find ..." 这样的错误,你可以尝试给 webpack.config.js 添加一个 resolve 对象,这样会让 Webpack 对预定的模块顺序有更好的理解。

const path = require("path");

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

我们规定了源文件目录,这样的做话 Webpack 可以解析地更加好。Webpack 会根据模块名首先去寻找我们的源文件目录,然后是安装的 Node 模块目录里(也就是分别用 "src""node_modules" 来代替源文件目录和 Node 模块目录)。

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 月的 bata 版本)...

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,你就可以在 output 目录里发现 app.bundle.css 文件。接着就是非常轻松地像平常一样在 HTML 里添加一个 tag 引入就可以了。

HTML

正如你想到的一样,Webpack 也有一个叫 html-loader 的插件。但是当我们准备用 JavaScript 去加载 HTML 的时候,这里有一点需要注意,我无法通过一个单独的例子就为你下一步做的事情准备好,因为这可能分出无数各种各样地方法。通常来说,你可能是为了在 ReactAngularVue,又或者 Ember 这些大型框架里使用类似 JSXMustache,或者 Handlebars 这样的 JavaScript-flavored 标记语言。又或者你只是在使用一个 HTML 的预处理器,例如:Pug (Jade 的前身)或者 Haml。再者你可能仅仅只是想按字面意思一样把源文件里的 HTML 丢到构建目录里。无论你想做什么,我都是无法假定。

结束语:你可以用 Webpack 加载标记语言文件,但是你需要决定自身的项目架构,这是 Webpack 和我都无法为你做这个决定的。不过根据上述例子作为参照,然后在 NPM 上搜索正确的 loaders,这些对于你来说应该是足够用的了。

用模块的方式思考

为了最大程度的使用 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/

最后的结果出来的是非常简洁,可复用的代码。每个独立的组件通过 import 来引入依赖,再通过 export 来暴露公共接口给其他模块。把上面这些特性与 Babel 和 ES6 结合,你可以使用 JavaScript Classes 来实现更好的模块化,而不需要考虑运行作用域。