webpack核心
为什么需要webpack
模块化的利与弊
优点:
- 代码复用
- 开发的时候效率提升 缺点:
- 兼容性问题:目前浏览器仅支持ES6模块
- 性能问题:划分过多的js文件,浏览器加载速度变慢
- 工具问题:浏览器不支持npm下载的第三方包
上面提到的问题,为什么在node端没有那么明显,反而到了浏览器端变得如此严重呢? 在node端,运行的JS文件在本地,因此可以本地读取文件,它的效率比浏览器远程传输文件高的多
根本原因
在浏览器端,开发时态(devtime)和运行时态(runtime)的侧重点不一样
开发时态,devtime:
- 模块划分越细越好
- 支持多种模块化标准
- 支持npm或其他包管理器下载的模块
- 能够解决其他工程化的问题
运行时态,runtime:
- 文件越少越好
- 文件体积越小越好
- 代码内容越乱越好
- 所有浏览器都要兼容
- 能够解决其他运行时的问题,主要是执行效率问题
webpack应运而生
我们需要一个工具(构建工具),能够将开发时态的代码转换成运行时态的代码,所以webpack应运而生,注意两个点:
- 构建工具是在node环境运行的
- 打包后的代码不存在模块化的概念,就是一个函数,一个js文件
webpack的安装
安装
npm install webpack --save-dev- webpack:核心包,包含了webpack构建过程中要用到的所有api
- webpack-cli:提供一个简单的cli命令,它调用了webpack核心包的api,来完成构建过程
使用
npx webpack模块化兼容性
不同模块化标准导入导出,webpack打包之后的处理结果如下
使用es6导出,commonjs导入会得到一个包含default属性的对象

使用commonjs导出,es6导入的时候需要使用导入默认值,或者全部导入

编译过程
初始化
读取配置:webpack会依托一个第三方库
yargs将CLI参数、配置文件、默认配置进行融合,形成一个最终的配置对象。初始化插件:实例化并调用配置中指定的插件。
构建编译对象:创建一个新的Compiler对象,并注册所有插件监听的钩子。
确定入口:根据entry配置确定一个或多个编译入口点。
编译
- 创建chunk
chunk是webpack在内部构建过程中的一个概念,译为块,它表示通过某个入口找到的所有依赖的统称。
根据入口模块(默认为./src/index.js)创建一个chunk
chunk 有两种形式:
- initial(初始化) 是入口起点的 main chunk。此 chunk 包含为入口起点指定的所有模块及其依赖项。
- non-initial 是可以延迟加载的块。可能会出现在使用 动态导入(dynamic imports) 或者 SplitChunksPlugin 时。
// webpack.config.js
module.exports = {
entry: './src/index.jsx',
};
// ./src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import('./app.jsx').then((App) => {
ReactDOM.render(<App />, root);
});
//Output:
/dist/main.js - 一个 initial chunk 包含:
- ./src/index.jsx
- react
- react-dom
/dist/394.js - non-initial chunk默认情况下,这些 non-initial chunk 没有名称,因此会使用唯一 ID 来替代名称。 在使用动态导入时,我们可以通过使用 magic comment(魔术注释) 来显式指定 chunk 名称:
import(
/* webpackChunkName: "app" */
'./app.jsx'
).then((App) => {
ReactDOM.render(<App />, root);
});
//Output:
/dist/main.js - 一个 initial chunk
/dist/app.js - non-initial chunk
每个chunk都有至少两个属性:
- name:默认为main
- id:唯一编号,开发环境和name相同,生产环境是一个数字,从0开始
- 构建所有依赖模块

- 产生chunk assets
在第二步完成后,chunk中会产生一个模块列表,列表中包含了模块id和模块转换后的代码(require-->webpack_require,路径变换...)
接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets,资源列表可以理解为是生成到最终文件的文件名和文件内容

chunk hash是根据所有chunk assets的内容生成的一个hash字符串
hash:一种算法,具体有很多分类,特点是将一个任意长度的字符串转换为一个固定长度的字符串,而且可以保证原始内容不变,产生的hash字符串就不变
- 合并chunk assets
将多个chunk的assets合并到一起,并产生一个总的hash

