webpack由浅入深看这一篇就够了

284 阅读16分钟

1.webpack简介

Webpack 是⼀个现代 JavaScript 应⽤程序的静态模块打包器(module bundler),当 webpack 处理应 ⽤程序时,它会递归地构建⼀个依赖关系图(dependency graph),其中包含应⽤程序需要的每个模块, 然后将所有这些模块打包成⼀个或多个 bundle。

Webpack是⼀个打包模块化JavaScript的⼯具,它会从⼊⼝模块出发,识别出源码中的模块化导⼊语句,递归 地找出⼊⼝⽂件的所有依赖,将⼊⼝和其所有的依赖打包到⼀个单独的⽂件中 是⼯程化、⾃动化思想在前端开发中的体现。

2.webpack 安装

环境准备

nodeJs :nodejs.org/en/ 版本参考官⽹发布的最新版本,可以提升webpack的打包速度

安装⽅式

局部安装(推荐)

npm init -y # 初始化npm配置⽂件

npm install --save-dev webpack # 安装核⼼库

npm install --save-dev webpack-cli # 安装命令⾏⼯具

# 安装最新的4.x稳定版本

npm i -D webpack@4.44.0

# 安装指定版本

npm i -D webpack@<version>

全局安装(不推荐)

# 安装webpack V4+版本时,需要额外安装webpack-cli

npm install webpack webpack-cli -g

# 检查版本

webpack -v

# 卸载

npm uninstall webpack webpack-cli -g

全局安装webpack,这会将你项⽬中的webpack锁定到指定版本,造成不同的项⽬中因为webpack依赖不同版 本⽽导致冲突,构建失败

3.启动webpack

3.1- webpack默认配置

webpack默认⽀持JS模块和JSON模块 ⽀持CommonJS Es moudule AMD等模块类型 webpack4⽀持零配置使⽤,但是很弱,稍微复杂些的场景都需要额外扩展

3.2- 准备执⾏构建

新建src⽂件夹

新建src/index.js、src/index.json、src/other.js

### index.js

const json = require("./index.json");//commonJS

import { add } from "./other.js";//es module

console.log(json, add(2, 3));

### index.json

{

"name": "JOSN"

}

### other.js

export function add(n1, n2) {

return n1 + n2;

}

3.3- 执⾏构建

# npx⽅式

npx webpack

# npm script

npm run test

修改package.json⽂件:

"scripts": {
    "test": "webpack"
},

原理就是通过shell脚本在node_modules/.bin⽬录下创建⼀个软链接。

3.4-构建成功

我们会发现⽬录下多出⼀个 dist ⽬录,⾥⾯有个 main.js ,这个⽂件是⼀个可执⾏的JavaScript⽂件, ⾥⾯包含webpackBootstrap启动函数。

3.5-默认配置

const path = require("path");

module.exports = {

// 必填 webpack执⾏构建⼊⼝

entry: "./src/index.js",

output: {

// 将所有依赖的模块合并输出到main.js

filename: "main.js",

// 输出⽂件的存放路径,必须是绝对路径

path: path.resolve(__dirname, "./dist")

}

};

4.webpack配置核⼼概念

chunk:指代码块,⼀个 chunk 可能由多个模块组合⽽成,也⽤于代码合并与分割。

bundle:资源经过Webpack 流程解析编译后最终结输出的成果⽂件。

entry:顾名思义,就是⼊⼝起点,⽤来告诉webpack⽤哪个⽂件作为构建依赖图的起点。

webpack会根据entry递归的去寻找依赖,每个依赖都将被它处理,最后输出到打包成果中。

output:output配置描述了webpack打包的输出配置,包含输出⽂件的命名、位置等信息。

loader:默认情况下,webpack仅⽀持 .js .json ⽂件,通过loader,可以让它解析其他类型的

⽂件,充当翻译官的⻆⾊。理论上只要有相应的loader,就可以处理任何类型的⽂件。

plugin:loader主要的职责是让webpack认识更多的⽂件类型,⽽plugin的职责则是让其可以控制

构建流程,从⽽执⾏⼀些特殊的任务。插件的功能⾮常强⼤,可以完成各种各样的任务。

webpack的功能补充

mode:4.0开始,webpack⽀持零配置,旨在为开发⼈员减少上⼿难度,同时加⼊了mode的概

念,⽤于指定打包的⽬标环境,以便在打包的过程中启⽤webpack针对不同的环境下内置的优

化。

零配置

零配置是很弱的,特定的需求,总是需要⾃⼰进⾏配置

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

使⽤默认的配置⽂件:webpack.config.js

不使⽤⾃定义配置⽂件: ⽐如webpackconfig.js,可以通过--config webpackconfig.js来指定

webpack使⽤哪个配置⽂件来执⾏构建

webpack.config.js配置基础结构

module.exports = {

entry: "./src/index.js", //打包⼊⼝⽂件

output: "./dist", //输出结构

mode: "production", //打包环境

module: {

rules: [

    //loader模块处理
    {
        test: /\.css$/,
        use: "style-loader"
    }
]
},

plugins: [new HtmlWebpackPlugin()] //插件配置

};

