【webpack】详解

什么是webpack?

【webpack】详解_第1张图片
从本质上来讲,webpack是一个现代的JavaScript应用的静态模块打包工具。
从两个点来解释上面这句话:模块打包

前端模块化:

在ES6之前,我们要想进行模块化开发,就必须借助于其他的工具,让我们可以进行模块化开发。并且在通过模块化开发完成了项目后,还需要处理模块间的各种依赖,并且将其进行整合打包。webpack其中一个核心就是让我们可能进行模块化开发,并且会帮助我们处理模块间的依赖关系
而且不仅仅是JavaScript文件,我们的CSS、图片、json文件等等在webpack中都可以被当做模块来使用,这就是webpack中模块化的概念。

打包如何理解呢?

理解了webpack可以帮助我们进行模块化,并且处理模块间的各种复杂关系后,打包的概念就非常好理解了。就是将webpack中的各种资源模块进行打包合并成一个或多个包(Bundle)。并且在打包的过程中,还可以对资源进行处理,比如压缩图片,将scss转成css,将ES6语法转成ES5语法,将TypeScript转成JavaScript等等操作。

webpack的整个打包流程:

  1. 读取webpack的配置参数;
  2. 启动webpack,创建Compiler对象并开始解析项目;
  3. 从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
  4. 对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;
  5. 整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

思考:但是打包的操作似乎grunt/gulp也可以帮助我们完成,它们有什么不同呢?
如果你的工程模块依赖非常简单,甚至是没有用到模块化的概念。只需要进行简单的合并、压缩,就使用grunt/gulp即可。但是如果整个项目使用了模块化管理,而且相互依赖非常强,我们就可以使用更加强大的webpack了。

  • grunt/gulp更加强调的是前端流程的自动化,模块化不是它的核心。
  • webpack更加强调模块化开发管理,而文件压缩合并、预处理等功能,是他附带的功能。

webpack和node和npm之间的关系?

【webpack】详解_第2张图片
如果我们要开发很多项目,这个项目里面包含很多js/png/less/css相关的东西,我们在开发的时候使用的是模块化开发。我们不能直接把这些东西放到服务器上部署。如果直接在服务器上部署,用户从我们的服务器上面请求下来的js/png/less/css这些东西是不能正常运行的。那这时,我们就可以通过webpack做一层中间的模块化打包,等你把项目开发完成以后,只需要用一下webpack,使用一条指令,就会自动的做一层模块化打包,打包之后会自动生成一个文件夹,最终只需要把这个文件夹放到服务器部署就可以了。
但是webpack要想能正常运行,它本身依赖于node环境,而node环境为了可以正常执行很多代码,必须其中包含各依赖的包,而这些包手动管理非常麻烦,所以一般在安装node的时候,它会自动帮你安装一个工具,叫npm工具,它只是为了方便管理node环境下的各种包。

什么是loader?

loader是webpack中一个非常核心的概念。
在我们之前的实例中,我们主要是用webpack来处理我们写的js代码,并且webpack会自动处理js之间相关的依赖。但是,在开发中我们不仅仅有基本的js代码处理,我们也需要加载css、图片,也包括一些高级的将ES6转成ES5代码,将TypeScript转成ES5代码,将scss、less转成css,将.jsx、.vue文件转成js文件等等。对于webpack本身的能力来说,对于这些转化是不支持的。那怎么办呢?给webpack扩展对应的loader就可以啦。

loader使用过程:

  1. 通过npm安装需要使用的loader
  2. 在webpack.config.js中的module关键字下进行配置

css文件处理 – css-loader

作用:css-loader用于将css文件打包到js中, 常常配合style-loader一起使用,将css文件打包并插入到页面中。

加载xxx.css文件必须有对应的loader.
按照官方配置webpack.config.js文件

npm install --save-dev css-loader

webpack.config.js的配置如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};

其中style-loader为什么有这个呢?原因是css-loader只负责加载css文件,但是并不负责将css具体样式嵌入到文档中。这个时候,我们还需要一个style-loader帮助我们处理。style-loader需要放在css-loader的前面。这是因为webpack在读取使用的loader的过程中,是按照从右向左的顺序读取的。

less文件处理 – less-loader

作用:将less代码转译为浏览器可以识别的CSS代码
如果我们希望在项目中使用less、scss、stylus来写样式,webpack是否可以帮助我们处理呢?这里以less为例,其他也是一样的
首先,还是需要安装对应的loader

npm install less less-loader --save-dev

其次,修改对应的配置文件
添加一个rules选项,用于处理.less文件

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/i,
        use: [
          // compiles Less to CSS
          "style-loader",
          "css-loader",
          "less-loader",
        ],
      },
    ],
  },
};

图片文件处理 – url-loader

图片处理,我们使用url-loader来处理,依然先安装url-loader

npm install url-loader --save-dev

