Skip to content

202311

INFO

不知道路在哪里?路在脚下,可脚下却无路可走

20231101

1101

  • react 版本的元素露出与隐藏以及IntersectionObserver API
js
  const ExposeRef = useRef(
    new IntersectionObserver(
      (entries) => {
        entries.forEach(({ intersectionRatio, target }) => {
          if (intersectionRatio > 0.5) {
            const _target = target as HTMLImageElement;
            const ids = _target.dataset.expose ?? "";
            console.log(ids, "ids");
            setShowIndex(+ids);
          }
        });
      },
      {
        root: null,
        threshold: 0.7,
      }
    )
  );

  useEffect(() => {
    const exposeRef = ExposeRef.current;
    const everyItems = document.querySelectorAll("[data-expose]");
    if (everyItems && everyItems.length) {
      Array.from(everyItems).forEach((item) => {
        if (item) {
          exposeRef.observe(item);
        }
      });
    }
    return () => {
      exposeRef.disconnect();
    };
  }, []);
  • js 正则表达式利用
js
str.replace(/(\?age=)(12|57)/g, "age=28");

browserslist 原理

browserslist[2] 用特定的语句来查询浏览器列表,如 last 2 Chrome versions。

js
npx browserslist "last 2 Chrome versions"

细说起来,「它是现代前端工程化不可或缺的工具,无论是处理 JS 的 babel,还是处理 CSS 的 postcss,凡是与垫片相关的,他们背后都有 browserslist 的身影。」

  • babel,在 @babel/preset-env 中使用 core-js 作为垫片
  • postcss 使用 autoprefixer 作为垫片

关于前端打包体积与垫片关系,我们有以下几点共识:

  • 由于低浏览器版本的存在,垫片是必不可少的
  • 垫片越少,则打包体积越小
  • 浏览器版本越新,则垫片越少
  • 那在前端工程化实践中,当我们确认了浏览器版本号,那么它的垫片体积就会确认。

假设项目只需要支持最新的两个谷歌浏览器。那么关于 browserslist 的查询,可以写作 last 2 Chrome versions。

而随着时间的推移,「该查询语句将会返回更新的浏览器,垫片体积便会减小。」

谈一下 browserslist 的原理

browserslist 根据正则解析查询语句,对浏览器版本数据库 caniuse-lite 进行查询,返回所得的浏览器版本列表。

一些常用的查询语法

  • 浏览器版本号: Chrome > 90: Chrome 大于 90 版本号的浏览器
  • 根据最新浏览器版本
js
last 2 versions: 所有浏览器的最新两个版本
last 2 Chrome versions: Chrome 浏览器的最新两个版本
  • 根据用户份额:
js
> 5%: 在全球用户份额大于 5% 的浏览器
> 5% in CN: 在中国用户份额大于 5% 的浏览器

1102

  • 每个 js 对象都有 toString()方法,单纯的数字.toString(16)会报语法错误
js
16.toString(16)
// Uncaught SyntaxError: Invalid or unexpected token
var test = 16
var text1 = test.toString(16)
console.log(text1)
// 10 ok

1103/1104

三分钟,教你 3 种前端埋点方式!

1105

  • 年底了 各地都在裁员,如何让自己处于主动点呢,唯有周末好好复习
  • 一周花 500,一个月需要花掉 2000,加上房租 2500,房贷 6200,一个月最基本的开销都要 10700,我的妈呀,4500+2800 = 7300

1106/1107

1108/1109

  • 当你要覆写某个原生方法时,为了防止他已经被其他库覆写(这样你覆写的就不是原生的那个),可以从 iframe 里取(比如图中取 Array.from) 这个方法是从@sodatea 的一个 issue 中看到的 (from 卡颂)
js
const iframe = document.body.appendChild(document, createElement("iframe"));
const iframeArray = iframe.contentwindow.Array;
document.body.removeChild(iframe);
Array.from = iframeArray.from;
js
const data = [
  { userId: 8, title: "title1" },
  { userId: 11, title: "other" },
  { userId: 15, title: null },
  { userId: 19, title: "title2" },
];