4.1-entry:

指定webpack打包⼊⼝⽂件:Webpack 执⾏构建的第⼀步将从 Entry 开始,可抽象成输⼊

//单⼊⼝ SPA,本质是个字符串

entry:{
    main: './src/index.js'
}
==相当于简写===

entry:"./src/index.js"

//多⼊⼝ entry是个对象
entry:{
    index:"./src/index.js",
    login:"./src/login.js"
}

4.2-output:

打包转换后的⽂件输出到磁盘位置:输出结果,在 Webpack 经过⼀系列处理并得出最终想要的代码后输 出结果

output: {

filename: "bundle.js",//输出⽂件的名称

path: path.resolve(__dirname, "dist")//输出⽂件到磁盘的⽬录,必须是绝对路径

},

//多⼊⼝的处理

output: {

filename: "[name][chunkhash:8].js",//利⽤占位符,⽂件名称不要重复

path: path.resolve(__dirname, "dist")//输出⽂件到磁盘的⽬录,必须是绝对路径

}

4.3-mode

Mode⽤来指定当前的构建环境

production、 development、 none 设置mode可以⾃动触发webpack内置的函数,达到优化的效果 开发阶段的开启会有利于热更新的处理,识别哪个模块变化 ⽣产阶段的开启会有帮助模块压缩,处理副作⽤等⼀些功能

4.4-loader

模块解析,模块转换器,⽤于把模块原内容按照需求转换成新内容。 webpack是模块打包⼯具,⽽模块不仅仅是js,还可以是css,图⽚或者其他格式 但是webpack默认只知道如何处理js和JSON模块,那么其他格式的模块处理,和处理⽅式就需要loader 了

常⻅的loader

style-loader

css-loader

less-loader

sass-loader

ts-loader //将Ts转换成js

babel-loader//转换ES6、7等js新特性语法

file-loader//处理图⽚⼦图

eslint-loader

...

4.5-moudle

模块,在 Webpack ⾥⼀切皆模块,⼀个模块对应着⼀个⽂件。Webpack 会从配置的 Entry 开始递归找 出所有依赖的模块。

当webpack处理到不认识的模块时,需要在webpack中的module处进⾏配置,当检测到是什么格式的 模块,使⽤什么loader来处理。

module:{
    rules:[
        {
            test:/\.xxx$/,//指定匹配规则
            use:{
            loader: 'xxx-load'//指定使⽤的loader
            }
        }
    ]
}

案例:样式处理:

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

Style-loader 会把css-loader⽣成的内容,以style挂载到⻚⾯的heade部分

npm install style-loader css-loader -D
{
    test: /\.css$/,
    use: ["style-loader", "css-loader"]
}

4.6 Plugins:webpack的扩展补充

作⽤于webpack打包整个过程

webpack的打包过程是有(⽣命周期概念)钩⼦

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

扩展插件,在 Webpack 构建流程中的特定时机注⼊扩展逻辑来改变构建结果或做你想要的事情。

作⽤于整个构建过程

HtmlWebpackPlugin

htmlwebpackplugin会在打包结束后,⾃动⽣成⼀个html⽂件,并把打包⽣成的js模块引⼊到该html 中。

npm install --save-dev html-webpack-plugin

配置:

title: ⽤来⽣成⻚⾯的 title 元素

filename: 输出的 HTML ⽂件名,默认是 index.html, 也可以直接配置带有⼦⽬录。

template: 模板⽂件路径,⽀持加载器,⽐如 html!./index.html

inject: true | 'head' | 'body' | false ,注⼊所有的资源到特定的 template 或者

templateContent 中,如果设置为 true 或者 body,所有的 javascript 资源将被放置到 body

元素的底部,'head' 将放置到 head 元素中。

favicon: 添加特定的 favicon 路径到输出的 HTML ⽂件中。

minify: {} | false , 传递 html-minifier 选项给 minify 输出

hash: true | false, 如果为 true, 将添加⼀个唯⼀的 webpack 编译 hash 到所有包含的脚本和

CSS ⽂件,对于解除 cache 很有⽤。

cache: true | false,如果为 true, 这是默认值,仅仅在⽂件修改之后才会发布⽂件。

showErrors: true | false, 如果为 true, 这是默认值,错误信息会写⼊到 HTML ⻚⾯中

chunks: 允许只添加某些块 (⽐如,仅仅 unit test 块)

chunksSortMode: 允许控制块在添加到⻚⾯之前的排序⽅式,⽀持的值:'none' | 'default' |

{function}-default:'auto'

excludeChunks: 允许跳过某些块,(⽐如,跳过单元测试的块)

案例:

const path = require("path");

const htmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {

...

plugins: [

    new htmlWebpackPlugin({

    title: "My App",

    filename: "app.html",

    template: "./src/index.html"

    })

]

};

//index.html

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<meta http-equiv="X-UA-Compatible" content="ie=edge" />

<title><%= htmlWebpackPlugin.options.title %></title>

