webpack 2 入门

10年服务1亿前端开发工程师

webpack 2将在文档完成后将正式发布(webpack 2.2 中文文档)。 但这并不意味着你现在不能开始使用版本2,如果你知道如何配置它的话。

webpack 2.2 已经正式发布! 现在通过 npm 安装 webpack 会自动安装 v2 版本。

什么是 webpack ?

简单来说,webpack 是一个针对 JavaScript 代码的模块打包工具。然而,自从它的发布,它逐渐发展成为所有前端代码的管理工具(不管是其本身有意还是社区的意愿)。

老的任务运行方式:HTML、CSS 和 JavaScript 都是分离的。您必须单独管理,并且还要确保所有东西正确地部署到生产环境。

任务运行器如?Gulp 可以处理许多不同的预处理器和转换器,但在所有情况下,它将获取一个源码输入并将其压缩到已编译好的输出。然而,它不关心整个系统,逐个去处理的。这对开发者来说是一个负担:找到任务运行器中断的位置,并为所有改动的部分找到正确的方式,将它们在生产环境上协调一致。

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

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

如果你过去几年一直是Web社区的一员,那么你肯定已经知道解决问题的首选方法:使用 JavaScript 构建。?因此,webpack 尝试通过用 ?JavaScript 传递依赖关系使构建过程更容易。但是其的精妙之处并不在于简单的代码管理部分;而在于它的管理层面是百分百有效的 JavaScript(还有 Node 特性)webpack使您能够编写有效的JavaScript,更好地了解系统。

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

简而言之,如果您遇到以下任何问题:

  • 无序地加载依赖关系
  • 在生产中包含了未使用的CSS或JS
  • 意外地重复加载(或多次加载)库
  • 遇到来自CSS和JavaScript的作用域问题
  • 不停寻找一个好的系统,好让你可以在 JavaScript 代码里使用 Node 或 Bower 的模块,或者依赖一系列疯狂的后端配置来正确地使用那些模块
  • 需要优化资源分发机制却又担心会破坏掉某些东西

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

第一步

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

npm i -g webpack webpack-dev-server@2
yarn add --dev webpack webpack-dev-server@2

注意:为了简单起见,我们在这里选择直接安装webpack,而不是像推荐的那样使用 NPM 脚本。任何一种方式都可以;文档解释了他们的区别。

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

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

注意:__dirname 指的是 webpack.config.js 所在的目录,在这篇博客中指的是根目录。

记住,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):

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”生产模式并且 uglifies(混淆) / minify(压缩) 输出。

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

处理多个文件

您可以通过仅修改?entry 对象来指定任意数量的 entry 或 output 点。

多个文件,打包在一起

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

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

多个文件,多个输出

const path = require('path');
const webpack = require('webpack');

module.exports = {
  context: path.resolve(__dirname, './src'),
  entry: {
    home: './home.js',
    events: './events.js',
    contact: './contact.js',
  },
  output: {
    path: path.resolve(__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 文件里,如果你有任何模块被加载2次或更多次(通过 minChunks 设置该值),它就会被打包进一个叫 commons.js 的文件中,然后可以在客户端中缓存这个文件。当然,这将导致一次额外的请求,但是避免了客户端多次下载相同的库。因此,在许多情况下,这是提升速度的举措。

手工打包第三方库

如果你喜欢自己做更多的事情,您可以选择采用更人工的方法:

module.exports = {
  entry: {
    index: './index.js',
    vendor: ['react', 'react-dom', 'rxjs'],
  },
  // …
}

在这里,你明确告诉 webpack 导出包含?react, react-dom, 和?rxjs Node 模块的vendor?包,而不是依靠 CommonsChunkPlugin自动这些事情。

开发

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

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

现在新建一个 src/index.html 文件包含下面这行代码:

<script src="/assets/app.bundle.js"></script>

然后在命令行中,运行:

webpack-dev-server

您的服务器现在正在?localhost:8080 上运行。注意 script 标记中的 /assets 对应的是 output.publicPath 的值,因此您可以从任何需要的地方加载资源(如果您使用CDN,则很有用)。

webpack 将热替换任何的 JavaScript 更改,因为您无需刷新浏览器。但是,webpack.config.js 文件的任何更改都需要重新启动服务器才能生效。

全局可访问的方法

需要从全局命名空间中使用某些函数?只需在 webpack.config.js 里设置 ?output.library?即可:

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

…这样将他附加到?window.myClassName 实例上。所以使用这种命名作用域,就可以调用 entry 点里面的方法了(
(您可以在文档中了解有关此设置的更多信息)。

加载器(Loaders)

到现在为止,我们只涉及到的都是使用JavaScript。从 JavaScript 代码开始是非常重要的,因为这是 Webpack 唯一使用的语言。我们几乎可以处理任何文件类型,只要我们把它传递给 JavaScript 。 我们使用?加载器(Loaders) 来实现。

加载器可以指向一个像 Sass 的预处理器,或者像 Babel 的编译器。在 NPM 中,它们通常被命名为*-loader,如sass-loaderbabel-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$/,
        exclude: [/node_modules/],
        use: [{
          loader: 'babel-loader',
          options: { presets: ['es2015'] },
        }],
      },
    
      // Loaders for other file types can go here
    ],
  },
  // …
};

