Webpack简介

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

Why Webpack?

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

  • 模块化,让我们可以把复杂的程序细化为小的文件;
  • 类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的-
    JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;
  • Scss,less等CSS预处理器
  • ...

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。

Webpack与gulp以及grunt的对比区别

  • Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案
  • Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务
  • Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件

基本概念

  • 入口

    • 含义:入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的
    • 用法:
      entry: './path/to/my/entry/file.js'
      //也可以这样写
      entry: {
          pageOne: './src/pageOne/index.js',
          pageTwo: './src/pageTwo/index.js',
          pageThree: './src/pageThree/index.js'
      }
      
  • 出口

    • 含义:output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件
    • 用法:
      output: {
          filename: '[name].js',
          path: __dirname + '/dist',
          publicPath: "/assets/"
      }
      
  • Loader

    • 含义:loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理
    • 用法
      module: {
      rules: [{
          test: /\.css$/,
          use: [{ loader: 'style-loader' },
                {
                  loader: 'css-loader',
                  options: {
                      modules: true
                  }
                }
               ]
          }]
      }
      
  • 插件

    • 含义:loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务
    • 用法:
        plugins: [
           new webpack.optimize.UglifyJsPlugin(),
           new HtmlWebpackPlugin({template: './src/index.html'})
        ]
      

开始使用

  • 创建package.json

在终端中使用npm init命令可以自动创建这个package.json文件

  • 安装

# 全局安装
npm install -g webpack
# 安装到你的项目目录
npm install --save-dev webpack
npm install --save-dev webpack-dev-server
  • 设置npm script

package.json里面编写:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "dev": "webpack-dev-server --open",
    "build": "set NODE_ENV=production && webpack --config ./webpack.production.config.js --progress"
}

不是windows系统build只需这样配置即可:

"build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"

然后就可以在终端使用命令进行调用了:

npm run start
npm run dev
npm run build

配置文件

Webpack拥有很多其它的比较高级的功能(比如说本文后面会介绍的loaders和plugins),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。

当前文件夹的根目录下新建一个名为webpack.config.js的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。

const path = require('path');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
module.exports = {
    entry: {
       //入口
    },
    output: {
       //输出
    },
    module: {
        rules: [
            //打包规则
        ]
    },
    devtool: 'cheap-module-source-map',  //生成Source Maps(使调试更容易)
    //使用webpack-dev-server
    devServer: {
       //本地调试
    },
    plugins: [
    	//插件
  	]
};

externals

如果我们想引用一个库,但是又不想让webpack打包,并且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用,那就可以通过配置externals

例如externals引入jquery后,那么不管在代码中使用

import $ from 'jquery'

还是

var $ = require('jquery');

这些代码都能在浏览器中很好的执行。

externals 支持以下模块上下文(module context)

  • global - 外部 library 能够作为全局变量使用。用户可以通过在 script 标签中引入来实现。这是 externals 的默认设置。
  • commonjs - 用户(consumer)应用程序可能使用 CommonJS 模块系统,因此外部 library 应该使用 CommonJS 模块系统,并且应该是一个 CommonJS 模块。
  • commonjs2 - 类似上面几行,但导出的是 module.exports.default
  • amd - 类似上面几行,但使用 AMD 模块系统。

如果你的代码想运行在Node环境中,那么你需要在external中添加前缀commonjs2或者commonjs

externals:{
  react:'commonjs2 react',
  jquery:'commonjs2 jquery'
}

如果需要requirejs等符合AMD规范的环境中加载,那就要添加amd

externals:{
  react:'amd React',
  jquery:'amd jQuery'
}

如果要在浏览器中运行,那么不用添加什么前缀,默认设置就是global。

externals:{
  react:'React',
  jquery:'jQuery'
}

也可以这样

externals:["React","jQuery"]

生成source maps

通过简单的配置,webpack就可以在打包时为我们生成的source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。

 devtool: 'cheap-module-source-map',  //生成Source Maps(使调试更容易)

在webpack的配置文件中配置source maps,需要配置devtool,它有以下四种不同的配置选项,各具优缺点,描述如下:

devtool选项 配置结果
source-map 在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度;
cheap-module-source-map 在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;
eval-source-map 使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项;
cheap-module-eval-source-map 这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;