</head>

<body>

    <div id="root"></div>

</body>

</html>

clean-webpack-plugin

npm install --save-dev clean-webpack-plugin
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

...

plugins: [

new CleanWebpackPlugin()

]

5.webpack——lodaer的深入应用

5.1 图片字体资源处理

loader: file-loader:处理静态资源模块

原理是把打包⼊⼝中识别出的资源模块,移动到输出⽬录,并且返回⼀个地址名称

所以我们什么时候⽤file-loader呢?

场景:就是当我们需要模块,仅仅是从源代码挪移到打包⽬录,就可以使⽤file-loader来处理,

txt,svg,csv,excel,图⽚资源啦等等

案例:

npm install file-loader -D

module: {
    rules: [
        {
            test: /\.(png|jpe?g|gif)$/,
            //use使⽤⼀个loader可以⽤对象,字符串,两个loader需要⽤数组
            use: {
                    loader: "file-loader",
                    // options额外的配置,⽐如资源名称
                    options: {
                    // placeholder 占位符 [name]⽼资源模块的名称
                    // [ext]⽼资源模块的后缀
                    // https://webpack.js.org/loaders/file-loader#placeholders
                    name: "[name]_[hash].[ext]",
                    //打包后的存放位置
                    outputPath: "images/"
                }
            }
        }
    ]
},
import pic from "./logo.png";

var img = new Image();

img.src = pic;

img.classList.add("logo");

var root = document.getElementById("root");

root.append(img);

处理字体 www.iconfont.cn/?spm=a313x.…

//css

@font-face {

font-family: "webfont";

font-display: swap;

src: url("webfont.woff2") format("woff2");

}

body {

background: blue;

font-family: "webfont" !important;

}

//webpack.config.js

{

test: /\.(eot|ttf|woff|woff2|svg)$/,

use: "file-loader"

}

url-loader file-loader加强版本

url-loader内部使⽤了file-loader,所以可以处理file-loader所有的事情,但是遇到jpg格式的模块,

会把该图⽚转换成base64格式字符串,并打包到js⾥。对⼩体积的图⽚⽐较合适,⼤图⽚不合 适。

npm install url-loader -D

案例;

module: {
    rules: [
        {
            test: /\.(png|jpe?g|gif)$/,
            use: {
                loader: "url-loader",
                options: {
                name: "[name]_[hash].[ext]",
                outputPath: "images/",
                //⼩于2048,才转换成base64
                limit: 2048
            }
         }
        }
    ]
},

** 样式处理:**

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

Style-loader 会把css-loader⽣成的内容,以style挂载到⻚⾯的heade部分

npm install style-loader css-loader -D
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}


{
    test:/\.css$/,
    use:[{
        loader:"style-loader",
        options: {
        injectType: "singletonStyleTag" // 将所有的style标签合并成⼀个
    }
    },"css-loader"]

}

Less样式处理

less-load 把less语法转换成css

$ npm install less less-loader --save-dev

案例:

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

{

test: /\.scss$/,

use: ["style-loader", "css-loader", "less-loader"]

}

样式⾃动添加前缀:

caniuse.com/

Postcss-loader

npm i postcss-loader autoprefixer -D

新建postcss.config.js

//webpack.config.js
{
    test: /\.css$/,
    use: ["style-loader", "css-loader", "postcss-loader"]
},

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

loader 处理webpack不⽀持的格式⽂件,模块

⼀个loader只处理⼀件事情

loader有执⾏顺序

5.2如何⾃⼰编写⼀个Loader

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

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

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

官⽅⽂档:webpack.js.org/contribute/…

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

简单案例

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

//index.js

console.log("hello zhy");

//replaceLoader.js

module.exports = function(source) {

console.log(source, this, this.query);

return source.replace('zhy','zhy')

};

//需要⽤声明式函数,因为要上到上下⽂的this,⽤到this的数据,该函数接受⼀个参数,是源码

在配置⽂件中使⽤loader

//需要使⽤node核⼼模块path来处理路径

const path = require('path')

module: { rules: [ { test: /.js$/, use: path.resolve(__dirname, "./loader/replaceLoader.js") } ] },

如何给loader配置参数,loader如何接受参数?

this.query、 loader-utils

//webpack.config.js
module: {
    rules: [
        {
            test: /\.js$/,
            use: [
                {
                    loader: path.resolve(__dirname, "./loader/replaceLoader.js"),
                    options: {
                    name: "zhy"
                    }
                }
            ]
        }
    ]
},
//replaceLoader.js
//const loaderUtils = require("loader-utils");//官⽅推荐处理loader,query的⼯具
module.exports = function(source) {
//this.query 通过this.query来接受配置⽂件传递进来的参数
//return source.replace("zhy", this.query.name);
const options = loaderUtils.getOptions(this);
const result = source.replace("zhy", options.name);
return source.replace("zhy", options.name);
}

this.callback :如何返回多个信息,不⽌是处理好的源码呢,可以使⽤this.callback来处理

