Webpack打包工具

23 Jan

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

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

先从官网上盗下图,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

webpack

会发现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,具体选项和特性见下图:

webpack

打包速度从上到下越来越慢,而且通常调试时我们并不关心列信息,因此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为例,用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里加载这两张图片,例如:

<img src="./small.png" />
<img src="./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来处理源文件间的依赖关系。

评论(1)

Leave a Reply

Your email address will not be published. Required fields are marked *