Giter VIP home page Giter VIP logo

front-end-development-notes's Introduction

Hi there 👋

logo

时不时手撕常见框架的源码,或者搞一些有趣的工具,当然还有前端知识的总结

  • 前端知识总结 知识总结,踩坑记录,最佳实践,八股文应有尽有

  • egg-react-ssr 自研的、从0到1搭建的、配置透明简约的、实战化的、具备首屏SSR、CSR/SSR无缝切换的、代码分割懒加载的react-ssr 开发框架

  • mini-react 针对react、react dom、react reconciler、react scheduler源码进行全方位深度解析,【进行中】

  • mini-parser 从零开始实现一个AST解析器

  • mini-react-redux 手写redux、react-redux源码

  • mini-react-router 手写react-router-dom、react-router源码

  • monaco-editor-app 基于monaco editor的在线代码编辑器

  • 程序员送外卖指南

  • mini-webpack 手写webpack源码,以及webpack常见loader,plugin源码

  • babel-plugin-react-directives babel插件开发,为react添加 r-ifr-show 指令

  • mini-webpack-dev-server 手写 webpack-dev-server 源码,如何注入热更新运行时代码,如何生成补丁文件等

  • mini-tapable webpack 插件机制核心。 mini-tapable 不仅解读官方 tapable 的源码,还用自己的思路去实现一遍,并且和官方的运行时间做了个比较,我和webpack作者相关的讨论可以点击查看

  • mini-promise 手写 es6-promise 源码,理解 async await 语法糖原理,这里有几道题帮助加强理解 Promise A+ 规范

  • mini-koa 手写koa源码,koa-compose 中洋葱圈模型的实现

  • web-monitor web前端性能监控埋点

  • create-app-cli 一个类似于vue-cli的脚手架工具

  • antd-learn 使用bisheng搭建的仿antd组件库的react组件库开发框架,bisheng是antd提供的组件库文档生成器

  • skeleton 前端骨架屏生成插件,自动抓取页面并生成页面骨架屏。使用puppeteer操作无头浏览器并抓取页面,生成页面骨架

  • mini-vite 手写vite源码

正在做的事情(Doing Now)

  • react原理及源码
  • 编译原理,源码转AST,AST转源码

即将要做的事情(Todo)

  • service worker
  • web rtc
  • cicd
  • docker
  • 组件库搭建
  • 微前端
  • go语言

front-end-development-notes's People

Contributors

lizuncong avatar zenglincient avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

front-end-development-notes's Issues

压缩混淆后的源码如何debug

前言

本篇文章介绍如何白嫖谷歌翻译服务翻译浏览器网页,以及如何从压缩混淆后一万多行代码中探索 bug 的真相。压缩混淆后的源码调试面临以下挑战:

  • 1.由于变量名或者函数名经过压缩,因此如果想要在文件中查找函数名称或者变量名称尤其困难
  • 2.追踪对象属性在哪里被修改变得更加困难

业务背景

