webpack折腾记(一)

gulp用了很长一段时间了,也挺顺手的,只是最近一直在用vue-cli进行开发,被其各种方便的特性给惊呆了。然而归根结底就是gulp和webpack的比较,作为新一代的前端流程开发工具(现在也不新了),还是大概了解一下吧。

<!--more-->

参考:

1. 概述

1.1. webpack与gulp

起初断断续续学了一段时间的webpack,后来终究还是给放弃了,因为之前的项目比较小,用gulp写两个任务搭好环境就可以了(这大概就是温水煮青蛙,不愿离开稳定的环境罢!)。

现在来看看webpack和gulp之间的区别:

  • gulp是一个自动化工具,其工作流程是基于文件流的,代替人工手动操作,实现自动化开发
  • webpack是一个打包工具,将项目中的各种文件合并打包,实现模块化开发

我认为在概念上,gulp和webpack并不冲突,尽管他们可以借助自身的插件或loader实现许多类似的功能,比如:

  • 文件压缩打包
  • css预编译器
  • 热更新环境

那么,为什么我现在要重新折腾webpack呢?因为之前一直在纠结开发过程中的样式表管理,脚本管理等问题,也写过“BEM命名”,requireJS使用方法等文章,说到底就是模块化开发的问题。webpack既能完成大部分自动化功能,更重要的是他解决了模块化开发的问题!但是口说无凭,webpack到底是如何解决模块化的问题的呢?

1.2. 模块化开发

为了实现模块化开发:

  • 在RequireJS中,我们使用define定义模块,使用require引入模块;
  • 在scss中,我们将样式组件拆分成数个独立的_*.scss,使用@import按需引入相应组件。

webpack是基于nodejs的,也就是说,我们可以使用CommonJS规范来管理JS模块。但是,样式表、图片和其他文件是怎么实现模块化管理的呢?注意,重点来了:webpack的核心思想就是将所有资源都视为JS模块,并允许我们通过相同的方式调用这些模块

更明白一点讲,webpack把我们的项目当作一个主体,通过一个给定的主文件找到整个项目所有的依赖文件,将这些依赖文件进行处理之后统一打包成一个浏览器可识别的JS文件。是不是很神奇?下面让我们慢慢揭开它的神秘面纱。

2. 使用方法

关于webpack的使用防范,网上也有大量的教程了,这里简单整理一下使用方法(PS:使用的是版本v2.2.1)。

2.1. 入口文件

由于是将多个资源模块打包成一个文件(打包成多个文件的做法后面会提到),这意味着我们的页面只需要调用这一个文件就够了。因此我们需要列出项目的依赖模块,这个列出资源列表的文件通常称作入口文件,我一般命名为entry.js(实际上入口文件可以不只一个哦);

//demo1.js
module.exports = {
    writeHello(){
        var oDiv = document.createElement("div");
        oDiv.innerHTML = "<h1>This div came from demo1.js</h1>";
        document.body.appendChild(oDiv);
    }
}
// entry.js
var demo1 = require("./lib/demo1.js");
demo1.writeHello();

最后调用webpack entry.js dist/bundle.js进行打包,输出文件为dist/bundle.js。可以看见,采用的是跟NodeJS完全相同的模块语法,用文档的话来讲:

Webpack 会分析入口文件,解析包含依赖关系的各个文件。这些文件(模块)都打包到 bundle.js 。Webpack 会给每个模块分配一个唯一的 id 并通过这个 id 索引和访问模块。在页面启动时,会先执行 entry.js 中的代码,其它模块会在运行 require 的时候再执行。

在我们的页面上只需要引入对应的bundle.js文件就可以了,如果使用equireJS,还必须得配置相关路径,最后使用r.js进行打包,所以这个确实方便得多。

<script src="dist/bundle.js"></script>

2.2. loader

使用CommonJS风格调用js文件是无可厚非的,但是对于其他非js的文件,比如图片样式表等,webpack使用的方案是loader加载器,使用loader,就可以像调用JS模块一样使用其他类型的资源文件(官方的叫法是any static resource)。举个打包样式表的例子,准备一个css文件,

// css/demo1.css
body {
      background-color: red;
}