// 查找data中,符合where中条件的数据,并根据orderBy中的条件进行排序
const result = find(data)
  .where({
    title: /\d$/, // 这里意思是过滤出数组中,满足title字段中符合 /\d$/的项
  })
  .orderBy("userId", "desc"); // 这里的意思是对数组中的项按照userId进行倒序排列

//=> 返回 [{ userId: 19, title: 'title2'}, { userId: 8, title: 'title1' }];
console.log(result.value);

1110

  • 写一个健康检查的中间件
js
module.exports = async function (ctx, next) {
  if (/share\/healthcheck/.test(ctx.path)) {
    ctx.body = "healthcheck success";
    return;
  }
  await next();
};

1113

ORM,全称为 Object-Relational Mapping,即对象关系映射,是一种编程技术,用于将数据库中的数据转换为对象,以便在编程语言中使用。它是一种设计模式,用于在关系数据库管理系统(RDBMS)和面向对象编程语言之间建立映射。

ORM 的主要目标是消除在数据库和编程语言之间转换数据时的差异。它允许开发人员使用他们熟悉的编程语言来操作数据库,而不需要直接编写 SQL 语句。这样可以提高开发效率,同时也可以减少因为手动编写 SQL 语句而引发的错误。

  • 重启 mysql 数据库
shell
sudo systemctl start mysqld
sudo systemctl stop mysqld
sudo systemctl restart mysqld

# 远程直连
mysql -h h47.101.50.136  -u root -p -P 3306

mysql -h h47.101.50.136 -u root -p --port=3306

# 监听端口
netstat -lnp | grep mysql

# check  MySQL服务是否正常
sudo systemctl status mysqld.service

1114

js
// 1.数组排平
const flatten = (list, level = +Infinity) => {
  // ...
};
const array = [1, [2, [3, 4, [5]], 3], -4];
const list1 = flatten(array);
const list2 = flatten(array, 2);
console.log(list1); // [1, 2, 3, 4, 5, 3, -4]
console.log(list2); // [1, 2, 3, 4, [5], 3, -4]
// 2.实现实例
const myPromise = (val) => Promise.resolve(val);
const delay = (duration) => {
  return (val) =>
    new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(val);
      }, duration);
    });
};
myPromise(`hello`)
  .then(delay(1000))
  .then((val) => console.log(val)); // 一秒之后输出 hello

1115

  • 阿里云 ESC

    991 年 不知道要不要买

  • npm 包中的语义版本,**~、>、^**符号各代表什么 版本规范:主版本号.次版本号.补丁版本号 主版本号:仅当程序发生了重大变化时才会增长,如新增了重要功能、新增了大量的 API、技术架构发生了重大变化 次版本号:仅当程序发生了一些小变化时才会增长,如新增了一些小功能、新增了一些辅助型的 API 补丁版本号:仅当解决了一些 bug 或 进行了一些局部优化时更新,如修复了某个函数的 bug、提升了某个函数的运行效率

js
x	不固定的版本号 	1.3.x	只要保证主版本号是1,次版本号是3即可
~	补丁版本号可增	~1.3.4	保证主版本号是1,次版本号是3,补丁版本号大于等于4
^	此版本和补丁版本可增	^1.3.4	保证主版本号是1,次版本号可以大于等于3,补丁版本号可以大于等于4 
*	最新版本 	*	始终安装最新版本

1116

  • 努力提升自己认知,其他的事情都是顺其自然,没有什么好念想的,做好自己!
  • 今天听到公司裁员 10%,内心无比紧张,啥时能拥抱这种时候?主要是目前自己底气还不够足,技术能力还不够?如何熬过这个寒冬,担心啊

千万不要乱了阵脚

  • 手写 extendsmockClass
js
function extends(child, parent, staticProps) {
  var proto = Object.create(parent.prototype)
  parent.constructor = child
  child.prototype = proto
  // 继承静态属性
  Object.setPrototypeOf(child, parent)

  for(let key in staticProps) {
    child.prototype[key] = staticProps[key]
  }
}