webpack.config.js修改对应的配置文件

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
            //当加载的图片,小于limit时,会将图片编译成base64字符串形式,
              limit: 8192,
              //当加载的图片,大于这个limit时,还需要引入file-loader模块进行加载
            },
          },
        ],
      },
    ],
  },
};

当加载的图片,大于这个limit时,图片不会编译成功,还需要引入file-loader,这个file-loader不需要特别配置,只需要直接安装一下这个file-loader就可以了

npm install file-loader --save-dev

但是,我们发现图片并没有显示出来,这是因为图片使用的路径不正确,默认情况下,webpack会将生成的路径直接返回给使用者。但是,我们整个程序是打包在dist文件夹下的,所以这里我们需要在路径下再添加一个dist/
【webpack】详解_第3张图片
我们发现webpack自动帮助我们生成一个非常长的名字,这个名字是一个32位hash值,目的是防止名字重复。比如,将所有的图片放在一个文件夹中,跟上图片原来的名称,同时也要防止重复 。

所以,我们可以在options中添加上如下选项:

name:'img/[name].[hash:8].[ext]'

img:文件要打包到的文件夹
name:获取图片原来的名字,放在该位置
hash:8:为了防止图片名称冲突,依然使用hash,但是我们只保留8位
ext:使用图片原来的扩展名(extension)

babel-loader

如果你仔细阅读webpack打包的js文件,发现写的ES6语法并没有转成ES5,那么就意味着可能一些对ES6还不支持的浏览器没有办法很好的运行我们的代码。如果希望将ES6的语法转成ES5,那么就需要使用babel

npm install -D babel-loader @babel/core @babel/preset-env webpack

配置:

module: {
  rules: [
    {
      test: /\.m?js$/,
      //node_modules和bower_components不需要转换,排除在外
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

.vue文件封装处理 – vue-loader

组件以js对象的形式进行组织和使用的时候是非常不方便。这种特殊的文件以及特殊的格式,必须有人帮助我们处理。谁来处理呢?vue-loader以及vue-template-compiler。
安装vue-loader和vue-template-compiler

npm install vue-loader vue-template-compiler --save-dev

修改webpack.config.js的配置文件:

{
      test: /\.vue$/,
      use: ['vue-loader']
  }

raw-loader

主要是用 raw-loader来实现静态资源内联。静态资源内联,即将css、js、图片等资源,内联到html。我们可以通过静态资源内联来减少文件请求数量,优化加载速度
安装:

npm install --save-dev raw-loader

配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: 'raw-loader'
      }
    ]
  }
}

认识plugin

如果说Loader负责文件转换,那么Plugin便是负责功能扩展。Loader和Plugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。

plugin是什么?

  • plugin是插件的意思,通常是用于对某个现有的架构进行扩展。
  • webpack中的插件,就是对webpack现有功能的各种扩展,比如打包优化,文件压缩等等。

loader和plugin区别

  • loader主要用于转换某些类型的模块,它是一个转换器。
  • plugin是插件,它是对webpack本身的扩展,是一个扩展器。

plugin的使用过程:

  1. 步骤一:通过npm安装需要使用的plugins(某些webpack已经内置的插件不需要安装)
  2. 步骤二:在webpack.config.js中的plugins中配置插件。

下面,我们就来看看可以通过哪些插件对现有的webpack打包过程进行扩容,让我们的webpack变得更加好用。

1. 添加版权的Plugin

该插件名字叫BannerPlugin,属于webpack自带的插件,为打包的文件添加版权声明。


        const path = require(' path')
        const webpack = require('webpack')
        module.exports = {
            ... 
            plugins: [
                new webpack.BannerPlugin('最终版权归chen所有')
            ]
        }

2. 打包html的plugin

在真实发布项目时,发布的是dist文件夹中的内容,但是dist文件夹中如果没有index.html文件,那么打包的js等文件也就没有意义了。所以,我们需要将index.html文件打包到dist文件夹中,这个时候就可以使用HtmlWebpackPlugin插件

HtmlWebpackPlugin插件可以为我们自动生成一个index.html文件(可以指定模板来生成),将打包的js文件,自动通过script标签插入到body中

安装HtmlWebpackPlugin插件

npm install html-webpack-plugin --save-dev

使用插件,修改webpack.config.js文件中plugins部分的内容如下

  const path = require('path')
   const webpack = require('webpack')
   const htmIWebpackPlugin = require('html-webpack-plugin')
        module.exports = {
            ... 
            plugins: [
                new webpack.BannerPlugin('最终版权归chen所有')
                 new htmIWebpackPlugin({
                    template: 'index.html'
                })
            ]
        }

这里的template表示根据什么模板来生成index.html。另外,如果之前在output中添加的publicPath属性需要删除,否则插入的script标签中的src可能会有问题

3. js压缩的Plugin

在项目发布之前,我们必然需要对js等文件进行压缩处理,使用一个第三方的插件uglifyjs-webpack-plugin,并且版本号指定1.1.1,和CLI2保持一致(开发阶段不建议使用丑化,不方便调试)

npm install uglifyjs-webpack-plugin@1.1.1 --save-dev