在我们的业务场景中,有些文案是商家自己输入的,此时我们无法针对这些输入做多语言的转换,因此只能另辟蹊径,借助谷歌翻译服务。具体接入方式如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>谷歌翻译服务Demo</title>
    <meta name="referrer" content="no-referrer" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style></style>
  </head>

  <body>
    <div id="root">
      <div id="繁体">庫存</div>
      <div id="简体">库存</div>
      <div id="英语">in stock</div>
      <div id="日本语">在庫あり</div>
      <div id="韩语">재고</div>
      <div id="google_translate_element"></div>
    </div>
    <script>
      function googleTranslateElementInit() {
        // pageLanguage指定页面语言,如果指定为 auto,则告诉谷歌自动检测文字语言类型
          new google.translate.TranslateElement({ pageLanguage: 'auto' }, "google_translate_element");
      }
      (function () {
        var gtConstEvalStartTime = new Date();
        var h = this || self,
          l = /^[\w+/_-]+[=]{0,2}$/,
          m = null;
        function n(a) {
          return (a = a.querySelector && a.querySelector("script[nonce]")) &&
            (a = a.nonce || a.getAttribute("nonce")) &&
            l.test(a)
            ? a
            : "";
        }
        function p(a, b) {
          function c() {}
          c.prototype = b.prototype;
          a.i = b.prototype;
          a.prototype = new c();
          a.prototype.constructor = a;
          a.h = function (g, f, k) {
            for (
              var e = Array(arguments.length - 2), d = 2;
              d < arguments.length;
              d++
            )
              e[d - 2] = arguments[d];
            return b.prototype[f].apply(g, e);
          };
        }
        function q(a) {
          return a;
        }
        function r(a) {
          if (Error.captureStackTrace) Error.captureStackTrace(this, r);
          else {
            var b = Error().stack;
            b && (this.stack = b);
          }
          a && (this.message = String(a));
        }
        p(r, Error);
        r.prototype.name = "CustomError";
        function u(a, b) {
          a = a.split("%s");
          for (var c = "", g = a.length - 1, f = 0; f < g; f++)
            c += a[f] + (f < b.length ? b[f] : "%s");
          r.call(this, c + a[g]);
        }
        p(u, r);
        u.prototype.name = "AssertionError";
        function v(a, b) {
          throw new u(
            "Failure" + (a ? ": " + a : ""),
            Array.prototype.slice.call(arguments, 1)
          );
        }
        var w;
        function x(a, b) {
          this.g = b === y ? a : "";
        }
        x.prototype.toString = function () {
          return this.g + "";
        };
        var y = {};
        function z(a) {
          var b = document.getElementsByTagName("head")[0];
          b ||
            (b = document.body.parentNode.appendChild(
              document.createElement("head")
            ));
          b.appendChild(a);
        }
        function _loadJs(a) {
          var b = document;
          var c = "SCRIPT";
          "application/xhtml+xml" === b.contentType && (c = c.toLowerCase());
          c = b.createElement(c);
          c.type = "text/javascript";
          c.charset = "UTF-8";
          if (void 0 === w) {
            b = null;
            var g = h.trustedTypes;
            if (g && g.createPolicy) {
              try {
                b = g.createPolicy("goog#html", {
                  createHTML: q,
                  createScript: q,
                  createScriptURL: q,
                });
              } catch (t) {
                h.console && h.console.error(t.message);
              }
              w = b;
            } else w = b;
          }
          a = (b = w) ? b.createScriptURL(a) : a;
          a = new x(a, y);
          a: {
            try {
              var f = c && c.ownerDocument,
                k = f && (f.defaultView || f.parentWindow);
              k = k || h;
              if (k.Element && k.Location) {
                var e = k;
                break a;
              }
            } catch (t) {}
            e = null;
          }
          if (
            e &&
            "undefined" != typeof e.HTMLScriptElement &&
            (!c ||
              (!(c instanceof e.HTMLScriptElement) &&
                (c instanceof e.Location || c instanceof e.Element)))
          ) {
            e = typeof c;
            if (("object" == e && null != c) || "function" == e)
              try {
                var d =
                  c.constructor.displayName ||
                  c.constructor.name ||
                  Object.prototype.toString.call(c);
              } catch (t) {
                d = "<object could not be stringified>";
              }
            else
              d = void 0 === c ? "undefined" : null === c ? "null" : typeof c;
            v(
              "Argument is not a %s (or a non-Element, non-Location mock); got: %s",
              "HTMLScriptElement",
              d
            );
          }
          a instanceof x && a.constructor === x
            ? (d = a.g)
            : ((d = typeof a),
              v(
                "expected object of type TrustedResourceUrl, got '" +
                  a +
                  "' of type " +
                  ("object" != d
                    ? d
                    : a
                    ? Array.isArray(a)
                      ? "array"
                      : d
                    : "null")
              ),
              (d = "type_error:TrustedResourceUrl"));
          c.src = d;
          (d = c.ownerDocument && c.ownerDocument.defaultView) && d != h
            ? (d = n(d.document))
            : (null === m && (m = n(h.document)), (d = m));
          d && c.setAttribute("nonce", d);
          z(c);
        }
        function _loadCss(a) {
          var b = document.createElement("link");
          b.type = "text/css";
          b.rel = "stylesheet";
          b.charset = "UTF-8";
          b.href = a;
          z(b);
        }
        function _isNS(a) {
          a = a.split(".");
          for (var b = window, c = 0; c < a.length; ++c)
            if (!(b = b[a[c]])) return !1;
          return !0;
        }
        function _setupNS(a) {
          a = a.split(".");
          for (var b = window, c = 0; c < a.length; ++c)
            b.hasOwnProperty
              ? b.hasOwnProperty(a[c])
                ? (b = b[a[c]])
                : (b = b[a[c]] = {})
              : (b = b[a[c]] || (b[a[c]] = {}));
          return b;
        }
        window.addEventListener &&
          "undefined" == typeof document.readyState &&
          window.addEventListener(
            "DOMContentLoaded",
            function () {
              document.readyState = "complete";
            },
            !1
          );
        if (_isNS("google.translate.Element")) {
          return;
        }
        (function () {
          var c = _setupNS("google.translate._const");
          c._cest = gtConstEvalStartTime;
          gtConstEvalStartTime = undefined;
          c._cl = "en";
          c._cuc = "googleTranslateElementInit";
          c._cac = "";
          c._cam = "";
          c._ctkk = "448204.2198466445";
          var h = "translate.googleapis.com";
          var s =
            (true
              ? "https"
              : window.location.protocol == "https:"
              ? "https"
              : "http") + "://";
          var b = s + h;
          c._pah = h;
          c._pas = s;
          c._pbi = b + "/translate_static/img/te_bk.gif";
          c._pci = b + "/translate_static/img/te_ctrl3.gif";
          c._pli = b + "/translate_static/img/loading.gif";
          c._plla = h + "/translate_a/l";
          c._pmi = b + "/translate_static/img/mini_google.png";
          c._ps = b + "/translate_static/css/translateelement.css";
          c._puh = "translate.google.com";
          _loadCss(c._ps);
          _loadJs(b + "/translate_static/js/element/main_zh-CN.js");
        })();
      })();
    </script>
  </body>