function defineProperties(target, obj) {
  for (let key in obj) {
    Object.defineProperty(target, key, {
      value: obj[key],
      writable: true,
      enumerable: false,
      configurable: true
    })
  }
}

function mockClass(con, proto, staticProps) {
  proto && defineProperties(con.prototype, proto)
  staticProps && defineProperties(con, staticProps)
  return con
}

1117

  • 要学的东西有点多,一点点来,面试优先,prisma 作为奖励
  • 一下子也不要学太多,不太好消化,别太紧张,又不是天塌了,自我安慰安慰

图片 Base64 有啥优势?

图片 Base64 编码有以下几个主要优点:

减少 HTTP 请求:使用 Base64 编码的图片可以直接嵌入到 HTML 或 CSS 中,从而减少一个图片的 HTTP 请求。这对于减少服务器负载和提高页面加载速度有着积极的作用 绕过二进制数据传输的限制:有些协议只能传输文本数据,如 HTTP 协议的 URL 和大多数现代语言的 String 类型。对于这些协议,我们需要将二进制数据(如图片)转换为文本格式,这就需要使用 Base64 编码 节省存储空间:Base64 编码可以将三个字节的二进制数据编码为四个字节的文本数据。例如,GUID 通常是 16 个字节,用 Hex 表示就需要 32 个字节,而如果用 Base64 则只需要 24 个字节

使用 Base64 编码也有一些缺点:

1.增大 CSS 文件体积:Base64 编码会增大 CSS 文件的体积,这可能会导致关键渲染路径(Critical Rendering Path,CRP)的阻塞,影响页面的渲染速度。 2.增加页面解析 CSS 生成的 CSSOM 时间:Base64 编码的图片与 CSS 混在一起,会增加浏览器解析 CSS 树的耗时。

  • 如何校验 https 证书的安全性
如果服务器返回的证书验证通过,浏览器就可获取到数字证书的明文、数字签名信息,做以下操作:

1.用 CA 机构里的公钥(CA 机构的公钥是不需要传输的,操作系统提供的根证书里会存在)去解密数字证书中的数字签名(RSA/PSK),最终客户端 得到数字摘要 hash value1
2.客户端用证书里指定的 hash 摘要算法对明文数据(包含服务器公钥和企业其他信息)做加密,生成一份摘要 hashCode2。
3.然后两种比对 如果明文数据未被篡改,hashCode2 应该等于 hashCode1。
4.现在证书是可信的,就可拿到服务器的公钥。(为了得到最初服务器的公钥 真心不容易!!!)

1118/19

1120

  • 对现在的处境比较失望,做好随时打仗的准备,给自己找一点的体面和尊严。
  • web 解析二维码
