可以看懂的webpack教程

关于webpack,真是让人又爱又恨。作为前台端水的一员,搞懂webpack是必不可少的!
Webpack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的预编译语言(scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。

一份webpack配置(webpack.config.js)包含以下内容:

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
  • Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。

从零开始配置

首先,当然是需要安装webpack工具

1
2
npm init --yes // 生成一份package.json文件,方便管理版本
npm i webpack webpack-cli --save-dev // 安装webpack

有了 Webpack 后,就可以直接运行 webpack 命令来打包 JS 模块代码

1
npx webpack

这个命令在执行的过程中,Webpack 会自动从 src/index.js 文件开始打包,然后根据代码中的模块导入操作,自动将所有用到的模块代码打包到一起。完成之后,打包后的文件会出现在dist文件夹里的main.js文件

  • 一个小tips:让vs code支持webpack智能提示
    我们通过 import 的方式导入 Webpack 模块中的 Configuration 类型,然后根据类型注释的方式将变量标注为这个类型,这样我们在编写这个对象的内部结构时就可以有正确的智能提示了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { Configuration } from 'webpack'
    /**
    * @type {Configuration}
    */

    // 或者是这种写法
    /** @type {import('webpack').Configuration} */

    module.exports = {
    // some config
    }

注意:在配置完成后记得注释掉这段import代码,否则编译会出错。因为node环境还不支持import语句

可以开始自定义配置webpack了

大多数情况下,我们都会有一些自定义的需求,因此,可以新建一个webpack.config.js文件来自定义配置webpack。
webpack.config.js 是一个运行在 Node.js 环境中的 JS 文件,也就是说我们需要按照 CommonJS 的方式编写代码,这个文件可以导出一个对象,我们可以通过所导出对象的属性完成相应的配置选项。

1
2
3
module.exports = {
// some config
}

自定义入口文件(entry)和输出结果(output)

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path')

module.exports = {
entry: './src/main.js', // @入口文件 string | object | array
output:{ // @输出选项:
filename: '[name].[hash].js', // 文件名: string
path: path.join(__dirname, 'output'), // 所有输出文件的出口目录
publicPath: "/assets/", // 构建文件的输出目录
},
module:{},
plugins:[],
devServer:{}
}

理想开发模式

如果我们每次修改完代码,都是通过命令行手动重复运行 Webpack 命令,从而得到最新的打包结果,那么这样的操作过程根本没有任何开发体验可言。
理想的开发模式应该是:修改代码 → Webpack 自动打包 → 自动刷新浏览器 → 预览运行结果

曲线救国方案:
  • Webpack 自动打包:在启动 Webpack 时,添加一个 –watch 的 CLI 参数
  • 自动刷新浏览器: BrowserSync 就可以帮我们实现文件变化过后浏览器自动刷新的功能。
    1
    2
    3
    # 可以先通过 npm 全局安装 browser-sync 模块,然后再使用这个模块
    npm install browser-sync --global
    browser-sync dist --watch
    它的原理就是 Webpack 监视源代码变化,自动打包源代码到 dist 中,而 dist 中文件的变化又被 BrowserSync 监听了,从而实现自动编译并且自动刷新浏览器的功能,整个过程由两个工具分别监视不同的内容。
配置开发服务器

webpack-dev-server 是 Webpack 官方推出的一款开发工具,它提供了一个开发服务器,并且将自动编译和自动刷新浏览器等一系列对开发友好的功能全部集成在了一起。

1
2
3
4
# 安装 webpack-dev-server
npm install webpack-dev-server --save-dev
# 运行 webpack-dev-server, --open参数,用于自动唤起浏览器打开我们的应用
npx webpack-dev-server --open

配置选项:

1
2
3
4
5
6
7
8
devServer: {
contentBase: path.resolve(__dirname,"dist"), // 静态文件目录
port: 5000, // 端口号
host: 'localhost',
contentBase: 'public', // 静态资源路径访问
overlay: true,
compress: true // 服务器返回浏览器的时候是否启动gzip压缩
}

由于 webpack-dev-server 是一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost 的一个端口上,而后端服务又是运行在另外一个地址上。但是最终上线过后,我们的应用一般又会和后端服务部署到同源地址下。
那这样就会出现一个非常常见的问题:在实际生产环境中能够直接访问的 API,回到我们的开发环境后,再次访问这些 API 就会产生跨域请求问题。
webpack-dev-server 就支持直接通过配置的方式,添加代理服务,解决这个问题

1
2
3
4
5
6
7
devServer: {
proxy: {
'/api': { //
target: 'https://www.xxx.com'
}
}
}

此时请求 http://localhost:8080/api/users –> https://www.xxx.com/api/users。
如果希望请求的地址是 https://www.xxx.com/users ,所以对于代理路径开头的 /api 要重写掉。我们可以添加一个 pathRewrite 属性来实现代理路径重写

1
2
3
4
5
6
7
8
9
10
11
12
13
devServer: {
proxy: {
'/api': { //
target: 'https://www.xxx.com',
pathRewrite: {
'^/api': '' // 替换掉代理地址中的 /api
},
// 会以实际代理请求地址中的主机名去请求,也就是正常请求这个地址的主机名是什么,实际请求 xxx 时就会设置成什么。
changeOrigin: true,

}
}
}

此时请求 http://localhost:8080/api/users –> https://wwww.xxx.com/users。

这样的配置代码才能调试

运行webpack命令后,打开调试器查看源代码,会发现source/index.js都是压缩过的代码,难以进行调试(比如打断点)。
通过Source Map可以解决这个问题(原理大概就是逆向转译成源码)

1
2
3
module.exports = {
devtool: 'source-map' // source map 设置
}
  • source-map: 把映射文件生成到单独的文件,最完整最慢
  • cheap-module-source-map: 在一个单独的文件中产生一个不带列映射的Map
  • eval-source-map: 使用eval打包源文件模块,在同一个文件中生成完整sourcemap
  • cheap-module-eval-source-map: sourcemap和打包后的JS同行显示,没有映射列
    现阶段 Webpack 支持的 Source Map 模式有很多种。每种模式下所生成的 Source Map 效果和生成速度都不一样
    可以从官方文档参考不同模式的效率。

tip: Eval模式
Webpack 会将每个模块转换后的代码都放到 eval 函数中执行,并且通过 sourceURL 声明对应的文件路径,这样浏览器就能知道某一行代码到底是在源代码的哪个文件中。因为在 eval 模式下并不会生成 Source Map文件,所以它的构建速度最快,但是缺点同样明显:它只能定位源代码的文件路径,无法知道具体的行列信息。

这样的配置调试起来更方便

解决完开发环境会产生的跨域和源码不可暴露调试的问题,还有一个常见的问题还没有解决。
在调试中,经常遇到因为修改了一处小细节(比如css样式),调整后,基于上面的设置,页面会自动刷新,那前面调试的步骤就会丢失,又要重复去进行调试步骤。

出现这个问题的原因,是因为我们每次修改完代码,Webpack 都可以监视到变化,然后自动打包,再通知浏览器自动刷新,一旦页面整体刷新,那页面中的任何操作状态都将会丢失,所以才会出现我们上面所看到的情况。

如果能够实现在页面不刷新的情况下,代码也可以及时的更新到浏览器的页面中,重新执行,避免页面状态丢失,那就最好了!
模块热替换(HMR)就可以实现这个需求!

使用这个特性最简单的方式就是,在运行 webpack-dev-server 命令时,通过--hot参数去开启这个特性
或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const webpack = require('webpack')

module.exports = {
// ...
devServer: {
// 开启 HMR 特性,如果资源不支持 HMR 会 fallback 到 live reloading
hot: true
// 只使用 HMR,不会 fallback 到 live reloading
// hotOnly: true
},
plugins: [
// ...
// HMR 特性所需要的插件
new webpack.HotModuleReplacementPlugin()
]
}

对于css文件的热替换,不会出现什么问题,对于js文件的热替换,还需要手动配合webpack

1
2
3
module.hot.accept('./user', () => {
// 图片的热替换也是一样的写法
})

配置不同环境的打包命令

通过修改package.json,可以简化在不同环境下要执行的打包任务

1
2
3
4
"script": {
"build": "webpack --mode production", // 开启代码压缩,用于生产环境
"dev": "webpack-dev-sever --open --mode development" // 热加载,用于开发环境
}

支持各种框架和预编译语言

  • 处理css,sass,以及css3属性前缀

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // cmd
    npm install style-loader css-loader postcss-loader autoprefixer -D

    // webpack.config.json
    module.exports = {
    // ...
    module: {
    rules:[
    // 处理css
    {
    test: /\.css$/,
    exclude: /node_modules/,
    include: path.resolve(__dirname,'src'), // 限制范围,提高打包速度
    use: [
    // 多个loader是有顺序的,从后往前写
    {
    loader: "style-loader",
    options:{
    singleton: true // 处理为单个style标签
    }
    },
    {
    loader: "css-loader",
    },
    {
    loader: "postcss-loader",
    }
    ]
    },
    // 处理scss文件:将sass编译成css,再将css转成CommonJS模块,再将js字符串转成style节点
    {
    test: /\.scss$/,
    use:['style-loader','css-loader','sass-loader']
    }
    ]
    }
    }

    // 处理css前缀:新建一个postcss.config.js
    module.exports = {
    plugins: [
    require('autoprefixer')
    ]
    }
  • 支持ES6,react,vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // cmd
    npm install babel-loader @babel/core @babel/preset-env @babel/preset-react -D

    // webpack.config.json
    module.exports = {
    // ...
    module: {
    rules:[
    // ...
    {
    test: /\.jsx?$/,
    exclude: /node_modules/,
    use: [
    {
    loader: "babel-loader"
    }
    ]
    }
    ]
    }
    }

    // .babelrc
    {
    "presets": ["@babel/core", "@babel/preset-env", "@babel/preset-react"]
    }

常见的加分配置

  • 提取css文件为单独文件
  • 产出html
  • 处理引用第三方库,将第三方库全局变量暴露出来使用
  • 懒加载(按需加载)
  • 图片处理: 将小图片使用base64编码,大图片使用file-loader
  • 全局注入环境变量:编译的时候将定义的变量替换成对应的字符串

webpack进阶部分

Tree Shaking

摇到未引用和冗余的代码,使用 Webpack 生产模式打包的优化过程中,就使用自动开启这个功能。
Tree-shaking 的本身没有太多需要你理解和思考的地方,你只需要了解它的效果,以及相关的配置即可。

sideEffects

Webpack 4 中新增了一个 sideEffects 特性,它允许我们通过配置标识我们的代码是否有副作用,从而提供更大的压缩空间。

TIPS:模块的副作用指的就是模块执行的时候除了导出成员,是否还做了其他的事情
Tree-shaking 只能移除没有用到的代码成员,而想要完整移除没有用到的模块,那就需要开启 sideEffects 特性了。
TIPS:注意这个特性在 production 模式下同样会自动开启。

1
2
3
4
5
6
7
8
9
10
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
optimization: {
sideEffects: true
}
}

