原理篇
详细剖析 webpack 打包原理和插件、Loader 的实现
一、通过源码掌握 webpack 打包原理
开始:从 webpack 命令行说起
通过 npm scripts 运行 webpack
-
开发环境: npm run dev
-
生产环境:npm run build
通过 webpack 直接运行
- webpack entry.js bundle.js
查找 webpack 入口文件
在命令行运行以上命令后,npm会让命令行工具进入node_modules.bin 目录 查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在,就执行,不 存在,就抛出错误。
实际的入口文件是:node_modules\webpack\bin\webpack.js
分析 webpack 的入口文件: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 是否安装了
if (installedClis.length === 0){...}else if // 6. 根据安装数量进行处理
(installedClis.length === 1){...}else{...}
启动后的结果
webpack 最终找到 webpack-cli (webpack-command) 这个 npm 包,并且 执行 CLI
webpack-cli 做的事情
引入 yargs,对命令行进行定制
分析命令行参数,对各个参数进行转换,组成编译配置项
引用webpack,根据配置项进行编译和构建
从NON_COMPILATION_CMD分析出不需要编译的命令
webpack-cli 处理不需要经过编译的命令
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的内容
webpack-cli 提供的不需要编译的命令
const NON_COMPILATION_ARGS = [
"init", //创建一份 webpack 配置文件
"migrate", //进行 webpack 版本迁移
"add", //往 webpack 配置文件中增加属性
"remove", //往 webpack 配置文件中删除属性
"serve", //运行 webpack-serve
"generate-loader", //生成 webpack loader 代码
"generate-plugin", //生成 webpack plugin 代码
"info” //返回与本地环境相关的一些信息
]
命令行工具包 yargs 介绍
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 对象,然后执行构建流程
Webpack 的本质
Webpack可以将其理解是一种基于事件流的编程范例,一系列的插件运行。
先看一段代码
核心对象 Compiler 继承 Tapable
class Compiler extends Tapable {
// ...
}
核心对象 Compilation 继承 Tapable
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"]);
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)
Tapable 的使用-实际例子演示
定义一个 Car 方法,在内部 hooks 上新建钩子。分别是同步钩子 accelerate、brake(accelerate 接受一个参数)、异步钩子 calculateRoutes
使用钩子对应的绑定和执行方法
calculateRoutes 使用 tapPromise 可以返回一个 promise 对象
Tapable 是如何和 webpack 联系起来的?
if (Array.isArray(options)) {
compiler = new MultiCompiler(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().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
const Compiler = require('./Compiler')
class MyPlugin{
constructor() {
}
apply(compiler){
compiler.hooks.brake.tap("WarningLampPlugin",()=>console.log('WarningLampPlugin'));
compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to${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();
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
Chunk 生成算法
-
webpack 先将 entry 中对应的 module 都生成一个新的 chunk
-
遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
-
如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个
新的 chunk,继续遍历依赖
- 重复上面的过程,直至得到所有的 chunks
模块化:增强代码可读性和维护性
传统的网页开发转变成 Web Apps 开发
代码复杂度在逐步增高
分离的 JS文件/模块,便于后续代码的维护性
部署时希望把代码优化成几个 HTTP 请求
常见的几种模块化方式
ES module
import * as largeNumber from 'large-number';
// ...
largeNumber.add('999', '1');
------------------------------------------------
CJS
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),或者语法树(syntax tree),是
源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都
表示源代码中的一种结构。
在线demo: esprima.org/demo/parse.…
复习一下 webpack 的模块机制
动手实现一个简易的 webpack
可以将 ES6 语法转换成 ES5 的语法
-
通过 babylon 生成AST
-
通过 babel-core 将AST重新生成源码
可以分析模块之间的依赖关系
- 通过 babel-traverse 的 ImportDeclaration 方法获取依赖属性
生成的 JS 文件可以在浏览器中运行
二、编写 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-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: 资源的绝对路径(可以增加查询字符串)
loaders: [“/abs/path/to/loader.js?query”], // String[]: loader 的绝对路径(可以增加查询字符串)
context: { minimize: true }, // 基础上下文之外的额外 loader 上下文
readResource: fs.readFile.bind(fs) // 读取资源的函数
}, function(err, result) {
// err: Error?
// result.result: Buffer | String
})
开发一个 raw-loader
src/raw-loader.js:
module.exports = function(source) {
const json = JSON.stringify(source)
.replace(/\u2028/g, ‘\\u2028 ' ) // 为了安全起见, ES6模板字符串的问题
.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/raw-
loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
运行查看结果:
node run-loader.js
loader 的参数获取
通过 loader-utils 的 getOptions 方法获取
const loaderUtils = require("loader-utils");
module.exports = function(content) {
const { name } = loaderUtils.getOptions(this);
};
loader 异常处理
loader 内直接通过 throw 抛出
通过 this.callback 传递错误
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
loader 的异步处理
通过 this.async 来返回一个异步函数
- 第一个参数是 Error,第二个参数是处理的结果
示意代码:
module.exports = function(input) {
const callback = this.async();
// No callback -> return synchronous results
// if (callback) { ... }
callback(null, input + input);
};
在 loader 中使用缓存
webpack 中默认开启 loader 缓存
- 可以使用 this.cacheable(false) 关掉缓存
缓存条件: loader 的结果在相同的输入下有确定的输出
- 有依赖的 loader 无法使用缓存
loader 如何进行文件输出?
通过 this.emitFile 进行文件写入
const loaderUtils = require("loader-utils");
module.exports = function(content) {
const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
content,
});
this.emitFile(url, content);
const path = `__webpack_public_path__ + ${JSON.stringify(url)};`;
return `export default ${path}`;
};
实战开发一个自动合成雪碧图的 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;
});
插件的运行环境
插件没有像 loader 那样的独立运行环境
只能在 webpack 里面运行
插件的基本结构
基本结构:
class MyPlugin { // 1.插件名称
apply(compiler) { // 2.插件上的 apply 方法
compiler.hooks.done.tap(' My Plugin',( // 3.插件的 hooks
stats /* stats is passed as argument when done hook is tapped. */
) => {
console.log('Hello World!'); // 4.插件处理逻辑
});
}
}
module.exports = MyPlugin;
插件使用:
plugins: [ new MyPlugin() ]
搭建插件的运行环境
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()],
}
开发一个最简单的插件
src/demo-plugin.js
module.exports = class DemoPlugin {
constructor(options) {
this.options = options;
}
apply() {
console.log("apply", this.options);
}
};
加入到 webpack 配置中
module.exports = {
...
plugins: [new DemoPlugin({ name: "demo" })]
};
插件中如何获取传递的参数?
通过插件的构造函数进行获取
module.exports = class MyPlugin {
constructor(options) {
this.options = options;
}
apply() {
console.log("apply", this.options);
}
};
插件的错误处理
参数校验阶段可以直接 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 (www.npmjs.com/package/web…)
const { RawSource } = require("webpack-sources");
module.exports = class DemoPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
const { name } = this.options;
compiler.plugin("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)
编写一个压缩构建资源为zip包的插件
要求:
-
生成的 zip 包文件名称可以通过插件传入
-
需要使用 compiler 对象上的特地 hooks 进行资源的生成
准备知识:Node.js 里面将文件压缩为 zip 包
使用 jszip (www.npmjs.com/package/jsz…)
jszip 使用示例
var zip = new JSZip();
zip.file("Hello.txt", "Hello World\n");
var 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 对象上
实战篇
从实际 Web 商城项⽬出发,讲解 webpack 实际使⽤
一、React 全家桶 和 webpack 开发商城项目
商城技术栈选型
商城架构设计
商城界⾯ UI 设计与模块拆分
前台模块拆分
后台模块拆分
React 全家桶环境搭建
. 初始化项⽬ npm init -y
· 创建项⽬⽬录
安装依赖
- 安装 react、react-dom、redux、react-redux
npm i react react-dom redux react-redux -S
- 安装 @babel/core
npm i @babel/core -D
- 安装 geektime-builder-webpack
npm i geektime-builder-webpack -D
创建 actions、reducers、store
- actions 和 reducers
src/actions/ 放置所有的actions、src/reducers 放置所有的 reducers
- rootReducer
src/reducers/rootReducer.js 将所有的 reducers 进⾏ Combine
- 使⽤ Provider 传递 store
Store 通过 Provider 传递给容器组件
数据库实体设计
编写服务端 API
- 登陆注册 API
⽤户注册(POST):/user/register
⽤户登陆(GET):/user/login
- 商品 API
商品列表(GET):/goods?page=1&per_page=10
商品详情(GET): /goods/:id
修改商品(PUT): /goods/:id
登陆注册模块开发
JSON Web Token ⼯作原理
登陆注册模块开发 - 注册功能
- 注册 API (POST): http://127.0.0.1:8001/api/v1/user/register
curl -X POST -d "username=test&password=123456&email=test@qq.com" "http://127.0.0.1:8001/api/v1/user/register"
- 注册截图
登陆注册模块开发 - 登录功能
- 登陆 API (POST): http://127.0.0.1:8001/api/v1/user/login
curl -X POST -d "username=test&password=123456" "http://127.0.0.1:8001/api/v1/user/login"
- 登录截图
商品模块开发
商品模块开发 - 创建商品
创建商品 API (POST): http://127.0.0.1:8001/api/v1/goods/new
商品模块开发 - 获取商品列表
商品列表 API (GET): http://127.0.0.1:8001/api/v1/goods?page=1&per_page=10
订单模块开发
订单模块开发 - 创建订单
创建订单 API (POST): http://127.0.0.1:8001/api/v1/order/new
谈谈 Web 商城的性能优化策略
- 渲染优化
⾸⻚、列表⻚、详情⻚采⽤ SSR 或者 Native 渲染
个⼈中⼼⻚预渲染
- 弱⽹优化
使⽤离线包、PWA 等离线缓存技术
- Webview 优化
打开 Webview 的同时并⾏的加载⻚⾯数据
功能开发要点
- 浏览器端:
组件化,组件颗粒度尽可能⼩
直接复⽤ builder-webpack-geektime 的构建配置,⽆需关注构建脚本
- 服务端:
MVC 开发⽅式,数据库基于 Sequelize
Rest API ⻛格
采⽤ JWT 进⾏鉴权
源码和演示地址
· 源码: github.com/cpselvis/ge…
· 演示步骤: README 有详细步骤