输出
- 文件生成:Webpack将chunk转换成文件,根据output配置确定输出的文件名称和路径。
- 代码优化:在chunks生成文件之前,可能会有压缩和优化的过程,此时会运行Optimization相关的插件。
- 生成浏览器可执行文件:输出最终构建的文件至指定目录,这通常是dist文件夹。
手写编译结果
手写一个webpack打包结果
//不能造成全局变量污染,所以需要创建一个闭包
(function (modules) {
var installedModules = {} //用于缓存模块的导出结果
function __webpack_require__(moudleId){//moudleId就是模块的路径
if(installedModules[moudleId]){ //检查缓存
return installedModules[moudleId]
}
var module = {
exports:{}
}
modules[moudleId].call(module.exports,module,module.exports,__webpack_require__) //运行模块 this指向module.exports
var result = module.exports //得到模块导出的结果
installedModules[moudleId] = result
return result
}
//执行入口模块
return __webpack_require__("./src/index.js") //require函数相当于是运行一个模块,得到模块导出结果
})({
//该对象保存了所有的模块,以及模块对应的代码
//使用eval有助于提高调试体验和减少源映射文件的大小
// //# sourceURL=webpack:///./src/a.js 注释可以保证调试的时候浏览器找到对应的js文件
"./src/a.js": function (module, exports) {
eval("console.log(\"module a\")\nmodule.exports = \"a\";\n //# sourceURL=webpack:///./src/a.js")
},
"./src/index.js": function (module, exports, __webpack_require__) {
eval("console.log(\"index module\")\nvar a = __webpack_require(\"./src/a.js\")\na.abc();\nconsole.log(a)\n //# sourceURL=webpack:///./src/index.js")
}
})配置文件
webpack提供的cli支持很多的参数,例如--mode,但更多的时候,我们会使用更加灵活的配置文件来控制webpack的行为
默认情况下,webpack会读取webpack.config.js文件作为配置文件(这也是必须使用commonjs格式的原因),但也可以通过CLI参数npx webpack --config custom.config.js来指定某个配置文件
配置文件中通过CommonJS模块导出一个对象,对象中的各种属性对应不同的webpack配置
当命令行参数与配置文件中的配置出现冲突时,以命令行参数为准。
注意点:
- 配置文件中的代码,必须是有效的node代码,这里是需要在编译过程中运行的
- 我们自己写的代码在编译过程中,webpack只会把他当成文件来分析其中的模块依赖关系,而不会运行里面的代码
基本配置:
- mode:编译模式,字符串,取值为development或production,指定编译结果代码运行的环境,会影响webpack对编译结果代码格式的处理
- entry:入口,字符串,指定入口文件
- 关于路径的一些前置知识:
./:- 模块化代码中,比如require(“./”),表示当前js所在的目录
- 在路径处理中,"./"表示node运行目录
__dirname:- 所有情况下,都表示当前运行的js文件所在的目录,他是一个绝对路径
path模块:- 根据多段路径拼接出一个绝对路径
path.resolve(__dirname,"./dist") - 不同操作系统的路径分隔符不一样,所以需要使用path模块来处理路径
- 根据多段路径拼接出一个绝对路径
module.exports = {
//单个入口语法
entry: "./src/index.js", //简写
entry: {
main: "./src/index.js"
}
//一次注入多个依赖文件,并且将它们的依赖关系绘制在一个 "chunk" 中时
entry: ['./src/file_1.js', './src/file_2.js'],
//对象语法
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js',
},
}- output:出口,对象,指定编译结果文件
- name:chunkname,默认为main
- hash: hash,chunkhash,通常用于解决缓存问题
module.exports = {
mode: "development",
entry: {
app: './src/app.js',
search: './src/search.js',
},
output: {
path: path.resolve(__dirname, "./dist"),
filename: '[name].[contenthash:5].js',
}
}入口出口配置最佳实践
- 应用程序和第三方库分开打包 如果要将应用程序的代码和第三方库的代码分开打包,可以使用多入口或利用SplitChunksPlugin。
// webpack.config.js
module.exports = {
entry: {
app: './src/index.js',
vendor: ['lodash', 'jquery'],
},
output: {
filename: '[name].bundle.js',
path: __dirname + '/dist',
},
};使用SplitChunksPlugin:
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
};项目目录结构:
root/
|- src/
| |- index.js
|- dist/
|- webpack.config.js- 多页应用(MPA)
module.exports = {
entry:{
pageA: "./src/pageA/index.js",
pageB: "./src/pageB/index.js",
statistics: "./src/statistics/index.js"
},
output:{
filename:"[name].[chunkhash:5].js"
}
}项目目录结构:
root/
|- src/
| |- pageOne.js
| |- pageTwo.js
| |- ...
|- dist/
|- webpack.config.js- 单页应用(SPA)
module.exports = {
entry: "./src/index.js",
output:{
filename:"index.[hash:5].js"
}
}项目目录结构:
root/
|- src/
| |- index.js
|- dist/
|- webpack.config.js- devtool:调试工具,字符串,指定调试工具


- 开发环境最佳实践:eval 、 eval-source-map 、eval-cheap-source-map、eval-cheap-module-source-map
- 生产环境最佳实践: none(不配置devtool)、source-map
loader
webpack loader: loader本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回。
module.exports = {
module: { //针对模块的配置,目前版本只有两个配置,rules、noParse
rules: [ //模块匹配规则,可以存在多个规则
{ //每个规则是一个对象
test: /\.js$/, //匹配的模块正则
use: [ //匹配到后应用的规则模块
{ //其中一个规则
loader: "模块路径", //loader模块的路径,该字符串会被放置到require中
options: { //向对应loader传递的额外参数
}
}
]
//use: ["模块路径1", "模块路径2"]//简化版
}
]
}
}plugin
webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。
class MyPlugin{
apply(compiler){
compiler.hooks.事件名称.事件类型(name, function(compilation){
//事件处理函数
})
}
}要将插件应用到webpack,需要把插件对象配置到webpack的plugins数组中,如下:
module.exports = {
plugins:[
new MyPlugin()
]
}webpack源码
babel 的原理
- parse: 把代码 code 变成 AST
- traverse: 遍历 AST 进行修改
- generate: 把 AST 变成代码 code2 即 code --(1)-> ast --(2)-> ast2 --(3)-> code2
为什么必须使用AST
- 你很难用正则表达式来替换,正则很容易把 let a = 'let' 变成 var a = 'var'
- 你需要识别每个单词的意思,才能做到只修改用于声明变量的 let
- 而 AST 能明确地告诉你每个 let 的意思