webpack

简介

什么是 webpack

webpack is a module bundler (模块打包工具)

webpack 可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其他的一些浏览器不能直接运行的拓展语言(scss, TypeScript 等),并将其打包为合适的格式以供浏览器使用。

安装

  • 环境:nodejs https://nodejs.org/en/

    版本参考官网发布的最新版本,可以提升 webpack 的打包速度

  • 全局安装(不推荐)

    1
    2
    3
    4
    5
    6
    7
    npm install webpack webpack-cli -g
    webpack-cli 可以帮助我们在命令行里使用 npx, webpack等命令

    webpack -v

    npm unistall webpack webpack-cli -g
    MacOS进行全局安装需要权限,命令前加上 sudo
  • 局部安装 项目内安装

    1
    npm install webpack webpack-cli --save-dev # -D
  • 安装指定版本

    1
    2
    3
    npm info webpack # 查看 webpack 的历史发布信息

    npm install webpack@x.xx webpack-cli -D

检查是否安装成功

1
2
3
4
5
webpack -v # 查看 webpack 版本(全局安装才能查看)

npx webpack -v # npx 帮助我们在项目中的 node_modules 里查找 webpack

./node_modules/.bin/webpack -v # 直接到当前 node_modules 模块里指定 webpack

测试:启动 webpack 打包

1
2
3
4
5

// CommonJS 模块引入

import add from './a'; // 需要使用 es module 导出
import minux from './b'; // 需要使用 CommonJS module 导出
1
npx webpack # 打包命令,使用webpack处理index.js这个文件

总结:webpack是一个模块打包工具,可以识别出引入模块的语法,早期的 webpack 只是个 js 模块的打包工具,现在可以使 css,png,vue 的模块打包工具

webpack 配置文件

零配置是很弱的,特定的需求,总是需要自己进行配置

1
2
3
4
module.exports = {
entry: './src/index.js', // 默认的入口文件
output: './dist/main.js' // 默认的输出文件
}

当我们使用 npx webpack ,表示的是使用 webpack 处理打包,./src/index.js 为入口模块。默认出口放在当前目录下的 dist 目录,打包后的模块名称是 main.js,当然我们可以自行修改

webpack 有默认的配置文件,webpack.config.js ,我们可以对这个文件进行修改,进行个性化配置

  • 默认配置文件:wbepack.config.js

    1
    npx webpack # 执行命令后,webpack 会找到默认的配置文件,并使用执行
  • 不使用默认的配置文件:webpackconfig.js

    1
    npx webpack --config webpackconfg.js # 指定webpack使用webpackconfig.js文件作为配置文件
  • 修改 package.json scripts 字段,使用 npm run xxx 来启动

    原理:模块局部安装会在 node_modules/.bin 目录下创建一个软链接

    1
    2
    3
    "scripts": {
    "dev": "webapck"
    }
    1
    npm run dev

webpack.config.js 配置结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
entry: './src/index.js', // 打包入口文件
output: './dist', // 输出目录
mode: 'production', // 打包环境
module: {
rules: [
// loaders 模块处理
{
test: /.css$/,
use: 'style-loader'
}
]
},
plugins: [ // 插件配置
new HtmlWebpackPlugin()
]
}

webpack 核心概念

entry

​ 指定 webpack 打包入口文件,webpack执行构建的第一步将从 entry 开始,可抽象成输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
// 单入口 SPA,本质是个字符串
entry: {
main: './src/index.js'
},
// === 相当于简写 ===
entry: './src/index.js',

// 多入口 MPA,entry是个对象
entry: {
index: './src/index.js',
login: './src/login.js'
}
}

output

​ 打包转换后的文件输出到磁盘位置以输出结果,在webpack经过一系列处理并得出最终代码后输出结果

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
output: {
filename: 'bundle.js', // 输出文件的名称
path: path.resolve(__dirname, 'dist') // 输出文件到磁盘目录,必须是绝对路径
}

// 多入口,使用占位符,保持文件名唯一
output: {
filename: '[name][chunkhash:8].js', // 利用占位符,文件名称不重复
path: path.resolve(__dirname, 'dist') // 输出文件到磁盘目录,必须是绝对路径
}
}

mode

​ mode 用来指定当前的构建环境

  • production
  • development
  • none
选项 描述
development 设置 process.env.NODE_ENV 的值为 development ,开启 NamedChunksPluginNamedModulesPlugin
production 设置 process.env.NODE_ENV 的值为 production ,开启 FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPluginOccurrenceOrderPluginSideEffectsFlagPluginTerserPlugin
node 不开启任何优化选项