js
const barcodeDetector = new BarcodeDetector({
  formats: ["qr_code"],
});
const eleImg = document.querySelector("img");
barcodeDetector
  .detect(eleImg)
  .then((barcodes) => {
    console.log("barcodes", barcodes);
    barcodes.forEach((barcode) => {
      result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`;
    });
  })
  .catch((err) => {
    result.innerHTML = `<span class="error">解析出错:${err}</span>`;
  });
  • 手写 hashRouter 路由和 historyRouter 路由
js
function hashRouter() {
  window.addEventListener("DOMContentLoaded", load);
  window.addEventListener("hashchange", load);
  var innerHtml = document.getElementById("route-view");
  function load() {
    var pathname = window.location.hash;
    switch (pathname) {
      case "#/page1":
        innerHtml.innerHTML = "This is Page1";
        break;

      case "#/page2":
        innerHtml.innerHTML = "This is Page2";
        break;
      default:
        innerHtml.innerHTML = "This is default";
        break;
    }
  }
}
function historyRouter() {
  window.addEventListener("DOMContentLoaded", load);
  window.addEventListener("popstate", historyChange);
  var routerView = document.getElementById("route-view");
  function load() {
    var aList = document.querySelectorAll("a");
    historyChange();
    for (let aTage of aList) {
      aTage.addEventListener("click", function (taget) {
        taget.preventDefault();
        var href = aTage.getAttribute("href");
        // 改变路径 同时不会刷新页面
        history.pushState(null, "", href);
        historyChange();
      });
    }
  }
  function historyChange() {
    var pathName = window.location.pathname.split("/").pop();
    switch (pathName) {
      case "page1":
        routerView.innerHTML = "This is page1";
        break;
      case "page2":
        routerView.innerHTML = "This is page2";
        break;
      default:
        routerView.innerHTML = "This is default";
    }
  }
}

1121

  • 895,马上快 900 了!!
  • 最后一次有想去的目标,希望尽快把面试常规八股文背诵完成,http 网络知识以及常用算法搞起来
  • 如果每天能刷五道算法题就 perfect 了,哈哈哈,这个真不如刚毕业的小伙子,智商比不上.关键还贼费劲,练习少了
js
// 归并排序
// 有三个过程 分解 解决 合并
// 分解:将原问题分解成一系列子问题。
// 解决:递归求解各个子问题,若子问题足够小,则直接求解。
// 合并:将子问题的结果合并成原问题。
function mergeSort(arr) {
  const merge = (left, right) => {
    let i = 0;
    let j = 0;
    let res = [];
    while (i < left.length && j < right.length) {
      if (left[i] < right[j]) {
        res.push(left[i++]);
      } else {
        res.push(right[j++]);
      }
    }
    while (i < left.length) {
      res.push(left[i++]);
    }
    while (j < right.length) {
      res.push(right[j++]);
    }
    return res;
  };
  var sort = (arr) => {
    if (arr.length === 1) return arr;
    let mid = Math.floor(arr.length / 2);
    let left = arr.slice(0, mid);
    let right = arr.slice(mid);
    return merge(mergeSort(left), mergeSort(right));
  };
  return sort(arr);
}
mergeSort([22, 11, 2, 33, 4, 55, 666]);

1122

  • foreach 如何终止循环?break 和 continue 区别
js
var array = [-3, -2, -1, 0, 1, 2, 3];
array.forEach((it) => {
  if (it >= 0) {
    console.log(it);
    // 0 1 2 3
    return; // or break
  }
});
for (let i = 0; i < array.length; i++) {
  var item = array[i];
  if (item >= 0) {
    console.log(item, "item");
    break;
  }
}
// 0 item
  • 滚动到页面顶部
js
export const scrollToTop = () => {
  const height = document.documentElement.scrollTop || document.body.scrollTop;
  if (height > 0) {
    window.requestAnimationFrame(scrollToTop);
    window.scrollTo(0, height - height / 8);
  }
};

1123

  • 随时做好准备,时刻准备打仗
  • 目前急需 准备 babel/postcss/vite/网络相关等

简历上写了这两个内容,该怎么回答面试官的问题呢?如果你是面试官,如何测试面试者是否真正掌握这两项技术呢

  • 了解 rollup/vite 打包构建流程以及相关 plugin 插件

  • 了解 babel 编译原理,了解 postcss 编译原理

  • 查看当前镜像以及设置镜像为淘宝镜像

  1. 查看当前地址: npm config get registry

  2. 设置当前地址(设置为淘宝镜像) npm config set registry http://registry.npm.taobao.org/

  • 为什么 Number 的最大整数是 2^53 -1 呢? 整数需要连续性,所以表示整数时不能使用指数位 E 区域,只有尾数 M 区域可表示连续的数据,上面说了其实 M 最多可以表示 53 位。所以最大的安全整数是2 ^ 53 -1 Array 索引既然是整数,那它的最大索引为什么不是 2 ** 53 - 1 呢?JS 语言中数组的索引最大就是**2^32 -1**。就当做语言规范

1124

  • 898, 如何利用寒冬这俩月冲刺下,如果冲刺不行就继续苟着,但苟着已经没啥意义。自己的年纪和学历是硬伤,搞不定
  • 如何整出一个前端监控系统?
  • 真的想把见过的面试题都深深印在脑海里,想征服所有的面试题,可惜就是精力实在有限?
  • 数据库设计原则有哪些?
  • 人到了一定的年纪自己就是屋檐,再也不能到处躲雨。
  • graphQL: GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。中文网

1125

  • 899,之前的准备太过于手写题导致了大部分需要准备的内容都没有准备充分,马上要面试了,手忙脚乱的
  • 真正要面试之前,还有很多部分需要完善,搞一个面试总流程思维导图

JS 执行机制:

根据事件循环机制以及宏任务和微任务,重新梳理一下流程

  1. 执行一个宏任务(首次执行的主代码块 script 或者任务队列中的回调函数)
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有任务(依次执行)
  4. JS 引擎线程挂起,GUI 线程执行渲染
  5. GUI 线程渲染完毕后挂起,JS 引擎线程执行任务队列中的下一个宏任务
  • 请求重试?重点 API await Promise.race([fn(), failFn()])
js
function tryQuest(fn, timer, limit) {
  var num = 0;
  async function quest() {
    try {
      return await Promise.race([fn(num), fail(timer)]);
    } catch (e) {
      num = num + 1;
      if (num <= limit) {
        console.log(`Retry #${num}`);
        return quest();
      } else {
        Promise.reject("max limit is Failed");
      }
    }
  }
  return quest();
}
var fail = (timer) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("error");
      new Error("Request timeout");
    }, timer);
  });
};
// 模拟真实业务请求
var fn = (num) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(console.log("success" + num));
    }, 2000);
  });
};
tryQuest(fn, 1000, 3);

