Webpack相关

160 阅读13分钟

Webpack相关

Webpack的Loader和Plugin的区别

  1. 作用不同:

    Loader直译为“加载器”。Webpack将一切文件视为模块,但是Webpack原生只能解析js文件,如果想将其他文件也打包的话,就会用到Loader。所以Loader的作用是让Webpack拥有了加载和解析非js文件的能力(可理解为转换器)

    Plugin直译为“插件”。Plugin可以扩展Webpack 的功能,让Webpack具有更多的灵活性。在Webpack运行的生命周期中广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack 提供的API改变输出结果(可理解为扩展器)

  2. 用法不同:

    Loader在module.rules中配置,也就是说作为模块的解析规则而存在。类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)

    plugin 在plugins中单独配置。类型为数组,每一项都是一个plugin实例,参数都能通过构造函数传入

常见Loader:

文件

  • raw-loader 加载文件原始内容(utf-8)
  • val-loader 将代码作为模块执行,并将 exports 转为 JS 代码
  • url-loader 像 file loader 一样工作,但如果文件小于限制,可以返回 data URL
  • file-loader 将文件发送到输出文件夹,并返回(相对)URL

JSON

转换编译(Transpiling)

模板(Templating)

  • html-loader 导出 HTML 为字符串,需要引用静态资源
  • pug-loader 加载 Pug 模板并返回一个函数
  • jade-loader 加载 Jade 模板并返回一个函数
  • markdown-loader 将 Markdown 转译为 HTML
  • react-markdown-loader 使用 markdown-parse parser(解析器) 将 Markdown 编译为 React 组件
  • posthtml-loader 使用 PostHTML 加载并转换 HTML 文件
  • handlebars-loader 将 Handlebars 转移为 HTML
  • markup-inline-loader 将内联的 SVG/MathML 文件转换为 HTML。在应用于图标字体,或将 CSS 动画应用于 SVG 时非常有用。

样式

  • style-loader 将模块的导出作为样式添加到 DOM 中
  • css-loader 解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
  • less-loader 加载和转译 LESS 文件
  • sass-loader 加载和转译 SASS/SCSS 文件
  • postcss-loader 使用 PostCSS 加载和转译 CSS/SSS 文件
  • stylus-loader 加载和转译 Stylus 文件

清理和测试(Linting && Testing)

框架(Frameworks)

  • vue-loader 加载和转译 Vue 组件
  • polymer-loader 使用选择预处理器(preprocessor)处理,并且 require() 类似一等模块(first-class)的 Web 组件
  • angular2-template-loader 加载和转译 Angular 组件

常见 Plugin

Webpack中的sourcemap

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要sourcemap。

map 文件只要不打开开发者工具,浏览器是不会加载的

线上一般有三种处理方案

  • hidden-source-map:借助第三方错误监控平台Sentry使用

  • nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比sourcemap高

  • source:通过nginx设置将.map文件只对白名单开放(公司内网)

    | 注意的是:避免在生产中使用inline-和eval-,因为他们会增加bundle体积大小,并降低整体性能

Webpack构建流程

webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程

  • 初始化参数:从配置文件和Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译
  • 确定入口:根据配置中的entry找出所有的入口文件
  • 从入口文件出发,调用所有配置的Loader对模块进行编译,再找出该模块依赖的模块,在递归本步骤知道所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第四步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间额依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:再确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

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

Es Module和common js 在打包过程中的区别

1.es6模块调用commonjs模块

可以直接使用commonjs 模块,commonjs模块将不会被webpack的模块系统编译而是原样输出,并且commonjs模块没有default属性

2.es6模块调用es6模块

被调用的es6模块不会添加{esModule:true},只有调用者才会添加{esModule:true},并且可以进行tree-shaking操作,如果被调用的es6模块只是import进来,但是并没有被用到,那么未被调用的es6模块将会被标记为/* unused harmony default export */,在压缩时此模块将被删除(如果被调用的es6模块里有立即执行语句,那么这些语句将会被保留)

3.commonjs模块引用es6模块

es6模块编译后会添加{__esModule:true}。如果被调用的es6模块中恰好有export default 语句,那么编译后的es6模块将会添加default 属性

4.commonjs模块调用commonjs模块

commonjs模块会原样输出

拓展:模块化

立即执行函数

在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题

(function(globalVariable){
   globalVariable.test = function() {}
   // ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)

AMD 和 CMD

鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。

// AMD
define(['./a', './b'], function(a, b) {
  // 加载模块完毕可以使用
  a.do()
  b.do()
})
// CMD
define(function(require, exports, module) {
  // 加载模块
  // 可以把 require 写在函数体的任意地方实现延迟加载
  var a = require('./a')
  a.doSomething()
})

CommonJS

CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了。

// a.js
module.exports = {
    a: 1
}
// or 
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

因为 CommonJS 还是会使用到的,所以这里会对一些疑难点进行解析

先说 require

var module = require('./a.js')
module.a 
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
    a: 1
}
// module 基本实现
var module = {
  id: 'xxxx', // 我总得知道怎么去找到他吧
  exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports 
var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};
// 然后当我 require 的时候去找到独特的
// id,然后将要使用的东西用立即执行函数包装下,over

另外虽然 exportsmodule.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了 exportsmodule.exports 享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效。

ES Module

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  • CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • ES Module 会编译成 require/exports 来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}

