1.webpack介绍
webpack概念
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler),将项目当作一个整体,通过一个给定的的主文件,webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包成一个或多个浏览器可识别的js文件
组成
- 入口(entry)
入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始 - 输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 bundles ,以及如何命名这些文件,默认值为 ./dist - loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript) - 插件(plugins)
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量 - 模式
通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化 - devtool
用于控制是否以及如何生成源代码映射,可以帮助开发快速定位错误
2.webpack构建流程
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
- 确定入口:根据配置中的 entry 找出所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
3.loader
常见loader
- css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
- style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
- slint-loader:通过 SLint 检查 JavaScript 代码
- babel-loader:把 ES6 转换成 ES5
- file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
4.plugin
常见plugin
define-plugin:定义环境变量
- commons-chunk-plugin:提取公共代码
- open-browser-webpack-plugin: 自动打开浏览器
- clean-webpack-plugin: 清除文件
- html-webpack-plugin: 向文件中插入html
5.loader和plugin的区别
loader 加载器
loader是指webpack打包方案,对于很多文件例如less,icon,图片等webpack不知道如何打包,通过loader来告诉webapck如何打包,让 webpack 拥有了加载和解析非 JavaScript 文件的能力
在 module.rules 中配置,也就是说他作为模块的解析规则而存在,类型为数组
Plugin 插件
plugin是指插件包,plugins里面的插件会帮助我们做一些其他的事情, 让 webpack 具有更多的灵活性,提升开发效率。
在 plugins 中单独配置。类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入
对于loader,它就是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程
plugin是一个扩展器,它丰富了wepack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。
6.bundle,chunk,module
bundle 是由 webpack 打包出来的文件,chunk 是指 webpack 在进行模块的依赖分析的时候,代码分割出来的代码块。module是开发中的单个模块
7.webpack配置跨域
devServer: {
contentBase: './dist', // 起一个在dist文件夹下的服务器
open: true, // 自动打开浏览器并访问服务器地址
proxy: { // 跨域代理
'/api': 'http: //localhost:3000' // 如果使用/api,会被转发(代理)到该地址
},
port: 8080,
hot: true, // 开启HMR功能
hotOnly: true // 即使HMR不生效,也不自动刷新
},
8.webpack模块热更新
HMR的有两种实现方式,一种是通过插件HotModuleReplacementPlugin和devserver配和实现,一种是通过在自定义开发服务下,使用插件webpack-dev-middleware和webpack-Hot-middleware配合实现HMR
通过插件HotModuleReplacementPlugin()
1.配置
在webpack.config.js中配置devServer
devServer: {
contentBase: './dist', // 起一个在dist文件夹下的服务器
open: true, // 自动打开浏览器并访问服务器地址
port: 8085,
hot: true, // 开启HMR功能
hotOnly: true // 即使HMR不生效,也不自动刷新
},
pluginsp配置中使用HotModuleReplacementPlugin插件
plugins: [
...// 其他插件
new webpack.HotModuleReplacementPlugin()
],
2.判断
然后进行手动判断进行模块热更新,如果你不想做以下判断那么可以使用module.hot.accept(),整个项目做hmr只要有代码变化就进行更新。
if(module.hot) {
module.hot.accept('./number', () => {
// 使用更新过的 library 模块执行某些操作...
})
}
3.启动
最重要一点不要忘了修改启动命令
"start": "webpack-dev-server"
此时运行npm start,即可实现模块热更新
通过 Node.js API
通过自己在本地搭建一个服务器,利用webpack-dev-middleware和webpack-Hot-middleware两个插件来配合实现HMR.
1.安装
// 安装express, webpack-dev-middleware , webpack-Hot-middleware
cnpm install express webpack-dev-middleware webpack-Hot-middleware -D
2.配置dev.server.js
dev.server.js
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require("webpack-Hot-middleware")
const config = require('./webpack.dev.js');
const complier = webpack(config); // 编译器,编译器执行一次就会重新打包一下代码
const app = express(); // 生成一个实例
const {devServer: {port, contentBase}} = config
const DIST_DIR = path.resolve(__dirname, '../', contentBase); // 设置静态访问文件路径
// 等同于const DIST_DIR = path.resolve(__dirname, '../dist');
let devMiddleware = webpackDevMiddleware(complier, { // 使用编译器
publicPath: config.output.publicPath,
quiet: true, //向控制台显示任何内容
noInfo: true
})
let hotMiddleware = webpackHotMiddleware(complier,{
log: false,
heartbeat: 2000
})
app.use(devMiddleware)
app.use(hotMiddleware)
// 设置访问静态文件的路径
app.use(express.static(DIST_DIR))
app.listen(port, () => {
console.log("成功启动:localhost:"+ port)
}) //监听端口
webpack.dev.js配置
module.exports = {
entry: { // 入口文件配置
//实现刷新浏览器webpack-hot-middleware/client?noInfo=true&reload=true 是必填的
main: ['webpack-hot-middleware/client?noInfo=true&reload=true', './src/index.js']
},
devServer: {
contentBase: 'dist',
port: 8081
},
plugins: [
new webpack.NamedModulesPlugin(), //用于启动HMR时可以显示模块的相对路径
new webpack.HotModuleReplacementPlugin(),
new OpenBrowserPlugin({ url: 'http://localhost:8081' }), // 自动打开浏览器
],
...// 其他配置
}
完整实现在这里
webpack-hot-middleware的配置项
配置项可以通过query 方式添加到webpack config中的路径来传递给客户端
配置项都有
- path - 中间件为事件流提供的路径
- name - 捆绑名称,专门用于多编译器模式
- timeout - 尝试重新连接后断开连接后等待的时间
- overlay - 设置为false禁用基于DOM的客户端覆盖。
- reload - 设置为true在Webpack卡住时自动重新加载页面。
- noInfo - 设置为true禁用信息控制台日志记录。
- quiet - 设置为true禁用所有控制台日志记录。
- dynamicPublicPath - 设置为true使用webpack publicPath作为前缀path。(我们可以webpack_public_path在入口点的运行时动态设置,参见output.- publicPath的注释)
- autoConnect - 设置为false用于防止从客户端自动打开连接到Webpack后端 - 如果需要使用该setOptionsAndConnect功能修改选项
通过传递第二个参数,可以将配置选项传递给中间件
webpackHotMiddleware(webpack,{
log: false,
path: "/__what",
heartbeat: 2000
})
- log - 用于记录行的函数,传递false到禁用。默认为console.log
- path - 中间件将服务事件流的路径必须与客户端设置相匹配
- heartbeat - 多长时间将心跳更新发送到客户端以保持连接的活动。应小于客户的timeout设置 - 通常设置为其一半值。
更多配置在这里webpack-hot-middleware
注意:通过express启动服务器后,devServer中的配置就不起作用了。
3.启动命令
"start": "node ./build/dev-server.js",
启动命令npm start,即可实现HMR的功能
9.HMR实现原理
1.HMR的更新流程
- 修改了一个或多个文件。
- 文件系统接收更改并通知Webpack。
- Webpack重新编译构建一个或多个模块,并通知HMR服务器进行了更新。
- HMR Server使用websockets通知HMR Runtime需要更新。(HMR运行时通过HTTP请求这些更新。)
- HMR运行时再替换更新中的模块。如果确定这些模块无法更新,则触发整个页面刷新
2.HMR 工作流程图解
此为更加详细的流程分析:
上图是webpack 配合 webpack-dev-server 进行应用开发的模块热更新流程图。
- 上图底部红色框内是服务端,而上面的橙色框是浏览器端。
- 绿色的方框是 webpack 代码控制的区域。蓝色方框是 webpack-dev-server 代码控制的区域,洋红色的方框是文件系统,文件修改后的变化就发生在这,而青色的方框是应用本身
步骤分析: - 第一步,在 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 操作,也就是进行浏览器刷新来获取最新打包代码。
10.sourceMap的工作原理
工作原理
sourceMap本质上是一种映射关系,打包出来的js文件中的代码可以映射到代码文件的具体位置。例如在打包后有代码错误,这种映射关系会帮助我们直接找到在源代码中的错误
11.Tree-Shaking
Tree Shaking可以剔除掉一个文件中未被引用掉部分(在producton环境下才会提出),并且只支持ES Modules模块的引入方式,不支持CommonJS的引入方式。原因:ES Modules是静态引入的方式,CommonJS是动态的引入方式,Tree Shaking只支持静态引入方式。
在开发环境下需要在webpack中配置,但是在生成环境下,由于已有默认配置可以不配置optimization,但是sideEffects依然需要配置
12.Code Spliting
使用Code Spliting可以有两种方式
第一手动实现:
配置
entry: {
lodash: './src/lodash.js',
main: './src/index.js'
}, // 入口文件
lodash.js
import _ from 'lodash';
window._ = _;
第二:使用optimization
同步代码:只需要在webpack中做optimization配置即可
异步代码:无需做任何配置,会自动进行代码分割,放入dist目录中
optimization: {
splitChunks: {
chunks: 'all' // 遇到公用当类库时,自动的Code Spliting
}
},
13.webpack性能优化
- 1.跟上技术的迭代更新
- 2.尽可能少的模块使用loader
- 3.plugin尽可能精简并保证可靠
例如尽可能使用官方的插件,并在合适的环境下使用对象的插件 - 4.resolve参数合理配置
当通过import child from ‘./child/child’形式引入文件时,会先去寻找.js为后缀当文件,再去寻找.jsx为后缀的文件
resolve: {
extensions: ['.js', '.jsx'],
mainFiles: ['index', 'child'], // 如果是直接引用一个文件夹,那么回去直接找index开头的文件,如果不存在再去找child开头的文件
alias: {
roshomon: path.resolve(__dirname, '../src/child'); // 别名替换,引入roshomon其实是引入../src/child
}
}
- 5.使用DellPlugin提高打包速度
对于第三方库,只打包分析一次,后面的每次打包都不会重复打包第三方库
webpack.dll.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
vendors: ['lodash'],
react: ['react', 'react-dom'],
jquery: ['jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({ // 使用该插件分析第三方库,并把库里面的映射关系放到[name].manifest.json里,并放在dll文件里
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json'),
})
]
}
webpack.common.js
// 引用
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
// plugins配置
....
const plugins = [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
];
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
if(/.*\.dll.js/.test(file)) {
plugins.push(new AddAssetHtmlWebpackPlugin({ // 将dll.js文件自动引入html
filepath: path.resolve(__dirname, '../dll', file)
}))
}
if(/.*\.manifest.json/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({ // 当打包第三方库时,会去manifest.json文件中寻找映射关系,如果找到了那么就直接从全局变量(即打包文件)中拿过来用就行,不用再进行第三方库的分析,以此优化打包速度
manifest: path.resolve(__dirname, '../dll', file)
}))
}
})
package.json
"build:dll": "webpack --config ./build/webpack.dll.js"
- 6.控制包文件大小
可以通过treeShaking或者拆分文件来优化打包速度 - 7.thread-loader, parallel-webpack,happywebpack多进程打包
- 8.合理使用sourceMap
- 9.结合stats分析打包结果
通过命令生成一个关于打包情况的stats文件,并借助工具进行打包情况分析,通过分析打包的流程对相应内容进行优化 - 10.开发环境内存编译
- 11.开发环境无用插件剔除
14.AST
抽象语法树 (Abstract Syntax Tree),是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础
es6转es5
将ES6的代码转换为AST语法树,然后再将ES6 AST转为ES5 AST,再将AST转为代码
15.babel编译原理
babylon 将 ES6/ES7 代码解析成 AST
babel-traverse 对 AST 进行遍历转译,得到新的 AST
新 AST 通过 babel-generator 转换成 ES5
或者:
- 它就是个编译器,输入语言是ES6+,编译目标语言是ES5
- babel 官方工作原理
- 解析:将代码字符串解析成抽象语法树
- 变换:对抽象语法树进行变换操作
- 再建:根据变换后的抽象语法树再生成代码字符串
16.babel-profilly和babel-transform-runtime的区别
一、babel-polyfill
由于babel默认只转换新的JavaScript语法,但对于一些新的API是不进行转化的(比如内建的Promise、WeakMap,静态方法如Array.from或者Object.assign),那么为了能够转化这些东西,我们就需要使用babel-polyfill这个插件
由于babel-polyfill是个运行时垫片,所以需要声明在dependencies而非devDependencies里
二、babel-plugin-transform-runtime
由于使用babel-polyfill,会产生以下问题:
1)babel-polyfill会将需要转化的API进行直接转化,这就导致用到这些API的地方会存在大量的重复代码
2)babel-polyfill是直接在全局作用域里进行垫片,所以会污染全局作用域
所以,babel同时提供了babel-plugin-transform-runtime这一插件,它的好处在于:
1)需要用到的垫片,会使用引用的方式引入,而不是直接替换,避免了垫片代码的重复
2)由于使用引用的方式引入,所以不会直接污染全局作用域。这就对于库和工具的开发带来了好处
但是babel-plugin-transform-runtime仍然不能单独作用。因为有一些静态方法,如”foobar”.includes(“foo”)仍然需要引入babel-polyfill才能使用
17.babel、babel-polyfill的区别
babel-polyfill:模拟一个es6环境,提供内置对象如Promise和WeakMap
引入babel-polyfill全量包后文件会变得非常大。它提供了诸如 Promise,Set 以及 Map 之类的内置插件,这些将污染全局作用域,可以编译原型链上的方法。
babel-plugin-transform-runtime & babel-runtime:转译器将这些内置插件起了别名 core-js,这样你就可以无缝的使用它们,并且无需使用 polyfill。但是无法编译原型链上的方法
runtime 编译器插件做了以下三件事:
- 当你使用 generators/async 函数时,自动引入 babel-runtime/regenerator 。
- 自动引入 babel-runtime/core-js 并映射 ES6 静态方法和内置插件。
- 移除内联的 Babel helper 并使用模块 babel-runtime/helpers 代替。
18.webpack与grunt、gulp区别
gulp、grunt
gulp和grunt强调的是前端开发的工作流程,我们可以通过配置一系列的task,定义task处理的事务(例如文件压缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序,来让gulp执行这些task,从而构建项目的整个前端开发流程。
PS:简单说就一个Task Runner
webpack
webpack是一个前端模块化方案,更侧重模块打包,我们可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。
PS:webpack is a module bundle
相同功能
- 文件合并与压缩(css)
- 文件合并与压缩(js)
- sass/less预编译
- 启动server
- 版本控制
两者区别
虽然都是前端自动化构建工具,但看他们的定位就知道不是对等的。
gulp严格上讲,模块化不是他强调的东西,他旨在规范前端开发流程。
webpack更是明显强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。
总结
gulp应该与grunt比较,而webpack应该与browserify(网上太多资料就这么说,这么说是没有错,不过单单这样一句话并不能让人清晰明了)。
gulp与webpack上是互补的,还是可替换的,取决于你项目的需求。如果只是个vue或react的单页应用,webpack也就够用;如果webpack某些功能使用起来麻烦甚至没有(雪碧图就没有),那就可以结合gulp一起用