Webpack 再深入再总结

15,229 阅读11分钟

前言

本文为作者第二次专门对 Webpack 的知识点进行深入和实践,根据理解和实践的结果进行总结的;

文章内容参考书籍《深入浅出 Webpack》,因为该书籍基于 Webpack 3.4.0 版本,本文的实践基于 Webpack 4.28.2 版本,所以也踩了不少由于模块版本问题出现的坑,已经汇总到第 6 章节 踩坑汇总,大家记得避免踩坑;也印证了那句哲理:纸上得来终觉浅,绝知此事要躬行 ...

博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,也欢迎关注及 star ~

本文实践 demo 的 github地址

一、Webpack 原理

1、构建作用

构建工具就是将源代码转换成可执行的 JavaScript、CSS、HTML 代码,包括以下内容:

  • 代码转换:将 TypeScript 编译成 JavaScript、将 SCSS 编译成 CSS 等;

  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等;

  • 代码分割:提取多个页面的公共代码,提取首屏不需要执行部分的代码,让其异步加载;

  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要通过构建功能将模块分类合并成一个文件;

  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器;

  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过;

  • 自动发布:更新代码后,自动构建出线上发布代码并传输给发布系统;

2、核心概念

Webpack 有以下几个核心概念:

  • Entry :入口,Webpack 执行构建的第一步将从 entry 开始,可抽象成输入;

  • Module:模块,配置处理模块的规则;在 Webpack 里一切皆模块,一个模块对应一个文件;Webpack 会从配置的 Entry 开始递归找出所有依赖的模块;

  • Loader:模块转换器,用于将模块的原内容按照需求转换成新内容;

  • Resolve:配置寻找模块的规则;

  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播对应的事件,插件可以监听这些事情的发生,在特定的时机做对应的事情;

  • Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果;

  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割;

3、流程概述

(1)初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;

(2)开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,通过执行对象的 run 方法开始执行编译;

(3)确定入口:根据配置中的 entry 找出所有入口文件;

(4)编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;

(5)完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容及它们之间的依赖关系;

(6)输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会;

(7)输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中;

在以上过程中,Webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果;

二、Webpack 配置

1、Webpack 项目初始化

1、新建 Web 项目

新建一个目录,再进入项目根目录执行 npm init 来初始化最简单的采用了模块化开发的项目;最终生成 package.json 文件;

$ npm init

2、安装 Webpack 到本项目

(1)查看 Webpack 版本

运行以下命令可以查看 Webpack 的版本号

$ npm view webpack versions

(2)安装 Webpack

可以选择(1)步骤罗列得到的 Webpack 版本号,也可以安装最新稳定版、最新体验版本,相关命令如下所示,我选择安装 4.28.2 版本(没有为什么,就想装个 4.x 的版本);

// 安装指定版本
npm i -D webpack@4.28.2

// 安装最新稳定版
npm i -D webpack

// 安装最新体验版本
npm i -D webpack@beta

(3)安装 Webpack 脚手架

需要安装 Webpack 脚手架,才能在命令窗口执行 Webpack 命令,运行以下命令安装 Webpack 脚手架;

$ npm i -D webpack-cli

3、使用 Webpack

使用 Webpack 构建一个采用 CommonJS 模块化编写的项目;

(1)新建页面入口文件 index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Webpack</title>
</head>
<body>
<!--导入 Webpack 输出的 JavaScript 文件-->
<script src="./dist/bundle.js"></script>  
</body>
</html>

(2)新建需要用到的 JS 文件

show.js 文件

// 操作 DOM 元素,把 content 显示到网页上
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}

// 通过 CommonJS 规范导出 show 函数
module.exports = show;

main.js 文件

// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
// 执行 show 函数
show('Webpack');

(3)新建 Webpack 配置文件 webpack.config.js

const path = require('path');

module.exports = {
  // JavaScript 执行入口文件
  entry: './main.js',
  output: {
    // 把所有依赖的模块合并输出到一个 bundle.js 文件
    filename: 'bundle.js',
    // 输出文件都放到 dist 目录下
    path: path.resolve(__dirname, './dist'),
  }
};

(4)执行 webpack 命令进行构建

在 package.json 文件中配置编译命令,如下所示:

  "scripts": {
    "build": "webpack --config webpack.config.js",
  },

执行以下命令进行项目的 Webpack 编译,成功后会在项目根目录下生成编译目录 dist ;

$ npm run build

(5)运行 index.html

编译成功后,我们用浏览器打开 index.html 文件,能看到页面成功显示 “Hello Webpack”;

2、Loader 配置

本节通过为之前的例子添加样式,来尝试使用 Loader;

(1)新建样式文件 main.css

#app{
  text-align: center;
  color:'#999';
}

(2)将 main.css 文件引入入口文件 main.js 中,如下所示:

// 通过 CommonJS 规范导入 CSS 模块
require('./main.css');

// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
// 执行 show 函数
show('Webpack');

(3)Loader 配置

以上修改后去执行 Webpack 构建是会报错的,因为 Webpack 不原生支持解析 CSS 文件。要支持非 JavaScript 类型的文件,需要使用 Webpack 的 Loader 机制;

(3.1)运行以下命令,安装 style-loader 和 css-loader,其中:

  • css-loader 用于读取 CSS 文件;
  • style-loader 把 CSS 内容注入到 JavaScript 中;
$ npm i -D style-loader css-loader

(3.2)进行以下配置

module:{
rules:[
  {
	// 用正则去匹配要用该 loader 转换的 CSS 文件
	test:/\.css$/,
	use:['style-loader','css-loader']
  }
 ]
}

(4)查看结果

编译后,刷新 index.html ,查看刚刚的样式 loader 已经起作用;

1

3、Plugin 配置

(1)安装样式提取插件 extract-text-webpack-plugin

$ npm i -D extract-text-webpack-plugin@next

(2)plugin 文件配置如下

  module:{
    rules:[
      {
        // 用正则去匹配要用该 loader 转换的 CSS 文件
        test:/\.css$/,
        use:ExtractTextPlugin.extract({
          use:['css-loader']
        }),
      }
    ]
  },
  plugins:[
    new ExtractTextPlugin({
       // 从 .js 文件中提取出来的 .css 文件的名称
       filename:`[name]_[hash:8].css`
    }),
  ]

(3)查看结果

通过以上配置后,执行 Webapack 的执行命令,发现在 dist 目录下,生成对应的 css 文件;存在的坑点:

  • 我们需要手动将生成的 css 文件引入到 index.html 中;
  • 修改 css 文件后,会生成新的 css 文件,原先的不会删除;

4、使用 DevServer

(1)执行以下命令安装 webpack-dev-server

$ npm i -D  webpack-dev-server

在 package.json 中配置启动命令

  "scripts": {
    "build": "webpack --config webpack.config.js",
    "dev": "webpack-dev-server",
  },

运行命令后,就可以启动 HTTP 服务

$ npm run dev

启动结果如下所示,我们可以通过 http://localhost:8080/ 访问我们的 index.html 的demo

1

(2)实时预览

我们在运行命令后面添加参数 --watch 实现实时预览,配置如下所示:

  "scripts": {
    "dev": "webpack-dev-server --watch"
  },

然后我们修改 main.js 的传入参数,发现并不能实时预览,也没有报错!!! why?

踩坑:

在 index.html 中需要将 js 的路径修改为:

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

而不能是之前的(因为这个是编译生成的,并不是通过 devServer 生成放在内存的)

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

(3)模块热替换

可以通过配置 -- hot 进行模块热替换;

三、Webpack 优化

关于优化的实践之前有进行过实践了,这里不再累述,感兴趣的童鞋可以查看作者写的另一篇文章《Vue项目Webpack优化实践,构建效率提高50%

四、编写 Loader

1、Loader 要点总结

(1)Loader 为模块转换器,用于将模块的原内容按照需求转换成新内容;

(2)Loader 的职责是单一的,只需要完成一种转换,遵守单一职责原则;

(3)Webpack 为 Loader 提供了一系列 API 供 Loader 调用,例如:

  • loader-utils.getOptions( this ) 获取用户传入的 options,
  • this.callback( ) 自定义返回结果,
  • this.async( ) 支持异步操作;
  • this.context 当前文件所在的目录;
  • this.resource 当前处理文件的完整请求路径;
  • 其它等等

2、编写 loader 源码

手写一个 loader 源码,其功能是将 /hello/gi 转换成 HELLO,当然这个 loader 其实没啥实际意义,纯碎是为了写 loader 而写 loader;当然如果你实际业务有需要编写 loader 需求,那就要反思这个业务的合理性,因为庞大的社区,一般合理的需求都能找到对应的 loader。

(1)源码编写

在原有的项目底下,新建目录 custom-loader 作为我们编写 loader 的名称,执行 npm init 命令,新建一个模块化项目,然后新建 index.js 文件,相关源码如下:

function convert(source){
  return source && source.replace(/hello/gi,'HELLO');
}

module.exports = function(content){
  return convert(content);
}

(2)Npm link 模块注册

正常我们安装 Loader 是从 Npm 公有仓库安装,也即将 Loader 发布到 Npm 仓库,然后再安装到本地使用;但是我们可以使用 Npm link 做到在不发布模块的情况下,将本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块;

在 custom-loader 目录底下,运行以下命令,将本地模块注册到全局:

$ npm link

成功结果如下:

1

然后在项目根目录执行以下命令,将注册到全局的本地 Npm 模块链接到项目的 node_modules 下:

$ npm link custom-loader

成功结果如下,并且在 node_modules 目录下能查找到对应的 loader;

1