如果没有设置,webpack 会将 mode 的默认值设置为 production

记住,设置 NODE_ENV 并不会自动的设置 mode

开发阶段的开启会有利于热更新的处理,识别哪个模块变化

生产阶段的开启会有帮助模块压缩,处理副作用等一些功能

loader

​ 模块解析,模块转换器,用于把模块原内容按照需求转换成新内容

​ webpack 是模块打包工具,儿模块不仅仅是 js,还可以是 css,图片或者其他格式

​ 但是 webpack 默认只知道处理 js 和 json 模块,那么其他格式的模块处理和处理方式就需要 loader

常见的 loader

1
2
3
4
5
6
7
8
9
style-loader
css-loader
less-loader
sass-loader
ts-loader
babel-loader
file-loader
eslint-loader
...

module

模块,在 webpack 里一切皆模块,一个模块对应一个文件,webpack 会从配置的 entry 开始递归找出所有依赖的模块

1
2
3
4
5
6
7
8
9
10
module: {
rules: [
{
test: /.xxx$/, // 指定匹配规则
use: {
loader: 'xxx-loader' // 指定使用的 loader
}
}
]
}

当 webpack 处理到不认识的模块时,需要再 webpack 中的 module 里进行配置,当检测到是什么格式的模块,使用什么 loader 来处理

file-loader

处理静态资源模块

原理是把打包入口中识别出的资源模块,移动到输出目录,并且返回一个地址名称

使用场景:

​ 当我们需要模块仅仅是从源代码挪移到打包目录,就可以使用 file-loader 来处理,txt, svg, csv, excel, 图片等资源

1
npm install file-loader -D
  • 处理图片

    案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    module: {
    rules: [
    {
    test: /.(png|jpe?g|gif)$/,
    use: {
    loader: 'file-loader',
    options: {
    name: '[name]_[hash].[ext]',
    outputPath: 'images/',
    publicPath: '../images'
    }
    }
    }
    ]
    }
    1
    2
    3
    4
    5
    6
    7
    8
    import logo from './webpack.jpg'

    let img = new Image();
    img.src = logo;
    img.classList.add('logo');

    let root = document.getElementById('root');
    root.append(img)
  • 处理字体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // css
    @font-face {
    font-family: "webfont";
    font-display: swap;
    src: url("webfont.woff2") format("woff2");
    }

    body {
    font-size: 24px;
    font-family: "webfont";
    background-color: #f40;
    }
    1
    2
    3
    4
    5
    // webpack.config.js
    {
    test: /.(eot|ttf|woff|woff2|svg)$/,
    use: 'file-loader'
    }

url-loader

url-loader 内部使用了 file-loader,所以可以处理 file-loader 所有的事情,但是遇到 体积较小图片模块会把该图片转换为 base64 格式字符串,并打包到 js 里。对小体积的图片比较适合,大图片得不偿失。

1
npm install url-loader -D

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module: {
rules: [
{
test: /.(png|jpe?g|gif)$/,
// use: 'file-loader'
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/',
limit: 2048 // 小于 2048,才转换为 base64
}
}
}
]
}

样式处理

css-loader 分析模块之间的关系,并合成一个css

style-loader 会把 css-loader 生成的内容,以style标签包裹并挂载到页面的head部分

1
npm install style-loader css-loader -D
1
2
3
4
5
// webpack.config.js
{
test: /.css$/,
use: ['style-loader', 'css-loader']
}
  • Less 样式处理

    less-loader 把 less 语法转换为 css

    1
    npm i less less-loader -D

    案例:

    loader 有顺序,从右到左,从下到上

    1
    2
    3
    4
    5
    // webpack.config.js
    {
    test: /.less$/,
    use: ['style-loader', 'css-loader', 'less-loader']
    }
  • 样式自动添加前缀,兼容

    https://caniuse.com/

    postcss-loader

    1
    npm i postcss-loader autoprefixer -D
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // webpack.config.js
    {
    test: /.less$/,
    use: [
    'style-loader',
    'css-loader',
    'less-loader',
    {
    loader: 'postcss-loader',
    options: {
    plugins: () => [
    require('autoprefixer')({
    overrideBrowserslist: ['last 2 versions', '>= 1%']
    })
    ]
    }
    }
    ]
    }

    可以直接在 webpack.config.js 中直接配置,也可以将 postcss 提取出来成一个文件 postcss.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // webpack.config.js
    {
    test: /.css$/,
    use: ['style-loader', 'css-loader', 'postcss-loadr']
    }

    // postcss.config.js
    module.exports = {
    plugins: [
    require('autoprefixer')({
    overrideBrowserslist: ['last 2 versions', '>= 1%']
    })
    ]
    }