然后安装对应loader:

cnpm i style-loader css-loader -D

接着在我们的入口文件引入对应的css资源,并配置相关loader,关于每个loader的作用下面马上讲解(顺道吐槽一下为啥是用!作为分割符呢?)

// entry.js
require("!style-loader!css-loader!./css/demo1.css");

最后进行打包就可以了。此时打开页面index.html可以发现,样式表原来是作为style节点插入页面头部的,这是为什么呢?没错,就是上面的loader的作用。

  • style-loader:将css插入到页面的style标签
  • css-loader:将 css 装载到 javascript

加载顺序 细心的你应该会发现一个问题:为什么先写style-loader,然后写css-loader,最后才是*.css这个资源文件呢?正常情况下不是先加载css资源,然后将css资源装载到js,最后通过js将样式表节点插入页面上吗?

恭喜你!你发现了一个天大的秘密:loader的加载顺序实际上是从右向左的!这里跟gulp中的文件流的概念相似,多个loader之间的工作是基于文件流进行的,因此,必须确保loader的从右向左的加载顺序!不信你把上面两个loader顺序调换试试。

相同文件配置 有代码洁癖的你应该会发现第二个问题:如果需要打包多个样式表,那不是每次引入都必须为这些css文件配置对应的loader?没错,的确如此,webpack提供了按照文件格式批量配置loader的方案:

webpack entry.js dist/bundle.js --module-bind "css=style-loader!css-loader"

看起来已经解决问题了。但是,每次都打包都配置这个长的参数真的好吗?webpack提供了一个更简单粗暴的方案:配置文件!

2.3. 配置文件

在gulp中我们使用gulpfile.js加载模块,配置环境和定义任务,在webpack中我们在webpack.config.js中定义我们的配置,而最终需要执行的只是一个webpack打包指令就可以了。尽管这个配置文件仅仅只是一个简单的JS模块,但是相关的配置参数还是比较繁复的,这也正是学习webpack的一个难点。

实际上配置参数也是根据webpack本身的功能来进行的(这不废话吗?)只要了解了webpack几个主要特性,相关的配置就迎刃而解了。我这里先简单整理了初学者需要了解的配置参数,强烈推荐官方文档

2.3.1. 路径

路径配置包括

// 入口文件路径
entry: __dirname + "./entry.js",

// 输入文件
output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
},

2.3.2. 模块

网上大量的教程都是关于module.loaders的配置,后来才发现那是webpack1的使用方法。在webpack2中,使用module.rules,个人认为这种配置方式更加直观。

module: {
    rules: [
        {    
            // 匹配规则
            test: /\.css$/,
            // 对应loader
            loader: ["style-loader", "css-loader"],
            // 其他参数...
            include: path.resolve(__dirname, "style"),
            exclude: path.resolve(__dirname, "node_modules"),
        },
    ]
},

大部分情况下,我们只需要配置正确的loader就可以 了。

2.3.3. 其他

此外还有一些不是特别常见的配置属性

  • resolve,配置模块的引入等,比如为模块取个别名
  • externals,扩展模块库,比如直接引入CDN文件

正确配置了相关的参数之后,打包的命令就只需要一个不带任何参数的webpack指令就可以了,或者使用npm指定一个打包命令npm run build之类的。

  • 大部分配置参数都可以是字符串或数组形式:为单参数时用字符串,为多参数时用数组
  • 建立使用绝对路径配置文件相关路径,path.resolve(__dirname, target),或者预先定义常量

2.4. 插件

loader只能让我们像加载js一样加载其他资源,但是还有某些特殊的需求,比如增加注释,打包多文件等,我们需要使用插件来完成。

来看一个简单的例子,在输出文件添加文档注释,这里使用webpack内置的插件BannerPlugin,通过配置文件的plugins参数进行配置:

plugins: [
    new webpack.BannerPlugin("Author: txm, 2017-08-28")
]

在gulp中并没有loader的概念,因此整个工作是通过文档流合并在一起的。在webpack中,loader可以让我们处理各种格式的文件,而插件可以让我们完成自动化的工作。换句话说,一般情况下常见的文件格式都有其对应的loader进行处理,而具体的任务需求所需要的插件,只能靠自己平常去收集了(汗)...

