webpack 学习笔记之原理篇和实战篇

304 阅读13分钟

1. webpack 的打包原理

1.1 webpack 启动过程分析

从 webpack 命令行说起

  • 方式1:通过 npm scripts 运行 webpack

开发环境: npm run dev
生产环境:npm run build

  • 方式2:通过 webpack 直接运行

在命令行运行以上命令后,npm 会让命令行工具进入node_modules\.bin目录查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在,就执行,不存在,就抛出错误。

(如果是全局安装某个包,就会从userLcoal/.bin 这个目录去找;如果是局部安装,就会从当前项目的 node_modules/.bin目录去找。)

运行 webpack 实际的入口文件是:node_modules\webpack\bin\webpack.js

分析 webpack 的入口文件:webpack.js

找到 node_modules/webpack, 去看一下 package.json 下的 "bin": "./bin/webpack.js"

// node_modules/webpack/.bin/webpack.js 代码
process.exitCode = 0; // 1. 正常执行返回

const runCommand = (command, args) =>{...}; // 2. 运行某个命令

const isInstalled = packageName =>{...}; // 3. 判断某个包是否安装

const CLIs =[...]; // 4. webpack 可用的CLI: webpack-cli和webpack-command 

const installedClis = CLIs.filter(cli => cli.installed); // 5. 判断是否两个ClI 是否安装了

// 6. 根据安装数量进行处理(installedClis.length === 1){...}else{...}.
if (installedClis.length === 0){
    ...
} else if(installedClis.length === 1) {
    ...
} else {
    ...
}

webpack 执行后的结果

webpack 最终找到 webpack-cli (或者webpack-command) 这个 npm 包,并且执行 CLI

1.2 webpack-cli 源码阅读

webpack-cli 做的事情

  • 引入 yargs,对命令行进行定制
  • 分析命令行参数,对各个参数进行转换,组成编译配置项
  • 引用 webpack,根据配置项进行编译和构建

NON_COMPILATION_CMD 不需要编译的命令

webpack-cli 处理不需要经过编译的命令

// node_modules/webpack-cli/.bin/cli.js代码
const { NON_COMPILATION_ARGS } = require("./utils/constants");
const NON_COMPILATION_CMD = process.argv.find(arg => {
    if (arg === "serve") {
        global.process.argv = global.process.argv.filter(a => a !== "serve");
        process.argv = global.process.argv;
    }
    return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {
    return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv);
}

NON_COMPILATION_ARGS(不需要编译)的内容如下:

const NON_COMPILATION_ARGS = [
	"init", // 创建一份webpack 配置文件
	"migrate", // 进行webpack 版本迁移
	"add", // 往 webpack 配置文件中增加属性
	"remove", // 往 webpack 配置文件中删除属性
	"serve", // 运行webpack-serve
	"generate-loader", // 生成 webpack loader 代码
	"generate-plugin", // 生成 webpack plugin代码
	"info", // 返回与本地环境相关的一些信息
];
// 如 webpack init 不需要编译,不生成 webpack 实例

命令行工具包 yargs 介绍

  • 提供命令和分组参数
  • 动态生成 help 帮助信息

webpack-cli 使用args 分析

参数分组 (config/config-args.js),将命令划分为9类:

  • Config options: 配置相关参数(文件名称、运行环境等)
  • Basic options: 基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)
  • Module options: 模块参数,给 loader 设置扩展
  • Output options: 输出参数(输出路径、输出文件名称)
  • Advanced options: 高级用法(记录设置、缓存设置、监听频率、bail等)
  • Resolving options: 解析参数(alias 和 解析的文件后缀设置)
  • Optimizing options: 优化参数
  • Stats options: 统计参数
  • options: 通用参数(帮助命令、版本信息等)

webpack-cli 执行的结果

  • webpack-cli 对配置文件和命令行参数进行转换最终生成配置选项参数 options
  • 最终会根据配置参数实例化 webpack 对象,然后执行构建流程

1.3 Tapable 插件架构与 Hooks 设计

Webpack 的本质

Webpack 可以将其理解是一种基于事件流的编程范例,一系列的插件运行。

  • 核心对象 Compiler 继承 Tapable
// node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable { 
    // ...
}
  • 核心对象 Compilation 也继承 Tapable
// node_modules/webpack/lib/Compilation.js
class Compilation extends Tapable { 
    // ... 
}

Tapable 是什么?