一个给 webpack 1.x用户的提示:加载器(Loaders)的核心概念保持不变,但语法有所改进。

这样做就可以为 /\.js$/ 正则表达式寻找以 .js 结尾的文件,最后通过 Babel 编译加载。webpack 依赖正则表达式给予你完整的控制 – 但它不会限制你的文件后缀,或者假设你的代码必须以某种特定形式组织起来。

如果你发现一个加载器(Loaders)的 mangling 文件,或其他不应该被处理的文件,您可以指定 exclude 选项以跳过这些文件。在这里,我们将我们的 node_modules 文件夹从 Babel 处理中排除 – 我们不需要它。但我们也可以在我们任何项目文件中应用这个,例如,如果我们有一个 my_legacy_code 文件夹。这不会阻止您加载这些文件;相反,你只是让 webpack知道可以直接导入,不处理它们。你可以使用 include?来包含文件(但通常不需要此选项)。

 

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,所以我们必须安装相应的加载器(Loaders):

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 手动地将样式写进了 <head> 中。乍一看这可能有点奇怪,但当你考虑足够多的时候就会慢慢发现这其实是有道理的。你保存了一个头部请求(在某些连接上节省宝贵的时间),并且如果你用 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 更好地理解你预期的模块顺序。

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 就会做它该做的事情了。记住:use 顺序是相反的,所以我们首先加载 Sass ,然后是 CSS 解析器,最后是 Style 加载器(loader) ,将我们解析的CSS加载到页面的 <head>

分开打包 CSS

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

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

本地安装插件(我们需要这个的测试版本):

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

添加到 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 目录里了。像往常一样简单地添加一个 <link> 标签到 HTML 文件里就可以了。

HTML

你可能已经猜到,Webpack 还有一个 html-loader?插件。但是,当我们开始用 JavaScript 加载 HTML 的时候,这其实是一个可以分支成不同方法的地方,而且我想不到一个单独的简单示例可以覆盖所有下一步操作的可能性。通常,你可能会在用 ReactAngularVue 或者 Ember 构建的大型系统中加载诸如 JSXMustache 或者 Handlebars 这样偏向 JavaScript 的模板 HTML;或者你可能使用一个像 Pug(以前的 Jade)这样的 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
    │
    └── index.js  // ~ 1KB of code; imports from ./components/

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

有关模块的更多信息,请参阅Preethi Kasreddy的这篇优秀文章

延伸阅读

原文地址:Getting Started with Webpack 2

部分翻译来自:https://llp0574.github.io/2016/11/29/getting-started-with-webpack2/

赞(0) 打赏
未经允许不得转载:WEB前端开发 » webpack 2 入门

评论 4

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #-49

    options: { presets: [‘es2015’] 应该以 test 用级别才对好嘛。。。 坑啊

    000002年前 (2017-05-23)回复
  2. #-48

    Babel + ES6
    这里exclude不应该放在use里面,而是同级的好嘛。。。坑啊

    000012年前 (2017-06-07)回复
    • 已经修改,谢谢

      2年前 (2017-06-07)回复
  3. #-47

    加强翻译水平

    xiaokang1年前 (2017-07-27)回复

前端开发相关广告投放 更专业 更精准

联系我们

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