Webpack打包工具

上一篇介绍了Gulp,公司的React项目用的是webpack打包,抽空将webpack的知识点整理进本篇。

先简单介绍一下webpack吧。前端工程模块化,组件化就不多讲了,关键是要解析出这些模块间的依赖关系,并将它们打包合并压缩,插入到html中执行。Webpack就是模块化管理工具,可以实现模块按需加载,预处理,打包等功能。

先从官网上盗下图,webpack就是一个模块打包工具,理顺各模块间的依赖关系后,将它们按照指定的规则打包成静态资源。


从图中可以看出,webpack可以处理不同类型的模块。除js外,还能处理less,css,jade,coffeeScript等。原理是通过Loader来适配各种非js资源,将它们(除图片资源外)全都转换成js模块。

各种不同的模块可能遵循不同的标准,如CommonJS或AMD等,webpack的解析器几乎可以处理所有通用标准的模块。

执行时,webpack采用异步IO和多级缓存策略,总之,你只要知道打包速度很快就是了。

  • 安装与执行
  • 配置文件
  • devServer
  • devTool
  • Loaders
  • Plugins

安装与执行

安装很简单:

npm install webpack -g                  //安装到全局
npm install webpack --save-dev      //安装到本地项目中

体验一下webpack是如何解析依赖关系并打包的,本地目录里新建两个子目录app(开发用目录)和release(发布用目录)。

app目录里新建一个Hello.js:

module.exports = function() {
    var showTxt = document.createElement('div');
    showTxt.textContent = "Hello webpack!";
    return showTxt;
};

app目录里新建一个main.js,依赖Hello.js:

var showTxt = require('./Hello.js');
document.getElementById('root').appendChild(showTxt());

release目录里新建一个index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>My Webpack</title>
</head>
<body>
<div id='root'>
</div>
<script src="bundle.js"></script>
</body>
</html>

该html格式非常简单,但body里加载的bundle.js哪里来的?这就是接下来webpack需要做的。将app下开发者写的main.js和Hello.js打包生成bundle.js,并放进release目录。执行:

webpack ./app/main.js ./release/bundle.js

会发现release目录下多了个bundle.js。观察上述打包过程,webpack先解析main.js,发现它依赖Hello.js,于是将这两个文件打包进指定的release目录,生成bundle.js。用浏览器打开index.html会看到页面正确显示了Hello webpack!。

稍有点经验的开发者都知道起名index.html就不是让你本地双击浏览器打开页面的,而是应该让web服务器读取该页面。webpack提供了web服务器,先安装:

npm install --save-dev webpack-dev-server

安装完后,执行:

webpack-dev-server

现在访问http://localhost:8080就能看到我们第一个webpack页面了。

配置文件

实际项目中,通常不会在终端敲上述命令,而是将各种需求写入webpack的配置文件中,然后一键执行。webpack的配置文件名叫webpack.config.js。看后缀就知道这个配置文件是个js文件,是个node.js模块,依赖于你前面安装的webpack模块,exports出json格式的配置信息对象。例如:

module.exports = {
  entry:  __dirname + "/app/main.js",   // __dirname是Node的全局变量,值为当前目录
  output: {
    path: __dirname + "/release",
    filename: "bundle.js"
  }
}

这个配置文件非常简单,即使不看文档,光阅读代码也能知道都干了些什么。现在终端执行webpack就能实现一键打包啦。

当然通常会在node环境中开发,因此可以将终端命令放入package.json里:

{
  ...  
  "scripts": {
    "start": "NODE_ENV=development webpack",
    "dev": "NODE_ENV=development webpack-dev-server --progress",
    "build": "NODE_ENV=production webpack -p"
  },
  ...
}

-p是Webpack的命令行参数,用于打包成压缩后的production形式。但--progress用于显示Node的构建进度,包括NODE_ENV,这些不属于Webpack知识范畴。

配置文件当然不止上述entry和output这么简单(当然说实话,即使是output也不简单,除了常用的path和filename,官网还提供了丰富的配置项。入门简单,但想要玩的6并不容易)。下面介绍几个重要的配置项。

devServer

安装webpack-dev-server后,如果webpack.config.js里不配置devServer,那web服务器用的都是默认参数。你可以在webpack.config.js自定义服务器,例如:

module.exports = {
  ...
  devServer: {
    contentBase: "./release",
    colors: true,
    historyApiFallback: true,   //针对HTML5 History API
    inline: true
  }
}

官网提供了很多参数,包括如果觉得localhost的127.0.0.1这个IP不爽,可以配置host设为当前PC的IP。包括如果觉得默认8080端口不爽,可以配置port,设成一个随机数Math.floor(Math.random() * 65536);。包括给web服务器配置https指定证书等。