Tapable 是一个类似于 Node.jsEventEmitter 的库, 主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。

Tapable 库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子:

const { 
    SyncHook, // 同步钩子 
    SyncBailHook, // 同步熔断钩子
    SyncWaterfallHook, // 同步流水钩子
    SyncLoopHook, // 同步循环钩子
    AsyncParallelHook, // 异步并发钩子
    AsyncParallelBailHook, // 异步并发熔断钩子
    AsyncSeriesHook, // 异步串行钩子
    AsyncSeriesBailHook, // 异步串行熔断钩子
    AsyncSeriesWaterfallHook // 异步串行流水钩子
} = require("tapable");

Tapable hooks 类型

image.png

Tapable 的使用-new Hook 新建钩子

Tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子

class 接受数组参数 options ,非必传。类方法会根据传参,接受同样数量的参数。const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);(参数个数可以为0,最多支持3个参数)

Tapable 的使用-钩子的绑定与执行

Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。

image.png

Tapable 的使用-hook 基本用法示例

const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
// 绑定事件到webapck事件流 
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) // 1,2,3 
// 执行绑定的事件 
hook1.call(1,2,3)

1.4 Tapable 是如何和 webpack 进行关联起来的

// node_modules/webpack/lib/webpack.js
if (Array.isArray(options)) {
        compiler = new MultiCompiler(
                Array.from(options).map(options => webpack(options))
        );
} else if (typeof options === "object") {
        options = new WebpackOptionsDefaulter().process(options);

        compiler = new Compiler(options.context);
        compiler.options = options;
        new NodeEnvironmentPlugin({
                infrastructureLogging: options.infrastructureLogging
        }).apply(compiler);
        if (options.plugins && Array.isArray(options.plugins)) {
                for (const plugin of options.plugins) {
                        if (typeof plugin === "function") {
                                plugin.call(compiler, compiler);
                        } else {
                                plugin.apply(compiler);
                        }
                }
        }
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        compiler.options = new WebpackOptionsApply().process(options, compiler);
}

模拟 Compiler.js

module.exports = class Compiler {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(['newspeed']), 
            brake: new SyncHook(), 
            calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
        }
    }
    run() {
        this.accelerate(10)
        this.break()
        this.calculateRoutes('Async', 'hook', 'demo')
    }
    accelerate(speed) {
        this.hooks.accelerate.call(speed);
    }
    break() {
        this.hooks.brake.call();
    }
    calculateRoutes() {
        this.hooks.calculateRoutes.promise(...arguments).then(() => {
        }, err => {
            console.error(err);
        });
    }
}

插件 my-plugin.js

