Webpack相关
Webpack的Loader和Plugin的区别
-
作用不同:
Loader直译为“加载器”。Webpack将一切文件视为模块,但是Webpack原生只能解析js文件,如果想将其他文件也打包的话,就会用到Loader。所以Loader的作用是让Webpack拥有了加载和解析非js文件的能力(可理解为转换器)
Plugin直译为“插件”。Plugin可以扩展Webpack 的功能,让Webpack具有更多的灵活性。在Webpack运行的生命周期中广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack 提供的API改变输出结果(可理解为扩展器)
-
用法不同:
Loader在module.rules中配置,也就是说作为模块的解析规则而存在。类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
plugin 在plugins中单独配置。类型为数组,每一项都是一个plugin实例,参数都能通过构造函数传入
常见Loader:
文件
raw-loader
加载文件原始内容(utf-8)val-loader
将代码作为模块执行,并将 exports 转为 JS 代码url-loader
像 file loader 一样工作,但如果文件小于限制,可以返回 data URLfile-loader
将文件发送到输出文件夹,并返回(相对)URL
JSON
json-loader
加载 JSON 文件(默认包含)json5-loader
加载和转译 JSON 5 文件cson-loader
加载和转译 CSON 文件
转换编译(Transpiling)
script-loader
在全局上下文中执行一次 JavaScript 文件(如在 script 标签),不需要解析babel-loader
加载 ES2015+ 代码,然后使用 Babel 转译为 ES5buble-loader
使用 Bublé 加载 ES2015+ 代码,并且将代码转译为 ES5traceur-loader
加载 ES2015+ 代码,然后使用 Traceur 转译为 ES5ts-loader
或awesome-typescript-loader
像 JavaScript 一样加载 TypeScript 2.0+coffee-loader
像 JavaScript 一样加载 CoffeeScript
模板(Templating)
html-loader
导出 HTML 为字符串,需要引用静态资源pug-loader
加载 Pug 模板并返回一个函数jade-loader
加载 Jade 模板并返回一个函数markdown-loader
将 Markdown 转译为 HTMLreact-markdown-loader
使用 markdown-parse parser(解析器) 将 Markdown 编译为 React 组件posthtml-loader
使用 PostHTML 加载并转换 HTML 文件handlebars-loader
将 Handlebars 转移为 HTMLmarkup-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)
mocha-loader
使用 mocha 测试(浏览器/NodeJS)eslint-loader
PreLoader,使用 ESLint 清理代码jshint-loader
PreLoader,使用 JSHint 清理代码jscs-loader
PreLoader,使用 JSCS 检查代码样式coverjs-loader
PreLoader,使用 CoverJS 确定测试覆盖率
框架(Frameworks)
vue-loader
加载和转译 Vue 组件polymer-loader
使用选择预处理器(preprocessor)处理,并且require()
类似一等模块(first-class)的 Web 组件angular2-template-loader
加载和转译 Angular 组件
常见 Plugin
-
define-plugin
定义环境变量(Webpack4之后指定mode会自动配置) -
ignore-plugin
忽略部分文件 -
commons-chunk-plugin
提取公共代码 -
html-webpack-plugin
简化HTML文件创建(依赖于 html-loader) -
web-webpack-plugin
可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用 -
uglifyjs-webpack-plugin
不支持 ES6 压缩(Webpack4 以前) -
terser-webpack-plugin
支持压缩 ES6 (Webpack5自带) -
mini-css-extract-plugin
分离样式文件,CSS提取为独立文件,支持按需加载(替代extract-text-webpack-plugin) -
ModuleConcatenationPlugin
开启 Scope Hoisting -
speed-measure-webpack-plugin
可以看到每个Loader和Plugin执行耗时(整个打包耗时、每个Plugin和 Loader 耗时) -
webpack-bundle-analyzer
可视化Webpack输出文件的体积(业务组件、依赖第三方模块)
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
另外虽然 exports
和 module.exports
用法相似,但是不能对 exports
直接赋值。因为 var exports = module.exports
这句代码表明了 exports
和 module.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中常用的配置对象属性如下
-
contentBase:'./' 本地服务器在哪个目录搭建页面,一般在当前目录即可
-
historyApiFallback:true 搭建spa应用时会用到。它使用的是HTML5 History Api,任意的跳转或404响应可以指向index.html页面
-
inline:true 用来支持dev-server自动刷新的配置,webpack有两种模式支持自动刷新,一种是iframe模式,一种是inline模式,使用iframe模式是不需要再devServer进行配置的。只需使用特定的URL格式访问即可,不过我们一般使用的是inline模式,在devServer中对inline设置为true后,当启动webpack-dev-server时仍需配置inline才能生效
-
hot:true 启动webpack 热模块替换特性
-
port:端口号(默认8080)
运行原理
- 启动HTTP服务
- webpack 构建时输出Bundle到内存,HTTP服务从内存中读取Bundle文件
- 监听文件变化,重新执行第二个步骤
静态资源访问
{
devServer:{
contentBase: 'public'
}
}
Proxy代理
{
devServer:{
proxy:{
'/api'{
target:'http://api.target.com'
}
}
}
}
如何实现webpack持久化缓存
- 服务端设置HTTP缓存头(Cache-Control等)
- 打包依赖(dependencies)和运行时(runtime)到不同chunk,即作splitChunk,因为他们几乎是不变的
- 延迟加载:使用import()方式,可以动态加载的文件分到独立的chunk,以得到自己的chunkHash
- 保证hash值稳定:编译过程和文件内容的更改尽量不影响其他文件hash的计算。
现在比较成熟的持久化缓存方案就是在静态资源的名字后面加hash值,因为每次修改文件生成的hash 值不一样,这样做的好处在于增量式发布文件,避免覆盖掉之前的文件而导致线上的用户访问失效
webpack 热更新原理
- webpack compiler: 将js编译成Bundle
- Bundle Server: 提供文件在浏览器的访问,实际上就是一个服务器
- HMRServer: 将热更新的文件输出给HMR Runtime
- HMR Runtime: 会注入到bundle.js中,与HRM Server通 过webSocket 链接,接收文件变化,并更新对应的文件
- .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服务器
- 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方法进行热更新