</html>

今天接到一个 bug,如果后端返回的页面中,文案是繁体字时,此时切换语言选择器,切换成简体中文,发现这部分文案没有被翻译

image

可以发现,当切换语言为简体中文时,只有繁体的字没有被翻译,其余的都被翻译成简体中文了。

如何从一万多行的压缩混淆的源码中探索真相

1. 首先在 DOM 上打个断点

谷歌在翻译我们的网页时,会自动在文本节点上插入 font 节点

image

借助这一点,我们可以在有问题的 dom 上打个断点,看看谷歌是如何更新我们的 dom 节点的

image

然后切换语言,这里可以选择英语

谷歌在翻译时,会先移除节点再插入新的节点,因此这里我们可以直接跳过

image

一直跳过,直到函数执行到 cu 调用栈,此时观察浏览器页面会发现新的翻译节点 in stock 已经插入进来

image

因此有理由相信这个 cu 函数就是插入节点的函数。在这个函数入口处打个断点:

image

此时将选择器切换成简体中文,会发现由于 a.Ktrue 导致 if 语句块没有执行,节点没有被翻译:

image

那为什么 庫存 节点的 a.Ktrue,其他节点的就是 false 呢?

顺着调用栈往上找:

image

image

很明显,当断点执行到 v.jg 时,库存节点的 K 还是 false,为啥函数执行完成,K 就变成了 true?这个方法肯定是做了某些操作。排查范围已经缩小到这个函数了。因此我们只需要一步步执行,最终执行到这里的时候发现:

image

image

由此可以得出初步结论,当我们切换的语言为简体中文,即 "zh-CN" 时,并且和 h[2] 所设置的语言一致时,此时会将 K 设置为 true,并跳过我们的翻译。因此只需要排查 h 是个什么东西。

image

h[2] 又是什么呢??

image

既然 c 是一个 zu 类型的对象,那它一定是通过构造函数 new zu 构造出来的,因此我们全局搜索 new zu 并在这些语句的地方打个断点

image

回到控制台,添加几行代码:

Object.defineProperty(a.m[0], 'g', {
    set: function(newVal){
        debugger;
        console.log('newVal====', newVal)
    }
})

image

继续执行代码:

image

沿着 set 的调用栈往上找:

image

于是在 send 函数的入口打一个断点

image

可以知道 zh-CN 就是接口返回来的,因此我们去控制台查看网络请求:

image

原来这是谷歌翻译接口返回来的标志,那这些标志代表什么?

回到我们的demo中

<div id="繁体">庫存</div>
<div id="简体">库存</div>
<div id="英语">in stock</div>
<div id="日本语">在庫あり</div>
<div id="韩语">재고</div>

当我们初始化谷歌翻译实例时

new google.translate.TranslateElement({ pageLanguage: 'en'}, "google_translate_element")

如果指定的 pageLanguage 为 特定语言,比如 en,那么告诉谷歌只帮忙翻译页面中的英文单词,其余语言不用翻译。此时谷歌翻译接口返回的数据中不会带有语言标志,比如 zh-CN

image

如果指定的 pageLanguageauto,那么相当于告诉谷歌自定检测页面所有字的语言类型,并翻译成我选择的语言

new google.translate.TranslateElement({ pageLanguage: 'auto'}, "google_translate_element")

image

可以看到谷歌翻译接口返回了谷歌检测的原始语言类型。注意,这里 庫存 这个繁体单词,谷歌检测到的是 zh-CN,而不是 zh-TW!!!其余单词的检测均是正常的

结论

  • 当我们指定 pageLanguageauto 时,谷歌会自动检测页面单词的原始语言类型,并在翻译接口中返回对应的语言类型给前端。但是谷歌在检测中文繁体时,一律返回的是简体的标志 zh-CN,而不是 zc-TW
  • 当我们切换语言时,比如从中文繁体 zh-TW 切换成中文简体 zh-CN,谷歌翻译脚本会判断我们切换的语言 zh-CN 和谷歌识别的语言是否相同,如果相同,则说明该节点不用翻译。举例如下:

in stock 谷歌翻译接口返回的是 enen !== zh-CN,因此这个节点会被翻译

在庫あり 谷歌翻译接口返回的是 jaja !== zh-CN,因此这个节点也会被翻译