Plugins

plugin 可以在 webpack运行到某个阶段的时候,帮你做一些事情,类似于生命周期的概念

扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情,作用于整个构建过程

HtmlWebpackPlugin

​ html-webpack-plugin 会在打包结束后,自动生成一个 html 文件,并把打包的生成的 js 模块注入到该 html 中

1
npm i html-webpack-plugin -D

配置

Name Type Default Description
title {String} Webpack App The title to use for the generated HTML document
filename {String} 'index.html' The file to write the HTML to. Defaults to index.html. You can specify a subdirectory here too (eg: assets/admin.html)
template {String} `` webpack relative or absolute path to the template. By default it will use src/index.ejs if it exists. Please see the docs for details
templateParameters `{Boolean Object Function}` `` Allows to overwrite the parameters used in the template - see example
inject `{Boolean String}` true `true ‘head’ ‘body’ falseInject all assets into the giventemplateortemplateContent. When passingtrueor‘body’all javascript resources will be placed at the bottom of the body element.‘head’` will place the scripts in the head element - see the inject:false example
favicon {String} `` Adds the given favicon path to the output HTML
meta {Object} {} Allows to inject meta-tags. E.g. meta: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}
base `{Object String false}` false Inject a base tag. E.g. base: "https://example.com/path/page.html
minify `{Boolean Object}` true if mode is 'production', otherwise false Controls if and in what ways the output should be minified. See minification below for more details.
hash {Boolean} false If true then append a unique webpack compilation hash to all included scripts and CSS files. This is useful for cache busting
cache {Boolean} true Emit the file only if it was changed
showErrors {Boolean} true Errors details will be written into the HTML page
chunks {?} ? Allows you to add only some chunks (e.g only the unit-test chunk)
chunksSortMode `{String Function}` auto Allows to control how chunks should be sorted before they are included to the HTML. Allowed values are `’none’ ‘auto’ ‘dependency’ ‘manual’ {Function}`
excludeChunks {Array.<string>} `` Allows you to skip some chunks (e.g don’t add the unit-test chunk)
xhtml {Boolean} false If true render the link tags as self-closing (XHTML compliant)

clean-webpack-plugin

1
npm i clean-webpack-plugin -D

mini-css-extract-plugin

1
npm i mini-css-extract-plugin -D
1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}

new MiniCssExtractPlugin({
filename: '[name][contenthash:8].css'
})

sourceMap

源代码与打包后的代码的映射关系

在 development 模式中,默认开启,可以在配置文件中设置

1
devtool: 'none'

devtool 的介绍:https://www.webpackjs.com/configuration/devtool/

  • eval: 速度最快,使用 eval 包裹模块代码

  • source-map:产生 .map 文件

  • cheap:较快,不用管列的信息,也不包含 loader 的 sourceMap

  • module:第三方模块,包含loader的sourceMap(比如 jsx to js,babel)

  • inline:将 .map 作为 DataURI 嵌入,不单独生成 .map 文件

配置推荐

1
2
3
devtool: 'cheap-module-eval-source-map'; // 开发环境配置
// 线上不推荐开启sourcemap,避免泄漏源代码
devtool: 'cheap-module-source-map'; // 线上生成配置

WebpackDevServer

提升开发效率的利器

因为每次改完代码都需要重新打包一次,打开浏览器,刷新一次,很麻烦

安装 webpack-dev-server 改善体验

启动服务后,会发现 dist 目录为空,这是因为 DevServer 吧打包后的模块放到内存中,从而提升速度

1
npm i webpack-dev-server -D

修改 package.json

1
2
3
4
// package.json
"script": {
"server": "webpack-dev-server"
}

配置

1
2
3
4
5
6
// webpack.config.js
devServer: {
contentBase: './dist',
open: true,
port: 8081
}

跨域

联调期间,前后端分离,直接获取数据会跨域,上线后我们使用 Nginx 转发,开发期间,webpack 就能搞定

启动一个服务器,mock 一个借口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// npm i express -D
// 创建一个 server.js 修改 scripts "server": "node server.js"

// server.js
const express = require('express');
const app = express();

app.get('/api/info', (req, res) => {
res.json({
name: 'hhh',
age: 19,
msg: 'hello express'
})
})
app.listen(9092)
1
npm i axios -D
1
2
3
4
5
// index.js
import axios from 'axios'
axios.get('http://localhost:9092/api/info').then(res => {
consol.elog(res)
})

!!会产生跨域问题

修改 webpack.config.js 设置服务器代理