3、Webpack 中配置编写的 loader

该配置跟第一章节的 Webpack 配置并没有任何区别,这里不再详述,配置参考如下:

  module:{
    rules:[
      {
        test:/\.js/,
        use:['custom-loader'],
        include:path.resolve(__dirname,'show')
      }
    ]
  }

执行运行 or 编译命令,就能看到我们的 loader 起作用了。

五、编写 Plugin

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果,这条生产线上的每个处理流程的职责都是单一的,多个流程之间存在依赖关系,只有在完成当前处理后才能提交给下一个流程去处理。插件就像生产线中的某个功能,在特定的时机对生产线上的资源进行处理。

Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

1、Plugin 要点总结

  • Webpack 在编译过程中,会广播很多事件,例如 run、compile、done、fail 等等,可以查看官网;
  • Webpack 的事件流机制应用了观察者模式,我们编写的插件可以监听 Webpack 事件来触发对应的处理逻辑;
  • 插件中可以使用很多 Webpack 提供的 API,例如读取输出资源、代码块、模块及依赖等;

2、编写 Plugin 源码

手写一个 plugin 源码,其功能是在 Webpack 编译成功或者失败时输出提示;当然这个 plugin 其实没啥实际意义,纯碎是为了写 plugin 而写 plugin;当然如果你实际业务有需要编写 plugin 需求,那就要反思这个业务的合理性,因为庞大的社区,一般合理的需求都能找到对应的 plugin。

(1)源码编写

在原有的项目底下,新建目录 custom-plugin 作为我们编写 plugin 的名称,执行 npm init 命令,新建一个模块化项目,然后新建 index.js 文件,相关源码如下:

class CustomPlugin{
  constructor(doneCallback, failCallback){
     // 保存在创建插件实例时传入的回调函数
     this.doneCallback = doneCallback;
     this.failCallback = failCallback;
  }
  apply(compiler){
    // 成功完成一次完整的编译和输出流程时,会触发 done 事件
    compiler.plugin('done',(stats)=>{
      this.doneCallback(stats);
    })
    // 在编译和输出的流程中遇到异常时,会触发 failed 事件
    compiler.plugin('failed',(err)=>{
      this.failCallback(err);
    })
  }
}
module.exports = CustomPlugin;

(2)Npm link 模块注册

跟 Loader 注册一样,我们使用 npm link 进行注册;

在 custom-plugin 目录底下,运行以下命令,将本地模块注册到全局:

$ npm link

然后在项目根目录执行以下命令,将注册到全局的本地 Npm 模块链接到项目的 node_modules 下:

$ npm link custom-plugin

如果一切顺利,可以在 node_modules 目录下能查找到对应的 plugin;

3、Webpack 中配置编写的 plugin

该配置跟第一章节的 Webpack 配置并没有任何区别,这里不再详述,配置参考如下:

  plugins:[
    new CustomPlugin(
     stats => {console.info('编译成功!')},
     err => {console.error('编译失败!')}
   ),
  ],

执行运行 or 编译命令,就能看到我们的 plugin 起作用了。

六、踩坑汇总

1、css-loader 以下配置

rules:[
  {
	// 用正则去匹配要用该 loader 转换的 CSS 文件
	test:/\.css$/,
	use:['style-loader','css-loader?minimize']
  }
]

报以下错误:

 - options has an unknown property 'minimize'. These properties are valid:
   object { url?, import?, modules?, sourceMap?, importLoaders?, localsConventio               n?, onlyLocals?, esModule? }

原因:

minimize 属性在新版本已经被移除,

解决:

先去掉 minimize 选项;

2、ExtractTextPlugin 编译以下错误:

1

原因:

extract-text-webpack-plugin 版本号问题

参考链接:github.com/webpack/web…

解决:

重新安装 extract-text-webpack-plugin

$ npm i -D extract-text-webpack-plugin@next

3、修复第2个坑之后,ExtractTextPlugin 编译继续报以下错误:

1

原因:

不存在 contenthash 这个变量

解决:

更改 extract-text-webpack-plugin 的配置:

  plugins:[
    new ExtractTextPlugin({
       // 从 .js 文件中提取出来的 .css 文件的名称
       filename:`[name]_[hash:8].css`
    }),
  ]

4、添加 HappyPack 后,编译 CSS 文件时报以下错误:

1

原因:

css-loader 版本的问题

解决:

重新安装 css-loader@3.2.0

七、总结

本文主要基于 Webpack 的作用、核心概念、流程,Webpack 的基础配置,Webpack 优化,编写 Loader,编写 Plugin ,从理论到实践,从基础到较难,对 Webpack 进行总结掌握,希望对你也有帮助。还是那句话:纸上得来终觉浅,绝知此事要躬行 ...,如果你没有手敲过,一定要多动动手 !

博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,也欢迎关注及 star ~

本文实践 demo 的 github地址