库存 谷歌翻译接口返回的是 zh-CNzh-CN === zh-CN,说明这个节点原本就是中文简体,然后我们切换的语言是中文简体,因此这个节点不需要翻译

庫存 谷歌翻译接口返回的是 zh-CN 而不是 zh-TWzh-CN === zh-CN,导致这个繁体字没有被翻译。

因此,这个说明谷歌翻译服务在检测繁体字时,是存在问题的。

阿拉伯数字转中文写法

function transform(num){
    const unitMap = ['亿', '千', '百', '十', '万', '千', '百', '十', '']
    const numMap = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
    const arr = String(num).split('')
    const start = unitMap.length - arr.length;
    let result = ''
    arr.forEach((n, idx) => {
        result = result + numMap[n] + unitMap[start + idx]
    })
    result = result.replace('零万', '万')
    result = result.replace(/零./g, '零')
    result = result.replace(/零+$/g, '')
    console.log(result)
    return result
    
}
transform(104340000) // 一亿零四百三十四万
transform(10403040) // 一千零四十万三千零四十
transform(403040) // 四十万三千零四十

302重定向到同源网站cookie丢失的问题

问题

上周有个 java 的朋友问我,为什么第三方域名重定向到我们自己的服务时,请求头中的 cookie 会丢失。

本着好奇心,我就试试,没想到却成了我一个悬而未解的问题

复现场景

新建一个 server.js 文件

const express = require("express");

const app = express();

app.get("/api/auth", (req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.redirect(302, "http://localhost:9000/server/api/authSuccess");
});

app.listen(4000);

这个服务非常简单,运行在 4000 前端,监听 /api/auth 并重定向到 http://localhost:9000/server/api/authSuccess

在实际的业务场景中,http://localhost:4000/api/auth是第三方服务提供的鉴权服务,鉴权成功会重定向到我们提供的前端的 redirect url,即 http://localhost:9000/server/api/authSuccess。这里只是简化复现问题

新建一个 index.js 文件

const express = require("express");

const app = express();

app.get("/api/authSuccess", (req, res) => {
  console.log("3000端口请求成功");
  res.send("请求成功");
});

app.listen(3000);

在实际的业务场景中,http://localhost:9000/server/api/authSuccess 是我们自己的后端服务。

本地起一个简单的前端服务:

import React, { Component, PureComponent } from "react";
import ReactDOM from "react-dom";
class Counter extends Component {
  constructor(props) {
    debugger;
    super(props);
  }
  handleClick() {
    fetch("http://localhost:4000/api/auth");
  }
  handleClickAuth() {
    fetch("/server/api/authSuccess");
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>点击发起鉴权请求</button>
        <button onClick={this.handleClickAuth}>
          手动发起 auth success 请求
        </button>
      </>
    );
  }
}

ReactDOM.render(<Counter />, document.getElementById("root"));

前端 webpack dev server 配置如下:

const devConfig = {
  devServer: {
    host: "0.0.0.0",
    port: "9000",
    contentBase: path.resolve(__dirname, "../dist"),
    // hot: true,
    headers: { "Access-Control-Allow-Origin": "*" },
    overlay: {
      errors: true,
    },
    proxy: {
      "/server": {
        target: "http://localhost:3000",
        // secure: false, // 如果请求的网址是https,需要配置secure: false
        pathRewrite: {
          "/server": "",
        },
        changeOrigin: true,
      },
    },
  },
};

注意这里代理了所有的 /server 的请求并将 /server 替换成空字符串

先来看下 cookie 面板,可以看到有设置了一个 cookie

image

点击 点击发起鉴权请求 按钮

image

首先向第三方提供的鉴权服务 http://localhost:4000/api/auth 发起请求,这个请求肯定是跨域请求。但由于是简单请求,不需要发起预检请求。请求成功后,重定向回我们的前端服务

image

可以看到重定向回来的请求,浏览器认为这是一个跨域请求,并不会在请求头中携带 cookie,为什么???
image

手动发起的同样的请求,cookie 是正常的:
image

猜测

由于未找到合理的解释,这里只是一个暂时的猜测。

当我们发起一个对第三方域名接口的访问时,如果这个接口又重定向回我们自己的接口,浏览器会认为我们自己的接口是第三方域名发起的(通过下图的 Initial 可以看出),因此浏览器认为这是一个跨域的请求

image

如果有朋友知道原因,还请帮忙解答一下

纯CSS实现滚动时给吸顶页头添加阴影

效果

image