//replaceLoader.js
const loaderUtils = require("loader-utils");//官⽅推荐处理loader,query的⼯具
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
const result = source.replace("zhy", options.name);
this.callback(null, result);
};

//this.callback(
    err: Error | null,
    content: string | Buffer,
    sourceMap?: SourceMap,
    meta?: any
);

**this.async:**如果loader⾥⾯有异步的事情要怎么处理呢

const loaderUtils = require("loader-utils");
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
setTimeout(() => {
    const result = source.replace("zhy", options.name);
    return result;
}, 1000);

};

//先⽤setTimeout处理下试试,发现会报错

我们使⽤this.asycn来处理,他会返回this.callback

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("zhy", options.name);
        callback(null, result);
    }, 3000);
};

多个loader的使⽤

//replaceLoader.js
module.exports = function(source) {
    return source.replace("zhy", "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("zhy", options.name);
        callback(null, result);
    }, 3000);
};

//webpack.config.js
module: {
    rules: [
        {
            test: /\.js$/,
            use: [
                path.resolve(__dirname, "./loader/replaceLoader.js"),
                {
                    loader: path.resolve(__dirname, "./loader/replaceLoaderAsync.js"),
                    options: {
                    name: "zhy"
                }
                }
            ]
    // use: [path.resolve(__dirname, "./loader/replaceLoader.js")]
      }
    ]
 },

顺序,⾃下⽽上,⾃右到左

处理loader的路径问题

  resolveLoader: {
    modules: ["node_modules", "./loader"]
  },
  module: {
    rules: [
        {
        test: /\.js$/,
        use: [
            "replaceLoader",
            {
                loader: "replaceLoaderAsync",
                options: {
                    name: "zhy"
                }
            }
        ]
        // use: [path.resolve(__dirname, "./loader/replaceLoader.js")]
        }
    ]
  },

参考:loader API

webpack.js.org/api/loaders

mini-css-extract-plugin

将css打包为单独的css文件

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
{
test: /\.css$/,
    use: [MiniCssExtractPlugin.loader, "css-loader"]
}
new MiniCssExtractPlugin({
    filename: "[name][chunkhash:8].css"
})

6.webpack⼯程化

6.1项⽬准备

初始化

npm init -y # 初始化npm配置⽂件
npm install --save-dev webpack # 安装核⼼库
npm install --save-dev webpack-cli # 安装命令⾏⼯具

.npmrc

⼤家⼀开始使⽤ npm 安装依赖包时,肯定感受过那挤⽛膏般的下载速度,上⽹⼀查只需要将 npm 源设 置为淘宝镜像源就⾏,在控制台执⾏⼀下以下命令:

npm config set registry registry.npm.taobao.org

从此过上了速度七⼗迈,⼼情是⾃由⾃在的⽣活。 但是⼤家想想,万⼀某个同学克隆了你的项⽬之后,准备在他本地开发的时候,并没有设置淘宝镜像 源,⼜要⼈家去⼿动设置⼀遍,我们作为项⽬的发起者,就先给别⼈省下这份时间吧,只需要在根⽬录 添加⼀个 .npmrc 并做简单的配置即可

# 创建 .npmrc ⽂件
touch .npmrc
# 在该⽂件内输⼊配置
registry=https://registry.npm.taobao.org/

创建src⽬录及⼊⼝⽂件

创建webpack配置⽂件,默认配置

# webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "[name].js",
},
mode: "development",
};

6.2样式处理

集成css样式处理:css-loader style-loader

创建index.css

# 安装
npm install style-loader css-loader -D
# 配置
module: {
    rules: [
        {
            test: /\.css$/,
            use: ["style-loader", "css-loader"],
        },
    ],
},

集成less sass

# sass
npm install node-sass sass-loader -D
# less
npm install less less-loader -D

#配置