1
2
3
4
5
6
7
8
// webpack.config.js
devServer: {
proxy: {
'/api': {
target: 'http://localhost:9092'
}
}
}

修改 index.js

1
2
3
4
// index.js
axios.get('/api/info').then(res => {
console.log(res)
})

搞定!

  1. 和服务器约定好接口,定义好字段
  2. 接口文档啥时候给到
  3. 根据接口文档mock数据,mock接口

文件监听

​ 轮询判断文件的最后编辑事件是否变化,某个文件发生变化,并不会立即告诉监听者,先缓存起来

webpack 开启监听模式,有两种

  • 启动 webpack 命令,带上 --watch 参数,启动监听后,还是需要手动刷新浏览器

    1
    2
    3
    scripts: {
    "watch": "webpack --watch"
    }
  • 在配置文件里设置 watch: true

    1
    2
    3
    4
    5
    6
    7
    8
    module.exports = {
    watch: true, // 默认 false
    watchOptions: { // 配合 watch: true
    ignored: /node_modules/, // 忽略的目录
    aggregateTimeout: 300, // 监听到文件变化后,等待300ms再去执行,默认300ms
    poll: 1000 // 轮询周期,每秒1次,1000ms
    }
    }

HMR

Hot Module Replacement (热模块替换)

启动 HMR

1
2
3
4
5
6
7
// webpack.config.js
devServer: {
contentBase: './dist',
open: true,
hot: true,
hotOnly: true // 即使HMR不生效,浏览器也不自动刷新,就开启 hotOnly
}

配置文件头部引入 webpack

1
2
3
4
5
6
// webpack.config.js
const webpack = require('webpack')
// ...
plugins: [
new webpack.HotModuleReplacementPlugin()
]

在插件配置处添加

1
2
3
4
5
6
7
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new Webpack.HotModuleReplacementPlugin()
]

案例:

1
2
3
4
5
6
7
8
9
10
11
12
// index.js
import './css/index.css';

let btn = document.createElement('button');
btn.innerHTML = 'add';
document.body.appendChild(btn);

btn.onclick = function () {
let div = document.createElement('div');
div.innerHTML = 'item';
document.body.appendChild(div);
}
1
2
3
4
// index.css
div:nth-of-type(odd) {
background-color: yellow;
}

注意:启动HMR后,css抽离会不生效,不支持 contenthash,chunkhash

处理 js 模块 HMR

​ 需要使用 module.hot.accept 来观察模块更新

案例:

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
// counter.js
function () {
let div = document.createElement('div');
div.setAttribute('id', 'counter');
div.innerHTML = 1;
div.onclick = function () {
div.innerHTML = parseInt(div.innerHTML, 10) + 1;
}
document.body.appendChild(div)
}
export default counter;

// number.js
function number () {
let div = document.createElement('div');
div.setAttribute('id', 'number');
div.innerHTML = 12000;
document.body.appendChild(div)
}
export default number

// index.js
import counter from './counter';
import number from './number';

counter()
number()

if (module.hot) {
module.hot.accept('./b', function () {
document.body.removeChild(document.getElementById('number'));
number();
})
}

Babel

官方网站:https://babeljs.io/

中文网站:https://www.babeljs.cn/

1
2
3
4
npm i babel-loader @babel/core @babel/preset-env -D

# babel-loader 是 webpack 与 babel 的通信桥梁,不会把es6转为es5,这部分工作需要用到 @babel/preset-env
# @babel/preset-env 包含了es6转es5的转换规则

测试代码

1
2
3
4
5
6
// index.js
const arr = [new Promise(() => { }), new Promise(() => { })]

arr.map(item => {
console.log(item)
})

webpack.config.js

1
2
3
4
5
6
7
8
9
10
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}

通过上面几部还不够,Promise 等一些特性还没有转换过来,这时候需要借助 @babel/polyfill ,把 es6 的新特性都装进来,来弥补低版本浏览器中缺失的特性

@babel/polyfill

以全局变量的方式注入进来,windows.Promise 会污染全局变量

1
npm i @babel/polyfill -S
1
2
3
4
5
6
7
8
9
// webpack.config.js
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
1
2
// index.js 顶部
import "@babel/polyfill"

虽然这样实现了,但是打包的体积大了不少,应为 polyfill 默认会把所有的特性注入进来,假如我想我⽤到的es6+,才会注⼊,没⽤到的不注⼊,从⽽减少打包 的体积,可不可以呢?

当然可以

修改 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
options: {
presets: [
[
"@babel/preset-env",
{
targets: {
edge: '17',
firefox: '60',
chrome: '67',
safari: '11.1'
}
},
corejs: 2, // 新版本需要指定核心库版本
useBuiltIns: 'usage' // 按需注入
]
]
}