1126

  • 900

  • 什么是向量化处理?

    向量化是将操作应用于整个数组或数据系列的过程,而不是逐个遍历每个元素。在 Pandas 中可以对整个列或 Series 执行操作,而无需编写显式循环。这种高效的方法利用了底层优化的库,使您的代码更快、更简洁。

  • 算法中的 DP 和链表是两座大山,无论怎样,要翻过

  • 既然我们终将失去这短暂而宝贵的一生,所以请你不妨大胆一些,爱一个人,攀一座山,跑一段路

1127

  • 901

  • 面试准备中最难的还是算法题,如果平时不努力刷题,要面试了着急刷题,反而得不到应有的效果.面试就是不断刷新自己的知识库,刚开始需要很久的时间才能消化,可能需要几天,面试题看的次数越多,回忆速度会越来越快

  • 为了获得 sourcemap 的能力同时保证源码安全,所以就有了私有的 sourcemap 服务。通过 CI/CD 进行前端静态项目发布,在线上环境发布时会对构建产物进行分析,将产物中的 .map 文件打包上传到 sourcemap 服务中.在 sourcemap 服务接收到 xx 上传的 .map 时,会将文件同步给 sentry 系统,辅助系统进行异常的地位。

  • Peer Dependency 同等依赖 是指一个模块(或包)所依赖的另一个模块(或包)的版本。与常规依赖不同,Peer Dependency 主要用于确保多个模块在同一个主模块的上下文中使用,并共享依赖的版本,这个概念的理解对于构建可维护、稳定和可扩展的 Node.js 应用程序和包非常重要。 Peer Dependency 的需求通常出现在以下几种情况下:

1、共享全局依赖: 当多个包需要与全局(项目级)安装的某个依赖库进行交互时,它们可能需要共享相同版本的这个依赖库。Peer Dependency 可以确保它们都依赖于同一个库的特定版本。

2、插件系统: 当一个主要的库或应用程序提供插件系统,并且希望插件能够与主要库的特定版本一起工作时,Peer Dependency 非常有用。这确保了插件与主要库兼容,并且不会因为主要库的更新而导致问题。

3、避免冲突: 如果两个包依赖于同一个库的不同版本,可能会导致冲突和错误。Peer Dependency 可以防止这种情况发生,因为它要求依赖包使用相同版本的库。

Immutable: 手写 Immutable 太难了

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象 Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变 同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享

1128

  • 902
  • 继续熟悉 Rollup/plugin/vite,继续用 xmind 画图
  • 熟悉了下手写 TS
ts
type EqualTT<A, B = A> = (<T>(arg: A) => T extends A ? 1 : 2) extends <T>(
  arg: B
) => T extends B ? 1 : 2
  ? true
  : false;