rules:[
    {
         test: /\.scss$/,
         use: ["style-loader","css-loader","sass-loader"]
    },
    {
         test: /\.less$/,
         use: ["style-loader","css-loader","less-loader""]
    }
]

集成postcss:
Github:github.com/postcss/pos…

相当于babel于JS

postcss主要功能只有两个:第⼀就是把css解析成JS可以操作的抽象语法树AST,第⼆就是调⽤插

件来处理AST并得到结果;所以postcss⼀般都是通过插件来处理css,并不会直接处理 ⽐如:

⾃动补⻬浏览器前缀: autoprefixer

css压缩等 cssnano

npm install postcss-loader autoprefixer cssnano -D

# 创建postcss.config.js

# 配置postcss.config.js

module.exports = {
plugins: [require("autoprefixer")],
};

# 配置package.json
"browserslist":["last 2 versions", "> 1%"],

# 或者直接在postcss.config.js⾥配置
module.exports = {
plugins: [
    require("autoprefixer")({
    overrideBrowserslist: ["last 2 versions", "> 1%"],
    }),
],
};

# 或者创建.browserslistrc⽂件
> 1%
last 2 versions
not ie <= 8    

经过如上⼏个loader处理,css最终是打包在js中的,运⾏时会动态插⼊head中,但是我们⼀般在⽣产环 境会把css⽂件分离出来(有利于⽤户端缓存、并⾏加载及减⼩js包的⼤⼩),这时候就⽤到 mini-css extract-plugin 插件。

⼀般⽤于⽣产环境

    # 安装
    npm install mini-css-extract-plugin -D
    # 使⽤
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    module.exports = {
    module: {
    rules: [
        {
            test: /\.less$/,
            use: [
                // 插件需要参与模块解析,须在此设置此项,不再需要style-loader
                {
                loader: MiniCssExtractPlugin.loader,
                options: {
                hmr: true, // 模块热替换,仅需在开发环境开启
                // reloadAll: true,
                // ... 其他配置
                }
                },
                'css-loader',
                'postcss-loader',
                'less-loader'
            ],
        },
      ],
    },
    plugins: [
        new MiniCssExtractPlugin({
        filename: '[name].css', // 输出⽂件的名字
        // ... 其他配置
        }),
    ]
 };

6.3图⽚/字体⽂件处理

url-loader

file-loader

url-loader 和 file-loader 都可以⽤来处理本地的资源⽂件,如图⽚、字体、⾳视频等。功能也是 类似的, 不过 url-loader 可以指定在⽂件⼤⼩⼩于指定的限制时,返回 DataURL ,不会输出真实的 ⽂件,可以减少昂贵的⽹络请求。

# 安装
npm install url-loader file-loader -D
# ⼊⼝⽂件
import pic from "./logo.png";
var img = new Image();
img.src = pic;
img.classList.add("logo");
var root = document.getElementById("root");
root.append(img);
# 使⽤
module.exports = {
    modules: {
        rules: [
            {
            test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
            use: [
                {
                loader: 'url-loader', // 仅配置url-loader即可,内部会⾃动调
                ⽤file-loader
                options: {
                        limit: 10240, //⼩于此值的⽂件会被转换成DataURL
                        name: '[name]_[hash:6].[ext]', // 设置输出⽂件的名字
                        outputPath: 'assets', // 设置资源输出的⽬录
                    }
                }
            ],
            exclude: /node_modules/
            }
        ]
    }
}

limit的设置要设置合理,太⼤会导致JS⽂件加载变慢,需要兼顾加载速度和⽹络请求次数。

如果需要使⽤图⽚压缩功能,可以使⽤ image-webpack-load

处理字体: www.iconfont.cn/?spm=a313x.…

//css
@font-face {
    font-family: "webfont";
    font-display: swap;
    src: url("webfont.woff2") format("woff2");
}

body {
    background: blue;
    font-family: "webfont" !important;
}

//webpack.config.js
{
    test: /\.(eot|ttf|woff|woff2|svg)$/,
    use: "file-loader"
}

6.4html⻚⾯处理

HtmlWebpackPlugin

htmlwebpackplugin会在打包结束后,⾃动⽣成⼀个html⽂件,并把打包⽣成的js模块引⼊到该html 中。

npm install --save-dev html-webpack-plugin

配置:

title: ⽤来⽣成⻚⾯的 title 元素

filename: 输出的 HTML ⽂件名,默认是 index.html, 也可以直接配置带有⼦⽬录。

template: 模板⽂件路径,⽀持加载器,⽐如 html!./index.html

inject: true | 'head' | 'body' | false ,注⼊所有的资源到特定的 template 或者

templateContent 中,如果设置为 true 或者 body,所有的 javascript 资源将被放置到 body

元素的底部,'head' 将放置到 head 元素中。

favicon: 添加特定的 favicon 路径到输出的 HTML ⽂件中。

minify: {} | false , 传递 html-minifier 选项给 minify 输出

hash: true | false, 如果为 true, 将添加⼀个唯⼀的 webpack 编译 hash 到所有包含的脚本和

CSS ⽂件,对于解除 cache 很有⽤。

cache: true | false,如果为 true, 这是默认值,仅仅在⽂件修改之后才会发布⽂件。

showErrors: true | false, 如果为 true, 这是默认值,错误信息会写⼊到 HTML ⻚⾯中

chunks: 允许只添加某些块 (⽐如,仅仅 unit test 块)

chunksSortMode: 允许控制块在添加到⻚⾯之前的排序⽅式,⽀持的值:'none' | 'default' |

{function}-default:'auto'

excludeChunks: 允许跳过某些块,(⽐如,跳过单元测试的块)

案例:

const path = require("path");

const htmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
...
    plugins: [
    new htmlWebpackPlugin({
        title: "My App",
        filename: "app.html",
        template: "./src/index.html"
    })
]
};

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="root"></div>
</body>
</html>

6.5 clean-webpack-plugin

npm install --save-dev clean-webpack-plugin
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

...

plugins: [
    new CleanWebpackPlugin()
]

clean-webpack-plugin:如何做到dist⽬录下某个⽂件或⽬录不被清空: 使⽤配置 项:cleanOnceBeforeBuildPatterns 案例:cleanOnceBeforeBuildPatterns: ["/", "!dll", "!dll/"], !感 叹号相当于exclude 排除,意思是清空操作排除dll⽬录,和dll⽬录下所有⽂件。 注意:数组列表⾥的 “/”是默认值,不可忽略,否则不做清空操作。

