- Webpack核心原理
Webpack解决的问题 Webpack做的事情简单来说就一件事情打包,先看下边这段简单的代码 [图片] [图片] [图片] 这段代码不能在浏览器中直接运行,因为浏览器不支持运行带有import和export关键字的代码,现代浏览器可以通过<script type=module>来支持,但是ie浏览器不支持,当项目比较大的时候我们对于代码拆分为多个文件的需求有很多,所以我们对于这个问题急需要解决。 webpack就是提供给解决这个问题的一个方案:把关键字转译为普通代码,并把所有文件打包成一个文件。 babel原理 要将代码打包webpack就需要借助babel对代码进行解析、转译等等工作。 babel工作步骤 babel转译代码分为三个步骤: 解读代码生成ast树 遍历ast树修改树节点属性生成新的ast树 通过ast生成代码 简单案例 通过一个简单案例,来理解下babel转换的过程,这个例子是个简单的将let 转化为var的过程: [代码]import { parse } from "@babel/core"; import traverse from "@babel/traverse"; import generate from "@babel/generator"; const code = `let a = 'let'; let b = 2`; const ast = parse(code, { sourceType: "module" }); if (ast) { traverse(ast, { enter: (item) => { if (item.node.type === 'VariableDeclaration') { if (item.node.kind === "let") { item.node.kind = "var"; } } }, }); const result = generate(ast, {}, code); console.log(result.code); } [代码] 先使用parse库将代码转换为ast树,然后通过traverse库遍历ast树,将所有类型声明语句中的类型从var改成let,最终结果为: [图片] babel库 import的几个库就是对应babel的转换过程: 生成ast树 @babel/core 遍历ast树 @babel/traverse ast树生成代码 @babel/generator AST AST是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,将上边的ast对象打印出来格式如下: [图片] ast是一个对象格式,program就是对代码做的整体抽象,例子中有两条声明语句,所以这里body数组中就是两个节点对象。 [图片] type属性表示这个语句的类型,例子中是个变量声明,kind表示这个语句用的是var声明变量。declarations语句由是一个节点数组,因为一个变量声明语句可以声明多个变量,declarations数组中的节点结构如下图: [图片] es6转es5案例 一个频繁使用babel的场景就是将js的新特性转换成低版本的js代码,要将新版本js转换成低版本js需要使用@babel/preset-env库,告诉babel新老版本js之间的转换关系。 [代码]import * as fs from "fs"; import { parse } from "@babel/core"; import * as babel from "@babel/core"; const code = fs.readFileSync("./test.js").toString(); const ast = parse(code, { sourceType: "module" }); if (ast) { const result = babel.transformFromAstSync(ast, code, { presets: ["@babel/preset-env"], }); if (result?.code) { fs.writeFileSync("./test.es5.js", result.code); } } [代码] test.js原代码: [代码]let a = "let"; let b = 2; const c = 3; function sum() { let a = 1, b = 1; return a + b; } [代码] 转义后的代码: [代码]"use strict"; var a = "let"; var b = 2; var c = 3; function sum() { var a = 1, b = 1; return a + b; } [代码] 打包文件 处理文件依赖关系 案例 通过一个简单的例子来了解webpack是怎么将几个文件的代码打包成一个文件。 代码结构 project_1/index.js [代码]import a from "./a.js"; import b from "./b.js"; console.log(a.value + b.value); [代码] project_1/a.js [代码]const a = { value: 1, } export default a [代码] project_1/b.js [代码]const b = { value: 2, } export default b [代码] 打包文件代码 [代码]import * as babel from "@babel/core"; import { parse } from "@babel/parser"; import traverse from "@babel/traverse"; import { readFileSync, writeFileSync } from "fs"; import { resolve, relative, dirname } from "path"; // 设置根目录 const projectRoot = resolve(__dirname, "project_2"); interface Dep { key: string; deps: string[]; code: string; } // 类型声明 type DepRelation = Dep[]; // 初始化一个空的 depRelation,用于收集依赖 const depRelation: DepRelation = []; // 数组! // 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js collectCodeAndDeps(resolve(projectRoot, "index.js")); console.log(depRelation); console.log("done"); /** * * @param filepath 文件绝对地址 */ function collectCodeAndDeps(filepath: string) { const key = getProjectPath(filepath); // 文件的项目路径,如 index.js if (depRelation.find((i) => i.key === key)) { // 注意,重复依赖不一定是循环依赖 return; } // 获取文件内容,将内容放至 depRelation const code = readFileSync(filepath).toString(); // 初始化 depRelation[key] // 将代码转为 AST const transformCode = babel.transform(code, { presets: ["@babel/preset-env"], }); const es5Code = transformCode?.code; if (!es5Code) { return; } const item: Dep = { key, deps: [], code: es5Code }; depRelation.push(item); const ast = parse(code, { sourceType: "module" }); // 分析文件依赖,将内容放至 depRelation traverse(ast, { enter: (path) => { if (path.node.type === "ImportDeclaration") { // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径 const depAbsolutePath = resolve( dirname(filepath), path.node.source.value ); // 然后转为项目路径 const depProjectPath = getProjectPath(depAbsolutePath); // 把依赖写进 depRelation item.deps.push(depProjectPath); collectCodeAndDeps(depAbsolutePath); } }, }); } // 获取文件相对于根目录的相对路径 function getProjectPath(path: string) { return relative(projectRoot, path).replace(/\\/g, "/"); } [代码] 代码思路 depRelation是依赖数组,存储所有的文件依赖,第一个元素是入口文件的依赖,depRelation数据项格式{ deps: [‘依赖的文件路径’], code: ‘文件的源码’ } collectCodeAndDeps是处理文件依赖的主函数,主要步骤是: 判断文件路径是否已经存在依赖数组中,如果已经存在不再重复处理 根据文件路径读取源代码 将源代码准换ast树 通过@babel/preset-env将代码转义然后存在依赖数据项的code属性中 遍历ast树,找到import语句,存入deps属性中,然后取出import路径,递归调用collectCodeAndDeps 打包后代码执行 最终需要通过生成的依赖关系数组对象,生成可以执行的代码,生成最终代码的函数如下: [代码]function generateCode() { let code = ""; code += "var depRelation = [" + depRelation .map((item) => { const { key, code, deps } = item; return `{ key: ${JSON.stringify(key)}, deps: ${JSON.stringify(deps)}, code: function(require, module, exports){ ${code} } }`; }) .join(",") + "];\n"; code += "var modules = {};\n"; code += `execute(depRelation[0].key)\n`; code += ` function execute(key) { if (modules[key]) { return modules[key] } var item = depRelation.find(i => i.key === key) if (!item) { throw new Error(\`\${item} is not found\`) } var pathToKey = (path) => { var dirname = key.substring(0, key.lastIndexOf('/') + 1) var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/') return projectPath } var require = (path) => { return execute(pathToKey(path)) } modules[key] = { __esModule: true } var module = { exports: modules[key] } item.code(require, module, module.exports) return modules[key] } `; return code; } [代码] 通过依赖关系数组拼装依赖对象,将code替换为方法 使用modules对象存储各个文件export出去的内容 自定义require函数替换每个文件中require方法,用modules对象替换每个文件的export方法对象 pathToKey方法处理"./"这种import 最终代码 [代码]import * as babel from "@babel/core"; import { parse } from "@babel/parser"; import traverse from "@babel/traverse"; import { readFileSync, writeFileSync } from "fs"; import { resolve, relative, dirname } from "path"; // 设置根目录 const projectRoot = resolve(__dirname, "project_2"); interface Dep { key: string; deps: string[]; code: string; } // 类型声明 type DepRelation = Dep[]; // 初始化一个空的 depRelation,用于收集依赖 const depRelation: DepRelation = []; // 数组! // 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js collectCodeAndDeps(resolve(projectRoot, "index.js")); function generateCode() { let code = ""; code += "var depRelation = [" + depRelation .map((item) => { const { key, code, deps } = item; return `{ key: ${JSON.stringify(key)}, deps: ${JSON.stringify(deps)}, code: function(require, module, exports){ ${code} } }`; }) .join(",") + "];\n"; code += "var modules = {};\n"; code += `execute(depRelation[0].key)\n`; code += ` function execute(key) { if (modules[key]) { return modules[key] } var item = depRelation.find(i => i.key === key) if (!item) { throw new Error(\`\${item} is not found\`) } var pathToKey = (path) => { var dirname = key.substring(0, key.lastIndexOf('/') + 1) var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/') return projectPath } var require = (path) => { return execute(pathToKey(path)) } modules[key] = { __esModule: true } var module = { exports: modules[key] } item.code(require, module, module.exports) return modules[key] } `; return code; } writeFileSync("dist_2.js", generateCode()); console.log(depRelation); console.log("done"); /** * * @param filepath 文件绝对地址 */ function collectCodeAndDeps(filepath: string) { const key = getProjectPath(filepath); // 文件的项目路径,如 index.js if (depRelation.find((i) => i.key === key)) { // 注意,重复依赖不一定是循环依赖 return; } // 获取文件内容,将内容放至 depRelation const code = readFileSync(filepath).toString(); // 初始化 depRelation[key] // 将代码转为 AST const transformCode = babel.transform(code, { presets: ["@babel/preset-env"], }); const es5Code = transformCode?.code; if (!es5Code) { return; } const item: Dep = { key, deps: [], code: es5Code }; depRelation.push(item); const ast = parse(code, { sourceType: "module" }); // 分析文件依赖,将内容放至 depRelation traverse(ast, { enter: (path) => { if (path.node.type === "ImportDeclaration") { // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径 const depAbsolutePath = resolve( dirname(filepath), path.node.source.value ); // 然后转为项目路径 const depProjectPath = getProjectPath(depAbsolutePath); // 把依赖写进 depRelation item.deps.push(depProjectPath); collectCodeAndDeps(depAbsolutePath); } }, }); } // 获取文件相对于根目录的相对路径 function getProjectPath(path: string) { return relative(projectRoot, path).replace(/\\/g, "/"); } [代码]
01-20 - 一个RequestTask.abort()引发的悲剧
背景介绍 我司有一款健康记录的微信小程序产品,为了程序的健壮性,前辈开发者们在产品开发初期就引入了[代码]wx.request[代码]响应超时,自动重发的逻辑。本月产品迎来重大迭代,用户量开始增加。sentry中突然有一个错误日志断断续续出现,日志显示前端收到了接口的response,但是response中没有数据。而后端确认接口是有数据的,多么诡异的问题,唯一有迹可循的是,接口报错时,是接口超时没响应后重发的第二次请求 [图片] 开始排查 作为公司唯一的实习生,我荣获了周末加班排查这个bug的殊荣,打开我的大宝剑,错了,打开我的macbook,打开IDE,我发现了前辈们在接口重发逻辑中写的一个我不认识的东西 [代码]this.requestTask.abort() [代码] [图片] 马上打开官方文档查看 [图片] 文档让我心里慌的一比,没办法,只能自己去尝试 [图片] 举个栗子🌰 为了胜利我把重发的核心逻辑剥离出来放在一个单页面的小程序中,大概就是这样👇 [代码] data: { requestTask: null, retryRequest: null, retryCount: 0, timeout: 50 // 为了测试我把接口超时的时间改成了50ms }, test: function() { if(this.retryCount>3) { clearTimeout(this.retryRequest) return false } this.retryRequest = setTimeout(() => { this.retryCount++ this.requestTask.abort() // 中止上一个请求 this.test() // 调用自己发起下一次请求 }, 50) this.requestTask = wx.request({ url: 'api', success: (res) => { console.log('in success:', res) clearTimeout(this.retryRequest) }, fail: (res) => { console.log('in fail:', res) } }) } [代码] RequestTask.abort()分析 来,请大家猜猜下面红色的error是代码错误了还是中断成功了? [图片] 恭喜,这是请求中止成功啦。鼓掌!!! abort()函数执行成功(请求被中止),会进入fail回调和complete回调,如果errMsg == “request:fail abort”,就表示之前的请求被中止了。至于为什么会有红色的报错(没有任何意义),这是爱的鼓励,不要问为什么。 [图片] 重现问题 我一遍又一遍的点击着小程序开发者工具的编译按钮,发现在我点击十次之内,一定会出现重发多个请求后,有一个请求得到响应,其他请求被取消,而在success中打印res,发现里面的data不见了,就是下面这样👇 [图片] 推测一下 [图片] abort()函数是已经封装好的函数,是一个异步的函数 原先的逻辑中,abort被执行,但是并不知道请求是否已经彻底终止,就发起了下一个请求 [代码]this.requestTask.abort() this.test() [代码] 就在终止请求的时,下一个请求响应,底层开始处理请求的响应,上一次的终止逻辑被停了。最后,上一个来不及终止的请求也得到了响应,各种巧合导致其中一个得到响应的请求,success回调中打印不出来data 改动一下 在定时器setTimeout中执行requestTask.abort(),在fail的回调中,只有判断出res.errMsg === 'request:fail abort’的情况下,表示上一个请求已经彻底终止。重发下一次请求 [代码] test: function() { this.retryRequest = setTimeout(() => { this.requestTask.abort() }, 50) this.requestTask = wx.request({ url: 'api', success: (res) => { console.log(res) clearTimeout(this.retryRequest) }, fail: (res) => { if (res.errMsg === 'request:fail abort') this.test() }, complete: function (res) {}, }) } [代码] 鸣谢 [图片]
2019-10-28