devTool

打包后的代码难以调试,这在开发阶段是无法接受的,因此需要source map。你可以将Source map打包进文件,也可以生成外联.map文件,当然通常推荐生成外联.map文件比较好。Webpack通过devtool来配置source map,具体选项和特性见下图:


打包速度从上到下越来越慢,而且通常调试时我们并不关心列信息,因此cheap模式可以提高效率。推荐开发阶段选择eval-source-map或cheap-module-eval-source-map,生产阶段选择cheap-module-source-map。例如:

module.exports = {
  ...
  devtool: 'eval-source-map',   // Source Maps
}

具体这些参数有什么差异,可以参考这里,整理的挺详细。

Loaders

前面已经简单提过,webpack支持将各种资源文件打包成js文件,依靠的就是Loaders加载器。参照官网的连接,通常项目中会用到多个Loader,它们都被配置在modules下面。Loaders有几个子项:

test:(必须)加载器可以处理的文件后缀名的正则表达式

loader / loaders:(必须)加载器名字。前者用字符串形式指定多个loader。后者用数组形式指定多个loader。(webpack 2.x版本中改名为rules)

include / exclude:(可选)除了正则匹配到的文件,还可以手动添加或排除某些文件

query:(可选)额外的设置选项。(webpack 2.x版本中改名为option)

babel-loader为例,用React开发或PC端用ES6语法兼容老版本浏览器时,需要用Babel转码。我们先将Babel下载下来:

npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react

其中babel-core是Babel的核心包,babel-preset-es2015用于解码ES6语法,babel-preset-react用于解码React的JSX语法(Babel的入门教程可以参照这里

在webpack.config.js里配置Babel加载器:

module.exports = {
  ...
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['es2015','react']
        }
      }
    ]
  }
}

当然Babel的配置项很多,通常推荐将它们单独放在.babelrc这个配置文件中。这样Loaders可以简化成:

module.exports = {
  ...
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      }
    ]
  }
}

Webpack启动babel加载器时,会自动读取.babelrc中的babel配置项。现在你可以把main.js和Hello.js的代码换成React+ES6,重启webpack-dev-server,React项目顺利启动_

再看一个webpack如何打包CSS的例子。Webpack提供了css-loaderstyle-loader,两者配合将css嵌入到js文件中。前者让你能用require(ES6里是import)来加载CSS模块,后者将计算好的样式加入页面中。先下载这两个加载器:

npm install --save-dev style-loader css-loader

在webpack.config.js里配置CSS加载器:

module.exports = {
  ...
  module: {
    loaders: [
      ...
      {
        test: /\.css$/,
        loader: 'style!css' // !惊叹号让同一文件能够使用不同类型的loader
      }
    ]
  }
}

现在你创建好css文件后,就可以在main.js里require(ES6是import)该文件了,例如:

import './hello.css';

再看一个webpack如何打包img的例子。先下载url-loader加载器:

npm install --save-dev url-loader

在webpack.config.js里配置图片加载器:

module.exports = {
  ...
  module: {
    loaders: [
      ...
      {
          test: /\.(png|jpg)$/,
          loader: 'url-loader?limit=8192'
      }
    ]
  }
}

加载器的参数可以直接追加在 loader后面,如上例中指定url-loader的limit参数为8K。(webpack 2.x版本中加载器的参数不必这样用问号拼接在loader后面了,而是新增了use参数)

准备两张图片,一张小于8K,一张大于8K,在Hello.js里加载这两张图片,例如:

![](./small.png)
![](./big.png)

url-loader会判断原始图片大小是否小于limit参数指定的8K,小于的话直接Base64转码后塞入页面,大于的话才作为单独的图片让浏览器发起请求去下载图片。

更多的加载器如何配置(例如sass,less,stylus,postcss-loaderjson-loaderfile-loaderraw-loader,i18n,jshint,coffeeScript等),可以见官网,Webpack提供了丰富的加载器能让你自由选择喜欢的开发工具。

Plugins

插件Plugins和加载器Loaders是不同东西。Loaders用于在打包过程中将不同类型的文件解析成js源代码,而Plugins是用于拓展Webpack功能的。介绍几个常用的plugin:

  • webpack.HotModuleReplacementPlugin
  • webpack.EnvironmentPlugin
  • webpack.DefinePlugin
  • webpack.optimize.CommonsChunkPlugin
  • html-webpack-plugin
  • open-browser-webpack-plugin

webpack.HotModuleReplacementPlugin热加载插件感觉是最实用的插件了。公司老项目习惯于改完代码,执行打包,通常要等2-3秒,然后刷新页面才能看到效果。但React项目由于使用了Webpack装了Hot Module Replacement(HMR)热加载插件,你的对代码的任何修改,都能自动刷新到页面上。总之一句话:用了之后再也回不去了。