构建本地服务器

Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现让你的浏览器监听你的代码的修改,并自动刷新显示修改后的结果,不过它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖

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

以下是它的一些配置选项:

  • contentBase 默认webpack-dev-server会为根文件夹提供本地服务器,如果想为另外一个目录下的文件提供本地服务器,应该在这里设置其所在目录(本例设置到“public"目录)
  • port 设置默认监听端口,如果省略,默认为”8080“
  • inline 设置为true,当源文件改变时会自动刷新页面
  • historyApiFallback 在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html

然后在webpack.config.js中进行配置:

 devServer: {
    contentBase: "./public",//本地服务器所加载的页面所在的目录
    historyApiFallback: true,//不跳转
    inline: true//实时刷新
  } 

在终端使用下面的命令即可启用

webpack-dev-server --open

Loaders使用

  • Babel使用

npm install --save-dev babel-core babel-loader babel-preset-env

webpack.config.js中加入

{
    test: /(\.jsx|\.js)$/,
    use: {
           loader: "babel-loader"
         },
    exclude: /node_modules/
    }

在项目根目录新建一个.babelrc文件

{
  "presets": [ "env" ]
}
  • Css处理

webpack提供两个工具处理样式表,css-loaderstyle-loader,二者处理的任务不同,css-loader使你能够使用类似@import 和 url(...)的方法实现 require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中

//安装
npm install --save-dev style-loader css-loader
{
     test: /\.css$/,
     use: [{
              loader: "style-loader"
            }, {
              loader: "css-loader"
            }
           ]
}
  • Css module

官方文档https://github.com/css-modules/css-modules
被称为CSS modules的技术意在把JS的模块化思想带入CSS中来,通过CSS模块,所有的类名,动画名默认都只作用于当前模块。Webpack对CSS模块化提供了非常好的支持,只需要在CSS loader中进行简单配置即可,然后就可以直接把CSS的类名传递到组件的代码中,这样做有效避免了全局污染。

{
    test: /\.css$/,
    use: [
          {
            loader: "style-loader"
           }, {
            loader: "css-loader",
            options: {
                modules: true, // 指定启用css modules
                localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的类名格式
                   }
           }
      ]
}
  • Css预处理器

以下是常用的CSS 处理loaders:
- Less Loader
- Sass Loader
- Stylus Loader
不过其实也存在一个CSS的处理平台-PostCSS,它可以帮助你的CSS实现更多的功能,官方文档https://github.com/postcss/postcss

{
    test: /\.less$/,
    use: [{
           loader: "style-loader"
         }, {
           loader: "css-loader",
           options: {
                modules: true
                  }
          }, {
            loader: "less-loader"
        }]
}
  • 静态资源处理

对于静态资源文件可以使用 file-loaderurl-loader,前者试用于任何文件格式,后者专注于图片格式,并且对于小图片可以生成base64格式

{
    test: /\.(woff|woff2|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
    use:["file-loader"]
},
{
    test: /\.(png|jpg|gif)$/,
    use: [{
          loader: 'url-loader',
          options: {
              limit: 8192
           }
      }]
}
  • html-loader

html-loader 将解析 URL,并请求图片和你所期望的一切资源

 {
    test: /\.html$/,
    use: {
       loader: 'html-loader',
       options: {
            attrs: ['img:src','img:data-src']
       }
     }
}

插件使用

  • HtmlWebpackPlugin

这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)

npm install --save-dev html-webpack-plugin
new HtmlWebpackPlugin({
  title: 'test',
  inject: 'head',
  favicon: '',
  minify: { //压缩HTML文件  
        removeComments: true, //移除HTML中的注释
        collapseWhitespace: true //删除空白符与换行符
  },
  hash: true,    //为静态资源生成hash值
  filename: 'index1.html',
  chunks: ['bundle1'] ,  //需要引入的chunk,不配置就会引入所有页面的资源
  template: __dirname + "/app/html/index1.html"//new 一个这个插件的实例,并传入相关的参数
})
  • Hot Module Replacement

Hot Module Replacement(HMR)也是webpack里很有用的一个插件,它允许你在修改组件代码后,自动刷新实时预览修改后的效果。

在webpack中实现HMR也很简单,只需要做两项配置
- 在webpack配置文件中添加HMR插件;
- 在Webpack Dev Server中添加“hot”参数;

不过配置完这些后,JS模块其实还是不能自动热加载的,还需要在你的JS模块中执行一个Webpack提供的API才能实现热加载,虽然这个API不难使用,但是如果是React模块,使用我们已经熟悉的Babel可以更方便的实现功能热加载。

整理下我们的思路,具体实现方法如下
- Babel和webpack是独立的工具
- 二者可以一起工作
- 二者都可以通过插件拓展功能
- HMR是一个webpack插件,它让你能浏览器中实时观察模块修改后的效果,但是如果你想让它工作,需要对模块进行额外的配额;
- Babel有一个叫做`react-transform-hrm`的插件,可以在不对React模块进行额外的配置的前提下让HMR正常工作;

还是继续上例来实际看看如何配置

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin()//热加载插件
    ],
};