当开发的是组件库,工具库这些场景的时候,polyfill 就不合适了,因为 polyfill 是注入到 全局变量window下,会污染全局变量,所以推荐闭包方式:@babel/plugin-transform-runtime

@babel/plugin-transform-runtime

它不会造成全局污染

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
// .babelrc
// 注释掉之前的 presets,添加 plugins
{
// "presets": [
// [
// "@babel/preset-env",
// {
// "targets": {
// "edge": "17",
// "firefox": "60",
// "chrome": "67",
// "safari": "11.1"
// },
// "useBuiltIns": "usage",
// "corejs": 2
// }
// ]
// ]
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModule": false
}
]
]
}

useBuiltIns 选项是 babel 7 的新功能,这个选项告诉 babel 如何配置 @babel/polyfill

三个参数

  1. entry 需要在 webpack 的入口文件里 import '@babel/polyfill' 一次。babel 会根据你的使用情况导入垫片,没有使用的功能不会被导入相应的垫片
  2. usage 不需要 import ,全自动检测,但是要安装 @babel/polyfill (试验阶段)
  3. false 如果你 import "@babel/polyfill" ,它不会排除掉没有使用的垫片,程序体积很庞大(不推荐)

请注意:usage 的行为类似 babel-tranform-runtime,不会造成 全局污染,因此也不会对类似 Array.prototype.includes() 进行 polyfill

扩展:

.babelrc 文件

新建 .babelrc 文件,把 options 部分移入到该文件中就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// .babelrc
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModule": false
}
]
]
}
// webpack.config.js
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}

配置 React 环境

安装 react 环境

1
npm i react react-dom -S

编写 react 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// index.js
import "@babel/polyfill";

import React, { Component } from "react";
import ReactDom from "react-dom";

class App extends Component 大专栏  webpackan>{
render() {
return <div>hello world</div>;
}
}

ReactDom.render(<App />, document.getElementById("app"));

安装 babel 和 react 转换的插件

1
npm i @babel/preset-react --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// .babelrc
{
"preset": [
[
"@babel/preset-env",
{
targets: {
edge: '17',
firefox: '60',
chrome: '67',
safari: '11.1',
Android: '6.0'
}
},
corejs: 2, // 新版本需要指定核心库版本
useBuiltIns: 'usage' // 按需注入
],
"@babel/preset-react"
]
}

扩展:多页面打包通用方案

1
2
3
4
5
6
7
8
9
10
11
entry:{
index: "./src/index",
list: "./src/list",
detail: "./src/detail"
}
new htmlWebpackPlugins({
title: "index.html",
template: path.join(__dirname, "./src/index/index.html"),
filename: "index.html",
chunks: [index]
})
  1. 目录结构调整
  • src
    • index
      • index.js
      • index.html
    • list
      • index.js
      • index.html
    • detail
      • index.js
      • index.html
  1. 使用 glob.sync 第三方库来匹配路径
1
npm i glob -D
1
const glob = require('glob')
1
2
3
4
5
6
7
8
9
10
// MPA 多页面打包通用方案
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
return {
entry, htmlWebpackPlugins
}
}

const { entry, htmlWebpackPlugins } = setMPA();
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
const setMPA = () => {
const entry = {}
const htmlWebpackPlugins = []

const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'))

entryFiles.map((item, index) => {
const entryFile = entryFiles[index]
const match = entryFile.match(/src/(.*)/index.js$/)
const pageName = match && match[1]
entry[pageName] = entryFile
htmlWebpackPlugins.push(
new htmlWebpackPlugin({
title: pageName,
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true
})
)
})

return {
entry, htmlWebpackPlugins
}
}

const { entry, htmlWebpackPlugins } = setMPA()

module.exports = {
entry,
output: {
path: path.resolve(__dirnmae, './dist'),
filename: '[name].js'
},
plugins: [
// ...
...htmlWebpackPlugins
]
}

webpack 打包原理

webpack 在执⾏npx 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
(function (modules) { // webpackBootstrap
// The module cache
var installedModules = {};

// The require function
function __webpack_require__ (moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};

// Execute the module function
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);

// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
return __webpack_require__((__webpack_require__.s = "./index.js"));
})({
"./index.js": function (module, exports) {
eval(
'// import a from "./a";nnconsole.log("hello word");nnn//# sourceURL=webpack:///./index.js?'
)
},
"./a.js": function (module, exports) {
eval(
'// import a from "./a";nnconsole.log("hello word");nnn//# sourceURL=webpack:///./index.js?'
)
},
"./b.js": function (module, exports) {
eval(
'// import a from "./a";nnconsole.log("hello word");nnn//# sourceURL=webpack:///./index.js?'
);
}
});