6.6 sourceMap

源代码与打包后的代码的映射关系,通过sourceMap定位到源代码。 在dev模式中,默认开启,关闭的话 可以在配置⽂件⾥

devtool:"none"

devtool的介绍:webpack.js.org/configurati…

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

source-map: 产⽣ .map ⽂件

cheap:较快,不包含列信息

Module:第三⽅模块,包含loader的sourcemap(⽐如jsx to js ,babel的sourcemap)

inline: 将 .map 作为DataURI嵌⼊,不单独⽣成 .map ⽂件

配置推荐:

devtool:"cheap-module-eval-source-map",// 开发环境配置

//线上不推荐开启
devtool:"cheap-module-source-map", // 线上⽣成配置

6.7 WebpackDevServer

提升开发效率的利器 每次改完代码都需要重新打包⼀次,打开浏览器,刷新⼀次,很麻烦,我们可以安装使⽤ webpackdevserver来改善这块的体验

安装

npm install webpack-dev-server@3.11.0 -D

配置

修改下package.json

"scripts": {
    "server": "webpack-dev-server"
},

在webpack.config.js配置:

devServer: {
    contentBase: "./dist",
    open: true,
    port: 8081
}, 

启动

npm run server

启动服务后,会发现dist⽬录没有了,这是因为devServer把打包后的模块不会放在dist⽬录下,⽽是放 到内存中,从⽽提升速度

本地mock,解决跨域:

联调期间,前后端分离,直接获取数据会跨域,上线后我们使⽤nginx转发,开发期间,webpack就可以 搞定这件事

启动⼀个服务器,mock⼀个接⼝:

// 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:'zhy',
        age:5,
        msg:'欢迎学习'
    })
})
app.listen('9092')
//node server.js
http://localhost:9092/api/info

项⽬中安装axios⼯具

 //npm i axios -D
//index.js
import axios from 'axios'
axios.get('http://localhost:9092/api/info').then(res=>{
    console.log(res)
})

会有跨域问题

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

 proxy: {
    "/api": {
    target: "http://localhost:9092"
    }
}

修改index.js

axios.get("/api/info").then(res => {
      console.log(res);
});

7.如何⾃⼰编写⼀个Plugin

webpack 在编译代码过程中,会触发⼀系列 Tapable 钩⼦事件,插件所做的,就是找到相应的钩⼦,往 上⾯挂上⾃⼰的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩⼦ 的触发⽽执⾏了。

Plugin: 开始打包,在某个时刻,帮助我们处理⼀些什么事情的机制 plugin要⽐loader稍微复杂⼀些,在webpack的源码中,⽤plugin的机制还是占有⾮常⼤的场景,可以 说plugin是webpack的灵魂

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

官⽅⽂档:webpack.js.org/contribute/…