配置HMR很简单,在webpack.config.js里配置plguins加载器,再在devServer里加上参数hot:

var webpack = require('webpack');

module.exports = {
  ...
  devServer: {
    hot: true,
    ...
  },
  ...
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}

webpack.EnvironmentPlugin插件可以让我们在client端获取到process.env的环境变量,例如:

var webpack = require('webpack');
module.exports = {
  ...
  plugins: [
    new webpack.EnvironmentPlugin([
      'NODE_ENV'
    ])
  ]
}

定义了变量process.env.NODE_ENV,你可以在代码中用这个变量获取到NODE_ENV的值:

var env = process.env.NODE_ENV;

webpack.DefinePlugin插件可以为项目定义全局变量,例如:

var webpack = require('webpack');
var NODE_ENV = process.env.NODE_ENV;   //从命令行获取NODE_ENV
module.exports = {
  ...
  plugins: [
    new webpack.DefinePlugin({
      PRODUCTION: JSON.stringify(true),
      VERSION: JSON.stringify("5fa3b9"),
      BROWSER_SUPPORTS_HTML5: true,
      TWO: "1+1",
      "typeof window": JSON.stringify("object"),
      "NODE_ENV": JSON.stringify(NODE_ENV)
    })
  ]
}

上面最后定义的NODE_ENV,同webpack.EnvironmentPlugin例子中的功能相同。你可以在代码中直接使用这些全局变量:

if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");

webpack.optimize.CommonsChunkPlugin用于将多个entry内相同的代码打包到一个共用的js里。这里相同的代码,不仅仅指源文件内写的代码,还包括打包后各个bundle.js里相同的编译后的代码。例如entry里两个文件源码有相同的依赖:

// main1.jsx
var React = require('react');
var ReactDOM = require('react-dom');
...

// main2.jsx
var React = require('react');
var ReactDOM = require('react-dom');
..

配置文件内:

module.exports = {
  entry: {
    bundle1: './main1.jsx',
    bundle2: './main2.jsx'
  },
  output: {
    filename: '[name].js'
  },
  ...
  plugins: [
    new webpack.optimize.CommonsChunkPlugin('init.js')
  ]
}

表示将entry里生成的代码中相同的部分抽出,放入新文件init.js中。HTML端需要引入init.js:

<html>
  <body>
    ...
    <script src="init.js"></script>
    <script src="bundle1.js"></script>
    <script src="bundle2.js"></script>
  </body>
</html>

html-webpack-plugin用于生成html文件,open-browser-webpack-plugin用于自动打开浏览器来加载页面。例如:

var HtmlwebpackPlugin = require('html-webpack-plugin');
var OpenBrowserPlugin = require('open-browser-webpack-plugin');

module.exports = {
  ...
  plugins: [
    new HtmlwebpackPlugin({
      title: 'Webpack-demos',
      filename: 'index.html'
    }),
    new OpenBrowserPlugin({
      url: 'http://localhost:8080'
    })
  ]
};

这样你就不用提供index.html,也不用打开浏览器访问localhost:8080了,只要webpack-dev-server启动,这些工作都交由webpack替你完成。

更多插件,例如压缩代码插件,可以到社区里去下载。

最后

Gulp和Webpack都基本可以满足前端自动化构建的任务。但侧重点不同,感觉Gulp偏重于整个过程的控制,用管道将文件连接起来。但这种处理文件的方式并不支持cmd模块化,因此需要借用browserify等工具来处理js间的依赖,最后完成打包。而Webpack正是解决了Gulp不支持处理cmd模块依赖这个痛点,能够梳理清模块间的依赖关系,最后完成打包。所以其实你可以Gulp + Webpack一起上,两者并不排它,用Gulp来控制过程(例如文件移动,压缩等),用Webpack来处理源文件间的依赖关系。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,084评论 7 35
  • 最近在学习 Webpack,网上大多数入门教程都是基于 Webpack 1.x 版本的,我学习 Webpack 的...
    My_Oh_My阅读 8,096评论 40 247
  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,616评论 7 110
  • 我就想这样单纯下去,简简单单,不刻意伪装什么,每天过好自己的生活,努力学习,我始终相信: 单身,说明你有足够的坚持...
    Sun爽阅读 131评论 0 1
  • 慕篱 我匍匐下五体, 大地的清香让我沉迷 我在月夜里屏住呼吸 为了生存为了杀敌 我不能示弱 那会让我丧失勇气 命运...
    慕篱先森阅读 131评论 0 2