⼤概的意思就是,我们实现了⼀个webpack_require 来实现⾃⼰的模块化,把代码都缓存在installedModules⾥,代码⽂件以对象传递进来,key 是路径,value是包裹的代码字符串,并且代码内部的require,都被替换成了webpack_require

实现一个 bundle.js

  • 模块分析:读取入口文件,分析代码

    1
    2
    3
    4
    5
    6
    7
    8
    const fs = require('fs')

    const moduleAnalyser = filename => {
    const content = fs.readFileSync(filename, 'utf-8')
    console.log(content)
    }

    moduleAnalyser('./index.js')
  • 拿到⽂件中依赖,这⾥我们不推荐使⽤字符串截取,引⼊的模块名越多,就越麻烦,不灵活,这⾥我们推荐使⽤@babel/parser,这是babel7的⼯具,来帮助我们分析内部的语法,包括es6,返回⼀个 ast 抽象语法树

    @babel/parser https://babeljs.io/docs/en/babel-parser

    安装 @babel/parser

    1
    npm i @babel/parser --save

    bundle.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const fs = require('fs')
    const parser = require('@babel/parser')

    const moduleAnalyser = filename => {
    const content = fs.readFileSync(filename, 'utf-8')

    const ast = parser.parse(content, {
    sourceType: 'module'
    })
    console.log(ast.program.body)
    }

    moduleAnalyser('./index.js')
  • 接下来我们就可以根据body⾥⾯的分析结果,遍历出所有的引⼊模 块,但是⽐较麻烦,这⾥还是推荐babel推荐的⼀个模块@babel/traverse,来帮我们处理。

    1
    npm i @babel/traverse --save
    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 fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default

    const moduleAnalyser = filename => {
    const content = fs.readFileSync(filename, 'utf-8')

    const ast = parser.parse(content, {
    sourceType: 'module'
    })

    const dependecies = [];
    // 分析ast抽象语法树,根据需要返回对应数据,
    // 根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
    traverse(ast, {
    ImportDeclaration ({ node }) {
    console.log(node)
    dependecies.push(node.source.value)
    }
    })
    console.log(dependecies)
    }

    moduleAnalyser('./index.js')

最终结果图片

image-20200204001011196

分析上图,我们要分析出信息:

  • 入口文件
  • 入口文件引入的模块
    • 引入路径
    • 在项目中里的路径
  • 可以在浏览器中执行的代码

处理现在的路径问题

1
2
3
4
5
6
7
8
9
10
11
// 需要⽤到path模块
const parser = require("@babel/parser");

// 修改 dependencies 为对象,保存更多的信息
const dependencies = {};

// 分析出引⼊模块,在项⽬中的路径
const newfilename = "./" + path.join(path.dirname(filename), node.source.value);

// 保存在dependencies⾥
dependencies[node.source.value] = newfilename;

把代码处理成浏览器可运⾏的代码,需要借助@babel/core,和 @babel/preset-env,把ast语法树转换成合适的代码

1
2
3
4
5
const babel = require('@babel/core')

const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})

到处所有分析出的信息

1
2
3
4
5
return {
filename,
dependecies,
code
}

完成代码参考

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
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})

const dependecies = {};
// 分析ast抽象语法树,根据需要返回对应数据,
// 根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
traverse(ast, {
ImportDeclaration ({ node }) {
const newfilename = './' + path.join(path.dirname(filename), node.source.value)
dependecies[node.source.value] = newfilename
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})

return {
filename,
dependecies,
code
}
}

const moduleInfo = moduleAnalyser('./src/index.js')

console.log(moduleInfo)
  • 分析依赖

    上一步我们已经完成了一个模块的分析,接下来我们要完成项目里所有模块的分析

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})

const dependecies = {};
// 分析ast抽象语法树,根据需要返回对应数据,
// 根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
traverse(ast, {
ImportDeclaration ({ node }) {
const newfilename = './' + path.join(path.dirname(filename), node.source.value)
dependecies[node.source.value] = newfilename
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})

return {
filename,
dependecies,
code
}
}

const makeDependenciesGraph = entry => {
const entryModule = moduleAnalyser(entry)
const graphArr = [entryModule]
for (let i = 0; i < graphArr.length; i++) {
const item = graphArr[i];
const { dependecies } = item
if (dependecies) {
for (let j in dependecies) {
graphArr.push(
moduleAnalyser(dependecies[j])
)
}
}
}
const graph = {}
graphArr.forEach(item => {
graph[item.filename] = {
dependecies: item.dependecies,
code: item.code
}
})

return graph
}