3. 搭建开发环境

使用webpack可以显著提高开发效率,且只需要进行简单的配置即可,不需要像gulp一样去编写对应的任务。下面从样式表、脚本、热更新、文件处理等方面对相关的配置进行总结

3.1. css

样式表的开发流程一般是

  • 使用scss编写源码,
  • 然后对编译后的css文件进行处理,比如添加浏览器前缀
  • 样式表中引用的外部文件,比如图片字体等,webpack也将他们识别为相应的模块,因此也必须配置对应的loader
  • 如果需要将css打包进js文件,还必须使用前面提到的一些相关loader

常用loader

  • scss-loader,编译scss文件
  • auto-prefixer,自动处理浏览器前缀
  • px2rem,将像素单位转换为rem单位
  • url-loader,用来处理background-image的url问题,还可以将图片进行base64转码
  • css-loader,让webpack加载css文件
  • style-loader,将样式表输出到页面上

常用插件 尽管使用style-loader将css一起打包到一个js文件看起来很酷,但是考虑到浏览器渲染流程,更通常是将样式表单独提出来,此时可以使用extract-text-webpack-plugin插件完成。

module: {
    rules: [
      {
          test: /\.css$/,
          use: ExtractTextPlugin.extract({
                  use: "css-loader"
          })
      }
    ]
,
plugins: [
    new ExtractTextPlugin("main.css"),
],

3.2. javascript

使用webpack编写脚本主要侧重两个方面:

  • 模块化
  • Babel语法转化

模块化 将js代码拆分成多个模块(包括第三方库文件),然后按需引入,实现模块化的开发,并不需要我们像使用RequireJS一样还得配置路径,声明依赖等,webpack会自动处理这一切。如果想要引入CDN上面的脚本资源,必须现在页面上使用script标签引入,然后在配置文件的externals参数中进行配置。

// index.html
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>

// webpack.config.js
externals: {
    jquery: "jQuery" 
}

这里的"jQuery"就是全局变量jQuery,查看编译后的文件,可以看见内部实现是module.exports = jQuery,也就是说引入jquery的这个文件是放在引入bundle.js的前面进行加载的,因为必须保证bundle.js能够访问到jQuery对象。在接下来的代码中就可以使用var $ = require("jquery")了。 个人觉得这里有点不合理,RequireJS可以在CDN文档加载失败之后调用备份的本地文件,此外这样的依赖顺序会导致浏览器的阻塞,在webpack这里我还没找到具体的处理措施,先挖个坑吧。

babel 实现babel转义需要安装

  • babel-core,babel功能文件
  • babel-preset-env,根据配置环境智能转换JS代码的版本,而不是一股脑都转换为旧代码

此外还需要对应的loader

  • babel-loader

然后添加对应的规则就可以了,甚至连.babellrc都不需要。

{
    test: /\.js$/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: ['env'] // 配置需要编译的版本
        }
    }
}

3.3. 热更新

在webpack中实现热更新也简单很多,首先全局安装webpack-dev-server

cnpm i webpack-dev-server -g

然后再运行就可以了,不用向gulp那样去配置livereload这些了,如果闲命令太长也可以在package.json中封装成npm命令。

webpack-dev-server --progress --colors

遇见热更新不生效的问题,需要将index.html中的dist/bundle.js转换为localhost:8080/dist/bundle.js这样,相关参考文档

3.4. 代码压缩

代码压缩这个问题就简单得多了,webpack内置了一个UglifyJsPlugin插件

plugins: [
    new webpack.optimize.UglifyJsPlugin({
        compress: {
            warnings: false
        }
    })
],

相关的配置参数也请移步文档

4. 小结

至此,简单理清了webpack的使用方法,并简单搭建了一套开发环境。想到之前学习webpack的时候,满世界找教程,都发现教程基本上都是webpack1的攻略,很多配置都“过时”了。这让我明白了一个道理:官方的文档才是首先应该去翻阅的教程!在今后的学习中,更应该注意这个问题,至于英语文档啥的这个总是得克服的不是嘛...

2018年五月面试发现的一些问题 BFC及其应用