源码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>阴影</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      body {
        margin: 0;
      }
      header {
        position: sticky;
        background: #fff;
        top: 0;
        font-size: 20px;
        padding: 10px;
        z-index: 1;
      }
      .shadow::before {
        content: "";
        box-shadow: 0 0 10px 1px #333;
        position: fixed;
        width: 100%;
      }
      .shadow::after {
        content: "";
        width: 100%;
        height: 30px;
        background: linear-gradient(to bottom, #fff 50%, transparent);
        position: absolute;
      }
      main {
        margin-top: 20px;
      }
    </style>
  </head>
  <body>
    <div id="root">
      <header>LOGO</header>
      <div class="shadow"></div>
      <main>
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
        超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本
      </main>
    </div>
  </body>
</html>

按需加载原理及加强版按需加载插件开发

本文介绍按需加载原理、babel 插件开发、抽象语法树、如何开发一个加强版的按需引入插件

按需加载原理

以 antd 组件库为例,来了解下为什么需要按需加载

antd 通过index.js文件暴露所有的组件,比如:

export { default as Button } from "./button";
export { default as Input } from "./input";
export { default as Select } from "./select";
export { default as Upload } from "./upload";
// 省略了很多

在我们的业务代码中,我们可以通过以下两种方式使用:

  • 第一种引入方法
import { Button, Input } from "antd";
  • 第二种引入方法
import Button from "antd/button";
import Input from "antd/input";

那么这两种方法有什么区别呢?

先来看下第一种方法:

  • 第一种方法引入的是 antd/index.js文件暴露的模块。第三方开发者使用简单,无需关注组件的具体路径。
  • 这种方法最大的缺点就是无法做到按需引入。假设我们的项目中只需要 Button 和 Input 组件,理论上打包只需要打包这两个组件的代码即可。但是,这种方式引入的是 import { Button, Input } from "antd/index.js",即antd/index.js 文件,又由于这个文件引入了 antd 所有的组件并导出,webpack 在打包 antd/index.js 时,就会打包这些所有的组件的代码,造成资源浪费

再来看下第二种方法:

  • 第二种方法通过指定组件的具体文件位置,比如 import Button from "antd/button"; 来引入组件,而无需经过 antd/index.js 文件引入。webpack 打包时,只会打包 antd/button 组件的代码,即可达到按需加载的目的
  • 此方法最大的缺点就是,第三方开发者需要关注组件的文件位置,同时,如果 antd 组件库组件的位置调整,就会给第三方开发者的业务带来风险

那有没有办法,既能兼顾第一种方法的引入方法,又能兼顾按需加载呢?

答案是肯定的,我们可以通过在 webpack 打包时进行特殊处理,写个 webpack loader 进行源码转换

import { Button, Input } from "antd";

当我们识别到 import 的是 antd 的模块时,可以用 loader 将其转换成:

import Button from "antd/button";
import Input from "antd/input";

第三方使用者无需关注底层实现细节,而是从构建层面进行转换。这也是babel-plugin-import的基本原理

至于在打包构建时如何识别 import 的是 antd 的模块,还是其他模块,如果你正则很强的话,当然可以通过正则表达式去识别。但首选抽象语法树

抽象语法树

抽象语法树的基础知识可以看这里,也可以通过在线的工具ast explorer体验一下。我们可以看下以下代码转换成抽象语法树是怎样的:

import util, { BB as CC } from "./util.js";
export { default as Home, AA as DD } from "./home.js";

image

import 语句对应的抽象语法树节点分析

从图中可以看出,import 语句对应的节点类型为 ImportDeclaration

注意观察 import util from './util.js'; 以及 import { BB as CC} from './util.js'; 这两种引入方式对应的语法树节点有何不同。前者是 ImportDefaultSpecifier 类型,并且没有 imported 属性。后者是 ImportSpecifier 类型,拥有 local 以及 imported 属性。

local 以及 imported 的含义是什么?看下面的代码:

import { BB as CC } from "./util.js";

这句代码的意思是从 util.js 中导入变量 BB,并且重命名为 CC,可以这样理解:

import { BB } from "./util.js";

const CC = BB;

在这里,CC 对应的就是 localBB对应的就是 imported,即 util.js 中暴露出的变量名称

export 语句对应的抽象语法树节点分析

从图中可以看出,export 语句对应的节点类型为 ExportNamedDeclaration

export { default as Home, AA as DD } from "./home.js";

这句代码和下面的方式等价:

import Home from "./home.js";
import { AA as DD } from "./home.js";
export { Home, DD };

export 出去的 Home 以及 DD 在抽象语法树中都是 ExportSpecifier 节点类型,同时拥有 local 以及 exported 属性。

local 表示 default 或者 AAexported 表示 Home 或者 DD

babel 编译原理

babel 在转换我们的源码时,会经过以下步骤:

  • 使用 @babel/parser 读取源代码并转换为抽象语法树,即 AST。
  • 其次使用 @babel/traverse 遍历抽象语法树,并根据 visitor 修改语法树。开发者可以通过 visitor 接口注册对应的节点类型监听事件
  • 最后使用 @babel/generator 将修改后的抽象语法树转为源代码。

在第二步修改抽象语法树节点时,可以使用 babel 官方提供给我们的 @babel/types工具构建语法树节点。这个工具有点类似于 Jquery,可以让我们很方便的构造抽象语法树任何类型的节点,并且检查某一节点的类型。

babel 官方还内置了 path工具用于操作节点,比如删除,插入,更新节点等等。

babel 插件开发

babel 插件开发可以查看 babel 官方提供的插件开发指南以及babel handbook

编写你的第一个 babel 插件

@babel/parser将源代码转换成抽象语法树,同时提供 visitor 接口给开发者订阅相应的节点类型,当调用@babel/traverse遍历抽象语法树时,如果遍历到我们订阅的节点类型,则调用我们的监听事件

以一个简单的逆转变量名称的插件为例:

export default function reverseNamePlugin() {
  return {
    visitor: {
      Identifier(path) {
        const name = path.node.name;
        if (name === "JavaScript") {
          // reverse the name: JavaScript -> tpircSavaJ
          path.node.name = name.split("").reverse().join("");
        }
      },
    },
  };
}

然后在 .babelrc 文件中使用这个插件:

{
  "plugins": ["./reverse-name-plugin"]
}

babel编译过程中,reverseNamePlugin 插件会找出所有的名字为 JavaScript 变量,并逆转变量名称:

const JavaScript = "hello javascript";
// 经过babel编译,插件转换后变成
const tpircSavaJ = "hello javascript";

reverseNamePlugin可以看出,babel 插件就是一个返回对象的普通函数,返回的对象中,必须定义 visitor 接口,这也叫做访问者模式。在 visitor 接口中,我们可以监听任何抽象语法树节点类型,比如前面介绍的 ImportDeclarationExportNamedDeclaration 等等。然后可以操作抽象语法树的节点,比如替换变量名称等等。babel 在遍历抽象语法树时,如果遍历到我们监听的节点类型,会调用我们在 visitor 中注册的监听事件。

babel 插件开发,本质上就是修改抽象语法树的过程,而这离不开 babel 提供给我们的操作抽象语法树的工具 babel/types。

babel types 的使用

@babel/types

以一个简单的例子说明。假设我们有以下代码,我需要将 javascript 在打包时替换成 typescript

const name = "javascript";

这段代码对应的抽象语法树节点如下:
image

从图中可以看出,变量声明的类型是VariableDeclarator,同时javascript 的类型是 StringLiteral,因此我们可以在 visitor 中监听 VariableDeclarator 节点类型,然后判断 init.value 如果是 javascript,则替换成 typescript,实现如下:

function replaceName() {
  return {
    visitor: {
      VariableDeclarator(path) {
        const node = path.node;
        if (node.init.value === "javascript") {
          node.init.value = "typescript";
        }
      },
    },
  };
}

如果借助 @babel/types,我们可以构造一个 StringLiteral 类型的值,并且覆盖 node.init,比如:

function replaceName({ types }) {
  return {
    visitor: {
      VariableDeclarator(path) {
        const node = path.node;
        if (node.init.value === "javascript") {
          node.init = types.stringLiteral("typescript");
        }
      },
    },
  };
}

StringLiteral 类型的用法可以在 @babel/types 文档查看:

image

babel types 让我们可以很方便的构造抽象语法树的节点类型。

path的使用可以参考文档

如何使用 babel 转换源码

现在,我们来看下如何开发一个 transform 函数,对我们的源代码进行转换。首先安装 @babel/generator@babel/parser@babel/traverse@babel/types这几个依赖。

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");

function transform(code) {
  const ast = parser.parse(code, {});

  const visitor = {
    Identifier(path) {
      const name = path.node.name;
      if (name === "JavaScript") {
        // reverse the name: JavaScript -> tpircSavaJ
        path.node.name = name.split("").reverse().join("");
      }
    },
  };

  traverse.default(ast, visitor);
  console.log("before transform\n", code);
  const result = generator.default(ast, {}, code);
  console.log("after transform\n", result.code);
}

const code = `const JavaScript = "Hello JavaScript"`;

transform(code);

这段代码将所有的名称为 JavaScript 的变量逆转成 tpircSavaJ,执行这段代码控制台输出:

before transform
 const JavaScript = "Hello JavaScript"
after transform
 const tpircSavaJ = "Hello JavaScript";

其中 parser.parse(code, {}) 第二个参数支持的配置项可以查看文档Identifier(path)path的用法可以查看这里

我们加大难度,看看在打包时如何将

import { util } from "./util.js";

转换成

import util from "./util.js";

image

从图中可以看出, util 对应的抽象语法树节点类型为 ImportSpecifier,因此我们需要在 visitor 中监听 ImportSpecifier 节点。

同时,import util from "./util.js"; 中的 util 对应的节点类型为 importDefaultSpecifier,因此我们可以借助 babel/types 生成一个importDefaultSpecifier类型的节点,并使用 path.replaceWith替换节点

ImportSpecifier(path) {
  if (path.node.imported.name === "util") {
    path.replaceWith(types.importDefaultSpecifier(path.node.imported));
  }
}

如果我们想要将 import { util } from "./util.js"; 替换成 import util from "@/util.js";,可以监听 ImportDeclaration 节点:

ImportDeclaration(path){
    if(path.node.source.value === './util.js'){
        path.node.source.value = '@/util.js'
    }
}

源码如下:

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");

function transform(code) {
  const ast = parser.parse(code, {
    sourceType: "module",
  });

  const visitor = {
    Identifier(path) {
      const name = path.node.name;
      if (name === "JavaScript") {
        // reverse the name: JavaScript -> tpircSavaJ
        path.node.name = name.split("").reverse().join("");
      }
    },
    ImportSpecifier(path) {
      if (path.node.imported.name === "util") {
        path.replaceWith(types.importDefaultSpecifier(path.node.imported));
      }
    },
    ImportDeclaration(path) {
      if (path.node.source.value === "./util.js") {
        path.node.source.value = "@/util.js";
      }
    },
  };

  traverse.default(ast, visitor);
  console.log("before transform\n", code);
  const result = generator.default(ast, {}, code);
  console.log("after transform\n", result.code);
}

const code = `import { util } from "./util.js";`;

transform(code);

加强版按需加载插件或者 webpack loader 的开发

业务背景

在我们的微前端业务场景中,主应用暴露模块给子应用使用。在子应用的项目中新增一个 remote.js 文件,用于统一收拢引入主应用暴露的远程模块。比如

子应用 A 项目新建一个 remote.js 文件,负责统一引入主应用暴露的远程模块:

export { util } from "shared/util";
export { default as PageLoad, CardLoad } from "shared/PageLoad";
export { useRequest as useCustomRequest, useAnimation } from "shared/hooks";
export { Button, Table } from "shared/Components";
// 省略了其他的共享模块

然后在子应用 A 项目的业务代码中,比如 home.js 中,就可以这么引用:

import {
  util,
  PageLoad as LocalPageLoad,
  CardLoad,
  useCustomRequest as LocalCustomRequest,
  useAnimation,
} from "@/remote"; // 远程模块
import LocalModule from "./localModule.js"; // 子应用A项目自身的模块
import { LocalModule2 } from "./localModule2.js"; // 子应用A项目自身的模块
console.log(
  util,
  LocalPageLoad,
  CardLoad,
  LocalCustomRequest,
  useAnimation,
  LocalModule,
  LocalModule2
);

这样就可以很方便的使用,同时将远程模块统一管理,后面即使远程模块的路径改变了,比如 shared/util 改成 host/util,那也只需要在 remote.js 中统一修改。

但是,这里又有一个问题,home.js 中没有使用到 Button 以及 Table 这两个共享模块,由于我们在 home.js 中直接 import {} from '@/remote.js' 将整个 remote.js 都引入了,所以 remote.js 里面所有的远程模块都会加载进来,造成资源的浪费。我们需要一种按需加载的方案

因此,我们需要在打包时,转换(修改)源码。我们识别出 @/remote.js 的路径,然后替换成真实的远程模块的路径,比如修改后的 home.js 如下:

import { util } from "shared/util"; // 在打包的过程进行转换
import { CardLoad } from "shared/PageLoad"; // 在打包的过程进行转换
import LocalPageLoad from "shared/PageLoad"; // 在打包的过程进行转换
import { useAnimation } from "shared/hooks"; // 在打包的过程进行转换
import { useRequest as LocalCustomRequest } from "shared/hooks";
import LocalModule from "./localModule.js"; // 子应用A项目自身的模块
import { LocalModule2 } from "./localModule2.js"; // 子应用A项目自身的模块

这个转换过程,在 webpack loader 处理源码的时候进行。因此我们可以写一个 webpack loader 来进行转换。当然也可以基于 babel loader 提供的插件能力进行转换。

remote.js 模块收集

为了在子应用的业务代码中进行模块路径转换,比如 home.js 中,为了将 import { util } from "@/remote.js"; 转换成 import { util } from "shared/util" ,我们需要收集 remote.js 中模块名称和模块路径的信息,比如:

{
  util: {
    name: 'util',
    exportKind: 'value',
    modulePath: 'shared/util'
  }
}

我们需要读取子应用 A 项目下的 remote.js 文件,并将源码转换成抽象语法树,方便收集模块信息。getRemoteModulePathMap.js 如下:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
const fs = require("fs");
const p = require("path");

module.exports = function getRemoteModulePathMap(code) {
  const remoteModulePathMap = {};
  // 将源码转换成抽象语法树
  const ast = parser.parse(code, {
    sourceType: "module",
    allowImportExportEverywhere: true,
  });
  // 注册节点监听事件
  const visitor = {
    // 监听 export 节点
    ExportNamedDeclaration: (path, state) => {
      const {
        node: { source = {}, specifiers },
      } = path;
      const modulePath = source.value;
      specifiers.forEach((specify) => {
        const { exported, exportKind } = specify;
        remoteModulePathMap[exported.name] = {
          name: exported.name, // 导出的模块名称
          exportKind, // 值:type或者value。正常的模块是value。typescript的类型声明是type
          modulePath, // 模块路径
          specify,
        };
      });
    },
  };
  // 开始遍历
  traverse.default(ast, visitor);
  return remoteModulePathMap;
};

import-path-place 插件实现

const fs = require("fs");
const p = require("path");
const getRemoteModulePathMap = require("./getRemoteModuleMap");
module.exports = function ({ types, ...rest }) {
  let remoteModulePathMap = {};
  function ImportDeclarationVisitor(path, { opts }) {
    const {
      node: {
        source: { value },
        specifiers,
      },
    } = path;
    if (value !== opts.match) return;
    specifiers.forEach((specify) => {
      const importedName = specify.imported.name;
      const realModulePath = remoteModulePathMap[importedName];
      if (!realModulePath) return;
      const realModuleSpecify = realModulePath.specify;
      if (realModuleSpecify.local) {
        if (realModuleSpecify.local.name === "default") {
          specify = types.importDefaultSpecifier(specify.local);
        } else {
          specify.imported = realModuleSpecify.local;
        }
      }
      path.insertBefore(
        types.importDeclaration(
          [specify],
          types.stringLiteral(realModulePath.modulePath)
        )
      );
    });
    path.remove();
  }
  return {
    visitor: {
      Program: {
        enter(path, { opts = {} }) {
          const remoteFile = opts.remoteFile;
          const code = fs.readFileSync(
            p.resolve(__dirname, "../", remoteFile),
            "utf-8"
          );
          remoteModulePathMap = getRemoteModulePathMap(code);
        },
      },
      ImportDeclaration: ImportDeclarationVisitor,
    },
  };
};

在 .babelrc 文件中可以这么使用:

{
  "presets": [],
  "sourceType": "module",
  "plugins": [
    [
      "./import-path-replace",
      {
        "match": "@/remote", // 匹配规则
        "remoteFile": "./src/remote.ts" // 远程模块所在的文件路径
      }
    ]
  ]
}

import-path-replace-loader 实现

如果不想使用 babel plugin 的形式,那么我们也可以通过实现 webpack loader 的方式进行模块路径转换

import-path-replace-loader.js:

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
const fs = require("fs");
const p = require("path");
const loaderUtils = require("loader-utils");
const getRemoteModulePathMap = require("./getRemoteModuleMap");
let remoteModulePathMap;

function replace(source) {
  const options = loaderUtils.getOptions(this);
  const cb = this.async();
  if (!remoteModulePathMap) {
    const remoteFile = options.remoteFile;
    const code = fs.readFileSync(remoteFile, "utf-8");
    remoteModulePathMap = getRemoteModulePathMap(code);
  }

  const ast = parser.parse(source, {
    sourceType: "module",
    allowImportExportEverywhere: true,
    // 实际上,模块映射路径替换的loader是在babel loader处理之后执行的,其实这里可以不用再使用typescript处理了
    plugins: ["typescript"],
  });

  const visitor = {
    ImportDeclaration: (path, state) => {
      const {
        node: {
          source: { value },
          specifiers,
        },
      } = path;
      if (value !== options.match) return;
      specifiers.forEach((specify) => {
        const importedName = specify.imported.name;
        const realModulePath = remoteModulePathMap[importedName];
        if (!realModulePath) return;
        const realModuleSpecify = realModulePath.specify;
        if (realModuleSpecify.local) {
          if (realModuleSpecify.local.name === "default") {
            specify = types.importDefaultSpecifier(specify.local);
          } else {
            specify.imported = realModuleSpecify.local;
          }
        }
        path.insertBefore(
          types.importDeclaration(
            [specify],
            types.stringLiteral(realModulePath.modulePath)
          )
        );
      });
      path.remove();
    },
  };

  traverse.default(ast, visitor);
  const result = generator.default(ast, {}, source);
  cb(null, result.code, result.map);
}

module.exports = replace;

然后在webpack config 配置文件中添加一个loader:

{
  test: /\.(jsx?|tsx?)$/,
  include: [path.resolve(__dirname, './src')], // 只处理子应用下面的模块
  enforce: 'post',
  use: [
    {
      loader: path.resolve(__dirname, './import-path-replace-loader'),
      options: {
        match: '@/remote',
        remoteFile: path.resolve(__dirname, './src/remote.js'),
      },
    },
  ],
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.