class MyPlugin {
    constructor() {
    }
    apply(compiler) {
        compiler.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
        compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Acceleratingto${newSpeed}`));
        compiler.hooks.calculateRoutes.tapPromise("calculateRoutes tapAsync", (source, target, routesList) => {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    console.log(`tapPromise to ${source} ${target} ${routesList}`)
                    resolve();
                }, 1000)
            });
        });
    }
}

模拟插件执行

const myPlugin = new MyPlugin();
const options = {
    plugins: [myPlugin]
}
const compiler = new Compiler();
for (const plugin of options.plugins) {
    if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
    } else {
        plugin.apply(compiler);
    }
}
compiler.run();

1.5 Webpack 流程篇

webpack 的编译都按照下面的钩子调用顺序执行

image.png

WebpackOptionsApply

将所有的配置 options 参数转换成 webpack 内部插件

使用默认插件列表

举例:

  • output.library -> LibraryTemplatePlugin
  • externals -> ExternalsPlugin
  • devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
  • AMDPlugin, CommonJsPlugin
  • RemoveEmptyChunksPlugin

Compiler hooks

流程相关:

  • (before-)run
  • (before-/after-)compile
  • make ·(after-)emit
  • done

监听相关:

  • watch-run
  • watch-close

Compilation

Compiler 调用 Compilation 生命周期方法

  • addEntry -> addModuleChain
  • finish (上报模块错误)
  • seal

ModuleFactory

image.png

Module

image.png

NormalModule

Build

  • 使用 loader-runner 运行 loaders
  • 通过 Parser 解析 (内部是 acron)
  • ParserPlugins 添加依赖

Compilation hooks

模块相关:build-module 、failed-module、succeed-module;

资源生成相关:module-asset、chunk-asset;

优化和 seal相关:

  • (after-)seal
  • optimize
  • optimize-modules (-basic/advanced)
  • after-optimize-modules
  • after-optimize-chunks
  • after-optimize-tree
  • optimize-chunk-modules (-basic/advanced)
  • after-optimize-chunk-modules
  • optimize-module/chunk-order
  • before-module/chunk-ids
  • (after-)optimize-module/chunk-ids
  • before/after-hash

Chunk 生成算法

    1. webpack 先将 entry 中对应的 module 都生成一个新的 chunk
    1. 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk
    1. 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖
    1. 重复上面的过程,直至得到所有的 chunks

模块化:增强代码可读性和维护性

  • 传统的网页开发转变成 Web Apps 开发
  • 代码复杂度在逐步增高
  • 分离的 JS 文件/模块,便于后续代码的维护性
  • 部署时希望把代码优化成几个 HTTP 请求

常见的几种模块化方式

ES module: 通过 import 静态导入模块

import * as largeNumber from'large-number'; 
// ... 
largeNumber.add('999', '1');

CJS:通过 require 导入模块,支持运行时动态导入模块

const largeNumbers = require('large-number'); 
// ... 
largeNumber.add('999', '1');

AMD:

require(['large-number'], function (large-number) {
// ... 
largeNumber.add('999', '1'); });

AST 抽象语法树

抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntaxtree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。

在线demo: esprima.org/demo/parse.…

webpack 的模块机制

image.png

2. loader 和插件

2.1 loader 的链式调用与执行顺序

一个最简单的 loader 代码结构

定义:loader 只是一个导出为函数的 JavaScript 模块。

module.exports = function(source) { 
    return source; 
};

多 Loader 时的执行顺序

多个 Loader 串行执行顺序从后到前

module.exports = {
    entry: './src/index.js', output: {
        filename: 'bundle.js', path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'less-loader'
                ]
            }
        ]
    }
};

函数组合的两种情况

  • Unix 中的 pipline(从左往右)
  • Composewebpack 采取的是这种:从右往左)

compose = (f, g) => (...args) => f(g(...args));

通过一个例子验证 loader 的执行顺序

a-loader.js:

module.exports = function(source) {
	console.log ('loader a is executed');
	return source;
};

b-loader.js:

module.exports = function(source) {
	console.log ('loader b is executed');
	return source;
};

验证 loader 的执行顺序

step1:新建文件夹 loader-order,然后执行初始化命令 npm init -y
step2:安装 webpack webpack-clinpm i webpack webpack-cli -D
step3:新建 webpack.config.js 配置文件,代码如下:

// webpack.config.js
const path = require('path');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'main.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    path.resolve('./loaders/a-loader.js'),
                    path.resolve('./loaders/b-loader.js')
                ]
            }
        ]
    }
}

step4:新建 loaders 文件夹,存放 a.loader.jsb.loader.js,其代码在上面;
step5:在 package.json 中新增执行脚本,"build": "webpack"
最后,运行 npm run build,我们根据打印结果就可以知道顺序是从右往左

2.2 使用 loader-runner 高效进行 loader 的调试

之前的 loader 的调试都是需要安装 webpackwebpack-cli 来进行,那这样其实是效率很低的。下面来介绍一下使用 loader-runner 高效进行 loader 的调试。

loader-runner 介绍

定义:loader-runner 允许你在不安装 webpack 的情况下运行 loaders

作用:

  • 作为 webpack 的依赖,webpack 中使用它执行 loader
  • 进行 loader 的开发和调试

loader-runner 的使用

loader-runner源码

import { runLoaders } from "loader-runner";

runLoaders({
	resource: "/abs/path/to/file.txt?query",
	// String: Absolute path to the resource (optionally including query string)

	loaders: ["/abs/path/to/loader.js?query"],
	// String[]: Absolute paths to the loaders (optionally including query string)
	// {loader, options}[]: Absolute paths to the loaders with options object

	context: { minimize: true },
	// Additional loader context which is used as base context

	processResource: (loaderContext, resourcePath, callback) => { ... },
	// Optional: A function to process the resource
	// Must have signature function(context, path, function(err, buffer))
	// By default readResource is used and the resource is added a fileDependency

	readResource: fs.readFile.bind(fs)
	// Optional: A function to read the resource
	// Only used when 'processResource' is not provided
	// Must have signature function(path, function(err, buffer))
	// By default fs.readFile is used
}, function(err, result) {
	// err: Error?

	// result.result: Buffer | String
	// The result
	// only available when no error occured

	// result.resourceBuffer: Buffer
	// The raw resource as Buffer (useful for SourceMaps)
	// only available when no error occured

	// result.cacheable: Bool
	// Is the result cacheable or do it require reexecution?

	// result.fileDependencies: String[]
	// An array of paths (existing files) on which the result depends on

	// result.missingDependencies: String[]
	// An array of paths (not existing files) on which the result depends on

	// result.contextDependencies: String[]
	// An array of paths (directories) on which the result depends on
})

开发一个 raw-loader

实现功能:将文件转换为 string

// src/raw-loader.js:
module.exports = function (source) {
    // 为了安全起见, ES6模板字符串的问题
    const json = JSON.stringify(source)
        .replace(/\u2028/g, '\\u2028' ) 
        .replace(/\u2029/g, '\\u2029');
    return `export default ${json}`;
};
// src/demo.txt 
foobar

使用 loader-runner 调试 loader

// run-loader.js:
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
    {
        resource: "./demo.txt",
        loaders: [path.resolve(__dirname, "./loaders/rawloader")], 
        readResource: fs.readFile.bind(fs),
    },
    (err, result) => (err ? console.error(err) : console.log(result))
);

运行查看结果:node run-loader.js

实战

step1: 新建 raw-loader 文件夹,初始化项目并且按照依赖

npm init -y
npm i loader-runner -S

step2:在项目根目录下新建 src 文件夹,在 src 文件夹下新建demo.txt,代码为foobar
step3:在 src 文件夹下面新建 raw-loader.js 文件

// src/raw-loader.js
module.exports = function(source) {
 const json = JSON.stringify(source)
        .replace(/\u2028/g, '\u2028')
        .replace(/\u2029/g, '\u2029');
        
   return `export default ${json}`;
}

step4:在项目根目录下新建 run-loader.js ,代码如下:

// run-loader.js
const { runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');

runLoaders({
    resource: path.join(__dirname, './src/demo.txt'),
    loaders: [
        {
            loader: path.join(__dirname, './src/raw-loader.js'),
        }
    ],
    context: {
        emitFile: () => { }
    },
    readResource: fs.readFile.bind(fs)
}, (err, result) => {
    err ? console.log(err) : console.log(result);
});

step5:执行 node run-loader.js, 结果如下:

image.png

step6:如果我们要替换 demo.txt 里面的 foohello,那么修改 raw-loader.js 的代码如下:

// src/raw-loader.js
module.exports = function (source) {
    const json = JSON.stringify(source)
        .replace('foo', 'hello')
        .replace(/\u2028/g, '\u2028')
        .replace(/\u2029/g, '\u2029');

    return `export default ${json}`;
}

执行 node run-loader.js,结果如下:

image.png

可见,使用 run-loader 来调试 loader 的功能非常方便。

2.3 更复杂的 loader 的开发场

如何获取 loader 的参数

通过 loader-utilsgetOptions 方法获取。

const loaderUtils = require("loader-utils");
module.exports = function (content) {
    const { name } = loaderUtils.getOptions(this);
};

step1:安装 loader-utilsnpm i loader-utils@1.2.3 -S

注意:loader-utils 3.x 版本已经移除getOptions方法,详见:github.com/webpack/loa…

step2:在 run-loader.js 中传递 options 参数 name

// run-loader.js
const { runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');

runLoaders({
    resource: path.join(__dirname, './src/demo.txt'),
    loaders: [
        {
            loader: path.join(__dirname, './src/raw-loader.js'),
            options: {
                name: 'test'
            }
        }
    ],
    context: {
        emitFile: () => { }
    },
    readResource: fs.readFile.bind(fs)
}, (err, result) => {
    err ? console.log(err) : console.log(result);
});

step3:在 raw-loader.js 中获取 name 参数并打印:

const loaderUtils = require('loader-utils');

module.exports = function (source) {
    const { name } = loaderUtils.getOptions(this);
    console.log("name==", name);

    const json = JSON.stringify(source)
        .replace('foo', 'hello')
        .replace(/\u2028/g, '\u2028')
        .replace(/\u2029/g, '\u2029');

    return `export default ${json}`;
}

step4:执行 node run-loader.js,可以看到 name 获取到了,为 test

image.png

同步 loader 异常处理

  • 方式1:loader 内直接通过 throw 抛出
  • 方式2:通过 this.callback 传递错误
const loaderUtils = require('loader-utils');

module.exports = function (source) {
    const { name } = loaderUtils.getOptions(this);
    console.log("name==", name);

    const json = JSON.stringify(source)
        .replace('foo', 'hello')
        .replace(/\u2028/g, '\u2028')
        .replace(/\u2029/g, '\u2029');

    /** loader 的异常处理 */
    // throw new Error('Error');
    // this.callback(new Error('Error'), json);

    /** 正常返回 */
    this.callback(null, json);
    // return `export default ${json}`;
}

loader 的异步处理

通过 this.async 来返回一个异步函数(第一个参数是 Error,第二个参数是处理的结果)。

示意代码:

image.png

step1: 在 src 文件夹下新建 async.txt,内容为 async
step2:修改 raw-loader.js 的代码为:

// src/raw-loader.js
const path = require('path');
const fs = require('fs');

module.exports = function (source) {
    const callback = this.async();
    fs.readFile(path.join(__dirname, './async.txt'), 'utf-8', (err, data) => {
        if (err) {
            callback(err, '');
        }
        callback(null, data);
    });
}

step3:执行 node run-loader.js 结果为:

image.png

在 loader 中使用缓存

webpack 中默认开启 loader 缓存,可以使用 this.cacheable(false) 关掉缓存,执行 node run-loader.js,可以查看缓存是否开启,即 cacheable 的值是否为 true

缓存条件: loader 的结果在相同的输入下有确定的输出,有依赖的 loader 无法使用缓存。

loader 如何进行文件输出?

通过 this.emitFile 进行文件写入

image.png

实战例子见 blog.csdn.net/kaimo313/ar…

实战开发一个自动合成雪碧图的loader

image.png

如何将两张图片合成一张图片?

使用 spritesmith (www.npmjs.com/package/spr…)

spritesmith 使用示例

const sprites = ['./images/1.jpg', './images/2.jpg'];
Spritesmith.run({ src: sprites }, function handleResult(err, result) {
    result.image;
    result.coordinates;
    result.properties;
});

2.4 插件的基本结构

插件的运行环境

插件没有像 loader 那样的独立运行环境,只能在 webpack 里面运行。也就是说 要安装 webpack,然后将某插件添加到 webpack.config.jsplugins 数组中。

插件的基本结构

image.png

搭建插件的运行环境

// webpack.config.js
const path = require("path");
const DemoPlugin = require("./plugins/demo-plugin.js");
const PATHS = {
    lib: path.join(__dirname, "app", "shake.js"), build: path.join(__dirname, "build"),
};
module.exports = {
    entry: {
        lib: PATHS.lib,
    }, 
    output: {
        path: PATHS.build, filename: "[name].js",
    }, 
    plugins: [new DemoPlugin()],
};

开发一个最简单的插件

目录如下:

image.png

step1: 新建 my-plugin 文件夹,初始化项目并且按照依赖

npm init -y
npm i webpack@4.39.1 webpack-cli@3.3.6 -D

step2:在项目根目录下新建 plugins 文件夹,在 plugins 文件夹下新建 my-plugin.js,代码为

// plugins/my-plugin.js
class MyPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        console.log('my plugin options ====>', this.options);
    }
}

module.exports = MyPlugin;

step3:新建 src/index.js,内容为 console.log("hello my plugin")

step4:添加 webpack.config.js 配置

// webpack.config.js
const path = require('path');
const MyPlugin = require('./plugins/my-plugin.js');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'main.js',
    },
    mode: 'production',
    plugins: [
        new MyPlugin({
            name: 'my plugin is called coco'
        })
    ]
};

step5:在 packsge.json 中添加构建命令"build": "webpack"
step6:运行构建 npm run build

image.png

2.5 更复杂的插件开发场景

插件中如何获取传递的参数?

通过插件的构造函数进行获取

class MyPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        console.log('my plugin options ====>', this.options);
    }
}

module.exports = MyPlugin;

插件的错误处理

  • 参数校验阶段可以直接 throw 的方式抛出,throw new Error(“ Error Message”)

  • 通过 compilation 对象的 warningserrors 接收

compilation.warnings.push("warning"); 
compilation.errors.push("error");

通过 Compilation 进行文件写入

Compilation 上的 assets 可以用于文件写入

  • 可以将 zip 资源包设置到 compilation.assets 对象上

文件写入需要使用 webpack-sourcesgithub.com/webpack/web…

const { RawSource } = require("webpack-sources");
module.exports = class DemoPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        const { name } = this.options;
        compiler.hoks.emit((compilation, cb) => {
            compilation.assets[name] = new RawSource("demo");
            cb();
        });
    }
};

插件扩展:编写插件的插件

插件自身也可以通过暴露 hooks 的方式进行自身扩展,以html-webpack-plugin 为例:

  • html-webpack-plugin-alter-chunks (Sync)
  • html-webpack-plugin-before-html-generation (Async)
  • html-webpack-plugin-alter-asset-tags (Async)
  • html-webpack-plugin-after-html-processing (Async)
  • html-webpack-plugin-after-emit (Async)

在插件的事件节点进行监听,一旦执行到就可以根据获取到的资源进行处理。

2.6 实战开发一个压缩构建资源为zip包的插件

实现要求

  • 生成的 zip 包文件名称可以通过插件传入
  • 需要使用 compiler 对象上的特定 hooks 进行资源的生成

准备知识:Node.js 里面将文件压缩为zip包

使用 jszip (www.npmjs.com/package/jsz…)

// jszip
const zip = new JSZip();

zip.file("Hello.txt", "Hello World\n");

const img = zip.folder("images");
img.file("smile.gif", imgData, { base64: true });

zip.generateAsync({ type: "blob" }).then(function (content) {
    // see FileSaver.js
    saveAs(content, "example.zip");
});

Compiler 上负责文件生成的 hooks

Hooksemit,是一个异步的 hook (AsyncSeriesHook)emit 生成文件阶段,读取的是 compilation.assets 对象的值

  • 可以将 zip 资源包设置到 compilation.assets 对象上

实战

step1:复制上一节的 my-plugin 文件夹,修改一下名称为 zip-plugin
step2:修改 plugins 文件夹下面的 my-plugin.js 的文件名称为 zip-plugin.js,并且将代码中 MyPlugin 相关的名字均改为 ZipPlugin
step3:安装 jszip 依赖,npm i jszip -S
step4:编写 zip-plugin.js 的代码,

// plugins/zip-plugin.js
const JSZip = require('jszip');
const path = require('path');
const RawSource = require('webpack-sources').RawSource;
const zip = new JSZip();

module.exports = class ZipPlugin {
    constructor(options) {
        this.options = options;
    }

    apply(compiler) {
        compiler.hooks.emit.tapAsync('ZipPlugin', (compilation, callback) => {
            const folder = zip.folder(this.options.filename);

            for (let filename in compilation.assets) {
                const source = compilation.assets[filename].source();
                folder.file(filename, source);
            }

            zip.generateAsync({
                type: 'nodebuffer'
            }).then((content) => {
                const outputPath = path.join(
                    compilation.options.output.path,
                    this.options.filename + '.zip'
                );

                const outputRelativePath = path.relative(
                    compilation.options.output.path,
                    outputPath
                );
                compilation.assets[outputRelativePath] = new RawSource(content);

                callback();
            });
        });
    }
}

step5:修改 webpack.config.jsplugins 参数:

// webpack.config.js
const path = require('path');
const ZipPlugin = require('./plugins/zip-plugin.js');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'main.js',
    },
    mode: 'production',
    plugins: [
        new ZipPlugin({
             name: 'my plugin is called coco',
            filename: 'offline'
        })
    ]
};

step6: 执行构建 npm run build,结果为

image.png

3. React 全家桶和 webpack 开发商城项目

参考笔记:blog.csdn.net/kaimo313/ar…

谈谈 Web 商城的性能优化策略

  • 渲染优化

1)⾸⻚、列表⻚、详情⻚采⽤ SSR 或者 Native 渲染
2)个⼈中⼼⻚预渲染

  • 弱⽹优化

1)使⽤离线包、PWA 等离线缓存技术

  • Webview 优化

1) 打开 Webview 的同时并⾏的加载⻚⾯数据

功能开发要点

  • 浏览器端:

1)组件化,组件颗粒度尽可能小
2)直接复⽤ builder-webpack-geektime 的构建配置,⽆需关注构建脚本

  • 服务端:

1)MVC 开发⽅式,数据库基于 Sequelize
2)Rest API ⻛格
3)采⽤ JWT (JSON Web Token) 进⾏鉴权