修改webpack.config.js文件,使用插件:

   const path = require('path')
   const webpack = require('webpack')
   const uglifyJsPlugin = require('uglifyjs-webpack-plugin')
        module.exports = {
            ... 
            plugins: [
                new webpack.BannerPlugin('最终版权归chen所有')
                new uglifyJsPlugin()
            ]
        }

最常用的 webpack 插件有如下3个:
① clean-webpack-plugin

每次打包时,自动清理 dist 目录

② webpack-dev-server

类似于 node.js 阶段用到的 nodemon 工具
每当修改了源代码,webpack 会自动进行项目的打包和构建

③ html-webpack-plugin

webpack 中的 HTML 插件
可以通过此插件自定制 index.html 页面的内容

搭建本地服务器

webpack提供了一个可选的本地开发服务器,这个本地服务器基于node.js搭建,内部使用express框架,可以实现我们想要的让浏览器自动刷新显示我们修改后的结果。
不过它是一个单独的模块,在webpack中使用之前需要先安装它

npm install --save-dev webpack-dev-server@2.9.1

devserver也是作为webpack中的一个选项,选项本身可以设置如下属性:

  • contentBase:为哪一个文件夹提供本地服务,默认是根文件夹,我们这里要填写./dist
  • port:端口号
  • inline:页面实时刷新
  • historyApiFallback:在SPA页面中,依赖HTML5的history模式

webpack.config.js文件配置修改如下:

 devServer: {
      contentBase: ' ./dist',
       inline: true
     },

我们可以再配置另外一个scripts:

"dev": "webpack-dev-server --open "

–open参数表示直接打开浏览器

热更新原理

模块热替换(Hot Module Replacement 或 HMR) 是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。
【webpack】详解_第4张图片
第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。

第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。

第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。

第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。

webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。

HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。

而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

特性

模块热替换功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面时丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 调整样式更加快速 – 几乎相当于在浏览器调试器中更改样式。

思考

1、为什么平时修改代码的时候不用监听module.hot.accept也能实现热更新?
那是因为我们使用的 loader 已经在幕后帮我们实现了。
2、为什么需要热更新?
Hot Module Replacement(以下简称 HMR)是 webpack 发展至今引入的最令人兴奋的特性之一 ,当你对代码进行修改并保存后,webpack 将对代码重新打包,并将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样在不刷新浏览器的前提下就能够对应用进行更新。例如,在开发 Web 页面过程中,当你点击按钮,出现一个弹窗的时候,发现弹窗标题没有对齐,这时候你修改 CSS 样式,然后保存,在浏览器没有刷新的前提下,标题样式发生了改变。感觉就像在 Chrome 的开发者工具中直接修改元素样式一样。
3、HMR是怎样实现自动编译的?
webpack通过watch可以监听文件编译完成和监听文件的变化,webpack-dev-middleware可以调用webpack的API监听代码的变化,webpack-dev-middleware利用sockjs和webpack-dev-server/client建立webSocket长连接。将webpack的编译编译打包的各个阶段告诉浏览器端。主要告诉新模块hash的变化,然后webpack-dev-server/client是无法获取更新的代码的,通过webpack/hot/server获取更新的模块,然后HMR对比更新模块和模块的依赖。
4、模块内容的变更浏览器又是如何感知的?
webpack-dev-middleware利用sockjs和webpack-dev-server/client建立webSocket长连接。将webpack的编译编译打包的各个阶段告诉浏览器端。
5、怎么实现局部更新的?
当hot-update.js文件加载好后,就会执行window.webpackHotUpdate,进而调用了hotApply。hotApply根据模块ID找到旧模块然后将它删除,然后执行父模块中注册的accept回调,从而实现模块内容的局部更新。
6、webpack 可以将不同的模块打包成 bundle 文件或者几个 chunk 文件,但是当我通过 webpack HMR 进行开发的过程中,我并没有在我的 dist 目录中找到 webpack 打包好的文件,它们去哪呢?
原来 webpack 将 bundle.js 文件打包到了内存中,不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memory-fs,memory-fs 是 webpack-dev-middleware 的一个依赖库,webpack-dev-middleware 将 webpack 原本的 outputFileSystem 替换成了MemoryFileSystem 实例,这样代码就将输出到内存中。
7、说一下webpack的热更新原理?
webpack通过watch可以监测代码的变化;webpack-dev-middleware可以调用webpack暴露的API检测代码变化,并且告诉webpack将代码保存到内存中;webpack-dev-middleware通过sockjs和webpack-dev-server/client建立webSocket长连接,将webpack打包阶段的各个状态告知浏览器端,最重要的是新模块的hash值。webpack-dev-server/client通过webpack/hot/dev-server中的HMR去请求新的更新模块,HMR主要借助JSONP。先拿到hash的json文件,然后根据hash拼接出更新的文件js,然后HotModulePlugin对比新旧模块和模块依赖完成更新。

你可能感兴趣的:(webpack,前端,javascript)