Webpack 接收配置 ⾛完整个构建过程(有很多阶段

7.1插件基本结构

// 插件名称

class MyPlugin {
    constructor(options) {}
    // 插件运⾏⽅法apply
        apply(compiler) {
            // 插件hooks
            compiler.hooks.done.tap('My Plugin', (/* xxx */) => {
            // 插件处理逻辑
        })
    }
}

7.2基本流程

Webpack 的基本流程可以分为 3 个阶段:

准备阶段:主要任务是创建 Compiler 和 Compilation 对象;

编译阶段:这个阶段任务是完成 modules 解析,并且⽣成 chunks;

module 解析:包含了三个主要步骤,创建实例、loaders 应⽤和依赖收集;

chunks ⽣成,主要步骤是找到每个 chunk 所需要包含的 modules 。

产出阶段:这个阶段的主要任务是根据 chunks ⽣成最终⽂件,主要有三个步骤:模板 Hash 更

新,模板渲染 chunk,⽣成⽂件

7.3 Compiler

Compiler 模块是 Webpack 最核⼼的模块。每次执⾏ Webpack 构建的时候,在 Webpack 内部,会⾸ 先实例化⼀个 Compiler 对象,然后调⽤它的 run ⽅法来开始⼀次完整的编译过程。我们直接使⽤ Webpack API webpack(options) 的⽅式得到的就是⼀个 Compiler 实例化的对象,这时候 Webpack 并不会⽴即开始构建,需要我们⼿动执⾏ comipler.run() 才可以。

const webpack = require('webpack');
const webpackConfig = require('../webpack.config.js');
// 只传⼊ config
const compiler = webpack(webpackConfig);
// 开始执⾏
compiler.run(callback);
// 上⾯两句等价于
webpack(webpackConfig, callback);

Tips:使⽤ webpack-dev-server API ⽅式时,只需要传⼊ compiler 对象给 dev server 即可,不 需要⼿动执⾏ compiler.run()

我们如果要⼿动实例化⼀个 Compiler 对象,可以通过 const Compiler = webpack.Compiler 来获取 它的类,⼀般只有⼀个⽗ Compiler ,⽽⼦ Compiler 可以⽤来处理⼀些特殊的事件。 在 webpack plugin 中,每个插件都有个 apply ⽅法。这个⽅法接收到的参数就是 Compiler 对象,我 们可以通过在对应的钩⼦时机绑定处理函数来编写插件,下⾯主要介绍下 Compiler 对象的钩⼦。

Compiler 钩⼦

在Webpack ⼯作流程中,我们通过下⾯的代码,获取了对应的钩⼦名称:

const compiler = webpack(config);
// 遍历hooks,添加回调,输出`hookName`
Object.keys(compiler.hooks).forEach(hookName => {
    if (compiler.hooks[hookName].tap) {
        compiler.hooks[hookName].tap('anyString', () => {
            console.log(`run -> ${hookName}`);
        });
    }
});
// 触发webpack的编译流程
compiler.run()

得到 compiler.run() 之后的⼯作流程:

run -> beforeRun
run -> run
run -> normalModuleFactory
run -> contextModuleFactory
run -> beforeCompile
run -> compile
run -> thisCompilation
run -> compilation
run -> make
run -> afterCompile
run -> shouldEmit
run -> emit
run -> afterEmit
run -> done

Compiler Hooks

Compiler 编译器模块是创建编译实例的主引擎。⼤多数⾯向⽤户的插件都⾸先在 Compiler 上注册。 compiler上暴露的⼀些常⽤的钩⼦:

image.png

原材料 --- 源代码

webpack⼯⼚处理

流⽔线 ----1 ------2---------3----------4-------5---------6---------emit

原材料(compilation)

7.4 案例

创建copyright-webpack-plugin.js

class CopyrightWebpackPlugin {
    constructor() {
    }

    //compiler:webpack实例
    apply(compiler) {
    }
}
module.exports = CopyrightWebpackPlugin;

配置⽂件⾥使⽤

const CopyrightWebpackPlugin = require("./plugin/copyright-webpack-plugin");
plugins: [new CopyrightWebpackPlugin()]

如何传递参数

//webpack配置⽂件
plugins: [
    new CopyrightWebpackPlugin({
        name: "zhy
    })
]

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

    apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin;

配置plugin在什么时刻进⾏

 class CopyrightWebpackPlugin {
    constructor(options) {
    // console.log(options);
    }
    
    apply(compiler) {
        //hooks.emit 定义在某个时刻
        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 => {
    // console.log("开始了");
    //});
  }
}

module.exports = CopyrightWebpackPlugin;

7.5开发⼀个⽂件清单插件

我希望每次webpack打包后,⾃动产⽣⼀个打包⽂件清单,上⾯要记录⽂件名、⽂件数量等信息。

思路: 显然这个操作需要在⽂件⽣成到dist⽬录之前进⾏,所以我们要注册的是 Compiler 上的 emit 钩 ⼦。 emit 是⼀个异步串⾏钩⼦,我们⽤ tapAsync 来注册。 在 emit 的回调函数⾥我们可以拿到 compilation 对象,所有待⽣成的⽂件都在它的 assets 属性上。 通过 compilation.assets 获取我们需要的⽂件信息,并将其整理为新的⽂件内容准备输出。 然后往 compilation.assets 添加这个新的⽂件。 插件完成后,最后将写好的插件放到 webpack 配置中,这个包含⽂件清单的⽂件就会在每次打包的时候 ⾃动⽣成了。

// plugins/FileListPlugin.js
class FileListPlugin {
    constructor (options) {
        // 获取插件配置项
        this.filename = options && options.filename ? options.filename :
        'FILELIST.md';
    }
    apply(compiler) {
        // 注册 compiler 上的 emit 钩⼦
        compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
                // 通过 compilation.assets 获取⽂件数量
                let len = Object.keys(compilation.assets).length;
                // 添加统计信息
                let content = `# ${len} file${len>1?'s':''} emitted by
                webpack\n\n`;
                // 通过 compilation.assets 获取⽂件名列表
                for(let filename in compilation.assets) {
                    content += `- ${filename}\n`;
                }

                // 往 compilation.assets 中添加清单⽂件
                compilation.assets[this.filename] = {
                    // 写⼊新⽂件的内容
                    source: function() {
                    return content;
                },

                // 新⽂件⼤⼩(给 webapck 输出展示⽤)
                size: function() {
                    return content.length;
                }
            }
            // 执⾏回调,让 webpack 继续执⾏
            cb();
        })
    }
}
module.exports = FileListPlugin;

在 webpack.config.js 中配置我们⾃⼰写的plugin:

 plugins: [
    new MyPlugin(),
    new FileListPlugin({
        filename: '_filelist.md'
    })
]

npm run build 执⾏

8.webpack打包原理分析

接收webpack配置 进⾏读取

⼊⼝:从哪个⽂件开始分析 哪些是依赖模块 这些依赖模块的位置 也就是路径信息