dev-server相关

dev-server运行配置

  • 安装webpack-dev-server 的npm 包
  • webpack.config.js进行配置

devServer中常用的配置对象属性如下

  1. contentBase:'./' 本地服务器在哪个目录搭建页面,一般在当前目录即可

  2. historyApiFallback:true 搭建spa应用时会用到。它使用的是HTML5 History Api,任意的跳转或404响应可以指向index.html页面

  3. inline:true 用来支持dev-server自动刷新的配置,webpack有两种模式支持自动刷新,一种是iframe模式,一种是inline模式,使用iframe模式是不需要再devServer进行配置的。只需使用特定的URL格式访问即可,不过我们一般使用的是inline模式,在devServer中对inline设置为true后,当启动webpack-dev-server时仍需配置inline才能生效

  4. hot:true 启动webpack 热模块替换特性

  5. port:端口号(默认8080)

运行原理

  1. 启动HTTP服务
  2. webpack 构建时输出Bundle到内存,HTTP服务从内存中读取Bundle文件
  3. 监听文件变化,重新执行第二个步骤

静态资源访问

{
	devServer:{
		contentBase: 'public'
	}
}

Proxy代理

{
	devServer:{
		proxy:{
			'/api'{
				target:'http://api.target.com'
			}
		}
	}
}

如何实现webpack持久化缓存

  1. 服务端设置HTTP缓存头(Cache-Control等)
  2. 打包依赖(dependencies)和运行时(runtime)到不同chunk,即作splitChunk,因为他们几乎是不变的
  3. 延迟加载:使用import()方式,可以动态加载的文件分到独立的chunk,以得到自己的chunkHash
  4. 保证hash值稳定:编译过程和文件内容的更改尽量不影响其他文件hash的计算。

现在比较成熟的持久化缓存方案就是在静态资源的名字后面加hash值,因为每次修改文件生成的hash 值不一样,这样做的好处在于增量式发布文件,避免覆盖掉之前的文件而导致线上的用户访问失效

webpack 热更新原理

  1. webpack compiler: 将js编译成Bundle
  2. Bundle Server: 提供文件在浏览器的访问,实际上就是一个服务器
  3. HMRServer: 将热更新的文件输出给HMR Runtime
  4. HMR Runtime: 会注入到bundle.js中,与HRM Server通 过webSocket 链接,接收文件变化,并更新对应的文件
  5. .bundle.js : 构建输出的文件

1启动阶段

  • webpack Compiler将对应文件打包成bundle.js(包含注入的HMR Server), 发送给Bundler Server
  • 浏览器即可访问服务器的方式去获取bundle.js

2更新阶段(文件发生变化)

  • webpack compiler重新编译,发 送给HMR Server

  • HMR Server可以知道有哪些资源 哪些模块发生了变化,通知 HRM Runtime

  • HRM Runtime更新代码

详解

基础

使用express启动本地服务,当浏览器访问资源时对此响应

1.服务端和客户端使用websocket实现长连接

2.webpack监听源文件的变化,即当开发者保存文件时触发webpack的重新编译

  • 每次编译都会生成hash值,已改动 模块的」son文件、已改动模块代码的js文件
  • 编译完成后通过socket向客户端推送当前编译的hash戳

3.客户端的websocket 监听到有文件改动推送过来的hash戳, 会和上—次对比

  • 一直就走缓存

  • 不—致就通过ajax和jsonp向服务端获取最新资源

详细步骤

1.server端

  • 启动webpack-dev-server 服务器
  • 创建webpack实例
  • 创建server服务器
  • 添加webpack的done事件回调
  • 编译完成向客户端发送消息
  • 创建express应用app
  • 设置文件系统为内存文件系统
  • 添加webpack-dev-middleware 中间件
  • 中间件负责返回生成的文件
  • 启动webpack编译
  • 创建http服务器并启动服务
  • 使用sockjs在浏览器端和服务端之间建立—个websocket长连接
  • 创建socket服务器
  1. client端
  • webpack-dev-server/ client端会监听到此hash消息
  • 客户端收到ok消息后会执行reloadApp方法进行更新
  • 在reloadApp中会进行判断,是否支持热更新, 如果支持的话发生
  • webpackHotUpdate事件, 如果不支持就直接刷新浏览器
  • 在webpack/hoUdev-server.js会监听webpackHotUpdate 事件
  • 在check方法里会调用module.hot.check方法
  • HotModuleReplacement.runtime请求Manifest
  • 通过调用JsonpMainTemplate.runtim的e hotDownloadManifest方法
  • 调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP谓求获取最新的模块代码
  • 补丁js取回来或会调用JsonpMainTemplate.runtime.js的webpackHotUpdate方法
  • 然后会调用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法动态更新 模块代码
  • 然后调用hotApply方法进行热更新

img