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.js 的 EventEmitter 的库, 主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。
Tapable 库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子:
const {
SyncHook, // 同步钩子
SyncBailHook, // 同步熔断钩子
SyncWaterfallHook, // 同步流水钩子
SyncLoopHook, // 同步循环钩子
AsyncParallelHook, // 异步并发钩子
AsyncParallelBailHook, // 异步并发熔断钩子
AsyncSeriesHook, // 异步串行钩子
AsyncSeriesBailHook, // 异步串行熔断钩子
AsyncSeriesWaterfallHook // 异步串行流水钩子
} = require("tapable");
Tapable hooks 类型
Tapable 的使用-new Hook 新建钩子
Tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子
class 接受数组参数 options ,非必传。类方法会根据传参,接受同样数量的参数。const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);(参数个数可以为0,最多支持3个参数)
Tapable 的使用-钩子的绑定与执行
Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。
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 的编译都按照下面的钩子调用顺序执行
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
Module
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 生成算法
-
webpack先将entry中对应的module都生成一个新的chunk
-
- 遍历
module的依赖列表,将依赖的module也加入到chunk中
- 遍历
-
- 如果一个依赖
module是动态引入的模块,那么就会根据这个module创建一个新的chunk,继续遍历依赖
- 如果一个依赖
-
- 重复上面的过程,直至得到所有的
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 的模块机制
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(从左往右)Compose(webpack采取的是这种:从右往左)
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-cli, npm 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.js 和 b.loader.js,其代码在上面;
step5:在 package.json 中新增执行脚本,"build": "webpack";
最后,运行 npm run build,我们根据打印结果就可以知道顺序是从右往左。
2.2 使用 loader-runner 高效进行 loader 的调试
之前的 loader 的调试都是需要安装 webpack 和 webpack-cli 来进行,那这样其实是效率很低的。下面来介绍一下使用 loader-runner 高效进行 loader 的调试。
loader-runner 介绍
定义:loader-runner 允许你在不安装 webpack 的情况下运行 loaders。
作用:
- 作为
webpack的依赖,webpack中使用它执行loader - 进行
loader的开发和调试
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, 结果如下:
step6:如果我们要替换 demo.txt 里面的 foo 为 hello,那么修改 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,结果如下:
可见,使用 run-loader 来调试 loader 的功能非常方便。
2.3 更复杂的 loader 的开发场
如何获取 loader 的参数
通过 loader-utils 的 getOptions 方法获取。
const loaderUtils = require("loader-utils");
module.exports = function (content) {
const { name } = loaderUtils.getOptions(this);
};
step1:安装 loader-utils,npm 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
同步 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,第二个参数是处理的结果)。
示意代码:
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 结果为:
在 loader 中使用缓存
webpack 中默认开启 loader 缓存,可以使用 this.cacheable(false) 关掉缓存,执行 node run-loader.js,可以查看缓存是否开启,即 cacheable 的值是否为 true。
缓存条件: loader 的结果在相同的输入下有确定的输出,有依赖的 loader 无法使用缓存。
loader 如何进行文件输出?
通过 this.emitFile 进行文件写入
实战例子见 blog.csdn.net/kaimo313/ar…
实战开发一个自动合成雪碧图的loader
如何将两张图片合成一张图片?
使用 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.js 的 plugins 数组中。
插件的基本结构
搭建插件的运行环境
// 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()],
};
开发一个最简单的插件
目录如下:
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,
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对象的warnings和errors接收
compilation.warnings.push("warning");
compilation.errors.push("error");
通过 Compilation 进行文件写入
Compilation 上的 assets 可以用于文件写入
- 可以将
zip资源包设置到compilation.assets对象上
文件写入需要使用 webpack-sources:github.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
Hooks 是 emit,是一个异步的 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.js 的 plugins 参数:
// 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,结果为
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) 进⾏鉴权