安装react-transform-hmr

npm install --save-dev babel-plugin-react-transform react-transform-hmr

配置Babel

// .babelrc
{
  "presets": ["react", "env"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",
         
         "imports": ["react"],
         
         "locals": ["module"]
       }]
     }]]
    }
  }
}

现在当你使用React时,可以热加载模块了,每次保存就能在浏览器上看到更新内容。

  • ExtractTextPlugin

ExtractTextPlugin可以分离CSS和JS文件

const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
                 fallback: "style-loader",
                 use: [{
                         loader: 'css-loader',
                         options:{
                              minimize: true //css压缩
                         }
                       },
                         "less-loader"
                   ]
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin("styles.css"),
  ]
}
  • UglifyJsPlugin

用来压缩JS代码

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
  plugins: [
    new UglifyJsPlugin()
  ]
}
  • clean-webpack-plugin

在再次build之前清除打包目录

const CleanWebpackPlugin = require("clean-webpack-plugin");
plugins: [
    new CleanWebpackPlugin('build/*.*', {
      root: __dirname,
      verbose: true,
      dry: false
  })
]
  • CommonsChunkPlugin

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存起来到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件

new webpack.optimize.CommonsChunkPlugin({
  name: "commons",
  // ( 公共chunk(commnons chunk) 的名称)
  filename: "commons.js",
  // ( 公共chunk 的文件名)
  // minChunks: 3,
  // (模块必须被3个 入口chunk 共享)
  // chunks: ["pageA", "pageB"],
  // (只使用这些 入口chunk)
})

可以查看这篇文章,写的很详细webpack CommonsChunkPlugin详细教程

发布

目前为止,我们已经使用webpack构建了一个完整的开发环境。但是在产品阶段,可能还需要对打包的文件进行额外的处理,比如说优化,压缩,缓存以及分离CSS和JS。

对于复杂的项目来说,需要复杂的配置,这时候分解配置文件为多个小的文件可以使得事情井井有条,以上面的例子来说,我们创建一个webpack.production.config.js的文件,在里面加上基本的配置,它和原始的webpack.config.js很像,不同的地方有:

  • devtool设置为 'null',这能大大压缩我们的打包代码
  • DefinePlugin:定义环境变量
  • webpack.LoaderOptionsPlugin:去除调试代码,压缩代码
  • 去掉热加载功能
  • webpack.optimize.UglifyJsPlugin:针对JS的混淆配置
  • CopyWebpackPlugin:复制手动引入的资源文件到指定目录
  • 去掉本地服务器功能
  • 修改publicPath静态资源路径

注意点

  • Loader与plugin区别

    • loader 用于加载某些资源文件。 因为webpack 本身只能打包commonjs规范的js文件,对于
      其他资源例如 css,图片,或者其他的语法集,比如 jsx, coffee,是没有办法加载的。 这就
      需要对应的loader将资源转化,加载进来。从字面意思也能看出,loader是用于加载的,它作用
      于一个个文件上。
    • plugin 用于扩展webpack的功能。它直接作用于 webpack,扩展了它的功能。当然loader也
      时变相的扩展了 webpack ,但是它只专注于转化文件(transform)这一个领域。而plugin的
      功能更加的丰富,而不仅局限于资源的加载。
  • publicPath

指定资源文件引用的目录,只会改变页面上资源的引用,不会改变资源打包后的位置

参考资料