sideEffects 可能需要花点时间去理解一下,重点就是想明白哪些副作用代码是可以随着模块的移除而移除,哪些又是不可以移除的。总结下来其实也很简单:对全局有影响的副作用代码不能移除,而只是对模块有影响的副作用代码就可以移除。
所以,尽可能不要写影响全局的副作用代码

Code Splitting(代码分割)

为了解决打包结果过大导致的问题,webpack提供了一种分包功能————代码分割

  • 多入口打包
    划分规则就是一个页面对应一个打包入口,对于不同页面间公用的部分,再提取到公共的结果中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    module.exports = {
    entry: {
    index: './src/index.js',
    album: './src/album.js'
    },
    output: {
    filename: '[name].bundle.js' // [name] 是入口名称
    },
    // ... 其他配置
    plugins: [
    new HtmlWebpackPlugin({
    title: 'Multi Entry',
    template: './src/index.html',
    filename: 'index.html',
    chunks: ['index'] // 指定使用 index.bundle.js
    }),
    new HtmlWebpackPlugin({
    title: 'Multi Entry',
    template: './src/album.html',
    filename: 'album.html',
    chunks: ['album'] // 指定使用 album.bundle.js
    })
    ]
    }

    一般 entry 属性中只会配置一个打包入口,如果我们需要配置多个入口,可以把 entry 定义成一个对象。一旦我们的入口配置为多入口形式,那输出文件名也需要修改,因为两个入口就有两个打包结果,不能都叫 bundle.js。我们可以在这里使用 [name] 这种占位符来输出动态的文件名,[name] 最终会被替换为入口的名称。

  • 抽取公共模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    module.exports = {
    entry: {},
    output: {},
    optimization: {
    splitChunks: {
    // 自动提取所有公共模块到单独 bundle
    chunks: 'all'
    }
    }
    }

可以看懂的webpack教程
https://appleking10.github.io/2020/10/26/可以看懂的webpack教程/
Author
金依妮
Posted on
October 26, 2020
Licensed under