const moduleInfo = makeDependenciesGraph('./src/index.js')

console.log(moduleInfo)
  • 生成代码
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})

const dependecies = {};
// 分析ast抽象语法树,根据需要返回对应数据,
// 根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
traverse(ast, {
ImportDeclaration ({ node }) {
const newfilename = './' + path.join(path.dirname(filename), node.source.value)
dependecies[node.source.value] = newfilename
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})

return {
filename,
dependecies,
code
}
}

const makeDependenciesGraph = entry => {
const entryModule = moduleAnalyser(entry)
const graphArr = [entryModule]
for (let i = 0; i < graphArr.length; i++) {
const item = graphArr[i];
const { dependecies } = item
if (dependecies) {
for (let j in dependecies) {
graphArr.push(
moduleAnalyser(dependecies[j])
)
}
}
}
const graph = {}
graphArr.forEach(item => {
graph[item.filename] = {
dependecies: item.dependecies,
code: item.code
}
})

return graph
}

const generateCode = entry => {
const graph = JSON.stringify(makeDependenciesGraph(entry))
return `
(function (graph) {
function require (module) {
function localRequire (relativePath) {
return require(graph[module].dependencies[relativePath])
}
var exports = {}
(function (require, exports, code) {
eval(code)
})(localRequire, exports, graph[module].code)
return exports
}
require('${entry}')
})(${graph})
`
}

const code = generateCode('./src/index.js')

console.log(code)

node 调试工具使用

  • 修改 scripts

    1
    "debug": "ndoe --inspect --inspect-brk ndoe_modules/webpack/bin/webpack.js"

如何自己编写一个 Loader

⾃⼰编写⼀个Loader的过程是⽐较简单的,

Loader就是⼀个函数,声明式函数,不能⽤箭头函数

拿到源代码,作进⼀步的修饰处理,再返回处理后的源码就可以了

官⽅⽂档:https://webpack.js.org/contribute/writing-a-loader/

接⼝⽂档:https://webpack.js.org/api/loaders/

简单案例

  • 创建⼀个替换源码中字符串的loader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // index.js
    console.log('hello kkb')

    // replaceLoader.js
    module.exports = function (source) {
    console.log(source, this, this.query)
    return source.replace('kkb', '开课吧')
    }

    // 需要⽤声明式函数,因为要上到上下⽂的this,⽤到this的数据,该函数接受⼀个参数,是源码
  • 在配置文件中使用 loader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 需要使用 node 核心模块 path 来处理路径
    const path = require('path')
    module: {
    rules: [
    {
    test: /.js$/,
    use: path.resolve(__dirname, './loader/replaceLoader.js')
    }
    ]
    }
  • 如何给 loader 配置参数,loader 如何接受参数?

    • this.query
    • loader-utils
    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
    // webpack.config.js
    module: {
    rules: [
    {
    test: /.js$/,
    use: [
    {
    loader: path.resolve(__dirname, './loader/replaceLoader.js'),
    options: {
    name: '开课吧'
    }
    }
    ]
    }
    ]
    }

    // replaceLoader.js
    const loaderUtils = require("loader-utils"); //官⽅推荐处理 loader,query的⼯具

    module.exports = function(source) {
    // this.query 通过this.query来接受配置⽂件传递进来的参数
    // return source.replace("kkb", this.query.name);
    const options = loaderUtils.getOptions(this);
    const result = source.replace("kkb", options.name);
    return source.replace("kkb", options.name);
    }
  • this.callback 如何返回多个信息,不只是处理好的源码,可以使用 this.callback 来处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // replaceLoader.js
    const loaderUtils = require("loader-utils"); // 官⽅推荐处理 loader,query的⼯具

    module.exports = function(source) {
    const options = loaderUtils.getOptions(this);
    const result = source.replace("kkb", options.name);
    this.callback(null, result);
    };

    // this.callback(
    err: Error | null,
    content: string | Buffer,
    sourceMap?: SourceMap,
    meta?: any
    );
  • this.async 如果 loader 里面有异步事件要怎么处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const loaderUtils = require("loader-utils")

    module.exports = function (source) {
    const options = loaderUtils.getOptions(this)
    setTimeout(() => {
    return source.replace('kkb', options.name)
    }, 1000)
    }
    // 先用 setTimeout 处理下试试,发现会报错

    我们使用 this.async来处理,他会返回 this.callback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const loaderUtils = require("loader-utils")

    module.exports = function (source) {
    const options = loaderUtils.getOptions(this)
    // 定义⼀个异步处理,告诉webpack,这个loader⾥有异步事件,在⾥⾯调⽤下这个异步
    // callback 就是 this.callback 注意参数的使⽤
    const callback = this.async();
    setTimeout(() => {
    const result = source.replace('kkb', options.name)
    callback(null, result)
    }, 1000)
    }
  • 多个 loader 的使用

    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
    // replaceLoader.js
    module.exports = function (source) {
    return source.replace('开课吧', 'word')
    }

    // replaceLoaderAsync.js
    const loaderUtils = require("loader-utils")
    module.exports = function (source) {
    const options = loaderUtils.getOptions(this)
    // 定义⼀个异步处理,告诉webpack,这个loader⾥有异步事件,在⾥⾯调⽤下这个异步
    const callback = this.async();
    setTimeout(() => {
    const result = source.replace('kkb', options.name)
    callback(null, result)
    }, 1000)
    }

    // webpack.config.js
    module: {
    rule: [
    {
    test: /.js$/,
    use: [
    path.resolve(__dirname, './loader/replaceLoaderAsync.js'),
    {
    loader: path.resolve(__dirname, './loader/replaceLoaderAsync.js'),
    options: {
    name: '开课吧'
    }
    }
    ]
    }
    ]
    }

    顺序,从上而下,从右到左

  • 处理 loader 的路径问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    resolveLoader: {
    modules: ['node_modules', './loader']
    },
    module: {
    rules: [
    {
    test: /.js$/,
    use: [
    'replaceLoader',
    {
    loader: 'replaceLoaderAsync',
    options: {
    name: '开课吧'
    }
    }
    ]
    }
    ]
    }