// 联合转交叉
type UnionToInsection<T> = (T extends T ? (x: T) => unknown : never) extends (
  x: infer P
) => unknown
  ? P
  : never;
  • 重温 island 架构: 这个模型主要用于 SSR (也包括 SSG) 应用,我们知道,在传统的 SSR 应用中,服务端会给浏览器响应完整的 HTML内容,并在 HTML 中注入一段完整的 JS 脚本用于完成事件的绑定,也就是完成 hydration (注水) 的过程。当注水的过程完成之后,页面也才能真正地能够进行交互。 当一个页面中只有部分的组件交互,那么对于这些可交互的组件,我们可以执行 hydration 过程,因为组件之间是互相独立的。而对于静态组件,即不可交互的组件,我们可以让其不参与 hydration 过程,直接复用服务端下发的 HTML 内容。可交互的组件就犹如整个页面中的孤岛(Island),因此这种模式叫做 Islands 架构。

  • Nextjs13 版本: 「默认走 Server Component,若有交互需要则走 Client Component」 通过这种原则,相信在一定程度上能给减轻开发者的心智负担。

1129

  • 903,今天有 2 个业务需求,限时做一下,node 环境下如何塞入 cookie 中的参数值
  • 惰性函数,以第一次执行的结果为准,后续不会再检测条件
js
function lazyDate() {
  var date = new Date();
  lazyDate = function () {
    return date;
  };
  return lazyDate();
}
lazyDate();

function addHandler(ele, type, fn) {
  if (ele.addEventListener) {
    addHandler = function (ele, type, fn) {
      ele.addEventListener(type, fn, false);
    };
  } else if (ele.attach) {
    addHandler = function (ele, type, fn) {
      ele.attach(type, fn);
    };
  } else {
    addHandler = function (ele, type, fn) {
      ele[type] = fn;
    };
  }
  return addHandler(ele, type, fn);
}
  • addEventListener 默认是捕获还是冒泡?默认是冒泡,addEventListener 第三个参数默认为 false 代表执行事件冒泡行为(代表冒泡时绑定)。当为 true 时执行事件捕获行为(代表捕获时绑定)。

1130

  • 904, 11 月的 lastDay,每一天都是一种飞逝,这种感觉说不好还是不好,只能默默充实自己的前端知识库,希望有一天能达到自己认为合格的状态

  • 到底应该如何准备面试,以至于在面试中才能从容应对?

    1.做思维导图还是不错的 2.自己整理笔记,自己跟自己和解 3.有些术语不好解释,所以需要练习口头表达,说给自己听

  • 手写 js:回到顶部

js
function scrollTop() {
  var top = document.body.scrollTop;
}

说说 nextjs 中运行时机制

Next.js 是一个基于 React 的开发框架,它的运行时机制主要包括两个部分:构建时(Build Time)和运行时(Runtime)

  • 构建时(Build Time)

构建时是指在开发者在本地开发环境中运行 next build 命令时,Next.js 会根据你的代码生成一个优化过的应用程序。这个过程包括以下步骤:

1.静态文件分析:Next.js 会扫描你的 pages 目录,并为每个 .js 或 .jsx 文件生成一个对应的 HTML 文件。这个过程被称为静态文件生成。 2.代码分割:Next.js 会自动将你的代码分割成多个包,这样可以实现按需加载,提高应用的性能。 3.服务器端渲染(SSR):如果你的页面使用了 getServerSideProps 或 getInitialProps 函数,Next.js 会在构建时预渲染这些页面。

  • 运行时(Runtime)

运行时是指在用户访问你的应用时,Next.js 会根据用户的请求动态生成页面。这个过程包括以下步骤:

路由处理:Next.js 使用了自己的路由系统,它会根据用户的请求路径,找到对应的页面组件并渲染。 数据获取:如果页面组件使用了 getServerSideProps 或 getInitialProps 函数,Next.js 会在运行时调用这些函数,获取页面所需的数据。 页面渲染:Next.js 会将获取到的数据和页面组件一起渲染成 HTML,然后发送给用户。 以上就是 Next.js 的运行时机制。通过这种机制,Next.js 既能提供静态网站的性能,又能提供动态网站的功能,是一个非常强大的框架。