内容: 对内容处理,处理成浏览器正确解析的 递归处理其他依赖模块 ⽣成chunk 代码⽚段

补⻬函数 ⽣成bundle⽂件的内容 -》 启动器函数

出⼝:⽣成资源⽂件的名称和位置

webpack 在执⾏npx webpack进⾏打包后,都⼲了什么事情?

(function(modules) {
    var installedModules = {};
    function __webpack_require__(moduleId) {
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }

        var module = (installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        });

        modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
        );
        module.l = true;
        return module.exports;
    }

return __webpack_require__((__webpack_require__.s = "./index.js"));
})({
"./index.js": function(module, exports) {
    eval(
    '// import a from "./a";\n\nconsole.log("hello word");\n\n\n//#
    sourceURL=webpack:///./index.js?'
    ),

    "./a.js": function(module, exports) {
        eval(
            '// import a from "./a";\n\nconsole.log("hello word");\n\n\n//#
            sourceURL=webpack:///./index.js?'
        ),

    "./b.js": function(module, exports) {
        eval(
        '// import a from "./a";\n\nconsole.log("hello word");\n\n\n//#
        sourceURL=webpack:///./index.js?'
        );
  }
});

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

创建⼀个webpack

接收⼀份配置(webpack.config.js)

分析出⼊⼝模块位置
读取⼊⼝模块的内容,分析内容
哪些是依赖
哪些是源码
es6,jsx,处理 需要编译 -》浏览器能够执⾏
分析其他模块

拿到对象数据结构

模块路径
处理好的内容

创建bundle.js

启动器函数,来补充代码⾥有可能出现的module exports require,让浏览器能够顺利的执⾏

实现bundle.js

模块分析:读取⼊⼝⽂件,分析代码 const fs = require("fs");

const fenximokuai = filename => {
const content = fs.readFileSync(filename, "utf-8");
    console.log(content);
};
fenximokuai("./index.js"); 

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

@babel/parser:babeljs.io/docs/en/bab…

//安装@babel/parser
npm install @babel/parser --save

//bundle.js
const fs = require("fs");
const parser = require("@babel/parser");
const fenximokuai = filename => {
const content = fs.readFileSync(filename, "utf-8");
const Ast = parser.parse(content, {
    sourceType: "module"
});
console.log(Ast.program.body);
};

fenximokuai("./index.js");

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

npm install @babel/traverse --save

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const fenximokuai = filename => {
const content = fs.readFileSync(filename, "utf-8")
const Ast = parser.parse(content, {
    sourceType: "module"
});

const dependencies = [];

//分析ast抽象语法树,根据需要返回对应数据,

//根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值

traverse(Ast, {

ImportDeclaration({ node }) {
    console.log(node);
    dependencies.push(node.source.value);
}
});
console.log(dependencies);
};

fenximokuai("./index.js")

处理现在的路径问题:

//需要⽤到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语法树转换 成合适的代码

const babel = require("@babel/core");
const { code } = babel.transformFromAst(Ast, null, {
    presets: ["@babel/preset-env"]
});

导出所有分析出的信息:

 return {
    filename,
    dependencies,
    code
};

完成代码参考:

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 dependencies = {};
    traverse(ast, {
        ImportDeclaration({ node }) {
        const dirname = path.dirname(filename);
        const newFile = './' + path.join(dirname, node.source.value);
        dependencies[node.source.value] = newFile;
    }

});

const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
});
return {
    filename,
    dependencies,
    code
 }

}

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

分析依赖

上⼀步我们已经完成了⼀个模块的分析,接下来我们要完成项⽬⾥所有模块的分析:

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 dependencies = {};

    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(filename);
            const newFile = './' + path.join(dirname, node.source.value);
            dependencies[node.source.value] = newFile;
        }
    });

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

    return {
        filename,
        dependencies,
        code
    }
}

const makeDependenciesGraph = (entry) => {
    const entryModule = moduleAnalyser(entry);
    const graphArray = [ entryModule ];
    for(let i = 0; i < graphArray.length; i++) {
        const item = graphArray[i];
        const { dependencies } = item;
        if(dependencies) {
            for(let j in dependencies) {
                graphArray.push(
                    moduleAnalyser(dependencies[j])
                );
            }
        }
    }

    const graph = {};
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    });
    return graph;
}

const graghInfo = makeDependenciesGraph('./src/index.js');
console.log(graghInfo);

⽣成代码

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 dependencies = {};
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(filename);
            const newFile = './' + path.join(dirname, node.source.value);
            dependencies[node.source.value] = newFile;
        }
    });
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    });
    return {
        filename,
        dependencies,
        code
    }
}

const makeDependenciesGraph = (entry) => {
    const entryModule = moduleAnalyser(entry);
    const graphArray = [ entryModule ];
    for(let i = 0; i < graphArray.length; i++) {
        const item = graphArray[i];
        const { dependencies } = item;
        if(dependencies) {
            for(let j in dependencies) {
                graphArray.push(
                    moduleAnalyser(dependencies[j])
                );
            }
        }
    }
    const graph = {};
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            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);