参考:loader API

https://webpack.js.org/api/loaders

如何自己编写一个 Plugins

Plugin 开始打包,在某个时刻,帮助我们处理⼀些什么事情的机制

plugin要⽐loader稍微复杂⼀些,在webpack的源码中,⽤plugin的机制还是占有⾮常⼤的场景,可以说plugin是webpack的灵魂

设计模式

事件驱动

发布订阅

plugin是⼀个类,⾥⾯包含⼀个apply函数,接受⼀个参数,compiler

官⽅⽂档:https://webpack.js.org/contribute/writing-a-plugin/

案例:

  • 创建 copyright-webpack-plugin.js

    1
    2
    3
    4
    5
    6
    7
    8
    class CopyrightWebpackPlugin {
    constructor () {}

    // compiler webpack 实例
    apply (compiler) {}
    }

    module.exports = CopyrightWebpackPlugin
  • 配置文件里使用

    1
    2
    3
    const CopyrightWebpackPlugin = require('./plugin/copyright-webpack-plugin')

    plugins: [new CopyrightWebpackPlugin()]
  • 如何传递参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // webpack.config.js
    plugins: [
    new CopyrightWebpackPlugin({
    name: '开课吧'
    })
    ]

    // copyright-webpack-plugin.js
    class CopyrightWebpackPlugin {
    constructor(options) {
    // 接收参数
    console.log(options)
    }
    apply(compiler) {}
    }

    module.exports = CopyrightWebpackPlugin
  • 配置 Plugin 在什么时刻进行

    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
    class CopyrightWebpackPlugin {
    constructor (options) {}
    apply (compiler) {
    // hooks.emt 定义在某个时刻
    compiler.hooks.emit.tapAsync(
    "CopyrightWebpackPlugin",
    (compilation, cb) => {
    compilation.assets['copyright.txt'] = {
    source: function () {
    return 'hello copy'
    },
    size: function () {
    return 20
    }
    }
    cb()
    }
    )
    // 同步的写法
    /*
    compiler.hooks.compile.tap(
    'CopyrightWebpackPlugin',
    compilation => {
    // ...
    }
    )
    */
    }
    }

    module.exports = CopyrightWebpackPlugin

参考:compiler-hooks

https://webpack.js.org/api/compiler-hooks

tree shaking

webpack 2.x 开始支持 tree shaking 概念,顾名思义,摇树,只支持ES module 的引入方式!!!!

1
2
3
4
// webpack.config.js
optimization: {
usedExports: true
}
1
2
3
4
// package.json
"sideEffects": false // 正常对所有模块进行 tree shaking 或 sideEffects

["*.css", "@babel/polyfill"]

开发模式设置后,不会帮助我们把没有引用的代码去掉

code spliting

​ 代码分割,比如讲一些第三方库抽离,形成单独的文件


This is copyright.

原文地址:https://www.cnblogs.com/lijianming180/p/12432958.html